@lastbrain/ai-ui-react 1.0.49 → 1.0.52

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,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, type ButtonHTMLAttributes } from "react";
4
- import { Loader2, X, FileText, Sparkle, Download } from "lucide-react";
4
+ import { Loader2, X, FileText, Sparkle, Download, Lock } from "lucide-react";
5
5
  import type { BaseAiProps } from "../types";
6
6
  import { useAiCallText } from "../hooks/useAiCallText";
7
7
  import { AiPromptPanel } from "./AiPromptPanel";
@@ -272,7 +272,7 @@ Analyse ces données et réponds de manière structurée et claire.`;
272
272
  border: "none",
273
273
  borderRadius: "12px",
274
274
  // padding: "12px 20px",
275
- margin: "0px 8px",
275
+
276
276
  fontSize: "14px",
277
277
  fontWeight: "600",
278
278
  minWidth: "20px",
@@ -332,17 +332,13 @@ Analyse ces données et réponds de manière structurée et claire.`;
332
332
  </>
333
333
  ) : !isAuthReady ? (
334
334
  <>
335
- <svg
336
- width="18"
337
- height="18"
338
- viewBox="0 0 24 24"
339
- fill="none"
340
- stroke="currentColor"
341
- strokeWidth="2"
342
- >
343
- <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
344
- <path d="M7 11V7a5 5 0 0 1 10 0v4" />
345
- </svg>
335
+ <Lock
336
+ size={18}
337
+ style={{
338
+ color: "white",
339
+ filter: "drop-shadow(0 0 2px rgba(255,255,255,0.2))",
340
+ }}
341
+ />
346
342
  {children || <span>Connexion requise</span>}
347
343
  </>
348
344
  ) : (
@@ -1,7 +1,14 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, type ButtonHTMLAttributes } from "react";
4
- import { ImageIcon, Loader2, Download, ExternalLink, X } from "lucide-react";
4
+ import {
5
+ ImageIcon,
6
+ Loader2,
7
+ Download,
8
+ ExternalLink,
9
+ X,
10
+ Lock,
11
+ } from "lucide-react";
5
12
  import type { BaseAiProps } from "../types";
6
13
  import { useAiCallImage } from "../hooks/useAiCallImage";
7
14
  import { AiPromptPanel } from "./AiPromptPanel";
