DEV Community

Huakun Shen
Huakun Shen

Posted on

Build Svelte AI Chat with Vercel's AI sdk

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"

import rehypeShikiFromHighlighter from "@shikijs/rehype/core" import rehypeClassNames from "rehype-class-names" import rehypeKatex from "rehype-katex" import remarkMath from "remark-math" import { createHighlighterCoreSync } from "shiki/core" import { createJavaScriptRegexEngine } from "shiki/engine/javascript" import svelte from "shiki/langs/svelte.mjs" import ts from "shiki/langs/typescript.mjs" import githubDarkDefault from "shiki/themes/github-dark-default.mjs" import Markdown from "svelte-exmarkdown" import type { Plugin } from "svelte-exmarkdown" import Pre from "./Pre.svelte" const addClass: Plugin = { rehypePlugin: [ rehypeClassNames, { pre: "p-4 rounded-md overflow-auto" } ] } const shikiPlugin = { rehypePlugin: [ rehypeShikiFromHighlighter, createHighlighterCoreSync({ themes: [githubDarkDefault], langs: [ ts, svelte, ], engine: createJavaScriptRegexEngine() }), { theme: "github-dark-default" } ] } satisfies Plugin const plugins: Plugin[] = [ shikiPlugin, { remarkPlugin: [remarkMath], rehypePlugin: [rehypeKatex] }, addClass, { renderer: { pre: Pre } } ] let { md }: { md: string } = $props()



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



```svelte title="Pre.svelte"
<script lang="ts">
    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();
</script>

<div class="relative">
    <pre class={className} {style} bind:this={pre}>{@render children()}</pre>
    <Button
        size="icon"
        variant="outline"
        onclick={() => navigator.clipboard.writeText(pre.textContent ?? '')}
        class="absolute right-2 top-2"
    >
        <CopyIcon />
    </Button>
</div>
Enter fullscreen mode Exit fullscreen mode

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.

Sample Hono Code ```ts title="server.ts" import { openai } from "@ai-sdk/openai"; import { serve } from "@hono/node-server"; import { streamText } from "ai"; import { Hono } from "hono"; import { stream } from "hono/streaming"; const app = new Hono(); app.post("/", async (c) => { const result = streamText({ model: openai("gpt-4o"), prompt: "Invent a new holiday and describe its traditions.", }); // Mark the response as a v1 data stream: c.header("X-Vercel-AI-Data-Stream", "v1"); c.header("Content-Type", "text/plain; charset=utf-8"); return stream(c, (stream) => stream.pipe(result.toDataStream())); }); serve({ fetch: app.fetch, port: 8080 }); ```

Chat Page

The SERVER_URL_URL constant is the url to the server. https://<domain>/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&lt;{
    completionTokens: number;
    promptTokens: number;
    totalTokens: number;
}&gt;({
    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) =&gt; {
        console.log(response.headers);
    },
    onFinish: (msg, options) =&gt; {
        console.log('finished', msg, options);
        usage = options.usage;
    }
});

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

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

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

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

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

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

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

const autoResize = (textarea: HTMLTextAreaElement) =&gt; {
    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) =&gt; {
    const textarea = event.target as HTMLTextAreaElement;
    autoResize(textarea);
};
Enter fullscreen mode Exit fullscreen mode

{#snippet usageDisplay()}


Tokens used: {usage.totalTokens}


Prompt tokens: {usage.promptTokens}


Completion tokens: {usage.completionTokens}



{/snippet} {#if $preferences.developerMode} {/if} {#each $messages as message} {#if message.role === 'user'}

{message.content}

        {:else}

                {message.role}


        {/if}
    {/each}








                    <span>Search</span>





            {@render usageDisplay()}
Enter fullscreen mode Exit fullscreen mode


Enter fullscreen mode Exit fullscreen mode

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay