@lastbrain/ai-ui-react 1.0.34 → 1.0.36

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 (38) hide show
  1. package/dist/components/AiChipLabel.d.ts.map +1 -1
  2. package/dist/components/AiChipLabel.js +5 -1
  3. package/dist/components/AiContextButton.d.ts.map +1 -1
  4. package/dist/components/AiContextButton.js +1 -0
  5. package/dist/components/AiImageButton.d.ts.map +1 -1
  6. package/dist/components/AiImageButton.js +15 -2
  7. package/dist/components/AiInput.d.ts.map +1 -1
  8. package/dist/components/AiInput.js +5 -1
  9. package/dist/components/AiPromptPanel.d.ts +1 -1
  10. package/dist/components/AiPromptPanel.d.ts.map +1 -1
  11. package/dist/components/AiPromptPanel.js +9 -5
  12. package/dist/components/AiSelect.d.ts.map +1 -1
  13. package/dist/components/AiSelect.js +5 -1
  14. package/dist/components/AiTextarea.d.ts.map +1 -1
  15. package/dist/components/AiTextarea.js +5 -1
  16. package/dist/context/AiProvider.d.ts +28 -0
  17. package/dist/context/AiProvider.d.ts.map +1 -1
  18. package/dist/context/AiProvider.js +159 -1
  19. package/dist/hooks/useAiModels.d.ts +6 -1
  20. package/dist/hooks/useAiModels.d.ts.map +1 -1
  21. package/dist/hooks/useAiModels.js +71 -24
  22. package/dist/hooks/useModelManagement.d.ts +2 -10
  23. package/dist/hooks/useModelManagement.d.ts.map +1 -1
  24. package/dist/hooks/useModelManagement.js +42 -150
  25. package/dist/utils/modelManagement.d.ts.map +1 -1
  26. package/dist/utils/modelManagement.js +7 -3
  27. package/package.json +2 -2
  28. package/src/components/AiChipLabel.tsx +5 -1
  29. package/src/components/AiContextButton.tsx +1 -0
  30. package/src/components/AiImageButton.tsx +16 -2
  31. package/src/components/AiInput.tsx +5 -1
  32. package/src/components/AiPromptPanel.tsx +24 -1
  33. package/src/components/AiSelect.tsx +5 -1
  34. package/src/components/AiTextarea.tsx +5 -1
  35. package/src/context/AiProvider.tsx +223 -1
  36. package/src/hooks/useAiModels.ts +96 -27
  37. package/src/hooks/useModelManagement.ts +52 -203
  38. package/src/utils/modelManagement.ts +7 -3
@@ -1,181 +1,73 @@
1
- import { useState, useCallback, useEffect, useMemo } from "react";
1
+ import { useState, useCallback, useMemo } from "react";
2
2
  import { useAiContext } from "../context/AiProvider";
3
- import { toggleUserModel, getAvailableModels, getUserModels, } from "../utils/modelManagement";
4
- import { getCached, setCache, isRateLimited, getRateLimitResetTime, } from "../utils/cache";
3
+ import { toggleUserModel, } from "../utils/modelManagement";
5
4
  /**
6
5
  * Hook pour gérer les modèles IA d'un utilisateur
6
+ * Utilise les données du contexte pour éviter les appels API multiples
7
7
  */
