Chat apps are very popular since ChatGPT got viral, and so many tools were created for building Chat apps, e.g. langchain.

However, almost all AI chat apps I've seen are created with React.
Svelte is my favourite and what I used to create Kunkun, and I have to implement a AI chat app with svelte.

Dependencies

Markdown Component

```svelte title="Markdown.svelte"

The `pre` custom renderer is optional. I used a custom `pre` renderer to add a copy button.



```svelte title="Pre.svelte"

    import { Button } from '@kksh/svelte5';
    import { CopyIcon } from 'lucide-svelte';
    import type { Snippet } from 'svelte';

    let pre: HTMLPreElement;

    let {
        children,
        class: className,
        style
    }: { children: Snippet; class?: string; style?: string } = $props();



    {@render children()}
     navigator.clipboard.writeText(pre.textContent ?? '')}
        class="absolute right-2 top-2"
    >

Server

https://sdk.vercel.ai/docs/getting-started/svelte contains example to create server with ai sdk with SvelteKit server routes.

However, I prefer using a separate server for this. My current favourite server framework is Hono.

See https://sdk.vercel.ai/cookbook/api-servers/hono#hono for AI Sdk's Hono sample code.

Chat Page

The SERVER_URL_URL constant is the url to the server. https:///api/chat.

```svelte title="chat/+page.svelte"

import { SERVER_URL_URL } from '@/constants';
import { useChat } from '@ai-sdk/svelte';
import { Markdown } from '@kksh/ui/markdown';
import { Button, Textarea, Card, Input, Badge } from '@kksh/svelte5';
import { ScrollArea } from '@kksh/ui';
import { IconMultiplexer } from '@kksh/ui';
import { IconEnum } from '@kksh/api/models';
import { GlobeIcon } from 'lucide-svelte';
import sampleMessages from './sample-messages.json';
import 'katex/dist/katex.min.css';
import { onMount } from 'svelte';
import Inspect from 'svelte-inspect-value';
import { dev } from '$app/environment';
import { preferences } from '@/stores/preference';
import { fade } from 'svelte/transition';
import { type UIMessage } from 'ai';

const { data } = $props();
let usage = $state<{
    completionTokens: number;
    promptTokens: number;
    totalTokens: number;
}>({
    completionTokens: 0,
    promptTokens: 0,
    totalTokens: 0
});
const { input, handleSubmit, messages, setMessages } = useChat({
    api: SERVER_URL_URL,
    headers: {
        Authorization: `Bearer ${data.session?.access_token}`
    },
    onResponse: (response) => {
        console.log(response.headers);
    },
    onFinish: (msg, options) => {
        console.log('finished', msg, options);
        usage = options.usage;
    }
});

let messagesContainer: HTMLDivElement | null = $state(null);

$effect(() => {
    if ($messages.length > 0) {
        scrollToBottom();
    }
});

onMount(() => {
    scrollToBottom();
    if (dev) {
        setMessages(sampleMessages as unknown as UIMessage[]);
    }
});

const scrollToBottom = () => {
    if (messagesContainer) {
        messagesContainer.scrollTo({
            top: messagesContainer.scrollHeight,
            behavior: 'smooth'
        });
    }
};

const handleKeyDown = (event: KeyboardEvent) => {
    // Handle Ctrl+K to clear messages
    if (event.key === 'k' && (event.ctrlKey || event.metaKey)) {
        event.preventDefault();
        setMessages([]);
        $input = '';
        return;
    }

    // Handle Enter for submission (but not with Shift)
    if (event.key === 'Enter' && !event.shiftKey) {
        event.preventDefault();
        handleSubmit(event);
    }
};

/* -------------------------------------------------------------------------- */
/*                               Textarea Expand                              */
/* -------------------------------------------------------------------------- */
let textareaEl: HTMLTextAreaElement | null = $state(null);

const autoResize = (textarea: HTMLTextAreaElement) => {
    textarea.style.height = 'auto';
    const newHeight = Math.min(textarea.scrollHeight, 10 * 24); // Assuming ~24px per row
    textarea.style.height = newHeight + 'px';
};

const handleInput = (event: Event) => {
    const textarea = event.target as HTMLTextAreaElement;
    autoResize(textarea);
};

{#snippet usageDisplay()}


Tokens used: {usage.totalTokens}


Prompt tokens: {usage.promptTokens}


Completion tokens: {usage.completionTokens}



{message.content}

{:else}

                {message.role}


        {/if}
    {/each}








                    Search





            {@render usageDisplay()}