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.
Files changed (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +731 -0
  3. package/dist/__sw__.js +394 -0
  4. package/dist/ai-chatbot-demo-entry.d.ts +6 -0
  5. package/dist/ai-chatbot-demo-entry.d.ts.map +1 -0
  6. package/dist/ai-chatbot-demo.d.ts +42 -0
  7. package/dist/ai-chatbot-demo.d.ts.map +1 -0
  8. package/dist/assets/runtime-worker-D9x_Ddwz.js +60543 -0
  9. package/dist/assets/runtime-worker-D9x_Ddwz.js.map +1 -0
  10. package/dist/convex-app-demo-entry.d.ts +6 -0
  11. package/dist/convex-app-demo-entry.d.ts.map +1 -0
  12. package/dist/convex-app-demo.d.ts +68 -0
  13. package/dist/convex-app-demo.d.ts.map +1 -0
  14. package/dist/cors-proxy.d.ts +46 -0
  15. package/dist/cors-proxy.d.ts.map +1 -0
  16. package/dist/create-runtime.d.ts +42 -0
  17. package/dist/create-runtime.d.ts.map +1 -0
  18. package/dist/demo.d.ts +6 -0
  19. package/dist/demo.d.ts.map +1 -0
  20. package/dist/dev-server.d.ts +97 -0
  21. package/dist/dev-server.d.ts.map +1 -0
  22. package/dist/frameworks/next-dev-server.d.ts +202 -0
  23. package/dist/frameworks/next-dev-server.d.ts.map +1 -0
  24. package/dist/frameworks/vite-dev-server.d.ts +85 -0
  25. package/dist/frameworks/vite-dev-server.d.ts.map +1 -0
  26. package/dist/index.cjs +14965 -0
  27. package/dist/index.cjs.map +1 -0
  28. package/dist/index.d.ts +71 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.mjs +14867 -0
  31. package/dist/index.mjs.map +1 -0
  32. package/dist/next-demo.d.ts +49 -0
  33. package/dist/next-demo.d.ts.map +1 -0
  34. package/dist/npm/index.d.ts +71 -0
  35. package/dist/npm/index.d.ts.map +1 -0
  36. package/dist/npm/registry.d.ts +66 -0
  37. package/dist/npm/registry.d.ts.map +1 -0
  38. package/dist/npm/resolver.d.ts +52 -0
  39. package/dist/npm/resolver.d.ts.map +1 -0
  40. package/dist/npm/tarball.d.ts +29 -0
  41. package/dist/npm/tarball.d.ts.map +1 -0
  42. package/dist/runtime-interface.d.ts +90 -0
  43. package/dist/runtime-interface.d.ts.map +1 -0
  44. package/dist/runtime.d.ts +103 -0
  45. package/dist/runtime.d.ts.map +1 -0
  46. package/dist/sandbox-helpers.d.ts +43 -0
  47. package/dist/sandbox-helpers.d.ts.map +1 -0
  48. package/dist/sandbox-runtime.d.ts +65 -0
  49. package/dist/sandbox-runtime.d.ts.map +1 -0
  50. package/dist/server-bridge.d.ts +89 -0
  51. package/dist/server-bridge.d.ts.map +1 -0
  52. package/dist/shims/assert.d.ts +51 -0
  53. package/dist/shims/assert.d.ts.map +1 -0
  54. package/dist/shims/async_hooks.d.ts +37 -0
  55. package/dist/shims/async_hooks.d.ts.map +1 -0
  56. package/dist/shims/buffer.d.ts +20 -0
  57. package/dist/shims/buffer.d.ts.map +1 -0
  58. package/dist/shims/child_process-browser.d.ts +92 -0
  59. package/dist/shims/child_process-browser.d.ts.map +1 -0
  60. package/dist/shims/child_process.d.ts +93 -0
  61. package/dist/shims/child_process.d.ts.map +1 -0
  62. package/dist/shims/chokidar.d.ts +55 -0
  63. package/dist/shims/chokidar.d.ts.map +1 -0
  64. package/dist/shims/cluster.d.ts +52 -0
  65. package/dist/shims/cluster.d.ts.map +1 -0
  66. package/dist/shims/crypto.d.ts +122 -0
  67. package/dist/shims/crypto.d.ts.map +1 -0
  68. package/dist/shims/dgram.d.ts +34 -0
  69. package/dist/shims/dgram.d.ts.map +1 -0
  70. package/dist/shims/diagnostics_channel.d.ts +80 -0
  71. package/dist/shims/diagnostics_channel.d.ts.map +1 -0
  72. package/dist/shims/dns.d.ts +87 -0
  73. package/dist/shims/dns.d.ts.map +1 -0
  74. package/dist/shims/domain.d.ts +25 -0
  75. package/dist/shims/domain.d.ts.map +1 -0
  76. package/dist/shims/esbuild.d.ts +105 -0
  77. package/dist/shims/esbuild.d.ts.map +1 -0
  78. package/dist/shims/events.d.ts +37 -0
  79. package/dist/shims/events.d.ts.map +1 -0
  80. package/dist/shims/fs.d.ts +115 -0
  81. package/dist/shims/fs.d.ts.map +1 -0
  82. package/dist/shims/fsevents.d.ts +67 -0
  83. package/dist/shims/fsevents.d.ts.map +1 -0
  84. package/dist/shims/http.d.ts +217 -0
  85. package/dist/shims/http.d.ts.map +1 -0
  86. package/dist/shims/http2.d.ts +81 -0
  87. package/dist/shims/http2.d.ts.map +1 -0
  88. package/dist/shims/https.d.ts +36 -0
  89. package/dist/shims/https.d.ts.map +1 -0
  90. package/dist/shims/inspector.d.ts +25 -0
  91. package/dist/shims/inspector.d.ts.map +1 -0
  92. package/dist/shims/module.d.ts +22 -0
  93. package/dist/shims/module.d.ts.map +1 -0
  94. package/dist/shims/net.d.ts +100 -0
  95. package/dist/shims/net.d.ts.map +1 -0
  96. package/dist/shims/os.d.ts +159 -0
  97. package/dist/shims/os.d.ts.map +1 -0
  98. package/dist/shims/path.d.ts +72 -0
  99. package/dist/shims/path.d.ts.map +1 -0
  100. package/dist/shims/perf_hooks.d.ts +50 -0
  101. package/dist/shims/perf_hooks.d.ts.map +1 -0
  102. package/dist/shims/process.d.ts +93 -0
  103. package/dist/shims/process.d.ts.map +1 -0
  104. package/dist/shims/querystring.d.ts +23 -0
  105. package/dist/shims/querystring.d.ts.map +1 -0
  106. package/dist/shims/readdirp.d.ts +52 -0
  107. package/dist/shims/readdirp.d.ts.map +1 -0
  108. package/dist/shims/readline.d.ts +62 -0
  109. package/dist/shims/readline.d.ts.map +1 -0
  110. package/dist/shims/rollup.d.ts +34 -0
  111. package/dist/shims/rollup.d.ts.map +1 -0
  112. package/dist/shims/sentry.d.ts +163 -0
  113. package/dist/shims/sentry.d.ts.map +1 -0
  114. package/dist/shims/stream.d.ts +181 -0
  115. package/dist/shims/stream.d.ts.map +1 -0
  116. package/dist/shims/tls.d.ts +53 -0
  117. package/dist/shims/tls.d.ts.map +1 -0
  118. package/dist/shims/tty.d.ts +30 -0
  119. package/dist/shims/tty.d.ts.map +1 -0
  120. package/dist/shims/url.d.ts +64 -0
  121. package/dist/shims/url.d.ts.map +1 -0
  122. package/dist/shims/util.d.ts +106 -0
  123. package/dist/shims/util.d.ts.map +1 -0
  124. package/dist/shims/v8.d.ts +73 -0
  125. package/dist/shims/v8.d.ts.map +1 -0
  126. package/dist/shims/vfs-adapter.d.ts +126 -0
  127. package/dist/shims/vfs-adapter.d.ts.map +1 -0
  128. package/dist/shims/vm.d.ts +45 -0
  129. package/dist/shims/vm.d.ts.map +1 -0
  130. package/dist/shims/worker_threads.d.ts +66 -0
  131. package/dist/shims/worker_threads.d.ts.map +1 -0
  132. package/dist/shims/ws.d.ts +66 -0
  133. package/dist/shims/ws.d.ts.map +1 -0
  134. package/dist/shims/zlib.d.ts +161 -0
  135. package/dist/shims/zlib.d.ts.map +1 -0
  136. package/dist/transform.d.ts +24 -0
  137. package/dist/transform.d.ts.map +1 -0
  138. package/dist/virtual-fs.d.ts +226 -0
  139. package/dist/virtual-fs.d.ts.map +1 -0
  140. package/dist/vite-demo.d.ts +35 -0
  141. package/dist/vite-demo.d.ts.map +1 -0
  142. package/dist/vite-sw.js +132 -0
  143. package/dist/worker/runtime-worker.d.ts +8 -0
  144. package/dist/worker/runtime-worker.d.ts.map +1 -0
  145. package/dist/worker-runtime.d.ts +50 -0
  146. package/dist/worker-runtime.d.ts.map +1 -0
  147. package/package.json +85 -0
  148. package/src/ai-chatbot-demo-entry.ts +244 -0
  149. package/src/ai-chatbot-demo.ts +509 -0
  150. package/src/convex-app-demo-entry.ts +1107 -0
  151. package/src/convex-app-demo.ts +1316 -0
  152. package/src/cors-proxy.ts +81 -0
  153. package/src/create-runtime.ts +147 -0
  154. package/src/demo.ts +304 -0
  155. package/src/dev-server.ts +274 -0
  156. package/src/frameworks/next-dev-server.ts +2224 -0
  157. package/src/frameworks/vite-dev-server.ts +702 -0
  158. package/src/index.ts +101 -0
  159. package/src/next-demo.ts +1784 -0
  160. package/src/npm/index.ts +347 -0
  161. package/src/npm/registry.ts +152 -0
  162. package/src/npm/resolver.ts +385 -0
  163. package/src/npm/tarball.ts +209 -0
  164. package/src/runtime-interface.ts +103 -0
  165. package/src/runtime.ts +1046 -0
  166. package/src/sandbox-helpers.ts +173 -0
  167. package/src/sandbox-runtime.ts +252 -0
  168. package/src/server-bridge.ts +426 -0
  169. package/src/shims/assert.ts +664 -0
  170. package/src/shims/async_hooks.ts +86 -0
  171. package/src/shims/buffer.ts +75 -0
  172. package/src/shims/child_process-browser.ts +217 -0
  173. package/src/shims/child_process.ts +463 -0
  174. package/src/shims/chokidar.ts +313 -0
  175. package/src/shims/cluster.ts +67 -0
  176. package/src/shims/crypto.ts +830 -0
  177. package/src/shims/dgram.ts +47 -0
  178. package/src/shims/diagnostics_channel.ts +196 -0
  179. package/src/shims/dns.ts +172 -0
  180. package/src/shims/domain.ts +58 -0
  181. package/src/shims/esbuild.ts +805 -0
  182. package/src/shims/events.ts +195 -0
  183. package/src/shims/fs.ts +803 -0
  184. package/src/shims/fsevents.ts +63 -0
  185. package/src/shims/http.ts +904 -0
  186. package/src/shims/http2.ts +96 -0
  187. package/src/shims/https.ts +86 -0
  188. package/src/shims/inspector.ts +30 -0
  189. package/src/shims/module.ts +82 -0
  190. package/src/shims/net.ts +359 -0
  191. package/src/shims/os.ts +195 -0
  192. package/src/shims/path.ts +199 -0
  193. package/src/shims/perf_hooks.ts +92 -0
  194. package/src/shims/process.ts +346 -0
  195. package/src/shims/querystring.ts +97 -0
  196. package/src/shims/readdirp.ts +228 -0
  197. package/src/shims/readline.ts +110 -0
  198. package/src/shims/rollup.ts +80 -0
  199. package/src/shims/sentry.ts +133 -0
  200. package/src/shims/stream.ts +1126 -0
  201. package/src/shims/tls.ts +95 -0
  202. package/src/shims/tty.ts +64 -0
  203. package/src/shims/url.ts +171 -0
  204. package/src/shims/util.ts +312 -0
  205. package/src/shims/v8.ts +113 -0
  206. package/src/shims/vfs-adapter.ts +402 -0
  207. package/src/shims/vm.ts +83 -0
  208. package/src/shims/worker_threads.ts +111 -0
  209. package/src/shims/ws.ts +382 -0
  210. package/src/shims/zlib.ts +289 -0
  211. package/src/transform.ts +313 -0
  212. package/src/types/external.d.ts +67 -0
  213. package/src/virtual-fs.ts +903 -0
  214. package/src/vite-demo.ts +577 -0
  215. package/src/worker/runtime-worker.ts +128 -0
  216. 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 };