@lastbrain/ai-ui-react 1.0.68 → 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 -668
  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 -1313
  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,273 +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
-
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
195
 
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
- const showCornerLoadingIndicator =
216
- lbStatus === "ready" &&
217
- (showFastStatusSkeleton || lbIsLoadingStatus || lbIsLoadingStorage);
218
-
219
- const [showTooltip, setShowTooltip] = useState(false);
220
- const [isHovered, setIsHovered] = useState(false);
221
- const [tooltipPosition, setTooltipPosition] = useState<CSSProperties>({});
222
196
  const buttonRef = useRef<HTMLButtonElement>(null);
223
197
  const tooltipRef = useRef<HTMLDivElement>(null);
198
+
224
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
+
225
210
  const hasApiKeySelected = Boolean(
226
- effectiveStatus?.apiKey?.id || effectiveStatus?.api_key?.id || lbSelectedKey?.id
211
+ effectiveStatus?.apiKey?.id ||
212
+ effectiveStatus?.api_key?.id ||
213
+ lbSelectedKey?.id
227
214
  );
228
- const isApiKeyAuthMode = effectiveStatus?.authType === "api_key";
229
215
  const requiresApiKeySelection =
230
216
  lbStatus === "ready" && !hasApiKeySelected && apiKeys.length > 0;
217
+ const isApiKeyAuthMode = effectiveStatus?.authType === "api_key";
218
+
219
+ const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
231
220
 
232
221
  useLayoutEffect(() => {
233
- if (!showTooltip || !buttonRef.current) {
222
+ if (!showTooltip || !buttonRef.current || !canPortal) {
234
223
  return;
235
224
  }
236
225
 
237
- const updatePosition = () => {
238
- if (!buttonRef.current) {
239
- return;
240
- }
241
-
242
- const buttonRect = buttonRef.current.getBoundingClientRect();
243
-
244
- if (canPortal) {
245
- const tooltipRect = tooltipRef.current?.getBoundingClientRect();
246
- const tooltipWidth = tooltipRect?.width ?? 360;
247
- const tooltipHeight = tooltipRect?.height ?? 520;
248
-
249
- const viewportWidth = window.innerWidth;
250
- const viewportHeight = window.innerHeight;
251
- const margin = 8;
252
-
253
- const spaceBelow = viewportHeight - buttonRect.bottom;
254
- const spaceAbove = buttonRect.top;
255
- const spaceRight = viewportWidth - buttonRect.right;
256
- const spaceLeft = buttonRect.left;
257
-
258
- const preferBelow = spaceBelow >= spaceAbove;
259
- const preferRight = spaceRight >= spaceLeft;
260
-
261
- let top = preferBelow
262
- ? buttonRect.bottom + margin
263
- : buttonRect.top - tooltipHeight - margin;
264
- if (top < margin) {
265
- top = margin;
266
- }
267
- if (top + tooltipHeight > viewportHeight - margin) {
268
- top = Math.max(margin, viewportHeight - tooltipHeight - margin);
269
- }
270
-
271
- let left = preferRight
272
- ? buttonRect.left
273
- : buttonRect.right - tooltipWidth;
274
- if (left < margin) {
275
- left = margin;
276
- }
277
- if (left + tooltipWidth > viewportWidth - margin) {
278
- left = Math.max(margin, viewportWidth - tooltipWidth - margin);
279
- }
280
-
281
- setTooltipPosition({
282
- top: `${top}px`,
283
- left: `${left}px`,
284
- position: "fixed",
285
- });
286
- } else {
287
- const position = calculateTooltipPosition(buttonRect);
288
- setTooltipPosition(position);
289
- }
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
+ });
290
252
  };
291
253
 
292
- const rafId = requestAnimationFrame(updatePosition);
293
- const rafId2 = requestAnimationFrame(updatePosition);
294
-
295
- const handleResize = () => updatePosition();
296
- window.addEventListener("resize", handleResize);
297
- 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);
298
258
 
299
259
  return () => {
300
- cancelAnimationFrame(rafId);
301
- cancelAnimationFrame(rafId2);
302
- window.removeEventListener("resize", handleResize);
303
- window.removeEventListener("scroll", handleResize, true);
260
+ cancelAnimationFrame(raf1);
261
+ cancelAnimationFrame(raf2);
262
+ window.removeEventListener("resize", update);
263
+ window.removeEventListener("scroll", update, true);
304
264
  };
305
265
  }, [showTooltip, canPortal]);
306
266
 
