orchid-ai 2.0.3 → 2.1.1

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 ADDED
@@ -0,0 +1,304 @@
1
+ # orchid-ai
2
+
3
+ Shared Orchid AI chat UI and visualization components. A source-distributed React component library — no compilation step; the consuming app's bundler handles JSX.
4
+
5
+ ---
6
+
7
+ ## Publishing
8
+
9
+ ```bash
10
+ npm version patch # or minor / major
11
+ npm publish
12
+ ```
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install orchid-ai
20
+ ```
21
+
22
+ Import the stylesheet once in your app entry point:
23
+
24
+ ```js
25
+ import 'orchid-ai/orchid-ai.css';
26
+ ```
27
+
28
+ ### Peer dependencies
29
+
30
+ Your app must provide:
31
+
32
+ ```
33
+ react >= 18
34
+ react-dom >= 18
35
+ react-markdown >= 9
36
+ remark-gfm >= 4
37
+ html2canvas >= 1.4 (for chart PNG export)
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Quick start
43
+
44
+ ```jsx
45
+ import { ChatWindow, ChatInput, useOrchidAiChat } from 'orchid-ai';
46
+ import 'orchid-ai/orchid-ai.css';
47
+
48
+ export default function App() {
49
+ const { messages, loading, statusText, sendMessage } = useOrchidAiChat({
50
+ endpoint: '/api/ai/chat',
51
+ buildBody: (userMessage, history) => ({ message: userMessage, history }),
52
+ getHeaders: () => ({ 'X-CSRF-Token': getCsrfToken() }),
53
+ });
54
+
55
+ return (
56
+ <div className="ai-chat-container">
57
+ <ChatWindow
58
+ messages={messages}
59
+ loading={loading}
60
+ statusText={statusText}
61
+ aiEnabled={true}
62
+ organisationName="Acme Ltd"
63
+ />
64
+ <ChatInput onSend={sendMessage} disabled={loading} />
65
+ </div>
66
+ );
67
+ }
68
+ ```
69
+
70
+ ---
71
+
72
+ ## `useOrchidAiChat`
73
+
74
+ ```ts
75
+ const { messages, loading, statusText, sendMessage, clearMessages } =
76
+ useOrchidAiChat(options);
77
+ ```
78
+
79
+ ### Options
80
+
81
+ | Option | Type | Default | Description |
82
+ |-------------------|------------------------------------------------------------------|---------|-----------------------------------------------------------------|
83
+ | `endpoint` | `string` | — | POST URL the hook fetches on each message |
84
+ | `buildBody` | `(userMessage, history, sendOptions?) => object` | — | Builds the JSON request body |
85
+ | `getHeaders` | `() => Record<string, string>` | — | Returns extra headers (e.g. CSRF token) |
86
+ | `showStatus` | `boolean` | `true` | Set `false` to suppress the live status text entirely |
87
+ | `initialMessages` | `ChatMessage[]` | `[]` | Seed the transcript (e.g. from localStorage) |
88
+
89
+ ### Returns
90
+
91
+ | Key | Type | Description |
92
+ |-----------------|----------------------------------------------|----------------------------------------------------------|
93
+ | `messages` | `ChatMessage[]` | Full transcript including streaming assistant messages |
94
+ | `loading` | `boolean` | True while a request is in flight |
95
+ | `statusText` | `string` | Latest `status` event text (e.g. "Looking up data") |
96
+ | `sendMessage` | `(text: string, opts?: SendOptions) => void` | Send a user message |
97
+ | `clearMessages` | `() => void` | Reset the transcript |
98
+
99
+ ### `ChatMessage` shape
100
+
101
+ ```ts
102
+ {
103
+ role: 'user' | 'assistant';
104
+ content: string;
105
+ truncated?: boolean;
106
+ isStreaming?: boolean;
107
+ processTrace?: { items: Array<{ type: 'status' | 'text'; value: string }>; defaultCollapsed?: boolean };
108
+ processInterimLive?: string;
109
+ queryContext?: Record<string, unknown>;
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Components
116
+
117
+ ### `<ChatWindow>`
118
+
119
+ Renders the full message list, empty state, and typing indicator. Wrap it with a `<div className="ai-chat-container">` (sets layout and font).
120
+
121
+ | Prop | Type | Default | Description |
122
+ |-----------------------|-----------------|----------------------|----------------------------------------------------------------------------------------------------|
123
+ | `messages` | `ChatMessage[]` | — | From `useOrchidAiChat` |
124
+ | `loading` | `boolean` | — | Shows typing indicator when true and no streaming message is present |
125
+ | `statusText` | `string` | — | Live status shown above the typing indicator |
126
+ | `aiEnabled` | `boolean` | — | Shows a disabled state with `unavailableMessage` when false |
127
+ | `appName` | `string` | `"Hermes Chat"` | Used in the disabled state heading and as the PDF export filename prefix |
128
+ | `organisationName` | `string` | — | Shown in the empty-state description ("Ask about ...") |
129
+ | `unavailableMessage` | `string` | — | Overrides the default "needs an API key" copy when `aiEnabled` is false |
130
+ | `emptyDescription` | `string` | — | Overrides the default empty-state paragraph |
131
+ | `suggestions` | `string[]` | 3 built-in prompts | Clickable suggestion chips shown in the empty state |
132
+ | `suggestionsDisabled` | `boolean` | `false` | Renders suggestion chips as non-interactive (e.g. while loading) |
133
+ | `onSuggestionClick` | `(text) => void`| — | Called when a suggestion chip is clicked |
134
+ | `showProcessTracePanel`| `boolean` | `true` | Set `false` to show statuses inline next to the typing dots instead of in the collapsible panel |
135
+ | `showQuerySummary` | `boolean` | `false` | Shows a collapsed "Filters used" disclosure on messages that have `queryContext` |
136
+
137
+ ### `<ChatInput>`
138
+
139
+ The text input bar with auto-growing textarea and send button.
140
+
141
+ | Prop | Type | Description |
142
+ |-------------------|-----------------|-----------------------------------------------------------------------|
143
+ | `onSend` | `(text) => void`| Called with trimmed text when the user submits |
144
+ | `disabled` | `boolean` | Disables the textarea and send button |
145
+ | `disabledReason` | `string` | When set, shows a clickable overlay and updates the placeholder text |
146
+ | `onDisabledClick` | `() => void` | Called when the user taps the disabled overlay |
147
+
148
+ ### `<Message>`
149
+
150
+ Renders a single message bubble. Used internally by `ChatWindow`; import directly if you need a custom layout.
151
+
152
+ | Prop | Type | Description |
153
+ |------------------------|-----------------|---------------------------------------------------------|
154
+ | `role` | `'user' \| 'assistant'` | — |
155
+ | `content` | `string` | Markdown for assistant; plain text for user |
156
+ | `truncated` | `boolean` | Shows a cut-off warning |
157
+ | `exportPrefix` | `string` | Filename prefix for PDF download (default `orchid-ai`) |
158
+ | `isStreaming` | `boolean` | Enables streaming placeholders for open code fences |
159
+ | `streamingStatusText` | `string` | Status text shown above streaming content |
160
+ | `processTrace` | object | See `ChatMessage.processTrace` |
161
+ | `processInterimLive` | `string` | Live interim preamble (streaming only) |
162
+ | `showProcessTracePanel`| `boolean` | Default `true` |
163
+ | `queryContext` | `object` | Filters to display when `showQuerySummary` is true |
164
+ | `showQuerySummary` | `boolean` | Default `false` |
165
+
166
+ ---
167
+
168
+ ## Server-side: SSE protocol
169
+
170
+ The hook handles both streaming (`Content-Type: text/event-stream`) and plain JSON responses. For streaming, emit newline-delimited `data:` events:
171
+
172
+ ```
173
+ data: {"type":"status","text":"Looking up data"}\n\n
174
+ data: {"type":"delta","text":"Here are the "}\n\n
175
+ data: {"type":"delta","text":"results..."}\n\n
176
+ data: {"type":"done","response":"Here are the results...","truncated":false}\n\n
177
+ ```
178
+
179
+ ### Event types
180
+
181
+ | `type` | Required fields | Optional fields |
182
+ |----------|------------------------------|------------------------------|
183
+ | `status` | `text: string` | — |
184
+ | `delta` | `text: string` | — |
185
+ | `done` | `response: string` | `truncated: boolean`, `queryContext: object` |
186
+ | `error` | `error: string` | — |
187
+
188
+ ### Status labels that trigger the Working panel
189
+
190
+ The collapsible "Working" panel appears when statuses matching these patterns are received:
191
+
192
+ - `Looking up…`
193
+ - `Found N…`
194
+ - `Searching the web…`
195
+ - `Searching knowledge base…`
196
+
197
+ The special status `"Compiling response"` (exported as `ORCHID_AI_SSE_STATUS_CLEAR_STREAM`) signals the boundary between interim tool preamble and the final answer — the collector flushes its interim buffer and begins accumulating the reply.
198
+
199
+ Use the exported constants to keep server and client in sync:
200
+
201
+ ```js
202
+ import { ORCHID_AI_DEFAULT_STATUS, ORCHID_AI_SSE_STATUS_CLEAR_STREAM } from 'orchid-ai';
203
+
204
+ // ORCHID_AI_DEFAULT_STATUS.thinking → 'Thinking'
205
+ // ORCHID_AI_DEFAULT_STATUS.lookingUpData → 'Looking up data'
206
+ // ORCHID_AI_DEFAULT_STATUS.compilingResponse → 'Compiling response'
207
+ // ORCHID_AI_SSE_STATUS_CLEAR_STREAM → 'Compiling response'
208
+ ```
209
+
210
+ ### Query context
211
+
212
+ Include `queryContext` on the `done` event to let users see what filters the AI applied:
213
+
214
+ ```js
215
+ res.write(`data: ${JSON.stringify({
216
+ type: 'done',
217
+ response: finalText,
218
+ queryContext: {
219
+ customerId: 123,
220
+ status: 'active',
221
+ dateFrom: '2024-01-01',
222
+ },
223
+ })}\n\n`);
224
+ ```
225
+
226
+ Enable display with `<ChatWindow showQuerySummary={true} />`. Keys are automatically converted from camelCase to Title Case ("customerId" → "Customer ID").
227
+
228
+ ---
229
+
230
+ ## AI response title
231
+
232
+ The assistant can set the PDF export filename by embedding an HTML comment anywhere in its reply:
233
+
234
+ ```
235
+ <!-- title: Monthly Shipment Summary -->
236
+ ```
237
+
238
+ The comment is stripped from the rendered content and used as the PDF title slug.
239
+
240
+ ---
241
+
242
+ ## Visualizations
243
+
244
+ The AI embeds charts using a fenced code block with language `orchid-ai-chart` (legacy alias `hemiq-chart` is still parsed):
245
+
246
+ ````
247
+ ```orchid-ai-chart
248
+ {
249
+ "type": "bar_chart",
250
+ "title": "Shipments by carrier",
251
+ "bars": [
252
+ { "label": "FedEx", "value": 42 },
253
+ { "label": "DHL", "value": 31 }
254
+ ]
255
+ }
256
+ ```
257
+ ````
258
+
259
+ The `type` field determines which component renders. All supported types:
260
+
261
+ | `type` | Component | Key fields |
262
+ |---------------------|--------------------|--------------------------------------------------------------|
263
+ | `bar_chart` | `BarChart` | `bars: [{ label, value }]` |
264
+ | `line_chart` | `LineChart` | `xAxis` (label + categories), `yAxis` (label), `series` |
265
+ | `stacked_bar_chart` | `StackedBarChart` | Same as `line_chart` |
266
+ | `grouped_bar_chart` | `GroupedBarChart` | Same as `line_chart` |
267
+ | `dot_chart` | `DotChart` | `series[].points` with numeric `x`, categorical `y` |
268
+ | `histogram` | `HistogramChart` | `bins: [{ start, end, value }]` or `{ range, count }` |
269
+ | `scatter_plot` | `ScatterPlot` | Standard numeric axes + series |
270
+ | `stat_cards` | `StatCards` | `cards: [{ label, value, unit?, subtitle?, trend? }]` |
271
+ | `table` | `DataTable` | `columns` + `rows` |
272
+ | `timeline` | `Timeline` | `items: [{ label, start, end }]` (ISO 8601 dates) |
273
+
274
+ Charts are downloadable as PNG (via `html2canvas`). Each chart block is validated against its schema on render — invalid JSON or schema errors show an error card instead of crashing.
275
+
276
+ ### System prompt constant
277
+
278
+ Import `ORCHID_AI_VISUALIZATION_INSTRUCTIONS` to inject the chart format instructions into your AI system prompt:
279
+
280
+ ```js
281
+ import { ORCHID_AI_VISUALIZATION_INSTRUCTIONS } from 'orchid-ai';
282
+
283
+ const systemPrompt = `You are a helpful assistant. ${ORCHID_AI_VISUALIZATION_INSTRUCTIONS}`;
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Using visualization components standalone
289
+
290
+ Each chart component can be used outside the chat context:
291
+
292
+ ```jsx
293
+ import { BarChart } from 'orchid-ai';
294
+ import 'orchid-ai/orchid-ai.css';
295
+
296
+ <BarChart chart={{
297
+ type: 'bar_chart',
298
+ title: 'Revenue by month',
299
+ bars: [
300
+ { label: 'Jan', value: 120000 },
301
+ { label: 'Feb', value: 98000 },
302
+ ],
303
+ }} />
304
+ ```
package/orchid-ai.css CHANGED
@@ -2048,6 +2048,141 @@
2048
2048
  color: #b45309;
