@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
package/dist/styles.css CHANGED
@@ -122,7 +122,7 @@
122
122
  /* Base surface */
123
123
  .ai-surface {
124
124
  border: 1px solid var(--ai-border);
125
- border-radius: var(--ai-radius-current, var(--ai-radius-md));
125
+ border-radius: 6px;
126
126
  color: var(--ai-foreground);
127
127
  background:
128
128
  linear-gradient(180deg, var(--ai-bg2), var(--ai-bg2)),
@@ -601,6 +601,14 @@
601
601
  margin-bottom: 10px;
602
602
  }
603
603
 
604
+ .ai-popover-header--sub {
605
+ font-size: 12px;
606
+ font-weight: 700;
607
+ letter-spacing: 0.01em;
608
+ margin-bottom: 8px;
609
+ color: var(--ai-foreground);
610
+ }
611
+
604
612
  .ai-popover-section {
605
613
  padding: 12px 0;
606
614
  border-top: 1px solid color-mix(in srgb, var(--ai-border) 72%, transparent);
@@ -648,6 +656,7 @@
648
656
  }
649
657
 
650
658
  .ai-status-action-btn {
659
+ position: relative;
651
660
  border: 1px solid color-mix(in srgb, var(--ai-primary) 34%, var(--ai-border));
652
661
  border-radius: 12px;
653
662
  width: 34px;
@@ -667,6 +676,34 @@
667
676
  transform: translateY(-1px);
668
677
  }
669
678
 
679
+ .ai-status-action-btn[data-ai-tip]::after {
680
+ content: attr(data-ai-tip);
681
+ position: absolute;
682
+ left: 50%;
683
+ bottom: calc(100% + 8px);
684
+ transform: translate(-50%, 4px);
685
+ white-space: nowrap;
686
+ opacity: 0;
687
+ pointer-events: none;
688
+ z-index: 2147483647;
689
+ padding: 4px 8px;
690
+ border-radius: 8px;
691
+ border: 1px solid var(--ai-border);
692
+ background: var(--ai-bg2);
693
+ color: var(--ai-foreground);
694
+ font-size: 11px;
695
+ font-weight: 600;
696
+ box-shadow: var(--ai-shadow-sm);
697
+ transition:
698
+ opacity var(--ai-transition),
699
+ transform var(--ai-transition);
700
+ }
701
+
702
+ .ai-status-action-btn[data-ai-tip]:hover::after {
703
+ opacity: 1;
704
+ transform: translate(-50%, 0);
705
+ }
706
+
670
707
  .ai-status-trigger {
671
708
  position: relative;
672
709
  border: 1px solid var(--ai-border);
@@ -726,6 +763,7 @@
726
763
  display: inline-flex;
727
764
  align-items: center;
728
765
  justify-content: center;
766
+ color: var(--ai-primary);
729
767
  pointer-events: none;
730
768
  }
731
769
 
@@ -1145,6 +1183,12 @@
1145
1183
  background: color-mix(in srgb, var(--ai-bg2) 82%, transparent);
1146
1184
  }
1147
1185
 
