create-theokit 0.4.0-beta.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -4
- package/templates/default/.env.example +5 -0
- package/templates/default/README.md.tmpl +71 -57
- package/templates/default/_gitignore +2 -3
- package/templates/default/app/layout.tsx +51 -82
- package/templates/default/app/page.tsx +169 -271
- package/templates/default/app.ts +23 -0
- package/templates/default/package.json.tmpl +10 -23
- package/templates/default/server/agents/assistant.agent.ts +46 -0
- package/templates/default/server/controllers/tasks.controller.ts +70 -0
- package/templates/default/server/filters/http-error.filter.ts +20 -0
- package/templates/default/server/guards/auth.guard.ts +47 -0
- package/templates/default/server/interceptors/timing.interceptor.ts +14 -0
- package/templates/default/server/middleware/logger.middleware.ts +12 -0
- package/templates/default/server/store.ts +46 -0
- package/templates/default/server/toolboxes/task.tools.ts +58 -0
- package/templates/default/tsconfig.json +7 -7
- package/LICENSE +0 -201
- package/templates/api-only/.nvmrc +0 -1
- package/templates/api-only/README.md.tmpl +0 -78
- package/templates/api-only/_gitignore +0 -5
- package/templates/api-only/app/page.tsx +0 -3
- package/templates/api-only/index.html +0 -12
- package/templates/api-only/package.json.tmpl +0 -28
- package/templates/api-only/public/.gitkeep +0 -0
- package/templates/api-only/public/favicon.ico +0 -0
- package/templates/api-only/server/routes/health.ts +0 -5
- package/templates/api-only/server/routes/users.ts +0 -27
- package/templates/api-only/server/routes/webhooks/echo.ts +0 -34
- package/templates/api-only/theo.config.ts +0 -3
- package/templates/api-only/tsconfig.json +0 -15
- package/templates/dashboard/.nvmrc +0 -1
- package/templates/dashboard/README.md.tmpl +0 -76
- package/templates/dashboard/_gitignore +0 -5
- package/templates/dashboard/app/about/page.tsx +0 -3
- package/templates/dashboard/app/dashboard/layout.tsx +0 -10
- package/templates/dashboard/app/dashboard/page.tsx +0 -3
- package/templates/dashboard/app/layout.tsx +0 -14
- package/templates/dashboard/app/page.tsx +0 -8
- package/templates/dashboard/index.html +0 -12
- package/templates/dashboard/package.json.tmpl +0 -28
- package/templates/dashboard/public/.gitkeep +0 -0
- package/templates/dashboard/public/favicon.ico +0 -0
- package/templates/dashboard/server/crons/cleanup-conversations.ts +0 -59
- package/templates/dashboard/server/routes/health.ts +0 -5
- package/templates/dashboard/theo.config.ts +0 -3
- package/templates/dashboard/tsconfig.json +0 -15
- package/templates/default/.nvmrc +0 -1
- package/templates/default/index.html +0 -12
- package/templates/default/public/.gitkeep +0 -0
- package/templates/default/public/favicon.ico +0 -0
- package/templates/default/server/crons/cleanup-conversations.ts +0 -59
- package/templates/default/server/routes/chat.ts +0 -69
- package/templates/default/server/routes/health.ts +0 -5
- package/templates/default/theo.config.ts +0 -3
- package/templates/default/types/jobs.d.ts +0 -25
- package/templates/postgres/.env.example +0 -5
- package/templates/postgres/.nvmrc +0 -1
- package/templates/postgres/README.md.tmpl +0 -83
- package/templates/postgres/_gitignore +0 -5
- package/templates/postgres/app/layout.tsx +0 -14
- package/templates/postgres/app/page.tsx +0 -8
- package/templates/postgres/db/index.ts +0 -7
- package/templates/postgres/db/schema.ts +0 -8
- package/templates/postgres/drizzle.config.ts +0 -10
- package/templates/postgres/index.html +0 -12
- package/templates/postgres/package.json.tmpl +0 -36
- package/templates/postgres/public/.gitkeep +0 -0
- package/templates/postgres/public/favicon.ico +0 -0
- package/templates/postgres/server/context.ts +0 -5
- package/templates/postgres/server/jobs/log-message.ts +0 -26
- package/templates/postgres/server/routes/health.ts +0 -5
- package/templates/postgres/server/routes/users.ts +0 -22
- package/templates/postgres/theo.config.ts +0 -3
- package/templates/postgres/tsconfig.json +0 -15
- package/templates/saas/.env.example +0 -7
- package/templates/saas/.nvmrc +0 -1
- package/templates/saas/README.md.tmpl +0 -103
- package/templates/saas/_gitignore +0 -5
- package/templates/saas/app/layout.tsx +0 -5
- package/templates/saas/app/page.tsx +0 -104
- package/templates/saas/db/index.ts +0 -6
- package/templates/saas/db/schema.ts +0 -20
- package/templates/saas/drizzle.config.ts +0 -10
- package/templates/saas/index.html +0 -12
- package/templates/saas/package.json.tmpl +0 -38
- package/templates/saas/public/.gitkeep +0 -0
- package/templates/saas/public/favicon.ico +0 -0
- package/templates/saas/server/context.ts +0 -37
- package/templates/saas/server/routes/agent.ts +0 -49
- package/templates/saas/server/routes/billing/stripe-webhook.ts +0 -49
- package/templates/saas/server/routes/login.ts +0 -25
- package/templates/saas/server/routes/logout.ts +0 -10
- package/templates/saas/server/routes/me.ts +0 -10
- package/templates/saas/theo.config.ts +0 -5
- package/templates/saas/tsconfig.json +0 -15
- package/templates/services/agent-node/Dockerfile.tmpl +0 -20
- package/templates/services/agent-node/README.md +0 -38
- package/templates/services/agent-node/package.json.tmpl +0 -18
- package/templates/services/agent-node/src/index.ts +0 -58
- package/templates/services/agent-node/tsconfig.json +0 -13
- package/templates/services/agent-python/Dockerfile.tmpl +0 -20
- package/templates/services/agent-python/README.md +0 -37
- package/templates/services/agent-python/main.py +0 -77
- package/templates/services/agent-python/pyproject.toml.tmpl +0 -16
|
@@ -1,285 +1,183 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
-
import {
|
|
5
|
-
ChatThread,
|
|
6
|
-
ChatMessage,
|
|
7
|
-
ChatComposer,
|
|
8
|
-
ToolCallCard,
|
|
9
|
-
AgentStreaming,
|
|
10
|
-
AgentErrorCard,
|
|
11
|
-
EmptyState,
|
|
12
|
-
QuickActionChips,
|
|
13
|
-
ContextWindowBar,
|
|
14
|
-
CommandPalette,
|
|
15
|
-
Avatar,
|
|
16
|
-
Tooltip,
|
|
17
|
-
Button,
|
|
18
|
-
ScrollArea,
|
|
19
|
-
type UIMessage,
|
|
20
|
-
type QuickAction,
|
|
21
|
-
type CommandItem,
|
|
22
|
-
type ToolCallStatus,
|
|
23
|
-
} from '@theokit/ui'
|
|
24
|
-
import { Sparkles, Wrench, RotateCcw, Command } from 'lucide-react'
|
|
25
|
-
import { useAgentStream } from 'theokit/client'
|
|
26
|
-
|
|
27
1
|
/**
|
|
28
|
-
*
|
|
2
|
+
* Main page — Task Manager + AI Chat.
|
|
29
3
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* AgentStreaming → streaming indicator
|
|
33
|
-
* AgentErrorCard → error display
|
|
34
|
-
* ChatComposer → bottom input bar
|
|
35
|
-
* EmptyState → first-load screen
|
|
36
|
-
* ContextWindowBar → context usage at top
|
|
37
|
-
* CommandPalette → ⌘K quick actions
|
|
38
|
-
* Avatar → assistant face in messages
|
|
39
|
-
* Tooltip → hints on icons
|
|
40
|
-
*
|
|
41
|
-
* `useAgentStream` handles SSE consumption, AbortController cleanup, and
|
|
42
|
-
* StrictMode safety. Replace the mock at server/routes/chat.ts with your
|
|
43
|
-
* real LLM provider (OpenAI / Anthropic / local).
|
|
4
|
+
* Split layout: left side CRUD, right side AI agent chat.
|
|
5
|
+
* SSE streaming renders token-by-token with tool call visualization.
|
|
44
6
|
*/
|
|
7
|
+
export default function Page() {
|
|
8
|
+
return (
|
|
9
|
+
<div id="app">
|
|
10
|
+
<header>
|
|
11
|
+
<h1><span className="accent">TheoKit</span> Task Manager</h1>
|
|
12
|
+
<p className="subtitle">HTTP Controllers + AI Agent — same pipeline, same guards</p>
|
|
13
|
+
<div className="role-bar">
|
|
14
|
+
<label>Role: </label>
|
|
15
|
+
<select id="role">
|
|
16
|
+
<option value="">None (public only)</option>
|
|
17
|
+
<option value="user" selected>User</option>
|
|
18
|
+
<option value="admin">Admin</option>
|
|
19
|
+
</select>
|
|
20
|
+
</div>
|
|
21
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<main className="grid">
|
|
24
|
+
{/* Left: CRUD Panel */}
|
|
25
|
+
<section className="card">
|
|
26
|
+
<h2>📋 Tasks <span className="badge">@Controller</span></h2>
|
|
27
|
+
<table>
|
|
28
|
+
<thead><tr><th>Task</th><th>Priority</th><th>Status</th></tr></thead>
|
|
29
|
+
<tbody id="task-list"></tbody>
|
|
30
|
+
</table>
|
|
31
|
+
<form id="create-form" className="create-bar">
|
|
32
|
+
<input id="new-title" placeholder="New task..." required minLength={3} />
|
|
33
|
+
<select id="new-priority">
|
|
34
|
+
<option value="medium">Medium</option>
|
|
35
|
+
<option value="high">High</option>
|
|
36
|
+
<option value="low">Low</option>
|
|
37
|
+
</select>
|
|
38
|
+
<button type="submit">Add</button>
|
|
39
|
+
</form>
|
|
40
|
+
<p id="form-error" className="error"></p>
|
|
41
|
+
</section>
|
|
42
|
+
|
|
43
|
+
{/* Right: AI Chat */}
|
|
44
|
+
<section className="card">
|
|
45
|
+
<h2>🤖 AI Assistant <span className="badge badge-ai">@Agent + SSE</span></h2>
|
|
46
|
+
<div id="chat" className="chat-box">
|
|
47
|
+
<div className="msg system">Ask me to list, create, or complete tasks...</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="chat-bar">
|
|
50
|
+
<input id="chat-input" placeholder="Message the AI assistant..." />
|
|
51
|
+
<button id="chat-send" onClick={() => {}}>Send</button>
|
|
52
|
+
</div>
|
|
53
|
+
<p id="chat-cost" className="cost"></p>
|
|
54
|
+
</section>
|
|
55
|
+
</main>
|
|
56
|
+
|
|
57
|
+
<ClientScript />
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
45
61
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
tool: string
|
|
52
|
-
target?: string
|
|
53
|
-
status: ToolCallStatus
|
|
54
|
-
output?: string
|
|
55
|
-
timestamp: string
|
|
56
|
-
}
|
|
57
|
-
| { kind: 'error'; id: string; message: string; timestamp: string }
|
|
58
|
-
|
|
59
|
-
const QUICK_ACTIONS: QuickAction[] = [
|
|
60
|
-
{ id: 'summarize', label: 'Summarize this page', icon: Sparkles },
|
|
61
|
-
{ id: 'tools', label: 'Show available tools', icon: Wrench },
|
|
62
|
-
{ id: 'reset', label: 'Start a new conversation', icon: RotateCcw },
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
const COMMAND_ITEMS: CommandItem[] = QUICK_ACTIONS.map((a) => ({
|
|
66
|
-
id: a.id,
|
|
67
|
-
label: a.label,
|
|
68
|
-
icon: a.icon,
|
|
69
|
-
group: 'Quick actions',
|
|
70
|
-
}))
|
|
71
|
-
|
|
72
|
-
// Mock context-window usage — replace with real model state.
|
|
73
|
-
const CONTEXT_USED = 4_200
|
|
74
|
-
const CONTEXT_TOTAL = 200_000
|
|
75
|
-
const MODEL_NAME = 'mock-llm'
|
|
76
|
-
|
|
77
|
-
// Modern chat UX: only the assistant carries an avatar. User messages are
|
|
78
|
-
// right-aligned with a distinct bubble style — that's enough signal.
|
|
79
|
-
// (TheoUI's ChatMessage uses flex-col, so a user avatar would land BELOW
|
|
80
|
-
// the bubble, not above — visually unusual.)
|
|
81
|
-
const ASSISTANT_AVATAR = (
|
|
82
|
-
<Avatar size="sm" tone="primary">
|
|
83
|
-
<Avatar.Fallback>TH</Avatar.Fallback>
|
|
84
|
-
</Avatar>
|
|
85
|
-
)
|
|
62
|
+
function ClientScript() {
|
|
63
|
+
return (
|
|
64
|
+
<script dangerouslySetInnerHTML={{ __html: CLIENT_JS }} />
|
|
65
|
+
)
|
|
66
|
+
}
|
|
86
67
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
68
|
+
const CLIENT_JS = `
|
|
69
|
+
const API = '';
|
|
70
|
+
let sessionId = 'session-' + Date.now();
|
|
71
|
+
const getRole = () => document.getElementById('role').value;
|
|
72
|
+
const headers = () => {
|
|
73
|
+
const h = { 'Content-Type': 'application/json' };
|
|
74
|
+
const r = getRole();
|
|
75
|
+
if (r) h['x-role'] = r;
|
|
76
|
+
return h;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ─── Tasks CRUD ─────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
async function loadTasks() {
|
|
82
|
+
const res = await fetch(API + '/api/tasks');
|
|
83
|
+
const tasks = await res.json();
|
|
84
|
+
document.getElementById('task-list').innerHTML = tasks.map(t => {
|
|
85
|
+
const statusClass = t.done ? 'done' : 'pending';
|
|
86
|
+
const prioClass = t.priority === 'high' ? 'prio-high' : t.priority === 'low' ? 'prio-low' : 'prio-med';
|
|
87
|
+
return '<tr class="' + statusClass + '"><td>' + (t.done ? '✅ ' : '○ ') + t.title + '</td><td><span class="prio ' + prioClass + '">' + t.priority + '</span></td><td>' + (t.done ? 'Done' : 'To do') + '</td></tr>';
|
|
88
|
+
}).join('');
|
|
89
|
+
}
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
document.getElementById('create-form').addEventListener('submit', async (e) => {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
const title = document.getElementById('new-title').value.trim();
|
|
94
|
+
const priority = document.getElementById('new-priority').value;
|
|
95
|
+
const err = document.getElementById('form-error');
|
|
96
|
+
err.textContent = '';
|
|
97
|
+
if (!title) return;
|
|
98
|
+
const res = await fetch(API + '/api/tasks', { method: 'POST', headers: headers(), body: JSON.stringify({ title, priority }) });
|
|
99
|
+
if (res.status === 403) { err.textContent = '403 — Need User role'; return; }
|
|
100
|
+
if (res.status === 422) { const e = await res.json(); err.textContent = e.error?.issues?.[0]?.message || 'Validation error'; return; }
|
|
101
|
+
if (!res.ok) { err.textContent = 'Error ' + res.status; return; }
|
|
102
|
+
document.getElementById('new-title').value = '';
|
|
103
|
+
loadTasks();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ─── AI Chat (SSE) ──────────────────────────────────
|
|
107
|
+
|
|
108
|
+
document.getElementById('chat-send').addEventListener('click', sendChat);
|
|
109
|
+
document.getElementById('chat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChat(); });
|
|
110
|
+
|
|
111
|
+
async function sendChat() {
|
|
112
|
+
const input = document.getElementById('chat-input');
|
|
113
|
+
const msg = input.value.trim();
|
|
114
|
+
if (!msg) return;
|
|
115
|
+
input.value = '';
|
|
116
|
+
|
|
117
|
+
const chat = document.getElementById('chat');
|
|
118
|
+
chat.innerHTML += '<div class="msg user">You: ' + escapeHtml(msg) + '</div>';
|
|
119
|
+
document.getElementById('chat-send').disabled = true;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(API + '/api/agents/assistant/chat', {
|
|
123
|
+
method: 'POST', headers: headers(),
|
|
124
|
+
body: JSON.stringify({ message: msg, sessionId })
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (res.status === 403) {
|
|
128
|
+
chat.innerHTML += '<div class="msg system">403 — Need User role to chat</div>';
|
|
129
|
+
document.getElementById('chat-send').disabled = false;
|
|
130
|
+
return;
|
|
100
131
|
}
|
|
101
|
-
window.addEventListener('keydown', onKey)
|
|
102
|
-
return () => window.removeEventListener('keydown', onKey)
|
|
103
|
-
}, [])
|
|
104
132
|
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
const reader = res.body.getReader();
|
|
134
|
+
const decoder = new TextDecoder();
|
|
135
|
+
let agentDiv = document.createElement('div');
|
|
136
|
+
agentDiv.className = 'msg agent';
|
|
137
|
+
agentDiv.textContent = '';
|
|
138
|
+
chat.appendChild(agentDiv);
|
|
139
|
+
|
|
140
|
+
let buffer = '';
|
|
141
|
+
while (true) {
|
|
142
|
+
const { done, value } = await reader.read();
|
|
143
|
+
if (done) break;
|
|
144
|
+
buffer += decoder.decode(value, { stream: true });
|
|
145
|
+
const lines = buffer.split('\\n');
|
|
146
|
+
buffer = lines.pop() || '';
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
if (!line.startsWith('data: ')) continue;
|
|
149
|
+
try {
|
|
150
|
+
const ev = JSON.parse(line.slice(6));
|
|
151
|
+
if (ev.type === 'text_delta') {
|
|
152
|
+
agentDiv.innerHTML += formatMarkdown(ev.content);
|
|
153
|
+
} else if (ev.type === 'tool_call') {
|
|
154
|
+
chat.insertBefore(toolMsg('🔧 Calling: ' + ev.toolName), agentDiv);
|
|
155
|
+
} else if (ev.type === 'tool_result') {
|
|
156
|
+
chat.insertBefore(toolMsg('✅ ' + (ev.output || '').substring(0, 80)), agentDiv);
|
|
157
|
+
} else if (ev.type === 'thinking') {
|
|
158
|
+
chat.insertBefore(sysMsg('💭 ' + ev.content), agentDiv);
|
|
159
|
+
} else if (ev.type === 'done') {
|
|
160
|
+
const cost = ev.cost ? ' · $' + ev.cost.toFixed(6) : '';
|
|
161
|
+
document.getElementById('chat-cost').textContent = ev.usage.totalTokens + ' tokens · ' + ev.durationMs + 'ms' + cost;
|
|
162
|
+
} else if (ev.type === 'error') {
|
|
163
|
+
chat.innerHTML += '<div class="msg error">' + ev.message + '</div>';
|
|
135
164
|
}
|
|
136
|
-
|
|
137
|
-
return { kind: 'error', id, message: event.message, timestamp: ts }
|
|
165
|
+
} catch {}
|
|
138
166
|
}
|
|
139
|
-
|
|
140
|
-
return [...userMessages, ...agentItems]
|
|
141
|
-
}, [userMessages, events])
|
|
142
|
-
|
|
143
|
-
function handleSubmit(value: string) {
|
|
144
|
-
const trimmed = value.trim()
|
|
145
|
-
if (!trimmed) return
|
|
146
|
-
const id = `u-${userMessages.length}`
|
|
147
|
-
setUserMessages((prev) => [
|
|
148
|
-
...prev,
|
|
149
|
-
{ kind: 'message', id, role: 'user', content: trimmed, timestamp: new Date().toISOString() },
|
|
150
|
-
])
|
|
151
|
-
send({ message: trimmed })
|
|
152
|
-
setComposerValue('')
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function handleQuickAction(id: string) {
|
|
156
|
-
setPaletteOpen(false)
|
|
157
|
-
if (id === 'reset') {
|
|
158
|
-
setUserMessages([])
|
|
159
|
-
reset()
|
|
160
|
-
return
|
|
167
|
+
chat.scrollTop = chat.scrollHeight;
|
|
161
168
|
}
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
loadTasks(); // refresh after agent actions
|
|
170
|
+
} catch (e) {
|
|
171
|
+
chat.innerHTML += '<div class="msg error">Error: ' + e.message + '</div>';
|
|
164
172
|
}
|
|
173
|
+
document.getElementById('chat-send').disabled = false;
|
|
174
|
+
chat.scrollTop = chat.scrollHeight;
|
|
175
|
+
}
|
|
165
176
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return (
|
|
171
|
-
<>
|
|
172
|
-
<ContextWindowBar
|
|
173
|
-
used={CONTEXT_USED}
|
|
174
|
-
total={CONTEXT_TOTAL}
|
|
175
|
-
trailing={MODEL_NAME}
|
|
176
|
-
label="Context window"
|
|
177
|
-
compact
|
|
178
|
-
className="border-border/60 border-b px-6 py-2"
|
|
179
|
-
/>
|
|
180
|
-
|
|
181
|
-
<ScrollArea className="flex-1">
|
|
182
|
-
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-6 py-6">
|
|
183
|
-
{isEmpty ? (
|
|
184
|
-
<EmptyState
|
|
185
|
-
eyebrow="Theo Agent"
|
|
186
|
-
icon={Sparkles}
|
|
187
|
-
title="What should we build today?"
|
|
188
|
-
description="Ask anything. This scaffold ships with a mock LLM at server/routes/chat.ts so you can see the wiring before plugging in a real model."
|
|
189
|
-
action={<QuickActionChips actions={QUICK_ACTIONS} onSelect={handleQuickAction} />}
|
|
190
|
-
/>
|
|
191
|
-
) : (
|
|
192
|
-
<ChatThread>
|
|
193
|
-
{items.map((item) => {
|
|
194
|
-
if (item.kind === 'message') {
|
|
195
|
-
const message: UIMessage = {
|
|
196
|
-
id: item.id,
|
|
197
|
-
role: item.role,
|
|
198
|
-
parts: [{ type: 'text', text: item.content, state: 'done' }],
|
|
199
|
-
}
|
|
200
|
-
return (
|
|
201
|
-
<ChatMessage
|
|
202
|
-
key={item.id}
|
|
203
|
-
message={message}
|
|
204
|
-
avatar={item.role === 'assistant' ? ASSISTANT_AVATAR : undefined}
|
|
205
|
-
/>
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
if (item.kind === 'tool') {
|
|
209
|
-
return (
|
|
210
|
-
<ToolCallCard
|
|
211
|
-
key={item.id}
|
|
212
|
-
tool={item.tool}
|
|
213
|
-
icon={Wrench}
|
|
214
|
-
target={item.target}
|
|
215
|
-
status={item.status}
|
|
216
|
-
output={item.output}
|
|
217
|
-
timestamp={item.timestamp}
|
|
218
|
-
/>
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
return (
|
|
222
|
-
<AgentErrorCard
|
|
223
|
-
key={item.id}
|
|
224
|
-
kind="tool-failure"
|
|
225
|
-
title="Agent error"
|
|
226
|
-
detail={item.message}
|
|
227
|
-
/>
|
|
228
|
-
)
|
|
229
|
-
})}
|
|
230
|
-
{isStreaming && <AgentStreaming model={MODEL_NAME} />}
|
|
231
|
-
</ChatThread>
|
|
232
|
-
)}
|
|
233
|
-
</div>
|
|
234
|
-
</ScrollArea>
|
|
235
|
-
|
|
236
|
-
<div className="border-border/60 border-t bg-background/50 backdrop-blur">
|
|
237
|
-
<div className="mx-auto w-full max-w-3xl px-6 py-4">
|
|
238
|
-
{hasError && (
|
|
239
|
-
<div className="mb-3">
|
|
240
|
-
<AgentErrorCard
|
|
241
|
-
kind="network"
|
|
242
|
-
title="Stream ended with an error"
|
|
243
|
-
detail="The connection to the agent endpoint was interrupted. Reset to try again."
|
|
244
|
-
actions={
|
|
245
|
-
<Button variant="ghost" size="sm" onClick={() => reset()}>
|
|
246
|
-
Reset
|
|
247
|
-
</Button>
|
|
248
|
-
}
|
|
249
|
-
/>
|
|
250
|
-
</div>
|
|
251
|
-
)}
|
|
252
|
-
<ChatComposer
|
|
253
|
-
value={composerValue}
|
|
254
|
-
onValueChange={setComposerValue}
|
|
255
|
-
onSubmit={handleSubmit}
|
|
256
|
-
running={isStreaming}
|
|
257
|
-
placeholder="Ask the agent…"
|
|
258
|
-
leadingActions={
|
|
259
|
-
<Tooltip label="Open command palette (⌘K)" side="top">
|
|
260
|
-
<Button
|
|
261
|
-
type="button"
|
|
262
|
-
variant="ghost"
|
|
263
|
-
size="icon"
|
|
264
|
-
onClick={() => setPaletteOpen(true)}
|
|
265
|
-
aria-label="Open command palette"
|
|
266
|
-
>
|
|
267
|
-
<Command className="size-4" />
|
|
268
|
-
</Button>
|
|
269
|
-
</Tooltip>
|
|
270
|
-
}
|
|
271
|
-
/>
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
177
|
+
function toolMsg(text) { const d = document.createElement('div'); d.className = 'msg tool'; d.textContent = text; return d; }
|
|
178
|
+
function sysMsg(text) { const d = document.createElement('div'); d.className = 'msg system'; d.textContent = text; return d; }
|
|
179
|
+
function escapeHtml(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
180
|
+
function formatMarkdown(s) { return escapeHtml(s).replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>').replace(/\\n/g, '<br>'); }
|
|
274
181
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
onOpenChange={setPaletteOpen}
|
|
278
|
-
items={COMMAND_ITEMS}
|
|
279
|
-
onSelect={handleQuickAction}
|
|
280
|
-
placeholder="Run a command…"
|
|
281
|
-
emptyMessage="No matching commands."
|
|
282
|
-
/>
|
|
283
|
-
</>
|
|
284
|
-
)
|
|
285
|
-
}
|
|
182
|
+
loadTasks();
|
|
183
|
+
`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TheoKit App — entry point.
|
|
3
|
+
*
|
|
4
|
+
* That's it. No manual wiring. No plumbing.
|
|
5
|
+
* TheoApp.create() handles everything:
|
|
6
|
+
* - Controller routes (HTTP CRUD)
|
|
7
|
+
* - Agent routes (SSE streaming + tool calling)
|
|
8
|
+
* - DI (providers injected into controllers + agents)
|
|
9
|
+
* - Shared pipeline (guards, interceptors, filters)
|
|
10
|
+
*/
|
|
11
|
+
import 'reflect-metadata'
|
|
12
|
+
import { TheoApp } from '@theokit/http-decorators/app'
|
|
13
|
+
import { TasksController } from './server/controllers/tasks.controller.js'
|
|
14
|
+
import { AssistantAgent } from './server/agents/assistant.agent.js'
|
|
15
|
+
import { TaskTools } from './server/toolboxes/task.tools.js'
|
|
16
|
+
|
|
17
|
+
const app = await TheoApp.create({
|
|
18
|
+
controllers: [TasksController],
|
|
19
|
+
agents: [AssistantAgent],
|
|
20
|
+
providers: [TaskTools],
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
await app.listen(3000)
|
|
@@ -4,32 +4,19 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"typecheck": "tsc --noEmit"
|
|
7
|
+
"dev": "bun app.ts",
|
|
8
|
+
"dev:node": "npx tsx app.ts",
|
|
9
|
+
"test": "bun test"
|
|
11
10
|
},
|
|
12
11
|
"dependencies": {
|
|
13
|
-
"theokit": "^0.4.0
|
|
14
|
-
"@theokit/
|
|
15
|
-
"@theokit/
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"react-dom": "^19.0.0",
|
|
19
|
-
"react-router": "^7.0.0",
|
|
20
|
-
"zod": "^3.24.0"
|
|
12
|
+
"theokit": "^0.4.0",
|
|
13
|
+
"@theokit/http-decorators": "^0.1.0",
|
|
14
|
+
"@theokit/agents": "^0.1.0",
|
|
15
|
+
"reflect-metadata": "^0.2.0",
|
|
16
|
+
"zod": "^3.22.0"
|
|
21
17
|
},
|
|
22
18
|
"devDependencies": {
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"@types/react": "^19.0.0",
|
|
26
|
-
"@types/react-dom": "^19.0.0",
|
|
27
|
-
"tailwindcss": "^4.0.0",
|
|
28
|
-
"@tailwindcss/vite": "^4.0.0"
|
|
29
|
-
},
|
|
30
|
-
"pnpm": {
|
|
31
|
-
"onlyBuiltDependencies": [
|
|
32
|
-
"esbuild"
|
|
33
|
-
]
|
|
19
|
+
"typescript": "^5.5.0",
|
|
20
|
+
"@swc/core": "^1.3.0"
|
|
34
21
|
}
|
|
35
22
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssistantAgent — AI-powered task management assistant.
|
|
3
|
+
*
|
|
4
|
+
* Uses the SAME guards and pipeline as HTTP controllers.
|
|
5
|
+
* Tools from @Mixin(TaskTools) are available to the LLM.
|
|
6
|
+
*/
|
|
7
|
+
import 'reflect-metadata'
|
|
8
|
+
import {
|
|
9
|
+
Agent, MainLoop, Mixin,
|
|
10
|
+
Memory, Budget, Hook,
|
|
11
|
+
} from '@theokit/agents'
|
|
12
|
+
import { UseGuards, UseInterceptors } from '@theokit/http-decorators'
|
|
13
|
+
import { RolesGuard, Roles, Role } from '../guards/auth.guard.js'
|
|
14
|
+
import { TimingInterceptor } from '../interceptors/timing.interceptor.js'
|
|
15
|
+
import { TaskTools } from '../toolboxes/task.tools.js'
|
|
16
|
+
|
|
17
|
+
@Agent({
|
|
18
|
+
name: 'assistant',
|
|
19
|
+
route: '/api/agents/assistant',
|
|
20
|
+
model: 'openai/gpt-4o-mini',
|
|
21
|
+
systemPrompt: `You are a helpful task management assistant.
|
|
22
|
+
Use the tasks.* tools to list, search, create, and complete tasks.
|
|
23
|
+
Be concise and actionable.`,
|
|
24
|
+
stream: true,
|
|
25
|
+
maxIterations: 5,
|
|
26
|
+
})
|
|
27
|
+
@UseGuards(RolesGuard)
|
|
28
|
+
@UseInterceptors(TimingInterceptor)
|
|
29
|
+
@Roles([Role.User])
|
|
30
|
+
@Memory({ provider: 'built-in', scope: 'per-user' })
|
|
31
|
+
@Budget({ maxCostUsd: 1.00, window: 'daily' })
|
|
32
|
+
@Mixin(TaskTools)
|
|
33
|
+
export class AssistantAgent {
|
|
34
|
+
@MainLoop({ strategy: 'react', maxIterations: 5 })
|
|
35
|
+
async run() {}
|
|
36
|
+
|
|
37
|
+
@Hook('before:llm-call')
|
|
38
|
+
async onBeforeLLM() {
|
|
39
|
+
console.log(' 🧠 Agent thinking...')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Hook('after:tool-call')
|
|
43
|
+
async onToolDone() {
|
|
44
|
+
console.log(' 🔧 Tool executed')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TasksController — CRUD API for tasks.
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates: @Controller, @Get/@Post/@Delete, @Body with Zod,
|
|
5
|
+
* @Param, @Query, @HttpCode, @UseGuards, @Roles, @IsPublic,
|
|
6
|
+
* @UseInterceptors, @UseFilters, NotFoundException.
|
|
7
|
+
*/
|
|
8
|
+
import 'reflect-metadata'
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
import {
|
|
11
|
+
Controller, Get, Post, Put, Delete,
|
|
12
|
+
Body, Param, Query, HttpCode,
|
|
13
|
+
UseGuards, UseInterceptors, UseFilters,
|
|
14
|
+
NotFoundException,
|
|
15
|
+
} from '@theokit/http-decorators'
|
|
16
|
+
import { RolesGuard, Roles, Role, IsPublic } from '../guards/auth.guard.js'
|
|
17
|
+
import { TimingInterceptor } from '../interceptors/timing.interceptor.js'
|
|
18
|
+
import { HttpErrorFilter } from '../filters/http-error.filter.js'
|
|
19
|
+
import { taskStore } from '../store.js'
|
|
20
|
+
|
|
21
|
+
const zCreateTask = z.object({
|
|
22
|
+
title: z.string().min(3, 'Title must be at least 3 characters'),
|
|
23
|
+
priority: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
@Controller('api/tasks')
|
|
27
|
+
@UseGuards(RolesGuard)
|
|
28
|
+
@UseInterceptors(TimingInterceptor)
|
|
29
|
+
@UseFilters(HttpErrorFilter)
|
|
30
|
+
@Roles([Role.User])
|
|
31
|
+
export class TasksController {
|
|
32
|
+
@Get()
|
|
33
|
+
@IsPublic(true)
|
|
34
|
+
list() {
|
|
35
|
+
return taskStore.list()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Get('search')
|
|
39
|
+
@IsPublic(true)
|
|
40
|
+
search(@Query('q') q: string) {
|
|
41
|
+
return taskStore.search(q ?? '')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Get(':id')
|
|
45
|
+
@IsPublic(true)
|
|
46
|
+
findById(@Param('id') id: string) {
|
|
47
|
+
const task = taskStore.get(Number(id))
|
|
48
|
+
if (!task) throw new NotFoundException(`Task ${id} not found`)
|
|
49
|
+
return task
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Post()
|
|
53
|
+
create(@Body(zCreateTask) body: z.infer<typeof zCreateTask>) {
|
|
54
|
+
return taskStore.create(body)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Put(':id')
|
|
58
|
+
update(@Param('id') id: string, @Body(z.object({ done: z.boolean() })) body: { done: boolean }) {
|
|
59
|
+
const task = taskStore.update(Number(id), body)
|
|
60
|
+
if (!task) throw new NotFoundException(`Task ${id} not found`)
|
|
61
|
+
return task
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Delete(':id')
|
|
65
|
+
@HttpCode(204)
|
|
66
|
+
@Roles([Role.Admin])
|
|
67
|
+
remove(@Param('id') id: string) {
|
|
68
|
+
if (!taskStore.remove(Number(id))) throw new NotFoundException(`Task ${id} not found`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpErrorFilter — custom error response format.
|
|
3
|
+
* Catches HttpException and returns structured JSON.
|
|
4
|
+
*/
|
|
5
|
+
import { Catch, HttpException, type ExceptionFilter, type ArgumentsHost } from '@theokit/http-decorators'
|
|
6
|
+
|
|
7
|
+
@Catch(HttpException)
|
|
8
|
+
export class HttpErrorFilter implements ExceptionFilter {
|
|
9
|
+
catch(exception: unknown, _host: ArgumentsHost): Response {
|
|
10
|
+
const ex = exception as HttpException
|
|
11
|
+
return new Response(JSON.stringify({
|
|
12
|
+
success: false,
|
|
13
|
+
error: { code: ex.statusCode, message: ex.message },
|
|
14
|
+
timestamp: new Date().toISOString(),
|
|
15
|
+
}), {
|
|
16
|
+
status: ex.statusCode,
|
|
17
|
+
headers: { 'content-type': 'application/json' },
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
}
|