@lastbrain/ai-ui-react 1.0.46 → 1.0.48

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.
@@ -1 +1 @@
1
- {"version":3,"file":"AiStatusButton.d.ts","sourceRoot":"","sources":["../../src/components/AiStatusButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAetD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,OAAe,EACf,SAAc,GACf,EAAE,mBAAmB,2CAk2BrB"}
1
+ {"version":3,"file":"AiStatusButton.d.ts","sourceRoot":"","sources":["../../src/components/AiStatusButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAoBtD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,OAAe,EACf,SAAc,GACf,EAAE,mBAAmB,2CAiiCrB"}
@@ -2,20 +2,28 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState, useRef, useLayoutEffect } from "react";
4
4
  import { createPortal } from "react-dom";
5
- import { BarChart3, Settings, FileText, History as HistoryIcon, FolderPlus, Power, } from "lucide-react";
5
+ import { BarChart3, Settings, FileText, History as HistoryIcon, FolderPlus, Power, LogOut, Key, RefreshCw, } from "lucide-react";
6
6
  import { aiStyles, calculateTooltipPosition } from "../styles/inline";
7
7
  import { useLB } from "../context/LBAuthProvider";
8
+ import { useAiContext } from "../context/AiProvider";
8
9
  import { LBSigninModal } from "./LBSigninModal";
10
+ import { LBApiKeySelector } from "./LBApiKeySelector";
9
11
  export function AiStatusButton({ status, loading = false, className = "", }) {
10
12
  // Rendre l'authentification optionnelle
11
13
  let lbStatus;
12
14
  let user;
13
15
  let logout;
16
+ let apiKeys = [];
17
+ let accessToken;
18
+ let selectApiKeyWithToken;
14
19
  try {
15
20
  const lbContext = useLB();
16
21
  lbStatus = lbContext.status;
17
22
  user = lbContext.user;
18
23
  logout = lbContext.logout;
24
+ apiKeys = lbContext.apiKeys || [];
25
+ accessToken = lbContext.accessToken;
26
+ selectApiKeyWithToken = lbContext.selectApiKeyWithToken;
19
27
  }
20
28
  catch {
21
29
  // LBProvider n'est pas disponible, ignorer
@@ -23,7 +31,18 @@ export function AiStatusButton({ status, loading = false, className = "", }) {
23
31
  user = undefined;
24
32
  logout = undefined;
25
33
  }
34
+ // Récupérer refetchProviders depuis AiProvider si disponible
35
+ let refetchProviders;
36
+ try {
37
+ const aiContext = useAiContext();
38
+ refetchProviders = aiContext.refetchProviders;
39
+ }
40
+ catch {
41
+ // AiProvider n'est pas disponible, ignorer
42
+ refetchProviders = undefined;
43
+ }
26
44
  const [showSigninModal, setShowSigninModal] = useState(false);
45
+ const [showApiKeySelector, setShowApiKeySelector] = useState(false);
27
46
  const formatNumber = (value) => typeof value === "number" ? value.toLocaleString() : "0";
28
47
  const formatFixed = (value, digits) => typeof value === "number" ? value.toFixed(digits) : "0.00";
29
48
  const formatStorage = (valueMb) => {
@@ -273,6 +292,10 @@ export function AiStatusButton({ status, loading = false, className = "", }) {
273
292
  }, title: "New Folder", children: _jsx(FolderPlus, { size: 18 }) }), _jsx("button", { onClick: async () => {
274
293
  if (logout) {
275
294
  await logout();
295
+ // Refresh provider data after logout
296
+ if (refetchProviders) {
297
+ await refetchProviders();
298
+ }
276
299
  }
277
300
  setShowTooltip(false);
278
301
  }, style: {
@@ -335,10 +358,50 @@ export function AiStatusButton({ status, loading = false, className = "", }) {
335
358
  ...aiStyles.tooltip,
336
359
  ...tooltipPosition,
337
360
  zIndex: 50,
338
- }, onMouseEnter: () => setShowTooltip(true), onMouseLeave: handleMouseLeave, children: [_jsx("div", { style: aiStyles.tooltipHeader, children: "API Status" }), _jsxs("div", { style: {
361
+ }, onMouseEnter: () => setShowTooltip(true), onMouseLeave: handleMouseLeave, children: [_jsx("div", { style: aiStyles.tooltipHeader, children: "API Status" }), status.user?.email && (_jsx("div", { style: {
339
362
  ...aiStyles.tooltipSection,
340
363
  ...aiStyles.tooltipSectionFirst,
341
- }, children: [_jsxs("div", { style: aiStyles.tooltipRow, children: [_jsx("span", { style: aiStyles.tooltipLabel, children: "API Key:" }), _jsx("span", { style: aiStyles.tooltipValue, children: status.api_key?.name || "N/A" })] }), _jsxs("div", { style: aiStyles.tooltipRow, children: [_jsx("span", { style: aiStyles.tooltipLabel, children: "Env:" }), _jsx("span", { style: aiStyles.tooltipValue, children: status.api_key?.env || "N/A" })] }), _jsxs("div", { style: aiStyles.tooltipRow, children: [_jsx("span", { style: aiStyles.tooltipLabel, children: "Rate Limit:" }), _jsxs("span", { style: aiStyles.tooltipValue, children: [status.api_key?.rate_limit_rpm || 0, " req/min"] })] })] }), _jsxs("div", { style: aiStyles.tooltipSection, children: [_jsx("div", { style: aiStyles.tooltipSubtitle, children: "Balance" }), _jsxs("div", { style: aiStyles.tooltipRow, children: [_jsx("span", { style: aiStyles.tooltipLabel, children: "Total:" }), _jsxs("span", { style: aiStyles.tooltipValue, children: ["$", formatFixed(balanceUsed, 6), " / $", formatNumber(balanceTotal)] }), renderUsageCircle(balancePercentage)] })] }), _jsxs("div", { style: aiStyles.tooltipSection, children: [_jsx("div", { style: aiStyles.tooltipSubtitle, children: "Storage" }), _jsxs("div", { style: aiStyles.tooltipRow, children: [_jsx("span", { style: aiStyles.tooltipLabel, children: "Total:" }), _jsxs("span", { style: aiStyles.tooltipValue, children: [formatStorage(storageUsed), " /", " ", formatStorage(storageAllocated)] }), renderUsageCircle(storagePercentage)] })] }), _jsxs("div", { style: {
364
+ }, children: _jsxs("div", { style: aiStyles.tooltipRow, children: [_jsx("span", { style: aiStyles.tooltipLabel, children: "User:" }), _jsx("span", { style: {
365
+ ...aiStyles.tooltipValue,
366
+ fontSize: "12px",
367
+ maxWidth: "200px",
368
+ overflow: "hidden",
369
+ textOverflow: "ellipsis",
370
+ }, children: status.user.email })] }) })), _jsxs("div", { style: {
371
+ ...aiStyles.tooltipSection,
372
+ ...(status.user?.email ? {} : aiStyles.tooltipSectionFirst),
373
+ }, children: [_jsxs("div", { style: aiStyles.tooltipRow, children: [_jsx("span", { style: aiStyles.tooltipLabel, children: "API Key:" }), _jsxs("div", { style: {
374
+ display: "flex",
375
+ alignItems: "center",
376
+ gap: "8px",
377
+ }, children: [_jsx("span", { style: aiStyles.tooltipValue, children: status.apiKey?.name || status.api_key?.name || "Unknown" }), apiKeys.length > 1 && selectApiKeyWithToken && (_jsx("button", { onClick: (e) => {
378
+ e.stopPropagation();
379
+ setShowApiKeySelector(true);
380
+ }, style: {
381
+ background: "rgba(139, 92, 246, 0.1)",
382
+ border: "1px solid rgba(139, 92, 246, 0.3)",
383
+ borderRadius: "50%",
384
+ width: "24px",
385
+ height: "24px",
386
+ display: "flex",
387
+ alignItems: "center",
388
+ justifyContent: "center",
389
+ cursor: "pointer",
390
+ padding: 0,
391
+ transition: "all 0.2s ease",
392
+ }, onMouseEnter: (e) => {
393
+ e.currentTarget.style.background =
394
+ "rgba(139, 92, 246, 0.2)";
395
+ e.currentTarget.style.borderColor =
396
+ "rgba(139, 92, 246, 0.5)";
397
+ }, onMouseLeave: (e) => {
398
+ e.currentTarget.style.background =
399
+ "rgba(139, 92, 246, 0.1)";
400
+ e.currentTarget.style.borderColor =
401
+ "rgba(139, 92, 246, 0.3)";
402
+ }, title: "Change API Key", children: _jsx(RefreshCw, { size: 12, style: { color: "rgba(139, 92, 246, 1)" } }) }))] })] }), _jsxs("div", { style: aiStyles.tooltipRow, children: [_jsx("span", { style: aiStyles.tooltipLabel, children: "Env:" }), _jsx("span", { style: aiStyles.tooltipValue, children: status.apiKey?.env || status.api_key?.env || "N/A" })] }), _jsxs("div", { style: aiStyles.tooltipRow, children: [_jsx("span", { style: aiStyles.tooltipLabel, children: "Rate Limit:" }), _jsxs("span", { style: aiStyles.tooltipValue, children: [status.apiKey?.rate_limit_rpm ||
403
+ status.api_key?.rate_limit_rpm ||
404
+ 0, " ", "req/min"] })] })] }), _jsxs("div", { style: aiStyles.tooltipSection, children: [_jsx("div", { style: aiStyles.tooltipSubtitle, children: "Balance" }), _jsxs("div", { style: aiStyles.tooltipRow, children: [_jsx("span", { style: aiStyles.tooltipLabel, children: "Total:" }), _jsxs("span", { style: aiStyles.tooltipValue, children: ["$", formatFixed(balanceUsed, 6), " / $", formatNumber(balanceTotal)] }), renderUsageCircle(balancePercentage)] })] }), _jsxs("div", { style: aiStyles.tooltipSection, children: [_jsx("div", { style: aiStyles.tooltipSubtitle, children: "Storage" }), _jsxs("div", { style: aiStyles.tooltipRow, children: [_jsx("span", { style: aiStyles.tooltipLabel, children: "Total:" }), _jsxs("span", { style: aiStyles.tooltipValue, children: [formatStorage(storageUsed), " /", " ", formatStorage(storageAllocated)] }), renderUsageCircle(storagePercentage)] })] }), _jsxs("div", { style: {
342
405
  ...aiStyles.tooltipActions,
343
406
  width: "100%",
344
407
  flexDirection: "row",
@@ -436,5 +499,66 @@ export function AiStatusButton({ status, loading = false, className = "", }) {
436
499
  Object.assign(e.currentTarget.style, {
437
500
  background: "transparent",
438
501
  });
439
- }, title: "New Folder", children: _jsx(FolderPlus, { size: 18 }) })] })] }), document.body)] }));
502
+ }, title: "New Folder", children: _jsx(FolderPlus, { size: 18 }) }), selectApiKeyWithToken && apiKeys.length > 1 && (_jsx("button", { onClick: () => setShowApiKeySelector(true), style: {
503
+ background: "transparent",
504
+ border: "none",
505
+ padding: "14px",
506
+ cursor: "pointer",
507
+ display: "flex",
508
+ alignItems: "center",
509
+ justifyContent: "center",
510
+ color: "#8b5cf6",
511
+ transition: "all 0.2s ease",
512
+ }, onMouseEnter: (e) => {
513
+ Object.assign(e.currentTarget.style, {
514
+ background: "rgba(139, 92, 246, 0.1)",
515
+ });
516
+ }, onMouseLeave: (e) => {
517
+ Object.assign(e.currentTarget.style, {
518
+ background: "transparent",
519
+ });
520
+ }, title: "Change API Key", children: _jsx(Key, { size: 18 }) })), logout && (_jsx("button", { onClick: async () => {
521
+ try {
522
+ await logout();
523
+ setShowTooltip(false);
524
+ // Refresh provider data after logout
525
+ if (refetchProviders) {
526
+ await refetchProviders();
527
+ }
528
+ }
529
+ catch (error) {
530
+ console.error("Logout failed:", error);
531
+ }
532
+ }, style: {
533
+ background: "transparent",
534
+ border: "none",
535
+ padding: "14px",
536
+ cursor: "pointer",
537
+ display: "flex",
538
+ alignItems: "center",
539
+ justifyContent: "center",
540
+ color: "#ef4444",
541
+ transition: "all 0.2s ease",
542
+ }, onMouseEnter: (e) => {
543
+ Object.assign(e.currentTarget.style, {
544
+ background: "rgba(239, 68, 68, 0.1)",
545
+ });
546
+ }, onMouseLeave: (e) => {
547
+ Object.assign(e.currentTarget.style, {
548
+ background: "transparent",
549
+ });
550
+ }, title: "Logout", children: _jsx(LogOut, { size: 18 }) }))] })] }), document.body), showApiKeySelector && selectApiKeyWithToken && (_jsx(LBApiKeySelector, { isOpen: showApiKeySelector, apiKeys: apiKeys, onSelect: async (keyId) => {
551
+ try {
552
+ await selectApiKeyWithToken(keyId);
553
+ setShowApiKeySelector(false);
554
+ setShowTooltip(false);
555
+ // Refresh provider data after API key selection
556
+ if (refetchProviders) {
557
+ await refetchProviders();
558
+ }
559
+ }
560
+ catch (error) {
561
+ console.error("Failed to select API key:", error);
562
+ }
563
+ }, onCancel: () => setShowApiKeySelector(false) }))] }));
440
564
  }
