"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 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?: any } } 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) // Settings state // Note: backend internal Langflow URL is not needed on the frontend const [flowId, setFlowId] = useState('1098eea1-6649-4e1d-aed1-b77249fb8dd0') const [langflowEditUrl, setLangflowEditUrl] = 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) { setFlowId(settings.flow_id) } if (settings.langflow_edit_url) { setLangflowEditUrl(settings.langflow_edit_url) } if (settings.langflow_public_url) { setPublicLangflowUrl(settings.langflow_public_url) } } } 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 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]) return (
{/* Agent Behavior Section */}

Agent behavior

Adjust your retrieval agent flow

{/* 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...}> ) }