Skip to content

Commit 67ace02

Browse files
authored
refactor: Chat Screen UI rendering (ggml-org#23333)
1 parent a807867 commit 67ace02

7 files changed

Lines changed: 119 additions & 55 deletions

File tree

tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte

Lines changed: 26 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
<script lang="ts">
2-
import { Trash2, AlertTriangle, RefreshCw } from '@lucide/svelte';
2+
import { Trash2 } from '@lucide/svelte';
33
import { afterNavigate } from '$app/navigation';
44
import { page } from '$app/state';
5-
import { fadeInView } from '$lib/actions/fade-in-view.svelte';
65
import {
76
ChatScreenForm,
87
ChatMessages,
@@ -13,9 +12,9 @@
1312
DialogFileUploadError,
1413
DialogChatError,
1514
ServerLoadingSplash,
16-
DialogConfirmation
15+
DialogConfirmation,
16+
ChatScreenServerError
1717
} from '$lib/components/app';
18-
import * as Alert from '$lib/components/ui/alert';
1918
import { setProcessingInfoContext } from '$lib/contexts';
2019
import { ErrorDialogType } from '$lib/enums';
2120
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
@@ -35,11 +34,12 @@
3534
activeConversation
3635
} from '$lib/stores/conversations.svelte';
3736
import { config } from '$lib/stores/settings.svelte';
38-
import { serverLoading, serverError, serverStore, isRouterMode } from '$lib/stores/server.svelte';
37+
import { serverLoading, serverError, isRouterMode } from '$lib/stores/server.svelte';
3938
import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
4039
import { isFileTypeSupported, filterFilesByModalities } from '$lib/utils';
4140
import { parseFilesToMessageExtras, processFilesToChatUploaded } from '$lib/utils/browser-only';
4241
import { onMount } from 'svelte';
42+
import ChatScreenGreeting from './ChatScreenGreeting.svelte';
4343
4444
let { showCenteredEmpty = false } = $props();
4545
@@ -68,6 +68,8 @@
6868
6969
let showEmptyFileDialog = $state(false);
7070
71+
let processingInfoVisible = $state(false);
72+
7173
let emptyFileNames = $state<string[]>([]);
7274
7375
let initialMessage = $state('');
@@ -175,6 +177,10 @@
175177
showDeleteDialog = false;
176178
}
177179
180+
function handleProcessingInfoVisibility(visible: boolean) {
181+
processingInfoVisible = visible;
182+
}
183+
178184
function handleDragEnter(event: DragEvent) {
179185
event.preventDefault();
180186
@@ -395,61 +401,32 @@
395401
{#if !isEmpty}
396402
<ChatMessages
397403
messages={activeMessages()}
404+
onMessagesReady={handleMessagesReady}
398405
onUserAction={() => {
399406
autoScroll.enable();
400407
if (!autoScroll.userScrolledUp) {
401408
autoScroll.scrollToBottom();
402409
}
403410
}}
404-
onMessagesReady={handleMessagesReady}
405411
/>
406412
{/if}
407413

408414
<div
409-
class="pointer-events-none {isEmpty
410-
? 'absolute bottom-[calc(50dvh-7rem)]'
411-
: 'sticky bottom-4'} right-4 left-4 mt-auto -mb-14 pt-16 transition-all duration-200"
415+
class={[
416+
'pointer-events-none sticky right-4 left-4 mt-auto transition-all duration-200',
417+
isEmpty ? 'bottom-[calc(50dvh-7rem)]' : 'bottom-4 pt-24 md:pt-32'
418+
]}
412419
>
413-
{#if isEmpty}
414-
<div class="mb-8 px-4 text-center" use:fadeInView={{ duration: 300 }}>
415-
<h1 class="mb-2 text-2xl font-semibold tracking-tight md:text-3xl">Hello there</h1>
416-
417-
<p class="text-muted-foreground md:text-lg">
418-
{serverStore.props?.modalities?.audio
419-
? 'Record audio, type a message '
420-
: 'Type a message'} or upload files to get started
421-
</p>
422-
</div>
423-
{/if}
424-
425-
<ChatScreenActionScrollDown container={chatScrollContainer} />
426-
427-
{#if page.params.id}
428-
<ChatScreenProcessingInfo />
429-
{/if}
430-
431-
{#if hasPropsError}
432-
<div
433-
class="pointer-events-auto mx-auto mb-4 max-w-[48rem] px-1"
434-
use:fadeInView={{ y: 10, duration: 250 }}
435-
>
436-
<Alert.Root variant="destructive">
437-
<AlertTriangle class="h-4 w-4" />
438-
<Alert.Title class="flex items-center justify-between">
439-
<span>Server unavailable</span>
440-
<button
441-
onclick={() => serverStore.fetch()}
442-
disabled={isServerLoading}
443-
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"
444-
>
445-
<RefreshCw class="h-3 w-3 {isServerLoading ? 'animate-spin' : ''}" />
446-
{isServerLoading ? 'Retrying...' : 'Retry'}
447-
</button>
448-
</Alert.Title>
449-
<Alert.Description>{serverError()}</Alert.Description>
450-
</Alert.Root>
451-
</div>
452-
{/if}
420+
<ChatScreenGreeting {isEmpty} />
421+
422+
<ChatScreenActionScrollDown
423+
container={chatScrollContainer}
424+
hasProcessingInfoVisible={processingInfoVisible}
425+
/>
426+
427+
<ChatScreenProcessingInfo onVisibilityChange={handleProcessingInfoVisibility} />
428+
429+
<ChatScreenServerError />
453430

454431
<div class="conversation-chat-form pointer-events-auto rounded-t-3xl">
455432
<ChatScreenForm

tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenActionScrollDown.svelte

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22
import { ArrowDown } from '@lucide/svelte';
33
import { Button } from '$lib/components/ui/button';
44
5-
let { container }: { container: HTMLDivElement | undefined } = $props();
5+
interface Props {
6+
container: HTMLDivElement | undefined;
7+
hasProcessingInfoVisible: boolean;
8+
}
9+
10+
let { container, hasProcessingInfoVisible }: Props = $props();
611
712
let show = $state(false);
813
14+
let buttonBottom = $derived(hasProcessingInfoVisible ? '2rem' : '0');
15+
916
function checkVisibility() {
1017
if (!container) return;
1118
const { scrollTop, scrollHeight, clientHeight } = container;
@@ -39,9 +46,11 @@
3946
onclick={scrollToBottom}
4047
variant="secondary"
4148
size="icon"
42-
class="h-10 w-10 rounded-full bg-background/80 shadow-lg backdrop-blur-sm transition-all duration-200 hover:bg-muted/80"
49+
class="absolute h-10 w-10 rounded-full bg-background/80 shadow-lg backdrop-blur-sm transition-all duration-200 hover:bg-muted/80"
50+
style="bottom: {buttonBottom}; transform: translateY({show ? '0' : '2rem'}); opacity: {show
51+
? 1
52+
: 0};"
4353
aria-label="Scroll to bottom"
44-
style="transform: translateY({show ? '0' : '20px'}); opacity: {show ? 1 : 0};"
4554
>
4655
<ArrowDown class="h-4 w-4" />
4756
</Button>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script lang="ts">
2+
import { fadeInView } from '$lib/actions/fade-in-view.svelte';
3+
import { serverStore } from '$lib/stores/server.svelte';
4+
5+
interface Props {
6+
isEmpty: boolean;
7+
}
8+
9+
let { isEmpty = false }: Props = $props();
10+
</script>
11+
12+
<div
13+
class={[
14+
'pointer-events-none mb-4 hidden px-4 text-center',
15+
isEmpty && 'pointer-events-auto block!'
16+
]}
17+
use:fadeInView={{ duration: 300 }}
18+
>
19+
<h1 class="mb-2 text-2xl font-semibold tracking-tight md:text-3xl">Hello there</h1>
20+
21+
<p class="text-muted-foreground md:text-lg">
22+
{serverStore.props?.modalities?.audio ? 'Record audio, type a message ' : 'Type a message'} or upload
23+
files to get started
24+
</p>
25+
</div>

tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenProcessingInfo.svelte

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { activeMessages, activeConversation } from '$lib/stores/conversations.svelte';
77
import { config } from '$lib/stores/settings.svelte';
88
import { getProcessingInfoContext } from '$lib/contexts';
9+
import { page } from '$app/state';
910
1011
const processingState = useProcessingState();
1112
const processingInfoCtx = getProcessingInfoContext();
@@ -16,6 +17,14 @@
1617
let isStreaming = $derived(isChatStreaming());
1718
let processingDetails = $derived(processingState.getTechnicalDetails());
1819
20+
let processingVisible = $derived(processingDetails.length > 0);
21+
22+
let { onVisibilityChange }: { onVisibilityChange?: (visible: boolean) => void } = $props();
23+
24+
$effect(() => {
25+
onVisibilityChange?.(processingVisible);
26+
});
27+
1928
$effect(() => {
2029
const conversation = activeConversation();
2130
@@ -60,9 +69,12 @@
6069
</script>
6170

6271
<div
63-
class={['chat-processing-info-container pointer-events-none', showProcessingInfo && 'visible']}
72+
class={[
73+
'chat-processing-info-container pointer-events-none relative',
74+
page.params.id && showProcessingInfo && 'visible'
75+
]}
6476
>
65-
<div class="chat-processing-info-content">
77+
<div class="chat-processing-info-content absolute bottom-4 left-1/2 -translate-x-1/2">
6678
{#each processingDetails as detail (detail)}
6779
<span class="chat-processing-info-detail pointer-events-auto backdrop-blur-sm">{detail}</span>
6880
{/each}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<script lang="ts">
2+
import { AlertTriangle, RefreshCw } from '@lucide/svelte';
3+
import { fadeInView } from '$lib/actions/fade-in-view.svelte';
4+
import * as Alert from '$lib/components/ui/alert';
5+
import { serverError, serverLoading, serverStore } from '$lib/stores/server.svelte';
6+
7+
let hasError = $derived(!!serverError());
8+
</script>
9+
10+
{#if hasError}
11+
<div
12+
class="pointer-events-auto mx-auto mb-4 max-w-[48rem] px-1"
13+
use:fadeInView={{ y: 10, duration: 250 }}
14+
>
15+
<Alert.Root variant="destructive">
16+
<AlertTriangle class="h-4 w-4" />
17+
18+
<Alert.Title class="flex items-center justify-between">
19+
<span>Server unavailable</span>
20+
21+
<button
22+
onclick={() => serverStore.fetch()}
23+
disabled={serverLoading()}
24+
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"
25+
>
26+
<RefreshCw class="h-3 w-3 {serverLoading() ? 'animate-spin' : ''}" />
27+
{serverLoading() ? 'Retrying...' : 'Retry'}
28+
</button>
29+
</Alert.Title>
30+
31+
<Alert.Description>{serverError()}</Alert.Description>
32+
</Alert.Root>
33+
</div>
34+
{/if}

tools/ui/src/lib/components/app/chat/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,3 +674,10 @@ export { default as ChatScreenProcessingInfo } from './ChatScreen/ChatScreenProc
674674
* Takes the chat container element as a prop to manage scroll state internally.
675675
*/
676676
export { default as ChatScreenActionScrollDown } from './ChatScreen/ChatScreenActionScrollDown.svelte';
677+
678+
/**
679+
* Server error alert displayed when the server is unreachable.
680+
* Shows the error message with a retry button.
681+
* Rendered inside ChatScreen when `serverError` store has a value.
682+
*/
683+
export { default as ChatScreenServerError } from './ChatScreen/ChatScreenServerError.svelte';

tools/ui/src/routes/+layout.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@
240240
/>
241241

242242
<Sidebar.Provider bind:open={sidebarOpen}>
243-
<div class="flex h-screen w-full" style:height="{innerHeight}px">
243+
<div class="flex h-screen w-full">
244244
<Sidebar.Root variant="floating" class="h-full"
245245
><SidebarNavigation bind:this={chatSidebar} /></Sidebar.Root
246246
>

0 commit comments

Comments
 (0)