)
);
};
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
}
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
}
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
}
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
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"
}
/>