2049
2049
  }
2050
2050
 
2051
+ /* ── Query Summary ── */
2052
+
2053
+ .ai-chat-query-summary {
2054
+ margin-top: 10px;
2055
+ border-radius: 7px;
2056
+ border: 1px solid #e5e7eb;
2057
+ background: #f9fafb;
2058
+ font-size: 12px;
2059
+ }
2060
+
2061
+ .ai-chat-query-summary__summary {
2062
+ display: flex;
2063
+ align-items: center;
2064
+ justify-content: space-between;
2065
+ padding: 6px 10px;
2066
+ cursor: pointer;
2067
+ -webkit-user-select: none;
2068
+ user-select: none;
2069
+ list-style: none;
2070
+ color: #6b7280;
2071
+ gap: 8px;
2072
+ }
2073
+
2074
+ .ai-chat-query-summary__summary::-webkit-details-marker {
2075
+ display: none;
2076
+ }
2077
+
2078
+ .ai-chat-query-summary__summary:hover {
2079
+ color: #374151;
2080
+ }
2081
+
2082
+ .ai-chat-query-summary__label {
2083
+ font-size: 11px;
2084
+ font-weight: 500;
2085
+ letter-spacing: 0.02em;
2086
+ text-transform: uppercase;
2087
+ }
2088
+
2089
+ .ai-chat-query-summary__chevron {
2090
+ font-size: 10px;
2091
+ transition: transform 0.15s;
2092
+ flex-shrink: 0;
2093
+ }
2094
+
2095
+ .ai-chat-query-summary[open] .ai-chat-query-summary__chevron {
2096
+ transform: rotate(90deg);
2097
+ }
2098
+
2099
+ .ai-chat-query-summary__list {
2100
+ list-style: none;
2101
+ margin: 0;
2102
+ padding: 4px 10px 8px;
2103
+ display: flex;
2104
+ flex-direction: column;
2105
+ gap: 3px;
2106
+ border-top: 1px solid #e5e7eb;
2107
+ }
2108
+
2109
+ .ai-chat-query-summary__item {
2110
+ display: flex;
2111
+ gap: 6px;
2112
+ align-items: baseline;
2113
+ line-height: 1.5;
2114
+ }
2115
+
2116
+ .ai-chat-query-summary__key {
2117
+ color: #6b7280;
2118
+ white-space: nowrap;
2119
+ flex-shrink: 0;
2120
+ }
2121
+
2122
+ .ai-chat-query-summary__key::after {
2123
+ content: ':';
2124
+ }
2125
+
2126
+ .ai-chat-query-summary__value {
2127
+ color: #374151;
2128
+ word-break: break-word;
2129
+ }
2130
+
2131
+ /* ── Query sub-step (nested under a tool status row) ── */
2132
+
2133
+ .ai-chat-process-trace__step--query {
2134
+ padding-left: 16px;
2135
+ }
2136
+
2137
+ .ai-chat-process-trace__step--query .ai-chat-process-trace__rail {
2138
+ opacity: 0.4;
2139
+ }
2140
+
2141
+ .ai-chat-process-trace__step--query .ai-chat-process-trace__dot {
2142
+ width: 6px;
2143
+ height: 6px;
2144
+ background: var(--ai-process-dot, #9aa5b1);
2145
+ }
2146
+
2147
+ .ai-chat-process-trace__query-collection {
2148
+ display: block;
2149
+ font-size: 11px;
2150
+ font-weight: 600;
2151
+ letter-spacing: 0.04em;
2152
+ text-transform: uppercase;
2153
+ color: var(--ai-process-text, #5c6570);
2154
+ margin-bottom: 4px;
2155
+ }
2156
+
2157
+ .ai-chat-process-trace__query-params {
2158
+ display: grid;
2159
+ grid-template-columns: auto 1fr;
2160
+ gap: 1px 10px;
2161
+ margin: 0;
2162
+ }
2163
+
2164
+ .ai-chat-process-trace__query-param {
2165
+ display: contents;
2166
+ font-size: 11.5px;
2167
+ line-height: 1.5;
2168
+ }
2169
+
2170
+ .ai-chat-process-trace__query-param dt {
2171
+ color: var(--ai-process-text, #5c6570);
2172
+ font-weight: 500;
2173
+ white-space: nowrap;
2174
+ }
2175
+
2176
+ .ai-chat-process-trace__query-param dt::after {
2177
+ content: ":";
2178
+ }
2179
+
2180
+ .ai-chat-process-trace__query-param dd {
2181
+ color: var(--ai-process-text-strong, #2d3748);
2182
+ margin: 0;
2183
+ word-break: break-word;
2184
+ }
2185
+
2051
2186
  /* ── Message Actions ── */
2052
2187
 
2053
2188
  .ai-chat-message-actions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchid-ai",
3
- "version": "2.0.3",
3
+ "version": "2.1.1",
4
4
  "description": "Shared Orchid AI chat UI and visualization components",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -23,6 +23,7 @@ export default function ChatWindow({
23
23
  suggestions = DEFAULT_SUGGESTIONS,
24
24
  suggestionsDisabled = false,
25
25
  showProcessTracePanel = true,
26
+ showQuerySummary = false,
26
27
  }) {
27
28
  const exportPrefix = appName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
28
29
  const bottomRef = useRef(null);
@@ -123,6 +124,8 @@ export default function ChatWindow({
123
124
  processTrace={msg.processTrace}
124
125
  processInterimLive={msg.processInterimLive}
125
126
  showProcessTracePanel={showProcessTracePanel}
127
+ queryContext={msg.queryContext}
128
+ showQuerySummary={showQuerySummary}
126
129
  />
127
130
  );
128
131
  })}
@@ -46,6 +46,53 @@ function getBlockLabel(lang) {
46
46
  return `Generating ${lang}`;
47
47
  }
48
48
 
49
+ const UPPER_ABBREV = new Set(['id', 'url', 'api', 'uuid', 'ip', 'sku', 'po', 'eta', 'ref']);
50
+
51
+ function camelToTitleCase(str) {
52
+ const words = String(str)
53
+ .replace(/_/g, ' ')
54
+ .replace(/([A-Z])/g, ' $1')
55
+ .trim()
56
+ .split(/\s+/);
57
+ return words
58
+ .map((w) => {
59
+ const lower = w.toLowerCase();
60
+ if (UPPER_ABBREV.has(lower)) return lower.toUpperCase();
61
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
62
+ })
63
+ .join(' ');
64
+ }
65
+
66
+ function formatQueryValue(value) {
67
+ if (Array.isArray(value)) return value.map(String).join(', ');
68
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No';
69
+ return String(value);
70
+ }
71
+
72
+ function QuerySummaryPanel({ queryContext }) {
73
+ if (!queryContext || typeof queryContext !== 'object' || Array.isArray(queryContext)) return null;
74
+ const entries = Object.entries(queryContext).filter(
75
+ ([, v]) => v !== null && v !== undefined && v !== ''
76
+ );
77
+ if (!entries.length) return null;
78
+ return (
79
+ <details className="ai-chat-query-summary">
80
+ <summary className="ai-chat-query-summary__summary">
81
+ <span className="ai-chat-query-summary__label">Filters used</span>
82
+ <span className="ai-chat-query-summary__chevron" aria-hidden>▸</span>
83
+ </summary>
84
+ <ul className="ai-chat-query-summary__list">
85
+ {entries.map(([key, value]) => (
86
+ <li key={key} className="ai-chat-query-summary__item">
87
+ <span className="ai-chat-query-summary__key">{camelToTitleCase(key)}</span>
88
+ <span className="ai-chat-query-summary__value">{formatQueryValue(value)}</span>
89
+ </li>
90
+ ))}
91
+ </ul>
92
+ </details>
93
+ );
94
+ }
95
+
49
96
  const TITLE_COMMENT_RE = /<!--\s*title:\s*([^-][^>]*?)\s*-->/gi;
50
97
 
51
98
  /** Prefer the last `<!--title:...-->` so end-of-reply titles work; still removes legacy start-of-reply comments. */
@@ -100,6 +147,37 @@ function UserBubbleContent({ content }) {
100
147
  ));
101
148
  }
102
149
 
150
+ function formatToolName(tool) {
151
+ return String(tool)
152
+ .replace(/^(query_|get_)/, '')
153
+ .replace(/_/g, ' ')
154
+ .replace(/\b\w/g, (c) => c.toUpperCase());
155
+ }
156
+
157
+ function QueryStep({ entry }) {
158
+ const params = Object.entries(entry.input || {}).filter(([, v]) => v !== null && v !== undefined && v !== '');
159
+ return (
160
+ <li className="ai-chat-process-trace__step ai-chat-process-trace__step--query">
161
+ <div className="ai-chat-process-trace__rail" aria-hidden>
162
+ <span className="ai-chat-process-trace__dot" />
163
+ </div>
164
+ <div className="ai-chat-process-trace__body">
165
+ <span className="ai-chat-process-trace__query-collection">{formatToolName(entry.tool)}</span>
166
+ {params.length > 0 && (
167
+ <dl className="ai-chat-process-trace__query-params">
168
+ {params.map(([key, value]) => (
169
+ <div key={key} className="ai-chat-process-trace__query-param">
170
+ <dt>{camelToTitleCase(key)}</dt>
171
+ <dd>{formatQueryValue(value)}</dd>
172
+ </div>
173
+ ))}
174
+ </dl>
175
+ )}
176
+ </div>
177
+ </li>
178
+ );
179
+ }
180
+
103
181
  function ProcessTracePanel({ processTrace, processInterimLive, isStreaming, showProcessTracePanel }) {
104
182
  if (showProcessTracePanel === false) return null;
105
183
 
@@ -140,6 +218,9 @@ function ProcessTracePanel({ processTrace, processInterimLive, isStreaming, show
140
218
  <div className="ai-chat-process-trace__panel">
141
219
  <ul className="ai-chat-process-trace__timeline">
142
220
  {items.map((entry, i) => {
221
+ if (entry.type === 'query') {
222
+ return <QueryStep key={i} entry={entry} />;
223
+ }
143
224
  const lane = orchidAiProcessTraceEntryKind(entry);
144
225
  const isLiveStatus = isStreaming && !processInterimLive && i === items.length - 1 && entry.type === "status";
145
226
  return (
@@ -154,9 +235,7 @@ function ProcessTracePanel({ processTrace, processInterimLive, isStreaming, show
154
235
  {entry.type === "status" ? (
155
236
  <span className="ai-chat-process-trace__line">{entry.value}</span>
156
237
  ) : (
157
- <div className="ai-chat-process-trace__prose">
158
- {entry.value}
159
- </div>
238
+ <div className="ai-chat-process-trace__prose">{entry.value}</div>
160
239
  )}
161
240
  </div>
162
241
  </li>
@@ -127,6 +127,9 @@ export function useOrchidAiChat({ endpoint, buildBody, getHeaders, showStatus =
127
127
  isClearStream: orchidAiStatusClearsStreamBuffer(event.text),
128
128
  });
129
129
  patchStreamingAssistant();
130
+ } else if (event.type === 'query') {
131
+ collector.onQuery(event.tool, event.input);
132
+ patchStreamingAssistant();
130
133
  } else if (event.type === 'delta') {
131
134
  collector.onDelta(event.text);
132
135
  patchStreamingAssistant();
@@ -144,6 +147,7 @@ export function useOrchidAiChat({ endpoint, buildBody, getHeaders, showStatus =
144
147
  content: event.response,
145
148
  truncated: event.truncated === true,
146
149
  ...(trace ? { processTrace: trace } : {}),
150
+ ...(event.queryContext ? { queryContext: event.queryContext } : {}),
147
151
  };
148
152
  if (last?.role === 'assistant' && last?.isStreaming) {
149
153
  next[next.length - 1] = finalMsg;
@@ -175,7 +179,12 @@ export function useOrchidAiChat({ endpoint, buildBody, getHeaders, showStatus =
175
179
  } else {
176
180
  const data = await response.json();
177
181
  if (response.ok) {
178
- addMessage({ role: 'assistant', content: data.response, truncated: data.truncated });
182
+ addMessage({
183
+ role: 'assistant',
184
+ content: data.response,
185
+ truncated: data.truncated,
186
+ ...(data.queryContext ? { queryContext: data.queryContext } : {}),
187
+ });
179
188
  } else {
180
189
  addMessage({ role: 'assistant', content: data.error || 'Something went wrong.' });
181
190
  }
package/src/index.d.ts CHANGED
@@ -15,6 +15,12 @@ export interface ChatMessage {
15
15
  };
16
16
  /** Live tool preamble not yet flushed into processTrace.items (streaming only). */
17
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>;
18
24
  }
19
25
 
20
26
  export interface ChatWindowProps {
@@ -26,6 +32,12 @@ export interface ChatWindowProps {
26
32
  organisationName?: string;
27
33
  /** When false, tool/interim statuses show inline next to typing dots (Hermes / iLink config). */
28
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;
29
41
  /** Any extra props are forwarded to the root element */
30
42
  [key: string]: unknown;
31
43
  }
@@ -49,6 +61,8 @@ export interface MessageProps {
49
61
  processTrace?: ChatMessage['processTrace'];
50
62
  processInterimLive?: string;
51
63
  showProcessTracePanel?: boolean;
64
+ queryContext?: Record<string, unknown>;
65
+ showQuerySummary?: boolean;
52
66
  }
53
67
 
54
68
  export const ChatWindow: React.FC<ChatWindowProps>;
@@ -159,27 +173,32 @@ export function orchidAiProcessTraceEntryKind(entry: {
159
173
  value: string;
160
174
  }): 'text' | 'tool' | 'compile' | 'mind';
161
175
 
176
+ export type OrchidAiProcessTraceItem =
177
+ | { type: 'status' | 'text'; value: string }
178
+ | { type: 'query'; tool: string; input: Record<string, unknown> };
179
+
162
180
  export function createOrchidAiProcessTraceCollector(): {
181
+ onQuery(tool: string, input: Record<string, unknown>): void;
163
182
  onStatus(text: string, opts?: { isClearStream?: boolean }): void;
164
183
  onDelta(text: string): void;
165
184
  getLiveMain(): string;
166
185
  getLiveInterim(): string;
167
- getItems(): Array<{ type: 'status' | 'text'; value: string }>;
186
+ getItems(): Array<OrchidAiProcessTraceItem>;
168
187
  reset(): void;
169
188
  buildPersistedTrace():
170
- | { items: Array<{ type: 'status' | 'text'; value: string }>; defaultCollapsed: boolean }
189
+ | { items: Array<OrchidAiProcessTraceItem>; defaultCollapsed: boolean }
171
190
  | undefined;
172
191
  };
173
192
 
174
193
  export function snapshotOrchidAiProcessTraceItems(
175
194
  collector: ReturnType<typeof createOrchidAiProcessTraceCollector>
176
- ): { items: Array<{ type: 'status' | 'text'; value: string }>; defaultCollapsed: boolean };
195
+ ): { items: Array<OrchidAiProcessTraceItem>; defaultCollapsed: boolean };
177
196
 
178
197
  export function augmentLiveProcessTraceSnapshot(
179
- trace: { items: Array<{ type: 'status' | 'text'; value: string }>; defaultCollapsed: boolean },
198
+ trace: { items: Array<OrchidAiProcessTraceItem>; defaultCollapsed: boolean },
180
199
  streamingMain: string,
181
200
  liveInterim?: string
182
- ): { items: Array<{ type: 'status' | 'text'; value: string }>; defaultCollapsed: boolean };
201
+ ): { items: Array<OrchidAiProcessTraceItem>; defaultCollapsed: boolean };
183
202
 
184
203
  export function orchidAiProcessTraceHasDisplayableContent(
185
204
  trace: ChatMessage['processTrace'] | undefined,
@@ -50,6 +50,13 @@ export function createOrchidAiProcessTraceCollector() {
50
50
  }
51
51
 
52
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
+ },
53
60
  /**
54
61
  * @param {string} text
55
62
  * @param {{ isClearStream?: boolean }} [opts] — true for {@link ORCHID_AI_SSE_STATUS_CLEAR_STREAM}
@@ -98,18 +105,23 @@ export function createOrchidAiProcessTraceCollector() {
98
105
  buildPersistedTrace() {
99
106
  if (!items.length) return undefined;
100
107
  return {
101
- items: items.map((x) => ({ type: x.type, value: x.value })),
108
+ items: items.map(serializeTraceItem),
102
109
  defaultCollapsed: true,
103
110
  };
104
111
  },
105
112
  };
106
113
  }
107
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
+
108
120
  /** Snapshot `items` for React state / persisted shape (live streaming; not collapsed). */
109
121
  export function snapshotOrchidAiProcessTraceItems(collector) {
110
122
  const items = collector.getItems();
111
123
  return {
112
- items: items.map((x) => ({ type: x.type, value: x.value })),
124
+ items: items.map(serializeTraceItem),
113
125
  defaultCollapsed: false,
114
126
  };
115
127
  }