mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-18 14:12:45 +00:00
Compare commits
9 Commits
refactor/t
...
v0.20.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8dacc133b | ||
|
|
90c3a99219 | ||
|
|
a7e58173b3 | ||
|
|
dbca952cc8 | ||
|
|
5833041ea1 | ||
|
|
f10342b083 | ||
|
|
e4cc1adbaa | ||
|
|
53bdd08703 | ||
|
|
63f6eaada5 |
@@ -1122,6 +1122,7 @@ export function ChatPage({
|
||||
"Continue Generating (pick up exactly where you left off)",
|
||||
});
|
||||
};
|
||||
const [gener, setFinishedStreaming] = useState(false);
|
||||
|
||||
const onSubmit = async ({
|
||||
messageIdToResend,
|
||||
@@ -1272,6 +1273,7 @@ export function ChatPage({
|
||||
let finalMessage: BackendMessage | null = null;
|
||||
let toolCall: ToolCallMetadata | null = null;
|
||||
let isImprovement: boolean | undefined = undefined;
|
||||
let isStreamingQuestions = true;
|
||||
|
||||
let initialFetchDetails: null | {
|
||||
user_message_id: number;
|
||||
@@ -1442,6 +1444,15 @@ export function ChatPage({
|
||||
Object.hasOwn(packet, "stop_reason") &&
|
||||
Object.hasOwn(packet, "level_question_num")
|
||||
) {
|
||||
if ((packet as StreamStopInfo).stream_type == "main_answer") {
|
||||
setFinishedStreaming(true);
|
||||
}
|
||||
if (
|
||||
(packet as StreamStopInfo).stream_type == "sub_questions" &&
|
||||
(packet as StreamStopInfo).level_question_num == undefined
|
||||
) {
|
||||
isStreamingQuestions = false;
|
||||
}
|
||||
sub_questions = constructSubQuestions(
|
||||
sub_questions,
|
||||
packet as StreamStopInfo
|
||||
@@ -1606,6 +1617,7 @@ export function ChatPage({
|
||||
latestChildMessageId: initialFetchDetails.assistant_message_id,
|
||||
},
|
||||
{
|
||||
isStreamingQuestions: isStreamingQuestions,
|
||||
is_generating: is_generating,
|
||||
isImprovement: isImprovement,
|
||||
messageId: initialFetchDetails.assistant_message_id!,
|
||||
@@ -2635,6 +2647,12 @@ export function ChatPage({
|
||||
{message.sub_questions &&
|
||||
message.sub_questions.length > 0 ? (
|
||||
<AgenticMessage
|
||||
isStreamingQuestions={
|
||||
message.isStreamingQuestions ?? false
|
||||
}
|
||||
isGenerating={
|
||||
message.is_generating ?? false
|
||||
}
|
||||
docSidebarToggled={
|
||||
documentSidebarToggled &&
|
||||
(selectedMessageForDocDisplay ==
|
||||
@@ -2732,7 +2750,8 @@ export function ChatPage({
|
||||
setMessageAsLatest(messageId);
|
||||
}}
|
||||
isActive={
|
||||
messageHistory.length - 1 == i
|
||||
messageHistory.length - 1 == i ||
|
||||
messageHistory.length - 2 == i
|
||||
}
|
||||
selectedDocuments={selectedDocuments}
|
||||
toggleDocumentSelection={(
|
||||
@@ -3070,7 +3089,7 @@ export function ChatPage({
|
||||
<div className="mx-auto w-fit !pointer-events-none flex sticky justify-center">
|
||||
<button
|
||||
onClick={() => clientScrollToBottom()}
|
||||
className="p-1 pointer-events-auto rounded-2xl bg-background-strong border border-border mx-auto "
|
||||
className="p-1 pointer-events-auto text-neutral-700 dark:text-neutral-800 rounded-2xl bg-neutral-200 border border-border mx-auto "
|
||||
>
|
||||
<FiArrowDown size={18} />
|
||||
</button>
|
||||
|
||||
@@ -115,21 +115,61 @@ export function RefinemenetBadge({
|
||||
const isDone = displayedPhases.includes(StreamingPhase.COMPLETE);
|
||||
|
||||
// Expand/collapse, hover states
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const [expanded] = useState(true);
|
||||
const [toolTipHoveredInternal, setToolTipHoveredInternal] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [shouldShow, setShouldShow] = useState(true);
|
||||
|
||||
// Refs for bounding area checks
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Keep the tooltip open if hovered on container or tooltip
|
||||
// Remove the old onMouseLeave calls and rely on bounding area checks
|
||||
useEffect(() => {
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (!containerRef.current || !tooltipRef.current) return;
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
||||
const [x, y] = [e.clientX, e.clientY];
|
||||
|
||||
const inContainer =
|
||||
x >= containerRect.left &&
|
||||
x <= containerRect.right &&
|
||||
y >= containerRect.top &&
|
||||
y <= containerRect.bottom;
|
||||
|
||||
const inTooltip =
|
||||
x >= tooltipRect.left &&
|
||||
x <= tooltipRect.right &&
|
||||
y >= tooltipRect.top &&
|
||||
y <= tooltipRect.bottom;
|
||||
|
||||
// If not hovering in either region, close tooltip
|
||||
if (!inContainer && !inTooltip) {
|
||||
setToolTipHoveredInternal(false);
|
||||
setToolTipHovered(false);
|
||||
setIsHovered(false);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
}, [setToolTipHovered]);
|
||||
|
||||
// Once "done", hide after a short delay if not hovered
|
||||
useEffect(() => {
|
||||
if (isDone) {
|
||||
const timer = setTimeout(() => {
|
||||
setShouldShow(false);
|
||||
setCanShowResponse(true);
|
||||
}, 800); // e.g. 0.8s
|
||||
}, 800);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isDone, isHovered]);
|
||||
}, [isDone, isHovered, setCanShowResponse]);
|
||||
|
||||
if (!shouldShow) {
|
||||
return null; // entire box disappears
|
||||
@@ -137,13 +177,22 @@ export function RefinemenetBadge({
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
{/*
|
||||
IMPORTANT: We rely on open={ isHovered || toolTipHoveredInternal }
|
||||
to keep the tooltip visible if either the badge or tooltip is hovered.
|
||||
*/}
|
||||
<Tooltip open={isHovered || toolTipHoveredInternal}>
|
||||
<div
|
||||
className="relative w-fit max-w-sm"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
ref={containerRef}
|
||||
// onMouseEnter keeps the tooltip open
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true);
|
||||
setToolTipHoveredInternal(true);
|
||||
setToolTipHovered(true);
|
||||
}}
|
||||
// Remove the explicit onMouseLeave – the global bounding check will close it
|
||||
>
|
||||
{/* Original snippet's tooltip usage */}
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-x-1 text-black text-sm font-medium cursor-pointer hover:text-blue-600 transition-colors duration-200">
|
||||
<p className="text-sm loading-text font-medium">
|
||||
@@ -159,36 +208,32 @@ export function RefinemenetBadge({
|
||||
</TooltipTrigger>
|
||||
{expanded && (
|
||||
<TooltipContent
|
||||
ref={tooltipRef}
|
||||
// onMouseEnter keeps the tooltip open when cursor enters tooltip
|
||||
onMouseEnter={() => {
|
||||
setToolTipHoveredInternal(true);
|
||||
setToolTipHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setToolTipHoveredInternal(false);
|
||||
}}
|
||||
// Remove onMouseLeave and rely on bounding box logic to close
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="w-fit -mt-1 p-4 bg-white border-2 border-border shadow-lg rounded-md"
|
||||
width="w-fit"
|
||||
className=" -mt-1 p-4 bg-[#fff] dark:bg-[#000] border-2 border-border dark:border-neutral-800 shadow-lg rounded-md"
|
||||
>
|
||||
{/* If not done, show the "Refining" box + a chevron */}
|
||||
|
||||
{/* Expanded area: each displayed phase in order */}
|
||||
|
||||
<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
|
||||
Array.from(new Set(displayedPhases)).length - 1 &&
|
||||
phase !== StreamingPhase.COMPLETE
|
||||
) {
|
||||
status = ToggleState.InProgress;
|
||||
}
|
||||
if (phase === StreamingPhase.COMPLETE) {
|
||||
status = ToggleState.Done;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -338,6 +383,7 @@ export function StatusRefinement({
|
||||
onMouseLeave={() => setToolTipHovered(false)}
|
||||
side="bottom"
|
||||
align="start"
|
||||
width="w-fit"
|
||||
className="w-fit p-4 bg-[#fff] border-2 border-border dark:border-neutral-800 shadow-lg rounded-md"
|
||||
>
|
||||
{/* If not done, show the "Refining" box + a chevron */}
|
||||
@@ -355,7 +401,6 @@ export function StatusRefinement({
|
||||
</div>
|
||||
<span className="text-neutral-800 text-sm font-medium">
|
||||
{StreamingPhaseText[phase]}
|
||||
LLL
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -110,6 +110,7 @@ export interface Message {
|
||||
second_level_message?: string;
|
||||
second_level_subquestions?: SubQuestionDetail[] | null;
|
||||
isImprovement?: boolean | null;
|
||||
isStreamingQuestions?: boolean;
|
||||
}
|
||||
|
||||
export interface BackendChatSession {
|
||||
@@ -219,6 +220,7 @@ export interface SubQuestionDetail extends BaseQuestionIdentifier {
|
||||
context_docs?: { top_documents: OnyxDocument[] } | null;
|
||||
is_complete?: boolean;
|
||||
is_stopped?: boolean;
|
||||
answer_done?: boolean;
|
||||
}
|
||||
|
||||
export interface SubQueryDetail {
|
||||
@@ -255,8 +257,12 @@ export const constructSubQuestions = (
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
if (subQuestion) {
|
||||
subQuestion.is_complete = true;
|
||||
subQuestion.is_stopped = true;
|
||||
if (newDetail.stream_type == "sub_answer") {
|
||||
subQuestion.answer_done = true;
|
||||
} else {
|
||||
subQuestion.is_complete = true;
|
||||
subQuestion.is_stopped = true;
|
||||
}
|
||||
}
|
||||
} else if ("top_documents" in newDetail) {
|
||||
const { level, level_question_num, top_documents } = newDetail;
|
||||
|
||||
@@ -48,9 +48,10 @@ import rehypeKatex from "rehype-katex";
|
||||
import "katex/dist/katex.min.css";
|
||||
import SubQuestionsDisplay from "./SubQuestionsDisplay";
|
||||
import { StatusRefinement } from "../Refinement";
|
||||
import SubQuestionProgress from "./SubQuestionProgress";
|
||||
|
||||
export const AgenticMessage = ({
|
||||
isStreamingQuestions,
|
||||
isGenerating,
|
||||
docSidebarToggled,
|
||||
isImprovement,
|
||||
secondLevelAssistantMessage,
|
||||
@@ -81,6 +82,8 @@ export const AgenticMessage = ({
|
||||
secondLevelSubquestions,
|
||||
toggleDocDisplay,
|
||||
}: {
|
||||
isStreamingQuestions: boolean;
|
||||
isGenerating: boolean;
|
||||
docSidebarToggled?: boolean;
|
||||
isImprovement?: boolean | null;
|
||||
secondLevelSubquestions?: SubQuestionDetail[] | null;
|
||||
@@ -230,6 +233,13 @@ export const AgenticMessage = ({
|
||||
);
|
||||
const [currentlyOpenQuestion, setCurrentlyOpenQuestion] =
|
||||
useState<BaseQuestionIdentifier | null>(null);
|
||||
const [finishedGenerating, setFinishedGenerating] = useState(!isGenerating);
|
||||
|
||||
useEffect(() => {
|
||||
if (streamedContent.length == finalContent.length && !isGenerating) {
|
||||
setFinishedGenerating(true);
|
||||
}
|
||||
}, [streamedContent, finalContent, isGenerating]);
|
||||
|
||||
const openQuestion = useCallback(
|
||||
(question: SubQuestionDetail) => {
|
||||
@@ -400,12 +410,10 @@ export const AgenticMessage = ({
|
||||
<div className="w-full desktop:ml-4">
|
||||
{subQuestions && subQuestions.length > 0 && (
|
||||
<SubQuestionsDisplay
|
||||
isStreamingQuestions={isStreamingQuestions}
|
||||
allowDocuments={() => setAllowDocuments(true)}
|
||||
docSidebarToggled={docSidebarToggled || false}
|
||||
finishedGenerating={
|
||||
finalContent.length > 2 &&
|
||||
streamedContent.length == finalContent.length
|
||||
}
|
||||
finishedGenerating={finishedGenerating}
|
||||
overallAnswerGenerating={
|
||||
!!(
|
||||
secondLevelSubquestions &&
|
||||
|
||||
@@ -56,12 +56,19 @@ const DOC_DELAY_MS = 100;
|
||||
export const useStreamingMessages = (
|
||||
subQuestions: SubQuestionDetail[],
|
||||
allowStreaming: () => void,
|
||||
onComplete: () => void
|
||||
onComplete: () => void,
|
||||
isStreamingQuestions: boolean
|
||||
) => {
|
||||
const [dynamicSubQuestions, setDynamicSubQuestions] = useState<
|
||||
SubQuestionDetail[]
|
||||
>([]);
|
||||
|
||||
const isStreamingQuestionsRef = useRef(isStreamingQuestions);
|
||||
|
||||
useEffect(() => {
|
||||
isStreamingQuestionsRef.current = isStreamingQuestions;
|
||||
}, [isStreamingQuestions]);
|
||||
|
||||
const subQuestionsRef = useRef<SubQuestionDetail[]>(subQuestions);
|
||||
useEffect(() => {
|
||||
subQuestionsRef.current = subQuestions;
|
||||
@@ -149,7 +156,11 @@ export const useStreamingMessages = (
|
||||
}
|
||||
}
|
||||
|
||||
if (allQuestionsComplete && !didStreamQuestion) {
|
||||
if (
|
||||
allQuestionsComplete &&
|
||||
!didStreamQuestion &&
|
||||
!isStreamingQuestionsRef.current
|
||||
) {
|
||||
onComplete();
|
||||
}
|
||||
|
||||
@@ -163,6 +174,8 @@ export const useStreamingMessages = (
|
||||
for (let i = 0; i < actualSubQs.length; i++) {
|
||||
const sq = actualSubQs[i];
|
||||
const dynSQ = dynamicSubQuestionsRef.current[i];
|
||||
dynSQ.answer_done = sq.answer_done;
|
||||
|
||||
const p = progressRef.current[i];
|
||||
|
||||
// Wait for subquestion #0 or the previous subquestion's progress
|
||||
@@ -193,6 +206,7 @@ export const useStreamingMessages = (
|
||||
|
||||
switch (p.currentPhase) {
|
||||
case StreamingPhase.SUB_QUERIES: {
|
||||
onComplete();
|
||||
const subQueries = sq.sub_queries || [];
|
||||
const docs = sq.context_docs?.top_documents || [];
|
||||
const hasDocs = docs.length > 0;
|
||||
|
||||
@@ -65,6 +65,7 @@ export interface TemporaryDisplay {
|
||||
tinyQuestion: string;
|
||||
}
|
||||
interface SubQuestionsDisplayProps {
|
||||
isStreamingQuestions: boolean;
|
||||
docSidebarToggled: boolean;
|
||||
finishedGenerating: boolean;
|
||||
currentlyOpenQuestion?: BaseQuestionIdentifier | null;
|
||||
@@ -152,7 +153,7 @@ const SubQuestionDisplay: React.FC<{
|
||||
content = content.replace(/\]\](?!\()/g, "]]()");
|
||||
|
||||
return (
|
||||
preprocessLaTeX(content) + (!subQuestion?.is_complete ? " [*]() " : "")
|
||||
preprocessLaTeX(content) + (!subQuestion?.answer_done ? " [*]() " : "")
|
||||
);
|
||||
};
|
||||
|
||||
@@ -461,6 +462,7 @@ const SubQuestionDisplay: React.FC<{
|
||||
};
|
||||
|
||||
const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
|
||||
isStreamingQuestions,
|
||||
finishedGenerating,
|
||||
subQuestions,
|
||||
allowStreaming,
|
||||
@@ -477,23 +479,29 @@ const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
|
||||
const [showSummarizing, setShowSummarizing] = useState(
|
||||
finishedGenerating && !overallAnswerGenerating
|
||||
);
|
||||
const [initiallyFinishedGenerating, setInitiallyFinishedGenerating] =
|
||||
useState(finishedGenerating);
|
||||
// const []
|
||||
const { dynamicSubQuestions } = useStreamingMessages(
|
||||
subQuestions,
|
||||
() => {},
|
||||
() => {
|
||||
setShowSummarizing(true);
|
||||
}
|
||||
},
|
||||
isStreamingQuestions
|
||||
);
|
||||
const { dynamicSubQuestions: dynamicSecondLevelQuestions } =
|
||||
useStreamingMessages(
|
||||
secondLevelQuestions || [],
|
||||
() => {},
|
||||
() => {}
|
||||
() => {},
|
||||
false
|
||||
);
|
||||
|
||||
const memoizedSubQuestions = useMemo(() => {
|
||||
return finishedGenerating ? subQuestions : dynamicSubQuestions;
|
||||
}, [finishedGenerating, dynamicSubQuestions, subQuestions]);
|
||||
// const memoizedSubQuestions = dynamicSubQuestions;
|
||||
return initiallyFinishedGenerating ? subQuestions : dynamicSubQuestions;
|
||||
}, [initiallyFinishedGenerating, dynamicSubQuestions, subQuestions]);
|
||||
|
||||
const memoizedSecondLevelQuestions = useMemo(() => {
|
||||
return overallAnswerGenerating
|
||||
? dynamicSecondLevelQuestions
|
||||
@@ -509,12 +517,6 @@ const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
|
||||
(subQuestion) => (subQuestion?.sub_queries || [])?.length > 0
|
||||
).length == 0;
|
||||
|
||||
const overallAnswer =
|
||||
memoizedSubQuestions.length > 0 &&
|
||||
memoizedSubQuestions.filter(
|
||||
(subQuestion) => subQuestion?.answer.length > 10
|
||||
).length == memoizedSubQuestions.length;
|
||||
|
||||
const [streamedText, setStreamedText] = useState(
|
||||
finishedGenerating ? "Summarize findings" : ""
|
||||
);
|
||||
@@ -525,9 +527,12 @@ const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (documents && documents.length > 0) {
|
||||
setTimeout(() => {
|
||||
setShownDocuments(documents);
|
||||
}, 800);
|
||||
setTimeout(
|
||||
() => {
|
||||
setShownDocuments(documents);
|
||||
},
|
||||
finishedGenerating ? 0 : 800
|
||||
);
|
||||
}
|
||||
}, [documents]);
|
||||
|
||||
|
||||
@@ -656,6 +656,11 @@ ul > li > p {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark li {
|
||||
.dark li,
|
||||
.dark h1,
|
||||
.dark h2,
|
||||
.dark h3,
|
||||
.dark h4,
|
||||
.dark h5 {
|
||||
color: #e5e5e5;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function IndexAttemptStatus({
|
||||
);
|
||||
} else if (status === "not_started") {
|
||||
badge = (
|
||||
<Badge variant="purple" icon={FiClock}>
|
||||
<Badge variant="not_started" icon={FiClock}>
|
||||
Scheduled
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -65,7 +65,7 @@ export function Citation({
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="dark:border dark:!bg-[#000] border-neutral-700"
|
||||
className="border border-neutral-300 hover:text-neutral-900 bg-neutral-100 dark:!bg-[#000] dark:border-neutral-700"
|
||||
width="mb-2 max-w-lg"
|
||||
>
|
||||
{document_info?.document ? (
|
||||
|
||||
@@ -37,7 +37,7 @@ const badgeVariants = cva(
|
||||
destructive:
|
||||
"border-red-200 bg-red-50 text-red-600 dark:border-red-700 dark:bg-red-900 dark:text-neutral-50",
|
||||
not_started:
|
||||
"border-neutral-200 bg-neutral-50 text-neutral-600 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100",
|
||||
"border-purple-200 bg-purple-50 text-purple-700 dark:border-purple-700 dark:bg-purple-900 dark:text-purple-100",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface StreamStopInfo {
|
||||
stop_reason: StreamStopReason;
|
||||
level?: number;
|
||||
level_question_num?: number;
|
||||
stream_type?: "sub_answer" | "sub_questions" | "main_answer";
|
||||
}
|
||||
|
||||
export interface ErrorMessagePacket {
|
||||
|
||||
Reference in New Issue
Block a user