@getjack/jack 0.1.32 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/deploys.ts +95 -0
- package/src/commands/link.ts +8 -0
- package/src/commands/mcp.ts +179 -4
- package/src/commands/rollback.ts +53 -0
- package/src/commands/services.ts +11 -1
- package/src/commands/ship.ts +3 -1
- package/src/commands/tokens.ts +16 -1
- package/src/commands/whoami.ts +43 -8
- package/src/index.ts +16 -0
- package/src/lib/agent-files.ts +54 -4
- package/src/lib/agent-integration.ts +4 -166
- package/src/lib/claude-hooks-installer.ts +55 -0
- package/src/lib/control-plane.ts +78 -40
- package/src/lib/debug.ts +2 -1
- package/src/lib/deploy-upload.ts +6 -0
- package/src/lib/hooks.ts +3 -1
- package/src/lib/managed-deploy.ts +12 -9
- package/src/lib/project-link.ts +6 -0
- package/src/lib/project-operations.ts +68 -22
- package/src/lib/telemetry.ts +2 -0
- package/src/mcp/README.md +1 -1
- package/src/mcp/resources/index.ts +1 -16
- package/src/mcp/server.ts +23 -0
- package/src/mcp/tools/index.ts +133 -17
- package/src/mcp/types.ts +1 -0
- package/src/mcp/utils.ts +2 -1
- package/src/templates/index.ts +25 -73
- package/templates/CLAUDE.md +41 -0
- package/templates/ai-chat/.jack.json +10 -5
- package/templates/ai-chat/bun.lock +50 -1
- package/templates/ai-chat/package.json +5 -0
- package/templates/ai-chat/public/app.js +73 -0
- package/templates/ai-chat/public/index.html +14 -197
- package/templates/ai-chat/schema.sql +14 -0
- package/templates/ai-chat/src/index.ts +86 -102
- package/templates/ai-chat/wrangler.jsonc +8 -1
- package/templates/cron/.jack.json +66 -0
- package/templates/cron/bun.lock +23 -0
- package/templates/cron/package.json +16 -0
- package/templates/cron/schema.sql +24 -0
- package/templates/cron/src/index.ts +117 -0
- package/templates/cron/src/jobs.ts +139 -0
- package/templates/cron/src/webhooks.ts +95 -0
- package/templates/cron/tsconfig.json +17 -0
- package/templates/cron/wrangler.jsonc +11 -0
- package/templates/miniapp/.jack.json +1 -1
- package/templates/nextjs/.jack.json +1 -1
- package/templates/nextjs-auth/.jack.json +44 -0
- package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
- package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
- package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
- package/templates/nextjs-auth/app/error.tsx +44 -0
- package/templates/nextjs-auth/app/globals.css +1 -0
- package/templates/nextjs-auth/app/health/route.ts +3 -0
- package/templates/nextjs-auth/app/layout.tsx +24 -0
- package/templates/nextjs-auth/app/login/page.tsx +10 -0
- package/templates/nextjs-auth/app/page.tsx +86 -0
- package/templates/nextjs-auth/app/signup/page.tsx +10 -0
- package/templates/nextjs-auth/bun.lock +1065 -0
- package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
- package/templates/nextjs-auth/components/auth-form.tsx +191 -0
- package/templates/nextjs-auth/components/header.tsx +50 -0
- package/templates/nextjs-auth/components/user-menu.tsx +23 -0
- package/templates/nextjs-auth/lib/auth-client.ts +3 -0
- package/templates/nextjs-auth/lib/auth.ts +43 -0
- package/templates/nextjs-auth/lib/utils.ts +6 -0
- package/templates/nextjs-auth/middleware.ts +33 -0
- package/templates/nextjs-auth/next.config.ts +8 -0
- package/templates/nextjs-auth/open-next.config.ts +6 -0
- package/templates/nextjs-auth/package.json +33 -0
- package/templates/nextjs-auth/postcss.config.mjs +8 -0
- package/templates/nextjs-auth/schema.sql +49 -0
- package/templates/nextjs-auth/tsconfig.json +28 -0
- package/templates/nextjs-auth/wrangler.jsonc +23 -0
- package/templates/nextjs-clerk/.jack.json +54 -0
- package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
- package/templates/nextjs-clerk/app/globals.css +1 -0
- package/templates/nextjs-clerk/app/health/route.ts +3 -0
- package/templates/nextjs-clerk/app/layout.tsx +26 -0
- package/templates/nextjs-clerk/app/page.tsx +86 -0
- package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
- package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
- package/templates/nextjs-clerk/bun.lock +1055 -0
- package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
- package/templates/nextjs-clerk/components/header.tsx +40 -0
- package/templates/nextjs-clerk/lib/utils.ts +6 -0
- package/templates/nextjs-clerk/middleware.ts +18 -0
- package/templates/nextjs-clerk/next.config.ts +8 -0
- package/templates/nextjs-clerk/open-next.config.ts +6 -0
- package/templates/nextjs-clerk/package.json +31 -0
- package/templates/nextjs-clerk/postcss.config.mjs +8 -0
- package/templates/nextjs-clerk/tsconfig.json +28 -0
- package/templates/nextjs-clerk/wrangler.jsonc +17 -0
- package/templates/nextjs-shadcn/.jack.json +34 -0
- package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
- package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
- package/templates/nextjs-shadcn/app/globals.css +126 -0
- package/templates/nextjs-shadcn/app/health/route.ts +3 -0
- package/templates/nextjs-shadcn/app/layout.tsx +24 -0
- package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
- package/templates/nextjs-shadcn/app/page.tsx +180 -0
- package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
- package/templates/nextjs-shadcn/bun.lock +1789 -0
- package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
- package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
- package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
- package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
- package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
- package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
- package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
- package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
- package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
- package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
- package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
- package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
- package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
- package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
- package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
- package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
- package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
- package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
- package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
- package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
- package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
- package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
- package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
- package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
- package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
- package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
- package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
- package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
- package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
- package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
- package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
- package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
- package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
- package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
- package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
- package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
- package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
- package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
- package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
- package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
- package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
- package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
- package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
- package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
- package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
- package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
- package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
- package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
- package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
- package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
- package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
- package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
- package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
- package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
- package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
- package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
- package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
- package/templates/nextjs-shadcn/components.json +23 -0
- package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
- package/templates/nextjs-shadcn/lib/utils.ts +6 -0
- package/templates/nextjs-shadcn/next-env.d.ts +6 -0
- package/templates/nextjs-shadcn/next.config.ts +8 -0
- package/templates/nextjs-shadcn/open-next.config.ts +6 -0
- package/templates/nextjs-shadcn/package.json +55 -0
- package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
- package/templates/nextjs-shadcn/tsconfig.json +28 -0
- package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
- package/templates/resend/.jack.json +64 -0
- package/templates/resend/bun.lock +23 -0
- package/templates/resend/package.json +16 -0
- package/templates/resend/schema.sql +13 -0
- package/templates/resend/src/email.ts +165 -0
- package/templates/resend/src/index.ts +108 -0
- package/templates/resend/tsconfig.json +17 -0
- package/templates/resend/wrangler.jsonc +11 -0
- package/templates/saas/.jack.json +1 -1
- package/templates/ai-chat/public/chat.js +0 -149
package/src/templates/index.ts
CHANGED
|
@@ -14,11 +14,15 @@ export const BUILTIN_TEMPLATES = [
|
|
|
14
14
|
"hello",
|
|
15
15
|
"miniapp",
|
|
16
16
|
"api",
|
|
17
|
+
"cron",
|
|
18
|
+
"resend",
|
|
17
19
|
"nextjs",
|
|
18
20
|
"saas",
|
|
19
|
-
"simple-api-starter",
|
|
20
21
|
"ai-chat",
|
|
21
22
|
"semantic-search",
|
|
23
|
+
"nextjs-shadcn",
|
|
24
|
+
"nextjs-clerk",
|
|
25
|
+
"nextjs-auth",
|
|
22
26
|
];
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -211,64 +215,32 @@ function extractMetadataFromZip(zipData: ArrayBuffer): Record<string, unknown> {
|
|
|
211
215
|
}
|
|
212
216
|
|
|
213
217
|
/**
|
|
214
|
-
* Fetch a
|
|
218
|
+
* Fetch a remote template (own project or published).
|
|
219
|
+
* Both paths require auth.
|
|
215
220
|
*/
|
|
216
|
-
async function
|
|
217
|
-
const response = await fetch(
|
|
218
|
-
`${getControlApiUrl()}/v1/projects/${encodeURIComponent(username)}/${encodeURIComponent(slug)}/source`,
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
if (!response.ok) {
|
|
222
|
-
if (response.status === 404) {
|
|
223
|
-
throw new Error(
|
|
224
|
-
`Template not found: ${username}/${slug}\n\nMake sure the project exists and is published with: jack publish`,
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
throw new Error(`Failed to fetch template: ${response.status}`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const zipData = await response.arrayBuffer();
|
|
231
|
-
const metadata = extractMetadataFromZip(zipData);
|
|
232
|
-
const files = extractZipToFiles(zipData);
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
description: (metadata.description as string) || `Fork of ${username}/${slug}`,
|
|
236
|
-
secrets: (metadata.secrets as string[]) || [],
|
|
237
|
-
optionalSecrets: metadata.optionalSecrets as Template["optionalSecrets"],
|
|
238
|
-
envVars: metadata.envVars as Template["envVars"],
|
|
239
|
-
capabilities: metadata.capabilities as Template["capabilities"],
|
|
240
|
-
requires: metadata.requires as Template["requires"],
|
|
241
|
-
hooks: metadata.hooks as Template["hooks"],
|
|
242
|
-
agentContext: metadata.agentContext as Template["agentContext"],
|
|
243
|
-
intent: metadata.intent as Template["intent"],
|
|
244
|
-
files,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Fetch user's own project as a template (authenticated)
|
|
250
|
-
*/
|
|
251
|
-
async function fetchUserTemplate(slug: string): Promise<Template | null> {
|
|
221
|
+
async function fetchRemoteTemplate(identifier: string): Promise<Template | null> {
|
|
252
222
|
const { authFetch } = await import("../lib/auth/index.ts");
|
|
253
223
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
224
|
+
// Route to the correct endpoint based on format
|
|
225
|
+
let url: string;
|
|
226
|
+
if (identifier.includes("/")) {
|
|
227
|
+
const [owner, slug] = identifier.split("/", 2) as [string, string];
|
|
228
|
+
url = `${getControlApiUrl()}/v1/projects/${encodeURIComponent(owner)}/${encodeURIComponent(slug)}/source`;
|
|
229
|
+
} else {
|
|
230
|
+
url = `${getControlApiUrl()}/v1/me/projects/${encodeURIComponent(identifier)}/source`;
|
|
260
231
|
}
|
|
261
232
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
233
|
+
const response = await authFetch(url);
|
|
234
|
+
|
|
235
|
+
if (response.status === 404) return null;
|
|
236
|
+
if (!response.ok) throw new Error(`Failed to fetch template: ${response.status}`);
|
|
265
237
|
|
|
266
238
|
const zipData = await response.arrayBuffer();
|
|
267
239
|
const metadata = extractMetadataFromZip(zipData);
|
|
268
240
|
const files = extractZipToFiles(zipData);
|
|
269
241
|
|
|
270
242
|
return {
|
|
271
|
-
description: (metadata.description as string) || `
|
|
243
|
+
description: (metadata.description as string) || `Fork of ${identifier}`,
|
|
272
244
|
secrets: (metadata.secrets as string[]) || [],
|
|
273
245
|
optionalSecrets: metadata.optionalSecrets as Template["optionalSecrets"],
|
|
274
246
|
envVars: metadata.envVars as Template["envVars"],
|
|
@@ -285,33 +257,13 @@ async function fetchUserTemplate(slug: string): Promise<Template | null> {
|
|
|
285
257
|
* Resolve template by name
|
|
286
258
|
*/
|
|
287
259
|
export async function resolveTemplate(template?: string): Promise<Template> {
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
return loadTemplate("hello");
|
|
291
|
-
}
|
|
260
|
+
if (!template) return loadTemplate("hello");
|
|
261
|
+
if (BUILTIN_TEMPLATES.includes(template)) return loadTemplate(template);
|
|
292
262
|
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// username/slug format - fetch from jack cloud
|
|
299
|
-
if (template.includes("/")) {
|
|
300
|
-
const [username, slug] = template.split("/", 2) as [string, string];
|
|
301
|
-
return fetchPublishedTemplate(username, slug);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Try as user's own project first
|
|
305
|
-
try {
|
|
306
|
-
const userTemplate = await fetchUserTemplate(template);
|
|
307
|
-
if (userTemplate) {
|
|
308
|
-
return userTemplate;
|
|
309
|
-
}
|
|
310
|
-
} catch (_err) {
|
|
311
|
-
// If auth fails or project not found, fall through to error
|
|
312
|
-
}
|
|
263
|
+
// Remote: "username/slug" or "my-own-project"
|
|
264
|
+
const result = await fetchRemoteTemplate(template);
|
|
265
|
+
if (result) return result;
|
|
313
266
|
|
|
314
|
-
// Unknown template
|
|
315
267
|
throw new Error(
|
|
316
268
|
`Unknown template: ${template}\n\nAvailable built-in templates: ${BUILTIN_TEMPLATES.join(", ")}\nOr use username/slug format for published projects`,
|
|
317
269
|
);
|
package/templates/CLAUDE.md
CHANGED
|
@@ -568,6 +568,47 @@ if (!baseUrl) {
|
|
|
568
568
|
|
|
569
569
|
The Host header approach is reliable because Cloudflare always sets it to the actual domain being accessed.
|
|
570
570
|
|
|
571
|
+
## OpenNext (Next.js on Cloudflare) Gotchas
|
|
572
|
+
|
|
573
|
+
Templates using Next.js via `@opennextjs/cloudflare` have platform-specific limitations. These apply to all Next.js templates (`nextjs-auth`, `nextjs-shadcn`, `nextjs-clerk`, etc.).
|
|
574
|
+
|
|
575
|
+
### Client-side navigation after auth state changes: use window.location, not router.push
|
|
576
|
+
|
|
577
|
+
OpenNext's webpack runtime has an empty chunk URL resolver (`r.u=e=>{}`). This means `router.push()` to a page whose chunks aren't already loaded fails with `ChunkLoadError: Loading chunk X failed`.
|
|
578
|
+
|
|
579
|
+
**Rule:** After any auth state change (sign-in, sign-up, sign-out), always use `window.location.href` for a full page reload. This ensures middleware, server components, and cookies re-evaluate with the new session.
|
|
580
|
+
|
|
581
|
+
```tsx
|
|
582
|
+
// BAD — ChunkLoadError on OpenNext
|
|
583
|
+
await authClient.signOut();
|
|
584
|
+
router.push("/");
|
|
585
|
+
router.refresh();
|
|
586
|
+
|
|
587
|
+
// GOOD — full reload, clean auth state
|
|
588
|
+
await authClient.signOut();
|
|
589
|
+
window.location.href = "/";
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
`<Link>` components work fine for normal navigation because Next.js prefetches their chunks via `<script>` tags in the HTML.
|
|
593
|
+
|
|
594
|
+
### Pages using getCloudflareContext() need `export const dynamic = 'force-dynamic'`
|
|
595
|
+
|
|
596
|
+
Without this, Next.js tries to statically prerender the page at build time, which fails because `getCloudflareContext()` is only available at runtime in the Workers environment.
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
import { getAuth } from "@/lib/auth";
|
|
600
|
+
export const dynamic = "force-dynamic";
|
|
601
|
+
|
|
602
|
+
export default async function DashboardPage() {
|
|
603
|
+
const auth = await getAuth(); // calls getCloudflareContext() internally
|
|
604
|
+
// ...
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Edge middleware cannot use Node.js APIs
|
|
609
|
+
|
|
610
|
+
Next.js middleware runs in the edge runtime. Don't call `auth.api.getSession()` (requires `perf_hooks`) — use cookie-only checks instead.
|
|
611
|
+
|
|
571
612
|
## Adding New Templates
|
|
572
613
|
|
|
573
614
|
1. Create directory: `templates/my-template/`
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-chat",
|
|
3
|
-
"description": "AI chat with streaming
|
|
3
|
+
"description": "AI chat with streaming, persistence, and multi-model support",
|
|
4
4
|
"secrets": [],
|
|
5
|
-
"capabilities": ["ai"],
|
|
5
|
+
"capabilities": ["ai", "db"],
|
|
6
|
+
"requires": ["DB"],
|
|
6
7
|
"intent": {
|
|
7
|
-
"keywords": ["ai", "chat", "llm", "llama", "completion", "chatbot"],
|
|
8
|
-
"examples": ["AI chatbot", "chat interface", "LLM chat app"]
|
|
8
|
+
"keywords": ["ai", "chat", "llm", "llama", "completion", "chatbot", "streaming"],
|
|
9
|
+
"examples": ["AI chatbot", "chat interface", "LLM chat app", "persistent chat"]
|
|
10
|
+
},
|
|
11
|
+
"agentContext": {
|
|
12
|
+
"summary": "An AI chat app with streaming UI, D1 chat persistence, and multi-model support via AI SDK.",
|
|
13
|
+
"full_text": "## Project Structure\n\n- `src/index.ts` - Hono API with chat endpoints and AI SDK integration\n- `src/jack-ai.ts` - Jack AI proxy wrapper (do not modify)\n- `public/index.html` - React app shell\n- `public/app.js` - Chat UI with useChat hook from AI SDK\n- `schema.sql` - D1 schema for chat and message persistence\n\n## AI Integration\n\nUses AI SDK with `workers-ai-provider` routed through Jack's metered AI proxy.\n\n### Changing Models\n\nEdit the model string in `src/index.ts`:\n```typescript\nmodel: provider('@cf/meta/llama-3.3-70b-instruct-fp8-fast')\n```\n\nAvailable models: https://developers.cloudflare.com/workers-ai/models/\n\n## Chat Persistence\n\nChats and messages are stored in D1:\n- `chats` table: chat metadata (id, created_at)\n- `messages` table: individual messages (role, content, chat_id)\n\nMessages persist across page refreshes. Each browser session creates a new chat.\n\n## API Endpoints\n\n- `POST /api/chat` - Send message and get streaming AI response\n- `GET /api/chat/:id` - Load chat history\n- `POST /api/chat/new` - Create a new chat\n\n## Resources\n\n- [AI SDK Docs](https://sdk.vercel.ai/docs)\n- [Workers AI Models](https://developers.cloudflare.com/workers-ai/models)"
|
|
9
14
|
},
|
|
10
15
|
"hooks": {
|
|
11
16
|
"postDeploy": [
|
|
@@ -17,7 +22,7 @@
|
|
|
17
22
|
{
|
|
18
23
|
"action": "box",
|
|
19
24
|
"title": "{{name}}",
|
|
20
|
-
"lines": ["{{url}}", "", "jack open to
|
|
25
|
+
"lines": ["{{url}}", "", "Chat history saved to D1.", "jack open to start chatting"]
|
|
21
26
|
}
|
|
22
27
|
]
|
|
23
28
|
}
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
"workspaces": {
|
|
5
5
|
"": {
|
|
6
6
|
"name": "jack-template",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"ai": "^4",
|
|
9
|
+
"hono": "^4.6.0",
|
|
10
|
+
"workers-ai-provider": "^0.7",
|
|
11
|
+
},
|
|
7
12
|
"devDependencies": {
|
|
8
13
|
"@cloudflare/workers-types": "^4.20241205.0",
|
|
9
14
|
"typescript": "^5.0.0",
|
|
@@ -11,8 +16,52 @@
|
|
|
11
16
|
},
|
|
12
17
|
},
|
|
13
18
|
"packages": {
|
|
14
|
-
"@
|
|
19
|
+
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
|
20
|
+
|
|
21
|
+
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
|
22
|
+
|
|
23
|
+
"@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="],
|
|
24
|
+
|
|
25
|
+
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
|
|
26
|
+
|
|
27
|
+
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260212.0", "", {}, "sha512-ZK+e8T/2tWBCrE8PoAi9oqTxcBen9Apq+dxbsy1R5LFVdB6M4pY+oP49OFuHTTezrvNXbyvmzbf/vjtrCPGdNg=="],
|
|
28
|
+
|
|
29
|
+
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
|
30
|
+
|
|
31
|
+
"@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
|
|
32
|
+
|
|
33
|
+
"ai": ["ai@4.3.19", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q=="],
|
|
34
|
+
|
|
35
|
+
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
|
36
|
+
|
|
37
|
+
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
|
38
|
+
|
|
39
|
+
"diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
|
|
40
|
+
|
|
41
|
+
"hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="],
|
|
42
|
+
|
|
43
|
+
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
|
44
|
+
|
|
45
|
+
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
|
|
46
|
+
|
|
47
|
+
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
|
48
|
+
|
|
49
|
+
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
|
50
|
+
|
|
51
|
+
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
|
|
52
|
+
|
|
53
|
+
"swr": ["swr@2.4.0", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw=="],
|
|
54
|
+
|
|
55
|
+
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
|
|
15
56
|
|
|
16
57
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
58
|
+
|
|
59
|
+
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
|
60
|
+
|
|
61
|
+
"workers-ai-provider": ["workers-ai-provider@0.7.5", "", { "dependencies": { "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8" } }, "sha512-dhCwgc3D65oDDTpH3k8Gf0Ek7KItzvaQidn2N5L5cqLo3WG8GM/4+Nr4rU56o8O3oZRsloB1gUCHYaRv2j7Y0A=="],
|
|
62
|
+
|
|
63
|
+
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
|
64
|
+
|
|
65
|
+
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
|
17
66
|
}
|
|
18
67
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { useChat } from "@ai-sdk/react";
|
|
4
|
+
|
|
5
|
+
const { useState, useEffect, useRef } = React;
|
|
6
|
+
|
|
7
|
+
function App() {
|
|
8
|
+
const [chatId, setChatId] = useState(null);
|
|
9
|
+
const messagesEndRef = useRef(null);
|
|
10
|
+
|
|
11
|
+
// Create a new chat on mount
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
fetch("/api/chat/new", { method: "POST" })
|
|
14
|
+
.then((r) => r.json())
|
|
15
|
+
.then((data) => setChatId(data.id));
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
|
|
19
|
+
api: "/api/chat",
|
|
20
|
+
body: { chatId },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Auto-scroll
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
26
|
+
}, [messages]);
|
|
27
|
+
|
|
28
|
+
return React.createElement("div", { style: styles.container },
|
|
29
|
+
React.createElement("header", { style: styles.header },
|
|
30
|
+
React.createElement("h1", { style: styles.title }, "AI Chat"),
|
|
31
|
+
),
|
|
32
|
+
React.createElement("div", { style: styles.messages },
|
|
33
|
+
messages.length === 0 && React.createElement("div", { style: styles.empty }, "Send a message to start chatting"),
|
|
34
|
+
messages.map((m) =>
|
|
35
|
+
React.createElement("div", { key: m.id, style: { ...styles.message, ...(m.role === "user" ? styles.userMessage : styles.assistantMessage) } },
|
|
36
|
+
React.createElement("div", { style: styles.messageRole }, m.role === "user" ? "You" : "AI"),
|
|
37
|
+
React.createElement("div", { style: styles.messageContent }, m.content || (m.parts?.map(p => p.text).join("") || "")),
|
|
38
|
+
)
|
|
39
|
+
),
|
|
40
|
+
React.createElement("div", { ref: messagesEndRef }),
|
|
41
|
+
),
|
|
42
|
+
React.createElement("form", { onSubmit: handleSubmit, style: styles.form },
|
|
43
|
+
React.createElement("input", {
|
|
44
|
+
value: input,
|
|
45
|
+
onChange: handleInputChange,
|
|
46
|
+
placeholder: "Type a message...",
|
|
47
|
+
style: styles.input,
|
|
48
|
+
disabled: isLoading,
|
|
49
|
+
}),
|
|
50
|
+
React.createElement("button", { type: "submit", style: styles.button, disabled: isLoading || !input.trim() },
|
|
51
|
+
isLoading ? "..." : "Send"
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const styles = {
|
|
58
|
+
container: { display: "flex", flexDirection: "column", height: "100vh", maxWidth: "800px", margin: "0 auto" },
|
|
59
|
+
header: { padding: "16px 20px", borderBottom: "1px solid #262626" },
|
|
60
|
+
title: { fontSize: "18px", fontWeight: "600" },
|
|
61
|
+
messages: { flex: 1, overflow: "auto", padding: "20px", display: "flex", flexDirection: "column", gap: "16px" },
|
|
62
|
+
empty: { color: "#737373", textAlign: "center", marginTop: "40px" },
|
|
63
|
+
message: { padding: "12px 16px", borderRadius: "12px", maxWidth: "80%" },
|
|
64
|
+
userMessage: { alignSelf: "flex-end", background: "#2563eb", color: "white" },
|
|
65
|
+
assistantMessage: { alignSelf: "flex-start", background: "#1c1c1c", border: "1px solid #262626" },
|
|
66
|
+
messageRole: { fontSize: "11px", fontWeight: "600", marginBottom: "4px", opacity: 0.7, textTransform: "uppercase" },
|
|
67
|
+
messageContent: { fontSize: "14px", lineHeight: "1.5", whiteSpace: "pre-wrap" },
|
|
68
|
+
form: { padding: "16px 20px", borderTop: "1px solid #262626", display: "flex", gap: "8px" },
|
|
69
|
+
input: { flex: 1, padding: "10px 14px", borderRadius: "8px", border: "1px solid #333", background: "#1c1c1c", color: "#e5e5e5", fontSize: "14px", outline: "none" },
|
|
70
|
+
button: { padding: "10px 20px", borderRadius: "8px", border: "none", background: "#2563eb", color: "white", fontSize: "14px", fontWeight: "500", cursor: "pointer" },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
createRoot(document.getElementById("root")).render(React.createElement(App));
|
|
@@ -5,205 +5,22 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>AI Chat</title>
|
|
7
7
|
<style>
|
|
8
|
-
* {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
padding: 0;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
body {
|
|
15
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
16
|
-
background: #f5f5f5;
|
|
17
|
-
min-height: 100vh;
|
|
18
|
-
display: flex;
|
|
19
|
-
justify-content: center;
|
|
20
|
-
padding: 1rem;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.chat-container {
|
|
24
|
-
width: 100%;
|
|
25
|
-
max-width: 700px;
|
|
26
|
-
display: flex;
|
|
27
|
-
flex-direction: column;
|
|
28
|
-
height: calc(100vh - 2rem);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
header {
|
|
32
|
-
text-align: center;
|
|
33
|
-
padding: 1rem;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
header h1 {
|
|
37
|
-
font-size: 1.5rem;
|
|
38
|
-
color: #333;
|
|
39
|
-
margin-bottom: 0.25rem;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
header p {
|
|
43
|
-
font-size: 0.875rem;
|
|
44
|
-
color: #666;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.messages {
|
|
48
|
-
flex: 1;
|
|
49
|
-
overflow-y: auto;
|
|
50
|
-
padding: 1rem;
|
|
51
|
-
background: white;
|
|
52
|
-
border-radius: 12px;
|
|
53
|
-
margin-bottom: 1rem;
|
|
54
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.message {
|
|
58
|
-
padding: 0.75rem 1rem;
|
|
59
|
-
margin-bottom: 0.75rem;
|
|
60
|
-
border-radius: 12px;
|
|
61
|
-
max-width: 85%;
|
|
62
|
-
line-height: 1.5;
|
|
63
|
-
word-wrap: break-word;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.message.user {
|
|
67
|
-
background: #007bff;
|
|
68
|
-
color: white;
|
|
69
|
-
margin-left: auto;
|
|
70
|
-
border-bottom-right-radius: 4px;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.message.assistant {
|
|
74
|
-
background: #e9ecef;
|
|
75
|
-
color: #333;
|
|
76
|
-
margin-right: auto;
|
|
77
|
-
border-bottom-left-radius: 4px;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.message.error {
|
|
81
|
-
background: #fee;
|
|
82
|
-
color: #c00;
|
|
83
|
-
border: 1px solid #fcc;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.message.typing {
|
|
87
|
-
color: #666;
|
|
88
|
-
font-style: italic;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.input-area {
|
|
92
|
-
display: flex;
|
|
93
|
-
gap: 0.5rem;
|
|
94
|
-
background: white;
|
|
95
|
-
padding: 1rem;
|
|
96
|
-
border-radius: 12px;
|
|
97
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.input-area input {
|
|
101
|
-
flex: 1;
|
|
102
|
-
padding: 0.75rem 1rem;
|
|
103
|
-
border: 1px solid #ddd;
|
|
104
|
-
border-radius: 8px;
|
|
105
|
-
font-size: 1rem;
|
|
106
|
-
outline: none;
|
|
107
|
-
transition: border-color 0.2s;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.input-area input:focus {
|
|
111
|
-
border-color: #007bff;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.input-area input:disabled {
|
|
115
|
-
background: #f5f5f5;
|
|
116
|
-
cursor: not-allowed;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.input-area button {
|
|
120
|
-
padding: 0.75rem 1.5rem;
|
|
121
|
-
background: #007bff;
|
|
122
|
-
color: white;
|
|
123
|
-
border: none;
|
|
124
|
-
border-radius: 8px;
|
|
125
|
-
font-size: 1rem;
|
|
126
|
-
cursor: pointer;
|
|
127
|
-
transition: background 0.2s;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.input-area button:hover:not(:disabled) {
|
|
131
|
-
background: #0056b3;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.input-area button:disabled {
|
|
135
|
-
background: #ccc;
|
|
136
|
-
cursor: not-allowed;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.empty-state {
|
|
140
|
-
text-align: center;
|
|
141
|
-
color: #999;
|
|
142
|
-
padding: 3rem 1rem;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.empty-state p {
|
|
146
|
-
font-size: 1.1rem;
|
|
147
|
-
margin-bottom: 0.5rem;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.empty-state small {
|
|
151
|
-
font-size: 0.875rem;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
@media (max-width: 480px) {
|
|
155
|
-
body {
|
|
156
|
-
padding: 0.5rem;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.chat-container {
|
|
160
|
-
height: calc(100vh - 1rem);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
header h1 {
|
|
164
|
-
font-size: 1.25rem;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.message {
|
|
168
|
-
max-width: 90%;
|
|
169
|
-
padding: 0.625rem 0.875rem;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
.input-area {
|
|
173
|
-
padding: 0.75rem;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.input-area button {
|
|
177
|
-
padding: 0.75rem 1rem;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0a0a0a; color: #e5e5e5; height: 100vh; }
|
|
10
|
+
#root { height: 100%; }
|
|
180
11
|
</style>
|
|
181
12
|
</head>
|
|
182
13
|
<body>
|
|
183
|
-
<div
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
</div>
|
|
195
|
-
|
|
196
|
-
<div class="input-area">
|
|
197
|
-
<input
|
|
198
|
-
type="text"
|
|
199
|
-
id="input"
|
|
200
|
-
placeholder="Type your message..."
|
|
201
|
-
autocomplete="off"
|
|
202
|
-
/>
|
|
203
|
-
<button id="send">Send</button>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
<script src="/chat.js"></script>
|
|
14
|
+
<div id="root"></div>
|
|
15
|
+
<script type="importmap">
|
|
16
|
+
{
|
|
17
|
+
"imports": {
|
|
18
|
+
"react": "https://esm.sh/react@19",
|
|
19
|
+
"react-dom/client": "https://esm.sh/react-dom@19/client",
|
|
20
|
+
"@ai-sdk/react": "https://esm.sh/@ai-sdk/react@1?external=react"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
<script type="module" src="/app.js"></script>
|
|
208
25
|
</body>
|
|
209
26
|
</html>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS chats (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
4
|
+
);
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
9
|
+
role TEXT NOT NULL,
|
|
10
|
+
content TEXT NOT NULL,
|
|
11
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id);
|