@lastbrain/ai-ui-react 1.0.40 → 1.0.42

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.
@@ -9,8 +9,11 @@ import {
9
9
  FileText,
10
10
  History as HistoryIcon,
11
11
  FolderPlus,
12
+ Power,
12
13
  } from "lucide-react";
13
14
  import { aiStyles, calculateTooltipPosition } from "../styles/inline";
15
+ import { useLB } from "../context/LBAuthProvider";
16
+ import { LBSigninModal } from "./LBSigninModal";
14
17
 
15
18
  export interface AiStatusButtonProps {
16
19
  status: AiStatus | null;
@@ -23,6 +26,25 @@ export function AiStatusButton({
23
26
  loading = false,
24
27
  className = "",
25
28
  }: AiStatusButtonProps) {
29
+ // Rendre l'authentification optionnelle
30
+ let lbStatus: string | undefined;
31
+ let user: any;
32
+ let logout: (() => Promise<void>) | undefined;
33
+
34
+ try {
35
+ const lbContext = useLB();
36
+ lbStatus = lbContext.status;
37
+ user = lbContext.user;
38
+ logout = lbContext.logout;
39
+ } catch {
40
+ // LBProvider n'est pas disponible, ignorer
41
+ lbStatus = undefined;
42
+ user = undefined;
43
+ logout = undefined;
44
+ }
45
+
46
+ const [showSigninModal, setShowSigninModal] = useState(false);
47
+
26
48
  type BalanceUsage = {
27
49
  used?: number;
28
50
  total?: number;
@@ -257,47 +279,343 @@ export function AiStatusButton({
257
279
  }
258
280
 
259
281
  if (!status) {
282
+ // Si pas de statut API et pas de LBProvider, afficher message simple
283
+ if (!lbStatus && lbStatus !== "ready") {
284
+ return (
285
+ <div style={{ position: "relative", display: "inline-block" }}>
286
+ <button
287
+ ref={buttonRef}
288
+ style={{
289
+ ...aiStyles.statusButton,
290
+ color: "#ef4444",
291
+ ...(isHovered && aiStyles.statusButtonHover),
292
+ }}
293
+ className={className}
294
+ onMouseEnter={handleMouseEnter}
295
+ onMouseLeave={handleMouseLeave}
296
+ >
297
+ <svg
298
+ width="16"
299
+ height="16"
300
+ viewBox="0 0 24 24"
301
+ fill="none"
302
+ stroke="currentColor"
303
+ >
304
+ <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
305
+ </svg>
306
+ </button>
307
+ {showTooltip &&
308
+ canPortal &&
309
+ createPortal(
310
+ <div
311
+ ref={tooltipRef}
312
+ style={{
313
+ ...aiStyles.tooltip,
314
+ ...tooltipPosition,
315
+ zIndex: 50,
316
+ }}
317
+ onMouseEnter={() => setShowTooltip(true)}
318
+ onMouseLeave={handleMouseLeave}
319
+ >
320
+ No status available
321
+ </div>,
322
+ document.body
323
+ )}
324
+ </div>
325
+ );
326
+ }
327
+
260
328
  return (
261
- <div style={{ position: "relative", display: "inline-block" }}>
262
- <button
263
- ref={buttonRef}
264
- style={{
265
- ...aiStyles.statusButton,
266
- color: "#ef4444",
267
- ...(isHovered && aiStyles.statusButtonHover),
268
- }}
269
- className={className}
270
- onMouseEnter={handleMouseEnter}
271
- onMouseLeave={handleMouseLeave}
272
- >
273
- <svg
274
- width="16"
275
- height="16"
276
- viewBox="0 0 24 24"
277
- fill="none"
278
- stroke="currentColor"
329
+ <>
330
+ <div style={{ position: "relative", display: "inline-block" }}>
331
+ <button
332
+ ref={buttonRef}
333
+ style={{
334
+ ...aiStyles.statusButton,
335
+ color: lbStatus === "ready" ? "#10b981" : "#ef4444",
336
+ ...(isHovered && aiStyles.statusButtonHover),
337
+ }}
338
+ className={className}
339
+ onMouseEnter={handleMouseEnter}
340
+ onMouseLeave={handleMouseLeave}
341
+ onClick={() => {
342
+ if (lbStatus !== "ready") {
343
+ setShowSigninModal(true);
344
+ }
345
+ }}
279
346
  >
280
- <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
281
- </svg>
282
- </button>
283
- {showTooltip &&
284
- canPortal &&
285
- createPortal(
286
- <div
287
- ref={tooltipRef}
288
- style={{
289
- ...aiStyles.tooltip,
290
- ...tooltipPosition,
291
- zIndex: 50,
292
- }}
293
- onMouseEnter={() => setShowTooltip(true)}
294
- onMouseLeave={handleMouseLeave}
347
+ <svg
348
+ width="16"
349
+ height="16"
350
+ viewBox="0 0 24 24"
351
+ fill="none"
352
+ stroke="currentColor"
295
353
  >
296
- No status available
297
- </div>,
298
- document.body
299
- )}
300
- </div>
354
+ <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
355
+ </svg>
356
+ </button>
357
+ {showTooltip &&
358
+ canPortal &&
359
+ createPortal(
360
+ <div
361
+ ref={tooltipRef}
362
+ style={{
363
+ ...aiStyles.tooltip,
364
+ ...tooltipPosition,
365
+ zIndex: 50,
366
+ }}
367
+ onMouseEnter={() => setShowTooltip(true)}
368
+ onMouseLeave={handleMouseLeave}
369
+ >
370
+ {lbStatus === "ready" && user ? (
371
+ <>
372
+ <div style={aiStyles.tooltipHeader}>
373
+ LastBrain Connected
374
+ </div>
375
+ <div
376
+ style={{
377
+ ...aiStyles.tooltipSection,
378
+ paddingBottom: "8px",
379
+ }}
380
+ >
381
+ <div style={aiStyles.tooltipRow}>
382
+ <span style={aiStyles.tooltipLabel}>User:</span>
383
+ <span style={aiStyles.tooltipValue}>{user.email}</span>
384
+ </div>
385
+ </div>
386
+ <div
387
+ style={{
388
+ display: "flex",
389
+ gap: "8px",
390
+ borderTop:
391
+ "1px solid var(--ai-border-primary, #374151)",
392
+ paddingTop: "12px",
393
+ }}
394
+ >
395
+ <button
396
+ onClick={() =>
397
+ window.open("https://lastbrain.io/metrics", "_blank")
398
+ }
399
+ style={{
400
+ flex: 1,
401
+ background: "transparent",
402
+ border: "none",
403
+ padding: "14px",
404
+ cursor: "pointer",
405
+ display: "flex",
406
+ alignItems: "center",
407
+ justifyContent: "center",
408
+ color: "#8b5cf6",
409
+ transition: "all 0.2s ease",
410
+ }}
411
+ onMouseEnter={(e) => {
412
+ Object.assign(e.currentTarget.style, {
413
+ background: "rgba(139, 92, 246, 0.1)",
414
+ });
415
+ }}
416
+ onMouseLeave={(e) => {
417
+ Object.assign(e.currentTarget.style, {
418
+ background: "transparent",
419
+ });
420
+ }}
421
+ title="View Metrics"
422
+ >
423
+ <BarChart3 size={18} />
424
+ </button>
425
+ <button
426
+ onClick={() =>
427
+ window.open("https://lastbrain.io/settings", "_blank")
428
+ }
429
+ style={{
430
+ flex: 1,
431
+ background: "transparent",
432
+ border: "none",
433
+ padding: "14px",
434
+ cursor: "pointer",
435
+ display: "flex",
436
+ alignItems: "center",
437
+ justifyContent: "center",
438
+ color: "#8b5cf6",
439
+ transition: "all 0.2s ease",
440
+ }}
441
+ onMouseEnter={(e) => {
442
+ Object.assign(e.currentTarget.style, {
443
+ background: "rgba(139, 92, 246, 0.1)",
444
+ });
445
+ }}
446
+ onMouseLeave={(e) => {
447
+ Object.assign(e.currentTarget.style, {
448
+ background: "transparent",
449
+ });
450
+ }}
451
+ title="Settings"
452
+ >
453
+ <Settings size={18} />
454
+ </button>
455
+ <button
456
+ onClick={() =>
457
+ window.open(
458
+ "https://prompt.lastbrain.io/auth/prompts",
459
+ "_blank"
460
+ )
461
+ }
462
+ style={{
463
+ flex: 1,
464
+ background: "transparent",
465
+ border: "none",
466
+ padding: "14px",
467
+ cursor: "pointer",
468
+ display: "flex",
469
+ alignItems: "center",
470
+ justifyContent: "center",
471
+ color: "#8b5cf6",
472
+ transition: "all 0.2s ease",
473
+ }}
474
+ onMouseEnter={(e) => {
475
+ Object.assign(e.currentTarget.style, {
476
+ background: "rgba(139, 92, 246, 0.1)",
477
+ });
478
+ }}
479
+ onMouseLeave={(e) => {
480
+ Object.assign(e.currentTarget.style, {
481
+ background: "transparent",
482
+ });
483
+ }}
484
+ title="My Prompts"
485
+ >
486
+ <FileText size={18} />
487
+ </button>
488
+ <button
489
+ onClick={() =>
490
+ window.open(
491
+ "https://prompt.lastbrain.io/auth/folder",
492
+ "_blank"
493
+ )
494
+ }
495
+ style={{
496
+ flex: 1,
497
+ background: "transparent",
498
+ border: "none",
499
+ padding: "14px",
500
+ cursor: "pointer",
501
+ display: "flex",
502
+ alignItems: "center",
503
+ justifyContent: "center",
504
+ color: "#8b5cf6",
505
+ transition: "all 0.2s ease",
506
+ }}
507
+ onMouseEnter={(e) => {
508
+ Object.assign(e.currentTarget.style, {
509
+ background: "rgba(139, 92, 246, 0.1)",
510
+ });
511
+ }}
512
+ onMouseLeave={(e) => {
513
+ Object.assign(e.currentTarget.style, {
514
+ background: "transparent",
515
+ });
516
+ }}
517
+ title="New Folder"
518
+ >
519
+ <FolderPlus size={18} />
520
+ </button>
521
+ <button
522
+ onClick={async () => {
523
+ if (logout) {
524
+ await logout();
525
+ }
526
+ setShowTooltip(false);
527
+ }}
528
+ style={{
529
+ flex: 1,
530
+ background: "transparent",
531
+ border: "none",
532
+ padding: "14px",
533
+ cursor: "pointer",
534
+ display: "flex",
535
+ alignItems: "center",
536
+ justifyContent: "center",
537
+ color: "#ef4444",
538
+ transition: "all 0.2s ease",
539
+ }}
540
+ onMouseEnter={(e) => {
541
+ Object.assign(e.currentTarget.style, {
542
+ background: "rgba(239, 68, 68, 0.1)",
543
+ });
544
+ }}
545
+ onMouseLeave={(e) => {
546
+ Object.assign(e.currentTarget.style, {
547
+ background: "transparent",
548
+ });
549
+ }}
550
+ title="Logout"
551
+ >
552
+ <Power size={18} />
553
+ </button>
554
+ </div>
555
+ </>
556
+ ) : (
557
+ <>
558
+ <div style={aiStyles.tooltipHeader}>
559
+ LastBrain Authentication
560
+ </div>
561
+ <div
562
+ style={{
563
+ ...aiStyles.tooltipSection,
564
+ paddingBottom: "12px",
565
+ }}
566
+ >
567
+ <p
568
+ style={{
569
+ margin: 0,
570
+ fontSize: "13px",
571
+ color: "var(--ai-text-secondary, #9ca3af)",
572
+ lineHeight: "1.5",
573
+ }}
574
+ >
575
+ Connectez-vous pour accéder aux fonctionnalités IA
576
+ </p>
577
+ </div>
578
+ <button
579
+ onClick={() => {
580
+ setShowSigninModal(true);
581
+ setShowTooltip(false);
582
+ }}
583
+ style={{
584
+ width: "100%",
585
+ padding: "10px",
586
+ background:
587
+ "linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)",
588
+ border: "none",
589
+ borderRadius: "6px",
590
+ color: "#ffffff",
591
+ fontSize: "13px",
592
+ fontWeight: 600,
593
+ cursor: "pointer",
594
+ transition: "all 0.2s ease",
595
+ }}
596
+ onMouseEnter={(e) => {
597
+ e.currentTarget.style.transform = "translateY(-1px)";
598
+ e.currentTarget.style.boxShadow =
599
+ "0 4px 12px rgba(139, 92, 246, 0.3)";
600
+ }}
601
+ onMouseLeave={(e) => {
602
+ e.currentTarget.style.transform = "translateY(0)";
603
+ e.currentTarget.style.boxShadow = "none";
604
+ }}
605
+ >
606
+ 🔐 Se connecter
607
+ </button>
608
+ </>
609
+ )}
610
+ </div>,
611
+ document.body
612
+ )}
613
+ </div>
614
+ <LBSigninModal
615
+ isOpen={showSigninModal}
616
+ onClose={() => setShowSigninModal(false)}
617
+ />
618
+ </>
301
619
  );
302
620
  }
303
621
 
@@ -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 { LBSigninModal } from "./LBSigninModal";
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,16 @@ 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
+
56
+ // Rendre l'authentification optionnelle
57
+ let lbStatus: string | undefined;
58
+ try {
59
+ const lbContext = useLB();
60
+ lbStatus = lbContext.status;
61
+ } catch {
62
+ // LBProvider n'est pas disponible, ignorer
63
+ lbStatus = undefined;
64
+ }
52
65
 
53
66
  const { models } = useAiModels({
54
67
  baseUrl,
@@ -58,8 +71,14 @@ export function AiTextarea({
58
71
  const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
59
72
 
60
73
  const hasConfiguration = Boolean(model && prompt);
74
+ const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
75
+ const shouldShowSparkles = isAuthReady && !disabled;
61
76
 
62
77
  const handleOpenPanel = () => {
78
+ if (!isAuthReady) {
79
+ setShowAuthModal(true);
80
+ return;
81
+ }
63
82
  setIsOpen(true);
64
83
  };
65
84
 
@@ -190,9 +209,15 @@ export function AiTextarea({
190
209
  onClick={hasConfiguration ? handleQuickGenerate : handleOpenPanel}
191
210
  onMouseEnter={() => setIsButtonHovered(true)}
192
211
  onMouseLeave={() => setIsButtonHovered(false)}
193
- disabled={disabled || loading}
212
+ disabled={disabled || loading || !isAuthReady}
194
213
  type="button"
195
- title={hasConfiguration ? "Generate with AI" : "Setup AI"}
214
+ title={
215
+ !isAuthReady
216
+ ? "Authentication required"
217
+ : hasConfiguration
218
+ ? "Generate with AI"
219
+ : "Setup AI"
220
+ }
196
221
  >
197
222
  {loading ? (
198
223
  <svg
@@ -205,8 +230,20 @@ export function AiTextarea({
205
230
  >
206
231
  <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
232
  </svg>
208
- ) : (
233
+ ) : shouldShowSparkles ? (
209
234
  <Sparkles size={16} />
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>
210
247
  )}
211
248
  </button>
212
249
  {isOpen && (
@@ -231,6 +268,10 @@ export function AiTextarea({
231
268
  onComplete={clearToast}
232
269
  />
233
270
  )}
271
+ <LBSigninModal
272
+ isOpen={showAuthModal}
273
+ onClose={() => setShowAuthModal(false)}
274
+ />
234
275
  </div>
235
276
  );
236
277
  }