@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.
Files changed (43) hide show
  1. package/package.json +5 -2
  2. package/src/commands/clone.ts +5 -5
  3. package/src/commands/down.ts +44 -69
  4. package/src/commands/link.ts +9 -6
  5. package/src/commands/new.ts +55 -25
  6. package/src/commands/publish.ts +8 -3
  7. package/src/commands/secrets.ts +2 -1
  8. package/src/commands/services.ts +41 -15
  9. package/src/commands/update.ts +2 -2
  10. package/src/index.ts +43 -2
  11. package/src/lib/agent-integration.ts +217 -0
  12. package/src/lib/auth/login-flow.ts +2 -1
  13. package/src/lib/binding-validator.ts +2 -3
  14. package/src/lib/build-helper.ts +7 -1
  15. package/src/lib/hooks.ts +101 -21
  16. package/src/lib/managed-down.ts +40 -42
  17. package/src/lib/project-detection.ts +48 -21
  18. package/src/lib/project-operations.ts +38 -44
  19. package/src/lib/prompts.ts +16 -23
  20. package/src/lib/services/db-execute.ts +39 -0
  21. package/src/lib/services/sql-classifier.test.ts +2 -2
  22. package/src/lib/services/sql-classifier.ts +5 -4
  23. package/src/lib/version-check.ts +15 -10
  24. package/src/lib/zip-packager.ts +16 -0
  25. package/src/mcp/resources/index.ts +42 -2
  26. package/src/templates/index.ts +63 -3
  27. package/templates/CLAUDE.md +117 -53
  28. package/templates/ai-chat/.jack.json +29 -0
  29. package/templates/ai-chat/bun.lock +18 -0
  30. package/templates/ai-chat/package.json +14 -0
  31. package/templates/ai-chat/public/chat.js +149 -0
  32. package/templates/ai-chat/public/index.html +209 -0
  33. package/templates/ai-chat/src/index.ts +105 -0
  34. package/templates/ai-chat/wrangler.jsonc +12 -0
  35. package/templates/semantic-search/.jack.json +26 -0
  36. package/templates/semantic-search/bun.lock +18 -0
  37. package/templates/semantic-search/package.json +12 -0
  38. package/templates/semantic-search/public/app.js +120 -0
  39. package/templates/semantic-search/public/index.html +210 -0
  40. package/templates/semantic-search/schema.sql +5 -0
  41. package/templates/semantic-search/src/index.ts +144 -0
  42. package/templates/semantic-search/tsconfig.json +13 -0
  43. package/templates/semantic-search/wrangler.jsonc +27 -0
@@ -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 (supports `validate: "json" | "accountAssociation"`) |
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 env | `{"action": "require", "source": "secret", "key": "API_KEY"}` |
149
- | `prompt` | Prompt for input and update JSON file | `{"action": "prompt", "message": "Paste JSON", "validate": "json", "successMessage": "Saved", "writeJson": {"path": "public/data.json", "set": {"data": {"from": "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
- ### Proposed Hook Extensions
211
+ ### Advanced Hook Features
209
212
 
210
- These extensions are planned to support more complex setup wizards (like SaaS templates with Stripe):
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
- Currently `require` fails if a secret is missing. This extension allows prompting the user instead:
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 (no change)
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
- #### 2. `shell` + `captureAs`
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": "shell",
239
- "command": "stripe listen --print-secret",
240
- "captureAs": "secret:STRIPE_WEBHOOK_SECRET",
241
- "message": "Starting Stripe webhook listener..."
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
- **Use cases:**
246
- - Capture Stripe CLI webhook signing secret
247
- - Capture generated API keys or tokens
248
- - Capture any CLI output needed for configuration
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
- **`captureAs` syntax:**
251
- - `secret:KEY_NAME` saves to `.secrets.json`
252
- - `var:NAME` saves to hook variables for later hooks
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` + `saveAs`
282
+ #### 3. `prompt` with `secret` Flag
255
283
 
256
- Currently `prompt` only writes to JSON files via `writeJson`. This extension allows saving input as a secret:
284
+ Mask sensitive input (like API keys):
257
285
 
258
286
  ```json
259
287
  {
260
288
  "action": "prompt",
261
- "message": "Enter your Stripe Webhook Secret (whsec_...):",
262
- "saveAs": "secret:STRIPE_WEBHOOK_SECRET",
263
- "validate": "startsWith:whsec_",
264
- "successMessage": "Webhook secret saved"
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
- **Difference from `require+onMissing`:**
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
- ### Design Principles for Hook Extensions
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. **Reusable primitives** - `captureAs` works on any action that produces output
278
- 3. **Consistent syntax** - `secret:KEY` pattern for writing to `.secrets.json`
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: Complex Setup Wizard
324
+ ### Example: SaaS Template Setup Wizard
282
325
 
283
- A SaaS template with Stripe might use these extensions:
326
+ The `saas` template uses `preCreate` hooks for a complete setup wizard:
284
327
 
285
328
  ```json
286
329
  {
287
330
  "hooks": {
288
- "preDeploy": [
289
- {"action": "require", "source": "secret", "key": "BETTER_AUTH_SECRET", "onMissing": "prompt", "promptMessage": "Enter a random secret (32+ chars):"},
290
- {"action": "require", "source": "secret", "key": "STRIPE_SECRET_KEY", "onMissing": "prompt", "promptMessage": "Enter Stripe Secret Key:", "setupUrl": "https://dashboard.stripe.com/apikeys"}
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": "Stripe Webhook Setup", "lines": ["1. Go to Stripe Dashboard → Webhooks", "2. Add endpoint: {{url}}/api/auth/stripe/webhook", "3. Select events: checkout.session.completed, customer.subscription.*"]},
294
- {"action": "url", "url": "https://dashboard.stripe.com/webhooks/create?endpoint_url={{url}}/api/auth/stripe/webhook", "label": "Create webhook"},
295
- {"action": "prompt", "message": "Paste webhook signing secret (whsec_...):", "saveAs": "secret:STRIPE_WEBHOOK_SECRET", "validate": "startsWith: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. Ensures auth secret exists (prompts if missing)
305
- 2. Ensures Stripe key exists (prompts if missing, with setup link)
306
- 3. Deploys the app
307
- 4. Guides user through webhook setup with direct link
308
- 5. Captures webhook secret
309
- 6. Re-deploys with complete configuration
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>