Easily integrate your agent into your Svelte.js frontend
npx sv create
npm i @pompeii-labs/svelte-obsidian
<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>
MagmaChatProvider
from the svelte-obsidian
package you installed earlier:
<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>
<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>
MagmaChatProvider
from the svelte-obsidian
package you installed earlier:
<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>
<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>
.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;
}
}
MagmaChatProvider
from the svelte-obsidian
package you installed earlier:
<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>