diff --git a/frontend/app/auth/callback/page.tsx b/frontend/app/auth/callback/page.tsx index 3bc359cb..193873c5 100644 --- a/frontend/app/auth/callback/page.tsx +++ b/frontend/app/auth/callback/page.tsx @@ -206,9 +206,7 @@ function AuthCallbackContent() {

{error}

)} - {availableFilters - .filter((filter) => - filter.name - .toLowerCase() - .includes(filterSearchTerm.toLowerCase()), - ) - .map((filter, index) => ( - - ))} - {availableFilters.filter((filter) => - filter.name - .toLowerCase() - .includes(filterSearchTerm.toLowerCase()), - ).length === 0 && - filterSearchTerm && ( -
- No filters match "{filterSearchTerm}"
- )} + {selectedFilter?.id === filter.id && ( + + )} + + ))} + {filteredFilters.length === 0 && filterSearchTerm && ( +
+ No filters match "{filterSearchTerm}" +
+ )} )} diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx index efcb3cc5..9594a0ea 100644 --- a/frontend/app/chat/page.tsx +++ b/frontend/app/chat/page.tsx @@ -1,12 +1,11 @@ "use client"; import { Loader2, Zap } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; import { ProtectedRoute } from "@/components/protected-route"; import { Button } from "@/components/ui/button"; import { type EndpointType, useChat } from "@/contexts/chat-context"; -import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; import { useTask } from "@/contexts/task-context"; import { useChatStreaming } from "@/hooks/useChatStreaming"; import { FILE_CONFIRMATION, FILES_REGEX } from "@/lib/constants"; @@ -40,6 +39,8 @@ function ChatPage() { previousResponseIds, setPreviousResponseIds, placeholderConversation, + conversationFilter, + setConversationFilter, } = useChat(); const [messages, setMessages] = useState([ { @@ -56,20 +57,9 @@ function ChatPage() { >(new Set()); // previousResponseIds now comes from useChat context const [isUploading, setIsUploading] = useState(false); - const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false); - const [availableFilters, setAvailableFilters] = useState< - KnowledgeFilterData[] - >([]); - const [filterSearchTerm, setFilterSearchTerm] = useState(""); - const [selectedFilterIndex, setSelectedFilterIndex] = useState(0); const [isFilterHighlighted, setIsFilterHighlighted] = useState(false); - const [dropdownDismissed, setDropdownDismissed] = useState(false); const [isUserInteracting, setIsUserInteracting] = useState(false); const [isForkingInProgress, setIsForkingInProgress] = useState(false); - const [anchorPosition, setAnchorPosition] = useState<{ - x: number; - y: number; - } | null>(null); const [uploadedFile, setUploadedFile] = useState(null); const [waitingTooLong, setWaitingTooLong] = useState(false); @@ -79,8 +69,20 @@ function ChatPage() { const lastLoadedConversationRef = useRef(null); const { addTask } = useTask(); - const { selectedFilter, parsedFilterData, setSelectedFilter } = - useKnowledgeFilter(); + + // Use conversation-specific filter instead of global filter + const selectedFilter = conversationFilter; + + // Parse the conversation filter data + const parsedFilterData = useMemo(() => { + if (!selectedFilter?.query_data) return null; + try { + return JSON.parse(selectedFilter.query_data); + } catch (error) { + console.error("Error parsing filter data:", error); + return null; + } + }, [selectedFilter]); // Use the chat streaming hook const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow"; @@ -95,7 +97,6 @@ function ChatPage() { setMessages((prev) => [...prev, message]); setLoading(false); setWaitingTooLong(false); - if (responseId) { cancelNudges(); setPreviousResponseIds((prev) => ({ @@ -124,11 +125,11 @@ function ChatPage() { setMessages((prev) => [...prev, errorMessage]); }, }); - + // Show warning if waiting too long (20 seconds) useEffect(() => { let timeoutId: NodeJS.Timeout | null = null; - + if (isStreamLoading && !streamingMessage) { timeoutId = setTimeout(() => { setWaitingTooLong(true); @@ -136,66 +137,12 @@ function ChatPage() { } else { setWaitingTooLong(false); } - + return () => { if (timeoutId) clearTimeout(timeoutId); }; }, [isStreamLoading, streamingMessage]); - const getCursorPosition = (textarea: HTMLTextAreaElement) => { - // Create a hidden div with the same styles as the textarea - const div = document.createElement("div"); - const computedStyle = getComputedStyle(textarea); - - // Copy all computed styles to the hidden div - for (const style of computedStyle) { - (div.style as unknown as Record)[style] = - computedStyle.getPropertyValue(style); - } - - // Set the div to be hidden but not un-rendered - div.style.position = "absolute"; - div.style.visibility = "hidden"; - div.style.whiteSpace = "pre-wrap"; - div.style.wordWrap = "break-word"; - div.style.overflow = "hidden"; - div.style.height = "auto"; - div.style.width = `${textarea.getBoundingClientRect().width}px`; - - // Get the text up to the cursor position - const cursorPos = textarea.selectionStart || 0; - const textBeforeCursor = textarea.value.substring(0, cursorPos); - - // Add the text before cursor - div.textContent = textBeforeCursor; - - // Create a span to mark the end position - const span = document.createElement("span"); - span.textContent = "|"; // Cursor marker - div.appendChild(span); - - // Add the text after cursor to handle word wrapping - const textAfterCursor = textarea.value.substring(cursorPos); - div.appendChild(document.createTextNode(textAfterCursor)); - - // Add the div to the document temporarily - document.body.appendChild(div); - - // Get positions - const inputRect = textarea.getBoundingClientRect(); - const divRect = div.getBoundingClientRect(); - const spanRect = span.getBoundingClientRect(); - - // Calculate the cursor position relative to the input - const x = inputRect.left + (spanRect.left - divRect.left); - const y = inputRect.top + (spanRect.top - divRect.top); - - // Clean up - document.body.removeChild(div); - - return { x, y }; - }; - const handleEndpointChange = (newEndpoint: EndpointType) => { setEndpoint(newEndpoint); // Clear the conversation when switching endpoints to avoid response ID conflicts @@ -317,54 +264,12 @@ function ChatPage() { chatInputRef.current?.clickFileInput(); }; - const loadAvailableFilters = async () => { - try { - const response = await fetch("/api/knowledge-filter/search", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: "", - limit: 20, - }), - }); - - const result = await response.json(); - if (response.ok && result.success) { - setAvailableFilters(result.filters); - } else { - console.error("Failed to load knowledge filters:", result.error); - setAvailableFilters([]); - } - } catch (error) { - console.error("Failed to load knowledge filters:", error); - setAvailableFilters([]); - } - }; - const handleFilterSelect = (filter: KnowledgeFilterData | null) => { - setSelectedFilter(filter); - setIsFilterDropdownOpen(false); - setFilterSearchTerm(""); + // Update conversation-specific filter + setConversationFilter(filter); setIsFilterHighlighted(false); - - // Remove the @searchTerm from the input and replace with filter pill - const words = input.split(" "); - const lastWord = words[words.length - 1]; - - if (lastWord.startsWith("@")) { - // Remove the @search term - words.pop(); - setInput(words.join(" ") + (words.length > 0 ? " " : "")); - } }; - // Reset selected index when search term changes - useEffect(() => { - setSelectedFilterIndex(0); - }, []); - // Auto-focus the input on component mount useEffect(() => { chatInputRef.current?.focusInput(); @@ -388,6 +293,11 @@ function ChatPage() { setIsFilterHighlighted(false); setLoading(false); lastLoadedConversationRef.current = null; + + // Focus input after a short delay to ensure rendering is complete + setTimeout(() => { + chatInputRef.current?.focusInput(); + }, 100); }; const handleFocusInput = () => { @@ -409,8 +319,7 @@ function ChatPage() { // 2. It's different from the last loaded conversation AND // 3. User is not in the middle of an interaction if ( - conversationData && - conversationData.messages && + conversationData?.messages && lastLoadedConversationRef.current !== conversationData.response_id && !isUserInteracting && !isForkingInProgress @@ -576,6 +485,11 @@ function ChatPage() { ...prev, [conversationData.endpoint]: conversationData.response_id, })); + + // Focus input when loading a conversation + setTimeout(() => { + chatInputRef.current?.focusInput(); + }, 100); } }, [ conversationData, @@ -596,6 +510,11 @@ function ChatPage() { }, ]); lastLoadedConversationRef.current = null; + + // Focus input when starting a new conversation + setTimeout(() => { + chatInputRef.current?.focusInput(); + }, 100); } }, [placeholderConversation, currentConversationId]); @@ -1035,162 +954,6 @@ function ChatPage() { handleSendMessage(suggestion); }; - const handleKeyDown = (e: React.KeyboardEvent) => { - // Handle backspace for filter clearing - if (e.key === "Backspace" && selectedFilter && input.trim() === "") { - e.preventDefault(); - - if (isFilterHighlighted) { - // Second backspace - remove the filter - setSelectedFilter(null); - setIsFilterHighlighted(false); - } else { - // First backspace - highlight the filter - setIsFilterHighlighted(true); - } - return; - } - - if (isFilterDropdownOpen) { - const filteredFilters = availableFilters.filter((filter) => - filter.name.toLowerCase().includes(filterSearchTerm.toLowerCase()), - ); - - if (e.key === "Escape") { - e.preventDefault(); - setIsFilterDropdownOpen(false); - setFilterSearchTerm(""); - setSelectedFilterIndex(0); - setDropdownDismissed(true); - - // Keep focus on the textarea so user can continue typing normally - chatInputRef.current?.focusInput(); - return; - } - - if (e.key === "ArrowDown") { - e.preventDefault(); - setSelectedFilterIndex((prev) => - prev < filteredFilters.length - 1 ? prev + 1 : 0, - ); - return; - } - - if (e.key === "ArrowUp") { - e.preventDefault(); - setSelectedFilterIndex((prev) => - prev > 0 ? prev - 1 : filteredFilters.length - 1, - ); - return; - } - - if (e.key === "Enter") { - // Check if we're at the end of an @ mention (space before cursor or end of input) - const cursorPos = e.currentTarget.selectionStart || 0; - const textBeforeCursor = input.slice(0, cursorPos); - const words = textBeforeCursor.split(" "); - const lastWord = words[words.length - 1]; - - if (lastWord.startsWith("@") && filteredFilters[selectedFilterIndex]) { - e.preventDefault(); - handleFilterSelect(filteredFilters[selectedFilterIndex]); - return; - } - } - - if (e.key === " ") { - // Select filter on space if we're typing an @ mention - const cursorPos = e.currentTarget.selectionStart || 0; - const textBeforeCursor = input.slice(0, cursorPos); - const words = textBeforeCursor.split(" "); - const lastWord = words[words.length - 1]; - - if (lastWord.startsWith("@") && filteredFilters[selectedFilterIndex]) { - e.preventDefault(); - handleFilterSelect(filteredFilters[selectedFilterIndex]); - return; - } - } - } - - if (e.key === "Enter" && !e.shiftKey && !isFilterDropdownOpen) { - e.preventDefault(); - if (input.trim() && !loading) { - // Trigger form submission by finding the form and calling submit - const form = e.currentTarget.closest("form"); - if (form) { - form.requestSubmit(); - } - } - } - }; - - const onChange = (e: React.ChangeEvent) => { - const newValue = e.target.value; - setInput(newValue); - - // Clear filter highlight when user starts typing - if (isFilterHighlighted) { - setIsFilterHighlighted(false); - } - - // Find if there's an @ at the start of the last word - const words = newValue.split(" "); - const lastWord = words[words.length - 1]; - - if (lastWord.startsWith("@") && !dropdownDismissed) { - const searchTerm = lastWord.slice(1); // Remove the @ - console.log("Setting search term:", searchTerm); - setFilterSearchTerm(searchTerm); - setSelectedFilterIndex(0); - - // Only set anchor position when @ is first detected (search term is empty) - if (searchTerm === "") { - const pos = getCursorPosition(e.target); - setAnchorPosition(pos); - } - - if (!isFilterDropdownOpen) { - loadAvailableFilters(); - setIsFilterDropdownOpen(true); - } - } else if (isFilterDropdownOpen) { - // Close dropdown if @ is no longer present - console.log("Closing dropdown - no @ found"); - setIsFilterDropdownOpen(false); - setFilterSearchTerm(""); - } - - // Reset dismissed flag when user moves to a different word - if (dropdownDismissed && !lastWord.startsWith("@")) { - setDropdownDismissed(false); - } - }; - - const onAtClick = () => { - if (!isFilterDropdownOpen) { - loadAvailableFilters(); - setIsFilterDropdownOpen(true); - setFilterSearchTerm(""); - setSelectedFilterIndex(0); - - // Get button position for popover anchoring - const button = document.querySelector( - "[data-filter-button]", - ) as HTMLElement; - if (button) { - const rect = button.getBoundingClientRect(); - setAnchorPosition({ - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2 - 12, - }); - } - } else { - setIsFilterDropdownOpen(false); - setAnchorPosition(null); - } - }; - return ( <> {/* Debug header - only show in debug mode */} @@ -1313,6 +1076,11 @@ function ChatPage() { onFork={(e) => handleForkConversation(index, e)} animate={false} isInactive={index < messages.length - 1} + isInitialGreeting={ + index === 0 && + messages.length === 1 && + message.content === "How can I assist?" + } /> ), @@ -1331,7 +1099,7 @@ function ChatPage() { isCompleted={false} /> )} - + {/* Waiting too long indicator */} {waitingTooLong && !streamingMessage && loading && (
@@ -1340,7 +1108,8 @@ function ChatPage() { The server is taking longer than expected...

- This may be due to high server load. The request will timeout after 60 seconds. + This may be due to high server load. The request will + timeout after 60 seconds.

)} @@ -1364,23 +1133,46 @@ function ChatPage() { loading={loading} isUploading={isUploading} selectedFilter={selectedFilter} - isFilterDropdownOpen={isFilterDropdownOpen} - availableFilters={availableFilters} - filterSearchTerm={filterSearchTerm} - selectedFilterIndex={selectedFilterIndex} - anchorPosition={anchorPosition} parsedFilterData={parsedFilterData} uploadedFile={uploadedFile} onSubmit={handleSubmit} - onChange={onChange} - onKeyDown={handleKeyDown} + onChange={setInput} + onKeyDown={(e) => { + // Handle backspace for filter clearing + if ( + e.key === "Backspace" && + selectedFilter && + input.trim() === "" + ) { + e.preventDefault(); + if (isFilterHighlighted) { + // Second backspace - remove the filter + setConversationFilter(null); + setIsFilterHighlighted(false); + } else { + // First backspace - highlight the filter + setIsFilterHighlighted(true); + } + return; + } + + // Handle Enter key for form submission + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + if (input.trim() && !loading) { + // Trigger form submission by finding the form and calling submit + const form = e.currentTarget.closest("form"); + if (form) { + form.requestSubmit(); + } + } + } + }} onFilterSelect={handleFilterSelect} - onAtClick={onAtClick} onFilePickerClick={handleFilePickerClick} onFileSelected={setUploadedFile} - setSelectedFilter={setSelectedFilter} + setSelectedFilter={setConversationFilter} setIsFilterHighlighted={setIsFilterHighlighted} - setIsFilterDropdownOpen={setIsFilterDropdownOpen} /> diff --git a/frontend/app/onboarding/_components/animated-provider-steps.tsx b/frontend/app/onboarding/_components/animated-provider-steps.tsx index 4a1b0e47..026b330f 100644 --- a/frontend/app/onboarding/_components/animated-provider-steps.tsx +++ b/frontend/app/onboarding/_components/animated-provider-steps.tsx @@ -5,211 +5,211 @@ import { CheckIcon, XIcon } from "lucide-react"; import { useEffect, useState } from "react"; import AnimatedProcessingIcon from "@/components/icons/animated-processing-icon"; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@/components/ui/accordion"; import { ONBOARDING_CARD_STEPS_KEY } from "@/lib/constants"; import { cn } from "@/lib/utils"; export function AnimatedProviderSteps({ - currentStep, - isCompleted, - setCurrentStep, - steps, - storageKey = ONBOARDING_CARD_STEPS_KEY, - processingStartTime, - hasError = false, + currentStep, + isCompleted, + setCurrentStep, + steps, + storageKey = ONBOARDING_CARD_STEPS_KEY, + processingStartTime, + hasError = false, }: { - currentStep: number; - isCompleted: boolean; - setCurrentStep: (step: number) => void; - steps: string[]; - storageKey?: string; - processingStartTime?: number | null; - hasError?: boolean; + currentStep: number; + isCompleted: boolean; + setCurrentStep: (step: number) => void; + steps: string[]; + storageKey?: string; + processingStartTime?: number | null; + hasError?: boolean; }) { - const [startTime, setStartTime] = useState(null); - const [elapsedTime, setElapsedTime] = useState(0); + const [startTime, setStartTime] = useState(null); + const [elapsedTime, setElapsedTime] = useState(0); - // Initialize start time from prop or local storage - useEffect(() => { - const storedElapsedTime = localStorage.getItem(storageKey); + // Initialize start time from prop or local storage + useEffect(() => { + const storedElapsedTime = localStorage.getItem(storageKey); - if (isCompleted && storedElapsedTime) { - // If completed, use stored elapsed time - setElapsedTime(parseFloat(storedElapsedTime)); - } else if (processingStartTime) { - // Use the start time passed from parent (when user clicked Complete) - setStartTime(processingStartTime); - } - }, [storageKey, isCompleted, processingStartTime]); + if (isCompleted && storedElapsedTime) { + // If completed, use stored elapsed time + setElapsedTime(parseFloat(storedElapsedTime)); + } else if (processingStartTime) { + // Use the start time passed from parent (when user clicked Complete) + setStartTime(processingStartTime); + } + }, [storageKey, isCompleted, processingStartTime]); - // Progress through steps - useEffect(() => { - if (currentStep < steps.length - 1 && !isCompleted) { - const interval = setInterval(() => { - setCurrentStep(currentStep + 1); - }, 1500); - return () => clearInterval(interval); - } - }, [currentStep, setCurrentStep, steps, isCompleted]); + // Progress through steps + useEffect(() => { + if (currentStep < steps.length - 1 && !isCompleted) { + const interval = setInterval(() => { + setCurrentStep(currentStep + 1); + }, 1500); + return () => clearInterval(interval); + } + }, [currentStep, setCurrentStep, steps, isCompleted]); - // Calculate and store elapsed time when completed - useEffect(() => { - if (isCompleted && startTime) { - const elapsed = Date.now() - startTime; - setElapsedTime(elapsed); - localStorage.setItem(storageKey, elapsed.toString()); - } - }, [isCompleted, startTime, storageKey]); + // Calculate and store elapsed time when completed + useEffect(() => { + if (isCompleted && startTime) { + const elapsed = Date.now() - startTime; + setElapsedTime(elapsed); + localStorage.setItem(storageKey, elapsed.toString()); + } + }, [isCompleted, startTime, storageKey]); - const isDone = currentStep >= steps.length && !isCompleted && !hasError; + const isDone = currentStep >= steps.length && !isCompleted && !hasError; - return ( - - {!isCompleted ? ( - -
-
- - - -
+ return ( + + {!isCompleted ? ( + +
+
+ + + +
- - {hasError ? "Error" : isDone ? "Done" : "Thinking"} - -
-
- - {!isDone && !hasError && ( - -
-
- - - {steps[currentStep]} - - -
- - )} - -
-
- ) : ( - - - - -
- - {`Initialized in ${(elapsedTime / 1000).toFixed(1)} seconds`} - -
-
- -
- {/* Connecting line on the left */} - + + {hasError ? "Error" : isDone ? "Done" : "Thinking"} + +
+
+ + {!isDone && !hasError && ( + +
+
+ + + {steps[currentStep]} + + +
+ + )} + +
+
+ ) : ( + + + + +
+ + {`Initialized in ${(elapsedTime / 1000).toFixed(1)} seconds`} + +
+
+ +
+ {/* Connecting line on the left */} + -
- - {steps.map((step, index) => ( - - - - - - - - {step} - - - ))} - -
-
-
-
-
-
- )} -
- ); +
+ + {steps.map((step, index) => ( + + + + + + + + {step} + + + ))} + +
+
+
+
+
+
+ )} +
+ ); } diff --git a/frontend/app/onboarding/_components/onboarding-card.tsx b/frontend/app/onboarding/_components/onboarding-card.tsx index 34f3680c..3dffb21e 100644 --- a/frontend/app/onboarding/_components/onboarding-card.tsx +++ b/frontend/app/onboarding/_components/onboarding-card.tsx @@ -6,8 +6,8 @@ import { Info, X } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { - type OnboardingVariables, - useOnboardingMutation, + type OnboardingVariables, + useOnboardingMutation, } from "@/app/api/mutations/useOnboardingMutation"; import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery"; import { useGetTasksQuery } from "@/app/api/queries/useGetTasksQuery"; @@ -20,9 +20,9 @@ import OpenAILogo from "@/components/icons/openai-logo"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { - Tooltip, - TooltipContent, - TooltipTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, } from "@/components/ui/tooltip"; import { ONBOARDING_CARD_STEPS_KEY } from "@/lib/constants"; import { cn } from "@/lib/utils"; @@ -34,507 +34,507 @@ import { OpenAIOnboarding } from "./openai-onboarding"; import { TabTrigger } from "./tab-trigger"; interface OnboardingCardProps { - onComplete: () => void; - isCompleted?: boolean; - isEmbedding?: boolean; - setIsLoadingModels?: (isLoading: boolean) => void; - setLoadingStatus?: (status: string[]) => void; + onComplete: () => void; + isCompleted?: boolean; + isEmbedding?: boolean; + setIsLoadingModels?: (isLoading: boolean) => void; + setLoadingStatus?: (status: string[]) => void; } const STEP_LIST = [ - "Setting up your model provider", - "Defining schema", - "Configuring Langflow", + "Setting up your model provider", + "Defining schema", + "Configuring Langflow", ]; const EMBEDDING_STEP_LIST = [ - "Setting up your model provider", - "Defining schema", - "Configuring Langflow", - "Ingesting sample data", + "Setting up your model provider", + "Defining schema", + "Configuring Langflow", + "Ingesting sample data", ]; const OnboardingCard = ({ - onComplete, - isEmbedding = false, - isCompleted = false, + onComplete, + isEmbedding = false, + isCompleted = false, }: OnboardingCardProps) => { - const { isHealthy: isDoclingHealthy } = useDoclingHealth(); + const { isHealthy: isDoclingHealthy } = useDoclingHealth(); - const [modelProvider, setModelProvider] = useState( - isEmbedding ? "openai" : "anthropic", - ); + const [modelProvider, setModelProvider] = useState( + isEmbedding ? "openai" : "anthropic", + ); - const [sampleDataset, setSampleDataset] = useState(true); + const [sampleDataset, setSampleDataset] = useState(true); - const [isLoadingModels, setIsLoadingModels] = useState(false); + const [isLoadingModels, setIsLoadingModels] = useState(false); - const queryClient = useQueryClient(); + const queryClient = useQueryClient(); - // Fetch current settings to check if providers are already configured - const { data: currentSettings } = useGetSettingsQuery(); + // Fetch current settings to check if providers are already configured + const { data: currentSettings } = useGetSettingsQuery(); - const handleSetModelProvider = (provider: string) => { - setIsLoadingModels(false); - setModelProvider(provider); - setSettings({ - [isEmbedding ? "embedding_provider" : "llm_provider"]: provider, - embedding_model: "", - llm_model: "", - }); - setError(null); - }; + const handleSetModelProvider = (provider: string) => { + setIsLoadingModels(false); + setModelProvider(provider); + setSettings({ + [isEmbedding ? "embedding_provider" : "llm_provider"]: provider, + embedding_model: "", + llm_model: "", + }); + setError(null); + }; - // Check if the selected provider is already configured - const isProviderAlreadyConfigured = (provider: string): boolean => { - if (!isEmbedding || !currentSettings?.providers) return false; + // Check if the selected provider is already configured + const isProviderAlreadyConfigured = (provider: string): boolean => { + if (!isEmbedding || !currentSettings?.providers) return false; - // Check if provider has been explicitly configured (not just from env vars) - if (provider === "openai") { - return currentSettings.providers.openai?.configured === true; - } else if (provider === "anthropic") { - return currentSettings.providers.anthropic?.configured === true; - } else if (provider === "watsonx") { - return currentSettings.providers.watsonx?.configured === true; - } else if (provider === "ollama") { - return currentSettings.providers.ollama?.configured === true; - } - return false; - }; + // Check if provider has been explicitly configured (not just from env vars) + if (provider === "openai") { + return currentSettings.providers.openai?.configured === true; + } else if (provider === "anthropic") { + return currentSettings.providers.anthropic?.configured === true; + } else if (provider === "watsonx") { + return currentSettings.providers.watsonx?.configured === true; + } else if (provider === "ollama") { + return currentSettings.providers.ollama?.configured === true; + } + return false; + }; - const showProviderConfiguredMessage = - isProviderAlreadyConfigured(modelProvider); - const providerAlreadyConfigured = - isEmbedding && showProviderConfiguredMessage; + const showProviderConfiguredMessage = + isProviderAlreadyConfigured(modelProvider); + const providerAlreadyConfigured = + isEmbedding && showProviderConfiguredMessage; - const totalSteps = isEmbedding - ? EMBEDDING_STEP_LIST.length - : STEP_LIST.length; + const totalSteps = isEmbedding + ? EMBEDDING_STEP_LIST.length + : STEP_LIST.length; - const [settings, setSettings] = useState({ - [isEmbedding ? "embedding_provider" : "llm_provider"]: modelProvider, - embedding_model: "", - llm_model: "", - // Provider-specific fields will be set by provider components - openai_api_key: "", - anthropic_api_key: "", - watsonx_api_key: "", - watsonx_endpoint: "", - watsonx_project_id: "", - ollama_endpoint: "", - }); + const [settings, setSettings] = useState({ + [isEmbedding ? "embedding_provider" : "llm_provider"]: modelProvider, + embedding_model: "", + llm_model: "", + // Provider-specific fields will be set by provider components + openai_api_key: "", + anthropic_api_key: "", + watsonx_api_key: "", + watsonx_endpoint: "", + watsonx_project_id: "", + ollama_endpoint: "", + }); - const [currentStep, setCurrentStep] = useState( - isCompleted ? totalSteps : null, - ); + const [currentStep, setCurrentStep] = useState( + isCompleted ? totalSteps : null, + ); - const [processingStartTime, setProcessingStartTime] = useState( - null, - ); + const [processingStartTime, setProcessingStartTime] = useState( + null, + ); - const [error, setError] = useState(null); + const [error, setError] = useState(null); - // Query tasks to track completion - const { data: tasks } = useGetTasksQuery({ - enabled: currentStep !== null, // Only poll when onboarding has started - refetchInterval: currentStep !== null ? 1000 : false, // Poll every 1 second during onboarding - }); + // Query tasks to track completion + const { data: tasks } = useGetTasksQuery({ + enabled: currentStep !== null, // Only poll when onboarding has started + refetchInterval: currentStep !== null ? 1000 : false, // Poll every 1 second during onboarding + }); - // Monitor tasks and call onComplete when all tasks are done - useEffect(() => { - if (currentStep === null || !tasks || !isEmbedding) { - return; - } + // Monitor tasks and call onComplete when all tasks are done + useEffect(() => { + if (currentStep === null || !tasks || !isEmbedding) { + return; + } - // Check if there are any active tasks (pending, running, or processing) - const activeTasks = tasks.find( - (task) => - task.status === "pending" || - task.status === "running" || - task.status === "processing", - ); + // Check if there are any active tasks (pending, running, or processing) + const activeTasks = tasks.find( + (task) => + task.status === "pending" || + task.status === "running" || + task.status === "processing", + ); - // If no active tasks and we've started onboarding, complete it - if ( - (!activeTasks || (activeTasks.processed_files ?? 0) > 0) && - tasks.length > 0 && - !isCompleted - ) { - // Set to final step to show "Done" - setCurrentStep(totalSteps); - // Wait a bit before completing - setTimeout(() => { - onComplete(); - }, 1000); - } - }, [tasks, currentStep, onComplete, isCompleted, isEmbedding, totalSteps]); + // If no active tasks and we've started onboarding, complete it + if ( + (!activeTasks || (activeTasks.processed_files ?? 0) > 0) && + tasks.length > 0 && + !isCompleted + ) { + // Set to final step to show "Done" + setCurrentStep(totalSteps); + // Wait a bit before completing + setTimeout(() => { + onComplete(); + }, 1000); + } + }, [tasks, currentStep, onComplete, isCompleted, isEmbedding, totalSteps]); - // Mutations - const onboardingMutation = useOnboardingMutation({ - onSuccess: (data) => { - console.log("Onboarding completed successfully", data); - // Update provider health cache to healthy since backend just validated - const provider = - (isEmbedding ? settings.embedding_provider : settings.llm_provider) || - modelProvider; - const healthData: ProviderHealthResponse = { - status: "healthy", - message: "Provider is configured and working correctly", - provider: provider, - }; - queryClient.setQueryData(["provider", "health"], healthData); - setError(null); - if (!isEmbedding) { - setCurrentStep(totalSteps); - setTimeout(() => { - onComplete(); - }, 1000); - } else { - setCurrentStep(0); - } - }, - onError: (error) => { - setError(error.message); - setCurrentStep(totalSteps); - // Reset to provider selection after 1 second - setTimeout(() => { - setCurrentStep(null); - }, 1000); - }, - }); + // Mutations + const onboardingMutation = useOnboardingMutation({ + onSuccess: (data) => { + console.log("Onboarding completed successfully", data); + // Update provider health cache to healthy since backend just validated + const provider = + (isEmbedding ? settings.embedding_provider : settings.llm_provider) || + modelProvider; + const healthData: ProviderHealthResponse = { + status: "healthy", + message: "Provider is configured and working correctly", + provider: provider, + }; + queryClient.setQueryData(["provider", "health"], healthData); + setError(null); + if (!isEmbedding) { + setCurrentStep(totalSteps); + setTimeout(() => { + onComplete(); + }, 1000); + } else { + setCurrentStep(0); + } + }, + onError: (error) => { + setError(error.message); + setCurrentStep(totalSteps); + // Reset to provider selection after 1 second + setTimeout(() => { + setCurrentStep(null); + }, 1000); + }, + }); - const handleComplete = () => { - const currentProvider = isEmbedding - ? settings.embedding_provider - : settings.llm_provider; + const handleComplete = () => { + const currentProvider = isEmbedding + ? settings.embedding_provider + : settings.llm_provider; - if ( - !currentProvider || - (isEmbedding && - !settings.embedding_model && - !showProviderConfiguredMessage) || - (!isEmbedding && !settings.llm_model) - ) { - toast.error("Please complete all required fields"); - return; - } + if ( + !currentProvider || + (isEmbedding && + !settings.embedding_model && + !showProviderConfiguredMessage) || + (!isEmbedding && !settings.llm_model) + ) { + toast.error("Please complete all required fields"); + return; + } - // Clear any previous error - setError(null); + // Clear any previous error + setError(null); - // Prepare onboarding data with provider-specific fields - const onboardingData: OnboardingVariables = { - sample_data: sampleDataset, - }; + // Prepare onboarding data with provider-specific fields + const onboardingData: OnboardingVariables = { + sample_data: sampleDataset, + }; - // Set the provider field - if (isEmbedding) { - onboardingData.embedding_provider = currentProvider; - // If provider is already configured, use the existing embedding model from settings - // Otherwise, use the embedding model from the form - if ( - showProviderConfiguredMessage && - currentSettings?.knowledge?.embedding_model - ) { - onboardingData.embedding_model = - currentSettings.knowledge.embedding_model; - } else { - onboardingData.embedding_model = settings.embedding_model; - } - } else { - onboardingData.llm_provider = currentProvider; - onboardingData.llm_model = settings.llm_model; - } + // Set the provider field + if (isEmbedding) { + onboardingData.embedding_provider = currentProvider; + // If provider is already configured, use the existing embedding model from settings + // Otherwise, use the embedding model from the form + if ( + showProviderConfiguredMessage && + currentSettings?.knowledge?.embedding_model + ) { + onboardingData.embedding_model = + currentSettings.knowledge.embedding_model; + } else { + onboardingData.embedding_model = settings.embedding_model; + } + } else { + onboardingData.llm_provider = currentProvider; + onboardingData.llm_model = settings.llm_model; + } - // Add provider-specific credentials based on the selected provider - if (currentProvider === "openai" && settings.openai_api_key) { - onboardingData.openai_api_key = settings.openai_api_key; - } else if (currentProvider === "anthropic" && settings.anthropic_api_key) { - onboardingData.anthropic_api_key = settings.anthropic_api_key; - } else if (currentProvider === "watsonx") { - if (settings.watsonx_api_key) { - onboardingData.watsonx_api_key = settings.watsonx_api_key; - } - if (settings.watsonx_endpoint) { - onboardingData.watsonx_endpoint = settings.watsonx_endpoint; - } - if (settings.watsonx_project_id) { - onboardingData.watsonx_project_id = settings.watsonx_project_id; - } - } else if (currentProvider === "ollama" && settings.ollama_endpoint) { - onboardingData.ollama_endpoint = settings.ollama_endpoint; - } + // Add provider-specific credentials based on the selected provider + if (currentProvider === "openai" && settings.openai_api_key) { + onboardingData.openai_api_key = settings.openai_api_key; + } else if (currentProvider === "anthropic" && settings.anthropic_api_key) { + onboardingData.anthropic_api_key = settings.anthropic_api_key; + } else if (currentProvider === "watsonx") { + if (settings.watsonx_api_key) { + onboardingData.watsonx_api_key = settings.watsonx_api_key; + } + if (settings.watsonx_endpoint) { + onboardingData.watsonx_endpoint = settings.watsonx_endpoint; + } + if (settings.watsonx_project_id) { + onboardingData.watsonx_project_id = settings.watsonx_project_id; + } + } else if (currentProvider === "ollama" && settings.ollama_endpoint) { + onboardingData.ollama_endpoint = settings.ollama_endpoint; + } - // Record the start time when user clicks Complete - setProcessingStartTime(Date.now()); - onboardingMutation.mutate(onboardingData); - setCurrentStep(0); - }; + // Record the start time when user clicks Complete + setProcessingStartTime(Date.now()); + onboardingMutation.mutate(onboardingData); + setCurrentStep(0); + }; - const isComplete = - (isEmbedding && - (!!settings.embedding_model || showProviderConfiguredMessage)) || - (!isEmbedding && !!settings.llm_model && isDoclingHealthy); + const isComplete = + (isEmbedding && + (!!settings.embedding_model || showProviderConfiguredMessage)) || + (!isEmbedding && !!settings.llm_model && isDoclingHealthy); - return ( - - {currentStep === null ? ( - -
- - {error && ( - -
- - - {error} - -
-
- )} -
-
- - - {!isEmbedding && ( - - -
- -
- Anthropic -
-
- )} - - -
- -
- OpenAI -
-
- - -
- -
- IBM watsonx.ai -
-
- - -
- -
- Ollama -
-
-
- {!isEmbedding && ( - - - - )} - - - - - - - - - -
+ return ( + + {currentStep === null ? ( + +
+ + {error && ( + +
+ + + {error} + +
+
+ )} +
+
+ + + {!isEmbedding && ( + + +
+ +
+ Anthropic +
+
+ )} + + +
+ +
+ OpenAI +
+
+ + +
+ +
+ IBM watsonx.ai +
+
+ + +
+ +
+ Ollama +
+
+
+ {!isEmbedding && ( + + + + )} + + + + + + + + + +
- - -
- -
-
- {!isComplete && ( - - {isLoadingModels - ? "Loading models..." - : !!settings.llm_model && - !!settings.embedding_model && - !isDoclingHealthy - ? "docling-serve must be running to continue" - : "Please fill in all required fields"} - - )} -
-
-
-
- ) : ( - - - - )} -
- ); + + +
+ +
+
+ {!isComplete && ( + + {isLoadingModels + ? "Loading models..." + : !!settings.llm_model && + !!settings.embedding_model && + !isDoclingHealthy + ? "docling-serve must be running to continue" + : "Please fill in all required fields"} + + )} +
+
+
+
+ ) : ( + + + + )} +
+ ); }; export default OnboardingCard; diff --git a/frontend/app/onboarding/_components/onboarding-content.tsx b/frontend/app/onboarding/_components/onboarding-content.tsx index 3e5428ba..6c6cffac 100644 --- a/frontend/app/onboarding/_components/onboarding-content.tsx +++ b/frontend/app/onboarding/_components/onboarding-content.tsx @@ -36,7 +36,9 @@ export function OnboardingContent({ () => { // Retrieve assistant message from localStorage on mount if (typeof window === "undefined") return null; - const savedMessage = localStorage.getItem(ONBOARDING_ASSISTANT_MESSAGE_KEY); + const savedMessage = localStorage.getItem( + ONBOARDING_ASSISTANT_MESSAGE_KEY, + ); if (savedMessage) { try { const parsed = JSON.parse(savedMessage); @@ -78,7 +80,10 @@ export function OnboardingContent({ JSON.stringify(message), ); } catch (error) { - console.error("Failed to save assistant message to localStorage:", error); + console.error( + "Failed to save assistant message to localStorage:", + error, + ); } } if (newResponseId) { diff --git a/frontend/app/onboarding/_components/onboarding-upload.tsx b/frontend/app/onboarding/_components/onboarding-upload.tsx index 60fb676e..3855ff83 100644 --- a/frontend/app/onboarding/_components/onboarding-upload.tsx +++ b/frontend/app/onboarding/_components/onboarding-upload.tsx @@ -8,152 +8,152 @@ import { ONBOARDING_UPLOAD_STEPS_KEY } from "@/lib/constants"; import { uploadFile } from "@/lib/upload-utils"; interface OnboardingUploadProps { - onComplete: () => void; + onComplete: () => void; } const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => { - const fileInputRef = useRef(null); - const [isUploading, setIsUploading] = useState(false); - const [currentStep, setCurrentStep] = useState(null); + const fileInputRef = useRef(null); + const [isUploading, setIsUploading] = useState(false); + const [currentStep, setCurrentStep] = useState(null); - const STEP_LIST = [ - "Uploading your document", - "Generating embeddings", - "Ingesting document", - "Processing your document", - ]; + const STEP_LIST = [ + "Uploading your document", + "Generating embeddings", + "Ingesting document", + "Processing your document", + ]; - // Query tasks to track completion - const { data: tasks } = useGetTasksQuery({ - enabled: currentStep !== null, // Only poll when upload has started - refetchInterval: currentStep !== null ? 1000 : false, // Poll every 1 second during upload - }); + // Query tasks to track completion + const { data: tasks } = useGetTasksQuery({ + enabled: currentStep !== null, // Only poll when upload has started + refetchInterval: currentStep !== null ? 1000 : false, // Poll every 1 second during upload + }); - const { refetch: refetchNudges } = useGetNudgesQuery(null); + const { refetch: refetchNudges } = useGetNudgesQuery(null); - // Monitor tasks and call onComplete when file processing is done - useEffect(() => { - if (currentStep === null || !tasks) { - return; - } + // Monitor tasks and call onComplete when file processing is done + useEffect(() => { + if (currentStep === null || !tasks) { + return; + } - // Check if there are any active tasks (pending, running, or processing) - const activeTasks = tasks.find( - (task) => - task.status === "pending" || - task.status === "running" || - task.status === "processing", - ); + // Check if there are any active tasks (pending, running, or processing) + const activeTasks = tasks.find( + (task) => + task.status === "pending" || + task.status === "running" || + task.status === "processing", + ); - // If no active tasks and we have more than 1 task (initial + new upload), complete it - if ( - (!activeTasks || (activeTasks.processed_files ?? 0) > 0) && - tasks.length > 1 - ) { - // Set to final step to show "Done" - setCurrentStep(STEP_LIST.length); + // If no active tasks and we have more than 1 task (initial + new upload), complete it + if ( + (!activeTasks || (activeTasks.processed_files ?? 0) > 0) && + tasks.length > 1 + ) { + // Set to final step to show "Done" + setCurrentStep(STEP_LIST.length); - // Refetch nudges to get new ones - refetchNudges(); + // Refetch nudges to get new ones + refetchNudges(); - // Wait a bit before completing - setTimeout(() => { - onComplete(); - }, 1000); - } - }, [tasks, currentStep, onComplete, refetchNudges]); + // Wait a bit before completing + setTimeout(() => { + onComplete(); + }, 1000); + } + }, [tasks, currentStep, onComplete, refetchNudges]); - const resetFileInput = () => { - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - }; + const resetFileInput = () => { + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; - const handleUploadClick = () => { - fileInputRef.current?.click(); - }; + const handleUploadClick = () => { + fileInputRef.current?.click(); + }; - const performUpload = async (file: File) => { - setIsUploading(true); - try { - setCurrentStep(0); - await uploadFile(file, true); - console.log("Document upload task started successfully"); - // Move to processing step - task monitoring will handle completion - setTimeout(() => { - setCurrentStep(1); - }, 1500); - } catch (error) { - console.error("Upload failed", (error as Error).message); - // Reset on error - setCurrentStep(null); - } finally { - setIsUploading(false); - } - }; + const performUpload = async (file: File) => { + setIsUploading(true); + try { + setCurrentStep(0); + await uploadFile(file, true); + console.log("Document upload task started successfully"); + // Move to processing step - task monitoring will handle completion + setTimeout(() => { + setCurrentStep(1); + }, 1500); + } catch (error) { + console.error("Upload failed", (error as Error).message); + // Reset on error + setCurrentStep(null); + } finally { + setIsUploading(false); + } + }; - const handleFileChange = async (event: ChangeEvent) => { - const selectedFile = event.target.files?.[0]; - if (!selectedFile) { - resetFileInput(); - return; - } + const handleFileChange = async (event: ChangeEvent) => { + const selectedFile = event.target.files?.[0]; + if (!selectedFile) { + resetFileInput(); + return; + } - try { - await performUpload(selectedFile); - } catch (error) { - console.error( - "Unable to prepare file for upload", - (error as Error).message, - ); - } finally { - resetFileInput(); - } - }; + try { + await performUpload(selectedFile); + } catch (error) { + console.error( + "Unable to prepare file for upload", + (error as Error).message, + ); + } finally { + resetFileInput(); + } + }; - return ( - - {currentStep === null ? ( - - - - - ) : ( - - - - )} - - ); + return ( + + {currentStep === null ? ( + + + + + ) : ( + + + + )} + + ); }; export default OnboardingUpload; diff --git a/frontend/app/settings/_components/anthropic-settings-dialog.tsx b/frontend/app/settings/_components/anthropic-settings-dialog.tsx index 9501d0e9..0a37afe4 100644 --- a/frontend/app/settings/_components/anthropic-settings-dialog.tsx +++ b/frontend/app/settings/_components/anthropic-settings-dialog.tsx @@ -9,161 +9,161 @@ import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealth import AnthropicLogo from "@/components/icons/anthropic-logo"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { - AnthropicSettingsForm, - type AnthropicSettingsFormData, + AnthropicSettingsForm, + type AnthropicSettingsFormData, } from "./anthropic-settings-form"; import { useRouter } from "next/navigation"; const AnthropicSettingsDialog = ({ - open, - setOpen, + open, + setOpen, }: { - open: boolean; - setOpen: (open: boolean) => void; + open: boolean; + setOpen: (open: boolean) => void; }) => { - const queryClient = useQueryClient(); - const [isValidating, setIsValidating] = useState(false); - const [validationError, setValidationError] = useState(null); - const router = useRouter(); + const queryClient = useQueryClient(); + const [isValidating, setIsValidating] = useState(false); + const [validationError, setValidationError] = useState(null); + const router = useRouter(); - const methods = useForm({ - mode: "onSubmit", - defaultValues: { - apiKey: "", - }, - }); + const methods = useForm({ + mode: "onSubmit", + defaultValues: { + apiKey: "", + }, + }); - const { handleSubmit, watch } = methods; - const apiKey = watch("apiKey"); + const { handleSubmit, watch } = methods; + const apiKey = watch("apiKey"); - const { refetch: validateCredentials } = useGetAnthropicModelsQuery( - { - apiKey: apiKey, - }, - { - enabled: false, - }, - ); + const { refetch: validateCredentials } = useGetAnthropicModelsQuery( + { + apiKey: apiKey, + }, + { + enabled: false, + }, + ); - const settingsMutation = useUpdateSettingsMutation({ - onSuccess: () => { - // Update provider health cache to healthy since backend validated the setup - const healthData: ProviderHealthResponse = { - status: "healthy", - message: "Provider is configured and working correctly", - provider: "anthropic", - }; - queryClient.setQueryData(["provider", "health"], healthData); + const settingsMutation = useUpdateSettingsMutation({ + onSuccess: () => { + // Update provider health cache to healthy since backend validated the setup + const healthData: ProviderHealthResponse = { + status: "healthy", + message: "Provider is configured and working correctly", + provider: "anthropic", + }; + queryClient.setQueryData(["provider", "health"], healthData); - toast.message("Anthropic successfully configured", { - description: "You can now access the provided language models.", - duration: Infinity, - closeButton: true, - icon: , - action: { - label: "Settings", - onClick: () => { - router.push("/settings?focusLlmModel=true"); - }, - }, - }); - setOpen(false); - }, - }); + toast.message("Anthropic successfully configured", { + description: "You can now access the provided language models.", + duration: Infinity, + closeButton: true, + icon: , + action: { + label: "Settings", + onClick: () => { + router.push("/settings?focusLlmModel=true"); + }, + }, + }); + setOpen(false); + }, + }); - const onSubmit = async (data: AnthropicSettingsFormData) => { - // Clear any previous validation errors - setValidationError(null); + const onSubmit = async (data: AnthropicSettingsFormData) => { + // Clear any previous validation errors + setValidationError(null); - // Only validate if a new API key was entered - if (data.apiKey) { - setIsValidating(true); - const result = await validateCredentials(); - setIsValidating(false); + // Only validate if a new API key was entered + if (data.apiKey) { + setIsValidating(true); + const result = await validateCredentials(); + setIsValidating(false); - if (result.isError) { - setValidationError(result.error); - return; - } - } + if (result.isError) { + setValidationError(result.error); + return; + } + } - const payload: { - anthropic_api_key?: string; - } = {}; + const payload: { + anthropic_api_key?: string; + } = {}; - // Only include api_key if a value was entered - if (data.apiKey) { - payload.anthropic_api_key = data.apiKey; - } + // Only include api_key if a value was entered + if (data.apiKey) { + payload.anthropic_api_key = data.apiKey; + } - // Submit the update - settingsMutation.mutate(payload); - }; + // Submit the update + settingsMutation.mutate(payload); + }; - return ( - - - - - - -
- -
- Anthropic Setup -
-
+ return ( + + + + + + +
+ +
+ Anthropic Setup +
+
- + - - {settingsMutation.isError && ( - -

- {settingsMutation.error?.message} -

-
- )} -
- - - - - -
-
-
- ); + + {settingsMutation.isError && ( + +

+ {settingsMutation.error?.message} +

+
+ )} +
+ + + + + +
+
+
+ ); }; export default AnthropicSettingsDialog; diff --git a/frontend/app/settings/_components/ollama-settings-dialog.tsx b/frontend/app/settings/_components/ollama-settings-dialog.tsx index 87addcea..0d46def4 100644 --- a/frontend/app/settings/_components/ollama-settings-dialog.tsx +++ b/frontend/app/settings/_components/ollama-settings-dialog.tsx @@ -10,162 +10,162 @@ import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealth import OllamaLogo from "@/components/icons/ollama-logo"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { useAuth } from "@/contexts/auth-context"; import { - OllamaSettingsForm, - type OllamaSettingsFormData, + OllamaSettingsForm, + type OllamaSettingsFormData, } from "./ollama-settings-form"; import { useRouter } from "next/navigation"; const OllamaSettingsDialog = ({ - open, - setOpen, + open, + setOpen, }: { - open: boolean; - setOpen: (open: boolean) => void; + open: boolean; + setOpen: (open: boolean) => void; }) => { - const { isAuthenticated, isNoAuthMode } = useAuth(); - const queryClient = useQueryClient(); - const [isValidating, setIsValidating] = useState(false); - const [validationError, setValidationError] = useState(null); - const router = useRouter(); + const { isAuthenticated, isNoAuthMode } = useAuth(); + const queryClient = useQueryClient(); + const [isValidating, setIsValidating] = useState(false); + const [validationError, setValidationError] = useState(null); + const router = useRouter(); - const { data: settings = {} } = useGetSettingsQuery({ - enabled: isAuthenticated || isNoAuthMode, - }); + const { data: settings = {} } = useGetSettingsQuery({ + enabled: isAuthenticated || isNoAuthMode, + }); - const isOllamaConfigured = settings.providers?.ollama?.configured === true; + const isOllamaConfigured = settings.providers?.ollama?.configured === true; - const methods = useForm({ - mode: "onSubmit", - defaultValues: { - endpoint: isOllamaConfigured - ? settings.providers?.ollama?.endpoint - : "http://localhost:11434", - }, - }); + const methods = useForm({ + mode: "onSubmit", + defaultValues: { + endpoint: isOllamaConfigured + ? settings.providers?.ollama?.endpoint + : "http://localhost:11434", + }, + }); - const { handleSubmit, watch } = methods; - const endpoint = watch("endpoint"); + const { handleSubmit, watch } = methods; + const endpoint = watch("endpoint"); - const { refetch: validateCredentials } = useGetOllamaModelsQuery( - { - endpoint: endpoint, - }, - { - enabled: false, - }, - ); + const { refetch: validateCredentials } = useGetOllamaModelsQuery( + { + endpoint: endpoint, + }, + { + enabled: false, + }, + ); - const settingsMutation = useUpdateSettingsMutation({ - onSuccess: () => { - // Update provider health cache to healthy since backend validated the setup - const healthData: ProviderHealthResponse = { - status: "healthy", - message: "Provider is configured and working correctly", - provider: "ollama", - }; - queryClient.setQueryData(["provider", "health"], healthData); + const settingsMutation = useUpdateSettingsMutation({ + onSuccess: () => { + // Update provider health cache to healthy since backend validated the setup + const healthData: ProviderHealthResponse = { + status: "healthy", + message: "Provider is configured and working correctly", + provider: "ollama", + }; + queryClient.setQueryData(["provider", "health"], healthData); - toast.message("Ollama successfully configured", { - description: - "You can now access the provided language and embedding models.", - duration: Infinity, - closeButton: true, - icon: , - action: { - label: "Settings", - onClick: () => { - router.push("/settings?focusLlmModel=true"); - }, - }, - }); - setOpen(false); - }, - }); + toast.message("Ollama successfully configured", { + description: + "You can now access the provided language and embedding models.", + duration: Infinity, + closeButton: true, + icon: , + action: { + label: "Settings", + onClick: () => { + router.push("/settings?focusLlmModel=true"); + }, + }, + }); + setOpen(false); + }, + }); - const onSubmit = async (data: OllamaSettingsFormData) => { - // Clear any previous validation errors - setValidationError(null); + const onSubmit = async (data: OllamaSettingsFormData) => { + // Clear any previous validation errors + setValidationError(null); - // Validate endpoint by fetching models - setIsValidating(true); - const result = await validateCredentials(); - setIsValidating(false); + // Validate endpoint by fetching models + setIsValidating(true); + const result = await validateCredentials(); + setIsValidating(false); - if (result.isError) { - setValidationError(result.error); - return; - } + if (result.isError) { + setValidationError(result.error); + return; + } - settingsMutation.mutate({ - ollama_endpoint: data.endpoint, - }); - }; + settingsMutation.mutate({ + ollama_endpoint: data.endpoint, + }); + }; - return ( - - - -
- - -
- -
- Ollama Setup -
-
+ return ( + + + + + + +
+ +
+ Ollama Setup +
+
- + - - {settingsMutation.isError && ( - -

- {settingsMutation.error?.message} -

-
- )} -
- - - - - -
-
-
- ); + + {settingsMutation.isError && ( + +

+ {settingsMutation.error?.message} +

+
+ )} +
+ + + + + +
+
+
+ ); }; export default OllamaSettingsDialog; diff --git a/frontend/app/settings/_components/openai-settings-dialog.tsx b/frontend/app/settings/_components/openai-settings-dialog.tsx index 099c3474..82910c62 100644 --- a/frontend/app/settings/_components/openai-settings-dialog.tsx +++ b/frontend/app/settings/_components/openai-settings-dialog.tsx @@ -9,162 +9,162 @@ import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealth import OpenAILogo from "@/components/icons/openai-logo"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { - OpenAISettingsForm, - type OpenAISettingsFormData, + OpenAISettingsForm, + type OpenAISettingsFormData, } from "./openai-settings-form"; import { useRouter } from "next/navigation"; const OpenAISettingsDialog = ({ - open, - setOpen, + open, + setOpen, }: { - open: boolean; - setOpen: (open: boolean) => void; + open: boolean; + setOpen: (open: boolean) => void; }) => { - const queryClient = useQueryClient(); - const [isValidating, setIsValidating] = useState(false); - const [validationError, setValidationError] = useState(null); - const router = useRouter(); + const queryClient = useQueryClient(); + const [isValidating, setIsValidating] = useState(false); + const [validationError, setValidationError] = useState(null); + const router = useRouter(); - const methods = useForm({ - mode: "onSubmit", - defaultValues: { - apiKey: "", - }, - }); + const methods = useForm({ + mode: "onSubmit", + defaultValues: { + apiKey: "", + }, + }); - const { handleSubmit, watch } = methods; - const apiKey = watch("apiKey"); + const { handleSubmit, watch } = methods; + const apiKey = watch("apiKey"); - const { refetch: validateCredentials } = useGetOpenAIModelsQuery( - { - apiKey: apiKey, - }, - { - enabled: false, - }, - ); + const { refetch: validateCredentials } = useGetOpenAIModelsQuery( + { + apiKey: apiKey, + }, + { + enabled: false, + }, + ); - const settingsMutation = useUpdateSettingsMutation({ - onSuccess: () => { - // Update provider health cache to healthy since backend validated the setup - const healthData: ProviderHealthResponse = { - status: "healthy", - message: "Provider is configured and working correctly", - provider: "openai", - }; - queryClient.setQueryData(["provider", "health"], healthData); + const settingsMutation = useUpdateSettingsMutation({ + onSuccess: () => { + // Update provider health cache to healthy since backend validated the setup + const healthData: ProviderHealthResponse = { + status: "healthy", + message: "Provider is configured and working correctly", + provider: "openai", + }; + queryClient.setQueryData(["provider", "health"], healthData); - toast.message("OpenAI successfully configured", { - description: - "You can now access the provided language and embedding models.", - duration: Infinity, - closeButton: true, - icon: , - action: { - label: "Settings", - onClick: () => { - router.push("/settings?focusLlmModel=true"); - }, - }, - }); - setOpen(false); - }, - }); + toast.message("OpenAI successfully configured", { + description: + "You can now access the provided language and embedding models.", + duration: Infinity, + closeButton: true, + icon: , + action: { + label: "Settings", + onClick: () => { + router.push("/settings?focusLlmModel=true"); + }, + }, + }); + setOpen(false); + }, + }); - const onSubmit = async (data: OpenAISettingsFormData) => { - // Clear any previous validation errors - setValidationError(null); + const onSubmit = async (data: OpenAISettingsFormData) => { + // Clear any previous validation errors + setValidationError(null); - // Only validate if a new API key was entered - if (data.apiKey) { - setIsValidating(true); - const result = await validateCredentials(); - setIsValidating(false); + // Only validate if a new API key was entered + if (data.apiKey) { + setIsValidating(true); + const result = await validateCredentials(); + setIsValidating(false); - if (result.isError) { - setValidationError(result.error); - return; - } - } + if (result.isError) { + setValidationError(result.error); + return; + } + } - const payload: { - openai_api_key?: string; - } = {}; + const payload: { + openai_api_key?: string; + } = {}; - // Only include api_key if a value was entered - if (data.apiKey) { - payload.openai_api_key = data.apiKey; - } + // Only include api_key if a value was entered + if (data.apiKey) { + payload.openai_api_key = data.apiKey; + } - // Submit the update - settingsMutation.mutate(payload); - }; + // Submit the update + settingsMutation.mutate(payload); + }; - return ( - - - -
- - -
- -
- OpenAI Setup -
-
+ return ( + + + + + + +
+ +
+ OpenAI Setup +
+
- + - - {settingsMutation.isError && ( - -

- {settingsMutation.error?.message} -

-
- )} -
- - - - - -
-
-
- ); + + {settingsMutation.isError && ( + +

+ {settingsMutation.error?.message} +

+
+ )} +
+ + + + + +
+
+
+ ); }; export default OpenAISettingsDialog; diff --git a/frontend/app/settings/_components/watsonx-settings-dialog.tsx b/frontend/app/settings/_components/watsonx-settings-dialog.tsx index 5ac7fe01..012c868d 100644 --- a/frontend/app/settings/_components/watsonx-settings-dialog.tsx +++ b/frontend/app/settings/_components/watsonx-settings-dialog.tsx @@ -9,171 +9,171 @@ import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealth import IBMLogo from "@/components/icons/ibm-logo"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { - WatsonxSettingsForm, - type WatsonxSettingsFormData, + WatsonxSettingsForm, + type WatsonxSettingsFormData, } from "./watsonx-settings-form"; import { useRouter } from "next/navigation"; const WatsonxSettingsDialog = ({ - open, - setOpen, + open, + setOpen, }: { - open: boolean; - setOpen: (open: boolean) => void; + open: boolean; + setOpen: (open: boolean) => void; }) => { - const queryClient = useQueryClient(); - const [isValidating, setIsValidating] = useState(false); - const [validationError, setValidationError] = useState(null); - const router = useRouter(); + const queryClient = useQueryClient(); + const [isValidating, setIsValidating] = useState(false); + const [validationError, setValidationError] = useState(null); + const router = useRouter(); - const methods = useForm({ - mode: "onSubmit", - defaultValues: { - endpoint: "https://us-south.ml.cloud.ibm.com", - apiKey: "", - projectId: "", - }, - }); + const methods = useForm({ + mode: "onSubmit", + defaultValues: { + endpoint: "https://us-south.ml.cloud.ibm.com", + apiKey: "", + projectId: "", + }, + }); - const { handleSubmit, watch } = methods; - const endpoint = watch("endpoint"); - const apiKey = watch("apiKey"); - const projectId = watch("projectId"); + const { handleSubmit, watch } = methods; + const endpoint = watch("endpoint"); + const apiKey = watch("apiKey"); + const projectId = watch("projectId"); - const { refetch: validateCredentials } = useGetIBMModelsQuery( - { - endpoint: endpoint, - apiKey: apiKey, - projectId: projectId, - }, - { - enabled: false, - }, - ); + const { refetch: validateCredentials } = useGetIBMModelsQuery( + { + endpoint: endpoint, + apiKey: apiKey, + projectId: projectId, + }, + { + enabled: false, + }, + ); - const settingsMutation = useUpdateSettingsMutation({ - onSuccess: () => { - // Update provider health cache to healthy since backend validated the setup - const healthData: ProviderHealthResponse = { - status: "healthy", - message: "Provider is configured and working correctly", - provider: "watsonx", - }; - queryClient.setQueryData(["provider", "health"], healthData); + const settingsMutation = useUpdateSettingsMutation({ + onSuccess: () => { + // Update provider health cache to healthy since backend validated the setup + const healthData: ProviderHealthResponse = { + status: "healthy", + message: "Provider is configured and working correctly", + provider: "watsonx", + }; + queryClient.setQueryData(["provider", "health"], healthData); - toast.message("IBM watsonx.ai successfully configured", { - description: - "You can now access the provided language and embedding models.", - duration: Infinity, - closeButton: true, - icon: , - action: { - label: "Settings", - onClick: () => { - router.push("/settings?focusLlmModel=true"); - }, - }, - }); - setOpen(false); - }, - }); + toast.message("IBM watsonx.ai successfully configured", { + description: + "You can now access the provided language and embedding models.", + duration: Infinity, + closeButton: true, + icon: , + action: { + label: "Settings", + onClick: () => { + router.push("/settings?focusLlmModel=true"); + }, + }, + }); + setOpen(false); + }, + }); - const onSubmit = async (data: WatsonxSettingsFormData) => { - // Clear any previous validation errors - setValidationError(null); + const onSubmit = async (data: WatsonxSettingsFormData) => { + // Clear any previous validation errors + setValidationError(null); - // Validate credentials by fetching models - setIsValidating(true); - const result = await validateCredentials(); - setIsValidating(false); + // Validate credentials by fetching models + setIsValidating(true); + const result = await validateCredentials(); + setIsValidating(false); - if (result.isError) { - setValidationError(result.error); - return; - } + if (result.isError) { + setValidationError(result.error); + return; + } - const payload: { - watsonx_endpoint: string; - watsonx_api_key?: string; - watsonx_project_id: string; - } = { - watsonx_endpoint: data.endpoint, - watsonx_project_id: data.projectId, - }; + const payload: { + watsonx_endpoint: string; + watsonx_api_key?: string; + watsonx_project_id: string; + } = { + watsonx_endpoint: data.endpoint, + watsonx_project_id: data.projectId, + }; - // Only include api_key if a value was entered - if (data.apiKey) { - payload.watsonx_api_key = data.apiKey; - } + // Only include api_key if a value was entered + if (data.apiKey) { + payload.watsonx_api_key = data.apiKey; + } - // Submit the update - settingsMutation.mutate(payload); - }; + // Submit the update + settingsMutation.mutate(payload); + }; - return ( - - - -
- - -
- -
- IBM watsonx.ai Setup -
-
+ return ( + + + + + + +
+ +
+ IBM watsonx.ai Setup +
+
- + - - {settingsMutation.isError && ( - -

- {settingsMutation.error?.message} -

-
- )} -
- - - - - -
-
-
- ); + + {settingsMutation.isError && ( + +

+ {settingsMutation.error?.message} +

+
+ )} +
+ + + + + +
+
+
+ ); }; export default WatsonxSettingsDialog; diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index c122aa65..9d48133b 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -6,10 +6,10 @@ import { useRouter, useSearchParams } from "next/navigation"; import { Suspense, useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { - useGetAnthropicModelsQuery, - useGetIBMModelsQuery, - useGetOllamaModelsQuery, - useGetOpenAIModelsQuery, + useGetAnthropicModelsQuery, + useGetIBMModelsQuery, + useGetOllamaModelsQuery, + useGetOpenAIModelsQuery, } from "@/app/api/queries/useGetModelsQuery"; import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery"; import { ConfirmationDialog } from "@/components/confirmation-dialog"; @@ -17,11 +17,11 @@ import { LabelWrapper } from "@/components/label-wrapper"; import { ProtectedRoute } from "@/components/protected-route"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -30,9 +30,9 @@ import { Textarea } from "@/components/ui/textarea"; import { useAuth } from "@/contexts/auth-context"; import { useTask } from "@/contexts/task-context"; import { - DEFAULT_AGENT_SETTINGS, - DEFAULT_KNOWLEDGE_SETTINGS, - UI_CONSTANTS, + DEFAULT_AGENT_SETTINGS, + DEFAULT_KNOWLEDGE_SETTINGS, + UI_CONSTANTS, } from "@/lib/constants"; import { useDebounce } from "@/lib/debounce"; import GoogleDriveIcon from "../../components/icons/google-drive-logo"; @@ -47,1279 +47,1279 @@ import { cn } from "@/lib/utils"; const { MAX_SYSTEM_PROMPT_CHARS } = UI_CONSTANTS; interface GoogleDriveFile { - id: string; - name: string; - mimeType: string; - webViewLink?: string; - iconLink?: string; + 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; - }; + 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[]; - available?: boolean; + 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[]; + available?: boolean; } interface SyncResult { - processed?: number; - added?: number; - errors?: number; - skipped?: number; - total?: number; + processed?: number; + added?: number; + errors?: number; + skipped?: number; + total?: number; } interface Connection { - connection_id: string; - is_active: boolean; - created_at: string; - last_sync?: string; + 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(); - const router = useRouter(); - - // Check if we should auto-open the LLM model selector - const focusLlmModel = searchParams.get("focusLlmModel") === "true"; - // Use a trigger state that changes each time we detect the query param - const [openLlmSelector, setOpenLlmSelector] = useState(false); - - // 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); - - // Only keep systemPrompt state since it needs manual save button - const [systemPrompt, setSystemPrompt] = useState(""); - const [chunkSize, setChunkSize] = useState(1024); - const [chunkOverlap, setChunkOverlap] = useState(50); - const [tableStructure, setTableStructure] = useState(true); - const [ocr, setOcr] = useState(false); - const [pictureDescriptions, setPictureDescriptions] = - useState(false); - - // Fetch settings using React Query - const { data: settings = {} } = useGetSettingsQuery({ - enabled: isAuthenticated || isNoAuthMode, - }); - - // Fetch models for each provider - const { data: openaiModels, isLoading: openaiLoading } = - useGetOpenAIModelsQuery( - { apiKey: "" }, - { enabled: settings?.providers?.openai?.configured === true }, - ); - - const { data: anthropicModels, isLoading: anthropicLoading } = - useGetAnthropicModelsQuery( - { apiKey: "" }, - { enabled: settings?.providers?.anthropic?.configured === true }, - ); - - const { data: ollamaModels, isLoading: ollamaLoading } = - useGetOllamaModelsQuery( - { endpoint: settings?.providers?.ollama?.endpoint }, - { - enabled: - settings?.providers?.ollama?.configured === true && - !!settings?.providers?.ollama?.endpoint, - }, - ); - - const { data: watsonxModels, isLoading: watsonxLoading } = - useGetIBMModelsQuery( - { - endpoint: settings?.providers?.watsonx?.endpoint, - apiKey: "", - projectId: settings?.providers?.watsonx?.project_id, - }, - { - enabled: - settings?.providers?.watsonx?.configured === true && - !!settings?.providers?.watsonx?.endpoint && - !!settings?.providers?.watsonx?.project_id, - }, - ); - - // Build grouped LLM model options from all configured providers - const groupedLlmModels = [ - { - group: "OpenAI", - provider: "openai", - icon: getModelLogo("", "openai"), - models: openaiModels?.language_models || [], - configured: settings.providers?.openai?.configured === true, - }, - { - group: "Anthropic", - provider: "anthropic", - icon: getModelLogo("", "anthropic"), - models: anthropicModels?.language_models || [], - configured: settings.providers?.anthropic?.configured === true, - }, - { - group: "Ollama", - provider: "ollama", - icon: getModelLogo("", "ollama"), - models: ollamaModels?.language_models || [], - configured: settings.providers?.ollama?.configured === true, - }, - { - group: "IBM watsonx.ai", - provider: "watsonx", - icon: getModelLogo("", "watsonx"), - models: watsonxModels?.language_models || [], - configured: settings.providers?.watsonx?.configured === true, - }, - ] - .filter((provider) => provider.configured) - .map((provider) => ({ - group: provider.group, - icon: provider.icon, - options: provider.models.map((model) => ({ - ...model, - provider: provider.provider, - })), - })) - .filter((provider) => provider.options.length > 0); - - // Build grouped embedding model options from all configured providers (excluding Anthropic) - const groupedEmbeddingModels = [ - { - group: "OpenAI", - provider: "openai", - icon: getModelLogo("", "openai"), - models: openaiModels?.embedding_models || [], - configured: settings.providers?.openai?.configured === true, - }, - { - group: "Ollama", - provider: "ollama", - icon: getModelLogo("", "ollama"), - models: ollamaModels?.embedding_models || [], - configured: settings.providers?.ollama?.configured === true, - }, - { - group: "IBM watsonx.ai", - provider: "watsonx", - icon: getModelLogo("", "watsonx"), - models: watsonxModels?.embedding_models || [], - configured: settings.providers?.watsonx?.configured === true, - }, - ] - .filter((provider) => provider.configured) - .map((provider) => ({ - group: provider.group, - icon: provider.icon, - options: provider.models.map((model) => ({ - ...model, - provider: provider.provider, - })), - })) - .filter((provider) => provider.options.length > 0); - - const isLoadingAnyLlmModels = - openaiLoading || anthropicLoading || ollamaLoading || watsonxLoading; - const isLoadingAnyEmbeddingModels = - openaiLoading || ollamaLoading || watsonxLoading; - - // Mutations - const updateSettingsMutation = useUpdateSettingsMutation({ - onSuccess: () => { - toast.success("Settings updated successfully"); - }, - onError: (error) => { - toast.error("Failed to update settings", { - description: error.message, - }); - }, - }); - - // Debounced update function - const debouncedUpdate = useDebounce( - (variables: Parameters[0]) => { - updateSettingsMutation.mutate(variables); - }, - 500, - ); - - // Sync system prompt state with settings data - useEffect(() => { - if (settings.agent?.system_prompt) { - setSystemPrompt(settings.agent.system_prompt); - } - }, [settings.agent?.system_prompt]); - - // Sync chunk size and overlap state with settings data - useEffect(() => { - if (settings.knowledge?.chunk_size) { - setChunkSize(settings.knowledge.chunk_size); - } - }, [settings.knowledge?.chunk_size]); - - useEffect(() => { - if (settings.knowledge?.chunk_overlap) { - setChunkOverlap(settings.knowledge.chunk_overlap); - } - }, [settings.knowledge?.chunk_overlap]); - - // Sync docling settings with settings data - useEffect(() => { - if (settings.knowledge?.table_structure !== undefined) { - setTableStructure(settings.knowledge.table_structure); - } - }, [settings.knowledge?.table_structure]); - - useEffect(() => { - if (settings.knowledge?.ocr !== undefined) { - setOcr(settings.knowledge.ocr); - } - }, [settings.knowledge?.ocr]); - - useEffect(() => { - if (settings.knowledge?.picture_descriptions !== undefined) { - setPictureDescriptions(settings.knowledge.picture_descriptions); - } - }, [settings.knowledge?.picture_descriptions]); - - // Handle auto-focus on LLM model selector when coming from provider setup - useEffect(() => { - if (focusLlmModel) { - // Trigger the selector to open - setOpenLlmSelector(true); - - // Scroll to the agent card - const agentCard = document.getElementById("agent-card"); - if (agentCard) { - agentCard.scrollIntoView({ behavior: "smooth", block: "start" }); - } - - // Clear the query parameter - const newSearchParams = new URLSearchParams(searchParams.toString()); - newSearchParams.delete("focusLlmModel"); - router.replace(`/settings?${newSearchParams.toString()}`, { - scroll: false, - }); - - // Reset the trigger after a brief delay so it can be triggered again - setTimeout(() => setOpenLlmSelector(false), 100); - } - }, [focusLlmModel, searchParams, router]); - - // Update model selection immediately (also updates provider) - const handleModelChange = (newModel: string, provider?: string) => { - if (newModel && provider) { - updateSettingsMutation.mutate({ - llm_model: newModel, - llm_provider: provider, - }); - } else if (newModel) { - updateSettingsMutation.mutate({ llm_model: newModel }); - } - }; - - // Update system prompt with save button - const handleSystemPromptSave = () => { - updateSettingsMutation.mutate({ system_prompt: systemPrompt }); - }; - - // Update embedding model selection immediately (also updates provider) - const handleEmbeddingModelChange = (newModel: string, provider?: string) => { - if (newModel && provider) { - updateSettingsMutation.mutate({ - embedding_model: newModel, - embedding_provider: provider, - }); - } else if (newModel) { - updateSettingsMutation.mutate({ embedding_model: newModel }); - } - }; - - // Update chunk size setting with debounce - const handleChunkSizeChange = (value: string) => { - const numValue = Math.max(0, parseInt(value) || 0); - setChunkSize(numValue); - debouncedUpdate({ chunk_size: numValue }); - }; - - // Update chunk overlap setting with debounce - const handleChunkOverlapChange = (value: string) => { - const numValue = Math.max(0, parseInt(value) || 0); - setChunkOverlap(numValue); - debouncedUpdate({ chunk_overlap: numValue }); - }; - - // Update docling settings - const handleTableStructureChange = (checked: boolean) => { - setTableStructure(checked); - updateSettingsMutation.mutate({ table_structure: checked }); - }; - - const handleOcrChange = (checked: boolean) => { - setOcr(checked); - updateSettingsMutation.mutate({ ocr: checked }); - }; - - const handlePictureDescriptionsChange = (checked: boolean) => { - setPictureDescriptions(checked); - updateSettingsMutation.mutate({ picture_descriptions: checked }); - }; - - // Helper function to get connector icon - const getConnectorIcon = useCallback((iconName: string) => { - const iconMap: { [key: string]: React.ReactElement } = { - "google-drive": , - sharepoint: , - onedrive: , - }; - 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, - available: connectorsResult.connectors[type].available, - })); - - 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); - } - }, [getConnectorIcon]); - - 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 navigateToKnowledgePage = (connector: Connector) => { - const provider = connector.type.replace(/-/g, "_"); - router.push(`/upload/${provider}`); - }; - - // 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", - closeDialog: () => void, - ) => { - // Select the appropriate flow ID and edit URL based on flow type - 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 = ( - 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 - }; - - const handleRestoreRetrievalFlow = (closeDialog: () => void) => { - fetch(`/api/reset-flow/retrieval`, { - method: "POST", - }) - .then((response) => { - if (response.ok) { - return response.json(); - } - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - }) - .then(() => { - // Only reset form values if the API call was successful - setSystemPrompt(DEFAULT_AGENT_SETTINGS.system_prompt); - // Trigger model update to default model - handleModelChange(DEFAULT_AGENT_SETTINGS.llm_model); - 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) => { - if (response.ok) { - return response.json(); - } - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - }) - .then(() => { - // Only reset form values if the API call was successful - setChunkSize(DEFAULT_KNOWLEDGE_SETTINGS.chunk_size); - setChunkOverlap(DEFAULT_KNOWLEDGE_SETTINGS.chunk_overlap); - setTableStructure(false); - setOcr(false); - setPictureDescriptions(false); - closeDialog(); // Close after successful completion - }) - .catch((error) => { - console.error("Error restoring ingest flow:", error); - closeDialog(); // Close even on error (could show error toast instead) - }); - }; - - return ( -
- {/* Connectors Section */} -
-
-

- Cloud Connectors -

-
- - {/* Conditional Sync Settings or No-Auth Message */} - { - isNoAuthMode ? ( - - - - Cloud connectors require authentication - - - Add the Google OAuth variables below to your .env{" "} - then restart the OpenRAG containers. - - - -
-
-
- - 27 - - # Google OAuth -
-
- - 28 - - # Create credentials here: -
-
- - 29 - - - # https://console.cloud.google.com/apis/credentials - -
-
-
- 30 - GOOGLE_OAUTH_CLIENT_ID= -
-
- 31 - GOOGLE_OAUTH_CLIENT_SECRET= -
-
-
-
- ) : null - //
- //
- //

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) => { - return ( - - -
-
-
-
- {connector.icon} -
-
- - {connector.name} - - - {connector?.available - ? `${connector.name} is configured.` - : "Not configured."} - -
-
-
- - {connector?.available ? ( -
- {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} -
- )} -
- )} - - ) : ( - - )} -
- ) : ( -
-

- See our{" "} - - Cloud Connectors installation guide - {" "} - for more detail. -

-
- )} -
-
- ); - })} -
-
- - {/* Model Providers Section */} -
-
-

- Model Providers -

-
- -
- - {/* Agent Behavior Section */} - - -
- Agent -
- - 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} - /> - - - Langflow icon - - - - - 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. -

-

You can restore this flow from Settings.

- - } - confirmText="Proceed" - confirmIcon={} - onConfirm={(closeDialog) => - handleEditInLangflow("chat", closeDialog) - } - variant="warning" - /> -
-
- - This Agent retrieves from your knowledge and generates chat - responses. Edit in Langflow for full control. - -
- -
-
- - - -
-
- -