@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.
@@ -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);