@lastbrain/ai-ui-react 1.0.73 → 1.0.75
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/dist/components/AiChipLabel.d.ts.map +1 -1
- package/dist/components/AiChipLabel.js +10 -7
- package/dist/components/AiContextButton.d.ts +1 -1
- package/dist/components/AiContextButton.d.ts.map +1 -1
- package/dist/components/AiContextButton.js +26 -12
- package/dist/components/AiImageButton.d.ts.map +1 -1
- package/dist/components/AiImageButton.js +35 -16
- package/dist/components/AiInput.d.ts.map +1 -1
- package/dist/components/AiInput.js +15 -5
- package/dist/components/AiModelSelect.d.ts.map +1 -1
- package/dist/components/AiModelSelect.js +3 -1
- package/dist/components/AiPromptPanel.d.ts.map +1 -1
- package/dist/components/AiPromptPanel.js +72 -47
- package/dist/components/AiSelect.d.ts.map +1 -1
- package/dist/components/AiSelect.js +8 -3
- package/dist/components/AiStatusButton.d.ts.map +1 -1
- package/dist/components/AiStatusButton.js +23 -20
- package/dist/components/AiTextarea.d.ts.map +1 -1
- package/dist/components/AiTextarea.js +19 -6
- package/dist/components/ErrorToast.d.ts.map +1 -1
- package/dist/components/ErrorToast.js +4 -2
- package/dist/components/LBApiKeySelector.d.ts.map +1 -1
- package/dist/components/LBApiKeySelector.js +13 -5
- package/dist/components/LBConnectButton.d.ts.map +1 -1
- package/dist/components/LBConnectButton.js +6 -3
- package/dist/components/LBKeyPicker.d.ts.map +1 -1
- package/dist/components/LBKeyPicker.js +8 -4
- package/dist/components/LBSigninModal.d.ts.map +1 -1
- package/dist/components/LBSigninModal.js +13 -7
- package/dist/components/UsageToast.d.ts.map +1 -1
- package/dist/components/UsageToast.js +4 -2
- package/dist/context/I18nContext.d.ts +15 -0
- package/dist/context/I18nContext.d.ts.map +1 -0
- package/dist/context/I18nContext.js +44 -0
- package/dist/context/LBAuthProvider.d.ts +4 -1
- package/dist/context/LBAuthProvider.d.ts.map +1 -1
- package/dist/context/LBAuthProvider.js +3 -2
- package/dist/hooks/useAiCallImage.d.ts.map +1 -1
- package/dist/hooks/useAiCallImage.js +1 -107
- package/dist/hooks/useAiCallText.d.ts.map +1 -1
- package/dist/hooks/useAiCallText.js +1 -25
- package/dist/hooks/useLoadingTimer.d.ts +5 -0
- package/dist/hooks/useLoadingTimer.d.ts.map +1 -0
- package/dist/hooks/useLoadingTimer.js +31 -0
- package/dist/i18n/de.json +62 -0
- package/dist/i18n/en.json +128 -0
- package/dist/i18n/es.json +70 -0
- package/dist/i18n/fr.json +128 -0
- package/dist/i18n/it.json +62 -0
- package/dist/i18n/pt.json +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/styles.css +141 -1
- package/package.json +3 -3
- package/src/components/AiChipLabel.tsx +14 -8
- package/src/components/AiContextButton.tsx +53 -20
- package/src/components/AiImageButton.tsx +58 -25
- package/src/components/AiInput.tsx +20 -5
- package/src/components/AiModelSelect.tsx +5 -1
- package/src/components/AiPromptPanel.tsx +203 -76
- package/src/components/AiSelect.tsx +8 -3
- package/src/components/AiStatusButton.tsx +75 -46
- package/src/components/AiTextarea.tsx +24 -6
- package/src/components/ErrorToast.tsx +4 -2
- package/src/components/LBApiKeySelector.tsx +29 -9
- package/src/components/LBConnectButton.tsx +7 -3
- package/src/components/LBKeyPicker.tsx +10 -4
- package/src/components/LBSigninModal.tsx +33 -15
- package/src/components/UsageToast.tsx +4 -2
- package/src/context/I18nContext.tsx +75 -0
- package/src/context/LBAuthProvider.tsx +9 -1
- package/src/hooks/useAiCallImage.ts +1 -149
- package/src/hooks/useAiCallText.ts +1 -30
- package/src/hooks/useLoadingTimer.ts +38 -0
- package/src/i18n/de.json +62 -0
- package/src/i18n/en.json +128 -0
- package/src/i18n/es.json +70 -0
- package/src/i18n/fr.json +128 -0
- package/src/i18n/it.json +62 -0
- package/src/i18n/pt.json +62 -0
- package/src/index.ts +2 -0
- package/src/styles.css +141 -1
|
@@ -12,6 +12,8 @@ import { handleAIError } from "../utils/errorHandler";
|
|
|
12
12
|
import { useLB } from "../context/LBAuthProvider";
|
|
13
13
|
import { LBSigninModal } from "./LBSigninModal";
|
|
14
14
|
import { useAiContext } from "../context/AiProvider";
|
|
15
|
+
import { useI18n } from "../context/I18nContext";
|
|
16
|
+
import { useLoadingTimer } from "../hooks/useLoadingTimer";
|
|
15
17
|
|
|
16
18
|
export interface AiInputProps
|
|
17
19
|
extends
|
|
@@ -42,6 +44,7 @@ export function AiInput({
|
|
|
42
44
|
className,
|
|
43
45
|
...inputProps
|
|
44
46
|
}: AiInputProps) {
|
|
47
|
+
const { t } = useI18n();
|
|
45
48
|
const [isOpen, setIsOpen] = useState(false);
|
|
46
49
|
const [showAuthModal, setShowAuthModal] = useState(false);
|
|
47
50
|
const [inputValue, setInputValue] = useState(
|
|
@@ -80,6 +83,7 @@ export function AiInput({
|
|
|
80
83
|
modelType: "text-or-language",
|
|
81
84
|
});
|
|
82
85
|
const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
|
|
86
|
+
const { formatted: loadingElapsed } = useLoadingTimer(loading);
|
|
83
87
|
|
|
84
88
|
const hasConfiguration = Boolean(model && prompt);
|
|
85
89
|
const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
|
|
@@ -125,7 +129,10 @@ export function AiInput({
|
|
|
125
129
|
inputRef.current.value = result.text;
|
|
126
130
|
}
|
|
127
131
|
onValue?.(result.text);
|
|
128
|
-
onToast?.({
|
|
132
|
+
onToast?.({
|
|
133
|
+
type: "success",
|
|
134
|
+
message: t("ai.generationSuccess", "AI generation successful"),
|
|
135
|
+
});
|
|
129
136
|
}
|
|
130
137
|
} catch (error) {
|
|
131
138
|
console.error("AiInput error:", error);
|
|
@@ -159,7 +166,10 @@ export function AiInput({
|
|
|
159
166
|
inputRef.current.value = result.text;
|
|
160
167
|
}
|
|
161
168
|
onValue?.(result.text);
|
|
162
|
-
onToast?.({
|
|
169
|
+
onToast?.({
|
|
170
|
+
type: "success",
|
|
171
|
+
message: t("ai.generationSuccess", "AI generation successful"),
|
|
172
|
+
});
|
|
163
173
|
}
|
|
164
174
|
} catch (error) {
|
|
165
175
|
console.error("AiInput handleQuickGenerate error:", error);
|
|
@@ -201,10 +211,10 @@ export function AiInput({
|
|
|
201
211
|
type="button"
|
|
202
212
|
title={
|
|
203
213
|
!isAuthReady
|
|
204
|
-
? "Authentication required"
|
|
214
|
+
? t("auth.required", "Authentication required")
|
|
205
215
|
: hasConfiguration
|
|
206
|
-
? "Generate with AI"
|
|
207
|
-
: "Setup AI"
|
|
216
|
+
? t("ai.generate", "Generate with AI")
|
|
217
|
+
: t("ai.setup", "Setup AI")
|
|
208
218
|
}
|
|
209
219
|
>
|
|
210
220
|
{loading ? (
|
|
@@ -216,6 +226,11 @@ export function AiInput({
|
|
|
216
226
|
)}
|
|
217
227
|
</button>
|
|
218
228
|
</div>
|
|
229
|
+
{loading ? (
|
|
230
|
+
<span className="ai-control-timer">
|
|
231
|
+
{t("ai.loading.elapsed", "{seconds}", { seconds: loadingElapsed })}
|
|
232
|
+
</span>
|
|
233
|
+
) : null}
|
|
219
234
|
{isOpen && (
|
|
220
235
|
<AiPromptPanel
|
|
221
236
|
isOpen={isOpen}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import "../styles/register";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import type { ModelRef } from "@lastbrain/ai-ui-core";
|
|
6
|
+
import { useI18n } from "../context/I18nContext";
|
|
6
7
|
|
|
7
8
|
export interface AiModelSelectProps {
|
|
8
9
|
models: ModelRef[];
|
|
@@ -19,6 +20,7 @@ export function AiModelSelect({
|
|
|
19
20
|
className,
|
|
20
21
|
disabled,
|
|
21
22
|
}: AiModelSelectProps) {
|
|
23
|
+
const { t } = useI18n();
|
|
22
24
|
return (
|
|
23
25
|
<select
|
|
24
26
|
value={value}
|
|
@@ -27,7 +29,9 @@ export function AiModelSelect({
|
|
|
27
29
|
disabled={disabled}
|
|
28
30
|
data-ai-model-select
|
|
29
31
|
>
|
|
30
|
-
<option value="">
|
|
32
|
+
<option value="">
|
|
33
|
+
{t("ai.select.modelPlaceholder", "Select a model")}
|
|
34
|
+
</option>
|
|
31
35
|
{models.map((model) => (
|
|
32
36
|
<option key={model.id} value={model.id}>
|
|
33
37
|
{model.name}
|
|
@@ -9,7 +9,15 @@ import {
|
|
|
9
9
|
type ReactNode,
|
|
10
10
|
} from "react";
|
|
11
11
|
import { createPortal } from "react-dom";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
BookOpen,
|
|
14
|
+
Search,
|
|
15
|
+
Sparkles,
|
|
16
|
+
Star,
|
|
17
|
+
Tag,
|
|
18
|
+
Settings,
|
|
19
|
+
Loader2,
|
|
20
|
+
} from "lucide-react";
|
|
13
21
|
import type { ModelRef } from "@lastbrain/ai-ui-core";
|
|
14
22
|
import type { UiMode } from "../types";
|
|
15
23
|
import { aiStyles } from "../styles/inline";
|
|
@@ -22,6 +30,8 @@ import {
|
|
|
22
30
|
import { useModelManagement } from "../hooks/useModelManagement";
|
|
23
31
|
import { type AIModel } from "../context/AiProvider";
|
|
24
32
|
import { AiProvider, useAiContext } from "../context/AiProvider";
|
|
33
|
+
import { useI18n } from "../context/I18nContext";
|
|
34
|
+
import { useLoadingTimer } from "../hooks/useLoadingTimer";
|
|
25
35
|
|
|
26
36
|
export interface AiPromptPanelProps {
|
|
27
37
|
isOpen: boolean;
|
|
@@ -105,6 +115,7 @@ function AiPromptPanelInternal({
|
|
|
105
115
|
baseUrl,
|
|
106
116
|
showOnlyUserModels = false,
|
|
107
117
|
}: AiPromptPanelProps) {
|
|
118
|
+
const { t } = useI18n();
|
|
108
119
|
const [selectedModel, setSelectedModel] = useState("");
|
|
109
120
|
const [prompt, setPrompt] = useState("");
|
|
110
121
|
const [promptId, setPromptId] = useState<string | undefined>(undefined);
|
|
@@ -125,6 +136,7 @@ function AiPromptPanelInternal({
|
|
|
125
136
|
const [isModelManagementOpen, setIsModelManagementOpen] = useState(false);
|
|
126
137
|
const [loadingModels, setLoadingModels] = useState<string[]>([]);
|
|
127
138
|
const [modelSearchQuery, setModelSearchQuery] = useState("");
|
|
139
|
+
const { formatted: loadingElapsed } = useLoadingTimer(isGenerating);
|
|
128
140
|
|
|
129
141
|
const {
|
|
130
142
|
prompts,
|
|
@@ -164,6 +176,8 @@ function AiPromptPanelInternal({
|
|
|
164
176
|
const effectiveUserModels =
|
|
165
177
|
userModels.length > 0 ? userModels : autoModelManagement.userModels;
|
|
166
178
|
const effectiveToggleModel = onModelToggle || autoModelManagement.toggleModel;
|
|
179
|
+
const isModelsLoading =
|
|
180
|
+
autoModelManagement.loading && effectiveAvailableModels.length === 0;
|
|
167
181
|
|
|
168
182
|
// Gestion des modèles
|
|
169
183
|
const handleModelToggle = async (modelId: string, isActive: boolean) => {
|
|
@@ -187,7 +201,7 @@ function AiPromptPanelInternal({
|
|
|
187
201
|
id: m.id,
|
|
188
202
|
name: m.name,
|
|
189
203
|
category: m.type === "image" ? ("image" as const) : ("text" as const),
|
|
190
|
-
provider: "Unknown",
|
|
204
|
+
provider: t("common.unknown", "Unknown"),
|
|
191
205
|
}));
|
|
192
206
|
}
|
|
193
207
|
|
|
@@ -464,7 +478,12 @@ function AiPromptPanelInternal({
|
|
|
464
478
|
letterSpacing: "0.02em",
|
|
465
479
|
}}
|
|
466
480
|
>
|
|
467
|
-
|
|
481
|
+
{t("prompt.modal.generating", "Generating...")}
|
|
482
|
+
</div>
|
|
483
|
+
<div className="ai-loading-meta">
|
|
484
|
+
{t("ai.loading.elapsed", "{seconds}", {
|
|
485
|
+
seconds: loadingElapsed,
|
|
486
|
+
})}
|
|
468
487
|
</div>
|
|
469
488
|
</div>
|
|
470
489
|
)}
|
|
@@ -479,7 +498,9 @@ function AiPromptPanelInternal({
|
|
|
479
498
|
}}
|
|
480
499
|
>
|
|
481
500
|
<h2 style={aiStyles.modalTitle}>
|
|
482
|
-
{showPromptLibrary
|
|
501
|
+
{showPromptLibrary
|
|
502
|
+
? t("prompt.modal.selectPrompt", "Select a Prompt")
|
|
503
|
+
: t("prompt.modal.title", "AI Prompt Configuration")}
|
|
483
504
|
</h2>
|
|
484
505
|
<button
|
|
485
506
|
style={{
|
|
@@ -489,7 +510,7 @@ function AiPromptPanelInternal({
|
|
|
489
510
|
onClick={handleClose}
|
|
490
511
|
onMouseEnter={() => setIsCloseHovered(true)}
|
|
491
512
|
onMouseLeave={() => setIsCloseHovered(false)}
|
|
492
|
-
aria-label="Close"
|
|
513
|
+
aria-label={t("common.closeLabel", "Close")}
|
|
493
514
|
>
|
|
494
515
|
×
|
|
495
516
|
</button>
|
|
@@ -511,7 +532,9 @@ function AiPromptPanelInternal({
|
|
|
511
532
|
marginBottom: "16px",
|
|
512
533
|
}}
|
|
513
534
|
>
|
|
514
|
-
<label style={aiStyles.modalLabel}>
|
|
535
|
+
<label style={aiStyles.modalLabel}>
|
|
536
|
+
{t("prompt.modal.sourceText", "Source Text")}
|
|
537
|
+
</label>
|
|
515
538
|
<div
|
|
516
539
|
style={{
|
|
517
540
|
padding: "12px",
|
|
@@ -546,14 +569,22 @@ function AiPromptPanelInternal({
|
|
|
546
569
|
<label htmlFor="model-select" style={aiStyles.modalLabel}>
|
|
547
570
|
AI Model
|
|
548
571
|
</label>
|
|
549
|
-
{
|
|
572
|
+
{isModelsLoading ? (
|
|
573
|
+
<span
|
|
574
|
+
className="ai-inline-skeleton"
|
|
575
|
+
aria-hidden="true"
|
|
576
|
+
/>
|
|
577
|
+
) : effectiveAvailableModels.length > 0 ? (
|
|
550
578
|
<button
|
|
551
579
|
onClick={() => setIsModelManagementOpen(true)}
|
|
552
580
|
className="ai-inline-btn"
|
|
553
|
-
title=
|
|
581
|
+
title={t(
|
|
582
|
+
"prompt.modal.manageModels",
|
|
583
|
+
"Manage models"
|
|
584
|
+
)}
|
|
554
585
|
>
|
|
555
586
|
<Settings size={14} />
|
|
556
|
-
|
|
587
|
+
{t("prompt.modal.manageModels", "Manage models")}
|
|
557
588
|
{effectiveAvailableModels.filter(
|
|
558
589
|
(m) =>
|
|
559
590
|
m.category === modelCategory &&
|
|
@@ -581,7 +612,7 @@ function AiPromptPanelInternal({
|
|
|
581
612
|
</span>
|
|
582
613
|
)}
|
|
583
614
|
</button>
|
|
584
|
-
)}
|
|
615
|
+
) : null}
|
|
585
616
|
</div>
|
|
586
617
|
<select
|
|
587
618
|
id="model-select"
|
|
@@ -597,8 +628,14 @@ function AiPromptPanelInternal({
|
|
|
597
628
|
{modelOptions.length === 0 && (
|
|
598
629
|
<option value="">
|
|
599
630
|
{showOnlyUserModels
|
|
600
|
-
?
|
|
601
|
-
|
|
631
|
+
? t(
|
|
632
|
+
"prompt.modal.noActiveModels",
|
|
633
|
+
"No active models. Open 'Manage models'."
|
|
634
|
+
)
|
|
635
|
+
: t(
|
|
636
|
+
"prompt.modal.loadingModels",
|
|
637
|
+
"Loading models..."
|
|
638
|
+
)}
|
|
602
639
|
</option>
|
|
603
640
|
)}
|
|
604
641
|
{modelOptions.map((model) => {
|
|
@@ -613,7 +650,9 @@ function AiPromptPanelInternal({
|
|
|
613
650
|
>
|
|
614
651
|
{model.name}
|
|
615
652
|
{showAllModels && isActive && " ✓"}
|
|
616
|
-
{showAllModels && !isActive
|
|
653
|
+
{showAllModels && !isActive
|
|
654
|
+
? ` (${t("common.inactive", "Inactive")})`
|
|
655
|
+
: ""}
|
|
617
656
|
</option>
|
|
618
657
|
);
|
|
619
658
|
})}
|
|
@@ -626,7 +665,11 @@ function AiPromptPanelInternal({
|
|
|
626
665
|
marginTop: "4px",
|
|
627
666
|
}}
|
|
628
667
|
>
|
|
629
|
-
💡
|
|
668
|
+
💡{" "}
|
|
669
|
+
{t(
|
|
670
|
+
"prompt.modal.clickSettingsHint",
|
|
671
|
+
"Click ⚙️ to enable/disable models"
|
|
672
|
+
)}
|
|
630
673
|
</div>
|
|
631
674
|
)}
|
|
632
675
|
</div>
|
|
@@ -648,7 +691,9 @@ function AiPromptPanelInternal({
|
|
|
648
691
|
}}
|
|
649
692
|
>
|
|
650
693
|
{models.length === 0 && (
|
|
651
|
-
<option value="">
|
|
694
|
+
<option value="">
|
|
695
|
+
{t("prompt.modal.loadingModels", "Loading models...")}
|
|
696
|
+
</option>
|
|
652
697
|
)}
|
|
653
698
|
{models.map((model) => (
|
|
654
699
|
<option key={model.id} value={model.id}>
|
|
@@ -670,7 +715,7 @@ function AiPromptPanelInternal({
|
|
|
670
715
|
}}
|
|
671
716
|
>
|
|
672
717
|
<label htmlFor="prompt-input" style={aiStyles.modalLabel}>
|
|
673
|
-
Prompt
|
|
718
|
+
{t("prompt.modal.prompt", "Prompt")}
|
|
674
719
|
<span
|
|
675
720
|
style={{
|
|
676
721
|
color: "var(--ai-text-secondary)",
|
|
@@ -679,18 +724,24 @@ function AiPromptPanelInternal({
|
|
|
679
724
|
fontWeight: 400,
|
|
680
725
|
}}
|
|
681
726
|
>
|
|
682
|
-
(
|
|
727
|
+
{t(
|
|
728
|
+
"prompt.modal.promptHint",
|
|
729
|
+
"(Cmd/Ctrl + Enter to submit)"
|
|
730
|
+
)}
|
|
683
731
|
</span>
|
|
684
732
|
</label>
|
|
685
|
-
{
|
|
733
|
+
{promptsLoading ? (
|
|
734
|
+
<span className="ai-inline-skeleton" aria-hidden="true" />
|
|
735
|
+
) : filteredPrompts.length > 0 ? (
|
|
686
736
|
<button
|
|
687
737
|
onClick={() => setShowPromptLibrary(true)}
|
|
688
738
|
className="ai-inline-btn"
|
|
689
739
|
>
|
|
690
740
|
<BookOpen size={14} />
|
|
691
|
-
Browse Prompts (
|
|
741
|
+
{t("prompt.modal.browsePrompts", "Browse Prompts")} (
|
|
742
|
+
{filteredPrompts.length})
|
|
692
743
|
</button>
|
|
693
|
-
)}
|
|
744
|
+
) : null}
|
|
694
745
|
</div>
|
|
695
746
|
<textarea
|
|
696
747
|
id="prompt-input"
|
|
@@ -701,8 +752,14 @@ function AiPromptPanelInternal({
|
|
|
701
752
|
onBlur={() => setPromptFocused(false)}
|
|
702
753
|
placeholder={
|
|
703
754
|
sourceText
|
|
704
|
-
?
|
|
705
|
-
|
|
755
|
+
? t(
|
|
756
|
+
"prompt.modal.promptPlaceholderWithSource",
|
|
757
|
+
"Enter your AI prompt... e.g., 'Fix grammar', 'Make it more professional', 'Translate to English'"
|
|
758
|
+
)
|
|
759
|
+
: t(
|
|
760
|
+
"prompt.modal.promptPlaceholderNoSource",
|
|
761
|
+
"Enter your AI prompt... e.g., 'Write a blog post about AI', 'Generate product description'"
|
|
762
|
+
)
|
|
706
763
|
}
|
|
707
764
|
rows={6}
|
|
708
765
|
style={{
|
|
@@ -730,7 +787,7 @@ function AiPromptPanelInternal({
|
|
|
730
787
|
gap: "4px",
|
|
731
788
|
}}
|
|
732
789
|
>
|
|
733
|
-
← Back to form
|
|
790
|
+
← {t("prompt.modal.backToFormNoArrow", "Back to form")}
|
|
734
791
|
</button>
|
|
735
792
|
|
|
736
793
|
<div style={{ marginBottom: "12px" }}>
|
|
@@ -753,7 +810,10 @@ function AiPromptPanelInternal({
|
|
|
753
810
|
<input
|
|
754
811
|
value={searchQuery}
|
|
755
812
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
756
|
-
placeholder=
|
|
813
|
+
placeholder={t(
|
|
814
|
+
"prompt.modal.searchPrompts",
|
|
815
|
+
"Search prompts..."
|
|
816
|
+
)}
|
|
757
817
|
style={{
|
|
758
818
|
...aiStyles.input,
|
|
759
819
|
padding: "10px 12px 10px 36px",
|
|
@@ -780,7 +840,7 @@ function AiPromptPanelInternal({
|
|
|
780
840
|
}}
|
|
781
841
|
>
|
|
782
842
|
<Tag size={14} />
|
|
783
|
-
Tags
|
|
843
|
+
{t("prompt.modal.tags", "Tags")}
|
|
784
844
|
</span>
|
|
785
845
|
<button
|
|
786
846
|
onClick={() => setSelectedTag("all")}
|
|
@@ -796,7 +856,7 @@ function AiPromptPanelInternal({
|
|
|
796
856
|
: (aiStyles.chip.background as string),
|
|
797
857
|
}}
|
|
798
858
|
>
|
|
799
|
-
All
|
|
859
|
+
{t("common.all", "All")}
|
|
800
860
|
</button>
|
|
801
861
|
{availableTags.map((tag) => (
|
|
802
862
|
<button
|
|
@@ -822,8 +882,24 @@ function AiPromptPanelInternal({
|
|
|
822
882
|
</div>
|
|
823
883
|
|
|
824
884
|
{promptsLoading ? (
|
|
825
|
-
<div
|
|
826
|
-
|
|
885
|
+
<div
|
|
886
|
+
style={{
|
|
887
|
+
display: "flex",
|
|
888
|
+
flexDirection: "column",
|
|
889
|
+
gap: "12px",
|
|
890
|
+
maxHeight: "400px",
|
|
891
|
+
overflow: "auto",
|
|
892
|
+
}}
|
|
893
|
+
>
|
|
894
|
+
{Array.from({ length: 4 }).map((_, idx) => (
|
|
895
|
+
<div
|
|
896
|
+
key={`prompt-skeleton-${idx}`}
|
|
897
|
+
className="ai-list-skeleton"
|
|
898
|
+
>
|
|
899
|
+
<div className="ai-list-skeleton__line ai-list-skeleton__line--lg" />
|
|
900
|
+
<div className="ai-list-skeleton__line ai-list-skeleton__line--md" />
|
|
901
|
+
</div>
|
|
902
|
+
))}
|
|
827
903
|
</div>
|
|
828
904
|
) : visiblePrompts.length === 0 ? (
|
|
829
905
|
<div
|
|
@@ -833,7 +909,10 @@ function AiPromptPanelInternal({
|
|
|
833
909
|
color: "var(--ai-text-secondary)",
|
|
834
910
|
}}
|
|
835
911
|
>
|
|
836
|
-
|
|
912
|
+
{t(
|
|
913
|
+
"prompt.modal.noPrompts",
|
|
914
|
+
"No prompts available for this model type"
|
|
915
|
+
)}
|
|
837
916
|
</div>
|
|
838
917
|
) : (
|
|
839
918
|
<div
|
|
@@ -856,7 +935,7 @@ function AiPromptPanelInternal({
|
|
|
856
935
|
marginBottom: "8px",
|
|
857
936
|
}}
|
|
858
937
|
>
|
|
859
|
-
Favorites
|
|
938
|
+
{t("prompt.modal.favorites", "Favorites")}
|
|
860
939
|
</div>
|
|
861
940
|
<div
|
|
862
941
|
style={{
|
|
@@ -944,7 +1023,7 @@ function AiPromptPanelInternal({
|
|
|
944
1023
|
marginBottom: "8px",
|
|
945
1024
|
}}
|
|
946
1025
|
>
|
|
947
|
-
All prompts
|
|
1026
|
+
{t("prompt.modal.allPrompts", "All prompts")}
|
|
948
1027
|
</div>
|
|
949
1028
|
<div
|
|
950
1029
|
style={{
|
|
@@ -1031,14 +1110,34 @@ function AiPromptPanelInternal({
|
|
|
1031
1110
|
}}
|
|
1032
1111
|
>
|
|
1033
1112
|
<button onClick={handleClose} className="ai-btn ai-btn--ghost">
|
|
1034
|
-
Cancel
|
|
1113
|
+
{t("prompt.modal.cancel", "Cancel")}
|
|
1035
1114
|
</button>
|
|
1036
1115
|
<button
|
|
1037
1116
|
onClick={handleSubmit}
|
|
1038
|
-
disabled={!selectedModel || !prompt.trim()}
|
|
1117
|
+
disabled={isGenerating || !selectedModel || !prompt.trim()}
|
|
1039
1118
|
className="ai-btn ai-btn--primary"
|
|
1040
1119
|
>
|
|
1041
|
-
{
|
|
1120
|
+
{isGenerating ? (
|
|
1121
|
+
<span className="ai-loading-stack">
|
|
1122
|
+
<span className="ai-loading-row">
|
|
1123
|
+
<Loader2 size={16} className="ai-spinner" />
|
|
1124
|
+
<span>
|
|
1125
|
+
{sourceText
|
|
1126
|
+
? t("prompt.modal.transforming", "Transforming...")
|
|
1127
|
+
: t("prompt.modal.generating", "Generating...")}
|
|
1128
|
+
</span>
|
|
1129
|
+
</span>
|
|
1130
|
+
<span className="ai-loading-meta">
|
|
1131
|
+
{t("ai.loading.elapsed", "{seconds}", {
|
|
1132
|
+
seconds: loadingElapsed,
|
|
1133
|
+
})}
|
|
1134
|
+
</span>
|
|
1135
|
+
</span>
|
|
1136
|
+
) : sourceText ? (
|
|
1137
|
+
t("prompt.modal.transform", "Transform with AI")
|
|
1138
|
+
) : (
|
|
1139
|
+
t("prompt.modal.generate", "Generate with AI")
|
|
1140
|
+
)}
|
|
1042
1141
|
</button>
|
|
1043
1142
|
</div>
|
|
1044
1143
|
</div>
|
|
@@ -1073,14 +1172,16 @@ function AiPromptPanelInternal({
|
|
|
1073
1172
|
}}
|
|
1074
1173
|
>
|
|
1075
1174
|
<div style={aiStyles.modalHeader}>
|
|
1076
|
-
<h2 style={aiStyles.modalTitle}>
|
|
1175
|
+
<h2 style={aiStyles.modalTitle}>
|
|
1176
|
+
{t("prompt.modal.modelMgmtTitle", "AI Model Management")}
|
|
1177
|
+
</h2>
|
|
1077
1178
|
<button
|
|
1078
1179
|
style={aiStyles.modalCloseButton}
|
|
1079
1180
|
onClick={() => {
|
|
1080
1181
|
setIsModelManagementOpen(false);
|
|
1081
1182
|
setModelSearchQuery("");
|
|
1082
1183
|
}}
|
|
1083
|
-
aria-label="Close"
|
|
1184
|
+
aria-label={t("common.closeLabel", "Close")}
|
|
1084
1185
|
>
|
|
1085
1186
|
×
|
|
1086
1187
|
</button>
|
|
@@ -1101,7 +1202,10 @@ function AiPromptPanelInternal({
|
|
|
1101
1202
|
margin: "0 0 8px 0",
|
|
1102
1203
|
}}
|
|
1103
1204
|
>
|
|
1104
|
-
|
|
1205
|
+
{t(
|
|
1206
|
+
"prompt.modal.modelMgmtSubtitle",
|
|
1207
|
+
"Enable or disable models based on your needs"
|
|
1208
|
+
)}
|
|
1105
1209
|
</p>
|
|
1106
1210
|
<p
|
|
1107
1211
|
style={{
|
|
@@ -1110,20 +1214,20 @@ function AiPromptPanelInternal({
|
|
|
1110
1214
|
margin: "0 0 16px 0",
|
|
1111
1215
|
}}
|
|
1112
1216
|
>
|
|
1113
|
-
{
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1217
|
+
{t(
|
|
1218
|
+
"prompt.modal.modelAvailableCount",
|
|
1219
|
+
"{available} models available • {active} enabled",
|
|
1220
|
+
{
|
|
1221
|
+
available: effectiveAvailableModels.filter(
|
|
1222
|
+
(m) => m.category === modelCategory
|
|
1223
|
+
).length,
|
|
1224
|
+
active: effectiveUserModels.filter((id) =>
|
|
1225
|
+
effectiveAvailableModels.some(
|
|
1226
|
+
(m) => m.id === id && m.category === modelCategory
|
|
1227
|
+
)
|
|
1228
|
+
).length,
|
|
1229
|
+
}
|
|
1230
|
+
)}
|
|
1127
1231
|
</p>
|
|
1128
1232
|
{/* Champ de recherche */}
|
|
1129
1233
|
<div
|
|
@@ -1145,7 +1249,10 @@ function AiPromptPanelInternal({
|
|
|
1145
1249
|
<input
|
|
1146
1250
|
value={modelSearchQuery}
|
|
1147
1251
|
onChange={(e) => setModelSearchQuery(e.target.value)}
|
|
1148
|
-
placeholder=
|
|
1252
|
+
placeholder={t(
|
|
1253
|
+
"prompt.modal.searchModel",
|
|
1254
|
+
"Search a model..."
|
|
1255
|
+
)}
|
|
1149
1256
|
style={{
|
|
1150
1257
|
...aiStyles.input,
|
|
1151
1258
|
padding: "10px 12px 10px 36px",
|
|
@@ -1156,6 +1263,19 @@ function AiPromptPanelInternal({
|
|
|
1156
1263
|
</div>
|
|
1157
1264
|
|
|
1158
1265
|
<div className="ai-model-mgmt-list">
|
|
1266
|
+
{isModelsLoading ? (
|
|
1267
|
+
<>
|
|
1268
|
+
{Array.from({ length: 4 }).map((_, idx) => (
|
|
1269
|
+
<div
|
|
1270
|
+
key={`model-skeleton-${idx}`}
|
|
1271
|
+
className="ai-list-skeleton"
|
|
1272
|
+
>
|
|
1273
|
+
<div className="ai-list-skeleton__line ai-list-skeleton__line--lg" />
|
|
1274
|
+
<div className="ai-list-skeleton__line ai-list-skeleton__line--md" />
|
|
1275
|
+
</div>
|
|
1276
|
+
))}
|
|
1277
|
+
</>
|
|
1278
|
+
) : null}
|
|
1159
1279
|
{effectiveAvailableModels
|
|
1160
1280
|
.filter((model) => {
|
|
1161
1281
|
if (model.category !== modelCategory) return false;
|
|
@@ -1253,29 +1373,36 @@ function AiPromptPanelInternal({
|
|
|
1253
1373
|
</div>
|
|
1254
1374
|
);
|
|
1255
1375
|
})}
|
|
1256
|
-
{
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1376
|
+
{!isModelsLoading &&
|
|
1377
|
+
effectiveAvailableModels.filter((model) => {
|
|
1378
|
+
if (model.category !== modelCategory) return false;
|
|
1379
|
+
if (!modelSearchQuery.trim()) return true;
|
|
1380
|
+
const query = modelSearchQuery.toLowerCase();
|
|
1381
|
+
return (
|
|
1382
|
+
model.name.toLowerCase().includes(query) ||
|
|
1383
|
+
model.provider.toLowerCase().includes(query) ||
|
|
1384
|
+
model.description?.toLowerCase().includes(query)
|
|
1385
|
+
);
|
|
1386
|
+
}).length === 0 && (
|
|
1387
|
+
<div
|
|
1388
|
+
style={{
|
|
1389
|
+
textAlign: "center",
|
|
1390
|
+
padding: "32px 16px",
|
|
1391
|
+
color: "var(--ai-text-tertiary)",
|
|
1392
|
+
fontSize: "14px",
|
|
1393
|
+
}}
|
|
1394
|
+
>
|
|
1395
|
+
{modelSearchQuery.trim()
|
|
1396
|
+
? t(
|
|
1397
|
+
"prompt.modal.noModelMatch",
|
|
1398
|
+
"No model matches your search"
|
|
1399
|
+
)
|
|
1400
|
+
: t(
|
|
1401
|
+
"prompt.modal.noModelAvailable",
|
|
1402
|
+
"No models available"
|
|
1403
|
+
)}
|
|
1404
|
+
</div>
|
|
1405
|
+
)}
|
|
1279
1406
|
</div>
|
|
1280
1407
|
</div>
|
|
1281
1408
|
|
|
@@ -1287,7 +1414,7 @@ function AiPromptPanelInternal({
|
|
|
1287
1414
|
}}
|
|
1288
1415
|
className="ai-btn ai-btn--ghost"
|
|
1289
1416
|
>
|
|
1290
|
-
|
|
1417
|
+
{t("prompt.modal.closeModelMgmt", "Close")}
|
|
1291
1418
|
</button>
|
|
1292
1419
|
</div>
|
|
1293
1420
|
</div>
|
|
@@ -12,6 +12,7 @@ import { LBSigninModal } from "./LBSigninModal";
|
|
|
12
12
|
import { handleAIError } from "../utils/errorHandler";
|
|
13
13
|
import { useLB } from "../context/LBAuthProvider";
|
|
14
14
|
import { useAiContext } from "../context/AiProvider";
|
|
15
|
+
import { useI18n } from "../context/I18nContext";
|
|
15
16
|
|
|
16
17
|
export interface AiSelectProps
|
|
17
18
|
extends
|
|
@@ -41,6 +42,7 @@ export function AiSelect({
|
|
|
41
42
|
children,
|
|
42
43
|
...selectProps
|
|
43
44
|
}: AiSelectProps) {
|
|
45
|
+
const { t } = useI18n();
|
|
44
46
|
const [isOpen, setIsOpen] = useState(false);
|
|
45
47
|
const [showAuthModal, setShowAuthModal] = useState(false);
|
|
46
48
|
const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
|
|
@@ -107,7 +109,10 @@ export function AiSelect({
|
|
|
107
109
|
|
|
108
110
|
if (result.text) {
|
|
109
111
|
onValue?.(result.text);
|
|
110
|
-
onToast?.({
|
|
112
|
+
onToast?.({
|
|
113
|
+
type: "success",
|
|
114
|
+
message: t("ai.select.suggestionReady", "AI suggestion ready"),
|
|
115
|
+
});
|
|
111
116
|
showUsageToast(result);
|
|
112
117
|
}
|
|
113
118
|
} catch (error) {
|
|
@@ -140,8 +145,8 @@ export function AiSelect({
|
|
|
140
145
|
onClick={handleOpenPanel}
|
|
141
146
|
title={
|
|
142
147
|
shouldShowSparkles
|
|
143
|
-
? "
|
|
144
|
-
: "
|
|
148
|
+
? t("ai.generate", "Generate with AI")
|
|
149
|
+
: t("auth.connectToUseAi", "Sign in to use AI")
|
|
145
150
|
}
|
|
146
151
|
disabled={disabled || loading}
|
|
147
152
|
type="button"
|