Compare commits

...

9 Commits

Author SHA1 Message Date
pablodanswer
f8dacc133b quick nit 2025-02-10 18:30:59 -08:00
pablodanswer
90c3a99219 k 2025-02-10 18:04:47 -08:00
pablodanswer
a7e58173b3 k 2025-02-10 18:04:18 -08:00
pablodanswer
dbca952cc8 k 2025-02-10 17:50:27 -08:00
pablodanswer
5833041ea1 update 2025-02-10 16:59:40 -08:00
pablodanswer
f10342b083 k 2025-02-10 15:55:13 -08:00
pablodanswer
e4cc1adbaa address 2025-02-10 15:52:54 -08:00
pablodanswer
53bdd08703 k 2025-02-10 15:34:26 -08:00
pablodanswer
63f6eaada5 k 2025-02-10 13:45:25 -08:00
11 changed files with 151 additions and 48 deletions

View File

@@ -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>

View File

@@ -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>
))}

View File

@@ -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;

View File

@@ -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 &&

View File

@@ -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;

View File

@@ -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]);

View File

@@ -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;
}

View File

@@ -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>
);

View File

@@ -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 ? (

View File

@@ -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: {

View File

@@ -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 {