"use client"; import { Loader2, PlugZap, RefreshCw } from "lucide-react"; import { useSearchParams } from "next/navigation"; import { Suspense, useCallback, useEffect, useState } from "react"; import { ConfirmationDialog } from "@/components/confirmation-dialog"; 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 { useAuth } from "@/contexts/auth-context"; import { useTask } from "@/contexts/task-context"; 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; } // Helper function to get connector icon (moved outside component to avoid re-renders) const getConnectorIcon = (iconName: string) => { const iconMap: { [key: string]: React.ReactElement } = { "google-drive": (
G
), sharepoint: (
SP
), onedrive: (
OD
), }; return ( iconMap[iconName] || (
?
) ); }; 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); // Settings state // Note: backend internal Langflow URL is not needed on the frontend const [chatFlowId, setChatFlowId] = useState( "1098eea1-6649-4e1d-aed1-b77249fb8dd0", ); const [ingestFlowId, setIngestFlowId] = useState( "5488df7c-b93f-4f87-a446-b67028bc0813", ); const [nudgesFlowId, setNudgesFlowId] = useState( "ebc01d31-1976-46ce-a385-b0240327226c", ); const [langflowEditUrl, setLangflowEditUrl] = useState(""); const [langflowIngestEditUrl, setLangflowIngestEditUrl] = useState(""); const [langflowNudgesEditUrl, setLangflowNudgesEditUrl] = useState(""); const [publicLangflowUrl, setPublicLangflowUrl] = useState(""); // Fetch settings from backend const fetchSettings = useCallback(async () => { try { const response = await fetch("/api/settings"); if (response.ok) { const settings = await response.json(); if (settings.flow_id) { setChatFlowId(settings.flow_id); } if (settings.ingest_flow_id) { setIngestFlowId(settings.ingest_flow_id); } if (settings.langflow_nudges_flow_id) { setNudgesFlowId(settings.langflow_nudges_flow_id); } if (settings.langflow_edit_url) { setLangflowEditUrl(settings.langflow_edit_url); } if (settings.langflow_ingest_edit_url) { setLangflowIngestEditUrl(settings.langflow_ingest_edit_url); } if (settings.langflow_nudges_edit_url) { setLangflowNudgesEditUrl(settings.langflow_nudges_edit_url); } if (settings.langflow_public_url) { setPublicLangflowUrl(settings.langflow_public_url); } } } catch (error) { console.error("Failed to fetch settings:", error); } }, []); // 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); } }, []); 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 ); } }; // Fetch settings on mount when authenticated useEffect(() => { if (isAuthenticated) { fetchSettings(); } }, [isAuthenticated, fetchSettings]); // 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" | "nudges", closeDialog: () => void, ) => { // Select the appropriate flow ID and edit URL based on flow type let targetFlowId: string; let editUrl: string; if (flowType === "ingest") { targetFlowId = ingestFlowId; editUrl = langflowIngestEditUrl; } else if (flowType === "nudges") { targetFlowId = nudgesFlowId; editUrl = langflowNudgesEditUrl; } else { targetFlowId = chatFlowId; editUrl = langflowEditUrl; } const derivedFromWindow = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:7860` : ""; const base = ( publicLangflowUrl || 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) }); }; const handleRestoreNudgesFlow = (closeDialog: () => void) => { fetch(`/api/reset-flow/nudges`, { method: "POST", }) .then((response) => response.json()) .then(() => { closeDialog(); // Close after successful completion }) .catch((error) => { console.error("Error restoring nudges flow:", error); closeDialog(); // Close even on error (could show error toast instead) }); }; return (
{/* Knowledge Ingest Section */}
Knowledge Ingest Quick ingest options. Edit in Langflow for full control.
Restore flow} title="Restore default Ingest flow" description="This restores defaults and discards all custom settings and overrides. This can't be undone." confirmText="Restore" variant="destructive" onConfirm={handleRestoreIngestFlow} /> Edit in Langflow Edit in Langflow } title="Edit Ingest flow in Langflow" description="You're entering Langflow. You can edit the Ingest flow and other underlying flows. Manual changes to components, wiring, or I/O can break this experience." confirmText="Proceed" onConfirm={(closeDialog) => handleEditInLangflow("ingest", closeDialog) } />
{/* Hidden for now */} {/*
Extracts text from images/PDFs. Ingest is slower when enabled.
setOcrEnabled(checked)} />
Adds captions for images. Ingest is more expensive when enabled.
setPictureDescriptionsEnabled(checked) } />
*/}
{/* Agent Behavior Section */}
Agent behavior Adjust your retrieval agent flow
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} /> Edit in Langflow 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) } />
{/* Nudges Section */}
Nudges Manage prompt suggestions that appear in the chat interface
Restore flow} title="Restore default Nudges flow" description="This restores defaults and discards all custom settings and overrides. This can't be undone." confirmText="Restore" variant="destructive" onConfirm={handleRestoreNudgesFlow} /> Edit in Langflow Edit in Langflow } title="Edit Nudges flow in Langflow" description="You're entering Langflow. You can edit the Nudges flow and other underlying flows. Manual changes to components, wiring, or I/O can break this experience." confirmText="Proceed" onConfirm={(closeDialog) => handleEditInLangflow("nudges", closeDialog) } />
{/* 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" } />
)} {/* 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}
)}
)}
) : ( )}
))}
); } export default function ProtectedKnowledgeSourcesPage() { return ( Loading knowledge sources...}> ); }