@lastbrain/ai-ui-react 1.0.68 → 1.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/components/AiChipLabel.d.ts +8 -3
  2. package/dist/components/AiChipLabel.d.ts.map +1 -1
  3. package/dist/components/AiChipLabel.js +21 -70
  4. package/dist/components/AiContextButton.d.ts +5 -1
  5. package/dist/components/AiContextButton.d.ts.map +1 -1
  6. package/dist/components/AiContextButton.js +67 -291
  7. package/dist/components/AiImageButton.d.ts +5 -1
  8. package/dist/components/AiImageButton.d.ts.map +1 -1
  9. package/dist/components/AiImageButton.js +6 -142
  10. package/dist/components/AiInput.d.ts +5 -3
  11. package/dist/components/AiInput.d.ts.map +1 -1
  12. package/dist/components/AiInput.js +13 -25
  13. package/dist/components/AiPromptPanel.d.ts.map +1 -1
  14. package/dist/components/AiPromptPanel.js +58 -212
  15. package/dist/components/AiSelect.d.ts +5 -3
  16. package/dist/components/AiSelect.d.ts.map +1 -1
  17. package/dist/components/AiSelect.js +21 -30
  18. package/dist/components/AiStatusButton.d.ts +4 -1
  19. package/dist/components/AiStatusButton.d.ts.map +1 -1
  20. package/dist/components/AiStatusButton.js +198 -668
  21. package/dist/components/AiTextarea.d.ts +4 -2
  22. package/dist/components/AiTextarea.d.ts.map +1 -1
  23. package/dist/components/AiTextarea.js +14 -26
  24. package/dist/components/LBApiKeySelector.d.ts.map +1 -1
  25. package/dist/components/LBApiKeySelector.js +5 -166
  26. package/dist/components/LBConnectButton.d.ts +4 -7
  27. package/dist/components/LBConnectButton.d.ts.map +1 -1
  28. package/dist/components/LBConnectButton.js +17 -86
  29. package/dist/components/LBSigninModal.d.ts +1 -1
  30. package/dist/components/LBSigninModal.d.ts.map +1 -1
  31. package/dist/components/LBSigninModal.js +42 -320
  32. package/dist/examples/AiUiPremiumShowcase.d.ts +2 -0
  33. package/dist/examples/AiUiPremiumShowcase.d.ts.map +1 -0
  34. package/dist/examples/AiUiPremiumShowcase.js +15 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -0
  38. package/dist/styles/inline.d.ts +1 -0
  39. package/dist/styles/inline.d.ts.map +1 -1
  40. package/dist/styles/inline.js +25 -129
  41. package/dist/styles.css +1268 -369
  42. package/dist/types.d.ts +3 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +2 -2
  45. package/src/components/AiChipLabel.tsx +64 -101
  46. package/src/components/AiContextButton.tsx +138 -430
  47. package/src/components/AiImageButton.tsx +29 -190
  48. package/src/components/AiInput.tsx +49 -74
  49. package/src/components/AiPromptPanel.tsx +71 -254
  50. package/src/components/AiSelect.tsx +61 -69
  51. package/src/components/AiStatusButton.tsx +477 -1313
  52. package/src/components/AiTextarea.tsx +49 -64
  53. package/src/components/LBApiKeySelector.tsx +86 -274
  54. package/src/components/LBConnectButton.tsx +46 -334
  55. package/src/components/LBSigninModal.tsx +140 -481
  56. package/src/examples/AiUiPremiumShowcase.tsx +91 -0
  57. package/src/index.ts +3 -0
  58. package/src/styles/inline.ts +27 -148
  59. package/src/styles.css +1268 -369
  60. package/src/types.ts +3 -0
@@ -6,13 +6,12 @@ import React, {
6
6
  useLayoutEffect,
7
7
  type TextareaHTMLAttributes,
8
8
  } from "react";
9
- import { Sparkles, Lock } from "lucide-react";
10
- import type { BaseAiProps } from "../types";
9
+ import { Loader2, Lock, Sparkles } from "lucide-react";
10
+ import type { AiRadius, AiSize, BaseAiProps } from "../types";
11
11
  import { useAiCallText } from "../hooks/useAiCallText";
12
12
  import { useAiModels } from "../hooks/useAiModels";
13
13
  import { AiPromptPanel } from "./AiPromptPanel";
14
14
  import { UsageToast, useUsageToast } from "./UsageToast";
