strapi-plugin-mcp-chat 0.1.0 → 0.5.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/README.md +91 -9
- package/admin/src/components/AdminOverlays.tsx +35 -1
- package/admin/src/components/ErrorBoundary.tsx +29 -0
- package/admin/src/components/FloatingChat.tsx +22 -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/index.tsx +39 -5
- package/admin/src/pages/HomePage.tsx +55 -24
- package/admin/src/pages/ProvisionPage.tsx +54 -59
- package/dist/server/index.js +358 -200
- package/package.json +1 -1
- package/server/src/content-tools.ts +42 -8
- package/server/src/controllers/chat.ts +2 -2
- package/server/src/controllers/frontend.ts +7 -2
- package/server/src/index.ts +6 -1
- package/server/src/mcp/define.ts +72 -0
- package/server/src/mcp/index.ts +19 -6
- package/server/src/mcp/tools/buscar-texto.ts +18 -24
- package/server/src/mcp/tools/criar-locale.ts +20 -26
- package/server/src/mcp/tools/editar-campo.ts +29 -35
- package/server/src/mcp/tools/habilitar-i18n.ts +23 -29
- package/server/src/mcp/tools/listar-locales.ts +17 -23
- package/server/src/mcp/tools/publicar.ts +21 -27
- package/server/src/mcp/tools/traduzir.ts +26 -32
- package/server/src/mcp/types.ts +12 -9
- package/server/src/mcp-client.ts +15 -3
- package/server/src/provision/integrate.ts +15 -1
- package/server/src/provision/write.ts +92 -0
- package/server/src/services/chat.ts +56 -14
|
@@ -2,6 +2,9 @@ import { useRef, useState, useEffect } from 'react';
|
|
|
2
2
|
import { Box, Flex, Typography, Button, Loader, Textarea } 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 { StackLogos } from '../components/StackLogos';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Provisão de frontend em duas etapas (cenário Figma/Lovable, sem manifest):
|
|
@@ -41,6 +44,8 @@ const POLL_TIMEOUT_MS = 120000;
|
|
|
41
44
|
const ProvisionPage = () => {
|
|
42
45
|
const { post, get } = useFetchClient();
|
|
43
46
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
47
|
+
const [lang] = useLang();
|
|
48
|
+
const t = makeT(lang);
|
|
44
49
|
|
|
45
50
|
const [file, setFile] = useState<File | null>(null);
|
|
46
51
|
const [phase, setPhase] = useState<Phase>('idle');
|
|
@@ -96,11 +101,11 @@ const ProvisionPage = () => {
|
|
|
96
101
|
setFilesAnalyzed(res.filesAnalyzed || []);
|
|
97
102
|
setManifestText(res.manifest ? JSON.stringify(res.manifest, null, 2) : '');
|
|
98
103
|
if (!res.ok && res.errors?.length) {
|
|
99
|
-
setError(
|
|
104
|
+
setError(`${t('prov.analyzeWarn')}\n• ${res.errors.join('\n• ')}`);
|
|
100
105
|
}
|
|
101
106
|
setPhase('review');
|
|
102
107
|
} catch (e: any) {
|
|
103
|
-
setError(errDetail(e, '
|
|
108
|
+
setError(errDetail(e, t('prov.analyzeFail')));
|
|
104
109
|
setPhase('error');
|
|
105
110
|
}
|
|
106
111
|
};
|
|
@@ -111,7 +116,7 @@ const ProvisionPage = () => {
|
|
|
111
116
|
try {
|
|
112
117
|
manifest = JSON.parse(manifestText);
|
|
113
118
|
} catch {
|
|
114
|
-
setError('
|
|
119
|
+
setError(t('prov.invalidJson'));
|
|
115
120
|
return;
|
|
116
121
|
}
|
|
117
122
|
setError(null);
|
|
@@ -126,7 +131,7 @@ const ProvisionPage = () => {
|
|
|
126
131
|
setPhase('done-noreload');
|
|
127
132
|
}
|
|
128
133
|
} catch (e: any) {
|
|
129
|
-
setError(errDetail(e, '
|
|
134
|
+
setError(errDetail(e, t('prov.provisionFail')));
|
|
130
135
|
setPhase('error');
|
|
131
136
|
}
|
|
132
137
|
};
|
|
@@ -135,7 +140,7 @@ const ProvisionPage = () => {
|
|
|
135
140
|
const startedAt = Date.now();
|
|
136
141
|
while (!stopRef.current) {
|
|
137
142
|
if (Date.now() - startedAt > POLL_TIMEOUT_MS) {
|
|
138
|
-
setError('
|
|
143
|
+
setError(t('prov.provisionFail'));
|
|
139
144
|
setPhase('error');
|
|
140
145
|
return;
|
|
141
146
|
}
|
|
@@ -160,17 +165,12 @@ const ProvisionPage = () => {
|
|
|
160
165
|
try {
|
|
161
166
|
const { data } = await post('/mcp-chat/frontend/integrate', {});
|
|
162
167
|
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
|
-
);
|
|
168
|
+
setIntegrateMsg(t('prov.relinkOk', { files: data.filesRewritten.join(', ') }));
|
|
167
169
|
} else {
|
|
168
|
-
setIntegrateMsg(
|
|
169
|
-
`⚠️ Não consegui religar: ${(data.errors || []).join('; ') || 'sem arquivo de dados'}.`
|
|
170
|
-
);
|
|
170
|
+
setIntegrateMsg(t('prov.relinkFail', { err: (data.errors || []).join('; ') || t('prov.noData') }));
|
|
171
171
|
}
|
|
172
172
|
} catch (e: any) {
|
|
173
|
-
setIntegrateMsg(`⚠️ ${errDetail(e, '
|
|
173
|
+
setIntegrateMsg(`⚠️ ${errDetail(e, t('prov.relinkErr'))}`);
|
|
174
174
|
} finally {
|
|
175
175
|
setIntegrating(false);
|
|
176
176
|
}
|
|
@@ -194,15 +194,15 @@ const ProvisionPage = () => {
|
|
|
194
194
|
<Box padding={6} background="neutral100" style={{ minHeight: '100vh' }}>
|
|
195
195
|
<Flex justifyContent="space-between" alignItems="center" paddingBottom={4}>
|
|
196
196
|
<Box>
|
|
197
|
-
<Typography variant="alpha" tag="h1">
|
|
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>
|
|
197
|
+
<Typography variant="alpha" tag="h1">{t('prov.title')}</Typography>
|
|
198
|
+
<Typography variant="pi" textColor="neutral600">{t('prov.subtitle')}</Typography>
|
|
202
199
|
</Box>
|
|
203
|
-
<
|
|
204
|
-
<
|
|
205
|
-
|
|
200
|
+
<Flex gap={2} alignItems="center">
|
|
201
|
+
<LangSwitcher />
|
|
202
|
+
<Link to="..">
|
|
203
|
+
<Button variant="tertiary">{t('prov.back')}</Button>
|
|
204
|
+
</Link>
|
|
205
|
+
</Flex>
|
|
206
206
|
</Flex>
|
|
207
207
|
|
|
208
208
|
<Box
|
|
@@ -213,12 +213,17 @@ const ProvisionPage = () => {
|
|
|
213
213
|
style={{ maxWidth: 820, margin: '0 auto' }}
|
|
214
214
|
>
|
|
215
215
|
<Flex direction="column" alignItems="stretch" gap={4}>
|
|
216
|
+
{/* Stacks suportados */}
|
|
217
|
+
<Box>
|
|
218
|
+
<Typography variant="sigma" textColor="neutral600" tag="div">{t('prov.supported')}</Typography>
|
|
219
|
+
<Box paddingTop={2}><StackLogos /></Box>
|
|
220
|
+
</Box>
|
|
221
|
+
|
|
222
|
+
<Box height="1px" background="neutral200" />
|
|
223
|
+
|
|
216
224
|
{/* Seleção do arquivo */}
|
|
217
|
-
<Typography variant="delta" tag="h2">
|
|
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>
|
|
225
|
+
<Typography variant="delta" tag="h2">{t('prov.step1')}</Typography>
|
|
226
|
+
<Typography textColor="neutral600">{t('prov.step1desc')}</Typography>
|
|
222
227
|
|
|
223
228
|
<input
|
|
224
229
|
ref={inputRef}
|
|
@@ -236,27 +241,25 @@ const ProvisionPage = () => {
|
|
|
236
241
|
|
|
237
242
|
<Flex gap={2} alignItems="center">
|
|
238
243
|
<Button variant="secondary" onClick={() => inputRef.current?.click()} disabled={busy}>
|
|
239
|
-
|
|
244
|
+
{t('prov.selectFile')}
|
|
240
245
|
</Button>
|
|
241
246
|
<Typography textColor={file ? 'neutral800' : 'neutral500'}>
|
|
242
|
-
{file ? file.name : '
|
|
247
|
+
{file ? file.name : t('prov.noFile')}
|
|
243
248
|
</Typography>
|
|
244
249
|
</Flex>
|
|
245
250
|
|
|
246
251
|
{(phase === 'idle' || phase === 'analyzing') && (
|
|
247
252
|
<Box paddingTop={2}>
|
|
248
253
|
<Button onClick={analyze} loading={phase === 'analyzing'} disabled={!file || busy}>
|
|
249
|
-
|
|
254
|
+
{t('prov.analyze')}
|
|
250
255
|
</Button>
|
|
251
256
|
</Box>
|
|
252
257
|
)}
|
|
253
258
|
|
|
254
259
|
{phase === 'analyzing' && (
|
|
255
260
|
<Flex gap={3} alignItems="center" background="primary100" padding={4} hasRadius>
|
|
256
|
-
<Loader small>
|
|
257
|
-
<Typography textColor="primary700">
|
|
258
|
-
Lendo o código e inferindo o modelo de conteúdo (content-types + seed)…
|
|
259
|
-
</Typography>
|
|
261
|
+
<Loader small>{t('prov.analyzing')}</Loader>
|
|
262
|
+
<Typography textColor="primary700">{t('prov.analyzingDesc')}</Typography>
|
|
260
263
|
</Flex>
|
|
261
264
|
)}
|
|
262
265
|
|
|
@@ -264,23 +267,21 @@ const ProvisionPage = () => {
|
|
|
264
267
|
{phase === 'review' && (
|
|
265
268
|
<>
|
|
266
269
|
<Box height="1px" background="neutral200" />
|
|
267
|
-
<Typography variant="delta" tag="h2">
|
|
270
|
+
<Typography variant="delta" tag="h2">{t('prov.step2')}</Typography>
|
|
268
271
|
<Flex gap={2} alignItems="center" wrap="wrap">
|
|
269
272
|
<Box background={inferred ? 'warning100' : 'success100'} padding={2} hasRadius>
|
|
270
273
|
<Typography variant="pi" textColor={inferred ? 'warning700' : 'success700'}>
|
|
271
|
-
{inferred ? '
|
|
274
|
+
{inferred ? t('prov.inferred') : t('prov.fromManifest')} • {t('prov.framework')}: {framework}
|
|
272
275
|
</Typography>
|
|
273
276
|
</Box>
|
|
274
277
|
{filesAnalyzed.length > 0 && (
|
|
275
278
|
<Typography variant="pi" textColor="neutral600">
|
|
276
|
-
|
|
279
|
+
{t('prov.analyzed')}: {filesAnalyzed.slice(0, 6).join(', ')}
|
|
277
280
|
{filesAnalyzed.length > 6 ? ` +${filesAnalyzed.length - 6}` : ''}
|
|
278
281
|
</Typography>
|
|
279
282
|
)}
|
|
280
283
|
</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
|
+
<Typography variant="pi" textColor="neutral600">{t('prov.editJson')}</Typography>
|
|
284
285
|
<Textarea
|
|
285
286
|
name="manifest"
|
|
286
287
|
value={manifestText}
|
|
@@ -289,9 +290,9 @@ const ProvisionPage = () => {
|
|
|
289
290
|
/>
|
|
290
291
|
<Flex gap={2}>
|
|
291
292
|
<Button onClick={provision} disabled={!manifestText.trim()}>
|
|
292
|
-
|
|
293
|
+
{t('prov.provision')}
|
|
293
294
|
</Button>
|
|
294
|
-
<Button variant="tertiary" onClick={reset}>
|
|
295
|
+
<Button variant="tertiary" onClick={reset}>{t('prov.restart')}</Button>
|
|
295
296
|
</Flex>
|
|
296
297
|
</>
|
|
297
298
|
)}
|
|
@@ -300,15 +301,10 @@ const ProvisionPage = () => {
|
|
|
300
301
|
{phase === 'provisioning' && (
|
|
301
302
|
<Flex direction="column" gap={2} background="primary100" padding={4} hasRadius>
|
|
302
303
|
<Flex gap={3} alignItems="center">
|
|
303
|
-
<Loader small>
|
|
304
|
-
<Typography fontWeight="bold" textColor="primary700">
|
|
305
|
-
Configurando tudo — isso leva alguns segundos
|
|
306
|
-
</Typography>
|
|
304
|
+
<Loader small>{t('prov.provisioning')}</Loader>
|
|
305
|
+
<Typography fontWeight="bold" textColor="primary700">{t('prov.provisioningTitle')}</Typography>
|
|
307
306
|
</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>
|
|
307
|
+
<Typography variant="pi" textColor="neutral700">{t('prov.provisioningDesc')}</Typography>
|
|
312
308
|
</Flex>
|
|
313
309
|
)}
|
|
314
310
|
|
|
@@ -322,24 +318,24 @@ const ProvisionPage = () => {
|
|
|
322
318
|
{phase === 'ready' && done && (
|
|
323
319
|
<Box background="success100" padding={4} hasRadius>
|
|
324
320
|
<Typography variant="beta" textColor="success700" tag="div">
|
|
325
|
-
|
|
321
|
+
{t('prov.doneTitle')}
|
|
326
322
|
</Typography>
|
|
327
323
|
<Box paddingTop={3}>
|
|
328
324
|
<Typography variant="pi" textColor="neutral700" tag="div">
|
|
329
|
-
|
|
325
|
+
{t('prov.typesCreated')} {done.contentTypes.join(', ')}
|
|
330
326
|
</Typography>
|
|
331
327
|
{done.seedCreated.length > 0 && (
|
|
332
328
|
<Typography variant="pi" textColor="neutral700" tag="div">
|
|
333
|
-
|
|
329
|
+
{t('prov.seeded')} {done.seedCreated.map((s) => `${s.uid} (${s.count})`).join(', ')}
|
|
334
330
|
</Typography>
|
|
335
331
|
)}
|
|
336
332
|
<Typography variant="pi" textColor="neutral700" tag="div">
|
|
337
|
-
|
|
333
|
+
{t('prov.frontendAt')} <code>{done.frontendDir}</code>
|
|
338
334
|
</Typography>
|
|
339
335
|
</Box>
|
|
340
336
|
<Box paddingTop={3}>
|
|
341
337
|
<Typography variant="pi" textColor="neutral700" tag="div">
|
|
342
|
-
|
|
338
|
+
{t('prov.runFrontend')}
|
|
343
339
|
</Typography>
|
|
344
340
|
<Box background="neutral0" padding={2} hasRadius marginTop={1}
|
|
345
341
|
style={{ fontFamily: 'monospace', fontSize: 12 }}>
|
|
@@ -348,12 +344,11 @@ const ProvisionPage = () => {
|
|
|
348
344
|
</Box>
|
|
349
345
|
<Box paddingTop={3}>
|
|
350
346
|
<Typography variant="pi" textColor="neutral700" tag="div">
|
|
351
|
-
|
|
352
|
-
Strapi, mantendo as imagens. Os componentes não mudam.
|
|
347
|
+
{t('prov.relinkDesc')}
|
|
353
348
|
</Typography>
|
|
354
349
|
<Box paddingTop={1}>
|
|
355
350
|
<Button onClick={integrate} loading={integrating} variant="default">
|
|
356
|
-
|
|
351
|
+
{t('prov.relink')}
|
|
357
352
|
</Button>
|
|
358
353
|
</Box>
|
|
359
354
|
{integrateMsg && (
|
|
@@ -367,9 +362,9 @@ const ProvisionPage = () => {
|
|
|
367
362
|
|
|
368
363
|
<Flex gap={2} paddingTop={3}>
|
|
369
364
|
<a href={done.previewUrl} target="_blank" rel="noreferrer">
|
|
370
|
-
<Button variant="success">
|
|
365
|
+
<Button variant="success">{t('prov.open')} {done.previewUrl} ↗</Button>
|
|
371
366
|
</a>
|
|
372
|
-
<Button variant="tertiary" onClick={reset}>
|
|
367
|
+
<Button variant="tertiary" onClick={reset}>{t('prov.provisionAnother')}</Button>
|
|
373
368
|
</Flex>
|
|
374
369
|
</Box>
|
|
375
370
|
)}
|