@lastbrain/ai-ui-react 1.0.67 → 1.0.69

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 (60) hide show
  1. package/dist/components/AiChipLabel.d.ts +8 -3
  2. package/dist/components/AiChipLabel.d.ts.map +1 -1
  3. package/dist/components/AiChipLabel.js +21 -70
  4. package/dist/components/AiContextButton.d.ts +5 -1
  5. package/dist/components/AiContextButton.d.ts.map +1 -1
  6. package/dist/components/AiContextButton.js +67 -291
  7. package/dist/components/AiImageButton.d.ts +5 -1
  8. package/dist/components/AiImageButton.d.ts.map +1 -1
  9. package/dist/components/AiImageButton.js +6 -142
  10. package/dist/components/AiInput.d.ts +5 -3
  11. package/dist/components/AiInput.d.ts.map +1 -1
  12. package/dist/components/AiInput.js +13 -25
  13. package/dist/components/AiPromptPanel.d.ts.map +1 -1
  14. package/dist/components/AiPromptPanel.js +58 -212
  15. package/dist/components/AiSelect.d.ts +5 -3
  16. package/dist/components/AiSelect.d.ts.map +1 -1
  17. package/dist/components/AiSelect.js +21 -30
  18. package/dist/components/AiStatusButton.d.ts +4 -1
  19. package/dist/components/AiStatusButton.d.ts.map +1 -1
  20. package/dist/components/AiStatusButton.js +198 -626
  21. package/dist/components/AiTextarea.d.ts +4 -2
  22. package/dist/components/AiTextarea.d.ts.map +1 -1
  23. package/dist/components/AiTextarea.js +14 -26
  24. package/dist/components/LBApiKeySelector.d.ts.map +1 -1
  25. package/dist/components/LBApiKeySelector.js +5 -166
  26. package/dist/components/LBConnectButton.d.ts +4 -7
  27. package/dist/components/LBConnectButton.d.ts.map +1 -1
  28. package/dist/components/LBConnectButton.js +17 -86
  29. package/dist/components/LBSigninModal.d.ts +1 -1
  30. package/dist/components/LBSigninModal.d.ts.map +1 -1
  31. package/dist/components/LBSigninModal.js +42 -320
  32. package/dist/examples/AiUiPremiumShowcase.d.ts +2 -0
  33. package/dist/examples/AiUiPremiumShowcase.d.ts.map +1 -0
  34. package/dist/examples/AiUiPremiumShowcase.js +15 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -0
  38. package/dist/styles/inline.d.ts +1 -0
  39. package/dist/styles/inline.d.ts.map +1 -1
  40. package/dist/styles/inline.js +25 -129
  41. package/dist/styles.css +1268 -369
  42. package/dist/types.d.ts +3 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +2 -2
  45. package/src/components/AiChipLabel.tsx +64 -101
  46. package/src/components/AiContextButton.tsx +138 -430
  47. package/src/components/AiImageButton.tsx +29 -190
  48. package/src/components/AiInput.tsx +49 -74
  49. package/src/components/AiPromptPanel.tsx +71 -254
  50. package/src/components/AiSelect.tsx +61 -69
  51. package/src/components/AiStatusButton.tsx +477 -1219
  52. package/src/components/AiTextarea.tsx +49 -64
  53. package/src/components/LBApiKeySelector.tsx +86 -274
  54. package/src/components/LBConnectButton.tsx +46 -334
  55. package/src/components/LBSigninModal.tsx +140 -481
  56. package/src/examples/AiUiPremiumShowcase.tsx +91 -0
  57. package/src/index.ts +3 -0
  58. package/src/styles/inline.ts +27 -148
  59. package/src/styles.css +1268 -369
  60. package/src/types.ts +3 -0
@@ -1,48 +1,163 @@
1
1
  "use client";
2
2
 
3
3
  import type { AiStatus } from "@lastbrain/ai-ui-core";
4
- import { useState, useRef, useLayoutEffect, type CSSProperties } from "react";
4
+ import { useLayoutEffect, useMemo, useRef, useState } from "react";
5
5
  import { createPortal } from "react-dom";
6
6
  import {
7
+ ArrowRightLeft,
7
8
  BarChart3,
8
- Settings,
9
9
  FileText,
10
- History as HistoryIcon,
11
- FolderPlus,
12
- Power,
10
+ Folder,
11
+ History,
12
+ Loader2,
13
13
  LogOut,
14
- ArrowRightLeft,
14
+ Settings,
15
+ Shield,
15
16
  } from "lucide-react";
16
- import { aiStyles, calculateTooltipPosition } from "../styles/inline";
17
17
  import { useLB } from "../context/LBAuthProvider";
18
18
  import { useAiContext } from "../context/AiProvider";
19
- import { LBSigninModal } from "./LBSigninModal";
20
19
  import { LBApiKeySelector } from "./LBApiKeySelector";
20
+ import { LBSigninModal } from "./LBSigninModal";
21
+ import type { AiRadius, AiSize } from "../types";
21
22
 
22
23
  export interface AiStatusButtonProps {
23
24
  status: AiStatus | null;
24
25
  loading?: boolean;
25
26
  className?: string;
27
+ size?: AiSize;
28
+ radius?: AiRadius;
29
+ }
30
+
31
+ type BalanceUsage = {
32
+ used?: number;
33
+ total?: number;
34
+ percentage?: number;
35
+ purchased?: number;
36
+ quota?: number;
37
+ remaining?: number;
38
+ };
39
+
40
+ type StorageUsage = {
41
+ used_mb?: number;
42
+ allocated_mb?: number;
43
+ percentage?: number;
44
+ total_mb?: number;
45
+ };
46
+
47
+ const QUICK_LINKS = [
48
+ {
49
+ href: "https://prompt.lastbrain.io/auth/ai/tokens",
50
+ title: "Dashboard",
51
+ icon: BarChart3,
52
+ },
53
+ {
54
+ href: "https://prompt.lastbrain.io/auth/ai/history",
55
+ title: "Historique",
56
+ icon: History,
57
+ },
58
+ {
59
+ href: "https://prompt.lastbrain.io/auth/ai/settings",
60
+ title: "Settings",
61
+ icon: Settings,
62
+ },
63
+ {
64
+ href: "https://prompt.lastbrain.io/auth/ai/prompts",
65
+ title: "Prompts",
66
+ icon: FileText,
67
+ },
68
+ {
69
+ href: "https://prompt.lastbrain.io/auth/folder",
70
+ title: "Dossiers",
71
+ icon: Folder,
72
+ },
73
+ ];
74
+
75
+ const clamp = (value?: number) => Math.min(100, Math.max(0, value || 0));
76
+ const num = (value?: number) => (typeof value === "number" ? value : 0);
77
+ const fixed = (value: number | undefined, digits: number) =>
78
+ num(value).toFixed(digits);
79
+
80
+ function formatStorage(mb?: number) {
81
+ const value = num(mb);
82
+ if (value >= 1024) {
83
+ return `${(value / 1024).toFixed(2)} GB`;
84
+ }
85
+ return `${value.toFixed(2)} MB`;
86
+ }
87
+
88
+ function UsageCircle({ percentage }: { percentage?: number }) {
89
+ const safe = clamp(percentage);
90
+ const toneClass =
91
+ safe > 90
92
+ ? "ai-usage-circle-value--high"
93
+ : safe > 75
94
+ ? "ai-usage-circle-value--warn"
95
+ : "ai-usage-circle-value--low";
96
+ const size = 28;
97
+ const stroke = 3;
98
+ const radius = (size - stroke) / 2;
99
+ const circumference = 2 * Math.PI * radius;
100
+ const dashOffset = circumference - (safe / 100) * circumference;
101
+ return (
102
+ <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
103
+ <circle
104
+ cx={size / 2}
105
+ cy={size / 2}
106
+ r={radius}
107
+ className="ai-usage-circle-track"
108
+ strokeWidth={stroke}
109
+ fill="transparent"
110
+ />
111
+ <circle
112
+ cx={size / 2}
113
+ cy={size / 2}
114
+ r={radius}
115
+ className={toneClass}
116
+ strokeWidth={stroke}
117
+ fill="transparent"
118
+ strokeDasharray={circumference}
119
+ strokeDashoffset={dashOffset}
120
+ strokeLinecap="round"
121
+ transform={`rotate(-90 ${size / 2} ${size / 2})`}
122
+ />
123
+ <text
124
+ x={size / 2}
125
+ y={size / 2}
126
+ textAnchor="middle"
127
+ dominantBaseline="central"
128
+ fontSize="7px"
129
+ className={
130
+ toneClass === "ai-usage-circle-value--high"
131
+ ? "ai-usage-circle-text--high"
132
+ : toneClass === "ai-usage-circle-value--warn"
133
+ ? "ai-usage-circle-text--warn"
134
+ : "ai-usage-circle-text--low"
135
+ }
136
+ fontWeight="700"
137
+ >
138
+ {safe.toFixed(0)}%
139
+ </text>
140
+ </svg>
141
+ );
26
142
  }
