@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.
Files changed (83) hide show
  1. package/dist/components/AiChipLabel.d.ts.map +1 -1
  2. package/dist/components/AiChipLabel.js +10 -7
  3. package/dist/components/AiContextButton.d.ts +1 -1
  4. package/dist/components/AiContextButton.d.ts.map +1 -1
  5. package/dist/components/AiContextButton.js +26 -12
  6. package/dist/components/AiImageButton.d.ts.map +1 -1
  7. package/dist/components/AiImageButton.js +35 -16
  8. package/dist/components/AiInput.d.ts.map +1 -1
  9. package/dist/components/AiInput.js +15 -5
  10. package/dist/components/AiModelSelect.d.ts.map +1 -1
  11. package/dist/components/AiModelSelect.js +3 -1
  12. package/dist/components/AiPromptPanel.d.ts.map +1 -1
  13. package/dist/components/AiPromptPanel.js +72 -47
  14. package/dist/components/AiSelect.d.ts.map +1 -1
  15. package/dist/components/AiSelect.js +8 -3
  16. package/dist/components/AiStatusButton.d.ts.map +1 -1
  17. package/dist/components/AiStatusButton.js +23 -20
  18. package/dist/components/AiTextarea.d.ts.map +1 -1
  19. package/dist/components/AiTextarea.js +19 -6
  20. package/dist/components/ErrorToast.d.ts.map +1 -1
  21. package/dist/components/ErrorToast.js +4 -2
  22. package/dist/components/LBApiKeySelector.d.ts.map +1 -1
  23. package/dist/components/LBApiKeySelector.js +13 -5
  24. package/dist/components/LBConnectButton.d.ts.map +1 -1
  25. package/dist/components/LBConnectButton.js +6 -3
  26. package/dist/components/LBKeyPicker.d.ts.map +1 -1
  27. package/dist/components/LBKeyPicker.js +8 -4
  28. package/dist/components/LBSigninModal.d.ts.map +1 -1
  29. package/dist/components/LBSigninModal.js +13 -7
  30. package/dist/components/UsageToast.d.ts.map +1 -1
  31. package/dist/components/UsageToast.js +4 -2
  32. package/dist/context/I18nContext.d.ts +15 -0
  33. package/dist/context/I18nContext.d.ts.map +1 -0
  34. package/dist/context/I18nContext.js +44 -0
  35. package/dist/context/LBAuthProvider.d.ts +4 -1
  36. package/dist/context/LBAuthProvider.d.ts.map +1 -1
  37. package/dist/context/LBAuthProvider.js +3 -2
  38. package/dist/hooks/useAiCallImage.d.ts.map +1 -1
  39. package/dist/hooks/useAiCallImage.js +1 -107
  40. package/dist/hooks/useAiCallText.d.ts.map +1 -1
  41. package/dist/hooks/useAiCallText.js +1 -25
  42. package/dist/hooks/useLoadingTimer.d.ts +5 -0
  43. package/dist/hooks/useLoadingTimer.d.ts.map +1 -0
  44. package/dist/hooks/useLoadingTimer.js +31 -0
  45. package/dist/i18n/de.json +62 -0
  46. package/dist/i18n/en.json +128 -0
  47. package/dist/i18n/es.json +70 -0
  48. package/dist/i18n/fr.json +128 -0
  49. package/dist/i18n/it.json +62 -0
  50. package/dist/i18n/pt.json +62 -0
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +2 -0
  54. package/dist/styles.css +141 -1
  55. package/package.json +3 -3
  56. package/src/components/AiChipLabel.tsx +14 -8
  57. package/src/components/AiContextButton.tsx +53 -20
  58. package/src/components/AiImageButton.tsx +58 -25
  59. package/src/components/AiInput.tsx +20 -5
  60. package/src/components/AiModelSelect.tsx +5 -1
  61. package/src/components/AiPromptPanel.tsx +203 -76
  62. package/src/components/AiSelect.tsx +8 -3
  63. package/src/components/AiStatusButton.tsx +75 -46
  64. package/src/components/AiTextarea.tsx +24 -6
  65. package/src/components/ErrorToast.tsx +4 -2
  66. package/src/components/LBApiKeySelector.tsx +29 -9
  67. package/src/components/LBConnectButton.tsx +7 -3
  68. package/src/components/LBKeyPicker.tsx +10 -4
  69. package/src/components/LBSigninModal.tsx +33 -15
  70. package/src/components/UsageToast.tsx +4 -2
  71. package/src/context/I18nContext.tsx +75 -0
  72. package/src/context/LBAuthProvider.tsx +9 -1
  73. package/src/hooks/useAiCallImage.ts +1 -149
  74. package/src/hooks/useAiCallText.ts +1 -30
  75. package/src/hooks/useLoadingTimer.ts +38 -0
  76. package/src/i18n/de.json +62 -0
  77. package/src/i18n/en.json +128 -0
  78. package/src/i18n/es.json +70 -0
  79. package/src/i18n/fr.json +128 -0
  80. package/src/i18n/it.json +62 -0
  81. package/src/i18n/pt.json +62 -0
  82. package/src/index.ts +2 -0
  83. 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?.({ type: "success", message: "AI generation successful" });
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?.({ type: "success", message: "AI generation successful" });
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="">Select a model</option>
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 { BookOpen, Search, Sparkles, Star, Tag, Settings } from "lucide-react";
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
- Génération en cours…
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 ? "Select a Prompt" : "AI Prompt Configuration"}
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}>Source Text</label>
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
- {effectiveAvailableModels.length > 0 && (
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="Gérer les modèles"
581
+ title={t(
582
+ "prompt.modal.manageModels",
583
+ "Manage models"
584
+ )}
554
585
  >
555
586
  <Settings size={14} />
556
- Gérer les modèles
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
- ? "No active models. Open 'Gérer les modèles'."
601
- : "Loading models..."}
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 && " (Désactivé)"}
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
- 💡 Cliquez sur "⚙️" pour activer/désactiver les modèles
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="">Loading models...</option>
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
- (Cmd/Ctrl + Enter to submit)
727
+ {t(
728
+ "prompt.modal.promptHint",
729
+ "(Cmd/Ctrl + Enter to submit)"
730
+ )}
683
731
  </span>
