@lastbrain/ai-ui-react 1.0.68 → 1.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/components/AiChipLabel.d.ts +8 -3
  2. package/dist/components/AiChipLabel.d.ts.map +1 -1
  3. package/dist/components/AiChipLabel.js +23 -70
  4. package/dist/components/AiContextButton.d.ts +10 -2
  5. package/dist/components/AiContextButton.d.ts.map +1 -1
  6. package/dist/components/AiContextButton.js +73 -291
  7. package/dist/components/AiImageButton.d.ts +5 -1
  8. package/dist/components/AiImageButton.d.ts.map +1 -1
  9. package/dist/components/AiImageButton.js +6 -142
  10. package/dist/components/AiInput.d.ts +5 -3
  11. package/dist/components/AiInput.d.ts.map +1 -1
  12. package/dist/components/AiInput.js +13 -25
  13. package/dist/components/AiPromptPanel.d.ts.map +1 -1
  14. package/dist/components/AiPromptPanel.js +64 -212
  15. package/dist/components/AiSelect.d.ts +5 -3
  16. package/dist/components/AiSelect.d.ts.map +1 -1
  17. package/dist/components/AiSelect.js +21 -30
  18. package/dist/components/AiStatusButton.d.ts +4 -1
  19. package/dist/components/AiStatusButton.d.ts.map +1 -1
  20. package/dist/components/AiStatusButton.js +211 -676
  21. package/dist/components/AiTextarea.d.ts +4 -2
  22. package/dist/components/AiTextarea.d.ts.map +1 -1
  23. package/dist/components/AiTextarea.js +14 -26
  24. package/dist/components/LBApiKeySelector.d.ts.map +1 -1
  25. package/dist/components/LBApiKeySelector.js +5 -166
  26. package/dist/components/LBConnectButton.d.ts +4 -7
  27. package/dist/components/LBConnectButton.d.ts.map +1 -1
  28. package/dist/components/LBConnectButton.js +17 -86
  29. package/dist/components/LBSigninModal.d.ts +1 -1
  30. package/dist/components/LBSigninModal.d.ts.map +1 -1
  31. package/dist/components/LBSigninModal.js +42 -320
  32. package/dist/context/LBAuthProvider.d.ts +35 -3
  33. package/dist/context/LBAuthProvider.d.ts.map +1 -1
  34. package/dist/context/LBAuthProvider.js +2 -0
  35. package/dist/examples/AiUiPremiumShowcase.d.ts +2 -0
  36. package/dist/examples/AiUiPremiumShowcase.d.ts.map +1 -0
  37. package/dist/examples/AiUiPremiumShowcase.js +15 -0
  38. package/dist/hooks/useAiModels.d.ts.map +1 -1
  39. package/dist/hooks/useModelManagement.d.ts.map +1 -1
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +2 -0
  43. package/dist/styles/inline.d.ts +1 -0
  44. package/dist/styles/inline.d.ts.map +1 -1
  45. package/dist/styles/inline.js +25 -129
  46. package/dist/styles.css +1268 -369
  47. package/dist/types.d.ts +3 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/utils/errorHandler.d.ts +2 -2
  50. package/dist/utils/errorHandler.d.ts.map +1 -1
  51. package/dist/utils/errorHandler.js +8 -1
  52. package/dist/utils/modelManagement.d.ts +13 -10
  53. package/dist/utils/modelManagement.d.ts.map +1 -1
  54. package/dist/utils/modelManagement.js +19 -2
  55. package/package.json +2 -2
  56. package/src/components/AiChipLabel.tsx +68 -101
  57. package/src/components/AiContextButton.tsx +142 -413
  58. package/src/components/AiImageButton.tsx +29 -190
  59. package/src/components/AiInput.tsx +49 -74
  60. package/src/components/AiPromptPanel.tsx +81 -260
  61. package/src/components/AiSelect.tsx +61 -69
  62. package/src/components/AiStatusButton.tsx +496 -1327
  63. package/src/components/AiTextarea.tsx +50 -63
  64. package/src/components/LBApiKeySelector.tsx +93 -271
  65. package/src/components/LBConnectButton.tsx +39 -336
  66. package/src/components/LBSigninModal.tsx +141 -472
  67. package/src/context/LBAuthProvider.tsx +45 -6
  68. package/src/examples/AiUiPremiumShowcase.tsx +94 -0
  69. package/src/hooks/useAiModels.ts +2 -1
  70. package/src/hooks/useModelManagement.ts +2 -1
  71. package/src/index.ts +3 -0
  72. package/src/styles/inline.ts +27 -148
  73. package/src/styles.css +1268 -369
  74. package/src/types.ts +3 -0
  75. package/src/utils/errorHandler.ts +16 -3
  76. package/src/utils/modelManagement.ts +53 -15
