diff --git a/frontend/src/app/settings/helpers/model-helpers.tsx b/frontend/src/app/settings/helpers/model-helpers.tsx index 093b8862..829053fe 100644 --- a/frontend/src/app/settings/helpers/model-helpers.tsx +++ b/frontend/src/app/settings/helpers/model-helpers.tsx @@ -1,87 +1,97 @@ -import OpenAILogo from "@/components/logo/openai-logo"; -import OllamaLogo from "@/components/logo/ollama-logo"; import IBMLogo from "@/components/logo/ibm-logo"; +import OllamaLogo from "@/components/logo/ollama-logo"; +import OpenAILogo from "@/components/logo/openai-logo"; -export type ModelProvider = 'openai' | 'ollama' | 'ibm'; +export type ModelProvider = "openai" | "ollama" | "watsonx"; export interface ModelOption { - value: string; - label: string; + value: string; + label: string; } // Helper function to get model logo based on provider or model name export function getModelLogo(modelValue: string, provider?: ModelProvider) { - // First check by provider - if (provider === 'openai') { - return ; - } else if (provider === 'ollama') { - return ; - } else if (provider === 'ibm') { - return ; - } + // First check by provider + if (provider === "openai") { + return ; + } else if (provider === "ollama") { + return ; + } else if (provider === "watsonx") { + return ; + } - // Fallback to model name analysis - if (modelValue.includes('gpt') || modelValue.includes('text-embedding')) { - return ; - } else if (modelValue.includes('llama') || modelValue.includes('ollama')) { - return ; - } else if (modelValue.includes('granite') || modelValue.includes('slate') || modelValue.includes('ibm')) { - return ; - } + // Fallback to model name analysis + if (modelValue.includes("gpt") || modelValue.includes("text-embedding")) { + return ; + } else if (modelValue.includes("llama") || modelValue.includes("ollama")) { + return ; + } else if ( + modelValue.includes("granite") || + modelValue.includes("slate") || + modelValue.includes("ibm") + ) { + return ; + } - return ; // Default to OpenAI logo + return ; // Default to OpenAI logo } // Helper function to get fallback models by provider export function getFallbackModels(provider: ModelProvider) { - switch (provider) { - case 'openai': - return { - language: [ - { value: 'gpt-4', label: 'GPT-4' }, - { value: 'gpt-4-turbo', label: 'GPT-4 Turbo' }, - { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo' }, - ], - embedding: [ - { value: 'text-embedding-ada-002', label: 'text-embedding-ada-002' }, - { value: 'text-embedding-3-small', label: 'text-embedding-3-small' }, - { value: 'text-embedding-3-large', label: 'text-embedding-3-large' }, - ], - }; - case 'ollama': - return { - language: [ - { value: 'llama2', label: 'Llama 2' }, - { value: 'llama2:13b', label: 'Llama 2 13B' }, - { value: 'codellama', label: 'Code Llama' }, - ], - embedding: [ - { value: 'mxbai-embed-large', label: 'MxBai Embed Large' }, - { value: 'nomic-embed-text', label: 'Nomic Embed Text' }, - ], - }; - case 'ibm': - return { - language: [ - { value: 'meta-llama/llama-3-1-70b-instruct', label: 'Llama 3.1 70B Instruct' }, - { value: 'ibm/granite-13b-chat-v2', label: 'Granite 13B Chat v2' }, - ], - embedding: [ - { value: 'ibm/slate-125m-english-rtrvr', label: 'Slate 125M English Retriever' }, - ], - }; - default: - return { - language: [ - { value: 'gpt-4', label: 'GPT-4' }, - { value: 'gpt-4-turbo', label: 'GPT-4 Turbo' }, - { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo' }, - ], - embedding: [ - { value: 'text-embedding-ada-002', label: 'text-embedding-ada-002' }, - { value: 'text-embedding-3-small', label: 'text-embedding-3-small' }, - { value: 'text-embedding-3-large', label: 'text-embedding-3-large' }, - ], - }; - } -} \ No newline at end of file + switch (provider) { + case "openai": + return { + language: [ + { value: "gpt-4", label: "GPT-4" }, + { value: "gpt-4-turbo", label: "GPT-4 Turbo" }, + { value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" }, + ], + embedding: [ + { value: "text-embedding-ada-002", label: "text-embedding-ada-002" }, + { value: "text-embedding-3-small", label: "text-embedding-3-small" }, + { value: "text-embedding-3-large", label: "text-embedding-3-large" }, + ], + }; + case "ollama": + return { + language: [ + { value: "llama2", label: "Llama 2" }, + { value: "llama2:13b", label: "Llama 2 13B" }, + { value: "codellama", label: "Code Llama" }, + ], + embedding: [ + { value: "mxbai-embed-large", label: "MxBai Embed Large" }, + { value: "nomic-embed-text", label: "Nomic Embed Text" }, + ], + }; + case "watsonx": + return { + language: [ + { + value: "meta-llama/llama-3-1-70b-instruct", + label: "Llama 3.1 70B Instruct", + }, + { value: "ibm/granite-13b-chat-v2", label: "Granite 13B Chat v2" }, + ], + embedding: [ + { + value: "ibm/slate-125m-english-rtrvr", + label: "Slate 125M English Retriever", + }, + ], + }; + default: + return { + language: [ + { value: "gpt-4", label: "GPT-4" }, + { value: "gpt-4-turbo", label: "GPT-4 Turbo" }, + { value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" }, + ], + embedding: [ + { value: "text-embedding-ada-002", label: "text-embedding-ada-002" }, + { value: "text-embedding-3-small", label: "text-embedding-3-small" }, + { value: "text-embedding-3-large", label: "text-embedding-3-large" }, + ], + }; + } +} diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index a63d91d3..f39ea221 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -1,1152 +1,1157 @@ "use client"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@radix-ui/react-tooltip"; import { ArrowUpRight, Loader2, PlugZap, RefreshCw } from "lucide-react"; import { 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 { 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 { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { - Select, - SelectContent, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { useAuth } from "@/contexts/auth-context"; import { useTask } from "@/contexts/task-context"; +import { + DEFAULT_AGENT_SETTINGS, + DEFAULT_KNOWLEDGE_SETTINGS, + UI_CONSTANTS, +} from "@/lib/constants"; import { useDebounce } from "@/lib/debounce"; -import { DEFAULT_AGENT_SETTINGS, DEFAULT_KNOWLEDGE_SETTINGS, UI_CONSTANTS } from "@/lib/constants"; import { getFallbackModels, type ModelProvider } from "./helpers/model-helpers"; import { ModelSelectItems } from "./helpers/model-select-item"; -import { LabelWrapper } from "@/components/label-wrapper"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@radix-ui/react-tooltip"; 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 { isAuthenticated, isNoAuthMode } = useAuth(); + const { addTask, tasks } = useTask(); + const searchParams = useSearchParams(); - // 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); + // 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 [processingMode, setProcessingMode] = useState("standard"); + // 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 [processingMode, setProcessingMode] = useState("standard"); - // Fetch settings using React Query - const { data: settings = {} } = useGetSettingsQuery({ - enabled: isAuthenticated || isNoAuthMode, - }); + // 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; + // 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", - } - ); + // 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: 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", - } - ); + const { data: ibmModelsData } = useGetIBMModelsQuery( + undefined, // No params for now, could be extended later + { + enabled: + (isAuthenticated || isNoAuthMode) && currentProvider === "watsonx", + }, + ); - // Select the appropriate models data based on provider - const modelsData = - currentProvider === "openai" - ? openaiModelsData - : currentProvider === "ollama" - ? ollamaModelsData - : currentProvider === "ibm" - ? ibmModelsData - : openaiModelsData; // fallback to openai + // Select the appropriate models data based on provider + const modelsData = + currentProvider === "openai" + ? openaiModelsData + : currentProvider === "ollama" + ? ollamaModelsData + : currentProvider === "watsonx" + ? 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); - }, - }); + // 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 - ); + // 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 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]); + // 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]); + useEffect(() => { + if (settings.knowledge?.chunk_overlap) { + setChunkOverlap(settings.knowledge.chunk_overlap); + } + }, [settings.knowledge?.chunk_overlap]); - // Sync processing mode with settings data - useEffect(() => { - if (settings.knowledge?.doclingPresets) { - setProcessingMode(settings.knowledge.doclingPresets); - } - }, [settings.knowledge?.doclingPresets]); + // Sync processing mode with settings data + useEffect(() => { + if (settings.knowledge?.doclingPresets) { + setProcessingMode(settings.knowledge.doclingPresets); + } + }, [settings.knowledge?.doclingPresets]); - // Update model selection immediately - const handleModelChange = (newModel: string) => { - updateFlowSettingMutation.mutate({ llm_model: newModel }); - }; + // 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 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 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 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 chunk overlap setting with debounce + const handleChunkOverlapChange = (value: string) => { + const numValue = Math.max(0, parseInt(value) || 0); + setChunkOverlap(numValue); + debouncedUpdate({ chunk_overlap: numValue }); + }; - // Update processing mode - const handleProcessingModeChange = (mode: string) => { - setProcessingMode(mode); - // Update the configuration setting (backend will also update the flow automatically) - debouncedUpdate({ doclingPresets: mode }); - }; + // Update processing mode + const handleProcessingModeChange = (mode: string) => { + setProcessingMode(mode); + // Update the configuration setting (backend will also update the flow automatically) + debouncedUpdate({ doclingPresets: mode }); + }; - // 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] || ( -
- ? -
- ) - ); - }, []); + // 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"); - } + // 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); + 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, - })); + // 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); + setConnectors(initialConnectors); - // Check status for each connector type + // 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; + 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]); + 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 })); + 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`; + 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, - }), - }); + 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 (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); + 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}`; + 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); - } - }; + 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; + const handleSync = async (connector: Connector) => { + if (!connector.connectionId) return; - setIsSyncing(connector.id); - setSyncResults((prev) => ({ ...prev, [connector.id]: null })); + 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, - }; + 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 + // 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 response = await fetch(`/api/connectors/${connector.type}/sync`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(syncBody), + }); - const result = await response.json(); + 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); - } - }; + 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 getStatusBadge = (status: Connector["status"]) => { + switch (status) { + case "connected": + return ( + + Connected + + ); + case "connecting": + return ( + + Connecting... + + ); + case "error": + return Error; + default: + return ( + + Not Connected + + ); + } + }; - // Check connector status on mount and when returning from OAuth - useEffect(() => { - if (isAuthenticated) { - checkConnectorStatuses(); - } + // 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]); + 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([]); + // 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; - }); + // 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); + 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); + // Update previous tasks state + setPrevTasks(tasks); - return () => clearTimeout(timeoutId); - } else { - // Always update previous tasks state - setPrevTasks(tasks); - } - }, [tasks, prevTasks]); + 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 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 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; + const url = editUrl || computed; - window.open(url, "_blank"); - closeDialog(); // Close immediately after opening Langflow - }; + 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 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); - setProcessingMode(DEFAULT_KNOWLEDGE_SETTINGS.processing_mode); - closeDialog(); // Close after successful completion - }) - .catch((error) => { - console.error("Error restoring ingest 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); + setProcessingMode(DEFAULT_KNOWLEDGE_SETTINGS.processing_mode); + 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 -

-
+ 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=
-
-
-
- ) : ( -
-
-

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" - } - /> -
-
-
- )} + {/* 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=
+
+
+
+ ) : ( +
+
+

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" ? ( -
- + {/* 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" - /> -
-
-
- -
-
- - - -
-
- -