orchid-ai 2.1.4 → 2.2.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/orchid-ai.css +297 -4
- package/package.json +1 -1
- package/src/components/ChatInput.jsx +199 -43
- package/src/components/ChatWindow.jsx +62 -3
- package/src/components/Message.jsx +213 -8
- package/src/components/visualizations/DataTable.jsx +48 -1
- package/src/hooks/useOrchidAiChat.js +236 -145
- package/src/index.d.ts +45 -2
|
@@ -10,13 +10,6 @@ import {
|
|
|
10
10
|
orchidAiProcessTraceHasDisplayableContent,
|
|
11
11
|
} from '../orchidAiProcessTrace';
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* Default status message strings. Export these so server-side code can import
|
|
15
|
-
* and reference the same values, making it easy to keep client and server in sync
|
|
16
|
-
* or swap them out per-app.
|
|
17
|
-
*
|
|
18
|
-
* Set showStatus: false on the hook to suppress status display entirely.
|
|
19
|
-
*/
|
|
20
13
|
export const ORCHID_AI_DEFAULT_STATUS = {
|
|
21
14
|
thinking: 'Thinking',
|
|
22
15
|
compilingResponse: ORCHID_AI_SSE_STATUS_CLEAR_STREAM,
|
|
@@ -24,25 +17,29 @@ export const ORCHID_AI_DEFAULT_STATUS = {
|
|
|
24
17
|
};
|
|
25
18
|
|
|
26
19
|
/**
|
|
27
|
-
* React hook for Orchid AI chat. Handles SSE streaming
|
|
28
|
-
*
|
|
29
|
-
*
|
|
20
|
+
* React hook for Orchid AI chat. Handles SSE streaming, stop-generation, and
|
|
21
|
+
* multi-slot request queuing (messages submitted while loading are held out of
|
|
22
|
+
* chat history until they fire, then inserted in order).
|
|
30
23
|
*
|
|
31
24
|
* @param {object} opts
|
|
32
|
-
* @param {string} opts.endpoint
|
|
33
|
-
* @param {(userMessage: string, history: Array, sendOptions?: object) => object} opts.buildBody
|
|
34
|
-
* @param {() => object} [opts.getHeaders]
|
|
35
|
-
* @param {boolean} [opts.showStatus=true]
|
|
36
|
-
* @param {Array} [opts.initialMessages=[]]
|
|
25
|
+
* @param {string} opts.endpoint
|
|
26
|
+
* @param {(userMessage: string, history: Array, sendOptions?: object) => object} opts.buildBody
|
|
27
|
+
* @param {() => object} [opts.getHeaders]
|
|
28
|
+
* @param {boolean} [opts.showStatus=true]
|
|
29
|
+
* @param {Array} [opts.initialMessages=[]]
|
|
37
30
|
*
|
|
38
|
-
* @returns {{ messages, loading, statusText, sendMessage, clearMessages }}
|
|
31
|
+
* @returns {{ messages, loading, statusText, queuedMessages, sendMessage, stopGeneration, clearQueuedMessage, clearMessages }}
|
|
39
32
|
*/
|
|
40
33
|
export function useOrchidAiChat({ endpoint, buildBody, getHeaders, showStatus = true, initialMessages = [] }) {
|
|
41
34
|
const [messages, setMessages] = useState(initialMessages);
|
|
42
35
|
const [loading, setLoading] = useState(false);
|
|
43
36
|
const [statusText, setStatusText] = useState('');
|
|
37
|
+
const [queuedMessages, setQueuedMessages] = useState([]);
|
|
44
38
|
|
|
45
39
|
const messagesRef = useRef(initialMessages);
|
|
40
|
+
const abortRef = useRef(null);
|
|
41
|
+
// Array of { message, sendOptions } held back from chat history until each fires.
|
|
42
|
+
const queueRef = useRef([]);
|
|
46
43
|
|
|
47
44
|
const buildBodyRef = useRef(buildBody);
|
|
48
45
|
const getHeadersRef = useRef(getHeaders);
|
|
@@ -57,149 +54,243 @@ export function useOrchidAiChat({ endpoint, buildBody, getHeaders, showStatus =
|
|
|
57
54
|
});
|
|
58
55
|
}, []);
|
|
59
56
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
addMessage({ role: 'user', content: userMessage });
|
|
64
|
-
setLoading(true);
|
|
65
|
-
setStatusText('');
|
|
57
|
+
const stopGeneration = useCallback(() => {
|
|
58
|
+
abortRef.current?.abort();
|
|
59
|
+
}, []);
|
|
66
60
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
61
|
+
const clearQueuedMessage = useCallback((index) => {
|
|
62
|
+
if (typeof index === 'number') {
|
|
63
|
+
queueRef.current = queueRef.current.filter((_, i) => i !== index);
|
|
64
|
+
} else {
|
|
65
|
+
queueRef.current = [];
|
|
66
|
+
}
|
|
67
|
+
setQueuedMessages(queueRef.current.map((q) => q.message));
|
|
68
|
+
}, []);
|
|
76
69
|
|
|
77
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Internal — makes the actual API call without touching message history.
|
|
72
|
+
* `history` is a snapshot captured before the user message was appended.
|
|
73
|
+
*/
|
|
74
|
+
const _doRequest = useCallback(async (userMessage, sendOptions, history) => {
|
|
75
|
+
setLoading(true);
|
|
76
|
+
setStatusText('');
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const collector = createOrchidAiProcessTraceCollector();
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
abortRef.current = controller;
|
|
80
|
+
let collector = null;
|
|
81
|
+
let rafId = null;
|
|
84
82
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(endpoint, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
...getHeadersRef.current?.(),
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify(buildBodyRef.current(userMessage, history, sendOptions)),
|
|
91
|
+
signal: controller.signal,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
95
|
+
|
|
96
|
+
if (contentType.includes('text/event-stream') && response.body) {
|
|
97
|
+
const reader = response.body.getReader();
|
|
98
|
+
const decoder = new TextDecoder();
|
|
99
|
+
let buffer = '';
|
|
100
|
+
collector = createOrchidAiProcessTraceCollector();
|
|
101
|
+
|
|
102
|
+
addMessage({
|
|
103
|
+
role: 'assistant',
|
|
104
|
+
content: '',
|
|
105
|
+
isStreaming: true,
|
|
106
|
+
processTrace: snapshotOrchidAiProcessTraceItems(collector),
|
|
107
|
+
processInterimLive: '',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const patchStreamingAssistant = () => {
|
|
111
|
+
setMessages((prev) => {
|
|
112
|
+
const next = [...prev];
|
|
113
|
+
const last = next[next.length - 1];
|
|
114
|
+
if (last?.role !== 'assistant' || last?.isStreaming !== true) return prev;
|
|
115
|
+
next[next.length - 1] = {
|
|
116
|
+
...last,
|
|
117
|
+
content: collector.getLiveMain(),
|
|
118
|
+
processTrace: augmentLiveProcessTraceSnapshot(
|
|
119
|
+
snapshotOrchidAiProcessTraceItems(collector),
|
|
120
|
+
collector.getLiveMain(),
|
|
121
|
+
collector.getLiveInterim()
|
|
122
|
+
),
|
|
123
|
+
processInterimLive: collector.getLiveInterim(),
|
|
124
|
+
};
|
|
125
|
+
messagesRef.current = next;
|
|
126
|
+
return next;
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Batch delta-driven re-renders to one per animation frame (~60fps max).
|
|
131
|
+
// Status/query events still update immediately since they're infrequent.
|
|
132
|
+
const schedulePatch = () => {
|
|
133
|
+
if (rafId !== null) return;
|
|
134
|
+
rafId = requestAnimationFrame(() => {
|
|
135
|
+
rafId = null;
|
|
136
|
+
patchStreamingAssistant();
|
|
91
137
|
});
|
|
138
|
+
};
|
|
92
139
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
next[next.length - 1] = finalMsg;
|
|
154
|
-
} else {
|
|
155
|
-
next.push(finalMsg);
|
|
156
|
-
}
|
|
157
|
-
messagesRef.current = next;
|
|
158
|
-
return next;
|
|
159
|
-
});
|
|
160
|
-
} else if (event.type === 'error') {
|
|
161
|
-
setMessages((prev) => {
|
|
162
|
-
const next = [...prev];
|
|
163
|
-
const last = next[next.length - 1];
|
|
164
|
-
const errMsg = { role: 'assistant', content: event.error || 'Something went wrong.' };
|
|
165
|
-
if (last?.role === 'assistant' && last?.isStreaming) {
|
|
166
|
-
next[next.length - 1] = errMsg;
|
|
167
|
-
} else {
|
|
168
|
-
next.push(errMsg);
|
|
169
|
-
}
|
|
170
|
-
messagesRef.current = next;
|
|
171
|
-
return next;
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
} catch {
|
|
175
|
-
// ignore malformed SSE lines
|
|
140
|
+
while (true) {
|
|
141
|
+
const { done, value } = await reader.read();
|
|
142
|
+
if (done) break;
|
|
143
|
+
buffer += decoder.decode(value, { stream: true });
|
|
144
|
+
const lines = buffer.split('\n');
|
|
145
|
+
buffer = lines.pop() ?? '';
|
|
146
|
+
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
if (!line.startsWith('data: ')) continue;
|
|
149
|
+
try {
|
|
150
|
+
const event = JSON.parse(line.slice(6));
|
|
151
|
+
if (event.type === 'status') {
|
|
152
|
+
if (showStatus) setStatusText(event.text);
|
|
153
|
+
collector.onStatus(event.text, {
|
|
154
|
+
isClearStream: orchidAiStatusClearsStreamBuffer(event.text),
|
|
155
|
+
});
|
|
156
|
+
patchStreamingAssistant();
|
|
157
|
+
} else if (event.type === 'query') {
|
|
158
|
+
collector.onQuery(event.tool, event.input);
|
|
159
|
+
patchStreamingAssistant();
|
|
160
|
+
} else if (event.type === 'delta') {
|
|
161
|
+
collector.onDelta(event.text);
|
|
162
|
+
schedulePatch();
|
|
163
|
+
} else if (event.type === 'done') {
|
|
164
|
+
const rawTrace = collector.buildPersistedTrace();
|
|
165
|
+
const trace =
|
|
166
|
+
rawTrace && orchidAiProcessTraceHasDisplayableContent(rawTrace, '')
|
|
167
|
+
? rawTrace
|
|
168
|
+
: undefined;
|
|
169
|
+
setMessages((prev) => {
|
|
170
|
+
const next = [...prev];
|
|
171
|
+
const last = next[next.length - 1];
|
|
172
|
+
const finalMsg = {
|
|
173
|
+
role: 'assistant',
|
|
174
|
+
content: event.response,
|
|
175
|
+
truncated: event.truncated === true,
|
|
176
|
+
...(trace ? { processTrace: trace } : {}),
|
|
177
|
+
...(event.queryContext ? { queryContext: event.queryContext } : {}),
|
|
178
|
+
};
|
|
179
|
+
if (last?.role === 'assistant' && last?.isStreaming) {
|
|
180
|
+
next[next.length - 1] = finalMsg;
|
|
181
|
+
} else {
|
|
182
|
+
next.push(finalMsg);
|
|
183
|
+
}
|
|
184
|
+
messagesRef.current = next;
|
|
185
|
+
return next;
|
|
186
|
+
});
|
|
187
|
+
} else if (event.type === 'error') {
|
|
188
|
+
setMessages((prev) => {
|
|
189
|
+
const next = [...prev];
|
|
190
|
+
const last = next[next.length - 1];
|
|
191
|
+
const errMsg = { role: 'assistant', content: event.error || 'Something went wrong.' };
|
|
192
|
+
if (last?.role === 'assistant' && last?.isStreaming) {
|
|
193
|
+
next[next.length - 1] = errMsg;
|
|
194
|
+
} else {
|
|
195
|
+
next.push(errMsg);
|
|
196
|
+
}
|
|
197
|
+
messagesRef.current = next;
|
|
198
|
+
return next;
|
|
199
|
+
});
|
|
176
200
|
}
|
|
201
|
+
} catch {
|
|
202
|
+
// ignore malformed SSE lines
|
|
177
203
|
}
|
|
178
204
|
}
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
const data = await response.json();
|
|
208
|
+
if (response.ok) {
|
|
209
|
+
addMessage({
|
|
210
|
+
role: 'assistant',
|
|
211
|
+
content: data.response,
|
|
212
|
+
truncated: data.truncated,
|
|
213
|
+
...(data.queryContext ? { queryContext: data.queryContext } : {}),
|
|
214
|
+
});
|
|
179
215
|
} else {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
216
|
+
addMessage({ role: 'assistant', content: data.error || 'Something went wrong.' });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (err?.name === 'AbortError') {
|
|
221
|
+
// User-initiated stop — finalise the partial streaming message if one exists.
|
|
222
|
+
setMessages((prev) => {
|
|
223
|
+
const next = [...prev];
|
|
224
|
+
const last = next[next.length - 1];
|
|
225
|
+
if (last?.role === 'assistant' && last?.isStreaming) {
|
|
226
|
+
const rawTrace = collector?.buildPersistedTrace();
|
|
227
|
+
const trace =
|
|
228
|
+
rawTrace && orchidAiProcessTraceHasDisplayableContent(rawTrace, '')
|
|
229
|
+
? rawTrace
|
|
230
|
+
: undefined;
|
|
231
|
+
next[next.length - 1] = {
|
|
183
232
|
role: 'assistant',
|
|
184
|
-
content:
|
|
185
|
-
|
|
186
|
-
...(
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
addMessage({ role: 'assistant', content: data.error || 'Something went wrong.' });
|
|
233
|
+
content: collector?.getLiveMain() ?? '',
|
|
234
|
+
stopped: true,
|
|
235
|
+
...(trace ? { processTrace: trace } : {}),
|
|
236
|
+
};
|
|
237
|
+
messagesRef.current = next;
|
|
190
238
|
}
|
|
191
|
-
|
|
192
|
-
} catch {
|
|
193
|
-
addMessage({
|
|
194
|
-
role: 'assistant',
|
|
195
|
-
content: 'Network error. Please check your connection and try again.',
|
|
239
|
+
return next;
|
|
196
240
|
});
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
setStatusText('');
|
|
241
|
+
// Fall through to finally — do not add a network-error message.
|
|
242
|
+
return;
|
|
200
243
|
}
|
|
244
|
+
setMessages((prev) => {
|
|
245
|
+
const next = [...prev];
|
|
246
|
+
const last = next[next.length - 1];
|
|
247
|
+
const errMsg = { role: 'assistant', content: 'Network error. Please check your connection and try again.' };
|
|
248
|
+
if (last?.role === 'assistant' && last?.isStreaming) {
|
|
249
|
+
next[next.length - 1] = errMsg;
|
|
250
|
+
} else {
|
|
251
|
+
next.push(errMsg);
|
|
252
|
+
}
|
|
253
|
+
messagesRef.current = next;
|
|
254
|
+
return next;
|
|
255
|
+
});
|
|
256
|
+
} finally {
|
|
257
|
+
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; }
|
|
258
|
+
abortRef.current = null;
|
|
259
|
+
setLoading(false);
|
|
260
|
+
setStatusText('');
|
|
261
|
+
|
|
262
|
+
// Auto-fire the next queued message if one exists. The queued user message
|
|
263
|
+
// was NOT added to messages while waiting, so we insert it now before firing.
|
|
264
|
+
if (queueRef.current.length > 0) {
|
|
265
|
+
const [next, ...rest] = queueRef.current;
|
|
266
|
+
queueRef.current = rest;
|
|
267
|
+
setQueuedMessages(rest.map((q) => q.message));
|
|
268
|
+
const history = messagesRef.current.slice();
|
|
269
|
+
const attPreviews = next.sendOptions?.attachments?.map(({ name, mediaType, preview }) => ({ name, mediaType, preview }));
|
|
270
|
+
const withUser = [...history, { role: 'user', content: next.message, ...(attPreviews?.length ? { attachments: attPreviews } : {}) }];
|
|
271
|
+
messagesRef.current = withUser;
|
|
272
|
+
setMessages(withUser);
|
|
273
|
+
_doRequest(next.message, next.sendOptions, history);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
277
|
+
}, [endpoint, showStatus, addMessage]);
|
|
278
|
+
|
|
279
|
+
const sendMessage = useCallback(
|
|
280
|
+
(userMessage, sendOptions = {}) => {
|
|
281
|
+
if (abortRef.current) {
|
|
282
|
+
// A request is in flight — hold this message out of chat history until it fires.
|
|
283
|
+
queueRef.current = [...queueRef.current, { message: userMessage, sendOptions }];
|
|
284
|
+
setQueuedMessages(queueRef.current.map((q) => q.message));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const history = messagesRef.current.slice();
|
|
288
|
+
// Store display-only attachment info (preview + name) — raw base64 data stays in sendOptions for buildBody.
|
|
289
|
+
const attPreviews = sendOptions.attachments?.map(({ name, mediaType, preview }) => ({ name, mediaType, preview }));
|
|
290
|
+
addMessage({ role: 'user', content: userMessage, ...(attPreviews?.length ? { attachments: attPreviews } : {}) });
|
|
291
|
+
_doRequest(userMessage, sendOptions, history);
|
|
201
292
|
},
|
|
202
|
-
[
|
|
293
|
+
[addMessage, _doRequest]
|
|
203
294
|
);
|
|
204
295
|
|
|
205
296
|
const clearMessages = useCallback(() => {
|
|
@@ -207,5 +298,5 @@ export function useOrchidAiChat({ endpoint, buildBody, getHeaders, showStatus =
|
|
|
207
298
|
messagesRef.current = [];
|
|
208
299
|
}, []);
|
|
209
300
|
|
|
210
|
-
return { messages, loading, statusText, sendMessage, clearMessages };
|
|
301
|
+
return { messages, loading, statusText, queuedMessages, sendMessage, stopGeneration, clearQueuedMessage, clearMessages };
|
|
211
302
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -2,12 +2,36 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
// ─── Chat UI ────────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
|
+
/** Full attachment object held in ChatInput state and passed via onSend / sendOptions. */
|
|
6
|
+
export interface ChatAttachment {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
/** MIME type, e.g. "image/jpeg" or "application/pdf". */
|
|
10
|
+
mediaType: string;
|
|
11
|
+
/** Base64-encoded file content (no data-URL prefix). Pass this to the server. */
|
|
12
|
+
data: string;
|
|
13
|
+
/** Data URL for image preview — images only, null for other files. */
|
|
14
|
+
preview: string | null;
|
|
15
|
+
size: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Slim attachment record stored in ChatMessage (no raw base64 — display only). */
|
|
19
|
+
export interface ChatMessageAttachment {
|
|
20
|
+
name: string;
|
|
21
|
+
mediaType: string;
|
|
22
|
+
preview: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
export interface ChatMessage {
|
|
6
26
|
role: 'user' | 'assistant';
|
|
7
27
|
content: string;
|
|
8
28
|
truncated?: boolean;
|
|
9
29
|
/** When true, UI may show streaming placeholders (orchid-ai ChatWindow). */
|
|
10
30
|
isStreaming?: boolean;
|
|
31
|
+
/** When true, generation was stopped by the user before the response completed. */
|
|
32
|
+
stopped?: boolean;
|
|
33
|
+
/** Attachments shown in the user bubble (display-only — no raw base64 data). */
|
|
34
|
+
attachments?: ChatMessageAttachment[];
|
|
11
35
|
/** Collapsible interim trace (statuses, text, and query steps). Persisted for Hermes-style chats. */
|
|
12
36
|
processTrace?: {
|
|
13
37
|
items: Array<OrchidAiProcessTraceItem>;
|
|
@@ -27,6 +51,12 @@ export interface ChatWindowProps {
|
|
|
27
51
|
messages: ChatMessage[];
|
|
28
52
|
loading?: boolean;
|
|
29
53
|
statusText?: string;
|
|
54
|
+
/** Messages currently waiting in the queue (held out of chat history until each fires). */
|
|
55
|
+
queuedMessages?: string[];
|
|
56
|
+
/** Called with the index of the queued item to remove, or omit index to clear all. */
|
|
57
|
+
onCancelQueue?: (index: number) => void;
|
|
58
|
+
/** Called when the user clicks "Send now" on the first queued item (stops current + auto-fires). */
|
|
59
|
+
onStop?: () => void;
|
|
30
60
|
onSuggestionClick?: (text: string) => void;
|
|
31
61
|
aiEnabled?: boolean;
|
|
32
62
|
organisationName?: string;
|
|
@@ -43,7 +73,11 @@ export interface ChatWindowProps {
|
|
|
43
73
|
}
|
|
44
74
|
|
|
45
75
|
export interface ChatInputProps {
|
|
46
|
-
onSend: (text: string) => void;
|
|
76
|
+
onSend: (text: string, attachments?: ChatAttachment[]) => void;
|
|
77
|
+
/** Called when the user clicks the Stop button during generation. */
|
|
78
|
+
onStop?: () => void;
|
|
79
|
+
/** When true, shows the Stop button instead of Send. Does not disable the textarea. */
|
|
80
|
+
loading?: boolean;
|
|
47
81
|
disabled?: boolean;
|
|
48
82
|
disabledReason?: string;
|
|
49
83
|
onDisabledClick?: () => void;
|
|
@@ -63,6 +97,7 @@ export interface MessageProps {
|
|
|
63
97
|
showProcessTracePanel?: boolean;
|
|
64
98
|
queryContext?: Record<string, unknown>;
|
|
65
99
|
showQuerySummary?: boolean;
|
|
100
|
+
attachments?: ChatMessageAttachment[];
|
|
66
101
|
}
|
|
67
102
|
|
|
68
103
|
export const ChatWindow: React.FC<ChatWindowProps>;
|
|
@@ -75,6 +110,8 @@ export const Message: React.FC<MessageProps>;
|
|
|
75
110
|
export interface OrchidAiChatSendOptions {
|
|
76
111
|
allowWebResearch?: boolean;
|
|
77
112
|
urlsToCheck?: string[];
|
|
113
|
+
/** Files/images to include in this message. buildBody receives these via sendOptions. */
|
|
114
|
+
attachments?: ChatAttachment[];
|
|
78
115
|
}
|
|
79
116
|
|
|
80
117
|
export interface OrchidAiChatOptions {
|
|
@@ -105,7 +142,13 @@ export interface OrchidAiChatState {
|
|
|
105
142
|
loading: boolean;
|
|
106
143
|
/** Live status text emitted by the server (e.g. "Thinking", "Compiling response") */
|
|
107
144
|
statusText: string;
|
|
108
|
-
|
|
145
|
+
/** Messages waiting in the send queue (held out of chat history until each fires). */
|
|
146
|
+
queuedMessages: string[];
|
|
147
|
+
sendMessage: (userMessage: string, sendOptions?: OrchidAiChatSendOptions) => void;
|
|
148
|
+
/** Abort the current in-flight request. The next queued message auto-fires after abort. */
|
|
149
|
+
stopGeneration: () => void;
|
|
150
|
+
/** Remove a specific queued item by index, or clear the entire queue when called with no argument. */
|
|
151
|
+
clearQueuedMessage: (index?: number) => void;
|
|
109
152
|
clearMessages: () => void;
|
|
110
153
|
}
|
|
111
154
|
|