Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/svelte-chat/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# OpenAI API Key
OPENAI_API_KEY=your_api_key_here
10 changes: 10 additions & 0 deletions examples/svelte-chat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
96 changes: 96 additions & 0 deletions examples/svelte-chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Svelte OpenUI Chat Example

A SvelteKit chat application demonstrating `@openuidev/svelte-lang` with streaming OpenAI responses rendered as interactive UI components.

## Setup

```bash
# From the repo root
pnpm install
pnpm --filter @openuidev/react-lang build
pnpm --filter @openuidev/svelte-lang build
```

Create a `.env` file in this directory:

```
OPENAI_API_KEY=sk-...
```

Run the dev server:

```bash
pnpm --filter svelte-chat dev
```

## Features

- Streaming chat with OpenAI (GPT-4)
- Real-time progressive rendering of OpenUI Lang output
- **shadcn-svelte** UI components (built on Bits UI + Tailwind CSS v4)
- 14 components: Stack, Card, Table, Chart, Form, Input, Button, TextContent, CodeBlock, Callout, Separator, Tabs, Accordion, Steps
- Form state management with field isolation
- Action events (e.g., form submission continues the conversation)
- Prompt examples and rules for reliable LLM output

## UI Framework

This example uses [shadcn-svelte](https://www.shadcn-svelte.com/) for the component renderers. The installed shadcn components are in `src/lib/components/ui/` and include: accordion, alert, badge, button, card, input, separator, and tabs.

To add more shadcn-svelte components:

```bash
cd examples/svelte-chat
npx shadcn-svelte@next add <component-name>
```

## Architecture

```
src/
├── routes/
│ ├── +page.svelte # Chat UI with message list and input
│ └── api/chat/+server.ts # OpenAI streaming endpoint
├── lib/
│ ├── library.ts # Component library definition (Zod schemas)
│ └── components/
│ ├── ui/ # shadcn-svelte base components
│ │ ├── accordion/
│ │ ├── alert/
│ │ ├── badge/
│ │ ├── button/
│ │ ├── card/
│ │ ├── input/
│ │ ├── separator/
│ │ └── tabs/
│ ├── library/ # OpenUI Lang component renderers
│ │ ├── Stack.svelte
│ │ ├── Card.svelte
│ │ ├── Table.svelte
│ │ ├── Chart.svelte
│ │ ├── Form.svelte
│ │ ├── Input.svelte
│ │ ├── Button.svelte
│ │ ├── TextContent.svelte
│ │ ├── CodeBlock.svelte
│ │ ├── Callout.svelte
│ │ ├── Separator.svelte
│ │ ├── Tabs.svelte
│ │ ├── Accordion.svelte
│ │ └── Steps.svelte
│ ├── ChatMessage.svelte # Message bubble with Renderer
│ └── ChatInput.svelte # Text input with send button
└── app.css # Tailwind v4 + shadcn theme
```

### How it works

1. `library.ts` defines components with Zod schemas and Svelte renderers using `defineComponent` and `createLibrary`
2. The chat API endpoint generates a system prompt from the library via `library.prompt()`, then streams OpenAI completions
3. `ChatMessage.svelte` passes the streaming text to `<Renderer>` from `@openuidev/svelte-lang`
4. The Renderer parses OpenUI Lang syntax, resolves component references, and renders the matching Svelte components
5. Interactive components (Form, Input, Button) use the context API for state management and action events

## License

MIT
16 changes: 16 additions & 0 deletions examples/svelte-chat/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
}
36 changes: 36 additions & 0 deletions examples/svelte-chat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "svelte-chat",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@internationalized/date": "^3.12.0",
"@lucide/svelte": "^0.561.0",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.2.1",
"bits-ui": "^2.16.3",
"clsx": "^2.1.1",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwind-merge": "^3.5.0",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0",
"typescript": "^5.0.0",
"vite": "^6.0.0"
},
"dependencies": {
"@openuidev/svelte-lang": "workspace:*",
"openai": "^4.0.0",
"zod": "^4.0.0"
}
}
121 changes: 121 additions & 0 deletions examples/svelte-chat/src/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
@import "tailwindcss";

@import "tw-animate-css";

@custom-variant dark (&:is(.dark *));

:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar-ring: oklch(0.704 0.04 256.788);
}

.dark {
--background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755);
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.208 0.042 265.755);
--popover-foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.929 0.013 255.508);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.208 0.042 265.755);
--sidebar-foreground: oklch(0.984 0.003 247.858);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031);
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);
}

@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
12 changes: 12 additions & 0 deletions examples/svelte-chat/src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
50 changes: 50 additions & 0 deletions examples/svelte-chat/src/lib/components/ChatInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";

interface Props {
value: string;
disabled?: boolean;
onSubmit: (message: string) => void;
onInput: (value: string) => void;
}

let { value, disabled = false, onSubmit, onInput }: Props = $props();

function handleSubmit(event: Event) {
event.preventDefault();
if (value.trim() && !disabled) {
onSubmit(value);
}
}

function handleInput(event: Event) {
const target = event.target as HTMLTextAreaElement;
onInput(target.value);
}

function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
if (value.trim() && !disabled) {
onSubmit(value);
}
}
}
</script>

<form onsubmit={handleSubmit} class="rounded-lg border bg-card p-4 shadow-sm">
<div class="flex items-end gap-3">
<textarea
{value}
{disabled}
placeholder="Type your message... (Shift+Enter for new line)"
oninput={handleInput}
onkeydown={handleKeyDown}
rows="3"
class="flex-1 resize-y rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
></textarea>
<Button type="submit" disabled={disabled || !value.trim()}>
Send
</Button>
</div>
</form>
31 changes: 31 additions & 0 deletions examples/svelte-chat/src/lib/components/ChatMessage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import { Renderer } from "@openuidev/svelte-lang";
import type { Library, ActionEvent } from "@openuidev/svelte-lang";

interface Props {
role: "user" | "assistant";
content: string;
library?: Library;
isStreaming?: boolean;
onAction?: (event: ActionEvent) => void;
}

let { role, content, library, isStreaming = false, onAction }: Props = $props();
</script>

<div
class="mb-4 rounded-lg p-4 {role === 'user'
? 'ml-8 bg-primary/10'
: 'mr-8 border bg-card shadow-sm'}"
>
<div class="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{role === "user" ? "You" : "Assistant"}
</div>
<div class="leading-relaxed text-foreground">
{#if role === "assistant" && library}
<Renderer response={content} {library} {isStreaming} {onAction} />
{:else}
<p class="m-0">{content}</p>
{/if}
</div>
</div>
Loading