create-theokit 1.0.3 → 1.0.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-theokit",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "description": "Scaffold a new TheoKit project",
6
6
  "license": "Apache-2.0",
@@ -204,3 +204,93 @@ tr.done td { opacity: 0.45; text-decoration: line-through; }
204
204
  @keyframes spin {
205
205
  to { transform: rotate(360deg); }
206
206
  }
207
+
208
+ /* ─── Hero ──────────────────────────────────────────── */
209
+
210
+ .hero {
211
+ text-align: center;
212
+ padding: 48px 24px 32px;
213
+ }
214
+
215
+ .hero img {
216
+ margin-bottom: 16px;
217
+ border-radius: 16px;
218
+ }
219
+
220
+ .hero h1 {
221
+ font-size: 2.5rem;
222
+ font-weight: 800;
223
+ letter-spacing: -0.03em;
224
+ margin-bottom: 4px;
225
+ }
226
+
227
+ .hero .tagline {
228
+ color: var(--text-secondary);
229
+ font-size: 1.1rem;
230
+ margin-bottom: 24px;
231
+ }
232
+
233
+ .hero-links {
234
+ display: flex;
235
+ justify-content: center;
236
+ gap: 12px;
237
+ margin-bottom: 24px;
238
+ }
239
+
240
+ .btn {
241
+ display: inline-flex;
242
+ align-items: center;
243
+ padding: 10px 24px;
244
+ border-radius: 99px;
245
+ font-weight: 600;
246
+ font-size: 0.9rem;
247
+ text-decoration: none;
248
+ transition: background 0.2s, border-color 0.2s;
249
+ }
250
+
251
+ .btn.primary {
252
+ background: var(--accent);
253
+ color: white;
254
+ }
255
+
256
+ .btn.secondary {
257
+ background: transparent;
258
+ border: 1px solid var(--border);
259
+ color: var(--text);
260
+ }
261
+
262
+ @media (hover: hover) and (pointer: fine) {
263
+ .btn.primary:hover { background: var(--accent-hover); }
264
+ .btn.secondary:hover { background: var(--bg-secondary); }
265
+ }
266
+
267
+ .hero .hint {
268
+ color: var(--text-muted);
269
+ font-size: 0.8rem;
270
+ }
271
+
272
+ .hero .hint code {
273
+ font-family: var(--font-mono);
274
+ background: var(--bg-secondary);
275
+ padding: 2px 6px;
276
+ border-radius: 4px;
277
+ font-size: 0.75rem;
278
+ }
279
+
280
+ /* ─── Footer ────────────────────────────────────────── */
281
+
282
+ .footer {
283
+ text-align: center;
284
+ padding: 32px 24px;
285
+ color: var(--text-muted);
286
+ font-size: 0.8rem;
287
+ }
288
+
289
+ .footer a {
290
+ color: var(--accent);
291
+ text-decoration: none;
292
+ }
293
+
294
+ .footer a:hover {
295
+ text-decoration: underline;
296
+ }
@@ -2,8 +2,10 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef, type FormEvent } from 'react'
4
4
 