307
- const handleMouseEnter = () => {
308
- if (requiresApiKeySelection) {
309
- return;
310
- }
311
- setShowTooltip(true);
312
- setIsHovered(true);
313
- };
314
-
315
- const handleMouseLeave = () => {
316
- if (requiresApiKeySelection) {
317
- return;
318
- }
319
- // Keep tooltip visible if hovering over it
320
- setTimeout(() => {
321
- if (
322
- !tooltipRef.current?.matches(":hover") &&
323
- !buttonRef.current?.matches(":hover")
324
- ) {
325
- setShowTooltip(false);
326
- setIsHovered(false);
327
- }
328
- }, 100);
329
- };
330
-
331
267
  useLayoutEffect(() => {
332
- if (!showTooltip || requiresApiKeySelection || lbStatus !== "ready") {
268
+ if (!showTooltip || lbStatus !== "ready" || requiresApiKeySelection) {
333
269
  return;
334
270
  }
271
+
335
272
  if (!lbBasicStatus && !lbIsLoadingStatus && lbRefreshBasicStatus) {
336
273
  lbRefreshBasicStatus().catch(() => undefined);
337
274
  }
@@ -344,8 +281,8 @@ export function AiStatusButton({
344
281
  }
345
282
  }, [
346
283
  showTooltip,
347
- requiresApiKeySelection,
348
284
  lbStatus,
285
+ requiresApiKeySelection,
349
286
  lbBasicStatus,
350
287
  lbStorageStatus,
351
288
  lbIsLoadingStatus,
@@ -354,1107 +291,330 @@ export function AiStatusButton({
354
291
  lbRefreshStorageStatus,
355
292
  ]);
356
293
 
357
- if (loading || isSelectingApiKey) {
358
- return (
359
- <button
360
- ref={buttonRef}
361
- style={{
362
- ...aiStyles.statusButton,
363
- ...aiStyles.statusButtonDisabled,
364
- }}
365
- className={className}
366
- disabled
367
- >
368
- <svg
369
- style={aiStyles.spinner}
370
- width="16"
371
- height="16"
372
- viewBox="0 0 24 24"
373
- fill="none"
374
- stroke="currentColor"
375
- >
376
- <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" />
377
- </svg>
378
- </button>
379
- );
380
- }
294
+ const balance = (effectiveStatus?.balance || {}) as BalanceUsage;
295
+ const storage = (effectiveStatus?.storage || {}) as StorageUsage;
381
296
 
382
- if (requiresApiKeySelection) {
383
- return (
384
- <div style={{ position: "relative", display: "inline-block" }}>
385
- <button
386
- ref={buttonRef}
387
- style={{
388
- ...aiStyles.statusButton,
389
- color: "#f59e0b",
390
- ...(isHovered && aiStyles.statusButtonHover),
391
- }}
392
- className={className}
393
- onMouseEnter={() => setIsHovered(true)}
394
- onMouseLeave={() => setIsHovered(false)}
395
- onClick={() => setShowApiKeySelector(true)}
396
- title="Select an API key to enable AI status and generation"
397
- >
398
- <svg
399
- width="16"
400
- height="16"
401
- viewBox="0 0 24 24"
402
- fill="none"
403
- stroke="currentColor"
404
- >
405
- <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
406
- </svg>
407
- </button>
408
- {showCornerLoadingIndicator && (
409
- <div
410
- style={{
411
- position: "absolute",
412
- top: -2,
413
- right: -2,
414
- width: 12,
415
- height: 12,
416
- borderRadius: "999px",
417
- background: "rgba(2, 6, 23, 0.95)",
418
- border: "1px solid rgba(139, 92, 246, 0.55)",
419
- display: "flex",
420
- alignItems: "center",
421
- justifyContent: "center",
422
- pointerEvents: "none",
423
- }}
424
- >
425
- <svg
426
- style={aiStyles.spinner}
427
- width="8"
428
- height="8"
429
- viewBox="0 0 24 24"
430
- fill="none"
431
- stroke="#8b5cf6"
432
- strokeWidth="2"
433
- >
434
- <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" />
435
- </svg>
436
- </div>
437
- )}
438
- {showApiKeySelector && apiKeys.length > 0 && (
439
- <LBApiKeySelector
440
- isOpen={showApiKeySelector}
441
- apiKeys={apiKeys}
442
- onSelect={async (keyId) => {
443
- setIsSelectingApiKey(true);
444
- try {
445
- if (switchApiKey) {
446
- await switchApiKey(keyId);
447
- }
448
- setShowApiKeySelector(false);
449
- if (refetchProviders) {
450
- await refetchProviders();
451
- }
452
- } catch (error) {
453
- console.error("Failed to select API key:", error);
454
- } finally {
455
- setIsSelectingApiKey(false);
456
- }
457
- }}
458
- onCancel={() => setShowApiKeySelector(false)}
459
- />
460
- )}
461
- </div>
462
- );
463
- }
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);
464
306
 
465
- if (!effectiveStatus) {
466
- // Si pas de statut API et pas de LBProvider, afficher message simple
467
- if (!lbStatus && lbStatus !== "ready") {
468
- return (
469
- <div style={{ position: "relative", display: "inline-block" }}>
470
- <button
471
- ref={buttonRef}
472
- style={{
473
- ...aiStyles.statusButton,
474
- color: "#ef4444",
475
- ...(isHovered && aiStyles.statusButtonHover),
476
- }}
477
- className={className}
478
- onMouseEnter={handleMouseEnter}
479
- onMouseLeave={handleMouseLeave}
480
- >
481
- <svg
482
- width="16"
483
- height="16"
484
- viewBox="0 0 24 24"
485
- fill="none"
486
- stroke="currentColor"
487
- >
488
- <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
489
- </svg>
490
- </button>
491
- {showTooltip &&
492
- canPortal &&
493
- createPortal(
494
- <div
495
- ref={tooltipRef}
496
- style={{
497
- ...aiStyles.tooltip,
498
- ...tooltipPosition,
499
- zIndex: 50,
500
- }}
501
- onMouseEnter={() => setShowTooltip(true)}
502
- onMouseLeave={handleMouseLeave}
503
- >
504
- No status available
505
- </div>,
506
- document.body
507
- )}
508
- </div>
509
- );
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;
510
333
  }
334
+ setShowTooltip(true);
335
+ };
511
336
 
512
- return (
513
- <>
514
- <div style={{ position: "relative", display: "inline-block" }}>
515
- <button
516
- ref={buttonRef}
517
- style={{
518
- ...aiStyles.statusButton,
519
- color: lbStatus === "ready" ? "#10b981" : "#ef4444",
520
- ...(isHovered && aiStyles.statusButtonHover),
521
- }}
522
- className={className}
523
- onMouseEnter={handleMouseEnter}
524
- onMouseLeave={handleMouseLeave}
525
- onClick={() => {
526
- if (lbStatus !== "ready") {
527
- setShowSigninModal(true);
528
- }
529
- }}
337
+ const closeTooltip = () => {
338
+ if (requiresApiKeySelection) {
339
+ return;
340
+ }
341
+ setTimeout(() => {
342
+ if (
343
+ !tooltipRef.current?.matches(":hover") &&
344
+ !buttonRef.current?.matches(":hover")
345
+ ) {
346
+ setShowTooltip(false);
347
+ }
348
+ }, 100);
349
+ };
350
+
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}
530
379
  >
531
- <svg
532
- width="16"
533
- height="16"
534
- viewBox="0 0 24 24"
535
- fill="none"
536
- stroke="currentColor"
537
- >
538
- <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
539
- </svg>
540
- </button>
541
- {showCornerLoadingIndicator && (
542
- <div
543
- style={{
544
- position: "absolute",
545
- top: -2,
546
- right: -2,
547
- width: 12,
548
- height: 12,
549
- borderRadius: "999px",
550
- background: "rgba(2, 6, 23, 0.95)",
551
- border: "1px solid rgba(139, 92, 246, 0.55)",
552
- display: "flex",
553
- alignItems: "center",
554
- justifyContent: "center",
555
- pointerEvents: "none",
556
- }}
557
- >
558
- <svg
559
- style={aiStyles.spinner}
560
- width="8"
561
- height="8"
562
- viewBox="0 0 24 24"
563
- fill="none"
564
- stroke="#8b5cf6"
565
- strokeWidth="2"
566
- >
567
- <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" />
568
- </svg>
569
- </div>
570
- )}
571
- {showTooltip &&
572
- canPortal &&
573
- createPortal(
574
- <div
575
- ref={tooltipRef}
576
- style={{
577
- ...aiStyles.tooltip,
578
- ...tooltipPosition,
579
- zIndex: 50,
580
- }}
581
- onMouseEnter={() => setShowTooltip(true)}
582
- onMouseLeave={handleMouseLeave}
583
- >
584
- {lbStatus === "ready" && user ? (
585
- <>
586
- <div style={aiStyles.tooltipHeader}>API Status</div>
587
- <div
588
- style={{
589
- ...aiStyles.tooltipSection,
590
- ...aiStyles.tooltipSectionFirst,
591
- }}
592
- >
593
- <div style={aiStyles.tooltipRow}>
594
- <span style={aiStyles.tooltipLabel}>User:</span>
595
- <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}
596
420
  </div>
597
421
  </div>
598
- {showFastStatusSkeleton && (
599
- <>
600
- <div style={aiStyles.tooltipSection}>
601
- <div style={aiStyles.tooltipRow}>
602
- <span style={aiStyles.tooltipLabel}>API Key:</span>
603
- <div
604
- style={{
605
- height: "16px",
606
- width: "110px",
607
- background: "rgba(139, 92, 246, 0.12)",
608
- borderRadius: "4px",
609
- animation: "pulse 2s ease-in-out infinite",
610
- }}
611
- />
612
- </div>
613
- <div style={aiStyles.tooltipRow}>
614
- <span style={aiStyles.tooltipLabel}>Env:</span>
615
- <div
616
- style={{
617
- height: "16px",
618
- width: "48px",
619
- background: "rgba(139, 92, 246, 0.12)",
620
- borderRadius: "4px",
621
- animation: "pulse 2s ease-in-out infinite",
622
- }}
623
- />
624
- </div>
625
- <div style={aiStyles.tooltipRow}>
626
- <span style={aiStyles.tooltipLabel}>Rate Limit:</span>
627
- <div
628
- style={{
629
- height: "16px",
630
- width: "84px",
631
- background: "rgba(139, 92, 246, 0.12)",
632
- borderRadius: "4px",
633
- animation: "pulse 2s ease-in-out infinite",
634
- }}
635
- />
636
- </div>
637
- <div style={aiStyles.tooltipRow}>
638
- <span style={aiStyles.tooltipLabel}>Auth:</span>
639
- <div
640
- style={{
641
- height: "16px",
642
- width: "72px",
643
- background: "rgba(139, 92, 246, 0.12)",
644
- borderRadius: "4px",
645
- animation: "pulse 2s ease-in-out infinite",
646
- }}
647
- />
648
- </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" />
649
469
  </div>
650
- <div style={aiStyles.tooltipSection}>
651
- <div style={aiStyles.tooltipSubtitle}>Wallet</div>
652
- <div style={aiStyles.tooltipRow}>
653
- <span style={aiStyles.tooltipLabel}>Total:</span>
654
- <div
655
- style={{
656
- height: "16px",
657
- width: "120px",
658
- background: "rgba(139, 92, 246, 0.12)",
659
- borderRadius: "4px",
660
- animation: "pulse 2s ease-in-out infinite",
661
- }}
662
- />
663
- </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} />
664
476
  </div>
665
- </>
666
- )}
667
- <div
668
- style={{
669
- display: "flex",
670
- gap: "8px",
671
- borderTop:
672
- "1px solid var(--ai-border-primary, #374151)",
673
- paddingTop: "12px",
674
- }}
675
- >
676
- <button
677
- onClick={() =>
678
- window.open(
679
- "https://prompt.lastbrain.io/auth/dashboard",
680
- "_blank"
681
- )
682
- }
683
- style={{
684
- flex: 1,
685
- background: "transparent",
686
- border: "none",
687
- padding: "14px",
688
- cursor: "pointer",
689
- display: "flex",
690
- alignItems: "center",
691
- justifyContent: "center",
692
- color: "#8b5cf6",
693
- transition: "all 0.2s ease",
694
- }}
695
- onMouseEnter={(e) => {
696
- Object.assign(e.currentTarget.style, {
697
- background: "rgba(139, 92, 246, 0.1)",
698
- });
699
- }}
700
- onMouseLeave={(e) => {
701
- Object.assign(e.currentTarget.style, {
702
- background: "transparent",
703
- });
704
- }}
705
- title="View Metrics"
706
- >
707
- <BarChart3 size={18} />
708
- </button>
709
- <button
710
- onClick={() =>
711
- window.open(
712
- "https://prompt.lastbrain.io/auth/ai/settings",
713
- "_blank"
714
- )
715
- }
716
- style={{
717
- flex: 1,
718
- background: "transparent",
719
- border: "none",
720
- padding: "14px",
721
- cursor: "pointer",
722
- display: "flex",
723
- alignItems: "center",
724
- justifyContent: "center",
725
- color: "#8b5cf6",
726
- transition: "all 0.2s ease",
727
- }}
728
- onMouseEnter={(e) => {
729
- Object.assign(e.currentTarget.style, {
730
- background: "rgba(139, 92, 246, 0.1)",
731
- });
732
- }}
733
- onMouseLeave={(e) => {
734
- Object.assign(e.currentTarget.style, {
735
- background: "transparent",
736
- });
737
- }}
738
- title="Settings"
739
- >
740
- <Settings size={18} />
741
- </button>
742
- <button
743
- onClick={() =>
744
- window.open(
745
- "https://prompt.lastbrain.io/auth/ai/prompts",
746
- "_blank"
747
- )
748
- }
749
- style={{
750
- flex: 1,
751
- background: "transparent",
752
- border: "none",
753
- padding: "14px",
754
- cursor: "pointer",
755
- display: "flex",
756
- alignItems: "center",
757
- justifyContent: "center",
758
- color: "#8b5cf6",
759
- transition: "all 0.2s ease",
760
- }}
761
- onMouseEnter={(e) => {
762
- Object.assign(e.currentTarget.style, {
763
- background: "rgba(139, 92, 246, 0.1)",
764
- });
765
- }}
766
- onMouseLeave={(e) => {
767
- Object.assign(e.currentTarget.style, {
768
- background: "transparent",
769
- });
770
- }}
771
- title="My Prompts"
772
- >
773
- <FileText size={18} />
774
- </button>
775
- <button
776
- onClick={() =>
777
- window.open(
778
- "https://prompt.lastbrain.io/auth/folder",
779
- "_blank"
780
- )
781
- }
782
- style={{
783
- flex: 1,
784
- background: "transparent",
785
- border: "none",
786
- padding: "14px",
787
- cursor: "pointer",
788
- display: "flex",
789
- alignItems: "center",
790
- justifyContent: "center",
791
- color: "#8b5cf6",
792
- transition: "all 0.2s ease",
793
- }}
794
- onMouseEnter={(e) => {
795
- Object.assign(e.currentTarget.style, {
796
- background: "rgba(139, 92, 246, 0.1)",
797
- });
798
- }}
799
- onMouseLeave={(e) => {
800
- Object.assign(e.currentTarget.style, {
801
- background: "transparent",
802
- });
803
- }}
804
- title="New Folder"
805
- >
806
- <FolderPlus size={18} />
807
- </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 ? (
808
521
  <button
522
+ type="button"
523
+ className="ai-status-action-btn ai-status-action-btn--danger"
809
524
  onClick={async () => {
810
- if (logout) {
525
+ try {
811
526
  await logout();
812
- // Refresh provider data after logout
527
+ setShowTooltip(false);
813
528
  if (refetchProviders) {
814
529
  await refetchProviders();
815
530
  }
531
+ } catch (error) {
532
+ console.error("Logout failed:", error);
816
533
  }
817
- setShowTooltip(false);
818
- }}
819
- style={{
820
- flex: 1,
821
- background: "transparent",
822
- border: "none",
823
- padding: "14px",
824
- cursor: "pointer",
825
- display: "flex",
826
- alignItems: "center",
827
- justifyContent: "center",
828
- color: "#ef4444",
829
- transition: "all 0.2s ease",
830
- }}
831
- onMouseEnter={(e) => {
832
- Object.assign(e.currentTarget.style, {
833
- background: "rgba(239, 68, 68, 0.1)",
834
- });
835
- }}
836
- onMouseLeave={(e) => {
837
- Object.assign(e.currentTarget.style, {
838
- background: "transparent",
839
- });
840
534
  }}
841
535
  title="Logout"
842
536
  >
843
- <Power size={18} />
537
+ <LogOut size={17} />
844
538
  </button>
845
- </div>
846
- </>
847
- ) : (
848
- <>
849
- <div style={aiStyles.tooltipHeader}>
850
- LastBrain Authentication
851
- </div>
852
- <div
853
- style={{
854
- paddingBottom: "12px",
855
- }}
856
- >
857
- <p
858
- style={{
859
- margin: 0,
860
- fontSize: "13px",
861
- color: "var(--ai-text-secondary, #9ca3af)",
862
- lineHeight: "1.5",
863
- }}
864
- >
865
- Connectez-vous pour accéder aux fonctionnalités IA
866
- </p>
867
- </div>
868
- <button
869
- onClick={() => {
870
- setShowSigninModal(true);
871
- setShowTooltip(false);
872
- }}
873
- style={{
874
- width: "100%",
875
- padding: "10px",
876
- background:
877
- "linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)",
878
- border: "none",
879
- borderRadius: "6px",
880
- color: "#ffffff",
881
- fontSize: "13px",
882
- fontWeight: 600,
883
- cursor: "pointer",
884
- transition: "all 0.2s ease",
885
- }}
886
- onMouseEnter={(e) => {
887
- e.currentTarget.style.transform = "translateY(-1px)";
888
- e.currentTarget.style.boxShadow =
889
- "0 4px 12px rgba(139, 92, 246, 0.3)";
890
- }}
891
- onMouseLeave={(e) => {
892
- e.currentTarget.style.transform = "translateY(0)";
893
- e.currentTarget.style.boxShadow = "none";
894
- }}
895
- >
896
- 🔐 Se connecter
897
- </button>
898
- </>
899
- )}
900
- </div>,
901
- document.body
902
- )}
903
- </div>
904
- <LBSigninModal
905
- isOpen={showSigninModal}
906
- onClose={() => setShowSigninModal(false)}
907
- />
908
- </>
909
- );
910
- }
911
-
912
- return (
913
- <div style={{ position: "relative", display: "inline-block" }}>
914
- <button
915
- ref={buttonRef}
916
- style={{
917
- ...aiStyles.statusButton,
918
- color: "#10b981",
919
- ...(isHovered && aiStyles.statusButtonHover),
920
- }}
921
- className={className}
922
- onMouseEnter={handleMouseEnter}
923
- onMouseLeave={handleMouseLeave}
924
- >
925
- <svg
926
- width="16"
927
- height="16"
928
- viewBox="0 0 24 24"
929
- fill="none"
930
- stroke="currentColor"
931
- >
932
- <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
933
- </svg>
934
- </button>
935
- {showCornerLoadingIndicator && (
936
- <div
937
- style={{
938
- position: "absolute",
939
- top: -2,
940
- right: -2,
941
- width: 12,
942
- height: 12,
943
- borderRadius: "999px",
944
- background: "rgba(2, 6, 23, 0.95)",
945
- border: "1px solid rgba(139, 92, 246, 0.55)",
946
- display: "flex",
947
- alignItems: "center",
948
- justifyContent: "center",
949
- pointerEvents: "none",
950
- }}
951
- >
952
- <svg
953
- style={aiStyles.spinner}
954
- width="8"
955
- height="8"
956
- viewBox="0 0 24 24"
957
- fill="none"
958
- stroke="#8b5cf6"
959
- strokeWidth="2"
960
- >
961
- <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" />
962
- </svg>
963
- </div>
964
- )}
965
-
966
- {showTooltip &&
967
- canPortal &&
968
- createPortal(
969
- <div
970
- ref={tooltipRef}
971
- style={{
972
- ...aiStyles.tooltip,
973
- ...tooltipPosition,
974
- zIndex: 50,
975
- }}
976
- onMouseEnter={() => setShowTooltip(true)}
977
- onMouseLeave={handleMouseLeave}
978
- >
979
- <div style={aiStyles.tooltipHeader}>API Status</div>
980
-
981
- {/* User Info Section */}
982
- {effectiveStatus.user?.email && (
983
- <div
984
- style={{
985
- ...aiStyles.tooltipSection,
986
- ...aiStyles.tooltipSectionFirst,
987
- }}
988
- >
989
- <div style={aiStyles.tooltipRow}>
990
- <span style={aiStyles.tooltipLabel}>User:</span>
991
- <span
992
- style={{
993
- ...aiStyles.tooltipValue,
994
- fontSize: "12px",
995
- maxWidth: "200px",
996
- overflow: "hidden",
997
- textOverflow: "ellipsis",
998
- }}
999
- >
1000
- {effectiveStatus.user.email}
1001
- </span>
539
+ ) : null}
540
+ </div>
1002
541
  </div>
1003
- </div>
1004
- )}
1005
-
1006
- <div
1007
- style={{
1008
- ...aiStyles.tooltipSection,
1009
- ...(effectiveStatus.user?.email
1010
- ? {}
1011
- : aiStyles.tooltipSectionFirst),
1012
- }}
1013
- >
1014
- <div style={aiStyles.tooltipRow}>
1015
- <span style={aiStyles.tooltipLabel}>API Key:</span>
1016
- <div
1017
- style={{
1018
- display: "flex",
1019
- alignItems: "center",
1020
- gap: "8px",
1021
- }}
1022
- >
1023
- <span style={aiStyles.tooltipValue}>
1024
- {lbIsLoadingStatus ? (
1025
- <div
1026
- style={{
1027
- height: "16px",
1028
- width: "100px",
1029
- background: "rgba(139, 92, 246, 0.1)",
1030
- borderRadius: "4px",
1031
- animation: "pulse 2s ease-in-out infinite",
1032
- }}
1033
- />
1034
- ) : (
1035
- effectiveStatus?.apiKey?.name ||
1036
- effectiveStatus?.api_key?.name ||
1037
- "Unknown"
1038
- )}
1039
- </span>
1040
- {switchApiKey && (
1041
- <button
1042
- onClick={(e) => {
1043
- e.stopPropagation();
1044
- setShowTooltip(false);
1045
- setShowApiKeySelector(true);
1046
- }}
1047
- style={{
1048
- background: "rgba(139, 92, 246, 0.1)",
1049
- border: "1px solid rgba(139, 92, 246, 0.3)",
1050
- borderRadius: "50%",
1051
- width: "24px",
1052
- height: "24px",
1053
- display: "flex",
1054
- alignItems: "center",
1055
- justifyContent: "center",
1056
- cursor: "pointer",
1057
- padding: 0,
1058
- transition: "all 0.2s ease",
1059
- }}
1060
- onMouseEnter={(e) => {
1061
- e.currentTarget.style.background =
1062
- "rgba(139, 92, 246, 0.2)";
1063
- e.currentTarget.style.borderColor =
1064
- "rgba(139, 92, 246, 0.5)";
1065
- }}
1066
- onMouseLeave={(e) => {
1067
- e.currentTarget.style.background =
1068
- "rgba(139, 92, 246, 0.1)";
1069
- e.currentTarget.style.borderColor =
1070
- "rgba(139, 92, 246, 0.3)";
1071
- }}
1072
- title="Change API Key"
1073
- >
1074
- <ArrowRightLeft
1075
- size={12}
1076
- style={{ color: "rgba(139, 92, 246, 1)" }}
1077
- />
1078
- </button>
1079
- )}
542
+ </>
543
+ ) : (
544
+ <div className="ai-popover-body">
545
+ <div className="ai-popover-header">
546
+ LastBrain Authentication
1080
547
  </div>
1081
- </div>
1082
- <div style={aiStyles.tooltipRow}>
1083
- <span style={aiStyles.tooltipLabel}>Env:</span>
1084
- {lbIsLoadingStatus && !effectiveStatus?.apiKey?.env ? (
1085
- <div
1086
- style={{
1087
- height: "16px",
1088
- width: "48px",
1089
- background: "rgba(139, 92, 246, 0.1)",
1090
- borderRadius: "4px",
1091
- animation: "pulse 2s ease-in-out infinite",
1092
- }}
1093
- />
1094
- ) : (
1095
- <span style={aiStyles.tooltipValue}>
1096
- {effectiveStatus.apiKey?.env ||
1097
- effectiveStatus.api_key?.env ||
1098
- "N/A"}
1099
- </span>
1100
- )}
1101
- </div>
1102
- <div style={aiStyles.tooltipRow}>
1103
- <span style={aiStyles.tooltipLabel}>Rate Limit:</span>
1104
- {lbIsLoadingStatus &&
1105
- !effectiveStatus?.apiKey?.rate_limit_rpm &&
1106
- !effectiveStatus?.api_key?.rate_limit_rpm ? (
1107
- <div
1108
- style={{
1109
- height: "16px",
1110
- width: "92px",
1111
- background: "rgba(139, 92, 246, 0.1)",
1112
- borderRadius: "4px",
1113
- animation: "pulse 2s ease-in-out infinite",
1114
- }}
1115
- />
1116
- ) : (
1117
- <span style={aiStyles.tooltipValue}>
1118
- {effectiveStatus.apiKey?.rate_limit_rpm ||
1119
- effectiveStatus.api_key?.rate_limit_rpm ||
1120
- 0}{" "}
1121
- req/min
1122
- </span>
1123
- )}
1124
- </div>
1125
- <div style={aiStyles.tooltipRow}>
1126
- <span style={aiStyles.tooltipLabel}>Auth:</span>
1127
- <span style={aiStyles.tooltipValue}>
1128
- {lbIsLoadingStatus
1129
- ? "..."
1130
- : effectiveStatus?.authType || lbStatus || "unknown"}
1131
- </span>
1132
- </div>
1133
- </div>
1134
-
1135
- <div style={aiStyles.tooltipSection}>
1136
- <div style={aiStyles.tooltipSubtitle}>Wallet</div>
1137
- <div style={aiStyles.tooltipRow}>
1138
- <span style={aiStyles.tooltipLabel}>Total:</span>
1139
- {lbIsLoadingStatus && !effectiveStatus?.balance ? (
1140
- <div
1141
- style={{
1142
- display: "flex",
1143
- alignItems: "center",
1144
- gap: "8px",
1145
- }}
1146
- >
1147
- <div
1148
- style={{
1149
- height: "16px",
1150
- width: "120px",
1151
- background: "rgba(139, 92, 246, 0.1)",
1152
- borderRadius: "4px",
1153
- animation: "pulse 2s ease-in-out infinite",
1154
- }}
1155
- />
1156
- <div
1157
- style={{
1158
- width: "28px",
1159
- height: "28px",
1160
- borderRadius: "50%",
1161
- background: "rgba(139, 92, 246, 0.1)",
1162
- animation: "pulse 2s ease-in-out infinite",
1163
- }}
1164
- />
1165
- </div>
1166
- ) : (
1167
- <>
1168
- <span style={aiStyles.tooltipValue}>
1169
- ${formatFixed(balanceUsed, 2)} / ${formatFixed(balanceTotal, 2)}
1170
- </span>
1171
- {renderUsageCircle(balancePercentage)}
1172
- </>
1173
- )}
1174
- </div>
1175
- </div>
1176
-
1177
- <div style={aiStyles.tooltipSection}>
1178
- <div style={aiStyles.tooltipSubtitle}>Storage</div>
1179
- <div style={aiStyles.tooltipRow}>
1180
- <span style={aiStyles.tooltipLabel}>Total:</span>
1181
- {lbIsLoadingStorage ? (
1182
- <div
1183
- style={{
1184
- display: "flex",
1185
- alignItems: "center",
1186
- gap: "8px",
1187
- }}
1188
- >
1189
- <div
1190
- style={{
1191
- height: "16px",
1192
- width: "120px",
1193
- background: "rgba(139, 92, 246, 0.1)",
1194
- borderRadius: "4px",
1195
- animation: "pulse 2s ease-in-out infinite",
1196
- }}
1197
- />
1198
- <div
1199
- style={{
1200
- width: "28px",
1201
- height: "28px",
1202
- borderRadius: "50%",
1203
- background: "rgba(139, 92, 246, 0.1)",
1204
- animation: "pulse 2s ease-in-out infinite",
1205
- }}
1206
- />
1207
- </div>
1208
- ) : (
1209
- <>
1210
- <span style={aiStyles.tooltipValue}>
1211
- {formatStorage(storageUsed)} /{" "}
1212
- {formatStorage(storageAllocated)}
1213
- </span>
1214
- {renderUsageCircle(storagePercentage)}
1215
- </>
1216
- )}
1217
- </div>
1218
- </div>
1219
-
1220
- <div
1221
- style={{
1222
- ...aiStyles.tooltipActions,
1223
- width: "100%",
1224
-
1225
- flexDirection: "row",
1226
- }}
1227
- >
1228
- <button
1229
- onClick={() =>
1230
- window.open(
1231
- "https://prompt.lastbrain.io/auth/ai/tokens",
1232
- "_blank"
1233
- )
1234
- }
1235
- style={{
1236
- background: "transparent",
1237
- border: "none",
1238
- borderRadius: "4px",
1239
- padding: "14px",
1240
- cursor: "pointer",
1241
- display: "flex",
1242
- alignItems: "center",
1243
- justifyContent: "center",
1244
- color: "#8b5cf6",
1245
- transition: "all 0.2s ease",
1246
- }}
1247
- onMouseEnter={(e) => {
1248
- Object.assign(e.currentTarget.style, {
1249
- background: "rgba(139, 92, 246, 0.1)",
1250
- });
1251
- }}
1252
- onMouseLeave={(e) => {
1253
- Object.assign(e.currentTarget.style, {
1254
- background: "transparent",
1255
- });
1256
- }}
1257
- title="Dashboard"
1258
- >
1259
- <BarChart3 size={18} />
1260
- </button>
1261
- <button
1262
- onClick={() =>
1263
- window.open(
1264
- "https://prompt.lastbrain.io/auth/ai/history",
1265
- "_blank"
1266
- )
1267
- }
1268
- style={{
1269
- background: "transparent",
1270
- border: "none",
1271
- borderRadius: "4px",
1272
- padding: "14px",
1273
- cursor: "pointer",
1274
- display: "flex",
1275
- alignItems: "center",
1276
- justifyContent: "center",
1277
- color: "#8b5cf6",
1278
- transition: "all 0.2s ease",
1279
- }}
1280
- onMouseEnter={(e) => {
1281
- Object.assign(e.currentTarget.style, {
1282
- background: "rgba(139, 92, 246, 0.1)",
1283
- });
1284
- }}
1285
- onMouseLeave={(e) => {
1286
- Object.assign(e.currentTarget.style, {
1287
- background: "transparent",
1288
- });
1289
- }}
1290
- title="History"
1291
- >
1292
- <HistoryIcon size={18} />
1293
- </button>
1294
- <button
1295
- onClick={() =>
1296
- window.open(
1297
- "https://prompt.lastbrain.io/auth/ai/settings",
1298
- "_blank"
1299
- )
1300
- }
1301
- style={{
1302
- background: "transparent",
1303
- border: "none",
1304
- borderRadius: "4px",
1305
- padding: "14px",
1306
- cursor: "pointer",
1307
- display: "flex",
1308
- alignItems: "center",
1309
- justifyContent: "center",
1310
- color: "#8b5cf6",
1311
- transition: "all 0.2s ease",
1312
- }}
1313
- onMouseEnter={(e) => {
1314
- Object.assign(e.currentTarget.style, {
1315
- background: "rgba(139, 92, 246, 0.1)",
1316
- });
1317
- }}
1318
- onMouseLeave={(e) => {
1319
- Object.assign(e.currentTarget.style, {
1320
- background: "transparent",
1321
- });
1322
- }}
1323
- title="Settings"
1324
- >
1325
- <Settings size={18} />
1326
- </button>
1327
- <button
1328
- onClick={() =>
1329
- window.open(
1330
- "https://prompt.lastbrain.io/auth/ai/prompts",
1331
- "_blank"
1332
- )
1333
- }
1334
- style={{
1335
- background: "transparent",
1336
- border: "none",
1337
- borderRadius: "4px",
1338
- padding: "14px",
1339
- cursor: "pointer",
1340
- display: "flex",
1341
- alignItems: "center",
1342
- justifyContent: "center",
1343
- color: "#8b5cf6",
1344
- transition: "all 0.2s ease",
1345
- }}
1346
- onMouseEnter={(e) => {
1347
- Object.assign(e.currentTarget.style, {
1348
- background: "rgba(139, 92, 246, 0.1)",
1349
- });
1350
- }}
1351
- onMouseLeave={(e) => {
1352
- Object.assign(e.currentTarget.style, {
1353
- background: "transparent",
1354
- });
1355
- }}
1356
- title="New Prompt"
1357
- >
1358
- <FileText size={18} />
1359
- </button>
1360
- <button
1361
- onClick={() =>
1362
- window.open(
1363
- "https://prompt.lastbrain.io/auth/folder",
1364
- "_blank"
1365
- )
1366
- }
1367
- style={{
1368
- background: "transparent",
1369
- border: "none",
1370
- padding: "14px",
1371
- cursor: "pointer",
1372
- display: "flex",
1373
- alignItems: "center",
1374
- justifyContent: "center",
1375
- color: "#8b5cf6",
1376
- transition: "all 0.2s ease",
1377
- }}
1378
- onMouseEnter={(e) => {
1379
- Object.assign(e.currentTarget.style, {
1380
- background: "rgba(139, 92, 246, 0.1)",
1381
- });
1382
- }}
1383
- onMouseLeave={(e) => {
1384
- Object.assign(e.currentTarget.style, {
1385
- background: "transparent",
1386
- });
1387
- }}
1388
- title="New Folder"
1389
- >
1390
- <FolderPlus size={18} />
1391
- </button>
1392
-
1393
- {/* Logout Button */}
1394
- {logout && !isApiKeyAuthMode && (
548
+ <p className="ai-signin-subtitle mt-0">
549
+ Connectez-vous pour accéder aux fonctionnalités IA.
550
+ </p>
1395
551
  <button
1396
- onClick={async () => {
1397
- try {
1398
- await logout();
1399
- setShowTooltip(false);
1400
- // Refresh provider data after logout
1401
- if (refetchProviders) {
1402
- await refetchProviders();
1403
- }
1404
- } catch (error) {
1405
- console.error("Logout failed:", error);
1406
- }
1407
- }}
1408
- style={{
1409
- background: "transparent",
1410
- border: "none",
1411
- padding: "14px",
1412
- cursor: "pointer",
1413
- display: "flex",
1414
- alignItems: "center",
1415
- justifyContent: "center",
1416
- color: "#ef4444",
1417
- transition: "all 0.2s ease",
1418
- borderRadius: "4px",
1419
- }}
1420
- onMouseEnter={(e) => {
1421
- Object.assign(e.currentTarget.style, {
1422
- background: "rgba(239, 68, 68, 0.1)",
1423
- });
1424
- }}
1425
- onMouseLeave={(e) => {
1426
- Object.assign(e.currentTarget.style, {
1427
- background: "transparent",
1428
- });
552
+ type="button"
553
+ className="ai-btn ai-btn--auth w-full mt-2"
554
+ onClick={() => {
555
+ setShowSigninModal(true);
556
+ setShowTooltip(false);
1429
557
  }}
1430
- title="Logout"
1431
558
  >
1432
- <LogOut size={18} />
559
+ Se connecter
1433
560
  </button>
1434
- )}
1435
- </div>
561
+ </div>
562
+ )}
1436
563
  </div>,
1437
564
  document.body
1438
- )}
565
+ )
566
+ : null;
567
+
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>
1439
595
 