8
8
  export function useModelManagement(options = {}) {
9
- const { autoFetch = false, category, ...apiOptions } = options;
10
- const { baseUrl, apiKeyId } = useAiContext();
11
- const [availableModels, setAvailableModels] = useState([]);
12
- const [userModels, setUserModels] = useState([]);
9
+ const { category } = options;
10
+ const context = useAiContext();
13
11
  const [loading, setLoading] = useState(false);
14
12
  const [error, setError] = useState(null);
15
- const [apiUnavailable, setApiUnavailable] = useState(false);
16
- // Utiliser baseUrl et apiKey du contexte avec memoization
17
- const effectiveOptions = useMemo(() => ({
18
- baseUrl: options.baseUrl || baseUrl,
19
- apiKey: options.apiKey || apiKeyId,
20
- }), [options.baseUrl, options.apiKey, baseUrl, apiKeyId]);
13
+ // Utiliser les données du contexte si disponibles
14
+ const useContextData = (!options.baseUrl || options.baseUrl === context.baseUrl) &&
15
+ (!options.apiKey || options.apiKey === context.apiKeyId);
16
+ // Filtrer par catégorie si nécessaire
17
+ const filteredModels = useMemo(() => {
18
+ if (!useContextData)
19
+ return [];
20
+ return category
21
+ ? context.availableModels.filter((m) => m.category === category)
22
+ : context.availableModels;
23
+ }, [useContextData, category, context.availableModels]);
21
24
  const refreshModels = useCallback(async () => {
22
- try {
23
- setLoading(true);
24
- setError(null);
25
- // Check cache first (5 minutes TTL for models)
26
- const cacheKey = `models_${category || "all"}`;
27
- const cached = getCached(cacheKey, 300000);
28
- if (cached) {
29
- console.log("[useModelManagement] Using cached models");
30
- setAvailableModels(cached);
31
- setLoading(false);
32
- return;
33
- }
34
- // Check rate limit (max 5 calls per minute)
35
- if (isRateLimited("models_fetch", 5, 60000)) {
36
- const resetTime = getRateLimitResetTime("models_fetch");
37
- const resetSeconds = Math.ceil(resetTime / 1000);
38
- throw new Error(`Rate limit exceeded. Please wait ${resetSeconds}s before trying again.`);
39
- }
40
- console.log("[useModelManagement] Fetching available models with options:", {
41
- apiKey: effectiveOptions.apiKey
42
- ? effectiveOptions.apiKey.substring(0, 10) + "..."
43
- : "none",
44
- baseUrl: effectiveOptions.baseUrl || "default",
45
- });
46
- const models = await getAvailableModels(effectiveOptions);
47
- console.log("[useModelManagement] Got available models:", models.length);
48
- const filteredModels = category
49
- ? models.filter((m) => m.category === category)
50
- : models;
51
- setAvailableModels(filteredModels);
52
- // Cache the results
53
- setCache(cacheKey, filteredModels);
54
- console.log("[useModelManagement] Set filtered models:", filteredModels.length);
55
- }
56
- catch (err) {
57
- const errorMessage = err instanceof Error ? err.message : "Erreur inconnue";
58
- console.error("[useModelManagement] Error fetching available models:", errorMessage);
59
- setError(errorMessage);
60
- // En cas d'erreur 404 (API non disponible), utiliser un array vide
61
- if (errorMessage.includes("404") || errorMessage.includes("Not Found")) {
62
- setAvailableModels([]);
63
- }
25
+ if (useContextData) {
26
+ await context.refetchProviders();
64
27
  }
65
- finally {
66
- setLoading(false);
67
- }
68
- }, [category, effectiveOptions]);
28
+ }, [useContextData, context]);
69
29
  const refreshUserModels = useCallback(async () => {
70
- // Si l'API est marquée comme indisponible, ne pas faire l'appel
71
- if (apiUnavailable) {
72
- console.log("[useModelManagement] Skipping user models fetch - API marked as unavailable");
73
- return;
74
- }
75
- try {
76
- setLoading(true);
77
- setError(null);
78
- console.log("[useModelManagement] Fetching user models with options:", {
79
- apiKey: effectiveOptions.apiKey
80
- ? effectiveOptions.apiKey.substring(0, 10) + "..."
81
- : "none",
82
- baseUrl: effectiveOptions.baseUrl || "default",
83
- });
84
- const models = await getUserModels(effectiveOptions);
85
- console.log("[useModelManagement] Got user models:", models);
86
- setUserModels(models);
30
+ if (useContextData) {
31
+ await context.refetchUserModels();
87
32
  }
88
- catch (err) {
89
- const errorMessage = err instanceof Error ? err.message : "Erreur inconnue";
90
- console.error("[useModelManagement] Error fetching user models:", errorMessage);
91
- setError(errorMessage);
92
- // En cas d'erreur 404 (API non disponible), utiliser un array vide et ne pas retry
93
- if (errorMessage.includes("404") || errorMessage.includes("Not Found")) {
94
- setUserModels([]);
95
- setApiUnavailable(true);
96
- console.warn("[useModelManagement] User models API not available (404), disabling further requests");
97
- }
98
- }
99
- finally {
100
- setLoading(false);
101
- }
102
- }, [effectiveOptions, apiUnavailable]);
33
+ }, [useContextData, context]);
103
34
  const toggleModel = useCallback(async (modelId, isActive) => {
104
- const currentlyActive = userModels.includes(modelId);
35
+ const currentlyActive = context.userModels.includes(modelId);
105
36
  const targetState = isActive !== undefined ? isActive : !currentlyActive;
106
37
  try {
107
38
  setLoading(true);
108
39
  setError(null);
40
+ const effectiveOptions = {
41
+ baseUrl: options.baseUrl || context.baseUrl,
42
+ apiKey: options.apiKey || context.apiKeyId,
43
+ };
109
44
  await toggleUserModel(modelId, targetState, effectiveOptions);
110
- // Mise à jour optimiste
111
- if (targetState) {
112
- setUserModels((prev) => [
113
- ...prev.filter((id) => id !== modelId),
114
- modelId,
115
- ]);
116
- }
117
- else {
118
- setUserModels((prev) => prev.filter((id) => id !== modelId));
119
- }
45
+ // Rafraîchir les modèles utilisateur depuis le contexte
46
+ await context.refetchUserModels();
120
47
  }
121
48
  catch (err) {
122
- setError(err instanceof Error ? err.message : "Erreur inconnue");
49
+ const errorMessage = err instanceof Error ? err.message : "Erreur inconnue";
50
+ console.error("[useModelManagement] Error toggling model:", errorMessage);
51
+ setError(errorMessage);
123
52
  throw err;
124
53
  }
125
54
  finally {
126
55
  setLoading(false);
127
56
  }
128
- }, [userModels, effectiveOptions]);
129
- // Helpers
57
+ }, [context, options.baseUrl, options.apiKey]);
130
58
  const isModelActive = useCallback((modelId) => {
131
- return userModels.includes(modelId);
132
- }, [userModels]);
59
+ return context.userModels.includes(modelId);
60
+ }, [context.userModels]);
133
61
  const getActiveModels = useCallback(() => {
134
- return availableModels.filter((model) => userModels.includes(model.id));
135
- }, [availableModels, userModels]);
62
+ return filteredModels.filter((model) => context.userModels.includes(model.id));
63
+ }, [filteredModels, context.userModels]);
136
64
  const getInactiveModels = useCallback(() => {
137
- return availableModels.filter((model) => !userModels.includes(model.id));
138
- }, [availableModels, userModels]);
139
- // Auto-fetch au mount - si autoFetch ET (apiKey OU baseUrl avec proxy externe) ET API disponible
140
- useEffect(() => {
141
- const isExternalProxy = effectiveOptions.baseUrl &&
142
- effectiveOptions.baseUrl.includes("/api/lastbrain");
143
- console.log("[useModelManagement] useEffect triggered:", {
144
- autoFetch,
145
- hasApiKey: !!effectiveOptions.apiKey,
146
- hasBaseUrl: !!effectiveOptions.baseUrl,
147
- isExternalProxy,
148
- apiUnavailable,
149
- apiKeyPreview: effectiveOptions.apiKey
150
- ? effectiveOptions.apiKey.substring(0, 10) + "..."
151
- : "none",
152
- baseUrl: effectiveOptions.baseUrl || "none",
153
- });
154
- // Auto-fetch si autoFetch est activé ET qu'on a soit une apiKey soit un proxy externe ET API disponible
155
- if (autoFetch &&
156
- !apiUnavailable &&
157
- (effectiveOptions.apiKey || isExternalProxy)) {
158
- console.log("[useModelManagement] Starting auto-fetch...");
159
- Promise.all([refreshModels(), refreshUserModels()]);
160
- }
161
- else if (autoFetch && !effectiveOptions.apiKey && !isExternalProxy) {
162
- console.warn("[useModelManagement] autoFetch is enabled but no apiKey or baseUrl provided. Skipping automatic fetch.");
163
- }
164
- else if (autoFetch && apiUnavailable) {
165
- console.log("[useModelManagement] autoFetch skipped - API marked as unavailable");
166
- }
167
- }, [
168
- autoFetch,
169
- refreshModels,
170
- refreshUserModels,
171
- effectiveOptions.apiKey,
172
- effectiveOptions.baseUrl,
173
- apiUnavailable,
174
- ]);
65
+ return filteredModels.filter((model) => !context.userModels.includes(model.id));
66
+ }, [filteredModels, context.userModels]);
175
67
  return {
176
- availableModels,
177
- userModels,
178
- loading,
68
+ availableModels: filteredModels,
69
+ userModels: context.userModels,
70
+ loading: context.loadingProviders || context.loadingUserModels || loading,
179
71
  error,
180
72
  toggleModel,
181
73
  refreshModels,
@@ -1 +1 @@
1
- {"version":3,"file":"modelManagement.d.ts","sourceRoot":"","sources":["../../src/utils/modelManagement.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,OAAO,EACjB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CACR,KAAK,CAAC;IACJ,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,CACH,CA6CA;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,EAAE,CAAC,CA6CnB"}
1
+ {"version":3,"file":"modelManagement.d.ts","sourceRoot":"","sources":["../../src/utils/modelManagement.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,OAAO,EACjB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAqCf;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CACR,KAAK,CAAC;IACJ,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,CACH,CA6CA;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,EAAE,CAAC,CA6CnB"}
@@ -14,11 +14,15 @@ export async function toggleUserModel(modelId, isActive, options = {}) {
14
14
  headers["Authorization"] = `Bearer ${apiKey}`;
15
15
  }
16
16
  const isPublicApi = baseUrl && baseUrl.includes("/api/public/v1");
17
+ const isExternalProxy = baseUrl && baseUrl.includes("/api/lastbrain");
18
+ // Route mapping pour toggle
17
19
  const endpoint = isPublicApi
18
20
  ? `${baseUrl}/ai/user/models/toggle`
19
- : baseUrl
20
- ? `${baseUrl}/auth/ai-models-toggle`
21
- : `/api/ai/auth/ai-models-toggle`;
21
+ : isExternalProxy
22
+ ? `${baseUrl}/ai/user/models/toggle` // Gateway mappe automatiquement
23
+ : baseUrl
24
+ ? `${baseUrl}/auth/ai-models-toggle`
25
+ : `/api/ai/auth/ai-models-toggle`;
22
26
  const response = await fetch(endpoint, {
23
27
  method: "PUT",
24
28
  headers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/ai-ui-react",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "Headless React components for LastBrain AI UI Kit",
5
5
  "private": false,
6
6
  "type": "module",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "lucide-react": "^0.257.0",
51
- "@lastbrain/ai-ui-core": "1.0.23"
51
+ "@lastbrain/ai-ui-core": "1.0.25"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/react": "^19.2.0",
@@ -93,7 +93,11 @@ export function AiChipInput({
93
93
  const apiKeyId = propApiKeyId ?? aiContext.apiKeyId;
94
94
 
95
95
  // Hooks pour l'IA avec les valeurs du contexte
96
- const { models } = useAiModels({ baseUrl, apiKeyId });
96
+ const { models } = useAiModels({
97
+ baseUrl,
98
+ apiKeyId,
99
+ modelType: "text-or-language",
100
+ });
97
101
  const { generateText } = useAiCallText({ baseUrl, apiKeyId });
98
102
 
99
103
  const addChip = (text: string) => {
@@ -72,6 +72,7 @@ export function AiContextButton({
72
72
  const { models, loading: modelsLoading } = useAiModels({
73
73
  baseUrl,
74
74
  apiKeyId,
75
+ modelType: "text-or-language",
75
76
  });
76
77
  const { generateText: callText, loading } = useAiCallText({
77
78
  baseUrl,
@@ -63,9 +63,15 @@ export function AiImageButton({
63
63
  const baseUrl = propBaseUrl ?? aiContext.baseUrl;
64
64
  const apiKeyId = propApiKeyId ?? aiContext.apiKeyId;
65
65
 
66
- const { models } = useAiModels({ baseUrl, apiKeyId });
66
+ // Récupérer uniquement les modèles image
67
+ const { models } = useAiModels({ baseUrl, apiKeyId, modelType: "image" });
67
68
  const { generateImage, loading } = useAiCallImage({ baseUrl, apiKeyId });
68
69
 
70
+ console.log("[AiImageButton] Models received:", {
71
+ count: models.length,
72
+ models: models.map((m) => ({ id: m.id, name: m.name, type: m.type })),
73
+ });
74
+
69
75
  const handleOpenPanel = () => {
70
76
  setIsOpen(true);
71
77
  };
@@ -287,7 +293,15 @@ export function AiImageButton({
287
293
  onClose={handleClosePanel}
288
294
  onSubmit={handleSubmit}
289
295
  uiMode={uiMode}
290
- models={models?.filter((m) => m.type === "image") || []}
296
+ models={(() => {
297
+ const filteredModels = models.filter((m) => m.type === "image");
298
+ console.log("[AiImageButton] Passing to AiPromptPanel:", {
299
+ originalCount: models.length,
300
+ filteredCount: filteredModels.length,
301
+ models: filteredModels,
302
+ });
303
+ return filteredModels;
304
+ })()}
291
305
  />
292
306
  )}
293
307
  </div>
@@ -44,7 +44,11 @@ export function AiInput({
44
44
  const inputRef = useRef<HTMLInputElement>(null);
45
45
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
46
46
 
47
- const { models } = useAiModels({ baseUrl, apiKeyId });
47
+ const { models } = useAiModels({
48
+ baseUrl,
49
+ apiKeyId,
50
+ modelType: "text-or-language",
51
+ });
48
52
  const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
49
53
 
50
54
  const hasConfiguration = Boolean(model && prompt);
@@ -17,7 +17,8 @@ import {
17
17
  type Prompt,
18
18
  type PublicPrompt,
19
19
  } from "../hooks/usePrompts";
20
- import { useModelManagement, type AIModel } from "../hooks/useModelManagement";
20
+ import { useModelManagement } from "../hooks/useModelManagement";
21
+ import { type AIModel } from "../context/AiProvider";
21
22
  import { AiProvider } from "../context/AiProvider";
22
23
 
23
24
  export interface AiPromptPanelProps {
@@ -1070,6 +1071,28 @@ function AiPromptPanelInternal({
1070
1071
  >
1071
1072
  Activez ou désactivez les modèles selon vos besoins
1072
1073
  </p>
1074
+ <p
1075
+ style={{
1076
+ fontSize: "12px",
1077
+ color: "#9ca3af",
1078
+ margin: "0",
1079
+ }}
1080
+ >
1081
+ {
1082
+ effectiveAvailableModels.filter(
1083
+ (m) => m.category === "text"
1084
+ ).length
1085
+ }{" "}
1086
+ modèles disponibles •{" "}
1087
+ {
1088
+ effectiveUserModels.filter((id) =>
1089
+ effectiveAvailableModels.some(
1090
+ (m) => m.id === id && m.category === "text"
1091
+ )
1092
+ ).length
1093
+ }{" "}
1094
+ activés
1095
+ </p>
1073
1096
  </div>
1074
1097
 
1075
1098
  <div
@@ -37,7 +37,11 @@ export function AiSelect({
37
37
  const [isFocused, setIsFocused] = useState(false);
38
38
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
39
39
 
40
- const { models } = useAiModels({ baseUrl, apiKeyId });
40
+ const { models } = useAiModels({
41
+ baseUrl,
42
+ apiKeyId,
43
+ modelType: "text-or-language",
44
+ });
41
45
  const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
42
46
 
43
47
  const handleOpenPanel = () => {
@@ -50,7 +50,11 @@ export function AiTextarea({
50
50
  const textareaRef = useRef<HTMLTextAreaElement>(null);
51
51
  const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
52
52
 
53
- const { models } = useAiModels({ baseUrl, apiKeyId });
53
+ const { models } = useAiModels({
54
+ baseUrl,
55
+ apiKeyId,
56
+ modelType: "text-or-language",
57
+ });
54
58
  const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
55
59
 
56
60
  const hasConfiguration = Boolean(model && prompt);
@@ -1,12 +1,58 @@
1
1
  "use client";
2
2
 
3
- import { createContext, useContext, type ReactNode } from "react";
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useState,
7
+ useEffect,
8
+ useCallback,
9
+ useRef,
10
+ type ReactNode,
11
+ } from "react";
4
12
  import type { UiMode } from "../types";
13
+ import type { ModelRef } from "@lastbrain/ai-ui-core";
14
+ import { createClient } from "@lastbrain/ai-ui-core";
15
+ import { getAvailableModels, getUserModels } from "../utils/modelManagement";
16
+
17
+ export interface AIModel {
18
+ id: string;
19
+ name: string;
20
+ description?: string;
21
+ provider: string;
22
+ category: "text" | "image" | "audio" | "video";
23
+ isActive?: boolean;
24
+ isPro?: boolean;
25
+ costPer1M?: number;
26
+ }
27
+
28
+ export interface ProviderData {
29
+ id: string;
30
+ name: string;
31
+ description?: string;
32
+ models: ModelRef[];
33
+ }
5
34
 
6
35
  export interface AiContextValue {
7
36
  baseUrl: string;
8
37
  apiKeyId: string;
9
38
  uiMode: UiMode;
39
+ // Providers et modèles (récupérés une seule fois)
40
+ providers: ProviderData[];
41
+ allModels: ModelRef[]; // Tous les modèles de tous les providers
42
+ availableModels: AIModel[]; // Modèles disponibles avec métadonnées
43
+ userModels: string[]; // IDs des modèles activés par l'utilisateur
44
+ // États de chargement
45
+ loadingProviders: boolean;
46
+ loadingUserModels: boolean;
47
+ // Actions
48
+ refetchProviders: () => Promise<void>;
49
+ refetchUserModels: () => Promise<void>;
50
+ // Helpers pour filtrer les modèles
51
+ getModelsByType: (
52
+ type: "text" | "language" | "image" | "embed"
53
+ ) => ModelRef[];
54
+ getTextModels: () => ModelRef[];
55
+ getImageModels: () => ModelRef[];
10
56
  }
11
57
 
12
58
  const AiContext = createContext<AiContextValue | undefined>(undefined);
@@ -24,10 +70,186 @@ export function AiProvider({
24
70
  uiMode = "modal",
25
71
  children,
26
72
  }: AiProviderProps) {
73
+ const [providers, setProviders] = useState<ProviderData[]>([]);
74
+ const [allModels, setAllModels] = useState<ModelRef[]>([]);
75
+ const [availableModels, setAvailableModels] = useState<AIModel[]>([]);
76
+ const [userModels, setUserModels] = useState<string[]>([]);
77
+ const [loadingProviders, setLoadingProviders] = useState(false);
78
+ const [loadingUserModels, setLoadingUserModels] = useState(false);
79
+
80
+ // Flags pour éviter les appels multiples et les boucles infinies
81
+ const isFetchingProviders = useRef(false);
82
+ const isFetchingUserModels = useRef(false);
83
+ const hasFetchedProviders = useRef(false);
84
+ const hasFetchedUserModels = useRef(false);
85
+ const providersAvailable = useRef(true); // false si 404
86
+ const userModelsAvailable = useRef(true); // false si 404
87
+
88
+ // Récupérer les providers et leurs modèles + available models en parallèle
89
+ const fetchProviders = useCallback(async () => {
90
+ // Éviter les appels multiples
91
+ if (isFetchingProviders.current) {
92
+ console.log("[AiProvider] Already fetching providers, skipping");
93
+ return;
94
+ }
95
+
96
+ // Si déjà fetch et non disponible, ne pas réessayer
97
+ if (hasFetchedProviders.current && !providersAvailable.current) {
98
+ console.log("[AiProvider] Providers API not available (404), skipping");
99
+ return;
100
+ }
101
+
102
+ isFetchingProviders.current = true;
103
+ setLoadingProviders(true);
104
+
105
+ try {
106
+ console.log("[AiProvider] Fetching providers from:", baseUrl);
107
+
108
+ // Utiliser createClient pour avoir buildUrl qui gère les routes correctement
109
+ const client = createClient({ baseUrl, apiKeyId });
110
+
111
+ // Fetch providers et available models en parallèle
112
+ const [providersData, availableModelsData] = await Promise.all([
113
+ client
114
+ .getModels()
115
+ .then((models) => {
116
+ // getModels retourne directement les modèles, pas les providers
117
+ // On doit reconstruire la structure providers
118
+ return { providers: [{ id: "default", name: "Default", models }] };
119
+ })
120
+ .catch((error) => {
121
+ console.error("[AiProvider] Error fetching models:", error);
122
+ if (error.message?.includes("404")) {
123
+ providersAvailable.current = false;
124
+ }
125
+ return { providers: [] };
126
+ }),
127
+ getAvailableModels({ baseUrl, apiKey: apiKeyId }).catch((error) => {
128
+ console.warn("[AiProvider] Could not fetch available models:", error);
129
+ return [];
130
+ }),
131
+ ]);
132
+ console.log("[AiProvider] Providers data received:", providersData);
133
+
134
+ if (providersData.providers && Array.isArray(providersData.providers)) {
135
+ setProviders(providersData.providers);
136
+
137
+ // Extraire tous les modèles
138
+ const models: ModelRef[] = [];
139
+ for (const provider of providersData.providers) {
140
+ if (provider.models && Array.isArray(provider.models)) {
141
+ models.push(...provider.models);
142
+ }
143
+ }
144
+ console.log("[AiProvider] Extracted models:", models.length, models);
145
+ setAllModels(models);
146
+ }
147
+
148
+ // Stocker available models
149
+ console.log("[AiProvider] Available models:", availableModelsData.length);
150
+ setAvailableModels(availableModelsData);
151
+ hasFetchedProviders.current = true;
152
+ } catch (error) {
153
+ console.error("[AiProvider] Error fetching providers:", error);
154
+ // En cas d'erreur, utiliser des valeurs vides
155
+ setProviders([]);
156
+ setAllModels([]);
157
+ setAvailableModels([]);
158
+ hasFetchedProviders.current = true;
159
+ } finally {
160
+ setLoadingProviders(false);
161
+ isFetchingProviders.current = false;
162
+ }
163
+ }, [baseUrl, apiKeyId]); // Retirer loadingProviders des dépendances
164
+
165
+ // Récupérer les modèles activés par l'utilisateur
166
+ const fetchUserModels = useCallback(async () => {
167
+ // Éviter les appels multiples
168
+ if (isFetchingUserModels.current) {
169
+ console.log("[AiProvider] Already fetching user models, skipping");
170
+ return;
171
+ }
172
+
173
+ // Si déjà fetch et non disponible, ne pas réessayer
174
+ if (hasFetchedUserModels.current && !userModelsAvailable.current) {
175
+ console.log("[AiProvider] User models API not available (404), skipping");
176
+ return;
177
+ }
178
+
179
+ isFetchingUserModels.current = true;
180
+ setLoadingUserModels(true);
181
+
182
+ try {
183
+ const models = await getUserModels({ baseUrl, apiKey: apiKeyId });
184
+ setUserModels(models);
185
+ hasFetchedUserModels.current = true;
186
+ } catch (error) {
187
+ console.error("[AiProvider] Error fetching user models:", error);
188
+ // En cas d'erreur 404, ne pas logger comme erreur critique
189
+ if (error instanceof Error && error.message.includes("404")) {
190
+ console.warn("[AiProvider] User models API not available (404)");
191
+ userModelsAvailable.current = false;
192
+ setUserModels([]);
193
+ }
194
+ hasFetchedUserModels.current = true;
195
+ } finally {
196
+ setLoadingUserModels(false);
197
+ isFetchingUserModels.current = false;
198
+ }
199
+ }, [baseUrl, apiKeyId]); // Retirer loadingUserModels des dépendances
200
+
201
+ // Récupérer les données au montage du provider
202
+ useEffect(() => {
203
+ fetchProviders(); // Fetch providers + available models en même temps
204
+ fetchUserModels();
205
+ }, [fetchProviders, fetchUserModels]);
206
+
207
+ // Helpers pour filtrer les modèles par type
208
+ const getModelsByType = useCallback(
209
+ (type: "text" | "language" | "image" | "embed") => {
210
+ const filtered = allModels.filter((model) => model.type === type);
211
+ console.log(`[AiProvider] getModelsByType(${type}):`, {
212
+ total: allModels.length,
213
+ filtered: filtered.length,
214
+ models: filtered.map((m) => ({ id: m.id, name: m.name, type: m.type })),
215
+ });
216
+ return filtered;
217
+ },
218
+ [allModels]
219
+ );
220
+
221
+ const getTextModels = useCallback(() => {
222
+ return allModels.filter(
223
+ (model) => model.type === "text" || model.type === "language"
224
+ );
225
+ }, [allModels]);
226
+
227
+ const getImageModels = useCallback(() => {
228
+ const imageModels = allModels.filter((model) => model.type === "image");
229
+ console.log("[AiProvider] getImageModels:", {
230
+ totalModels: allModels.length,
231
+ imageModels: imageModels.length,
232
+ allTypes: allModels.map((m) => ({ id: m.id, type: m.type })),
233
+ imageModelsList: imageModels.map((m) => ({ id: m.id, name: m.name })),
234
+ });
235
+ return imageModels;
236
+ }, [allModels]);
237
+
27
238
  const value: AiContextValue = {
28
239
  baseUrl,
29
240
  apiKeyId,
30
241
  uiMode,
242
+ providers,
243
+ allModels,
244
+ availableModels,
245
+ userModels,
246
+ loadingProviders,
247
+ loadingUserModels,
248
+ refetchProviders: fetchProviders,
249
+ refetchUserModels: fetchUserModels,
250
+ getModelsByType,
251
+ getTextModels,
252
+ getImageModels,
31
253
  };
32
254
 
33
255
  return <AiContext.Provider value={value}>{children}</AiContext.Provider>;