From e4e3d462037708358da434aedeebba8bee928957 Mon Sep 17 00:00:00 2001 From: phact Date: Fri, 24 Oct 2025 01:58:09 -0400 Subject: [PATCH] openai call thinking animation --- .../components/onboarding-content.tsx | 10 +- .../components/onboarding-step.tsx | 83 +++++++++++++-- .../onboarding/components/ibm-onboarding.tsx | 19 +++- .../components/ollama-onboarding.tsx | 17 +++ .../onboarding/components/onboarding-card.tsx | 100 +++++++++++++----- .../components/openai-onboarding.tsx | 19 +++- frontend/tailwind.config.ts | 9 ++ 7 files changed, 219 insertions(+), 38 deletions(-) diff --git a/frontend/src/app/new-onboarding/components/onboarding-content.tsx b/frontend/src/app/new-onboarding/components/onboarding-content.tsx index 68860e1c..addcfdc2 100644 --- a/frontend/src/app/new-onboarding/components/onboarding-content.tsx +++ b/frontend/src/app/new-onboarding/components/onboarding-content.tsx @@ -24,6 +24,8 @@ export function OnboardingContent({ const [assistantMessage, setAssistantMessage] = useState( null, ); + const [isLoadingModels, setIsLoadingModels] = useState(false); + const [loadingStatus, setLoadingStatus] = useState([]); const { streamingMessage, isLoading, sendMessage } = useChatStreaming({ onComplete: (message, newResponseId) => { @@ -79,8 +81,14 @@ export function OnboardingContent({ isVisible={currentStep >= 0} isCompleted={currentStep > 0} text="Let's get started by setting up your model provider." + isLoadingModels={isLoadingModels} + loadingStatus={loadingStatus} > - + {/* Step 2 */} diff --git a/frontend/src/app/new-onboarding/components/onboarding-step.tsx b/frontend/src/app/new-onboarding/components/onboarding-step.tsx index 84cfd6f2..72efb033 100644 --- a/frontend/src/app/new-onboarding/components/onboarding-step.tsx +++ b/frontend/src/app/new-onboarding/components/onboarding-step.tsx @@ -2,6 +2,7 @@ import { AnimatePresence, motion } from "motion/react"; import { type ReactNode, useEffect, useState } from "react"; import { Message } from "@/app/chat/components/message"; import DogIcon from "@/components/logo/dog-icon"; +import { AnimatedProcessingIcon } from "@/components/ui/animated-processing-icon"; import { MarkdownRenderer } from "@/components/markdown-renderer"; import { cn } from "@/lib/utils"; @@ -13,6 +14,8 @@ interface OnboardingStepProps { icon?: ReactNode; isMarkdown?: boolean; hideIcon?: boolean; + isLoadingModels?: boolean; + loadingStatus?: string[]; } export function OnboardingStep({ @@ -23,9 +26,34 @@ export function OnboardingStep({ icon, isMarkdown = false, hideIcon = false, + isLoadingModels = false, + loadingStatus = [], }: OnboardingStepProps) { const [displayedText, setDisplayedText] = useState(""); const [showChildren, setShowChildren] = useState(false); + const [currentStatusIndex, setCurrentStatusIndex] = useState(0); + + // Cycle through loading status messages once + useEffect(() => { + if (!isLoadingModels || loadingStatus.length === 0) { + setCurrentStatusIndex(0); + return; + } + + const interval = setInterval(() => { + setCurrentStatusIndex((prev) => { + const nextIndex = prev + 1; + // Stop at the last message + if (nextIndex >= loadingStatus.length - 1) { + clearInterval(interval); + return loadingStatus.length - 1; + } + return nextIndex; + }); + }, 1500); // Change status every 1.5 seconds + + return () => clearInterval(interval); + }, [isLoadingModels, loadingStatus]); useEffect(() => { if (!isVisible) { @@ -83,7 +111,37 @@ export function OnboardingStep({ } >
- {isMarkdown ? ( + {isLoadingModels && loadingStatus.length > 0 ? ( +
+
+
+ +
+ + Thinking + +
+
+
+
+
+ + + {loadingStatus[currentStatusIndex]} + + +
+
+
+
+ ) : isMarkdown ? ( ) : ( -

- {displayedText} - {!showChildren && !isCompleted && ( - - )} -

+
+

+ {displayedText} + {!showChildren && !isCompleted && ( + + )} +

+
+
)} {children && ( diff --git a/frontend/src/app/onboarding/components/ibm-onboarding.tsx b/frontend/src/app/onboarding/components/ibm-onboarding.tsx index b696e220..cd638025 100644 --- a/frontend/src/app/onboarding/components/ibm-onboarding.tsx +++ b/frontend/src/app/onboarding/components/ibm-onboarding.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { LabelInput } from "@/components/label-input"; import { LabelWrapper } from "@/components/label-wrapper"; import IBMLogo from "@/components/logo/ibm-logo"; @@ -14,10 +14,14 @@ export function IBMOnboarding({ setSettings, sampleDataset, setSampleDataset, + setIsLoadingModels, + setLoadingStatus, }: { setSettings: (settings: OnboardingVariables) => void; sampleDataset: boolean; setSampleDataset: (dataset: boolean) => void; + setIsLoadingModels?: (isLoading: boolean) => void; + setLoadingStatus?: (status: string[]) => void; }) { const [endpoint, setEndpoint] = useState("https://us-south.ml.cloud.ibm.com"); const [apiKey, setApiKey] = useState(""); @@ -99,6 +103,19 @@ export function IBMOnboarding({ }, setSettings, ); + + // Notify parent about loading state + useEffect(() => { + setIsLoadingModels?.(isLoadingModels); + + // Set detailed loading status + if (isLoadingModels) { + const status = ["Connecting to IBM watsonx.ai", "Fetching language models", "Fetching embedding models"]; + setLoadingStatus?.(status); + } else { + setLoadingStatus?.([]); + } + }, [isLoadingModels, setIsLoadingModels, setLoadingStatus]); return ( <>
diff --git a/frontend/src/app/onboarding/components/ollama-onboarding.tsx b/frontend/src/app/onboarding/components/ollama-onboarding.tsx index 9c86b94c..82d86d83 100644 --- a/frontend/src/app/onboarding/components/ollama-onboarding.tsx +++ b/frontend/src/app/onboarding/components/ollama-onboarding.tsx @@ -13,10 +13,14 @@ export function OllamaOnboarding({ setSettings, sampleDataset, setSampleDataset, + setIsLoadingModels, + setLoadingStatus, }: { setSettings: (settings: OnboardingVariables) => void; sampleDataset: boolean; setSampleDataset: (dataset: boolean) => void; + setIsLoadingModels?: (isLoading: boolean) => void; + setLoadingStatus?: (status: string[]) => void; }) { const [endpoint, setEndpoint] = useState(`http://localhost:11434`); const [showConnecting, setShowConnecting] = useState(false); @@ -71,6 +75,19 @@ export function OllamaOnboarding({ setSettings, ); + // Notify parent about loading state + useEffect(() => { + setIsLoadingModels?.(isLoadingModels); + + // Set detailed loading status + if (isLoadingModels) { + const status = ["Connecting to Ollama", "Fetching language models", "Fetching embedding models"]; + setLoadingStatus?.(status); + } else { + setLoadingStatus?.([]); + } + }, [isLoadingModels, setIsLoadingModels, setLoadingStatus]); + // Check validation state based on models query const hasConnectionError = debouncedEndpoint && modelsError; const hasNoModels = diff --git a/frontend/src/app/onboarding/components/onboarding-card.tsx b/frontend/src/app/onboarding/components/onboarding-card.tsx index 8dc60c91..64413fe3 100644 --- a/frontend/src/app/onboarding/components/onboarding-card.tsx +++ b/frontend/src/app/onboarding/components/onboarding-card.tsx @@ -12,6 +12,7 @@ import { 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 { AnimatedProcessingIcon } from "@/components/ui/animated-processing-icon"; import { Button } from "@/components/ui/button"; import { Card, @@ -33,6 +34,8 @@ import { OpenAIOnboarding } from "./openai-onboarding"; interface OnboardingCardProps { onComplete: () => void; + setIsLoadingModels?: (isLoading: boolean) => void; + setLoadingStatus?: (status: string[]) => void; } @@ -45,13 +48,54 @@ const STEP_LIST = [ const TOTAL_PROVIDER_STEPS = STEP_LIST.length; -const OnboardingCard = ({ onComplete }: OnboardingCardProps) => { +const OnboardingCard = ({ + onComplete, + setIsLoadingModels: setIsLoadingModelsParent, + setLoadingStatus: setLoadingStatusParent, +}: OnboardingCardProps) => { const { isHealthy: isDoclingHealthy } = useDoclingHealth(); const [modelProvider, setModelProvider] = useState("openai"); const [sampleDataset, setSampleDataset] = useState(true); + const [isLoadingModels, setIsLoadingModels] = useState(false); + + const [loadingStatus, setLoadingStatus] = useState([]); + + const [currentStatusIndex, setCurrentStatusIndex] = useState(0); + + // Pass loading state to parent + useEffect(() => { + setIsLoadingModelsParent?.(isLoadingModels); + }, [isLoadingModels, setIsLoadingModelsParent]); + + useEffect(() => { + setLoadingStatusParent?.(loadingStatus); + }, [loadingStatus, setLoadingStatusParent]); + + // Cycle through loading status messages once + useEffect(() => { + if (!isLoadingModels || loadingStatus.length === 0) { + setCurrentStatusIndex(0); + return; + } + + const interval = setInterval(() => { + setCurrentStatusIndex((prev) => { + const nextIndex = prev + 1; + // Stop at the last message + if (nextIndex >= loadingStatus.length - 1) { + clearInterval(interval); + return loadingStatus.length - 1; + } + return nextIndex; + }); + }, 1500); // Change status every 1.5 seconds + + return () => clearInterval(interval); + }, [isLoadingModels, loadingStatus]); + const handleSetModelProvider = (provider: string) => { setModelProvider(provider); setSettings({ @@ -206,6 +250,8 @@ const OnboardingCard = ({ onComplete }: OnboardingCardProps) => { setSettings={setSettings} sampleDataset={sampleDataset} setSampleDataset={setSampleDataset} + setIsLoadingModels={setIsLoadingModels} + setLoadingStatus={setLoadingStatus} /> @@ -213,6 +259,8 @@ const OnboardingCard = ({ onComplete }: OnboardingCardProps) => { setSettings={setSettings} sampleDataset={sampleDataset} setSampleDataset={setSampleDataset} + setIsLoadingModels={setIsLoadingModels} + setLoadingStatus={setLoadingStatus} /> @@ -220,33 +268,37 @@ const OnboardingCard = ({ onComplete }: OnboardingCardProps) => { setSettings={setSettings} sampleDataset={sampleDataset} setSampleDataset={setSampleDataset} + setIsLoadingModels={setIsLoadingModels} + setLoadingStatus={setLoadingStatus} /> - - -
- -
-
- {!isComplete && ( - - {!!settings.llm_model && - !!settings.embedding_model && - !isDoclingHealthy - ? "docling-serve must be running to continue" - : "Please fill in all required fields"} - - )} -
+ {!isLoadingModels && ( + + +
+ +
+
+ {!isComplete && ( + + {!!settings.llm_model && + !!settings.embedding_model && + !isDoclingHealthy + ? "docling-serve must be running to continue" + : "Please fill in all required fields"} + + )} +
+ )}
) : ( diff --git a/frontend/src/app/onboarding/components/openai-onboarding.tsx b/frontend/src/app/onboarding/components/openai-onboarding.tsx index b057efc0..01646ad9 100644 --- a/frontend/src/app/onboarding/components/openai-onboarding.tsx +++ b/frontend/src/app/onboarding/components/openai-onboarding.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { LabelInput } from "@/components/label-input"; import { LabelWrapper } from "@/components/label-wrapper"; import OpenAILogo from "@/components/logo/openai-logo"; @@ -14,10 +14,14 @@ export function OpenAIOnboarding({ setSettings, sampleDataset, setSampleDataset, + setIsLoadingModels, + setLoadingStatus, }: { setSettings: (settings: OnboardingVariables) => void; sampleDataset: boolean; setSampleDataset: (dataset: boolean) => void; + setIsLoadingModels?: (isLoading: boolean) => void; + setLoadingStatus?: (status: string[]) => void; }) { const [apiKey, setApiKey] = useState(""); const [getFromEnv, setGetFromEnv] = useState(true); @@ -68,6 +72,19 @@ export function OpenAIOnboarding({ }, setSettings, ); + + // Notify parent about loading state + useEffect(() => { + setIsLoadingModels?.(isLoadingModels); + + // Set detailed loading status + if (isLoadingModels) { + const status = ["Connecting to OpenAI", "Fetching language models", "Fetching embedding models"]; + setLoadingStatus?.(status); + } else { + setLoadingStatus?.([]); + } + }, [isLoadingModels, setIsLoadingModels, setLoadingStatus]); return ( <>
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index a65cef05..6567616b 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -72,6 +72,14 @@ const config = { height: "0", }, }, + shimmer: { + "0%": { + backgroundPosition: "200% 0", + }, + "100%": { + backgroundPosition: "-200% 0", + }, + }, }, animation: { overlayShow: "overlayShow 400ms cubic-bezier(0.16, 1, 0.3, 1)", @@ -79,6 +87,7 @@ const config = { wiggle: "wiggle 150ms ease-in-out 1", "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + shimmer: "shimmer 3s ease-in-out infinite", }, colors: { border: "hsl(var(--border))",