@lastbrain/ai-ui-react 1.0.68 → 1.0.70

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 (76) 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 +23 -70
  4. package/dist/components/AiContextButton.d.ts +10 -2
  5. package/dist/components/AiContextButton.d.ts.map +1 -1
  6. package/dist/components/AiContextButton.js +73 -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 +64 -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 +211 -676
  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/context/LBAuthProvider.d.ts +35 -3
  33. package/dist/context/LBAuthProvider.d.ts.map +1 -1
  34. package/dist/context/LBAuthProvider.js +2 -0
  35. package/dist/examples/AiUiPremiumShowcase.d.ts +2 -0
  36. package/dist/examples/AiUiPremiumShowcase.d.ts.map +1 -0
  37. package/dist/examples/AiUiPremiumShowcase.js +15 -0
  38. package/dist/hooks/useAiModels.d.ts.map +1 -1
  39. package/dist/hooks/useModelManagement.d.ts.map +1 -1
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +2 -0
  43. package/dist/styles/inline.d.ts +1 -0
  44. package/dist/styles/inline.d.ts.map +1 -1
  45. package/dist/styles/inline.js +25 -129
  46. package/dist/styles.css +1268 -369
  47. package/dist/types.d.ts +3 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/utils/errorHandler.d.ts +2 -2
  50. package/dist/utils/errorHandler.d.ts.map +1 -1
  51. package/dist/utils/errorHandler.js +8 -1
  52. package/dist/utils/modelManagement.d.ts +13 -10
  53. package/dist/utils/modelManagement.d.ts.map +1 -1
  54. package/dist/utils/modelManagement.js +19 -2
  55. package/package.json +2 -2
  56. package/src/components/AiChipLabel.tsx +68 -101
  57. package/src/components/AiContextButton.tsx +142 -413
  58. package/src/components/AiImageButton.tsx +29 -190
  59. package/src/components/AiInput.tsx +49 -74
  60. package/src/components/AiPromptPanel.tsx +81 -260
  61. package/src/components/AiSelect.tsx +61 -69
  62. package/src/components/AiStatusButton.tsx +496 -1327
  63. package/src/components/AiTextarea.tsx +50 -63
  64. package/src/components/LBApiKeySelector.tsx +93 -271
  65. package/src/components/LBConnectButton.tsx +39 -336
  66. package/src/components/LBSigninModal.tsx +141 -472
  67. package/src/context/LBAuthProvider.tsx +45 -6
  68. package/src/examples/AiUiPremiumShowcase.tsx +94 -0
  69. package/src/hooks/useAiModels.ts +2 -1
  70. package/src/hooks/useModelManagement.ts +2 -1
  71. package/src/index.ts +3 -0
  72. package/src/styles/inline.ts +27 -148
  73. package/src/styles.css +1268 -369
  74. package/src/types.ts +3 -0
  75. package/src/utils/errorHandler.ts +16 -3
  76. package/src/utils/modelManagement.ts +53 -15
@@ -1,60 +1,173 @@
1
1
  "use client";
2
2
 
3
- import type { AiStatus } from "@lastbrain/ai-ui-core";
4
- import { useState, useRef, useLayoutEffect, type CSSProperties } from "react";
3
+ import type { AiStatus, LBUser } from "@lastbrain/ai-ui-core";
4
+ import { useLayoutEffect, useMemo, useRef, useState, useContext } 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
- import { useLB } from "../context/LBAuthProvider";
18
- import { useAiContext } from "../context/AiProvider";
19
- import { LBSigninModal } from "./LBSigninModal";
17
+ import { LBContext, type LBApiKey, type BasicStatus, type StorageStatus } from "../context/LBAuthProvider";
18
+ import { AiContext } from "../context/AiProvider";
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
- let user: any;
152
+ let user: LBUser | null = null;
36
153
  let logout: (() => Promise<void>) | undefined;
37
- let apiKeys: any[] = [];
38
- let accessToken: string | undefined;
39
- let selectApiKeyWithToken: ((apiKeyId: string) => Promise<void>) | undefined;
154
+ let apiKeys: LBApiKey[] = [];
40
155
  let switchApiKey: ((apiKeyId: string) => Promise<void>) | undefined;