@@ -12,6 +12,8 @@ interface LBProviderProps {
12
12
  proxyUrl?: string;
13
13
  /** Fonction appelée lors des changements d'état */
14
14
  onStatusChange?: (status: LBAuthState["status"]) => void;
15
+ /** Fonction appelée après signin/logout pour refresh les providers */
16
+ onAuthChange?: () => void;
15
17
  }
16
18
  interface LBContextValue extends LBAuthState {
17
19
  /** Fonction de connexion */
@@ -35,7 +37,7 @@ interface LBContextValue extends LBAuthState {
35
37
  /** Access token temporaire (après login) */
36
38
  accessToken?: string;
37
39
  }
38
- export declare function LBProvider({ children, baseUrl: _baseUrl, proxyUrl, onStatusChange, }: LBProviderProps): import("react/jsx-runtime").JSX.Element;
40
+ export declare function LBProvider({ children, baseUrl: _baseUrl, proxyUrl, onStatusChange, onAuthChange, }: LBProviderProps): import("react/jsx-runtime").JSX.Element;
39
41
  /**
40
42
  * Hook pour accéder au contexte LastBrain
41
43
  */
@@ -1 +1 @@
1
- {"version":3,"file":"LBAuthProvider.d.ts","sourceRoot":"","sources":["../../src/context/LBAuthProvider.tsx"],"names":[],"mappings":"AAEA;;;GAGG;AAEH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EAIT,MAAM,uBAAuB,CAAC;AAE/B,UAAU,eAAe;IACvB,QAAQ,EAAE,SAAS,CAAC;IACpB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;CAC1D;AAED,UAAU,cAAe,SAAQ,WAAW;IAC1C,4BAA4B;IAC5B,KAAK,EAAE,CACL,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC;QACX,OAAO,EAAE,OAAO,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,CAAC,CAAC;IACH,8BAA8B;IAC9B,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,6CAA6C;IAC7C,YAAY,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC3D,gEAAgE;IAChE,YAAY,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,mDAAmD;IACnD,qBAAqB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,oCAAoC;IACpC,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,2BAA2B;IAC3B,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,OAAO,EAAE,QAA2B,EACpC,QAA2B,EAC3B,cAAc,GACf,EAAE,eAAe,2CA0UjB;AAED;;GAEG;AACH,wBAAgB,KAAK,IAAI,cAAc,CAMtC"}
1
+ {"version":3,"file":"LBAuthProvider.d.ts","sourceRoot":"","sources":["../../src/context/LBAuthProvider.tsx"],"names":[],"mappings":"AAEA;;;GAGG;AAEH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EAIT,MAAM,uBAAuB,CAAC;AAE/B,UAAU,eAAe;IACvB,QAAQ,EAAE,SAAS,CAAC;IACpB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;IACzD,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED,UAAU,cAAe,SAAQ,WAAW;IAC1C,4BAA4B;IAC5B,KAAK,EAAE,CACL,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC;QACX,OAAO,EAAE,OAAO,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,CAAC,CAAC;IACH,8BAA8B;IAC9B,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,6CAA6C;IAC7C,YAAY,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC3D,gEAAgE;IAChE,YAAY,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,mDAAmD;IACnD,qBAAqB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,oCAAoC;IACpC,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,2BAA2B;IAC3B,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,OAAO,EAAE,QAA2B,EACpC,QAA2B,EAC3B,cAAc,EACd,YAAY,GACb,EAAE,eAAe,2CA4UjB;AAED;;GAEG;AACH,wBAAgB,KAAK,IAAI,cAAc,CAMtC"}
@@ -6,7 +6,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
6
6
  */
7
7
  import { createContext, useContext, useEffect, useCallback, useState, } from "react";
8
8
  const LBContext = createContext(undefined);
9
- export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", proxyUrl = "/api/lastbrain", onStatusChange, }) {
9
+ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", proxyUrl = "/api/lastbrain", onStatusChange, onAuthChange, }) {
10
10
  const [state, setState] = useState({
11
11
  status: "loading",
12
12
  });
@@ -114,6 +114,7 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
114
114
  setAccessToken(undefined); // Nettoyer l'access token temporaire
115
115
  setApiKeys([]); // Nettoyer les clés API temporaires
116
116
  onStatusChange?.("ready");
117
+ onAuthChange?.(); // Refresh provider after signin
117
118
  }
