mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
added smooth streaming
This commit is contained in:
parent
a4febb09c0
commit
4a331c7e5c
2 changed files with 55 additions and 1 deletions
|
|
@ -33,6 +33,7 @@ import {
|
||||||
} from '@/components/ai-elements/prompt-input';
|
} from '@/components/ai-elements/prompt-input';
|
||||||
|
|
||||||
import { Shimmer } from '@/components/ai-elements/shimmer';
|
import { Shimmer } from '@/components/ai-elements/shimmer';
|
||||||
|
import { useSmoothedText } from './hooks/useSmoothedText';
|
||||||
import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from '@/components/ai-elements/tool';
|
import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from '@/components/ai-elements/tool';
|
||||||
import { WebSearchResult } from '@/components/ai-elements/web-search-result';
|
import { WebSearchResult } from '@/components/ai-elements/web-search-result';
|
||||||
import { AppActionCard } from '@/components/ai-elements/app-action-card';
|
import { AppActionCard } from '@/components/ai-elements/app-action-card';
|
||||||
|
|
@ -93,6 +94,11 @@ interface TreeNode extends DirEntry {
|
||||||
|
|
||||||
const streamdownComponents = { pre: MarkdownPreOverride }
|
const streamdownComponents = { pre: MarkdownPreOverride }
|
||||||
|
|
||||||
|
function SmoothStreamingMessage({ text, components }: { text: string; components: typeof streamdownComponents }) {
|
||||||
|
const smoothText = useSmoothedText(text)
|
||||||
|
return <MessageResponse components={components}>{smoothText}</MessageResponse>
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_SIDEBAR_WIDTH = 256
|
const DEFAULT_SIDEBAR_WIDTH = 256
|
||||||
const wikiLinkRegex = /\[\[([^[\]]+)\]\]/g
|
const wikiLinkRegex = /\[\[([^[\]]+)\]\]/g
|
||||||
const graphPalette = [
|
const graphPalette = [
|
||||||
|
|
@ -4237,7 +4243,7 @@ function App() {
|
||||||
{tabState.currentAssistantMessage && (
|
{tabState.currentAssistantMessage && (
|
||||||
<Message from="assistant">
|
<Message from="assistant">
|
||||||
<MessageContent>
|
<MessageContent>
|
||||||
<MessageResponse components={streamdownComponents}>{tabState.currentAssistantMessage.replace(/<\/?voice>/g, '')}</MessageResponse>
|
<SmoothStreamingMessage text={tabState.currentAssistantMessage.replace(/<\/?voice>/g, '')} components={streamdownComponents} />
|
||||||
</MessageContent>
|
</MessageContent>
|
||||||
</Message>
|
</Message>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
48
apps/x/apps/renderer/src/hooks/useSmoothedText.ts
Normal file
48
apps/x/apps/renderer/src/hooks/useSmoothedText.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smoothly reveals streamed text by buffering incoming chunks and releasing
|
||||||
|
* them gradually via requestAnimationFrame, producing the fluid typing effect
|
||||||
|
* seen in apps like Claude and ChatGPT.
|
||||||
|
*/
|
||||||
|
export function useSmoothedText(targetText: string): string {
|
||||||
|
const [displayText, setDisplayText] = useState('')
|
||||||
|
const targetRef = useRef('')
|
||||||
|
const displayLenRef = useRef(0)
|
||||||
|
const rafRef = useRef<number>(0)
|
||||||
|
|
||||||
|
targetRef.current = targetText
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Target cleared → immediately clear display
|
||||||
|
if (!targetText) {
|
||||||
|
displayLenRef.current = 0
|
||||||
|
setDisplayText('')
|
||||||
|
cancelAnimationFrame(rafRef.current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
const target = targetRef.current
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
const currentLen = displayLenRef.current
|
||||||
|
if (currentLen < target.length) {
|
||||||
|
const remaining = target.length - currentLen
|
||||||
|
// Adaptive speed: reveal faster when buffer is large, slower when small
|
||||||
|
const step = Math.max(2, Math.ceil(remaining * 0.18))
|
||||||
|
displayLenRef.current = Math.min(currentLen + step, target.length)
|
||||||
|
setDisplayText(target.slice(0, displayLenRef.current))
|
||||||
|
rafRef.current = requestAnimationFrame(tick)
|
||||||
|
}
|
||||||
|
// When caught up, stop. New useEffect call restarts when more text arrives.
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelAnimationFrame(rafRef.current)
|
||||||
|
rafRef.current = requestAnimationFrame(tick)
|
||||||
|
|
||||||
|
return () => cancelAnimationFrame(rafRef.current)
|
||||||
|
}, [targetText])
|
||||||
|
|
||||||
|
return displayText
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue