Skip to content

Commit e01669b

Browse files
Add test app (#62)
* Scaffold test app * Wire up AI SDK + Gateway * Finish test app * Resolves #53 * For #55 * Update config.json * Create fuzzy-zoos-drive.md
1 parent bf8c798 commit e01669b

19 files changed

+807
-48
lines changed

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"access": "public",
88
"baseBranch": "main",
99
"updateInternalDependencies": "patch",
10-
"ignore": ["website"]
10+
"ignore": ["website", "test"]
1111
}

.changeset/fuzzy-zoos-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"streamdown": patch
3+
---
4+
5+
add test app, fix code block incomplete parsing

apps/test/.gitignore

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts

apps/test/app/api/chat/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { convertToModelMessages, streamText, type UIMessage } from 'ai';
2+
3+
// Allow streaming responses up to 30 seconds
4+
export const maxDuration = 30;
5+
6+
export async function POST(req: Request) {
7+
const { messages }: { messages: UIMessage[] } = await req.json();
8+
9+
const result = streamText({
10+
model: 'anthropic/claude-sonnet-4',
11+
system: 'You are a helpful assistant.',
12+
messages: convertToModelMessages(messages),
13+
});
14+
15+
return result.toUIMessageStreamResponse();
16+
}

apps/test/app/favicon.ico

25.3 KB
Binary file not shown.

apps/test/app/globals.css

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
@import "tailwindcss";
2+
@import "tw-animate-css";
3+
4+
@custom-variant dark (&:is(.dark *));
5+
@source "../node_modules/streamdown/dist/index.js";
6+
7+
@theme inline {
8+
--color-background: var(--background);
9+
--color-foreground: var(--foreground);
10+
--font-sans: var(--font-geist-sans);
11+
--font-mono: var(--font-geist-mono);
12+
--color-sidebar-ring: var(--sidebar-ring);
13+
--color-sidebar-border: var(--sidebar-border);
14+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
15+
--color-sidebar-accent: var(--sidebar-accent);
16+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
17+
--color-sidebar-primary: var(--sidebar-primary);
18+
--color-sidebar-foreground: var(--sidebar-foreground);
19+
--color-sidebar: var(--sidebar);
20+
--color-chart-5: var(--chart-5);
21+
--color-chart-4: var(--chart-4);
22+
--color-chart-3: var(--chart-3);
23+
--color-chart-2: var(--chart-2);
24+
--color-chart-1: var(--chart-1);
25+
--color-ring: var(--ring);
26+
--color-input: var(--input);
27+
--color-border: var(--border);
28+
--color-destructive: var(--destructive);
29+
--color-accent-foreground: var(--accent-foreground);
30+
--color-accent: var(--accent);
31+
--color-muted-foreground: var(--muted-foreground);
32+
--color-muted: var(--muted);
33+
--color-secondary-foreground: var(--secondary-foreground);
34+
--color-secondary: var(--secondary);
35+
--color-primary-foreground: var(--primary-foreground);
36+
--color-primary: var(--primary);
37+
--color-popover-foreground: var(--popover-foreground);
38+
--color-popover: var(--popover);
39+
--color-card-foreground: var(--card-foreground);
40+
--color-card: var(--card);
41+
--radius-sm: calc(var(--radius) - 4px);
42+
--radius-md: calc(var(--radius) - 2px);
43+
--radius-lg: var(--radius);
44+
--radius-xl: calc(var(--radius) + 4px);
45+
}
46+
47+
:root {
48+
--radius: 0.625rem;
49+
--background: oklch(1 0 0);
50+
--foreground: oklch(0.145 0 0);
51+
--card: oklch(1 0 0);
52+
--card-foreground: oklch(0.145 0 0);
53+
--popover: oklch(1 0 0);
54+
--popover-foreground: oklch(0.145 0 0);
55+
--primary: oklch(0.205 0 0);
56+
--primary-foreground: oklch(0.985 0 0);
57+
--secondary: oklch(0.97 0 0);
58+
--secondary-foreground: oklch(0.205 0 0);
59+
--muted: oklch(0.97 0 0);
60+
--muted-foreground: oklch(0.556 0 0);
61+
--accent: oklch(0.97 0 0);
62+
--accent-foreground: oklch(0.205 0 0);
63+
--destructive: oklch(0.577 0.245 27.325);
64+
--border: oklch(0.922 0 0);
65+
--input: oklch(0.922 0 0);
66+
--ring: oklch(0.708 0 0);
67+
--chart-1: oklch(0.646 0.222 41.116);
68+
--chart-2: oklch(0.6 0.118 184.704);
69+
--chart-3: oklch(0.398 0.07 227.392);
70+
--chart-4: oklch(0.828 0.189 84.429);
71+
--chart-5: oklch(0.769 0.188 70.08);
72+
--sidebar: oklch(0.985 0 0);
73+
--sidebar-foreground: oklch(0.145 0 0);
74+
--sidebar-primary: oklch(0.205 0 0);
75+
--sidebar-primary-foreground: oklch(0.985 0 0);
76+
--sidebar-accent: oklch(0.97 0 0);
77+
--sidebar-accent-foreground: oklch(0.205 0 0);
78+
--sidebar-border: oklch(0.922 0 0);
79+
--sidebar-ring: oklch(0.708 0 0);
80+
}
81+
82+
.dark {
83+
--background: oklch(0.145 0 0);
84+
--foreground: oklch(0.985 0 0);
85+
--card: oklch(0.205 0 0);
86+
--card-foreground: oklch(0.985 0 0);
87+
--popover: oklch(0.205 0 0);
88+
--popover-foreground: oklch(0.985 0 0);
89+
--primary: oklch(0.922 0 0);
90+
--primary-foreground: oklch(0.205 0 0);
91+
--secondary: oklch(0.269 0 0);
92+
--secondary-foreground: oklch(0.985 0 0);
93+
--muted: oklch(0.269 0 0);
94+
--muted-foreground: oklch(0.708 0 0);
95+
--accent: oklch(0.269 0 0);
96+
--accent-foreground: oklch(0.985 0 0);
97+
--destructive: oklch(0.704 0.191 22.216);
98+
--border: oklch(1 0 0 / 10%);
99+
--input: oklch(1 0 0 / 15%);
100+
--ring: oklch(0.556 0 0);
101+
--chart-1: oklch(0.488 0.243 264.376);
102+
--chart-2: oklch(0.696 0.17 162.48);
103+
--chart-3: oklch(0.769 0.188 70.08);
104+
--chart-4: oklch(0.627 0.265 303.9);
105+
--chart-5: oklch(0.645 0.246 16.439);
106+
--sidebar: oklch(0.205 0 0);
107+
--sidebar-foreground: oklch(0.985 0 0);
108+
--sidebar-primary: oklch(0.488 0.243 264.376);
109+
--sidebar-primary-foreground: oklch(0.985 0 0);
110+
--sidebar-accent: oklch(0.269 0 0);
111+
--sidebar-accent-foreground: oklch(0.985 0 0);
112+
--sidebar-border: oklch(1 0 0 / 10%);
113+
--sidebar-ring: oklch(0.556 0 0);
114+
}
115+
116+
@layer base {
117+
* {
118+
@apply border-border outline-ring/50;
119+
}
120+
body {
121+
@apply bg-background text-foreground;
122+
}
123+
}

apps/test/app/layout.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Metadata } from "next";
2+
import { Geist, Geist_Mono } from "next/font/google";
3+
import "./globals.css";
4+
5+
const geistSans = Geist({
6+
variable: "--font-geist-sans",
7+
subsets: ["latin"],
8+
});
9+
10+
const geistMono = Geist_Mono({
11+
variable: "--font-geist-mono",
12+
subsets: ["latin"],
13+
});
14+
15+
export const metadata: Metadata = {
16+
title: "Create Next App",
17+
description: "Generated by create next app",
18+
};
19+
20+
export default function RootLayout({
21+
children,
22+
}: Readonly<{
23+
children: React.ReactNode;
24+
}>) {
25+
return (
26+
<html lang="en">
27+
<body
28+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
29+
>
30+
{children}
31+
</body>
32+
</html>
33+
);
34+
}