118
119
  catch (error) {
119
120
  const message = error instanceof Error ? error.message : "Failed to select API key";
@@ -123,7 +124,7 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
123
124
  });
124
125
  throw error;
125
126
  }
126
- }, [proxyUrl, state.user, onStatusChange]);
127
+ }, [proxyUrl, state.user, onStatusChange, onAuthChange]);
127
128
  /**
128
129
  * Connexion utilisateur (étape 1 : login)
129
130
  * Retourne le token et les clés API sans créer de session
@@ -248,8 +249,9 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
248
249
  setApiKeys([]);
249
250
  setAccessToken(undefined);
250
251
  onStatusChange?.("needs_auth");
252
+ onAuthChange?.(); // Refresh provider after logout
251
253
  }
252
- }, [proxyUrl, onStatusChange]);
254
+ }, [proxyUrl, onStatusChange, onAuthChange]);
253
255
  /**
254
256
  * Recharge la session
255
257
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/ai-ui-react",
3
- "version": "1.0.46",
3
+ "version": "1.0.48",
4
4
  "description": "Headless React components for LastBrain AI UI Kit",
5
5
  "private": false,
6
6
  "type": "module",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "lucide-react": "^0.257.0",
51
- "@lastbrain/ai-ui-core": "1.0.36"
51
+ "@lastbrain/ai-ui-core": "1.0.39"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/react": "^19.2.0",
@@ -10,10 +10,15 @@ import {
10
10
  History as HistoryIcon,
11
11
  FolderPlus,
12
12
  Power,
13
+ LogOut,
14
+ Key,
15
+ RefreshCw,
13
16
  } from "lucide-react";
14
17
  import { aiStyles, calculateTooltipPosition } from "../styles/inline";
15
18
  import { useLB } from "../context/LBAuthProvider";
19
+ import { useAiContext } from "../context/AiProvider";
16
20
  import { LBSigninModal } from "./LBSigninModal";
21
+ import { LBApiKeySelector } from "./LBApiKeySelector";
17
22
 
18
23
  export interface AiStatusButtonProps {
19
24
  status: AiStatus | null;
@@ -30,12 +35,18 @@ export function AiStatusButton({
30
35
  let lbStatus: string | undefined;
31
36
  let user: any;
32
37
  let logout: (() => Promise<void>) | undefined;
38
+ let apiKeys: any[] = [];
39
+ let accessToken: string | undefined;
40
+ let selectApiKeyWithToken: ((apiKeyId: string) => Promise<void>) | undefined;
33
41
 
34
42
  try {
35
43
  const lbContext = useLB();
36
44
  lbStatus = lbContext.status;
37
45
  user = lbContext.user;
38
46
  logout = lbContext.logout;
47
+ apiKeys = lbContext.apiKeys || [];
48
+ accessToken = lbContext.accessToken;
49
+ selectApiKeyWithToken = lbContext.selectApiKeyWithToken;
39
50
  } catch {
40
51
  // LBProvider n'est pas disponible, ignorer
41
52
  lbStatus = undefined;
@@ -43,7 +54,18 @@ export function AiStatusButton({
43
54
  logout = undefined;
44
55
  }
45
56
 
57
+ // Récupérer refetchProviders depuis AiProvider si disponible
58
+ let refetchProviders: (() => Promise<void>) | undefined;
59
+ try {
60
+ const aiContext = useAiContext();
61
+ refetchProviders = aiContext.refetchProviders;
62
+ } catch {
63
+ // AiProvider n'est pas disponible, ignorer
64
+ refetchProviders = undefined;
65
+ }
66
+
46
67
  const [showSigninModal, setShowSigninModal] = useState(false);
68
+ const [showApiKeySelector, setShowApiKeySelector] = useState(false);
47
69
 
48
70
  type BalanceUsage = {
49
71
  used?: number;
@@ -528,6 +550,10 @@ export function AiStatusButton({
528
550
  onClick={async () => {
529
551
  if (logout) {
530
552
  await logout();
553
+ // Refresh provider data after logout
554
+ if (refetchProviders) {
555
+ await refetchProviders();
556
+ }
531
557
  }
532
558
  setShowTooltip(false);
533
559
  }}
@@ -664,28 +690,103 @@ export function AiStatusButton({
664
690
  >
665
691
  <div style={aiStyles.tooltipHeader}>API Status</div>
666
692
 
693
+ {/* User Info Section */}
694
+ {status.user?.email && (
695
+ <div
696
+ style={{
697
+ ...aiStyles.tooltipSection,
698
+ ...aiStyles.tooltipSectionFirst,
699
+ }}
700
+ >
701
+ <div style={aiStyles.tooltipRow}>
702
+ <span style={aiStyles.tooltipLabel}>User:</span>
703
+ <span
704
+ style={{
705
+ ...aiStyles.tooltipValue,
706
+ fontSize: "12px",
707
+ maxWidth: "200px",
708
+ overflow: "hidden",
709
+ textOverflow: "ellipsis",
710
+ }}
711
+ >
712
+ {status.user.email}
713
+ </span>
714
+ </div>
715
+ </div>
716
+ )}
717
+
667
718
  <div
