@octavus/docs 0.0.9 → 1.0.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/README.md +127 -0
- package/content/01-getting-started/01-introduction.md +3 -3
- package/content/01-getting-started/02-quickstart.md +40 -17
- package/content/02-server-sdk/01-overview.md +54 -11
- package/content/02-server-sdk/02-sessions.md +166 -15
- package/content/02-server-sdk/03-tools.md +21 -21
- package/content/02-server-sdk/04-streaming.md +50 -20
- package/content/02-server-sdk/05-cli.md +247 -0
- package/content/03-client-sdk/01-overview.md +65 -35
- package/content/03-client-sdk/02-messages.md +116 -8
- package/content/03-client-sdk/03-streaming.md +36 -17
- package/content/03-client-sdk/04-execution-blocks.md +8 -12
- package/content/03-client-sdk/05-socket-transport.md +161 -45
- package/content/03-client-sdk/06-http-transport.md +48 -24
- package/content/03-client-sdk/07-structured-output.md +412 -0
- package/content/03-client-sdk/08-file-uploads.md +473 -0
- package/content/03-client-sdk/09-error-handling.md +274 -0
- package/content/04-protocol/01-overview.md +24 -14
- package/content/04-protocol/02-input-resources.md +35 -35
- package/content/04-protocol/03-triggers.md +9 -11
- package/content/04-protocol/04-tools.md +63 -29
- package/content/04-protocol/05-skills.md +304 -0
- package/content/04-protocol/06-handlers.md +304 -0
- package/content/04-protocol/07-agent-config.md +334 -0
- package/content/04-protocol/{07-provider-options.md → 08-provider-options.md} +54 -35
- package/content/04-protocol/09-skills-advanced.md +439 -0
- package/content/04-protocol/10-types.md +719 -0
- package/content/05-api-reference/01-overview.md +20 -21
- package/content/05-api-reference/02-sessions.md +192 -37
- package/content/05-api-reference/03-agents.md +25 -37
- package/content/06-examples/01-overview.md +6 -4
- package/content/06-examples/02-nextjs-chat.md +28 -18
- package/content/06-examples/03-socket-chat.md +53 -30
- package/content/06-examples/_meta.md +0 -1
- package/dist/chunk-WJ2W3DUC.js +663 -0
- package/dist/chunk-WJ2W3DUC.js.map +1 -0
- package/dist/content.js +1 -1
- package/dist/docs.json +99 -36
- package/dist/index.js +1 -1
- package/dist/search-index.json +1 -1
- package/dist/search.js +1 -1
- package/dist/search.js.map +1 -1
- package/dist/sections.json +99 -36
- package/package.json +12 -2
- package/content/04-protocol/05-handlers.md +0 -251
- package/content/04-protocol/06-agent-config.md +0 -242
- package/dist/chunk-232K4EME.js +0 -439
- package/dist/chunk-232K4EME.js.map +0 -1
- package/dist/chunk-2JDZLMS3.js +0 -439
- package/dist/chunk-2JDZLMS3.js.map +0 -1
- package/dist/chunk-2YMRODFE.js +0 -421
- package/dist/chunk-2YMRODFE.js.map +0 -1
- package/dist/chunk-2ZBPX5QB.js +0 -421
- package/dist/chunk-2ZBPX5QB.js.map +0 -1
- package/dist/chunk-3PIIST4D.js +0 -421
- package/dist/chunk-3PIIST4D.js.map +0 -1
- package/dist/chunk-42JETGDO.js +0 -421
- package/dist/chunk-42JETGDO.js.map +0 -1
- package/dist/chunk-4WWUKU4V.js +0 -421
- package/dist/chunk-4WWUKU4V.js.map +0 -1
- package/dist/chunk-5M7DS4DF.js +0 -519
- package/dist/chunk-5M7DS4DF.js.map +0 -1
- package/dist/chunk-6JQ3OMGF.js +0 -421
- package/dist/chunk-6JQ3OMGF.js.map +0 -1
- package/dist/chunk-7AOWCJHW.js +0 -421
- package/dist/chunk-7AOWCJHW.js.map +0 -1
- package/dist/chunk-7AS4ST73.js +0 -421
- package/dist/chunk-7AS4ST73.js.map +0 -1
- package/dist/chunk-7F5WOCIL.js +0 -421
- package/dist/chunk-7F5WOCIL.js.map +0 -1
- package/dist/chunk-7FPUAWSX.js +0 -421
- package/dist/chunk-7FPUAWSX.js.map +0 -1
- package/dist/chunk-7KXF63FV.js +0 -537
- package/dist/chunk-7KXF63FV.js.map +0 -1
- package/dist/chunk-APASMJBS.js +0 -421
- package/dist/chunk-APASMJBS.js.map +0 -1
- package/dist/chunk-BCEV3WV2.js +0 -421
- package/dist/chunk-BCEV3WV2.js.map +0 -1
- package/dist/chunk-CHGY4G27.js +0 -421
- package/dist/chunk-CHGY4G27.js.map +0 -1
- package/dist/chunk-CI7JDWKU.js +0 -421
- package/dist/chunk-CI7JDWKU.js.map +0 -1
- package/dist/chunk-CVFWWRL7.js +0 -421
- package/dist/chunk-CVFWWRL7.js.map +0 -1
- package/dist/chunk-EPDM2NIJ.js +0 -421
- package/dist/chunk-EPDM2NIJ.js.map +0 -1
- package/dist/chunk-ESGSYVGK.js +0 -421
- package/dist/chunk-ESGSYVGK.js.map +0 -1
- package/dist/chunk-GDCTM2SV.js +0 -421
- package/dist/chunk-GDCTM2SV.js.map +0 -1
- package/dist/chunk-GJ6FMIPD.js +0 -421
- package/dist/chunk-GJ6FMIPD.js.map +0 -1
- package/dist/chunk-H6JGSSAJ.js +0 -519
- package/dist/chunk-H6JGSSAJ.js.map +0 -1
- package/dist/chunk-IKQHGGUZ.js +0 -421
- package/dist/chunk-IKQHGGUZ.js.map +0 -1
- package/dist/chunk-IUKE3XDN.js +0 -421
- package/dist/chunk-IUKE3XDN.js.map +0 -1
- package/dist/chunk-J26MLMLN.js +0 -421
- package/dist/chunk-J26MLMLN.js.map +0 -1
- package/dist/chunk-J7BMB3ZW.js +0 -421
- package/dist/chunk-J7BMB3ZW.js.map +0 -1
- package/dist/chunk-JCBQRD5N.js +0 -421
- package/dist/chunk-JCBQRD5N.js.map +0 -1
- package/dist/chunk-JOB6YWEF.js +0 -421
- package/dist/chunk-JOB6YWEF.js.map +0 -1
- package/dist/chunk-JZRABTHU.js +0 -519
- package/dist/chunk-JZRABTHU.js.map +0 -1
- package/dist/chunk-K3GFQUMC.js +0 -421
- package/dist/chunk-K3GFQUMC.js.map +0 -1
- package/dist/chunk-LWYMRXBF.js +0 -421
- package/dist/chunk-LWYMRXBF.js.map +0 -1
- package/dist/chunk-M2R2NDPR.js +0 -421
- package/dist/chunk-M2R2NDPR.js.map +0 -1
- package/dist/chunk-MA3P7WCA.js +0 -421
- package/dist/chunk-MA3P7WCA.js.map +0 -1
- package/dist/chunk-MDMRCS4W.mjs +0 -421
- package/dist/chunk-MDMRCS4W.mjs.map +0 -1
- package/dist/chunk-MJXTA2R6.js +0 -421
- package/dist/chunk-MJXTA2R6.js.map +0 -1
- package/dist/chunk-NFVJQNDP.js +0 -421
- package/dist/chunk-NFVJQNDP.js.map +0 -1
- package/dist/chunk-O5TLYMQP.js +0 -421
- package/dist/chunk-O5TLYMQP.js.map +0 -1
- package/dist/chunk-OECAPVSX.js +0 -439
- package/dist/chunk-OECAPVSX.js.map +0 -1
- package/dist/chunk-OL5QDJ42.js +0 -483
- package/dist/chunk-OL5QDJ42.js.map +0 -1
- package/dist/chunk-PMOVVTHO.js +0 -519
- package/dist/chunk-PMOVVTHO.js.map +0 -1
- package/dist/chunk-QCHDPR2D.js +0 -421
- package/dist/chunk-QCHDPR2D.js.map +0 -1
- package/dist/chunk-R5MTVABN.js +0 -439
- package/dist/chunk-R5MTVABN.js.map +0 -1
- package/dist/chunk-RJ4H4YVA.js +0 -519
- package/dist/chunk-RJ4H4YVA.js.map +0 -1
- package/dist/chunk-S5U4IWCR.js +0 -439
- package/dist/chunk-S5U4IWCR.js.map +0 -1
- package/dist/chunk-SCKIOGKI.js +0 -421
- package/dist/chunk-SCKIOGKI.js.map +0 -1
- package/dist/chunk-TGJSIJYP.js +0 -421
- package/dist/chunk-TGJSIJYP.js.map +0 -1
- package/dist/chunk-TQJG6EBM.js +0 -537
- package/dist/chunk-TQJG6EBM.js.map +0 -1
- package/dist/chunk-TQZRBMU7.js +0 -421
- package/dist/chunk-TQZRBMU7.js.map +0 -1
- package/dist/chunk-TRL4RSEO.js +0 -421
- package/dist/chunk-TRL4RSEO.js.map +0 -1
- package/dist/chunk-TWUMRHQ7.js +0 -421
- package/dist/chunk-TWUMRHQ7.js.map +0 -1
- package/dist/chunk-UCJE36LL.js +0 -519
- package/dist/chunk-UCJE36LL.js.map +0 -1
- package/dist/chunk-VCASA6KL.js +0 -421
- package/dist/chunk-VCASA6KL.js.map +0 -1
- package/dist/chunk-VWPQ6ORV.js +0 -421
- package/dist/chunk-VWPQ6ORV.js.map +0 -1
- package/dist/chunk-WPXKIHLT.js +0 -421
- package/dist/chunk-WPXKIHLT.js.map +0 -1
- package/dist/chunk-WUNFFJ32.js +0 -421
- package/dist/chunk-WUNFFJ32.js.map +0 -1
- package/dist/chunk-WW7TRC7S.js +0 -519
- package/dist/chunk-WW7TRC7S.js.map +0 -1
- package/dist/chunk-XVSMRXBJ.js +0 -421
- package/dist/chunk-XVSMRXBJ.js.map +0 -1
- package/dist/chunk-YPPXXV3I.js +0 -421
- package/dist/chunk-YPPXXV3I.js.map +0 -1
- package/dist/chunk-ZKZVV4OQ.js +0 -421
- package/dist/chunk-ZKZVV4OQ.js.map +0 -1
- package/dist/chunk-ZOFEX73I.js +0 -421
- package/dist/chunk-ZOFEX73I.js.map +0 -1
- package/dist/content.mjs +0 -17
- package/dist/content.mjs.map +0 -1
- package/dist/index.mjs +0 -11
- package/dist/index.mjs.map +0 -1
- package/dist/search.mjs +0 -30
- package/dist/search.mjs.map +0 -1
- package/dist/types-BltYGlWI.d.ts +0 -36
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: File Uploads
|
|
3
|
+
description: Uploading images and files for vision models and document processing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# File Uploads
|
|
7
|
+
|
|
8
|
+
The Client SDK supports uploading images and documents that can be sent with messages. This enables vision model capabilities (analyzing images) and document processing.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
File uploads follow a two-step flow:
|
|
13
|
+
|
|
14
|
+
1. **Request upload URLs** from the platform via your backend
|
|
15
|
+
2. **Upload files directly to S3** using presigned URLs
|
|
16
|
+
3. **Send file references** with your message
|
|
17
|
+
|
|
18
|
+
This architecture keeps your API key secure on the server while enabling fast, direct uploads.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
### Backend: Upload URLs Endpoint
|
|
23
|
+
|
|
24
|
+
Create an endpoint that proxies upload URL requests to the Octavus platform:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// app/api/upload-urls/route.ts (Next.js)
|
|
28
|
+
import { NextResponse } from 'next/server';
|
|
29
|
+
import { octavus } from '@/lib/octavus';
|
|
30
|
+
|
|
31
|
+
export async function POST(request: Request) {
|
|
32
|
+
const { sessionId, files } = await request.json();
|
|
33
|
+
|
|
34
|
+
// Get presigned URLs from Octavus
|
|
35
|
+
const result = await octavus.files.getUploadUrls(sessionId, files);
|
|
36
|
+
|
|
37
|
+
return NextResponse.json(result);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Client: Configure File Uploads
|
|
42
|
+
|
|
43
|
+
Pass `requestUploadUrls` to the chat hook:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { useMemo, useCallback } from 'react';
|
|
47
|
+
import { useOctavusChat, createHttpTransport } from '@octavus/react';
|
|
48
|
+
|
|
49
|
+
function Chat({ sessionId }: { sessionId: string }) {
|
|
50
|
+
const transport = useMemo(
|
|
51
|
+
() =>
|
|
52
|
+
createHttpTransport({
|
|
53
|
+
triggerRequest: (triggerName, input) =>
|
|
54
|
+
fetch('/api/trigger', {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({ sessionId, triggerName, input }),
|
|
58
|
+
}),
|
|
59
|
+
}),
|
|
60
|
+
[sessionId],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Request upload URLs from your backend
|
|
64
|
+
const requestUploadUrls = useCallback(
|
|
65
|
+
async (files: { filename: string; mediaType: string; size: number }[]) => {
|
|
66
|
+
const response = await fetch('/api/upload-urls', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
body: JSON.stringify({ sessionId, files }),
|
|
70
|
+
});
|
|
71
|
+
return response.json();
|
|
72
|
+
},
|
|
73
|
+
[sessionId],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const { messages, status, send, uploadFiles } = useOctavusChat({
|
|
77
|
+
transport,
|
|
78
|
+
requestUploadUrls,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ...
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Uploading Files
|
|
86
|
+
|
|
87
|
+
### Method 1: Upload Before Sending
|
|
88
|
+
|
|
89
|
+
For the best UX (showing upload progress), upload files first, then send:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { useState, useRef } from 'react';
|
|
93
|
+
import type { FileReference } from '@octavus/react';
|
|
94
|
+
|
|
95
|
+
function ChatInput({ sessionId }: { sessionId: string }) {
|
|
96
|
+
const [pendingFiles, setPendingFiles] = useState<FileReference[]>([]);
|
|
97
|
+
const [uploading, setUploading] = useState(false);
|
|
98
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
99
|
+
|
|
100
|
+
const { send, uploadFiles } = useOctavusChat({
|
|
101
|
+
transport,
|
|
102
|
+
requestUploadUrls,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
async function handleFileSelect(event: React.ChangeEvent<HTMLInputElement>) {
|
|
106
|
+
const files = event.target.files;
|
|
107
|
+
if (!files?.length) return;
|
|
108
|
+
|
|
109
|
+
setUploading(true);
|
|
110
|
+
try {
|
|
111
|
+
// Upload files with progress tracking
|
|
112
|
+
const fileRefs = await uploadFiles(files, (fileIndex, progress) => {
|
|
113
|
+
console.log(`File ${fileIndex}: ${progress}%`);
|
|
114
|
+
});
|
|
115
|
+
setPendingFiles((prev) => [...prev, ...fileRefs]);
|
|
116
|
+
} finally {
|
|
117
|
+
setUploading(false);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function handleSend(message: string) {
|
|
122
|
+
await send(
|
|
123
|
+
'user-message',
|
|
124
|
+
{
|
|
125
|
+
USER_MESSAGE: message,
|
|
126
|
+
FILES: pendingFiles.length > 0 ? pendingFiles : undefined,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
userMessage: {
|
|
130
|
+
content: message,
|
|
131
|
+
files: pendingFiles.length > 0 ? pendingFiles : undefined,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
setPendingFiles([]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div>
|
|
140
|
+
{/* File preview */}
|
|
141
|
+
{pendingFiles.map((file) => (
|
|
142
|
+
<img key={file.id} src={file.url} alt={file.filename} className="h-16" />
|
|
143
|
+
))}
|
|
144
|
+
|
|
145
|
+
<input
|
|
146
|
+
ref={fileInputRef}
|
|
147
|
+
type="file"
|
|
148
|
+
accept="image/*,.pdf"
|
|
149
|
+
multiple
|
|
150
|
+
onChange={handleFileSelect}
|
|
151
|
+
className="hidden"
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
<button onClick={() => fileInputRef.current?.click()} disabled={uploading}>
|
|
155
|
+
{uploading ? 'Uploading...' : 'Attach'}
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Method 2: Upload on Send (Automatic)
|
|
163
|
+
|
|
164
|
+
For simpler implementations, pass `File` objects directly:
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
async function handleSend(message: string, files?: File[]) {
|
|
168
|
+
await send(
|
|
169
|
+
'user-message',
|
|
170
|
+
{ USER_MESSAGE: message, FILES: files },
|
|
171
|
+
{ userMessage: { content: message, files } },
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The SDK automatically uploads the files before sending. Note: This doesn't provide upload progress.
|
|
177
|
+
|
|
178
|
+
## FileReference Type
|
|
179
|
+
|
|
180
|
+
File references contain metadata and URLs:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
interface FileReference {
|
|
184
|
+
/** Unique file ID (platform-generated) */
|
|
185
|
+
id: string;
|
|
186
|
+
/** IANA media type (e.g., 'image/png', 'application/pdf') */
|
|
187
|
+
mediaType: string;
|
|
188
|
+
/** Presigned download URL (S3) */
|
|
189
|
+
url: string;
|
|
190
|
+
/** Original filename */
|
|
191
|
+
filename?: string;
|
|
192
|
+
/** File size in bytes */
|
|
193
|
+
size?: number;
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Protocol Integration
|
|
198
|
+
|
|
199
|
+
To accept files in your agent protocol, use the `file[]` type:
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
triggers:
|
|
203
|
+
user-message:
|
|
204
|
+
input:
|
|
205
|
+
USER_MESSAGE:
|
|
206
|
+
type: string
|
|
207
|
+
description: The user's message
|
|
208
|
+
FILES:
|
|
209
|
+
type: file[]
|
|
210
|
+
optional: true
|
|
211
|
+
description: User-attached images for vision analysis
|
|
212
|
+
|
|
213
|
+
handlers:
|
|
214
|
+
user-message:
|
|
215
|
+
Add user message:
|
|
216
|
+
block: add-message
|
|
217
|
+
role: user
|
|
218
|
+
prompt: user-message
|
|
219
|
+
input:
|
|
220
|
+
- USER_MESSAGE
|
|
221
|
+
files:
|
|
222
|
+
- FILES # Attach files to the message
|
|
223
|
+
display: hidden
|
|
224
|
+
|
|
225
|
+
Respond to user:
|
|
226
|
+
block: next-message
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The `file` type is a built-in type representing uploaded files. Use `file[]` for arrays of files.
|
|
230
|
+
|
|
231
|
+
## Supported File Types
|
|
232
|
+
|
|
233
|
+
| Type | Media Types |
|
|
234
|
+
| --------- | -------------------------------------------------------------------- |
|
|
235
|
+
| Images | `image/jpeg`, `image/png`, `image/gif`, `image/webp` |
|
|
236
|
+
| Documents | `application/pdf`, `text/plain`, `text/markdown`, `application/json` |
|
|
237
|
+
|
|
238
|
+
## File Limits
|
|
239
|
+
|
|
240
|
+
| Limit | Value |
|
|
241
|
+
| --------------------- | ---------- |
|
|
242
|
+
| Max file size | 10 MB |
|
|
243
|
+
| Max total per request | 50 MB |
|
|
244
|
+
| Max files per request | 20 |
|
|
245
|
+
| Upload URL expiry | 15 minutes |
|
|
246
|
+
| Download URL expiry | 24 hours |
|
|
247
|
+
|
|
248
|
+
## Rendering User Files
|
|
249
|
+
|
|
250
|
+
User-uploaded files appear as `UIFilePart` in user messages:
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
function UserMessage({ message }: { message: UIMessage }) {
|
|
254
|
+
return (
|
|
255
|
+
<div>
|
|
256
|
+
{message.parts.map((part, i) => {
|
|
257
|
+
if (part.type === 'file') {
|
|
258
|
+
if (part.mediaType.startsWith('image/')) {
|
|
259
|
+
return (
|
|
260
|
+
<img
|
|
261
|
+
key={i}
|
|
262
|
+
src={part.url}
|
|
263
|
+
alt={part.filename || 'Uploaded image'}
|
|
264
|
+
className="max-h-48 rounded-lg"
|
|
265
|
+
/>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return (
|
|
269
|
+
<a key={i} href={part.url} className="text-blue-500">
|
|
270
|
+
📄 {part.filename}
|
|
271
|
+
</a>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (part.type === 'text') {
|
|
275
|
+
return <p key={i}>{part.text}</p>;
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
})}
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Server SDK: Files API
|
|
285
|
+
|
|
286
|
+
The Server SDK provides direct access to the Files API:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import { OctavusClient } from '@octavus/server-sdk';
|
|
290
|
+
|
|
291
|
+
const client = new OctavusClient({
|
|
292
|
+
baseUrl: 'https://octavus.ai',
|
|
293
|
+
apiKey: 'your-api-key',
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Get presigned upload URLs
|
|
297
|
+
const { files } = await client.files.getUploadUrls(sessionId, [
|
|
298
|
+
{ filename: 'photo.jpg', mediaType: 'image/jpeg', size: 102400 },
|
|
299
|
+
{ filename: 'doc.pdf', mediaType: 'application/pdf', size: 204800 },
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
// files[0].id - Use in FileReference
|
|
303
|
+
// files[0].uploadUrl - PUT to this URL to upload
|
|
304
|
+
// files[0].downloadUrl - Use as FileReference.url
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Complete Example
|
|
308
|
+
|
|
309
|
+
Here's a full chat input component with file upload:
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
'use client';
|
|
313
|
+
|
|
314
|
+
import { useState, useRef, useMemo, useCallback } from 'react';
|
|
315
|
+
import { useOctavusChat, createHttpTransport, type FileReference } from '@octavus/react';
|
|
316
|
+
|
|
317
|
+
interface PendingFile {
|
|
318
|
+
file: File;
|
|
319
|
+
id: string;
|
|
320
|
+
status: 'uploading' | 'done' | 'error';
|
|
321
|
+
progress: number;
|
|
322
|
+
fileRef?: FileReference;
|
|
323
|
+
error?: string;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function Chat({ sessionId }: { sessionId: string }) {
|
|
327
|
+
const [input, setInput] = useState('');
|
|
328
|
+
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
|
329
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
330
|
+
const fileIdCounter = useRef(0);
|
|
331
|
+
|
|
332
|
+
const transport = useMemo(
|
|
333
|
+
() =>
|
|
334
|
+
createHttpTransport({
|
|
335
|
+
triggerRequest: (triggerName, input) =>
|
|
336
|
+
fetch('/api/trigger', {
|
|
337
|
+
method: 'POST',
|
|
338
|
+
headers: { 'Content-Type': 'application/json' },
|
|
339
|
+
body: JSON.stringify({ sessionId, triggerName, input }),
|
|
340
|
+
}),
|
|
341
|
+
}),
|
|
342
|
+
[sessionId],
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const requestUploadUrls = useCallback(
|
|
346
|
+
async (files: { filename: string; mediaType: string; size: number }[]) => {
|
|
347
|
+
const res = await fetch('/api/upload-urls', {
|
|
348
|
+
method: 'POST',
|
|
349
|
+
headers: { 'Content-Type': 'application/json' },
|
|
350
|
+
body: JSON.stringify({ sessionId, files }),
|
|
351
|
+
});
|
|
352
|
+
return res.json();
|
|
353
|
+
},
|
|
354
|
+
[sessionId],
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const { messages, status, send, uploadFiles } = useOctavusChat({
|
|
358
|
+
transport,
|
|
359
|
+
requestUploadUrls,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const isUploading = pendingFiles.some((f) => f.status === 'uploading');
|
|
363
|
+
const hasErrors = pendingFiles.some((f) => f.status === 'error');
|
|
364
|
+
const allReady = pendingFiles.every((f) => f.status === 'done');
|
|
365
|
+
|
|
366
|
+
async function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
|
|
367
|
+
const files = Array.from(e.target.files ?? []);
|
|
368
|
+
if (!files.length) return;
|
|
369
|
+
e.target.value = '';
|
|
370
|
+
|
|
371
|
+
const newPending: PendingFile[] = files.map((file) => ({
|
|
372
|
+
file,
|
|
373
|
+
id: `pending-${++fileIdCounter.current}`,
|
|
374
|
+
status: 'uploading',
|
|
375
|
+
progress: 0,
|
|
376
|
+
}));
|
|
377
|
+
|
|
378
|
+
setPendingFiles((prev) => [...prev, ...newPending]);
|
|
379
|
+
|
|
380
|
+
for (const pending of newPending) {
|
|
381
|
+
try {
|
|
382
|
+
const [fileRef] = await uploadFiles([pending.file], (_, progress) => {
|
|
383
|
+
setPendingFiles((prev) =>
|
|
384
|
+
prev.map((f) => (f.id === pending.id ? { ...f, progress } : f)),
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
setPendingFiles((prev) =>
|
|
388
|
+
prev.map((f) => (f.id === pending.id ? { ...f, status: 'done', fileRef } : f)),
|
|
389
|
+
);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
setPendingFiles((prev) =>
|
|
392
|
+
prev.map((f) =>
|
|
393
|
+
f.id === pending.id ? { ...f, status: 'error', error: String(err) } : f,
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function handleSubmit() {
|
|
401
|
+
if ((!input.trim() && !pendingFiles.length) || !allReady) return;
|
|
402
|
+
|
|
403
|
+
const fileRefs = pendingFiles.filter((f) => f.fileRef).map((f) => f.fileRef!);
|
|
404
|
+
|
|
405
|
+
await send(
|
|
406
|
+
'user-message',
|
|
407
|
+
{
|
|
408
|
+
USER_MESSAGE: input,
|
|
409
|
+
FILES: fileRefs.length > 0 ? fileRefs : undefined,
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
userMessage: {
|
|
413
|
+
content: input,
|
|
414
|
+
files: fileRefs.length > 0 ? fileRefs : undefined,
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
setInput('');
|
|
420
|
+
setPendingFiles([]);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<div>
|
|
425
|
+
{/* Messages */}
|
|
426
|
+
{messages.map((msg) => (
|
|
427
|
+
<div key={msg.id}>{/* ... render message */}</div>
|
|
428
|
+
))}
|
|
429
|
+
|
|
430
|
+
{/* Pending files */}
|
|
431
|
+
{pendingFiles.length > 0 && (
|
|
432
|
+
<div className="flex gap-2">
|
|
433
|
+
{pendingFiles.map((f) => (
|
|
434
|
+
<div key={f.id} className="relative">
|
|
435
|
+
<img
|
|
436
|
+
src={URL.createObjectURL(f.file)}
|
|
437
|
+
alt={f.file.name}
|
|
438
|
+
className="h-16 w-16 object-cover rounded"
|
|
439
|
+
/>
|
|
440
|
+
{f.status === 'uploading' && (
|
|
441
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
|
|
442
|
+
<span className="text-white text-xs">{f.progress}%</span>
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
<button
|
|
446
|
+
onClick={() => setPendingFiles((prev) => prev.filter((p) => p.id !== f.id))}
|
|
447
|
+
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5"
|
|
448
|
+
>
|
|
449
|
+
×
|
|
450
|
+
</button>
|
|
451
|
+
</div>
|
|
452
|
+
))}
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{/* Input */}
|
|
457
|
+
<div className="flex gap-2">
|
|
458
|
+
<input type="file" ref={fileInputRef} onChange={handleFileSelect} hidden />
|
|
459
|
+
<button onClick={() => fileInputRef.current?.click()}>📎</button>
|
|
460
|
+
<input
|
|
461
|
+
value={input}
|
|
462
|
+
onChange={(e) => setInput(e.target.value)}
|
|
463
|
+
placeholder="Type a message..."
|
|
464
|
+
className="flex-1"
|
|
465
|
+
/>
|
|
466
|
+
<button onClick={handleSubmit} disabled={isUploading || hasErrors}>
|
|
467
|
+
{isUploading ? 'Uploading...' : 'Send'}
|
|
468
|
+
</button>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
```
|