@getjack/jack 0.1.20 → 0.1.23
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 +5 -2
- package/src/commands/clone.ts +5 -5
- package/src/commands/down.ts +44 -69
- package/src/commands/link.ts +9 -6
- package/src/commands/new.ts +55 -25
- package/src/commands/publish.ts +8 -3
- package/src/commands/secrets.ts +2 -1
- package/src/commands/services.ts +41 -15
- package/src/commands/update.ts +2 -2
- package/src/index.ts +43 -2
- package/src/lib/agent-integration.ts +217 -0
- package/src/lib/auth/login-flow.ts +2 -1
- package/src/lib/binding-validator.ts +2 -3
- package/src/lib/build-helper.ts +7 -1
- package/src/lib/hooks.ts +101 -21
- package/src/lib/managed-down.ts +40 -42
- package/src/lib/project-detection.ts +48 -21
- package/src/lib/project-operations.ts +38 -44
- package/src/lib/prompts.ts +16 -23
- package/src/lib/services/db-execute.ts +39 -0
- package/src/lib/services/sql-classifier.test.ts +2 -2
- package/src/lib/services/sql-classifier.ts +5 -4
- package/src/lib/version-check.ts +15 -10
- package/src/lib/zip-packager.ts +16 -0
- package/src/mcp/resources/index.ts +42 -2
- package/src/templates/index.ts +63 -3
- package/templates/CLAUDE.md +117 -53
- package/templates/ai-chat/.jack.json +29 -0
- package/templates/ai-chat/bun.lock +18 -0
- package/templates/ai-chat/package.json +14 -0
- package/templates/ai-chat/public/chat.js +149 -0
- package/templates/ai-chat/public/index.html +209 -0
- package/templates/ai-chat/src/index.ts +105 -0
- package/templates/ai-chat/wrangler.jsonc +12 -0
- package/templates/semantic-search/.jack.json +26 -0
- package/templates/semantic-search/bun.lock +18 -0
- package/templates/semantic-search/package.json +12 -0
- package/templates/semantic-search/public/app.js +120 -0
- package/templates/semantic-search/public/index.html +210 -0
- package/templates/semantic-search/schema.sql +5 -0
- package/templates/semantic-search/src/index.ts +144 -0
- package/templates/semantic-search/tsconfig.json +13 -0
- package/templates/semantic-search/wrangler.jsonc +27 -0
package/templates/CLAUDE.md
CHANGED
|
@@ -119,16 +119,18 @@ Templates can define hooks in `.jack.json` that run at specific lifecycle points
|
|
|
119
119
|
| `url` | `url` | Prints label + URL |
|
|
120
120
|
| `clipboard` | `text` | Prints text |
|
|
121
121
|
| `pause` | _(none)_ | Skipped |
|
|
122
|
-
| `require` | `source`, `key` | Validates, prints setup if provided |
|
|
122
|
+
| `require` | `source`, `key` | Validates, prints setup if provided. Supports `onMissing: "prompt" \| "generate"` |
|
|
123
123
|
| `shell` | `command` | Runs with stdin ignored |
|
|
124
|
-
| `prompt` | `message` | Skipped
|
|
124
|
+
| `prompt` | `message` | Skipped. Supports `secret: true` for masked input, `validate`, `writeJson`, `deployAfter` |
|
|
125
125
|
| `writeJson` | `path`, `set` | Runs (safe in CI) |
|
|
126
|
+
| `stripe-setup` | `plans` | Creates Stripe products/prices, saves price IDs to secrets |
|
|
126
127
|
|
|
127
128
|
### Hook Lifecycle
|
|
128
129
|
|
|
129
130
|
```json
|
|
130
131
|
{
|
|
131
132
|
"hooks": {
|
|
133
|
+
"preCreate": [...], // During project creation (secret collection, auto-generation)
|
|
132
134
|
"preDeploy": [...], // Before wrangler deploy (validation)
|
|
133
135
|
"postDeploy": [...] // After successful deploy (notifications, testing)
|
|
134
136
|
}
|
|
@@ -145,9 +147,10 @@ Templates can define hooks in `.jack.json` that run at specific lifecycle points
|
|
|
145
147
|
| `clipboard` | Copy text to clipboard | `{"action": "clipboard", "text": "{{url}}", "message": "Copied!"}` |
|
|
146
148
|
| `shell` | Execute shell command | `{"action": "shell", "command": "curl {{url}}/health"}` |
|
|
147
149
|
| `pause` | Wait for Enter key | `{"action": "pause", "message": "Press Enter..."}` |
|
|
148
|
-
| `require` | Verify secret or
|
|
149
|
-
| `prompt` | Prompt for input
|
|
150
|
+
| `require` | Verify secret/env, optionally prompt or generate | `{"action": "require", "source": "secret", "key": "API_KEY", "onMissing": "prompt"}` |
|
|
151
|
+
| `prompt` | Prompt for input, optionally masked | `{"action": "prompt", "message": "Secret:", "secret": true, "writeJson": {...}}` |
|
|
150
152
|
| `writeJson` | Update JSON file with template vars | `{"action": "writeJson", "path": "public/data.json", "set": {"siteUrl": "{{url}}"}}` |
|
|
153
|
+
| `stripe-setup` | Create Stripe products/prices | `{"action": "stripe-setup", "plans": [{"name": "Pro", "priceKey": "STRIPE_PRO_PRICE_ID", "amount": 1900, "interval": "month"}]}` |
|
|
151
154
|
|
|
152
155
|
### Non-Interactive Mode
|
|
153
156
|
|
|
@@ -169,7 +172,7 @@ These variables are substituted at runtime (different from template placeholders
|
|
|
169
172
|
|----------|-------|--------------|
|
|
170
173
|
| `{{url}}` | Full deployed URL | postDeploy |
|
|
171
174
|
| `{{domain}}` | Domain without protocol | postDeploy |
|
|
172
|
-
| `{{name}}` | Project name | preDeploy, postDeploy |
|
|
175
|
+
| `{{name}}` | Project name | preCreate, preDeploy, postDeploy |
|
|
173
176
|
|
|
174
177
|
### Example: API Template Hooks
|
|
175
178
|
|
|
@@ -205,13 +208,13 @@ These variables are substituted at runtime (different from template placeholders
|
|
|
205
208
|
}
|
|
206
209
|
```
|
|
207
210
|
|
|
208
|
-
###
|
|
211
|
+
### Advanced Hook Features
|
|
209
212
|
|
|
210
|
-
These
|
|
213
|
+
These features support complex setup wizards (like the SaaS template with Stripe):
|
|
211
214
|
|
|
212
|
-
#### 1. `require` + `onMissing: "prompt"`
|
|
215
|
+
#### 1. `require` + `onMissing: "prompt" | "generate"`
|
|
213
216
|
|
|
214
|
-
|
|
217
|
+
The `require` action supports automatic secret collection when a secret is missing:
|
|
215
218
|
|
|
216
219
|
```json
|
|
217
220
|
{
|
|
@@ -225,88 +228,149 @@ Currently `require` fails if a secret is missing. This extension allows promptin
|
|
|
225
228
|
```
|
|
226
229
|
|
|
227
230
|
**Behavior:**
|
|
228
|
-
- If secret exists → continue (
|
|
231
|
+
- If secret exists → continue (shows "Using saved KEY")
|
|
229
232
|
- If secret missing + interactive → prompt user, save to `.secrets.json`
|
|
230
233
|
- If secret missing + non-interactive → fail with setup instructions
|
|
231
234
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
Run a command and save its output as a secret or variable:
|
|
235
|
+
**Auto-generate secrets with `onMissing: "generate"`:**
|
|
235
236
|
|
|
236
237
|
```json
|
|
237
238
|
{
|
|
238
|
-
"action": "
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
"message": "
|
|
239
|
+
"action": "require",
|
|
240
|
+
"source": "secret",
|
|
241
|
+
"key": "BETTER_AUTH_SECRET",
|
|
242
|
+
"message": "Generating authentication secret...",
|
|
243
|
+
"onMissing": "generate",
|
|
244
|
+
"generateCommand": "openssl rand -base64 32"
|
|
242
245
|
}
|
|
243
246
|
```
|
|
244
247
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
This runs the command, captures stdout, and saves it as the secret automatically.
|
|
249
|
+
|
|
250
|
+
#### 2. `stripe-setup` Action
|
|
251
|
+
|
|
252
|
+
Automatically creates Stripe products and prices, saving the price IDs as secrets:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"action": "stripe-setup",
|
|
257
|
+
"message": "Setting up Stripe subscription plans...",
|
|
258
|
+
"plans": [
|
|
259
|
+
{
|
|
260
|
+
"name": "Pro",
|
|
261
|
+
"priceKey": "STRIPE_PRO_PRICE_ID",
|
|
262
|
+
"amount": 1900,
|
|
263
|
+
"interval": "month",
|
|
264
|
+
"description": "Pro monthly subscription"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"name": "Enterprise",
|
|
268
|
+
"priceKey": "STRIPE_ENTERPRISE_PRICE_ID",
|
|
269
|
+
"amount": 9900,
|
|
270
|
+
"interval": "month"
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
```
|
|
249
275
|
|
|
250
|
-
|
|
251
|
-
- `
|
|
252
|
-
-
|
|
276
|
+
**Behavior:**
|
|
277
|
+
- Requires `STRIPE_SECRET_KEY` to be set first
|
|
278
|
+
- Checks for existing prices by lookup key (`jack_pro_month`)
|
|
279
|
+
- Creates product + price if not found
|
|
280
|
+
- Saves price IDs to secrets
|
|
253
281
|
|
|
254
|
-
#### 3. `prompt`
|
|
282
|
+
#### 3. `prompt` with `secret` Flag
|
|
255
283
|
|
|
256
|
-
|
|
284
|
+
Mask sensitive input (like API keys):
|
|
257
285
|
|
|
258
286
|
```json
|
|
259
287
|
{
|
|
260
288
|
"action": "prompt",
|
|
261
|
-
"message": "
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
|
|
289
|
+
"message": "Paste your webhook signing secret (whsec_...):",
|
|
290
|
+
"secret": true,
|
|
291
|
+
"writeJson": {
|
|
292
|
+
"path": ".secrets.json",
|
|
293
|
+
"set": { "STRIPE_WEBHOOK_SECRET": { "from": "input" } }
|
|
294
|
+
}
|
|
265
295
|
}
|
|
266
296
|
```
|
|
267
297
|
|
|
268
|
-
|
|
269
|
-
- `require+onMissing` checks first, prompts only if missing
|
|
270
|
-
- `prompt+saveAs` always prompts (for update flows or explicit input)
|
|
298
|
+
#### 4. `prompt` with `deployAfter`
|
|
271
299
|
|
|
272
|
-
|
|
300
|
+
Automatically redeploy after user provides input:
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"action": "prompt",
|
|
305
|
+
"message": "Paste webhook signing secret:",
|
|
306
|
+
"secret": true,
|
|
307
|
+
"deployAfter": true,
|
|
308
|
+
"deployMessage": "Deploying with webhook support...",
|
|
309
|
+
"writeJson": {
|
|
310
|
+
"path": ".secrets.json",
|
|
311
|
+
"set": { "STRIPE_WEBHOOK_SECRET": { "from": "input" } }
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Design Principles
|
|
273
317
|
|
|
274
318
|
When extending the hook system:
|
|
275
319
|
|
|
276
320
|
1. **Extend existing actions** - prefer `require+onMissing` over a new `requireOrPrompt` action
|
|
277
|
-
2. **
|
|
278
|
-
3. **
|
|
279
|
-
4. **Non-interactive fallback** - every interactive feature must degrade gracefully in CI/MCP
|
|
321
|
+
2. **Non-interactive fallback** - every interactive feature must degrade gracefully in CI/MCP
|
|
322
|
+
3. **Secrets via `.secrets.json`** - use `writeJson` with `.secrets.json` for secret storage
|
|
280
323
|
|
|
281
|
-
### Example:
|
|
324
|
+
### Example: SaaS Template Setup Wizard
|
|
282
325
|
|
|
283
|
-
|
|
326
|
+
The `saas` template uses `preCreate` hooks for a complete setup wizard:
|
|
284
327
|
|
|
285
328
|
```json
|
|
286
329
|
{
|
|
287
330
|
"hooks": {
|
|
288
|
-
"
|
|
289
|
-
{
|
|
290
|
-
|
|
331
|
+
"preCreate": [
|
|
332
|
+
{
|
|
333
|
+
"action": "require",
|
|
334
|
+
"source": "secret",
|
|
335
|
+
"key": "STRIPE_SECRET_KEY",
|
|
336
|
+
"message": "Stripe API key required for payments",
|
|
337
|
+
"setupUrl": "https://dashboard.stripe.com/apikeys",
|
|
338
|
+
"onMissing": "prompt",
|
|
339
|
+
"promptMessage": "Enter your Stripe Secret Key (sk_test_... or sk_live_...):"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"action": "require",
|
|
343
|
+
"source": "secret",
|
|
344
|
+
"key": "BETTER_AUTH_SECRET",
|
|
345
|
+
"message": "Generating authentication secret...",
|
|
346
|
+
"onMissing": "generate",
|
|
347
|
+
"generateCommand": "openssl rand -base64 32"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"action": "stripe-setup",
|
|
351
|
+
"message": "Setting up Stripe subscription plans...",
|
|
352
|
+
"plans": [
|
|
353
|
+
{"name": "Pro", "priceKey": "STRIPE_PRO_PRICE_ID", "amount": 1900, "interval": "month"},
|
|
354
|
+
{"name": "Enterprise", "priceKey": "STRIPE_ENTERPRISE_PRICE_ID", "amount": 9900, "interval": "month"}
|
|
355
|
+
]
|
|
356
|
+
}
|
|
291
357
|
],
|
|
292
358
|
"postDeploy": [
|
|
293
|
-
{"action": "box", "title": "
|
|
294
|
-
{"action": "
|
|
295
|
-
{"action": "prompt", "message": "Paste webhook signing secret (whsec_...):", "
|
|
296
|
-
{"action": "message", "text": "Re-deploying with webhook secret..."},
|
|
297
|
-
{"action": "shell", "command": "jack ship --quiet"}
|
|
359
|
+
{"action": "box", "title": "Your SaaS is live!", "lines": ["{{url}}"]},
|
|
360
|
+
{"action": "clipboard", "text": "{{url}}/api/auth/stripe/webhook", "message": "Webhook URL copied"},
|
|
361
|
+
{"action": "prompt", "message": "Paste your webhook signing secret (whsec_...):", "secret": true, "deployAfter": true, "writeJson": {"path": ".secrets.json", "set": {"STRIPE_WEBHOOK_SECRET": {"from": "input"}}}}
|
|
298
362
|
]
|
|
299
363
|
}
|
|
300
364
|
}
|
|
301
365
|
```
|
|
302
366
|
|
|
303
367
|
This creates a guided wizard that:
|
|
304
|
-
1.
|
|
305
|
-
2.
|
|
306
|
-
3.
|
|
307
|
-
4.
|
|
308
|
-
5.
|
|
309
|
-
6. Re-deploys with
|
|
368
|
+
1. Prompts for Stripe key (with setup URL)
|
|
369
|
+
2. Auto-generates auth secret
|
|
370
|
+
3. Creates Stripe products/prices automatically
|
|
371
|
+
4. Deploys the app
|
|
372
|
+
5. Guides through webhook setup
|
|
373
|
+
6. Re-deploys with webhook secret
|
|
310
374
|
|
|
311
375
|
## Farcaster Miniapp Embeds
|
|
312
376
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-chat",
|
|
3
|
+
"description": "AI chat with streaming UI (free Cloudflare AI)",
|
|
4
|
+
"secrets": [],
|
|
5
|
+
"capabilities": ["ai"],
|
|
6
|
+
"intent": {
|
|
7
|
+
"keywords": ["ai", "chat", "llm", "mistral", "completion", "chatbot"],
|
|
8
|
+
"examples": ["AI chatbot", "chat interface", "LLM chat app"]
|
|
9
|
+
},
|
|
10
|
+
"hooks": {
|
|
11
|
+
"postDeploy": [
|
|
12
|
+
{
|
|
13
|
+
"action": "clipboard",
|
|
14
|
+
"text": "{{url}}",
|
|
15
|
+
"message": "URL copied to clipboard"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"action": "box",
|
|
19
|
+
"title": "AI Chat: {{name}}",
|
|
20
|
+
"lines": [
|
|
21
|
+
"{{url}}",
|
|
22
|
+
"",
|
|
23
|
+
"Open in browser to start chatting!",
|
|
24
|
+
"Rate limit: 10 requests/minute"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "jack-template",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
9
|
+
"typescript": "^5.0.0",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
"packages": {
|
|
14
|
+
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260120.0", "", {}, "sha512-B8pueG+a5S+mdK3z8oKu1ShcxloZ7qWb68IEyLLaepvdryIbNC7JVPcY0bWsjS56UQVKc5fnyRge3yZIwc9bxw=="],
|
|
15
|
+
|
|
16
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jack-template",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler deploy"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
12
|
+
"typescript": "^5.0.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const messagesEl = document.getElementById('messages');
|
|
2
|
+
const inputEl = document.getElementById('input');
|
|
3
|
+
const sendBtn = document.getElementById('send');
|
|
4
|
+
|
|
5
|
+
let history = [];
|
|
6
|
+
let isLoading = false;
|
|
7
|
+
|
|
8
|
+
function setLoading(loading) {
|
|
9
|
+
isLoading = loading;
|
|
10
|
+
inputEl.disabled = loading;
|
|
11
|
+
sendBtn.disabled = loading;
|
|
12
|
+
sendBtn.textContent = loading ? '...' : 'Send';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function clearEmptyState() {
|
|
16
|
+
const emptyState = messagesEl.querySelector('.empty-state');
|
|
17
|
+
if (emptyState) {
|
|
18
|
+
emptyState.remove();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function appendMessage(role, content, className = '') {
|
|
23
|
+
clearEmptyState();
|
|
24
|
+
const el = document.createElement('div');
|
|
25
|
+
el.className = `message ${role} ${className}`.trim();
|
|
26
|
+
el.textContent = content;
|
|
27
|
+
messagesEl.appendChild(el);
|
|
28
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
29
|
+
return el;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function sendMessage() {
|
|
33
|
+
const content = inputEl.value.trim();
|
|
34
|
+
if (!content || isLoading) return;
|
|
35
|
+
|
|
36
|
+
// Add user message to history and display
|
|
37
|
+
history.push({ role: 'user', content });
|
|
38
|
+
appendMessage('user', content);
|
|
39
|
+
inputEl.value = '';
|
|
40
|
+
|
|
41
|
+
// Create assistant message placeholder
|
|
42
|
+
const assistantEl = appendMessage('assistant', '', 'typing');
|
|
43
|
+
setLoading(true);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch('/api/chat', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({ messages: history }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
let errorMessage = 'Something went wrong. Please try again.';
|
|
54
|
+
try {
|
|
55
|
+
const err = await response.json();
|
|
56
|
+
if (err.error) {
|
|
57
|
+
errorMessage = err.error;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Use default error message
|
|
61
|
+
}
|
|
62
|
+
assistantEl.textContent = errorMessage;
|
|
63
|
+
assistantEl.className = 'message assistant error';
|
|
64
|
+
setLoading(false);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Stream response
|
|
69
|
+
const reader = response.body.getReader();
|
|
70
|
+
const decoder = new TextDecoder();
|
|
71
|
+
let assistantContent = '';
|
|
72
|
+
let buffer = '';
|
|
73
|
+
|
|
74
|
+
while (true) {
|
|
75
|
+
const { done, value } = await reader.read();
|
|
76
|
+
if (done) break;
|
|
77
|
+
|
|
78
|
+
buffer += decoder.decode(value, { stream: true });
|
|
79
|
+
|
|
80
|
+
// Process complete SSE messages
|
|
81
|
+
const lines = buffer.split('\n');
|
|
82
|
+
// Keep the last potentially incomplete line in the buffer
|
|
83
|
+
buffer = lines.pop() || '';
|
|
84
|
+
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (line.startsWith('data: ')) {
|
|
87
|
+
const data = line.slice(6).trim();
|
|
88
|
+
if (data === '[DONE]') continue;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(data);
|
|
92
|
+
if (parsed.response) {
|
|
93
|
+
assistantContent += parsed.response;
|
|
94
|
+
assistantEl.textContent = assistantContent;
|
|
95
|
+
assistantEl.className = 'message assistant';
|
|
96
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Skip malformed JSON chunks
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Process any remaining buffer content
|
|
106
|
+
if (buffer.startsWith('data: ')) {
|
|
107
|
+
const data = buffer.slice(6).trim();
|
|
108
|
+
if (data && data !== '[DONE]') {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(data);
|
|
111
|
+
if (parsed.response) {
|
|
112
|
+
assistantContent += parsed.response;
|
|
113
|
+
assistantEl.textContent = assistantContent;
|
|
114
|
+
assistantEl.className = 'message assistant';
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// Skip malformed JSON
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Save to history if we got content
|
|
123
|
+
if (assistantContent) {
|
|
124
|
+
history.push({ role: 'assistant', content: assistantContent });
|
|
125
|
+
} else {
|
|
126
|
+
assistantEl.textContent = 'No response received. Please try again.';
|
|
127
|
+
assistantEl.className = 'message assistant error';
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error('Chat error:', err);
|
|
131
|
+
assistantEl.textContent = 'Connection error. Please check your network and try again.';
|
|
132
|
+
assistantEl.className = 'message assistant error';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setLoading(false);
|
|
136
|
+
inputEl.focus();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Event listeners
|
|
140
|
+
sendBtn.addEventListener('click', sendMessage);
|
|
141
|
+
inputEl.addEventListener('keypress', (e) => {
|
|
142
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
sendMessage();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Focus input on load
|
|
149
|
+
inputEl.focus();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>AI Chat</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
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
|
+
}
|
|
180
|
+
</style>
|
|
181
|
+
</head>
|
|
182
|
+
<body>
|
|
183
|
+
<div class="chat-container">
|
|
184
|
+
<header>
|
|
185
|
+
<h1>AI Chat</h1>
|
|
186
|
+
<p>Powered by Cloudflare AI (Mistral 7B)</p>
|
|
187
|
+
</header>
|
|
188
|
+
|
|
189
|
+
<div id="messages" class="messages">
|
|
190
|
+
<div class="empty-state">
|
|
191
|
+
<p>Start a conversation</p>
|
|
192
|
+
<small>Type a message below to begin</small>
|
|
193
|
+
</div>
|
|
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>
|
|
208
|
+
</body>
|
|
209
|
+
</html>
|