668
719
  style={{
669
720
  ...aiStyles.tooltipSection,
670
- ...aiStyles.tooltipSectionFirst,
721
+ ...(status.user?.email ? {} : aiStyles.tooltipSectionFirst),
671
722
  }}
672
723
  >
673
724
  <div style={aiStyles.tooltipRow}>
674
725
  <span style={aiStyles.tooltipLabel}>API Key:</span>
675
- <span style={aiStyles.tooltipValue}>
676
- {status.api_key?.name || "N/A"}
677
- </span>
726
+ <div
727
+ style={{
728
+ display: "flex",
729
+ alignItems: "center",
730
+ gap: "8px",
731
+ }}
732
+ >
733
+ <span style={aiStyles.tooltipValue}>
734
+ {status.apiKey?.name || status.api_key?.name || "Unknown"}
735
+ </span>
736
+ {apiKeys.length > 1 && selectApiKeyWithToken && (
737
+ <button
738
+ onClick={(e) => {
739
+ e.stopPropagation();
740
+ setShowApiKeySelector(true);
741
+ }}
742
+ style={{
743
+ background: "rgba(139, 92, 246, 0.1)",
744
+ border: "1px solid rgba(139, 92, 246, 0.3)",
745
+ borderRadius: "50%",
746
+ width: "24px",
747
+ height: "24px",
748
+ display: "flex",
749
+ alignItems: "center",
750
+ justifyContent: "center",
751
+ cursor: "pointer",
752
+ padding: 0,
753
+ transition: "all 0.2s ease",
754
+ }}
755
+ onMouseEnter={(e) => {
756
+ e.currentTarget.style.background =
757
+ "rgba(139, 92, 246, 0.2)";
758
+ e.currentTarget.style.borderColor =
759
+ "rgba(139, 92, 246, 0.5)";
760
+ }}
761
+ onMouseLeave={(e) => {
762
+ e.currentTarget.style.background =
763
+ "rgba(139, 92, 246, 0.1)";
764
+ e.currentTarget.style.borderColor =
765
+ "rgba(139, 92, 246, 0.3)";
766
+ }}
767
+ title="Change API Key"
768
+ >
769
+ <RefreshCw
770
+ size={12}
771
+ style={{ color: "rgba(139, 92, 246, 1)" }}
772
+ />
773
+ </button>
774
+ )}
775
+ </div>
678
776
  </div>
