@octavus/docs 0.0.3 → 0.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/content/01-getting-started/02-quickstart.md +38 -35
- package/content/02-server-sdk/01-overview.md +13 -3
- package/content/02-server-sdk/02-sessions.md +38 -27
- package/content/02-server-sdk/04-streaming.md +0 -42
- package/content/03-client-sdk/01-overview.md +69 -54
- package/content/03-client-sdk/02-messages.md +153 -125
- package/content/03-client-sdk/03-streaming.md +112 -111
- package/content/03-client-sdk/04-execution-blocks.md +80 -176
- package/content/04-protocol/01-overview.md +0 -3
- package/content/04-protocol/03-triggers.md +8 -9
- package/content/04-protocol/04-tools.md +17 -27
- package/content/04-protocol/06-agent-config.md +0 -7
- package/content/05-api-reference/02-sessions.md +20 -7
- package/dist/chunk-7F5WOCIL.js +421 -0
- package/dist/chunk-7F5WOCIL.js.map +1 -0
- package/dist/chunk-CHGY4G27.js +421 -0
- package/dist/chunk-CHGY4G27.js.map +1 -0
- package/dist/chunk-CI7JDWKU.js +421 -0
- package/dist/chunk-CI7JDWKU.js.map +1 -0
- package/dist/chunk-CVFWWRL7.js +421 -0
- package/dist/chunk-CVFWWRL7.js.map +1 -0
- package/dist/chunk-EPDM2NIJ.js +421 -0
- package/dist/chunk-EPDM2NIJ.js.map +1 -0
- package/dist/chunk-J7BMB3ZW.js +421 -0
- package/dist/chunk-J7BMB3ZW.js.map +1 -0
- package/dist/chunk-K3GFQUMC.js +421 -0
- package/dist/chunk-K3GFQUMC.js.map +1 -0
- package/dist/chunk-M2R2NDPR.js +421 -0
- package/dist/chunk-M2R2NDPR.js.map +1 -0
- package/dist/chunk-MDMRCS4W.mjs +421 -0
- package/dist/chunk-MDMRCS4W.mjs.map +1 -0
- package/dist/chunk-QCHDPR2D.js +421 -0
- package/dist/chunk-QCHDPR2D.js.map +1 -0
- package/dist/chunk-TWUMRHQ7.js +421 -0
- package/dist/chunk-TWUMRHQ7.js.map +1 -0
- package/dist/chunk-YPPXXV3I.js +421 -0
- package/dist/chunk-YPPXXV3I.js.map +1 -0
- package/dist/content.js +1 -1
- package/dist/content.mjs +17 -0
- package/dist/content.mjs.map +1 -0
- package/dist/docs.json +19 -19
- package/dist/index.js +1 -1
- package/dist/index.mjs +11 -0
- package/dist/index.mjs.map +1 -0
- package/dist/search-index.json +1 -1
- package/dist/search.js +1 -1
- package/dist/search.js.map +1 -1
- package/dist/search.mjs +30 -0
- package/dist/search.mjs.map +1 -0
- package/dist/sections.json +19 -19
- package/package.json +1 -1
|
@@ -5,159 +5,197 @@ description: Working with message state in the Client SDK.
|
|
|
5
5
|
|
|
6
6
|
# Messages
|
|
7
7
|
|
|
8
|
-
Messages represent the conversation history. The Client SDK tracks messages automatically and provides structured access to their content.
|
|
8
|
+
Messages represent the conversation history. The Client SDK tracks messages automatically and provides structured access to their content through typed parts.
|
|
9
9
|
|
|
10
10
|
## Message Structure
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
|
-
interface
|
|
13
|
+
interface UIMessage {
|
|
14
14
|
id: string;
|
|
15
|
-
role: 'user' | 'assistant'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
toolCalls?: ToolCallWithDescription[];
|
|
19
|
-
reasoning?: string;
|
|
20
|
-
visible?: boolean;
|
|
15
|
+
role: 'user' | 'assistant';
|
|
16
|
+
parts: UIMessagePart[];
|
|
17
|
+
status: 'streaming' | 'done';
|
|
21
18
|
createdAt: Date;
|
|
22
19
|
}
|
|
23
20
|
```
|
|
24
21
|
|
|
25
22
|
### Message Parts
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
Messages contain ordered `parts` that preserve content ordering:
|
|
28
25
|
|
|
29
26
|
```typescript
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
type UIMessagePart =
|
|
28
|
+
| UITextPart
|
|
29
|
+
| UIReasoningPart
|
|
30
|
+
| UIToolCallPart
|
|
31
|
+
| UIOperationPart;
|
|
32
|
+
|
|
33
|
+
// Text content
|
|
34
|
+
interface UITextPart {
|
|
35
|
+
type: 'text';
|
|
36
|
+
text: string;
|
|
37
|
+
status: 'streaming' | 'done';
|
|
38
|
+
thread?: string; // For named threads (e.g., "summary")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Extended reasoning/thinking
|
|
42
|
+
interface UIReasoningPart {
|
|
43
|
+
type: 'reasoning';
|
|
44
|
+
text: string;
|
|
45
|
+
status: 'streaming' | 'done';
|
|
46
|
+
thread?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Tool execution
|
|
50
|
+
interface UIToolCallPart {
|
|
51
|
+
type: 'tool-call';
|
|
52
|
+
toolCallId: string;
|
|
53
|
+
toolName: string;
|
|
54
|
+
displayName?: string; // Human-readable name
|
|
55
|
+
args: Record<string, unknown>;
|
|
56
|
+
result?: unknown;
|
|
57
|
+
error?: string;
|
|
58
|
+
status: 'pending' | 'running' | 'done' | 'error';
|
|
59
|
+
thread?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Internal operations (set-resource, serialize-thread)
|
|
63
|
+
interface UIOperationPart {
|
|
64
|
+
type: 'operation';
|
|
65
|
+
operationId: string;
|
|
66
|
+
name: string;
|
|
67
|
+
operationType: string;
|
|
68
|
+
status: 'running' | 'done';
|
|
69
|
+
thread?: string;
|
|
36
70
|
}
|
|
37
71
|
```
|
|
38
72
|
|
|
39
|
-
##
|
|
73
|
+
## Sending Messages
|
|
40
74
|
|
|
41
75
|
```tsx
|
|
42
|
-
const {
|
|
43
|
-
|
|
44
|
-
async function
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
await triggerAction('user-message', { USER_MESSAGE: text });
|
|
76
|
+
const { send } = useOctavusChat({...});
|
|
77
|
+
|
|
78
|
+
async function handleSend(text: string) {
|
|
79
|
+
// Add user message to UI and trigger agent
|
|
80
|
+
await send('user-message', { USER_MESSAGE: text }, {
|
|
81
|
+
userMessage: { content: text },
|
|
82
|
+
});
|
|
50
83
|
}
|
|
51
84
|
```
|
|
52
85
|
|
|
53
|
-
|
|
86
|
+
The `send` function:
|
|
87
|
+
1. Adds the user message to the UI immediately (if `userMessage` is provided)
|
|
88
|
+
2. Triggers the agent with the specified trigger name and input
|
|
89
|
+
3. Streams the assistant's response back
|
|
54
90
|
|
|
55
91
|
## Rendering Messages
|
|
56
92
|
|
|
57
93
|
### Basic Rendering
|
|
58
94
|
|
|
59
95
|
```tsx
|
|
60
|
-
function MessageList({ messages }: { messages:
|
|
96
|
+
function MessageList({ messages }: { messages: UIMessage[] }) {
|
|
61
97
|
return (
|
|
62
98
|
<div className="space-y-4">
|
|
63
99
|
{messages.map((msg) => (
|
|
64
|
-
<
|
|
65
|
-
key={msg.id}
|
|
66
|
-
className={msg.role === 'user' ? 'text-right' : 'text-left'}
|
|
67
|
-
>
|
|
68
|
-
<div className="inline-block p-3 rounded-lg">
|
|
69
|
-
{msg.content}
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
100
|
+
<MessageBubble key={msg.id} message={msg} />
|
|
72
101
|
))}
|
|
73
102
|
</div>
|
|
74
103
|
);
|
|
75
104
|
}
|
|
76
|
-
```
|
|
77
105
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
For messages with tool calls and reasoning, render parts in order:
|
|
81
|
-
|
|
82
|
-
```tsx
|
|
83
|
-
function MessageContent({ message }: { message: Message }) {
|
|
84
|
-
if (!message.parts) {
|
|
85
|
-
return <p>{message.content}</p>;
|
|
86
|
-
}
|
|
106
|
+
function MessageBubble({ message }: { message: UIMessage }) {
|
|
107
|
+
const isUser = message.role === 'user';
|
|
87
108
|
|
|
88
109
|
return (
|
|
89
|
-
<div className=
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return <p key={i}>{part.content}</p>;
|
|
96
|
-
|
|
97
|
-
case 'reasoning':
|
|
98
|
-
return (
|
|
99
|
-
<details key={i} className="text-gray-500">
|
|
100
|
-
<summary>Thinking...</summary>
|
|
101
|
-
<pre className="text-sm">{part.content}</pre>
|
|
102
|
-
</details>
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
case 'tool-call':
|
|
106
|
-
return (
|
|
107
|
-
<div key={i} className="bg-gray-100 p-2 rounded text-sm">
|
|
108
|
-
🔧 {part.toolCall?.name}
|
|
109
|
-
{part.toolCall?.status === 'available' && ' ✓'}
|
|
110
|
-
</div>
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
default:
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
})}
|
|
110
|
+
<div className={isUser ? 'text-right' : 'text-left'}>
|
|
111
|
+
<div className="inline-block p-3 rounded-lg">
|
|
112
|
+
{message.parts.map((part, i) => (
|
|
113
|
+
<PartRenderer key={i} part={part} />
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
117
116
|
</div>
|
|
118
117
|
);
|
|
119
118
|
}
|
|
120
119
|
```
|
|
121
120
|
|
|
122
|
-
|
|
121
|
+
### Rendering Parts
|
|
123
122
|
|
|
124
|
-
|
|
123
|
+
```tsx
|
|
124
|
+
import { isOtherThread, type UIMessagePart } from '@octavus/client-sdk';
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
function PartRenderer({ part }: { part: UIMessagePart }) {
|
|
127
|
+
// Check if part belongs to a named thread (e.g., "summary")
|
|
128
|
+
if (isOtherThread(part)) {
|
|
129
|
+
return <OtherThreadPart part={part} />;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
switch (part.type) {
|
|
133
|
+
case 'text':
|
|
134
|
+
return <TextPart part={part} />;
|
|
135
|
+
|
|
136
|
+
case 'reasoning':
|
|
137
|
+
return (
|
|
138
|
+
<details className="text-gray-500">
|
|
139
|
+
<summary>Thinking...</summary>
|
|
140
|
+
<pre className="text-sm">{part.text}</pre>
|
|
141
|
+
</details>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
case 'tool-call':
|
|
145
|
+
return (
|
|
146
|
+
<div className="bg-gray-100 p-2 rounded text-sm">
|
|
147
|
+
🔧 {part.displayName || part.toolName}
|
|
148
|
+
{part.status === 'done' && ' ✓'}
|
|
149
|
+
{part.status === 'error' && ` ✗ ${part.error}`}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
case 'operation':
|
|
154
|
+
return (
|
|
155
|
+
<div className="text-gray-500 text-sm">
|
|
156
|
+
{part.name}
|
|
157
|
+
{part.status === 'done' && ' ✓'}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
default:
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function TextPart({ part }: { part: UITextPart }) {
|
|
167
|
+
return (
|
|
168
|
+
<p>
|
|
169
|
+
{part.text}
|
|
170
|
+
{part.status === 'streaming' && (
|
|
171
|
+
<span className="inline-block w-2 h-4 bg-gray-400 animate-pulse ml-1" />
|
|
172
|
+
)}
|
|
173
|
+
</p>
|
|
174
|
+
);
|
|
135
175
|
}
|
|
136
176
|
```
|
|
137
177
|
|
|
138
|
-
|
|
178
|
+
## Named Threads
|
|
179
|
+
|
|
180
|
+
Content from named threads (like "summary") is identified by the `thread` property. Use the `isOtherThread` helper:
|
|
139
181
|
|
|
140
182
|
```tsx
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
183
|
+
import { isOtherThread } from '@octavus/client-sdk';
|
|
184
|
+
|
|
185
|
+
function PartRenderer({ part }: { part: UIMessagePart }) {
|
|
186
|
+
if (isOtherThread(part)) {
|
|
187
|
+
// Render differently for named threads
|
|
188
|
+
return (
|
|
189
|
+
<div className="bg-amber-50 p-2 rounded border border-amber-200">
|
|
190
|
+
<span className="text-amber-600 text-sm">
|
|
191
|
+
{part.thread}: {part.type === 'text' && part.text}
|
|
192
|
+
</span>
|
|
148
193
|
</div>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)}
|
|
155
|
-
|
|
156
|
-
{toolCall.status === 'error' && (
|
|
157
|
-
<p className="mt-2 text-red-500 text-sm">{toolCall.error}</p>
|
|
158
|
-
)}
|
|
159
|
-
</div>
|
|
160
|
-
);
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Regular rendering for main thread
|
|
198
|
+
// ...
|
|
161
199
|
}
|
|
162
200
|
```
|
|
163
201
|
|
|
@@ -169,38 +207,28 @@ When restoring a session, pass existing messages:
|
|
|
169
207
|
// Fetch session state from your backend
|
|
170
208
|
const sessionState = await fetchSession(sessionId);
|
|
171
209
|
|
|
172
|
-
// Convert ChatMessage[] to Message[]
|
|
173
|
-
const initialMessages = sessionState.messages
|
|
174
|
-
.filter(msg => msg.visible !== false)
|
|
175
|
-
.map(msg => ({
|
|
176
|
-
id: msg.id,
|
|
177
|
-
role: msg.role,
|
|
178
|
-
content: msg.content,
|
|
179
|
-
parts: msg.parts,
|
|
180
|
-
toolCalls: msg.toolCalls,
|
|
181
|
-
reasoning: msg.reasoning,
|
|
182
|
-
createdAt: new Date(msg.createdAt),
|
|
183
|
-
}));
|
|
184
|
-
|
|
185
210
|
// Pass to hook
|
|
186
211
|
const { messages } = useOctavusChat({
|
|
187
|
-
initialMessages,
|
|
212
|
+
initialMessages: sessionState.messages,
|
|
188
213
|
onTrigger: ...
|
|
189
214
|
});
|
|
190
215
|
```
|
|
191
216
|
|
|
192
|
-
##
|
|
193
|
-
|
|
194
|
-
Get notified when messages are added:
|
|
217
|
+
## Callbacks
|
|
195
218
|
|
|
196
219
|
```tsx
|
|
197
220
|
useOctavusChat({
|
|
198
221
|
onTrigger: ...,
|
|
199
|
-
|
|
200
|
-
console.log('
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
222
|
+
onFinish: () => {
|
|
223
|
+
console.log('Stream completed');
|
|
224
|
+
// Scroll to bottom, play sound, etc.
|
|
225
|
+
},
|
|
226
|
+
onError: (error) => {
|
|
227
|
+
console.error('Error:', error);
|
|
228
|
+
toast.error('Failed to get response');
|
|
229
|
+
},
|
|
230
|
+
onResourceUpdate: (name, value) => {
|
|
231
|
+
console.log('Resource updated:', name, value);
|
|
204
232
|
},
|
|
205
233
|
});
|
|
206
234
|
```
|
|
@@ -5,105 +5,129 @@ description: Building streaming UIs with the Client SDK.
|
|
|
5
5
|
|
|
6
6
|
# Streaming
|
|
7
7
|
|
|
8
|
-
The Client SDK provides real-time access to streaming content, enabling responsive UIs that update as the agent generates responses.
|
|
8
|
+
The Client SDK provides real-time access to streaming content through the message `parts` array. Each part has its own status, enabling responsive UIs that update as the agent generates responses.
|
|
9
9
|
|
|
10
10
|
## Streaming State
|
|
11
11
|
|
|
12
12
|
```tsx
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
isLoading, // true during loading or streaming
|
|
19
|
-
} = useOctavusChat({...});
|
|
13
|
+
const { messages, status, error } = useOctavusChat({...});
|
|
14
|
+
|
|
15
|
+
// status: 'idle' | 'streaming' | 'error'
|
|
16
|
+
// Each message has status: 'streaming' | 'done'
|
|
17
|
+
// Each part has its own status too
|
|
20
18
|
```
|
|
21
19
|
|
|
22
|
-
##
|
|
20
|
+
## Building a Streaming UI
|
|
23
21
|
|
|
24
22
|
```tsx
|
|
25
23
|
function Chat() {
|
|
26
|
-
const { messages,
|
|
24
|
+
const { messages, status, error, send, stop } = useOctavusChat({...});
|
|
27
25
|
|
|
28
26
|
return (
|
|
29
27
|
<div>
|
|
30
|
-
{/*
|
|
28
|
+
{/* Messages with streaming parts */}
|
|
31
29
|
{messages.map((msg) => (
|
|
32
|
-
<
|
|
30
|
+
<MessageBubble key={msg.id} message={msg} />
|
|
33
31
|
))}
|
|
34
32
|
|
|
35
|
-
{/*
|
|
36
|
-
{
|
|
37
|
-
<div className="
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
{/* Error state */}
|
|
34
|
+
{error && (
|
|
35
|
+
<div className="text-red-500">{error.message}</div>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
{/* Stop button during streaming */}
|
|
39
|
+
{status === 'streaming' && (
|
|
40
|
+
<button onClick={stop}>Stop</button>
|
|
41
41
|
)}
|
|
42
42
|
</div>
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
## Streaming Parts
|
|
47
|
+
## Rendering Streaming Parts
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Parts update in real-time during streaming. Use the part's `status` to show appropriate UI:
|
|
50
50
|
|
|
51
51
|
```tsx
|
|
52
|
-
|
|
53
|
-
const { streamingParts } = useOctavusChat({...});
|
|
52
|
+
import type { UITextPart, UIReasoningPart } from '@octavus/client-sdk';
|
|
54
53
|
|
|
54
|
+
function TextPart({ part }: { part: UITextPart }) {
|
|
55
55
|
return (
|
|
56
56
|
<div>
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
57
|
+
{part.text}
|
|
58
|
+
{part.status === 'streaming' && (
|
|
59
|
+
<span className="inline-block w-2 h-4 bg-gray-400 animate-pulse ml-1" />
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ReasoningPart({ part }: { part: UIReasoningPart }) {
|
|
66
|
+
// Expand while streaming, collapse when done
|
|
67
|
+
const [expanded, setExpanded] = useState(part.status === 'streaming');
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="bg-purple-50 p-3 rounded-lg">
|
|
71
|
+
<button onClick={() => setExpanded(!expanded)}>
|
|
72
|
+
{part.status === 'streaming' ? '💭 Thinking...' : '💭 Thought process'}
|
|
73
|
+
{expanded ? '▼' : '▶'}
|
|
74
|
+
</button>
|
|
75
|
+
|
|
76
|
+
{expanded && (
|
|
77
|
+
<pre className="mt-2 text-sm text-gray-600">
|
|
78
|
+
{part.text}
|
|
79
|
+
</pre>
|
|
80
|
+
)}
|
|
78
81
|
</div>
|
|
79
82
|
);
|
|
80
83
|
}
|
|
81
84
|
```
|
|
82
85
|
|
|
83
|
-
##
|
|
86
|
+
## Tool Call States
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
Tool calls progress through multiple states:
|
|
86
89
|
|
|
87
90
|
```tsx
|
|
88
|
-
|
|
89
|
-
const { reasoningText, status } = useOctavusChat({...});
|
|
90
|
-
|
|
91
|
-
if (!reasoningText || status !== 'streaming') {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
91
|
+
import type { UIToolCallPart } from '@octavus/client-sdk';
|
|
94
92
|
|
|
93
|
+
function ToolCallPart({ part }: { part: UIToolCallPart }) {
|
|
95
94
|
return (
|
|
96
|
-
<div className="
|
|
97
|
-
<div className="flex items-center gap-2
|
|
98
|
-
<
|
|
99
|
-
<span className="font-medium">
|
|
95
|
+
<div className="border rounded p-3">
|
|
96
|
+
<div className="flex items-center gap-2">
|
|
97
|
+
<span className="text-lg">🔧</span>
|
|
98
|
+
<span className="font-medium">
|
|
99
|
+
{part.displayName || part.toolName}
|
|
100
|
+
</span>
|
|
101
|
+
<StatusBadge status={part.status} />
|
|
100
102
|
</div>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
|
|
104
|
+
{/* Show result when done */}
|
|
105
|
+
{part.status === 'done' && part.result && (
|
|
106
|
+
<pre className="mt-2 text-xs bg-gray-50 p-2 rounded">
|
|
107
|
+
{JSON.stringify(part.result, null, 2)}
|
|
108
|
+
</pre>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{/* Show error if failed */}
|
|
112
|
+
{part.status === 'error' && (
|
|
113
|
+
<p className="mt-2 text-red-500 text-sm">{part.error}</p>
|
|
114
|
+
)}
|
|
104
115
|
</div>
|
|
105
116
|
);
|
|
106
117
|
}
|
|
118
|
+
|
|
119
|
+
function StatusBadge({ status }: { status: UIToolCallPart['status'] }) {
|
|
120
|
+
switch (status) {
|
|
121
|
+
case 'pending':
|
|
122
|
+
return <span className="text-gray-400">○</span>;
|
|
123
|
+
case 'running':
|
|
124
|
+
return <span className="text-blue-500 animate-spin">◐</span>;
|
|
125
|
+
case 'done':
|
|
126
|
+
return <span className="text-green-500">✓</span>;
|
|
127
|
+
case 'error':
|
|
128
|
+
return <span className="text-red-500">✗</span>;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
107
131
|
```
|
|
108
132
|
|
|
109
133
|
## Status Indicator
|
|
@@ -115,8 +139,6 @@ function StatusIndicator() {
|
|
|
115
139
|
switch (status) {
|
|
116
140
|
case 'idle':
|
|
117
141
|
return null;
|
|
118
|
-
case 'loading':
|
|
119
|
-
return <div>Starting...</div>;
|
|
120
142
|
case 'streaming':
|
|
121
143
|
return <div>Agent is responding...</div>;
|
|
122
144
|
case 'error':
|
|
@@ -125,12 +147,12 @@ function StatusIndicator() {
|
|
|
125
147
|
}
|
|
126
148
|
```
|
|
127
149
|
|
|
128
|
-
## Handling
|
|
150
|
+
## Handling Completion
|
|
129
151
|
|
|
130
152
|
```tsx
|
|
131
153
|
useOctavusChat({
|
|
132
154
|
onTrigger: ...,
|
|
133
|
-
|
|
155
|
+
onFinish: () => {
|
|
134
156
|
console.log('Stream completed');
|
|
135
157
|
// Scroll to bottom, play sound, etc.
|
|
136
158
|
},
|
|
@@ -141,71 +163,50 @@ useOctavusChat({
|
|
|
141
163
|
});
|
|
142
164
|
```
|
|
143
165
|
|
|
144
|
-
##
|
|
166
|
+
## Stop Function
|
|
145
167
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
```
|
|
149
|
-
1. User sends message
|
|
150
|
-
2. streamingText starts filling: "Let me look that up..."
|
|
151
|
-
3. Tool call starts (visible in streamingParts)
|
|
152
|
-
4. Tool executes (handled by server-sdk)
|
|
153
|
-
5. streamingText continues: "I found your account..."
|
|
154
|
-
6. Stream completes, message finalized
|
|
155
|
-
```
|
|
168
|
+
Stop the current stream and finalize any partial message:
|
|
156
169
|
|
|
157
170
|
```tsx
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return (
|
|
167
|
-
<div>
|
|
168
|
-
{messages.map(msg => <Message key={msg.id} message={msg} />)}
|
|
169
|
-
|
|
170
|
-
{/* Show streaming text */}
|
|
171
|
-
{streamingText && <p>{streamingText}</p>}
|
|
172
|
-
|
|
173
|
-
{/* Show active tool */}
|
|
174
|
-
{activeToolCall && (
|
|
175
|
-
<div className="flex items-center gap-2 text-blue-600">
|
|
176
|
-
<Spinner />
|
|
177
|
-
{activeToolCall.toolCall?.description}
|
|
178
|
-
</div>
|
|
179
|
-
)}
|
|
180
|
-
</div>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
171
|
+
const { status, stop } = useOctavusChat({...});
|
|
172
|
+
|
|
173
|
+
// Stop button
|
|
174
|
+
{status === 'streaming' && (
|
|
175
|
+
<button onClick={stop} className="text-gray-500">
|
|
176
|
+
Stop generating
|
|
177
|
+
</button>
|
|
178
|
+
)}
|
|
183
179
|
```
|
|
184
180
|
|
|
185
|
-
|
|
181
|
+
When `stop()` is called:
|
|
182
|
+
1. The current request is aborted
|
|
183
|
+
2. Any partial message is finalized with current content
|
|
184
|
+
3. Status changes to `'idle'`
|
|
185
|
+
|
|
186
|
+
## Named Thread Content
|
|
186
187
|
|
|
187
|
-
|
|
188
|
+
Content from named threads (like "summary") streams separately and is identified by the `thread` property:
|
|
188
189
|
|
|
189
190
|
```tsx
|
|
190
|
-
|
|
191
|
-
const { streamingParts } = useOctavusChat({...});
|
|
191
|
+
import { isOtherThread } from '@octavus/client-sdk';
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
193
|
+
function MessageBubble({ message }: { message: UIMessage }) {
|
|
194
|
+
// Separate main thread from named threads
|
|
195
|
+
const mainParts = message.parts.filter(p => !isOtherThread(p));
|
|
196
|
+
const otherParts = message.parts.filter(p => isOtherThread(p));
|
|
196
197
|
|
|
197
198
|
return (
|
|
198
199
|
<div>
|
|
199
200
|
{/* Main conversation */}
|
|
200
|
-
|
|
201
|
+
{mainParts.map((part, i) => <PartRenderer key={i} part={part} />)}
|
|
201
202
|
|
|
202
|
-
{/* Named thread (e.g., summarization) */}
|
|
203
|
+
{/* Named thread content (e.g., summarization) */}
|
|
203
204
|
{otherParts.length > 0 && (
|
|
204
|
-
<div className="bg-
|
|
205
|
-
<div className="text-
|
|
206
|
-
|
|
205
|
+
<div className="bg-amber-50 p-3 rounded mt-4 border border-amber-200">
|
|
206
|
+
<div className="text-amber-600 font-medium mb-2">
|
|
207
|
+
Background processing
|
|
207
208
|
</div>
|
|
208
|
-
{otherParts.map(
|
|
209
|
+
{otherParts.map((part, i) => <PartRenderer key={i} part={part} />)}
|
|
209
210
|
</div>
|
|
210
211
|
)}
|
|
211
212
|
</div>
|