Skip to content

Commit d6f17c9

Browse files
feat: add global support chat
1 parent 511fc83 commit d6f17c9

17 files changed

Lines changed: 884 additions & 0 deletions

File tree

.changeset/better-readers-tell.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
pnpm-debug.log*
26+
27+
# local env files
28+
.env*.local
29+
30+
# vercel
31+
.vercel
32+
33+
# typescript
34+
*.tsbuildinfo
35+
next-env.d.ts
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Global Support Chat Demo
2+
3+
A real-time multilingual customer support dashboard built with Next.js 16, Tailwind v4, and **Lingo.dev**.
4+
5+
## Overview
6+
This application simulates a support agent dashboard where:
7+
- The agent speaks English.
8+
- The customers speak various languages (Spanish, German, Japanese, etc.).
9+
- **Lingo.dev SDK** translates messages in real-time.
10+
- **Ollama** (optional) powers AI simulated responses in the customer's native language.
11+
12+
## Features Highlights
13+
- **Real-time Translation**: Instant translation of bidirectional chat using Lingo.dev.
14+
- **Hybrid AI Simulation**: Uses local LLMs to generate context-aware foreign language replies.
15+
- **Modern UI**: Built with Tailwind CSS v4 and React 19.
16+
17+
## Getting Started
18+
19+
1. **Install Dependencies**:
20+
```bash
21+
pnpm install
22+
```
23+
24+
2. **Run Locally**:
25+
```bash
26+
pnpm dev
27+
```
28+
Open http://localhost:3000.
29+
30+
3. **AI Setup (Optional)**:
31+
To enable the AI responses, ensure you have [Ollama](https://ollama.ai/) running:
32+
```bash
33+
ollama serve
34+
ollama pull qwen3:4b
35+
```
36+
Then toggle "AI Responses" in the UI.
37+
38+
4. **Lingo API Key (Optional)**:
39+
For best translation quality, create a `.env.local` file:
40+
```
41+
LINGO_API_KEY=your_key_here
42+
```
43+
(The app works with mock fallbacks without a key).
44+
45+
## Tech Stack
46+
- **Framework**: Next.js 16 (App Router)
47+
- **Styling**: Tailwind CSS v4
48+
- **Translation**: Lingo.dev SDK (via API routes)
49+
- **AI**: Ollama
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { NextResponse } from 'next/server';
2+
3+
export async function POST(request: Request) {
4+
const { history, locale } = await request.json();
5+
6+
// Construct a prompt from history
7+
// History is array of { role: 'user'|'agent', content: string }
8+
// We want the AI (which plays the 'user' in this simulation context?
9+
// Wait, spec says "simulateUserReply".
10+
// "AI Auto-Response": A toggleable AI mode where an LLM generates context-aware replies in the user's native language.
11+
// So the AI pretends to be the Customer (User).
12+
// The Agent (Real Human) is English.
13+
// So AI Prompt: "You are a customer communicating with support. Speak in {locale}. Here is the conversation..."
14+
15+
const prompt = `Roleplay Instructions:
16+
You are a customer communicating with a customer support agent.
17+
- Your language is '${locale}'.
18+
- Respond naturally to the support agent's last message in your language.
19+
- Do NOT repeat your previous messages.
20+
- If the agent asks a question, answer it directly.
21+
- Keep your response concise (1-2 sentences).
22+
- Act like a real person with a specific issue (e.g., login problem, refund, product question).
23+
24+
Conversation history:
25+
${history.map((m: any) => `${m.role === 'agent' ? 'Support Agent' : 'Customer'}: ${m.content}`).join('\n')}
26+
27+
Response (as Customer):`;
28+
29+
try {
30+
const res = await fetch('http://localhost:11434/api/generate', {
31+
method: 'POST',
32+
headers: { 'Content-Type': 'application/json' },
33+
body: JSON.stringify({
34+
model: 'qwen3:4b', // Configurable, but using a reasonable default
35+
prompt,
36+
stream: false
37+
}),
38+
});
39+
40+
if (!res.ok) {
41+
throw new Error(`Ollama status: ${res.status}`);
42+
}
43+
44+
const data = await res.json();
45+
return NextResponse.json({ reply: data.response });
46+
47+
} catch (error) {
48+
console.error('Ollama Error:', error);
49+
return NextResponse.json({
50+
reply: `(AI Error: Ensure Ollama is running. Mock response in ${locale})`
51+
});
52+
}
53+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { NextResponse } from 'next/server';
2+
import { LingoDotDevEngine } from '@lingo.dev/_sdk';
3+
4+
// Fallback dictionary for demo purposes when SDK fails
5+
const FALLBACKS: Record<string, Record<string, string>> = {
6+
'en-es': {
7+
'Hello, I have a problem with my order.': 'Hola, tengo un problema con mi pedido.',
8+
'How can I help you today?': '¿Cómo puedo ayudarte hoy?',
9+
},
10+
'es-en': {
11+
'Hola, tengo un problema con mi pedido.': 'Hello, I have a problem with my order.',
12+
'Gracias por la ayuda.': 'Thanks for the help.',
13+
'Entendido.': 'Understood.',
14+
'¿Puede explicar eso de nuevo?': 'Can you explain that again?',
15+
'Esperaré su respuesta.': 'I will wait for your response.',
16+
},
17+
'de-en': {
18+
'Funktioniert dieses Produkt auf dem Mac?': 'Does this product work on Mac?',
19+
'Danke für die Hilfe.': 'Thanks for the help.',
20+
'Können Sie das wiederholen?': 'Can you repeat that?',
21+
'Verstanden.': 'Understood.',
22+
'Ich werde warten.': 'I will wait.',
23+
},
24+
'ja-en': {
25+
'ありがとうございます。': 'Thank you.',
26+
'もう一度説明していただけますか?': 'Could you explain that again?',
27+
'分かりました。': 'I understand.',
28+
'お待ちしております。': 'I will be waiting.',
29+
},
30+
'fr-en': {
31+
'Merci pour l\'aide.': 'Thanks for the help.',
32+
'Pouvez-vous répéter ?': 'Can you repeat that?',
33+
'Compris.': 'Understood.',
34+
'J\'attendrai.': 'I will wait.',
35+
},
36+
};
37+
38+
export async function POST(request: Request) {
39+
const { text, sourceLocale, targetLocale } = await request.json();
40+
41+
const apiKey = process.env.LINGO_API_KEY;
42+
43+
// Try SDK translation first
44+
if (apiKey) {
45+
try {
46+
const engine = new LingoDotDevEngine({ apiKey });
47+
const translatedText = await engine.localizeText(text, {
48+
sourceLocale,
49+
targetLocale,
50+
});
51+
return NextResponse.json({ translatedText });
52+
} catch (error) {
53+
console.error('Lingo SDK Error:', error);
54+
// Fall through to fallback
55+
}
56+
}
57+
58+
// Fallback Logic (used if no API key OR if SDK execution failed)
59+
const key = `${sourceLocale}-${targetLocale}`;
60+
const exactMatch = FALLBACKS[key]?.[text];
61+
62+
if (exactMatch) {
63+
return NextResponse.json({ translatedText: exactMatch });
64+
}
65+
66+
// Pseudo-translation fallback if no match
67+
return NextResponse.json({
68+
translatedText: `[${targetLocale.toUpperCase()}] ${text}`
69+
});
70+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@import "tailwindcss";
2+
3+
@theme inline {
4+
--color-background: var(--background);
5+
--color-foreground: var(--foreground);
6+
}
7+
8+
:root {
9+
--background: #ffffff;
10+
--foreground: #171717;
11+
}
12+
13+
@media (prefers-color-scheme: dark) {
14+
:root {
15+
--background: #0a0a0a;
16+
--foreground: #ededed;
17+
}
18+
}
19+
20+
body {
21+
color: var(--foreground);
22+
background: var(--background);
23+
font-family: Arial, Helvetica, sans-serif;
24+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Metadata } from 'next';
2+
import './globals.css';
3+
4+
export const metadata: Metadata = {
5+
title: 'Global Support Chat',
6+
description: 'Multilingual support dashboard powered by Lingo.dev',
7+
};
8+
9+
export default function RootLayout({
10+
children,
11+
}: {
12+
children: React.ReactNode;
13+
}) {
14+
return (
15+
<html lang="en">
16+
<body className="antialiased h-screen overflow-hidden">
17+
{children}
18+
</body>
19+
</html>
20+
);
21+
}

0 commit comments

Comments
 (0)