679
777
  <div style={aiStyles.tooltipRow}>
680
778
  <span style={aiStyles.tooltipLabel}>Env:</span>
681
779
  <span style={aiStyles.tooltipValue}>
682
- {status.api_key?.env || "N/A"}
780
+ {status.apiKey?.env || status.api_key?.env || "N/A"}
683
781
  </span>
684
782
  </div>
685
783
  <div style={aiStyles.tooltipRow}>
686
784
  <span style={aiStyles.tooltipLabel}>Rate Limit:</span>
687
785
  <span style={aiStyles.tooltipValue}>
688
- {status.api_key?.rate_limit_rpm || 0} req/min
786
+ {status.apiKey?.rate_limit_rpm ||
787
+ status.api_key?.rate_limit_rpm ||
788
+ 0}{" "}
789
+ req/min
689
790
  </span>
690
791
  </div>
691
792
  </div>
@@ -885,10 +986,105 @@ export function AiStatusButton({
885
986
  >
886
987
  <FolderPlus size={18} />
887
988
  </button>
989
+
990
+ {/* Change API Key Button */}
991
+ {selectApiKeyWithToken && apiKeys.length > 1 && (
992
+ <button
993
+ onClick={() => setShowApiKeySelector(true)}
994
+ style={{
995
+ background: "transparent",
996
+ border: "none",
997
+ padding: "14px",
998
+ cursor: "pointer",
999
+ display: "flex",
1000
+ alignItems: "center",
1001
+ justifyContent: "center",
1002
+ color: "#8b5cf6",
1003
+ transition: "all 0.2s ease",
1004
+ }}
1005
+ onMouseEnter={(e) => {
1006
+ Object.assign(e.currentTarget.style, {
1007
+ background: "rgba(139, 92, 246, 0.1)",
1008
+ });
1009
+ }}
1010
+ onMouseLeave={(e) => {
1011
+ Object.assign(e.currentTarget.style, {
1012
+ background: "transparent",
1013
+ });
1014
+ }}
1015
+ title="Change API Key"
1016
+ >
1017
+ <Key size={18} />
1018
+ </button>
1019
+ )}
1020
+
1021
+ {/* Logout Button */}
1022
+ {logout && (
1023
+ <button
1024
+ onClick={async () => {
1025
+ try {
1026
+ await logout();
1027
+ setShowTooltip(false);
1028
+ // Refresh provider data after logout
1029
+ if (refetchProviders) {
1030
+ await refetchProviders();
1031
+ }
1032
+ } catch (error) {
1033
+ console.error("Logout failed:", error);
1034
+ }
1035
+ }}
1036
+ style={{
1037
+ background: "transparent",
1038
+ border: "none",
1039
+ padding: "14px",
1040
+ cursor: "pointer",
1041
+ display: "flex",
1042
+ alignItems: "center",
1043
+ justifyContent: "center",
1044
+ color: "#ef4444",
1045
+ transition: "all 0.2s ease",
1046
+ }}
1047
+ onMouseEnter={(e) => {
1048
+ Object.assign(e.currentTarget.style, {
1049
+ background: "rgba(239, 68, 68, 0.1)",
1050
+ });
1051
+ }}
1052
+ onMouseLeave={(e) => {
1053
+ Object.assign(e.currentTarget.style, {
1054
+ background: "transparent",
1055
+ });
1056
+ }}
1057
+ title="Logout"
1058
+ >
1059
+ <LogOut size={18} />
1060
+ </button>
1061
+ )}
888
1062
  </div>
889
1063
  </div>,
890
1064
  document.body
891
1065
  )}
