From 4225bbeb0d0ca50d5e371eb6d7057b64782c7e36 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Thu, 2 Oct 2025 14:09:46 -0300 Subject: [PATCH] Changed settings page to use model selector --- frontend/src/app/settings/page.tsx | 2202 ++++++++++++++-------------- 1 file changed, 1096 insertions(+), 1106 deletions(-) diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 7530bcbb..a2cf3428 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -1,1159 +1,1149 @@ "use client"; -import { ArrowUpRight, Loader2, PlugZap, Plus, RefreshCw } from "lucide-react"; +import { ArrowUpRight, Loader2, PlugZap, Plus } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import { Suspense, useCallback, useEffect, useState } from "react"; import { useUpdateFlowSettingMutation } from "@/app/api/mutations/useUpdateFlowSettingMutation"; import { - useGetIBMModelsQuery, - useGetOllamaModelsQuery, - useGetOpenAIModelsQuery, + useGetIBMModelsQuery, + useGetOllamaModelsQuery, + useGetOpenAIModelsQuery, } from "@/app/api/queries/useGetModelsQuery"; import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery"; import { ConfirmationDialog } from "@/components/confirmation-dialog"; +import { LabelWrapper } from "@/components/label-wrapper"; +import OpenAILogo from "@/components/logo/openai-logo"; import { ProtectedRoute } from "@/components/protected-route"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Switch } from "@/components/ui/switch"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { - Select, - SelectContent, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { useAuth } from "@/contexts/auth-context"; import { useTask } from "@/contexts/task-context"; -import { useDebounce } from "@/lib/debounce"; import { - DEFAULT_AGENT_SETTINGS, - DEFAULT_KNOWLEDGE_SETTINGS, - UI_CONSTANTS, + DEFAULT_AGENT_SETTINGS, + DEFAULT_KNOWLEDGE_SETTINGS, + UI_CONSTANTS, } from "@/lib/constants"; +import { useDebounce } from "@/lib/debounce"; +import { ModelSelector } from "../onboarding/components/model-selector"; import { getFallbackModels, type ModelProvider } from "./helpers/model-helpers"; import { ModelSelectItems } from "./helpers/model-select-item"; -import { LabelWrapper } from "@/components/label-wrapper"; const { MAX_SYSTEM_PROMPT_CHARS } = UI_CONSTANTS; interface GoogleDriveFile { - id: string; - name: string; - mimeType: string; - webViewLink?: string; - iconLink?: string; + id: string; + name: string; + mimeType: string; + webViewLink?: string; + iconLink?: string; } interface OneDriveFile { - id: string; - name: string; - mimeType?: string; - webUrl?: string; - driveItem?: { - file?: { mimeType: string }; - folder?: unknown; - }; + id: string; + name: string; + mimeType?: string; + webUrl?: string; + driveItem?: { + file?: { mimeType: string }; + folder?: unknown; + }; } interface Connector { - id: string; - name: string; - description: string; - icon: React.ReactNode; - status: "not_connected" | "connecting" | "connected" | "error"; - type: string; - connectionId?: string; - access_token?: string; - selectedFiles?: GoogleDriveFile[] | OneDriveFile[]; + id: string; + name: string; + description: string; + icon: React.ReactNode; + status: "not_connected" | "connecting" | "connected" | "error"; + type: string; + connectionId?: string; + access_token?: string; + selectedFiles?: GoogleDriveFile[] | OneDriveFile[]; } interface SyncResult { - processed?: number; - added?: number; - errors?: number; - skipped?: number; - total?: number; + processed?: number; + added?: number; + errors?: number; + skipped?: number; + total?: number; } interface Connection { - connection_id: string; - is_active: boolean; - created_at: string; - last_sync?: string; + connection_id: string; + is_active: boolean; + created_at: string; + last_sync?: string; } function KnowledgeSourcesPage() { - const { isAuthenticated, isNoAuthMode } = useAuth(); - const { addTask, tasks } = useTask(); - const searchParams = useSearchParams(); - const router = useRouter(); - - // Connectors state - const [connectors, setConnectors] = useState([]); - const [isConnecting, setIsConnecting] = useState(null); - const [isSyncing, setIsSyncing] = useState(null); - const [syncResults, setSyncResults] = useState<{ - [key: string]: SyncResult | null; - }>({}); - const [maxFiles, setMaxFiles] = useState(10); - const [syncAllFiles, setSyncAllFiles] = useState(false); - - // Only keep systemPrompt state since it needs manual save button - const [systemPrompt, setSystemPrompt] = useState(""); - const [chunkSize, setChunkSize] = useState(1024); - const [chunkOverlap, setChunkOverlap] = useState(50); - const [tableStructure, setTableStructure] = useState(false); - const [ocr, setOcr] = useState(false); - const [pictureDescriptions, setPictureDescriptions] = useState(false); - - // Fetch settings using React Query - const { data: settings = {} } = useGetSettingsQuery({ - enabled: isAuthenticated || isNoAuthMode, - }); - - // Get the current provider from settings - const currentProvider = (settings.provider?.model_provider || - "openai") as ModelProvider; - - // Fetch available models based on provider - const { data: openaiModelsData } = useGetOpenAIModelsQuery( - undefined, // Let backend use stored API key from configuration - { - enabled: - (isAuthenticated || isNoAuthMode) && currentProvider === "openai", - } - ); - - const { data: ollamaModelsData } = useGetOllamaModelsQuery( - undefined, // No params for now, could be extended later - { - enabled: - (isAuthenticated || isNoAuthMode) && currentProvider === "ollama", - } - ); - - const { data: ibmModelsData } = useGetIBMModelsQuery( - undefined, // No params for now, could be extended later - { - enabled: (isAuthenticated || isNoAuthMode) && currentProvider === "ibm", - } - ); - - // Select the appropriate models data based on provider - const modelsData = - currentProvider === "openai" - ? openaiModelsData - : currentProvider === "ollama" - ? ollamaModelsData - : currentProvider === "ibm" - ? ibmModelsData - : openaiModelsData; // fallback to openai - - // Mutations - const updateFlowSettingMutation = useUpdateFlowSettingMutation({ - onSuccess: () => { - console.log("Setting updated successfully"); - }, - onError: error => { - console.error("Failed to update setting:", error.message); - }, - }); - - // Debounced update function - const debouncedUpdate = useDebounce( - (variables: Parameters[0]) => { - updateFlowSettingMutation.mutate(variables); - }, - 500 - ); - - // Sync system prompt state with settings data - useEffect(() => { - if (settings.agent?.system_prompt) { - setSystemPrompt(settings.agent.system_prompt); - } - }, [settings.agent?.system_prompt]); - - // Sync chunk size and overlap state with settings data - useEffect(() => { - if (settings.knowledge?.chunk_size) { - setChunkSize(settings.knowledge.chunk_size); - } - }, [settings.knowledge?.chunk_size]); - - useEffect(() => { - if (settings.knowledge?.chunk_overlap) { - setChunkOverlap(settings.knowledge.chunk_overlap); - } - }, [settings.knowledge?.chunk_overlap]); - - // Sync docling settings with settings data - useEffect(() => { - if (settings.knowledge?.table_structure !== undefined) { - setTableStructure(settings.knowledge.table_structure); - } - }, [settings.knowledge?.table_structure]); - - useEffect(() => { - if (settings.knowledge?.ocr !== undefined) { - setOcr(settings.knowledge.ocr); - } - }, [settings.knowledge?.ocr]); - - useEffect(() => { - if (settings.knowledge?.picture_descriptions !== undefined) { - setPictureDescriptions(settings.knowledge.picture_descriptions); - } - }, [settings.knowledge?.picture_descriptions]); - - // Update model selection immediately - const handleModelChange = (newModel: string) => { - updateFlowSettingMutation.mutate({ llm_model: newModel }); - }; - - // Update system prompt with save button - const handleSystemPromptSave = () => { - updateFlowSettingMutation.mutate({ system_prompt: systemPrompt }); - }; - - // Update embedding model selection immediately - const handleEmbeddingModelChange = (newModel: string) => { - updateFlowSettingMutation.mutate({ embedding_model: newModel }); - }; - - // Update chunk size setting with debounce - const handleChunkSizeChange = (value: string) => { - const numValue = Math.max(0, parseInt(value) || 0); - setChunkSize(numValue); - debouncedUpdate({ chunk_size: numValue }); - }; - - // Update chunk overlap setting with debounce - const handleChunkOverlapChange = (value: string) => { - const numValue = Math.max(0, parseInt(value) || 0); - setChunkOverlap(numValue); - debouncedUpdate({ chunk_overlap: numValue }); - }; - - // Update docling settings - const handleTableStructureChange = (checked: boolean) => { - setTableStructure(checked); - updateFlowSettingMutation.mutate({ table_structure: checked }); - }; - - const handleOcrChange = (checked: boolean) => { - setOcr(checked); - updateFlowSettingMutation.mutate({ ocr: checked }); - }; - - const handlePictureDescriptionsChange = (checked: boolean) => { - setPictureDescriptions(checked); - updateFlowSettingMutation.mutate({ picture_descriptions: checked }); - }; - - // Helper function to get connector icon - const getConnectorIcon = useCallback((iconName: string) => { - const iconMap: { [key: string]: React.ReactElement } = { - "google-drive": ( -
- G -
- ), - sharepoint: ( -
- SP -
- ), - onedrive: ( -
- OD -
- ), - }; - return ( - iconMap[iconName] || ( -
- ? -
- ) - ); - }, []); - - // Connector functions - const checkConnectorStatuses = useCallback(async () => { - try { - // Fetch available connectors from backend - const connectorsResponse = await fetch("/api/connectors"); - if (!connectorsResponse.ok) { - throw new Error("Failed to load connectors"); - } - - const connectorsResult = await connectorsResponse.json(); - const connectorTypes = Object.keys(connectorsResult.connectors); - - // Initialize connectors list with metadata from backend - const initialConnectors = connectorTypes - .filter(type => connectorsResult.connectors[type].available) // Only show available connectors - .map(type => ({ - id: type, - name: connectorsResult.connectors[type].name, - description: connectorsResult.connectors[type].description, - icon: getConnectorIcon(connectorsResult.connectors[type].icon), - status: "not_connected" as const, - type: type, - })); - - setConnectors(initialConnectors); - - // Check status for each connector type - - for (const connectorType of connectorTypes) { - const response = await fetch(`/api/connectors/${connectorType}/status`); - if (response.ok) { - const data = await response.json(); - const connections = data.connections || []; - const activeConnection = connections.find( - (conn: Connection) => conn.is_active - ); - const isConnected = activeConnection !== undefined; - - setConnectors(prev => - prev.map(c => - c.type === connectorType - ? { - ...c, - status: isConnected ? "connected" : "not_connected", - connectionId: activeConnection?.connection_id, - } - : c - ) - ); - } - } - } catch (error) { - console.error("Failed to check connector statuses:", error); - } - }, [getConnectorIcon]); - - const handleConnect = async (connector: Connector) => { - setIsConnecting(connector.id); - setSyncResults(prev => ({ ...prev, [connector.id]: null })); - - try { - // Use the shared auth callback URL, same as connectors page - const redirectUri = `${window.location.origin}/auth/callback`; - - const response = await fetch("/api/auth/init", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - connector_type: connector.type, - purpose: "data_source", - name: `${connector.name} Connection`, - redirect_uri: redirectUri, - }), - }); - - if (response.ok) { - const result = await response.json(); - - if (result.oauth_config) { - localStorage.setItem("connecting_connector_id", result.connection_id); - localStorage.setItem("connecting_connector_type", connector.type); - - const authUrl = - `${result.oauth_config.authorization_endpoint}?` + - `client_id=${result.oauth_config.client_id}&` + - `response_type=code&` + - `scope=${result.oauth_config.scopes.join(" ")}&` + - `redirect_uri=${encodeURIComponent( - result.oauth_config.redirect_uri - )}&` + - `access_type=offline&` + - `prompt=consent&` + - `state=${result.connection_id}`; - - window.location.href = authUrl; - } - } else { - console.error("Failed to initiate connection"); - setIsConnecting(null); - } - } catch (error) { - console.error("Connection error:", error); - setIsConnecting(null); - } - }; - - // const handleSync = async (connector: Connector) => { - // if (!connector.connectionId) return; - - // setIsSyncing(connector.id); - // setSyncResults(prev => ({ ...prev, [connector.id]: null })); - - // try { - // const syncBody: { - // connection_id: string; - // max_files?: number; - // selected_files?: string[]; - // } = { - // connection_id: connector.connectionId, - // max_files: syncAllFiles ? 0 : maxFiles || undefined, - // }; - - // // Note: File selection is now handled via the cloud connectors dialog - - // const response = await fetch(`/api/connectors/${connector.type}/sync`, { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // }, - // body: JSON.stringify(syncBody), - // }); - - // const result = await response.json(); - - // if (response.status === 201) { - // const taskId = result.task_id; - // if (taskId) { - // addTask(taskId); - // setSyncResults(prev => ({ - // ...prev, - // [connector.id]: { - // processed: 0, - // total: result.total_files || 0, - // }, - // })); - // } - // } else if (response.ok) { - // setSyncResults(prev => ({ ...prev, [connector.id]: result })); - // // Note: Stats will auto-refresh via task completion watcher for async syncs - // } else { - // console.error("Sync failed:", result.error); - // } - // } catch (error) { - // console.error("Sync error:", error); - // } finally { - // setIsSyncing(null); - // } - // }; - - const getStatusBadge = (status: Connector["status"]) => { - switch (status) { - case "connected": - return ( - - Connected - - ); - case "connecting": - return ( - - Connecting... - - ); - case "error": - return Error; - default: - return ( - - Not Connected - - ); - } - }; - - const navigateToKnowledgePage = (connector: Connector) => { - const provider = connector.type.replace(/-/g, "_"); - router.push(`/upload/${provider}`); - }; - - // Check connector status on mount and when returning from OAuth - useEffect(() => { - if (isAuthenticated) { - checkConnectorStatuses(); - } - - if (searchParams.get("oauth_success") === "true") { - const url = new URL(window.location.href); - url.searchParams.delete("oauth_success"); - window.history.replaceState({}, "", url.toString()); - } - }, [searchParams, isAuthenticated, checkConnectorStatuses]); - - // Track previous tasks to detect new completions - const [prevTasks, setPrevTasks] = useState([]); - - // Watch for task completions and refresh stats - useEffect(() => { - // Find newly completed tasks by comparing with previous state - const newlyCompletedTasks = tasks.filter(task => { - const wasCompleted = - prevTasks.find(prev => prev.task_id === task.task_id)?.status === - "completed"; - return task.status === "completed" && !wasCompleted; - }); - - if (newlyCompletedTasks.length > 0) { - // Task completed - could refresh data here if needed - const timeoutId = setTimeout(() => { - // Stats refresh removed - }, 1000); - - // Update previous tasks state - setPrevTasks(tasks); - - return () => clearTimeout(timeoutId); - } else { - // Always update previous tasks state - setPrevTasks(tasks); - } - }, [tasks, prevTasks]); - - const handleEditInLangflow = ( - flowType: "chat" | "ingest", - closeDialog: () => void - ) => { - // Select the appropriate flow ID and edit URL based on flow type - const targetFlowId = - flowType === "ingest" ? settings.ingest_flow_id : settings.flow_id; - const editUrl = - flowType === "ingest" - ? settings.langflow_ingest_edit_url - : settings.langflow_edit_url; - - const derivedFromWindow = - typeof window !== "undefined" - ? `${window.location.protocol}//${window.location.hostname}:7860` - : ""; - const base = ( - settings.langflow_public_url || - derivedFromWindow || - "http://localhost:7860" - ).replace(/\/$/, ""); - const computed = targetFlowId ? `${base}/flow/${targetFlowId}` : base; - - const url = editUrl || computed; - - window.open(url, "_blank"); - closeDialog(); // Close immediately after opening Langflow - }; - - const handleRestoreRetrievalFlow = (closeDialog: () => void) => { - fetch(`/api/reset-flow/retrieval`, { - method: "POST", - }) - .then(response => { - if (response.ok) { - return response.json(); - } - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - }) - .then(() => { - // Only reset form values if the API call was successful - setSystemPrompt(DEFAULT_AGENT_SETTINGS.system_prompt); - // Trigger model update to default model - handleModelChange(DEFAULT_AGENT_SETTINGS.llm_model); - closeDialog(); // Close after successful completion - }) - .catch(error => { - console.error("Error restoring retrieval flow:", error); - closeDialog(); // Close even on error (could show error toast instead) - }); - }; - - const handleRestoreIngestFlow = (closeDialog: () => void) => { - fetch(`/api/reset-flow/ingest`, { - method: "POST", - }) - .then(response => { - if (response.ok) { - return response.json(); - } - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - }) - .then(() => { - // Only reset form values if the API call was successful - setChunkSize(DEFAULT_KNOWLEDGE_SETTINGS.chunk_size); - setChunkOverlap(DEFAULT_KNOWLEDGE_SETTINGS.chunk_overlap); - setTableStructure(false); - setOcr(false); - setPictureDescriptions(false); - closeDialog(); // Close after successful completion - }) - .catch(error => { - console.error("Error restoring ingest flow:", error); - closeDialog(); // Close even on error (could show error toast instead) - }); - }; - - return ( -
- {/* Connectors Section */} -
-
-