684
732
  </label>
685
- {filteredPrompts.length > 0 && (
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 ({filteredPrompts.length})
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
- ? "Enter your AI prompt... e.g., 'Correct spelling and grammar', 'Make it more professional', 'Translate to English'"
705
- : "Enter your AI prompt... e.g., 'Write a blog post about AI', 'Generate product description'"
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="Search prompts..."
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 style={{ textAlign: "center", padding: "40px 0" }}>
826
- Loading prompts...
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
- No prompts available for this model type
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
- {sourceText ? "Transform with AI" : "Generate with AI"}
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}>Gestion des modèles IA</h2>
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
- Activez ou désactivez les modèles selon vos besoins
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
- effectiveAvailableModels.filter(
1115
- (m) => m.category === modelCategory
1116
- ).length
1117
- }{" "}
1118
- modèles disponibles •{" "}
1119
- {
1120
- effectiveUserModels.filter((id) =>
1121
- effectiveAvailableModels.some(
1122
- (m) => m.id === id && m.category === modelCategory
1123
- )
1124
- ).length
1125
- }{" "}
1126
- activés
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="Rechercher un modèle..."
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
- {effectiveAvailableModels.filter((model) => {
1257
- if (model.category !== modelCategory) return false;
1258
- if (!modelSearchQuery.trim()) return true;
1259
- const query = modelSearchQuery.toLowerCase();
1260
- return (
1261
- model.name.toLowerCase().includes(query) ||
1262
- model.provider.toLowerCase().includes(query) ||
1263
- model.description?.toLowerCase().includes(query)
1264
- );
1265
- }).length === 0 && (
1266
- <div
1267
- style={{
1268
- textAlign: "center",
1269
- padding: "32px 16px",
1270
- color: "var(--ai-text-tertiary)",
1271
- fontSize: "14px",
1272
- }}
1273
- >
1274
- {modelSearchQuery.trim()
1275
- ? "Aucun modèle ne correspond à votre recherche"
1276
- : "Aucun modèle disponible"}
1277
- </div>
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
- Fermer
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?.({ type: "success", message: "AI suggestion ready" });
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
- ? "Générer avec l'IA"
144
- : "Se connecter pour utiliser l'IA"
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"