1066
+
1067
+ {/* API Key Selector Modal */}
1068
+ {showApiKeySelector && selectApiKeyWithToken && (
1069
+ <LBApiKeySelector
1070
+ isOpen={showApiKeySelector}
1071
+ apiKeys={apiKeys}
1072
+ onSelect={async (keyId) => {
1073
+ try {
1074
+ await selectApiKeyWithToken(keyId);
1075
+ setShowApiKeySelector(false);
1076
+ setShowTooltip(false);
1077
+ // Refresh provider data after API key selection
1078
+ if (refetchProviders) {
1079
+ await refetchProviders();
1080
+ }
1081
+ } catch (error) {
1082
+ console.error("Failed to select API key:", error);
1083
+ }
1084
+ }}
1085
+ onCancel={() => setShowApiKeySelector(false)}
1086
+ />
1087
+ )}
892
1088
  </div>
893
1089
  );
894
1090
  }
@@ -29,6 +29,8 @@ interface LBProviderProps {
29
29
  proxyUrl?: string;
30
30
  /** Fonction appelée lors des changements d'état */
31
31
  onStatusChange?: (status: LBAuthState["status"]) => void;
32
+ /** Fonction appelée après signin/logout pour refresh les providers */
33
+ onAuthChange?: () => void;
32
34
  }