- Cloud Connectors -

-
- - {/* Conditional Sync Settings or No-Auth Message */} - { - isNoAuthMode ? ( - - - - Cloud connectors are only available with auth mode enabled - - - Please provide the following environment variables and - restart: - - - -
-
- # make here - https://console.cloud.google.com/apis/credentials -
-
GOOGLE_OAUTH_CLIENT_ID=
-
GOOGLE_OAUTH_CLIENT_SECRET=
-
-
-
- ) : null - //
- //
- //

Sync Settings

- //

- // Configure how many files to sync when manually triggering a sync - //

- //
- //
- //
- // { - // setSyncAllFiles(!!checked); - // if (checked) { - // setMaxFiles(0); - // } else { - // setMaxFiles(10); - // } - // }} - // /> - // - //
- // - //
- // setMaxFiles(parseInt(e.target.value) || 10)} - // disabled={syncAllFiles} - // className="w-16 min-w-16 max-w-16 flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed" - // min="1" - // max="100" - // title={ - // syncAllFiles - // ? "Disabled when 'Sync all files' is checked" - // : "Leave blank or set to 0 for unlimited" - // } - // /> - //
- //
- //
- } - - {/* Connectors Grid */} -
- {connectors.map(connector => ( - - -
-
- {connector.icon} -
- - {connector.name} - - - {connector.description} - -
-
- {getStatusBadge(connector.status)} -
-
- - {connector.status === "connected" ? ( -
- - - {syncResults[connector.id] && ( -
-
- Processed: {syncResults[connector.id]?.processed || 0} -
-
- Added: {syncResults[connector.id]?.added || 0} -
- {syncResults[connector.id]?.errors && ( -
Errors: {syncResults[connector.id]?.errors}
- )} -
- )} -
- ) : ( - - )} -
-
- ))} -
-
- {/* Agent Behavior Section */} - - -
-
- Agent - - Quick Agent settings. Edit in Langflow for full control. - -
-
- - Restore flow - - } - title="Restore default Retrieval flow" - description="This restores defaults and discards all custom settings and overrides. This can’t be undone." - confirmText="Restore" - variant="destructive" - onConfirm={handleRestoreRetrievalFlow} - /> - - - Langflow icon - - - - - Edit in Langflow - - } - title="Edit Retrieval flow in Langflow" - description={ - <> -

- You're entering Langflow. You can edit the{" "} - Retrieval flow and other underlying flows. Manual - changes to components, wiring, or I/O can break this - experience. -

-

You can restore this flow from Settings.

- - } - confirmText="Proceed" - confirmIcon={} - onConfirm={closeDialog => - handleEditInLangflow("chat", closeDialog) - } - variant="warning" - /> -
-
-
- -
-
- - - -
-
- -