refactor: improve chat layout updates and remove unused imports

- Remove unused StoredFunctionCall/Response imports from AI classes
- Replace settled flag with shouldSettle parameter for better control
- Add ResizeObserver to handle dynamic content size changes
- Bind indicator elements for accurate height calculations
- Update layout calculation to account for indicators when not settled
This commit is contained in:
Andrew Beal 2025-12-11 08:42:09 +00:00
parent 565bfdfda7
commit 187b831155
6 changed files with 43 additions and 20 deletions

View file

@ -8,7 +8,6 @@ import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFun
import type { ConversationContent } from "Conversations/ConversationContent";
import { Role } from "Enums/Role";
import type { RawMessageStreamEvent, ContentBlockParam, Tool } from '@anthropic-ai/sdk/resources/messages';
import type { StoredFunctionCall, StoredFunctionResponse } from "AIClasses/Schemas/AIFunctionTypes";
import { Exception } from "Helpers/Exception";
export class Claude extends BaseAIClass {

View file

@ -6,7 +6,6 @@ import { AIProvider, AIProviderURL, toProviderModel } from "Enums/ApiProvider";
import { AIFunctionCall } from "AIClasses/AIFunctionCall";
import { fromString as aiFunctionFromString } from "Enums/AIFunction";
import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFunctionDefinition";
import type { StoredFunctionCall, StoredFunctionResponse } from "AIClasses/Schemas/AIFunctionTypes";
import type { ResponseEvent, ResponseOutputTextDelta, ResponseOutputItemDone, ResponseErrorEvent, ResponseFailedEvent, OpenAIFunctionTool, ResponsesAPIInput } from "./OpenAITypes";
import { Exception } from "Helpers/Exception";
import { ApiErrorType } from "Types/ApiError";

View file

@ -31,10 +31,8 @@
chatContainer.scroll({ top: 0, behavior: "instant" });
}
export function updateChatAreaLayout(behavior: ScrollBehavior | undefined) {
export function updateChatAreaLayout(behavior: ScrollBehavior | undefined, shouldSettle: boolean = false) {
tick().then(() => {
settled = false;
if (messageElements.length <= 0 || !chatAreaPaddingElement) {
if (chatAreaPaddingElement) {
chatAreaPaddingElement.style.padding = "0px";
@ -46,25 +44,39 @@
const paddingBottom = parseFloat(getComputedStyle(chatContainer).paddingBottom) || 0;
const messageElement = messageElements.sort((a, b) => a.index - b.index)[messageElements.length - 1];
const messageSpace = messageElement.element.offsetHeight;
let messageSpace = messageElement.element.offsetHeight;
const padding = chatContainer.offsetHeight - paddingTop - paddingBottom - messageSpace;
if (!shouldSettle) {
const gap = parseFloat(getComputedStyle(chatContainer).gap) || 0;
if (thoughtIndicatorElement) {
messageSpace += thoughtIndicatorElement.offsetHeight + gap + gap;
}
if (streamingIndicatorElement) {
messageSpace += streamingIndicatorElement.offsetHeight + gap + gap;
}
}
let padding = chatContainer.offsetHeight - paddingTop - paddingBottom - messageSpace;
if (!shouldSettle) {
padding = Math.max(padding, chatContainer.offsetHeight * 0.25);
}
chatAreaPaddingElement.style.padding = `${Math.max(0, padding / 2)}px`;
tick().then(() => {
if (autoScroll && behavior) {
chatContainer.scroll({ top: chatContainer.scrollHeight, behavior: behavior })
}
tick().then(() => settled = true);
});
});
}
let settled: boolean = false;
let autoScroll: boolean = true;
let lastScrollTop: number = 0;
let chatAreaPaddingElement: HTMLElement | undefined;
let thoughtIndicatorElement: HTMLElement | undefined;
let streamingIndicatorElement: HTMLElement | undefined;
let streamingMarkdownService: StreamingMarkdownService = Resolve<StreamingMarkdownService>(Services.StreamingMarkdownService);
@ -138,6 +150,20 @@
messageElements.push({ index: index, element: element });
}
function observeResize(element: HTMLElement) {
const observer = new ResizeObserver(() => {
updateChatAreaLayout("smooth", false);
});
observer.observe(element);
return {
destroy() {
observer.disconnect();
}
};
}
// decide if we should be auto scrolling
function handleScroll() {
if (!chatContainer) {
@ -196,7 +222,7 @@
}
</script>
<div class="chat-area" bind:this={chatContainer} on:scroll={handleScroll}>
<div class="chat-area" bind:this={chatContainer} on:scroll={handleScroll} use:observeResize>
{#each messages as message, index}
{#if !message.isFunctionCallResponse && message.content.trim() !== ""}
{#if message.role === Role.User}
@ -222,11 +248,9 @@
{/if}
{/each}
{#if settled}
<ThoughtIndicator thought={currentThought}/>
{#if isSubmitting}
<StreamingIndicator editModeActive={editModeActive}/>
{/if}
<ThoughtIndicator thought={currentThought} bind:thoughtIndicatorElement={thoughtIndicatorElement}/>
{#if isSubmitting}
<StreamingIndicator editModeActive={editModeActive} bind:streamingIndicatorElement={streamingIndicatorElement}/>
{/if}
{#if cancelling}

View file

@ -89,7 +89,6 @@
function handleStop() {
chatService.stop();
currentThought = null;
chatArea.updateChatAreaLayout("smooth");
}
async function handleSubmit(userRequest: string, formattedRequest: string) {
@ -121,7 +120,7 @@
cancelling = false;
isSubmitting = false;
abortService.reset();
chatArea.updateChatAreaLayout("smooth");
chatArea.updateChatAreaLayout("smooth", true);
await chatService.updateTokenDisplay(conversation);
},
onCancel: () => {
@ -158,7 +157,7 @@
chatService.onNameChanged?.(loadedConversation.title);
chatService.updateTokenDisplay(loadedConversation);
conversationStore.clearLoadFlag();
chatArea.updateChatAreaLayout("instant");
chatArea.updateChatAreaLayout("instant", true);
}
});
}

View file

@ -1,8 +1,9 @@
<script lang="ts">
export let editModeActive: boolean = false;
export let streamingIndicatorElement: HTMLElement | undefined = undefined;
</script>
<div class="loader" class:edit-mode={editModeActive}>
<div class="loader" class:edit-mode={editModeActive} bind:this={streamingIndicatorElement}>
<div class="circle">
<div class="dot"></div>
<div class="outline"></div>

View file

@ -1,11 +1,12 @@
<script lang="ts">
import { fade } from "svelte/transition";
export let thought: string | null = null;
export let thoughtIndicatorElement: HTMLElement | undefined = undefined;
$: isVisible = thought !== null && thought.trim().length > 0;
</script>
{#if isVisible}
<div class="ai-thought-container" in:fade={{ duration: 200 }} out:fade={{ duration: 200 }}>
<div class="ai-thought-container" in:fade={{ duration: 200 }} out:fade={{ duration: 200 }} bind:this={thoughtIndicatorElement}>
<div class="ai-thought-bubble">
<span>{thought}</span>
</div>