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,
};
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) {
const providerModels = catalog[provider]?.models ?? [];
if (providerModels.length) return providerModels;
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() {
if (typeof window === "undefined") return EMPTY_MODEL_PREFERENCES;
try {
@@ -443,6 +456,7 @@ export default function App() {
return stored.openai ?? PROVIDER_FALLBACK_MODELS.openai[0];
});
const [error, setError] = useState<string | null>(null);
const [transcriptTailSpacerHeight, setTranscriptTailSpacerHeight] = useState(TRANSCRIPT_BOTTOM_GAP);
const transcriptContainerRef = useRef<HTMLDivElement>(null);
const transcriptEndRef = useRef<HTMLDivElement>(null);
const contextMenuRef = useRef<HTMLDivElement>(null);
@@ -453,12 +467,41 @@ export default function App() {
const shouldAutoScrollRef = useRef(true);
const wasSendingRef = useRef(false);
const pendingReplyScrollRef = useRef(false);
const transcriptTailSpacerHeightRef = useRef(TRANSCRIPT_BOTTOM_GAP);
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
const [sidebarQuery, setSidebarQuery] = useState("");
const initialRouteSelectionRef = useRef<SidebarSelection | null>(readSidebarSelectionFromUrl());
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 = () => {
if (typeof window === "undefined") return;
window.requestAnimationFrame(() => {
@@ -653,6 +696,9 @@ export default function App() {
useEffect(() => {
shouldAutoScrollRef.current = true;
if (!isSending || !isChatReplyStreamingInView) {
setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
}
}, [draftKind, selectedItem?.kind, selectedKey]);
useEffect(() => {
@@ -855,6 +901,7 @@ export default function App() {
const handleSendChat = async (content: string) => {
pendingReplyScrollRef.current = true;
expandTranscriptTailSpacer(getReplyScrollBufferHeight());
const optimisticUserMessage: Message = {
id: `temp-user-${Date.now()}`,
@@ -1043,6 +1090,7 @@ export default function App() {
if (currentSelection?.kind === "chat" && currentSelection.id === chatId) {
await refreshChat(chatId);
}
settleTranscriptTailSpacer();
setPendingChatState(null);
};
@@ -1462,9 +1510,11 @@ export default function App() {
onStartChat={selectedSearch ? handleStartChatFromSearch : undefined}
/>
)}
{isChatReplyStreamingInView ? (
<div className="mx-auto mt-6 h-[52vh] min-h-72 max-h-[36rem] max-w-4xl" aria-hidden="true" />
) : null}
<div
className="mx-auto max-w-4xl"
style={{ height: `${transcriptTailSpacerHeight}px` }}
aria-hidden="true"
/>
<div ref={transcriptEndRef} />
</div>