almostnode 0.1.0
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/LICENSE +21 -0
- package/README.md +731 -0
- package/dist/__sw__.js +394 -0
- package/dist/ai-chatbot-demo-entry.d.ts +6 -0
- package/dist/ai-chatbot-demo-entry.d.ts.map +1 -0
- package/dist/ai-chatbot-demo.d.ts +42 -0
- package/dist/ai-chatbot-demo.d.ts.map +1 -0
- package/dist/assets/runtime-worker-D9x_Ddwz.js +60543 -0
- package/dist/assets/runtime-worker-D9x_Ddwz.js.map +1 -0
- package/dist/convex-app-demo-entry.d.ts +6 -0
- package/dist/convex-app-demo-entry.d.ts.map +1 -0
- package/dist/convex-app-demo.d.ts +68 -0
- package/dist/convex-app-demo.d.ts.map +1 -0
- package/dist/cors-proxy.d.ts +46 -0
- package/dist/cors-proxy.d.ts.map +1 -0
- package/dist/create-runtime.d.ts +42 -0
- package/dist/create-runtime.d.ts.map +1 -0
- package/dist/demo.d.ts +6 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/dev-server.d.ts +97 -0
- package/dist/dev-server.d.ts.map +1 -0
- package/dist/frameworks/next-dev-server.d.ts +202 -0
- package/dist/frameworks/next-dev-server.d.ts.map +1 -0
- package/dist/frameworks/vite-dev-server.d.ts +85 -0
- package/dist/frameworks/vite-dev-server.d.ts.map +1 -0
- package/dist/index.cjs +14965 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +14867 -0
- package/dist/index.mjs.map +1 -0
- package/dist/next-demo.d.ts +49 -0
- package/dist/next-demo.d.ts.map +1 -0
- package/dist/npm/index.d.ts +71 -0
- package/dist/npm/index.d.ts.map +1 -0
- package/dist/npm/registry.d.ts +66 -0
- package/dist/npm/registry.d.ts.map +1 -0
- package/dist/npm/resolver.d.ts +52 -0
- package/dist/npm/resolver.d.ts.map +1 -0
- package/dist/npm/tarball.d.ts +29 -0
- package/dist/npm/tarball.d.ts.map +1 -0
- package/dist/runtime-interface.d.ts +90 -0
- package/dist/runtime-interface.d.ts.map +1 -0
- package/dist/runtime.d.ts +103 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/sandbox-helpers.d.ts +43 -0
- package/dist/sandbox-helpers.d.ts.map +1 -0
- package/dist/sandbox-runtime.d.ts +65 -0
- package/dist/sandbox-runtime.d.ts.map +1 -0
- package/dist/server-bridge.d.ts +89 -0
- package/dist/server-bridge.d.ts.map +1 -0
- package/dist/shims/assert.d.ts +51 -0
- package/dist/shims/assert.d.ts.map +1 -0
- package/dist/shims/async_hooks.d.ts +37 -0
- package/dist/shims/async_hooks.d.ts.map +1 -0
- package/dist/shims/buffer.d.ts +20 -0
- package/dist/shims/buffer.d.ts.map +1 -0
- package/dist/shims/child_process-browser.d.ts +92 -0
- package/dist/shims/child_process-browser.d.ts.map +1 -0
- package/dist/shims/child_process.d.ts +93 -0
- package/dist/shims/child_process.d.ts.map +1 -0
- package/dist/shims/chokidar.d.ts +55 -0
- package/dist/shims/chokidar.d.ts.map +1 -0
- package/dist/shims/cluster.d.ts +52 -0
- package/dist/shims/cluster.d.ts.map +1 -0
- package/dist/shims/crypto.d.ts +122 -0
- package/dist/shims/crypto.d.ts.map +1 -0
- package/dist/shims/dgram.d.ts +34 -0
- package/dist/shims/dgram.d.ts.map +1 -0
- package/dist/shims/diagnostics_channel.d.ts +80 -0
- package/dist/shims/diagnostics_channel.d.ts.map +1 -0
- package/dist/shims/dns.d.ts +87 -0
- package/dist/shims/dns.d.ts.map +1 -0
- package/dist/shims/domain.d.ts +25 -0
- package/dist/shims/domain.d.ts.map +1 -0
- package/dist/shims/esbuild.d.ts +105 -0
- package/dist/shims/esbuild.d.ts.map +1 -0
- package/dist/shims/events.d.ts +37 -0
- package/dist/shims/events.d.ts.map +1 -0
- package/dist/shims/fs.d.ts +115 -0
- package/dist/shims/fs.d.ts.map +1 -0
- package/dist/shims/fsevents.d.ts +67 -0
- package/dist/shims/fsevents.d.ts.map +1 -0
- package/dist/shims/http.d.ts +217 -0
- package/dist/shims/http.d.ts.map +1 -0
- package/dist/shims/http2.d.ts +81 -0
- package/dist/shims/http2.d.ts.map +1 -0
- package/dist/shims/https.d.ts +36 -0
- package/dist/shims/https.d.ts.map +1 -0
- package/dist/shims/inspector.d.ts +25 -0
- package/dist/shims/inspector.d.ts.map +1 -0
- package/dist/shims/module.d.ts +22 -0
- package/dist/shims/module.d.ts.map +1 -0
- package/dist/shims/net.d.ts +100 -0
- package/dist/shims/net.d.ts.map +1 -0
- package/dist/shims/os.d.ts +159 -0
- package/dist/shims/os.d.ts.map +1 -0
- package/dist/shims/path.d.ts +72 -0
- package/dist/shims/path.d.ts.map +1 -0
- package/dist/shims/perf_hooks.d.ts +50 -0
- package/dist/shims/perf_hooks.d.ts.map +1 -0
- package/dist/shims/process.d.ts +93 -0
- package/dist/shims/process.d.ts.map +1 -0
- package/dist/shims/querystring.d.ts +23 -0
- package/dist/shims/querystring.d.ts.map +1 -0
- package/dist/shims/readdirp.d.ts +52 -0
- package/dist/shims/readdirp.d.ts.map +1 -0
- package/dist/shims/readline.d.ts +62 -0
- package/dist/shims/readline.d.ts.map +1 -0
- package/dist/shims/rollup.d.ts +34 -0
- package/dist/shims/rollup.d.ts.map +1 -0
- package/dist/shims/sentry.d.ts +163 -0
- package/dist/shims/sentry.d.ts.map +1 -0
- package/dist/shims/stream.d.ts +181 -0
- package/dist/shims/stream.d.ts.map +1 -0
- package/dist/shims/tls.d.ts +53 -0
- package/dist/shims/tls.d.ts.map +1 -0
- package/dist/shims/tty.d.ts +30 -0
- package/dist/shims/tty.d.ts.map +1 -0
- package/dist/shims/url.d.ts +64 -0
- package/dist/shims/url.d.ts.map +1 -0
- package/dist/shims/util.d.ts +106 -0
- package/dist/shims/util.d.ts.map +1 -0
- package/dist/shims/v8.d.ts +73 -0
- package/dist/shims/v8.d.ts.map +1 -0
- package/dist/shims/vfs-adapter.d.ts +126 -0
- package/dist/shims/vfs-adapter.d.ts.map +1 -0
- package/dist/shims/vm.d.ts +45 -0
- package/dist/shims/vm.d.ts.map +1 -0
- package/dist/shims/worker_threads.d.ts +66 -0
- package/dist/shims/worker_threads.d.ts.map +1 -0
- package/dist/shims/ws.d.ts +66 -0
- package/dist/shims/ws.d.ts.map +1 -0
- package/dist/shims/zlib.d.ts +161 -0
- package/dist/shims/zlib.d.ts.map +1 -0
- package/dist/transform.d.ts +24 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts +226 -0
- package/dist/virtual-fs.d.ts.map +1 -0
- package/dist/vite-demo.d.ts +35 -0
- package/dist/vite-demo.d.ts.map +1 -0
- package/dist/vite-sw.js +132 -0
- package/dist/worker/runtime-worker.d.ts +8 -0
- package/dist/worker/runtime-worker.d.ts.map +1 -0
- package/dist/worker-runtime.d.ts +50 -0
- package/dist/worker-runtime.d.ts.map +1 -0
- package/package.json +85 -0
- package/src/ai-chatbot-demo-entry.ts +244 -0
- package/src/ai-chatbot-demo.ts +509 -0
- package/src/convex-app-demo-entry.ts +1107 -0
- package/src/convex-app-demo.ts +1316 -0
- package/src/cors-proxy.ts +81 -0
- package/src/create-runtime.ts +147 -0
- package/src/demo.ts +304 -0
- package/src/dev-server.ts +274 -0
- package/src/frameworks/next-dev-server.ts +2224 -0
- package/src/frameworks/vite-dev-server.ts +702 -0
- package/src/index.ts +101 -0
- package/src/next-demo.ts +1784 -0
- package/src/npm/index.ts +347 -0
- package/src/npm/registry.ts +152 -0
- package/src/npm/resolver.ts +385 -0
- package/src/npm/tarball.ts +209 -0
- package/src/runtime-interface.ts +103 -0
- package/src/runtime.ts +1046 -0
- package/src/sandbox-helpers.ts +173 -0
- package/src/sandbox-runtime.ts +252 -0
- package/src/server-bridge.ts +426 -0
- package/src/shims/assert.ts +664 -0
- package/src/shims/async_hooks.ts +86 -0
- package/src/shims/buffer.ts +75 -0
- package/src/shims/child_process-browser.ts +217 -0
- package/src/shims/child_process.ts +463 -0
- package/src/shims/chokidar.ts +313 -0
- package/src/shims/cluster.ts +67 -0
- package/src/shims/crypto.ts +830 -0
- package/src/shims/dgram.ts +47 -0
- package/src/shims/diagnostics_channel.ts +196 -0
- package/src/shims/dns.ts +172 -0
- package/src/shims/domain.ts +58 -0
- package/src/shims/esbuild.ts +805 -0
- package/src/shims/events.ts +195 -0
- package/src/shims/fs.ts +803 -0
- package/src/shims/fsevents.ts +63 -0
- package/src/shims/http.ts +904 -0
- package/src/shims/http2.ts +96 -0
- package/src/shims/https.ts +86 -0
- package/src/shims/inspector.ts +30 -0
- package/src/shims/module.ts +82 -0
- package/src/shims/net.ts +359 -0
- package/src/shims/os.ts +195 -0
- package/src/shims/path.ts +199 -0
- package/src/shims/perf_hooks.ts +92 -0
- package/src/shims/process.ts +346 -0
- package/src/shims/querystring.ts +97 -0
- package/src/shims/readdirp.ts +228 -0
- package/src/shims/readline.ts +110 -0
- package/src/shims/rollup.ts +80 -0
- package/src/shims/sentry.ts +133 -0
- package/src/shims/stream.ts +1126 -0
- package/src/shims/tls.ts +95 -0
- package/src/shims/tty.ts +64 -0
- package/src/shims/url.ts +171 -0
- package/src/shims/util.ts +312 -0
- package/src/shims/v8.ts +113 -0
- package/src/shims/vfs-adapter.ts +402 -0
- package/src/shims/vm.ts +83 -0
- package/src/shims/worker_threads.ts +111 -0
- package/src/shims/ws.ts +382 -0
- package/src/shims/zlib.ts +289 -0
- package/src/transform.ts +313 -0
- package/src/types/external.d.ts +67 -0
- package/src/virtual-fs.ts +903 -0
- package/src/vite-demo.ts +577 -0
- package/src/worker/runtime-worker.ts +128 -0
- package/src/worker-runtime.ts +145 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Chatbot Demo with Next.js + Vercel AI SDK
|
|
3
|
+
*
|
|
4
|
+
* This demo creates a chatbot application using:
|
|
5
|
+
* - Next.js App Router for the frontend
|
|
6
|
+
* - Pages Router API routes for the streaming endpoint
|
|
7
|
+
* - Vercel AI SDK with useChat hook
|
|
8
|
+
* - OpenAI (via CORS proxy for browser environment)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { VirtualFS } from './virtual-fs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Package.json for the AI chatbot app
|
|
15
|
+
*/
|
|
16
|
+
const PACKAGE_JSON = {
|
|
17
|
+
name: "ai-chatbot-demo",
|
|
18
|
+
version: "0.1.0",
|
|
19
|
+
private: true,
|
|
20
|
+
scripts: {
|
|
21
|
+
dev: "next dev",
|
|
22
|
+
build: "next build",
|
|
23
|
+
start: "next start",
|
|
24
|
+
},
|
|
25
|
+
dependencies: {
|
|
26
|
+
"next": "^14.0.0",
|
|
27
|
+
"react": "^18.2.0",
|
|
28
|
+
"react-dom": "^18.2.0",
|
|
29
|
+
"ai": "^4.0.0",
|
|
30
|
+
"@ai-sdk/openai": "^1.0.0",
|
|
31
|
+
},
|
|
32
|
+
devDependencies: {
|
|
33
|
+
"@types/node": "^20",
|
|
34
|
+
"@types/react": "^19",
|
|
35
|
+
"@types/react-dom": "^19",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create the AI chatbot project structure in the virtual filesystem
|
|
42
|
+
*/
|
|
43
|
+
export function createAIChatbotProject(vfs: VirtualFS): void {
|
|
44
|
+
// Create package.json
|
|
45
|
+
vfs.writeFileSync('/package.json', JSON.stringify(PACKAGE_JSON, null, 2));
|
|
46
|
+
|
|
47
|
+
// Create directories - App Router + Pages Router (for API)
|
|
48
|
+
vfs.mkdirSync('/app', { recursive: true });
|
|
49
|
+
vfs.mkdirSync('/pages/api', { recursive: true });
|
|
50
|
+
vfs.mkdirSync('/public', { recursive: true });
|
|
51
|
+
|
|
52
|
+
// Create TypeScript config
|
|
53
|
+
vfs.writeFileSync('/tsconfig.json', JSON.stringify({
|
|
54
|
+
compilerOptions: {
|
|
55
|
+
target: "es5",
|
|
56
|
+
lib: ["dom", "dom.iterable", "esnext"],
|
|
57
|
+
allowJs: true,
|
|
58
|
+
skipLibCheck: true,
|
|
59
|
+
strict: true,
|
|
60
|
+
noEmit: true,
|
|
61
|
+
esModuleInterop: true,
|
|
62
|
+
module: "esnext",
|
|
63
|
+
moduleResolution: "bundler",
|
|
64
|
+
resolveJsonModule: true,
|
|
65
|
+
isolatedModules: true,
|
|
66
|
+
jsx: "preserve",
|
|
67
|
+
incremental: true,
|
|
68
|
+
paths: {
|
|
69
|
+
"@/*": ["./*"]
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
include: ["**/*.ts", "**/*.tsx"],
|
|
73
|
+
exclude: ["node_modules"]
|
|
74
|
+
}, null, 2));
|
|
75
|
+
|
|
76
|
+
// Create global CSS with Tailwind
|
|
77
|
+
vfs.writeFileSync('/app/globals.css', `@tailwind base;
|
|
78
|
+
@tailwind components;
|
|
79
|
+
@tailwind utilities;
|
|
80
|
+
|
|
81
|
+
body {
|
|
82
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
83
|
+
min-height: 100vh;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.chat-container {
|
|
87
|
+
max-width: 800px;
|
|
88
|
+
margin: 0 auto;
|
|
89
|
+
padding: 2rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.message-bubble {
|
|
93
|
+
padding: 1rem;
|
|
94
|
+
border-radius: 1rem;
|
|
95
|
+
margin-bottom: 0.75rem;
|
|
96
|
+
max-width: 80%;
|
|
97
|
+
animation: fadeIn 0.3s ease-out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.message-user {
|
|
101
|
+
background: #3b82f6;
|
|
102
|
+
color: white;
|
|
103
|
+
margin-left: auto;
|
|
104
|
+
border-bottom-right-radius: 0.25rem;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.message-assistant {
|
|
108
|
+
background: white;
|
|
109
|
+
color: #1f2937;
|
|
110
|
+
margin-right: auto;
|
|
111
|
+
border-bottom-left-radius: 0.25rem;
|
|
112
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.loading-dots::after {
|
|
116
|
+
content: '';
|
|
117
|
+
animation: dots 1.5s steps(5, end) infinite;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@keyframes dots {
|
|
121
|
+
0%, 20% { content: '.'; }
|
|
122
|
+
40% { content: '..'; }
|
|
123
|
+
60%, 100% { content: '...'; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@keyframes fadeIn {
|
|
127
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
128
|
+
to { opacity: 1; transform: translateY(0); }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.input-container {
|
|
132
|
+
position: sticky;
|
|
133
|
+
bottom: 0;
|
|
134
|
+
background: rgba(255, 255, 255, 0.95);
|
|
135
|
+
backdrop-filter: blur(10px);
|
|
136
|
+
border-radius: 1rem;
|
|
137
|
+
padding: 1rem;
|
|
138
|
+
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
|
139
|
+
}
|
|
140
|
+
`);
|
|
141
|
+
|
|
142
|
+
// Create root layout (App Router)
|
|
143
|
+
vfs.writeFileSync('/app/layout.tsx', `import React from 'react';
|
|
144
|
+
import './globals.css';
|
|
145
|
+
|
|
146
|
+
export const metadata = {
|
|
147
|
+
title: 'AI Chatbot Demo',
|
|
148
|
+
description: 'A chatbot demo using Next.js and Vercel AI SDK',
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default function RootLayout({
|
|
152
|
+
children,
|
|
153
|
+
}: {
|
|
154
|
+
children: React.ReactNode;
|
|
155
|
+
}) {
|
|
156
|
+
return (
|
|
157
|
+
<div className="min-h-screen">
|
|
158
|
+
<header className="bg-white/10 backdrop-blur-sm border-b border-white/20">
|
|
159
|
+
<div className="max-w-4xl mx-auto px-4 py-4">
|
|
160
|
+
<h1 className="text-2xl font-bold text-white flex items-center gap-2">
|
|
161
|
+
<span>🤖</span>
|
|
162
|
+
AI Chatbot Demo
|
|
163
|
+
</h1>
|
|
164
|
+
<p className="text-white/70 text-sm mt-1">
|
|
165
|
+
Powered by Vercel AI SDK + OpenAI
|
|
166
|
+
</p>
|
|
167
|
+
</div>
|
|
168
|
+
</header>
|
|
169
|
+
<main>{children}</main>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
`);
|
|
174
|
+
|
|
175
|
+
// Create home page with chat UI (App Router)
|
|
176
|
+
vfs.writeFileSync('/app/page.tsx', `"use client";
|
|
177
|
+
|
|
178
|
+
import React from 'react';
|
|
179
|
+
import { useChat } from 'ai/react';
|
|
180
|
+
|
|
181
|
+
// Get the virtual base path for API calls
|
|
182
|
+
// The iframe runs at /__virtual__/PORT/ so we need to prefix API calls
|
|
183
|
+
function getApiUrl(path: string): string {
|
|
184
|
+
const match = window.location.pathname.match(/^(\\/__virtual__\\/\\d+)/);
|
|
185
|
+
if (match) {
|
|
186
|
+
return match[1] + path;
|
|
187
|
+
}
|
|
188
|
+
return path;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export default function ChatPage() {
|
|
192
|
+
// Use the correct API URL based on the virtual base path
|
|
193
|
+
const apiUrl = React.useMemo(() => getApiUrl('/api/chat'), []);
|
|
194
|
+
|
|
195
|
+
const { messages, input, handleInputChange, handleSubmit, isLoading, error } = useChat({
|
|
196
|
+
api: apiUrl,
|
|
197
|
+
onResponse: (response) => {
|
|
198
|
+
console.log('[useChat] onResponse - status:', response.status, 'headers:', Object.fromEntries(response.headers.entries()));
|
|
199
|
+
},
|
|
200
|
+
onFinish: (message) => {
|
|
201
|
+
console.log('[useChat] onFinish - message:', message);
|
|
202
|
+
},
|
|
203
|
+
onError: (err) => {
|
|
204
|
+
console.error('[useChat] onError:', err);
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Debug: log messages changes
|
|
209
|
+
React.useEffect(() => {
|
|
210
|
+
console.log('[ChatPage] messages updated:', messages.length, messages.map(m => ({ role: m.role, contentLength: m.content.length })));
|
|
211
|
+
}, [messages]);
|
|
212
|
+
|
|
213
|
+
const messagesEndRef = React.useRef<HTMLDivElement>(null);
|
|
214
|
+
|
|
215
|
+
// Auto-scroll to bottom when new messages arrive
|
|
216
|
+
React.useEffect(() => {
|
|
217
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
218
|
+
}, [messages]);
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<div className="chat-container">
|
|
222
|
+
{/* Welcome message when no messages */}
|
|
223
|
+
{messages.length === 0 && (
|
|
224
|
+
<div className="text-center py-12">
|
|
225
|
+
<div className="text-6xl mb-4">💬</div>
|
|
226
|
+
<h2 className="text-2xl font-semibold text-white mb-2">
|
|
227
|
+
Start a conversation
|
|
228
|
+
</h2>
|
|
229
|
+
<p className="text-white/70 max-w-md mx-auto">
|
|
230
|
+
Type a message below to chat with the AI assistant.
|
|
231
|
+
Your conversation will stream in real-time.
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{/* Messages list */}
|
|
237
|
+
<div className="space-y-4 pb-32">
|
|
238
|
+
{messages.map((message) => (
|
|
239
|
+
<div
|
|
240
|
+
key={message.id}
|
|
241
|
+
className={\`message-bubble \${
|
|
242
|
+
message.role === 'user' ? 'message-user' : 'message-assistant'
|
|
243
|
+
}\`}
|
|
244
|
+
>
|
|
245
|
+
<div className="flex items-start gap-3">
|
|
246
|
+
<span className="text-lg">
|
|
247
|
+
{message.role === 'user' ? '👤' : '🤖'}
|
|
248
|
+
</span>
|
|
249
|
+
<div className="flex-1">
|
|
250
|
+
<p className="font-medium text-sm opacity-70 mb-1">
|
|
251
|
+
{message.role === 'user' ? 'You' : 'Assistant'}
|
|
252
|
+
</p>
|
|
253
|
+
<div className="whitespace-pre-wrap">{message.content}</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
))}
|
|
258
|
+
|
|
259
|
+
{/* Loading indicator - only show when waiting for response to start streaming */}
|
|
260
|
+
{isLoading && messages.length > 0 && messages[messages.length - 1].role === 'user' && (
|
|
261
|
+
<div className="message-bubble message-assistant">
|
|
262
|
+
<div className="flex items-start gap-3">
|
|
263
|
+
<span className="text-lg">🤖</span>
|
|
264
|
+
<div className="flex-1">
|
|
265
|
+
<p className="font-medium text-sm opacity-70 mb-1">Assistant</p>
|
|
266
|
+
<div className="loading-dots text-gray-500">Thinking</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{/* Error message */}
|
|
273
|
+
{error && (
|
|
274
|
+
<div className="message-bubble bg-red-100 text-red-700">
|
|
275
|
+
<div className="flex items-start gap-3">
|
|
276
|
+
<span className="text-lg">⚠️</span>
|
|
277
|
+
<div>
|
|
278
|
+
<p className="font-medium">Error</p>
|
|
279
|
+
<p>{error.message}</p>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
<div ref={messagesEndRef} />
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* Input form */}
|
|
289
|
+
<div className="input-container">
|
|
290
|
+
<form onSubmit={handleSubmit} className="flex gap-3">
|
|
291
|
+
<input
|
|
292
|
+
type="text"
|
|
293
|
+
value={input}
|
|
294
|
+
onChange={handleInputChange}
|
|
295
|
+
placeholder="Type your message..."
|
|
296
|
+
disabled={isLoading}
|
|
297
|
+
className="flex-1 px-4 py-3 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-50 disabled:cursor-not-allowed"
|
|
298
|
+
/>
|
|
299
|
+
<button
|
|
300
|
+
type="submit"
|
|
301
|
+
disabled={isLoading || !input.trim()}
|
|
302
|
+
className="px-6 py-3 bg-blue-500 text-white font-medium rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
|
|
303
|
+
>
|
|
304
|
+
{isLoading ? (
|
|
305
|
+
<span className="flex items-center gap-2">
|
|
306
|
+
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
|
|
307
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
|
308
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
309
|
+
</svg>
|
|
310
|
+
Sending
|
|
311
|
+
</span>
|
|
312
|
+
) : (
|
|
313
|
+
'Send'
|
|
314
|
+
)}
|
|
315
|
+
</button>
|
|
316
|
+
</form>
|
|
317
|
+
<p className="text-xs text-gray-400 mt-2 text-center">
|
|
318
|
+
Press Enter to send • Streaming responses powered by Vercel AI SDK
|
|
319
|
+
</p>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
`);
|
|
325
|
+
|
|
326
|
+
// Create API route for chat (Pages Router - works in our environment)
|
|
327
|
+
// Uses CORS proxy to call OpenAI from browser
|
|
328
|
+
vfs.writeFileSync('/pages/api/chat.ts', `/**
|
|
329
|
+
* AI Chat API Route
|
|
330
|
+
*
|
|
331
|
+
* This endpoint handles chat requests using the Vercel AI SDK.
|
|
332
|
+
* It uses a CORS proxy (corsproxy.io) to make OpenAI API calls
|
|
333
|
+
* from the browser environment.
|
|
334
|
+
*/
|
|
335
|
+
|
|
336
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
337
|
+
|
|
338
|
+
// Get API key from environment and sanitize it
|
|
339
|
+
const getApiKey = () => {
|
|
340
|
+
// Debug: log what we're seeing
|
|
341
|
+
console.log('[API /chat] getApiKey called');
|
|
342
|
+
console.log('[API /chat] typeof process:', typeof process);
|
|
343
|
+
console.log('[API /chat] process.env keys:', typeof process !== 'undefined' && process.env ? Object.keys(process.env) : 'N/A');
|
|
344
|
+
|
|
345
|
+
// Check process.env (set by demo entry)
|
|
346
|
+
if (typeof process !== 'undefined' && process.env?.OPENAI_API_KEY) {
|
|
347
|
+
const rawKey = process.env.OPENAI_API_KEY;
|
|
348
|
+
console.log('[API /chat] Raw key length:', rawKey?.length);
|
|
349
|
+
console.log('[API /chat] Raw key starts with:', rawKey?.substring(0, 10));
|
|
350
|
+
console.log('[API /chat] Raw key ends with:', rawKey?.substring(rawKey.length - 10));
|
|
351
|
+
|
|
352
|
+
// Sanitize: trim whitespace and remove any non-ASCII characters
|
|
353
|
+
// This prevents "String contains non ISO-8859-1 code point" errors
|
|
354
|
+
const key = rawKey
|
|
355
|
+
.trim()
|
|
356
|
+
.replace(/[^\x00-\x7F]/g, ''); // Remove non-ASCII characters
|
|
357
|
+
|
|
358
|
+
console.log('[API /chat] Sanitized key length:', key?.length);
|
|
359
|
+
return key || null;
|
|
360
|
+
}
|
|
361
|
+
console.log('[API /chat] No OPENAI_API_KEY found in process.env');
|
|
362
|
+
return null;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// CORS proxy for OpenAI API calls from browser
|
|
366
|
+
const CORS_PROXY = 'https://corsproxy.io/?';
|
|
367
|
+
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
368
|
+
|
|
369
|
+
export default async function handler(
|
|
370
|
+
req: NextApiRequest,
|
|
371
|
+
res: NextApiResponse
|
|
372
|
+
) {
|
|
373
|
+
if (req.method !== 'POST') {
|
|
374
|
+
return res.status(405).json({ error: 'Method not allowed' });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const apiKey = getApiKey();
|
|
378
|
+
if (!apiKey) {
|
|
379
|
+
return res.status(500).json({
|
|
380
|
+
error: 'OpenAI API key not configured. Please enter your API key in the demo panel.'
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const { messages } = req.body;
|
|
386
|
+
|
|
387
|
+
if (!messages || !Array.isArray(messages)) {
|
|
388
|
+
return res.status(400).json({ error: 'Invalid messages format' });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Format messages for OpenAI API
|
|
392
|
+
const formattedMessages = messages.map((m: any) => ({
|
|
393
|
+
role: m.role,
|
|
394
|
+
content: m.content,
|
|
395
|
+
}));
|
|
396
|
+
|
|
397
|
+
// Make request to OpenAI via CORS proxy
|
|
398
|
+
const response = await fetch(CORS_PROXY + encodeURIComponent(OPENAI_API_URL), {
|
|
399
|
+
method: 'POST',
|
|
400
|
+
headers: {
|
|
401
|
+
'Content-Type': 'application/json',
|
|
402
|
+
'Authorization': \`Bearer \${apiKey}\`,
|
|
403
|
+
},
|
|
404
|
+
body: JSON.stringify({
|
|
405
|
+
model: 'gpt-4o-mini',
|
|
406
|
+
messages: formattedMessages,
|
|
407
|
+
stream: true,
|
|
408
|
+
}),
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
if (!response.ok) {
|
|
412
|
+
const errorText = await response.text();
|
|
413
|
+
console.error('OpenAI API error:', errorText);
|
|
414
|
+
return res.status(response.status).json({
|
|
415
|
+
error: \`OpenAI API error: \${response.statusText}\`
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Set up streaming response headers
|
|
420
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
421
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
422
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
423
|
+
|
|
424
|
+
// Stream the response using AI SDK data stream format
|
|
425
|
+
// Format: 0:"text chunk"\\n (text deltas)
|
|
426
|
+
const reader = response.body?.getReader();
|
|
427
|
+
if (!reader) {
|
|
428
|
+
return res.status(500).json({ error: 'Failed to get response stream' });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
console.log('[API /chat] Starting to stream response...');
|
|
432
|
+
const decoder = new TextDecoder();
|
|
433
|
+
let buffer = '';
|
|
434
|
+
let chunkCount = 0;
|
|
435
|
+
|
|
436
|
+
// Collect all chunks first (CORS proxy may buffer entire response)
|
|
437
|
+
const pendingChunks: string[] = [];
|
|
438
|
+
|
|
439
|
+
while (true) {
|
|
440
|
+
const { done, value } = await reader.read();
|
|
441
|
+
if (done) {
|
|
442
|
+
console.log('[API /chat] OpenAI stream done');
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
buffer += decoder.decode(value, { stream: true });
|
|
447
|
+
const lines = buffer.split('\\n');
|
|
448
|
+
buffer = lines.pop() || '';
|
|
449
|
+
|
|
450
|
+
for (const line of lines) {
|
|
451
|
+
if (line.startsWith('data: ')) {
|
|
452
|
+
const data = line.slice(6);
|
|
453
|
+
if (data === '[DONE]') {
|
|
454
|
+
console.log('[API /chat] Received [DONE] from OpenAI');
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
const parsed = JSON.parse(data);
|
|
459
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
460
|
+
if (content) {
|
|
461
|
+
// AI SDK data stream format: 0:"text"\\n
|
|
462
|
+
const chunk = \`0:"\${content.replace(/"/g, '\\\\"').replace(/\\n/g, '\\\\n')}"\\n\`;
|
|
463
|
+
pendingChunks.push(chunk);
|
|
464
|
+
}
|
|
465
|
+
} catch (e) {
|
|
466
|
+
// Ignore parse errors for incomplete chunks
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
console.log('[API /chat] Collected', pendingChunks.length, 'chunks, streaming with delays...');
|
|
473
|
+
|
|
474
|
+
// Helper to create delay
|
|
475
|
+
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
476
|
+
|
|
477
|
+
// Stream chunks with delays to ensure they arrive separately
|
|
478
|
+
// The CORS proxy buffers the entire OpenAI response, so we simulate streaming
|
|
479
|
+
// by writing chunks with delays. 50ms gives the message channel time to process
|
|
480
|
+
// each chunk before the next one arrives.
|
|
481
|
+
for (const chunk of pendingChunks) {
|
|
482
|
+
chunkCount++;
|
|
483
|
+
console.log('[API /chat] Writing chunk', chunkCount);
|
|
484
|
+
res.write(chunk);
|
|
485
|
+
// Longer delay to ensure message channel processes each chunk separately
|
|
486
|
+
await delay(50);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// End the stream with finish reason
|
|
490
|
+
console.log('[API /chat] Writing finish message, total chunks:', chunkCount);
|
|
491
|
+
res.write('d:{"finishReason":"stop"}\\n');
|
|
492
|
+
res.end();
|
|
493
|
+
|
|
494
|
+
} catch (error) {
|
|
495
|
+
console.error('Chat API error:', error);
|
|
496
|
+
res.status(500).json({
|
|
497
|
+
error: error instanceof Error ? error.message : 'Internal server error'
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
`);
|
|
502
|
+
|
|
503
|
+
// Create public files
|
|
504
|
+
vfs.writeFileSync('/public/favicon.ico', 'favicon placeholder');
|
|
505
|
+
vfs.writeFileSync('/public/robots.txt', 'User-agent: *\nAllow: /');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Export for use in HTML demos
|
|
509
|
+
export { PACKAGE_JSON };
|