web: transcript improvements

This commit is contained in:
2026-05-02 18:25:20 -07:00
parent d7967eaa75
commit 5a690b276f

View File

@@ -98,12 +98,25 @@ const EMPTY_MODEL_PREFERENCES: ProviderModelPreferences = {
xai: null, xai: null,
}; };
const TRANSCRIPT_BOTTOM_GAP = 20;
const REPLY_SCROLL_BUFFER_MIN = 288;
const REPLY_SCROLL_BUFFER_MAX = 576;
const REPLY_SCROLL_BUFFER_VIEWPORT_RATIO = 0.52;
function getModelOptions(catalog: ModelCatalogResponse["providers"], provider: Provider) { function getModelOptions(catalog: ModelCatalogResponse["providers"], provider: Provider) {
const providerModels = catalog[provider]?.models ?? []; const providerModels = catalog[provider]?.models ?? [];
if (providerModels.length) return providerModels; if (providerModels.length) return providerModels;
return PROVIDER_FALLBACK_MODELS[provider]; return PROVIDER_FALLBACK_MODELS[provider];
} }
function getReplyScrollBufferHeight() {
if (typeof window === "undefined") return REPLY_SCROLL_BUFFER_MIN;
return Math.min(
REPLY_SCROLL_BUFFER_MAX,
Math.max(REPLY_SCROLL_BUFFER_MIN, Math.round(window.innerHeight * REPLY_SCROLL_BUFFER_VIEWPORT_RATIO))
);
}
function loadStoredModelPreferences() { function loadStoredModelPreferences() {
if (typeof window === "undefined") return EMPTY_MODEL_PREFERENCES; if (typeof window === "undefined") return EMPTY_MODEL_PREFERENCES;
try { try {
@@ -443,6 +456,7 @@ export default function App() {
return stored.openai ?? PROVIDER_FALLBACK_MODELS.openai[0]; return stored.openai ?? PROVIDER_FALLBACK_MODELS.openai[0];
}); });
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [transcriptTailSpacerHeight, setTranscriptTailSpacerHeight] = useState(TRANSCRIPT_BOTTOM_GAP);
const transcriptContainerRef = useRef<HTMLDivElement>(null); const transcriptContainerRef = useRef<HTMLDivElement>(null);
const transcriptEndRef = useRef<HTMLDivElement>(null); const transcriptEndRef = useRef<HTMLDivElement>(null);
const contextMenuRef = useRef<HTMLDivElement>(null); const contextMenuRef = useRef<HTMLDivElement>(null);
@@ -453,12 +467,41 @@ export default function App() {
const shouldAutoScrollRef = useRef(true); const shouldAutoScrollRef = useRef(true);
const wasSendingRef = useRef(false); const wasSendingRef = useRef(false);
const pendingReplyScrollRef = useRef(false); const pendingReplyScrollRef = useRef(false);
const transcriptTailSpacerHeightRef = useRef(TRANSCRIPT_BOTTOM_GAP);
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null); const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
const [sidebarQuery, setSidebarQuery] = useState(""); const [sidebarQuery, setSidebarQuery] = useState("");
const initialRouteSelectionRef = useRef<SidebarSelection | null>(readSidebarSelectionFromUrl()); const initialRouteSelectionRef = useRef<SidebarSelection | null>(readSidebarSelectionFromUrl());
const hasSyncedSelectionHistoryRef = useRef(false); const hasSyncedSelectionHistoryRef = useRef(false);
const setTranscriptTailSpacer = (height: number) => {
const nextHeight = Math.max(TRANSCRIPT_BOTTOM_GAP, Math.ceil(height));
transcriptTailSpacerHeightRef.current = nextHeight;
setTranscriptTailSpacerHeight(nextHeight);
};
const expandTranscriptTailSpacer = (height: number) => {
const targetHeight = Math.max(TRANSCRIPT_BOTTOM_GAP, Math.ceil(height));
setTranscriptTailSpacerHeight((currentHeight) => {
const nextHeight = Math.max(currentHeight, targetHeight);
transcriptTailSpacerHeightRef.current = nextHeight;
return nextHeight;
});
};
const settleTranscriptTailSpacer = () => {
const container = transcriptContainerRef.current;
const currentSpacerHeight = transcriptTailSpacerHeightRef.current;
if (!container) {
setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
return;
}
const scrollHeightWithoutSpacer = container.scrollHeight - currentSpacerHeight;
const requiredSpacerHeight = container.scrollTop + container.clientHeight - scrollHeightWithoutSpacer;
setTranscriptTailSpacer(requiredSpacerHeight);
};
const focusComposer = () => { const focusComposer = () => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
@@ -653,6 +696,9 @@ export default function App() {
useEffect(() => { useEffect(() => {
shouldAutoScrollRef.current = true; shouldAutoScrollRef.current = true;
if (!isSending || !isChatReplyStreamingInView) {
setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
}
}, [draftKind, selectedItem?.kind, selectedKey]); }, [draftKind, selectedItem?.kind, selectedKey]);
useEffect(() => { useEffect(() => {
@@ -855,6 +901,7 @@ export default function App() {
const handleSendChat = async (content: string) => { const handleSendChat = async (content: string) => {
pendingReplyScrollRef.current = true; pendingReplyScrollRef.current = true;
expandTranscriptTailSpacer(getReplyScrollBufferHeight());
const optimisticUserMessage: Message = { const optimisticUserMessage: Message = {
id: `temp-user-${Date.now()}`, id: `temp-user-${Date.now()}`,
@@ -1043,6 +1090,7 @@ export default function App() {
if (currentSelection?.kind === "chat" && currentSelection.id === chatId) { if (currentSelection?.kind === "chat" && currentSelection.id === chatId) {
await refreshChat(chatId); await refreshChat(chatId);
} }
settleTranscriptTailSpacer();
setPendingChatState(null); setPendingChatState(null);
}; };
@@ -1462,9 +1510,11 @@ export default function App() {
onStartChat={selectedSearch ? handleStartChatFromSearch : undefined} onStartChat={selectedSearch ? handleStartChatFromSearch : undefined}
/> />
)} )}
{isChatReplyStreamingInView ? ( <div
<div className="mx-auto mt-6 h-[52vh] min-h-72 max-h-[36rem] max-w-4xl" aria-hidden="true" /> className="mx-auto max-w-4xl"
) : null} style={{ height: `${transcriptTailSpacerHeight}px` }}
aria-hidden="true"
/>
<div ref={transcriptEndRef} /> <div ref={transcriptEndRef} />
</div> </div>