From 8e267e152861c92601abadd3842561bbd6e40cee Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Thu, 18 Sep 2025 13:33:56 -0300 Subject: [PATCH] update settings to use react query --- frontend/lib/debounce.ts | 23 ++ .../mutations/useUpdateFlowSettingMutation.ts | 13 +- .../app/api/queries/useGetSettingsQuery.ts | 92 +++++++ frontend/src/app/settings/page.tsx | 245 ++++++++---------- 4 files changed, 230 insertions(+), 143 deletions(-) create mode 100644 frontend/lib/debounce.ts create mode 100644 frontend/src/app/api/queries/useGetSettingsQuery.ts diff --git a/frontend/lib/debounce.ts b/frontend/lib/debounce.ts new file mode 100644 index 00000000..9ff4c59a --- /dev/null +++ b/frontend/lib/debounce.ts @@ -0,0 +1,23 @@ +import { useCallback, useRef } from "react"; + +export function useDebounce void>( + callback: T, + delay: number, +): T { + const timeoutRef = useRef(null); + + const debouncedCallback = useCallback( + (...args: Parameters) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(() => { + callback(...args); + }, delay); + }, + [callback, delay], + ) as T; + + return debouncedCallback; +} diff --git a/frontend/src/app/api/mutations/useUpdateFlowSettingMutation.ts b/frontend/src/app/api/mutations/useUpdateFlowSettingMutation.ts index 80adff97..f789ebda 100644 --- a/frontend/src/app/api/mutations/useUpdateFlowSettingMutation.ts +++ b/frontend/src/app/api/mutations/useUpdateFlowSettingMutation.ts @@ -1,12 +1,13 @@ import { + type UseMutationOptions, useMutation, useQueryClient, - UseMutationOptions, } from "@tanstack/react-query"; interface UpdateFlowSettingVariables { llm_model?: string; system_prompt?: string; + embedding_model?: string; ocr?: boolean; picture_descriptions?: boolean; chunk_size?: number; @@ -19,7 +20,11 @@ interface UpdateFlowSettingResponse { export const useUpdateFlowSettingMutation = ( options?: Omit< - UseMutationOptions, + UseMutationOptions< + UpdateFlowSettingResponse, + Error, + UpdateFlowSettingVariables + >, "mutationFn" >, ) => { @@ -46,10 +51,10 @@ export const useUpdateFlowSettingMutation = ( return useMutation({ mutationFn: updateFlowSetting, - onSuccess: () => { + onSettled: () => { // Invalidate settings query to refetch updated data queryClient.invalidateQueries({ queryKey: ["settings"] }); }, ...options, }); -}; \ No newline at end of file +}; diff --git a/frontend/src/app/api/queries/useGetSettingsQuery.ts b/frontend/src/app/api/queries/useGetSettingsQuery.ts new file mode 100644 index 00000000..59ed0538 --- /dev/null +++ b/frontend/src/app/api/queries/useGetSettingsQuery.ts @@ -0,0 +1,92 @@ +import { + type UseQueryOptions, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; + +interface AgentSettings { + llm_model?: string; + system_prompt?: string; +} + +interface IngestSettings { + embedding_model?: string; + chunk_size?: number; + chunk_overlap?: number; +} + +interface Settings { + flow_id?: string; + ingest_flow_id?: string; + langflow_edit_url?: string; + langflow_ingest_edit_url?: string; + langflow_public_url?: string; + agent?: AgentSettings; + ingest?: IngestSettings; +} + +const DEFAULT_SETTINGS: Settings = { + flow_id: "1098eea1-6649-4e1d-aed1-b77249fb8dd0", + ingest_flow_id: "5488df7c-b93f-4f87-a446-b67028bc0813", + langflow_edit_url: "", + langflow_ingest_edit_url: "", + langflow_public_url: "", + agent: { + llm_model: "gpt-4", + system_prompt: "", + }, + ingest: { + embedding_model: "text-embedding-ada-002", + chunk_size: 1000, + chunk_overlap: 200, + }, +}; + +export const useGetSettingsQuery = ( + options?: Omit, "queryKey" | "queryFn">, +) => { + const queryClient = useQueryClient(); + + function cancel() { + queryClient.removeQueries({ queryKey: ["settings"] }); + } + + async function getSettings(): Promise { + try { + const response = await fetch("/api/settings"); + if (response.ok) { + const settings = await response.json(); + // Merge with defaults to ensure all properties exist + return { + ...DEFAULT_SETTINGS, + ...settings, + agent: { + ...DEFAULT_SETTINGS.agent, + ...settings.agent, + }, + ingest: { + ...DEFAULT_SETTINGS.ingest, + ...settings.ingest, + }, + }; + } else { + console.error("Failed to fetch settings"); + return DEFAULT_SETTINGS; + } + } catch (error) { + console.error("Error getting settings", error); + return DEFAULT_SETTINGS; + } + } + + const queryResult = useQuery( + { + queryKey: ["settings"], + queryFn: getSettings, + ...options, + }, + queryClient, + ); + + return { ...queryResult, cancel }; +}; diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 0fe49311..2d9a6275 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -2,7 +2,9 @@ import { Loader2, PlugZap, RefreshCw } from "lucide-react"; import { useSearchParams } from "next/navigation"; -import { Suspense, useCallback, useEffect, useRef, useState } from "react"; +import { Suspense, useCallback, useEffect, useState } from "react"; +import { useUpdateFlowSettingMutation } from "@/app/api/mutations/useUpdateFlowSettingMutation"; +import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery"; import { ConfirmationDialog } from "@/components/confirmation-dialog"; import { ProtectedRoute } from "@/components/protected-route"; import { Badge } from "@/components/ui/badge"; @@ -17,11 +19,17 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { + Select, + SelectContent, + SelectItem, + 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 { useUpdateFlowSettingMutation } from "@/app/api/mutations/useUpdateFlowSettingMutation"; +import { useDebounce } from "@/lib/debounce"; interface GoogleDriveFile { id: string; @@ -55,11 +63,11 @@ interface Connector { } interface SyncResult { - processed?: number; - added?: number; - errors?: number; - skipped?: number; - total?: number; + processed?: number; + added?: number; + errors?: number; + skipped?: number; + total?: number; } interface Connection { @@ -84,30 +92,13 @@ function KnowledgeSourcesPage() { 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 [langflowEditUrl, setLangflowEditUrl] = useState(""); - const [langflowIngestEditUrl, setLangflowIngestEditUrl] = useState(""); - const [publicLangflowUrl, setPublicLangflowUrl] = useState(""); - - // Agent Behavior state - const [selectedModel, setSelectedModel] = useState("gpt-4"); + // Only keep systemPrompt state since it needs manual save button const [systemPrompt, setSystemPrompt] = useState(""); - // Knowledge Ingest state - const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState("text-embedding-ada-002"); - const [chunkSize, setChunkSize] = useState(1000); - const [chunkOverlap, setChunkOverlap] = useState(200); - - // Debounce refs for chunk settings - const chunkSizeTimeoutRef = useRef(); - const chunkOverlapTimeoutRef = useRef(); + // Fetch settings using React Query + const { data: settings = {} } = useGetSettingsQuery({ + enabled: isAuthenticated, + }); // Mutations const updateFlowSettingMutation = useUpdateFlowSettingMutation({ @@ -119,52 +110,23 @@ function KnowledgeSourcesPage() { }, }); + // Debounced update function + const debouncedUpdate = useDebounce( + (variables: Parameters[0]) => { + updateFlowSettingMutation.mutate(variables); + }, + 500, + ); - // 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_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.agent?.llm_model) { - setSelectedModel(settings.agent.llm_model); - } - if (settings.agent?.system_prompt) { - setSystemPrompt(settings.agent.system_prompt); - } - if (settings.ingest?.embedding_model) { - setSelectedEmbeddingModel(settings.ingest.embedding_model); - } - if (settings.ingest?.chunk_size) { - setChunkSize(settings.ingest.chunk_size); - } - if (settings.ingest?.chunk_overlap !== undefined) { - setChunkOverlap(settings.ingest.chunk_overlap); - } - } - } catch (error) { - console.error("Failed to fetch settings:", error); + // Sync system prompt state with settings data + useEffect(() => { + if (settings.agent?.system_prompt) { + setSystemPrompt(settings.agent.system_prompt); } - }, []); + }, [settings.agent?.system_prompt]); // Update model selection immediately const handleModelChange = (newModel: string) => { - setSelectedModel(newModel); updateFlowSettingMutation.mutate({ llm_model: newModel }); }; @@ -175,44 +137,23 @@ function KnowledgeSourcesPage() { // Update embedding model selection immediately const handleEmbeddingModelChange = (newModel: string) => { - setSelectedEmbeddingModel(newModel); 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); - - // Clear existing timeout - if (chunkSizeTimeoutRef.current) { - clearTimeout(chunkSizeTimeoutRef.current); - } - - // Set new timeout for API call - chunkSizeTimeoutRef.current = setTimeout(() => { - updateFlowSettingMutation.mutate({ chunk_size: numValue }); - }, 500); + debouncedUpdate({ chunk_size: numValue }); }; // Update chunk overlap setting with debounce const handleChunkOverlapChange = (value: string) => { const numValue = Math.max(0, parseInt(value) || 0); - setChunkOverlap(numValue); - - // Clear existing timeout - if (chunkOverlapTimeoutRef.current) { - clearTimeout(chunkOverlapTimeoutRef.current); - } - - // Set new timeout for API call - chunkOverlapTimeoutRef.current = setTimeout(() => { - updateFlowSettingMutation.mutate({ chunk_overlap: numValue }); - }, 500); + debouncedUpdate({ chunk_overlap: numValue }); }; // Helper function to get connector icon - const getConnectorIcon = (iconName: string) => { + const getConnectorIcon = useCallback((iconName: string) => { const iconMap: { [key: string]: React.ReactElement } = { "google-drive": (
@@ -237,7 +178,7 @@ function KnowledgeSourcesPage() {
) ); - }; + }, []); // Connector functions const checkConnectorStatuses = useCallback(async () => { @@ -293,7 +234,7 @@ function KnowledgeSourcesPage() { } catch (error) { console.error("Failed to check connector statuses:", error); } - }, []); + }, [getConnectorIcon]); const handleConnect = async (connector: Connector) => { setIsConnecting(connector.id); @@ -434,13 +375,6 @@ function KnowledgeSourcesPage() { } }; - // 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) { @@ -483,36 +417,31 @@ function KnowledgeSourcesPage() { } }, [tasks, prevTasks]); - // Cleanup timeouts on unmount - useEffect(() => { - return () => { - if (chunkSizeTimeoutRef.current) { - clearTimeout(chunkSizeTimeoutRef.current); - } - if (chunkOverlapTimeoutRef.current) { - clearTimeout(chunkOverlapTimeoutRef.current); - } - }; - }, []); - - const handleEditInLangflow = (flowType: "chat" | "ingest", closeDialog: () => void) => { + const handleEditInLangflow = ( + flowType: "chat" | "ingest", + closeDialog: () => void, + ) => { // Select the appropriate flow ID and edit URL based on flow type - const targetFlowId = flowType === "ingest" ? ingestFlowId : chatFlowId; - const editUrl = flowType === "ingest" ? langflowIngestEditUrl : langflowEditUrl; - + 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 = ( - publicLangflowUrl || + 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 }; @@ -575,7 +504,9 @@ function KnowledgeSourcesPage() { height="22" viewBox="0 0 24 22" className="h-4 w-4 mr-2" + aria-label="Langflow icon" > + Langflow icon handleEditInLangflow("chat", closeDialog)} + onConfirm={(closeDialog) => + handleEditInLangflow("chat", closeDialog) + } /> @@ -606,7 +539,10 @@ function KnowledgeSourcesPage() { - @@ -615,7 +551,9 @@ function KnowledgeSourcesPage() { GPT-4 Turbo GPT-3.5 Turbo Claude 3 Opus - Claude 3 Sonnet + + Claude 3 Sonnet + Claude 3 Haiku @@ -682,7 +620,9 @@ function KnowledgeSourcesPage() { height="22" viewBox="0 0 24 22" className="h-4 w-4 mr-2" + aria-label="Langflow icon" > + Langflow icon handleEditInLangflow("ingest", closeDialog)} + onConfirm={(closeDialog) => + handleEditInLangflow("ingest", closeDialog) + } /> @@ -710,19 +652,37 @@ function KnowledgeSourcesPage() {
-
@@ -736,17 +696,22 @@ function KnowledgeSourcesPage() { id="chunk-size" type="number" min="1" - value={chunkSize} + defaultValue={settings.ingest?.chunk_size || 1000} onChange={(e) => handleChunkSizeChange(e.target.value)} className="w-full pr-20" />
- characters + + characters +
-