41
- let lbApiStatus: any = null;
42
- let lbBasicStatus: any = null;
43
- let lbStorageStatus: any = null;
44
- let lbIsLoadingStatus: boolean = false;
45
- let lbIsLoadingStorage: boolean = false;
46
- let lbSelectedKey: any = null;
156
+ let lbApiStatus: AiStatus | null = null;
157
+ let lbBasicStatus: BasicStatus | null = null;
158
+ let lbStorageStatus: StorageStatus | null = null;
159
+ let lbIsLoadingStatus = false;
160
+ let lbIsLoadingStorage = false;
161
+ let lbSelectedKey: LBApiKey | null = null;
47
162
  let lbRefreshBasicStatus: (() => Promise<void>) | undefined;
48
163
  let lbRefreshStorageStatus: ((force?: boolean) => Promise<void>) | undefined;
49
164
 
50
- try {
51
- const lbContext = useLB();
165
+ const lbContext = useContext(LBContext);
166
+ if (lbContext) {
52
167
  lbStatus = lbContext.status;
53
- user = lbContext.user;
168
+ user = lbContext.user ?? null;
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;
@@ -64,274 +177,99 @@ export function AiStatusButton({
64
177
  lbSelectedKey = lbContext.selectedKey || null;
65
178
  lbRefreshBasicStatus = lbContext.refreshBasicStatus;
66
179
  lbRefreshStorageStatus = lbContext.refreshStorageStatus;
67
- } catch {
68
- // LBProvider n'est pas disponible, ignorer
180
+ } else {
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
184
+ const aiContext = useContext(AiContext);
87
185
  let refetchProviders: (() => Promise<void>) | undefined;
88
- try {
89
- const aiContext = useAiContext();
186
+ if (aiContext) {
90
187
  refetchProviders = aiContext.refetchProviders;
91
- } catch {
92
- // AiProvider n'est pas disponible, ignorer
188
+ } else {
93
189
  refetchProviders = undefined;
94
190
  }
95
191
 
96
192
  const [showSigninModal, setShowSigninModal] = useState(false);
97
193
  const [showApiKeySelector, setShowApiKeySelector] = useState(false);
194
+ const [showTooltip, setShowTooltip] = useState(false);
98
195
  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
-
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
196
 
219
- const [showTooltip, setShowTooltip] = useState(false);
220
- const [isHovered, setIsHovered] = useState(false);
221
- const [tooltipPosition, setTooltipPosition] = useState<CSSProperties>({});
222
197
  const buttonRef = useRef<HTMLButtonElement>(null);
223
198
  const tooltipRef = useRef<HTMLDivElement>(null);
199
+
224
200
  const canPortal = typeof document !== "undefined";
201
+
202
+ const effectiveStatus =
203
+ lbStatus === "ready"
204
+ ? {
205
+ ...(lbApiStatus || {}),
206
+ ...(lbBasicStatus || {}),
207
+ storage: lbStorageStatus?.storage || lbApiStatus?.storage,
208
+ }
209
+ : status || null;
210
+
225
211
  const hasApiKeySelected = Boolean(
226
- effectiveStatus?.apiKey?.id || effectiveStatus?.api_key?.id || lbSelectedKey?.id
212
+ effectiveStatus?.apiKey?.id ||
213
+ effectiveStatus?.api_key?.id ||
214
+ lbSelectedKey?.id
227
215
  );
228
- const isApiKeyAuthMode = effectiveStatus?.authType === "api_key";
229
216
  const requiresApiKeySelection =
230
217
  lbStatus === "ready" && !hasApiKeySelected && apiKeys.length > 0;
218
+ const isApiKeyAuthMode = effectiveStatus && 'authType' in effectiveStatus && effectiveStatus.authType === "api_key";
219
+
220
+ const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
231
221
 
232
222
  useLayoutEffect(() => {
233
- if (!showTooltip || !buttonRef.current) {
223
+ if (!showTooltip || !buttonRef.current || !canPortal) {
234
224
  return;
235
225
  }
236
226
 
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
- }
227
+ const update = () => {
228
+ if (!buttonRef.current) return;
229
+ const rect = buttonRef.current.getBoundingClientRect();
230
+ const tipRect = tooltipRef.current?.getBoundingClientRect();
231
+ const tipWidth = tipRect?.width ?? 360;
232
+ const tipHeight = tipRect?.height ?? 520;
233
+ const margin = 8;
234
+
235
+ const viewportW = window.innerWidth;
236
+ const viewportH = window.innerHeight;
237
+
238
+ const placeBelow = viewportH - rect.bottom >= rect.top;
239
+ let top = placeBelow
240
+ ? rect.bottom + margin
241
+ : rect.top - tipHeight - margin;
242
+ top = Math.max(margin, Math.min(top, viewportH - tipHeight - margin));
243
+
244
+ const placeRight = viewportW - rect.right >= rect.left;
245
+ let left = placeRight ? rect.left : rect.right - tipWidth;
246
+ left = Math.max(margin, Math.min(left, viewportW - tipWidth - margin));
247
+
248
+ setTooltipStyle({
249
+ position: "fixed",
250
+ top: `${top}px`,
251
+ left: `${left}px`,
252
+ });
290
253
  };
291
254
 
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);
255
+ const raf1 = requestAnimationFrame(update);
256
+ const raf2 = requestAnimationFrame(update);
257
+ window.addEventListener("resize", update);
258
+ window.addEventListener("scroll", update, true);
298
259
 
299
260
  return () => {
300
- cancelAnimationFrame(rafId);
301
- cancelAnimationFrame(rafId2);
302
- window.removeEventListener("resize", handleResize);
303
- window.removeEventListener("scroll", handleResize, true);
261
+ cancelAnimationFrame(raf1);
262
+ cancelAnimationFrame(raf2);
263
+ window.removeEventListener("resize", update);
264
+ window.removeEventListener("scroll", update, true);
304
265
  };
305
266
  }, [showTooltip, canPortal]);
306
267
 
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
268
  useLayoutEffect(() => {
332
- if (!showTooltip || requiresApiKeySelection || lbStatus !== "ready") {
269
+ if (!showTooltip || lbStatus !== "ready" || requiresApiKeySelection) {
333
270
  return;
334
271
  }
272
+
335
273
  if (!lbBasicStatus && !lbIsLoadingStatus && lbRefreshBasicStatus) {
336
274
  lbRefreshBasicStatus().catch(() => undefined);
337
275
  }
@@ -344,8 +282,8 @@ export function AiStatusButton({
344
282
  }
345
283
  }, [
346
284
  showTooltip,
347
- requiresApiKeySelection,
348
285
  lbStatus,
286
+ requiresApiKeySelection,
349
287
  lbBasicStatus,
350
288
  lbStorageStatus,
351
289
  lbIsLoadingStatus,
@@ -354,1107 +292,334 @@ export function AiStatusButton({
354
292
  lbRefreshStorageStatus,
355
293
  ]);
356
294
 
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
- }
295
+ const balance = (effectiveStatus?.balance || {}) as BalanceUsage;
296
+ const storage = (effectiveStatus?.storage || {}) as StorageUsage;
381
297
 
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
- }
298
+ const balanceUsed = num(balance.used);
299
+ const balanceRemaining = num(balance.remaining);
300
+ const rawBalanceTotal =
301
+ balance.total ?? num(balance.purchased) + num(balance.quota);
302
+ const balanceTotal =
303
+ rawBalanceTotal > 0 ? rawBalanceTotal : balanceUsed + balanceRemaining;
304
+ const balancePct =
305
+ balance.percentage ??
306
+ (balanceTotal > 0 ? Math.round((balanceUsed / balanceTotal) * 100) : 0);
464
307
 
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
- );
308
+ const storageUsed = storage.used_mb ?? storage.total_mb ?? 0;
309
+ const storageTotal = storage.allocated_mb ?? storage.total_mb ?? 0;
310
+ const storagePct =
311
+ storage.percentage ??
312
+ (storageTotal > 0 ? Math.round((storageUsed / storageTotal) * 100) : 0);
313
+
314
+ const showFastSkeleton =
315
+ lbStatus === "ready" &&
316
+ lbIsLoadingStatus &&
317
+ !lbBasicStatus &&
318
+ !effectiveStatus;
319
+
320
+ const showCornerLoading =
321
+ lbStatus === "ready" &&
322
+ (showFastSkeleton || lbIsLoadingStatus || lbIsLoadingStorage);
323
+
324
+ const triggerTone = useMemo(() => {
325
+ if (requiresApiKeySelection) return "warning";
326
+ if (lbStatus && lbStatus !== "ready") return "danger";
327
+ if (effectiveStatus) return "success";
328
+ return "neutral";
329
+ }, [requiresApiKeySelection, lbStatus, effectiveStatus]);
330
+
331
+ const openTooltip = () => {
332
+ if (requiresApiKeySelection) {
333
+ return;
334
+ }
335
+ setShowTooltip(true);
336
+ };
337
+
338
+ const closeTooltip = () => {
339
+ if (requiresApiKeySelection) {
340
+ return;
510
341
  }
342
+ setTimeout(() => {
343
+ if (
344
+ !tooltipRef.current?.matches(":hover") &&
345
+ !buttonRef.current?.matches(":hover")
346
+ ) {
347
+ setShowTooltip(false);
348
+ }
349
+ }, 100);
350
+ };
511
351
 
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
- }}
352
+ const renderTriggerIcon = () => {
353
+ if (loading || isSelectingApiKey) {
354
+ return <Loader2 size={14} className="ai-spinner" />;
355
+ }
356
+ return <Shield size={14} />;
357
+ };
358
+
359
+ const triggerClass = [
360
+ "ai-status-trigger",
361
+ `ai-size-${size}`,
362
+ `ai-radius-${radius}`,
363
+ triggerTone === "warning" ? "ai-status-trigger--warning" : "",
364
+ triggerTone === "danger" ? "ai-status-trigger--danger" : "",
365
+ triggerTone === "success" ? "ai-status-trigger--success" : "",
366
+ className,
367
+ ]
368
+ .filter(Boolean)
369
+ .join(" ");
370
+
371
+ const tooltipNode =
372
+ showTooltip && canPortal
373
+ ? createPortal(
374
+ <div
375
+ ref={tooltipRef}
376
+ className="ai-popover ai-tooltip ai-status-tooltip"
377
+ style={tooltipStyle}
378
+ onMouseEnter={() => setShowTooltip(true)}
379
+ onMouseLeave={closeTooltip}
530
380
  >
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>
381
+ {lbStatus === "ready" && user ? (
382
+ <>
383
+ <div className="ai-popover-body">
384
+ <div className="ai-popover-header">API Status</div>
385
+ <div className="ai-popover-section ai-popover-section--first">
386
+ <div className="ai-popover-row">
387
+ <span className="ai-popover-label">User</span>
388
+ <span className="ai-popover-value ai-truncate max-w-[200px]">
389
+ {user.email}
390
+ </span>
391
+ </div>
392
+ </div>
393
+
394
+ <div className="ai-popover-section">
395
+ <div className="ai-popover-row">
396
+ <span className="ai-popover-label">API Key</span>
397
+ <div className="ai-row">
398
+ {lbIsLoadingStatus ? (
399
+ <div className="ai-kv-skeleton w-[110px]" />
400
+ ) : (
401
+ <span className="ai-popover-value">
402
+ {effectiveStatus?.apiKey?.name ||
403
+ effectiveStatus?.api_key?.name ||
404
+ "Unknown"}
405
+ </span>
406
+ )}
407
+ {switchApiKey ? (
408
+ <button
409
+ type="button"
410
+ className="ai-icon-btn"
411
+ onClick={(e) => {
412
+ e.stopPropagation();
413
+ setShowTooltip(false);
414
+ setShowApiKeySelector(true);
415
+ }}
416
+ title="Changer de clé API"
417
+ >
418
+ <ArrowRightLeft size={12} />
419
+ </button>
420
+ ) : null}
596
421
  </div>
597
422
  </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>
423
+
424
+ <div className="ai-popover-row">
425
+ <span className="ai-popover-label">Env</span>
426
+ {lbIsLoadingStatus && !effectiveStatus?.apiKey?.env ? (
427
+ <div className="ai-kv-skeleton w-12" />
428
+ ) : (
429
+ <span className="ai-popover-value">
430
+ {effectiveStatus?.apiKey?.env ||
431
+ effectiveStatus?.api_key?.env ||
432
+ "N/A"}
433
+ </span>
434
+ )}
435
+ </div>
436
+
437
+ <div className="ai-popover-row">
438
+ <span className="ai-popover-label">Rate Limit</span>
439
+ {lbIsLoadingStatus &&
440
+ !effectiveStatus?.apiKey?.rate_limit_rpm ? (
441
+ <div className="ai-kv-skeleton w-[92px]" />
442
+ ) : (
443
+ <span className="ai-popover-value">
444
+ {effectiveStatus?.apiKey?.rate_limit_rpm ||
445
+ effectiveStatus?.api_key?.rate_limit_rpm ||
446
+ 0}{" "}
447
+ req/min
448
+ </span>
449
+ )}
450
+ </div>
451
+
452
+ <div className="ai-popover-row">
453
+ <span className="ai-popover-label">Auth</span>
454
+ <span className="ai-popover-value">
455
+ {lbIsLoadingStatus
456
+ ? "..."
457
+ : (effectiveStatus &&
458
+ "authType" in effectiveStatus &&
459
+ effectiveStatus.authType) ||
460
+ lbStatus ||
461
+ "unknown"}
462
+ </span>
463
+ </div>
464
+ </div>
465
+
466
+ <div className="ai-popover-section">
467
+ <div className="ai-popover-header text-xs mb-2">Wallet</div>
468
+ <div className="ai-popover-row">
469
+ <span className="ai-popover-label">Total</span>
470
+ {lbIsLoadingStatus && !effectiveStatus?.balance ? (
471
+ <div className="ai-row">
472
+ <div className="ai-kv-skeleton w-[120px]" />
473
+ <div className="ai-kv-skeleton w-7 h-7 rounded-full" />
474
+ </div>
475
+ ) : (
476
+ <div className="ai-row">
477
+ <span className="ai-popover-value">
478
+ ${fixed(balanceUsed, 2)} / ${fixed(balanceTotal, 2)}
479
+ </span>
480
+ <UsageCircle percentage={balancePct} />
481
+ </div>
482
+ )}
483
+ </div>
484
+ </div>
485
+
486
+ <div className="ai-popover-section">
487
+ <div className="ai-popover-header text-xs mb-2">
488
+ Storage
489
+ </div>
490
+ <div className="ai-popover-row">
491
+ <span className="ai-popover-label">Total</span>
492
+ {lbIsLoadingStorage ? (
493
+ <div className="ai-row">
494
+ <div className="ai-kv-skeleton w-[120px]" />
495
+ <div className="ai-kv-skeleton w-7 h-7 rounded-full" />
649
496
  </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>
497
+ ) : (
498
+ <div className="ai-row">
499
+ <span className="ai-popover-value">
500
+ {formatStorage(storageUsed)} /{" "}
501
+ {formatStorage(storageTotal)}
502
+ </span>
503
+ <UsageCircle percentage={storagePct} />
664
504
  </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>
505
+ )}
506
+ </div>
507
+ </div>
508
+
509
+ <div className="ai-status-actions">
510
+ {QUICK_LINKS.map((item) => {
511
+ const Icon = item.icon;
512
+ return (
513
+ <button
514
+ key={item.href}
515
+ type="button"
516
+ className="ai-status-action-btn"
517
+ onClick={() => window.open(item.href, "_blank")}
518
+ title={item.title}
519
+ >
520
+ <Icon size={17} />
521
+ </button>
522
+ );
523
+ })}
524
+
525
+ {logout && !isApiKeyAuthMode ? (
808
526
  <button
527
+ type="button"
528
+ className="ai-status-action-btn ai-status-action-btn--danger"
809
529
  onClick={async () => {
810
- if (logout) {
530
+ try {
811
531
  await logout();
812
- // Refresh provider data after logout
532
+ setShowTooltip(false);
813
533
  if (refetchProviders) {
814
534
  await refetchProviders();
815
535
  }
536
+ } catch (error) {
537
+ console.error("Logout failed:", error);
816
538
  }
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
539
  }}
841
540
  title="Logout"
842
541
  >
843
- <Power size={18} />
542
+ <LogOut size={17} />
844
543
  </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>
544
+ ) : null}
545
+ </div>
1002
546
  </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
- )}
547
+ </>
548
+ ) : (
549
+ <div className="ai-popover-body">
550
+ <div className="ai-popover-header">
551
+ LastBrain Authentication
1080
552
  </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 && (
553
+ <p className="ai-signin-subtitle mt-0">
554
+ Connectez-vous pour accéder aux fonctionnalités IA.
555
+ </p>
1395
556
  <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
- });
557
+ type="button"
558
+ className="ai-btn ai-btn--auth w-full mt-2"
559
+ onClick={() => {
560
+ setShowSigninModal(true);
561
+ setShowTooltip(false);
1429
562
  }}
1430
- title="Logout"
1431
563
  >
1432
- <LogOut size={18} />
564
+ Se connecter
1433
565
  </button>
1434
- )}
1435
- </div>
566
+ </div>
567
+ )}
1436
568
  </div>,