@@ -291,17 +298,13 @@ export function AiImageButton({
291
298
  </>
292
299
  ) : !isAuthReady ? (
293
300
  <>
294
- <svg
295
- width="18"
296
- height="18"
297
- viewBox="0 0 24 24"
298
- fill="none"
299
- stroke="currentColor"
300
- strokeWidth="2"
301
- >
302
- <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
303
- <path d="M7 11V7a5 5 0 0 1 10 0v4" />
304
- </svg>
301
+ <Lock
302
+ size={18}
303
+ style={{
304
+ color: "white",
305
+ filter: "drop-shadow(0 0 2px rgba(255,255,255,0.2))",
306
+ }}
307
+ />
305
308
  {children || <span>Connexion requise</span>}
306
309
  </>
307
310
  ) : (
@@ -1,13 +1,16 @@
1
1
  "use client";
2
2
 
3
3
  import React, { useState, type SelectHTMLAttributes } from "react";
4
+ import { Sparkles, Lock } from "lucide-react";
4
5
  import type { BaseAiProps } from "../types";
5
6
  import { useAiCallText } from "../hooks/useAiCallText";
6
7
  import { useAiModels } from "../hooks/useAiModels";
7
8
  import { AiPromptPanel } from "./AiPromptPanel";
8
9
  import { UsageToast, useUsageToast } from "./UsageToast";
10
+ import { LBSigninModal } from "./LBSigninModal";
9
11
  import { aiStyles } from "../styles/inline";
10
12
  import { handleAIError } from "../utils/errorHandler";
13
+ import { useLB } from "../context/LBAuthProvider";
11
14
 
12
15
  export interface AiSelectProps
13
16
  extends
@@ -34,9 +37,20 @@ export function AiSelect({
34
37
  ...selectProps
35
38
  }: AiSelectProps) {
36
39
  const [isOpen, setIsOpen] = useState(false);
40
+ const [showAuthModal, setShowAuthModal] = useState(false);
37
41
  const [isFocused, setIsFocused] = useState(false);
38
42
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
39
43
 
44
+ // Rendre l'authentification optionnelle
45
+ let lbStatus: string | undefined;
46
+ try {
47
+ const lbContext = useLB();
48
+ lbStatus = lbContext.status;
49
+ } catch {
50
+ // LBProvider n'est pas disponible, ignorer
51
+ lbStatus = undefined;
52
+ }
53
+
40
54
  const { models } = useAiModels({
41
55
  baseUrl,
42
56
  apiKeyId,
@@ -44,7 +58,14 @@ export function AiSelect({
44
58
  });
45
59
  const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
46
60
 
61
+ const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
62
+ const shouldShowSparkles = isAuthReady && !disabled;
63
+
47
64
  const handleOpenPanel = () => {
65
+ if (!isAuthReady) {
66
+ setShowAuthModal(true);
67
+ return;
68
+ }
48
69
  setIsOpen(true);
49
70
  };
50
71
 
@@ -79,12 +100,13 @@ export function AiSelect({
79
100
  };
80
101
 
81
102
  return (
82
- <div style={{ width: "100%" }} className={className}>
103
+ <div style={{ width: "100%", position: "relative" }} className={className}>
83
104
  <select
84
105
  {...selectProps}
85
106
  style={{
86
107
  ...aiStyles.select,
87
108
  ...(isFocused && aiStyles.selectFocus),
109
+ paddingRight: "40px", // Space for AI button
88
110
  }}
89
111
  onFocus={(e) => {
90
112
  setIsFocused(true);
@@ -98,6 +120,48 @@ export function AiSelect({
98
120
  >
99
121
  {children}
100
122
  </select>
123
+
124
+ {/* AI Button */}
125
+ <button
126
+ onClick={handleOpenPanel}
127
+ style={{
128
+ position: "absolute",
129
+ right: "8px",
130
+ top: "50%",
131
+ transform: "translateY(-50%)",
132
+ background: "none",
133
+ border: "none",
134
+ cursor: "pointer",
135
+ padding: "4px",
136
+ borderRadius: "4px",
137
+ display: "flex",
138
+ alignItems: "center",
139
+ color: shouldShowSparkles ? "#6366f1" : "#ef4444",
140
+ }}
141
+ title={
142
+ shouldShowSparkles
143
+ ? "Générer avec l'IA"
144
+ : "Se connecter pour utiliser l'IA"
145
+ }
146
+ disabled={disabled || loading}
147
+ >
148
+ {loading ? (
149
+ <svg
150
+ style={aiStyles.spinner}
151
+ width="16"
152
+ height="16"
153
+ viewBox="0 0 24 24"
154
+ fill="none"
155
+ stroke="currentColor"
156
+ >
157
+ <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" />
158
+ </svg>
159
+ ) : shouldShowSparkles ? (
160
+ <Sparkles size={16} />
161
+ ) : (
162
+ <Lock size={16} />
163
+ )}
164
+ </button>
101
165
  {isOpen && (
102
166
  <AiPromptPanel
103
167
  isOpen={isOpen}
@@ -115,6 +179,12 @@ export function AiSelect({
115
179
  onComplete={clearToast}
116
180
  />
117
181
  )}
182
+
183
+ {/* Modal signin pour les utilisateurs non connectés */}
184
+ <LBSigninModal
185
+ isOpen={showAuthModal}
186
+ onClose={() => setShowAuthModal(false)}
187
+ />
118
188
  </div>
119
189
  );
120
190
  }
@@ -12,7 +12,7 @@ import {
12
12
  Power,
13
13
  LogOut,
14
14
  Key,
15
- RefreshCw,
15
+ ArrowRightLeft,
16
16
  } from "lucide-react";
17
17
  import { aiStyles, calculateTooltipPosition } from "../styles/inline";
18
18
  import { useLB } from "../context/LBAuthProvider";
@@ -38,6 +38,7 @@ export function AiStatusButton({
38
38
  let apiKeys: any[] = [];
39
39
  let accessToken: string | undefined;
40
40
  let selectApiKeyWithToken: ((apiKeyId: string) => Promise<void>) | undefined;
41
+ let lbApiStatus: any = null;
41
42
 
42
43
  try {
43
44
  const lbContext = useLB();
@@ -47,6 +48,7 @@ export function AiStatusButton({
47
48
  apiKeys = lbContext.apiKeys || [];
48
49
  accessToken = lbContext.accessToken;
49
50
  selectApiKeyWithToken = lbContext.selectApiKeyWithToken;
51
+ lbApiStatus = lbContext.apiStatus;
50
52
  } catch {
51
53
  // LBProvider n'est pas disponible, ignorer
52
54
  lbStatus = undefined;
@@ -54,6 +56,9 @@ export function AiStatusButton({
54
56
  logout = undefined;
55
57
  }
56
58
 
59
+ // Utiliser le status du contexte LB si pas de prop status
60
+ const effectiveStatus = status || lbApiStatus;
61
+
57
62
  // Récupérer refetchProviders depuis AiProvider si disponible
58
63
  let refetchProviders: (() => Promise<void>) | undefined;
59
64
  try {
@@ -155,8 +160,8 @@ export function AiStatusButton({
155
160
  );
156
161
  };
157
162
 
158
- const balanceUsage = status?.balance as BalanceUsage | undefined;
159
- const storageUsage = status?.storage as StorageUsage | undefined;
163
+ const balanceUsage = effectiveStatus?.balance as BalanceUsage | undefined;
164
+ const storageUsage = effectiveStatus?.storage as StorageUsage | undefined;
160
165
 
161
166
  const balanceTotal =
162
167
  balanceUsage?.total ??
@@ -300,7 +305,7 @@ export function AiStatusButton({
300
305
  );
301
306
  }
302
307
 
303
- if (!status) {
308
+ if (!effectiveStatus) {
304
309
  // Si pas de statut API et pas de LBProvider, afficher message simple
305
310
  if (!lbStatus && lbStatus !== "ready") {
306
311
  return (
@@ -592,7 +597,6 @@ export function AiStatusButton({
592
597
  </div>
593
598
  <div
594
599
  style={{
595
- ...aiStyles.tooltipSection,
596
600
  paddingBottom: "12px",
597
601
  }}
598
602
  >
@@ -691,7 +695,7 @@ export function AiStatusButton({
691
695
  <div style={aiStyles.tooltipHeader}>API Status</div>
692
696
 
693
697
  {/* User Info Section */}
694
- {status.user?.email && (
698
+ {effectiveStatus.user?.email && (
695
699
  <div
696
700
  style={{
697
701
  ...aiStyles.tooltipSection,
@@ -709,7 +713,7 @@ export function AiStatusButton({
709
713
  textOverflow: "ellipsis",
710
714
  }}
711
715
  >
712
- {status.user.email}
716
+ {effectiveStatus.user.email}
713
717
  </span>
714
718
  </div>
715
719
  </div>
@@ -718,7 +722,9 @@ export function AiStatusButton({
718
722
  <div
719
723
  style={{
720
724
  ...aiStyles.tooltipSection,
721
- ...(status.user?.email ? {} : aiStyles.tooltipSectionFirst),
725
+ ...(effectiveStatus.user?.email
726
+ ? {}
727
+ : aiStyles.tooltipSectionFirst),
722
728
  }}
723
729
  >
724
730
  <div style={aiStyles.tooltipRow}>
@@ -731,12 +737,15 @@ export function AiStatusButton({
731
737
  }}
732
738
  >
733
739
  <span style={aiStyles.tooltipValue}>
734
- {status.apiKey?.name || status.api_key?.name || "Unknown"}
740
+ {effectiveStatus.apiKey?.name ||
741
+ effectiveStatus.api_key?.name ||
742
+ "Unknown"}
735
743
  </span>
736
- {apiKeys.length > 1 && selectApiKeyWithToken && (
744
+ {selectApiKeyWithToken && (
737
745
  <button
738
746
  onClick={(e) => {
739
747
  e.stopPropagation();
748
+ setShowTooltip(false);
740
749
  setShowApiKeySelector(true);
741
750
  }}
742
751
  style={{
@@ -766,7 +775,7 @@ export function AiStatusButton({
766
775
  }}
767
776
  title="Change API Key"
768
777
  >
769
- <RefreshCw
778
+ <ArrowRightLeft
770
779
  size={12}
771
780
  style={{ color: "rgba(139, 92, 246, 1)" }}
772
781
  />
@@ -777,14 +786,16 @@ export function AiStatusButton({
777
786
  <div style={aiStyles.tooltipRow}>
778
787
  <span style={aiStyles.tooltipLabel}>Env:</span>
779
788
  <span style={aiStyles.tooltipValue}>
780
- {status.apiKey?.env || status.api_key?.env || "N/A"}
789
+ {effectiveStatus.apiKey?.env ||
790
+ effectiveStatus.api_key?.env ||
791
+ "N/A"}
781
792
  </span>
782
793
  </div>
783
794
  <div style={aiStyles.tooltipRow}>
784
795
  <span style={aiStyles.tooltipLabel}>Rate Limit:</span>
785
796
  <span style={aiStyles.tooltipValue}>
786
- {status.apiKey?.rate_limit_rpm ||
787
- status.api_key?.rate_limit_rpm ||
797
+ {effectiveStatus.apiKey?.rate_limit_rpm ||
798
+ effectiveStatus.api_key?.rate_limit_rpm ||
788
799
  0}{" "}
789
800
  req/min
790
801
  </span>
@@ -792,7 +803,7 @@ export function AiStatusButton({
792
803
  </div>
793
804
 
794
805
  <div style={aiStyles.tooltipSection}>
795
- <div style={aiStyles.tooltipSubtitle}>Balance</div>
806
+ <div style={aiStyles.tooltipSubtitle}>Wallet</div>
796
807
  <div style={aiStyles.tooltipRow}>
797
808
  <span style={aiStyles.tooltipLabel}>Total:</span>
798
809
  <span style={aiStyles.tooltipValue}>
@@ -1001,6 +1012,7 @@ export function AiStatusButton({
1001
1012
  justifyContent: "center",
1002
1013
  color: "#8b5cf6",
1003
1014
  transition: "all 0.2s ease",
1015
+ borderRadius: "4px",
1004
1016
  }}
1005
1017
  onMouseEnter={(e) => {
1006
1018
  Object.assign(e.currentTarget.style, {
@@ -1043,6 +1055,7 @@ export function AiStatusButton({
1043
1055
  justifyContent: "center",
1044
1056
  color: "#ef4444",
1045
1057
  transition: "all 0.2s ease",
1058
+ borderRadius: "4px",
1046
1059
  }}
1047
1060
  onMouseEnter={(e) => {
1048
1061
  Object.assign(e.currentTarget.style, {
@@ -6,7 +6,7 @@ import React, {
6
6
  useLayoutEffect,
7
7
  type TextareaHTMLAttributes,
8
8
  } from "react";
9
- import { Sparkles } from "lucide-react";
9
+ import { Sparkles, Lock } from "lucide-react";
10
10
  import type { BaseAiProps } from "../types";
11
11
  import { useAiCallText } from "../hooks/useAiCallText";
12
12
  import { useAiModels } from "../hooks/useAiModels";
@@ -233,17 +233,7 @@ export function AiTextarea({
233
233
  ) : shouldShowSparkles ? (
234
234
  <Sparkles size={16} />
235
235
  ) : (
236
- <svg
237
- width="16"
238
- height="16"
239
- viewBox="0 0 24 24"
240
- fill="none"
241
- stroke="currentColor"
242
- strokeWidth="2"
243
- >
244
- <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
245
- <path d="M7 11V7a5 5 0 0 1 10 0v4" />
246
- </svg>
236
+ <Lock size={16} />
247
237
  )}
248
238
  </button>
249
239
  {isOpen && (
@@ -66,7 +66,7 @@ export function LBApiKeySelector({
66
66
  left: 0,
67
67
  right: 0,
68
68
  bottom: 0,
69
- background: "var(--ai-overlay-bg, rgba(0, 0, 0, 0.75))",
69
+ background: "rgba(0, 0, 0, 0.75)",
70
70
  backdropFilter: "blur(8px)",
71
71
  }}
72
72
  />
@@ -75,8 +75,8 @@ export function LBApiKeySelector({
75
75
  <div
76
76
  style={{
77
77
  position: "relative",
78
- background: "var(--ai-modal-bg, #1f2937)",
79
- border: "1px solid var(--ai-border-primary, #374151)",
78
+ background: "light-dark(#ffffff, #1e293b)",
79
+ border: "1px solid light-dark(#e2e8f0, #334155)",
80
80
  borderRadius: "12px",
81
81
  padding: "24px",
82
82
  maxWidth: "500px",
@@ -90,7 +90,7 @@ export function LBApiKeySelector({
90
90
  margin: "0 0 8px 0",
91
91
  fontSize: "20px",
92
92
  fontWeight: 600,
93
- color: "var(--ai-text-primary, #f9fafb)",
93
+ color: "light-dark(#1e293b, #f8fafc)",
94
94
  textAlign: "center",
95
95
  }}
96
96
  >
@@ -101,7 +101,7 @@ export function LBApiKeySelector({
101
101
  style={{
102
102
  margin: "0 0 24px 0",
103
103
  fontSize: "14px",
104
- color: "var(--ai-text-secondary, #9ca3af)",
104
+ color: "light-dark(#64748b, #94a3b8)",
105
105
  textAlign: "center",
106
106
  lineHeight: "1.5",
107
107
  }}
@@ -133,12 +133,10 @@ export function LBApiKeySelector({
133
133
  alignItems: "center",
134
134
  padding: "12px 16px",
135
135
  background: isSelected
136
- ? "var(--ai-input-bg-focus, #374151)"
137
- : "var(--ai-input-bg, #111827)",
136
+ ? "light-dark(#f1f5f9, #334155)"
137
+ : "light-dark(#f8fafc, #0f172a)",
138
138
  border: `2px solid ${
139
- isSelected
140
- ? "var(--ai-accent-primary, #8b5cf6)"
141
- : "var(--ai-border-primary, #374151)"
139
+ isSelected ? "#8b5cf6" : "light-dark(#e2e8f0, #334155)"
142
140
  }`,
143
141
  borderRadius: "8px",
144
142
  cursor: isActive ? "pointer" : "not-allowed",
@@ -148,17 +146,17 @@ export function LBApiKeySelector({
148
146
  onMouseEnter={(e) => {
149
147
  if (isActive && !isSelected) {
150
148
  e.currentTarget.style.borderColor =
151
- "var(--ai-border-hover, #4b5563)";
149
+ "light-dark(#cbd5e1, #475569)";
152
150
  e.currentTarget.style.background =
153
- "var(--ai-input-bg-focus, #374151)";
151
+ "light-dark(#f1f5f9, #334155)";
154
152
  }
155
153
  }}
156
154
  onMouseLeave={(e) => {
157
155
  if (isActive && !isSelected) {
158
156
  e.currentTarget.style.borderColor =
159
- "var(--ai-border-primary, #374151)";
157
+ "light-dark(#e2e8f0, #334155)";
160
158
  e.currentTarget.style.background =
161
- "var(--ai-input-bg, #111827)";
159
+ "light-dark(#f8fafc, #0f172a)";
162
160
  }
163
161
  }}
164
162
  >
@@ -171,7 +169,7 @@ export function LBApiKeySelector({
171
169
  onChange={(e) => setSelectedKeyId(e.target.value)}
172
170
  style={{
173
171
  marginRight: "12px",
174
- accentColor: "var(--ai-accent-primary, #8b5cf6)",
172
+ accentColor: "#8b5cf6",
175
173
  cursor: isActive ? "pointer" : "not-allowed",
176
174
  }}
177
175
  />
@@ -180,7 +178,7 @@ export function LBApiKeySelector({
180
178
  style={{
181
179
  fontSize: "14px",
182
180
  fontWeight: 500,
183
- color: "var(--ai-text-primary, #f9fafb)",
181
+ color: "light-dark(#1e293b, #f8fafc)",
184
182
  marginBottom: "4px",
185
183
  }}
186
184
  >
@@ -189,7 +187,7 @@ export function LBApiKeySelector({
189
187
  <div
190
188
  style={{
191
189
  fontSize: "12px",
192
- color: "var(--ai-text-secondary, #9ca3af)",
190
+ color: "light-dark(#64748b, #94a3b8)",
193
191
  fontFamily: "monospace",
194
192
  }}
195
193
  >
@@ -261,9 +259,9 @@ export function LBApiKeySelector({
261
259
  flex: 1,
262
260
  padding: "12px",
263
261
  background: "transparent",
264
- border: "1px solid var(--ai-border-primary, #374151)",
262
+ border: "1px solid light-dark(#e2e8f0, #334155)",
265
263
  borderRadius: "8px",
266
- color: "var(--ai-text-secondary, #9ca3af)",
264
+ color: "light-dark(#64748b, #94a3b8)",
267
265
  fontSize: "14px",
268
266
  fontWeight: 600,
269
267
  cursor: loading ? "not-allowed" : "pointer",
@@ -273,16 +271,16 @@ export function LBApiKeySelector({
273
271
  onMouseEnter={(e) => {
274
272
  if (!loading) {
275
273
  e.currentTarget.style.background =
276
- "var(--ai-input-bg, #111827)";
274
+ "light-dark(#f8fafc, #0f172a)";
277
275
  e.currentTarget.style.borderColor =
278
- "var(--ai-border-hover, #4b5563)";
276
+ "light-dark(#cbd5e1, #475569)";
279
277
  }
280
278
  }}
281
279
  onMouseLeave={(e) => {
282
280
  if (!loading) {
283
281
  e.currentTarget.style.background = "transparent";
284
282
  e.currentTarget.style.borderColor =
285
- "var(--ai-border-primary, #374151)";
283
+ "light-dark(#e2e8f0, #334155)";
286
284
  }
287
285
  }}
288
286
  >
@@ -295,7 +293,7 @@ export function LBApiKeySelector({
295
293
  flex: 1,
296
294
  padding: "12px",
297
295
  background: loading
298
- ? "var(--ai-input-bg, #111827)"
296
+ ? "light-dark(#e2e8f0, #0f172a)"
299
297
  : "linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)",
300
298
  border: "none",
301
299
  borderRadius: "8px",