Skip to content

Commit 55152e6

Browse files
committed
frontend: reduce route-switch flicker with cached chat chrome
1 parent d6fa552 commit 55152e6

5 files changed

Lines changed: 327 additions & 5 deletions

File tree

frontend/src/app/(dashboard)/app/page.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22

33
import { Suspense } from "react"
44
import { AppContentRouter } from "@/components/app/AppContentRouter"
5+
import ChatInput from "@/components/app/ChatInput"
56
import { useThemeColors } from "@/context/ThemeContext"
67

78
function SuspenseFallback() {
89
const COLORS = useThemeColors()
9-
return <div style={{ flex: 1, minHeight: 0, background: COLORS.bg }} />
10+
return (
11+
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", background: COLORS.bg, position: "relative", overflow: "hidden" }}>
12+
<div style={{ flex: 1, minHeight: 0, background: COLORS.bg }} />
13+
<div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "flex-end", justifyContent: "center", paddingBottom: "6px", pointerEvents: "none", zIndex: 10 }}>
14+
<div style={{ pointerEvents: "auto" }}>
15+
<ChatInput />
16+
</div>
17+
</div>
18+
</div>
19+
)
1020
}
1121

1222
export default function AppPage() {

frontend/src/app/(dashboard)/session/SessionRouteFallback.tsx

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
"use client"
22

3+
import { useMemo } from "react"
34
import { useThemeColors, useIsDark } from "@/context/ThemeContext"
45
import { FONTS } from "@/lib/constants"
6+
import { readSessionCache } from "@/lib/sessionCache"
7+
import { readLatestSessionUiCache, readSessionUiCache } from "@/lib/sessionUiCache"
8+
9+
function readRouteSessionId(): string {
10+
if (typeof window === "undefined") return ""
11+
try {
12+
return new URLSearchParams(window.location.search).get("id")?.trim() || ""
13+
} catch {
14+
return ""
15+
}
16+
}
517

618
/**
719
* Shown while the session page is loading (Suspense). Keeps TopBar + content area
@@ -10,12 +22,32 @@ import { FONTS } from "@/lib/constants"
1022
export function SessionRouteFallback() {
1123
const COLORS = useThemeColors()
1224
const isDark = useIsDark()
25+
const sessionId = useMemo(() => readRouteSessionId(), [])
26+
const cachedUi = useMemo(
27+
() => (sessionId ? readSessionUiCache(sessionId) : null),
28+
[sessionId],
29+
)
30+
const cachedSession = useMemo(
31+
() => (sessionId ? readSessionCache(sessionId) : null),
32+
[sessionId],
33+
)
34+
const latestUi = useMemo(
35+
() => (!cachedUi ? readLatestSessionUiCache() : null),
36+
[cachedUi],
37+
)
38+
const fallbackTitle = (
39+
cachedUi?.title
40+
|| cachedSession?.question
41+
|| latestUi?.entry.title
42+
|| "Session"
43+
).trim() || "Session"
44+
const fallbackDraft = cachedUi?.draft ?? latestUi?.entry.draft ?? ""
1345
const topBarGradient = isDark
1446
? "linear-gradient(180deg, transparent 0%, rgba(255, 255, 255, 0.02) 50%, rgba(255, 255, 255, 0.08) 100%)"
1547
: "linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.02) 50%, rgba(0, 0, 0, 0.06) 100%)"
1648

1749
return (
18-
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden", background: COLORS.bg }}>
50+
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden", background: COLORS.bg, position: "relative" }}>
1951
<header
2052
style={{
2153
height: "60px",
@@ -27,10 +59,39 @@ export function SessionRouteFallback() {
2759
}}
2860
>
2961
<span style={{ fontFamily: FONTS.sans, fontSize: "16px", fontWeight: 600, color: COLORS.textPrimary }}>
30-
Untitled
62+
{fallbackTitle}
3163
</span>
3264
</header>
3365
<div style={{ flex: 1, minHeight: 0, background: COLORS.bg }} />
66+
<div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "flex-end", justifyContent: "center", paddingBottom: "6px", pointerEvents: "none", zIndex: 10 }}>
67+
<div
68+
style={{
69+
width: "720px",
70+
maxWidth: "85vw",
71+
borderRadius: "24px",
72+
border: `1px solid ${COLORS.border}`,
73+
background: isDark ? "#0A0A0A" : COLORS.modalBg,
74+
padding: "18px 24px",
75+
minHeight: "64px",
76+
display: "flex",
77+
alignItems: "center",
78+
}}
79+
>
80+
<span
81+
style={{
82+
fontFamily: FONTS.sans,
83+
fontSize: "16px",
84+
color: fallbackDraft.trim() ? COLORS.textPrimary : COLORS.textDimmed,
85+
whiteSpace: "nowrap",
86+
overflow: "hidden",
87+
textOverflow: "ellipsis",
88+
width: "100%",
89+
}}
90+
>
91+
{fallbackDraft.trim() ? fallbackDraft : "Add to thread..."}
92+
</span>
93+
</div>
94+
</div>
3495
</div>
3596
)
3697
}

frontend/src/components/blocks/SessionView.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { useScrollbarOnScroll } from "@/hooks/useScrollbarOnScroll"
2727
import { useStreamingDisplay } from "@/hooks/useStreamingDisplay"
2828
import { logHiddenError, toUserFacingError } from "@/lib/userFacingErrors"
2929
import { trackFrontendEvent } from "@/lib/frontendAnalytics"
30+
import { readSessionUiCache, writeSessionUiCache } from "@/lib/sessionUiCache"
3031
import { AgentThinkingStatus, useAgentThinkingPhase } from "@/components/app/AgentThinkingStatus"
3132
import {
3233
MODEL_OPTIONS,
@@ -76,6 +77,13 @@ function normalizeUserText(value: string | undefined): string {
7677
return (value || "").replace(/\s+/g, " ").trim().toLowerCase()
7778
}
7879

80+
function normalizeCachedModelId(value: string): ModelOptionId {
81+
if (MODEL_OPTIONS.some((option) => option.id === value)) {
82+
return value as ModelOptionId
83+
}
84+
return DEFAULT_MODEL_ID
85+
}
86+
7987
/** Format originQuestion for display: strip [Focus: "..."] and show as input: "highlighted" */
8088
function formatOriginQuestionForDisplay(originQuestion: string): string {
8189
const focusMatch = originQuestion.match(/^(.*?)\s*\[Focus:\s*"([^"]*)"\]\s*$/i)
@@ -359,6 +367,7 @@ export function SessionView({ initialSession, backendSessionId, readOnly = false
359367
const programmaticScrollRef = useRef(false)
360368
const shouldScrollToBlockRef = useRef(false)
361369
const printContentRef = useRef<HTMLDivElement>(null)
370+
const sessionCacheId = (backendId || initialSession.id || session.id || "").trim()
362371
const handlePrint = useReactToPrint({
363372
contentRef: printContentRef,
364373
documentTitle: session.question,
@@ -392,6 +401,31 @@ export function SessionView({ initialSession, backendSessionId, readOnly = false
392401
/* ── Section in view for TOC magnifier (which entry to emphasize) ── */
393402
const [activeTocEntryId, setActiveTocEntryId] = useState<string | null>(null)
394403

404+
useEffect(() => {
405+
if (!sessionCacheId) return
406+
const cachedUi = readSessionUiCache(sessionCacheId)
407+
if (!cachedUi) return
408+
setChatInput(cachedUi.draft || "")
409+
setInputModel(normalizeCachedModelId(cachedUi.model))
410+
setInputLength(cachedUi.lengthPreset)
411+
if (!sidebarTitle && cachedUi.title.trim()) {
412+
setSession((prev) => {
413+
if (prev.question === cachedUi.title) return prev
414+
return { ...prev, question: cachedUi.title }
415+
})
416+
}
417+
}, [sessionCacheId, sidebarTitle])
418+
419+
useEffect(() => {
420+
if (!sessionCacheId) return
421+
writeSessionUiCache(sessionCacheId, {
422+
title: sidebarTitle ?? session.question,
423+
draft: chatInput,
424+
model: inputModel,
425+
lengthPreset: inputLength,
426+
})
427+
}, [sessionCacheId, sidebarTitle, session.question, chatInput, inputModel, inputLength])
428+
395429
/* ════════════ Derived data ════════════ */
396430

397431
const activeBlock = useMemo(

0 commit comments

Comments
 (0)