1437
569
  document.body
1438
- )}
570
+ )
571
+ : null;
572
+
573
+ return (
574
+ <>
575
+ <div className="relative inline-block">
576
+ <button
577
+ ref={buttonRef}
578
+ className={triggerClass}
579
+ onMouseEnter={openTooltip}
580
+ onMouseLeave={closeTooltip}
581
+ onClick={() => {
582
+ if (requiresApiKeySelection) {
583
+ setShowApiKeySelector(true);
584
+ return;
585
+ }
586
+ if (!effectiveStatus && lbStatus !== "ready") {
587
+ setShowSigninModal(true);
588
+ }
589
+ }}
590
+ disabled={loading || isSelectingApiKey}
591
+ title={
592
+ requiresApiKeySelection
593
+ ? "Sélectionnez une clé API"
594
+ : "Voir le status"
595
+ }
596
+ aria-label="AI status"
597
+ >
598
+ {renderTriggerIcon()}
599
+ </button>
1439
600
 
1440
- {/* API Key Selector Modal */}
1441
- {showApiKeySelector && apiKeys.length > 0 && (
601
+ {showCornerLoading ? (
602
+ <span className="ai-status-loading-dot" aria-hidden="true">
603
+ <Loader2 size={7} className="ai-spinner text-[var(--ai-primary)]" />
604
+ </span>
605
+ ) : null}
606
+ </div>
607
+
608
+ {tooltipNode}
609
+
610
+ {showApiKeySelector && apiKeys.length > 0 ? (
1442
611
  <LBApiKeySelector
1443
612
  isOpen={showApiKeySelector}
1444
613
  apiKeys={apiKeys}
1445
614
  onSelect={async (keyId) => {
1446
615
  setIsSelectingApiKey(true);
1447
616
  try {
1448
- // Utiliser la nouvelle fonction switchApiKey qui gère automatiquement le contexte
1449
- if (switchApiKey) {
1450
- await switchApiKey(keyId);
1451
- } else {
617
+ if (!switchApiKey) {
1452
618
  throw new Error("Switch API key function not available");
1453
619
  }
620
+ await switchApiKey(keyId);
1454
621
  setShowApiKeySelector(false);
1455
622
  setShowTooltip(false);
1456
- setIsLoadingStatus(true);
1457
- // Refresh provider data after API key selection
1458
623
  if (refetchProviders) {
1459
624
  await refetchProviders();
1460
625
  }
@@ -1462,12 +627,16 @@ export function AiStatusButton({
1462
627
  console.error("Failed to select API key:", error);
1463
628
  } finally {
1464
629
  setIsSelectingApiKey(false);
1465
- setIsLoadingStatus(false);
1466
630
  }
1467
631
  }}
1468
632
  onCancel={() => setShowApiKeySelector(false)}
1469
633
  />
1470
- )}
1471
- </div>
634
+ ) : null}
635
+
636
+ <LBSigninModal
637
+ isOpen={showSigninModal}
638
+ onClose={() => setShowSigninModal(false)}
639
+ />
640
+ </>
1472
641
  );
1473
642
  }