@arqel-dev/ai 0.8.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/LICENSE +21 -0
- package/README.md +24 -0
- package/SKILL.md +425 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.js +1133 -0
- package/dist/index.js.map +1 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +1342 -0
- package/dist/register.js.map +1 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arqel Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @arqel-dev/ai
|
|
2
|
+
|
|
3
|
+
Componentes React do Arqel AI.
|
|
4
|
+
|
|
5
|
+
Exporta `<AiTextInput>`, par React do PHP `Arqel\Ai\Fields\AiTextField`.
|
|
6
|
+
Veja [SKILL.md](./SKILL.md) para o contrato completo de props e
|
|
7
|
+
exemplos.
|
|
8
|
+
|
|
9
|
+
## Instalação
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @arqel-dev/ai
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Uso
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import '@arqel-dev/ai/register'; // registra no FieldRegistry de @arqel-dev/ui
|
|
19
|
+
|
|
20
|
+
// ou import direto:
|
|
21
|
+
import { AiTextInput } from '@arqel-dev/ai';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Licença: MIT.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# SKILL.md — arqel-dev/ai (JS)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Pacote React que entrega os componentes UI do Arqel AI. Por enquanto
|
|
6
|
+
exporta apenas `<AiTextInput>`, espelhando o PHP
|
|
7
|
+
`Arqel\Ai\Fields\AiTextField` (component string `AiTextInput`).
|
|
8
|
+
|
|
9
|
+
O componente é **apresentacional** mas precisa fazer **uma chamada de
|
|
10
|
+
rede explícita** quando o usuário clica em "Generate with AI" — o
|
|
11
|
+
backend (`AiGenerateController`) é quem resolve o prompt template
|
|
12
|
+
(que **nunca** trafega para o cliente) e devolve `{ text }`. O
|
|
13
|
+
contrato de rota canônico é `POST /admin/{resource}/fields/{field}/generate`.
|
|
14
|
+
|
|
15
|
+
## Key Contracts
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface AiTextInputProps {
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
onChange?: (value: string) => void;
|
|
22
|
+
props: {
|
|
23
|
+
provider?: string | null;
|
|
24
|
+
buttonLabel: string;
|
|
25
|
+
maxLength?: number | null;
|
|
26
|
+
hasContextFields: boolean;
|
|
27
|
+
} | undefined;
|
|
28
|
+
resource?: string;
|
|
29
|
+
field?: string;
|
|
30
|
+
formData?: Record<string, unknown>;
|
|
31
|
+
generateUrl?: string;
|
|
32
|
+
csrfToken?: string;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Comportamento ao clicar no botão:
|
|
37
|
+
|
|
38
|
+
1. Resolve URL: `generateUrl` (override) ou
|
|
39
|
+
`/admin/${resource}/fields/${field}/generate`.
|
|
40
|
+
2. `fetch(url, { method: 'POST', credentials: 'same-origin', headers:
|
|
41
|
+
{ Content-Type: application/json, Accept: application/json,
|
|
42
|
+
X-CSRF-TOKEN: csrfToken ?? '' }, body: JSON.stringify({ formData })
|
|
43
|
+
})`.
|
|
44
|
+
3. Sucesso (`response.ok`) → lê `{ text }` do JSON, chama
|
|
45
|
+
`onChange(text)` (ou seta state interno se uncontrolled), troca o
|
|
46
|
+
label para `Regenerate`.
|
|
47
|
+
4. Falha → renderiza banner com `Generation failed (HTTP 500).` e
|
|
48
|
+
reabilita o botão.
|
|
49
|
+
|
|
50
|
+
## Conventions
|
|
51
|
+
|
|
52
|
+
- TypeScript strict, sem `any` (apenas asserts pontuais comentados em
|
|
53
|
+
`register.tsx` para compatibilidade com a assinatura genérica do
|
|
54
|
+
FieldRegistry).
|
|
55
|
+
- Component name canônico: `AiTextInput`. Não renomear — o PHP usa
|
|
56
|
+
essa string em `getComponent()`.
|
|
57
|
+
- Lint: Biome (`pnpm lint`); testes: Vitest + Testing Library; build:
|
|
58
|
+
tsup.
|
|
59
|
+
- Side-effects ficam isolados em `dist/register.js` para tree-shaking.
|
|
60
|
+
- SSR-safe: nada no render path toca `window`/`document`.
|
|
61
|
+
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
### Uso direto
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { AiTextInput } from '@arqel-dev/ai';
|
|
68
|
+
|
|
69
|
+
<AiTextInput
|
|
70
|
+
name="description"
|
|
71
|
+
value={form.description}
|
|
72
|
+
onChange={(v) => setForm((f) => ({ ...f, description: v }))}
|
|
73
|
+
props={{
|
|
74
|
+
provider: 'claude',
|
|
75
|
+
buttonLabel: 'Gerar descrição',
|
|
76
|
+
maxLength: 1000,
|
|
77
|
+
hasContextFields: true,
|
|
78
|
+
}}
|
|
79
|
+
resource="products"
|
|
80
|
+
field="description"
|
|
81
|
+
formData={form}
|
|
82
|
+
csrfToken={csrfToken}
|
|
83
|
+
/>;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Via FieldRegistry do `@arqel-dev/ui`
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// boot.ts
|
|
90
|
+
import '@arqel-dev/ai/register';
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A partir daí qualquer schema com `component: 'AiTextInput'` (que é
|
|
94
|
+
exatamente o que o PHP `AiTextField::getComponent()` retorna) é
|
|
95
|
+
renderizado pelo adapter desta lib.
|
|
96
|
+
|
|
97
|
+
## AiTextInput (AI-007 React)
|
|
98
|
+
|
|
99
|
+
O componente fecha o slice React do ticket AI-007 — o slice PHP foi
|
|
100
|
+
mergeado no batch #32. Renderiza:
|
|
101
|
+
|
|
102
|
+
- `<textarea>` controlado (ou uncontrolled, se `onChange` for omitido)
|
|
103
|
+
com `maxLength` aplicado quando configurado.
|
|
104
|
+
- Botão "Generate with AI" (label do PHP via `props.buttonLabel`); vira
|
|
105
|
+
`Regenerate` após o primeiro sucesso.
|
|
106
|
+
- Spinner SVG inline + `disabled` durante a chamada.
|
|
107
|
+
- Banner `role="alert"` quando o `fetch` falha (com status code).
|
|
108
|
+
- Contador `X / maxLength` quando `maxLength` está presente.
|
|
109
|
+
|
|
110
|
+
Não toque no `aria-label` do botão sem alinhar com o PHP — testes do
|
|
111
|
+
form usam essa string para encontrar o trigger.
|
|
112
|
+
|
|
113
|
+
## AiTranslateInput (AI-008 React)
|
|
114
|
+
|
|
115
|
+
Componente React que fecha o slice React do ticket AI-008 — o slice
|
|
116
|
+
PHP foi mergeado no batch #33 (`Arqel\Ai\Fields\AiTranslateField`,
|
|
117
|
+
component string `AiTranslateInput`).
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
interface AiTranslateInputProps {
|
|
121
|
+
name: string;
|
|
122
|
+
value: Record<string, string> | null;
|
|
123
|
+
onChange?: (value: Record<string, string>) => void;
|
|
124
|
+
props: {
|
|
125
|
+
languages: string[];
|
|
126
|
+
defaultLanguage: string;
|
|
127
|
+
autoTranslate: boolean;
|
|
128
|
+
provider?: string | null;
|
|
129
|
+
} | undefined;
|
|
130
|
+
resource?: string;
|
|
131
|
+
field?: string;
|
|
132
|
+
translateUrl?: string;
|
|
133
|
+
csrfToken?: string;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Render:
|
|
138
|
+
|
|
139
|
+
- `role="tablist"` com uma `<button role="tab">` por idioma de
|
|
140
|
+
`props.languages`. Tabs com tradução vazia/null carregam
|
|
141
|
+
`data-missing="true"` + um pontinho visual (`Missing translation`).
|
|
142
|
+
- Painel ativo: `<textarea>` controlado editando
|
|
143
|
+
`translations[activeLang]`; o `onChange` recebe sempre o objeto
|
|
144
|
+
completo `Record<lang, string>`.
|
|
145
|
+
- Botão `Translate from {defaultLanguage}` em cada tab **não-default**:
|
|
146
|
+
`POST` com `targetLanguages: [activeLang]`.
|
|
147
|
+
- Botão `Translate all missing` no topo: itera as línguas com
|
|
148
|
+
tradução vazia e dispara um único `POST` com `targetLanguages: missing[]`.
|
|
149
|
+
- Loading per-language (`Set<string>`); banner `role="alert"` quando o
|
|
150
|
+
`fetch` falha (HTTP code visível).
|
|
151
|
+
|
|
152
|
+
Rota canônica: `POST /admin/{resource}/fields/{field}/translate`
|
|
153
|
+
(override via `translateUrl`). Body:
|
|
154
|
+
`{ sourceLanguage, targetLanguages, sourceText }`. Resposta esperada:
|
|
155
|
+
`{ translations: { [lang]: text } }` — mesclada no state e propagada
|
|
156
|
+
via `onChange`.
|
|
157
|
+
|
|
158
|
+
### Uso direto
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import { AiTranslateInput } from '@arqel-dev/ai';
|
|
162
|
+
|
|
163
|
+
<AiTranslateInput
|
|
164
|
+
name="title"
|
|
165
|
+
value={form.title}
|
|
166
|
+
onChange={(v) => setForm((f) => ({ ...f, title: v }))}
|
|
167
|
+
props={{
|
|
168
|
+
languages: ['en', 'pt', 'es'],
|
|
169
|
+
defaultLanguage: 'en',
|
|
170
|
+
autoTranslate: false,
|
|
171
|
+
provider: 'claude',
|
|
172
|
+
}}
|
|
173
|
+
resource="posts"
|
|
174
|
+
field="title"
|
|
175
|
+
csrfToken={csrfToken}
|
|
176
|
+
/>;
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## AiSelectInput (AI-009 React)
|
|
180
|
+
|
|
181
|
+
Componente React que fecha o slice React do ticket AI-009 — o slice
|
|
182
|
+
PHP foi mergeado no batch #34 (`Arqel\Ai\Fields\AiSelectField`,
|
|
183
|
+
component string `AiSelectInput`).
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
interface AiSelectInputProps {
|
|
187
|
+
name: string;
|
|
188
|
+
value: string | null;
|
|
189
|
+
onChange?: (value: string | null) => void;
|
|
190
|
+
props: {
|
|
191
|
+
options: Record<string, string>;
|
|
192
|
+
classifyFromFields: string[];
|
|
193
|
+
provider?: string | null;
|
|
194
|
+
fallbackOption?: string | null;
|
|
195
|
+
hasContextFields: boolean;
|
|
196
|
+
} | undefined;
|
|
197
|
+
resource?: string;
|
|
198
|
+
field?: string;
|
|
199
|
+
formData?: Record<string, unknown>;
|
|
200
|
+
classifyUrl?: string;
|
|
201
|
+
csrfToken?: string;
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Render:
|
|
206
|
+
|
|
207
|
+
- `<select>` controlado (uncontrolled fallback se `onChange` for
|
|
208
|
+
omitido) com `<option value="">Select...</option>` mais uma
|
|
209
|
+
`<option key={k} value={k}>{label}</option>` por entry de
|
|
210
|
+
`props.options`.
|
|
211
|
+
- Botão `Classify with AI` ao lado do select. `disabled` quando
|
|
212
|
+
`hasContextFields` é `false` (com `title` explicando o motivo —
|
|
213
|
+
exibido como tooltip nativo).
|
|
214
|
+
- Loading state: spinner SVG inline + botão `disabled` durante o
|
|
215
|
+
`fetch`.
|
|
216
|
+
- Em sucesso (`key !== null`): `onChange(key)` é chamado e um banner
|
|
217
|
+
sutil `Suggested by AI` aparece com botões `Accept` e `Pick another`
|
|
218
|
+
(ambos apenas dismissam o banner — o valor já está aplicado no
|
|
219
|
+
state).
|
|
220
|
+
- Em sucesso com `key: null` + `fallbackOption` configurado: aplica o
|
|
221
|
+
fallback e mostra o banner `Used fallback`.
|
|
222
|
+
- Em sucesso com `key: null` sem fallback: banner `role="alert"` com
|
|
223
|
+
`Could not classify`.
|
|
224
|
+
- Em falha HTTP: banner `role="alert"` com o status code visível.
|
|
225
|
+
|
|
226
|
+
Rota canônica: `POST /admin/{resource}/fields/{field}/classify`
|
|
227
|
+
(override via `classifyUrl`). Body: `{ formData }`. Resposta esperada:
|
|
228
|
+
`{ key: string | null, label: string | null }`.
|
|
229
|
+
|
|
230
|
+
### Uso direto
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
import { AiSelectInput } from '@arqel-dev/ai';
|
|
234
|
+
|
|
235
|
+
<AiSelectInput
|
|
236
|
+
name="priority"
|
|
237
|
+
value={form.priority}
|
|
238
|
+
onChange={(v) => setForm((f) => ({ ...f, priority: v }))}
|
|
239
|
+
props={{
|
|
240
|
+
options: { low: 'Low', medium: 'Medium', high: 'High' },
|
|
241
|
+
classifyFromFields: ['title', 'body'],
|
|
242
|
+
provider: 'claude',
|
|
243
|
+
fallbackOption: 'low',
|
|
244
|
+
hasContextFields: true,
|
|
245
|
+
}}
|
|
246
|
+
resource="tickets"
|
|
247
|
+
field="priority"
|
|
248
|
+
formData={form}
|
|
249
|
+
csrfToken={csrfToken}
|
|
250
|
+
/>;
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## AiExtractInput (AI-010 React)
|
|
254
|
+
|
|
255
|
+
Componente React que fecha o slice React do ticket AI-010 — o slice
|
|
256
|
+
PHP foi mergeado no batch #35 (`Arqel\Ai\Fields\AiExtractField`,
|
|
257
|
+
component string `AiExtractInput`).
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
interface AiExtractInputProps {
|
|
261
|
+
name: string;
|
|
262
|
+
value: Record<string, unknown> | null;
|
|
263
|
+
onChange?: (value: Record<string, unknown>) => void;
|
|
264
|
+
props: {
|
|
265
|
+
sourceField: string;
|
|
266
|
+
targetFields: string[];
|
|
267
|
+
buttonLabel: string;
|
|
268
|
+
usingJsonMode: boolean;
|
|
269
|
+
provider?: string | null;
|
|
270
|
+
} | undefined;
|
|
271
|
+
resource?: string;
|
|
272
|
+
field?: string;
|
|
273
|
+
formData?: Record<string, unknown>;
|
|
274
|
+
extractUrl?: string;
|
|
275
|
+
csrfToken?: string;
|
|
276
|
+
onPopulateField?: (targetField: string, value: unknown) => void;
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Render:
|
|
281
|
+
|
|
282
|
+
- Label `Source: {sourceField}` indicando o campo do formulário usado
|
|
283
|
+
como texto-fonte.
|
|
284
|
+
- Botão `Extract with AI` (label vem de `props.buttonLabel`); spinner
|
|
285
|
+
inline + `disabled` durante o `fetch`.
|
|
286
|
+
- Empty state `No extraction yet — click button to start` antes da
|
|
287
|
+
primeira extração.
|
|
288
|
+
- Após sucesso: `<dl>` preview com cada `targetField: extractedValue`;
|
|
289
|
+
cada entry tem botão `Apply` individual + toolbar `Apply all`.
|
|
290
|
+
- `Apply` chama `onPopulateField?.(targetField, value)` quando provido,
|
|
291
|
+
caso contrário emite `onChange({ [target]: value })` ou
|
|
292
|
+
`onChange(extracted)` para `Apply all`.
|
|
293
|
+
- Banner `role="alert"` quando o `fetch` falha; usa `message` da
|
|
294
|
+
response 422 quando disponível, senão fallback genérico com HTTP
|
|
295
|
+
code.
|
|
296
|
+
|
|
297
|
+
Rota canônica: `POST /admin/{resource}/fields/{field}/extract`
|
|
298
|
+
(override via `extractUrl`). Body: `{ sourceText }` (lido de
|
|
299
|
+
`formData?.[sourceField]`). Resposta esperada:
|
|
300
|
+
`{ extracted: Record<string, unknown> }` (200) ou `{ message }` (422).
|
|
301
|
+
|
|
302
|
+
### Uso direto
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
import { AiExtractInput } from '@arqel-dev/ai';
|
|
306
|
+
|
|
307
|
+
<AiExtractInput
|
|
308
|
+
name="contact"
|
|
309
|
+
value={form.contact}
|
|
310
|
+
onChange={(v) => setForm((f) => ({ ...f, contact: v }))}
|
|
311
|
+
props={{
|
|
312
|
+
sourceField: 'rawText',
|
|
313
|
+
targetFields: ['name', 'email', 'phone'],
|
|
314
|
+
buttonLabel: 'Extract contact info',
|
|
315
|
+
usingJsonMode: true,
|
|
316
|
+
provider: 'claude',
|
|
317
|
+
}}
|
|
318
|
+
resource="contacts"
|
|
319
|
+
field="contact"
|
|
320
|
+
formData={form}
|
|
321
|
+
csrfToken={csrfToken}
|
|
322
|
+
onPopulateField={(target, value) =>
|
|
323
|
+
setForm((f) => ({ ...f, [target]: value }))
|
|
324
|
+
}
|
|
325
|
+
/>;
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## AiImageInput (AI-011 React)
|
|
329
|
+
|
|
330
|
+
Componente React que fecha o slice React do ticket AI-011 — o slice
|
|
331
|
+
PHP foi mergeado no batch #36 (`Arqel\Ai\Fields\AiImageField`,
|
|
332
|
+
component string `AiImageInput`).
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
interface AiImageInputProps {
|
|
336
|
+
name: string;
|
|
337
|
+
value: string | null;
|
|
338
|
+
onChange?: (value: string) => void;
|
|
339
|
+
props: {
|
|
340
|
+
analyses: string[];
|
|
341
|
+
populateFields: Record<string, string>;
|
|
342
|
+
provider?: string | null;
|
|
343
|
+
acceptedMimes: string[];
|
|
344
|
+
maxFileSize: number;
|
|
345
|
+
buttonLabel: string;
|
|
346
|
+
} | undefined;
|
|
347
|
+
resource?: string;
|
|
348
|
+
field?: string;
|
|
349
|
+
analyzeUrl?: string;
|
|
350
|
+
csrfToken?: string;
|
|
351
|
+
onPopulateField?: (targetField: string, value: string) => void;
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Render:
|
|
356
|
+
|
|
357
|
+
- `<input type="file">` escondido + `<label>` clicável formando o
|
|
358
|
+
drop-zone visual; `accept` é o `props.acceptedMimes.join(',')`.
|
|
359
|
+
- Preview da imagem após selecionar (via `URL.createObjectURL`, só
|
|
360
|
+
chamado dentro do change handler — render path SSR-safe).
|
|
361
|
+
- Validação client-side: arquivos maiores que `props.maxFileSize`
|
|
362
|
+
(em bytes) viram banner `role="alert"` + botão fica `disabled`.
|
|
363
|
+
- Botão `Analyze with AI` (label vem de `props.buttonLabel`); spinner
|
|
364
|
+
inline + `disabled` durante o `fetch`. Sem arquivo selecionado o
|
|
365
|
+
botão também fica `disabled`.
|
|
366
|
+
- Após sucesso: `<dl>` preview com cada `analysis_key: value`; cada
|
|
367
|
+
entry com mapping em `populateFields` mostra botão `Apply`
|
|
368
|
+
individual; toolbar mostra `Apply all` que itera todas as analyses
|
|
369
|
+
com mapping.
|
|
370
|
+
- `Apply` chama `onPopulateField?.(target, value)` usando o
|
|
371
|
+
mapeamento `populateFields[analysisKey]` (ou `populateMapping`
|
|
372
|
+
vindo da response, que tem precedência se presente).
|
|
373
|
+
- Banner `role="alert"` quando o `fetch` falha; usa `message` da
|
|
374
|
+
response 422 quando disponível, senão fallback genérico com HTTP
|
|
375
|
+
code.
|
|
376
|
+
|
|
377
|
+
Rota canônica: `POST /admin/{resource}/fields/{field}/analyze-image`
|
|
378
|
+
(override via `analyzeUrl`). Body: `{ imageBase64 }` (data URI
|
|
379
|
+
completo, produzido por `FileReader.readAsDataURL`). Resposta
|
|
380
|
+
esperada: `{ analyses: Record<string,string>, populateMapping:
|
|
381
|
+
Record<string,string> }` (200) ou `{ message }` (422).
|
|
382
|
+
|
|
383
|
+
### Uso direto
|
|
384
|
+
|
|
385
|
+
```tsx
|
|
386
|
+
import { AiImageInput } from '@arqel-dev/ai';
|
|
387
|
+
|
|
388
|
+
<AiImageInput
|
|
389
|
+
name="cover"
|
|
390
|
+
value={form.cover}
|
|
391
|
+
onChange={(v) => setForm((f) => ({ ...f, cover: v }))}
|
|
392
|
+
props={{
|
|
393
|
+
analyses: ['alt_text', 'tags'],
|
|
394
|
+
populateFields: { alt_text: 'cover_alt', tags: 'cover_tags' },
|
|
395
|
+
provider: 'claude',
|
|
396
|
+
acceptedMimes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
397
|
+
maxFileSize: 10_485_760,
|
|
398
|
+
buttonLabel: 'Analyze with AI',
|
|
399
|
+
}}
|
|
400
|
+
resource="posts"
|
|
401
|
+
field="cover"
|
|
402
|
+
csrfToken={csrfToken}
|
|
403
|
+
onPopulateField={(target, value) =>
|
|
404
|
+
setForm((f) => ({ ...f, [target]: value }))
|
|
405
|
+
}
|
|
406
|
+
/>;
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Anti-patterns
|
|
410
|
+
|
|
411
|
+
- Trazer o prompt template para o cliente — segurança/IP. O backend
|
|
412
|
+
resolve `{fieldName}` placeholders.
|
|
413
|
+
- Adicionar TanStack Query / SWR / Inertia router só para esse fetch:
|
|
414
|
+
uma chamada `fetch` simples basta (ADR-016).
|
|
415
|
+
- Renomear o component name para algo sem casamento com o PHP — o
|
|
416
|
+
field aponta exatamente para `AiTextInput`.
|
|
417
|
+
- Tocar `window`/`document` durante render — quebra SSR.
|
|
418
|
+
|
|
419
|
+
## Related
|
|
420
|
+
|
|
421
|
+
- PHP field: `packages/ai/src/Fields/AiTextField.php`
|
|
422
|
+
- PHP controller: `packages/ai/src/Http/Controllers/AiGenerateController.php`
|
|
423
|
+
- Rota: `packages/ai/routes/web.php` (`arqel.ai.generate`)
|
|
424
|
+
- Ticket: `PLANNING/10-fase-3-avancadas.md` → AI-007
|
|
425
|
+
- FieldRegistry: `packages-js/ui/src/form/FieldRegistry.tsx`
|