15
- import { aiStyles } from "../styles/inline";
16
15
  import { handleAIError } from "../utils/errorHandler";
17
16
  import { useLB } from "../context/LBAuthProvider";
18
17
  import { LBSigninModal } from "./LBSigninModal";
@@ -23,12 +22,16 @@ export interface AiTextareaProps
23
22
  Omit<BaseAiProps, "type">,
24
23
  Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onValue"> {
25
24
  uiMode?: "modal" | "drawer";
25
+ size?: AiSize;
26
+ radius?: AiRadius;
26
27
  }
27
28
 
28
29
  export function AiTextarea({
29
30
  baseUrl: propBaseUrl,
30
31
  apiKeyId: propApiKeyId,
31
32
  uiMode = "modal",
33
+ size = "md",
34
+ radius = "lg",
32
35
  context,
33
36
  model,
34
37
  prompt,
@@ -49,8 +52,6 @@ export function AiTextarea({
49
52
  textareaProps.defaultValue?.toString() ||
50
53
  ""
51
54
  );
52
- const [isFocused, setIsFocused] = useState(false);
53
- const [isButtonHovered, setIsButtonHovered] = useState(false);
54
55
  const textareaRef = useRef<HTMLTextAreaElement>(null);
55
56
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
56
57
 
@@ -191,66 +192,50 @@ export function AiTextarea({
191
192
  adjustHeight();
192
193
  }, [textareaValue]);
193
194
 
195
+ const sizeClass = `ai-size-${size}`;
196
+ const radiusClass = `ai-radius-${radius}`;
197
+
194
198
  return (
195
- <div style={aiStyles.textareaWrapper} className={className}>
196
- <textarea
197
- ref={textareaRef}
198
- {...textareaProps}
199
- style={{
200
- ...aiStyles.textarea,
201
- ...(isFocused && aiStyles.textareaFocus),
202
- }}
203
- value={textareaValue}
204
- onChange={handleTextareaChange}
205
- onFocus={(e) => {
206
- setIsFocused(true);
207
- textareaProps.onFocus?.(e);
208
- adjustHeight();
209
- }}
210
- onBlur={(e) => {
211
- setIsFocused(false);
212
- textareaProps.onBlur?.(e);
213
- }}
214
- disabled={disabled || loading}
215
- />
216
- <button
217
- style={{
218
- ...aiStyles.textareaAiButton,
219
- ...(isButtonHovered && aiStyles.textareaAiButtonHover),
220
- ...(disabled || loading
221
- ? { opacity: 0.5, cursor: "not-allowed" }
222
- : {}),
223
- }}
224
- onClick={hasConfiguration ? handleQuickGenerate : handleOpenPanel}
225
- onMouseEnter={() => setIsButtonHovered(true)}
226
- onMouseLeave={() => setIsButtonHovered(false)}
227
- disabled={disabled || loading || !isAuthReady}
228
- type="button"
229
- title={
230
- !isAuthReady
231
- ? "Authentication required"
232
- : hasConfiguration
233
- ? "Generate with AI"
234
- : "Setup AI"
235
- }
236
- >
237
- {loading ? (
238
- <svg
239
- style={aiStyles.spinner}
240
- width="16"
241
- height="16"
242
- viewBox="0 0 24 24"
243
- fill="none"
244
- stroke="currentColor"
245
- >
246
- <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" />
247
- </svg>
248
- ) : shouldShowSparkles ? (
249
- <Sparkles size={16} />
250
- ) : (
251
- <Lock size={16} />
252
- )}
253
- </button>
199
+ <div className={`ai-control-group ai-glow ${className || ""}`}>
200
+ <div className={`ai-shell ai-shell--textarea ${sizeClass} ${radiusClass}`}>
201
+ <textarea
202
+ ref={textareaRef}
203
+ {...textareaProps}
204
+ className={`ai-control ai-control-textarea ${sizeClass} ${radiusClass}`}
205
+ value={textareaValue}
206
+ onChange={handleTextareaChange}
207
+ onFocus={(e) => {
208
+ textareaProps.onFocus?.(e);
209
+ adjustHeight();
210
+ }}
211
+ onBlur={(e) => {
212
+ textareaProps.onBlur?.(e);
213
+ }}
214
+ aria-invalid={Boolean(textareaProps["aria-invalid"])}
215
+ disabled={disabled || loading}
216
+ />
217
+ <button
218
+ className={`ai-control-action ai-spark ${sizeClass} ${radiusClass}`}
219
+ onClick={hasConfiguration ? handleQuickGenerate : handleOpenPanel}
220
+ disabled={disabled || loading || !isAuthReady}
221
+ type="button"
222
+ title={
223
+ !isAuthReady
224
+ ? "Authentication required"
225
+ : hasConfiguration
226
+ ? "Generate with AI"
227
+ : "Setup AI"
228
+ }
229
+ >
230
+ {loading ? (
231
+ <Loader2 size={16} className="ai-spinner" />
232
+ ) : shouldShowSparkles ? (
233
+ <Sparkles size={16} />
234
+ ) : (
235
+ <Lock size={16} />
236
+ )}
237
+ </button>
238
+ </div>
254
239
  {isOpen && (
255
240
  <AiPromptPanel
256
241
  isOpen={isOpen}
@@ -1,4 +1,5 @@
1
1
  import React, { useState } from "react";
2
+ import { CheckCircle2, KeyRound, Loader2, XCircle } from "lucide-react";
2
3
  import type { LBApiKey } from "@lastbrain/ai-ui-core";
3
4
 
4
5
  interface LBApiKeySelectorProps {
@@ -43,286 +44,97 @@ export function LBApiKeySelector({
43
44
  };
44
45
 
45
46
  return (
46
- <div
47
- style={{
48
- position: "fixed",
49
- top: 0,
50
- left: 0,
51
- right: 0,
52
- bottom: 0,
53
- zIndex: 10000,
54
- display: "flex",
55
- alignItems: "center",
56
- justifyContent: "center",
57
- padding: "16px",
58
- }}
59
- onClick={onCancel}
60
- >
61
- {/* Backdrop */}
62
- <div
63
- style={{
64
- position: "absolute",
65
- top: 0,
66
- left: 0,
67
- right: 0,
68
- bottom: 0,
69
- background: "rgba(0, 0, 0, 0.75)",
70
- backdropFilter: "blur(8px)",
71
- height: "110%",
72
- }}
73
- />
74
-
75
- {/* Modal */}
76
- <div
77
- style={{
78
- position: "relative",
79
- background: "light-dark(#ffffff, #1e293b)",
80
- border: "1px solid light-dark(#e2e8f0, #334155)",
81
- borderRadius: "12px",
82
- padding: "24px",
83
- maxWidth: "500px",
84
- width: "100%",
85
- boxShadow: "0 20px 50px rgba(0, 0, 0, 0.5)",
86
- }}
87
- onClick={(e) => e.stopPropagation()}
88
- >
89
- <h2
90
- style={{
91
- margin: "0 0 8px 0",
92
- fontSize: "20px",
93
- fontWeight: 600,
94
- color: "light-dark(#1e293b, #f8fafc)",
95
- textAlign: "center",
96
- }}
97
- >
98
- Sélectionnez une clé API
99
- </h2>
100
-
101
- <p
102
- style={{
103
- margin: "0 0 24px 0",
104
- fontSize: "14px",
105
- color: "light-dark(#64748b, #94a3b8)",
106
- textAlign: "center",
107
- lineHeight: "1.5",
108
- }}
109
- >
110
- Choisissez la clé API à utiliser pour vos requêtes IA
111
- </p>
112
-
113
- <form onSubmit={handleSubmit}>
114
- {/* Liste des clés API */}
115
- <div
116
- style={{
117
- display: "flex",
118
- flexDirection: "column",
119
- gap: "8px",
120
- marginBottom: "24px",
121
- maxHeight: "300px",
122
- overflowY: "auto",
123
- }}
124
- >
125
- {apiKeys.map((key) => {
126
- const isSelected = key.id === selectedKeyId;
127
- const isActive = key.isActive;
128
-
129
- return (
130
- <label
131
- key={key.id}
132
- style={{
133
- display: "flex",
134
- alignItems: "center",
135
- padding: "12px 16px",
136
- background: isSelected
137
- ? "light-dark(#f1f5f9, #334155)"
138
- : "light-dark(#f8fafc, #0f172a)",
139
- border: `2px solid ${
140
- isSelected ? "#8b5cf6" : "light-dark(#e2e8f0, #334155)"
141
- }`,
142
- borderRadius: "8px",
143
- cursor: isActive ? "pointer" : "not-allowed",
144
- opacity: isActive ? 1 : 0.5,
145
- transition: "all 0.2s ease",
146
- }}
147
- onMouseEnter={(e) => {
148
- if (isActive && !isSelected) {
149
- e.currentTarget.style.borderColor =
150
- "light-dark(#cbd5e1, #475569)";
151
- e.currentTarget.style.background =
152
- "light-dark(#f1f5f9, #334155)";
153
- }
154
- }}
155
- onMouseLeave={(e) => {
156
- if (isActive && !isSelected) {
157
- e.currentTarget.style.borderColor =
158
- "light-dark(#e2e8f0, #334155)";
159
- e.currentTarget.style.background =
160
- "light-dark(#f8fafc, #0f172a)";
161
- }
162
- }}
163
- >
164
- <input
165
- type="radio"
166
- name="apiKey"
167
- value={key.id}
168
- checked={isSelected}
169
- disabled={!isActive}
170
- onChange={(e) => setSelectedKeyId(e.target.value)}
171
- style={{
172
- marginRight: "12px",
173
- accentColor: "#8b5cf6",
174
- cursor: isActive ? "pointer" : "not-allowed",
175
- }}
176
- />
177
- <div style={{ flex: 1 }}>
178
- <div
179
- style={{
180
- fontSize: "14px",
181
- fontWeight: 500,
182
- color: "light-dark(#1e293b, #f8fafc)",
183
- marginBottom: "4px",
184
- }}
185
- >
186
- {key.name}
187
- </div>
188
- <div
189
- style={{
190
- fontSize: "12px",
191
- color: "light-dark(#64748b, #94a3b8)",
192
- fontFamily: "monospace",
193
- }}
194
- >
195
- {key.keyPrefix || key.id.substring(0, 12) + "..."}
196
- </div>
197
- </div>
198
- {isActive ? (
199
- <div
200
- style={{
201
- fontSize: "11px",
202
- padding: "4px 8px",
203
- borderRadius: "4px",
204
- background: "rgba(16, 185, 129, 0.1)",
205
- color: "#10b981",
206
- fontWeight: 600,
207
- }}
208
- >
209
- Active
210
- </div>
211
- ) : (
212
- <div
213
- style={{
214
- fontSize: "11px",
215
- padding: "4px 8px",
216
- borderRadius: "4px",
217
- background: "rgba(239, 68, 68, 0.1)",
218
- color: "#ef4444",
219
- fontWeight: 600,
220
- }}
221
- >
222
- Inactive
223
- </div>
224
- )}
225
- </label>
226
- );
227
- })}
47
+ <div className="ai-signin-overlay" onClick={onCancel}>
48
+ <div className="ai-signin-panel ai-key-modal-panel" onClick={(e) => e.stopPropagation()}>
49
+ <div className="ai-signin-header">
50
+ <div className="ai-center mb-3">
51
+ <span className="ai-icon-badge">
52
+ <KeyRound size={20} />
53
+ </span>
228
54
  </div>
55
+ <h2 className="ai-signin-title">Sélectionnez une clé API</h2>
56
+ <p className="ai-signin-subtitle">
57
+ Choisissez la clé API à utiliser pour vos requêtes IA.
58
+ </p>
59
+ </div>
229
60
 
230
- {error && (
61
+ <div className="ai-signin-content">
62
+ <form onSubmit={handleSubmit}>
231
63
  <div
232
- style={{
233
- padding: "12px",
234
- background: "rgba(239, 68, 68, 0.1)",
235
- border: "1px solid rgba(239, 68, 68, 0.3)",
236
- borderRadius: "6px",
237
- marginBottom: "16px",
238
- }}
64
+ className="ai-model-mgmt-list"
65
+ style={{ maxHeight: 300, overflowY: "auto", marginBottom: 16 }}
239
66
  >
240
- <p
241
- style={{
242
- margin: 0,
243
- fontSize: "13px",
244
- color: "#ef4444",
245
- lineHeight: "1.5",
246
- }}
247
- >
248
- {error}
249
- </p>
67
+ {apiKeys.map((key) => {
68
+ const isSelected = key.id === selectedKeyId;
69
+ const isActive = key.isActive;
70
+ return (
71
+ <label
72
+ key={key.id}
73
+ className={`ai-model-item ${isSelected ? "ai-model-item--active" : ""} ${
74
+ !isActive ? "ai-model-item--disabled" : ""
75
+ }`}
76
+ >
77
+ <div className="ai-model-item-main">
78
+ <input
79
+ type="radio"
80
+ name="apiKey"
81
+ value={key.id}
82
+ checked={isSelected}
83
+ disabled={!isActive}
84
+ onChange={(e) => setSelectedKeyId(e.target.value)}
85
+ className="ai-key-radio"
86
+ />
87
+ <div>
88
+ <div className="ai-model-item-title">{key.name}</div>
89
+ <div className="ai-model-item-meta">
90
+ <span>{key.keyPrefix || key.id.substring(0, 12) + "..."}</span>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ {isActive ? (
95
+ <span className="ai-pill ai-pill--cost">
96
+ <CheckCircle2 size={12} />
97
+ Active
98
+ </span>
99
+ ) : (
100
+ <span className="ai-pill ai-pill--cost">
101
+ <XCircle size={12} />
102
+ Inactive
103
+ </span>
104
+ )}
105
+ </label>
106
+ );
107
+ })}
250
108
  </div>
251
- )}
252
109
 
253
- {/* Boutons */}
254
- <div style={{ display: "flex", gap: "12px" }}>
255
- <button
256
- type="button"
257
- onClick={onCancel}
258
- disabled={loading}
259
- style={{
260
- flex: 1,
261
- padding: "12px",
262
- background: "transparent",
263
- border: "1px solid light-dark(#e2e8f0, #334155)",
264
- borderRadius: "8px",
265
- color: "light-dark(#64748b, #94a3b8)",
266
- fontSize: "14px",
267
- fontWeight: 600,
268
- cursor: loading ? "not-allowed" : "pointer",
269
- opacity: loading ? 0.5 : 1,
270
- transition: "all 0.2s ease",
271
- }}
272
- onMouseEnter={(e) => {
273
- if (!loading) {
274
- e.currentTarget.style.background =
275
- "light-dark(#f8fafc, #0f172a)";
276
- e.currentTarget.style.borderColor =
277
- "light-dark(#cbd5e1, #475569)";
278
- }
279
- }}
280
- onMouseLeave={(e) => {
281
- if (!loading) {
282
- e.currentTarget.style.background = "transparent";
283
- e.currentTarget.style.borderColor =
284
- "light-dark(#e2e8f0, #334155)";
285
- }
286
- }}
287
- >
288
- Annuler
289
- </button>
290
- <button
291
- type="submit"
292
- disabled={loading || !selectedKeyId}
293
- style={{
294
- flex: 1,
295
- padding: "12px",
296
- background: loading
297
- ? "light-dark(#e2e8f0, #0f172a)"
298
- : "linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)",
299
- border: "none",
300
- borderRadius: "8px",
301
- color: "#ffffff",
302
- fontSize: "14px",
303
- fontWeight: 600,
304
- cursor: loading || !selectedKeyId ? "not-allowed" : "pointer",
305
- opacity: loading || !selectedKeyId ? 0.5 : 1,
306
- transition: "all 0.2s ease",
307
- }}
308
- onMouseEnter={(e) => {
309
- if (!loading && selectedKeyId) {
310
- e.currentTarget.style.transform = "translateY(-1px)";
311
- e.currentTarget.style.boxShadow =
312
- "0 8px 20px rgba(139, 92, 246, 0.4)";
313
- }
314
- }}
315
- onMouseLeave={(e) => {
316
- if (!loading && selectedKeyId) {
317
- e.currentTarget.style.transform = "none";
318
- e.currentTarget.style.boxShadow = "none";
319
- }
320
- }}
321
- >
322
- {loading ? "Connexion..." : "Continuer"}
323
- </button>
324
- </div>
325
- </form>
110
+ {error ? (
111
+ <div className="ai-signin-error" role="alert">
112
+ <XCircle size={16} />
113
+ <span>{error}</span>
114
+ </div>
115
+ ) : null}
116
+
117
+ <div className="ai-signin-actions">
118
+ <button type="button" onClick={onCancel} disabled={loading} className="ai-btn ai-btn--ghost">
119
+ Annuler
120
+ </button>
121
+ <button
122
+ type="submit"
123
+ disabled={loading || !selectedKeyId}
124
+ className="ai-btn ai-btn--primary"
125
+ >
126
+ {loading ? (
127
+ <>
128
+ <Loader2 size={16} className="ai-spinner" />
129
+ Connexion...
130
+ </>
131
+ ) : (
132
+ "Continuer"
133
+ )}
134
+ </button>
135
+ </div>
136
+ </form>
137
+ </div>
326
138
  </div>
327
139
  </div>
328
140
  );