@smart-cloud/ai-kit-ui 1.0.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/.gitattributes +8 -0
- package/dist/ai-kit-ui.css +201 -0
- package/dist/index.cjs +14 -0
- package/dist/index.d.cts +87 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.js +14 -0
- package/eslint.config.js +18 -0
- package/package.json +75 -0
- package/src/ShadowBoundary.tsx +266 -0
- package/src/ai-feature/AiFeature.tsx +1824 -0
- package/src/ai-feature/AiFeatureBorder.tsx +22 -0
- package/src/ai-feature/ProofreadDiff.tsx +118 -0
- package/src/ai-feature/index.tsx +2 -0
- package/src/ai-feature/utils.tsx +20 -0
- package/src/i18n/ar.ts +156 -0
- package/src/i18n/de.ts +157 -0
- package/src/i18n/en.ts +156 -0
- package/src/i18n/es.ts +157 -0
- package/src/i18n/fr.ts +158 -0
- package/src/i18n/he.ts +156 -0
- package/src/i18n/hi.ts +157 -0
- package/src/i18n/hu.ts +156 -0
- package/src/i18n/id.ts +157 -0
- package/src/i18n/index.ts +47 -0
- package/src/i18n/it.ts +159 -0
- package/src/i18n/ja.ts +157 -0
- package/src/i18n/ko.ts +155 -0
- package/src/i18n/nb.ts +156 -0
- package/src/i18n/nl.ts +157 -0
- package/src/i18n/pl.ts +158 -0
- package/src/i18n/pt.ts +155 -0
- package/src/i18n/ru.ts +157 -0
- package/src/i18n/sv.ts +156 -0
- package/src/i18n/th.ts +154 -0
- package/src/i18n/tr.ts +158 -0
- package/src/i18n/ua.ts +159 -0
- package/src/i18n/zh.ts +151 -0
- package/src/index.tsx +4 -0
- package/src/styles/ai-kit-ui.css +201 -0
- package/src/useAiRun.ts +177 -0
- package/src/withAiKitShell.tsx +208 -0
- package/tsconfig.json +32 -0
- package/tsconfig.node.json +13 -0
- package/tsup.config.ts +21 -0
|
@@ -0,0 +1,1824 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Alert,
|
|
3
|
+
Button,
|
|
4
|
+
Collapse,
|
|
5
|
+
Divider,
|
|
6
|
+
Group,
|
|
7
|
+
Input,
|
|
8
|
+
Loader,
|
|
9
|
+
Modal,
|
|
10
|
+
Paper,
|
|
11
|
+
Select,
|
|
12
|
+
Stack,
|
|
13
|
+
Text,
|
|
14
|
+
Textarea,
|
|
15
|
+
TextInput,
|
|
16
|
+
Tooltip,
|
|
17
|
+
} from "@mantine/core";
|
|
18
|
+
import {
|
|
19
|
+
AiFeatureProps,
|
|
20
|
+
AiKitFeatureIcon,
|
|
21
|
+
type AiKitLanguageCode,
|
|
22
|
+
type AiKitStatusEvent,
|
|
23
|
+
type AiModePreference,
|
|
24
|
+
type ContextKind,
|
|
25
|
+
detectLanguage,
|
|
26
|
+
type DetectLanguageOutput,
|
|
27
|
+
getAiKitPlugin,
|
|
28
|
+
LANGUAGE_OPTIONS,
|
|
29
|
+
prompt,
|
|
30
|
+
type PromptArgs,
|
|
31
|
+
proofread,
|
|
32
|
+
type ProofreadArgs,
|
|
33
|
+
rewrite,
|
|
34
|
+
type RewriteArgs,
|
|
35
|
+
summarize,
|
|
36
|
+
type SummarizeArgs,
|
|
37
|
+
translate,
|
|
38
|
+
type TranslateArgs,
|
|
39
|
+
waitForAiKitReady,
|
|
40
|
+
write,
|
|
41
|
+
type WriteArgs,
|
|
42
|
+
} from "@smart-cloud/ai-kit-core";
|
|
43
|
+
import { I18n } from "aws-amplify/utils";
|
|
44
|
+
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
45
|
+
import ReactMarkdown from "react-markdown";
|
|
46
|
+
import remarkGfm from "remark-gfm";
|
|
47
|
+
|
|
48
|
+
import {
|
|
49
|
+
IconCircleDashedCheck,
|
|
50
|
+
IconLanguage,
|
|
51
|
+
IconPencilCode,
|
|
52
|
+
IconSeo,
|
|
53
|
+
IconSum,
|
|
54
|
+
} from "@tabler/icons-react";
|
|
55
|
+
|
|
56
|
+
import { translations } from "../i18n";
|
|
57
|
+
import {
|
|
58
|
+
isBackendConfigured,
|
|
59
|
+
readDefaultOutputLanguage,
|
|
60
|
+
stripCodeFence,
|
|
61
|
+
useAiRun,
|
|
62
|
+
} from "../useAiRun";
|
|
63
|
+
import { AiKitShellInjectedProps, withAiKitShell } from "../withAiKitShell";
|
|
64
|
+
import { AiFeatureBorder } from "./AiFeatureBorder";
|
|
65
|
+
import { ProofreadDiff } from "./ProofreadDiff";
|
|
66
|
+
import { markdownToHtml } from "./utils";
|
|
67
|
+
|
|
68
|
+
I18n.putVocabularies(translations);
|
|
69
|
+
|
|
70
|
+
type GeneratedImageMetadata = {
|
|
71
|
+
alt_text?: string;
|
|
72
|
+
title?: string;
|
|
73
|
+
caption?: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type GeneratedPostMetadata = {
|
|
78
|
+
title?: string;
|
|
79
|
+
excerpt?: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const postResponseConstraint = {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
title: {
|
|
86
|
+
type: "string",
|
|
87
|
+
minLength: 1,
|
|
88
|
+
maxLength: 60,
|
|
89
|
+
},
|
|
90
|
+
excerpt: {
|
|
91
|
+
type: "string",
|
|
92
|
+
minLength: 1,
|
|
93
|
+
maxLength: 155,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ["title", "excerpt"],
|
|
97
|
+
additionalProperties: false,
|
|
98
|
+
} as PromptArgs["responseConstraint"];
|
|
99
|
+
|
|
100
|
+
const imageResponseConstraint = {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
alt: {
|
|
104
|
+
type: "string",
|
|
105
|
+
minLength: 1,
|
|
106
|
+
maxLength: 125,
|
|
107
|
+
},
|
|
108
|
+
title: {
|
|
109
|
+
type: "string",
|
|
110
|
+
minLength: 1,
|
|
111
|
+
maxLength: 80,
|
|
112
|
+
},
|
|
113
|
+
caption: {
|
|
114
|
+
type: "string",
|
|
115
|
+
minLength: 1,
|
|
116
|
+
maxLength: 150,
|
|
117
|
+
},
|
|
118
|
+
description: {
|
|
119
|
+
type: "string",
|
|
120
|
+
minLength: 1,
|
|
121
|
+
maxLength: 300,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
required: ["alt", "title", "caption", "description"],
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
} as PromptArgs["responseConstraint"];
|
|
127
|
+
|
|
128
|
+
function normalizeLang(
|
|
129
|
+
code: string | null | undefined,
|
|
130
|
+
): AiKitLanguageCode | null {
|
|
131
|
+
const c = (code ?? "").trim();
|
|
132
|
+
if (!c) return null;
|
|
133
|
+
return (c.toLowerCase().split("-")[0] as AiKitLanguageCode) || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function detectTopLanguage(
|
|
137
|
+
text: string,
|
|
138
|
+
args: {
|
|
139
|
+
signal: AbortSignal;
|
|
140
|
+
onStatus?: (e: AiKitStatusEvent) => void;
|
|
141
|
+
context?: ContextKind;
|
|
142
|
+
modeOverride?: AiModePreference;
|
|
143
|
+
},
|
|
144
|
+
): Promise<AiKitLanguageCode> {
|
|
145
|
+
const res: DetectLanguageOutput = await detectLanguage(
|
|
146
|
+
{ text },
|
|
147
|
+
{
|
|
148
|
+
signal: args.signal,
|
|
149
|
+
onStatus: args.onStatus,
|
|
150
|
+
context: args.context,
|
|
151
|
+
modeOverride: args.modeOverride,
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
const top =
|
|
155
|
+
normalizeLang(res.result?.candidates?.[0]?.detectedLanguage) ?? "en";
|
|
156
|
+
return top;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function parseImageMetadataFromPromptResult(
|
|
160
|
+
text: string,
|
|
161
|
+
outputLang: AiKitLanguageCode | "",
|
|
162
|
+
): Promise<GeneratedImageMetadata> {
|
|
163
|
+
const cleaned = stripCodeFence(text || "").trim();
|
|
164
|
+
if (!cleaned) return {};
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(cleaned) as {
|
|
168
|
+
alt?: string;
|
|
169
|
+
title?: string;
|
|
170
|
+
caption?: string;
|
|
171
|
+
description?: string;
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
alt_text:
|
|
175
|
+
typeof parsed.alt === "string"
|
|
176
|
+
? outputLang && outputLang !== "en"
|
|
177
|
+
? (
|
|
178
|
+
await translate({
|
|
179
|
+
text: parsed.alt,
|
|
180
|
+
sourceLanguage: "en",
|
|
181
|
+
targetLanguage: outputLang,
|
|
182
|
+
})
|
|
183
|
+
).result
|
|
184
|
+
: parsed.alt
|
|
185
|
+
: "",
|
|
186
|
+
title:
|
|
187
|
+
typeof parsed.title === "string"
|
|
188
|
+
? outputLang && outputLang !== "en"
|
|
189
|
+
? (
|
|
190
|
+
await translate({
|
|
191
|
+
text: parsed.title,
|
|
192
|
+
sourceLanguage: "en",
|
|
193
|
+
targetLanguage: outputLang,
|
|
194
|
+
})
|
|
195
|
+
).result
|
|
196
|
+
: parsed.title
|
|
197
|
+
: "",
|
|
198
|
+
caption:
|
|
199
|
+
typeof parsed.caption === "string"
|
|
200
|
+
? outputLang && outputLang !== "en"
|
|
201
|
+
? (
|
|
202
|
+
await translate({
|
|
203
|
+
text: parsed.caption,
|
|
204
|
+
sourceLanguage: "en",
|
|
205
|
+
targetLanguage: outputLang,
|
|
206
|
+
})
|
|
207
|
+
).result
|
|
208
|
+
: parsed.caption
|
|
209
|
+
: "",
|
|
210
|
+
description:
|
|
211
|
+
typeof parsed.description === "string"
|
|
212
|
+
? outputLang && outputLang !== "en"
|
|
213
|
+
? (
|
|
214
|
+
await translate({
|
|
215
|
+
text: parsed.description,
|
|
216
|
+
sourceLanguage: "en",
|
|
217
|
+
targetLanguage: outputLang,
|
|
218
|
+
})
|
|
219
|
+
).result
|
|
220
|
+
: parsed.description
|
|
221
|
+
: "",
|
|
222
|
+
};
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.warn("AI Kit: failed to parse JSON metadata output", e);
|
|
225
|
+
return {};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function parsePostMetadataFromPromptResult(
|
|
230
|
+
text: string,
|
|
231
|
+
outputLang: AiKitLanguageCode | "",
|
|
232
|
+
): Promise<GeneratedPostMetadata> {
|
|
233
|
+
const cleaned = stripCodeFence(text || "").trim();
|
|
234
|
+
if (!cleaned) return {};
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const parsed = JSON.parse(cleaned) as {
|
|
238
|
+
title?: string;
|
|
239
|
+
excerpt?: string;
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
title:
|
|
243
|
+
typeof parsed.title === "string"
|
|
244
|
+
? outputLang && outputLang !== "en"
|
|
245
|
+
? (
|
|
246
|
+
await translate({
|
|
247
|
+
text: parsed.title,
|
|
248
|
+
sourceLanguage: "en",
|
|
249
|
+
targetLanguage: outputLang,
|
|
250
|
+
})
|
|
251
|
+
).result
|
|
252
|
+
: parsed.title
|
|
253
|
+
: "",
|
|
254
|
+
excerpt:
|
|
255
|
+
typeof parsed.excerpt === "string"
|
|
256
|
+
? outputLang && outputLang !== "en"
|
|
257
|
+
? (
|
|
258
|
+
await translate({
|
|
259
|
+
text: parsed.excerpt,
|
|
260
|
+
sourceLanguage: "en",
|
|
261
|
+
targetLanguage: outputLang,
|
|
262
|
+
})
|
|
263
|
+
).result
|
|
264
|
+
: parsed.excerpt
|
|
265
|
+
: "",
|
|
266
|
+
};
|
|
267
|
+
} catch (e) {
|
|
268
|
+
console.warn("AI Kit: failed to parse JSON metadata output", e);
|
|
269
|
+
return {};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Wrapper around WP with:
|
|
275
|
+
* - higher z-index (Media Library grid view can be aggressive)
|
|
276
|
+
* - standard status + error area
|
|
277
|
+
* - optional Cancel action
|
|
278
|
+
*/
|
|
279
|
+
const AiFeatureBase: FC<AiFeatureProps & AiKitShellInjectedProps> = (props) => {
|
|
280
|
+
const {
|
|
281
|
+
allowOverride: allowOverrideDefaults,
|
|
282
|
+
autoRun = true,
|
|
283
|
+
editable = true,
|
|
284
|
+
variation = props.variation || "default",
|
|
285
|
+
title,
|
|
286
|
+
showOpenButton = false,
|
|
287
|
+
showOpenButtonTitle = true,
|
|
288
|
+
showOpenButtonIcon = true,
|
|
289
|
+
openButtonTitle,
|
|
290
|
+
openButtonIcon,
|
|
291
|
+
showRegenerateOnBackendButton = true,
|
|
292
|
+
acceptButtonTitle = props.acceptButtonTitle || "Accept",
|
|
293
|
+
optionsDisplay = props.optionsDisplay || "collapse",
|
|
294
|
+
mode,
|
|
295
|
+
context,
|
|
296
|
+
modeOverride,
|
|
297
|
+
colorMode,
|
|
298
|
+
default: defaults,
|
|
299
|
+
onClose,
|
|
300
|
+
onAccept,
|
|
301
|
+
language,
|
|
302
|
+
rootElement,
|
|
303
|
+
} = props;
|
|
304
|
+
|
|
305
|
+
const allowOverride = {
|
|
306
|
+
text: allowOverrideDefaults?.text ?? true,
|
|
307
|
+
instructions: allowOverrideDefaults?.instructions ?? true,
|
|
308
|
+
tone: allowOverrideDefaults?.tone ?? true,
|
|
309
|
+
length: allowOverrideDefaults?.length ?? true,
|
|
310
|
+
type: allowOverrideDefaults?.type ?? true,
|
|
311
|
+
outputLanguage: allowOverrideDefaults?.outputLanguage ?? true,
|
|
312
|
+
outputFormat: allowOverrideDefaults?.outputFormat ?? true,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const allowOverrideParameters = useMemo(() => {
|
|
316
|
+
return Boolean(
|
|
317
|
+
(mode === "write" && allowOverride?.text) ||
|
|
318
|
+
((mode === "write" ||
|
|
319
|
+
mode === "rewrite" ||
|
|
320
|
+
mode === "generateImageMetadata" ||
|
|
321
|
+
mode === "generatePostMetadata") &&
|
|
322
|
+
allowOverride?.instructions) ||
|
|
323
|
+
((mode === "write" || mode === "rewrite") && allowOverride?.tone) ||
|
|
324
|
+
((mode === "write" || mode === "rewrite" || mode === "summarize") &&
|
|
325
|
+
allowOverride?.length) ||
|
|
326
|
+
(mode === "summarize" && allowOverride?.type) ||
|
|
327
|
+
allowOverride?.outputLanguage,
|
|
328
|
+
);
|
|
329
|
+
}, [allowOverride]);
|
|
330
|
+
|
|
331
|
+
const [featureOpen, setFeatureOpen] = useState<boolean>(!showOpenButton);
|
|
332
|
+
const [optionsOpen, setOptionsOpen] = useState<boolean>(false);
|
|
333
|
+
const [backendConfigured, setBackendConfigured] = useState(false);
|
|
334
|
+
const [error, setError] = useState<string | null>(null);
|
|
335
|
+
const [generated, setGenerated] = useState<never | null>(null);
|
|
336
|
+
const [text, setText] = useState<string | undefined>(defaults?.text);
|
|
337
|
+
const [image] = useState<Blob | undefined>(defaults?.image);
|
|
338
|
+
const [instructions, setInstructions] = useState<string | undefined>(
|
|
339
|
+
defaults?.instructions,
|
|
340
|
+
);
|
|
341
|
+
const [inputLanguage, setInputLanguage] = useState<
|
|
342
|
+
AiKitLanguageCode | "auto" | undefined
|
|
343
|
+
>(defaults?.inputLanguage);
|
|
344
|
+
const [outputFormat, setOutputFormat] = useState<
|
|
345
|
+
"plain-text" | "markdown" | "html" | undefined
|
|
346
|
+
>(defaults?.outputFormat);
|
|
347
|
+
const [outputLanguage, setOutputLanguage] = useState<
|
|
348
|
+
AiKitLanguageCode | "auto" | undefined
|
|
349
|
+
>(defaults?.outputLanguage);
|
|
350
|
+
const [length, setLength] = useState<
|
|
351
|
+
WriterLength | RewriterLength | SummarizerLength | undefined
|
|
352
|
+
>(defaults?.length);
|
|
353
|
+
const [tone, setTone] = useState<WriterTone | RewriterTone | undefined>(
|
|
354
|
+
defaults?.tone,
|
|
355
|
+
);
|
|
356
|
+
const [type, setType] = useState<SummarizerType | undefined>(defaults?.type);
|
|
357
|
+
|
|
358
|
+
const autoRunOnceRef = useRef(false);
|
|
359
|
+
|
|
360
|
+
const defaultTitle = useMemo(() => {
|
|
361
|
+
if (language) {
|
|
362
|
+
I18n.setLanguage(language || "en");
|
|
363
|
+
}
|
|
364
|
+
let title;
|
|
365
|
+
switch (mode) {
|
|
366
|
+
default:
|
|
367
|
+
case "summarize":
|
|
368
|
+
title = I18n.get("Summarize");
|
|
369
|
+
break;
|
|
370
|
+
case "proofread":
|
|
371
|
+
title = I18n.get("Proofread");
|
|
372
|
+
break;
|
|
373
|
+
case "write":
|
|
374
|
+
title = I18n.get("Write");
|
|
375
|
+
break;
|
|
376
|
+
case "rewrite":
|
|
377
|
+
title = I18n.get("Rewrite");
|
|
378
|
+
break;
|
|
379
|
+
case "translate":
|
|
380
|
+
title = I18n.get("Translate");
|
|
381
|
+
break;
|
|
382
|
+
case "generatePostMetadata":
|
|
383
|
+
title = I18n.get("Generate Post Metadata");
|
|
384
|
+
break;
|
|
385
|
+
case "generateImageMetadata":
|
|
386
|
+
title = I18n.get("Generate Image Metadata");
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
return title;
|
|
390
|
+
}, [mode, language]);
|
|
391
|
+
|
|
392
|
+
const formatAiKitStatus = useCallback(
|
|
393
|
+
(e: AiKitStatusEvent | null): string | null => {
|
|
394
|
+
if (!e) return null;
|
|
395
|
+
|
|
396
|
+
const step = e.step;
|
|
397
|
+
const msg = I18n.get((e.message ?? "").trim());
|
|
398
|
+
const p = typeof e.progress === "number" ? e.progress : null;
|
|
399
|
+
const pct = p == null ? null : Math.round(p * 100);
|
|
400
|
+
|
|
401
|
+
switch (step) {
|
|
402
|
+
case "decide":
|
|
403
|
+
return msg || I18n.get("Checking capabilities...");
|
|
404
|
+
case "on-device:init":
|
|
405
|
+
return msg || I18n.get("Initializing on-device AI...");
|
|
406
|
+
case "on-device:download":
|
|
407
|
+
return (
|
|
408
|
+
msg ||
|
|
409
|
+
(pct == null
|
|
410
|
+
? I18n.get("Downloading model...")
|
|
411
|
+
: I18n.get("Downloading model...") + " " + pct + "%")
|
|
412
|
+
);
|
|
413
|
+
case "on-device:ready":
|
|
414
|
+
return msg || I18n.get("On-device model ready.");
|
|
415
|
+
case "on-device:run":
|
|
416
|
+
return msg || I18n.get("Generating...");
|
|
417
|
+
case "backend:request":
|
|
418
|
+
return msg || I18n.get("Sending request to backend...");
|
|
419
|
+
case "backend:waiting":
|
|
420
|
+
return msg || I18n.get("Waiting for backend response...");
|
|
421
|
+
case "backend:response":
|
|
422
|
+
return msg || I18n.get("Received backend response.");
|
|
423
|
+
case "done":
|
|
424
|
+
return msg || I18n.get("Done.");
|
|
425
|
+
case "error":
|
|
426
|
+
return msg || I18n.get("Something went wrong.");
|
|
427
|
+
default:
|
|
428
|
+
return msg || I18n.get("Working...");
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
[language],
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const inputText = useMemo(() => {
|
|
435
|
+
return text ?? defaults?.getText;
|
|
436
|
+
}, [text, defaults]);
|
|
437
|
+
|
|
438
|
+
const canGenerate = useMemo(() => {
|
|
439
|
+
const text = typeof inputText === "function" ? inputText() : inputText;
|
|
440
|
+
switch (mode) {
|
|
441
|
+
case "generateImageMetadata":
|
|
442
|
+
return Boolean(image);
|
|
443
|
+
case "translate":
|
|
444
|
+
return (
|
|
445
|
+
Boolean(text && text.trim().length > 0) &&
|
|
446
|
+
outputLanguage &&
|
|
447
|
+
inputLanguage !== outputLanguage
|
|
448
|
+
);
|
|
449
|
+
case "summarize":
|
|
450
|
+
case "proofread":
|
|
451
|
+
case "rewrite":
|
|
452
|
+
case "write":
|
|
453
|
+
case "generatePostMetadata":
|
|
454
|
+
return Boolean(text && text.trim().length > 0);
|
|
455
|
+
default:
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}, [inputText, mode, image, inputLanguage, outputLanguage]);
|
|
459
|
+
|
|
460
|
+
const ai = useAiRun();
|
|
461
|
+
const statusText = formatAiKitStatus(ai.statusEvent);
|
|
462
|
+
|
|
463
|
+
const runGenerate = useCallback(
|
|
464
|
+
async (modeOverride?: AiModePreference) => {
|
|
465
|
+
if (!canGenerate) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (allowOverrideParameters && mode !== "proofread" && canGenerate) {
|
|
469
|
+
setOptionsOpen(false);
|
|
470
|
+
}
|
|
471
|
+
setError(null);
|
|
472
|
+
setGenerated(null);
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
const text = typeof inputText === "function" ? inputText() : inputText;
|
|
476
|
+
switch (mode) {
|
|
477
|
+
case "summarize": {
|
|
478
|
+
const res = await ai.run(async ({ signal, onStatus }) => {
|
|
479
|
+
const outLang =
|
|
480
|
+
(outputLanguage && outputLanguage !== "auto"
|
|
481
|
+
? outputLanguage
|
|
482
|
+
: null) || readDefaultOutputLanguage();
|
|
483
|
+
const args: SummarizeArgs = {
|
|
484
|
+
text: text!.trim(),
|
|
485
|
+
format:
|
|
486
|
+
outputFormat === "plain-text" ? "plain-text" : "markdown",
|
|
487
|
+
length: length as SummarizerLength,
|
|
488
|
+
type: type as SummarizerType,
|
|
489
|
+
outputLanguage: outLang as SummarizeArgs["outputLanguage"],
|
|
490
|
+
};
|
|
491
|
+
const out = await summarize(args, {
|
|
492
|
+
signal,
|
|
493
|
+
onStatus,
|
|
494
|
+
context,
|
|
495
|
+
modeOverride,
|
|
496
|
+
});
|
|
497
|
+
return out.result;
|
|
498
|
+
});
|
|
499
|
+
setGenerated((res as never) ?? "");
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
case "proofread": {
|
|
503
|
+
const res = await ai.run(async ({ signal, onStatus }) => {
|
|
504
|
+
const expectedInputLanguages: AiKitLanguageCode[] = [];
|
|
505
|
+
try {
|
|
506
|
+
const res = await detectLanguage(
|
|
507
|
+
{ text: text!.trim() },
|
|
508
|
+
{ signal, onStatus },
|
|
509
|
+
);
|
|
510
|
+
const langCodes = res.result?.candidates
|
|
511
|
+
?.filter((c) => c.confidence && c.confidence > 0.1)
|
|
512
|
+
.map((c) => c.detectedLanguage as AiKitLanguageCode);
|
|
513
|
+
expectedInputLanguages.push(...langCodes);
|
|
514
|
+
} catch {
|
|
515
|
+
expectedInputLanguages.push("en");
|
|
516
|
+
}
|
|
517
|
+
const args: ProofreadArgs = {
|
|
518
|
+
text: text!.trim(),
|
|
519
|
+
expectedInputLanguages,
|
|
520
|
+
};
|
|
521
|
+
const out = await proofread(args, {
|
|
522
|
+
signal,
|
|
523
|
+
onStatus,
|
|
524
|
+
context,
|
|
525
|
+
modeOverride,
|
|
526
|
+
});
|
|
527
|
+
return out.result;
|
|
528
|
+
});
|
|
529
|
+
setGenerated((res as never) ?? "");
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case "translate": {
|
|
533
|
+
const res = await ai.run(async ({ signal, onStatus }) => {
|
|
534
|
+
let inputLang = inputLanguage ?? "auto";
|
|
535
|
+
if (inputLang === "auto") {
|
|
536
|
+
inputLang = await detectTopLanguage(text!.trim(), {
|
|
537
|
+
signal,
|
|
538
|
+
});
|
|
539
|
+
setInputLanguage(inputLang);
|
|
540
|
+
}
|
|
541
|
+
const outLang =
|
|
542
|
+
(outputLanguage && outputLanguage !== "auto"
|
|
543
|
+
? outputLanguage
|
|
544
|
+
: null) || readDefaultOutputLanguage();
|
|
545
|
+
if (outLang === inputLang) {
|
|
546
|
+
setError(
|
|
547
|
+
I18n.get("Input and output languages cannot be the same."),
|
|
548
|
+
);
|
|
549
|
+
throw new Error(
|
|
550
|
+
I18n.get("Input and output languages cannot be the same."),
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
const args: TranslateArgs = {
|
|
554
|
+
text: text!.trim(),
|
|
555
|
+
sourceLanguage: inputLang!,
|
|
556
|
+
targetLanguage: outLang,
|
|
557
|
+
};
|
|
558
|
+
const out = await translate(args, {
|
|
559
|
+
signal,
|
|
560
|
+
onStatus,
|
|
561
|
+
context,
|
|
562
|
+
modeOverride,
|
|
563
|
+
});
|
|
564
|
+
return out.result;
|
|
565
|
+
});
|
|
566
|
+
setGenerated((res as never) ?? "");
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
case "rewrite": {
|
|
570
|
+
const res = await ai.run(async ({ signal, onStatus }) => {
|
|
571
|
+
let outLang =
|
|
572
|
+
(outputLanguage && outputLanguage !== "auto"
|
|
573
|
+
? outputLanguage
|
|
574
|
+
: null) || readDefaultOutputLanguage();
|
|
575
|
+
if (outputLanguage === "auto") {
|
|
576
|
+
outLang = await detectTopLanguage(text!.trim(), {
|
|
577
|
+
signal,
|
|
578
|
+
});
|
|
579
|
+
setOutputLanguage(outLang);
|
|
580
|
+
}
|
|
581
|
+
const args: RewriteArgs = {
|
|
582
|
+
text: text!.trim(),
|
|
583
|
+
context: instructions?.trim() || undefined,
|
|
584
|
+
format:
|
|
585
|
+
outputFormat === "plain-text" ? "plain-text" : "markdown",
|
|
586
|
+
tone: tone as RewriterTone,
|
|
587
|
+
length: length as RewriterLength,
|
|
588
|
+
outputLanguage: outLang as RewriteArgs["outputLanguage"],
|
|
589
|
+
};
|
|
590
|
+
const out = await rewrite(args, {
|
|
591
|
+
signal,
|
|
592
|
+
onStatus,
|
|
593
|
+
context,
|
|
594
|
+
modeOverride,
|
|
595
|
+
});
|
|
596
|
+
return out.result;
|
|
597
|
+
});
|
|
598
|
+
setGenerated((res as never) ?? "");
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
case "write": {
|
|
602
|
+
const outLang =
|
|
603
|
+
(outputLanguage && outputLanguage !== "auto"
|
|
604
|
+
? outputLanguage
|
|
605
|
+
: null) || readDefaultOutputLanguage();
|
|
606
|
+
const args: WriteArgs = {
|
|
607
|
+
prompt: text!.trim(),
|
|
608
|
+
context: instructions?.trim() || undefined,
|
|
609
|
+
format: outputFormat === "plain-text" ? "plain-text" : "markdown",
|
|
610
|
+
tone: tone as WriterTone,
|
|
611
|
+
length: length as WriterLength,
|
|
612
|
+
outputLanguage: outLang as WriteArgs["outputLanguage"],
|
|
613
|
+
};
|
|
614
|
+
const res = await ai.run(async ({ signal, onStatus }) => {
|
|
615
|
+
const inLang = await detectTopLanguage(
|
|
616
|
+
text!.trim() + "\n" + (instructions?.trim() || ""),
|
|
617
|
+
{
|
|
618
|
+
signal,
|
|
619
|
+
},
|
|
620
|
+
);
|
|
621
|
+
if (inLang !== outLang && inLang !== "en") {
|
|
622
|
+
args.prompt = (
|
|
623
|
+
await translate({
|
|
624
|
+
text: args.prompt,
|
|
625
|
+
sourceLanguage: inLang,
|
|
626
|
+
targetLanguage: "en",
|
|
627
|
+
})
|
|
628
|
+
).result;
|
|
629
|
+
if (instructions) {
|
|
630
|
+
args.context = (
|
|
631
|
+
await translate({
|
|
632
|
+
text: instructions,
|
|
633
|
+
sourceLanguage: inLang,
|
|
634
|
+
targetLanguage: "en",
|
|
635
|
+
})
|
|
636
|
+
).result;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const out = await write(args, {
|
|
640
|
+
signal,
|
|
641
|
+
onStatus,
|
|
642
|
+
context,
|
|
643
|
+
modeOverride,
|
|
644
|
+
});
|
|
645
|
+
return out.result;
|
|
646
|
+
});
|
|
647
|
+
setGenerated((res as never) ?? "");
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case "generatePostMetadata": {
|
|
651
|
+
const messages = [
|
|
652
|
+
{
|
|
653
|
+
role: "system" as const,
|
|
654
|
+
content:
|
|
655
|
+
"You generate SEO metadata for a WordPress post. " +
|
|
656
|
+
"Return a minified JSON object with keys: title, excerpt. " +
|
|
657
|
+
"Constraints: title <= 60 chars, excerpt <= 155 chars. " +
|
|
658
|
+
"Do not add extra keys." +
|
|
659
|
+
(instructions
|
|
660
|
+
? `
|
|
661
|
+
Follow these additional instructions: ${instructions}`
|
|
662
|
+
: ""),
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
role: "user" as const,
|
|
666
|
+
content: `Post content:\n${text!.trim()}\n\nGenerate JSON now.`,
|
|
667
|
+
},
|
|
668
|
+
];
|
|
669
|
+
const res = (await ai.run(async ({ signal, onStatus }) => {
|
|
670
|
+
const out = await prompt(
|
|
671
|
+
{
|
|
672
|
+
messages,
|
|
673
|
+
outputLanguage: "en",
|
|
674
|
+
responseConstraint: postResponseConstraint,
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
signal,
|
|
678
|
+
onStatus,
|
|
679
|
+
context,
|
|
680
|
+
modeOverride,
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
return out.result;
|
|
684
|
+
})) as string | null;
|
|
685
|
+
if (!res) {
|
|
686
|
+
setGenerated("" as never);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const cleaned = stripCodeFence(res).trim();
|
|
690
|
+
const outLang =
|
|
691
|
+
(outputLanguage && outputLanguage !== "auto"
|
|
692
|
+
? outputLanguage
|
|
693
|
+
: null) || readDefaultOutputLanguage();
|
|
694
|
+
try {
|
|
695
|
+
const parsed = await parsePostMetadataFromPromptResult(
|
|
696
|
+
cleaned,
|
|
697
|
+
outLang,
|
|
698
|
+
);
|
|
699
|
+
setGenerated(parsed as never);
|
|
700
|
+
} catch (e) {
|
|
701
|
+
// If parsing fails, keep raw in the modal. User can still copy/paste.
|
|
702
|
+
setGenerated(cleaned as never);
|
|
703
|
+
console.warn("AI Kit: failed to parse SEO JSON", e);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
case "generateImageMetadata": {
|
|
709
|
+
{
|
|
710
|
+
const messages = [
|
|
711
|
+
{
|
|
712
|
+
role: "system",
|
|
713
|
+
content:
|
|
714
|
+
"You are an assistant that writes WordPress media metadata for accessibility and SEO. " +
|
|
715
|
+
"Return a minified JSON object with keys: alt, title, caption, description. " +
|
|
716
|
+
"Do not include any extra keys. Keep it concise and non-promotional." +
|
|
717
|
+
(instructions
|
|
718
|
+
? `
|
|
719
|
+
Follow these additional instructions: ${instructions}`
|
|
720
|
+
: ""),
|
|
721
|
+
},
|
|
722
|
+
{ role: "user", content: "Generate the JSON now." },
|
|
723
|
+
].filter(Boolean) as Array<{
|
|
724
|
+
role: "system" | "user" | "assistant";
|
|
725
|
+
content: string;
|
|
726
|
+
}>;
|
|
727
|
+
const res = (await ai.run(async ({ signal, onStatus }) => {
|
|
728
|
+
const out = await prompt(
|
|
729
|
+
{
|
|
730
|
+
messages,
|
|
731
|
+
images: [image!],
|
|
732
|
+
outputLanguage: "en",
|
|
733
|
+
responseConstraint: imageResponseConstraint,
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
signal,
|
|
737
|
+
onStatus,
|
|
738
|
+
context,
|
|
739
|
+
modeOverride,
|
|
740
|
+
},
|
|
741
|
+
);
|
|
742
|
+
return out.result;
|
|
743
|
+
})) as string | null;
|
|
744
|
+
if (!res) {
|
|
745
|
+
setGenerated("" as never);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const outLang =
|
|
749
|
+
(outputLanguage && outputLanguage !== "auto"
|
|
750
|
+
? outputLanguage
|
|
751
|
+
: null) || readDefaultOutputLanguage();
|
|
752
|
+
|
|
753
|
+
const cleaned = stripCodeFence(res).trim();
|
|
754
|
+
try {
|
|
755
|
+
const parsed = await parseImageMetadataFromPromptResult(
|
|
756
|
+
cleaned,
|
|
757
|
+
outLang,
|
|
758
|
+
);
|
|
759
|
+
setGenerated(parsed as never);
|
|
760
|
+
} catch (e) {
|
|
761
|
+
// If parsing fails, keep raw in the modal. User can still copy/paste.
|
|
762
|
+
setGenerated(cleaned as never);
|
|
763
|
+
console.warn("AI Kit: failed to parse SEO JSON", e);
|
|
764
|
+
}
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} catch (e) {
|
|
770
|
+
setError(
|
|
771
|
+
e instanceof Error
|
|
772
|
+
? e.message
|
|
773
|
+
: I18n.get("An unknown error occurred."),
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
[
|
|
778
|
+
language,
|
|
779
|
+
ai,
|
|
780
|
+
instructions,
|
|
781
|
+
length,
|
|
782
|
+
outputLanguage,
|
|
783
|
+
text,
|
|
784
|
+
tone,
|
|
785
|
+
context,
|
|
786
|
+
mode,
|
|
787
|
+
type,
|
|
788
|
+
inputLanguage,
|
|
789
|
+
canGenerate,
|
|
790
|
+
allowOverrideParameters,
|
|
791
|
+
],
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
const runGenerateOnBackend = useCallback(async () => {
|
|
795
|
+
await runGenerate("backend-only");
|
|
796
|
+
}, [runGenerate]);
|
|
797
|
+
|
|
798
|
+
const getOpenButtonDefaultIcon = useCallback(
|
|
799
|
+
(className?: string) => {
|
|
800
|
+
switch (mode) {
|
|
801
|
+
case "proofread":
|
|
802
|
+
return <IconCircleDashedCheck className={className} />;
|
|
803
|
+
case "translate":
|
|
804
|
+
return <IconLanguage className={className} />;
|
|
805
|
+
case "summarize":
|
|
806
|
+
return <IconSum className={className} />;
|
|
807
|
+
case "rewrite":
|
|
808
|
+
case "write":
|
|
809
|
+
return <IconPencilCode className={className} />;
|
|
810
|
+
case "generateImageMetadata":
|
|
811
|
+
case "generatePostMetadata":
|
|
812
|
+
return <IconSeo className={className} />;
|
|
813
|
+
default:
|
|
814
|
+
return <AiKitFeatureIcon mode={mode} className={className} />;
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
[mode],
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
const getGenerateTitle = useCallback(() => {
|
|
821
|
+
switch (mode) {
|
|
822
|
+
case "proofread":
|
|
823
|
+
return ai.lastSource
|
|
824
|
+
? I18n.get("Proofread again")
|
|
825
|
+
: I18n.get("Proofread");
|
|
826
|
+
case "translate":
|
|
827
|
+
return ai.lastSource
|
|
828
|
+
? I18n.get("Translate again")
|
|
829
|
+
: I18n.get("Translate");
|
|
830
|
+
case "rewrite":
|
|
831
|
+
return ai.lastSource ? I18n.get("Rewrite again") : I18n.get("Rewrite");
|
|
832
|
+
case "summarize":
|
|
833
|
+
return ai.lastSource
|
|
834
|
+
? I18n.get("Summarize again")
|
|
835
|
+
: I18n.get("Summarize");
|
|
836
|
+
default:
|
|
837
|
+
return ai.lastSource ? I18n.get("Regenerate") : I18n.get("Generate");
|
|
838
|
+
}
|
|
839
|
+
}, [language, ai.lastSource, mode]);
|
|
840
|
+
|
|
841
|
+
const getRegenerateOnBackendTitle = useCallback(() => {
|
|
842
|
+
switch (mode) {
|
|
843
|
+
case "proofread":
|
|
844
|
+
return I18n.get("Proofread on Backend");
|
|
845
|
+
case "translate":
|
|
846
|
+
return I18n.get("Translate on Backend");
|
|
847
|
+
case "rewrite":
|
|
848
|
+
return I18n.get("Rewrite on Backend");
|
|
849
|
+
case "summarize":
|
|
850
|
+
return I18n.get("Summarize on Backend");
|
|
851
|
+
default:
|
|
852
|
+
return I18n.get("Regenerate on Backend");
|
|
853
|
+
}
|
|
854
|
+
}, [language, mode]);
|
|
855
|
+
|
|
856
|
+
const close = useCallback(async () => {
|
|
857
|
+
setFeatureOpen(false);
|
|
858
|
+
setGenerated(null);
|
|
859
|
+
setError(null);
|
|
860
|
+
autoRunOnceRef.current = false;
|
|
861
|
+
ai.reset();
|
|
862
|
+
if (!showOpenButton) {
|
|
863
|
+
onClose();
|
|
864
|
+
}
|
|
865
|
+
}, [onClose, autoRunOnceRef, ai, showOpenButton]);
|
|
866
|
+
|
|
867
|
+
const cancel = useCallback(async () => {
|
|
868
|
+
if (ai.busy) {
|
|
869
|
+
ai.cancel();
|
|
870
|
+
}
|
|
871
|
+
}, [ai]);
|
|
872
|
+
|
|
873
|
+
useEffect(() => {
|
|
874
|
+
if (
|
|
875
|
+
!featureOpen ||
|
|
876
|
+
!autoRun ||
|
|
877
|
+
!canGenerate ||
|
|
878
|
+
ai.busy ||
|
|
879
|
+
generated ||
|
|
880
|
+
autoRunOnceRef.current
|
|
881
|
+
) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
autoRunOnceRef.current = true;
|
|
885
|
+
queueMicrotask(() => {
|
|
886
|
+
void runGenerate(modeOverride);
|
|
887
|
+
});
|
|
888
|
+
}, [ai.busy, canGenerate, autoRun, generated, runGenerate, modeOverride]);
|
|
889
|
+
|
|
890
|
+
useEffect(() => {
|
|
891
|
+
if (!allowOverrideParameters) return;
|
|
892
|
+
if (mode === "proofread") return;
|
|
893
|
+
if (!canGenerate) setOptionsOpen(true);
|
|
894
|
+
}, [allowOverrideParameters, canGenerate, mode]);
|
|
895
|
+
|
|
896
|
+
useEffect(() => {
|
|
897
|
+
let alive = true;
|
|
898
|
+
(async () => {
|
|
899
|
+
try {
|
|
900
|
+
await waitForAiKitReady();
|
|
901
|
+
const v = await isBackendConfigured();
|
|
902
|
+
if (alive) setBackendConfigured(v);
|
|
903
|
+
} catch (e) {
|
|
904
|
+
console.error(e);
|
|
905
|
+
if (alive) setBackendConfigured(false);
|
|
906
|
+
}
|
|
907
|
+
})();
|
|
908
|
+
return () => {
|
|
909
|
+
alive = false;
|
|
910
|
+
};
|
|
911
|
+
}, []);
|
|
912
|
+
|
|
913
|
+
const optionsSummary = useMemo(() => {
|
|
914
|
+
const parts: string[] = [];
|
|
915
|
+
|
|
916
|
+
if (mode === "translate") {
|
|
917
|
+
const lang = LANGUAGE_OPTIONS.find(
|
|
918
|
+
(lo) => lo.value === inputLanguage,
|
|
919
|
+
)?.label;
|
|
920
|
+
parts.push(
|
|
921
|
+
I18n.get("Input language") + ": " + (lang ? I18n.get(lang) : "auto"),
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
if (outputLanguage && allowOverride?.outputLanguage) {
|
|
925
|
+
const lang = LANGUAGE_OPTIONS.find(
|
|
926
|
+
(lo) => lo.value === outputLanguage,
|
|
927
|
+
)?.label;
|
|
928
|
+
parts.push(
|
|
929
|
+
I18n.get("Output language") +
|
|
930
|
+
": " +
|
|
931
|
+
(lang ? I18n.get(lang) : outputLanguage),
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
if (mode === "summarize" && type && allowOverride?.type) {
|
|
935
|
+
parts.push(I18n.get("Type") + ": " + I18n.get(type));
|
|
936
|
+
}
|
|
937
|
+
if (
|
|
938
|
+
(mode === "write" || mode === "rewrite") &&
|
|
939
|
+
tone &&
|
|
940
|
+
allowOverride?.tone
|
|
941
|
+
) {
|
|
942
|
+
parts.push(I18n.get("Tone") + ": " + I18n.get(tone));
|
|
943
|
+
}
|
|
944
|
+
if (
|
|
945
|
+
(mode === "write" || mode === "rewrite" || mode === "summarize") &&
|
|
946
|
+
length &&
|
|
947
|
+
allowOverride?.length
|
|
948
|
+
) {
|
|
949
|
+
parts.push(I18n.get("Length") + ": " + I18n.get(length));
|
|
950
|
+
}
|
|
951
|
+
if (instructions?.trim() && allowOverride?.instructions) {
|
|
952
|
+
parts.push(I18n.get("Instructions") + ": ✓");
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return parts.length ? parts.join(" • ") : I18n.get("No overrides");
|
|
956
|
+
}, [
|
|
957
|
+
language,
|
|
958
|
+
mode,
|
|
959
|
+
inputLanguage,
|
|
960
|
+
outputLanguage,
|
|
961
|
+
type,
|
|
962
|
+
tone,
|
|
963
|
+
length,
|
|
964
|
+
instructions,
|
|
965
|
+
]);
|
|
966
|
+
|
|
967
|
+
const compactFieldStyles = {
|
|
968
|
+
label: { fontSize: 11, opacity: 0.85 },
|
|
969
|
+
description: { fontSize: 11, opacity: 0.65, marginTop: 2 },
|
|
970
|
+
input: { fontSize: 12 },
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
const RootComponent: typeof Modal.Root | typeof Group =
|
|
974
|
+
variation === "modal" ? Modal.Root : Group;
|
|
975
|
+
const ContentComponent: typeof Modal.Content | typeof Group =
|
|
976
|
+
variation === "modal" ? Modal.Content : Group;
|
|
977
|
+
const BodyComponent: typeof Modal.Body | typeof Group =
|
|
978
|
+
variation === "modal" ? Modal.Body : Group;
|
|
979
|
+
const CollapseComponent = optionsDisplay === "collapse" ? Collapse : Stack;
|
|
980
|
+
const OptionsComponent = optionsDisplay === "horizontal" ? Group : Stack;
|
|
981
|
+
|
|
982
|
+
useEffect(() => {
|
|
983
|
+
if (variation !== "modal" || !featureOpen) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
document.body.style.overflow = "hidden";
|
|
987
|
+
document.body.onkeydown = (e: KeyboardEvent) => {
|
|
988
|
+
if (e.key === "Escape") {
|
|
989
|
+
e.preventDefault();
|
|
990
|
+
close();
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
return () => {
|
|
994
|
+
// remove overflow: hidden; from body element
|
|
995
|
+
document.body.style.overflow = "";
|
|
996
|
+
document.body.onkeydown = null;
|
|
997
|
+
};
|
|
998
|
+
}, [close, variation]);
|
|
999
|
+
|
|
1000
|
+
return (
|
|
1001
|
+
<>
|
|
1002
|
+
{showOpenButton && (
|
|
1003
|
+
<Button
|
|
1004
|
+
leftSection={
|
|
1005
|
+
showOpenButtonIcon &&
|
|
1006
|
+
(openButtonIcon ? (
|
|
1007
|
+
<span dangerouslySetInnerHTML={{ __html: openButtonIcon }} />
|
|
1008
|
+
) : (
|
|
1009
|
+
getOpenButtonDefaultIcon()
|
|
1010
|
+
))
|
|
1011
|
+
}
|
|
1012
|
+
className={
|
|
1013
|
+
showOpenButtonTitle
|
|
1014
|
+
? "ai-feature-open-button"
|
|
1015
|
+
: "ai-feature-open-button-no-title"
|
|
1016
|
+
}
|
|
1017
|
+
variant={"filled"}
|
|
1018
|
+
disabled={featureOpen}
|
|
1019
|
+
onClick={() => setFeatureOpen(true)}
|
|
1020
|
+
data-ai-kit-open-button
|
|
1021
|
+
>
|
|
1022
|
+
{showOpenButtonTitle && I18n.get(openButtonTitle || defaultTitle)}
|
|
1023
|
+
</Button>
|
|
1024
|
+
)}
|
|
1025
|
+
|
|
1026
|
+
{featureOpen && (
|
|
1027
|
+
<RootComponent
|
|
1028
|
+
opened={true}
|
|
1029
|
+
className="ai-feature-root"
|
|
1030
|
+
onClose={close}
|
|
1031
|
+
padding="md"
|
|
1032
|
+
gap="md"
|
|
1033
|
+
size="md"
|
|
1034
|
+
portalProps={
|
|
1035
|
+
variation === "modal"
|
|
1036
|
+
? { target: rootElement, reuseTargetNode: true }
|
|
1037
|
+
: undefined
|
|
1038
|
+
}
|
|
1039
|
+
data-ai-kit-theme={colorMode}
|
|
1040
|
+
data-ai-kit-variation={variation}
|
|
1041
|
+
>
|
|
1042
|
+
{variation === "modal" && <Modal.Overlay />}
|
|
1043
|
+
<ContentComponent
|
|
1044
|
+
w="100%"
|
|
1045
|
+
style={{
|
|
1046
|
+
left: 0,
|
|
1047
|
+
}}
|
|
1048
|
+
>
|
|
1049
|
+
{variation === "modal" && (
|
|
1050
|
+
<Modal.Header style={{ zIndex: 1000 }}>
|
|
1051
|
+
{getOpenButtonDefaultIcon("ai-feature-title-icon")}
|
|
1052
|
+
<Modal.Title>{I18n.get(title || defaultTitle)}</Modal.Title>
|
|
1053
|
+
<Modal.CloseButton />
|
|
1054
|
+
</Modal.Header>
|
|
1055
|
+
)}
|
|
1056
|
+
<BodyComponent w="100%" style={{ zIndex: 1001 }}>
|
|
1057
|
+
<AiFeatureBorder
|
|
1058
|
+
enabled={variation !== "modal"}
|
|
1059
|
+
working={ai.busy}
|
|
1060
|
+
variation={variation}
|
|
1061
|
+
>
|
|
1062
|
+
<Stack gap="sm" mb="sm" p="sm">
|
|
1063
|
+
{/* ERROR */}
|
|
1064
|
+
{error && <Alert color="red">{I18n.get(error)}</Alert>}
|
|
1065
|
+
|
|
1066
|
+
{/* OVERRIDABLE PARAMETERS */}
|
|
1067
|
+
{allowOverrideParameters && mode !== "proofread" && (
|
|
1068
|
+
<Paper
|
|
1069
|
+
withBorder
|
|
1070
|
+
p="sm"
|
|
1071
|
+
mt="md"
|
|
1072
|
+
className="ai-feature-options"
|
|
1073
|
+
data-options-display={optionsDisplay}
|
|
1074
|
+
>
|
|
1075
|
+
<Group
|
|
1076
|
+
justify="space-between"
|
|
1077
|
+
align="center"
|
|
1078
|
+
className="ai-feature-options-summary"
|
|
1079
|
+
onClick={
|
|
1080
|
+
optionsDisplay === "collapse"
|
|
1081
|
+
? () => setOptionsOpen((v) => !v)
|
|
1082
|
+
: undefined
|
|
1083
|
+
}
|
|
1084
|
+
>
|
|
1085
|
+
{optionsDisplay === "collapse" && (
|
|
1086
|
+
<Stack gap={0}>
|
|
1087
|
+
<Text
|
|
1088
|
+
size="sm"
|
|
1089
|
+
fw={600}
|
|
1090
|
+
style={{ lineHeight: 1.1 }}
|
|
1091
|
+
>
|
|
1092
|
+
{I18n.get("Options")}
|
|
1093
|
+
</Text>
|
|
1094
|
+
<Text size="xs" c="dimmed" style={{ marginTop: 2 }}>
|
|
1095
|
+
{optionsSummary}
|
|
1096
|
+
</Text>
|
|
1097
|
+
</Stack>
|
|
1098
|
+
)}
|
|
1099
|
+
|
|
1100
|
+
{optionsDisplay === "collapse" && (
|
|
1101
|
+
<Button
|
|
1102
|
+
variant="subtle"
|
|
1103
|
+
size="xs"
|
|
1104
|
+
style={{ minWidth: "fit-content" }}
|
|
1105
|
+
onClick={(e) => {
|
|
1106
|
+
e.stopPropagation();
|
|
1107
|
+
setOptionsOpen((v) => !v);
|
|
1108
|
+
}}
|
|
1109
|
+
>
|
|
1110
|
+
{optionsOpen ? I18n.get("Hide") : I18n.get("Show")}
|
|
1111
|
+
</Button>
|
|
1112
|
+
)}
|
|
1113
|
+
</Group>
|
|
1114
|
+
|
|
1115
|
+
<CollapseComponent in={optionsOpen}>
|
|
1116
|
+
{optionsDisplay === "collapse" && <Divider my="sm" />}
|
|
1117
|
+
<OptionsComponent gap="xs" justify="space-between">
|
|
1118
|
+
{/* TOPIC */}
|
|
1119
|
+
{mode === "write" && allowOverride?.text && (
|
|
1120
|
+
<Tooltip
|
|
1121
|
+
label={I18n.get(
|
|
1122
|
+
"The topic or subject for the AI to write about.",
|
|
1123
|
+
)}
|
|
1124
|
+
disabled={optionsDisplay !== "horizontal"}
|
|
1125
|
+
position="top"
|
|
1126
|
+
>
|
|
1127
|
+
<TextInput
|
|
1128
|
+
size="xs"
|
|
1129
|
+
className="ai-feature-option"
|
|
1130
|
+
styles={compactFieldStyles}
|
|
1131
|
+
disabled={ai.busy}
|
|
1132
|
+
label={I18n.get("Topic")}
|
|
1133
|
+
description={
|
|
1134
|
+
optionsDisplay !== "horizontal"
|
|
1135
|
+
? I18n.get(
|
|
1136
|
+
"The topic or subject for the AI to write about.",
|
|
1137
|
+
)
|
|
1138
|
+
: undefined
|
|
1139
|
+
}
|
|
1140
|
+
value={text || ""}
|
|
1141
|
+
onChange={(
|
|
1142
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
1143
|
+
) => setText(e.target.value)}
|
|
1144
|
+
/>
|
|
1145
|
+
</Tooltip>
|
|
1146
|
+
)}
|
|
1147
|
+
{/* INSTRUCTIONS */}
|
|
1148
|
+
{(mode === "write" ||
|
|
1149
|
+
mode === "rewrite" ||
|
|
1150
|
+
mode === "generateImageMetadata" ||
|
|
1151
|
+
mode === "generatePostMetadata") &&
|
|
1152
|
+
allowOverride?.instructions && (
|
|
1153
|
+
<Tooltip
|
|
1154
|
+
label={I18n.get(
|
|
1155
|
+
"Additional instructions to guide the AI.",
|
|
1156
|
+
)}
|
|
1157
|
+
disabled={optionsDisplay !== "horizontal"}
|
|
1158
|
+
position="top"
|
|
1159
|
+
>
|
|
1160
|
+
<TextInput
|
|
1161
|
+
disabled={ai.busy}
|
|
1162
|
+
size="xs"
|
|
1163
|
+
className="ai-feature-option"
|
|
1164
|
+
styles={compactFieldStyles}
|
|
1165
|
+
label={I18n.get("Instructions")}
|
|
1166
|
+
description={
|
|
1167
|
+
optionsDisplay !== "horizontal"
|
|
1168
|
+
? I18n.get(
|
|
1169
|
+
"Additional instructions to guide the AI.",
|
|
1170
|
+
)
|
|
1171
|
+
: undefined
|
|
1172
|
+
}
|
|
1173
|
+
value={instructions || ""}
|
|
1174
|
+
onChange={(
|
|
1175
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
1176
|
+
) => setInstructions(e.target.value)}
|
|
1177
|
+
/>
|
|
1178
|
+
</Tooltip>
|
|
1179
|
+
)}
|
|
1180
|
+
{/* INPUT LANGUAGE */}
|
|
1181
|
+
{mode === "translate" && (
|
|
1182
|
+
<Tooltip
|
|
1183
|
+
label={I18n.get(
|
|
1184
|
+
"The language of the input text.",
|
|
1185
|
+
)}
|
|
1186
|
+
disabled={optionsDisplay !== "horizontal"}
|
|
1187
|
+
position="top"
|
|
1188
|
+
>
|
|
1189
|
+
<Select
|
|
1190
|
+
disabled={ai.busy}
|
|
1191
|
+
size="xs"
|
|
1192
|
+
styles={compactFieldStyles}
|
|
1193
|
+
className="ai-feature-option"
|
|
1194
|
+
label={I18n.get("Input language")}
|
|
1195
|
+
description={
|
|
1196
|
+
optionsDisplay !== "horizontal"
|
|
1197
|
+
? I18n.get(
|
|
1198
|
+
"The language of the input text.",
|
|
1199
|
+
)
|
|
1200
|
+
: undefined
|
|
1201
|
+
}
|
|
1202
|
+
data={[
|
|
1203
|
+
{
|
|
1204
|
+
value: "auto",
|
|
1205
|
+
label: I18n.get("Auto-detect"),
|
|
1206
|
+
},
|
|
1207
|
+
...LANGUAGE_OPTIONS.map((lo) => ({
|
|
1208
|
+
value: lo.value,
|
|
1209
|
+
label: I18n.get(lo.label),
|
|
1210
|
+
})).sort((a, b) =>
|
|
1211
|
+
a.label.localeCompare(b.label),
|
|
1212
|
+
),
|
|
1213
|
+
]}
|
|
1214
|
+
value={inputLanguage || "auto"}
|
|
1215
|
+
onChange={(value) =>
|
|
1216
|
+
setInputLanguage(value as AiKitLanguageCode)
|
|
1217
|
+
}
|
|
1218
|
+
/>
|
|
1219
|
+
</Tooltip>
|
|
1220
|
+
)}
|
|
1221
|
+
{/* OUTPUT LANGUAGE */}
|
|
1222
|
+
{allowOverride?.outputLanguage && (
|
|
1223
|
+
<Tooltip
|
|
1224
|
+
label={I18n.get(
|
|
1225
|
+
"The language AI-Kit should use for generated text by default (when applicable).",
|
|
1226
|
+
)}
|
|
1227
|
+
disabled={optionsDisplay !== "horizontal"}
|
|
1228
|
+
position="top"
|
|
1229
|
+
>
|
|
1230
|
+
<Select
|
|
1231
|
+
disabled={ai.busy}
|
|
1232
|
+
size="xs"
|
|
1233
|
+
styles={compactFieldStyles}
|
|
1234
|
+
className="ai-feature-option"
|
|
1235
|
+
label={I18n.get("Output language")}
|
|
1236
|
+
description={
|
|
1237
|
+
optionsDisplay !== "horizontal"
|
|
1238
|
+
? I18n.get(
|
|
1239
|
+
"The language AI-Kit should use for generated text by default (when applicable).",
|
|
1240
|
+
)
|
|
1241
|
+
: undefined
|
|
1242
|
+
}
|
|
1243
|
+
data={[
|
|
1244
|
+
...([
|
|
1245
|
+
mode === "rewrite"
|
|
1246
|
+
? {
|
|
1247
|
+
value: "auto",
|
|
1248
|
+
label: I18n.get("Auto-detect"),
|
|
1249
|
+
}
|
|
1250
|
+
: undefined,
|
|
1251
|
+
].filter(Boolean) as {
|
|
1252
|
+
value: string;
|
|
1253
|
+
label: string;
|
|
1254
|
+
}[]),
|
|
1255
|
+
...LANGUAGE_OPTIONS.map((lo) => ({
|
|
1256
|
+
value: lo.value,
|
|
1257
|
+
label: I18n.get(lo.label),
|
|
1258
|
+
})).sort((a, b) =>
|
|
1259
|
+
a.label.localeCompare(b.label),
|
|
1260
|
+
),
|
|
1261
|
+
]}
|
|
1262
|
+
value={
|
|
1263
|
+
outputLanguage ||
|
|
1264
|
+
getAiKitPlugin().settings
|
|
1265
|
+
.defaultOutputLanguage ||
|
|
1266
|
+
(mode === "rewrite" ? "auto" : "")
|
|
1267
|
+
}
|
|
1268
|
+
onChange={(value) =>
|
|
1269
|
+
setOutputLanguage(value as AiKitLanguageCode)
|
|
1270
|
+
}
|
|
1271
|
+
/>
|
|
1272
|
+
</Tooltip>
|
|
1273
|
+
)}
|
|
1274
|
+
{/* TYPE */}
|
|
1275
|
+
{mode === "summarize" && allowOverride?.type && (
|
|
1276
|
+
<Tooltip
|
|
1277
|
+
label={I18n.get("The summary style to generate.")}
|
|
1278
|
+
disabled={optionsDisplay !== "horizontal"}
|
|
1279
|
+
position="top"
|
|
1280
|
+
>
|
|
1281
|
+
<Select
|
|
1282
|
+
disabled={ai.busy}
|
|
1283
|
+
size="xs"
|
|
1284
|
+
className="ai-feature-option"
|
|
1285
|
+
styles={compactFieldStyles}
|
|
1286
|
+
label={I18n.get("Type")}
|
|
1287
|
+
description={
|
|
1288
|
+
optionsDisplay !== "horizontal"
|
|
1289
|
+
? I18n.get("The summary style to generate.")
|
|
1290
|
+
: undefined
|
|
1291
|
+
}
|
|
1292
|
+
data={[
|
|
1293
|
+
{
|
|
1294
|
+
value: "headline",
|
|
1295
|
+
label: I18n.get("Headline"),
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
value: "key-points",
|
|
1299
|
+
label: I18n.get("Key Points"),
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
value: "teaser",
|
|
1303
|
+
label: I18n.get("Teaser"),
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
value: "tldr",
|
|
1307
|
+
label: I18n.get("TL;DR"),
|
|
1308
|
+
},
|
|
1309
|
+
]}
|
|
1310
|
+
value={type || "key-points"}
|
|
1311
|
+
onChange={(value) =>
|
|
1312
|
+
setType(value as SummarizerType)
|
|
1313
|
+
}
|
|
1314
|
+
/>
|
|
1315
|
+
</Tooltip>
|
|
1316
|
+
)}
|
|
1317
|
+
{/* TONE */}
|
|
1318
|
+
{(mode === "write" || mode === "rewrite") &&
|
|
1319
|
+
allowOverride?.tone && (
|
|
1320
|
+
<Tooltip
|
|
1321
|
+
label={I18n.get(
|
|
1322
|
+
"The tone or style for the AI to use.",
|
|
1323
|
+
)}
|
|
1324
|
+
disabled={optionsDisplay !== "horizontal"}
|
|
1325
|
+
position="top"
|
|
1326
|
+
>
|
|
1327
|
+
<Select
|
|
1328
|
+
disabled={ai.busy}
|
|
1329
|
+
size="xs"
|
|
1330
|
+
className="ai-feature-option"
|
|
1331
|
+
styles={compactFieldStyles}
|
|
1332
|
+
label={I18n.get("Tone")}
|
|
1333
|
+
description={
|
|
1334
|
+
optionsDisplay !== "horizontal"
|
|
1335
|
+
? I18n.get(
|
|
1336
|
+
"The tone or style for the AI to use.",
|
|
1337
|
+
)
|
|
1338
|
+
: undefined
|
|
1339
|
+
}
|
|
1340
|
+
data={
|
|
1341
|
+
mode === "write"
|
|
1342
|
+
? [
|
|
1343
|
+
{
|
|
1344
|
+
value: "neutral",
|
|
1345
|
+
label: I18n.get("Neutral"),
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
value: "formal",
|
|
1349
|
+
label: I18n.get("Formal"),
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
value: "casual",
|
|
1353
|
+
label: I18n.get("Casual"),
|
|
1354
|
+
},
|
|
1355
|
+
]
|
|
1356
|
+
: [
|
|
1357
|
+
{
|
|
1358
|
+
value: "as-is",
|
|
1359
|
+
label: I18n.get("As-Is"),
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
value: "more-formal",
|
|
1363
|
+
label: I18n.get("More formal"),
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
value: "more-casual",
|
|
1367
|
+
label: I18n.get("More casual"),
|
|
1368
|
+
},
|
|
1369
|
+
]
|
|
1370
|
+
}
|
|
1371
|
+
value={
|
|
1372
|
+
tone ||
|
|
1373
|
+
(mode === "write" ? "neutral" : "as-is")
|
|
1374
|
+
}
|
|
1375
|
+
onChange={(value) =>
|
|
1376
|
+
setTone(value as WriterTone | RewriterTone)
|
|
1377
|
+
}
|
|
1378
|
+
/>
|
|
1379
|
+
</Tooltip>
|
|
1380
|
+
)}
|
|
1381
|
+
{/* LENGTH */}
|
|
1382
|
+
{(mode === "write" ||
|
|
1383
|
+
mode === "rewrite" ||
|
|
1384
|
+
mode === "summarize") &&
|
|
1385
|
+
allowOverride?.length && (
|
|
1386
|
+
<Tooltip
|
|
1387
|
+
label={I18n.get("The target output length.")}
|
|
1388
|
+
disabled={optionsDisplay !== "horizontal"}
|
|
1389
|
+
position="top"
|
|
1390
|
+
>
|
|
1391
|
+
<Select
|
|
1392
|
+
disabled={ai.busy}
|
|
1393
|
+
size="xs"
|
|
1394
|
+
className="ai-feature-option"
|
|
1395
|
+
styles={compactFieldStyles}
|
|
1396
|
+
label={I18n.get("Length")}
|
|
1397
|
+
description={
|
|
1398
|
+
optionsDisplay !== "horizontal"
|
|
1399
|
+
? I18n.get("The target output length.")
|
|
1400
|
+
: undefined
|
|
1401
|
+
}
|
|
1402
|
+
data={
|
|
1403
|
+
mode === "write" || mode === "summarize"
|
|
1404
|
+
? [
|
|
1405
|
+
{
|
|
1406
|
+
value: "short",
|
|
1407
|
+
label: I18n.get("Short"),
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
value: "medium",
|
|
1411
|
+
label: I18n.get("Medium"),
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
value: "long",
|
|
1415
|
+
label: I18n.get("Long"),
|
|
1416
|
+
},
|
|
1417
|
+
]
|
|
1418
|
+
: [
|
|
1419
|
+
{
|
|
1420
|
+
value: "as-is",
|
|
1421
|
+
label: I18n.get("As-Is"),
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
value: "shorter",
|
|
1425
|
+
label: I18n.get("Shorter"),
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
value: "longer",
|
|
1429
|
+
label: I18n.get("Longer"),
|
|
1430
|
+
},
|
|
1431
|
+
]
|
|
1432
|
+
}
|
|
1433
|
+
value={
|
|
1434
|
+
length ||
|
|
1435
|
+
(mode === "rewrite" ? "as-is" : "short")
|
|
1436
|
+
}
|
|
1437
|
+
onChange={(value) =>
|
|
1438
|
+
setLength(
|
|
1439
|
+
value as
|
|
1440
|
+
| WriterLength
|
|
1441
|
+
| RewriterLength
|
|
1442
|
+
| SummarizerLength,
|
|
1443
|
+
)
|
|
1444
|
+
}
|
|
1445
|
+
/>
|
|
1446
|
+
</Tooltip>
|
|
1447
|
+
)}
|
|
1448
|
+
{/* OUTPUT FORMAT */}
|
|
1449
|
+
{mode === "summarize" ||
|
|
1450
|
+
mode === "write" ||
|
|
1451
|
+
(mode === "rewrite" &&
|
|
1452
|
+
allowOverride?.outputFormat && (
|
|
1453
|
+
<Tooltip
|
|
1454
|
+
label={I18n.get(
|
|
1455
|
+
"The format for the generated output.",
|
|
1456
|
+
)}
|
|
1457
|
+
disabled={optionsDisplay !== "horizontal"}
|
|
1458
|
+
position="top"
|
|
1459
|
+
>
|
|
1460
|
+
<Select
|
|
1461
|
+
disabled={ai.busy}
|
|
1462
|
+
size="xs"
|
|
1463
|
+
className="ai-feature-option"
|
|
1464
|
+
styles={compactFieldStyles}
|
|
1465
|
+
label={I18n.get("Output format")}
|
|
1466
|
+
description={
|
|
1467
|
+
optionsDisplay !== "horizontal"
|
|
1468
|
+
? I18n.get(
|
|
1469
|
+
"The format for the generated output.",
|
|
1470
|
+
)
|
|
1471
|
+
: undefined
|
|
1472
|
+
}
|
|
1473
|
+
data={[
|
|
1474
|
+
{
|
|
1475
|
+
value: "plain-text",
|
|
1476
|
+
label: I18n.get("Plain Text"),
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
value: "markdown",
|
|
1480
|
+
label: I18n.get("Markdown"),
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
value: "html",
|
|
1484
|
+
label: I18n.get("HTML"),
|
|
1485
|
+
},
|
|
1486
|
+
]}
|
|
1487
|
+
value={outputFormat || "markdown"}
|
|
1488
|
+
onChange={(value) =>
|
|
1489
|
+
setOutputFormat(
|
|
1490
|
+
value as
|
|
1491
|
+
| "plain-text"
|
|
1492
|
+
| "markdown"
|
|
1493
|
+
| "html",
|
|
1494
|
+
)
|
|
1495
|
+
}
|
|
1496
|
+
/>
|
|
1497
|
+
</Tooltip>
|
|
1498
|
+
))}
|
|
1499
|
+
</OptionsComponent>
|
|
1500
|
+
</CollapseComponent>
|
|
1501
|
+
</Paper>
|
|
1502
|
+
)}
|
|
1503
|
+
|
|
1504
|
+
{/* AI STATUS */}
|
|
1505
|
+
{ai.busy && statusText && (
|
|
1506
|
+
<AiFeatureBorder
|
|
1507
|
+
enabled={variation === "modal"}
|
|
1508
|
+
working={ai.busy}
|
|
1509
|
+
variation={variation}
|
|
1510
|
+
>
|
|
1511
|
+
<Group
|
|
1512
|
+
justify="center"
|
|
1513
|
+
align="center"
|
|
1514
|
+
gap="sm"
|
|
1515
|
+
m="sm"
|
|
1516
|
+
pr="lg"
|
|
1517
|
+
>
|
|
1518
|
+
<Loader size="sm" />
|
|
1519
|
+
<Input.Label className="ai-feature-status-text">
|
|
1520
|
+
{statusText ?? "VALAMILYEN SZÖVEG"}
|
|
1521
|
+
</Input.Label>
|
|
1522
|
+
</Group>
|
|
1523
|
+
</AiFeatureBorder>
|
|
1524
|
+
)}
|
|
1525
|
+
|
|
1526
|
+
{/* GENERATED OUTPUT */}
|
|
1527
|
+
{generated && (
|
|
1528
|
+
<Stack mt="md">
|
|
1529
|
+
{mode === "proofread" &&
|
|
1530
|
+
((generated as ProofreadResult).corrections.length ===
|
|
1531
|
+
0 ? (
|
|
1532
|
+
<Alert color="green">
|
|
1533
|
+
{I18n.get(
|
|
1534
|
+
"No issues found. Your text looks great!",
|
|
1535
|
+
)}
|
|
1536
|
+
</Alert>
|
|
1537
|
+
) : (
|
|
1538
|
+
<>
|
|
1539
|
+
<p style={{ marginTop: 0, opacity: 0.85 }}>
|
|
1540
|
+
{I18n.get(
|
|
1541
|
+
"Hover highlights to see explanations.",
|
|
1542
|
+
)}
|
|
1543
|
+
</p>
|
|
1544
|
+
<ProofreadDiff
|
|
1545
|
+
original={text!}
|
|
1546
|
+
corrections={
|
|
1547
|
+
(generated as ProofreadResult).corrections
|
|
1548
|
+
}
|
|
1549
|
+
/>
|
|
1550
|
+
{(generated as ProofreadResult).correctedInput ? (
|
|
1551
|
+
<>
|
|
1552
|
+
<h4
|
|
1553
|
+
style={{
|
|
1554
|
+
marginTop: 16,
|
|
1555
|
+
marginBottom: 8,
|
|
1556
|
+
}}
|
|
1557
|
+
>
|
|
1558
|
+
{I18n.get("Corrected")}
|
|
1559
|
+
</h4>
|
|
1560
|
+
<Group
|
|
1561
|
+
c="pre"
|
|
1562
|
+
className="ai-feature-generated-content"
|
|
1563
|
+
>
|
|
1564
|
+
{
|
|
1565
|
+
(generated as ProofreadResult)
|
|
1566
|
+
.correctedInput
|
|
1567
|
+
}
|
|
1568
|
+
</Group>
|
|
1569
|
+
</>
|
|
1570
|
+
) : null}
|
|
1571
|
+
</>
|
|
1572
|
+
))}
|
|
1573
|
+
{mode === "generateImageMetadata" && (
|
|
1574
|
+
<>
|
|
1575
|
+
<TextInput
|
|
1576
|
+
readOnly={!editable}
|
|
1577
|
+
label={I18n.get("Alt Text")}
|
|
1578
|
+
description={I18n.get(
|
|
1579
|
+
"The alt text for the image.",
|
|
1580
|
+
)}
|
|
1581
|
+
value={
|
|
1582
|
+
(generated as GeneratedImageMetadata).alt_text ||
|
|
1583
|
+
""
|
|
1584
|
+
}
|
|
1585
|
+
onChange={(
|
|
1586
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
1587
|
+
) =>
|
|
1588
|
+
setGenerated({
|
|
1589
|
+
...(generated as GeneratedImageMetadata),
|
|
1590
|
+
alt_text: e.target.value,
|
|
1591
|
+
} as never)
|
|
1592
|
+
}
|
|
1593
|
+
/>
|
|
1594
|
+
<TextInput
|
|
1595
|
+
readOnly={!editable}
|
|
1596
|
+
label={I18n.get("Title")}
|
|
1597
|
+
description={I18n.get("The title for the image.")}
|
|
1598
|
+
value={
|
|
1599
|
+
(generated as GeneratedImageMetadata).title || ""
|
|
1600
|
+
}
|
|
1601
|
+
onChange={(
|
|
1602
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
1603
|
+
) =>
|
|
1604
|
+
setGenerated({
|
|
1605
|
+
...(generated as GeneratedImageMetadata),
|
|
1606
|
+
title: e.target.value,
|
|
1607
|
+
} as never)
|
|
1608
|
+
}
|
|
1609
|
+
/>
|
|
1610
|
+
<TextInput
|
|
1611
|
+
readOnly={!editable}
|
|
1612
|
+
label={I18n.get("Caption")}
|
|
1613
|
+
description={I18n.get("The caption for the image.")}
|
|
1614
|
+
value={
|
|
1615
|
+
(generated as GeneratedImageMetadata).caption ||
|
|
1616
|
+
""
|
|
1617
|
+
}
|
|
1618
|
+
onChange={(
|
|
1619
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
1620
|
+
) =>
|
|
1621
|
+
setGenerated({
|
|
1622
|
+
...(generated as GeneratedImageMetadata),
|
|
1623
|
+
caption: e.target.value,
|
|
1624
|
+
} as never)
|
|
1625
|
+
}
|
|
1626
|
+
/>
|
|
1627
|
+
<TextInput
|
|
1628
|
+
readOnly={!editable}
|
|
1629
|
+
label={I18n.get("Description")}
|
|
1630
|
+
description={I18n.get(
|
|
1631
|
+
"The description for the image.",
|
|
1632
|
+
)}
|
|
1633
|
+
value={
|
|
1634
|
+
(generated as GeneratedImageMetadata)
|
|
1635
|
+
.description || ""
|
|
1636
|
+
}
|
|
1637
|
+
onChange={(
|
|
1638
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
1639
|
+
) =>
|
|
1640
|
+
setGenerated({
|
|
1641
|
+
...(generated as GeneratedImageMetadata),
|
|
1642
|
+
description: e.target.value,
|
|
1643
|
+
} as never)
|
|
1644
|
+
}
|
|
1645
|
+
/>
|
|
1646
|
+
</>
|
|
1647
|
+
)}
|
|
1648
|
+
{mode === "generatePostMetadata" && (
|
|
1649
|
+
<>
|
|
1650
|
+
<TextInput
|
|
1651
|
+
readOnly={!editable}
|
|
1652
|
+
label={I18n.get("Title")}
|
|
1653
|
+
description={I18n.get("The title for the post.")}
|
|
1654
|
+
value={
|
|
1655
|
+
(generated as GeneratedPostMetadata).title || ""
|
|
1656
|
+
}
|
|
1657
|
+
onChange={(
|
|
1658
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
1659
|
+
) =>
|
|
1660
|
+
setGenerated({
|
|
1661
|
+
...(generated as GeneratedPostMetadata),
|
|
1662
|
+
title: e.target.value,
|
|
1663
|
+
} as never)
|
|
1664
|
+
}
|
|
1665
|
+
/>
|
|
1666
|
+
<TextInput
|
|
1667
|
+
readOnly={!editable}
|
|
1668
|
+
label={I18n.get("Excerpt")}
|
|
1669
|
+
description={I18n.get("The excerpt for the post.")}
|
|
1670
|
+
value={
|
|
1671
|
+
(generated as GeneratedPostMetadata).excerpt || ""
|
|
1672
|
+
}
|
|
1673
|
+
onChange={(
|
|
1674
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
1675
|
+
) =>
|
|
1676
|
+
setGenerated({
|
|
1677
|
+
...(generated as GeneratedPostMetadata),
|
|
1678
|
+
excerpt: e.target.value,
|
|
1679
|
+
} as never)
|
|
1680
|
+
}
|
|
1681
|
+
/>
|
|
1682
|
+
</>
|
|
1683
|
+
)}
|
|
1684
|
+
{mode !== "proofread" &&
|
|
1685
|
+
mode !== "generateImageMetadata" &&
|
|
1686
|
+
mode !== "generatePostMetadata" &&
|
|
1687
|
+
typeof generated === "string" && (
|
|
1688
|
+
<MarkdownResult
|
|
1689
|
+
value={generated}
|
|
1690
|
+
editable={!!editable}
|
|
1691
|
+
onChange={(v) => {
|
|
1692
|
+
setGenerated(v as never);
|
|
1693
|
+
}}
|
|
1694
|
+
/>
|
|
1695
|
+
)}
|
|
1696
|
+
</Stack>
|
|
1697
|
+
)}
|
|
1698
|
+
{generated === "" && (
|
|
1699
|
+
<MarkdownResult value={generated} editable={false} />
|
|
1700
|
+
)}
|
|
1701
|
+
</Stack>
|
|
1702
|
+
{/* ACTIONS */}
|
|
1703
|
+
<Group className="ai-kit-actions" gap="sm" mb="sm" p="sm">
|
|
1704
|
+
{ai.busy && (
|
|
1705
|
+
<Button
|
|
1706
|
+
variant="outline"
|
|
1707
|
+
size="sm"
|
|
1708
|
+
onClick={cancel}
|
|
1709
|
+
data-ai-kit-cancel-button
|
|
1710
|
+
>
|
|
1711
|
+
{I18n.get("Cancel")}
|
|
1712
|
+
</Button>
|
|
1713
|
+
)}
|
|
1714
|
+
|
|
1715
|
+
{!ai.busy && (
|
|
1716
|
+
<Button
|
|
1717
|
+
variant="filled"
|
|
1718
|
+
size="sm"
|
|
1719
|
+
disabled={!canGenerate}
|
|
1720
|
+
onClick={() => runGenerate()}
|
|
1721
|
+
data-ai-kit-generate-button
|
|
1722
|
+
>
|
|
1723
|
+
{getGenerateTitle()}
|
|
1724
|
+
</Button>
|
|
1725
|
+
)}
|
|
1726
|
+
|
|
1727
|
+
{!ai.busy &&
|
|
1728
|
+
ai.lastSource === "on-device" &&
|
|
1729
|
+
backendConfigured &&
|
|
1730
|
+
showRegenerateOnBackendButton && (
|
|
1731
|
+
<Button
|
|
1732
|
+
variant="filled"
|
|
1733
|
+
size="sm"
|
|
1734
|
+
disabled={!canGenerate}
|
|
1735
|
+
onClick={runGenerateOnBackend}
|
|
1736
|
+
data-ai-kit-regenerate-on-backend-button
|
|
1737
|
+
>
|
|
1738
|
+
{getRegenerateOnBackendTitle()}
|
|
1739
|
+
</Button>
|
|
1740
|
+
)}
|
|
1741
|
+
|
|
1742
|
+
{!ai.busy && onAccept && (
|
|
1743
|
+
<Button
|
|
1744
|
+
variant="outline"
|
|
1745
|
+
size="sm"
|
|
1746
|
+
disabled={
|
|
1747
|
+
!generated ||
|
|
1748
|
+
(mode === "proofread" &&
|
|
1749
|
+
(generated as ProofreadResult).corrections.length ===
|
|
1750
|
+
0)
|
|
1751
|
+
}
|
|
1752
|
+
onClick={async () => {
|
|
1753
|
+
onAccept(
|
|
1754
|
+
outputFormat === "html"
|
|
1755
|
+
? await markdownToHtml(generated!)
|
|
1756
|
+
: generated!,
|
|
1757
|
+
);
|
|
1758
|
+
close();
|
|
1759
|
+
}}
|
|
1760
|
+
data-ai-kit-accept-button
|
|
1761
|
+
>
|
|
1762
|
+
{I18n.get(acceptButtonTitle!)}
|
|
1763
|
+
</Button>
|
|
1764
|
+
)}
|
|
1765
|
+
|
|
1766
|
+
<Button
|
|
1767
|
+
variant="default"
|
|
1768
|
+
size="sm"
|
|
1769
|
+
onClick={close}
|
|
1770
|
+
data-ai-kit-close-button
|
|
1771
|
+
>
|
|
1772
|
+
{I18n.get("Close")}
|
|
1773
|
+
</Button>
|
|
1774
|
+
</Group>
|
|
1775
|
+
</AiFeatureBorder>
|
|
1776
|
+
</BodyComponent>
|
|
1777
|
+
</ContentComponent>
|
|
1778
|
+
</RootComponent>
|
|
1779
|
+
)}
|
|
1780
|
+
</>
|
|
1781
|
+
);
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1784
|
+
function MarkdownResult(props: {
|
|
1785
|
+
value: string;
|
|
1786
|
+
editable: boolean;
|
|
1787
|
+
onChange?: (v: string) => void;
|
|
1788
|
+
}) {
|
|
1789
|
+
const { value, editable, onChange } = props;
|
|
1790
|
+
|
|
1791
|
+
if (editable) {
|
|
1792
|
+
return (
|
|
1793
|
+
<Stack p={0} gap="sm">
|
|
1794
|
+
<Input.Label>{I18n.get("Generated content")}</Input.Label>
|
|
1795
|
+
<Textarea
|
|
1796
|
+
value={value}
|
|
1797
|
+
onChange={(e) => onChange?.(e.currentTarget.value)}
|
|
1798
|
+
autosize
|
|
1799
|
+
minRows={2}
|
|
1800
|
+
maxRows={12}
|
|
1801
|
+
p={0}
|
|
1802
|
+
className="ai-feature-generated-content ai-feature-editor"
|
|
1803
|
+
/>
|
|
1804
|
+
|
|
1805
|
+
<Input.Label>{I18n.get("Preview")}</Input.Label>
|
|
1806
|
+
<Stack className="ai-feature-generated-content ai-feature-preview">
|
|
1807
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{value}</ReactMarkdown>
|
|
1808
|
+
</Stack>
|
|
1809
|
+
</Stack>
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
return (
|
|
1814
|
+
<Stack className="ai-feature-generated-content">
|
|
1815
|
+
{value ? (
|
|
1816
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{value}</ReactMarkdown>
|
|
1817
|
+
) : (
|
|
1818
|
+
<Alert color="yellow">{I18n.get("No content generated.")}</Alert>
|
|
1819
|
+
)}
|
|
1820
|
+
</Stack>
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
export const AiFeature = withAiKitShell(AiFeatureBase);
|