1186
+ .ai-pill--warning {
1187
+ color: #f59e0b;
1188
+ border-color: color-mix(in srgb, #f59e0b 40%, var(--ai-border));
1189
+ background: color-mix(in srgb, #f59e0b 14%, var(--ai-bg2));
1190
+ }
1191
+
1148
1192
  .ai-toggle {
1149
1193
  position: relative;
1150
1194
  display: inline-flex;
@@ -1369,6 +1413,41 @@
1369
1413
  letter-spacing: 0.01em;
1370
1414
  }
1371
1415
 
1416
+ .ai-control-timer {
1417
+ position: absolute;
1418
+ right: 10px;
1419
+ top: calc(100% + 5px);
1420
+ font-size: 11px;
1421
+ line-height: 1;
1422
+ color: var(--ai-tertiary);
1423
+ pointer-events: none;
1424
+ z-index: 2;
1425
+ }
1426
+
1427
+ .ai-loading-stack {
1428
+ display: inline-flex;
1429
+ flex-direction: column;
1430
+ align-items: center;
1431
+ justify-content: center;
1432
+ gap: 2px;
1433
+ }
1434
+
1435
+ .ai-loading-row {
1436
+ display: inline-flex;
1437
+ align-items: center;
1438
+ justify-content: center;
1439
+ gap: 8px;
1440
+ }
1441
+
1442
+ .ai-loading-meta {
1443
+ display: inline-flex;
1444
+ align-items: center;
1445
+ justify-content: center;
1446
+ font-size: 11px;
1447
+ line-height: 1;
1448
+ color: color-mix(in srgb, var(--ai-foreground) 68%, transparent);
1449
+ }
1450
+
1372
1451
  .ai-inline-icon {
1373
1452
  color: var(--ai-muted);
1374
1453
  }
@@ -1380,6 +1459,67 @@
1380
1459
  animation: ai-pulse 1.4s ease-in-out infinite;
1381
1460
  }
1382
1461
 
1462
+ .ai-kv-skeleton--48 {
1463
+ width: 48px;
1464
+ }
1465
+
1466
+ .ai-kv-skeleton--92 {
1467
+ width: 92px;
1468
+ }
1469
+
1470
+ .ai-kv-skeleton--110 {
1471
+ width: 110px;
1472
+ }
1473
+
1474
+ .ai-kv-skeleton--120 {
1475
+ width: 120px;
1476
+ }
1477
+
1478
+ .ai-kv-skeleton--circle {
1479
+ width: 28px;
1480
+ height: 28px;
1481
+ border-radius: 999px;
1482
+ }
1483
+
1484
+ .ai-inline-skeleton {
1485
+ display: inline-flex;
1486
+ align-items: center;
1487
+ min-width: 124px;
1488
+ height: 30px;
1489
+ border-radius: 10px;
1490
+ background: color-mix(in srgb, var(--ai-primary) 12%, var(--ai-bg2));
1491
+ animation: ai-pulse 1.4s ease-in-out infinite;
1492
+ }
1493
+
1494
+ .ai-list-skeleton {
1495
+ border: 1px solid var(--ai-border);
1496
+ border-radius: 12px;
1497
+ background:
1498
+ linear-gradient(180deg, var(--ai-bg2), var(--ai-bg2)),
1499
+ radial-gradient(
1500
+ 120% 120% at 100% 0%,
1501
+ rgba(var(--ai-glow-rgb), 0.05),
1502
+ transparent 76%
1503
+ );
1504
+ padding: 14px 16px;
1505
+ }
1506
+
1507
+ .ai-list-skeleton__line {
1508
+ height: 12px;
1509
+ border-radius: 6px;
1510
+ background: color-mix(in srgb, var(--ai-primary) 14%, var(--ai-bg2));
1511
+ animation: ai-pulse 1.4s ease-in-out infinite;
1512
+ }
1513
+
1514
+ .ai-list-skeleton__line--lg {
1515
+ width: 56%;
1516
+ margin-bottom: 10px;
1517
+ }
1518
+
1519
+ .ai-list-skeleton__line--md {
1520
+ width: 34%;
1521
+ }
1522
+
1383
1523
  .ai-spinner,
1384
1524
  .ai-spin {
1385
1525
  animation: ai-spin 1s linear infinite;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/ai-ui-react",
3
- "version": "1.0.73",
3
+ "version": "1.0.75",
4
4
  "description": "Headless React components for LastBrain AI UI Kit",
5
5
  "private": false,
6
6
  "type": "module",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "lucide-react": "^0.257.0",
54
- "@lastbrain/ai-ui-core": "1.0.53"
54
+ "@lastbrain/ai-ui-core": "1.0.54"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/react": "^19.2.0",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "scripts": {
64
64
  "dev": "tsc -p tsconfig.json --watch",
65
- "build": "tsc -p tsconfig.json && cp src/styles.css dist/styles.css && cp styles.d.ts dist/styles.d.ts",
65
+ "build": "tsc -p tsconfig.json && mkdir -p dist/i18n && cp src/i18n/*.json dist/i18n/ && cp src/styles.css dist/styles.css && cp styles.d.ts dist/styles.d.ts",
66
66
  "lint": "eslint ."
67
67
  }
68
68
  }