33
35
 
34
36
  interface LBContextValue extends LBAuthState {
@@ -64,6 +66,7 @@ export function LBProvider({
64
66
  baseUrl: _baseUrl = "/api/lastbrain",
65
67
  proxyUrl = "/api/lastbrain",
66
68
  onStatusChange,
69
+ onAuthChange,
67
70
  }: LBProviderProps) {
68
71
  const [state, setState] = useState<LBAuthState>({
69
72
  status: "loading",
@@ -195,6 +198,7 @@ export function LBProvider({
195
198
  setAccessToken(undefined); // Nettoyer l'access token temporaire
196
199
  setApiKeys([]); // Nettoyer les clés API temporaires
197
200
  onStatusChange?.("ready");
201
+ onAuthChange?.(); // Refresh provider after signin
198
202
  } catch (error) {
199
203
  const message =
200
204
  error instanceof Error ? error.message : "Failed to select API key";
@@ -205,7 +209,7 @@ export function LBProvider({
205
209
  throw error;
206
210
  }
207
211
  },
208
- [proxyUrl, state.user, onStatusChange]
212
+ [proxyUrl, state.user, onStatusChange, onAuthChange]
209
213
  );
210
214
 
211
215
  /**
@@ -371,8 +375,9 @@ export function LBProvider({
371
375
  setApiKeys([]);
372
376
  setAccessToken(undefined);
373
377
  onStatusChange?.("needs_auth");
378
+ onAuthChange?.(); // Refresh provider after logout
374
379
  }
375
- }, [proxyUrl, onStatusChange]);
380
+ }, [proxyUrl, onStatusChange, onAuthChange]);
376
381
 
377
382
  /**
378
383
  * Recharge la session