orchid-ai 2.0.2 → 2.1.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 +304 -0
- package/orchid-ai.css +517 -0
- package/package.json +1 -1
- package/src/components/ChatWindow.jsx +28 -5
- package/src/components/Message.jsx +285 -14
- package/src/components/visualizations/chartSchema.js +79 -28
- package/src/constants/visualizationInstructions.js +2 -2
- package/src/hooks/useOrchidAiChat.js +93 -18
- package/src/index.d.ts +71 -0
- package/src/index.js +11 -0
- package/src/orchidAiProcessTrace.js +175 -0
- package/src/orchidAiStreamingTitle.js +12 -0
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ORCHID_AI_SSE_STATUS_CLEAR_STREAM,
|
|
4
|
+
orchidAiStatusClearsStreamBuffer,
|
|
5
|
+
} from '../orchidAiStreamingTitle';
|
|
6
|
+
import {
|
|
7
|
+
augmentLiveProcessTraceSnapshot,
|
|
8
|
+
createOrchidAiProcessTraceCollector,
|
|
9
|
+
snapshotOrchidAiProcessTraceItems,
|
|
10
|
+
orchidAiProcessTraceHasDisplayableContent,
|
|
11
|
+
} from '../orchidAiProcessTrace';
|
|
2
12
|
|
|
3
13
|
/**
|
|
4
14
|
* Default status message strings. Export these so server-side code can import
|
|
5
15
|
* and reference the same values, making it easy to keep client and server in sync
|
|
6
16
|
* or swap them out per-app.
|
|
7
17
|
*
|
|
8
|
-
* Set showStatus: false on the hook to
|
|
18
|
+
* Set showStatus: false on the hook to suppress status display entirely.
|
|
9
19
|
*/
|
|
10
20
|
export const ORCHID_AI_DEFAULT_STATUS = {
|
|
11
21
|
thinking: 'Thinking',
|
|
12
|
-
compilingResponse:
|
|
22
|
+
compilingResponse: ORCHID_AI_SSE_STATUS_CLEAR_STREAM,
|
|
13
23
|
lookingUpData: 'Looking up data',
|
|
14
24
|
};
|
|
15
25
|
|
|
@@ -32,12 +42,8 @@ export function useOrchidAiChat({ endpoint, buildBody, getHeaders, showStatus =
|
|
|
32
42
|
const [loading, setLoading] = useState(false);
|
|
33
43
|
const [statusText, setStatusText] = useState('');
|
|
34
44
|
|
|
35
|
-
// Track messages in a ref so sendMessage always reads the latest without needing
|
|
36
|
-
// messages in its dependency array (avoids capturing stale history).
|
|
37
45
|
const messagesRef = useRef(initialMessages);
|
|
38
46
|
|
|
39
|
-
// Keep latest callbacks in refs so sendMessage identity stays stable regardless
|
|
40
|
-
// of whether the parent re-creates buildBody/getHeaders each render.
|
|
41
47
|
const buildBodyRef = useRef(buildBody);
|
|
42
48
|
const getHeadersRef = useRef(getHeaders);
|
|
43
49
|
buildBodyRef.current = buildBody;
|
|
@@ -70,10 +76,39 @@ export function useOrchidAiChat({ endpoint, buildBody, getHeaders, showStatus =
|
|
|
70
76
|
|
|
71
77
|
const contentType = response.headers.get('content-type') ?? '';
|
|
72
78
|
|
|
73
|
-
if (contentType.includes('text/event-stream')) {
|
|
79
|
+
if (contentType.includes('text/event-stream') && response.body) {
|
|
74
80
|
const reader = response.body.getReader();
|
|
75
81
|
const decoder = new TextDecoder();
|
|
76
82
|
let buffer = '';
|
|
83
|
+
const collector = createOrchidAiProcessTraceCollector();
|
|
84
|
+
|
|
85
|
+
addMessage({
|
|
86
|
+
role: 'assistant',
|
|
87
|
+
content: '',
|
|
88
|
+
isStreaming: true,
|
|
89
|
+
processTrace: snapshotOrchidAiProcessTraceItems(collector),
|
|
90
|
+
processInterimLive: '',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const patchStreamingAssistant = () => {
|
|
94
|
+
setMessages((prev) => {
|
|
95
|
+
const next = [...prev];
|
|
96
|
+
const last = next[next.length - 1];
|
|
97
|
+
if (last?.role !== 'assistant' || last?.isStreaming !== true) return prev;
|
|
98
|
+
next[next.length - 1] = {
|
|
99
|
+
...last,
|
|
100
|
+
content: collector.getLiveMain(),
|
|
101
|
+
processTrace: augmentLiveProcessTraceSnapshot(
|
|
102
|
+
snapshotOrchidAiProcessTraceItems(collector),
|
|
103
|
+
collector.getLiveMain(),
|
|
104
|
+
collector.getLiveInterim()
|
|
105
|
+
),
|
|
106
|
+
processInterimLive: collector.getLiveInterim(),
|
|
107
|
+
};
|
|
108
|
+
messagesRef.current = next;
|
|
109
|
+
return next;
|
|
110
|
+
});
|
|
111
|
+
};
|
|
77
112
|
|
|
78
113
|
while (true) {
|
|
79
114
|
const { done, value } = await reader.read();
|
|
@@ -86,18 +121,54 @@ export function useOrchidAiChat({ endpoint, buildBody, getHeaders, showStatus =
|
|
|
86
121
|
if (!line.startsWith('data: ')) continue;
|
|
87
122
|
try {
|
|
88
123
|
const event = JSON.parse(line.slice(6));
|
|
89
|
-
if (event.type === 'status'
|
|
90
|
-
setStatusText(event.text);
|
|
124
|
+
if (event.type === 'status') {
|
|
125
|
+
if (showStatus) setStatusText(event.text);
|
|
126
|
+
collector.onStatus(event.text, {
|
|
127
|
+
isClearStream: orchidAiStatusClearsStreamBuffer(event.text),
|
|
128
|
+
});
|
|
129
|
+
patchStreamingAssistant();
|
|
130
|
+
} else if (event.type === 'query') {
|
|
131
|
+
collector.onQuery(event.tool, event.input);
|
|
132
|
+
patchStreamingAssistant();
|
|
133
|
+
} else if (event.type === 'delta') {
|
|
134
|
+
collector.onDelta(event.text);
|
|
135
|
+
patchStreamingAssistant();
|
|
91
136
|
} else if (event.type === 'done') {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
137
|
+
const rawTrace = collector.buildPersistedTrace();
|
|
138
|
+
const trace =
|
|
139
|
+
rawTrace && orchidAiProcessTraceHasDisplayableContent(rawTrace, '')
|
|
140
|
+
? rawTrace
|
|
141
|
+
: undefined;
|
|
142
|
+
setMessages((prev) => {
|
|
143
|
+
const next = [...prev];
|
|
144
|
+
const last = next[next.length - 1];
|
|
145
|
+
const finalMsg = {
|
|
146
|
+
role: 'assistant',
|
|
147
|
+
content: event.response,
|
|
148
|
+
truncated: event.truncated === true,
|
|
149
|
+
...(trace ? { processTrace: trace } : {}),
|
|
150
|
+
...(event.queryContext ? { queryContext: event.queryContext } : {}),
|
|
151
|
+
};
|
|
152
|
+
if (last?.role === 'assistant' && last?.isStreaming) {
|
|
153
|
+
next[next.length - 1] = finalMsg;
|
|
154
|
+
} else {
|
|
155
|
+
next.push(finalMsg);
|
|
156
|
+
}
|
|
157
|
+
messagesRef.current = next;
|
|
158
|
+
return next;
|
|
96
159
|
});
|
|
97
160
|
} else if (event.type === 'error') {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
|
101
172
|
});
|
|
102
173
|
}
|
|
103
174
|
} catch {
|
|
@@ -106,10 +177,14 @@ export function useOrchidAiChat({ endpoint, buildBody, getHeaders, showStatus =
|
|
|
106
177
|
}
|
|
107
178
|
}
|
|
108
179
|
} else {
|
|
109
|
-
// Plain JSON fallback for non-streaming backends
|
|
110
180
|
const data = await response.json();
|
|
111
181
|
if (response.ok) {
|
|
112
|
-
addMessage({
|
|
182
|
+
addMessage({
|
|
183
|
+
role: 'assistant',
|
|
184
|
+
content: data.response,
|
|
185
|
+
truncated: data.truncated,
|
|
186
|
+
...(data.queryContext ? { queryContext: data.queryContext } : {}),
|
|
187
|
+
});
|
|
113
188
|
} else {
|
|
114
189
|
addMessage({ role: 'assistant', content: data.error || 'Something went wrong.' });
|
|
115
190
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -6,6 +6,21 @@ export interface ChatMessage {
|
|
|
6
6
|
role: 'user' | 'assistant';
|
|
7
7
|
content: string;
|
|
8
8
|
truncated?: boolean;
|
|
9
|
+
/** When true, UI may show streaming placeholders (orchid-ai ChatWindow). */
|
|
10
|
+
isStreaming?: boolean;
|
|
11
|
+
/** Collapsible interim trace (statuses + pre-final text). Persisted for Hermes-style chats. */
|
|
12
|
+
processTrace?: {
|
|
13
|
+
items: Array<{ type: 'status' | 'text'; value: string }>;
|
|
14
|
+
defaultCollapsed?: boolean;
|
|
15
|
+
};
|
|
16
|
+
/** Live tool preamble not yet flushed into processTrace.items (streaming only). */
|
|
17
|
+
processInterimLive?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Filters/parameters the AI used to query the database. Sent by the server in the
|
|
20
|
+
* `done` SSE event or JSON response as `queryContext`. Displayed when `showQuerySummary`
|
|
21
|
+
* is true on ChatWindow or Message.
|
|
22
|
+
*/
|
|
23
|
+
queryContext?: Record<string, unknown>;
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
export interface ChatWindowProps {
|
|
@@ -15,6 +30,14 @@ export interface ChatWindowProps {
|
|
|
15
30
|
onSuggestionClick?: (text: string) => void;
|
|
16
31
|
aiEnabled?: boolean;
|
|
17
32
|
organisationName?: string;
|
|
33
|
+
/** When false, tool/interim statuses show inline next to typing dots (Hermes / iLink config). */
|
|
34
|
+
showProcessTracePanel?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* When true, shows a collapsed "Filters used" disclosure under each assistant message
|
|
37
|
+
* that has a `queryContext`. Helps users understand why the AI returned specific data.
|
|
38
|
+
* Defaults to false.
|
|
39
|
+
*/
|
|
40
|
+
showQuerySummary?: boolean;
|
|
18
41
|
/** Any extra props are forwarded to the root element */
|
|
19
42
|
[key: string]: unknown;
|
|
20
43
|
}
|
|
@@ -31,6 +54,15 @@ export interface MessageProps {
|
|
|
31
54
|
role: 'user' | 'assistant';
|
|
32
55
|
content: string;
|
|
33
56
|
truncated?: boolean;
|
|
57
|
+
exportPrefix?: string;
|
|
58
|
+
isStreaming?: boolean;
|
|
59
|
+
/** Shown above streaming content while server emits status (e.g. “Compiling response”). */
|
|
60
|
+
streamingStatusText?: string;
|
|
61
|
+
processTrace?: ChatMessage['processTrace'];
|
|
62
|
+
processInterimLive?: string;
|
|
63
|
+
showProcessTracePanel?: boolean;
|
|
64
|
+
queryContext?: Record<string, unknown>;
|
|
65
|
+
showQuerySummary?: boolean;
|
|
34
66
|
}
|
|
35
67
|
|
|
36
68
|
export const ChatWindow: React.FC<ChatWindowProps>;
|
|
@@ -134,6 +166,45 @@ export function validateTablePayload(data: unknown): boolean;
|
|
|
134
166
|
export function parseChartBlock(block: string): unknown;
|
|
135
167
|
export function resolveChartBlock(block: string): unknown;
|
|
136
168
|
|
|
169
|
+
// ─── Process trace + SSE markers (Hermes) ───────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export function orchidAiProcessTraceEntryKind(entry: {
|
|
172
|
+
type: 'status' | 'text';
|
|
173
|
+
value: string;
|
|
174
|
+
}): 'text' | 'tool' | 'compile' | 'mind';
|
|
175
|
+
|
|
176
|
+
export function createOrchidAiProcessTraceCollector(): {
|
|
177
|
+
onStatus(text: string, opts?: { isClearStream?: boolean }): void;
|
|
178
|
+
onDelta(text: string): void;
|
|
179
|
+
getLiveMain(): string;
|
|
180
|
+
getLiveInterim(): string;
|
|
181
|
+
getItems(): Array<{ type: 'status' | 'text'; value: string }>;
|
|
182
|
+
reset(): void;
|
|
183
|
+
buildPersistedTrace():
|
|
184
|
+
| { items: Array<{ type: 'status' | 'text'; value: string }>; defaultCollapsed: boolean }
|
|
185
|
+
| undefined;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export function snapshotOrchidAiProcessTraceItems(
|
|
189
|
+
collector: ReturnType<typeof createOrchidAiProcessTraceCollector>
|
|
190
|
+
): { items: Array<{ type: 'status' | 'text'; value: string }>; defaultCollapsed: boolean };
|
|
191
|
+
|
|
192
|
+
export function augmentLiveProcessTraceSnapshot(
|
|
193
|
+
trace: { items: Array<{ type: 'status' | 'text'; value: string }>; defaultCollapsed: boolean },
|
|
194
|
+
streamingMain: string,
|
|
195
|
+
liveInterim?: string
|
|
196
|
+
): { items: Array<{ type: 'status' | 'text'; value: string }>; defaultCollapsed: boolean };
|
|
197
|
+
|
|
198
|
+
export function orchidAiProcessTraceHasDisplayableContent(
|
|
199
|
+
trace: ChatMessage['processTrace'] | undefined,
|
|
200
|
+
liveInterim: string | undefined,
|
|
201
|
+
options?: { isStreaming?: boolean }
|
|
202
|
+
): boolean;
|
|
203
|
+
|
|
204
|
+
/** SSE status before post-tool assistant text; client separates interim vs final answer. */
|
|
205
|
+
export const ORCHID_AI_SSE_STATUS_CLEAR_STREAM: string;
|
|
206
|
+
export function orchidAiStatusClearsStreamBuffer(statusText: unknown): boolean;
|
|
207
|
+
|
|
137
208
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
138
209
|
|
|
139
210
|
export const ORCHID_AI_VISUALIZATION_INSTRUCTIONS: string;
|
package/src/index.js
CHANGED
|
@@ -50,3 +50,14 @@ export {
|
|
|
50
50
|
|
|
51
51
|
// AI system prompt constant
|
|
52
52
|
export { ORCHID_AI_VISUALIZATION_INSTRUCTIONS } from './constants/visualizationInstructions';
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
augmentLiveProcessTraceSnapshot,
|
|
56
|
+
createOrchidAiProcessTraceCollector,
|
|
57
|
+
orchidAiProcessTraceHasDisplayableContent,
|
|
58
|
+
snapshotOrchidAiProcessTraceItems,
|
|
59
|
+
orchidAiProcessTraceEntryKind,
|
|
60
|
+
} from './orchidAiProcessTrace';
|
|
61
|
+
|
|
62
|
+
// SSE + interim/process trace (Hermes)
|
|
63
|
+
export { ORCHID_AI_SSE_STATUS_CLEAR_STREAM, orchidAiStatusClearsStreamBuffer } from './orchidAiStreamingTitle';
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { ORCHID_AI_SSE_STATUS_CLEAR_STREAM } from './orchidAiStreamingTitle';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Visual timeline lane for Message.jsx: tool-style statuses vs compile vs generic vs prose.
|
|
5
|
+
* @param {{ type: 'status' | 'text', value: string }} entry
|
|
6
|
+
* @returns {'text' | 'tool' | 'compile' | 'mind'}
|
|
7
|
+
*/
|
|
8
|
+
export function orchidAiProcessTraceEntryKind(entry) {
|
|
9
|
+
if (!entry || entry.type === 'text') return 'text';
|
|
10
|
+
const t = String(entry.value || '');
|
|
11
|
+
if (t.trim() === ORCHID_AI_SSE_STATUS_CLEAR_STREAM) return 'compile';
|
|
12
|
+
if (
|
|
13
|
+
/^Looking up/i.test(t) ||
|
|
14
|
+
/^Found \d+/i.test(t) ||
|
|
15
|
+
/^Searching the web/i.test(t) ||
|
|
16
|
+
/^Searching knowledge base/i.test(t)
|
|
17
|
+
) {
|
|
18
|
+
return 'tool';
|
|
19
|
+
}
|
|
20
|
+
return 'mind';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Collects interim tool/preamble text + statuses for the Hermes / Orchid SSE stream.
|
|
25
|
+
* Final answer deltas go to getLiveMain(); pre-tool/streaming preamble goes to
|
|
26
|
+
* getLiveInterim() and (on "Compiling response") into frozen `items`.
|
|
27
|
+
*
|
|
28
|
+
* Phase machine:
|
|
29
|
+
* - `open_answer`: deltas accumulate in main (direct replies, or post-compile answer).
|
|
30
|
+
* - When a tool-style status arrives (e.g. "Looking up jobs"), migrate main → interim and enter `tool_interim`.
|
|
31
|
+
* - `tool_interim`: deltas go to interim; on `Compiling response`, flush interim to items and return to `open_answer`.
|
|
32
|
+
*/
|
|
33
|
+
export function createOrchidAiProcessTraceCollector() {
|
|
34
|
+
/** @type {'open_answer' | 'tool_interim'} */
|
|
35
|
+
let phase = 'open_answer';
|
|
36
|
+
let preBuf = '';
|
|
37
|
+
let mainBuf = '';
|
|
38
|
+
/** @type {{ type: 'status' | 'text', value: string }[]} */
|
|
39
|
+
const items = [];
|
|
40
|
+
|
|
41
|
+
/** @param {string} text */
|
|
42
|
+
function toolishStatus(text) {
|
|
43
|
+
const t = String(text || '');
|
|
44
|
+
return (
|
|
45
|
+
/^Looking up/i.test(t) ||
|
|
46
|
+
/^Found \d+/i.test(t) ||
|
|
47
|
+
/^Searching the web/i.test(t) ||
|
|
48
|
+
/^Searching knowledge base/i.test(t)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} tool - tool name e.g. "query_orders"
|
|
55
|
+
* @param {Record<string, unknown>} input - raw tool input parameters
|
|
56
|
+
*/
|
|
57
|
+
onQuery(tool, input) {
|
|
58
|
+
items.push({ type: 'query', tool: String(tool), input: input || {} });
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} text
|
|
62
|
+
* @param {{ isClearStream?: boolean }} [opts] — true for {@link ORCHID_AI_SSE_STATUS_CLEAR_STREAM}
|
|
63
|
+
*/
|
|
64
|
+
onStatus(text, opts = {}) {
|
|
65
|
+
const isClearStream = opts.isClearStream === true;
|
|
66
|
+
if (isClearStream) {
|
|
67
|
+
if (preBuf) items.push({ type: 'text', value: preBuf });
|
|
68
|
+
preBuf = '';
|
|
69
|
+
phase = 'open_answer';
|
|
70
|
+
items.push({ type: 'status', value: String(text) });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (toolishStatus(text) && phase === 'open_answer') {
|
|
74
|
+
phase = 'tool_interim';
|
|
75
|
+
if (mainBuf) {
|
|
76
|
+
preBuf = mainBuf;
|
|
77
|
+
mainBuf = '';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
items.push({ type: 'status', value: String(text) });
|
|
81
|
+
},
|
|
82
|
+
/** @param {string} text */
|
|
83
|
+
onDelta(text) {
|
|
84
|
+
const t = String(text);
|
|
85
|
+
if (phase === 'tool_interim') preBuf += t;
|
|
86
|
+
else mainBuf += t;
|
|
87
|
+
},
|
|
88
|
+
getLiveMain() {
|
|
89
|
+
return mainBuf;
|
|
90
|
+
},
|
|
91
|
+
getLiveInterim() {
|
|
92
|
+
return phase === 'tool_interim' ? preBuf : '';
|
|
93
|
+
},
|
|
94
|
+
/** @returns {{ type: 'status' | 'text', value: string }[]} */
|
|
95
|
+
getItems() {
|
|
96
|
+
return items;
|
|
97
|
+
},
|
|
98
|
+
reset() {
|
|
99
|
+
phase = 'open_answer';
|
|
100
|
+
preBuf = '';
|
|
101
|
+
mainBuf = '';
|
|
102
|
+
items.length = 0;
|
|
103
|
+
},
|
|
104
|
+
/** Persisted shape for assistant bubbles; omit when nothing to show. */
|
|
105
|
+
buildPersistedTrace() {
|
|
106
|
+
if (!items.length) return undefined;
|
|
107
|
+
return {
|
|
108
|
+
items: items.map(serializeTraceItem),
|
|
109
|
+
defaultCollapsed: true,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function serializeTraceItem(x) {
|
|
116
|
+
if (x.type === 'query') return { type: 'query', tool: x.tool, input: x.input };
|
|
117
|
+
return { type: x.type, value: x.value };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Snapshot `items` for React state / persisted shape (live streaming; not collapsed). */
|
|
121
|
+
export function snapshotOrchidAiProcessTraceItems(collector) {
|
|
122
|
+
const items = collector.getItems();
|
|
123
|
+
return {
|
|
124
|
+
items: items.map(serializeTraceItem),
|
|
125
|
+
defaultCollapsed: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Display-only: Hermes sends one `status` ("Thinking") then `delta` chunks for direct replies.
|
|
131
|
+
* Deltas never become timeline rows, so the Working panel would stay on Thinking forever.
|
|
132
|
+
* Append a synthetic step while streamed content exists (not persisted — see `buildPersistedTrace`).
|
|
133
|
+
*
|
|
134
|
+
* @param {{ items: Array<{ type: 'status' | 'text', value: string }>, defaultCollapsed: boolean }} trace
|
|
135
|
+
* @param {string} streamingMain
|
|
136
|
+
* @param {string} [liveInterim]
|
|
137
|
+
*/
|
|
138
|
+
export function augmentLiveProcessTraceSnapshot(trace, streamingMain, liveInterim = '') {
|
|
139
|
+
if (!trace || !Array.isArray(trace.items)) return trace;
|
|
140
|
+
const items = trace.items;
|
|
141
|
+
const mainTrim = typeof streamingMain === 'string' ? streamingMain.trim() : '';
|
|
142
|
+
const interimTrim = typeof liveInterim === 'string' ? liveInterim.trim() : '';
|
|
143
|
+
|
|
144
|
+
const onlyThinking =
|
|
145
|
+
items.length === 1 &&
|
|
146
|
+
items[0]?.type === 'status' &&
|
|
147
|
+
String(items[0].value || '').trim().toLowerCase() === 'thinking';
|
|
148
|
+
|
|
149
|
+
if (!onlyThinking) return trace;
|
|
150
|
+
|
|
151
|
+
if (mainTrim.length > 0) {
|
|
152
|
+
return {
|
|
153
|
+
...trace,
|
|
154
|
+
items: [...items, { type: 'status', value: 'Drafting response' }],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (interimTrim.length > 0) {
|
|
158
|
+
return {
|
|
159
|
+
...trace,
|
|
160
|
+
items: [...items, { type: 'status', value: 'Gathering details' }],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return trace;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Whether to render the process trace panel. Pass `{ isStreaming: true }` during SSE so a lone “Thinking” status still opens the live tray. */
|
|
167
|
+
export function orchidAiProcessTraceHasDisplayableContent(trace, liveInterim, options) {
|
|
168
|
+
const isStreaming = options?.isStreaming === true;
|
|
169
|
+
if (typeof liveInterim === 'string' && liveInterim.trim() !== '') return true;
|
|
170
|
+
const list = trace?.items;
|
|
171
|
+
if (!Array.isArray(list) || list.length === 0) return false;
|
|
172
|
+
if (isStreaming) return true;
|
|
173
|
+
if (list.some((i) => i && i.type === 'text' && String(i.value || '').trim() !== '')) return true;
|
|
174
|
+
return list.length > 1;
|
|
175
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE `status` emitted by Hermes immediately before streaming the post-tool assistant message.
|
|
3
|
+
* Clients use this to separate interim text from the final answer (see `orchidAiProcessTrace`).
|
|
4
|
+
*
|
|
5
|
+
* Keep in sync with `onStatus?.('Compiling response')` in iLink `orchidAiAnthropicChat.js`.
|
|
6
|
+
*/
|
|
7
|
+
export const ORCHID_AI_SSE_STATUS_CLEAR_STREAM = 'Compiling response';
|
|
8
|
+
|
|
9
|
+
/** @param {unknown} statusText from `{ type: 'status', text }` */
|
|
10
|
+
export function orchidAiStatusClearsStreamBuffer(statusText) {
|
|
11
|
+
return typeof statusText === 'string' && statusText.trim() === ORCHID_AI_SSE_STATUS_CLEAR_STREAM;
|
|
12
|
+
}
|