Skip to content

Commit 7c99a76

Browse files
committed
feat: streaming markdown optimization, shimmer effect, elapsed time in thinking
Streaming Markdown: - Debounced parsing during streaming (150ms / 100-char threshold) - Prevents re-parsing entire content on every delta (~50ms) - Pass streaming prop from message-list to MarkdownPreview Shimmer Effect: - .shimmer-text CSS class: gradient text animation on thinking state - Uses theme-aware --text-secondary/--text-tertiary for all 7 themes - prefers-reduced-motion: falls back to static text Thinking State: - Shows elapsed seconds counter next to thinking trail - Formats as Xs (e.g. '12s') for at-a-glance timing
1 parent 784f515 commit 7c99a76

3 files changed

Lines changed: 51 additions & 5 deletions

File tree

app/globals.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2170,6 +2170,28 @@ p {
21702170
animation: shimmer 2s infinite;
21712171
}
21722172

2173+
.shimmer-text {
2174+
background: linear-gradient(
2175+
90deg,
2176+
var(--text-tertiary) 0%,
2177+
var(--text-secondary) 40%,
2178+
var(--text-tertiary) 80%
2179+
);
2180+
background-size: 200% 100%;
2181+
-webkit-background-clip: text;
2182+
background-clip: text;
2183+
-webkit-text-fill-color: transparent;
2184+
animation: shimmer 2.5s ease-in-out infinite;
2185+
}
2186+
2187+
@media (prefers-reduced-motion: reduce) {
2188+
.shimmer-text {
2189+
background: none;
2190+
-webkit-text-fill-color: var(--text-tertiary);
2191+
animation: none;
2192+
}
2193+
}
2194+
21732195
.animate-spin {
21742196
animation: spin 1s linear infinite;
21752197
}

components/chat/message-list.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ export function MessageList({
631631
style={{ fontSize: `${chatFontSize}px`, fontFamily: chatFontCss }}
632632
>
633633
<div className="prose-chat">
634-
<MarkdownPreview content={streamBuffer} />
634+
<MarkdownPreview content={streamBuffer} streaming />
635635
</div>
636636
<span className="inline-block w-1.5 h-3.5 bg-[var(--brand)] animate-pulse ml-0.5 align-text-bottom rounded-sm" />
637637
</div>
@@ -704,11 +704,16 @@ export function MessageList({
704704
<span />
705705
<span />
706706
</div>
707-
<span className="text-[11px] text-[var(--text-tertiary)]">
707+
<span className="text-[11px] text-[var(--text-tertiary)] shimmer-text">
708708
{thinkingTrail.length > 0
709709
? thinkingTrail[thinkingTrail.length - 1]
710710
: 'Thinking...'}
711711
</span>
712+
{turnElapsedMs > 0 && (
713+
<span className="text-[9px] text-[var(--text-disabled)] tabular-nums">
714+
{turnElapsedMs < 1000 ? '' : `${(turnElapsedMs / 1000).toFixed(0)}s`}
715+
</span>
716+
)}
712717
</div>
713718
</div>
714719
)}

components/markdown-preview.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,34 @@ function FloatingCard({
195195
)
196196
}
197197

198-
export function MarkdownPreview({ content, className }: MarkdownPreviewProps) {
198+
export function MarkdownPreview({ content, className, streaming }: MarkdownPreviewProps & { streaming?: boolean }) {
199+
// Debounce parsing during streaming — parse at most every 150ms
200+
const lastParsedRef = useRef('')
201+
const lastBlocksRef = useRef<ReturnType<typeof parse>>([])
202+
const lastParseTimeRef = useRef(0)
203+
199204
const blocks = useMemo(() => {
205+
// During streaming, skip re-parse if content hasn't grown enough or too recent
206+
if (streaming && lastBlocksRef.current.length > 0) {
207+
const now = Date.now()
208+
const timeSince = now - lastParseTimeRef.current
209+
const growth = content.length - lastParsedRef.current.length
210+
if (timeSince < 150 && growth < 100) {
211+
return lastBlocksRef.current
212+
}
213+
}
214+
200215
const clean =
201216
typeof window !== 'undefined'
202217
? DOMPurify.sanitize(content, { ALLOWED_TAGS: [], KEEP_CONTENT: true })
203218
: content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
204219
const normalized = normalizeToMarkdown(clean)
205-
return parse(normalized)
206-
}, [content])
220+
const result = parse(normalized)
221+
lastParsedRef.current = content
222+
lastBlocksRef.current = result
223+
lastParseTimeRef.current = Date.now()
224+
return result
225+
}, [content, streaming])
207226

208227
const containerRef = useRef<HTMLDivElement>(null)
209228
const [hoverTarget, setHoverTarget] = useState<HoverTarget | null>(null)

0 commit comments

Comments
 (0)