diff --git a/web/src/app/chat/interfaces.ts b/web/src/app/chat/interfaces.ts index 281872680..a7233256a 100644 --- a/web/src/app/chat/interfaces.ts +++ b/web/src/app/chat/interfaces.ts @@ -250,25 +250,21 @@ export const constructSubQuestions = ( console.log("STOP REASON"); console.log(newDetail); const { level, level_question_nr } = newDetail; - const actual_level_question_nr = level_question_nr ?? 0; let subQuestion = updatedSubQuestions.find( - (sq) => - sq.level === level && sq.level_question_nr === actual_level_question_nr + (sq) => sq.level === level && sq.level_question_nr === level_question_nr ); if (subQuestion) { subQuestion.is_complete = true; } } else if ("top_documents" in newDetail) { const { level, level_question_nr, top_documents } = newDetail; - const actual_level_question_nr = level_question_nr ?? 0; let subQuestion = updatedSubQuestions.find( - (sq) => - sq.level === level && sq.level_question_nr === actual_level_question_nr + (sq) => sq.level === level && sq.level_question_nr === level_question_nr ); if (!subQuestion) { subQuestion = { level: level ?? 0, - level_question_nr: actual_level_question_nr, + level_question_nr: level_question_nr ?? 0, question: "", answer: "", sub_queries: [], @@ -280,17 +276,15 @@ export const constructSubQuestions = ( } else if ("answer_piece" in newDetail) { // Handle AgentAnswerPiece const { level, level_question_nr, answer_piece } = newDetail; - const actual_level_question_nr = level_question_nr; // Find or create the relevant SubQuestionDetail let subQuestion = updatedSubQuestions.find( - (sq) => - sq.level === level && sq.level_question_nr === actual_level_question_nr + (sq) => sq.level === level && sq.level_question_nr === level_question_nr ); if (!subQuestion) { subQuestion = { level, - level_question_nr: actual_level_question_nr, + level_question_nr, question: "", answer: "", sub_queries: [], @@ -327,19 +321,17 @@ export const constructSubQuestions = ( } else if ("sub_query" in newDetail) { // Handle SubQueryPiece const { level, level_question_nr, query_id, sub_query } = newDetail; - const actual_level_question_nr = level_question_nr; // Find the relevant SubQuestionDetail let subQuestion = updatedSubQuestions.find( - (sq) => - sq.level === level && sq.level_question_nr === actual_level_question_nr + (sq) => sq.level === level && sq.level_question_nr === level_question_nr ); if (!subQuestion) { // If we receive a sub_query before its parent question, create a placeholder subQuestion = { level, - level_question_nr: actual_level_question_nr, + level_question_nr: level_question_nr, question: "", answer: "", sub_queries: [], diff --git a/web/src/app/chat/message/AgenticMessage.tsx b/web/src/app/chat/message/AgenticMessage.tsx index a448198f4..7afebffbe 100644 --- a/web/src/app/chat/message/AgenticMessage.tsx +++ b/web/src/app/chat/message/AgenticMessage.tsx @@ -67,18 +67,9 @@ import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; import "katex/dist/katex.min.css"; import SubQuestionsDisplay from "./SubQuestionsDisplay"; -import SubQuestionProgress from "./SubQuestionProgress"; -import { Button } from "@/components/ui/button"; -import { Spinner } from "@/components/Spinner"; -import { LoadingAnimation } from "@/components/Loading"; -import { LoadingIndicator } from "react-select/dist/declarations/src/components/indicators"; -import { - StreamingPhase, - StreamingPhaseText, - useStreamingMessages, -} from "./StreamingMessages"; import { Badge } from "@/components/ui/badge"; import RefinemenetBadge from "../refinmentBadge"; +import SubQuestionProgress from "./SubQuestionProgress"; export const AgenticMessage = ({ secondLevelAssistantMessage, @@ -466,7 +457,7 @@ export const AgenticMessage = ({ {/* For debugging purposes */} {/* */} - + {/* */} {(allowStreaming && finalContent && finalContent.length > 8) || @@ -479,8 +470,13 @@ export const AgenticMessage = ({ Answer - {true ? ( + {!secondLevelAssistantMessage && + // !isGenerating && + subQuestions && + subQuestions.length > 0 ? ( { setIsViewingInitialAnswer( diff --git a/web/src/app/chat/message/SubQuestionsDisplay.tsx b/web/src/app/chat/message/SubQuestionsDisplay.tsx index e216e330f..1ad78501a 100644 --- a/web/src/app/chat/message/SubQuestionsDisplay.tsx +++ b/web/src/app/chat/message/SubQuestionsDisplay.tsx @@ -25,6 +25,41 @@ import { CheckIcon, ChevronDown } from "lucide-react"; import { PHASE_MIN_MS, useStreamingMessages } from "./StreamingMessages"; import { CirclingArrowIcon } from "@/components/icons/icons"; +export const StatusIndicator = ({ status }: { status: ToggleState }) => { + return ( + <> + {" "} + {status != ToggleState.InProgress ? ( +
+ {status === ToggleState.Done && ( + + )} +
+ ) : ( +
+
+ + +
+ )} + + ); +}; + export interface TemporaryDisplay { question: string; tinyQuestion: string; @@ -43,7 +78,7 @@ interface SubQuestionsDisplayProps { overallAnswerGenerating?: boolean; } -enum ToggleState { +export enum ToggleState { Todo, InProgress, Done, @@ -300,34 +335,9 @@ const SubQuestionDisplay: React.FC<{ ref={questionRef} className={`flex items-start ${!isLast ? "pb-2" : ""}`} > - {status != ToggleState.InProgress ? ( -
- {status === ToggleState.Done && ( - - )} -
- ) : ( - <> -
- - - - )} - +
+ +
= { + [StreamingPhase.WAITING]: "Extracting key concepts", + [StreamingPhase.SUB_QUERIES]: "Identifying additional questions", + [StreamingPhase.CONTEXT_DOCS]: "Reading more documents", + [StreamingPhase.ANSWER]: "Generating refined answer", + [StreamingPhase.EVALUATE]: "Evaluating new context", + [StreamingPhase.COMPARE]: "Comparing results", + [StreamingPhase.COMPLETE]: "Finished", +}; + +/** -------------------------------------------------------------------------------- + * 2. Ordered Phases: Ensure the COMPARE phase is inserted before COMPLETE + * -------------------------------------------------------------------------------- */ const PHASES_ORDER: StreamingPhase[] = [ StreamingPhase.WAITING, StreamingPhase.SUB_QUERIES, StreamingPhase.CONTEXT_DOCS, StreamingPhase.ANSWER, + StreamingPhase.COMPARE, StreamingPhase.COMPLETE, ]; +/** -------------------------------------------------------------------------------- + * 3. Hook to queue up phases in order, each with a minimum visible time of 0.5s + * -------------------------------------------------------------------------------- */ export function useOrderedPhases(externalPhase: StreamingPhase) { const [phaseQueue, setPhaseQueue] = useState([]); - const [displayedPhase, setDisplayedPhase] = useState( - StreamingPhase.WAITING - ); - const lastDisplayTimestampRef = useRef(Date.now()); + const [displayedPhases, setDisplayedPhases] = useState([]); + const lastDisplayTimeRef = useRef(Date.now()); + const MIN_DELAY = 1000; // 0.5 seconds - const getPhaseIndex = (phase: StreamingPhase) => { - return PHASES_ORDER.indexOf(phase); - }; + const getPhaseIndex = (phase: StreamingPhase) => PHASES_ORDER.indexOf(phase); + // Whenever externalPhase changes, add any missing steps into the queue useEffect(() => { setPhaseQueue((prevQueue) => { - const lastQueuedPhase = - prevQueue.length > 0 ? prevQueue[prevQueue.length - 1] : displayedPhase; + const lastDisplayed = displayedPhases[displayedPhases.length - 1]; + const lastIndex = lastDisplayed + ? getPhaseIndex(lastDisplayed) + : getPhaseIndex(StreamingPhase.WAITING); - const lastQueuedIndex = getPhaseIndex(lastQueuedPhase); - const externalIndex = getPhaseIndex(externalPhase); + let targetPhase = externalPhase; + let targetIndex = getPhaseIndex(targetPhase); - if (externalIndex <= lastQueuedIndex) { + // If externalPhase is COMPLETE, show "COMPARE" first (unless we've shown it already) + if (externalPhase === StreamingPhase.COMPLETE) { + if (!displayedPhases.includes(StreamingPhase.COMPARE)) { + targetPhase = StreamingPhase.COMPARE; + targetIndex = getPhaseIndex(targetPhase); + } + } + + // If the new target is before or at the last displayed, do nothing + if (targetIndex <= lastIndex) { return prevQueue; } + // Otherwise, collect all missing phases from lastDisplayed+1 up to targetIndex const missingPhases: StreamingPhase[] = []; - for (let i = lastQueuedIndex + 1; i <= externalIndex; i++) { + for (let i = lastIndex + 1; i <= targetIndex; i++) { missingPhases.push(PHASES_ORDER[i]); } return [...prevQueue, ...missingPhases]; }); - }, [externalPhase, displayedPhase]); + }, [externalPhase, displayedPhases]); + // Process the queue, displaying each queued phase for at least MIN_DELAY (0.5s) useEffect(() => { if (phaseQueue.length === 0) return; - let rafId: number; + let rafId: number; const processQueue = () => { const now = Date.now(); - const elapsed = now - lastDisplayTimestampRef.current; - // Keep this at 1000ms from the original example (unchanged), - // but you can adjust if you want a different visible time in *this* component. - if (elapsed >= 1000) { + if (now - lastDisplayTimeRef.current >= MIN_DELAY) { setPhaseQueue((prevQueue) => { if (prevQueue.length > 0) { - const [next, ...rest] = prevQueue; - setDisplayedPhase(next); - lastDisplayTimestampRef.current = Date.now(); + const [nextPhase, ...rest] = prevQueue; + setDisplayedPhases((prev) => [...prev, nextPhase]); + lastDisplayTimeRef.current = Date.now(); return rest; } return prevQueue; @@ -78,75 +119,348 @@ export function useOrderedPhases(externalPhase: StreamingPhase) { }; rafId = requestAnimationFrame(processQueue); - return () => { - if (rafId) { - cancelAnimationFrame(rafId); - } - }; + return () => cancelAnimationFrame(rafId); }, [phaseQueue]); - return StreamingPhaseText[displayedPhase]; + // displayedPhases are the ones currently shown, in the order they appeared + return displayedPhases; } +/** -------------------------------------------------------------------------------- + * 4. StatusIndicator: shows "running" (spinner) or "finished" (check) + * -------------------------------------------------------------------------------- */ + +/** -------------------------------------------------------------------------------- + * 5. Example SubQuestionDetail interface (from your snippet) + * -------------------------------------------------------------------------------- */ +export interface SubQuestionDetail { + question: string; + answer: string; + sub_queries?: { query: string }[] | null; + context_docs?: { top_documents: any[] } | null; + is_complete?: boolean; +} + +/** -------------------------------------------------------------------------------- + * 6. Final "RefinemenetBadge" component + * - Renders a "Refining" box with phases shown in order + * - Disappears once COMPLETE is reached, unless hovered + * - Preserves any states already shown + * -------------------------------------------------------------------------------- */ export default function RefinemenetBadge({ + overallAnswer, secondLevelSubquestions, toggleInitialAnswerVieinwg, isViewingInitialAnswer, + finished, }: { + finished: boolean; + overallAnswer: string; secondLevelSubquestions?: SubQuestionDetail[] | null; toggleInitialAnswerVieinwg: () => void; isViewingInitialAnswer: boolean; }) { - const currentState = secondLevelSubquestions?.[0] - ? secondLevelSubquestions[0].answer - ? secondLevelSubquestions[0].is_complete + // Derive the 'externalPhase' from your existing logic: + const currentState = + overallAnswer.length > 0 + ? finished ? StreamingPhase.COMPLETE : StreamingPhase.ANSWER - : secondLevelSubquestions[0].context_docs - ? StreamingPhase.CONTEXT_DOCS - : secondLevelSubquestions[0].sub_queries - ? StreamingPhase.SUB_QUERIES - : secondLevelSubquestions[0].question - ? StreamingPhase.WAITING - : StreamingPhase.WAITING - : StreamingPhase.WAITING; + : secondLevelSubquestions?.[0] + ? secondLevelSubquestions.every((q) => q.answer && q.answer.length > 0) + ? StreamingPhase.EVALUATE + : secondLevelSubquestions?.[0].context_docs + ? StreamingPhase.CONTEXT_DOCS + : secondLevelSubquestions?.[0].sub_queries + ? StreamingPhase.SUB_QUERIES + : StreamingPhase.WAITING + : StreamingPhase.WAITING; - const message = useOrderedPhases(currentState); + // secondLevelSubquestions?.[0] + // : secondLevelSubquestions[0].context_docs + // ? StreamingPhase.CONTEXT_DOCS + // : secondLevelSubquestions[0].sub_queries + // ? StreamingPhase.SUB_QUERIES + // : secondLevelSubquestions[0].question + // ? StreamingPhase.WAITING + // : StreamingPhase.WAITING + // : StreamingPhase.WAITING; + + // Once the first query token comes through, it should be in the sub queries + // Once the first set of documents come through it should be in context docs + // Once all of the analysis have started generating, it should say evaluate + // Once the refined answer starts generating, it should say Answer + // Once the refined answer finishes generating, show COMPARE (we don't have this yet) + + // export const StreamingPhaseText: Record = { + // [StreamingPhase.WAITING]: "Extracting key concepts", + // [StreamingPhase.SUB_QUERIES]: "Identifying additional questions", + // [StreamingPhase.CONTEXT_DOCS]: "Reading more documents", + // [StreamingPhase.ANSWER]: "Generating refined answer", + // [StreamingPhase.EVALUATE]: "Evaluating new context", + // [StreamingPhase.COMPARE]: "Comparing results", + // [StreamingPhase.COMPLETE]: "Finished", + // }; + + // Get the array of displayed phases + const displayedPhases = useOrderedPhases(currentState); + const isDone = displayedPhases.includes(StreamingPhase.COMPLETE); + + // Expand/collapse, hover states + const [expanded, setExpanded] = useState(true); + const [isHovered, setIsHovered] = useState(false); + const [shouldShow, setShouldShow] = useState(true); + + // Once "done", hide after a short delay if not hovered + useEffect(() => { + if (isDone) { + const timer = setTimeout(() => { + if (!isHovered) { + setShouldShow(false); + } + }, 800); // e.g. 0.8s + return () => clearTimeout(timer); + } + }, [isDone, isHovered]); + + if (!shouldShow) { + return null; // entire box disappears + } return ( - -
- Refining answer... - -
-
- -
-

- {message} -

-

- The answer is being refined based on additional context and - analysis. -

- -
-
+ Refining Answer + +
+ + + {/* If not done, show the "Refining" box + a chevron */} + + {/* Expanded area: each displayed phase in order */} + {expanded && ( +
+ {currentState !== StreamingPhase.WAITING ? ( + Array.from(new Set(displayedPhases)).map((phase, index) => { + const phaseIndex = displayedPhases.indexOf(phase); + // The last displayed item is "running" if not COMPLETE + let status = ToggleState.Done; + if ( + index === + Array.from(new Set(displayedPhases)).length - 1 + ) { + status = ToggleState.InProgress; + } + if (phase === StreamingPhase.COMPLETE) { + status = ToggleState.Done; + } + + return ( +
+
+ +
+ + {StreamingPhaseText[phase]} + +
+ ); + }) + ) : ( +
+
+ +
+ + {StreamingPhaseText[StreamingPhase.SUB_QUERIES]} + +
+ )} +
+ )} +
+
); } + +// "use client"; +// import { +// Tooltip, +// TooltipTrigger, +// TooltipProvider, +// TooltipContent, +// } from "@/components/ui/tooltip"; +// import { Button } from "@/components/ui/button"; +// import { FiChevronRight, FiGlobe } from "react-icons/fi"; +// import { +// StreamingPhase, +// StreamingPhaseText, +// } from "./message/StreamingMessages"; +// import { SubQuestionDetail } from "./interfaces"; +// import { useEffect, useRef, useState } from "react"; + +// const PHASES_ORDER: StreamingPhase[] = [ +// StreamingPhase.WAITING, +// StreamingPhase.SUB_QUERIES, +// StreamingPhase.CONTEXT_DOCS, +// StreamingPhase.ANSWER, +// StreamingPhase.COMPLETE, +// ]; + +// export function useOrderedPhases(externalPhase: StreamingPhase) { +// const [phaseQueue, setPhaseQueue] = useState([]); +// const [displayedPhase, setDisplayedPhase] = useState( +// StreamingPhase.WAITING +// ); +// const lastDisplayTimestampRef = useRef(Date.now()); + +// const getPhaseIndex = (phase: StreamingPhase) => { +// return PHASES_ORDER.indexOf(phase); +// }; + +// useEffect(() => { +// setPhaseQueue((prevQueue) => { +// const lastQueuedPhase = +// prevQueue.length > 0 ? prevQueue[prevQueue.length - 1] : displayedPhase; + +// const lastQueuedIndex = getPhaseIndex(lastQueuedPhase); +// const externalIndex = getPhaseIndex(externalPhase); + +// if (externalIndex <= lastQueuedIndex) { +// return prevQueue; +// } + +// const missingPhases: StreamingPhase[] = []; +// for (let i = lastQueuedIndex + 1; i <= externalIndex; i++) { +// missingPhases.push(PHASES_ORDER[i]); +// } +// return [...prevQueue, ...missingPhases]; +// }); +// }, [externalPhase, displayedPhase]); + +// useEffect(() => { +// if (phaseQueue.length === 0) return; +// let rafId: number; + +// const processQueue = () => { +// const now = Date.now(); +// const elapsed = now - lastDisplayTimestampRef.current; + +// // Keep this at 1000ms from the original example (unchanged), +// // but you can adjust if you want a different visible time in *this* component. +// if (elapsed >= 1000) { +// setPhaseQueue((prevQueue) => { +// if (prevQueue.length > 0) { +// const [next, ...rest] = prevQueue; +// setDisplayedPhase(next); +// lastDisplayTimestampRef.current = Date.now(); +// return rest; +// } +// return prevQueue; +// }); +// } +// rafId = requestAnimationFrame(processQueue); +// }; + +// rafId = requestAnimationFrame(processQueue); +// return () => { +// if (rafId) { +// cancelAnimationFrame(rafId); +// } +// }; +// }, [phaseQueue]); + +// return StreamingPhaseText[displayedPhase]; +// } + +// export default function RefinemenetBadge({ +// secondLevelSubquestions, +// toggleInitialAnswerVieinwg, +// isViewingInitialAnswer, +// }: { +// secondLevelSubquestions?: SubQuestionDetail[] | null; +// toggleInitialAnswerVieinwg: () => void; +// isViewingInitialAnswer: boolean; +// }) { +// const currentState = secondLevelSubquestions?.[0] +// ? secondLevelSubquestions[0].answer +// ? secondLevelSubquestions[0].is_complete +// ? StreamingPhase.COMPLETE +// : StreamingPhase.ANSWER +// : secondLevelSubquestions[0].context_docs +// ? StreamingPhase.CONTEXT_DOCS +// : secondLevelSubquestions[0].sub_queries +// ? StreamingPhase.SUB_QUERIES +// : secondLevelSubquestions[0].question +// ? StreamingPhase.WAITING +// : StreamingPhase.WAITING +// : StreamingPhase.WAITING; + +// const message = useOrderedPhases(currentState); + +// return ( +// +// +// +//
+// Refining answer... +// +//
+//
+// +//
+//

+// {message} +//

+//

+// The answer is being refined based on additional context and +// analysis. +//

+// +//
+//
+//
+//
+// ); +// } diff --git a/web/src/components/WebResultIcon.tsx b/web/src/components/WebResultIcon.tsx index 13ca363ac..d9aabf0f9 100644 --- a/web/src/components/WebResultIcon.tsx +++ b/web/src/components/WebResultIcon.tsx @@ -13,7 +13,13 @@ export function WebResultIcon({ size?: number; }) { const [error, setError] = useState(false); - const hostname = new URL(url).hostname; + let hostname; + try { + hostname = new URL(url).hostname; + } catch (e) { + console.log(e); + hostname = "docs.onyx.app"; + } return ( <> {hostname == "docs.onyx.app" ? (