create-theokit 1.0.3 → 1.0.5
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,10 +1,11 @@
|
|
|
1
|
-
/* TheoKit —
|
|
1
|
+
/* TheoKit — design system */
|
|
2
2
|
|
|
3
3
|
:root {
|
|
4
4
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
5
5
|
--font-mono: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas, monospace;
|
|
6
|
-
|
|
7
|
-
--bg
|
|
6
|
+
|
|
7
|
+
--bg: #fafafa;
|
|
8
|
+
--bg-content: #ffffff;
|
|
8
9
|
--card: #ffffff;
|
|
9
10
|
--border: #e5e5e5;
|
|
10
11
|
--text: #171717;
|
|
@@ -12,6 +13,10 @@
|
|
|
12
13
|
--text-muted: #999999;
|
|
13
14
|
--accent: #6366f1;
|
|
14
15
|
--accent-hover: #4f46e5;
|
|
16
|
+
--btn-primary-bg: #171717;
|
|
17
|
+
--btn-primary-hover: #383838;
|
|
18
|
+
--btn-secondary-hover: #f2f2f2;
|
|
19
|
+
--btn-secondary-border: #ebebeb;
|
|
15
20
|
--green: #22c55e;
|
|
16
21
|
--red: #ef4444;
|
|
17
22
|
--yellow: #eab308;
|
|
@@ -20,7 +25,7 @@
|
|
|
20
25
|
@media (prefers-color-scheme: dark) {
|
|
21
26
|
:root {
|
|
22
27
|
--bg: #0a0a0a;
|
|
23
|
-
--bg-
|
|
28
|
+
--bg-content: #0a0a0a;
|
|
24
29
|
--card: #141414;
|
|
25
30
|
--border: #2a2a2a;
|
|
26
31
|
--text: #ededed;
|
|
@@ -28,11 +33,14 @@
|
|
|
28
33
|
--text-muted: #666666;
|
|
29
34
|
--accent: #818cf8;
|
|
30
35
|
--accent-hover: #6366f1;
|
|
36
|
+
--btn-primary-bg: #ededed;
|
|
37
|
+
--btn-primary-hover: #cccccc;
|
|
38
|
+
--btn-secondary-hover: #1a1a1a;
|
|
39
|
+
--btn-secondary-border: #2a2a2a;
|
|
31
40
|
--green: #22c55e;
|
|
32
41
|
--red: #ef4444;
|
|
33
42
|
--yellow: #eab308;
|
|
34
43
|
}
|
|
35
|
-
|
|
36
44
|
html { color-scheme: dark; }
|
|
37
45
|
}
|
|
38
46
|
|
|
@@ -50,31 +58,146 @@ body {
|
|
|
50
58
|
min-height: 100vh;
|
|
51
59
|
display: flex;
|
|
52
60
|
flex-direction: column;
|
|
61
|
+
align-items: center;
|
|
53
62
|
-webkit-font-smoothing: antialiased;
|
|
54
63
|
-moz-osx-font-smoothing: grayscale;
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
a { color: inherit; text-decoration: none; }
|
|
57
67
|
code, pre { font-family: var(--font-mono); }
|
|
58
68
|
|
|
59
|
-
|
|
69
|
+
/* ─── Page container ────────────────────────────────── */
|
|
60
70
|
|
|
61
|
-
|
|
71
|
+
.page {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex: 1;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
align-items: center;
|
|
76
|
+
width: 100%;
|
|
77
|
+
}
|
|
62
78
|
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
margin: 0 auto;
|
|
66
|
-
padding: 24px;
|
|
79
|
+
.main {
|
|
80
|
+
display: flex;
|
|
67
81
|
flex: 1;
|
|
82
|
+
flex-direction: column;
|
|
68
83
|
width: 100%;
|
|
84
|
+
max-width: 900px;
|
|
85
|
+
background: var(--bg-content);
|
|
86
|
+
padding: 80px 48px 48px;
|
|
69
87
|
}
|
|
70
88
|
|
|
71
|
-
|
|
89
|
+
@media (max-width: 768px) {
|
|
90
|
+
.main { padding: 48px 20px 32px; }
|
|
91
|
+
}
|
|
72
92
|
|
|
73
|
-
|
|
93
|
+
/* ─── Hero ──────────────────────────────────────────── */
|
|
94
|
+
|
|
95
|
+
.hero {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
align-items: center;
|
|
99
|
+
text-align: center;
|
|
100
|
+
gap: 16px;
|
|
101
|
+
margin-bottom: 48px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.hero-logo {
|
|
105
|
+
border-radius: 16px;
|
|
106
|
+
margin-bottom: 8px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.hero h1 {
|
|
110
|
+
font-size: 40px;
|
|
111
|
+
font-weight: 700;
|
|
112
|
+
letter-spacing: -2.4px;
|
|
113
|
+
line-height: 48px;
|
|
114
|
+
text-wrap: balance;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.hero .tagline {
|
|
118
|
+
font-size: 18px;
|
|
119
|
+
line-height: 28px;
|
|
120
|
+
color: var(--text-secondary);
|
|
121
|
+
max-width: 440px;
|
|
122
|
+
text-wrap: balance;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.hero .hint {
|
|
126
|
+
font-size: 14px;
|
|
127
|
+
color: var(--text-muted);
|
|
128
|
+
margin-top: 8px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.hero .hint code {
|
|
74
132
|
background: var(--card);
|
|
75
133
|
border: 1px solid var(--border);
|
|
76
|
-
|
|
77
|
-
|
|
134
|
+
padding: 2px 8px;
|
|
135
|
+
border-radius: 6px;
|
|
136
|
+
font-size: 13px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@media (max-width: 768px) {
|
|
140
|
+
.hero h1 { font-size: 32px; line-height: 40px; letter-spacing: -1.92px; }
|
|
141
|
+
.hero .tagline { font-size: 16px; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ─── CTA Buttons ───────────────────────────────────── */
|
|
145
|
+
|
|
146
|
+
.ctas {
|
|
147
|
+
display: flex;
|
|
148
|
+
gap: 12px;
|
|
149
|
+
margin-top: 8px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.btn {
|
|
153
|
+
display: inline-flex;
|
|
154
|
+
justify-content: center;
|
|
155
|
+
align-items: center;
|
|
156
|
+
height: 40px;
|
|
157
|
+
padding: 0 20px;
|
|
158
|
+
border-radius: 128px;
|
|
159
|
+
border: 1px solid transparent;
|
|
160
|
+
font-size: 14px;
|
|
161
|
+
font-weight: 500;
|
|
162
|
+
cursor: pointer;
|
|
163
|
+
transition: background 0.2s, border-color 0.2s;
|
|
164
|
+
text-decoration: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.btn.primary {
|
|
168
|
+
background: var(--btn-primary-bg);
|
|
169
|
+
color: var(--bg);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.btn.secondary {
|
|
173
|
+
border-color: var(--btn-secondary-border);
|
|
174
|
+
color: var(--text);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@media (hover: hover) and (pointer: fine) {
|
|
178
|
+
.btn.primary:hover { background: var(--btn-primary-hover); }
|
|
179
|
+
.btn.secondary:hover { background: var(--btn-secondary-hover); border-color: transparent; }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ─── Role selector ─────────────────────────────────── */
|
|
183
|
+
|
|
184
|
+
.role-bar {
|
|
185
|
+
display: flex;
|
|
186
|
+
justify-content: center;
|
|
187
|
+
align-items: center;
|
|
188
|
+
gap: 8px;
|
|
189
|
+
margin-bottom: 32px;
|
|
190
|
+
font-size: 14px;
|
|
191
|
+
color: var(--text-muted);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.role-bar select {
|
|
195
|
+
padding: 6px 12px;
|
|
196
|
+
background: var(--card);
|
|
197
|
+
border: 1px solid var(--border);
|
|
198
|
+
border-radius: 6px;
|
|
199
|
+
color: var(--text);
|
|
200
|
+
font-size: 13px;
|
|
78
201
|
}
|
|
79
202
|
|
|
80
203
|
/* ─── Grid ──────────────────────────────────────────── */
|
|
@@ -83,24 +206,35 @@ a { color: inherit; text-decoration: none; }
|
|
|
83
206
|
display: grid;
|
|
84
207
|
grid-template-columns: 1fr 1fr;
|
|
85
208
|
gap: 24px;
|
|
209
|
+
width: 100%;
|
|
86
210
|
}
|
|
87
211
|
|
|
88
212
|
@media (max-width: 768px) {
|
|
89
213
|
.grid { grid-template-columns: 1fr; }
|
|
90
214
|
}
|
|
91
215
|
|
|
92
|
-
/* ───
|
|
216
|
+
/* ─── Cards ─────────────────────────────────────────── */
|
|
93
217
|
|
|
94
|
-
|
|
95
|
-
|
|
218
|
+
.card {
|
|
219
|
+
background: var(--card);
|
|
220
|
+
border: 1px solid var(--border);
|
|
221
|
+
border-radius: 12px;
|
|
222
|
+
padding: 24px;
|
|
223
|
+
}
|
|
96
224
|
|
|
97
|
-
.
|
|
98
|
-
|
|
225
|
+
.card h2 {
|
|
226
|
+
font-size: 15px;
|
|
227
|
+
font-weight: 600;
|
|
228
|
+
margin-bottom: 16px;
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
gap: 8px;
|
|
232
|
+
}
|
|
99
233
|
|
|
100
234
|
/* ─── Badges ────────────────────────────────────────── */
|
|
101
235
|
|
|
102
236
|
.badge {
|
|
103
|
-
font-size:
|
|
237
|
+
font-size: 11px;
|
|
104
238
|
padding: 2px 10px;
|
|
105
239
|
border-radius: 99px;
|
|
106
240
|
font-weight: 500;
|
|
@@ -115,87 +249,88 @@ h2 { font-size: 1rem; font-weight: 600; margin-bottom: 16px; display: flex; alig
|
|
|
115
249
|
|
|
116
250
|
/* ─── Table ─────────────────────────────────────────── */
|
|
117
251
|
|
|
118
|
-
table { width: 100%; border-collapse: collapse; font-size:
|
|
119
|
-
th {
|
|
120
|
-
|
|
121
|
-
|
|
252
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
253
|
+
th {
|
|
254
|
+
text-align: left; padding: 8px 6px;
|
|
255
|
+
color: var(--text-muted); border-bottom: 1px solid var(--border);
|
|
256
|
+
font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
|
|
257
|
+
}
|
|
258
|
+
td { padding: 8px 6px; border-bottom: 1px solid var(--border); }
|
|
259
|
+
tr.done td { opacity: 0.4; text-decoration: line-through; }
|
|
122
260
|
|
|
123
|
-
.prio { font-size:
|
|
261
|
+
.prio { font-size: 11px; padding: 2px 8px; border-radius: 99px; font-weight: 500; }
|
|
124
262
|
.prio-high { background: color-mix(in srgb, var(--red) 12%, transparent); color: var(--red); }
|
|
125
|
-
.prio-
|
|
263
|
+
.prio-medium { background: color-mix(in srgb, var(--yellow) 12%, transparent); color: var(--yellow); }
|
|
126
264
|
.prio-low { background: color-mix(in srgb, var(--green) 12%, transparent); color: var(--green); }
|
|
127
265
|
|
|
128
266
|
/* ─── Forms ─────────────────────────────────────────── */
|
|
129
267
|
|
|
130
268
|
.create-bar { display: flex; gap: 8px; margin-top: 16px; }
|
|
131
269
|
.create-bar input {
|
|
132
|
-
flex: 1; padding:
|
|
133
|
-
background: var(--bg
|
|
134
|
-
border-radius: 8px; color: var(--text); font-size:
|
|
270
|
+
flex: 1; padding: 8px 12px;
|
|
271
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
272
|
+
border-radius: 8px; color: var(--text); font-size: 13px;
|
|
135
273
|
outline: none; transition: border-color 0.2s;
|
|
136
274
|
}
|
|
137
275
|
.create-bar input:focus { border-color: var(--accent); }
|
|
138
276
|
.create-bar select {
|
|
139
|
-
padding: 10px
|
|
277
|
+
padding: 8px 10px; background: var(--bg);
|
|
140
278
|
border: 1px solid var(--border); border-radius: 8px;
|
|
141
|
-
color: var(--text); font-size:
|
|
279
|
+
color: var(--text); font-size: 13px;
|
|
142
280
|
}
|
|
143
281
|
.create-bar button, .chat-bar button {
|
|
144
|
-
padding:
|
|
282
|
+
padding: 8px 16px; background: var(--accent); color: white;
|
|
145
283
|
border: none; border-radius: 8px; cursor: pointer;
|
|
146
|
-
font-weight: 600; font-size:
|
|
284
|
+
font-weight: 600; font-size: 13px; transition: background 0.2s;
|
|
147
285
|
}
|
|
148
|
-
|
|
149
286
|
@media (hover: hover) and (pointer: fine) {
|
|
150
287
|
.create-bar button:hover, .chat-bar button:hover { background: var(--accent-hover); }
|
|
151
288
|
}
|
|
152
|
-
|
|
153
289
|
.create-bar button:disabled, .chat-bar button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
154
|
-
.error { color: var(--red); font-size:
|
|
155
|
-
|
|
156
|
-
/* ─── Role selector ─────────────────────────────────── */
|
|
157
|
-
|
|
158
|
-
.role-bar { margin-top: 14px; display: flex; align-items: center; gap: 8px; }
|
|
159
|
-
.role-bar label { font-size: 0.8rem; color: var(--text-muted); }
|
|
160
|
-
.role-bar select {
|
|
161
|
-
padding: 6px 12px; background: var(--bg-secondary);
|
|
162
|
-
border: 1px solid var(--border); border-radius: 6px;
|
|
163
|
-
color: var(--text); font-size: 0.8rem;
|
|
164
|
-
}
|
|
290
|
+
.error { color: var(--red); font-size: 13px; margin-top: 6px; }
|
|
165
291
|
|
|
166
292
|
/* ─── Chat ──────────────────────────────────────────── */
|
|
167
293
|
|
|
168
294
|
.chat-box {
|
|
169
|
-
height:
|
|
170
|
-
background: var(--bg
|
|
295
|
+
height: 320px; overflow-y: auto; padding: 14px;
|
|
296
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
171
297
|
border-radius: 10px; margin-bottom: 12px;
|
|
172
|
-
font-size:
|
|
298
|
+
font-size: 13px; line-height: 1.6;
|
|
173
299
|
}
|
|
174
300
|
|
|
175
|
-
.msg { margin-bottom:
|
|
301
|
+
.msg { margin-bottom: 8px; padding: 8px 12px; border-radius: 8px; }
|
|
176
302
|
.msg.user { background: color-mix(in srgb, var(--accent) 10%, transparent); color: var(--accent); }
|
|
177
303
|
.msg.agent { background: var(--card); border: 1px solid var(--border); }
|
|
178
|
-
.msg.tool { background: color-mix(in srgb, var(--yellow) 8%, transparent); color: var(--yellow); font-size:
|
|
179
|
-
.msg.system { color: var(--text-muted); font-size:
|
|
180
|
-
.msg.error { color: var(--red); font-size:
|
|
304
|
+
.msg.tool { background: color-mix(in srgb, var(--yellow) 8%, transparent); color: var(--yellow); font-size: 12px; font-family: var(--font-mono); }
|
|
305
|
+
.msg.system { color: var(--text-muted); font-size: 12px; font-style: italic; }
|
|
306
|
+
.msg.error { color: var(--red); font-size: 13px; }
|
|
181
307
|
|
|
182
308
|
.chat-bar { display: flex; gap: 8px; }
|
|
183
309
|
.chat-bar input {
|
|
184
|
-
flex: 1; padding:
|
|
185
|
-
background: var(--bg
|
|
186
|
-
border-radius: 10px; color: var(--text); font-size:
|
|
310
|
+
flex: 1; padding: 10px 14px;
|
|
311
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
312
|
+
border-radius: 10px; color: var(--text); font-size: 14px;
|
|
187
313
|
outline: none; transition: border-color 0.2s;
|
|
188
314
|
}
|
|
189
315
|
.chat-bar input:focus { border-color: var(--accent); }
|
|
190
316
|
|
|
191
|
-
|
|
317
|
+
/* ─── Footer ────────────────────────────────────────── */
|
|
318
|
+
|
|
319
|
+
.footer {
|
|
320
|
+
text-align: center;
|
|
321
|
+
padding: 40px 24px 32px;
|
|
322
|
+
font-size: 13px;
|
|
323
|
+
color: var(--text-muted);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.footer a { color: var(--text-secondary); transition: color 0.2s; }
|
|
327
|
+
.footer a:hover { color: var(--accent); }
|
|
192
328
|
|
|
193
329
|
/* ─── Loading ───────────────────────────────────────── */
|
|
194
330
|
|
|
195
331
|
.loading-spinner {
|
|
196
|
-
width:
|
|
197
|
-
|
|
198
|
-
border: 3px solid var(--border);
|
|
332
|
+
width: 24px; height: 24px;
|
|
333
|
+
border: 2px solid var(--border);
|
|
199
334
|
border-top-color: var(--accent);
|
|
200
335
|
border-radius: 50%;
|
|
201
336
|
animation: spin 0.8s linear infinite;
|
|
@@ -2,307 +2,168 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef, type FormEvent } from 'react'
|
|
4
4
|
|
|
5
|
-
interface Task {
|
|
6
|
-
id: string
|
|
7
|
-
title: string
|
|
8
|
-
priority: 'high' | 'medium' | 'low'
|
|
9
|
-
done: boolean
|
|
10
|
-
}
|
|
11
|
-
|
|
5
|
+
interface Task { id: number; title: string; priority: 'high' | 'medium' | 'low'; done: boolean }
|
|
12
6
|
type Role = '' | 'user' | 'admin'
|
|
13
|
-
|
|
14
|
-
interface ChatMessage {
|
|
15
|
-
type: 'user' | 'agent' | 'tool' | 'system' | 'error'
|
|
16
|
-
content: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function escapeHtml(str: string): string {
|
|
20
|
-
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
21
|
-
}
|
|
7
|
+
interface ChatMsg { role: 'user' | 'agent' | 'tool' | 'system' | 'error'; text: string }
|
|
22
8
|
|
|
23
9
|
export default function Page() {
|
|
24
10
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
25
11
|
const [role, setRole] = useState<Role>('user')
|
|
26
|
-
const [
|
|
27
|
-
const [
|
|
12
|
+
const [title, setTitle] = useState('')
|
|
13
|
+
const [priority, setPriority] = useState<Task['priority']>('medium')
|
|
28
14
|
const [formError, setFormError] = useState('')
|
|
29
|
-
|
|
30
|
-
const [messages, setMessages] = useState<ChatMessage[]>([
|
|
31
|
-
{ type: 'system', content: 'Ask me to list, create, or complete tasks...' },
|
|
32
|
-
])
|
|
15
|
+
const [chat, setChat] = useState<ChatMsg[]>([{ role: 'system', text: 'Ask me to list, create, or complete tasks...' }])
|
|
33
16
|
const [chatInput, setChatInput] = useState('')
|
|
34
|
-
const [
|
|
35
|
-
const
|
|
36
|
-
const chatBoxRef = useRef<HTMLDivElement>(null)
|
|
37
|
-
const sessionId = useRef(`session-${Date.now()}`)
|
|
17
|
+
const [chatBusy, setChatBusy] = useState(false)
|
|
18
|
+
const chatRef = useRef<HTMLDivElement>(null)
|
|
38
19
|
|
|
39
|
-
const
|
|
20
|
+
const hdrs = useCallback((): Record<string, string> => {
|
|
40
21
|
const h: Record<string, string> = { 'Content-Type': 'application/json' }
|
|
41
22
|
if (role) h['x-role'] = role
|
|
42
23
|
return h
|
|
43
24
|
}, [role])
|
|
44
25
|
|
|
45
26
|
const loadTasks = useCallback(async () => {
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
}
|
|
27
|
+
const res = await fetch('/api/tasks')
|
|
28
|
+
if (res.ok) setTasks(await res.json())
|
|
55
29
|
}, [])
|
|
56
30
|
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
loadTasks()
|
|
59
|
-
}, [loadTasks])
|
|
60
|
-
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
if (chatBoxRef.current) {
|
|
63
|
-
chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight
|
|
64
|
-
}
|
|
65
|
-
}, [messages])
|
|
31
|
+
useEffect(() => { loadTasks() }, [loadTasks])
|
|
66
32
|
|
|
67
|
-
const
|
|
33
|
+
const createTask = async (e: FormEvent) => {
|
|
68
34
|
e.preventDefault()
|
|
69
35
|
setFormError('')
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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('')
|
|
36
|
+
if (!title.trim()) return
|
|
37
|
+
const res = await fetch('/api/tasks', { method: 'POST', headers: hdrs(), body: JSON.stringify({ title, priority }) })
|
|
38
|
+
if (res.status === 403) { setFormError('403 — Need User role'); return }
|
|
39
|
+
if (!res.ok) { const b = await res.json(); setFormError(b.error?.issues?.[0]?.message ?? `Error ${res.status}`); return }
|
|
40
|
+
setTitle('')
|
|
91
41
|
loadTasks()
|
|
92
42
|
}
|
|
93
43
|
|
|
94
|
-
const
|
|
44
|
+
const sendChat = async () => {
|
|
95
45
|
const msg = chatInput.trim()
|
|
96
|
-
if (!msg ||
|
|
46
|
+
if (!msg || chatBusy) return
|
|
97
47
|
setChatInput('')
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
setMessages((prev) => [...prev, { type: 'user', content: `You: ${msg}` }])
|
|
101
|
-
|
|
48
|
+
setChat(c => [...c, { role: 'user', text: msg }])
|
|
49
|
+
setChatBusy(true)
|
|
102
50
|
try {
|
|
103
51
|
const res = await fetch('/api/agents/assistant/chat', {
|
|
104
|
-
method: 'POST',
|
|
105
|
-
|
|
106
|
-
body: JSON.stringify({ message: msg, sessionId: sessionId.current }),
|
|
52
|
+
method: 'POST', headers: hdrs(),
|
|
53
|
+
body: JSON.stringify({ message: msg, sessionId: 'session-' + Date.now() }),
|
|
107
54
|
})
|
|
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
|
-
}
|
|
117
|
-
|
|
55
|
+
if (res.status === 403) { setChat(c => [...c, { role: 'error', text: '403 — Need User role' }]); return }
|
|
118
56
|
const reader = res.body?.getReader()
|
|
119
|
-
if (!reader)
|
|
120
|
-
setChatSending(false)
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
|
|
57
|
+
if (!reader) return
|
|
124
58
|
const decoder = new TextDecoder()
|
|
125
|
-
let buf = ''
|
|
126
|
-
let agentText = ''
|
|
127
|
-
|
|
128
|
-
setMessages((prev) => [...prev, { type: 'agent', content: '' }])
|
|
129
|
-
|
|
59
|
+
let buf = '', agentText = ''
|
|
130
60
|
while (true) {
|
|
131
61
|
const { done, value } = await reader.read()
|
|
132
62
|
if (done) break
|
|
133
|
-
|
|
134
63
|
buf += decoder.decode(value, { stream: true })
|
|
135
|
-
const lines = buf.split('\n')
|
|
136
|
-
buf = lines.pop() ?? ''
|
|
137
|
-
|
|
64
|
+
const lines = buf.split('\n'); buf = lines.pop() ?? ''
|
|
138
65
|
for (const line of lines) {
|
|
139
66
|
if (!line.startsWith('data: ')) continue
|
|
140
67
|
try {
|
|
141
68
|
const ev = JSON.parse(line.slice(6))
|
|
142
|
-
if (ev.type === 'text_delta')
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
}
|
|
69
|
+
if (ev.type === 'text_delta') agentText += ev.content
|
|
70
|
+
else if (ev.type === 'tool_call') setChat(c => [...c, { role: 'tool', text: `🔧 ${ev.toolName}` }])
|
|
71
|
+
else if (ev.type === 'tool_result') setChat(c => [...c, { role: 'tool', text: `✅ ${(ev.output ?? '').slice(0, 80)}` }])
|
|
72
|
+
else if (ev.type === 'error') setChat(c => [...c, { role: 'error', text: ev.message }])
|
|
73
|
+
} catch { /* partial */ }
|
|
186
74
|
}
|
|
187
75
|
}
|
|
188
|
-
|
|
76
|
+
if (agentText) setChat(c => [...c, { role: 'agent', text: agentText }])
|
|
189
77
|
loadTasks()
|
|
190
78
|
} catch (err) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
setChatSending(false)
|
|
79
|
+
setChat(c => [...c, { role: 'error', text: `Error: ${err instanceof Error ? err.message : String(err)}` }])
|
|
80
|
+
} finally { setChatBusy(false) }
|
|
196
81
|
}
|
|
197
82
|
|
|
83
|
+
useEffect(() => { chatRef.current?.scrollTo(0, chatRef.current.scrollHeight) }, [chat])
|
|
84
|
+
|
|
198
85
|
return (
|
|
199
|
-
<div className="
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
86
|
+
<div className="page">
|
|
87
|
+
<div className="main">
|
|
88
|
+
{/* Hero */}
|
|
89
|
+
<header className="hero">
|
|
90
|
+
<img src="/logo.png" alt="TheoKit" width={72} height={72} className="hero-logo" />
|
|
91
|
+
<h1>TheoKit</h1>
|
|
92
|
+
<p className="tagline">Build the app your agent lives in.</p>
|
|
93
|
+
<nav className="ctas">
|
|
94
|
+
<a href="https://usetheo.dev" target="_blank" rel="noopener noreferrer" className="btn primary">
|
|
95
|
+
Get Started
|
|
96
|
+
</a>
|
|
97
|
+
<a href="https://github.com/usetheodev/theokit" target="_blank" rel="noopener noreferrer" className="btn secondary">
|
|
98
|
+
Documentation
|
|
99
|
+
</a>
|
|
100
|
+
</nav>
|
|
101
|
+
<p className="hint">
|
|
102
|
+
Edit <code>app/page.tsx</code> to get started.
|
|
103
|
+
</p>
|
|
104
|
+
</header>
|
|
105
|
+
|
|
106
|
+
{/* Role */}
|
|
208
107
|
<div className="role-bar">
|
|
209
108
|
<label htmlFor="role">Role:</label>
|
|
210
|
-
<select id="role" value={role} onChange={
|
|
211
|
-
<option value="">None (public
|
|
109
|
+
<select id="role" value={role} onChange={e => setRole(e.target.value as Role)}>
|
|
110
|
+
<option value="">None (public)</option>
|
|
212
111
|
<option value="user">User</option>
|
|
213
112
|
<option value="admin">Admin</option>
|
|
214
113
|
</select>
|
|
215
114
|
</div>
|
|
216
|
-
</header>
|
|
217
115
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
</td>
|
|
252
|
-
<td>{task.done ? 'Done' : 'To do'}</td>
|
|
253
|
-
</tr>
|
|
116
|
+
{/* Content */}
|
|
117
|
+
<div className="grid">
|
|
118
|
+
<section className="card">
|
|
119
|
+
<h2>Tasks <span className="badge">@Controller</span></h2>
|
|
120
|
+
<table>
|
|
121
|
+
<thead><tr><th>Task</th><th>Priority</th><th>Status</th></tr></thead>
|
|
122
|
+
<tbody>
|
|
123
|
+
{tasks.map(t => (
|
|
124
|
+
<tr key={t.id} className={t.done ? 'done' : ''}>
|
|
125
|
+
<td>{t.done ? '✅ ' : '○ '}{t.title}</td>
|
|
126
|
+
<td><span className={`prio prio-${t.priority}`}>{t.priority}</span></td>
|
|
127
|
+
<td>{t.done ? 'Done' : 'To do'}</td>
|
|
128
|
+
</tr>
|
|
129
|
+
))}
|
|
130
|
+
</tbody>
|
|
131
|
+
</table>
|
|
132
|
+
<form onSubmit={createTask} className="create-bar">
|
|
133
|
+
<input value={title} onChange={e => setTitle(e.target.value)} placeholder="New task..." required minLength={3} />
|
|
134
|
+
<select value={priority} onChange={e => setPriority(e.target.value as Task['priority'])}>
|
|
135
|
+
<option value="medium">Medium</option>
|
|
136
|
+
<option value="high">High</option>
|
|
137
|
+
<option value="low">Low</option>
|
|
138
|
+
</select>
|
|
139
|
+
<button type="submit">Add</button>
|
|
140
|
+
</form>
|
|
141
|
+
{formError && <p className="error">{formError}</p>}
|
|
142
|
+
</section>
|
|
143
|
+
|
|
144
|
+
<section className="card">
|
|
145
|
+
<h2>AI Assistant <span className="badge badge-ai">@Agent + SSE</span></h2>
|
|
146
|
+
<div ref={chatRef} className="chat-box">
|
|
147
|
+
{chat.map((m, i) => (
|
|
148
|
+
<div key={i} className={`msg ${m.role}`}>{m.role === 'user' ? `You: ${m.text}` : m.text}</div>
|
|
254
149
|
))}
|
|
255
|
-
</
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
>
|
|
269
|
-
<option value="medium">Medium</option>
|
|
270
|
-
<option value="high">High</option>
|
|
271
|
-
<option value="low">Low</option>
|
|
272
|
-
</select>
|
|
273
|
-
<button type="submit">Add</button>
|
|
274
|
-
</form>
|
|
275
|
-
{formError && <p className="error">{formError}</p>}
|
|
276
|
-
</section>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="chat-bar">
|
|
152
|
+
<input value={chatInput} onChange={e => setChatInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && sendChat()} placeholder="Message the AI assistant..." disabled={chatBusy} />
|
|
153
|
+
<button type="button" onClick={sendChat} disabled={chatBusy}>Send</button>
|
|
154
|
+
</div>
|
|
155
|
+
</section>
|
|
156
|
+
</div>
|
|
277
157
|
|
|
278
|
-
{/*
|
|
279
|
-
<
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
</
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
</div>
|
|
288
|
-
))}
|
|
289
|
-
</div>
|
|
290
|
-
<div className="chat-bar">
|
|
291
|
-
<input
|
|
292
|
-
placeholder="Message the AI assistant..."
|
|
293
|
-
value={chatInput}
|
|
294
|
-
onChange={(e) => setChatInput(e.target.value)}
|
|
295
|
-
onKeyDown={(e) => {
|
|
296
|
-
if (e.key === 'Enter') handleSendChat()
|
|
297
|
-
}}
|
|
298
|
-
/>
|
|
299
|
-
<button type="button" onClick={handleSendChat} disabled={chatSending}>
|
|
300
|
-
Send
|
|
301
|
-
</button>
|
|
302
|
-
</div>
|
|
303
|
-
{chatCost && <p className="cost">{chatCost}</p>}
|
|
304
|
-
</section>
|
|
305
|
-
</main>
|
|
158
|
+
{/* Footer */}
|
|
159
|
+
<footer className="footer">
|
|
160
|
+
Powered by <a href="https://usetheo.dev" target="_blank" rel="noopener noreferrer">TheoKit</a>
|
|
161
|
+
{' · '}
|
|
162
|
+
<a href="https://github.com/usetheodev/theokit" target="_blank" rel="noopener noreferrer">GitHub</a>
|
|
163
|
+
{' · '}
|
|
164
|
+
<a href="https://discord.usetheo.dev" target="_blank" rel="noopener noreferrer">Discord</a>
|
|
165
|
+
</footer>
|
|
166
|
+
</div>
|
|
306
167
|
</div>
|
|
307
168
|
)
|
|
308
169
|
}
|
|
Binary file
|