]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
refactor: Chat Screen UI rendering (#23333)
authorAleksander Grygier <redacted>
Tue, 19 May 2026 20:38:42 +0000 (22:38 +0200)
committerGitHub <redacted>
Tue, 19 May 2026 20:38:42 +0000 (22:38 +0200)
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenActionScrollDown.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenGreeting.svelte [new file with mode: 0644]
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenProcessingInfo.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenServerError.svelte [new file with mode: 0644]
tools/ui/src/lib/components/app/chat/index.ts
tools/ui/src/routes/+layout.svelte

index bd93a569ce62dc3359f146b3c516935b716c7c7a..e733a64a97a2667e0b11a6c077a49491523608e3 100644 (file)
@@ -1,8 +1,7 @@
 <script lang="ts">
-       import { Trash2, AlertTriangle, RefreshCw } from '@lucide/svelte';
+       import { Trash2 } from '@lucide/svelte';
        import { afterNavigate } from '$app/navigation';
        import { page } from '$app/state';
-       import { fadeInView } from '$lib/actions/fade-in-view.svelte';
        import {
                ChatScreenForm,
                ChatMessages,
@@ -13,9 +12,9 @@
                DialogFileUploadError,
                DialogChatError,
                ServerLoadingSplash,
-               DialogConfirmation
+               DialogConfirmation,
+               ChatScreenServerError
        } from '$lib/components/app';
-       import * as Alert from '$lib/components/ui/alert';
        import { setProcessingInfoContext } from '$lib/contexts';
        import { ErrorDialogType } from '$lib/enums';
        import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
                activeConversation
        } from '$lib/stores/conversations.svelte';
        import { config } from '$lib/stores/settings.svelte';
-       import { serverLoading, serverError, serverStore, isRouterMode } from '$lib/stores/server.svelte';
+       import { serverLoading, serverError, isRouterMode } from '$lib/stores/server.svelte';
        import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
        import { isFileTypeSupported, filterFilesByModalities } from '$lib/utils';
        import { parseFilesToMessageExtras, processFilesToChatUploaded } from '$lib/utils/browser-only';
        import { onMount } from 'svelte';
+       import ChatScreenGreeting from './ChatScreenGreeting.svelte';
 
        let { showCenteredEmpty = false } = $props();
 
@@ -68,6 +68,8 @@
 
        let showEmptyFileDialog = $state(false);
 
+       let processingInfoVisible = $state(false);
+
        let emptyFileNames = $state<string[]>([]);
 
        let initialMessage = $state('');
                showDeleteDialog = false;
        }
 
