@lastbrain/ai-ui-react 1.0.40 → 1.0.41

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":"AiContextButton.d.ts","sourceRoot":"","sources":["../../src/components/AiContextButton.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAS5C,MAAM,WAAW,oBACf,SACE,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,MAAM,CAAC,EACrC,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IAEvE,WAAW,EAAE,GAAG,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,CACT,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAC7C,IAAI,CAAC;IACV,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,YAAY,EACtB,MAAgB,EAChB,WAAW,EACX,kBAAyC,EACzC,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,gBAA0C,EAC1C,YAAY,EACZ,aAAa,EACb,OAAO,EAAE,QAAQ,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,OAAO,EACf,GAAG,WAAW,EACf,EAAE,oBAAoB,2CAwftB"}
1
+ {"version":3,"file":"AiContextButton.d.ts","sourceRoot":"","sources":["../../src/components/AiContextButton.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAW5C,MAAM,WAAW,oBACf,SACE,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,MAAM,CAAC,EACrC,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IAEvE,WAAW,EAAE,GAAG,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,CACT,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAC7C,IAAI,CAAC;IACV,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,YAAY,EACtB,MAAgB,EAChB,WAAW,EACX,kBAAyC,EACzC,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,gBAA0C,EAC1C,YAAY,EACZ,aAAa,EACb,OAAO,EAAE,QAAQ,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,OAAO,EACf,GAAG,WAAW,EACf,EAAE,oBAAoB,2CA2hBtB"}
@@ -9,12 +9,16 @@ import { useErrorToast, ErrorToast } from "./ErrorToast";
9
9
  import { aiStyles } from "../styles/inline";
10
10
  import { useAiContext } from "../context/AiProvider";
11
11
  import { handleAIError } from "../utils/errorHandler";
12
+ import { useLB } from "../context/LBAuthProvider";
13
+ import { LBAuthModal } from "./LBConnectButton";
12
14
  export function AiContextButton({ baseUrl: propBaseUrl, apiKeyId: propApiKeyId, uiMode = "modal", contextData, contextDescription = "Données à analyser", onResult, onToast, disabled, className, children, resultModalTitle = "Résultat de l'analyse", storeOutputs, artifactTitle, context: _context, model: _model, prompt: _prompt, ...buttonProps }) {
13
15
  const [isOpen, setIsOpen] = useState(false);
16
+ const [showAuthModal, setShowAuthModal] = useState(false);
14
17
  const [isResultOpen, setIsResultOpen] = useState(false);
15
18
  const [analysisResult, setAnalysisResult] = useState(null);
16
19
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
17
20
  const { showErrorToast, errorData, errorKey, clearError } = useErrorToast();
21
+ const { status: lbStatus } = useLB();
18
22
  // Récupérer le contexte AiProvider avec fallback sur les props
19
23
  const aiContext = useAiContext();
20
24
  const baseUrl = propBaseUrl ?? aiContext.baseUrl;
@@ -23,7 +27,12 @@ export function AiContextButton({ baseUrl: propBaseUrl, apiKeyId: propApiKeyId,
23
27
  baseUrl,
24
28
  apiKeyId,
25
29
  });
30
+ const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
26
31
  const handleOpenPanel = () => {
32
+ if (!isAuthReady) {
33
+ setShowAuthModal(true);
34
+ return;
35
+ }
27
36
  setIsOpen(true);
28
37
  };
29
38
  const handleClosePanel = () => {
@@ -160,14 +169,18 @@ Analyse ces données et réponds de manière structurée et claire.`;
160
169
  setIsOpen(false);
161
170
  }
162
171
  };
163
- return (_jsxs(_Fragment, { children: [_jsxs("div", { style: { position: "relative", display: "inline-block" }, children: [_jsx("button", { ...buttonProps, onClick: handleOpenPanel, disabled: disabled || loading, className: className, style: {
172
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { style: { position: "relative", display: "inline-block" }, children: [_jsx("button", { ...buttonProps, onClick: handleOpenPanel, disabled: disabled || loading || !isAuthReady, className: className, style: {
164
173
  ...aiStyles.button,
165
174
  display: "flex",
166
175
  alignItems: "center",
167
176
  gap: "8px",
168
- cursor: disabled || loading ? "not-allowed" : "pointer",
169
- opacity: disabled || loading ? 0.6 : 1,
170
- backgroundColor: loading ? "#8b5cf6" : "#7c3aed",
177
+ cursor: disabled || loading || !isAuthReady ? "not-allowed" : "pointer",
178
+ opacity: disabled || loading || !isAuthReady ? 0.6 : 1,
179
+ backgroundColor: loading
180
+ ? "#8b5cf6"
181
+ : !isAuthReady
182
+ ? "#94a3b8"
183
+ : "#7c3aed",
171
184
  color: "white",
172
185
  border: "none",
173
186
  borderRadius: "12px",
@@ -188,30 +201,30 @@ Analyse ces données et réponds de manière structurée et claire.`;
188
201
  }),
189
202
  ...buttonProps.style,
190
203
  }, onMouseEnter: (e) => {
191
- if (!disabled && !loading) {
204
+ if (!disabled && !loading && isAuthReady) {
192
205
  e.currentTarget.style.transform = "scale(1.02)";
193
206
  e.currentTarget.style.boxShadow =
194
207
  "0 6px 16px rgba(124, 58, 237, 0.3)";
195
208
  }
196
209
  }, onMouseLeave: (e) => {
197
- if (!disabled && !loading) {
210
+ if (!disabled && !loading && isAuthReady) {
198
211
  e.currentTarget.style.transform = "scale(1)";
199
212
  e.currentTarget.style.boxShadow = loading
200
213
  ? "0 4px 12px rgba(139, 92, 246, 0.3)"
201
214
  : "0 2px 8px rgba(124, 58, 237, 0.2)";
202
215
  }
203
216
  }, onMouseDown: (e) => {
204
- if (!disabled && !loading) {
217
+ if (!disabled && !loading && isAuthReady) {
205
218
  e.currentTarget.style.transform = "scale(0.98)";
206
219
  }
207
220
  }, onMouseUp: (e) => {
208
- if (!disabled && !loading) {
221
+ if (!disabled && !loading && isAuthReady) {
209
222
  e.currentTarget.style.transform = "scale(1.02)";
210
223
  }
211
- }, "data-ai-context-button": true, children: loading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 18, className: "animate-spin", style: {
224
+ }, "data-ai-context-button": true, title: !isAuthReady ? "Authentication required" : "Analyser avec l'IA", children: loading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 18, className: "animate-spin", style: {
212
225
  color: "white",
213
226
  filter: "drop-shadow(0 0 2px rgba(255,255,255,0.3))",
214
- } }), _jsx("span", { style: { letterSpacing: "0.025em" }, children: "Analyse..." })] })) : (_jsx(_Fragment, { children: _jsx(Sparkle, { size: 18, style: {
227
+ } }), _jsx("span", { style: { letterSpacing: "0.025em" }, children: "Analyse..." })] })) : !isAuthReady ? (_jsxs(_Fragment, { children: [_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }), _jsx("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })] }), children || _jsx("span", { children: "Connexion requise" })] })) : (_jsx(_Fragment, { children: _jsx(Sparkle, { size: 18, style: {
215
228
  color: "white",
216
229
  filter: "drop-shadow(0 0 2px rgba(255,255,255,0.2))",
217
230
  } }) })) }), isOpen && (_jsx(AiPromptPanel, { isOpen: isOpen, onClose: handleClosePanel, onSubmit: handleSubmit, uiMode: uiMode, models: [], enableModelManagement: true, modelCategory: "text", baseUrl: baseUrl, apiKey: apiKeyId }))] }), isResultOpen && analysisResult && (_jsx("div", { style: {
@@ -332,5 +345,5 @@ Analyse ces données et réponds de manière structurée et claire.`;
332
345
  ...getThemeStyles().content,
333
346
  }, children: [_jsxs("span", { children: ["Co\u00FBt: $", (apiKeyId?.includes("dev")
334
347
  ? 0
335
- : analysisResult.cost).toFixed(6)] }), _jsxs("span", { children: ["ID: ", analysisResult.requestId?.slice(-8) || "N/A"] })] }) })] })] }) }) })), _jsx(ErrorToast, { error: errorData, onComplete: clearError }, errorKey)] }));
348
+ : analysisResult.cost).toFixed(6)] }), _jsxs("span", { children: ["ID: ", analysisResult.requestId?.slice(-8) || "N/A"] })] }) })] })] }) }) })), showAuthModal && (_jsx(LBAuthModal, { onClose: () => setShowAuthModal(false) })), _jsx(ErrorToast, { error: errorData, onComplete: clearError }, errorKey)] }));
336
349
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AiImageButton.d.ts","sourceRoot":"","sources":["../../src/components/AiImageButton.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAS5C,MAAM,WAAW,kBACf,SACE,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,MAAM,CAAC,EACrC,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IACvE,OAAO,CAAC,EAAE,CACR,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAC7C,IAAI,CAAC;IACV,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC5B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,YAAY,EACtB,MAAgB,EAChB,OAAO,EAAE,QAAQ,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,OAAO,EACf,OAAO,EACP,OAAO,EACP,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,aAAoB,EACpB,WAAW,EACX,YAAY,EACZ,aAAa,EACb,GAAG,WAAW,EACf,EAAE,kBAAkB,2CAqXpB"}
1
+ {"version":3,"file":"AiImageButton.d.ts","sourceRoot":"","sources":["../../src/components/AiImageButton.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAW5C,MAAM,WAAW,kBACf,SACE,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,MAAM,CAAC,EACrC,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IACvE,OAAO,CAAC,EAAE,CACR,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAC7C,IAAI,CAAC;IACV,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC5B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,YAAY,EACtB,MAAgB,EAChB,OAAO,EAAE,QAAQ,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,OAAO,EACf,OAAO,EACP,OAAO,EACP,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,aAAoB,EACpB,WAAW,EACX,YAAY,EACZ,aAAa,EACb,GAAG,WAAW,EACf,EAAE,kBAAkB,2CAsZpB"}
@@ -9,17 +9,26 @@ import { useErrorToast, ErrorToast } from "./ErrorToast";
9
9
  import { aiStyles } from "../styles/inline";
10
10
  import { useAiContext } from "../context/AiProvider";
11
11
  import { handleAIError } from "../utils/errorHandler";
12
+ import { useLB } from "../context/LBAuthProvider";
13
+ import { LBAuthModal } from "./LBConnectButton";
12
14
  export function AiImageButton({ baseUrl: propBaseUrl, apiKeyId: propApiKeyId, uiMode = "modal", context: _context, model: _model, prompt: _prompt, onImage, onToast, disabled, className, children, showImageCard = true, onImageSave, storeOutputs, artifactTitle, ...buttonProps }) {
13
15
  const [isOpen, setIsOpen] = useState(false);
16
+ const [showAuthModal, setShowAuthModal] = useState(false);
14
17
  const [generatedImage, setGeneratedImage] = useState(null);
15
18
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
16
19
  const { showErrorToast, errorData, errorKey, clearError } = useErrorToast();
20
+ const { status: lbStatus } = useLB();
17
21
  // Récupérer le contexte AiProvider avec fallback sur les props
18
22
  const aiContext = useAiContext();
19
23
  const baseUrl = propBaseUrl ?? aiContext.baseUrl;
20
24
  const apiKeyId = propApiKeyId ?? aiContext.apiKeyId;
21
25
  const { generateImage, loading } = useAiCallImage({ baseUrl, apiKeyId });
26
+ const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
22
27
  const handleOpenPanel = () => {
28
+ if (!isAuthReady) {
29
+ setShowAuthModal(true);
30
+ return;
31
+ }
23
32
  setIsOpen(true);
24
33
  };
25
34
  const handleClosePanel = () => {
@@ -134,14 +143,18 @@ export function AiImageButton({ baseUrl: propBaseUrl, apiKeyId: propApiKeyId, ui
134
143
  setIsOpen(false);
135
144
  }
136
145
  };
137
- return (_jsxs("div", { className: "flex items-start gap-4", children: [_jsxs("div", { style: { position: "relative", display: "inline-block" }, children: [_jsx("button", { ...buttonProps, onClick: handleOpenPanel, disabled: disabled || loading, className: className, style: {
146
+ return (_jsxs("div", { className: "flex items-start gap-4", children: [_jsxs("div", { style: { position: "relative", display: "inline-block" }, children: [_jsx("button", { ...buttonProps, onClick: handleOpenPanel, disabled: disabled || loading || !isAuthReady, className: className, style: {
138
147
  ...aiStyles.button,
139
148
  display: "flex",
140
149
  alignItems: "center",
141
150
  gap: "8px",
142
- cursor: disabled || loading ? "not-allowed" : "pointer",
143
- opacity: disabled || loading ? 0.6 : 1,
144
- backgroundColor: loading ? "#8b5cf6" : "#6366f1",
151
+ cursor: disabled || loading || !isAuthReady ? "not-allowed" : "pointer",
152
+ opacity: disabled || loading || !isAuthReady ? 0.6 : 1,
153
+ backgroundColor: loading
154
+ ? "#8b5cf6"
155
+ : !isAuthReady
156
+ ? "#94a3b8"
157
+ : "#6366f1",
145
158
  color: "white",
146
159
  border: "none",
147
160
  borderRadius: "12px",
@@ -161,30 +174,30 @@ export function AiImageButton({ baseUrl: propBaseUrl, apiKeyId: propApiKeyId, ui
161
174
  }),
162
175
  ...buttonProps.style,
163
176
  }, onMouseEnter: (e) => {
164
- if (!disabled && !loading) {
177
+ if (!disabled && !loading && isAuthReady) {
165
178
  e.currentTarget.style.transform = "scale(1.02)";
166
179
  e.currentTarget.style.boxShadow =
167
180
  "0 6px 16px rgba(99, 102, 241, 0.3)";
168
181
  }
169
182
  }, onMouseLeave: (e) => {
170
- if (!disabled && !loading) {
183
+ if (!disabled && !loading && isAuthReady) {
171
184
  e.currentTarget.style.transform = "scale(1)";
172
185
  e.currentTarget.style.boxShadow = loading
173
186
  ? "0 4px 12px rgba(139, 92, 246, 0.3)"
174
187
  : "0 2px 8px rgba(99, 102, 241, 0.2)";
175
188
  }
176
189
  }, onMouseDown: (e) => {
177
- if (!disabled && !loading) {
190
+ if (!disabled && !loading && isAuthReady) {
178
191
  e.currentTarget.style.transform = "scale(0.98)";
179
192
  }
180
193
  }, onMouseUp: (e) => {
181
- if (!disabled && !loading) {
194
+ if (!disabled && !loading && isAuthReady) {
182
195
  e.currentTarget.style.transform = "scale(1.02)";
183
196
  }
184
- }, "data-ai-image-button": true, children: loading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 18, className: "animate-spin", style: {
197
+ }, "data-ai-image-button": true, title: !isAuthReady ? "Authentication required" : "Générer une image", children: loading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 18, className: "animate-spin", style: {
185
198
  color: "white",
186
199
  filter: "drop-shadow(0 0 2px rgba(255,255,255,0.3))",
187
- } }), _jsx("span", { style: { letterSpacing: "0.025em" }, children: "G\u00E9n\u00E9ration..." })] })) : (_jsxs(_Fragment, { children: [_jsx(ImageIcon, { size: 18, style: {
200
+ } }), _jsx("span", { style: { letterSpacing: "0.025em" }, children: "G\u00E9n\u00E9ration..." })] })) : !isAuthReady ? (_jsxs(_Fragment, { children: [_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }), _jsx("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })] }), children || _jsx("span", { children: "Connexion requise" })] })) : (_jsxs(_Fragment, { children: [_jsx(ImageIcon, { size: 18, style: {
188
201
  color: "white",
189
202
  filter: "drop-shadow(0 0 2px rgba(255,255,255,0.2))",
190
203
  } }), _jsx("span", { style: { letterSpacing: "0.025em" }, children: children || "Générer une image" })] })) }), isOpen && (_jsx(AiPromptPanel, { isOpen: isOpen, onClose: handleClosePanel, onSubmit: handleSubmit, uiMode: uiMode, enableModelManagement: true, modelCategory: "image", baseUrl: baseUrl, apiKey: apiKeyId, models: [] }))] }), showImageCard && generatedImage && (_jsxs("div", { className: "relative", style: {
@@ -231,5 +244,5 @@ export function AiImageButton({ baseUrl: propBaseUrl, apiKeyId: propApiKeyId, ui
231
244
  e.currentTarget.style.boxShadow = "";
232
245
  }, title: "T\u00E9l\u00E9charger l'image", children: [_jsx(Download, { size: 16 }), "T\u00E9l\u00E9charger l'image"] }), onImageSave && (_jsxs("button", { onClick: handleSave, className: "flex items-center gap-1 px-3 py-2 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors", title: "Sauvegarder en base", children: [_jsx(ExternalLink, { size: 14 }), "Sauvegarder"] }))] }), _jsx("div", { className: "mt-3 pt-3 text-xs", style: {
233
246
  ...getThemeStyles().metadata,
234
- }, children: _jsx("div", { className: "flex justify-center", children: _jsxs("span", { children: ["ID: ", generatedImage.requestId.slice(-8)] }) }) })] })), _jsx(ErrorToast, { error: errorData, onComplete: clearError }, errorKey)] }));
247
+ }, children: _jsx("div", { className: "flex justify-center", children: _jsxs("span", { children: ["ID: ", generatedImage.requestId.slice(-8)] }) }) })] })), showAuthModal && (_jsx(LBAuthModal, { onClose: () => setShowAuthModal(false) })), _jsx(ErrorToast, { error: errorData, onComplete: clearError }, errorKey)] }));
235
248
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AiInput.d.ts","sourceRoot":"","sources":["../../src/components/AiInput.tsx"],"names":[],"mappings":"AAEA,OAAc,EAAoB,KAAK,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAE1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAQ5C,MAAM,WAAW,YACf,SACE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EACzB,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,SAAS,CAAC;IACxD,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC5B,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,wBAAgB,OAAO,CAAC,EACtB,OAAO,EACP,QAAQ,EACR,MAAgB,EAChB,OAAO,EACP,KAAK,EACL,MAAM,EACN,QAAgB,EAChB,qBAA4B,EAC5B,YAAY,EACZ,aAAa,EACb,OAAO,EACP,OAAO,EACP,QAAQ,EACR,SAAS,EACT,GAAG,UAAU,EACd,EAAE,YAAY,2CAkLd"}
1
+ {"version":3,"file":"AiInput.d.ts","sourceRoot":"","sources":["../../src/components/AiInput.tsx"],"names":[],"mappings":"AAEA,OAAc,EAAoB,KAAK,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAE1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAU5C,MAAM,WAAW,YACf,SACE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EACzB,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,SAAS,CAAC;IACxD,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC5B,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,wBAAgB,OAAO,CAAC,EACtB,OAAO,EACP,QAAQ,EACR,MAAgB,EAChB,OAAO,EACP,KAAK,EACL,MAAM,EACN,QAAgB,EAChB,qBAA4B,EAC5B,YAAY,EACZ,aAAa,EACb,OAAO,EACP,OAAO,EACP,QAAQ,EACR,SAAS,EACT,GAAG,UAAU,EACd,EAAE,YAAY,2CA+Md"}
@@ -8,13 +8,17 @@ import { AiPromptPanel } from "./AiPromptPanel";
8
8
  import { UsageToast, useUsageToast } from "./UsageToast";
9
9
  import { aiStyles } from "../styles/inline";
10
10
  import { handleAIError } from "../utils/errorHandler";
11
+ import { useLB } from "../context/LBAuthProvider";
12
+ import { LBAuthModal } from "./LBConnectButton";
11
13
  export function AiInput({ baseUrl, apiKeyId, uiMode = "modal", context, model, prompt, editMode = false, enableModelManagement = true, storeOutputs, artifactTitle, onValue, onToast, disabled, className, ...inputProps }) {
12
14
  const [isOpen, setIsOpen] = useState(false);
15
+ const [showAuthModal, setShowAuthModal] = useState(false);
13
16
  const [inputValue, setInputValue] = useState(inputProps.value?.toString() || inputProps.defaultValue?.toString() || "");
14
17
  const [isFocused, setIsFocused] = useState(false);
15
18
  const [isButtonHovered, setIsButtonHovered] = useState(false);
16
19
  const inputRef = useRef(null);
17
20
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
21
+ const { status: lbStatus } = useLB();
18
22
  const { models } = useAiModels({
19
23
  baseUrl,
20
24
  apiKeyId,
@@ -22,7 +26,13 @@ export function AiInput({ baseUrl, apiKeyId, uiMode = "modal", context, model, p
22
26
  });
23
27
  const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
24
28
  const hasConfiguration = Boolean(model && prompt);
29
+ const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
30
+ const shouldShowSparkles = isAuthReady && !disabled;
25
31
  const handleOpenPanel = () => {
32
+ if (!isAuthReady) {
33
+ setShowAuthModal(true);
34
+ return;
35
+ }
26
36
  setIsOpen(true);
27
37
  };
28
38
  const handleClosePanel = () => {
@@ -109,5 +119,9 @@ export function AiInput({ baseUrl, apiKeyId, uiMode = "modal", context, model, p
109
119
  ...(disabled || loading
110
120
  ? { opacity: 0.5, cursor: "not-allowed" }
111
121
  : {}),
112
- }, onClick: hasConfiguration ? handleQuickGenerate : handleOpenPanel, onMouseEnter: () => setIsButtonHovered(true), onMouseLeave: () => setIsButtonHovered(false), disabled: disabled || loading, type: "button", title: hasConfiguration ? "Generate with AI" : "Setup AI", children: loading ? (_jsx("svg", { style: aiStyles.spinner, width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", children: _jsx("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" }) })) : (_jsx(Sparkles, { size: 16 })) }), isOpen && (_jsx(AiPromptPanel, { isOpen: isOpen, onClose: handleClosePanel, onSubmit: handleSubmit, uiMode: uiMode, models: [], modelCategory: "text", sourceText: inputValue || undefined, apiKey: apiKeyId, baseUrl: baseUrl, enableModelManagement: enableModelManagement })), Boolean(toastData) && (_jsx(UsageToast, { result: toastData, position: "bottom-right", onComplete: clearToast }, toastKey))] }));
122
+ }, onClick: hasConfiguration ? handleQuickGenerate : handleOpenPanel, onMouseEnter: () => setIsButtonHovered(true), onMouseLeave: () => setIsButtonHovered(false), disabled: disabled || loading || !isAuthReady, type: "button", title: !isAuthReady
123
+ ? "Authentication required"
124
+ : hasConfiguration
125
+ ? "Generate with AI"
126
+ : "Setup AI", children: loading ? (_jsx("svg", { style: aiStyles.spinner, width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", children: _jsx("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" }) })) : shouldShowSparkles ? (_jsx(Sparkles, { size: 16 })) : (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }), _jsx("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })] })) }), isOpen && (_jsx(AiPromptPanel, { isOpen: isOpen, onClose: handleClosePanel, onSubmit: handleSubmit, uiMode: uiMode, models: [], modelCategory: "text", sourceText: inputValue || undefined, apiKey: apiKeyId, baseUrl: baseUrl, enableModelManagement: enableModelManagement })), showAuthModal && (_jsx(LBAuthModal, { onClose: () => setShowAuthModal(false) })), Boolean(toastData) && (_jsx(UsageToast, { result: toastData, position: "bottom-right", onComplete: clearToast }, toastKey))] }));
113
127
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AiTextarea.d.ts","sourceRoot":"","sources":["../../src/components/AiTextarea.tsx"],"names":[],"mappings":"AAEA,OAAc,EAIZ,KAAK,sBAAsB,EAC5B,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAQ5C,MAAM,WAAW,eACf,SACE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EACzB,IAAI,CAAC,sBAAsB,CAAC,mBAAmB,CAAC,EAAE,SAAS,CAAC;IAC9D,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC7B;AAED,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,QAAQ,EACR,MAAgB,EAChB,OAAO,EACP,KAAK,EACL,MAAM,EACN,QAAgB,EAChB,qBAAqB,EACrB,YAAY,EACZ,aAAa,EACb,OAAO,EACP,OAAO,EACP,QAAQ,EACR,SAAS,EACT,GAAG,aAAa,EACjB,EAAE,eAAe,2CAmMjB"}
1
+ {"version":3,"file":"AiTextarea.d.ts","sourceRoot":"","sources":["../../src/components/AiTextarea.tsx"],"names":[],"mappings":"AAEA,OAAc,EAIZ,KAAK,sBAAsB,EAC5B,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAU5C,MAAM,WAAW,eACf,SACE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EACzB,IAAI,CAAC,sBAAsB,CAAC,mBAAmB,CAAC,EAAE,SAAS,CAAC;IAC9D,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC7B;AAED,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,QAAQ,EACR,MAAgB,EAChB,OAAO,EACP,KAAK,EACL,MAAM,EACN,QAAgB,EAChB,qBAAqB,EACrB,YAAY,EACZ,aAAa,EACb,OAAO,EACP,OAAO,EACP,QAAQ,EACR,SAAS,EACT,GAAG,aAAa,EACjB,EAAE,eAAe,2CA6NjB"}
@@ -8,8 +8,10 @@ import { AiPromptPanel } from "./AiPromptPanel";
8
8
  import { UsageToast, useUsageToast } from "./UsageToast";
9
9
  import { aiStyles } from "../styles/inline";
10
10
  import { handleAIError } from "../utils/errorHandler";
11
+ import { useLB } from "../context/LBAuthProvider";
11
12
  export function AiTextarea({ baseUrl, apiKeyId, uiMode = "modal", context, model, prompt, editMode = false, enableModelManagement, storeOutputs, artifactTitle, onValue, onToast, disabled, className, ...textareaProps }) {
12
13
  const [isOpen, setIsOpen] = useState(false);
14
+ const [showAuthModal, setShowAuthModal] = useState(false);
13
15
  const [textareaValue, setTextareaValue] = useState(textareaProps.value?.toString() ||
14
16
  textareaProps.defaultValue?.toString() ||
15
17
  "");
@@ -17,6 +19,7 @@ export function AiTextarea({ baseUrl, apiKeyId, uiMode = "modal", context, model
17
19
  const [isButtonHovered, setIsButtonHovered] = useState(false);
18
20
  const textareaRef = useRef(null);
19
21
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
22
+ const { status: lbStatus } = useLB();
20
23
  const { models } = useAiModels({
21
24
  baseUrl,
22
25
  apiKeyId,
@@ -24,7 +27,13 @@ export function AiTextarea({ baseUrl, apiKeyId, uiMode = "modal", context, model
24
27
  });
25
28
  const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
26
29
  const hasConfiguration = Boolean(model && prompt);
30
+ const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
31
+ const shouldShowSparkles = isAuthReady && !disabled;
27
32
  const handleOpenPanel = () => {
33
+ if (!isAuthReady) {
34
+ setShowAuthModal(true);
35
+ return;
36
+ }
28
37
  setIsOpen(true);
29
38
  };
30
39
  const handleClosePanel = () => {
@@ -123,5 +132,9 @@ export function AiTextarea({ baseUrl, apiKeyId, uiMode = "modal", context, model
123
132
  ...(disabled || loading
124
133
  ? { opacity: 0.5, cursor: "not-allowed" }
125
134
  : {}),
126
- }, onClick: hasConfiguration ? handleQuickGenerate : handleOpenPanel, onMouseEnter: () => setIsButtonHovered(true), onMouseLeave: () => setIsButtonHovered(false), disabled: disabled || loading, type: "button", title: hasConfiguration ? "Generate with AI" : "Setup AI", children: loading ? (_jsx("svg", { style: aiStyles.spinner, width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", children: _jsx("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" }) })) : (_jsx(Sparkles, { size: 16 })) }), isOpen && (_jsx(AiPromptPanel, { isOpen: isOpen, onClose: handleClosePanel, onSubmit: handleSubmit, uiMode: uiMode, models: [], modelCategory: "text", sourceText: textareaValue || undefined, baseUrl: baseUrl, apiKey: apiKeyId, enableModelManagement: enableModelManagement })), Boolean(toastData) && (_jsx(UsageToast, { result: toastData, position: "bottom-right", onComplete: clearToast }, toastKey))] }));
135
+ }, onClick: hasConfiguration ? handleQuickGenerate : handleOpenPanel, onMouseEnter: () => setIsButtonHovered(true), onMouseLeave: () => setIsButtonHovered(false), disabled: disabled || loading || !isAuthReady, type: "button", title: !isAuthReady
136
+ ? "Authentication required"
137
+ : hasConfiguration
138
+ ? "Generate with AI"
139
+ : "Setup AI", children: loading ? (_jsx("svg", { style: aiStyles.spinner, width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", children: _jsx("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" }) })) : shouldShowSparkles ? (_jsx(Sparkles, { size: 16 })) : (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }), _jsx("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })] })) }), isOpen && (_jsx(AiPromptPanel, { isOpen: isOpen, onClose: handleClosePanel, onSubmit: handleSubmit, uiMode: uiMode, models: [], modelCategory: "text", sourceText: textareaValue || undefined, baseUrl: baseUrl, apiKey: apiKeyId, enableModelManagement: enableModelManagement })), Boolean(toastData) && (_jsx(UsageToast, { result: toastData, position: "bottom-right", onComplete: clearToast }, toastKey))] }));
127
140
  }
@@ -1 +1 @@
1
- {"version":3,"file":"LBConnectButton.d.ts","sourceRoot":"","sources":["../../src/components/LBConnectButton.tsx"],"names":[],"mappings":"AAUA,UAAU,oBAAoB;IAC5B,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,EAC9B,KAAkC,EAClC,SAAc,EACd,WAAW,EACX,WAAW,GACZ,EAAE,oBAAoB,2CA0CtB;AAED;;GAEG;AACH,UAAU,gBAAgB;IACxB,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACrC;AAED,iBAAS,WAAW,CAAC,EAAE,OAAO,EAAE,EAAE,gBAAgB,2CAsJjD;AAED,OAAO,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"LBConnectButton.d.ts","sourceRoot":"","sources":["../../src/components/LBConnectButton.tsx"],"names":[],"mappings":"AAUA,UAAU,oBAAoB;IAC5B,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,EAC9B,KAAkC,EAClC,SAAc,EACd,WAAW,EACX,WAAW,GACZ,EAAE,oBAAoB,2CA4CtB;AAED;;GAEG;AACH,UAAU,gBAAgB;IACxB,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACrC;AAED,iBAAS,WAAW,CAAC,EAAE,OAAO,EAAE,EAAE,gBAAgB,2CAsRjD;AAED,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -27,11 +27,13 @@ export function LBConnectButton({ label = "Se connecter à LastBrain", className
27
27
  }
28
28
  };
29
29
  return (_jsxs(_Fragment, { children: [_jsx("button", { onClick: handleClick, className: className ||
30
- "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700", disabled: status === "loading", children: status === "loading"
31
- ? "Chargement..."
30
+ (status === "ready" && user
31
+ ? "px-4 py-2 bg-gradient-to-r from-emerald-500 to-teal-600 text-white rounded-lg hover:from-emerald-600 hover:to-teal-700 transition-all duration-200 shadow-md hover:shadow-lg"
32
+ : "px-4 py-2 bg-gradient-to-r from-violet-500 to-purple-600 text-white rounded-lg hover:from-violet-600 hover:to-purple-700 transition-all duration-200 shadow-md hover:shadow-lg"), disabled: status === "loading", children: status === "loading"
33
+ ? "⏳ Chargement..."
32
34
  : status === "ready" && user
33
- ? `Connecté (${user.email})`
34
- : label }), showModal && _jsx(LBAuthModal, { onClose: handleModalClose })] }));
35
+ ? `✓ ${user.email}`
36
+ : `🔐 ${label}` }), showModal && _jsx(LBAuthModal, { onClose: handleModalClose })] }));
35
37
  }
36
38
  function LBAuthModal({ onClose }) {
37
39
  const { login, fetchApiKeys, selectApiKey, status } = useLB();
@@ -80,8 +82,10 @@ function LBAuthModal({ onClose }) {
80
82
  setLoading(false);
81
83
  }
82
84
  };
83
- return (_jsx("div", { className: "fixed inset-0 bg-black/50 flex items-center justify-center z-50", children: _jsxs("div", { className: "bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4", children: [_jsxs("div", { className: "flex justify-between items-center mb-4", children: [_jsx("h2", { className: "text-xl font-bold", children: step === "login"
84
- ? "Connexion LastBrain"
85
- : "Sélectionner une clé API" }), _jsx("button", { onClick: () => onClose(false), className: "text-gray-500 hover:text-gray-700", children: "\u2715" })] }), step === "login" ? (_jsxs("form", { onSubmit: handleLogin, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-sm font-medium mb-1", children: "Email" }), _jsx("input", { type: "email", value: email, onChange: (e) => setEmail(e.target.value), className: "w-full px-3 py-2 border rounded dark:bg-gray-700", required: true, autoFocus: true })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-sm font-medium mb-1", children: "Mot de passe" }), _jsx("input", { type: "password", value: password, onChange: (e) => setPassword(e.target.value), className: "w-full px-3 py-2 border rounded dark:bg-gray-700", required: true })] }), error && _jsx("div", { className: "text-red-600 text-sm", children: error }), _jsx("button", { type: "submit", disabled: loading, className: "w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50", children: loading ? "Connexion..." : "Se connecter" }), _jsxs("p", { className: "text-sm text-gray-600 dark:text-gray-400 text-center", children: ["Pas encore de compte ?", " ", _jsx("a", { href: "https://lastbrain.io/signup", target: "_blank", rel: "noopener noreferrer", className: "text-blue-600 hover:underline", children: "Cr\u00E9er un compte" })] })] })) : (_jsxs("div", { className: "space-y-4", children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400", children: "S\u00E9lectionnez une cl\u00E9 API pour cr\u00E9er une session de 72h" }), _jsx("div", { className: "space-y-2", children: apiKeys.map((key) => (_jsxs("button", { onClick: () => handleSelectKey(key.id), disabled: !key.isActive || loading, className: "w-full text-left px-4 py-3 border rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed", children: [_jsx("div", { className: "font-medium", children: key.name }), _jsxs("div", { className: "text-sm text-gray-500", children: [key.keyPrefix, "..."] }), !key.isActive && (_jsx("div", { className: "text-xs text-red-600", children: "Inactive" }))] }, key.id))) }), error && _jsx("div", { className: "text-red-600 text-sm", children: error })] }))] }) }));
85
+ return (_jsx("div", { className: "fixed inset-0 bg-gradient-to-br from-slate-900/95 via-purple-900/95 to-slate-900/95 backdrop-blur-sm flex items-center justify-center z-50 p-4", children: _jsxs("div", { className: "bg-white dark:bg-gradient-to-br dark:from-slate-800 dark:to-slate-900 rounded-2xl shadow-2xl max-w-md w-full border border-slate-200 dark:border-slate-700 overflow-hidden", children: [_jsx("div", { className: "bg-gradient-to-r from-violet-500 to-purple-600 p-6 text-white", children: _jsxs("div", { className: "flex justify-between items-center", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-bold flex items-center gap-2", children: step === "login" ? "🔐 Connexion" : "🔑 Sélection clé API" }), _jsx("p", { className: "text-violet-100 text-sm mt-1", children: step === "login"
86
+ ? "Accédez à vos outils IA"
87
+ : "Créez une session sécurisée 72h" })] }), _jsx("button", { onClick: () => onClose(false), className: "text-white/80 hover:text-white hover:bg-white/20 rounded-lg p-2 transition-colors", children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }) }), _jsx("div", { className: "p-6", children: step === "login" ? (_jsxs("form", { onSubmit: handleLogin, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300", children: "\uD83D\uDCE7 Email" }), _jsx("input", { type: "email", value: email, onChange: (e) => setEmail(e.target.value), className: "w-full px-4 py-3 border-2 border-slate-200 dark:border-slate-600 rounded-lg dark:bg-slate-800 dark:text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-200 dark:focus:ring-violet-900 transition-all outline-none", placeholder: "votre@email.com", required: true, autoFocus: true })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300", children: "\uD83D\uDD12 Mot de passe" }), _jsx("input", { type: "password", value: password, onChange: (e) => setPassword(e.target.value), className: "w-full px-4 py-3 border-2 border-slate-200 dark:border-slate-600 rounded-lg dark:bg-slate-800 dark:text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-200 dark:focus:ring-violet-900 transition-all outline-none", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", required: true })] }), error && (_jsx("div", { className: "bg-red-50 dark:bg-red-900/20 border-l-4 border-red-500 p-4 rounded", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-red-500 text-xl", children: "\u26A0\uFE0F" }), _jsx("p", { className: "text-red-700 dark:text-red-400 text-sm font-medium", children: error })] }) })), _jsx("button", { type: "submit", disabled: loading, className: "w-full px-6 py-3 bg-gradient-to-r from-violet-500 to-purple-600 text-white font-semibold rounded-lg hover:from-violet-600 hover:to-purple-700 disabled:from-slate-400 disabled:to-slate-500 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5", children: loading ? (_jsxs("span", { className: "flex items-center justify-center gap-2", children: [_jsxs("svg", { className: "animate-spin h-5 w-5", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [_jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), _jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), "Connexion en cours..."] })) : ("🚀 Se connecter") }), _jsxs("div", { className: "relative my-6", children: [_jsx("div", { className: "absolute inset-0 flex items-center", children: _jsx("div", { className: "w-full border-t border-slate-200 dark:border-slate-700" }) }), _jsx("div", { className: "relative flex justify-center text-sm", children: _jsx("span", { className: "px-4 bg-white dark:bg-slate-800 text-slate-500", children: "ou" }) })] }), _jsxs("div", { className: "bg-gradient-to-r from-violet-50 to-purple-50 dark:from-violet-900/20 dark:to-purple-900/20 rounded-lg p-4 border border-violet-200 dark:border-violet-800", children: [_jsx("p", { className: "text-sm text-slate-700 dark:text-slate-300 text-center", children: "Pas encore de compte ?" }), _jsx("a", { href: "https://lastbrain.io/signup", target: "_blank", rel: "noopener noreferrer", className: "block mt-2 text-center text-violet-600 dark:text-violet-400 hover:text-violet-700 dark:hover:text-violet-300 font-semibold text-sm hover:underline", children: "\u2728 Cr\u00E9er un compte gratuitement" })] })] })) : (_jsxs("div", { className: "space-y-4", children: [_jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400 bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg border border-blue-200 dark:border-blue-800", children: "\u2139\uFE0F S\u00E9lectionnez une cl\u00E9 API pour cr\u00E9er une session s\u00E9curis\u00E9e de 72h" }), _jsx("div", { className: "space-y-3 max-h-96 overflow-y-auto", children: apiKeys.map((key) => (_jsx("button", { onClick: () => handleSelectKey(key.id), disabled: !key.isActive || loading, className: `w-full text-left px-5 py-4 border-2 rounded-lg transition-all transform hover:scale-[1.02] ${key.isActive
88
+ ? "border-slate-200 dark:border-slate-600 hover:border-violet-400 dark:hover:border-violet-500 hover:bg-gradient-to-r hover:from-violet-50 hover:to-purple-50 dark:hover:from-violet-900/20 dark:hover:to-purple-900/20"
89
+ : "border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50 opacity-50 cursor-not-allowed"}`, children: _jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "font-semibold text-slate-900 dark:text-white flex items-center gap-2", children: [key.isActive ? "🔑" : "🔒", key.name] }), _jsxs("div", { className: "text-sm text-slate-500 dark:text-slate-400 mt-1 font-mono", children: [key.keyPrefix, "..."] }), key.scopes && (_jsxs("div", { className: "flex gap-1 mt-2 flex-wrap", children: [key.scopes.slice(0, 3).map((scope) => (_jsx("span", { className: "text-xs px-2 py-1 bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 rounded", children: scope }, scope))), key.scopes.length > 3 && (_jsxs("span", { className: "text-xs px-2 py-1 bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 rounded", children: ["+", key.scopes.length - 3] }))] }))] }), key.isActive ? (_jsx("svg", { className: "w-5 h-5 text-violet-500 flex-shrink-0 mt-1", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })) : (_jsx("span", { className: "text-xs text-red-600 dark:text-red-400 font-semibold bg-red-100 dark:bg-red-900/30 px-2 py-1 rounded", children: "Inactive" }))] }) }, key.id))) }), error && (_jsx("div", { className: "bg-red-50 dark:bg-red-900/20 border-l-4 border-red-500 p-4 rounded", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-red-500 text-xl", children: "\u26A0\uFE0F" }), _jsx("p", { className: "text-red-700 dark:text-red-400 text-sm font-medium", children: error })] }) }))] })) })] }) }));
86
90
  }
87
91
  export { LBAuthModal };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/ai-ui-react",
3
- "version": "1.0.40",
3
+ "version": "1.0.41",
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.29"
51
+ "@lastbrain/ai-ui-core": "1.0.30"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/react": "^19.2.0",
@@ -10,6 +10,8 @@ import { useErrorToast, ErrorToast } from "./ErrorToast";
10
10
  import { aiStyles } from "../styles/inline";
11
11
  import { useAiContext } from "../context/AiProvider";
12
12
  import { handleAIError } from "../utils/errorHandler";
13
+ import { useLB } from "../context/LBAuthProvider";
14
+ import { LBAuthModal } from "./LBConnectButton";
13
15
 
14
16
  export interface AiContextButtonProps
15
17
  extends
@@ -51,6 +53,7 @@ export function AiContextButton({
51
53
  ...buttonProps
52
54
  }: AiContextButtonProps) {
53
55
  const [isOpen, setIsOpen] = useState(false);
56
+ const [showAuthModal, setShowAuthModal] = useState(false);
54
57
  const [isResultOpen, setIsResultOpen] = useState(false);
55
58
  const [analysisResult, setAnalysisResult] = useState<{
56
59
  content: string;
@@ -61,6 +64,7 @@ export function AiContextButton({
61
64
  } | null>(null);
62
65
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
63
66
  const { showErrorToast, errorData, errorKey, clearError } = useErrorToast();
67
+ const { status: lbStatus } = useLB();
64
68
 
65
69
  // Récupérer le contexte AiProvider avec fallback sur les props
66
70
  const aiContext = useAiContext();
@@ -72,7 +76,13 @@ export function AiContextButton({
72
76
  apiKeyId,
73
77
  });
74
78
 
79
+ const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
80
+
75
81
  const handleOpenPanel = () => {
82
+ if (!isAuthReady) {
83
+ setShowAuthModal(true);
84
+ return;
85
+ }
76
86
  setIsOpen(true);
77
87
  };
78
88
 
@@ -234,16 +244,21 @@ Analyse ces données et réponds de manière structurée et claire.`;
234
244
  <button
235
245
  {...buttonProps}
236
246
  onClick={handleOpenPanel}
237
- disabled={disabled || loading}
247
+ disabled={disabled || loading || !isAuthReady}
238
248
  className={className}
239
249
  style={{
240
250
  ...aiStyles.button,
241
251
  display: "flex",
242
252
  alignItems: "center",
243
253
  gap: "8px",
244
- cursor: disabled || loading ? "not-allowed" : "pointer",
245
- opacity: disabled || loading ? 0.6 : 1,
246
- backgroundColor: loading ? "#8b5cf6" : "#7c3aed",
254
+ cursor:
255
+ disabled || loading || !isAuthReady ? "not-allowed" : "pointer",
256
+ opacity: disabled || loading || !isAuthReady ? 0.6 : 1,
257
+ backgroundColor: loading
258
+ ? "#8b5cf6"
259
+ : !isAuthReady
260
+ ? "#94a3b8"
261
+ : "#7c3aed",
247
262
  color: "white",
248
263
  border: "none",
249
264
  borderRadius: "12px",
@@ -265,14 +280,14 @@ Analyse ces données et réponds de manière structurée et claire.`;
265
280
  ...buttonProps.style,
266
281
  }}
267
282
  onMouseEnter={(e) => {
268
- if (!disabled && !loading) {
283
+ if (!disabled && !loading && isAuthReady) {
269
284
  e.currentTarget.style.transform = "scale(1.02)";
270
285
  e.currentTarget.style.boxShadow =
271
286
  "0 6px 16px rgba(124, 58, 237, 0.3)";
272
287
  }
273
288
  }}
274
289
  onMouseLeave={(e) => {
275
- if (!disabled && !loading) {
290
+ if (!disabled && !loading && isAuthReady) {
276
291
  e.currentTarget.style.transform = "scale(1)";
277
292
  e.currentTarget.style.boxShadow = loading
278
293
  ? "0 4px 12px rgba(139, 92, 246, 0.3)"
@@ -280,16 +295,19 @@ Analyse ces données et réponds de manière structurée et claire.`;
280
295
  }
281
296
  }}
282
297
  onMouseDown={(e) => {
283
- if (!disabled && !loading) {
298
+ if (!disabled && !loading && isAuthReady) {
284
299
  e.currentTarget.style.transform = "scale(0.98)";
285
300
  }
286
301
  }}
287
302
  onMouseUp={(e) => {
288
- if (!disabled && !loading) {
303
+ if (!disabled && !loading && isAuthReady) {
289
304
  e.currentTarget.style.transform = "scale(1.02)";
290
305
  }
291
306
  }}
292
307
  data-ai-context-button
308
+ title={
309
+ !isAuthReady ? "Authentication required" : "Analyser avec l'IA"
310
+ }
293
311
  >
294
312
  {loading ? (
295
313
  <>
@@ -303,6 +321,21 @@ Analyse ces données et réponds de manière structurée et claire.`;
303
321
  />
304
322
  <span style={{ letterSpacing: "0.025em" }}>Analyse...</span>
305
323
  </>
324
+ ) : !isAuthReady ? (
325
+ <>
326
+ <svg
327
+ width="18"
328
+ height="18"
329
+ viewBox="0 0 24 24"
330
+ fill="none"
331
+ stroke="currentColor"
332
+ strokeWidth="2"
333
+ >
334
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
335
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
336
+ </svg>
337
+ {children || <span>Connexion requise</span>}
338
+ </>
306
339
  ) : (
307
340
  <>
308
341
  <Sparkle
@@ -549,6 +582,10 @@ Analyse ces données et réponds de manière structurée et claire.`;
549
582
  </div>
550
583
  )}
551
584
 
585
+ {showAuthModal && (
586
+ <LBAuthModal onClose={() => setShowAuthModal(false)} />
587
+ )}
588
+
552
589
  {/* Error Toast */}
553
590
  <ErrorToast key={errorKey} error={errorData} onComplete={clearError} />
554
591
  </>
@@ -10,6 +10,8 @@ import { useErrorToast, ErrorToast } from "./ErrorToast";
10
10
  import { aiStyles } from "../styles/inline";
11
11
  import { useAiContext } from "../context/AiProvider";
12
12
  import { handleAIError } from "../utils/errorHandler";
13
+ import { useLB } from "../context/LBAuthProvider";
14
+ import { LBAuthModal } from "./LBConnectButton";
13
15
 
14
16
  export interface AiImageButtonProps
15
17
  extends
@@ -48,6 +50,7 @@ export function AiImageButton({
48
50
  ...buttonProps
49
51
  }: AiImageButtonProps) {
50
52
  const [isOpen, setIsOpen] = useState(false);
53
+ const [showAuthModal, setShowAuthModal] = useState(false);
51
54
  const [generatedImage, setGeneratedImage] = useState<{
52
55
  url: string;
53
56
  prompt: string;
@@ -56,6 +59,7 @@ export function AiImageButton({
56
59
  } | null>(null);
57
60
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
58
61
  const { showErrorToast, errorData, errorKey, clearError } = useErrorToast();
62
+ const { status: lbStatus } = useLB();
59
63
 
60
64
  // Récupérer le contexte AiProvider avec fallback sur les props
61
65
  const aiContext = useAiContext();
@@ -64,7 +68,13 @@ export function AiImageButton({
64
68
 
65
69
  const { generateImage, loading } = useAiCallImage({ baseUrl, apiKeyId });
66
70
 
71
+ const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
72
+
67
73
  const handleOpenPanel = () => {
74
+ if (!isAuthReady) {
75
+ setShowAuthModal(true);
76
+ return;
77
+ }
68
78
  setIsOpen(true);
69
79
  };
70
80
 
@@ -196,16 +206,21 @@ export function AiImageButton({
196
206
  <button
197
207
  {...buttonProps}
198
208
  onClick={handleOpenPanel}
199
- disabled={disabled || loading}
209
+ disabled={disabled || loading || !isAuthReady}
200
210
  className={className}
201
211
  style={{
202
212
  ...aiStyles.button,
203
213
  display: "flex",
204
214
  alignItems: "center",
205
215
  gap: "8px",
206
- cursor: disabled || loading ? "not-allowed" : "pointer",
207
- opacity: disabled || loading ? 0.6 : 1,
208
- backgroundColor: loading ? "#8b5cf6" : "#6366f1",
216
+ cursor:
217
+ disabled || loading || !isAuthReady ? "not-allowed" : "pointer",
218
+ opacity: disabled || loading || !isAuthReady ? 0.6 : 1,
219
+ backgroundColor: loading
220
+ ? "#8b5cf6"
221
+ : !isAuthReady
222
+ ? "#94a3b8"
223
+ : "#6366f1",
209
224
  color: "white",
210
225
  border: "none",
211
226
  borderRadius: "12px",
@@ -226,14 +241,14 @@ export function AiImageButton({
226
241
  ...buttonProps.style,
227
242
  }}
228
243
  onMouseEnter={(e) => {
229
- if (!disabled && !loading) {
244
+ if (!disabled && !loading && isAuthReady) {
230
245
  e.currentTarget.style.transform = "scale(1.02)";
231
246
  e.currentTarget.style.boxShadow =
232
247
  "0 6px 16px rgba(99, 102, 241, 0.3)";
233
248
  }
234
249
  }}
235
250
  onMouseLeave={(e) => {
236
- if (!disabled && !loading) {
251
+ if (!disabled && !loading && isAuthReady) {
237
252
  e.currentTarget.style.transform = "scale(1)";
238
253
  e.currentTarget.style.boxShadow = loading
239
254
  ? "0 4px 12px rgba(139, 92, 246, 0.3)"
@@ -241,16 +256,17 @@ export function AiImageButton({
241
256
  }
242
257
  }}
243
258
  onMouseDown={(e) => {
244
- if (!disabled && !loading) {
259
+ if (!disabled && !loading && isAuthReady) {
245
260
  e.currentTarget.style.transform = "scale(0.98)";
246
261
  }
247
262
  }}
248
263
  onMouseUp={(e) => {
249
- if (!disabled && !loading) {
264
+ if (!disabled && !loading && isAuthReady) {
250
265
  e.currentTarget.style.transform = "scale(1.02)";
251
266
  }
252
267
  }}
253
268
  data-ai-image-button
269
+ title={!isAuthReady ? "Authentication required" : "Générer une image"}
254
270
  >
255
271
  {loading ? (
256
272
  <>
@@ -264,6 +280,21 @@ export function AiImageButton({
264
280
  />
265
281
  <span style={{ letterSpacing: "0.025em" }}>Génération...</span>
266
282
  </>
283
+ ) : !isAuthReady ? (
284
+ <>
285
+ <svg
286
+ width="18"
287
+ height="18"
288
+ viewBox="0 0 24 24"
289
+ fill="none"
290
+ stroke="currentColor"
291
+ strokeWidth="2"
292
+ >
293
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
294
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
295
+ </svg>
296
+ {children || <span>Connexion requise</span>}
297
+ </>
267
298
  ) : (
268
299
  <>
269
300
  <ImageIcon
@@ -415,6 +446,10 @@ export function AiImageButton({
415
446
  </div>
416
447
  )}
417
448
 
449
+ {showAuthModal && (
450
+ <LBAuthModal onClose={() => setShowAuthModal(false)} />
451
+ )}
452
+
418
453
  {/* Error Toast */}
419
454
  <ErrorToast key={errorKey} error={errorData} onComplete={clearError} />
420
455
  </div>
@@ -9,6 +9,8 @@ import { AiPromptPanel } from "./AiPromptPanel";
9
9
  import { UsageToast, useUsageToast } from "./UsageToast";
10
10
  import { aiStyles } from "../styles/inline";
11
11
  import { handleAIError } from "../utils/errorHandler";
12
+ import { useLB } from "../context/LBAuthProvider";
13
+ import { LBAuthModal } from "./LBConnectButton";
12
14
 
13
15
  export interface AiInputProps
14
16
  extends
@@ -36,6 +38,7 @@ export function AiInput({
36
38
  ...inputProps
37
39
  }: AiInputProps) {
38
40
  const [isOpen, setIsOpen] = useState(false);
41
+ const [showAuthModal, setShowAuthModal] = useState(false);
39
42
  const [inputValue, setInputValue] = useState(
40
43
  inputProps.value?.toString() || inputProps.defaultValue?.toString() || ""
41
44
  );
@@ -43,6 +46,7 @@ export function AiInput({
43
46
  const [isButtonHovered, setIsButtonHovered] = useState(false);
44
47
  const inputRef = useRef<HTMLInputElement>(null);
45
48
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
49
+ const { status: lbStatus } = useLB();
46
50
 
47
51
  const { models } = useAiModels({
48
52
  baseUrl,
@@ -52,8 +56,14 @@ export function AiInput({
52
56
  const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
53
57
 
54
58
  const hasConfiguration = Boolean(model && prompt);
59
+ const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
60
+ const shouldShowSparkles = isAuthReady && !disabled;
55
61
 
56
62
  const handleOpenPanel = () => {
63
+ if (!isAuthReady) {
64
+ setShowAuthModal(true);
65
+ return;
66
+ }
57
67
  setIsOpen(true);
58
68
  };
59
69
 
@@ -169,9 +179,15 @@ export function AiInput({
169
179
  onClick={hasConfiguration ? handleQuickGenerate : handleOpenPanel}
170
180
  onMouseEnter={() => setIsButtonHovered(true)}
171
181
  onMouseLeave={() => setIsButtonHovered(false)}
172
- disabled={disabled || loading}
182
+ disabled={disabled || loading || !isAuthReady}
173
183
  type="button"
174
- title={hasConfiguration ? "Generate with AI" : "Setup AI"}
184
+ title={
185
+ !isAuthReady
186
+ ? "Authentication required"
187
+ : hasConfiguration
188
+ ? "Generate with AI"
189
+ : "Setup AI"
190
+ }
175
191
  >
176
192
  {loading ? (
177
193
  <svg
@@ -184,8 +200,20 @@ export function AiInput({
184
200
  >
185
201
  <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" />
186
202
  </svg>
187
- ) : (
203
+ ) : shouldShowSparkles ? (
188
204
  <Sparkles size={16} />
205
+ ) : (
206
+ <svg
207
+ width="16"
208
+ height="16"
209
+ viewBox="0 0 24 24"
210
+ fill="none"
211
+ stroke="currentColor"
212
+ strokeWidth="2"
213
+ >
214
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
215
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
216
+ </svg>
189
217
  )}
190
218
  </button>
191
219
  {isOpen && (
@@ -202,6 +230,9 @@ export function AiInput({
202
230
  enableModelManagement={enableModelManagement}
203
231
  />
204
232
  )}
233
+ {showAuthModal && (
234
+ <LBAuthModal onClose={() => setShowAuthModal(false)} />
235
+ )}
205
236
  {Boolean(toastData) && (
206
237
  <UsageToast
207
238
  key={toastKey}
@@ -14,6 +14,8 @@ import { AiPromptPanel } from "./AiPromptPanel";
14
14
  import { UsageToast, useUsageToast } from "./UsageToast";
15
15
  import { aiStyles } from "../styles/inline";
16
16
  import { handleAIError } from "../utils/errorHandler";
17
+ import { useLB } from "../context/LBAuthProvider";
18
+ import { LBAuthModal } from "./LBConnectButton";
17
19
 
18
20
  export interface AiTextareaProps
19
21
  extends
@@ -40,6 +42,7 @@ export function AiTextarea({
40
42
  ...textareaProps
41
43
  }: AiTextareaProps) {
42
44
  const [isOpen, setIsOpen] = useState(false);
45
+ const [showAuthModal, setShowAuthModal] = useState(false);
43
46
  const [textareaValue, setTextareaValue] = useState(
44
47
  textareaProps.value?.toString() ||
45
48
  textareaProps.defaultValue?.toString() ||
@@ -49,6 +52,7 @@ export function AiTextarea({
49
52
  const [isButtonHovered, setIsButtonHovered] = useState(false);
50
53
  const textareaRef = useRef<HTMLTextAreaElement>(null);
51
54
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
55
+ const { status: lbStatus } = useLB();
52
56
 
53
57
  const { models } = useAiModels({
54
58
  baseUrl,
@@ -58,8 +62,14 @@ export function AiTextarea({
58
62
  const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
59
63
 
60
64
  const hasConfiguration = Boolean(model && prompt);
65
+ const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
66
+ const shouldShowSparkles = isAuthReady && !disabled;
61
67
 
62
68
  const handleOpenPanel = () => {
69
+ if (!isAuthReady) {
70
+ setShowAuthModal(true);
71
+ return;
72
+ }
63
73
  setIsOpen(true);
64
74
  };
65
75
 
@@ -190,9 +200,15 @@ export function AiTextarea({
190
200
  onClick={hasConfiguration ? handleQuickGenerate : handleOpenPanel}
191
201
  onMouseEnter={() => setIsButtonHovered(true)}
192
202
  onMouseLeave={() => setIsButtonHovered(false)}
193
- disabled={disabled || loading}
203
+ disabled={disabled || loading || !isAuthReady}
194
204
  type="button"
195
- title={hasConfiguration ? "Generate with AI" : "Setup AI"}
205
+ title={
206
+ !isAuthReady
207
+ ? "Authentication required"
208
+ : hasConfiguration
209
+ ? "Generate with AI"
210
+ : "Setup AI"
211
+ }
196
212
  >
197
213
  {loading ? (
198
214
  <svg
@@ -205,8 +221,20 @@ export function AiTextarea({
205
221
  >
206
222
  <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" />
207
223
  </svg>
208
- ) : (
224
+ ) : shouldShowSparkles ? (
209
225
  <Sparkles size={16} />
226
+ ) : (
227
+ <svg
228
+ width="16"
229
+ height="16"
230
+ viewBox="0 0 24 24"
231
+ fill="none"
232
+ stroke="currentColor"
233
+ strokeWidth="2"
234
+ >
235
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
236
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
237
+ </svg>
210
238
  )}
211
239
  </button>
212
240
  {isOpen && (
@@ -52,15 +52,17 @@ export function LBConnectButton({
52
52
  onClick={handleClick}
53
53
  className={
54
54
  className ||
55
- "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
55
+ (status === "ready" && user
56
+ ? "px-4 py-2 bg-gradient-to-r from-emerald-500 to-teal-600 text-white rounded-lg hover:from-emerald-600 hover:to-teal-700 transition-all duration-200 shadow-md hover:shadow-lg"
57
+ : "px-4 py-2 bg-gradient-to-r from-violet-500 to-purple-600 text-white rounded-lg hover:from-violet-600 hover:to-purple-700 transition-all duration-200 shadow-md hover:shadow-lg")
56
58
  }
57
59
  disabled={status === "loading"}
58
60
  >
59
61
  {status === "loading"
60
- ? "Chargement..."
62
+ ? "Chargement..."
61
63
  : status === "ready" && user
62
- ? `Connecté (${user.email})`
63
- : label}
64
+ ? `✓ ${user.email}`
65
+ : `🔐 ${label}`}
64
66
  </button>
65
67
 
66
68
  {showModal && <LBAuthModal onClose={handleModalClose} />}
@@ -129,99 +131,227 @@ function LBAuthModal({ onClose }: LBAuthModalProps) {
129
131
  };
130
132
 
131
133
  return (
132
- <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
133
- <div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
134
- <div className="flex justify-between items-center mb-4">
135
- <h2 className="text-xl font-bold">
136
- {step === "login"
137
- ? "Connexion LastBrain"
138
- : "Sélectionner une clé API"}
139
- </h2>
140
- <button
141
- onClick={() => onClose(false)}
142
- className="text-gray-500 hover:text-gray-700"
143
- >
144
-
145
- </button>
146
- </div>
147
-
148
- {step === "login" ? (
149
- <form onSubmit={handleLogin} className="space-y-4">
150
- <div>
151
- <label className="block text-sm font-medium mb-1">Email</label>
152
- <input
153
- type="email"
154
- value={email}
155
- onChange={(e) => setEmail(e.target.value)}
156
- className="w-full px-3 py-2 border rounded dark:bg-gray-700"
157
- required
158
- autoFocus
159
- />
160
- </div>
161
-
134
+ <div className="fixed inset-0 bg-gradient-to-br from-slate-900/95 via-purple-900/95 to-slate-900/95 backdrop-blur-sm flex items-center justify-center z-50 p-4">
135
+ <div className="bg-white dark:bg-gradient-to-br dark:from-slate-800 dark:to-slate-900 rounded-2xl shadow-2xl max-w-md w-full border border-slate-200 dark:border-slate-700 overflow-hidden">
136
+ {/* Header avec gradient */}
137
+ <div className="bg-gradient-to-r from-violet-500 to-purple-600 p-6 text-white">
138
+ <div className="flex justify-between items-center">
162
139
  <div>
163
- <label className="block text-sm font-medium mb-1">
164
- Mot de passe
165
- </label>
166
- <input
167
- type="password"
168
- value={password}
169
- onChange={(e) => setPassword(e.target.value)}
170
- className="w-full px-3 py-2 border rounded dark:bg-gray-700"
171
- required
172
- />
140
+ <h2 className="text-2xl font-bold flex items-center gap-2">
141
+ {step === "login" ? "🔐 Connexion" : "🔑 Sélection clé API"}
142
+ </h2>
143
+ <p className="text-violet-100 text-sm mt-1">
144
+ {step === "login"
145
+ ? "Accédez à vos outils IA"
146
+ : "Créez une session sécurisée 72h"}
147
+ </p>
173
148
  </div>
174
-
175
- {error && <div className="text-red-600 text-sm">{error}</div>}
176
-
177
149
  <button
178
- type="submit"
179
- disabled={loading}
180
- className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
150
+ onClick={() => onClose(false)}
151
+ className="text-white/80 hover:text-white hover:bg-white/20 rounded-lg p-2 transition-colors"
181
152
  >
182
- {loading ? "Connexion..." : "Se connecter"}
153
+ <svg
154
+ className="w-5 h-5"
155
+ fill="none"
156
+ stroke="currentColor"
157
+ viewBox="0 0 24 24"
158
+ >
159
+ <path
160
+ strokeLinecap="round"
161
+ strokeLinejoin="round"
162
+ strokeWidth={2}
163
+ d="M6 18L18 6M6 6l12 12"
164
+ />
165
+ </svg>
183
166
  </button>
167
+ </div>
168
+ </div>
169
+
170
+ <div className="p-6">
171
+ {step === "login" ? (
172
+ <form onSubmit={handleLogin} className="space-y-4">
173
+ <div>
174
+ <label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">
175
+ 📧 Email
176
+ </label>
177
+ <input
178
+ type="email"
179
+ value={email}
180
+ onChange={(e) => setEmail(e.target.value)}
181
+ className="w-full px-4 py-3 border-2 border-slate-200 dark:border-slate-600 rounded-lg dark:bg-slate-800 dark:text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-200 dark:focus:ring-violet-900 transition-all outline-none"
182
+ placeholder="votre@email.com"
183
+ required
184
+ autoFocus
185
+ />
186
+ </div>
184
187
 
185
- <p className="text-sm text-gray-600 dark:text-gray-400 text-center">
186
- Pas encore de compte ?{" "}
187
- <a
188
- href="https://lastbrain.io/signup"
189
- target="_blank"
190
- rel="noopener noreferrer"
191
- className="text-blue-600 hover:underline"
188
+ <div>
189
+ <label className="block text-sm font-semibold mb-2 text-slate-700 dark:text-slate-300">
190
+ 🔒 Mot de passe
191
+ </label>
192
+ <input
193
+ type="password"
194
+ value={password}
195
+ onChange={(e) => setPassword(e.target.value)}
196
+ className="w-full px-4 py-3 border-2 border-slate-200 dark:border-slate-600 rounded-lg dark:bg-slate-800 dark:text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-200 dark:focus:ring-violet-900 transition-all outline-none"
197
+ placeholder="••••••••"
198
+ required
199
+ />
200
+ </div>
201
+
202
+ {error && (
203
+ <div className="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-500 p-4 rounded">
204
+ <div className="flex items-start gap-2">
205
+ <span className="text-red-500 text-xl">⚠️</span>
206
+ <p className="text-red-700 dark:text-red-400 text-sm font-medium">
207
+ {error}
208
+ </p>
209
+ </div>
210
+ </div>
211
+ )}
212
+
213
+ <button
214
+ type="submit"
215
+ disabled={loading}
216
+ className="w-full px-6 py-3 bg-gradient-to-r from-violet-500 to-purple-600 text-white font-semibold rounded-lg hover:from-violet-600 hover:to-purple-700 disabled:from-slate-400 disabled:to-slate-500 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
192
217
  >
193
- Créer un compte
194
- </a>
195
- </p>
196
- </form>
197
- ) : (
198
- <div className="space-y-4">
199
- <p className="text-sm text-gray-600 dark:text-gray-400">
200
- Sélectionnez une clé API pour créer une session de 72h
201
- </p>
202
-
203
- <div className="space-y-2">
204
- {apiKeys.map((key) => (
205
- <button
206
- key={key.id}
207
- onClick={() => handleSelectKey(key.id)}
208
- disabled={!key.isActive || loading}
209
- className="w-full text-left px-4 py-3 border rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
218
+ {loading ? (
219
+ <span className="flex items-center justify-center gap-2">
220
+ <svg
221
+ className="animate-spin h-5 w-5"
222
+ xmlns="http://www.w3.org/2000/svg"
223
+ fill="none"
224
+ viewBox="0 0 24 24"
225
+ >
226
+ <circle
227
+ className="opacity-25"
228
+ cx="12"
229
+ cy="12"
230
+ r="10"
231
+ stroke="currentColor"
232
+ strokeWidth="4"
233
+ ></circle>
234
+ <path
235
+ className="opacity-75"
236
+ fill="currentColor"
237
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
238
+ ></path>
239
+ </svg>
240
+ Connexion en cours...
241
+ </span>
242
+ ) : (
243
+ "🚀 Se connecter"
244
+ )}
245
+ </button>
246
+
247
+ <div className="relative my-6">
248
+ <div className="absolute inset-0 flex items-center">
249
+ <div className="w-full border-t border-slate-200 dark:border-slate-700"></div>
250
+ </div>
251
+ <div className="relative flex justify-center text-sm">
252
+ <span className="px-4 bg-white dark:bg-slate-800 text-slate-500">
253
+ ou
254
+ </span>
255
+ </div>
256
+ </div>
257
+
258
+ <div className="bg-gradient-to-r from-violet-50 to-purple-50 dark:from-violet-900/20 dark:to-purple-900/20 rounded-lg p-4 border border-violet-200 dark:border-violet-800">
259
+ <p className="text-sm text-slate-700 dark:text-slate-300 text-center">
260
+ Pas encore de compte ?
261
+ </p>
262
+ <a
263
+ href="https://lastbrain.io/signup"
264
+ target="_blank"
265
+ rel="noopener noreferrer"
266
+ className="block mt-2 text-center text-violet-600 dark:text-violet-400 hover:text-violet-700 dark:hover:text-violet-300 font-semibold text-sm hover:underline"
210
267
  >
211
- <div className="font-medium">{key.name}</div>
212
- <div className="text-sm text-gray-500">
213
- {key.keyPrefix}...
268
+ Créer un compte gratuitement
269
+ </a>
270
+ </div>
271
+ </form>
272
+ ) : (
273
+ <div className="space-y-4">
274
+ <p className="text-sm text-slate-600 dark:text-slate-400 bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg border border-blue-200 dark:border-blue-800">
275
+ ℹ️ Sélectionnez une clé API pour créer une session sécurisée de
276
+ 72h
277
+ </p>
278
+
279
+ <div className="space-y-3 max-h-96 overflow-y-auto">
280
+ {apiKeys.map((key) => (
281
+ <button
282
+ key={key.id}
283
+ onClick={() => handleSelectKey(key.id)}
284
+ disabled={!key.isActive || loading}
285
+ className={`w-full text-left px-5 py-4 border-2 rounded-lg transition-all transform hover:scale-[1.02] ${
286
+ key.isActive
287
+ ? "border-slate-200 dark:border-slate-600 hover:border-violet-400 dark:hover:border-violet-500 hover:bg-gradient-to-r hover:from-violet-50 hover:to-purple-50 dark:hover:from-violet-900/20 dark:hover:to-purple-900/20"
288
+ : "border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50 opacity-50 cursor-not-allowed"
289
+ }`}
290
+ >
291
+ <div className="flex items-start justify-between gap-2">
292
+ <div className="flex-1">
293
+ <div className="font-semibold text-slate-900 dark:text-white flex items-center gap-2">
294
+ {key.isActive ? "🔑" : "🔒"}
295
+ {key.name}
296
+ </div>
297
+ <div className="text-sm text-slate-500 dark:text-slate-400 mt-1 font-mono">
298
+ {key.keyPrefix}...
299
+ </div>
300
+ {key.scopes && (
301
+ <div className="flex gap-1 mt-2 flex-wrap">
302
+ {key.scopes.slice(0, 3).map((scope: string) => (
303
+ <span
304
+ key={scope}
305
+ className="text-xs px-2 py-1 bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 rounded"
306
+ >
307
+ {scope}
308
+ </span>
309
+ ))}
310
+ {key.scopes.length > 3 && (
311
+ <span className="text-xs px-2 py-1 bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 rounded">
312
+ +{key.scopes.length - 3}
313
+ </span>
314
+ )}
315
+ </div>
316
+ )}
317
+ </div>
318
+ {key.isActive ? (
319
+ <svg
320
+ className="w-5 h-5 text-violet-500 flex-shrink-0 mt-1"
321
+ fill="none"
322
+ stroke="currentColor"
323
+ viewBox="0 0 24 24"
324
+ >
325
+ <path
326
+ strokeLinecap="round"
327
+ strokeLinejoin="round"
328
+ strokeWidth={2}
329
+ d="M9 5l7 7-7 7"
330
+ />
331
+ </svg>
332
+ ) : (
333
+ <span className="text-xs text-red-600 dark:text-red-400 font-semibold bg-red-100 dark:bg-red-900/30 px-2 py-1 rounded">
334
+ Inactive
335
+ </span>
336
+ )}
337
+ </div>
338
+ </button>
339
+ ))}
340
+ </div>
341
+
342
+ {error && (
343
+ <div className="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-500 p-4 rounded">
344
+ <div className="flex items-start gap-2">
345
+ <span className="text-red-500 text-xl">⚠️</span>
346
+ <p className="text-red-700 dark:text-red-400 text-sm font-medium">
347
+ {error}
348
+ </p>
214
349
  </div>
215
- {!key.isActive && (
216
- <div className="text-xs text-red-600">Inactive</div>
217
- )}
218
- </button>
219
- ))}
350
+ </div>
351
+ )}
220
352
  </div>
221
-
222
- {error && <div className="text-red-600 text-sm">{error}</div>}
223
- </div>
224
- )}
353
+ )}
354
+ </div>
225
355
  </div>
226
356
  </div>
227
357
  );