@lastbrain/ai-ui-react 1.0.78 → 1.0.80
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/AiContextButton.d.ts.map +1 -1
- package/dist/components/AiContextButton.js +12 -6
- package/dist/components/AiImageButton.d.ts.map +1 -1
- package/dist/components/AiImageButton.js +11 -5
- package/dist/components/AiInput.js +4 -4
- package/dist/components/AiPromptPanel.d.ts +2 -0
- package/dist/components/AiPromptPanel.d.ts.map +1 -1
- package/dist/components/AiPromptPanel.js +138 -5
- package/dist/components/AiSelect.js +4 -4
- package/dist/components/AiStatusButton.js +3 -3
- package/dist/components/AiTextarea.d.ts.map +1 -1
- package/dist/components/AiTextarea.js +15 -4
- package/dist/components/LBApiKeySelector.d.ts.map +1 -1
- package/dist/components/LBApiKeySelector.js +12 -2
- package/dist/components/UsageToast.js +1 -1
- package/dist/context/LBAuthProvider.d.ts +3 -1
- package/dist/context/LBAuthProvider.d.ts.map +1 -1
- package/dist/context/LBAuthProvider.js +65 -29
- package/dist/styles.css +6 -4
- package/package.json +1 -1
- package/src/components/AiContextButton.tsx +14 -6
- package/src/components/AiImageButton.tsx +12 -6
- package/src/components/AiInput.tsx +4 -4
- package/src/components/AiPromptPanel.tsx +279 -12
- package/src/components/AiSelect.tsx +4 -4
- package/src/components/AiStatusButton.tsx +3 -3
- package/src/components/AiTextarea.tsx +16 -5
- package/src/components/LBApiKeySelector.tsx +14 -2
- package/src/components/UsageToast.tsx +1 -1
- package/src/context/LBAuthProvider.tsx +84 -30
- package/src/styles.css +6 -4
|
@@ -6,11 +6,15 @@ import {
|
|
|
6
6
|
useEffect,
|
|
7
7
|
useRef,
|
|
8
8
|
useLayoutEffect,
|
|
9
|
+
useMemo,
|
|
9
10
|
type ReactNode,
|
|
10
11
|
} from "react";
|
|
11
12
|
import { createPortal } from "react-dom";
|
|
12
13
|
import {
|
|
13
14
|
BookOpen,
|
|
15
|
+
Check,
|
|
16
|
+
Copy,
|
|
17
|
+
Eye,
|
|
14
18
|
Search,
|
|
15
19
|
Sparkles,
|
|
16
20
|
Star,
|
|
@@ -33,6 +37,19 @@ import { AiProvider, useAiContext } from "../context/AiProvider";
|
|
|
33
37
|
import { useI18n } from "../context/I18nContext";
|
|
34
38
|
import { useLoadingTimer } from "../hooks/useLoadingTimer";
|
|
35
39
|
|
|
40
|
+
function tryFormatJson(input?: string): { formatted: string; isJson: boolean } {
|
|
41
|
+
const raw = (input || "").trim();
|
|
42
|
+
if (!raw) {
|
|
43
|
+
return { formatted: "", isJson: false };
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
return { formatted: JSON.stringify(parsed, null, 2), isJson: true };
|
|
48
|
+
} catch {
|
|
49
|
+
return { formatted: input || "", isJson: false };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
export interface AiPromptPanelProps {
|
|
37
54
|
isOpen: boolean;
|
|
38
55
|
onClose: () => void;
|
|
@@ -50,6 +67,8 @@ export interface AiPromptPanelProps {
|
|
|
50
67
|
apiKey?: string;
|
|
51
68
|
baseUrl?: string;
|
|
52
69
|
showOnlyUserModels?: boolean;
|
|
70
|
+
contextPreview?: string;
|
|
71
|
+
contextPreviewTitle?: string;
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
export interface AiPromptPanelRenderProps {
|
|
@@ -114,6 +133,8 @@ function AiPromptPanelInternal({
|
|
|
114
133
|
apiKey,
|
|
115
134
|
baseUrl,
|
|
116
135
|
showOnlyUserModels = false,
|
|
136
|
+
contextPreview,
|
|
137
|
+
contextPreviewTitle,
|
|
117
138
|
}: AiPromptPanelProps) {
|
|
118
139
|
const { t } = useI18n();
|
|
119
140
|
const [selectedModel, setSelectedModel] = useState("");
|
|
@@ -127,6 +148,8 @@ function AiPromptPanelInternal({
|
|
|
127
148
|
const [selectedTag, setSelectedTag] = useState<string | "all">("all");
|
|
128
149
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
129
150
|
const [isClosing, setIsClosing] = useState(false);
|
|
151
|
+
const [showContextPreview, setShowContextPreview] = useState(false);
|
|
152
|
+
const [contextCopied, setContextCopied] = useState(false);
|
|
130
153
|
const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null);
|
|
131
154
|
const promptRef = useRef<HTMLTextAreaElement>(null);
|
|
132
155
|
const closeTimeoutRef = useRef<number | null>(null);
|
|
@@ -265,6 +288,8 @@ function AiPromptPanelInternal({
|
|
|
265
288
|
setPrompt("");
|
|
266
289
|
setPromptId(undefined);
|
|
267
290
|
setShowPromptLibrary(false);
|
|
291
|
+
setShowContextPreview(false);
|
|
292
|
+
setContextCopied(false);
|
|
268
293
|
setSearchQuery("");
|
|
269
294
|
setSelectedTag("all");
|
|
270
295
|
setIsClosing(false);
|
|
@@ -287,6 +312,12 @@ function AiPromptPanelInternal({
|
|
|
287
312
|
};
|
|
288
313
|
}, []);
|
|
289
314
|
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
if (!contextCopied) return;
|
|
317
|
+
const timeout = window.setTimeout(() => setContextCopied(false), 1200);
|
|
318
|
+
return () => window.clearTimeout(timeout);
|
|
319
|
+
}, [contextCopied]);
|
|
320
|
+
|
|
290
321
|
useEffect(() => {
|
|
291
322
|
setPortalRoot(document.body);
|
|
292
323
|
}, []);
|
|
@@ -340,6 +371,15 @@ function AiPromptPanelInternal({
|
|
|
340
371
|
adjustPromptHeight();
|
|
341
372
|
}, [prompt]);
|
|
342
373
|
|
|
374
|
+
const contextPreviewData = useMemo(
|
|
375
|
+
() => tryFormatJson(contextPreview),
|
|
376
|
+
[contextPreview]
|
|
377
|
+
);
|
|
378
|
+
const contextPreviewLines = useMemo(
|
|
379
|
+
() => contextPreviewData.formatted.split("\n"),
|
|
380
|
+
[contextPreviewData.formatted]
|
|
381
|
+
);
|
|
382
|
+
|
|
343
383
|
if (!isOpen) return null;
|
|
344
384
|
|
|
345
385
|
const activeModelId = selectedModel || modelOptions[0]?.id || "";
|
|
@@ -406,6 +446,8 @@ function AiPromptPanelInternal({
|
|
|
406
446
|
}
|
|
407
447
|
|
|
408
448
|
const isDrawer = uiMode === "drawer";
|
|
449
|
+
const hasContextPreview = Boolean(contextPreview?.trim());
|
|
450
|
+
const contextCharCount = contextPreview?.length || 0;
|
|
409
451
|
const panelContainerStyle = isDrawer
|
|
410
452
|
? {
|
|
411
453
|
...aiStyles.modal,
|
|
@@ -425,6 +467,78 @@ function AiPromptPanelInternal({
|
|
|
425
467
|
}
|
|
426
468
|
: aiStyles.modalContent;
|
|
427
469
|
|
|
470
|
+
const renderContextLine = (line: string, lineIndex: number) => {
|
|
471
|
+
if (!contextPreviewData.isJson) {
|
|
472
|
+
return (
|
|
473
|
+
<span
|
|
474
|
+
key={`line-${lineIndex}`}
|
|
475
|
+
style={{ color: "var(--ai-text-secondary)" }}
|
|
476
|
+
>
|
|
477
|
+
{line || " "}
|
|
478
|
+
</span>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const tokenRegex =
|
|
483
|
+
/("(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[{}[\],:])/g;
|
|
484
|
+
const parts: ReactNode[] = [];
|
|
485
|
+
let lastIndex = 0;
|
|
486
|
+
let match: RegExpExecArray | null;
|
|
487
|
+
|
|
488
|
+
while ((match = tokenRegex.exec(line)) !== null) {
|
|
489
|
+
const token = match[0];
|
|
490
|
+
const index = match.index;
|
|
491
|
+
if (index > lastIndex) {
|
|
492
|
+
parts.push(
|
|
493
|
+
<span
|
|
494
|
+
key={`txt-${lineIndex}-${lastIndex}`}
|
|
495
|
+
style={{ color: "var(--ai-text-secondary)" }}
|
|
496
|
+
>
|
|
497
|
+
{line.slice(lastIndex, index)}
|
|
498
|
+
</span>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const isKey = /^".*"$/.test(token) && line.slice(match.index).includes(":");
|
|
503
|
+
const color = isKey
|
|
504
|
+
? "#7cc5ff"
|
|
505
|
+
: /^".*"$/.test(token)
|
|
506
|
+
? "#9ad07f"
|
|
507
|
+
: /^(true|false)$/.test(token)
|
|
508
|
+
? "#f4a259"
|
|
509
|
+
: token === "null"
|
|
510
|
+
? "#d3869b"
|
|
511
|
+
: /^-?\d/.test(token)
|
|
512
|
+
? "#f7dc6f"
|
|
513
|
+
: "var(--ai-text-secondary)";
|
|
514
|
+
|
|
515
|
+
parts.push(
|
|
516
|
+
<span key={`tok-${lineIndex}-${index}`} style={{ color }}>
|
|
517
|
+
{token}
|
|
518
|
+
</span>
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
lastIndex = index + token.length;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (lastIndex < line.length) {
|
|
525
|
+
parts.push(
|
|
526
|
+
<span
|
|
527
|
+
key={`txt-end-${lineIndex}`}
|
|
528
|
+
style={{ color: "var(--ai-text-secondary)" }}
|
|
529
|
+
>
|
|
530
|
+
{line.slice(lastIndex)}
|
|
531
|
+
</span>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return (
|
|
536
|
+
<span key={`line-${lineIndex}`} style={{ whiteSpace: "pre" }}>
|
|
537
|
+
{parts.length > 0 ? parts : " "}
|
|
538
|
+
</span>
|
|
539
|
+
);
|
|
540
|
+
};
|
|
541
|
+
|
|
428
542
|
if (children) {
|
|
429
543
|
return createPortal(
|
|
430
544
|
<div style={panelContainerStyle} onKeyDown={handleKeyDown}>
|
|
@@ -755,18 +869,40 @@ function AiPromptPanelInternal({
|
|
|
755
869
|
)}
|
|
756
870
|
</span>
|
|
757
871
|
</label>
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
872
|
+
<div
|
|
873
|
+
style={{
|
|
874
|
+
display: "flex",
|
|
875
|
+
alignItems: "center",
|
|
876
|
+
flexWrap: "wrap",
|
|
877
|
+
justifyContent: "flex-end",
|
|
878
|
+
gap: "8px",
|
|
879
|
+
}}
|
|
880
|
+
>
|
|
881
|
+
{hasContextPreview ? (
|
|
882
|
+
<button
|
|
883
|
+
type="button"
|
|
884
|
+
onClick={() => setShowContextPreview(true)}
|
|
885
|
+
className="ai-inline-btn"
|
|
886
|
+
style={{ whiteSpace: "nowrap" }}
|
|
887
|
+
>
|
|
888
|
+
<Eye size={14} />
|
|
889
|
+
{t("prompt.modal.contextPreview", "Context preview")}
|
|
890
|
+
</button>
|
|
891
|
+
) : null}
|
|
892
|
+
{promptsLoading ? (
|
|
893
|
+
<span className="ai-inline-skeleton" aria-hidden="true" />
|
|
894
|
+
) : filteredPrompts.length > 0 ? (
|
|
895
|
+
<button
|
|
896
|
+
onClick={() => setShowPromptLibrary(true)}
|
|
897
|
+
className="ai-inline-btn"
|
|
898
|
+
style={{ whiteSpace: "nowrap" }}
|
|
899
|
+
>
|
|
900
|
+
<BookOpen size={14} />
|
|
901
|
+
{t("prompt.modal.browsePrompts", "Browse Prompts")} (
|
|
902
|
+
{filteredPrompts.length})
|
|
903
|
+
</button>
|
|
904
|
+
) : null}
|
|
905
|
+
</div>
|
|
770
906
|
</div>
|
|
771
907
|
<textarea
|
|
772
908
|
id="prompt-input"
|
|
@@ -1124,6 +1260,137 @@ function AiPromptPanelInternal({
|
|
|
1124
1260
|
)}
|
|
1125
1261
|
</div>
|
|
1126
1262
|
|
|
1263
|
+
{showContextPreview ? (
|
|
1264
|
+
<div
|
|
1265
|
+
style={{
|
|
1266
|
+
position: "absolute",
|
|
1267
|
+
inset: 0,
|
|
1268
|
+
zIndex: 12,
|
|
1269
|
+
background: "var(--ai-overlay)",
|
|
1270
|
+
backdropFilter: "blur(3px)",
|
|
1271
|
+
WebkitBackdropFilter: "blur(3px)",
|
|
1272
|
+
display: "flex",
|
|
1273
|
+
alignItems: "stretch",
|
|
1274
|
+
justifyContent: "center",
|
|
1275
|
+
padding: "10px",
|
|
1276
|
+
}}
|
|
1277
|
+
onClick={() => setShowContextPreview(false)}
|
|
1278
|
+
>
|
|
1279
|
+
<div
|
|
1280
|
+
style={{
|
|
1281
|
+
width: "min(980px, 100%)",
|
|
1282
|
+
maxHeight: "100%",
|
|
1283
|
+
background: "var(--ai-bg-secondary)",
|
|
1284
|
+
border: "1px solid var(--ai-border)",
|
|
1285
|
+
borderRadius: "12px",
|
|
1286
|
+
boxShadow: "var(--ai-shadow-lg)",
|
|
1287
|
+
overflow: "hidden",
|
|
1288
|
+
display: "flex",
|
|
1289
|
+
flexDirection: "column",
|
|
1290
|
+
}}
|
|
1291
|
+
onClick={(e) => e.stopPropagation()}
|
|
1292
|
+
>
|
|
1293
|
+
<div
|
|
1294
|
+
style={{
|
|
1295
|
+
padding: "12px 14px",
|
|
1296
|
+
borderBottom: "1px solid var(--ai-border)",
|
|
1297
|
+
display: "flex",
|
|
1298
|
+
alignItems: "center",
|
|
1299
|
+
justifyContent: "space-between",
|
|
1300
|
+
gap: "10px",
|
|
1301
|
+
}}
|
|
1302
|
+
>
|
|
1303
|
+
<div style={{ minWidth: 0 }}>
|
|
1304
|
+
<div style={{ fontSize: "14px", fontWeight: 600 }}>
|
|
1305
|
+
{contextPreviewTitle ||
|
|
1306
|
+
t("prompt.modal.contextPreview", "Context preview")}
|
|
1307
|
+
</div>
|
|
1308
|
+
<div
|
|
1309
|
+
style={{
|
|
1310
|
+
marginTop: "2px",
|
|
1311
|
+
fontSize: "12px",
|
|
1312
|
+
color: "var(--ai-text-secondary)",
|
|
1313
|
+
}}
|
|
1314
|
+
>
|
|
1315
|
+
{t("prompt.modal.contextChars", "{count} chars", {
|
|
1316
|
+
count: contextCharCount.toLocaleString(),
|
|
1317
|
+
})}
|
|
1318
|
+
</div>
|
|
1319
|
+
</div>
|
|
1320
|
+
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
|
1321
|
+
<button
|
|
1322
|
+
type="button"
|
|
1323
|
+
className="ai-inline-btn"
|
|
1324
|
+
onClick={async () => {
|
|
1325
|
+
try {
|
|
1326
|
+
await navigator.clipboard.writeText(
|
|
1327
|
+
contextPreviewData.formatted || ""
|
|
1328
|
+
);
|
|
1329
|
+
setContextCopied(true);
|
|
1330
|
+
} catch {
|
|
1331
|
+
setContextCopied(false);
|
|
1332
|
+
}
|
|
1333
|
+
}}
|
|
1334
|
+
style={{ whiteSpace: "nowrap" }}
|
|
1335
|
+
>
|
|
1336
|
+
{contextCopied ? <Check size={14} /> : <Copy size={14} />}
|
|
1337
|
+
{contextCopied
|
|
1338
|
+
? t("common.copied", "Copied")
|
|
1339
|
+
: t("common.copy", "Copy")}
|
|
1340
|
+
</button>
|
|
1341
|
+
<button
|
|
1342
|
+
type="button"
|
|
1343
|
+
className="ai-icon-btn"
|
|
1344
|
+
onClick={() => setShowContextPreview(false)}
|
|
1345
|
+
aria-label={t("common.closeLabel", "Close")}
|
|
1346
|
+
>
|
|
1347
|
+
×
|
|
1348
|
+
</button>
|
|
1349
|
+
</div>
|
|
1350
|
+
</div>
|
|
1351
|
+
<pre
|
|
1352
|
+
style={{
|
|
1353
|
+
margin: 0,
|
|
1354
|
+
padding: "14px",
|
|
1355
|
+
flex: 1,
|
|
1356
|
+
overflow: "auto",
|
|
1357
|
+
background: "color-mix(in srgb, var(--ai-bg) 70%, transparent)",
|
|
1358
|
+
borderTop: "1px solid var(--ai-border)",
|
|
1359
|
+
fontFamily:
|
|
1360
|
+
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
1361
|
+
fontSize: "12px",
|
|
1362
|
+
lineHeight: 1.5,
|
|
1363
|
+
}}
|
|
1364
|
+
>
|
|
1365
|
+
{contextPreviewLines.map((line, index) => (
|
|
1366
|
+
<div
|
|
1367
|
+
key={`context-line-${index}`}
|
|
1368
|
+
style={{
|
|
1369
|
+
display: "grid",
|
|
1370
|
+
gridTemplateColumns: "44px 1fr",
|
|
1371
|
+
gap: "12px",
|
|
1372
|
+
minHeight: "18px",
|
|
1373
|
+
}}
|
|
1374
|
+
>
|
|
1375
|
+
<span
|
|
1376
|
+
style={{
|
|
1377
|
+
color: "var(--ai-text-tertiary)",
|
|
1378
|
+
textAlign: "right",
|
|
1379
|
+
userSelect: "none",
|
|
1380
|
+
paddingRight: "8px",
|
|
1381
|
+
borderRight: "1px solid var(--ai-border)",
|
|
1382
|
+
}}
|
|
1383
|
+
>
|
|
1384
|
+
{index + 1}
|
|
1385
|
+
</span>
|
|
1386
|
+
{renderContextLine(line, index)}
|
|
1387
|
+
</div>
|
|
1388
|
+
))}
|
|
1389
|
+
</pre>
|
|
1390
|
+
</div>
|
|
1391
|
+
</div>
|
|
1392
|
+
) : null}
|
|
1393
|
+
|
|
1127
1394
|
<div
|
|
1128
1395
|
style={{
|
|
1129
1396
|
...aiStyles.modalFooter,
|
|
@@ -50,21 +50,20 @@ export function AiSelect({
|
|
|
50
50
|
// Rendre l'authentification optionnelle
|
|
51
51
|
let lbStatus: string | undefined;
|
|
52
52
|
let lbHasSession = false;
|
|
53
|
-
let lbHasSelectedKey = false;
|
|
54
53
|
let lbHasSelectedApiKeyCookie = false;
|
|
54
|
+
let lbHasSelectedKey = false;
|
|
55
55
|
let hasLBProvider = false;
|
|
56
56
|
try {
|
|
57
57
|
const lbContext = useLB();
|
|
58
58
|
lbStatus = lbContext.status;
|
|
59
59
|
lbHasSession = Boolean(lbContext.session?.sessionToken);
|
|
60
|
-
lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
|
|
61
60
|
lbHasSelectedApiKeyCookie = lbContext.hasSelectedApiKeyCookie;
|
|
61
|
+
lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
|
|
62
62
|
hasLBProvider = true;
|
|
63
63
|
} catch {
|
|
64
64
|
// LBProvider n'est pas disponible, ignorer
|
|
65
65
|
lbStatus = undefined;
|
|
66
66
|
lbHasSession = false;
|
|
67
|
-
lbHasSelectedKey = false;
|
|
68
67
|
hasLBProvider = false;
|
|
69
68
|
}
|
|
70
69
|
|
|
@@ -93,7 +92,8 @@ export function AiSelect({
|
|
|
93
92
|
hasLBProvider &&
|
|
94
93
|
lbStatus === "ready" &&
|
|
95
94
|
lbHasSession &&
|
|
96
|
-
|
|
95
|
+
!lbHasSelectedApiKeyCookie &&
|
|
96
|
+
!lbHasSelectedKey;
|
|
97
97
|
const isAuthReady =
|
|
98
98
|
!needsApiKeySelection &&
|
|
99
99
|
(lbStatus === "ready" || Boolean(process.env.LB_API_KEY));
|
|
@@ -245,8 +245,8 @@ export function AiStatusButton({
|
|
|
245
245
|
const requiresApiKeySelection =
|
|
246
246
|
lbStatus === "ready" &&
|
|
247
247
|
hasLbSession &&
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
!lbHasSelectedApiKeyCookie &&
|
|
249
|
+
!hasApiKeySelected;
|
|
250
250
|
const isApiKeyAuthMode = authTypeValue === "api_key";
|
|
251
251
|
|
|
252
252
|
const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
|
|
@@ -702,7 +702,7 @@ export function AiStatusButton({
|
|
|
702
702
|
|
|
703
703
|
{tooltipNode}
|
|
704
704
|
|
|
705
|
-
{showApiKeySelector
|
|
705
|
+
{showApiKeySelector ? (
|
|
706
706
|
<LBApiKeySelector
|
|
707
707
|
isOpen={showApiKeySelector}
|
|
708
708
|
apiKeys={apiKeys}
|
|
@@ -64,15 +64,15 @@ export function AiTextarea({
|
|
|
64
64
|
// Rendre l'authentification optionnelle
|
|
65
65
|
let lbStatus: string | undefined;
|
|
66
66
|
let lbHasSession = false;
|
|
67
|
-
let lbHasSelectedKey = false;
|
|
68
67
|
let lbHasSelectedApiKeyCookie = false;
|
|
68
|
+
let lbHasSelectedKey = false;
|
|
69
69
|
let hasLBProvider = false;
|
|
70
70
|
try {
|
|
71
71
|
const lbContext = useLB();
|
|
72
72
|
lbStatus = lbContext.status;
|
|
73
73
|
lbHasSession = Boolean(lbContext.session?.sessionToken);
|
|
74
|
-
lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
|
|
75
74
|
lbHasSelectedApiKeyCookie = lbContext.hasSelectedApiKeyCookie;
|
|
75
|
+
lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
|
|
76
76
|
hasLBProvider = true;
|
|
77
77
|
} catch {
|
|
78
78
|
// LBProvider n'est pas disponible, ignorer
|
|
@@ -97,7 +97,8 @@ export function AiTextarea({
|
|
|
97
97
|
hasLBProvider &&
|
|
98
98
|
lbStatus === "ready" &&
|
|
99
99
|
lbHasSession &&
|
|
100
|
-
|
|
100
|
+
!lbHasSelectedApiKeyCookie &&
|
|
101
|
+
!lbHasSelectedKey;
|
|
101
102
|
|
|
102
103
|
const { models: _models } = useAiModels({
|
|
103
104
|
baseUrl,
|
|
@@ -254,8 +255,18 @@ export function AiTextarea({
|
|
|
254
255
|
/>
|
|
255
256
|
<button
|
|
256
257
|
className={`ai-control-action ai-spark ${sizeClass} ${radiusClass}`}
|
|
257
|
-
onClick={
|
|
258
|
-
|
|
258
|
+
onClick={() => {
|
|
259
|
+
if (!isAuthReady) {
|
|
260
|
+
setShowAuthModal(true);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (hasConfiguration) {
|
|
264
|
+
void handleQuickGenerate();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
handleOpenPanel();
|
|
268
|
+
}}
|
|
269
|
+
disabled={disabled || loading}
|
|
259
270
|
type="button"
|
|
260
271
|
title={
|
|
261
272
|
!isAuthReady
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, { useState } from "react";
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
4
|
import { CheckCircle2, KeyRound, Loader2, XCircle } from "lucide-react";
|
|
5
|
+
import { createPortal } from "react-dom";
|
|
5
6
|
import type { LBApiKey } from "@lastbrain/ai-ui-core";
|
|
6
7
|
import { useI18n } from "../context/I18nContext";
|
|
7
8
|
|
|
@@ -19,12 +20,18 @@ export function LBApiKeySelector({
|
|
|
19
20
|
isOpen,
|
|
20
21
|
}: LBApiKeySelectorProps) {
|
|
21
22
|
const { t } = useI18n();
|
|
23
|
+
const [canPortal, setCanPortal] = useState(false);
|
|
22
24
|
const [selectedKeyId, setSelectedKeyId] = useState<string>(
|
|
23
25
|
apiKeys.find((k) => k.isActive)?.id || apiKeys[0]?.id || ""
|
|
24
26
|
);
|
|
25
27
|
const [loading, setLoading] = useState(false);
|
|
26
28
|
const [error, setError] = useState("");
|
|
27
29
|
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setCanPortal(true);
|
|
32
|
+
return () => setCanPortal(false);
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
28
35
|
if (!isOpen) return null;
|
|
29
36
|
|
|
30
37
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
@@ -50,7 +57,7 @@ export function LBApiKeySelector({
|
|
|
50
57
|
}
|
|
51
58
|
};
|
|
52
59
|
|
|
53
|
-
|
|
60
|
+
const modal = (
|
|
54
61
|
<div className="ai-signin-overlay" onClick={onCancel}>
|
|
55
62
|
<div
|
|
56
63
|
className="ai-signin-panel ai-key-modal-panel"
|
|
@@ -169,4 +176,9 @@ export function LBApiKeySelector({
|
|
|
169
176
|
</div>
|
|
170
177
|
</div>
|
|
171
178
|
);
|
|
179
|
+
|
|
180
|
+
if (!canPortal) {
|
|
181
|
+
return modal;
|
|
182
|
+
}
|
|
183
|
+
return createPortal(modal, document.body);
|
|
172
184
|
}
|
|
@@ -121,7 +121,7 @@ export function UsageToast({
|
|
|
121
121
|
padding: "4px 6px",
|
|
122
122
|
borderRadius: "6px",
|
|
123
123
|
marginBottom: "4px",
|
|
124
|
-
right: "
|
|
124
|
+
right: "52px",
|
|
125
125
|
background: "rgba(22, 163, 74, 0.12)",
|
|
126
126
|
border: "1px solid rgba(22, 163, 74, 0.3)",
|
|
127
127
|
color: "#16a34a",
|