apps/test/app/page.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use client';
2+
3+
import { useChat } from '@ai-sdk/react';
4+
import { DefaultChatTransport } from 'ai';
5+
import { useState } from 'react';
6+
import { Streamdown } from 'streamdown';
7+
8+
export default function Page() {
9+
const { messages, sendMessage, status } = useChat({
10+
transport: new DefaultChatTransport({
11+
api: '/api/chat',
12+
}),
13+
});
14+
const [input, setInput] = useState('');
15+
16+
return (
17+
<div className="mx-auto flex h-screen max-w-4xl flex-col divide-y border-x">
18+
<div className="flex flex-1 divide-x overflow-hidden">
19+
<div className="flex-1 space-y-4 overflow-y-auto bg-black p-4 text-white">
20+
{messages.map((message) => (
21+
<div key={message.id}>
22+
<span className="font-bold">
23+
{message.role === 'user' ? 'User: ' : 'AI: '}
24+
</span>
25+
{message.parts.map((part, index) => {
26+
switch (part.type) {
27+
case 'text':
28+
return (
29+
<pre className="whitespace-pre-wrap" key={index}>
30+
{part.text}
31+
</pre>
32+
);
33+
case 'reasoning':
34+
return (
35+
<pre className="italic" key={index}>
36+
{part.text}
37+
</pre>
38+
);
39+
default:
40+
return null;
41+
}
42+
})}
43+
</div>
44+
))}
45+
</div>
46+
<div className="flex-1 space-y-4 overflow-y-auto p-4">
47+
{messages.map((message) => (
48+
<div key={message.id}>
49+
<span className="font-bold">
50+
{message.role === 'user' ? 'User: ' : 'AI: '}
51+
</span>
52+
{message.parts.map((part, index) => {
53+
switch (part.type) {
54+
case 'text':
55+
return <Streamdown key={index}>{part.text}</Streamdown>;
56+
case 'reasoning':
57+
return (
58+
<p className="italic" key={part.text}>
59+
{part.text}
60+
</p>
61+
);
62+
default:
63+
return null;
64+
}
65+
})}
66+
</div>
67+
))}
68+
</div>
69+
</div>
70+
<form
71+
className="flex shrink-0 items-center gap-2 p-4"
72+
onSubmit={(e) => {
73+
e.preventDefault();
74+
if (input.trim()) {
75+
sendMessage({ text: input });
76+
setInput('');
77+
}
78+
}}
79+
>
80+
<input
81+
className="flex-1 rounded-md border border-neutral-200 p-2"
82+
disabled={status !== 'ready'}
83+
onChange={(e) => setInput(e.target.value)}
84+
placeholder="Say something..."
85+
value={input}
86+
/>
87+
<button
88+
className="shrink-0 rounded-md bg-blue-500 px-4 py-2 text-white"
89+
disabled={status !== 'ready'}
90+
type="submit"
91+
>
92+
Submit
93+
</button>
94+
</form>
95+
</div>
96+
);
97+
}

apps/test/components.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "new-york",
4+
"rsc": true,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "",
8+
"css": "app/globals.css",
9+
"baseColor": "neutral",
10+
"cssVariables": true,
11+
"prefix": ""
12+
},
13+
"iconLibrary": "lucide",
14+
"aliases": {
15+
"components": "@/components",
16+
"utils": "@/lib/utils",
17+
"ui": "@/components/ui",
18+
"lib": "@/lib",
19+
"hooks": "@/hooks"
20+
}
21+
}

apps/test/lib/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { clsx, type ClassValue } from "clsx"
2+
import { twMerge } from "tailwind-merge"
3+
4+
export function cn(...inputs: ClassValue[]) {
5+
return twMerge(clsx(inputs))
6+
}

0 commit comments

Comments
 (0)