@@ -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,52 @@ 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
- }
199
+ <div className={`ai-control-group ai-glow ${className || ""}`}>
200
+ <div
201
+ className={`ai-shell ai-shell--textarea ${sizeClass} ${radiusClass}`}
236
202
  >
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>
203
+ <textarea
204
+ ref={textareaRef}
205
+ {...textareaProps}
206
+ className={`ai-control ai-control-textarea ${sizeClass} ${radiusClass}`}
207
+ value={textareaValue}
208
+ onChange={handleTextareaChange}
209
+ onFocus={(e) => {
210
+ textareaProps.onFocus?.(e);
211
+ adjustHeight();
212
+ }}
213
+ onBlur={(e) => {
214
+ textareaProps.onBlur?.(e);
215
+ }}
216
+ aria-invalid={Boolean(textareaProps["aria-invalid"])}
217
+ disabled={disabled || loading}
218
+ />
219
+ <button
220
+ className={`ai-control-action ai-spark ${sizeClass} ${radiusClass}`}
221
+ onClick={hasConfiguration ? handleQuickGenerate : handleOpenPanel}
222
+ disabled={disabled || loading || !isAuthReady}
223
+ type="button"
224
+ title={
225
+ !isAuthReady
226
+ ? "Authentication required"
227
+ : hasConfiguration
228
+ ? "Generate with AI"
229
+ : "Setup AI"
230
+ }
231
+ >
232
+ {loading ? (
233
+ <Loader2 size={16} className="ai-spinner" />
234
+ ) : shouldShowSparkles ? (
235
+ <Sparkles size={16} />
236
+ ) : (
237
+ <Lock size={16} />
238
+ )}
239
+ </button>
240
+ </div>
254
241
  {isOpen && (
255
242
  <AiPromptPanel
256
243
  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,107 @@ 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 */}
47
+ <div className="ai-signin-overlay" onClick={onCancel}>
62
48
  <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
- }}
49
+ className="ai-signin-panel ai-key-modal-panel"
87
50
  onClick={(e) => e.stopPropagation()}
88
51
  >
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
- })}
52
+ <div className="ai-signin-header">
53
+ <div className="ai-center mb-3">
54
+ <span className="ai-icon-badge">
55
+ <KeyRound size={20} />
56
+ </span>
228
57
  </div>
58
+ <h2 className="ai-signin-title">Sélectionnez une clé API</h2>
59
+ <p className="ai-signin-subtitle">
60
+ Choisissez la clé API à utiliser pour vos requêtes IA.
61
+ </p>
62
+ </div>
229
63
 
230
- {error && (
64
+ <div className="ai-signin-content">
65
+ <form onSubmit={handleSubmit}>
231
66
  <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
- }}
67
+ className="ai-model-mgmt-list"
68
+ style={{ maxHeight: 300, overflowY: "auto", marginBottom: 16 }}
239
69
  >
240
- <p
241
- style={{
242
- margin: 0,
243
- fontSize: "13px",
244
- color: "#ef4444",
245
- lineHeight: "1.5",
246
- }}
247
- >
248
- {error}
249
- </p>
70
+ {apiKeys.map((key) => {
71
+ const isSelected = key.id === selectedKeyId;
72
+ const isActive = key.isActive;
73
+ return (
74
+ <label
75
+ key={key.id}
76
+ className={`ai-model-item ${isSelected ? "ai-model-item--active" : ""} ${
77
+ !isActive ? "ai-model-item--disabled" : ""
78
+ }`}
79
+ >
80
+ <div className="ai-model-item-main">
81
+ <input
82
+ type="radio"
83
+ name="apiKey"
84
+ value={key.id}
85
+ checked={isSelected}
86
+ disabled={!isActive}
87
+ onChange={(e) => setSelectedKeyId(e.target.value)}
88
+ className="ai-key-radio"
89
+ />
90
+ <div>
91
+ <div className="ai-model-item-title">{key.name}</div>
92
+ <div className="ai-model-item-meta">
93
+ <span>
94
+ {key.keyPrefix || key.id.substring(0, 12) + "..."}
95
+ </span>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ {isActive ? (
100
+ <span className="ai-pill ai-pill--cost">
101
+ <CheckCircle2 size={12} />
102
+ Active
103
+ </span>
104
+ ) : (
105
+ <span className="ai-pill ai-pill--cost">
106
+ <XCircle size={12} />
107
+ Inactive
108
+ </span>
109
+ )}
110
+ </label>
111
+ );
112
+ })}
250
113
  </div>
251
- )}
252
114
 
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>
115
+ {error ? (
116
+ <div className="ai-signin-error" role="alert">
117
+ <XCircle size={16} />
118
+ <span>{error}</span>
119
+ </div>
120
+ ) : null}
121
+
122
+ <div className="ai-signin-actions">
123
+ <button
124
+ type="button"
125
+ onClick={onCancel}
126
+ disabled={loading}
127
+ className="ai-btn ai-btn--ghost"
128
+ >
129
+ Annuler
130
+ </button>
131
+ <button
132
+ type="submit"
133
+ disabled={loading || !selectedKeyId}
134
+ className="ai-btn ai-btn--primary"
135
+ >
136
+ {loading ? (
137
+ <>
138
+ <Loader2 size={16} className="ai-spinner" />
139
+ Connexion...
140
+ </>
141
+ ) : (
142
+ "Continuer"
143
+ )}
144
+ </button>
145
+ </div>
146
+ </form>
147
+ </div>
326
148
  </div>
327
149
  </div>
328
150
  );