create-theokit 1.2.6 → 1.2.8
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
|
@@ -9,9 +9,12 @@ import Page from './page'
|
|
|
9
9
|
* must both type-check AND render. Renders the real React tree in jsdom.
|
|
10
10
|
*/
|
|
11
11
|
describe('default chat page', () => {
|
|
12
|
-
it('
|
|
13
|
-
render(<Page />)
|
|
14
|
-
|
|
12
|
+
it('opens with the agent greeting + quick actions + composer on first load', () => {
|
|
13
|
+
const { container } = render(<Page />)
|
|
14
|
+
// The agent greets first — the transcript starts warm (like the terminal surface). ChatMessage renders
|
|
15
|
+
// the text across markdown spans, so assert the assistant message container structurally, not by text.
|
|
16
|
+
expect(container.querySelector('[data-theo-chat-message="assistant"]')).not.toBeNull()
|
|
17
|
+
// … with the quick-action suggestions and the composer.
|
|
15
18
|
expect(screen.getByText('Summarize this page')).toBeDefined()
|
|
16
19
|
expect(screen.getByText('Show available tools')).toBeDefined()
|
|
17
20
|
expect(screen.getByText('Start a new conversation')).toBeDefined()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect,
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
4
|
import {
|
|
5
5
|
ChatThread,
|
|
6
6
|
ChatMessage,
|
|
@@ -12,15 +12,7 @@ import {
|
|
|
12
12
|
type UIMessage,
|
|
13
13
|
type QuickAction,
|
|
14
14
|
} from '@theokit/ui'
|
|
15
|
-
import {
|
|
16
|
-
EmptyState,
|
|
17
|
-
CommandPalette,
|
|
18
|
-
Avatar,
|
|
19
|
-
Tooltip,
|
|
20
|
-
Button,
|
|
21
|
-
ScrollArea,
|
|
22
|
-
type CommandItem,
|
|
23
|
-
} from '@usetheo/ui'
|
|
15
|
+
import { CommandPalette, Avatar, Tooltip, Button, ScrollArea, type CommandItem } from '@usetheo/ui'
|
|
24
16
|
import { Sparkles, Wrench, RotateCcw, Command } from 'lucide-react'
|
|
25
17
|
import { useAgent } from 'theokit/client'
|
|
26
18
|
|
|
@@ -32,16 +24,16 @@ import { useAgent } from 'theokit/client'
|
|
|
32
24
|
* AgentStreaming → streaming indicator
|
|
33
25
|
* AgentErrorCard → error display
|
|
34
26
|
* ChatComposer → bottom input bar
|
|
35
|
-
*
|
|
27
|
+
* QuickActionChips → first-load suggestions
|
|
36
28
|
* ContextWindowBar → context usage at top
|
|
37
29
|
* CommandPalette → ⌘K quick actions
|
|
38
|
-
* Avatar
|
|
39
|
-
* Tooltip → hints on icons
|
|
30
|
+
* Avatar / Tooltip → message faces + icon hints
|
|
40
31
|
*
|
|
41
|
-
* `useAgent` binds to the `agents/chat.ts` endpoint
|
|
42
|
-
* `
|
|
43
|
-
* `
|
|
44
|
-
*
|
|
32
|
+
* `useAgent` binds to the `agents/chat.ts` endpoint and consumes the ai-sdk `UIMessageStream`. It opens a
|
|
33
|
+
* FRESH stream per send, so `messages` hold only the CURRENT turn (and carry no stable id) — we OWN the
|
|
34
|
+
* transcript: `history` accumulates every finished turn with our own unique ids (`u-N` / `a-N` / `greeting`),
|
|
35
|
+
* and the in-flight reply is shown live until it commits. This keeps the order correct, the history complete,
|
|
36
|
+
* and every id unique. Edit `agents/chat.ts` to pick your model / add tools.
|
|
45
37
|
*/
|
|
46
38
|
|
|
47
39
|
const QUICK_ACTIONS: QuickAction[] = [
|
|
@@ -63,6 +55,18 @@ const CONTEXT_USED = 4_200
|
|
|
63
55
|
const CONTEXT_TOTAL = 200_000
|
|
64
56
|
const MODEL_NAME = 'gpt-4o-mini'
|
|
65
57
|
|
|
58
|
+
/** The agent's opening line — so the conversation starts warm instead of empty. */
|
|
59
|
+
const GREETING: UIMessage = {
|
|
60
|
+
id: 'greeting',
|
|
61
|
+
role: 'assistant',
|
|
62
|
+
parts: [
|
|
63
|
+
{
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: "Hi — I'm your TheoKit agent. Ask me anything and I'll stream a reply. Try a quick action below or type your own.",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
|
|
66
70
|
const ASSISTANT_AVATAR = (
|
|
67
71
|
<Avatar size="sm" tone="primary">
|
|
68
72
|
<Avatar.Fallback>TH</Avatar.Fallback>
|
|
@@ -76,8 +80,8 @@ const USER_AVATAR = (
|
|
|
76
80
|
|
|
77
81
|
export default function Page() {
|
|
78
82
|
const [composerValue, setComposerValue] = useState('')
|
|
79
|
-
const [userMessages, setUserMessages] = useState<UIMessage[]>([])
|
|
80
83
|
const [paletteOpen, setPaletteOpen] = useState(false)
|
|
84
|
+
const [history, setHistory] = useState<UIMessage[]>([GREETING])
|
|
81
85
|
const { messages, send, status, reset } = useAgent<{ message: string }>('/api/agents/chat')
|
|
82
86
|
|
|
83
87
|
// ⌘K / Ctrl+K opens the CommandPalette.
|
|
@@ -92,29 +96,39 @@ export default function Page() {
|
|
|
92
96
|
return () => window.removeEventListener('keydown', onKey)
|
|
93
97
|
}, [])
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
const isStreaming = status === 'streaming'
|
|
100
|
+
const users = history.filter((m) => m.role === 'user').length
|
|
101
|
+
const replies = history.filter((m) => m.role === 'assistant' && m.id !== 'greeting').length
|
|
102
|
+
// A sent prompt is still awaiting its committed reply — the current turn is "in flight".
|
|
103
|
+
const pending = users > replies
|
|
104
|
+
|
|
105
|
+
// Merge the in-flight turn's parts into ONE assistant message with our own unique id (the SDK's ids are
|
|
106
|
+
// empty, which would collide in the thread). `suffix` distinguishes the live copy from the committed one.
|
|
107
|
+
const inflightReply = (suffix: string): UIMessage => ({
|
|
108
|
+
id: `a-${String(replies)}${suffix}`,
|
|
109
|
+
role: 'assistant',
|
|
110
|
+
parts: messages.flatMap((m) => m.parts),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Commit the finished reply into history exactly once (the next send resets `messages`).
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!isStreaming && pending && messages.length > 0) {
|
|
116
|
+
setHistory((h) => [...h, inflightReply('')])
|
|
105
117
|
}
|
|
106
|
-
|
|
107
|
-
}, [
|
|
118
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
119
|
+
}, [isStreaming, pending, messages, replies])
|
|
120
|
+
|
|
121
|
+
// Transcript = committed history + the in-flight reply (shown until it commits — no flicker, no double).
|
|
122
|
+
const thread = pending && messages.length > 0 ? [...history, inflightReply('-live')] : history
|
|
123
|
+
const onlyGreeting = history.length === 1 && !isStreaming
|
|
108
124
|
|
|
109
125
|
function handleSubmit(value: string) {
|
|
110
126
|
const trimmed = value.trim()
|
|
111
127
|
if (!trimmed) return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
role: 'user',
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
setUserMessages((prev) => [...prev, userMessage])
|
|
128
|
+
setHistory((h) => [
|
|
129
|
+
...h,
|
|
130
|
+
{ id: `u-${String(h.length)}`, role: 'user', parts: [{ type: 'text', text: trimmed }] },
|
|
131
|
+
])
|
|
118
132
|
send({ message: trimmed })
|
|
119
133
|
setComposerValue('')
|
|
120
134
|
}
|
|
@@ -122,7 +136,7 @@ export default function Page() {
|
|
|
122
136
|
function handleQuickAction(id: string) {
|
|
123
137
|
setPaletteOpen(false)
|
|
124
138
|
if (id === 'reset') {
|
|
125
|
-
|
|
139
|
+
setHistory([GREETING])
|
|
126
140
|
reset()
|
|
127
141
|
return
|
|
128
142
|
}
|
|
@@ -131,8 +145,6 @@ export default function Page() {
|
|
|
131
145
|
if (action && typeof action.label === 'string') handleSubmit(action.label)
|
|
132
146
|
}
|
|
133
147
|
|
|
134
|
-
const isStreaming = status === 'streaming'
|
|
135
|
-
const isEmpty = thread.length === 0 && !isStreaming
|
|
136
148
|
const hasError = status === 'error'
|
|
137
149
|
|
|
138
150
|
return (
|
|
@@ -148,25 +160,18 @@ export default function Page() {
|
|
|
148
160
|
|
|
149
161
|
<ScrollArea className="flex-1">
|
|
150
162
|
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-6 py-6">
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
key={message.id}
|
|
164
|
-
message={message}
|
|
165
|
-
avatar={message.role === 'assistant' ? ASSISTANT_AVATAR : USER_AVATAR}
|
|
166
|
-
/>
|
|
167
|
-
))}
|
|
168
|
-
{isStreaming && <AgentStreaming model={MODEL_NAME} />}
|
|
169
|
-
</ChatThread>
|
|
163
|
+
<ChatThread>
|
|
164
|
+
{thread.map((message) => (
|
|
165
|
+
<ChatMessage
|
|
166
|
+
key={message.id}
|
|
167
|
+
message={message}
|
|
168
|
+
avatar={message.role === 'assistant' ? ASSISTANT_AVATAR : USER_AVATAR}
|
|
169
|
+
/>
|
|
170
|
+
))}
|
|
171
|
+
{isStreaming && <AgentStreaming model={MODEL_NAME} />}
|
|
172
|
+
</ChatThread>
|
|
173
|
+
{onlyGreeting && (
|
|
174
|
+
<QuickActionChips actions={QUICK_ACTIONS} onSelect={handleQuickAction} />
|
|
170
175
|
)}
|
|
171
176
|
</div>
|
|
172
177
|
</ScrollArea>
|