"use client"; import { 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 { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery"; import { useGetOpenAIModelsQuery, useGetOllamaModelsQuery, useGetIBMModelsQuery } from "@/app/api/queries/useGetModelsQuery"; import { ConfirmationDialog } from "@/components/confirmation-dialog"; import { ModelSelectItems } from "./helpers/model-select-item"; import { getFallbackModels, type ModelProvider } from "./helpers/model-helpers"; import { ProtectedRoute } from "@/components/protected-route"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { 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 { 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 { useDebounce } from "@/lib/debounce"; const MAX_SYSTEM_PROMPT_CHARS = 2000; interface GoogleDriveFile { 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; }; } 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[]; } interface SyncResult { processed?: number; added?: number; errors?: number; skipped?: number; total?: number; } interface Connection { 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(); // 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); // Fetch settings using React Query const { data: settings = {} } = useGetSettingsQuery({ enabled: isAuthenticated, }); // 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({ enabled: isAuthenticated && currentProvider === 'openai', }); const { data: ollamaModelsData } = useGetOllamaModelsQuery( undefined, // No params for now, could be extended later { enabled: isAuthenticated && currentProvider === 'ollama', } ); const { data: ibmModelsData } = useGetIBMModelsQuery( undefined, // No params for now, could be extended later { enabled: isAuthenticated && 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]); // 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 }); }; // 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 ); } }; // 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) => response.json()) .then(() => { 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) => response.json()) .then(() => { 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 (
{/* Agent Behavior Section */}
Agent Quick Agent settings. Edit in Langflow for full control.
Restore flow} title="Restore default Agent 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 Agent flow in Langflow" description="You're entering Langflow. You can edit the Agent flow and other underlying flows. Manual changes to components, wiring, or I/O can break this experience." confirmText="Proceed" onConfirm={(closeDialog) => handleEditInLangflow("chat", closeDialog) } />