@lastbrain/ai-ui-react 1.0.11 → 1.0.14

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 (44) hide show
  1. package/dist/components/AiInput.d.ts.map +1 -1
  2. package/dist/components/AiInput.js +1 -1
  3. package/dist/components/AiPromptPanel.d.ts +14 -1
  4. package/dist/components/AiPromptPanel.d.ts.map +1 -1
  5. package/dist/components/AiPromptPanel.js +255 -7
  6. package/dist/components/AiSettingsButton.d.ts.map +1 -1
  7. package/dist/components/AiSettingsButton.js +1 -1
  8. package/dist/components/AiStatusButton.d.ts.map +1 -1
  9. package/dist/components/AiStatusButton.js +106 -11
  10. package/dist/examples/AiImageGenerator.d.ts +34 -0
  11. package/dist/examples/AiImageGenerator.d.ts.map +1 -0
  12. package/dist/examples/AiImageGenerator.js +85 -0
  13. package/dist/examples/AiPromptPanelAdvanced.d.ts +20 -0
  14. package/dist/examples/AiPromptPanelAdvanced.d.ts.map +1 -0
  15. package/dist/examples/AiPromptPanelAdvanced.js +222 -0
  16. package/dist/examples/ExternalIntegration.d.ts +2 -0
  17. package/dist/examples/ExternalIntegration.d.ts.map +1 -0
  18. package/dist/examples/ExternalIntegration.js +2 -0
  19. package/dist/hooks/useAiStatus.d.ts.map +1 -1
  20. package/dist/hooks/useAiStatus.js +3 -0
  21. package/dist/hooks/useModelManagement.d.ts +32 -0
  22. package/dist/hooks/useModelManagement.d.ts.map +1 -0
  23. package/dist/hooks/useModelManagement.js +135 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +6 -0
  27. package/dist/styles/inline.d.ts.map +1 -1
  28. package/dist/styles/inline.js +10 -1
  29. package/dist/utils/modelManagement.d.ts +29 -0
  30. package/dist/utils/modelManagement.d.ts.map +1 -0
  31. package/dist/utils/modelManagement.js +82 -0
  32. package/package.json +2 -2
  33. package/src/components/AiInput.tsx +3 -0
  34. package/src/components/AiPromptPanel.tsx +502 -24
  35. package/src/components/AiSettingsButton.tsx +4 -2
  36. package/src/components/AiStatusButton.tsx +185 -39
  37. package/src/examples/AiImageGenerator.tsx +214 -0
  38. package/src/examples/AiPromptPanelAdvanced.tsx +381 -0
  39. package/src/examples/ExternalIntegration.ts +55 -0
  40. package/src/hooks/useAiStatus.ts +4 -0
  41. package/src/hooks/useModelManagement.ts +210 -0
  42. package/src/index.ts +8 -0
  43. package/src/styles/inline.ts +10 -1
  44. package/src/utils/modelManagement.ts +134 -0
@@ -7,7 +7,7 @@ import {
7
7
  useLayoutEffect,
8
8
  type ReactNode,
9
9
  } from "react";
10
- import { BookOpen, Search, Sparkles, Star, Tag } from "lucide-react";
10
+ import { BookOpen, Search, Sparkles, Star, Tag, Settings } from "lucide-react";
11
11
  import type { ModelRef } from "@lastbrain/ai-ui-core";
12
12
  import type { UiMode } from "../types";
13
13
  import { aiStyles } from "../styles/inline";
@@ -16,6 +16,7 @@ import {
16
16
  type Prompt,
17
17
  type PublicPrompt,
18
18
  } from "../hooks/usePrompts";
19
+ import { useModelManagement, type AIModel } from "../hooks/useModelManagement";
19
20
 
