From f51890a90f9ec312152224cadd289d3f0ec65ed4 Mon Sep 17 00:00:00 2001 From: Mike Fortman Date: Tue, 14 Oct 2025 16:32:40 -0500 Subject: [PATCH] break up chat and onboarding components --- .../app/chat/components/assistant-message.tsx | 57 ++ .../src/app/chat/components/chat-input.tsx | 284 ++++++++ .../app/chat/components/function-calls.tsx | 242 +++++++ .../src/app/chat/components/user-message.tsx | 31 + frontend/src/app/chat/page.tsx | 636 ++---------------- frontend/src/app/chat/types.ts | 53 ++ .../onboarding/components/onboarding-card.tsx | 195 ++++++ frontend/src/app/onboarding/page.tsx | 182 +---- 8 files changed, 927 insertions(+), 753 deletions(-) create mode 100644 frontend/src/app/chat/components/assistant-message.tsx create mode 100644 frontend/src/app/chat/components/chat-input.tsx create mode 100644 frontend/src/app/chat/components/function-calls.tsx create mode 100644 frontend/src/app/chat/components/user-message.tsx create mode 100644 frontend/src/app/chat/types.ts create mode 100644 frontend/src/app/onboarding/components/onboarding-card.tsx diff --git a/frontend/src/app/chat/components/assistant-message.tsx b/frontend/src/app/chat/components/assistant-message.tsx new file mode 100644 index 00000000..d52460c1 --- /dev/null +++ b/frontend/src/app/chat/components/assistant-message.tsx @@ -0,0 +1,57 @@ +import { Bot, GitBranch } from "lucide-react"; +import { MarkdownRenderer } from "@/components/markdown-renderer"; +import { FunctionCalls } from "./function-calls"; +import type { FunctionCall } from "../types"; + +interface AssistantMessageProps { + content: string; + functionCalls?: FunctionCall[]; + messageIndex?: number; + expandedFunctionCalls: Set; + onToggle: (functionCallId: string) => void; + isStreaming?: boolean; + showForkButton?: boolean; + onFork?: (e: React.MouseEvent) => void; +} + +export function AssistantMessage({ + content, + functionCalls = [], + messageIndex, + expandedFunctionCalls, + onToggle, + isStreaming = false, + showForkButton = false, + onFork, +}: AssistantMessageProps) { + return ( +
+
+ +
+
+ + + {isStreaming && ( + + )} +
+ {showForkButton && onFork && ( +
+ +
+ )} +
+ ); +} diff --git a/frontend/src/app/chat/components/chat-input.tsx b/frontend/src/app/chat/components/chat-input.tsx new file mode 100644 index 00000000..01494e12 --- /dev/null +++ b/frontend/src/app/chat/components/chat-input.tsx @@ -0,0 +1,284 @@ +import { Check, Funnel, Loader2, Plus, X } from "lucide-react"; +import TextareaAutosize from "react-textarea-autosize"; +import { forwardRef, useImperativeHandle, useRef } from "react"; +import { filterAccentClasses } from "@/components/knowledge-filter-panel"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverAnchor, + PopoverContent, +} from "@/components/ui/popover"; +import type { KnowledgeFilterData } from "../types"; +import { FilterColor } from "@/components/filter-icon-popover"; + +export interface ChatInputHandle { + focusInput: () => void; + clickFileInput: () => void; +} + +interface ChatInputProps { + input: string; + loading: boolean; + isUploading: boolean; + selectedFilter: KnowledgeFilterData | null; + isFilterHighlighted: boolean; + isFilterDropdownOpen: boolean; + availableFilters: KnowledgeFilterData[]; + filterSearchTerm: string; + selectedFilterIndex: number; + anchorPosition: { x: number; y: number } | null; + textareaHeight: number; + parsedFilterData: { color?: FilterColor } | null; + onSubmit: (e: React.FormEvent) => void; + onChange: (e: React.ChangeEvent) => void; + onKeyDown: (e: React.KeyboardEvent) => void; + onHeightChange: (height: number) => void; + onFilterSelect: (filter: KnowledgeFilterData | null) => void; + onAtClick: () => void; + onFilePickerChange: (e: React.ChangeEvent) => void; + onFilePickerClick: () => void; + setSelectedFilter: (filter: KnowledgeFilterData | null) => void; + setIsFilterHighlighted: (highlighted: boolean) => void; + setIsFilterDropdownOpen: (open: boolean) => void; +} + +export const ChatInput = forwardRef(( + { + input, + loading, + isUploading, + selectedFilter, + isFilterHighlighted, + isFilterDropdownOpen, + availableFilters, + filterSearchTerm, + selectedFilterIndex, + anchorPosition, + textareaHeight, + parsedFilterData, + onSubmit, + onChange, + onKeyDown, + onHeightChange, + onFilterSelect, + onAtClick, + onFilePickerChange, + onFilePickerClick, + setSelectedFilter, + setIsFilterHighlighted, + setIsFilterDropdownOpen, + }, + ref +) => { + const inputRef = useRef(null); + const fileInputRef = useRef(null); + + useImperativeHandle(ref, () => ({ + focusInput: () => { + inputRef.current?.focus(); + }, + clickFileInput: () => { + fileInputRef.current?.click(); + }, + })); + + return ( +
+
+
+
+ {selectedFilter && ( +
+ + @filter:{selectedFilter.name} + + +
+ )} +
+ + {/* Safe area at bottom for buttons */} +
+
+
+ + + { + setIsFilterDropdownOpen(open); + }} + > + {anchorPosition && ( + +
+ + )} + { + // Prevent auto focus on the popover content + e.preventDefault(); + // Keep focus on the input + }} + > +
+ {filterSearchTerm && ( +
+ Searching: @{filterSearchTerm} +
+ )} + {availableFilters.length === 0 ? ( +
+ No knowledge filters available +
+ ) : ( + <> + {!filterSearchTerm && ( + + )} + {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}" +
+ )} + + )} +
+
+ + + + +
+
+ ); +}); + +ChatInput.displayName = "ChatInput"; diff --git a/frontend/src/app/chat/components/function-calls.tsx b/frontend/src/app/chat/components/function-calls.tsx new file mode 100644 index 00000000..3a306a0f --- /dev/null +++ b/frontend/src/app/chat/components/function-calls.tsx @@ -0,0 +1,242 @@ +import { ChevronDown, ChevronRight, Settings } from "lucide-react"; +import type { FunctionCall } from "../types"; + +interface FunctionCallsProps { + functionCalls: FunctionCall[]; + messageIndex?: number; + expandedFunctionCalls: Set; + onToggle: (functionCallId: string) => void; +} + +export function FunctionCalls({ + functionCalls, + messageIndex, + expandedFunctionCalls, + onToggle, +}: FunctionCallsProps) { + if (!functionCalls || functionCalls.length === 0) return null; + + return ( +
+ {functionCalls.map((fc, index) => { + const functionCallId = `${messageIndex || "streaming"}-${index}`; + const isExpanded = expandedFunctionCalls.has(functionCallId); + + // Determine display name - show both name and type if available + const displayName = + fc.type && fc.type !== fc.name + ? `${fc.name} (${fc.type})` + : fc.name; + + return ( +
+
onToggle(functionCallId)} + > + + + Function Call: {displayName} + + {fc.id && ( + + {fc.id.substring(0, 8)}... + + )} +
+ {fc.status} +
+ {isExpanded ? ( + + ) : ( + + )} +
+ + {isExpanded && ( +
+ {/* Show type information if available */} + {fc.type && ( +
+ Type: + + {fc.type} + +
+ )} + + {/* Show ID if available */} + {fc.id && ( +
+ ID: + + {fc.id} + +
+ )} + + {/* Show arguments - either completed or streaming */} + {(fc.arguments || fc.argumentsString) && ( +
+ Arguments: +
+                      {fc.arguments
+                        ? JSON.stringify(fc.arguments, null, 2)
+                        : fc.argumentsString || "..."}
+                    
+
+ )} + + {fc.result && ( +
+ Result: + {Array.isArray(fc.result) ? ( +
+ {(() => { + // Handle different result formats + let resultsToRender = fc.result; + + // Check if this is function_call format with nested results + // Function call format: results = [{ results: [...] }] + // Tool call format: results = [{ text_key: ..., data: {...} }] + if ( + fc.result.length > 0 && + fc.result[0]?.results && + Array.isArray(fc.result[0].results) && + !fc.result[0].text_key + ) { + resultsToRender = fc.result[0].results; + } + + type ToolResultItem = { + text_key?: string; + data?: { file_path?: string; text?: string }; + filename?: string; + page?: number; + score?: number; + source_url?: string | null; + text?: string; + }; + const items = + resultsToRender as unknown as ToolResultItem[]; + return items.map((result, idx: number) => ( +
+ {/* Handle tool_call format (file_path in data) */} + {result.data?.file_path && ( +
+ 📄 {result.data.file_path || "Unknown file"} +
+ )} + + {/* Handle function_call format (filename directly) */} + {result.filename && !result.data?.file_path && ( +
+ 📄 {result.filename} + {result.page && ` (page ${result.page})`} + {result.score && ( + + Score: {result.score.toFixed(3)} + + )} +
+ )} + + {/* Handle tool_call text format */} + {result.data?.text && ( +
+ {result.data.text.length > 300 + ? result.data.text.substring(0, 300) + + "..." + : result.data.text} +
+ )} + + {/* Handle function_call text format */} + {result.text && !result.data?.text && ( +
+ {result.text.length > 300 + ? result.text.substring(0, 300) + "..." + : result.text} +
+ )} + + {/* Show additional metadata for function_call format */} + {result.source_url && ( + + )} + + {result.text_key && ( +
+ Key: {result.text_key} +
+ )} +
+ )); + })()} +
+ Found{" "} + {(() => { + let resultsToCount = fc.result; + if ( + fc.result.length > 0 && + fc.result[0]?.results && + Array.isArray(fc.result[0].results) && + !fc.result[0].text_key + ) { + resultsToCount = fc.result[0].results; + } + return resultsToCount.length; + })()}{" "} + result + {(() => { + let resultsToCount = fc.result; + if ( + fc.result.length > 0 && + fc.result[0]?.results && + Array.isArray(fc.result[0].results) && + !fc.result[0].text_key + ) { + resultsToCount = fc.result[0].results; + } + return resultsToCount.length !== 1 ? "s" : ""; + })()} +
+
+ ) : ( +
+                        {JSON.stringify(fc.result, null, 2)}
+                      
+ )} +
+ )} +
+ )} +
+ ); + })} +
+ ); +} diff --git a/frontend/src/app/chat/components/user-message.tsx b/frontend/src/app/chat/components/user-message.tsx new file mode 100644 index 00000000..5c01b7e8 --- /dev/null +++ b/frontend/src/app/chat/components/user-message.tsx @@ -0,0 +1,31 @@ +import { User } from "lucide-react"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { useAuth } from "@/contexts/auth-context"; + +interface UserMessageProps { + content: string; +} + +export function UserMessage({ content }: UserMessageProps) { + const { user } = useAuth(); + + return ( +
+ + + + {user?.name ? ( + user.name.charAt(0).toUpperCase() + ) : ( + + )} + + +
+

+ {content} +

+
+
+ ); +} diff --git a/frontend/src/app/chat/page.tsx b/frontend/src/app/chat/page.tsx index 8bae1e84..2ae927d1 100644 --- a/frontend/src/app/chat/page.tsx +++ b/frontend/src/app/chat/page.tsx @@ -1,98 +1,31 @@ "use client"; -import { - Bot, - Check, - ChevronDown, - ChevronRight, - Funnel, - GitBranch, - Loader2, - Plus, - Settings, - User, - X, - Zap, -} from "lucide-react"; +import { Bot, Loader2, Zap } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import TextareaAutosize from "react-textarea-autosize"; -import { filterAccentClasses } from "@/components/knowledge-filter-panel"; -import { MarkdownRenderer } from "@/components/markdown-renderer"; import { ProtectedRoute } from "@/components/protected-route"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { - Popover, - PopoverAnchor, - PopoverContent, -} from "@/components/ui/popover"; -import { useAuth } from "@/contexts/auth-context"; import { type EndpointType, useChat } from "@/contexts/chat-context"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; import { useTask } from "@/contexts/task-context"; import { useLoadingStore } from "@/stores/loadingStore"; import { useGetNudgesQuery } from "../api/queries/useGetNudgesQuery"; import Nudges from "./nudges"; - -interface Message { - role: "user" | "assistant"; - content: string; - timestamp: Date; - functionCalls?: FunctionCall[]; - isStreaming?: boolean; -} - -interface FunctionCall { - name: string; - arguments?: Record; - result?: Record | ToolCallResult[]; - status: "pending" | "completed" | "error"; - argumentsString?: string; - id?: string; - type?: string; -} - -interface ToolCallResult { - text_key?: string; - data?: { - file_path?: string; - text?: string; - [key: string]: unknown; - }; - default_value?: string; - [key: string]: unknown; -} - -interface SelectedFilters { - data_sources: string[]; - document_types: string[]; - owners: string[]; -} - -interface KnowledgeFilterData { - id: string; - name: string; - description: string; - query_data: string; - owner: string; - created_at: string; - updated_at: string; -} - -interface RequestBody { - prompt: string; - stream?: boolean; - previous_response_id?: string; - filters?: SelectedFilters; - limit?: number; - scoreThreshold?: number; -} +import { UserMessage } from "./components/user-message"; +import { AssistantMessage } from "./components/assistant-message"; +import { ChatInput, type ChatInputHandle } from "./components/chat-input"; +import { Button } from "@/components/ui/button"; +import type { + Message, + FunctionCall, + ToolCallResult, + SelectedFilters, + KnowledgeFilterData, + RequestBody, +} from "./types"; function ChatPage() { const isDebugMode = process.env.NODE_ENV === "development" || process.env.NEXT_PUBLIC_OPENRAG_DEBUG === "true"; - const { user } = useAuth(); const { endpoint, setEndpoint, @@ -143,8 +76,7 @@ function ChatPage() { y: number; } | null>(null); const messagesEndRef = useRef(null); - const inputRef = useRef(null); - const fileInputRef = useRef(null); + const chatInputRef = useRef(null); const streamAbortRef = useRef(null); const streamIdRef = useRef(0); const lastLoadedConversationRef = useRef(null); @@ -337,7 +269,7 @@ function ChatPage() { }; const handleFilePickerClick = () => { - fileInputRef.current?.click(); + chatInputRef.current?.clickFileInput(); }; const handleFilePickerChange = (e: React.ChangeEvent) => { @@ -345,10 +277,6 @@ function ChatPage() { if (files && files.length > 0) { handleFileUpload(files[0]); } - // Reset the input so the same file can be selected again - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } }; const loadAvailableFilters = async () => { @@ -412,7 +340,7 @@ function ChatPage() { // Auto-focus the input on component mount useEffect(() => { - inputRef.current?.focus(); + chatInputRef.current?.focusInput(); }, []); // Explicitly handle external new conversation trigger @@ -439,7 +367,7 @@ function ChatPage() { }; const handleFocusInput = () => { - inputRef.current?.focus(); + chatInputRef.current?.focusInput(); }; window.addEventListener("newConversation", handleNewConversation); @@ -1651,236 +1579,6 @@ function ChatPage() { // This new forked conversation will get its own response_id when the user sends the next message }; - const renderFunctionCalls = ( - functionCalls: FunctionCall[], - messageIndex?: number - ) => { - if (!functionCalls || functionCalls.length === 0) return null; - - return ( -
- {functionCalls.map((fc, index) => { - const functionCallId = `${messageIndex || "streaming"}-${index}`; - const isExpanded = expandedFunctionCalls.has(functionCallId); - - // Determine display name - show both name and type if available - const displayName = - fc.type && fc.type !== fc.name - ? `${fc.name} (${fc.type})` - : fc.name; - - return ( -
-
toggleFunctionCall(functionCallId)} - > - - - Function Call: {displayName} - - {fc.id && ( - - {fc.id.substring(0, 8)}... - - )} -
- {fc.status} -
- {isExpanded ? ( - - ) : ( - - )} -
- - {isExpanded && ( -
- {/* Show type information if available */} - {fc.type && ( -
- Type: - - {fc.type} - -
- )} - - {/* Show ID if available */} - {fc.id && ( -
- ID: - - {fc.id} - -
- )} - - {/* Show arguments - either completed or streaming */} - {(fc.arguments || fc.argumentsString) && ( -
- Arguments: -
-                        {fc.arguments
-                          ? JSON.stringify(fc.arguments, null, 2)
-                          : fc.argumentsString || "..."}
-                      
-
- )} - - {fc.result && ( -
- Result: - {Array.isArray(fc.result) ? ( -
- {(() => { - // Handle different result formats - let resultsToRender = fc.result; - - // Check if this is function_call format with nested results - // Function call format: results = [{ results: [...] }] - // Tool call format: results = [{ text_key: ..., data: {...} }] - if ( - fc.result.length > 0 && - fc.result[0]?.results && - Array.isArray(fc.result[0].results) && - !fc.result[0].text_key - ) { - resultsToRender = fc.result[0].results; - } - - type ToolResultItem = { - text_key?: string; - data?: { file_path?: string; text?: string }; - filename?: string; - page?: number; - score?: number; - source_url?: string | null; - text?: string; - }; - const items = - resultsToRender as unknown as ToolResultItem[]; - return items.map((result, idx: number) => ( -
- {/* Handle tool_call format (file_path in data) */} - {result.data?.file_path && ( -
- 📄 {result.data.file_path || "Unknown file"} -
- )} - - {/* Handle function_call format (filename directly) */} - {result.filename && !result.data?.file_path && ( -
- 📄 {result.filename} - {result.page && ` (page ${result.page})`} - {result.score && ( - - Score: {result.score.toFixed(3)} - - )} -
- )} - - {/* Handle tool_call text format */} - {result.data?.text && ( -
- {result.data.text.length > 300 - ? result.data.text.substring(0, 300) + - "..." - : result.data.text} -
- )} - - {/* Handle function_call text format */} - {result.text && !result.data?.text && ( -
- {result.text.length > 300 - ? result.text.substring(0, 300) + "..." - : result.text} -
- )} - - {/* Show additional metadata for function_call format */} - {result.source_url && ( - - )} - - {result.text_key && ( -
- Key: {result.text_key} -
- )} -
- )); - })()} -
- Found{" "} - {(() => { - let resultsToCount = fc.result; - if ( - fc.result.length > 0 && - fc.result[0]?.results && - Array.isArray(fc.result[0].results) && - !fc.result[0].text_key - ) { - resultsToCount = fc.result[0].results; - } - return resultsToCount.length; - })()}{" "} - result - {(() => { - let resultsToCount = fc.result; - if ( - fc.result.length > 0 && - fc.result[0]?.results && - Array.isArray(fc.result[0].results) && - !fc.result[0].text_key - ) { - resultsToCount = fc.result[0].results; - } - return resultsToCount.length !== 1 ? "s" : ""; - })()} -
-
- ) : ( -
-                          {JSON.stringify(fc.result, null, 2)}
-                        
- )} -
- )} -
- )} -
- ); - })} -
- ); - }; const handleSuggestionClick = (suggestion: string) => { handleSendMessage(suggestion); @@ -1915,7 +1613,7 @@ function ChatPage() { setDropdownDismissed(true); // Keep focus on the textarea so user can continue typing normally - inputRef.current?.focus(); + chatInputRef.current?.focusInput(); return; } @@ -2121,74 +1819,33 @@ function ChatPage() { {messages.map((message, index) => (
{message.role === "user" && ( -
- - - - {user?.name ? ( - user.name.charAt(0).toUpperCase() - ) : ( - - )} - - -
-

- {message.content} -

-
-
+ )} {message.role === "assistant" && ( -
-
- -
-
- {renderFunctionCalls( - message.functionCalls || [], - index - )} - -
- {endpoint === "chat" && ( -
- -
- )} -
+ handleForkConversation(index, e)} + /> )}
))} {/* Streaming Message Display */} {streamingMessage && ( -
-
- -
-
- {renderFunctionCalls( - streamingMessage.functionCalls, - messages.length - )} - - -
-
+ )} {/* Loading animation - shows immediately after user submits */} @@ -2223,201 +1880,32 @@ function ChatPage() { )} {/* Input Area - Fixed at bottom */} -
-
-
-
- {selectedFilter && ( -
- - @filter:{selectedFilter.name} - - -
- )} -
- setTextareaHeight(height)} - maxRows={7} - minRows={2} - placeholder="Type to ask a question..." - disabled={loading} - className={`w-full bg-transparent px-4 ${ - selectedFilter ? "pt-2" : "pt-4" - } focus-visible:outline-none resize-none`} - rows={2} - /> - {/* Safe area at bottom for buttons */} -
-
-
- - - { - setIsFilterDropdownOpen(open); - }} - > - {anchorPosition && ( - -
- - )} - { - // Prevent auto focus on the popover content - e.preventDefault(); - // Keep focus on the input - }} - > -
- {filterSearchTerm && ( -
- Searching: @{filterSearchTerm} -
- )} - {availableFilters.length === 0 ? ( -
- No knowledge filters available -
- ) : ( - <> - {!filterSearchTerm && ( - - )} - {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}" -
- )} - - )} -
-
- - - - -
-
+ setTextareaHeight(height)} + onFilterSelect={handleFilterSelect} + onAtClick={onAtClick} + onFilePickerChange={handleFilePickerChange} + onFilePickerClick={handleFilePickerClick} + setSelectedFilter={setSelectedFilter} + setIsFilterHighlighted={setIsFilterHighlighted} + setIsFilterDropdownOpen={setIsFilterDropdownOpen} + />
); } diff --git a/frontend/src/app/chat/types.ts b/frontend/src/app/chat/types.ts new file mode 100644 index 00000000..507dfe09 --- /dev/null +++ b/frontend/src/app/chat/types.ts @@ -0,0 +1,53 @@ +export interface Message { + role: "user" | "assistant"; + content: string; + timestamp: Date; + functionCalls?: FunctionCall[]; + isStreaming?: boolean; +} + +export interface FunctionCall { + name: string; + arguments?: Record; + result?: Record | ToolCallResult[]; + status: "pending" | "completed" | "error"; + argumentsString?: string; + id?: string; + type?: string; +} + +export interface ToolCallResult { + text_key?: string; + data?: { + file_path?: string; + text?: string; + [key: string]: unknown; + }; + default_value?: string; + [key: string]: unknown; +} + +export interface SelectedFilters { + data_sources: string[]; + document_types: string[]; + owners: string[]; +} + +export interface KnowledgeFilterData { + id: string; + name: string; + description: string; + query_data: string; + owner: string; + created_at: string; + updated_at: string; +} + +export interface RequestBody { + prompt: string; + stream?: boolean; + previous_response_id?: string; + filters?: SelectedFilters; + limit?: number; + scoreThreshold?: number; +} diff --git a/frontend/src/app/onboarding/components/onboarding-card.tsx b/frontend/src/app/onboarding/components/onboarding-card.tsx new file mode 100644 index 00000000..c8b6dc09 --- /dev/null +++ b/frontend/src/app/onboarding/components/onboarding-card.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { + type OnboardingVariables, + useOnboardingMutation, +} from "@/app/api/mutations/useOnboardingMutation"; +import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery"; +import IBMLogo from "@/components/logo/ibm-logo"; +import OllamaLogo from "@/components/logo/ollama-logo"; +import OpenAILogo from "@/components/logo/openai-logo"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, +} from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { IBMOnboarding } from "./ibm-onboarding"; +import { OllamaOnboarding } from "./ollama-onboarding"; +import { OpenAIOnboarding } from "./openai-onboarding"; + +const OnboardingCard = ({ + isDoclingHealthy, + boarderless = false, +}: { + isDoclingHealthy: boolean; + boarderless?: boolean; +}) => { + const { data: settingsDb, isLoading: isSettingsLoading } = + useGetSettingsQuery(); + + const redirect = "/"; + const router = useRouter(); + + // Redirect if already authenticated or in no-auth mode + useEffect(() => { + if (!isSettingsLoading && settingsDb && settingsDb.edited) { + router.push(redirect); + } + }, [isSettingsLoading, settingsDb, router]); + + + const [modelProvider, setModelProvider] = useState("openai"); + + const [sampleDataset, setSampleDataset] = useState(true); + + const handleSetModelProvider = (provider: string) => { + setModelProvider(provider); + setSettings({ + model_provider: provider, + embedding_model: "", + llm_model: "", + }); + }; + + const [settings, setSettings] = useState({ + model_provider: modelProvider, + embedding_model: "", + llm_model: "", + }); + + // Mutations + const onboardingMutation = useOnboardingMutation({ + onSuccess: (data) => { + console.log("Onboarding completed successfully", data); + router.push(redirect); + }, + onError: (error) => { + toast.error("Failed to complete onboarding", { + description: error.message, + }); + }, + }); + + const handleComplete = () => { + if ( + !settings.model_provider || + !settings.llm_model || + !settings.embedding_model + ) { + toast.error("Please complete all required fields"); + return; + } + + // Prepare onboarding data + const onboardingData: OnboardingVariables = { + model_provider: settings.model_provider, + llm_model: settings.llm_model, + embedding_model: settings.embedding_model, + sample_data: sampleDataset, + }; + + // Add API key if available + if (settings.api_key) { + onboardingData.api_key = settings.api_key; + } + + // Add endpoint if available + if (settings.endpoint) { + onboardingData.endpoint = settings.endpoint; + } + + // Add project_id if available + if (settings.project_id) { + onboardingData.project_id = settings.project_id; + } + + onboardingMutation.mutate(onboardingData); + }; + + const isComplete = !!settings.llm_model && !!settings.embedding_model && isDoclingHealthy; + + return ( + + + + + + + OpenAI + + + + IBM watsonx.ai + + + + Ollama + + + + + + + + + + + + + + + + + + +
+ +
+
+ {!isComplete && ( + + {!!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/src/app/onboarding/page.tsx b/frontend/src/app/onboarding/page.tsx index 094349c7..e48ce7a1 100644 --- a/frontend/src/app/onboarding/page.tsx +++ b/frontend/src/app/onboarding/page.tsx @@ -1,123 +1,15 @@ "use client"; -import { useRouter } from "next/navigation"; -import { Suspense, useEffect, useState } from "react"; -import { toast } from "sonner"; -import { - type OnboardingVariables, - useOnboardingMutation, -} from "@/app/api/mutations/useOnboardingMutation"; +import { Suspense } from "react"; import { DoclingHealthBanner, useDoclingHealth } from "@/components/docling-health-banner"; -import IBMLogo from "@/components/logo/ibm-logo"; -import OllamaLogo from "@/components/logo/ollama-logo"; -import OpenAILogo from "@/components/logo/openai-logo"; import { ProtectedRoute } from "@/components/protected-route"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardFooter, - CardHeader, -} from "@/components/ui/card"; import { DotPattern } from "@/components/ui/dot-pattern"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; -import { useGetSettingsQuery } from "../api/queries/useGetSettingsQuery"; -import { IBMOnboarding } from "./components/ibm-onboarding"; -import { OllamaOnboarding } from "./components/ollama-onboarding"; -import { OpenAIOnboarding } from "./components/openai-onboarding"; +import OnboardingCard from "./components/onboarding-card"; function OnboardingPage() { - const { data: settingsDb, isLoading: isSettingsLoading } = - useGetSettingsQuery(); const { isHealthy: isDoclingHealthy } = useDoclingHealth(); - const redirect = "/"; - - const router = useRouter(); - - // Redirect if already authenticated or in no-auth mode - useEffect(() => { - if (!isSettingsLoading && settingsDb && settingsDb.edited) { - router.push(redirect); - } - }, [isSettingsLoading, settingsDb, router]); - - const [modelProvider, setModelProvider] = useState("openai"); - - const [sampleDataset, setSampleDataset] = useState(true); - - const handleSetModelProvider = (provider: string) => { - setModelProvider(provider); - setSettings({ - model_provider: provider, - embedding_model: "", - llm_model: "", - }); - }; - - const [settings, setSettings] = useState({ - model_provider: modelProvider, - embedding_model: "", - llm_model: "", - }); - - // Mutations - const onboardingMutation = useOnboardingMutation({ - onSuccess: (data) => { - console.log("Onboarding completed successfully", data); - router.push(redirect); - }, - onError: (error) => { - toast.error("Failed to complete onboarding", { - description: error.message, - }); - }, - }); - - const handleComplete = () => { - if ( - !settings.model_provider || - !settings.llm_model || - !settings.embedding_model - ) { - toast.error("Please complete all required fields"); - return; - } - - // Prepare onboarding data - const onboardingData: OnboardingVariables = { - model_provider: settings.model_provider, - llm_model: settings.llm_model, - embedding_model: settings.embedding_model, - sample_data: sampleDataset, - }; - - // Add API key if available - if (settings.api_key) { - onboardingData.api_key = settings.api_key; - } - - // Add endpoint if available - if (settings.endpoint) { - onboardingData.endpoint = settings.endpoint; - } - - // Add project_id if available - if (settings.project_id) { - onboardingData.project_id = settings.project_id; - } - - onboardingMutation.mutate(onboardingData); - }; - - const isComplete = !!settings.llm_model && !!settings.embedding_model && isDoclingHealthy; - return (
- - - - - - - OpenAI - - - - IBM watsonx.ai - - - - Ollama - - - - - - - - - - - - - - - - - - -
- -
-
- {!isComplete && ( - - {!!settings.llm_model && !!settings.embedding_model && !isDoclingHealthy - ? "docling-serve must be running to continue" - : "Please fill in all required fields"} - - )} -
-
-
+
);