1440
- {/* API Key Selector Modal */}
1441
- {showApiKeySelector && apiKeys.length > 0 && (
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 ? (
1442
606
  <LBApiKeySelector
1443
607
  isOpen={showApiKeySelector}
1444
608
  apiKeys={apiKeys}
1445
609
  onSelect={async (keyId) => {
1446
610
  setIsSelectingApiKey(true);
1447
611
  try {
1448
- // Utiliser la nouvelle fonction switchApiKey qui gère automatiquement le contexte
1449
- if (switchApiKey) {
1450
- await switchApiKey(keyId);
1451
- } else {
612
+ if (!switchApiKey) {
1452
613
  throw new Error("Switch API key function not available");
1453
614
  }
615
+ await switchApiKey(keyId);
1454
616
  setShowApiKeySelector(false);
1455
617
  setShowTooltip(false);
1456
- setIsLoadingStatus(true);
1457
- // Refresh provider data after API key selection
1458
618
  if (refetchProviders) {
1459
619
  await refetchProviders();
1460
620
  }
@@ -1462,12 +622,16 @@ export function AiStatusButton({
1462
622
  console.error("Failed to select API key:", error);
1463
623
  } finally {
1464
624
  setIsSelectingApiKey(false);
1465
- setIsLoadingStatus(false);
1466
625
  }
1467
626
  }}
1468
627
  onCancel={() => setShowApiKeySelector(false)}
1469
628
  />
1470
- )}
1471
- </div>
629
+ ) : null}
630
+
631
+ <LBSigninModal
632
+ isOpen={showSigninModal}
633
+ onClose={() => setShowSigninModal(false)}
634
+ />
635
+ </>
1472
636
  );
1473
637
  }