strapi-plugin-mcp-chat 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +265 -0
  3. package/admin/src/components/AdminOverlays.tsx +190 -0
  4. package/admin/src/components/FloatingChat.tsx +370 -0
  5. package/admin/src/components/PreviewPanel.tsx +188 -0
  6. package/admin/src/index.tsx +49 -0
  7. package/admin/src/pages/App.tsx +14 -0
  8. package/admin/src/pages/HomePage.tsx +333 -0
  9. package/admin/src/pages/ProvisionPage.tsx +391 -0
  10. package/admin/src/pluginId.ts +1 -0
  11. package/dist/server/index.js +3511 -0
  12. package/package.json +77 -0
  13. package/server/src/content-tools.ts +520 -0
  14. package/server/src/controllers/audio.ts +45 -0
  15. package/server/src/controllers/chat.ts +22 -0
  16. package/server/src/controllers/frontend.ts +310 -0
  17. package/server/src/index.ts +43 -0
  18. package/server/src/mcp/index.ts +24 -0
  19. package/server/src/mcp/tools/buscar-texto.ts +28 -0
  20. package/server/src/mcp/tools/criar-locale.ts +30 -0
  21. package/server/src/mcp/tools/editar-campo.ts +39 -0
  22. package/server/src/mcp/tools/habilitar-i18n.ts +33 -0
  23. package/server/src/mcp/tools/index.ts +17 -0
  24. package/server/src/mcp/tools/listar-locales.ts +27 -0
  25. package/server/src/mcp/tools/publicar.ts +31 -0
  26. package/server/src/mcp/tools/traduzir.ts +36 -0
  27. package/server/src/mcp/types.ts +11 -0
  28. package/server/src/mcp-client.ts +96 -0
  29. package/server/src/provision/adapters.ts +91 -0
  30. package/server/src/provision/enable-i18n.ts +129 -0
  31. package/server/src/provision/generate.ts +216 -0
  32. package/server/src/provision/infer.ts +495 -0
  33. package/server/src/provision/integrate.ts +963 -0
  34. package/server/src/provision/link.ts +203 -0
  35. package/server/src/provision/manifest.ts +281 -0
  36. package/server/src/provision/orchestrate.ts +236 -0
  37. package/server/src/provision/permissions.ts +58 -0
  38. package/server/src/provision/runner.ts +176 -0
  39. package/server/src/provision/seed.ts +115 -0
  40. package/server/src/provision/translate.ts +153 -0
  41. package/server/src/provision/types-gen.ts +117 -0
  42. package/server/src/provision/write.ts +136 -0
  43. package/server/src/register.ts +17 -0
  44. package/server/src/routes/index.ts +66 -0
  45. package/server/src/services/audio.ts +53 -0
  46. package/server/src/services/chat.ts +263 -0
