Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.magmadeploy.com/llms.txt

Use this file to discover all available pages before exploring further.

The Svelte package we provide is designed for Svelte 5. If you are interested in support for Svelte 4, please contact us

Initializing a project

If you have not set up a project yet, you can easily initialize a ShadCN-Svelte / Svelte.js project by following the instructions here Or if you prefer not to use ShadCN-Svelte:
npx sv create

Overview

The logic of the websocket chat is handled by a Context provider. You can install it with this command:
npm i @pompeii-labs/svelte-obsidian
We support multiple project setups out of the box.

Using ShadCN

After you have installed the provider, copy this component into your ShadCN project:
chat.svelte
<script lang="ts">
	import { Button } from '$lib/components/ui/button';
	import { Textarea } from '$lib/components/ui/textarea';
	import { getChatContext } from '@pompeii-labs/svelte-obsidian';
	import { cn } from '$lib/utils';
	import type { MagmaUserMessage, MagmaAssistantMessage } from '@pompeii-labs/magma/types';
    import ArrowDown from 'lucide-svelte/icons/arrow-down';
    import ArrowUp from 'lucide-svelte/icons/arrow-up';

	const { sendMessage, messages, awaitingResponse } = $derived(getChatContext());

    let scrollArea: HTMLDivElement | null = $state(null);
    let textareaContainer: HTMLDivElement | null = $state(null);
    let textarea: HTMLTextAreaElement | null = $state(null);
    let message = $state('');

    const SCROLL_THRESHOLD = 196;
    const MIN_TEXTAREA_HEIGHT = 96;
    const MAX_TEXTAREA_HEIGHT = 192;

    let isAtBottom = $state(true);

    // Send the message to the websocket and clear the message state
    function handleSubmit() {
        if (message.trim().length === 0 || awaitingResponse) return;
        sendMessage(message);
        message = '';
        textarea?.focus();
    }

    // Auto resize the textarea
    $effect(() => {
        message;
        if (!textareaContainer) return;
        if (!textarea) return;

        textarea.style.height = 'auto';

        const scrollHeight = textarea.scrollHeight;

        const newHeight = Math.min(Math.max(scrollHeight, MIN_TEXTAREA_HEIGHT), MAX_TEXTAREA_HEIGHT);

        textareaContainer.style.height = `${newHeight}px`;

        textarea.style.height = '100%';
    });

    // Handle key press
    function handleKeyPress(event: KeyboardEvent) {
        if (event.key === 'Enter' && !event.shiftKey) {
            event.preventDefault();
            handleSubmit();
        }
    }

    // Scroll to the bottom of the chat
    function scrollToBottom() {
        if (!scrollArea) return;

        scrollArea.scrollTo({
            top: scrollArea.scrollHeight,
            behavior: 'smooth'
        });
    }

    // Scroll to the bottom of the chat when a new message is added
    $effect(() => {
        const lastMessage = messages[messages.length - 1];

        if (lastMessage?.type === 'message' || lastMessage?.type === 'loading') {
            scrollToBottom();
        }
    });

    // Add scroll and touch event listeners
    $effect(() => {
        if (!scrollArea) return;

        const handleScrollAndTouch = () => {
            if (!scrollArea) return;
            const distanceFromBottom = scrollArea.scrollHeight - (scrollArea.scrollTop + scrollArea.clientHeight);
            isAtBottom = distanceFromBottom <= SCROLL_THRESHOLD;
        };

        scrollArea.addEventListener('scroll', handleScrollAndTouch);
        scrollArea.addEventListener('touchmove', handleScrollAndTouch);
        
        return () => {
            if (!scrollArea) return;
            scrollArea.removeEventListener('scroll', handleScrollAndTouch);
            scrollArea.removeEventListener('touchmove', handleScrollAndTouch);
        };
    })
</script>

