strapi-plugin-mcp-chat 0.5.0 → 0.6.0
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/admin/src/components/AdminOverlays.tsx +8 -18
- package/admin/src/components/FloatingChat.tsx +70 -3
- package/dist/server/index.js +436 -213
- package/package.json +1 -1
- package/server/src/content-tools.ts +4 -2
- package/server/src/controllers/chat.ts +2 -2
- package/server/src/index.ts +8 -1
- package/server/src/provision/infer.ts +92 -20
- package/server/src/provision/link.ts +232 -35
- package/server/src/provision/orchestrate.ts +4 -3
- package/server/src/provision/runner.ts +44 -1
- package/server/src/services/chat.ts +8 -2
|
@@ -197,26 +197,16 @@ export const AdminOverlays = () => {
|
|
|
197
197
|
<FloatingChat
|
|
198
198
|
previewOn={previewOn}
|
|
199
199
|
previewUrl={liveHref}
|
|
200
|
+
draft={draftPreview}
|
|
200
201
|
onTogglePreview={() => setPreviewOn((v) => !v)}
|
|
201
|
-
onReply={
|
|
202
|
+
onReply={(didWrite) => {
|
|
202
203
|
if (!previewOn) return;
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
setRunText('Sincronizando alterações…');
|
|
210
|
-
setRunLoading(true);
|
|
211
|
-
const { post } = getFetchClient();
|
|
212
|
-
await post('/mcp-chat/frontend/integrate', {});
|
|
213
|
-
} catch {
|
|
214
|
-
/* sem integração: segue só com reload */
|
|
215
|
-
} finally {
|
|
216
|
-
setRunLoading(false);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
reload();
|
|
204
|
+
// Modelo LIVE-FETCH: a Strapi é a fonte da verdade e o front SEMPRE
|
|
205
|
+
// busca dela. A edição do chat já foi gravada na Strapi (no campo
|
|
206
|
+
// exato, via editar_campo) — aqui só recarregamos o preview para o
|
|
207
|
+
// front re-buscar (em draft quando o toggle de rascunho está ligado).
|
|
208
|
+
// NÃO reescrevemos arquivos do frontend (nada de snapshot/integrate).
|
|
209
|
+
if (didWrite) reload();
|
|
220
210
|
}}
|
|
221
211
|
/>
|
|
222
212
|
</>
|
|
@@ -13,6 +13,8 @@ type Pos = { x: number; y: number };
|
|
|
13
13
|
type Props = {
|
|
14
14
|
previewOn: boolean;
|
|
15
15
|
previewUrl: string;
|
|
16
|
+
/** modo do preview: true = rascunho (draft), false = publicado (live). */
|
|
17
|
+
draft?: boolean;
|
|
16
18
|
onTogglePreview: () => void;
|
|
17
19
|
/** chamado após uma resposta; didWrite indica que houve edição no Strapi. */
|
|
18
20
|
onReply: (didWrite: boolean) => void;
|
|
@@ -28,6 +30,7 @@ const STR: Record<Lang, Record<string, string>> = {
|
|
|
28
30
|
rec: '🎤 Enviar áudio', recStop: '⏹ Parar áudio', recTitle: 'Gravar áudio e enviar (transcreve e manda)',
|
|
29
31
|
previewOn: '🖼 Preview: ON', previewOff: '🖼 Preview: OFF', previewTitle: 'Abrir/fechar o preview do site ao lado da Strapi',
|
|
30
32
|
voiceOn: '🔊 Voz: ON', voiceOff: '🔈 Voz: OFF', voiceTitle: 'Ler as respostas em voz alta (TTS)',
|
|
33
|
+
stopVoice: '⏹ Parar voz', stopVoiceTitle: 'Parar a voz que está tocando',
|
|
31
34
|
pubOn: '🚀 Publicar: ON', pubOff: '📝 Rascunho', pubTitle: 'Auto-publicar OFF = a IA só salva rascunho (você revisa e publica). ON = publica direto no site.',
|
|
32
35
|
shareOn: '🛑 Parar tela', shareOff: '🖥 Compart. tela', shareTitle: 'Compartilhar a tela com a IA',
|
|
33
36
|
langTitle: 'Idioma do chat e da voz (PT-BR ↔ English)',
|
|
@@ -45,6 +48,7 @@ const STR: Record<Lang, Record<string, string>> = {
|
|
|
45
48
|
rec: '🎤 Send audio', recStop: '⏹ Stop audio', recTitle: 'Record audio and send (transcribes and sends)',
|
|
46
49
|
previewOn: '🖼 Preview: ON', previewOff: '🖼 Preview: OFF', previewTitle: 'Toggle the site preview next to Strapi',
|
|
47
50
|
voiceOn: '🔊 Voice: ON', voiceOff: '🔈 Voice: OFF', voiceTitle: 'Read replies out loud (TTS)',
|
|
51
|
+
stopVoice: '⏹ Stop voice', stopVoiceTitle: 'Stop the voice currently playing',
|
|
48
52
|
pubOn: '🚀 Publish: ON', pubOff: '📝 Draft', pubTitle: 'Auto-publish OFF = the AI only saves a draft (you review and publish). ON = publishes straight to the site.',
|
|
49
53
|
shareOn: '🛑 Stop screen', shareOff: '🖥 Share screen', shareTitle: 'Share your screen with the AI',
|
|
50
54
|
langTitle: 'Chat and voice language (PT-BR ↔ English)',
|
|
@@ -59,7 +63,7 @@ const STR: Record<Lang, Record<string, string>> = {
|
|
|
59
63
|
},
|
|
60
64
|
};
|
|
61
65
|
|
|
62
|
-
export const FloatingChat = ({ previewOn, previewUrl, onTogglePreview, onReply }: Props) => {
|
|
66
|
+
export const FloatingChat = ({ previewOn, previewUrl, draft = false, onTogglePreview, onReply }: Props) => {
|
|
63
67
|
const [open, setOpen] = useState(false);
|
|
64
68
|
const [pos, setPos] = useState<Pos | null>(null); // null = ancorado à direita
|
|
65
69
|
const [messages, setMessages] = useState<Msg[]>([]);
|
|
@@ -67,6 +71,7 @@ export const FloatingChat = ({ previewOn, previewUrl, onTogglePreview, onReply }
|
|
|
67
71
|
const [loading, setLoading] = useState(false);
|
|
68
72
|
const [sharing, setSharing] = useState(false);
|
|
69
73
|
const [voiceOn, setVoiceOn] = useState(false);
|
|
74
|
+
const [speaking, setSpeaking] = useState(false);
|
|
70
75
|
const [recording, setRecording] = useState(false);
|
|
71
76
|
const [autoPublish, setAutoPublish] = useState<boolean>(() => {
|
|
72
77
|
try { return localStorage.getItem('mcp-chat-autopublish') === '1'; } catch { return false; }
|
|
@@ -87,6 +92,7 @@ export const FloatingChat = ({ previewOn, previewUrl, onTogglePreview, onReply }
|
|
|
87
92
|
const chunksRef = useRef<Blob[]>([]);
|
|
88
93
|
const bodyRef = useRef<HTMLDivElement | null>(null);
|
|
89
94
|
const dragRef = useRef<{ dx: number; dy: number } | null>(null);
|
|
95
|
+
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
90
96
|
|
|
91
97
|
// ── Drag ────────────────────────────────────────────────────────────────
|
|
92
98
|
const onHeaderDown = (e: React.MouseEvent) => {
|
|
@@ -121,6 +127,38 @@ export const FloatingChat = ({ previewOn, previewUrl, onTogglePreview, onReply }
|
|
|
121
127
|
};
|
|
122
128
|
}, []);
|
|
123
129
|
|
|
130
|
+
// ── Logout: limpa a conversa e esconde o chat ──────────────────────────────
|
|
131
|
+
// O overlay vive num React root próprio (fora da árvore do admin), então não dá
|
|
132
|
+
// pra usar useAuth. Detectamos a sessão pela ROTA: a tela de login/logout do
|
|
133
|
+
// Strapi é /admin/auth/... Ao cair nela (logout), zeramos a conversa, encerramos
|
|
134
|
+
// áudio/gravação/compartilhamento e escondemos o FAB — o chat só existe logado.
|
|
135
|
+
const [loggedOut, setLoggedOut] = useState(() => /\/auth(\/|$)/.test(window.location.pathname));
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const check = () => {
|
|
138
|
+
const out = /\/auth(\/|$)/.test(window.location.pathname);
|
|
139
|
+
setLoggedOut((prev) => {
|
|
140
|
+
if (out && !prev) {
|
|
141
|
+
// acabou de deslogar → limpa tudo que é da sessão.
|
|
142
|
+
setMessages([]);
|
|
143
|
+
setInput('');
|
|
144
|
+
setError(null);
|
|
145
|
+
setOpen(false);
|
|
146
|
+
stopTTS();
|
|
147
|
+
try { streamRef.current?.getTracks().forEach((t) => t.stop()); } catch { /* noop */ }
|
|
148
|
+
try { recorderRef.current?.stop(); } catch { /* noop */ }
|
|
149
|
+
setSharing(false);
|
|
150
|
+
setRecording(false);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
check();
|
|
156
|
+
const id = window.setInterval(check, 1000);
|
|
157
|
+
window.addEventListener('popstate', check);
|
|
158
|
+
return () => { window.clearInterval(id); window.removeEventListener('popstate', check); };
|
|
159
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
124
162
|
// ── Screenshare ───────────────────────────────────────────────────────────
|
|
125
163
|
const startShare = async () => {
|
|
126
164
|
setError(null);
|
|
@@ -159,15 +197,34 @@ export const FloatingChat = ({ previewOn, previewUrl, onTogglePreview, onReply }
|
|
|
159
197
|
};
|
|
160
198
|
|
|
161
199
|
// ── TTS ────────────────────────────────────────────────────────────────────
|
|
200
|
+
// Para qualquer áudio em reprodução (botão ⏹ ou ao iniciar uma nova fala).
|
|
201
|
+
const stopTTS = () => {
|
|
202
|
+
const a = audioRef.current;
|
|
203
|
+
if (a) {
|
|
204
|
+
try { a.pause(); a.currentTime = 0; } catch { /* noop */ }
|
|
205
|
+
audioRef.current = null;
|
|
206
|
+
}
|
|
207
|
+
setSpeaking(false);
|
|
208
|
+
};
|
|
209
|
+
|
|
162
210
|
const playTTS = async (text: string) => {
|
|
163
211
|
try {
|
|
212
|
+
stopTTS(); // não sobrepõe falas
|
|
164
213
|
const { post } = getFetchClient();
|
|
165
214
|
const { data } = await post('/mcp-chat/tts', { text });
|
|
166
215
|
const audio = new Audio(`data:${data.content_type};base64,${data.audio_base64}`);
|
|
167
|
-
|
|
168
|
-
|
|
216
|
+
audioRef.current = audio;
|
|
217
|
+
const done = () => { if (audioRef.current === audio) audioRef.current = null; setSpeaking(false); };
|
|
218
|
+
audio.addEventListener('ended', done);
|
|
219
|
+
audio.addEventListener('error', done);
|
|
220
|
+
setSpeaking(true);
|
|
221
|
+
await audio.play().catch(() => done());
|
|
222
|
+
} catch { setSpeaking(false); /* voz é opcional */ }
|
|
169
223
|
};
|
|
170
224
|
|
|
225
|
+
// para a voz se o componente desmontar.
|
|
226
|
+
useEffect(() => () => stopTTS(), []);
|
|
227
|
+
|
|
171
228
|
// ── Enviar ──────────────────────────────────────────────────────────────────
|
|
172
229
|
const sendMessage = async (text: string) => {
|
|
173
230
|
if (!text || loading) return;
|
|
@@ -185,6 +242,8 @@ export const FloatingChat = ({ previewOn, previewUrl, onTogglePreview, onReply }
|
|
|
185
242
|
// Página que o usuário está olhando no preview (se aberto). Dá à IA o
|
|
186
243
|
// contexto do "isso aqui" sem precisar varrer o site inteiro.
|
|
187
244
|
previewUrl: previewOn ? previewUrl : null,
|
|
245
|
+
// A busca de texto segue o modo do preview: draft -> rascunhos, live -> publicado.
|
|
246
|
+
previewStatus: draft ? 'draft' : 'published',
|
|
188
247
|
// Draft-first: por padrão a IA só salva rascunho; só publica com isto ON.
|
|
189
248
|
autoPublish,
|
|
190
249
|
});
|
|
@@ -262,6 +321,9 @@ export const FloatingChat = ({ previewOn, previewUrl, onTogglePreview, onReply }
|
|
|
262
321
|
fontSize: 12, cursor: 'pointer', whiteSpace: 'nowrap',
|
|
263
322
|
});
|
|
264
323
|
|
|
324
|
+
// deslogado: o chat não existe (já foi limpo no efeito acima).
|
|
325
|
+
if (loggedOut) return null;
|
|
326
|
+
|
|
265
327
|
if (!open) {
|
|
266
328
|
return (
|
|
267
329
|
<button
|
|
@@ -337,6 +399,11 @@ export const FloatingChat = ({ previewOn, previewUrl, onTogglePreview, onReply }
|
|
|
337
399
|
title={t.voiceTitle}>
|
|
338
400
|
{voiceOn ? t.voiceOn : t.voiceOff}
|
|
339
401
|
</button>
|
|
402
|
+
{speaking && (
|
|
403
|
+
<button style={btn(true)} onClick={stopTTS} title={t.stopVoiceTitle}>
|
|
404
|
+
{t.stopVoice}
|
|
405
|
+
</button>
|
|
406
|
+
)}
|
|
340
407
|
<button style={btn(autoPublish)} onClick={() => setAutoPublish((v) => !v)}
|
|
341
408
|
title={t.pubTitle}>
|
|
342
409
|
{autoPublish ? t.pubOn : t.pubOff}
|