strapi-plugin-mcp-chat 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -4
- package/admin/src/components/AdminOverlays.tsx +35 -1
- package/admin/src/components/FloatingChat.tsx +14 -0
- package/admin/src/components/LangSwitcher.tsx +24 -0
- package/admin/src/components/Onboarding.tsx +192 -0
- package/admin/src/components/PreviewPanel.tsx +22 -1
- package/admin/src/components/StackLogos.tsx +60 -0
- package/admin/src/i18n.ts +214 -0
- package/admin/src/pages/HomePage.tsx +55 -24
- package/admin/src/pages/ProvisionPage.tsx +54 -59
- package/dist/server/index.js +60 -16
- package/package.json +1 -1
- package/server/src/content-tools.ts +26 -4
- package/server/src/controllers/chat.ts +2 -2
- package/server/src/provision/integrate.ts +15 -1
- package/server/src/services/chat.ts +28 -8
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n leve para as PÁGINAS DO PLUGIN no menu esquerdo (Home + Provisionar).
|
|
3
|
+
*
|
|
4
|
+
* Não usa o sistema de traduções do admin de propósito: estas páginas são
|
|
5
|
+
* autossuficientes e o idioma é escolhido por um seletor próprio, persistido em
|
|
6
|
+
* `localStorage` sob a MESMA chave do chat flutuante (`mcp-chat-lang`), para que
|
|
7
|
+
* o plugin inteiro siga um único idioma. Default: inglês.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useState } from 'react';
|
|
10
|
+
|
|
11
|
+
export type Lang = 'pt' | 'en';
|
|
12
|
+
|
|
13
|
+
const LS_KEY = 'mcp-chat-lang';
|
|
14
|
+
|
|
15
|
+
export const getLang = (): Lang => {
|
|
16
|
+
try {
|
|
17
|
+
return (localStorage.getItem(LS_KEY) as Lang) === 'pt' ? 'pt' : 'en';
|
|
18
|
+
} catch {
|
|
19
|
+
return 'en';
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Hook: idioma atual + setter persistido + sincronização entre abas/componentes. */
|
|
24
|
+
export const useLang = (): [Lang, (l: Lang) => void] => {
|
|
25
|
+
const [lang, setLangState] = useState<Lang>(getLang);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const onStorage = (e: StorageEvent) => {
|
|
29
|
+
if (e.key === LS_KEY) setLangState(getLang());
|
|
30
|
+
};
|
|
31
|
+
// evento custom para sincronizar componentes na MESMA aba
|
|
32
|
+
const onLocal = () => setLangState(getLang());
|
|
33
|
+
window.addEventListener('storage', onStorage);
|
|
34
|
+
window.addEventListener('mcp-chat-lang-change', onLocal);
|
|
35
|
+
return () => {
|
|
36
|
+
window.removeEventListener('storage', onStorage);
|
|
37
|
+
window.removeEventListener('mcp-chat-lang-change', onLocal);
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const setLang = (l: Lang) => {
|
|
42
|
+
try { localStorage.setItem(LS_KEY, l); } catch { /* noop */ }
|
|
43
|
+
setLangState(l);
|
|
44
|
+
try { window.dispatchEvent(new Event('mcp-chat-lang-change')); } catch { /* noop */ }
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return [lang, setLang];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type Dict = Record<string, string>;
|
|
51
|
+
|
|
52
|
+
export const STRINGS: Record<Lang, Dict> = {
|
|
53
|
+
en: {
|
|
54
|
+
// Home
|
|
55
|
+
'home.title': 'MCP Chat',
|
|
56
|
+
'home.subtitle': 'AI via MCP',
|
|
57
|
+
'home.seeingScreen': '• seeing your screen',
|
|
58
|
+
'home.voiceOn': '• voice ON',
|
|
59
|
+
'home.voiceBtnOn': 'Voice: ON',
|
|
60
|
+
'home.voiceBtnOff': 'Voice: OFF',
|
|
61
|
+
'home.pubOn': '🚀 Publish: ON',
|
|
62
|
+
'home.pubOff': '📝 Draft mode',
|
|
63
|
+
'home.pubTitle': 'Auto-publish OFF = the AI only saves a draft (you review and publish). ON = publishes straight to the site.',
|
|
64
|
+
'home.shareStop': 'Stop screen',
|
|
65
|
+
'home.shareStart': 'Share screen',
|
|
66
|
+
'home.provision': 'Provision frontend',
|
|
67
|
+
'home.previewOn': 'Live Preview: ON',
|
|
68
|
+
'home.previewOff': 'Live Preview: OFF',
|
|
69
|
+
'home.empty': 'Type, speak (🎤) or share your screen. E.g.: “Which content-types exist?”.',
|
|
70
|
+
'home.you': 'You',
|
|
71
|
+
'home.ai': 'AI',
|
|
72
|
+
'home.processing': 'Processing…',
|
|
73
|
+
'home.rec': '🎤 Speak',
|
|
74
|
+
'home.recStop': '⏹ Stop',
|
|
75
|
+
'home.placeholder': 'Type… (Cmd/Ctrl+Enter sends)',
|
|
76
|
+
'home.send': 'Send',
|
|
77
|
+
'home.reload': 'Reload',
|
|
78
|
+
'home.previewUrlLabel': 'Preview URL',
|
|
79
|
+
'home.errShare': 'Could not start screen sharing.',
|
|
80
|
+
'home.errMic': 'Could not access the microphone.',
|
|
81
|
+
'home.errStt': 'Transcription error.',
|
|
82
|
+
'home.errAudioEmpty': 'I could not understand the audio.',
|
|
83
|
+
'home.errChat': 'Error talking to the AI.',
|
|
84
|
+
'home.noReply': '(no reply)',
|
|
85
|
+
'home.tour': '❓ Tour',
|
|
86
|
+
'common.lang': '🌐 English',
|
|
87
|
+
|
|
88
|
+
// Provision
|
|
89
|
+
'prov.title': 'Provision frontend',
|
|
90
|
+
'prov.subtitle': 'Upload your frontend .zip (Figma/Lovable, Next or TanStack) — the AI infers the content model, you review it, and the plugin creates everything in Strapi.',
|
|
91
|
+
'prov.back': '← Back to chat',
|
|
92
|
+
'prov.supported': 'Supported stacks',
|
|
93
|
+
'prov.step1': '1. Choose the frontend .zip',
|
|
94
|
+
'prov.step1desc': 'No strapi.manifest.json needed: if it is missing, the AI creates one by analyzing the code (e.g. src/data/*.ts).',
|
|
95
|
+
'prov.selectFile': 'Select file…',
|
|
96
|
+
'prov.noFile': 'No file selected',
|
|
97
|
+
'prov.analyze': 'Analyze project',
|
|
98
|
+
'prov.analyzing': 'Analyzing…',
|
|
99
|
+
'prov.analyzingDesc': 'Reading the code and inferring the content model (content-types + seed)…',
|
|
100
|
+
'prov.step2': '2. Review the content model',
|
|
101
|
+
'prov.inferred': '🤖 Inferred by the AI',
|
|
102
|
+
'prov.fromManifest': '✓ Project manifest',
|
|
103
|
+
'prov.framework': 'framework',
|
|
104
|
+
'prov.analyzed': 'Analyzed',
|
|
105
|
+
'prov.editJson': 'Edit the JSON to adjust names, types or seeded content before creating.',
|
|
106
|
+
'prov.provision': 'Provision',
|
|
107
|
+
'prov.restart': 'Restart',
|
|
108
|
+
'prov.provisioning': 'Provisioning…',
|
|
109
|
+
'prov.provisioningTitle': 'Setting everything up — this takes a few seconds',
|
|
110
|
+
'prov.provisioningDesc': 'Strapi is restarting to recognize the content-types, then it seeds content, opens public read access and wires the preview. Do not close this page.',
|
|
111
|
+
'prov.invalidJson': 'The manifest is not valid JSON. Fix the syntax.',
|
|
112
|
+
'prov.analyzeFail': 'Failed to analyze the project.',
|
|
113
|
+
'prov.provisionFail': 'Provisioning failed.',
|
|
114
|
+
'prov.analyzeWarn': 'Analysis warning:',
|
|
115
|
+
'prov.doneTitle': '✅ All set! You can see the preview now.',
|
|
116
|
+
'prov.typesCreated': 'Content-types created:',
|
|
117
|
+
'prov.seeded': 'Seeded content:',
|
|
118
|
+
'prov.frontendAt': 'Frontend at:',
|
|
119
|
+
'prov.runFrontend': 'To see the preview, run the frontend (once):',
|
|
120
|
+
'prov.relinkDesc': 'Relink the frontend to Strapi (snapshot): swaps the hardcoded data for Strapi data, keeping the images. Components do not change.',
|
|
121
|
+
'prov.relink': 'Relink data to Strapi',
|
|
122
|
+
'prov.relinking': 'Relinking…',
|
|
123
|
+
'prov.open': 'Open',
|
|
124
|
+
'prov.provisionAnother': 'Provision another',
|
|
125
|
+
'prov.relinkOk': '✅ Relinked! Files updated: {files}. Reload the preview to see Strapi data. (Original saved as .bak.)',
|
|
126
|
+
'prov.relinkFail': '⚠️ Could not relink: {err}.',
|
|
127
|
+
'prov.relinkErr': 'Failed to relink.',
|
|
128
|
+
'prov.noData': 'no data file',
|
|
129
|
+
},
|
|
130
|
+
pt: {
|
|
131
|
+
// Home
|
|
132
|
+
'home.title': 'MCP Chat',
|
|
133
|
+
'home.subtitle': 'IA via MCP',
|
|
134
|
+
'home.seeingScreen': '• vendo sua tela',
|
|
135
|
+
'home.voiceOn': '• voz ON',
|
|
136
|
+
'home.voiceBtnOn': 'Voz: ON',
|
|
137
|
+
'home.voiceBtnOff': 'Voz: OFF',
|
|
138
|
+
'home.pubOn': '🚀 Publicar: ON',
|
|
139
|
+
'home.pubOff': '📝 Rascunho',
|
|
140
|
+
'home.pubTitle': 'Auto-publicar OFF = a IA só salva rascunho (você revisa e publica). ON = publica direto no site.',
|
|
141
|
+
'home.shareStop': 'Parar tela',
|
|
142
|
+
'home.shareStart': 'Compartilhar tela',
|
|
143
|
+
'home.provision': 'Provisionar frontend',
|
|
144
|
+
'home.previewOn': 'Live Preview: ON',
|
|
145
|
+
'home.previewOff': 'Live Preview: OFF',
|
|
146
|
+
'home.empty': 'Escreva, fale (🎤) ou compartilhe a tela. Ex.: “Quais content-types existem?”.',
|
|
147
|
+
'home.you': 'Você',
|
|
148
|
+
'home.ai': 'IA',
|
|
149
|
+
'home.processing': 'Processando…',
|
|
150
|
+
'home.rec': '🎤 Falar',
|
|
151
|
+
'home.recStop': '⏹ Parar',
|
|
152
|
+
'home.placeholder': 'Escreva… (Cmd/Ctrl+Enter envia)',
|
|
153
|
+
'home.send': 'Enviar',
|
|
154
|
+
'home.reload': 'Recarregar',
|
|
155
|
+
'home.previewUrlLabel': 'URL do preview',
|
|
156
|
+
'home.errShare': 'Não foi possível iniciar o compartilhamento de tela.',
|
|
157
|
+
'home.errMic': 'Não foi possível acessar o microfone.',
|
|
158
|
+
'home.errStt': 'Erro na transcrição.',
|
|
159
|
+
'home.errAudioEmpty': 'Não consegui entender o áudio.',
|
|
160
|
+
'home.errChat': 'Erro ao falar com a IA.',
|
|
161
|
+
'home.noReply': '(sem resposta)',
|
|
162
|
+
'home.tour': '❓ Tour',
|
|
163
|
+
'common.lang': '🌐 PT-BR',
|
|
164
|
+
|
|
165
|
+
// Provision
|
|
166
|
+
'prov.title': 'Provisionar frontend',
|
|
167
|
+
'prov.subtitle': 'Suba o .zip do seu frontend (Figma/Lovable, Next ou TanStack) — a IA infere o modelo de conteúdo, você revisa, e o plugin cria tudo no Strapi.',
|
|
168
|
+
'prov.back': '← Voltar ao chat',
|
|
169
|
+
'prov.supported': 'Stacks suportados',
|
|
170
|
+
'prov.step1': '1. Escolha o .zip do frontend',
|
|
171
|
+
'prov.step1desc': 'Não precisa de strapi.manifest.json: se ele não existir, a IA cria um analisando os dados do código (ex.: src/data/*.ts).',
|
|
172
|
+
'prov.selectFile': 'Selecionar arquivo…',
|
|
173
|
+
'prov.noFile': 'Nenhum arquivo selecionado',
|
|
174
|
+
'prov.analyze': 'Analisar projeto',
|
|
175
|
+
'prov.analyzing': 'Analisando…',
|
|
176
|
+
'prov.analyzingDesc': 'Lendo o código e inferindo o modelo de conteúdo (content-types + seed)…',
|
|
177
|
+
'prov.step2': '2. Revise o modelo de conteúdo',
|
|
178
|
+
'prov.inferred': '🤖 Inferido pela IA',
|
|
179
|
+
'prov.fromManifest': '✓ Manifest do projeto',
|
|
180
|
+
'prov.framework': 'framework',
|
|
181
|
+
'prov.analyzed': 'Analisou',
|
|
182
|
+
'prov.editJson': 'Edite o JSON se quiser ajustar nomes, tipos ou o conteúdo semeado antes de criar.',
|
|
183
|
+
'prov.provision': 'Provisionar',
|
|
184
|
+
'prov.restart': 'Recomeçar',
|
|
185
|
+
'prov.provisioning': 'Provisionando…',
|
|
186
|
+
'prov.provisioningTitle': 'Configurando tudo — isso leva alguns segundos',
|
|
187
|
+
'prov.provisioningDesc': 'A Strapi está reiniciando para reconhecer as content-types, depois semeia o conteúdo, libera leitura pública e liga o preview. Não feche esta página.',
|
|
188
|
+
'prov.invalidJson': 'O manifest não é um JSON válido. Corrija a sintaxe.',
|
|
189
|
+
'prov.analyzeFail': 'Falha ao analisar o projeto.',
|
|
190
|
+
'prov.provisionFail': 'Falha na provisão.',
|
|
191
|
+
'prov.analyzeWarn': 'Aviso da análise:',
|
|
192
|
+
'prov.doneTitle': '✅ Tudo pronto! Você já pode ver o preview.',
|
|
193
|
+
'prov.typesCreated': 'Content-types criadas:',
|
|
194
|
+
'prov.seeded': 'Conteúdo semeado:',
|
|
195
|
+
'prov.frontendAt': 'Frontend em:',
|
|
196
|
+
'prov.runFrontend': 'Para ver o preview, rode o frontend (uma vez):',
|
|
197
|
+
'prov.relinkDesc': 'Religar o frontend ao Strapi (snapshot): troca os dados hardcoded pelos do Strapi, mantendo as imagens. Os componentes não mudam.',
|
|
198
|
+
'prov.relink': 'Religar dados ao Strapi',
|
|
199
|
+
'prov.relinking': 'Religando…',
|
|
200
|
+
'prov.open': 'Abrir',
|
|
201
|
+
'prov.provisionAnother': 'Provisionar outro',
|
|
202
|
+
'prov.relinkOk': '✅ Religado! Arquivos atualizados: {files}. Recarregue o preview para ver os dados do Strapi. (Original salvo como .bak.)',
|
|
203
|
+
'prov.relinkFail': '⚠️ Não consegui religar: {err}.',
|
|
204
|
+
'prov.relinkErr': 'Falha ao religar.',
|
|
205
|
+
'prov.noData': 'sem arquivo de dados',
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** Tradutor com interpolação simples de {chaves}. */
|
|
210
|
+
export const makeT = (lang: Lang) => (key: string, vars?: Record<string, string>) => {
|
|
211
|
+
let s = STRINGS[lang][key] ?? STRINGS.en[key] ?? key;
|
|
212
|
+
if (vars) for (const k of Object.keys(vars)) s = s.replace(`{${k}}`, vars[k]);
|
|
213
|
+
return s;
|
|
214
|
+
};
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { useRef, useState } from 'react';
|
|
1
|
+
import { useRef, useState, useEffect } from 'react';
|
|
2
2
|
import { Box, Flex, Typography, Button, Textarea, TextInput } from '@strapi/design-system';
|
|
3
3
|
import { useFetchClient } from '@strapi/strapi/admin';
|
|
4
4
|
import { Link } from 'react-router-dom';
|
|
5
|
+
import { useLang, makeT } from '../i18n';
|
|
6
|
+
import { LangSwitcher } from '../components/LangSwitcher';
|
|
7
|
+
import { Onboarding, tourWasSeen } from '../components/Onboarding';
|
|
5
8
|
|
|
6
9
|
type Msg = { role: 'user' | 'assistant'; content: string; image?: string | null };
|
|
7
10
|
|
|
@@ -16,6 +19,8 @@ const DEFAULT_PREVIEW_URL = (() => {
|
|
|
16
19
|
|
|
17
20
|
const HomePage = () => {
|
|
18
21
|
const { post } = useFetchClient();
|
|
22
|
+
const [lang] = useLang();
|
|
23
|
+
const t = makeT(lang);
|
|
19
24
|
|
|
20
25
|
const [messages, setMessages] = useState<Msg[]>([]);
|
|
21
26
|
const [input, setInput] = useState('');
|
|
@@ -23,6 +28,10 @@ const HomePage = () => {
|
|
|
23
28
|
const [sharing, setSharing] = useState(false);
|
|
24
29
|
const [error, setError] = useState<string | null>(null);
|
|
25
30
|
|
|
31
|
+
// Onboarding (mini-curso): abre na 1ª vez; reabrível pelo botão Tour.
|
|
32
|
+
const [tourOpen, setTourOpen] = useState(false);
|
|
33
|
+
useEffect(() => { if (!tourWasSeen()) setTourOpen(true); }, []);
|
|
34
|
+
|
|
26
35
|
// Live preview
|
|
27
36
|
const [previewOn, setPreviewOn] = useState(false);
|
|
28
37
|
const [previewUrl, setPreviewUrl] = useState(DEFAULT_PREVIEW_URL);
|
|
@@ -32,6 +41,14 @@ const HomePage = () => {
|
|
|
32
41
|
const [voiceOn, setVoiceOn] = useState(false);
|
|
33
42
|
const [recording, setRecording] = useState(false);
|
|
34
43
|
|
|
44
|
+
// Draft-first: a IA só salva rascunho por padrão; publica só com isto ON.
|
|
45
|
+
const [autoPublish, setAutoPublish] = useState<boolean>(() => {
|
|
46
|
+
try { return localStorage.getItem('mcp-chat-autopublish') === '1'; } catch { return false; }
|
|
47
|
+
});
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
try { localStorage.setItem('mcp-chat-autopublish', autoPublish ? '1' : '0'); } catch { /* noop */ }
|
|
50
|
+
}, [autoPublish]);
|
|
51
|
+
|
|
35
52
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
36
53
|
const streamRef = useRef<MediaStream | null>(null);
|
|
37
54
|
const recorderRef = useRef<MediaRecorder | null>(null);
|
|
@@ -50,7 +67,7 @@ const HomePage = () => {
|
|
|
50
67
|
stream.getVideoTracks()[0].addEventListener('ended', stopShare);
|
|
51
68
|
setSharing(true);
|
|
52
69
|
} catch {
|
|
53
|
-
setError('
|
|
70
|
+
setError(t('home.errShare'));
|
|
54
71
|
}
|
|
55
72
|
};
|
|
56
73
|
const stopShare = () => {
|
|
@@ -98,15 +115,18 @@ const HomePage = () => {
|
|
|
98
115
|
const payload = {
|
|
99
116
|
messages: next.map((m) => ({ role: m.role, content: m.content })),
|
|
100
117
|
image,
|
|
118
|
+
lang,
|
|
119
|
+
previewUrl: previewOn ? previewUrl : null,
|
|
120
|
+
autoPublish,
|
|
101
121
|
};
|
|
102
122
|
const { data } = await post('/mcp-chat/message', payload);
|
|
103
|
-
const reply = data?.reply || '
|
|
123
|
+
const reply = data?.reply || t('home.noReply');
|
|
104
124
|
setMessages((cur) => [...cur, { role: 'assistant', content: reply }]);
|
|
105
125
|
if (previewOn) setIframeKey((k) => k + 1);
|
|
106
126
|
if (voiceOn) playTTS(reply);
|
|
107
127
|
} catch (e: any) {
|
|
108
128
|
const detail =
|
|
109
|
-
e?.response?.data?.error?.message || e?.message || '
|
|
129
|
+
e?.response?.data?.error?.message || e?.message || t('home.errChat');
|
|
110
130
|
setError(detail);
|
|
111
131
|
} finally {
|
|
112
132
|
setLoading(false);
|
|
@@ -144,7 +164,7 @@ const HomePage = () => {
|
|
|
144
164
|
recorder.start();
|
|
145
165
|
setRecording(true);
|
|
146
166
|
} catch {
|
|
147
|
-
setError('
|
|
167
|
+
setError(t('home.errMic'));
|
|
148
168
|
}
|
|
149
169
|
};
|
|
150
170
|
const stopRecording = () => {
|
|
@@ -157,14 +177,15 @@ const HomePage = () => {
|
|
|
157
177
|
const ext = blob.type.includes('mp4') ? 'mp4' : 'webm';
|
|
158
178
|
const form = new FormData();
|
|
159
179
|
form.append('audio', blob, `audio.${ext}`);
|
|
160
|
-
|
|
180
|
+
form.append('language', lang);
|
|
181
|
+
const { data } = await post(`/mcp-chat/stt?language=${lang}`, form);
|
|
161
182
|
const text = (data?.text || '').trim();
|
|
162
183
|
setLoading(false);
|
|
163
184
|
if (text) sendMessage(text);
|
|
164
|
-
else setError('
|
|
185
|
+
else setError(t('home.errAudioEmpty'));
|
|
165
186
|
} catch (e: any) {
|
|
166
187
|
setLoading(false);
|
|
167
|
-
setError(e?.response?.data?.error?.message || '
|
|
188
|
+
setError(e?.response?.data?.error?.message || t('home.errStt'));
|
|
168
189
|
}
|
|
169
190
|
};
|
|
170
191
|
|
|
@@ -173,9 +194,9 @@ const HomePage = () => {
|
|
|
173
194
|
<Flex direction="column" alignItems="stretch" gap={4} height="100%">
|
|
174
195
|
<Flex justifyContent="space-between" alignItems="center">
|
|
175
196
|
<Box>
|
|
176
|
-
<Typography variant="beta" tag="h1">
|
|
197
|
+
<Typography variant="beta" tag="h1">{t('home.title')}</Typography>
|
|
177
198
|
<Typography variant="pi" textColor="neutral600">
|
|
178
|
-
|
|
199
|
+
{t('home.subtitle')} {sharing ? t('home.seeingScreen') : ''} {voiceOn ? t('home.voiceOn') : ''}
|
|
179
200
|
</Typography>
|
|
180
201
|
</Box>
|
|
181
202
|
<Flex gap={1}>
|
|
@@ -184,14 +205,22 @@ const HomePage = () => {
|
|
|
184
205
|
variant={voiceOn ? 'success-light' : 'tertiary'}
|
|
185
206
|
onClick={() => setVoiceOn((v) => !v)}
|
|
186
207
|
>
|
|
187
|
-
{voiceOn ? '
|
|
208
|
+
{voiceOn ? t('home.voiceBtnOn') : t('home.voiceBtnOff')}
|
|
209
|
+
</Button>
|
|
210
|
+
<Button
|
|
211
|
+
size="S"
|
|
212
|
+
variant={autoPublish ? 'danger-light' : 'tertiary'}
|
|
213
|
+
onClick={() => setAutoPublish((v) => !v)}
|
|
214
|
+
title={t('home.pubTitle')}
|
|
215
|
+
>
|
|
216
|
+
{autoPublish ? t('home.pubOn') : t('home.pubOff')}
|
|
188
217
|
</Button>
|
|
189
218
|
<Button
|
|
190
219
|
size="S"
|
|
191
220
|
variant={sharing ? 'danger-light' : 'secondary'}
|
|
192
221
|
onClick={sharing ? stopShare : startShare}
|
|
193
222
|
>
|
|
194
|
-
{sharing ? '
|
|
223
|
+
{sharing ? t('home.shareStop') : t('home.shareStart')}
|
|
195
224
|
</Button>
|
|
196
225
|
</Flex>
|
|
197
226
|
</Flex>
|
|
@@ -205,9 +234,7 @@ const HomePage = () => {
|
|
|
205
234
|
style={{ overflowY: 'auto', minHeight: 240 }}
|
|
206
235
|
>
|
|
207
236
|
{messages.length === 0 && (
|
|
208
|
-
<Typography textColor="neutral500">
|
|
209
|
-
Escreva, fale (🎤) ou compartilhe a tela. Ex.: “Quais content-types existem?”.
|
|
210
|
-
</Typography>
|
|
237
|
+
<Typography textColor="neutral500">{t('home.empty')}</Typography>
|
|
211
238
|
)}
|
|
212
239
|
<Flex direction="column" alignItems="stretch" gap={3}>
|
|
213
240
|
{messages.map((m, i) => (
|
|
@@ -218,7 +245,7 @@ const HomePage = () => {
|
|
|
218
245
|
background={m.role === 'user' ? 'primary100' : 'neutral100'}
|
|
219
246
|
>
|
|
220
247
|
<Typography variant="sigma" textColor={m.role === 'user' ? 'primary600' : 'neutral600'}>
|
|
221
|
-
{m.role === 'user' ? '
|
|
248
|
+
{m.role === 'user' ? t('home.you') : t('home.ai')}
|
|
222
249
|
</Typography>
|
|
223
250
|
<Box paddingTop={1}>
|
|
224
251
|
<Typography style={{ whiteSpace: 'pre-wrap' }}>{m.content}</Typography>
|
|
@@ -234,7 +261,7 @@ const HomePage = () => {
|
|
|
234
261
|
)}
|
|
235
262
|
</Box>
|
|
236
263
|
))}
|
|
237
|
-
{loading && <Typography textColor="neutral500">
|
|
264
|
+
{loading && <Typography textColor="neutral500">{t('home.processing')}</Typography>}
|
|
238
265
|
</Flex>
|
|
239
266
|
</Box>
|
|
240
267
|
|
|
@@ -249,12 +276,12 @@ const HomePage = () => {
|
|
|
249
276
|
variant={recording ? 'danger-light' : 'tertiary'}
|
|
250
277
|
onClick={recording ? stopRecording : startRecording}
|
|
251
278
|
>
|
|
252
|
-
{recording ? '
|
|
279
|
+
{recording ? t('home.recStop') : t('home.rec')}
|
|
253
280
|
</Button>
|
|
254
281
|
<Box grow={1}>
|
|
255
282
|
<Textarea
|
|
256
283
|
name="message"
|
|
257
|
-
placeholder=
|
|
284
|
+
placeholder={t('home.placeholder')}
|
|
258
285
|
value={input}
|
|
259
286
|
onChange={(e: any) => setInput(e.target.value)}
|
|
260
287
|
onKeyDown={(e: any) => {
|
|
@@ -263,7 +290,7 @@ const HomePage = () => {
|
|
|
263
290
|
/>
|
|
264
291
|
</Box>
|
|
265
292
|
<Button onClick={send} loading={loading} disabled={!input.trim()}>
|
|
266
|
-
|
|
293
|
+
{t('home.send')}
|
|
267
294
|
</Button>
|
|
268
295
|
</Flex>
|
|
269
296
|
</Flex>
|
|
@@ -274,13 +301,13 @@ const HomePage = () => {
|
|
|
274
301
|
<Flex gap={2} alignItems="center">
|
|
275
302
|
<Box grow={1}>
|
|
276
303
|
<TextInput
|
|
277
|
-
aria-label=
|
|
304
|
+
aria-label={t('home.previewUrlLabel')}
|
|
278
305
|
value={previewUrl}
|
|
279
306
|
onChange={(e: any) => setPreviewUrl(e.target.value)}
|
|
280
307
|
/>
|
|
281
308
|
</Box>
|
|
282
309
|
<Button size="S" variant="tertiary" onClick={() => setIframeKey((k) => k + 1)}>
|
|
283
|
-
|
|
310
|
+
{t('home.reload')}
|
|
284
311
|
</Button>
|
|
285
312
|
</Flex>
|
|
286
313
|
<Box
|
|
@@ -304,15 +331,19 @@ const HomePage = () => {
|
|
|
304
331
|
<Box padding={6} background="neutral100" style={{ minHeight: '100vh' }}>
|
|
305
332
|
<video ref={videoRef} autoPlay muted style={{ display: 'none' }} />
|
|
306
333
|
|
|
334
|
+
<Onboarding lang={lang} open={tourOpen} onClose={() => setTourOpen(false)} />
|
|
335
|
+
|
|
307
336
|
<Flex justifyContent="flex-end" gap={2} paddingBottom={4}>
|
|
337
|
+
<Button variant="tertiary" onClick={() => setTourOpen(true)}>{t('home.tour')}</Button>
|
|
338
|
+
<LangSwitcher />
|
|
308
339
|
<Link to="provision">
|
|
309
|
-
<Button variant="secondary">
|
|
340
|
+
<Button variant="secondary">{t('home.provision')}</Button>
|
|
310
341
|
</Link>
|
|
311
342
|
<Button
|
|
312
343
|
variant={previewOn ? 'success-light' : 'default'}
|
|
313
344
|
onClick={() => setPreviewOn((v) => !v)}
|
|
314
345
|
>
|
|
315
|
-
{previewOn ? '
|
|
346
|
+
{previewOn ? t('home.previewOn') : t('home.previewOff')}
|
|
316
347
|
</Button>
|
|
317
348
|
</Flex>
|
|
318
349
|
|