@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
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:
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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={
|
|
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
|
-
? "
|
|
225
|
-
: "
|
|
230
|
+
? t("ai.chips.generate", "Generate tags with AI")
|
|
231
|
+
: t("auth.connectRequired", "Connection required")
|
|
226
232
|
}
|
|
227
233
|
title={
|
|
228
234
|
isAuthenticated
|
|
229
|
-
? "
|
|
230
|
-
: "
|
|
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="
|
|
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
|
|
53
|
+
contextDescription,
|
|
53
54
|
onResult,
|
|
54
55
|
onToast,
|
|
55
56
|
disabled,
|
|
56
57
|
className,
|
|
57
58
|
children,
|
|
58
|
-
resultModalTitle
|
|
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 (${
|
|
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:
|
|
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(
|
|
197
|
+
.toLocaleDateString(locale)
|
|
187
198
|
.replace(/\//g, "-");
|
|
188
|
-
const defaultName =
|
|
189
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
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>
|
|
247
|
+
<span>{t("ai.analyzing", "Analyzing...")}</span>
|
|
229
248
|
</>
|
|
230
249
|
) : !isAuthReady ? (
|
|
231
250
|
<>
|
|
232
251
|
<Lock size={16} />
|
|
233
|
-
{children ||
|
|
252
|
+
{children || (
|
|
253
|
+
<span>{t("auth.connectRequired", "Connection required")}</span>
|
|
254
|
+
)}
|
|
234
255
|
</>
|
|
235
256
|
) : (
|
|
236
257
|
<>
|
|
237
258
|
<Sparkles size={16} />
|
|
238
|
-
{children ||
|
|
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">
|
|
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
|
-
|
|
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="
|
|
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">
|
|
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">
|
|
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
|
-
|
|
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>
|
|
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?.({
|
|
125
|
+
onToast?.({
|
|
126
|
+
type: "success",
|
|
127
|
+
message: t("ai.image.savedSuccess", "Image saved"),
|
|
128
|
+
});
|
|
122
129
|
} catch (_error) {
|
|
123
|
-
onToast?.({
|
|
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:
|
|
157
|
-
tokens:
|
|
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:
|
|
164
|
-
tokens:
|
|
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:
|
|
171
|
-
debitTokens:
|
|
188
|
+
requestId: safeRequestId,
|
|
189
|
+
debitTokens: safeTokens,
|
|
172
190
|
usage: {
|
|
173
|
-
total_tokens:
|
|
174
|
-
prompt_tokens: Math.floor(
|
|
175
|
-
completion_tokens: Math.floor(
|
|
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 :
|
|
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={
|
|
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
|
-
<
|
|
206
|
-
|
|
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 ||
|
|
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 || "
|
|
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="
|
|
307
|
+
title={t("ai.image.download", "Download image")}
|
|
275
308
|
>
|
|
276
309
|
<Download size={16} />
|
|
277
|
-
|
|
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="
|
|
317
|
+
title={t("ai.image.saveToDb", "Save to database")}
|
|
285
318
|
>
|
|
286
319
|
<ExternalLink size={14} />
|
|
287
|
-
|
|
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>
|