<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,
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();
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
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>
--- /dev/null
+<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>
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}
--- /dev/null
+<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}
* 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';
/>
<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
>