@@ -0,0 +1,391 @@
1
+ import { useRef, useState, useEffect } from 'react';
2
+ import { Box, Flex, Typography, Button, Loader, Textarea } from '@strapi/design-system';
3
+ import { useFetchClient } from '@strapi/strapi/admin';
4
+ import { Link } from 'react-router-dom';
5
+
6
+ /**
7
+ * Provisão de frontend em duas etapas (cenário Figma/Lovable, sem manifest):
8
+ * 1. Analisar: sobe o .zip → o plugin extrai e, se não houver manifest, a IA o
9
+ * infere a partir do código. A UI mostra o manifest proposto para revisão.
10
+ * 2. Provisionar: o usuário confirma (podendo editar o JSON) → cria as
11
+ * content-types, semeia, libera leitura e liga o preview (a Strapi reinicia).
12
+ */
13
+
14
+ type Phase = 'idle' | 'analyzing' | 'review' | 'provisioning' | 'ready' | 'done-noreload' | 'error';
15
+
16
+ interface AnalyzeResp {
17
+ ok: boolean;
18
+ frontendDir: string;
19
+ inferred: boolean;
20
+ framework: string;
21
+ filesAnalyzed: string[];
22
+ manifest: any;
23
+ errors: string[];
24
+ message: string;
25
+ }
26
+
27
+ interface ProvisionDone {
28
+ name: string;
29
+ framework: string;
30
+ frontendDir: string;
31
+ contentTypes: string[];
32
+ previewUrl: string;
33
+ seedCreated: { uid: string; count: number }[];
34
+ linkErrors: string[];
35
+ finishedAt: string;
36
+ }
37
+
38
+ const POLL_MS = 2000;
39
+ const POLL_TIMEOUT_MS = 120000;
40
+
41
+ const ProvisionPage = () => {
42
+ const { post, get } = useFetchClient();
43
+ const inputRef = useRef<HTMLInputElement | null>(null);
44
+
45
+ const [file, setFile] = useState<File | null>(null);
46
+ const [phase, setPhase] = useState<Phase>('idle');
47
+ const [error, setError] = useState<string | null>(null);
48
+
49
+ // resultado da análise / revisão
50
+ const [frontendDir, setFrontendDir] = useState('');
51
+ const [manifestText, setManifestText] = useState('');
52
+ const [inferred, setInferred] = useState(false);
53
+ const [framework, setFramework] = useState('');
54
+ const [filesAnalyzed, setFilesAnalyzed] = useState<string[]>([]);
55
+
56
+ const [done, setDone] = useState<ProvisionDone | null>(null);
57
+ const [noReloadMsg, setNoReloadMsg] = useState('');
58
+
59
+ // religamento (snapshot) do frontend ao Strapi
60
+ const [integrating, setIntegrating] = useState(false);
61
+ const [integrateMsg, setIntegrateMsg] = useState('');
62
+
63
+ const stopRef = useRef(false);
64
+ useEffect(() => () => { stopRef.current = true; }, []);
65
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
66
+
67
+ const errDetail = (e: any, fallback: string) => {
68
+ const body = e?.response?.data;
69
+ const base =
70
+ body?.error?.message ||
71
+ body?.message ||
72
+ (Array.isArray(body?.errors) ? body.errors.join('; ') : null) ||
73
+ e?.message ||
74
+ fallback;
75
+ const extra =
76
+ body?.errors && Array.isArray(body.errors) && body?.message
77
+ ? `\n• ${body.errors.join('\n• ')}`
78
+ : '';
79
+ return `${base}${extra}`;
80
+ };
81
+
82
+ // ── Etapa 1: analisar ──────────────────────────────────────────────────────
83
+ const analyze = async () => {
84
+ if (!file || phase === 'analyzing') return;
85
+ setError(null);
86
+ setDone(null);
87
+ setPhase('analyzing');
88
+ try {
89
+ const form = new FormData();
90
+ form.append('frontend', file, file.name);
91
+ const { data } = await post('/mcp-chat/frontend/analyze', form);
92
+ const res = data as AnalyzeResp;
93
+ setFrontendDir(res.frontendDir);
94
+ setInferred(res.inferred);
95
+ setFramework(res.framework);
96
+ setFilesAnalyzed(res.filesAnalyzed || []);
97
+ setManifestText(res.manifest ? JSON.stringify(res.manifest, null, 2) : '');
98
+ if (!res.ok && res.errors?.length) {
99
+ setError(`Aviso da análise:\n• ${res.errors.join('\n• ')}`);
100
+ }
101
+ setPhase('review');
102
+ } catch (e: any) {
103
+ setError(errDetail(e, 'Falha ao analisar o projeto.'));
104
+ setPhase('error');
105
+ }
106
+ };
107
+
108
+ // ── Etapa 2: provisionar ───────────────────────────────────────────────────
109
+ const provision = async () => {
110
+ let manifest: any;
111
+ try {
112
+ manifest = JSON.parse(manifestText);
113
+ } catch {
114
+ setError('O manifest não é um JSON válido. Corrija a sintaxe.');
115
+ return;
116
+ }
117
+ setError(null);
118
+ setPhase('provisioning');
119
+ try {
120
+ const { data } = await post('/mcp-chat/frontend/provision', { frontendDir, manifest });
121
+ if (data.willReload) {
122
+ stopRef.current = false;
123
+ void pollUntilReady();
124
+ } else {
125
+ setNoReloadMsg(data.message);
126
+ setPhase('done-noreload');
127
+ }
128
+ } catch (e: any) {
129
+ setError(errDetail(e, 'Falha na provisão.'));
130
+ setPhase('error');
131
+ }
132
+ };
133
+
134
+ const pollUntilReady = async () => {
135
+ const startedAt = Date.now();
136
+ while (!stopRef.current) {
137
+ if (Date.now() - startedAt > POLL_TIMEOUT_MS) {
138
+ setError('A provisão demorou mais que o esperado. Verifique o terminal da Strapi.');
139
+ setPhase('error');
140
+ return;
141
+ }
142
+ await sleep(POLL_MS);
143
+ try {
144
+ const { data } = await get('/mcp-chat/frontend/status');
145
+ if (data && data.pending === false && data.done) {
146
+ setDone(data.done as ProvisionDone);
147
+ setPhase('ready');
148
+ return;
149
+ }
150
+ } catch {
151
+ // servidor reiniciando: continua tentando.
152
+ }
153
+ }
154
+ };
155
+
156
+ const integrate = async () => {
157
+ if (integrating) return;
158
+ setIntegrating(true);
159
+ setIntegrateMsg('');
160
+ try {
161
+ const { data } = await post('/mcp-chat/frontend/integrate', {});
162
+ if (data.ok) {
163
+ setIntegrateMsg(
164
+ `✅ Religado! Arquivos atualizados: ${data.filesRewritten.join(', ')}. ` +
165
+ `Recarregue o preview para ver os dados do Strapi. (Original salvo como .bak.)`
166
+ );
167
+ } else {
168
+ setIntegrateMsg(
169
+ `⚠️ Não consegui religar: ${(data.errors || []).join('; ') || 'sem arquivo de dados'}.`
170
+ );
171
+ }
172
+ } catch (e: any) {
173
+ setIntegrateMsg(`⚠️ ${errDetail(e, 'Falha ao religar.')}`);
174
+ } finally {
175
+ setIntegrating(false);
176
+ }
177
+ };
178
+
179
+ const reset = () => {
180
+ setFile(null);
181
+ setError(null);
182
+ setDone(null);
183
+ setManifestText('');
184
+ setFrontendDir('');
185
+ setFilesAnalyzed([]);
186
+ setIntegrateMsg('');
187
+ setPhase('idle');
188
+ if (inputRef.current) inputRef.current.value = '';
189
+ };
190
+
191
+ const busy = phase === 'analyzing' || phase === 'provisioning';
192
+
193
+ return (
194
+ <Box padding={6} background="neutral100" style={{ minHeight: '100vh' }}>
195
+ <Flex justifyContent="space-between" alignItems="center" paddingBottom={4}>
196
+ <Box>
197
+ <Typography variant="alpha" tag="h1">Provisionar frontend</Typography>
198
+ <Typography variant="pi" textColor="neutral600">
199
+ Suba o .zip do seu frontend (Figma/Lovable, Next ou TanStack) — a IA infere o
200
+ modelo de conteúdo, você revisa, e o plugin cria tudo no Strapi.
201
+ </Typography>
202
+ </Box>
203
+ <Link to="..">
204
+ <Button variant="tertiary">← Voltar ao chat</Button>
205
+ </Link>
206
+ </Flex>
207
+
208
+ <Box
209
+ background="neutral0"
210
+ hasRadius
211
+ shadow="tableShadow"
212
+ padding={6}
213
+ style={{ maxWidth: 820, margin: '0 auto' }}
214
+ >
215
+ <Flex direction="column" alignItems="stretch" gap={4}>
216
+ {/* Seleção do arquivo */}
217
+ <Typography variant="delta" tag="h2">1. Escolha o .zip do frontend</Typography>
218
+ <Typography textColor="neutral600">
219
+ Não precisa de <code>strapi.manifest.json</code>: se ele não existir, a IA cria um
220
+ analisando os dados do código (ex.: <code>src/data/*.ts</code>).
221
+ </Typography>
222
+
223
+ <input
224
+ ref={inputRef}
225
+ type="file"
226
+ accept=".zip,application/zip"
227
+ style={{ display: 'none' }}
228
+ onChange={(e) => {
229
+ setError(null);
230
+ setDone(null);
231
+ setPhase('idle');
232
+ setManifestText('');
233
+ setFile(e.target.files?.[0] ?? null);
234
+ }}
235
+ />
236
+
237
+ <Flex gap={2} alignItems="center">
238
+ <Button variant="secondary" onClick={() => inputRef.current?.click()} disabled={busy}>
239
+ Selecionar arquivo…
240
+ </Button>
241
+ <Typography textColor={file ? 'neutral800' : 'neutral500'}>
242
+ {file ? file.name : 'Nenhum arquivo selecionado'}
243
+ </Typography>
244
+ </Flex>
245
+
246
+ {(phase === 'idle' || phase === 'analyzing') && (
247
+ <Box paddingTop={2}>
248
+ <Button onClick={analyze} loading={phase === 'analyzing'} disabled={!file || busy}>
249
+ Analisar projeto
250
+ </Button>
251
+ </Box>
252
+ )}
253
+
254
+ {phase === 'analyzing' && (
255
+ <Flex gap={3} alignItems="center" background="primary100" padding={4} hasRadius>
256
+ <Loader small>Analisando…</Loader>
257
+ <Typography textColor="primary700">
258
+ Lendo o código e inferindo o modelo de conteúdo (content-types + seed)…
259
+ </Typography>
260
+ </Flex>
261
+ )}
262
+
263
+ {/* Etapa 2: revisão do manifest */}
264
+ {phase === 'review' && (
265
+ <>
266
+ <Box height="1px" background="neutral200" />
267
+ <Typography variant="delta" tag="h2">2. Revise o modelo de conteúdo</Typography>
268
+ <Flex gap={2} alignItems="center" wrap="wrap">
269
+ <Box background={inferred ? 'warning100' : 'success100'} padding={2} hasRadius>
270
+ <Typography variant="pi" textColor={inferred ? 'warning700' : 'success700'}>
271
+ {inferred ? '🤖 Inferido pela IA' : '✓ Manifest do projeto'} • framework: {framework}
272
+ </Typography>
273
+ </Box>
274
+ {filesAnalyzed.length > 0 && (
275
+ <Typography variant="pi" textColor="neutral600">
276
+ Analisou: {filesAnalyzed.slice(0, 6).join(', ')}
277
+ {filesAnalyzed.length > 6 ? ` +${filesAnalyzed.length - 6}` : ''}
278
+ </Typography>
279
+ )}
280
+ </Flex>
281
+ <Typography variant="pi" textColor="neutral600">
282
+ Edite o JSON se quiser ajustar nomes, tipos ou o conteúdo semeado antes de criar.
283
+ </Typography>
284
+ <Textarea
285
+ name="manifest"
286
+ value={manifestText}
287
+ onChange={(e: any) => setManifestText(e.target.value)}
288
+ style={{ fontFamily: 'monospace', fontSize: 12, minHeight: 320 }}
289
+ />
290
+ <Flex gap={2}>
291
+ <Button onClick={provision} disabled={!manifestText.trim()}>
292
+ Provisionar
293
+ </Button>
294
+ <Button variant="tertiary" onClick={reset}>Recomeçar</Button>
295
+ </Flex>
296
+ </>
297
+ )}
298
+
299
+ {/* Provisionando */}
300
+ {phase === 'provisioning' && (
301
+ <Flex direction="column" gap={2} background="primary100" padding={4} hasRadius>
302
+ <Flex gap={3} alignItems="center">
303
+ <Loader small>Provisionando…</Loader>
304
+ <Typography fontWeight="bold" textColor="primary700">
305
+ Configurando tudo — isso leva alguns segundos
306
+ </Typography>
307
+ </Flex>
308
+ <Typography variant="pi" textColor="neutral700">
309
+ A Strapi está reiniciando para reconhecer as content-types, depois semeia o
310
+ conteúdo, libera leitura pública e liga o preview. Não feche esta página.
311
+ </Typography>
312
+ </Flex>
313
+ )}
314
+
315
+ {phase === 'done-noreload' && (
316
+ <Box background="success100" padding={4} hasRadius>
317
+ <Typography textColor="success700">{noReloadMsg}</Typography>
318
+ </Box>
319
+ )}
320
+
321
+ {/* Pronto */}
322
+ {phase === 'ready' && done && (
323
+ <Box background="success100" padding={4} hasRadius>
324
+ <Typography variant="beta" textColor="success700" tag="div">
325
+ ✅ Tudo pronto! Você já pode ver o preview.
326
+ </Typography>
327
+ <Box paddingTop={3}>
328
+ <Typography variant="pi" textColor="neutral700" tag="div">
329
+ Content-types criadas: {done.contentTypes.join(', ')}
330
+ </Typography>
331
+ {done.seedCreated.length > 0 && (
332
+ <Typography variant="pi" textColor="neutral700" tag="div">
333
+ Conteúdo semeado: {done.seedCreated.map((s) => `${s.uid} (${s.count})`).join(', ')}
334
+ </Typography>
335
+ )}
336
+ <Typography variant="pi" textColor="neutral700" tag="div">
337
+ Frontend em: <code>{done.frontendDir}</code>
338
+ </Typography>
339
+ </Box>
340
+ <Box paddingTop={3}>
341
+ <Typography variant="pi" textColor="neutral700" tag="div">
342
+ Para ver o preview, rode o frontend (uma vez):
343
+ </Typography>
344
+ <Box background="neutral0" padding={2} hasRadius marginTop={1}
345
+ style={{ fontFamily: 'monospace', fontSize: 12 }}>
346
+ cd {done.frontendDir} && npm install && npm run dev
347
+ </Box>
348
+ </Box>
349
+ <Box paddingTop={3}>
350
+ <Typography variant="pi" textColor="neutral700" tag="div">
351
+ Religar o frontend ao Strapi (snapshot): troca os dados hardcoded pelos do
352
+ Strapi, mantendo as imagens. Os componentes não mudam.
353
+ </Typography>
354
+ <Box paddingTop={1}>
355
+ <Button onClick={integrate} loading={integrating} variant="default">
356
+ Religar dados ao Strapi
357
+ </Button>
358
+ </Box>
359
+ {integrateMsg && (
360
+ <Box paddingTop={2}>
361
+ <Typography variant="pi" textColor="neutral800" style={{ whiteSpace: 'pre-wrap' }}>
362
+ {integrateMsg}
363
+ </Typography>
364
+ </Box>
365
+ )}
366
+ </Box>
367
+
368
+ <Flex gap={2} paddingTop={3}>
369
+ <a href={done.previewUrl} target="_blank" rel="noreferrer">
370
+ <Button variant="success">Abrir {done.previewUrl} ↗</Button>
371
+ </a>
372
+ <Button variant="tertiary" onClick={reset}>Provisionar outro</Button>
373
+ </Flex>
374
+ </Box>
375
+ )}
376
+
377
+ {error && (
378
+ <Box background={phase === 'error' ? 'danger100' : 'warning100'} padding={3} hasRadius>
379
+ <Typography textColor={phase === 'error' ? 'danger600' : 'warning700'}
380
+ style={{ whiteSpace: 'pre-wrap' }}>
381
+ {error}
382
+ </Typography>
383
+ </Box>
384
+ )}
385
+ </Flex>
386
+ </Box>
387
+ </Box>
388
+ );
389
+ };
390
+
391
+ export { ProvisionPage };
@@ -0,0 +1 @@
1
+ export const PLUGIN_ID = 'mcp-chat';