5
+ // ── Types ──
6
+
5
7
  interface Task {
6
- id: string
8
+ id: number
7
9
  title: string
8
10
  priority: 'high' | 'medium' | 'low'
9
11
  done: boolean
@@ -11,261 +13,147 @@ interface Task {
11
13
 
12
14
  type Role = '' | 'user' | 'admin'
13
15
 
14
- interface ChatMessage {
15
- type: 'user' | 'agent' | 'tool' | 'system' | 'error'
16
- content: string
16
+ interface ChatMsg {
17
+ role: 'user' | 'agent' | 'tool' | 'system' | 'error'
18
+ text: string
17
19
  }
18
20
 
19
- function escapeHtml(str: string): string {
20
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
21
- }
21
+ // ── Page ──
22
22
 
23
23
  export default function Page() {
24
24
  const [tasks, setTasks] = useState<Task[]>([])
25
25
  const [role, setRole] = useState<Role>('user')
26
- const [newTitle, setNewTitle] = useState('')
27
- const [newPriority, setNewPriority] = useState<'high' | 'medium' | 'low'>('medium')
26
+ const [title, setTitle] = useState('')
27
+ const [priority, setPriority] = useState<Task['priority']>('medium')
28
28
  const [formError, setFormError] = useState('')
29
-
30
- const [messages, setMessages] = useState<ChatMessage[]>([
31
- { type: 'system', content: 'Ask me to list, create, or complete tasks...' },
32
- ])
29
+ const [chat, setChat] = useState<ChatMsg[]>([{ role: 'system', text: 'Ask me to list, create, or complete tasks...' }])
33
30
  const [chatInput, setChatInput] = useState('')
34
- const [chatSending, setChatSending] = useState(false)
35
- const [chatCost, setChatCost] = useState('')
36
- const chatBoxRef = useRef<HTMLDivElement>(null)
37
- const sessionId = useRef(`session-${Date.now()}`)
31
+ const [chatBusy, setChatBusy] = useState(false)
32
+ const chatRef = useRef<HTMLDivElement>(null)
38
33
 
39
- const headers = useCallback(() => {
34
+ const headers = useCallback((): Record<string, string> => {
40
35
  const h: Record<string, string> = { 'Content-Type': 'application/json' }
41
36
  if (role) h['x-role'] = role
42
37
  return h
43
38
  }, [role])
44
39
 
40
+ // Fetch tasks on mount + when role changes
45
41
  const loadTasks = useCallback(async () => {
46
- try {
47
- const res = await fetch('/api/tasks')
48
- if (res.ok) {
49
- const data = await res.json()
50
- setTasks(data)
51
- }
52
- } catch {
53
- // Network error — silently ignore on initial load
54
- }
42
+ const res = await fetch('/api/tasks')
43
+ if (res.ok) setTasks(await res.json())
55
44
  }, [])
56
45
 
57
- useEffect(() => {
58
- loadTasks()
59
- }, [loadTasks])
60
-
61
- useEffect(() => {
62
- if (chatBoxRef.current) {
63
- chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight
64
- }
65
- }, [messages])
46
+ useEffect(() => { loadTasks() }, [loadTasks])
66
47
 
67
- const handleCreateTask = async (e: FormEvent) => {
48
+ // Create task
49
+ const createTask = async (e: FormEvent) => {
68
50
  e.preventDefault()
69
51
  setFormError('')
70
- const title = newTitle.trim()
71
- if (!title) return
72
-
73
- const res = await fetch('/api/tasks', {
74
- method: 'POST',
75
- headers: headers(),
76
- body: JSON.stringify({ title, priority: newPriority }),
77
- })
78
-
79
- if (res.status === 403) {
80
- setFormError('403 — Need User role')
81
- return
82
- }
83
-
84
- if (!res.ok) {
85
- const data = await res.json()
86
- setFormError(data.error?.issues?.[0]?.message ?? `Error ${res.status}`)
87
- return
88
- }
89
-
90
- setNewTitle('')
52
+ if (!title.trim()) return
53
+ const res = await fetch('/api/tasks', { method: 'POST', headers: headers(), body: JSON.stringify({ title, priority }) })
54
+ if (res.status === 403) { setFormError('403 — Need User role'); return }
55
+ if (!res.ok) { const b = await res.json(); setFormError(b.error?.issues?.[0]?.message ?? `Error ${res.status}`); return }
56
+ setTitle('')
91
57
  loadTasks()
92
58
  }
93
59
 
94
- const handleSendChat = async () => {
60
+ // AI Chat
61
+ const sendChat = async () => {
95
62
  const msg = chatInput.trim()
96
- if (!msg || chatSending) return
63
+ if (!msg || chatBusy) return
97
64
  setChatInput('')
98
- setChatSending(true)
99
-
100
- setMessages((prev) => [...prev, { type: 'user', content: `You: ${msg}` }])
65
+ setChat(c => [...c, { role: 'user', text: msg }])
66
+ setChatBusy(true)
101
67
 
102
68
  try {
103
69
  const res = await fetch('/api/agents/assistant/chat', {
104
- method: 'POST',
105
- headers: headers(),
106
- body: JSON.stringify({ message: msg, sessionId: sessionId.current }),
70
+ method: 'POST', headers: headers(),
71
+ body: JSON.stringify({ message: msg, sessionId: 'session-' + Date.now() }),
107
72
  })
108
-
109
- if (res.status === 403) {
110
- setMessages((prev) => [
111
- ...prev,
112
- { type: 'system', content: '403 — Need User role to chat' },
113
- ])
114
- setChatSending(false)
115
- return
116
- }
73
+ if (res.status === 403) { setChat(c => [...c, { role: 'error', text: '403 — Need User role' }]); return }
117
74
 
118
75
  const reader = res.body?.getReader()
119
- if (!reader) {
120
- setChatSending(false)
121
- return
122
- }
123
-
76
+ if (!reader) return
124
77
  const decoder = new TextDecoder()
125
- let buf = ''
126
- let agentText = ''
127
-
128
- setMessages((prev) => [...prev, { type: 'agent', content: '' }])
78
+ let buf = '', agentText = ''
129
79
 
130
80
  while (true) {
131
81
  const { done, value } = await reader.read()
132
82
  if (done) break
133
-
134
83
  buf += decoder.decode(value, { stream: true })
135
- const lines = buf.split('\n')
136
- buf = lines.pop() ?? ''
137
-
84
+ const lines = buf.split('\n'); buf = lines.pop() ?? ''
138
85
  for (const line of lines) {
139
86
  if (!line.startsWith('data: ')) continue
140
87
  try {
141
88
  const ev = JSON.parse(line.slice(6))
142
- if (ev.type === 'text_delta') {
143
- agentText += ev.content
144
- setMessages((prev) => {
145
- const updated = [...prev]
146
- updated[updated.length - 1] = { type: 'agent', content: agentText }
147
- return updated
148
- })
149
- } else if (ev.type === 'tool_call') {
150
- setMessages((prev) => {
151
- const inserted = [...prev]
152
- inserted.splice(inserted.length - 1, 0, {
153
- type: 'tool',
154
- content: `\uD83D\uDD27 ${ev.toolName}`,
155
- })
156
- return inserted
157
- })
158
- } else if (ev.type === 'tool_result') {
159
- setMessages((prev) => {
160
- const inserted = [...prev]
161
- inserted.splice(inserted.length - 1, 0, {
162
- type: 'tool',
163
- content: `\u2705 ${(ev.output ?? '').substring(0, 80)}`,
164
- })
165
- return inserted
166
- })
167
- } else if (ev.type === 'thinking') {
168
- setMessages((prev) => {
169
- const inserted = [...prev]
170
- inserted.splice(inserted.length - 1, 0, {
171
- type: 'system',
172
- content: `\uD83D\uDCAD ${ev.content}`,
173
- })
174
- return inserted
175
- })
176
- } else if (ev.type === 'done') {
177
- const tokens = ev.usage?.totalTokens ?? 0
178
- const costStr = ev.cost ? ` \u00B7 $${ev.cost.toFixed(6)}` : ''
179
- setChatCost(`${tokens} tokens \u00B7 ${ev.durationMs}ms${costStr}`)
180
- } else if (ev.type === 'error') {
181
- setMessages((prev) => [...prev, { type: 'error', content: ev.message }])
182
- }
183
- } catch {
184
- /* partial JSON — skip */
185
- }
89
+ if (ev.type === 'text_delta') agentText += ev.content
90
+ else if (ev.type === 'tool_call') setChat(c => [...c, { role: 'tool', text: `🔧 ${ev.toolName}` }])
91
+ else if (ev.type === 'tool_result') setChat(c => [...c, { role: 'tool', text: `✅ ${(ev.output ?? '').slice(0, 80)}` }])
92
+ else if (ev.type === 'error') setChat(c => [...c, { role: 'error', text: ev.message }])
93
+ } catch { /* partial JSON */ }
186
94
  }
187
95
  }
188
-
96
+ if (agentText) setChat(c => [...c, { role: 'agent', text: agentText }])
189
97
  loadTasks()
190
98
  } catch (err) {
191
- const message = err instanceof Error ? err.message : String(err)
192
- setMessages((prev) => [...prev, { type: 'error', content: `Error: ${message}` }])
99
+ setChat(c => [...c, { role: 'error', text: `Error: ${err instanceof Error ? err.message : String(err)}` }])
100
+ } finally {
101
+ setChatBusy(false)
193
102
  }
194
-
195
- setChatSending(false)
196
103
  }
197
104
 
105
+ useEffect(() => { chatRef.current?.scrollTo(0, chatRef.current.scrollHeight) }, [chat])
106
+
198
107
  return (
199
- <div className="container">
200
- <header>
201
- <h1>
202
- <span className="accent">TheoKit</span> Task Manager
203
- </h1>
204
- <p className="subtitle">
205
- HTTP Controllers + AI Agent — same guards, distinct pipelines. Edit <code>server/</code>{' '}
206
- to get started.
108
+ <>
109
+ {/* Hero */}
110
+ <header className="hero">
111
+ <img src="/logo.png" alt="TheoKit" width={80} height={80} />
112
+ <h1>TheoKit</h1>
113
+ <p className="tagline">Build the app your agent lives in.</p>
114
+ <nav className="hero-links">
115
+ <a href="https://usetheo.dev" target="_blank" rel="noopener noreferrer" className="btn primary">
116
+ usetheo.dev
117
+ </a>
118
+ <a href="https://github.com/usetheodev/theokit" target="_blank" rel="noopener noreferrer" className="btn secondary">
119
+ Documentation
120
+ </a>
121
+ </nav>
122
+ <p className="hint">
123
+ Edit <code>app/page.tsx</code> to get started. Changes hot-reload instantly.
207
124
  </p>
208
- <div className="role-bar">
209
- <label htmlFor="role">Role:</label>
210
- <select id="role" value={role} onChange={(e) => setRole(e.target.value as Role)}>
211
- <option value="">None (public only)</option>
212
- <option value="user">User</option>
213
- <option value="admin">Admin</option>
214
- </select>
215
- </div>
216
125
  </header>
217
126
 
127
+ {/* Role selector */}
128
+ <div className="role-bar">
129
+ <label htmlFor="role">Role:</label>
130
+ <select id="role" value={role} onChange={e => setRole(e.target.value as Role)}>
131
+ <option value="">None (public only)</option>
132
+ <option value="user">User</option>
133
+ <option value="admin">Admin</option>
134
+ </select>
135
+ </div>
136
+
137
+ {/* Main grid */}
218
138
  <main className="grid">
219
- {/* Left: CRUD Panel */}
139
+ {/* Tasks CRUD */}
220
140
  <section className="card">
221
- <h2>
222
- Tasks <span className="badge">@Controller</span>
223
- </h2>
141
+ <h2>Tasks <span className="badge">@Controller</span></h2>
224
142
  <table>
225
- <thead>
226
- <tr>
227
- <th>Task</th>
228
- <th>Priority</th>
229
- <th>Status</th>
230
- </tr>
231
- </thead>
143
+ <thead><tr><th>Task</th><th>Priority</th><th>Status</th></tr></thead>
232
144
  <tbody>
233
- {tasks.map((task) => (
234
- <tr key={task.id} className={task.done ? 'done' : ''}>
235
- <td>
236
- {task.done ? '\u2705 ' : '\u25CB '}
237
- {escapeHtml(task.title)}
238
- </td>
239
- <td>
240
- <span
241
- className={`prio ${
242
- task.priority === 'high'
243
- ? 'prio-high'
244
- : task.priority === 'low'
245
- ? 'prio-low'
246
- : 'prio-med'
247
- }`}
248
- >
249
- {task.priority}
250
- </span>
251
- </td>
252
- <td>{task.done ? 'Done' : 'To do'}</td>
145
+ {tasks.map(t => (
146
+ <tr key={t.id} className={t.done ? 'done' : ''}>
147
+ <td>{t.done ? '✅ ' : '○ '}{t.title}</td>
148
+ <td><span className={`prio prio-${t.priority}`}>{t.priority}</span></td>
149
+ <td>{t.done ? 'Done' : 'To do'}</td>
253
150
  </tr>
254
151
  ))}
255
152
  </tbody>
256
153
  </table>
257
- <form onSubmit={handleCreateTask} className="create-bar">
258
- <input
259
- placeholder="New task..."
260
- required
261
- minLength={3}
262
- value={newTitle}
263
- onChange={(e) => setNewTitle(e.target.value)}
264
- />
265
- <select
266
- value={newPriority}
267
- onChange={(e) => setNewPriority(e.target.value as 'high' | 'medium' | 'low')}
268
- >
154
+ <form onSubmit={createTask} className="create-bar">
155
+ <input value={title} onChange={e => setTitle(e.target.value)} placeholder="New task..." required minLength={3} />
156
+ <select value={priority} onChange={e => setPriority(e.target.value as Task['priority'])}>
269
157
  <option value="medium">Medium</option>
270
158
  <option value="high">High</option>
271
159
  <option value="low">Low</option>
@@ -275,34 +163,38 @@ export default function Page() {
275
163
  {formError && <p className="error">{formError}</p>}
276
164
  </section>
277
165
 
278
- {/* Right: AI Chat */}
166
+ {/* AI Chat */}
279
167
  <section className="card">
280
- <h2>
281
- AI Assistant <span className="badge badge-ai">@Agent + SSE</span>
282
- </h2>
283
- <div ref={chatBoxRef} className="chat-box">
284
- {messages.map((msg, i) => (
285
- <div key={i} className={`msg ${msg.type}`}>
286
- {msg.content}
287
- </div>
168
+ <h2>AI Assistant <span className="badge badge-ai">@Agent + SSE</span></h2>
169
+ <div ref={chatRef} className="chat-box">
170
+ {chat.map((m, i) => (
171
+ <div key={i} className={`msg ${m.role}`}>{m.role === 'user' ? `You: ${m.text}` : m.text}</div>
288
172
  ))}
289
173
  </div>
290
174
  <div className="chat-bar">
291
175
  <input
292
- placeholder="Message the AI assistant..."
293
176
  value={chatInput}
294
- onChange={(e) => setChatInput(e.target.value)}
295
- onKeyDown={(e) => {
296
- if (e.key === 'Enter') handleSendChat()
297
- }}
177
+ onChange={e => setChatInput(e.target.value)}
178
+ onKeyDown={e => e.key === 'Enter' && sendChat()}
179
+ placeholder="Message the AI assistant..."
180
+ disabled={chatBusy}
298
181
  />
299
- <button type="button" onClick={handleSendChat} disabled={chatSending}>
300
- Send
301
- </button>
182
+ <button type="button" onClick={sendChat} disabled={chatBusy}>Send</button>
302
183
  </div>
303
- {chatCost && <p className="cost">{chatCost}</p>}
304
184
  </section>
305
185
  </main>
306
- </div>
186
+
187
+ {/* Footer */}
188
+ <footer className="footer">
189
+ <p>
190
+ Powered by{' '}
191
+ <a href="https://usetheo.dev" target="_blank" rel="noopener noreferrer">TheoKit</a>
192
+ {' · '}
193
+ <a href="https://github.com/usetheodev/theokit" target="_blank" rel="noopener noreferrer">GitHub</a>
194
+ {' · '}
195
+ <a href="https://discord.usetheo.dev" target="_blank" rel="noopener noreferrer">Discord</a>
196
+ </p>
197
+ </footer>
198
+ </>
307
199
  )
308
200
  }
@@ -14,7 +14,7 @@
14
14
  "typecheck": "tsc --noEmit"
15
15
  },
16
16
  "dependencies": {
17
- "theokit": "^0.5.1",
17
+ "theokit": "^0.5.2",
18
18
  "react": "^19.0.0",
19
19
  "react-dom": "^19.0.0",
20
20
  "react-router": "^7.0.0",