This commit is contained in:
pablonyx
2025-02-12 13:53:13 -08:00
committed by GitHub
parent 61136975ad
commit e030b0a6fc
15 changed files with 170 additions and 69 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,11 +1444,22 @@ export function ChatPage({
Object.hasOwn(packet, "stop_reason") &&
Object.hasOwn(packet, "level_question_num")
) {
if ((packet as StreamStopInfo).stream_type == "main_answer") {
setFinishedStreaming(true);
updateChatState("streaming", frozenSessionId);
}
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
);
} else if (Object.hasOwn(packet, "sub_question")) {
updateChatState("toolBuilding", frozenSessionId);
is_generating = true;
sub_questions = constructSubQuestions(
sub_questions,
@@ -1606,6 +1619,7 @@ export function ChatPage({
latestChildMessageId: initialFetchDetails.assistant_message_id,
},
{
isStreamingQuestions: isStreamingQuestions,
is_generating: is_generating,
isImprovement: isImprovement,
messageId: initialFetchDetails.assistant_message_id!,
@@ -2637,6 +2651,12 @@ export function ChatPage({
{message.sub_questions &&
message.sub_questions.length > 0 ? (
<AgenticMessage
isStreamingQuestions={
message.isStreamingQuestions ?? false
}
isGenerating={
message.is_generating ?? false
}
docSidebarToggled={
documentSidebarVisible &&
(selectedMessageForDocDisplay ==
@@ -2734,7 +2754,8 @@ export function ChatPage({
setMessageAsLatest(messageId);
}}
isActive={
messageHistory.length - 1 == i
messageHistory.length - 1 == i ||
messageHistory.length - 2 == i
}
selectedDocuments={selectedDocuments}
toggleDocumentSelection={(
@@ -3072,7 +3093,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

@@ -819,27 +819,17 @@ export function ChatInputBar({
chatState == "toolBuilding" ||
chatState == "loading"
? chatState != "streaming"
? "bg-neutral-900 dark:bg-neutral-400 "
: "bg-neutral-500 dark:bg-neutral-50"
: ""
? "bg-neutral-500 dark:bg-neutral-400 "
: "bg-neutral-900 dark:bg-neutral-50"
: "bg-red-200"
} h-[22px] w-[22px] rounded-full`}
onClick={() => {
if (
chatState == "streaming" ||
chatState == "toolBuilding" ||
chatState == "loading"
) {
if (chatState == "streaming") {
stopGenerating();
} else if (message) {
onSubmit();
}
}}
disabled={
(chatState == "streaming" ||
chatState == "toolBuilding" ||
chatState == "loading") &&
chatState != "streaming"
}
>
{chatState == "streaming" ||
chatState == "toolBuilding" ||

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_streaming?: boolean;
}
export interface SubQueryDetail {
@@ -245,9 +247,6 @@ export const constructSubQuestions = (
}
const updatedSubQuestions = [...subQuestions];
// .filter(
// (sq) => sq.level_question_num !== 0
// );
if ("stop_reason" in newDetail) {
const { level, level_question_num } = newDetail;
@@ -255,8 +254,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_streaming = false;
} 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

@@ -957,7 +957,7 @@ export const HumanMessage = ({
min-h-[38px]
py-2
px-3
hover:bg-accent-hover
hover:bg-agent-hovered
`}
onClick={handleEditSubmit}
>

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;
@@ -121,6 +128,7 @@ export const useStreamingMessages = (
// Stream high-level questions sequentially
let didStreamQuestion = false;
let allQuestionsComplete = true;
for (let i = 0; i < actualSubQs.length; i++) {
const sq = actualSubQs[i];
const p = progressRef.current[i];
@@ -138,6 +146,8 @@ export const useStreamingMessages = (
p.questionDone = true;
}
didStreamQuestion = true;
allQuestionsComplete = false;
// Break after streaming one question to ensure sequential behavior
break;
}
@@ -149,7 +159,11 @@ export const useStreamingMessages = (
}
}
if (allQuestionsComplete && !didStreamQuestion) {
if (
allQuestionsComplete &&
!didStreamQuestion &&
!isStreamingQuestionsRef.current
) {
onComplete();
}
@@ -163,6 +177,8 @@ export const useStreamingMessages = (
for (let i = 0; i < actualSubQs.length; i++) {
const sq = actualSubQs[i];
const dynSQ = dynamicSubQuestionsRef.current[i];
dynSQ.answer_streaming = sq.answer_streaming;
const p = progressRef.current[i];
// Wait for subquestion #0 or the previous subquestion's progress

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,8 @@ const SubQuestionDisplay: React.FC<{
content = content.replace(/\]\](?!\()/g, "]]()");
return (
preprocessLaTeX(content) + (!subQuestion?.is_complete ? " [*]() " : "")
preprocessLaTeX(content) +
(subQuestion?.answer_streaming ? " [*]() " : "")
);
};
@@ -461,6 +463,7 @@ const SubQuestionDisplay: React.FC<{
};
const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
isStreamingQuestions,
finishedGenerating,
subQuestions,
allowStreaming,
@@ -477,23 +480,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 +518,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" : ""
);
@@ -524,12 +527,15 @@ const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
const [shownDocuments, setShownDocuments] = useState(documents);
useEffect(() => {
if (documents && documents.length > 0) {
setTimeout(() => {
setShownDocuments(documents);
}, 800);
if (canShowSummarizing && documents && documents.length > 0) {
setTimeout(
() => {
setShownDocuments(documents);
},
finishedGenerating ? 0 : 800
);
}
}, [documents]);
}, [documents, canShowSummarizing]);
useEffect(() => {
if (

View File

@@ -263,6 +263,8 @@ export function SharedChatDisplay({
) {
return (
<AgenticMessage
isStreamingQuestions={false}
isGenerating={false}
shared
key={message.messageId}
isImprovement={message.isImprovement}
@@ -373,6 +375,8 @@ export function SharedChatDisplay({
<div key={message.messageId}>
<AgenticMessage
shared
isStreamingQuestions={false}
isGenerating={false}
subQuestions={message.sub_questions || []}
currentPersona={persona}
messageId={message.messageId}

View File

@@ -121,6 +121,7 @@
/* agent references */
--agent-sidebar: #be5d0e;
--agent-hovered: #d16b10;
--agent: #e47011;
--lighter-agent: #f59e0b;
@@ -341,6 +342,7 @@
/* Agent references */
--agent-sidebar: #be5d0e; /* You can keep or lighten/darken if desired */
--agent-hovered: #f07c13;
--agent: #e47011;
--lighter-agent: #f59e0b;
}
@@ -658,6 +660,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 {

View File

@@ -263,7 +263,7 @@ module.exports = {
"agent-sidebar": "var(--agent-sidebar)",
agent: "var(--agent)",
"lighter-agent": "var(--lighter-agent)",
"agent-hovered": "var(--agent-hovered)",
// hover
"hover-light": "var(--hover-light)",
"hover-lightish": "var(--neutral-125)",