@@ -11,6 +11,7 @@ import { useAiContext } from "../context/AiProvider";
11
11
  import { useLB } from "../context/LBAuthProvider";
12
12
  import { handleAIError } from "../utils/errorHandler";
13
13
  import type { AiRadius, AiSize } from "../types";
14
+ import { useI18n } from "../context/I18nContext";
14
15
 
15
16
  export interface AiChipLabelProps {
16
17
  children: React.ReactNode;
@@ -67,7 +68,7 @@ export interface AiChipInputProps {
67
68
  export function AiChipInput({
68
69
  value = [],
69
70
  onChange,
70
- placeholder = "Tapez et appuyez sur Entrée pour ajouter des tags...",
71
+ placeholder,
71
72
  context,
72
73
  maxChips,
73
74
  allowDuplicates = false,
@@ -79,6 +80,7 @@ export function AiChipInput({
79
80
  size = "md",
80
81
  radius = "full",
81
82
  }: AiChipInputProps) {
83
+ const { t } = useI18n();
82
84
  const [inputValue, setInputValue] = useState("");
83
85
  const [showPromptPanel, setShowPromptPanel] = useState(false);
84
86
  const [showSigninModal, setShowSigninModal] = useState(false);
@@ -156,7 +158,8 @@ Exemple de réponse attendue: javascript, react, frontend, api, development`;
156
158
  model,
157
159
  prompt: instruction,
158
160
  storeOutputs,
159
- artifactTitle: artifactTitle || "Tags générés par IA",
161
+ artifactTitle:
162
+ artifactTitle || t("ai.chips.artifactTitle", "AI generated tags"),
160
163
  });
161
164
 
162
165
  const chips = parseChipsFromResponse(response.text);
@@ -207,7 +210,10 @@ Exemple de réponse attendue: javascript, react, frontend, api, development`;
207
210
  value={inputValue}
208
211
  onChange={(e) => setInputValue(e.target.value)}
209
212
  onKeyDown={handleKeyDown}
210
- placeholder={placeholder}
213
+ placeholder={
214
+ placeholder ||
215
+ t("ai.chips.placeholder", "Type and press Enter to add tags...")
216
+ }
211
217
  className={`ai-control ai-control-input ai-control-input--with-action ${sizeClass} ${radiusClass}`}
212
218
  />
213
219
  <button
@@ -221,13 +227,13 @@ Exemple de réponse attendue: javascript, react, frontend, api, development`;
221
227
  className={`ai-control-action ai-spark ${sizeClass} ${radiusClass}`}
222
228
  aria-label={
223
229
  isAuthenticated
224
- ? "Générer des tags avec l'IA"
225
- : "Connexion requise"
230
+ ? t("ai.chips.generate", "Generate tags with AI")
231
+ : t("auth.connectRequired", "Connection required")
226
232
  }
227
233
  title={
228
234
  isAuthenticated
229
- ? "Générer des tags avec l'IA"
230
- : "Se connecter pour utiliser l'IA"
235
+ ? t("ai.chips.generate", "Generate tags with AI")
236
+ : t("auth.connectToUseAi", "Sign in to use AI")
231
237
  }
232
238
  >
233
239
  {isAuthenticated ? <Sparkles size={16} /> : <Lock size={16} />}
@@ -247,7 +253,7 @@ Exemple de réponse attendue: javascript, react, frontend, api, development`;
247
253
  <button
248
254
  onClick={() => removeChip(index)}
249
255
  className="ai-chip-remover"
250
- title="Supprimer"
256
+ title={t("ai.chips.remove", "Remove")}
251
257
  >
252
258
  <X size={14} />
253
259
  </button>
@@ -13,6 +13,7 @@ import { useAiContext } from "../context/AiProvider";
13
13
  import { handleAIError } from "../utils/errorHandler";
14
14
  import { useLB } from "../context/LBAuthProvider";
15
15
  import { LBSigninModal } from "./LBSigninModal";
16
+ import { useI18n } from "../context/I18nContext";
16
17
 
17
18
  // Types pour les données de contexte
18
19
  type ContextData =
@@ -49,19 +50,22 @@ export function AiContextButton({
49
50
  apiKeyId: propApiKeyId,
50
51
  uiMode = "modal",
51
52
  contextData,
52
- contextDescription = "Données à analyser",
53
+ contextDescription,
53
54
  onResult,
54
55
  onToast,
55
56
  disabled,
56
57
  className,
57
58
  children,
58
- resultModalTitle = "Résultat de l'analyse",
59
+ resultModalTitle,
60
+ storeOutputs,
61
+ artifactTitle,
59
62
  size = "md",
60
63
  radius = "full",
61
64
  variant = "default",
62
65
  context: _context,
63
66
  ...buttonProps
64
67
  }: AiContextButtonProps) {
68
+ const { t, lang } = useI18n();
65
69
  const [isOpen, setIsOpen] = useState(false);
66
70
  const [showAuthModal, setShowAuthModal] = useState(false);
67
71
  const [isResultOpen, setIsResultOpen] = useState(false);
@@ -75,6 +79,8 @@ export function AiContextButton({
75
79
 
76
80
  const { showUsageToast } = useUsageToast();
77
81
  const { showErrorToast, errorData, errorKey, clearError } = useErrorToast();
82
+ const resolvedContextDescription =
83
+ contextDescription || t("ai.context.description", "Data to analyze");
78
84
 
79
85
  let lbStatus: string | undefined;
80
86
  try {
@@ -119,7 +125,7 @@ export function AiContextButton({
119
125
  ) => {
120
126
  try {
121
127
  const contextString = formatContextData(contextData);
122
- const fullPrompt = `${selectedPrompt}\n\nCONTEXTE (${contextDescription}):\n${contextString}\n\nAnalyse ces données et réponds de manière structurée et claire.`;
128
+ const fullPrompt = `${selectedPrompt}\n\nCONTEXTE (${resolvedContextDescription}):\n${contextString}\n\n${t("ai.context.analyzeStructured", "Analyze this data and respond in a clear, structured way.")}`;
123
129
 
124
130
  const result = await callText({
125
131
  prompt: fullPrompt,
@@ -127,6 +133,8 @@ export function AiContextButton({
127
133
  context: _context || undefined,
128
134
  maxTokens: 4000,
129
135
  temperature: 0.7,
136
+ storeOutputs,
137
+ artifactTitle,
130
138
  });
131
139
 
132
140
  if (!result.text) {
@@ -156,7 +164,9 @@ export function AiContextButton({
156
164
 
157
165
  onToast?.({
158
166
  type: "success",
159
- message: `Analyse terminée - Coût: $${(apiKeyId?.includes("dev") ? 0 : actualCost).toFixed(6)}`,
167
+ message: t("ai.analysisDoneCost", "Analysis completed - Cost: {cost}", {
168
+ cost: `$${(apiKeyId?.includes("dev") ? 0 : actualCost).toFixed(6)}`,
169
+ }),
160
170
  });
161
171
 
162
172
  showUsageToast({
@@ -182,18 +192,25 @@ export function AiContextButton({
182
192
  return;
183
193
  }
184
194
 
195
+ const locale = lang === "fr" ? "fr-FR" : "en-US";
185
196
  const currentDate = new Date()
186
- .toLocaleDateString("fr-FR")
197
+ .toLocaleDateString(locale)
187
198
  .replace(/\//g, "-");
188
- const defaultName = `analyse-${currentDate}.txt`;
189
- const fileName = prompt("Nom du fichier :", defaultName) || defaultName;
199
+ const defaultName = t("ai.context.fileNameBase", "analysis-{date}.txt", {
200
+ date: currentDate,
201
+ });
202
+ const fileName =
203
+ prompt(t("ai.context.saveFileNamePrompt", "File name:"), defaultName) ||
204
+ defaultName;
190
205
 
191
- const content = `ANALYSE DES DONNÉES - ${new Date().toLocaleString("fr-FR")}\n\nPROMPT UTILISÉ :\n${analysisResult.prompt}\n\nRÉSULTAT DE L'ANALYSE :\n${analysisResult.content}\n\n--- MÉTADONNÉES ---\nTokens utilisés: ${analysisResult.tokens.toLocaleString()}\nCoût: $${(apiKeyId?.includes(
206
+ const content = `${t("ai.context.fileHeader", "DATA ANALYSIS")} - ${new Date().toLocaleString(locale)}\n\n${t("common.promptUsed", "Prompt used").toUpperCase()} :\n${analysisResult.prompt}\n\n${t("common.result", "Result").toUpperCase()} :\n${analysisResult.content}\n\n--- ${t("ai.context.metadata", "METADATA")} ---\n${t("ai.context.tokensUsed", "Tokens used")}: ${analysisResult.tokens.toLocaleString()}\n${t("ai.context.cost", "Cost")}: $${(apiKeyId?.includes(
192
207
  "dev"
193
208
  )
194
209
  ? 0
195
210
  : analysisResult.cost
196
- ).toFixed(6)}\nID de requête: ${analysisResult.requestId || "N/A"}`;
211
+ ).toFixed(
212
+ 6
213
+ )}\n${t("ai.context.requestId", "Request ID")}: ${analysisResult.requestId || "N/A"}`;
197
214
 
198
215
  const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
199
216
  const url = URL.createObjectURL(blob);
@@ -219,23 +236,29 @@ export function AiContextButton({
219
236
  disabled={disabled || loading || !isAuthReady}
220
237
  className={`ai-btn ai-context-btn ${variantClass} ${sizeClass} ${radiusClass} ${className || ""}`}
221
238
  title={
222
- !isAuthReady ? "Authentication required" : "Analyser avec l'IA"
239
+ !isAuthReady
240
+ ? t("auth.required", "Authentication required")
241
+ : t("ai.analyze", "Analyze with AI")
223
242
  }
224
243
  >
225
244
  {loading ? (
226
245
  <>
227
246
  <Loader2 size={16} className="ai-spinner" />
228
- <span>Analyse...</span>
247
+ <span>{t("ai.analyzing", "Analyzing...")}</span>
229
248
  </>
230
249
  ) : !isAuthReady ? (
231
250
  <>
232
251
  <Lock size={16} />
233
- {children || <span>Connexion requise</span>}
252
+ {children || (
253
+ <span>{t("auth.connectRequired", "Connection required")}</span>
254
+ )}
234
255
  </>
235
256
  ) : (
236
257
  <>
237
258
  <Sparkles size={16} />
238
- {children || <span>Analyser</span>}
259
+ {children || (
260
+ <span>{t("ai.context.buttonAnalyze", "Analyze")}</span>
261
+ )}
239
262
  </>
240
263
  )}
241
264
  </button>
@@ -272,7 +295,10 @@ export function AiContextButton({
272
295
  <div className="ai-result-header">
273
296
  <div className="ai-row">
274
297
  <FileText size={18} />
275
- <h2 className="ai-result-title">{resultModalTitle}</h2>
298
+ <h2 className="ai-result-title">
299
+ {resultModalTitle ||
300
+ t("ai.context.resultTitle", "Analysis result")}
301
+ </h2>
276
302
  </div>
277
303
  <div className="ai-row">
278
304
  <button
@@ -281,7 +307,7 @@ export function AiContextButton({
281
307
  onClick={saveToFile}
282
308
  >
283
309
  <Download size={14} />
284
- Sauvegarder
310
+ {t("common.save", "Save")}
285
311
  </button>
286
312
  <button
287
313
  type="button"
@@ -290,7 +316,7 @@ export function AiContextButton({
290
316
  setIsResultOpen(false);
291
317
  setAnalysisResult(null);
292
318
  }}
293
- aria-label="Fermer"
319
+ aria-label={t("common.closeLabel", "Close")}
294
320
  >
295
321
  <X size={16} />
296
322
  </button>
@@ -299,12 +325,16 @@ export function AiContextButton({
299
325
 
300
326
  <div className="ai-result-body">
301
327
  <div className="ai-result-block">
302
- <h3 className="ai-result-subtitle">Prompt utilisé</h3>
328
+ <h3 className="ai-result-subtitle">
329
+ {t("common.promptUsed", "Prompt used")}
330
+ </h3>
303
331
  <pre className="ai-result-code">{analysisResult.prompt}</pre>
304
332
  </div>
305
333
 
306
334
  <div className="ai-result-block">
307
- <h3 className="ai-result-subtitle">Résultat</h3>
335
+ <h3 className="ai-result-subtitle">
336
+ {t("common.result", "Result")}
337
+ </h3>
308
338
  <div className="ai-result-content">
309
339
  {analysisResult.content}
310
340
  </div>
@@ -312,13 +342,16 @@ export function AiContextButton({
312
342
 
313
343
  <div className="ai-result-meta ai-between">
314
344
  <span>
315
- Coût: $
345
+ {t("ai.context.cost", "Cost")}: $
316
346
  {(apiKeyId?.includes("dev")
317
347
  ? 0
318
348
  : analysisResult.cost
319
349
  ).toFixed(6)}
320
350
  </span>
321
- <span>ID: {analysisResult.requestId?.slice(-8) || "N/A"}</span>
351
+ <span>
352
+ {t("ai.context.requestId", "Request ID")}:{" "}
353
+ {analysisResult.requestId?.slice(-8) || "N/A"}
354
+ </span>
322
355
  </div>
323
356
  </div>
324
357
  </div>
@@ -20,6 +20,8 @@ import { useAiContext } from "../context/AiProvider";
20
20
  import { handleAIError } from "../utils/errorHandler";
21
21
  import { useLB } from "../context/LBAuthProvider";
22
22
  import { LBSigninModal } from "./LBSigninModal";
23
+ import { useI18n } from "../context/I18nContext";
24
+ import { useLoadingTimer } from "../hooks/useLoadingTimer";
23
25
 
24
26
  export interface AiImageButtonProps
25
27
  extends
@@ -63,6 +65,7 @@ export function AiImageButton({
63
65
  variant = "default",
64
66
  ...buttonProps
65
67
  }: AiImageButtonProps) {
68
+ const { t } = useI18n();
66
69
  const [isOpen, setIsOpen] = useState(false);
67
70
  const [showAuthModal, setShowAuthModal] = useState(false);
68
71
  const [generatedImage, setGeneratedImage] = useState<{
@@ -90,6 +93,7 @@ export function AiImageButton({
90
93
  const apiKeyId = propApiKeyId ?? aiContext.apiKeyId;
91
94
 
92
95
  const { generateImage, loading } = useAiCallImage({ baseUrl, apiKeyId });
96
+ const { formatted: loadingElapsed } = useLoadingTimer(loading);
93
97
 
94
98
  const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
95
99
 
@@ -118,9 +122,15 @@ export function AiImageButton({
118
122
  if (!generatedImage || !onImageSave) return;
119
123
  try {
120
124
  await onImageSave(generatedImage.url);
121
- onToast?.({ type: "success", message: "Image sauvegardée" });
125
+ onToast?.({
126
+ type: "success",
127
+ message: t("ai.image.savedSuccess", "Image saved"),
128
+ });
122
129
  } catch (_error) {
123
- onToast?.({ type: "error", message: "Erreur lors de la sauvegarde" });
130
+ onToast?.({
131
+ type: "error",
132
+ message: t("ai.image.saveError", "Error while saving"),
133
+ });
124
134
  }
125
135
  };
126
136
 
@@ -149,32 +159,40 @@ export function AiImageButton({
149
159
  });
150
160
 
151
161
  if (result.url) {
162
+ const safeRequestId = result.requestId || `img-${Date.now()}`;
163
+ const safeTokens = result.debitTokens || 0;
152
164
  // Stocker l'image générée
153
165
  const imageData = {
154
166
  url: result.url,
155
167
  prompt: selectedPrompt,
156
- requestId: result.requestId,
157
- tokens: result.debitTokens,
168
+ requestId: safeRequestId,
169
+ tokens: safeTokens,
158
170
  };
159
171
  setGeneratedImage(imageData);
160
172
  console.log("[AiImageButton] Image data stored:", imageData);
161
173
 
162
174
  onImage?.(result.url, {
163
- requestId: result.requestId,
164
- tokens: result.debitTokens,
175
+ requestId: safeRequestId,
176
+ tokens: safeTokens,
177
+ });
178
+ onToast?.({
179
+ type: "success",
180
+ message: t(
181
+ "ai.image.generatedSuccess",
182
+ "Image generated successfully"
183
+ ),
165
184
  });
166
- onToast?.({ type: "success", message: "Image générée avec succès" });
167
185
 
168
186
  // Afficher le toast de coût même en mode dev
169
187
  showUsageToast({
170
- requestId: result.requestId,
171
- debitTokens: result.debitTokens,
188
+ requestId: safeRequestId,
189
+ debitTokens: safeTokens,
172
190
  usage: {
173
- total_tokens: result.debitTokens,
174
- prompt_tokens: Math.floor(result.debitTokens * 0.8),
175
- completion_tokens: Math.floor(result.debitTokens * 0.2),
191
+ total_tokens: safeTokens,
192
+ prompt_tokens: Math.floor(safeTokens * 0.8),
193
+ completion_tokens: Math.floor(safeTokens * 0.2),
176
194
  },
177
- cost: apiKeyId?.includes("dev") ? 0 : result.debitTokens * 0.002, // Coût simulé
195
+ cost: apiKeyId?.includes("dev") ? 0 : safeTokens * 0.002, // Coût simulé
178
196
  });
179
197
  }
180
198
  } catch (error) {
@@ -198,25 +216,40 @@ export function AiImageButton({
198
216
  className={`ai-btn ai-image-btn ${variantClass} ${sizeClass} ${radiusClass} ${className || ""}`}
199
217
  style={buttonProps.style}
200
218
  data-ai-image-button
201
- title={!isAuthReady ? "Authentication required" : "Générer une image"}
219
+ title={
220
+ !isAuthReady
221
+ ? t("auth.required", "Authentication required")
222
+ : t("ai.image.generate", "Generate image")
223
+ }
202
224
  >
203
225
  {loading ? (
204
- <>
205
- <Loader2 size={18} className="ai-spinner" />
206
- <span className="ai-text-microtracking">Génération...</span>
207
- </>
226
+ <span className="ai-loading-stack">
227
+ <span className="ai-loading-row">
228
+ <Loader2 size={18} className="ai-spinner" />
229
+ <span className="ai-text-microtracking">
230
+ {t("ai.image.generating", "Generating...")}
231
+ </span>
232
+ </span>
233
+ <span className="ai-loading-meta">
234
+ {t("ai.loading.elapsed", "{seconds}", {
235
+ seconds: loadingElapsed,
236
+ })}
237
+ </span>
238
+ </span>
208
239
  ) : !isAuthReady ? (
209
240
  <>
210
241
  <Lock size={18} />
211
242
 
212
- {children || <span>Connexion requise</span>}
243
+ {children || (
244
+ <span>{t("auth.connectRequired", "Connection required")}</span>
245
+ )}
213
246
  </>
214
247
  ) : (
215
248
  <>
216
249
  <ImageIcon size={18} />
217
250
 
218
251
  <span className="ai-text-microtracking">
219
- {children || "Générer une image"}
252
+ {children || t("ai.image.generate", "Generate image")}
220
253
  </span>
221
254
  </>
222
255
  )}
@@ -271,20 +304,20 @@ export function AiImageButton({
271
304
  <button
272
305
  onClick={handleDownload}
273
306
  className="ai-btn ai-btn--primary"
274
- title="Télécharger l'image"
307
+ title={t("ai.image.download", "Download image")}
275
308
  >
276
309
  <Download size={16} />
277
- Télécharger l'image
310
+ {t("ai.image.download", "Download image")}
278
311
  </button>
279
312
 
280
313
  {onImageSave && (
281
314
  <button
282
315
  onClick={handleSave}
283
316
  className="ai-btn"
284
- title="Sauvegarder en base"
317
+ title={t("ai.image.saveToDb", "Save to database")}
285
318
  >
286
319
  <ExternalLink size={14} />
287
- Sauvegarder
320
+ {t("common.save", "Save")}
288
321
  </button>
289
322
  )}
290
323
  </div>
@@ -292,7 +325,7 @@ export function AiImageButton({
292
325
  {/* Metadata */}
293
326
  <div className="ai-image-meta">
294
327
  <div className="flex justify-center">
295
- <span>ID: {generatedImage.requestId.slice(-8)}</span>
328
+ <span>ID: {(generatedImage.requestId || "N/A").slice(-8)}</span>
296
329
  </div>
297
330
  </div>
298
331
  </div>