20
21
  export interface AiPromptPanelProps {
21
22
  isOpen: boolean;
@@ -25,6 +26,13 @@ export interface AiPromptPanelProps {
25
26
  models?: ModelRef[];
26
27
  sourceText?: string; // Current text from input/textarea
27
28
  children?: (props: AiPromptPanelRenderProps) => ReactNode;
29
+ // Nouvelles props pour la gestion avancée des modèles
30
+ enableModelManagement?: boolean;
31
+ availableModels?: AIModel[];
32
+ userModels?: string[];
33
+ onModelToggle?: (modelId: string, isActive: boolean) => Promise<void>;
34
+ apiKey?: string;
35
+ baseUrl?: string;
28
36
  }
29
37
 
30
38
  export interface AiPromptPanelRenderProps {
@@ -36,6 +44,13 @@ export interface AiPromptPanelRenderProps {
36
44
  sourceText?: string;
37
45
  handleSubmit: () => void;
38
46
  handleClose: () => void;
47
+ // Nouvelles props pour la gestion des modèles
48
+ enableModelManagement?: boolean;
49
+ availableModels?: AIModel[];
50
+ userModels?: string[];
51
+ showAllModels?: boolean;
52
+ setShowAllModels?: (show: boolean) => void;
53
+ onModelToggle?: (modelId: string, isActive: boolean) => Promise<void>;
39
54
  }
40
55
 
41
56
  export function AiPromptPanel({
@@ -46,6 +61,12 @@ export function AiPromptPanel({
46
61
  models = [],
47
62
  sourceText,
48
63
  children,
64
+ enableModelManagement = false,
65
+ availableModels = [],
66
+ userModels = [],
67
+ onModelToggle,
68
+ apiKey,
69
+ baseUrl,
49
70
  }: AiPromptPanelProps) {
50
71
  const [selectedModel, setSelectedModel] = useState("");
51
72
  const [prompt, setPrompt] = useState("");
@@ -63,6 +84,11 @@ export function AiPromptPanel({
63
84
  const promptRef = useRef<HTMLTextAreaElement>(null);
64
85
  const closeTimeoutRef = useRef<number | null>(null);
65
86
 
87
+ // États pour la gestion des modèles
88
+ const [showAllModels, setShowAllModels] = useState(false);
89
+ const [isModelManagementOpen, setIsModelManagementOpen] = useState(false);
90
+ const [loadingModels, setLoadingModels] = useState<string[]>([]);
91
+
66
92
  const {
67
93
  prompts,
68
94
  loading: promptsLoading,
@@ -70,6 +96,60 @@ export function AiPromptPanel({
70
96
  incrementStat,
71
97
  } = usePrompts();
72
98
 
99
+ // Hook de gestion des modèles (automatique si enableModelManagement et pas de props externes)
100
+ const autoModelManagement = useModelManagement({
101
+ apiKey,
102
+ baseUrl,
103
+ category: "text", // Par défaut pour AiPromptPanel
104
+ autoFetch:
105
+ enableModelManagement && availableModels.length === 0 && !!apiKey,
106
+ });
107
+
108
+ // Utiliser soit les props externes soit la gestion automatique
109
+ const effectiveAvailableModels =
110
+ availableModels.length > 0
111
+ ? availableModels
112
+ : autoModelManagement.availableModels;
113
+ const effectiveUserModels =
114
+ userModels.length > 0 ? userModels : autoModelManagement.userModels;
115
+ const effectiveToggleModel = onModelToggle || autoModelManagement.toggleModel;
116
+
117
+ // Gestion des modèles
118
+ const handleModelToggle = async (modelId: string, isActive: boolean) => {
119
+ if (!effectiveToggleModel) return;
120
+
121
+ setLoadingModels((prev) => [...prev, modelId]);
122
+ try {
123
+ await effectiveToggleModel(modelId, isActive);
124
+ } catch (error) {
125
+ console.error("Erreur lors du changement de modèle:", error);
126
+ } finally {
127
+ setLoadingModels((prev) => prev.filter((id) => id !== modelId));
128
+ }
129
+ };
130
+
131
+ const getFilteredModels = () => {
132
+ if (!enableModelManagement || effectiveAvailableModels.length === 0) {
133
+ // Mode classique : transformer les ModelRef en AIModel
134
+ return models.map((m) => ({
135
+ id: m.id,
136
+ name: m.name,
137
+ category: m.type === "image" ? ("image" as const) : ("text" as const),
138
+ provider: "Unknown",
139
+ }));
140
+ }
141
+
142
+ const textModels = effectiveAvailableModels.filter(
143
+ (m) => m.category === "text"
144
+ );
145
+
146
+ if (showAllModels) {
147
+ return textModels;
148
+ } else {
149
+ return textModels.filter((m) => effectiveUserModels.includes(m.id));
150
+ }
151
+ };
152
+
73
153
  // Fetch prompts when modal opens
74
154
  useEffect(() => {
75
155
  if (isOpen && models.length > 0) {
@@ -207,6 +287,13 @@ export function AiPromptPanel({
207
287
  sourceText,
208
288
  handleSubmit,
209
289
  handleClose,
290
+ // Nouvelles props pour la gestion des modèles
291
+ enableModelManagement,
292
+ availableModels: effectiveAvailableModels,
293
+ userModels: effectiveUserModels,
294
+ showAllModels,
295
+ setShowAllModels,
296
+ onModelToggle: handleModelToggle,
210
297
  };
211
298
 
212
299
  if (children) {
@@ -329,29 +416,173 @@ export function AiPromptPanel({
329
416
  )}
330
417
 
331
418
  <div style={aiStyles.modalInputGroup}>
332
- <label htmlFor="model-select" style={aiStyles.modalLabel}>
333
- AI Model
334
- </label>
335
- <select
336
- id="model-select"
337
- value={activeModelId}
338
- onChange={(e) => setSelectedModel(e.target.value)}
339
- onFocus={() => setModelFocused(true)}
340
- onBlur={() => setModelFocused(false)}
341
- style={{
342
- ...aiStyles.select,
343
- ...(modelFocused && aiStyles.selectFocus),
344
- }}
345
- >
346
- {models.length === 0 && (
347
- <option value="">Loading models...</option>
348
- )}
349
- {models.map((model) => (
350
- <option key={model.id} value={model.id}>
351
- {model.name}
352
- </option>
353
- ))}
354
- </select>
419
+ {enableModelManagement ? (
420
+ // Version avancée avec gestion des modèles
421
+ <div style={{ marginBottom: "16px" }}>
422
+ <div
423
+ style={{
424
+ display: "flex",
425
+ justifyContent: "space-between",
426
+ alignItems: "center",
427
+ marginBottom: "8px",
428
+ }}
429
+ >
430
+ <label htmlFor="model-select" style={aiStyles.modalLabel}>
431
+ AI Model
432
+ </label>
433
+ <div
434
+ style={{
435
+ display: "flex",
436
+ alignItems: "center",
437
+ gap: "8px",
438
+ }}
439
+ >
440
+ {effectiveAvailableModels.length > 0 && (
441
+ <button
442
+ onClick={() => setShowAllModels(!showAllModels)}
443
+ style={{
444
+ padding: "4px 8px",
445
+ fontSize: "12px",
446
+ color: "#8b5cf6",
447
+ background: "#8b5cf610",
448
+ border: "1px solid #8b5cf630",
449
+ borderRadius: "6px",
450
+ cursor: "pointer",
451
+ transition: "all 0.2s",
452
+ }}
453
+ onMouseEnter={(e) => {
454
+ e.currentTarget.style.background = "#8b5cf620";
455
+ }}
456
+ onMouseLeave={(e) => {
457
+ e.currentTarget.style.background = "#8b5cf610";
458
+ }}
459
+ >
460
+ {showAllModels ? "Mes modèles" : "Tous les modèles"}
461
+ {!showAllModels &&
462
+ effectiveAvailableModels.filter(
463
+ (m) =>
464
+ m.category === "text" &&
465
+ !effectiveUserModels.includes(m.id)
466
+ ).length > 0 && (
467
+ <span
468
+ style={{
469
+ marginLeft: "4px",
470
+ padding: "2px 6px",
471
+ fontSize: "10px",
472
+ background: "#8b5cf6",
473
+ color: "white",
474
+ borderRadius: "10px",
475
+ }}
476
+ >
477
+ +
478
+ {
479
+ effectiveAvailableModels.filter(
480
+ (m) =>
481
+ m.category === "text" &&
482
+ !effectiveUserModels.includes(m.id)
483
+ ).length
484
+ }
485
+ </span>
486
+ )}
487
+ </button>
488
+ )}
489
+ <button
490
+ onClick={() => setIsModelManagementOpen(true)}
491
+ style={{
492
+ padding: "4px 8px",
493
+ fontSize: "12px",
494
+ color: "#8b5cf6",
495
+ background: "#8b5cf610",
496
+ border: "1px solid #8b5cf630",
497
+ borderRadius: "6px",
498
+ cursor: "pointer",
499
+ transition: "all 0.2s",
500
+ display: "flex",
501
+ alignItems: "center",
502
+ gap: "4px",
503
+ }}
504
+ onMouseEnter={(e) => {
505
+ e.currentTarget.style.background = "#8b5cf620";
506
+ }}
507
+ onMouseLeave={(e) => {
508
+ e.currentTarget.style.background = "#8b5cf610";
509
+ }}
510
+ >
511
+ <Settings size={12} />
512
+ </button>
513
+ </div>
514
+ </div>
515
+ <select
516
+ id="model-select"
517
+ value={activeModelId}
518
+ onChange={(e) => setSelectedModel(e.target.value)}
519
+ onFocus={() => setModelFocused(true)}
520
+ onBlur={() => setModelFocused(false)}
521
+ style={{
522
+ ...aiStyles.select,
523
+ ...(modelFocused && aiStyles.selectFocus),
524
+ }}
525
+ >
526
+ {getFilteredModels().length === 0 && (
527
+ <option value="">Loading models...</option>
528
+ )}
529
+ {getFilteredModels().map((model) => {
530
+ const isActive = effectiveUserModels.includes(model.id);
531
+ return (
532
+ <option
533
+ key={model.id}
534
+ value={model.id}
535
+ style={{
536
+ opacity: showAllModels && !isActive ? 0.6 : 1,
537
+ }}
538
+ >
539
+ {model.name}
540
+ {showAllModels && isActive && " ✓"}
541
+ {showAllModels && !isActive && " (Désactivé)"}
542
+ </option>
543
+ );
544
+ })}
545
+ </select>
546
+ {showAllModels && onModelToggle && (
547
+ <div
548
+ style={{
549
+ fontSize: "11px",
550
+ color: "#6b7280",
551
+ marginTop: "4px",
552
+ }}
553
+ >
554
+ 💡 Cliquez sur "⚙️" pour activer/désactiver les modèles
555
+ </div>
556
+ )}
557
+ </div>
558
+ ) : (
559
+ // Version classique
560
+ <>
561
+ <label htmlFor="model-select" style={aiStyles.modalLabel}>
562
+ AI Model
563
+ </label>
564
+ <select
565
+ id="model-select"
566
+ value={activeModelId}
567
+ onChange={(e) => setSelectedModel(e.target.value)}
568
+ onFocus={() => setModelFocused(true)}
569
+ onBlur={() => setModelFocused(false)}
570
+ style={{
571
+ ...aiStyles.select,
572
+ ...(modelFocused && aiStyles.selectFocus),
573
+ }}
574
+ >
575
+ {models.length === 0 && (
576
+ <option value="">Loading models...</option>
577
+ )}
578
+ {models.map((model) => (
579
+ <option key={model.id} value={model.id}>
580
+ {model.name}
581
+ </option>
582
+ ))}
583
+ </select>
584
+ </>
585
+ )}
355
586
  </div>
356
587
 
357
588
  <div style={aiStyles.modalInputGroup}>
@@ -758,6 +989,253 @@ export function AiPromptPanel({
758
989
  </button>
759
990
  </div>
760
991
  </div>
992
+
993
+ {/* Modal de gestion des modèles */}
994
+ {enableModelManagement && isModelManagementOpen && (
995
+ <div
996
+ style={{
997
+ ...aiStyles.modal,
998
+ zIndex: 1001, // Au-dessus du modal principal
999
+ }}
1000
+ >
1001
+ <div
1002
+ style={{
1003
+ ...aiStyles.modalOverlay,
1004
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
1005
+ }}
1006
+ onClick={() => setIsModelManagementOpen(false)}
1007
+ />
1008
+ <div
1009
+ style={{
1010
+ ...aiStyles.modalContent,
1011
+ maxWidth: "600px",
1012
+ maxHeight: "80vh",
1013
+ overflow: "auto",
1014
+ }}
1015
+ >
1016
+ <div style={aiStyles.modalHeader}>
1017
+ <h2 style={aiStyles.modalTitle}>Gestion des modèles IA</h2>
1018
+ <button
1019
+ style={aiStyles.modalCloseButton}
1020
+ onClick={() => setIsModelManagementOpen(false)}
1021
+ aria-label="Close"
1022
+ >
1023
+ ×
1024
+ </button>
1025
+ </div>
1026
+
1027
+ <div style={aiStyles.modalBody}>
1028
+ <div style={{ marginBottom: "16px" }}>
1029
+ <p
1030
+ style={{
1031
+ fontSize: "14px",
1032
+ color: "#6b7280",
1033
+ margin: "0 0 16px 0",
1034
+ }}
1035
+ >
1036
+ Activez ou désactivez les modèles selon vos besoins
1037
+ </p>
1038
+ </div>
1039
+
1040
+ <div
1041
+ style={{
1042
+ display: "flex",
1043
+ flexDirection: "column",
1044
+ gap: "12px",
1045
+ }}
1046
+ >
1047
+ {effectiveAvailableModels
1048
+ .filter((model) => model.category === "text")
1049
+ .map((modelData) => {
1050
+ const isActive = effectiveUserModels.includes(modelData.id);
1051
+ const isLoading = loadingModels.includes(modelData.id);
1052
+
1053
+ return (
1054
+ <div
1055
+ key={modelData.id}
1056
+ style={{
1057
+ display: "flex",
1058
+ alignItems: "center",
1059
+ justifyContent: "space-between",
1060
+ padding: "16px",
1061
+ border: "1px solid #e5e7eb",
1062
+ borderRadius: "8px",
1063
+ backgroundColor: isActive ? "#f0fdf4" : "#ffffff",
1064
+ transition: "all 0.2s",
1065
+ }}
1066
+ >
1067
+ <div
1068
+ style={{
1069
+ display: "flex",
1070
+ alignItems: "center",
1071
+ gap: "12px",
1072
+ }}
1073
+ >
1074
+ <div
1075
+ style={{
1076
+ width: "12px",
1077
+ height: "12px",
1078
+ borderRadius: "50%",
1079
+ backgroundColor: isActive ? "#10b981" : "#d1d5db",
1080
+ }}
1081
+ />
1082
+ <div>
1083
+ <div
1084
+ style={{
1085
+ display: "flex",
1086
+ alignItems: "center",
1087
+ gap: "8px",
1088
+ marginBottom: "4px",
1089
+ }}
1090
+ >
1091
+ <span
1092
+ style={{
1093
+ fontWeight: "500",
1094
+ color: "#111827",
1095
+ }}
1096
+ >
1097
+ {modelData.name}
1098
+ </span>
1099
+ {modelData.isPro && (
1100
+ <span
1101
+ style={{
1102
+ padding: "2px 8px",
1103
+ fontSize: "11px",
1104
+ backgroundColor: "#8b5cf6",
1105
+ color: "white",
1106
+ borderRadius: "12px",
1107
+ fontWeight: "500",
1108
+ }}
1109
+ >
1110
+ PRO
1111
+ </span>
1112
+ )}
1113
+ </div>
1114
+ {modelData.description && (
1115
+ <p
1116
+ style={{
1117
+ fontSize: "13px",
1118
+ color: "#6b7280",
1119
+ margin: "0 0 4px 0",
1120
+ }}
1121
+ >
1122
+ {modelData.description}
1123
+ </p>
1124
+ )}
1125
+ <div
1126
+ style={{
1127
+ display: "flex",
1128
+ alignItems: "center",
1129
+ gap: "16px",
1130
+ fontSize: "12px",
1131
+ color: "#9ca3af",
1132
+ }}
1133
+ >
1134
+ <span>Fournisseur: {modelData.provider}</span>
1135
+ {modelData.costPer1M && (
1136
+ <span>
1137
+ Coût: ${modelData.costPer1M}/1M tokens
1138
+ </span>
1139
+ )}
1140
+ </div>
1141
+ </div>
1142
+ </div>
1143
+ <button
1144
+ onClick={() =>
1145
+ handleModelToggle(modelData.id, !isActive)
1146
+ }
1147
+ disabled={isLoading}
1148
+ style={{
1149
+ padding: "8px 16px",
1150
+ fontSize: "13px",
1151
+ fontWeight: "500",
1152
+ borderRadius: "6px",
1153
+ border: "1px solid",
1154
+ cursor: isLoading ? "not-allowed" : "pointer",
1155
+ transition: "all 0.2s",
1156
+ minWidth: "80px",
1157
+ ...(isActive
1158
+ ? {
1159
+ backgroundColor: "#fef2f2",
1160
+ borderColor: "#fecaca",
1161
+ color: "#dc2626",
1162
+ }
1163
+ : {
1164
+ backgroundColor: "#f0fdf4",
1165
+ borderColor: "#bbf7d0",
1166
+ color: "#16a34a",
1167
+ }),
1168
+ opacity: isLoading ? 0.6 : 1,
1169
+ }}
1170
+ onMouseEnter={(e) => {
1171
+ if (!isLoading) {
1172
+ if (isActive) {
1173
+ e.currentTarget.style.backgroundColor =
1174
+ "#fee2e2";
1175
+ } else {
1176
+ e.currentTarget.style.backgroundColor =
1177
+ "#dcfce7";
1178
+ }
1179
+ }
1180
+ }}
1181
+ onMouseLeave={(e) => {
1182
+ if (!isLoading) {
1183
+ if (isActive) {
1184
+ e.currentTarget.style.backgroundColor =
1185
+ "#fef2f2";
1186
+ } else {
1187
+ e.currentTarget.style.backgroundColor =
1188
+ "#f0fdf4";
1189
+ }
1190
+ }
1191
+ }}
1192
+ >
1193
+ {isLoading ? (
1194
+ <div
1195
+ style={{
1196
+ display: "flex",
1197
+ alignItems: "center",
1198
+ justifyContent: "center",
1199
+ gap: "4px",
1200
+ }}
1201
+ >
1202
+ <div
1203
+ style={{
1204
+ width: "12px",
1205
+ height: "12px",
1206
+ border: "2px solid currentColor",
1207
+ borderTop: "2px solid transparent",
1208
+ borderRadius: "50%",
1209
+ animation: "ai-spin 1s linear infinite",
1210
+ }}
1211
+ />
1212
+ </div>
1213
+ ) : isActive ? (
1214
+ "Désactiver"
1215
+ ) : (
1216
+ "Activer"
1217
+ )}
1218
+ </button>
1219
+ </div>
1220
+ );
1221
+ })}
1222
+ </div>
1223
+ </div>
1224
+
1225
+ <div style={aiStyles.modalFooter}>
1226
+ <button
1227
+ onClick={() => setIsModelManagementOpen(false)}
1228
+ style={{
1229
+ ...aiStyles.button,
1230
+ ...aiStyles.buttonSecondary,
1231
+ }}
1232
+ >
1233
+ Fermer
1234
+ </button>
1235
+ </div>
1236
+ </div>
1237
+ </div>
1238
+ )}
761
1239
  </div>
762
1240
  );
763
1241
  }
@@ -66,10 +66,12 @@ export function AiSettingsButton({
66
66
  <span>Tokens:</span>
67
67
  <span>{status.balance?.total || 0}</span>
68
68
  </div>
69
- {status.storage?.total_mb !== undefined && (
69
+ {status.storage?.allocated_mb !== undefined && (
70
70
  <div data-ai-status-item>
71
71
  <span>Storage:</span>
72
- <span>{status.storage.total_mb.toFixed(2)} MB</span>
72
+ <span>
73
+ {status.storage.allocated_mb.toFixed(2)} MB
74
+ </span>
73
75
  </div>
74
76
  )}
75
77
  </div>