<div class="h-full overflow-hidden w-full flex flex-col pb-2">
    <div class="flex-1 flex flex-col overflow-hidden relative container px-0 mx-auto max-w-5xl">
        <div bind:this={scrollArea} class="flex-1 flex flex-col overflow-y-auto">
            <div class="flex flex-col mt-auto gap-4 p-4">
                {#each messages as message}
                    {#if message.type === 'message'}
                        {@const data = message.data as MagmaUserMessage | MagmaAssistantMessage}
                        <div class="{data.role === 'user' ? 'bg-secondary text-secondary-foreground ml-auto' : 'text-foreground mr-auto'} p-2 rounded-md max-w-[85%]">
                            {data.content}
                        </div>
                    {/if}
                {/each}
                {#if awaitingResponse}
                    <div class="size-3 my-3.5 bg-foreground rounded-full animate-pulse"></div>
                {/if}
            </div>
            <div class={cn("absolute bottom-3 right-7 transition-all duration-150 ease-out",
                isAtBottom ? "pointer-events-none translate-y-4 scale-95 opacity-0"
                : "translate-y-0 scale-100 opacity-100"
            )}>
                <Button variant="outline" size="icon" onclick={scrollToBottom}>
                    <ArrowDown />
                </Button>
            </div>
        </div>
    </div>
    <div bind:this={textareaContainer} class="relative max-w-2xl container px-0 mx-auto">
        <Textarea
            bind:ref={textarea}
            placeholder="Type a message..."
            bind:value={message}
            class="w-full h-24 pr-16 resize-none border"
            onkeydown={handleKeyPress}
        />
        <div class="absolute bottom-2 right-6">
            <Button size="icon" onclick={handleSubmit} disabled={awaitingResponse || message.trim().length === 0} class="transition-opacity">
                <ArrowUp />
            </Button>
        </div>
    </div>
</div>
You can then use the component inside the MagmaChatProvider from the svelte-obsidian package you installed earlier:
+page.svelte
<script lang="ts">
    import Chat from '$lib/components/chat.svelte';
    import MagmaChatProvider from '@pompeii-labs/svelte-obsidian';
</script>

<div class="w-screen h-screen">
    <MagmaChatProvider agentId="YOUR_AGENT_ID" apiKey="YOUR_API_KEY">
        <Chat />
    </MagmaChatProvider>
</div>

Using TailwindCSS

This example assumes you have background and foreground tailwind color variables defined
After you have installed the provider, copy this component into your TailwindCSS project:
chat.svelte
<script lang="ts">
	import { getChatContext } from '@pompeii-labs/svelte-obsidian';
	import type { MagmaUserMessage, MagmaAssistantMessage } from '@pompeii-labs/magma/types';

	const { sendMessage, messages, awaitingResponse } = $derived(getChatContext());

    let scrollArea: HTMLDivElement | null = $state(null);
    let textareaContainer: HTMLDivElement | null = $state(null);
    let textarea: HTMLTextAreaElement | null = $state(null);
    let message = $state('');

    const SCROLL_THRESHOLD = 196;
    const MIN_TEXTAREA_HEIGHT = 96;
    const MAX_TEXTAREA_HEIGHT = 192;

    let isAtBottom = $state(true);

    // Send the message to the websocket and clear the message state
    function handleSubmit() {
        if (message.trim().length === 0 || awaitingResponse) return;
        sendMessage(message);
        message = '';
        textarea?.focus();
    }

    // Auto resize the textarea
    $effect(() => {
        message;
        if (!textareaContainer) return;
        if (!textarea) return;

        textarea.style.height = 'auto';

        const scrollHeight = textarea.scrollHeight;

        const newHeight = Math.min(Math.max(scrollHeight, MIN_TEXTAREA_HEIGHT), MAX_TEXTAREA_HEIGHT);

        textareaContainer.style.height = `${newHeight}px`;

        textarea.style.height = '100%';
    });

    // Handle key press
    function handleKeyPress(event: KeyboardEvent) {
        if (event.key === 'Enter' && !event.shiftKey) {
            event.preventDefault();
            handleSubmit();
        }
    }

    // Scroll to the bottom of the chat
    function scrollToBottom() {
        if (!scrollArea) return;

        scrollArea.scrollTo({
            top: scrollArea.scrollHeight,
            behavior: 'smooth'
        });
    }

    // Scroll to the bottom of the chat when a new message is added
    $effect(() => {
        const lastMessage = messages[messages.length - 1];

        if (lastMessage?.type === 'message' || lastMessage?.type === 'loading') {
            scrollToBottom();
        }
    });

    // Add scroll and touch event listeners
    $effect(() => {
        if (!scrollArea) return;

        const handleScrollAndTouch = () => {
            if (!scrollArea) return;
            const distanceFromBottom = scrollArea.scrollHeight - (scrollArea.scrollTop + scrollArea.clientHeight);
            isAtBottom = distanceFromBottom <= SCROLL_THRESHOLD;
        };

        scrollArea.addEventListener('scroll', handleScrollAndTouch);
        scrollArea.addEventListener('touchmove', handleScrollAndTouch);
        
        return () => {
            if (!scrollArea) return;
            scrollArea.removeEventListener('scroll', handleScrollAndTouch);
            scrollArea.removeEventListener('touchmove', handleScrollAndTouch);
        };
    })
</script>

<div class="h-full overflow-hidden w-full flex flex-col pb-2">
    <div class="flex-1 flex flex-col overflow-hidden relative container px-0 mx-auto max-w-5xl">
        <div bind:this={scrollArea} class="flex-1 flex flex-col overflow-y-auto">
            <div class="flex flex-col mt-auto gap-4 p-4">
                {#each messages as message}
                    {#if message.type === 'message'}
                        {@const data = message.data as MagmaUserMessage | MagmaAssistantMessage}
                        <div class="{data.role === 'user' ? 'bg-foreground/10 text-foreground ml-auto' : 'text-foreground mr-auto'} p-2 rounded-md max-w-[85%]">
                            {data.content}
                        </div>
                    {/if}
                {/each}
                {#if awaitingResponse}
                    <div class="size-3 my-3.5 bg-foreground rounded-full animate-pulse"></div>
                {/if}
            </div>
            <div class="absolute bottom-3 right-7 transition-all duration-150 ease-out {isAtBottom ? 'pointer-events-none translate-y-4 scale-95 opacity-0' : 'translate-y-0 scale-100 opacity-100'}">
                <button class="border rounded-md size-9 inline-flex items-center justify-center bg-background" onclick={scrollToBottom}>
                    v
                </button>
            </div>
        </div>
    </div>
    <div bind:this={textareaContainer} class="relative max-w-2xl container px-0 mx-auto">
        <textarea
            bind:this={textarea}
            placeholder="Type a message..."
            bind:value={message}
            class="w-full px-4 py-2 rounded-md h-24 pr-16 resize-none border"
            onkeydown={handleKeyPress}
        ></textarea>
        <div class="absolute bottom-2 right-6">
            <button class="border bg-foreground text-background inline-flex items-center justify-center rounded-md h-9 px-4 py-2 disabled:opacity-50" onclick={handleSubmit} disabled={awaitingResponse || message.trim().length === 0}>
                Send
            </button>
        </div>
    </div>
</div>
You can then use the component inside the MagmaChatProvider from the svelte-obsidian package you installed earlier:
+page.svelte
<script lang="ts">
    import Chat from '$lib/components/chat.svelte';
    import MagmaChatProvider from '@pompeii-labs/svelte-obsidian';
</script>

<div class="w-screen h-screen">
    <MagmaChatProvider agentId="YOUR_AGENT_ID" apiKey="YOUR_API_KEY">
        <Chat />
    </MagmaChatProvider>
</div>

Using CSS Modules

This example assumes you have background and foreground css color variables defined
After you have installed the provider, copy this component into your CSS Modules project:
chat.svelte
<script lang="ts">
	import { getChatContext } from '@pompeii-labs/svelte-obsidian';
	import type { MagmaUserMessage, MagmaAssistantMessage } from '@pompeii-labs/magma/types';
    import styles from './chat.module.css';

	const { sendMessage, messages, awaitingResponse } = $derived(getChatContext());

    let scrollArea: HTMLDivElement | null = $state(null);
    let textareaContainer: HTMLDivElement | null = $state(null);
    let textarea: HTMLTextAreaElement | null = $state(null);
    let message = $state('');

    const SCROLL_THRESHOLD = 196;
    const MIN_TEXTAREA_HEIGHT = 96;
    const MAX_TEXTAREA_HEIGHT = 192;

    let isAtBottom = $state(true);

    // Send the message to the websocket and clear the message state
    function handleSubmit() {
        if (message.trim().length === 0 || awaitingResponse) return;
        sendMessage(message);
        message = '';
        textarea?.focus();
    }

    // Auto resize the textarea
    $effect(() => {
        message;
        if (!textareaContainer) return;
        if (!textarea) return;

        textarea.style.height = 'auto';

        const scrollHeight = textarea.scrollHeight;

        const newHeight = Math.min(Math.max(scrollHeight, MIN_TEXTAREA_HEIGHT), MAX_TEXTAREA_HEIGHT);

        textareaContainer.style.height = `${newHeight}px`;

        textarea.style.height = '100%';
    });

    // Handle key press
    function handleKeyPress(event: KeyboardEvent) {
        if (event.key === 'Enter' && !event.shiftKey) {
            event.preventDefault();
            handleSubmit();
        }
    }

    // Scroll to the bottom of the chat
    function scrollToBottom() {
        if (!scrollArea) return;

        scrollArea.scrollTo({
            top: scrollArea.scrollHeight,
            behavior: 'smooth'
        });
    }

    // Scroll to the bottom of the chat when a new message is added
    $effect(() => {
        const lastMessage = messages[messages.length - 1];

        if (lastMessage?.type === 'message' || lastMessage?.type === 'loading') {
            scrollToBottom();
        }
    });

    // Add scroll and touch event listeners
    $effect(() => {
        if (!scrollArea) return;

        const handleScrollAndTouch = () => {
            if (!scrollArea) return;
            const distanceFromBottom = scrollArea.scrollHeight - (scrollArea.scrollTop + scrollArea.clientHeight);
            isAtBottom = distanceFromBottom <= SCROLL_THRESHOLD;
        };

        scrollArea.addEventListener('scroll', handleScrollAndTouch);
        scrollArea.addEventListener('touchmove', handleScrollAndTouch);
        
        return () => {
            if (!scrollArea) return;
            scrollArea.removeEventListener('scroll', handleScrollAndTouch);
            scrollArea.removeEventListener('touchmove', handleScrollAndTouch);
        };
    })
</script>

<div class={styles['chat-container']}>
    <div class={styles['scroll-container']}>
        <div bind:this={scrollArea} class={styles['scroll-area']}>
            <div class={styles['messages-container']}>
                {#each messages as message}
                    {#if message.type === 'message'}
                        {@const data = message.data as MagmaUserMessage | MagmaAssistantMessage}
                        <div class="{styles['message']} {data.role === 'user' ? styles['user-message'] : styles['assistant-message']}">
                            {data.content}
                        </div>
                    {/if}
                {/each}
                {#if awaitingResponse}
                    <div class={styles['loading-bubble']}></div>
                {/if}
            </div>
            <div class="{styles['scroll-button-container']} {isAtBottom ? styles['scroll-button-container-hidden'] : styles['scroll-button-container-visible']}">
                <button class={styles['scroll-button']} onclick={scrollToBottom}>
                    v
                </button>
            </div>
        </div>
    </div>
    <div bind:this={textareaContainer} class={styles['textarea-container']}>
        <textarea
            bind:this={textarea}
            placeholder="Type a message..."
            bind:value={message}
            class={styles['textarea']}
            onkeydown={handleKeyPress}
        ></textarea>
        <div class={styles['send-button-container']}>
            <button class={styles['send-button']} onclick={handleSubmit} disabled={awaitingResponse || message.trim().length === 0}>
                Send
            </button>
        </div>
    </div>
</div>
Also copy the corresponding styles:
chat.module.css
.chat-container {
    height: 100%;
    overflow: hidden;
    width: 100%;
    display: flex;
    flex-direction: column;
}

.scroll-container {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    position: relative;
    width: 100%;
    margin-inline: auto;
    max-width: 64rem;
}

.scroll-area {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
}

.messages-container {
    display: flex;
    flex-direction: column;
    margin-top: auto;
    gap: 1rem;
    padding: 1rem;
}

.message {
    padding: 0.5rem;
    border-radius: 0.375rem;
    max-width: 85%;
    line-height: 1.5;
}

.user-message {
    background-color: color-mix(in oklab, var(--foreground) 10%, transparent);
    color: var(--foreground);
    margin-left: auto;
}

.assistant-message {
    color: var(--foreground);
    margin-right: auto;
}

.loading-bubble {
    width: 0.75rem;
    height: 0.75rem;
    margin: 0.875rem 0rem;
    background-color: var(--foreground);
    border-radius: calc(infinity * 1px);
    animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

@keyframes pulse {
    0%, 100% {
        opacity: 1;
    }
    50% {
        opacity: .5;
    }
}

.scroll-button-container {
    position: absolute;
    bottom: 0.75rem;
    right: 1.75rem;
    transition-property: all;
    transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
    transition-duration: 150ms;
}

.scroll-button-container-hidden {
    pointer-events: none;
    translate: 0 1rem;
    scale: 0.95;
    opacity: 0;
}

.scroll-button-container-visible {
    translate: 0 0;
    scale: 1;
    opacity: 1;
}

.scroll-button {
    border-style: solid;
    border-width: 1px;
    border-radius: 0.375rem;
    width: 2.25rem;
    height: 2.25rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background-color: var(--background);
}

.textarea-container {
    position: relative;
    max-width: 42rem;
    width: 100%;
    margin-inline: auto;
}

.textarea {
    width: 100%;
    padding-inline: 1rem;
    padding-block: 0.5rem;
    border-radius: 0.375rem;
    height: 6rem;
    padding-right: 4rem;
    resize: none;
    border-style: solid;
    border-width: 1px;
}

.send-button-container {
    position: absolute;
    bottom: 0.5rem;
    right: 1.5rem;
}

.send-button {
    border-style: solid;
    border-width: 1px;
    background-color: var(--foreground);
    color: var(--background);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 0.375rem;
    height: 2.25rem;
    padding-inline: 1rem;
    padding-block: 0.5rem;
    &:disabled {
        opacity: 0.5;
    }
}
You can then use the component inside the MagmaChatProvider from the svelte-obsidian package you installed earlier:
+page.svelte
<script lang="ts">
    import Chat from '$lib/components/chat.svelte';
    import MagmaChatProvider from '@pompeii-labs/svelte-obsidian';
</script>

<div style="width: 100vw; height: 100vh;">
    <MagmaChatProvider agentId="YOUR_AGENT_ID" apiKey="YOUR_API_KEY">
        <Chat />
    </MagmaChatProvider>
</div>