+       function handleProcessingInfoVisibility(visible: boolean) {
+               processingInfoVisible = visible;
+       }
+
        function handleDragEnter(event: DragEvent) {
                event.preventDefault();
 
                        {#if !isEmpty}
                                <ChatMessages
                                        messages={activeMessages()}
+                                       onMessagesReady={handleMessagesReady}
                                        onUserAction={() => {
                                                autoScroll.enable();
                                                if (!autoScroll.userScrolledUp) {
                                                        autoScroll.scrollToBottom();
                                                }
                                        }}
-                                       onMessagesReady={handleMessagesReady}
                                />
                        {/if}
 
                        <div
-                               class="pointer-events-none {isEmpty
-                                       ? 'absolute bottom-[calc(50dvh-7rem)]'
-                                       : 'sticky bottom-4'} right-4 left-4 mt-auto -mb-14 pt-16 transition-all duration-200"
+                               class={[
+                                       'pointer-events-none sticky right-4 left-4 mt-auto transition-all duration-200',
+                                       isEmpty ? 'bottom-[calc(50dvh-7rem)]' : 'bottom-4 pt-24 md:pt-32'
+                               ]}
                        >
-                               {#if isEmpty}
-                                       <div class="mb-8 px-4 text-center" use:fadeInView={{ duration: 300 }}>
-                                               <h1 class="mb-2 text-2xl font-semibold tracking-tight md:text-3xl">Hello there</h1>
-
-                                               <p class="text-muted-foreground md:text-lg">
-                                                       {serverStore.props?.modalities?.audio
-                                                               ? 'Record audio, type a message '
-                                                               : 'Type a message'} or upload files to get started
-                                               </p>
-                                       </div>
-                               {/if}
-
-                               <ChatScreenActionScrollDown container={chatScrollContainer} />
-
-                               {#if page.params.id}
-                                       <ChatScreenProcessingInfo />
-                               {/if}
-
-                               {#if hasPropsError}
-                                       <div
-                                               class="pointer-events-auto mx-auto mb-4 max-w-[48rem] px-1"
-                                               use:fadeInView={{ y: 10, duration: 250 }}
-                                       >
-                                               <Alert.Root variant="destructive">
-                                                       <AlertTriangle class="h-4 w-4" />
-                                                       <Alert.Title class="flex items-center justify-between">
-                                                               <span>Server unavailable</span>
-                                                               <button
-                                                                       onclick={() => serverStore.fetch()}
-                                                                       disabled={isServerLoading}
-                                                                       class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-2 py-1 text-xs font-medium hover:bg-destructive/30 disabled:opacity-50"
-                                                               >
-                                                                       <RefreshCw class="h-3 w-3 {isServerLoading ? 'animate-spin' : ''}" />
-                                                                       {isServerLoading ? 'Retrying...' : 'Retry'}
-                                                               </button>
-                                                       </Alert.Title>
-                                                       <Alert.Description>{serverError()}</Alert.Description>
-                                               </Alert.Root>
-                                       </div>
-                               {/if}
+                               <ChatScreenGreeting {isEmpty} />
+
+                               <ChatScreenActionScrollDown
+                                       container={chatScrollContainer}
+                                       hasProcessingInfoVisible={processingInfoVisible}
+                               />
+
+                               <ChatScreenProcessingInfo onVisibilityChange={handleProcessingInfoVisibility} />
+
+                               <ChatScreenServerError />
 
                                <div class="conversation-chat-form pointer-events-auto rounded-t-3xl">
                                        <ChatScreenForm
index 3f3ee86771087e3fa46ea8789581bb0d635ddc8e..bbfed95e94877cf6560edfd495de0e9a3caa3b2e 100644 (file)
@@ -2,10 +2,17 @@
        import { ArrowDown } from '@lucide/svelte';
        import { Button } from '$lib/components/ui/button';
 
-       let { container }: { container: HTMLDivElement | undefined } = $props();
+       interface Props {
+               container: HTMLDivElement | undefined;
+               hasProcessingInfoVisible: boolean;
+       }
+
+       let { container, hasProcessingInfoVisible }: Props = $props();
 
        let show = $state(false);
 
+       let buttonBottom = $derived(hasProcessingInfoVisible ? '2rem' : '0');
+
        function checkVisibility() {
                if (!container) return;
                const { scrollTop, scrollHeight, clientHeight } = container;
                onclick={scrollToBottom}
                variant="secondary"
                size="icon"
-               class="h-10 w-10 rounded-full bg-background/80 shadow-lg backdrop-blur-sm transition-all duration-200 hover:bg-muted/80"
+               class="absolute h-10 w-10 rounded-full bg-background/80 shadow-lg backdrop-blur-sm transition-all duration-200 hover:bg-muted/80"
+               style="bottom: {buttonBottom}; transform: translateY({show ? '0' : '2rem'}); opacity: {show
+                       ? 1
+                       : 0};"
                aria-label="Scroll to bottom"
-               style="transform: translateY({show ? '0' : '20px'}); opacity: {show ? 1 : 0};"
        >
                <ArrowDown class="h-4 w-4" />
        </Button>
diff --git a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenGreeting.svelte b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenGreeting.svelte
new file mode 100644 (file)
index 0000000..141d4f4
--- /dev/null
@@ -0,0 +1,25 @@
+<script lang="ts">
+       import { fadeInView } from '$lib/actions/fade-in-view.svelte';
+       import { serverStore } from '$lib/stores/server.svelte';
+
+       interface Props {
+               isEmpty: boolean;
+       }
+
+       let { isEmpty = false }: Props = $props();
+</script>
+
+<div
+       class={[
+               'pointer-events-none mb-4 hidden px-4 text-center',
+               isEmpty && 'pointer-events-auto block!'
+       ]}
+       use:fadeInView={{ duration: 300 }}
+>
+       <h1 class="mb-2 text-2xl font-semibold tracking-tight md:text-3xl">Hello there</h1>
+
+       <p class="text-muted-foreground md:text-lg">
+               {serverStore.props?.modalities?.audio ? 'Record audio, type a message ' : 'Type a message'} or upload
+               files to get started
+       </p>
+</div>
index b5979db13c2fea44a8598d0f873fcb4dafe21245..f38f3519c3480f43fd22dbe0ac6dcfa6c1f537ab 100644 (file)
@@ -6,6 +6,7 @@
        import { activeMessages, activeConversation } from '$lib/stores/conversations.svelte';
        import { config } from '$lib/stores/settings.svelte';
        import { getProcessingInfoContext } from '$lib/contexts';
+       import { page } from '$app/state';
 
        const processingState = useProcessingState();
        const processingInfoCtx = getProcessingInfoContext();
        let isStreaming = $derived(isChatStreaming());
        let processingDetails = $derived(processingState.getTechnicalDetails());
 
+       let processingVisible = $derived(processingDetails.length > 0);
+
+       let { onVisibilityChange }: { onVisibilityChange?: (visible: boolean) => void } = $props();
+
+       $effect(() => {
+               onVisibilityChange?.(processingVisible);
+       });
+
        $effect(() => {
                const conversation = activeConversation();
 
 </script>
 
 <div
-       class={['chat-processing-info-container pointer-events-none', showProcessingInfo && 'visible']}
+       class={[
+               'chat-processing-info-container pointer-events-none relative',
+               page.params.id && showProcessingInfo && 'visible'
+       ]}
 >
-       <div class="chat-processing-info-content">
+       <div class="chat-processing-info-content absolute bottom-4 left-1/2 -translate-x-1/2">
                {#each processingDetails as detail (detail)}
                        <span class="chat-processing-info-detail pointer-events-auto backdrop-blur-sm">{detail}</span>
                {/each}
diff --git a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenServerError.svelte b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenServerError.svelte
new file mode 100644 (file)
index 0000000..2a998db
--- /dev/null
@@ -0,0 +1,34 @@
+<script lang="ts">
+       import { AlertTriangle, RefreshCw } from '@lucide/svelte';
+       import { fadeInView } from '$lib/actions/fade-in-view.svelte';
+       import * as Alert from '$lib/components/ui/alert';
+       import { serverError, serverLoading, serverStore } from '$lib/stores/server.svelte';
+
+       let hasError = $derived(!!serverError());
+</script>
+
+{#if hasError}
+       <div
+               class="pointer-events-auto mx-auto mb-4 max-w-[48rem] px-1"
+               use:fadeInView={{ y: 10, duration: 250 }}
+       >
+               <Alert.Root variant="destructive">
+                       <AlertTriangle class="h-4 w-4" />
+
+                       <Alert.Title class="flex items-center justify-between">
+                               <span>Server unavailable</span>
+
+                               <button
+                                       onclick={() => serverStore.fetch()}
+                                       disabled={serverLoading()}
+                                       class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-2 py-1 text-xs font-medium hover:bg-destructive/30 disabled:opacity-50"
+                               >
+                                       <RefreshCw class="h-3 w-3 {serverLoading() ? 'animate-spin' : ''}" />
+                                       {serverLoading() ? 'Retrying...' : 'Retry'}
+                               </button>
+                       </Alert.Title>
+
+                       <Alert.Description>{serverError()}</Alert.Description>
+               </Alert.Root>
+       </div>
+{/if}
index 9c7ce864e216ec9836d3b6d20eb71d0cd2fa5b84..be5535960247a08643b907beb77f9fee69949c09 100644 (file)
@@ -674,3 +674,10 @@ export { default as ChatScreenProcessingInfo } from './ChatScreen/ChatScreenProc
  * Takes the chat container element as a prop to manage scroll state internally.
  */
 export { default as ChatScreenActionScrollDown } from './ChatScreen/ChatScreenActionScrollDown.svelte';
+
+/**
+ * Server error alert displayed when the server is unreachable.
+ * Shows the error message with a retry button.
+ * Rendered inside ChatScreen when `serverError` store has a value.
+ */
+export { default as ChatScreenServerError } from './ChatScreen/ChatScreenServerError.svelte';
index 78227df3ce71b2880fe6e6d70d6377dbaf94477c..0610b07ae7761ca494a57c8d2d95701db492b228 100644 (file)
        />
 
        <Sidebar.Provider bind:open={sidebarOpen}>
-               <div class="flex h-screen w-full" style:height="{innerHeight}px">
+               <div class="flex h-screen w-full">
                        <Sidebar.Root variant="floating" class="h-full"
                                ><SidebarNavigation bind:this={chatSidebar} /></Sidebar.Root
                        >