"use client"; import { useState, useEffect, useCallback, Suspense } from "react"; import { useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { Loader2, PlugZap, RefreshCw } from "lucide-react"; import { ProtectedRoute } from "@/components/protected-route"; import { useTask } from "@/contexts/task-context"; import { useAuth } from "@/contexts/auth-context"; interface Connector { id: string; name: string; description: string; icon: React.ReactNode; status: "not_connected" | "connecting" | "connected" | "error"; type: string; connectionId?: string; access_token?: string; } 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); // Settings state // Note: backend internal Langflow URL is not needed on the frontend const [flowId, setFlowId] = useState( "1098eea1-6649-4e1d-aed1-b77249fb8dd0", ); const [ingestFlowId, setIngestFlowId] = useState(""); const [langflowEditUrl, setLangflowEditUrl] = useState(""); const [langflowIngestEditUrl, setLangflowIngestEditUrl] = useState(""); const [publicLangflowUrl, setPublicLangflowUrl] = useState(""); // Ingestion settings state - will be populated from Langflow flow defaults const [ingestionSettings, setIngestionSettings] = useState({ chunkSize: 1000, chunkOverlap: 200, separator: "\\n", embeddingModel: "text-embedding-3-small", }); // Fetch settings from backend const fetchSettings = useCallback(async () => { try { const response = await fetch("/api/settings"); if (response.ok) { const settings = await response.json(); // Update all state cleanly if (settings.flow_id) setFlowId(settings.flow_id); if (settings.ingest_flow_id) setIngestFlowId(settings.ingest_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_public_url) setPublicLangflowUrl(settings.langflow_public_url); if (settings.ingestion_defaults) { console.log( "Loading ingestion defaults from backend:", settings.ingestion_defaults, ); setIngestionSettings(settings.ingestion_defaults); } } } catch (error) { console.error("Failed to fetch settings:", error); } }, []); // Helper function to get connector icon const getConnectorIcon = (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); } }, []); 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 response = await fetch(`/api/connectors/${connector.type}/sync`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ connection_id: connector.connectionId, max_files: syncAllFiles ? 0 : maxFiles || undefined, }), }); 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]); return (
{/* Agent Behavior Section */}

Agent behavior

Adjust your retrieval agent flow

{/* Ingest Flow Section */}

File ingestion

Customize your file processing and indexing flow

{/* Ingestion Settings Section */}

Ingestion settings

Configure how your documents are processed and indexed

Document Processing Control how text is split and processed
setIngestionSettings((prev) => ({ ...prev, chunkSize: parseInt(e.target.value) || 1000, })) } min="100" max="4000" />

Maximum characters per text chunk (100-4000)

setIngestionSettings((prev) => ({ ...prev, chunkOverlap: parseInt(e.target.value) || 200, })) } min="0" max="500" />

Character overlap between chunks (0-500)

Embeddings Configure embedding model and search behavior
{/* 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...}> ); }