27
143
 
28
144
  export function AiStatusButton({
29
145
  status,
30
146
  loading = false,
31
147
  className = "",
148
+ size = "md",
149
+ radius = "full",
32
150
  }: AiStatusButtonProps) {
33
- // Rendre l'authentification optionnelle
34
151
  let lbStatus: string | undefined;
35
152
  let user: any;
36
153
  let logout: (() => Promise<void>) | undefined;
37
154
  let apiKeys: any[] = [];
38
- let accessToken: string | undefined;
39
- let selectApiKeyWithToken: ((apiKeyId: string) => Promise<void>) | undefined;
40
155
  let switchApiKey: ((apiKeyId: string) => Promise<void>) | undefined;
41
156
  let lbApiStatus: any = null;
42
157
  let lbBasicStatus: any = null;
43
158
  let lbStorageStatus: any = null;
44
- let lbIsLoadingStatus: boolean = false;
45
- let lbIsLoadingStorage: boolean = false;
159
+ let lbIsLoadingStatus = false;
160
+ let lbIsLoadingStorage = false;
46
161
  let lbSelectedKey: any = null;
47
162
  let lbRefreshBasicStatus: (() => Promise<void>) | undefined;
48
163
  let lbRefreshStorageStatus: ((force?: boolean) => Promise<void>) | undefined;
@@ -53,8 +168,6 @@ export function AiStatusButton({
53
168
  user = lbContext.user;
54
169
  logout = lbContext.logout;
55
170
  apiKeys = lbContext.apiKeys || [];
56
- accessToken = lbContext.accessToken;
57
- selectApiKeyWithToken = lbContext.selectApiKeyWithToken;
58
171
  switchApiKey = lbContext.switchApiKey;
59
172
  lbApiStatus = lbContext.apiStatus;
60
173
  lbBasicStatus = lbContext.basicStatus;
@@ -65,269 +178,97 @@ export function AiStatusButton({
65
178
  lbRefreshBasicStatus = lbContext.refreshBasicStatus;
66
179
  lbRefreshStorageStatus = lbContext.refreshStorageStatus;
67
180
  } catch {
68
- // LBProvider n'est pas disponible, ignorer
69
181
  lbStatus = undefined;
70
- user = undefined;
71
- logout = undefined;
72
182
  }
73
183
 
74
- // Toujours prioriser les données du contexte LB quand disponibles
75
- // pour éviter d'afficher un status externe obsolète (Unknown/0).
76
- const lbEffectiveStatus =
77
- lbStatus === "ready"
78
- ? {
79
- ...(lbApiStatus || {}),
80
- ...(lbBasicStatus || {}),
81
- storage: lbStorageStatus?.storage || lbApiStatus?.storage,
82
- }
83
- : null;
84
- const effectiveStatus = lbEffectiveStatus || status || null;
85
-
86
- // Récupérer refetchProviders depuis AiProvider si disponible
87
184
  let refetchProviders: (() => Promise<void>) | undefined;
88
185
  try {
89
- const aiContext = useAiContext();
90
- refetchProviders = aiContext.refetchProviders;
186
+ refetchProviders = useAiContext().refetchProviders;
91
187
  } catch {
92
- // AiProvider n'est pas disponible, ignorer
93
188
  refetchProviders = undefined;
94
189
  }
95
190
 
96
191
  const [showSigninModal, setShowSigninModal] = useState(false);
97
192
  const [showApiKeySelector, setShowApiKeySelector] = useState(false);
193
+ const [showTooltip, setShowTooltip] = useState(false);
98
194
  const [isSelectingApiKey, setIsSelectingApiKey] = useState(false);
99
- const [isLoadingStatus, setIsLoadingStatus] = useState(false);
100
195
 
101
- type BalanceUsage = {
102
- used?: number;
103
- total?: number;
104
- percentage?: number;
105
- purchased?: number;
106
- quota?: number;
107
- remaining?: number;
108
- };
109
- type StorageUsage = {
110
- used_mb?: number;
111
- allocated_mb?: number;
112
- percentage?: number;
113
- db_mb?: number;
114
- files_mb?: number;
115
- total_mb?: number;
116
- };
117
-
118
- const formatFixed = (value: number | null | undefined, digits: number) =>
119
- typeof value === "number" ? value.toFixed(digits) : "0.00";
120
- const formatStorage = (valueMb: number | null | undefined) => {
121
- const mb = typeof valueMb === "number" ? valueMb : 0;
122
- if (mb >= 1024) {
123
- const gb = mb / 1024;
124
- return `${gb.toFixed(2)} GB`;
125
- }
126
- return `${mb.toFixed(2)} MB`;
127
- };
128
- const safeNumber = (value: number | null | undefined) =>
129
- typeof value === "number" ? value : 0;
130
- const clampPercentage = (value: number | null | undefined) =>
131
- Math.min(100, Math.max(0, safeNumber(value)));
132
- const getUsageColor = (percentage: number) => {
133
- if (percentage > 90) {
134
- return aiStyles.tooltipValueWarning.color;
135
- }
136
- if (percentage > 75) {
137
- return aiStyles.tooltipValueWarning.color;
138
- }
139
- return aiStyles.tooltipValueSuccess.color;
140
- };
141
- const renderUsageCircle = (percentageValue: number | null | undefined) => {
142
- const percentage = clampPercentage(percentageValue);
143
- const size = 28;
144
- const stroke = 3;
145
- const radius = (size - stroke) / 2;
146
- const circumference = 2 * Math.PI * radius;
147
- const offset = circumference - (percentage / 100) * circumference;
148
- const color = getUsageColor(percentage);
149
-
150
- return (
151
- <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
152
- <circle
153
- cx={size / 2}
154
- cy={size / 2}
155
- r={radius}
156
- stroke={aiStyles.tooltipLabel.color}
157
- strokeWidth={stroke}
158
- fill="transparent"
159
- opacity={0.3}
160
- />
161
- <circle
162
- cx={size / 2}
163
- cy={size / 2}
164
- r={radius}
165
- stroke={color}
166
- strokeWidth={stroke}
167
- fill="transparent"
168
- strokeDasharray={circumference}
169
- strokeDashoffset={offset}
170
- strokeLinecap="round"
171
- transform={`rotate(-90 ${size / 2} ${size / 2})`}
172
- />
173
- <text
174
- x={size / 2}
175
- y={size / 2}
176
- textAnchor="middle"
177
- dominantBaseline="central"
178
- fontSize="7px"
179
- fill={color}
180
- fontWeight="600"
181
- >
182
- {percentage.toFixed(0)}%
183
- </text>
184
- </svg>
185
- );
186
- };
187
-
188
- const balanceUsage = effectiveStatus?.balance as BalanceUsage | undefined;
189
- const storageUsage = effectiveStatus?.storage as StorageUsage | undefined;
190
-
191
- const balanceUsed = safeNumber(balanceUsage?.used);
192
- const balanceRemaining = safeNumber(balanceUsage?.remaining);
193
- const rawBalanceTotal =
194
- balanceUsage?.total ??
195
- safeNumber(balanceUsage?.purchased) + safeNumber(balanceUsage?.quota);
196
- const balanceTotal =
197
- rawBalanceTotal > 0 ? rawBalanceTotal : balanceUsed + balanceRemaining;
198
- const balancePercentage =
199
- balanceUsage?.percentage ??
200
- (balanceTotal > 0 ? Math.round((balanceUsed / balanceTotal) * 100) : 0);
201
-
202
- const storageAllocated =
203
- storageUsage?.allocated_mb ?? storageUsage?.total_mb ?? 0;
204
- const storageUsed = storageUsage?.used_mb ?? storageUsage?.total_mb ?? 0;
205
- const storagePercentage =
206
- storageUsage?.percentage ??
207
- (storageAllocated > 0
208
- ? Math.round((storageUsed / storageAllocated) * 100)
209
- : 0);
210
- const showFastStatusSkeleton =
211
- lbStatus === "ready" &&
212
- (lbIsLoadingStatus || isLoadingStatus) &&
213
- !lbBasicStatus &&
214
- !effectiveStatus;
215
-
216
- const [showTooltip, setShowTooltip] = useState(false);
217
- const [isHovered, setIsHovered] = useState(false);
218
- const [tooltipPosition, setTooltipPosition] = useState<CSSProperties>({});
219
196
  const buttonRef = useRef<HTMLButtonElement>(null);
220
197
  const tooltipRef = useRef<HTMLDivElement>(null);
198
+
221
199
  const canPortal = typeof document !== "undefined";
200
+
201
+ const effectiveStatus =
202
+ lbStatus === "ready"
203
+ ? {
204
+ ...(lbApiStatus || {}),
205
+ ...(lbBasicStatus || {}),
206
+ storage: lbStorageStatus?.storage || lbApiStatus?.storage,
207
+ }
208
+ : status || null;
209
+
222
210
  const hasApiKeySelected = Boolean(
223
- effectiveStatus?.apiKey?.id || effectiveStatus?.api_key?.id || lbSelectedKey?.id
211
+ effectiveStatus?.apiKey?.id ||
212
+ effectiveStatus?.api_key?.id ||
213
+ lbSelectedKey?.id
224
214
  );
225
215
  const requiresApiKeySelection =
226
216
  lbStatus === "ready" && !hasApiKeySelected && apiKeys.length > 0;
217
+ const isApiKeyAuthMode = effectiveStatus?.authType === "api_key";
218
+
219
+ const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
227
220
 
228
221
  useLayoutEffect(() => {
229
- if (!showTooltip || !buttonRef.current) {
222
+ if (!showTooltip || !buttonRef.current || !canPortal) {
230
223
  return;
231
224
  }
232
225
 
233
- const updatePosition = () => {
234
- if (!buttonRef.current) {
235
- return;
236
- }
237
-
238
- const buttonRect = buttonRef.current.getBoundingClientRect();
239
-
240
- if (canPortal) {
241
- const tooltipRect = tooltipRef.current?.getBoundingClientRect();
242
- const tooltipWidth = tooltipRect?.width ?? 360;
243
- const tooltipHeight = tooltipRect?.height ?? 520;
244
-
245
- const viewportWidth = window.innerWidth;
246
- const viewportHeight = window.innerHeight;
247
- const margin = 8;
248
-
249
- const spaceBelow = viewportHeight - buttonRect.bottom;
250
- const spaceAbove = buttonRect.top;
251
- const spaceRight = viewportWidth - buttonRect.right;
252
- const spaceLeft = buttonRect.left;
253
-
254
- const preferBelow = spaceBelow >= spaceAbove;
255
- const preferRight = spaceRight >= spaceLeft;
256
-
257
- let top = preferBelow
258
- ? buttonRect.bottom + margin
259
- : buttonRect.top - tooltipHeight - margin;
260
- if (top < margin) {
261
- top = margin;
262
- }
263
- if (top + tooltipHeight > viewportHeight - margin) {
264
- top = Math.max(margin, viewportHeight - tooltipHeight - margin);
265
- }
266
-
267
- let left = preferRight
268
- ? buttonRect.left
269
- : buttonRect.right - tooltipWidth;
270
- if (left < margin) {
271
- left = margin;
272
- }
273
- if (left + tooltipWidth > viewportWidth - margin) {
274
- left = Math.max(margin, viewportWidth - tooltipWidth - margin);
275
- }
276
-
277
- setTooltipPosition({
278
- top: `${top}px`,
279
- left: `${left}px`,
280
- position: "fixed",
281
- });
282
- } else {
283
- const position = calculateTooltipPosition(buttonRect);
284
- setTooltipPosition(position);
285
- }
226
+ const update = () => {
227
+ if (!buttonRef.current) return;
228
+ const rect = buttonRef.current.getBoundingClientRect();
229
+ const tipRect = tooltipRef.current?.getBoundingClientRect();
230
+ const tipWidth = tipRect?.width ?? 360;
231
+ const tipHeight = tipRect?.height ?? 520;
232
+ const margin = 8;
233
+
234
+ const viewportW = window.innerWidth;
235
+ const viewportH = window.innerHeight;
236
+
237
+ const placeBelow = viewportH - rect.bottom >= rect.top;
238
+ let top = placeBelow
239
+ ? rect.bottom + margin
240
+ : rect.top - tipHeight - margin;
241
+ top = Math.max(margin, Math.min(top, viewportH - tipHeight - margin));
242
+
243
+ const placeRight = viewportW - rect.right >= rect.left;
244
+ let left = placeRight ? rect.left : rect.right - tipWidth;
245
+ left = Math.max(margin, Math.min(left, viewportW - tipWidth - margin));
246
+
247
+ setTooltipStyle({
248
+ position: "fixed",
249
+ top: `${top}px`,
250
+ left: `${left}px`,
251
+ });
286
252
  };
287
253
 
288
- const rafId = requestAnimationFrame(updatePosition);
289
- const rafId2 = requestAnimationFrame(updatePosition);
290
-
291
- const handleResize = () => updatePosition();
292
- window.addEventListener("resize", handleResize);
293
- window.addEventListener("scroll", handleResize, true);
254
+ const raf1 = requestAnimationFrame(update);
255
+ const raf2 = requestAnimationFrame(update);
256
+ window.addEventListener("resize", update);
257
+ window.addEventListener("scroll", update, true);
294
258
 
295
259
  return () => {
296
- cancelAnimationFrame(rafId);
297
- cancelAnimationFrame(rafId2);
298
- window.removeEventListener("resize", handleResize);
299
- window.removeEventListener("scroll", handleResize, true);
260
+ cancelAnimationFrame(raf1);
261
+ cancelAnimationFrame(raf2);
262
+ window.removeEventListener("resize", update);
263
+ window.removeEventListener("scroll", update, true);
300
264
  };
301
265
  }, [showTooltip, canPortal]);
302
266
 
303
- const handleMouseEnter = () => {
304
- if (requiresApiKeySelection) {
305
- return;
306
- }
307
- setShowTooltip(true);
308
- setIsHovered(true);
309
- };
310
-
311
- const handleMouseLeave = () => {
312
- if (requiresApiKeySelection) {
313
- return;
314
- }
315
- // Keep tooltip visible if hovering over it
316
- setTimeout(() => {
317
- if (
318
- !tooltipRef.current?.matches(":hover") &&
319
- !buttonRef.current?.matches(":hover")
320
- ) {
321
- setShowTooltip(false);
322
- setIsHovered(false);
323
- }
324
- }, 100);
325
- };
326
-
327
267
  useLayoutEffect(() => {
328
- if (!showTooltip || requiresApiKeySelection || lbStatus !== "ready") {
268
+ if (!showTooltip || lbStatus !== "ready" || requiresApiKeySelection) {
329
269
  return;
330
270
  }
271
+
331
272
  if (!lbBasicStatus && !lbIsLoadingStatus && lbRefreshBasicStatus) {
332
273
  lbRefreshBasicStatus().catch(() => undefined);
333
274
  }
@@ -340,8 +281,8 @@ export function AiStatusButton({
340
281
  }
341
282
  }, [
342
283
  showTooltip,
343
- requiresApiKeySelection,
344
284
  lbStatus,
285
+ requiresApiKeySelection,
345
286
  lbBasicStatus,
346
287
  lbStorageStatus,
347
288
  lbIsLoadingStatus,
@@ -350,1017 +291,330 @@ export function AiStatusButton({
350
291
  lbRefreshStorageStatus,
351
292
  ]);
352
293
 
353
- if (loading || isSelectingApiKey) {
354
- return (
355
- <button
356
- ref={buttonRef}
357
- style={{
358
- ...aiStyles.statusButton,
359
- ...aiStyles.statusButtonDisabled,
360
- }}
361
- className={className}
362
- disabled
363
- >
364
- <svg
365
- style={aiStyles.spinner}
366
- width="16"
367
- height="16"
368
- viewBox="0 0 24 24"
369
- fill="none"
370
- stroke="currentColor"
371
- >
372
- <path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" />
373
- </svg>
374
- </button>
375
- );
376
- }
294
+ const balance = (effectiveStatus?.balance || {}) as BalanceUsage;
295
+ const storage = (effectiveStatus?.storage || {}) as StorageUsage;
377
296
 
378
- if (requiresApiKeySelection) {
379
- return (
380
- <div style={{ position: "relative", display: "inline-block" }}>
381
- <button
382
- ref={buttonRef}
383
- style={{
384
- ...aiStyles.statusButton,
385
- color: "#f59e0b",
386
- ...(isHovered && aiStyles.statusButtonHover),
387
- }}
388
- className={className}
389
- onMouseEnter={() => setIsHovered(true)}
390
- onMouseLeave={() => setIsHovered(false)}
391
- onClick={() => setShowApiKeySelector(true)}
392
- title="Select an API key to enable AI status and generation"
393
- >
394
- <svg
395
- width="16"
396
- height="16"
397
- viewBox="0 0 24 24"
398
- fill="none"
399
- stroke="currentColor"
400
- >
401
- <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
402
- </svg>
403
- </button>
404
- {showApiKeySelector && apiKeys.length > 0 && (
405
- <LBApiKeySelector
406
- isOpen={showApiKeySelector}
407
- apiKeys={apiKeys}
408
- onSelect={async (keyId) => {
409
- setIsSelectingApiKey(true);
410
- try {
411
- if (switchApiKey) {
412
- await switchApiKey(keyId);
413
- }
414
- setShowApiKeySelector(false);
415
- if (refetchProviders) {
416
- await refetchProviders();
417
- }
418
- } catch (error) {
419
- console.error("Failed to select API key:", error);
420
- } finally {
421
- setIsSelectingApiKey(false);
422
- }
423
- }}
424
- onCancel={() => setShowApiKeySelector(false)}
425
- />
426
- )}
427
- </div>
428
- );
429
- }
297
+ const balanceUsed = num(balance.used);
298
+ const balanceRemaining = num(balance.remaining);
299
+ const rawBalanceTotal =
300
+ balance.total ?? num(balance.purchased) + num(balance.quota);
301
+ const balanceTotal =
302
+ rawBalanceTotal > 0 ? rawBalanceTotal : balanceUsed + balanceRemaining;
303
+ const balancePct =
304
+ balance.percentage ??
305
+ (balanceTotal > 0 ? Math.round((balanceUsed / balanceTotal) * 100) : 0);
430
306
 
431
- if (!effectiveStatus) {
432
- // Si pas de statut API et pas de LBProvider, afficher message simple
433
- if (!lbStatus && lbStatus !== "ready") {
434
- return (
435
- <div style={{ position: "relative", display: "inline-block" }}>
436
- <button
437
- ref={buttonRef}
438
- style={{
439
- ...aiStyles.statusButton,
440
- color: "#ef4444",
441
- ...(isHovered && aiStyles.statusButtonHover),
442
- }}
443
- className={className}
444
- onMouseEnter={handleMouseEnter}
445
- onMouseLeave={handleMouseLeave}
446
- >
447
- <svg
448
- width="16"
449
- height="16"
450
- viewBox="0 0 24 24"
451
- fill="none"
452
- stroke="currentColor"
453
- >
454
- <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
455
- </svg>
456
- </button>
457
- {showTooltip &&
458
- canPortal &&
459
- createPortal(
460
- <div
461
- ref={tooltipRef}
462
- style={{
463
- ...aiStyles.tooltip,
464
- ...tooltipPosition,
465
- zIndex: 50,
466
- }}
467
- onMouseEnter={() => setShowTooltip(true)}
468
- onMouseLeave={handleMouseLeave}
469
- >
470
- No status available
471
- </div>,
472
- document.body
473
- )}
474
- </div>
475
- );
307
+ const storageUsed = storage.used_mb ?? storage.total_mb ?? 0;
308
+ const storageTotal = storage.allocated_mb ?? storage.total_mb ?? 0;
309
+ const storagePct =
310
+ storage.percentage ??
311
+ (storageTotal > 0 ? Math.round((storageUsed / storageTotal) * 100) : 0);
312
+
313
+ const showFastSkeleton =
314
+ lbStatus === "ready" &&
315
+ lbIsLoadingStatus &&
316
+ !lbBasicStatus &&
317
+ !effectiveStatus;
318
+
319
+ const showCornerLoading =
320
+ lbStatus === "ready" &&
321
+ (showFastSkeleton || lbIsLoadingStatus || lbIsLoadingStorage);
322
+
323
+ const triggerTone = useMemo(() => {
324
+ if (requiresApiKeySelection) return "warning";
325
+ if (lbStatus && lbStatus !== "ready") return "danger";
326
+ if (effectiveStatus) return "success";
327
+ return "neutral";
328
+ }, [requiresApiKeySelection, lbStatus, effectiveStatus]);
329
+
330
+ const openTooltip = () => {
331
+ if (requiresApiKeySelection) {
332
+ return;
333
+ }
334
+ setShowTooltip(true);
335
+ };
336
+
337
+ const closeTooltip = () => {
338
+ if (requiresApiKeySelection) {
339
+ return;
476
340
  }
341
+ setTimeout(() => {
342
+ if (
343
+ !tooltipRef.current?.matches(":hover") &&
344
+ !buttonRef.current?.matches(":hover")
345
+ ) {
346
+ setShowTooltip(false);
347
+ }
348
+ }, 100);
349
+ };
477
350
 
478
- return (
479
- <>
480
- <div style={{ position: "relative", display: "inline-block" }}>
481
- <button
482
- ref={buttonRef}
483
- style={{
484
- ...aiStyles.statusButton,
485
- color: lbStatus === "ready" ? "#10b981" : "#ef4444",
486
- ...(isHovered && aiStyles.statusButtonHover),
487
- }}
488
- className={className}
489
- onMouseEnter={handleMouseEnter}
490
- onMouseLeave={handleMouseLeave}
491
- onClick={() => {
492
- if (lbStatus !== "ready") {
493
- setShowSigninModal(true);
494
- }
495
- }}
351
+ const renderTriggerIcon = () => {
352
+ if (loading || isSelectingApiKey) {
353
+ return <Loader2 size={14} className="ai-spinner" />;
354
+ }
355
+ return <Shield size={14} />;
356
+ };
357
+
358
+ const triggerClass = [
359
+ "ai-status-trigger",
360
+ `ai-size-${size}`,
361
+ `ai-radius-${radius}`,
362
+ triggerTone === "warning" ? "ai-status-trigger--warning" : "",
363
+ triggerTone === "danger" ? "ai-status-trigger--danger" : "",
364
+ triggerTone === "success" ? "ai-status-trigger--success" : "",
365
+ className,
366
+ ]
367
+ .filter(Boolean)
368
+ .join(" ");
369
+
370
+ const tooltipNode =
371
+ showTooltip && canPortal
372
+ ? createPortal(
373
+ <div
374
+ ref={tooltipRef}
375
+ className="ai-popover ai-tooltip ai-status-tooltip"
376
+ style={tooltipStyle}
377
+ onMouseEnter={() => setShowTooltip(true)}
378
+ onMouseLeave={closeTooltip}
496
379
  >
497
- <svg
498
- width="16"
499
- height="16"
500
- viewBox="0 0 24 24"
501
- fill="none"
502
- stroke="currentColor"
503
- >
504
- <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
505
- </svg>
506
- </button>
507
- {showTooltip &&
508
- canPortal &&
509
- createPortal(
510
- <div
511
- ref={tooltipRef}
512
- style={{
513
- ...aiStyles.tooltip,
514
- ...tooltipPosition,
515
- zIndex: 50,
516
- }}
517
- onMouseEnter={() => setShowTooltip(true)}
518
- onMouseLeave={handleMouseLeave}
519
- >
520
- {lbStatus === "ready" && user ? (
521
- <>
522
- <div style={aiStyles.tooltipHeader}>API Status</div>
523
- <div
524
- style={{
525
- ...aiStyles.tooltipSection,
526
- ...aiStyles.tooltipSectionFirst,
527
- }}
528
- >
529
- <div style={aiStyles.tooltipRow}>
530
- <span style={aiStyles.tooltipLabel}>User:</span>
531
- <span style={aiStyles.tooltipValue}>{user.email}</span>
380
+ {lbStatus === "ready" && user ? (
381
+ <>
382
+ <div className="ai-popover-body">
383
+ <div className="ai-popover-header">API Status</div>
384
+ <div className="ai-popover-section ai-popover-section--first">
385
+ <div className="ai-popover-row">
386
+ <span className="ai-popover-label">User</span>
387
+ <span className="ai-popover-value ai-truncate max-w-[200px]">
388
+ {user.email}
389
+ </span>
390
+ </div>
391
+ </div>
392
+
393
+ <div className="ai-popover-section">
394
+ <div className="ai-popover-row">
395
+ <span className="ai-popover-label">API Key</span>
396
+ <div className="ai-row">
397
+ {lbIsLoadingStatus ? (
398
+ <div className="ai-kv-skeleton w-[110px]" />
399
+ ) : (
400
+ <span className="ai-popover-value">
401
+ {effectiveStatus?.apiKey?.name ||
402
+ effectiveStatus?.api_key?.name ||
403
+ "Unknown"}
404
+ </span>
405
+ )}
406
+ {switchApiKey ? (
407
+ <button
408
+ type="button"
409
+ className="ai-icon-btn"
410
+ onClick={(e) => {
411
+ e.stopPropagation();
412
+ setShowTooltip(false);
413
+ setShowApiKeySelector(true);
414
+ }}
415
+ title="Changer de clé API"
416
+ >
417
+ <ArrowRightLeft size={12} />
418
+ </button>
419
+ ) : null}
532
420
  </div>
533
421
  </div>
534
- {showFastStatusSkeleton && (
535
- <>
536
- <div style={aiStyles.tooltipSection}>
537
- <div style={aiStyles.tooltipRow}>
538
- <span style={aiStyles.tooltipLabel}>API Key:</span>
539
- <div
540
- style={{
541
- height: "16px",
542
- width: "110px",
543
- background: "rgba(139, 92, 246, 0.12)",
544
- borderRadius: "4px",
545
- animation: "pulse 2s ease-in-out infinite",
546
- }}
547
- />
548
- </div>
549
- <div style={aiStyles.tooltipRow}>
550
- <span style={aiStyles.tooltipLabel}>Env:</span>
551
- <div
552
- style={{
553
- height: "16px",
554
- width: "48px",
555
- background: "rgba(139, 92, 246, 0.12)",
556
- borderRadius: "4px",
557
- animation: "pulse 2s ease-in-out infinite",
558
- }}
559
- />
560
- </div>
561
- <div style={aiStyles.tooltipRow}>
562
- <span style={aiStyles.tooltipLabel}>Rate Limit:</span>
563
- <div
564
- style={{
565
- height: "16px",
566
- width: "84px",
567
- background: "rgba(139, 92, 246, 0.12)",
568
- borderRadius: "4px",
569
- animation: "pulse 2s ease-in-out infinite",
570
- }}
571
- />
572
- </div>
573
- <div style={aiStyles.tooltipRow}>
574
- <span style={aiStyles.tooltipLabel}>Auth:</span>
575
- <div
576
- style={{
577
- height: "16px",
578
- width: "72px",
579
- background: "rgba(139, 92, 246, 0.12)",
580
- borderRadius: "4px",
581
- animation: "pulse 2s ease-in-out infinite",
582
- }}
583
- />
584
- </div>
422
+
423
+ <div className="ai-popover-row">
424
+ <span className="ai-popover-label">Env</span>
425
+ {lbIsLoadingStatus && !effectiveStatus?.apiKey?.env ? (
426
+ <div className="ai-kv-skeleton w-12" />
427
+ ) : (
428
+ <span className="ai-popover-value">
429
+ {effectiveStatus?.apiKey?.env ||
430
+ effectiveStatus?.api_key?.env ||
431
+ "N/A"}
432
+ </span>
433
+ )}
434
+ </div>
435
+
436
+ <div className="ai-popover-row">
437
+ <span className="ai-popover-label">Rate Limit</span>
438
+ {lbIsLoadingStatus &&
439
+ !effectiveStatus?.apiKey?.rate_limit_rpm ? (
440
+ <div className="ai-kv-skeleton w-[92px]" />
441
+ ) : (
442
+ <span className="ai-popover-value">
443
+ {effectiveStatus?.apiKey?.rate_limit_rpm ||
444
+ effectiveStatus?.api_key?.rate_limit_rpm ||
445
+ 0}{" "}
446
+ req/min
447
+ </span>
448
+ )}
449
+ </div>
450
+
451
+ <div className="ai-popover-row">
452
+ <span className="ai-popover-label">Auth</span>
453
+ <span className="ai-popover-value">
454
+ {lbIsLoadingStatus
455
+ ? "..."
456
+ : effectiveStatus?.authType || lbStatus || "unknown"}
457
+ </span>
458
+ </div>
459
+ </div>
460
+
461
+ <div className="ai-popover-section">
462
+ <div className="ai-popover-header text-xs mb-2">Wallet</div>
463
+ <div className="ai-popover-row">
464
+ <span className="ai-popover-label">Total</span>
465
+ {lbIsLoadingStatus && !effectiveStatus?.balance ? (
466
+ <div className="ai-row">
467
+ <div className="ai-kv-skeleton w-[120px]" />
468
+ <div className="ai-kv-skeleton w-7 h-7 rounded-full" />
585
469
  </div>
586
- <div style={aiStyles.tooltipSection}>
587
- <div style={aiStyles.tooltipSubtitle}>Wallet</div>
588
- <div style={aiStyles.tooltipRow}>
589
- <span style={aiStyles.tooltipLabel}>Total:</span>
590
- <div
591
- style={{
592
- height: "16px",
593
- width: "120px",
594
- background: "rgba(139, 92, 246, 0.12)",
595
- borderRadius: "4px",
596
- animation: "pulse 2s ease-in-out infinite",
597
- }}
598
- />
599
- </div>
470
+ ) : (
471
+ <div className="ai-row">
472
+ <span className="ai-popover-value">
473
+ ${fixed(balanceUsed, 2)} / ${fixed(balanceTotal, 2)}
474
+ </span>
475
+ <UsageCircle percentage={balancePct} />
600
476
  </div>
601
- </>
602
- )}
603
- <div
604
- style={{
605
- display: "flex",
606
- gap: "8px",
607
- borderTop:
608
- "1px solid var(--ai-border-primary, #374151)",
609
- paddingTop: "12px",
610
- }}
611
- >
612
- <button
613
- onClick={() =>
614
- window.open(
615
- "https://prompt.lastbrain.io/auth/dashboard",
616
- "_blank"
617
- )
618
- }
619
- style={{
620
- flex: 1,
621
- background: "transparent",
622
- border: "none",
623
- padding: "14px",
624
- cursor: "pointer",
625
- display: "flex",
626
- alignItems: "center",
627
- justifyContent: "center",
628
- color: "#8b5cf6",
629
- transition: "all 0.2s ease",
630
- }}
631
- onMouseEnter={(e) => {
632
- Object.assign(e.currentTarget.style, {
633
- background: "rgba(139, 92, 246, 0.1)",
634
- });
635
- }}
636
- onMouseLeave={(e) => {
637
- Object.assign(e.currentTarget.style, {
638
- background: "transparent",
639
- });
640
- }}
641
- title="View Metrics"
642
- >
643
- <BarChart3 size={18} />
644
- </button>
645
- <button
646
- onClick={() =>
647
- window.open(
648
- "https://prompt.lastbrain.io/auth/ai/settings",
649
- "_blank"
650
- )
651
- }
652
- style={{
653
- flex: 1,
654
- background: "transparent",
655
- border: "none",
656
- padding: "14px",
657
- cursor: "pointer",
658
- display: "flex",
659
- alignItems: "center",
660
- justifyContent: "center",
661
- color: "#8b5cf6",
662
- transition: "all 0.2s ease",
663
- }}
664
- onMouseEnter={(e) => {
665
- Object.assign(e.currentTarget.style, {
666
- background: "rgba(139, 92, 246, 0.1)",
667
- });
668
- }}
669
- onMouseLeave={(e) => {
670
- Object.assign(e.currentTarget.style, {
671
- background: "transparent",
672
- });
673
- }}
674
- title="Settings"
675
- >
676
- <Settings size={18} />
677
- </button>
678
- <button
679
- onClick={() =>
680
- window.open(
681
- "https://prompt.lastbrain.io/auth/ai/prompts",
682
- "_blank"
683
- )
684
- }
685
- style={{
686
- flex: 1,
687
- background: "transparent",
688
- border: "none",
689
- padding: "14px",
690
- cursor: "pointer",
691
- display: "flex",
692
- alignItems: "center",
693
- justifyContent: "center",
694
- color: "#8b5cf6",
695
- transition: "all 0.2s ease",
696
- }}
697
- onMouseEnter={(e) => {
698
- Object.assign(e.currentTarget.style, {
699
- background: "rgba(139, 92, 246, 0.1)",
700
- });
701
- }}
702
- onMouseLeave={(e) => {
703
- Object.assign(e.currentTarget.style, {
704
- background: "transparent",
705
- });
706
- }}
707
- title="My Prompts"
708
- >
709
- <FileText size={18} />
710
- </button>
711
- <button
712
- onClick={() =>
713
- window.open(
714
- "https://prompt.lastbrain.io/auth/folder",
715
- "_blank"
716
- )
717
- }
718
- style={{
719
- flex: 1,
720
- background: "transparent",
721
- border: "none",
722
- padding: "14px",
723
- cursor: "pointer",
724
- display: "flex",
725
- alignItems: "center",
726
- justifyContent: "center",
727
- color: "#8b5cf6",
728
- transition: "all 0.2s ease",
729
- }}
730
- onMouseEnter={(e) => {
731
- Object.assign(e.currentTarget.style, {
732
- background: "rgba(139, 92, 246, 0.1)",
733
- });
734
- }}
735
- onMouseLeave={(e) => {
736
- Object.assign(e.currentTarget.style, {
737
- background: "transparent",
738
- });
739
- }}
740
- title="New Folder"
741
- >
742
- <FolderPlus size={18} />
743
- </button>
477
+ )}
478
+ </div>
479
+ </div>
480
+
481
+ <div className="ai-popover-section">
482
+ <div className="ai-popover-header text-xs mb-2">
483
+ Storage
484
+ </div>
485
+ <div className="ai-popover-row">
486
+ <span className="ai-popover-label">Total</span>
487
+ {lbIsLoadingStorage ? (
488
+ <div className="ai-row">
489
+ <div className="ai-kv-skeleton w-[120px]" />
490
+ <div className="ai-kv-skeleton w-7 h-7 rounded-full" />
491
+ </div>
492
+ ) : (
493
+ <div className="ai-row">
494
+ <span className="ai-popover-value">
495
+ {formatStorage(storageUsed)} /{" "}
496
+ {formatStorage(storageTotal)}
497
+ </span>
498
+ <UsageCircle percentage={storagePct} />
499
+ </div>
500
+ )}
501
+ </div>
502
+ </div>
503
+
504
+ <div className="ai-status-actions">
505
+ {QUICK_LINKS.map((item) => {
506
+ const Icon = item.icon;
507
+ return (
508
+ <button
509
+ key={item.href}
510
+ type="button"
511
+ className="ai-status-action-btn"
512
+ onClick={() => window.open(item.href, "_blank")}
513
+ title={item.title}
514
+ >
515
+ <Icon size={17} />
516
+ </button>
517
+ );
518
+ })}
519
+
520
+ {logout && !isApiKeyAuthMode ? (
744
521
  <button
522
+ type="button"
523
+ className="ai-status-action-btn ai-status-action-btn--danger"
745
524
  onClick={async () => {
746
- if (logout) {
525
+ try {
747
526
  await logout();
748
- // Refresh provider data after logout
527
+ setShowTooltip(false);
749
528
  if (refetchProviders) {
750
529
  await refetchProviders();
751
530
  }
531
+ } catch (error) {
532
+ console.error("Logout failed:", error);
752
533
  }
753
- setShowTooltip(false);
754
- }}
755
- style={{
756
- flex: 1,
757
- background: "transparent",
758
- border: "none",
759
- padding: "14px",
760
- cursor: "pointer",
761
- display: "flex",
762
- alignItems: "center",
763
- justifyContent: "center",
764
- color: "#ef4444",
765
- transition: "all 0.2s ease",
766
- }}
767
- onMouseEnter={(e) => {
768
- Object.assign(e.currentTarget.style, {
769
- background: "rgba(239, 68, 68, 0.1)",
770
- });
771
- }}
772
- onMouseLeave={(e) => {
773
- Object.assign(e.currentTarget.style, {
774
- background: "transparent",
775
- });
776
534
  }}
777
535
  title="Logout"
778
536
  >
779
- <Power size={18} />
537
+ <LogOut size={17} />
780
538
  </button>
781
- </div>
782
- </>
783
- ) : (
784
- <>
785
- <div style={aiStyles.tooltipHeader}>
786
- LastBrain Authentication
787
- </div>
788
- <div
789
- style={{
790
- paddingBottom: "12px",
791
- }}
792
- >
793
- <p
794
- style={{
795
- margin: 0,
796
- fontSize: "13px",
797
- color: "var(--ai-text-secondary, #9ca3af)",
798
- lineHeight: "1.5",
799
- }}
800
- >
801
- Connectez-vous pour accéder aux fonctionnalités IA
802
- </p>
803
- </div>
804
- <button
805
- onClick={() => {
806
- setShowSigninModal(true);
807
- setShowTooltip(false);
808
- }}
809
- style={{
810
- width: "100%",
811
- padding: "10px",
812
- background:
813
- "linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)",
814
- border: "none",
815
- borderRadius: "6px",
816
- color: "#ffffff",
817
- fontSize: "13px",
818
- fontWeight: 600,
819
- cursor: "pointer",
820
- transition: "all 0.2s ease",
821
- }}
822
- onMouseEnter={(e) => {
823
- e.currentTarget.style.transform = "translateY(-1px)";
824
- e.currentTarget.style.boxShadow =
825
- "0 4px 12px rgba(139, 92, 246, 0.3)";
826
- }}
827
- onMouseLeave={(e) => {
828
- e.currentTarget.style.transform = "translateY(0)";
829
- e.currentTarget.style.boxShadow = "none";
830
- }}
831
- >
832
- 🔐 Se connecter
833
- </button>
834
- </>
835
- )}
836
- </div>,
837
- document.body
838
- )}
839
- </div>
840
- <LBSigninModal
841
- isOpen={showSigninModal}
842
- onClose={() => setShowSigninModal(false)}
843
- />
844
- </>
845
- );
846
- }
847
-
848
- return (
849
- <div style={{ position: "relative", display: "inline-block" }}>
850
- <button
851
- ref={buttonRef}
852
- style={{
853
- ...aiStyles.statusButton,
854
- color: "#10b981",
855
- ...(isHovered && aiStyles.statusButtonHover),
856
- }}
857
- className={className}
858
- onMouseEnter={handleMouseEnter}
859
- onMouseLeave={handleMouseLeave}
860
- >
861
- <svg
862
- width="16"
863
- height="16"
864
- viewBox="0 0 24 24"
865
- fill="none"
866
- stroke="currentColor"
867
- >
868
- <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
869
- </svg>
870
- </button>
871
-
872
- {showTooltip &&
873
- canPortal &&
874
- createPortal(
875
- <div
876
- ref={tooltipRef}
877
- style={{
878
- ...aiStyles.tooltip,
879
- ...tooltipPosition,
880
- zIndex: 50,
881
- }}
882
- onMouseEnter={() => setShowTooltip(true)}
883
- onMouseLeave={handleMouseLeave}
884
- >
885
- <div style={aiStyles.tooltipHeader}>API Status</div>
886
-
887
- {/* User Info Section */}
888
- {effectiveStatus.user?.email && (
889
- <div
890
- style={{
891
- ...aiStyles.tooltipSection,
892
- ...aiStyles.tooltipSectionFirst,
893
- }}
894
- >
895
- <div style={aiStyles.tooltipRow}>
896
- <span style={aiStyles.tooltipLabel}>User:</span>
897
- <span
898
- style={{
899
- ...aiStyles.tooltipValue,
900
- fontSize: "12px",
901
- maxWidth: "200px",
902
- overflow: "hidden",
903
- textOverflow: "ellipsis",
904
- }}
905
- >
906
- {effectiveStatus.user.email}
907
- </span>
539
+ ) : null}
540
+ </div>
908
541
  </div>
909
- </div>
910
- )}
911
-
912
- <div
913
- style={{
914
- ...aiStyles.tooltipSection,
915
- ...(effectiveStatus.user?.email
916
- ? {}
917
- : aiStyles.tooltipSectionFirst),
918
- }}
919
- >
920
- <div style={aiStyles.tooltipRow}>
921
- <span style={aiStyles.tooltipLabel}>API Key:</span>
922
- <div
923
- style={{
924
- display: "flex",
925
- alignItems: "center",
926
- gap: "8px",
927
- }}
928
- >
929
- <span style={aiStyles.tooltipValue}>
930
- {lbIsLoadingStatus ? (
931
- <div
932
- style={{
933
- height: "16px",
934
- width: "100px",
935
- background: "rgba(139, 92, 246, 0.1)",
936
- borderRadius: "4px",
937
- animation: "pulse 2s ease-in-out infinite",
938
- }}
939
- />
940
- ) : (
941
- effectiveStatus?.apiKey?.name ||
942
- effectiveStatus?.api_key?.name ||
943
- "Unknown"
944
- )}
945
- </span>
946
- {switchApiKey && (
947
- <button
948
- onClick={(e) => {
949
- e.stopPropagation();
950
- setShowTooltip(false);
951
- setShowApiKeySelector(true);
952
- }}
953
- style={{
954
- background: "rgba(139, 92, 246, 0.1)",
955
- border: "1px solid rgba(139, 92, 246, 0.3)",
956
- borderRadius: "50%",
957
- width: "24px",
958
- height: "24px",
959
- display: "flex",
960
- alignItems: "center",
961
- justifyContent: "center",
962
- cursor: "pointer",
963
- padding: 0,
964
- transition: "all 0.2s ease",
965
- }}
966
- onMouseEnter={(e) => {
967
- e.currentTarget.style.background =
968
- "rgba(139, 92, 246, 0.2)";
969
- e.currentTarget.style.borderColor =
970
- "rgba(139, 92, 246, 0.5)";
971
- }}
972
- onMouseLeave={(e) => {
973
- e.currentTarget.style.background =
974
- "rgba(139, 92, 246, 0.1)";
975
- e.currentTarget.style.borderColor =
976
- "rgba(139, 92, 246, 0.3)";
977
- }}
978
- title="Change API Key"
979
- >
980
- <ArrowRightLeft
981
- size={12}
982
- style={{ color: "rgba(139, 92, 246, 1)" }}
983
- />
984
- </button>
985
- )}
542
+ </>
543
+ ) : (
544
+ <div className="ai-popover-body">
545
+ <div className="ai-popover-header">
546
+ LastBrain Authentication
986
547
  </div>
987
- </div>
988
- <div style={aiStyles.tooltipRow}>
989
- <span style={aiStyles.tooltipLabel}>Env:</span>
990
- {lbIsLoadingStatus && !effectiveStatus?.apiKey?.env ? (
991
- <div
992
- style={{
993
- height: "16px",
994
- width: "48px",
995
- background: "rgba(139, 92, 246, 0.1)",
996
- borderRadius: "4px",
997
- animation: "pulse 2s ease-in-out infinite",
998
- }}
999
- />
1000
- ) : (
1001
- <span style={aiStyles.tooltipValue}>
1002
- {effectiveStatus.apiKey?.env ||
1003
- effectiveStatus.api_key?.env ||
1004
- "N/A"}
1005
- </span>
1006
- )}
1007
- </div>
1008
- <div style={aiStyles.tooltipRow}>
1009
- <span style={aiStyles.tooltipLabel}>Rate Limit:</span>
1010
- {lbIsLoadingStatus &&
1011
- !effectiveStatus?.apiKey?.rate_limit_rpm &&
1012
- !effectiveStatus?.api_key?.rate_limit_rpm ? (
1013
- <div
1014
- style={{
1015
- height: "16px",
1016
- width: "92px",
1017
- background: "rgba(139, 92, 246, 0.1)",
1018
- borderRadius: "4px",
1019
- animation: "pulse 2s ease-in-out infinite",
1020
- }}
1021
- />
1022
- ) : (
1023
- <span style={aiStyles.tooltipValue}>
1024
- {effectiveStatus.apiKey?.rate_limit_rpm ||
1025
- effectiveStatus.api_key?.rate_limit_rpm ||
1026
- 0}{" "}
1027
- req/min
1028
- </span>
1029
- )}
1030
- </div>
1031
- <div style={aiStyles.tooltipRow}>
1032
- <span style={aiStyles.tooltipLabel}>Auth:</span>
1033
- <span style={aiStyles.tooltipValue}>
1034
- {lbIsLoadingStatus
1035
- ? "..."
1036
- : effectiveStatus?.authType || lbStatus || "unknown"}
1037
- </span>
1038
- </div>
1039
- </div>
1040
-
1041
- <div style={aiStyles.tooltipSection}>
1042
- <div style={aiStyles.tooltipSubtitle}>Wallet</div>
1043
- <div style={aiStyles.tooltipRow}>
1044
- <span style={aiStyles.tooltipLabel}>Total:</span>
1045
- {lbIsLoadingStatus && !effectiveStatus?.balance ? (
1046
- <div
1047
- style={{
1048
- display: "flex",
1049
- alignItems: "center",
1050
- gap: "8px",
1051
- }}
1052
- >
1053
- <div
1054
- style={{
1055
- height: "16px",
1056
- width: "120px",
1057
- background: "rgba(139, 92, 246, 0.1)",
1058
- borderRadius: "4px",
1059
- animation: "pulse 2s ease-in-out infinite",
1060
- }}
1061
- />
1062
- <div
1063
- style={{
1064
- width: "28px",
1065
- height: "28px",
1066
- borderRadius: "50%",
1067
- background: "rgba(139, 92, 246, 0.1)",
1068
- animation: "pulse 2s ease-in-out infinite",
1069
- }}
1070
- />
1071
- </div>
1072
- ) : (
1073
- <>
1074
- <span style={aiStyles.tooltipValue}>
1075
- ${formatFixed(balanceUsed, 2)} / ${formatFixed(balanceTotal, 2)}
1076
- </span>
1077
- {renderUsageCircle(balancePercentage)}
1078
- </>
1079
- )}
1080
- </div>
1081
- </div>
1082
-
1083
- <div style={aiStyles.tooltipSection}>
1084
- <div style={aiStyles.tooltipSubtitle}>Storage</div>
1085
- <div style={aiStyles.tooltipRow}>
1086
- <span style={aiStyles.tooltipLabel}>Total:</span>
1087
- {lbIsLoadingStorage ? (
1088
- <div
1089
- style={{
1090
- display: "flex",
1091
- alignItems: "center",
1092
- gap: "8px",
1093
- }}
1094
- >
1095
- <div
1096
- style={{
1097
- height: "16px",
1098
- width: "120px",
1099
- background: "rgba(139, 92, 246, 0.1)",
1100
- borderRadius: "4px",
1101
- animation: "pulse 2s ease-in-out infinite",
1102
- }}
1103
- />
1104
- <div
1105
- style={{
1106
- width: "28px",
1107
- height: "28px",
1108
- borderRadius: "50%",
1109
- background: "rgba(139, 92, 246, 0.1)",
1110
- animation: "pulse 2s ease-in-out infinite",
1111
- }}
1112
- />
1113
- </div>
1114
- ) : (
1115
- <>
1116
- <span style={aiStyles.tooltipValue}>
1117
- {formatStorage(storageUsed)} /{" "}
1118
- {formatStorage(storageAllocated)}
1119
- </span>
1120
- {renderUsageCircle(storagePercentage)}
1121
- </>
1122
- )}
1123
- </div>
1124
- </div>
1125
-
1126
- <div
1127
- style={{
1128
- ...aiStyles.tooltipActions,
1129
- width: "100%",
1130
-
1131
- flexDirection: "row",
1132
- }}
1133
- >
1134
- <button
1135
- onClick={() =>
1136
- window.open(
1137
- "https://prompt.lastbrain.io/auth/ai/tokens",
1138
- "_blank"
1139
- )
1140
- }
1141
- style={{
1142
- background: "transparent",
1143
- border: "none",
1144
- borderRadius: "4px",
1145
- padding: "14px",
1146
- cursor: "pointer",
1147
- display: "flex",
1148
- alignItems: "center",
1149
- justifyContent: "center",
1150
- color: "#8b5cf6",
1151
- transition: "all 0.2s ease",
1152
- }}
1153
- onMouseEnter={(e) => {
1154
- Object.assign(e.currentTarget.style, {
1155
- background: "rgba(139, 92, 246, 0.1)",
1156
- });
1157
- }}
1158
- onMouseLeave={(e) => {
1159
- Object.assign(e.currentTarget.style, {
1160
- background: "transparent",
1161
- });
1162
- }}
1163
- title="Dashboard"
1164
- >
1165
- <BarChart3 size={18} />
1166
- </button>
1167
- <button
1168
- onClick={() =>
1169
- window.open(
1170
- "https://prompt.lastbrain.io/auth/ai/history",
1171
- "_blank"
1172
- )
1173
- }
1174
- style={{
1175
- background: "transparent",
1176
- border: "none",
1177
- borderRadius: "4px",
1178
- padding: "14px",
1179
- cursor: "pointer",
1180
- display: "flex",
1181
- alignItems: "center",
1182
- justifyContent: "center",
1183
- color: "#8b5cf6",
1184
- transition: "all 0.2s ease",
1185
- }}
1186
- onMouseEnter={(e) => {
1187
- Object.assign(e.currentTarget.style, {
1188
- background: "rgba(139, 92, 246, 0.1)",
1189
- });
1190
- }}
1191
- onMouseLeave={(e) => {
1192
- Object.assign(e.currentTarget.style, {
1193
- background: "transparent",
1194
- });
1195
- }}
1196
- title="History"
1197
- >
1198
- <HistoryIcon size={18} />
1199
- </button>
1200
- <button
1201
- onClick={() =>
1202
- window.open(
1203
- "https://prompt.lastbrain.io/auth/ai/settings",
1204
- "_blank"
1205
- )
1206
- }
1207
- style={{
1208
- background: "transparent",
1209
- border: "none",
1210
- borderRadius: "4px",
1211
- padding: "14px",
1212
- cursor: "pointer",
1213
- display: "flex",
1214
- alignItems: "center",
1215
- justifyContent: "center",
1216
- color: "#8b5cf6",
1217
- transition: "all 0.2s ease",
1218
- }}
1219
- onMouseEnter={(e) => {
1220
- Object.assign(e.currentTarget.style, {
1221
- background: "rgba(139, 92, 246, 0.1)",
1222
- });
1223
- }}
1224
- onMouseLeave={(e) => {
1225
- Object.assign(e.currentTarget.style, {
1226
- background: "transparent",
1227
- });
1228
- }}
1229
- title="Settings"
1230
- >
1231
- <Settings size={18} />
1232
- </button>
1233
- <button
1234
- onClick={() =>
1235
- window.open(
1236
- "https://prompt.lastbrain.io/auth/ai/prompts",
1237
- "_blank"
1238
- )
1239
- }
1240
- style={{
1241
- background: "transparent",
1242
- border: "none",
1243
- borderRadius: "4px",
1244
- padding: "14px",
1245
- cursor: "pointer",
1246
- display: "flex",
1247
- alignItems: "center",
1248
- justifyContent: "center",
1249
- color: "#8b5cf6",
1250
- transition: "all 0.2s ease",
1251
- }}
1252
- onMouseEnter={(e) => {
1253
- Object.assign(e.currentTarget.style, {
1254
- background: "rgba(139, 92, 246, 0.1)",
1255
- });
1256
- }}
1257
- onMouseLeave={(e) => {
1258
- Object.assign(e.currentTarget.style, {
1259
- background: "transparent",
1260
- });
1261
- }}
1262
- title="New Prompt"
1263
- >
1264
- <FileText size={18} />
1265
- </button>
1266
- <button
1267
- onClick={() =>
1268
- window.open(
1269
- "https://prompt.lastbrain.io/auth/folder",
1270
- "_blank"
1271
- )
1272
- }
1273
- style={{
1274
- background: "transparent",
1275
- border: "none",
1276
- padding: "14px",
1277
- cursor: "pointer",
1278
- display: "flex",
1279
- alignItems: "center",
1280
- justifyContent: "center",
1281
- color: "#8b5cf6",
1282
- transition: "all 0.2s ease",
1283
- }}
1284
- onMouseEnter={(e) => {
1285
- Object.assign(e.currentTarget.style, {
1286
- background: "rgba(139, 92, 246, 0.1)",
1287
- });
1288
- }}
1289
- onMouseLeave={(e) => {
1290
- Object.assign(e.currentTarget.style, {
1291
- background: "transparent",
1292
- });
1293
- }}
1294
- title="New Folder"
1295
- >
1296
- <FolderPlus size={18} />
1297
- </button>
1298
-
1299
- {/* Logout Button */}
1300
- {logout && (
548
+ <p className="ai-signin-subtitle mt-0">
549
+ Connectez-vous pour accéder aux fonctionnalités IA.
550
+ </p>
1301
551
  <button
1302
- onClick={async () => {
1303
- try {
1304
- await logout();
1305
- setShowTooltip(false);
1306
- // Refresh provider data after logout
1307
- if (refetchProviders) {
1308
- await refetchProviders();
1309
- }
1310
- } catch (error) {
1311
- console.error("Logout failed:", error);
1312
- }
552
+ type="button"
553
+ className="ai-btn ai-btn--auth w-full mt-2"
554
+ onClick={() => {
555
+ setShowSigninModal(true);
556
+ setShowTooltip(false);
1313
557
  }}
1314
- style={{
1315
- background: "transparent",
1316
- border: "none",
1317
- padding: "14px",
1318
- cursor: "pointer",
1319
- display: "flex",
1320
- alignItems: "center",
1321
- justifyContent: "center",
1322
- color: "#ef4444",
1323
- transition: "all 0.2s ease",
1324
- borderRadius: "4px",
1325
- }}
1326
- onMouseEnter={(e) => {
1327
- Object.assign(e.currentTarget.style, {
1328
- background: "rgba(239, 68, 68, 0.1)",
1329
- });
1330
- }}
1331
- onMouseLeave={(e) => {
1332
- Object.assign(e.currentTarget.style, {
1333
- background: "transparent",
1334
- });
1335
- }}
1336
- title="Logout"
1337
558
  >
1338
- <LogOut size={18} />
559
+ Se connecter
1339
560
  </button>
1340
- )}
1341
- </div>
561
+ </div>
562
+ )}
1342
563
  </div>,
1343
564
  document.body
1344
- )}
565
+ )
566
+ : null;
1345
567
 
1346
- {/* API Key Selector Modal */}
1347
- {showApiKeySelector && apiKeys.length > 0 && (
568
+ return (
569
+ <>
570
+ <div className="relative inline-block">
571
+ <button
572
+ ref={buttonRef}
573
+ className={triggerClass}
574
+ onMouseEnter={openTooltip}
575
+ onMouseLeave={closeTooltip}
576
+ onClick={() => {
577
+ if (requiresApiKeySelection) {
578
+ setShowApiKeySelector(true);
579
+ return;
580
+ }
581
+ if (!effectiveStatus && lbStatus !== "ready") {
582
+ setShowSigninModal(true);
583
+ }
584
+ }}
585
+ disabled={loading || isSelectingApiKey}
586
+ title={
587
+ requiresApiKeySelection
588
+ ? "Sélectionnez une clé API"
589
+ : "Voir le status"
590
+ }
591
+ aria-label="AI status"
592
+ >
593
+ {renderTriggerIcon()}
594
+ </button>
595
+
596
+ {showCornerLoading ? (
597
+ <span className="ai-status-loading-dot" aria-hidden="true">
598
+ <Loader2 size={7} className="ai-spinner text-[var(--ai-primary)]" />
599
+ </span>
600
+ ) : null}
601
+ </div>
602
+
603
+ {tooltipNode}
604
+
605
+ {showApiKeySelector && apiKeys.length > 0 ? (
1348
606
  <LBApiKeySelector
1349
607
  isOpen={showApiKeySelector}
1350
608
  apiKeys={apiKeys}
1351
609
  onSelect={async (keyId) => {
1352
610
  setIsSelectingApiKey(true);
1353
611
  try {
1354
- // Utiliser la nouvelle fonction switchApiKey qui gère automatiquement le contexte
1355
- if (switchApiKey) {
1356
- await switchApiKey(keyId);
1357
- } else {
612
+ if (!switchApiKey) {
1358
613
  throw new Error("Switch API key function not available");
1359
614
  }
615
+ await switchApiKey(keyId);
1360
616
  setShowApiKeySelector(false);
1361
617
  setShowTooltip(false);
1362
- setIsLoadingStatus(true);
1363
- // Refresh provider data after API key selection
1364
618
  if (refetchProviders) {
1365
619
  await refetchProviders();
1366
620
  }
@@ -1368,12 +622,16 @@ export function AiStatusButton({
1368
622
  console.error("Failed to select API key:", error);
1369
623
  } finally {
1370
624
  setIsSelectingApiKey(false);
1371
- setIsLoadingStatus(false);
1372
625
  }
1373
626
  }}
1374
627
  onCancel={() => setShowApiKeySelector(false)}
1375
628
  />
1376
- )}
1377
- </div>
629
+ ) : null}
630
+
631
+ <LBSigninModal
632
+ isOpen={showSigninModal}
633
+ onClose={() => setShowSigninModal(false)}
634
+ />
635
+ </>
1378
636
  );
1379
637
  }