mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-07-28 13:53:28 +02:00
updated + functional
This commit is contained in:
@@ -250,25 +250,21 @@ export const constructSubQuestions = (
|
|||||||
console.log("STOP REASON");
|
console.log("STOP REASON");
|
||||||
console.log(newDetail);
|
console.log(newDetail);
|
||||||
const { level, level_question_nr } = newDetail;
|
const { level, level_question_nr } = newDetail;
|
||||||
const actual_level_question_nr = level_question_nr ?? 0;
|
|
||||||
let subQuestion = updatedSubQuestions.find(
|
let subQuestion = updatedSubQuestions.find(
|
||||||
(sq) =>
|
(sq) => sq.level === level && sq.level_question_nr === level_question_nr
|
||||||
sq.level === level && sq.level_question_nr === actual_level_question_nr
|
|
||||||
);
|
);
|
||||||
if (subQuestion) {
|
if (subQuestion) {
|
||||||
subQuestion.is_complete = true;
|
subQuestion.is_complete = true;
|
||||||
}
|
}
|
||||||
} else if ("top_documents" in newDetail) {
|
} else if ("top_documents" in newDetail) {
|
||||||
const { level, level_question_nr, top_documents } = newDetail;
|
const { level, level_question_nr, top_documents } = newDetail;
|
||||||
const actual_level_question_nr = level_question_nr ?? 0;
|
|
||||||
let subQuestion = updatedSubQuestions.find(
|
let subQuestion = updatedSubQuestions.find(
|
||||||
(sq) =>
|
(sq) => sq.level === level && sq.level_question_nr === level_question_nr
|
||||||
sq.level === level && sq.level_question_nr === actual_level_question_nr
|
|
||||||
);
|
);
|
||||||
if (!subQuestion) {
|
if (!subQuestion) {
|
||||||
subQuestion = {
|
subQuestion = {
|
||||||
level: level ?? 0,
|
level: level ?? 0,
|
||||||
level_question_nr: actual_level_question_nr,
|
level_question_nr: level_question_nr ?? 0,
|
||||||
question: "",
|
question: "",
|
||||||
answer: "",
|
answer: "",
|
||||||
sub_queries: [],
|
sub_queries: [],
|
||||||
@@ -280,17 +276,15 @@ export const constructSubQuestions = (
|
|||||||
} else if ("answer_piece" in newDetail) {
|
} else if ("answer_piece" in newDetail) {
|
||||||
// Handle AgentAnswerPiece
|
// Handle AgentAnswerPiece
|
||||||
const { level, level_question_nr, answer_piece } = newDetail;
|
const { level, level_question_nr, answer_piece } = newDetail;
|
||||||
const actual_level_question_nr = level_question_nr;
|
|
||||||
// Find or create the relevant SubQuestionDetail
|
// Find or create the relevant SubQuestionDetail
|
||||||
let subQuestion = updatedSubQuestions.find(
|
let subQuestion = updatedSubQuestions.find(
|
||||||
(sq) =>
|
(sq) => sq.level === level && sq.level_question_nr === level_question_nr
|
||||||
sq.level === level && sq.level_question_nr === actual_level_question_nr
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!subQuestion) {
|
if (!subQuestion) {
|
||||||
subQuestion = {
|
subQuestion = {
|
||||||
level,
|
level,
|
||||||
level_question_nr: actual_level_question_nr,
|
level_question_nr,
|
||||||
question: "",
|
question: "",
|
||||||
answer: "",
|
answer: "",
|
||||||
sub_queries: [],
|
sub_queries: [],
|
||||||
@@ -327,19 +321,17 @@ export const constructSubQuestions = (
|
|||||||
} else if ("sub_query" in newDetail) {
|
} else if ("sub_query" in newDetail) {
|
||||||
// Handle SubQueryPiece
|
// Handle SubQueryPiece
|
||||||
const { level, level_question_nr, query_id, sub_query } = newDetail;
|
const { level, level_question_nr, query_id, sub_query } = newDetail;
|
||||||
const actual_level_question_nr = level_question_nr;
|
|
||||||
|
|
||||||
// Find the relevant SubQuestionDetail
|
// Find the relevant SubQuestionDetail
|
||||||
let subQuestion = updatedSubQuestions.find(
|
let subQuestion = updatedSubQuestions.find(
|
||||||
(sq) =>
|
(sq) => sq.level === level && sq.level_question_nr === level_question_nr
|
||||||
sq.level === level && sq.level_question_nr === actual_level_question_nr
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!subQuestion) {
|
if (!subQuestion) {
|
||||||
// If we receive a sub_query before its parent question, create a placeholder
|
// If we receive a sub_query before its parent question, create a placeholder
|
||||||
subQuestion = {
|
subQuestion = {
|
||||||
level,
|
level,
|
||||||
level_question_nr: actual_level_question_nr,
|
level_question_nr: level_question_nr,
|
||||||
question: "",
|
question: "",
|
||||||
answer: "",
|
answer: "",
|
||||||
sub_queries: [],
|
sub_queries: [],
|
||||||
|
@@ -67,18 +67,9 @@ import remarkMath from "remark-math";
|
|||||||
import rehypeKatex from "rehype-katex";
|
import rehypeKatex from "rehype-katex";
|
||||||
import "katex/dist/katex.min.css";
|
import "katex/dist/katex.min.css";
|
||||||
import SubQuestionsDisplay from "./SubQuestionsDisplay";
|
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 { Badge } from "@/components/ui/badge";
|
||||||
import RefinemenetBadge from "../refinmentBadge";
|
import RefinemenetBadge from "../refinmentBadge";
|
||||||
|
import SubQuestionProgress from "./SubQuestionProgress";
|
||||||
|
|
||||||
export const AgenticMessage = ({
|
export const AgenticMessage = ({
|
||||||
secondLevelAssistantMessage,
|
secondLevelAssistantMessage,
|
||||||
@@ -466,7 +457,7 @@ export const AgenticMessage = ({
|
|||||||
|
|
||||||
{/* For debugging purposes */}
|
{/* For debugging purposes */}
|
||||||
{/* <SubQuestionProgress subQuestions={subQuestions || []} /> */}
|
{/* <SubQuestionProgress subQuestions={subQuestions || []} /> */}
|
||||||
|
{/* */}
|
||||||
{(allowStreaming &&
|
{(allowStreaming &&
|
||||||
finalContent &&
|
finalContent &&
|
||||||
finalContent.length > 8) ||
|
finalContent.length > 8) ||
|
||||||
@@ -479,8 +470,13 @@ export const AgenticMessage = ({
|
|||||||
Answer
|
Answer
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{true ? (
|
{!secondLevelAssistantMessage &&
|
||||||
|
// !isGenerating &&
|
||||||
|
subQuestions &&
|
||||||
|
subQuestions.length > 0 ? (
|
||||||
<RefinemenetBadge
|
<RefinemenetBadge
|
||||||
|
finished={!secondLevelGenerating}
|
||||||
|
overallAnswer={secondLevelAssistantMessage || ""}
|
||||||
secondLevelSubquestions={secondLevelSubquestions}
|
secondLevelSubquestions={secondLevelSubquestions}
|
||||||
toggleInitialAnswerVieinwg={() => {
|
toggleInitialAnswerVieinwg={() => {
|
||||||
setIsViewingInitialAnswer(
|
setIsViewingInitialAnswer(
|
||||||
|
@@ -25,6 +25,41 @@ import { CheckIcon, ChevronDown } from "lucide-react";
|
|||||||
import { PHASE_MIN_MS, useStreamingMessages } from "./StreamingMessages";
|
import { PHASE_MIN_MS, useStreamingMessages } from "./StreamingMessages";
|
||||||
import { CirclingArrowIcon } from "@/components/icons/icons";
|
import { CirclingArrowIcon } from "@/components/icons/icons";
|
||||||
|
|
||||||
|
export const StatusIndicator = ({ status }: { status: ToggleState }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
{status != ToggleState.InProgress ? (
|
||||||
|
<div
|
||||||
|
className={` h-full w-full rounded-full z-10
|
||||||
|
bg-background border-3 border-background-900 "
|
||||||
|
${
|
||||||
|
status === ToggleState.Todo
|
||||||
|
? "!border-4 border border-background-900 bg-background"
|
||||||
|
: false
|
||||||
|
? "bg-background border-3 border border-background-900 rotating-border"
|
||||||
|
: "bg-background-900 flex items-center justify-center"
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{status === ToggleState.Done && (
|
||||||
|
<CheckIcon className="m-auto text-white" size={8} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative h-full w-full">
|
||||||
|
<div className="absolute top-0 left-0 z-[100] h-full w-full rounded-full bg-background" />
|
||||||
|
|
||||||
|
<CirclingArrowIcon
|
||||||
|
size={12}
|
||||||
|
className="absolute top-0 left-0 z-[2000] h-full w-full animate-spin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export interface TemporaryDisplay {
|
export interface TemporaryDisplay {
|
||||||
question: string;
|
question: string;
|
||||||
tinyQuestion: string;
|
tinyQuestion: string;
|
||||||
@@ -43,7 +78,7 @@ interface SubQuestionsDisplayProps {
|
|||||||
overallAnswerGenerating?: boolean;
|
overallAnswerGenerating?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ToggleState {
|
export enum ToggleState {
|
||||||
Todo,
|
Todo,
|
||||||
InProgress,
|
InProgress,
|
||||||
Done,
|
Done,
|
||||||
@@ -300,34 +335,9 @@ const SubQuestionDisplay: React.FC<{
|
|||||||
ref={questionRef}
|
ref={questionRef}
|
||||||
className={`flex items-start ${!isLast ? "pb-2" : ""}`}
|
className={`flex items-start ${!isLast ? "pb-2" : ""}`}
|
||||||
>
|
>
|
||||||
{status != ToggleState.InProgress ? (
|
<div className="absolute left-0 w-3 h-3 rounded-full mt-[12px] z-10">
|
||||||
<div
|
<StatusIndicator status={status} />
|
||||||
className={`absolute left-0 w-3 h-3 rounded-full mt-[12px] z-10
|
|
||||||
bg-background border-3 border-background-900 "
|
|
||||||
${
|
|
||||||
status === ToggleState.Todo
|
|
||||||
? "!border-4 border border-background-900 bg-background"
|
|
||||||
: false
|
|
||||||
? "bg-background border-3 border border-background-900 rotating-border"
|
|
||||||
: "bg-background-900 flex items-center justify-center"
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{status === ToggleState.Done && (
|
|
||||||
<CheckIcon className="m-auto text-white" size={8} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="absolute z-[100] left-0 mt-[12px] w-3 h-3 rounded-full bg-background" />
|
|
||||||
|
|
||||||
<CirclingArrowIcon
|
|
||||||
size={12}
|
|
||||||
className="absolute z-[2000] left-0 mt-[12px] w-3 h-3 animate-spin"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="ml-8 w-full">
|
<div className="ml-8 w-full">
|
||||||
<div
|
<div
|
||||||
className="flex -mx-2 rounded-md px-2 hover:bg-[#F5F3ED] items-start py-1.5 my-.5 cursor-pointer"
|
className="flex -mx-2 rounded-md px-2 hover:bg-[#F5F3ED] items-start py-1.5 my-.5 cursor-pointer"
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
@@ -6,69 +7,109 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { FiChevronRight, FiGlobe } from "react-icons/fi";
|
import { FiChevronRight, FiChevronDown, FiGlobe } from "react-icons/fi";
|
||||||
import {
|
import { CheckIcon } from "lucide-react";
|
||||||
StreamingPhase,
|
import { CirclingArrowIcon } from "@/components/icons/icons";
|
||||||
StreamingPhaseText,
|
import { StatusIndicator, ToggleState } from "./message/SubQuestionsDisplay";
|
||||||
} from "./message/StreamingMessages";
|
|
||||||
import { SubQuestionDetail } from "./interfaces";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
|
// Replace with your actual icons/components
|
||||||
|
// import { CheckIcon, CirclingArrowIcon } from "@/your-icons-or-components";
|
||||||
|
|
||||||
|
/** --------------------------------------------------------------------------------
|
||||||
|
* 1. StreamingPhase + Display Text
|
||||||
|
* We add a pseudo "COMPARE" phase before COMPLETE to show "Comparing results"
|
||||||
|
* for at least 0.5 seconds.
|
||||||
|
* -------------------------------------------------------------------------------- */
|
||||||
|
export enum StreamingPhase {
|
||||||
|
WAITING = "WAITING",
|
||||||
|
SUB_QUERIES = "SUB_QUERIES",
|
||||||
|
CONTEXT_DOCS = "CONTEXT_DOCS",
|
||||||
|
ANSWER = "ANSWER",
|
||||||
|
COMPARE = "COMPARE",
|
||||||
|
COMPLETE = "COMPLETE",
|
||||||
|
EVALUATE = "EVALUATE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StreamingPhaseText: Record<StreamingPhase, string> = {
|
||||||
|
[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[] = [
|
const PHASES_ORDER: StreamingPhase[] = [
|
||||||
StreamingPhase.WAITING,
|
StreamingPhase.WAITING,
|
||||||
StreamingPhase.SUB_QUERIES,
|
StreamingPhase.SUB_QUERIES,
|
||||||
StreamingPhase.CONTEXT_DOCS,
|
StreamingPhase.CONTEXT_DOCS,
|
||||||
StreamingPhase.ANSWER,
|
StreamingPhase.ANSWER,
|
||||||
|
StreamingPhase.COMPARE,
|
||||||
StreamingPhase.COMPLETE,
|
StreamingPhase.COMPLETE,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** --------------------------------------------------------------------------------
|
||||||
|
* 3. Hook to queue up phases in order, each with a minimum visible time of 0.5s
|
||||||
|
* -------------------------------------------------------------------------------- */
|
||||||
export function useOrderedPhases(externalPhase: StreamingPhase) {
|
export function useOrderedPhases(externalPhase: StreamingPhase) {
|
||||||
const [phaseQueue, setPhaseQueue] = useState<StreamingPhase[]>([]);
|
const [phaseQueue, setPhaseQueue] = useState<StreamingPhase[]>([]);
|
||||||
const [displayedPhase, setDisplayedPhase] = useState<StreamingPhase>(
|
const [displayedPhases, setDisplayedPhases] = useState<StreamingPhase[]>([]);
|
||||||
StreamingPhase.WAITING
|
const lastDisplayTimeRef = useRef<number>(Date.now());
|
||||||
);
|
const MIN_DELAY = 1000; // 0.5 seconds
|
||||||
const lastDisplayTimestampRef = useRef<number>(Date.now());
|
|
||||||
|
|
||||||
const getPhaseIndex = (phase: StreamingPhase) => {
|
const getPhaseIndex = (phase: StreamingPhase) => PHASES_ORDER.indexOf(phase);
|
||||||
return PHASES_ORDER.indexOf(phase);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Whenever externalPhase changes, add any missing steps into the queue
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPhaseQueue((prevQueue) => {
|
setPhaseQueue((prevQueue) => {
|
||||||
const lastQueuedPhase =
|
const lastDisplayed = displayedPhases[displayedPhases.length - 1];
|
||||||
prevQueue.length > 0 ? prevQueue[prevQueue.length - 1] : displayedPhase;
|
const lastIndex = lastDisplayed
|
||||||
|
? getPhaseIndex(lastDisplayed)
|
||||||
|
: getPhaseIndex(StreamingPhase.WAITING);
|
||||||
|
|
||||||
const lastQueuedIndex = getPhaseIndex(lastQueuedPhase);
|
let targetPhase = externalPhase;
|
||||||
const externalIndex = getPhaseIndex(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;
|
return prevQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise, collect all missing phases from lastDisplayed+1 up to targetIndex
|
||||||
const missingPhases: StreamingPhase[] = [];
|
const missingPhases: StreamingPhase[] = [];
|
||||||
for (let i = lastQueuedIndex + 1; i <= externalIndex; i++) {
|
for (let i = lastIndex + 1; i <= targetIndex; i++) {
|
||||||
missingPhases.push(PHASES_ORDER[i]);
|
missingPhases.push(PHASES_ORDER[i]);
|
||||||
}
|
}
|
||||||
return [...prevQueue, ...missingPhases];
|
return [...prevQueue, ...missingPhases];
|
||||||
});
|
});
|
||||||
}, [externalPhase, displayedPhase]);
|
}, [externalPhase, displayedPhases]);
|
||||||
|
|
||||||
|
// Process the queue, displaying each queued phase for at least MIN_DELAY (0.5s)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (phaseQueue.length === 0) return;
|
if (phaseQueue.length === 0) return;
|
||||||
let rafId: number;
|
|
||||||
|
|
||||||
|
let rafId: number;
|
||||||
const processQueue = () => {
|
const processQueue = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const elapsed = now - lastDisplayTimestampRef.current;
|
|
||||||
|
|
||||||
// Keep this at 1000ms from the original example (unchanged),
|
if (now - lastDisplayTimeRef.current >= MIN_DELAY) {
|
||||||
// but you can adjust if you want a different visible time in *this* component.
|
|
||||||
if (elapsed >= 1000) {
|
|
||||||
setPhaseQueue((prevQueue) => {
|
setPhaseQueue((prevQueue) => {
|
||||||
if (prevQueue.length > 0) {
|
if (prevQueue.length > 0) {
|
||||||
const [next, ...rest] = prevQueue;
|
const [nextPhase, ...rest] = prevQueue;
|
||||||
setDisplayedPhase(next);
|
setDisplayedPhases((prev) => [...prev, nextPhase]);
|
||||||
lastDisplayTimestampRef.current = Date.now();
|
lastDisplayTimeRef.current = Date.now();
|
||||||
return rest;
|
return rest;
|
||||||
}
|
}
|
||||||
return prevQueue;
|
return prevQueue;
|
||||||
@@ -78,75 +119,348 @@ export function useOrderedPhases(externalPhase: StreamingPhase) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
rafId = requestAnimationFrame(processQueue);
|
rafId = requestAnimationFrame(processQueue);
|
||||||
return () => {
|
return () => cancelAnimationFrame(rafId);
|
||||||
if (rafId) {
|
|
||||||
cancelAnimationFrame(rafId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [phaseQueue]);
|
}, [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({
|
export default function RefinemenetBadge({
|
||||||
|
overallAnswer,
|
||||||
secondLevelSubquestions,
|
secondLevelSubquestions,
|
||||||
toggleInitialAnswerVieinwg,
|
toggleInitialAnswerVieinwg,
|
||||||
isViewingInitialAnswer,
|
isViewingInitialAnswer,
|
||||||
|
finished,
|
||||||
}: {
|
}: {
|
||||||
|
finished: boolean;
|
||||||
|
overallAnswer: string;
|
||||||
secondLevelSubquestions?: SubQuestionDetail[] | null;
|
secondLevelSubquestions?: SubQuestionDetail[] | null;
|
||||||
toggleInitialAnswerVieinwg: () => void;
|
toggleInitialAnswerVieinwg: () => void;
|
||||||
isViewingInitialAnswer: boolean;
|
isViewingInitialAnswer: boolean;
|
||||||
}) {
|
}) {
|
||||||
const currentState = secondLevelSubquestions?.[0]
|
// Derive the 'externalPhase' from your existing logic:
|
||||||
? secondLevelSubquestions[0].answer
|
const currentState =
|
||||||
? secondLevelSubquestions[0].is_complete
|
overallAnswer.length > 0
|
||||||
|
? finished
|
||||||
? StreamingPhase.COMPLETE
|
? StreamingPhase.COMPLETE
|
||||||
: StreamingPhase.ANSWER
|
: StreamingPhase.ANSWER
|
||||||
: secondLevelSubquestions[0].context_docs
|
: secondLevelSubquestions?.[0]
|
||||||
|
? secondLevelSubquestions.every((q) => q.answer && q.answer.length > 0)
|
||||||
|
? StreamingPhase.EVALUATE
|
||||||
|
: secondLevelSubquestions?.[0].context_docs
|
||||||
? StreamingPhase.CONTEXT_DOCS
|
? StreamingPhase.CONTEXT_DOCS
|
||||||
: secondLevelSubquestions[0].sub_queries
|
: secondLevelSubquestions?.[0].sub_queries
|
||||||
? StreamingPhase.SUB_QUERIES
|
? StreamingPhase.SUB_QUERIES
|
||||||
: secondLevelSubquestions[0].question
|
|
||||||
? StreamingPhase.WAITING
|
|
||||||
: StreamingPhase.WAITING
|
: StreamingPhase.WAITING
|
||||||
: 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, string> = {
|
||||||
|
// [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 (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
<div
|
||||||
|
className="relative w-full max-w-sm"
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
{/* Original snippet's tooltip usage */}
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex loading-text items-center gap-x-2 text-black text-sm font-medium cursor-pointer hover:text-blue-600 transition-colors duration-200">
|
<div
|
||||||
Refining answer...
|
className="flex loading-text items-center gap-x-2 text-black text-sm
|
||||||
|
font-medium cursor-pointer hover:text-blue-600
|
||||||
|
transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Refining Answer
|
||||||
<FiChevronRight
|
<FiChevronRight
|
||||||
className="inline-block text-text-darker"
|
className={`inline-block transition-transform duration-200 text-text-darker ${
|
||||||
|
isHovered ? "rotate-90" : ""
|
||||||
|
}`}
|
||||||
size={16}
|
size={16}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="w-80 p-4 bg-white shadow-lg rounded-md">
|
<TooltipContent
|
||||||
<div className="space-y-4">
|
side="bottom"
|
||||||
<p className="text-lg leading-none font-semibold text-gray-800">
|
align="start"
|
||||||
{message}
|
className="w-80 p-4 bg-white shadow-lg rounded-md"
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
The answer is being refined based on additional context and
|
|
||||||
analysis.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
onClick={() => toggleInitialAnswerVieinwg()}
|
|
||||||
size="sm"
|
|
||||||
className="w-full"
|
|
||||||
>
|
>
|
||||||
{isViewingInitialAnswer
|
{/* If not done, show the "Refining" box + a chevron */}
|
||||||
? "See Live Updates"
|
|
||||||
: "Hide Live Updates"}
|
{/* Expanded area: each displayed phase in order */}
|
||||||
<FiGlobe className="inline-block mr-2" />
|
{expanded && (
|
||||||
</Button>
|
<div className="items-start flex flex-col gap-y-2">
|
||||||
|
{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 (
|
||||||
|
<div
|
||||||
|
key={phase}
|
||||||
|
className="text-text flex items-center justify-start gap-x-2"
|
||||||
|
>
|
||||||
|
<div className="w-3 h-3">
|
||||||
|
<StatusIndicator status={status} />
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{StreamingPhaseText[phase]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
key={currentState}
|
||||||
|
className="text-text flex items-center justify-start gap-x-2"
|
||||||
|
>
|
||||||
|
<div className="w-3 h-3">
|
||||||
|
<StatusIndicator status={ToggleState.InProgress} />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{StreamingPhaseText[StreamingPhase.SUB_QUERIES]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "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<StreamingPhase[]>([]);
|
||||||
|
// const [displayedPhase, setDisplayedPhase] = useState<StreamingPhase>(
|
||||||
|
// StreamingPhase.WAITING
|
||||||
|
// );
|
||||||
|
// const lastDisplayTimestampRef = useRef<number>(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 (
|
||||||
|
// <TooltipProvider delayDuration={0}>
|
||||||
|
// <Tooltip>
|
||||||
|
// <TooltipTrigger asChild>
|
||||||
|
// <div className="flex loading-text items-center gap-x-2 text-black text-sm font-medium cursor-pointer hover:text-blue-600 transition-colors duration-200">
|
||||||
|
// Refining answer...
|
||||||
|
// <FiChevronRight
|
||||||
|
// className="inline-block text-text-darker"
|
||||||
|
// size={16}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </TooltipTrigger>
|
||||||
|
// <TooltipContent className="w-80 p-4 bg-white shadow-lg rounded-md">
|
||||||
|
// <div className="space-y-4">
|
||||||
|
// <p className="text-lg leading-none font-semibold text-gray-800">
|
||||||
|
// {message}
|
||||||
|
// </p>
|
||||||
|
// <p className="text-sm text-gray-600">
|
||||||
|
// The answer is being refined based on additional context and
|
||||||
|
// analysis.
|
||||||
|
// </p>
|
||||||
|
// <Button
|
||||||
|
// onClick={() => toggleInitialAnswerVieinwg()}
|
||||||
|
// size="sm"
|
||||||
|
// className="w-full"
|
||||||
|
// >
|
||||||
|
// {isViewingInitialAnswer
|
||||||
|
// ? "See Live Updates"
|
||||||
|
// : "Hide Live Updates"}
|
||||||
|
// <FiGlobe className="inline-block mr-2" />
|
||||||
|
// </Button>
|
||||||
|
// </div>
|
||||||
|
// </TooltipContent>
|
||||||
|
// </Tooltip>
|
||||||
|
// </TooltipProvider>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
@@ -13,7 +13,13 @@ export function WebResultIcon({
|
|||||||
size?: number;
|
size?: number;
|
||||||
}) {
|
}) {
|
||||||
const [error, setError] = useState(false);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{hostname == "docs.onyx.app" ? (
|
{hostname == "docs.onyx.app" ? (
|
||||||
|
Reference in New Issue
Block a user