orchid-ai 2.0.0 → 2.0.2
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 +66 -25
- package/package.json +2 -1
- package/src/components/ChatWindow.jsx +36 -22
- package/src/components/Message.jsx +57 -15
- package/src/hooks/useOrchidAiChat.js +136 -0
- package/src/index.d.ts +139 -0
- package/src/index.js +1 -0
package/orchid-ai.css
CHANGED
|
@@ -244,6 +244,9 @@
|
|
|
244
244
|
border: 1px solid #e5e7eb;
|
|
245
245
|
border-radius: 12px;
|
|
246
246
|
padding: 16px 20px;
|
|
247
|
+
width: 100%;
|
|
248
|
+
max-width: 420px;
|
|
249
|
+
box-sizing: border-box;
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
.ai-chat-suggestions span {
|
|
@@ -260,6 +263,7 @@
|
|
|
260
263
|
display: flex;
|
|
261
264
|
flex-direction: column;
|
|
262
265
|
gap: 8px;
|
|
266
|
+
padding: 0;
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
.ai-chat-suggestions li {
|
|
@@ -271,6 +275,8 @@
|
|
|
271
275
|
border-radius: 8px;
|
|
272
276
|
cursor: pointer;
|
|
273
277
|
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
278
|
+
width: 100%;
|
|
279
|
+
box-sizing: border-box;
|
|
274
280
|
}
|
|
275
281
|
|
|
276
282
|
.ai-chat-suggestions li:hover {
|
|
@@ -279,6 +285,17 @@
|
|
|
279
285
|
color: #1eaaf1;
|
|
280
286
|
}
|
|
281
287
|
|
|
288
|
+
.ai-chat-suggestions--disabled li {
|
|
289
|
+
opacity: 0.45;
|
|
290
|
+
cursor: not-allowed;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.ai-chat-suggestions--disabled li:hover {
|
|
294
|
+
background: #f9fafb;
|
|
295
|
+
border-color: #e5e7eb;
|
|
296
|
+
color: #4b5563;
|
|
297
|
+
}
|
|
298
|
+
|
|
282
299
|
/* ── Messages ── */
|
|
283
300
|
|
|
284
301
|
.ai-chat-message {
|
|
@@ -347,6 +364,16 @@
|
|
|
347
364
|
border-bottom-right-radius: 4px;
|
|
348
365
|
}
|
|
349
366
|
|
|
367
|
+
.ai-chat-bubble.user .ai-chat-user-link {
|
|
368
|
+
color: #e0f2fe;
|
|
369
|
+
text-decoration: underline;
|
|
370
|
+
word-break: break-all;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.ai-chat-bubble.user .ai-chat-user-link:hover {
|
|
374
|
+
color: #ffffff;
|
|
375
|
+
}
|
|
376
|
+
|
|
350
377
|
.ai-chat-bubble.assistant {
|
|
351
378
|
position: relative;
|
|
352
379
|
background: #ffffff;
|
|
@@ -1917,47 +1944,61 @@
|
|
|
1917
1944
|
/* ── Print (Single Response) ── */
|
|
1918
1945
|
|
|
1919
1946
|
@media print {
|
|
1920
|
-
@page {
|
|
1921
|
-
|
|
1947
|
+
@page { margin: 12mm; }
|
|
1948
|
+
|
|
1949
|
+
/*
|
|
1950
|
+
* visibility:hidden on body allows #ai-cortex-print-section descendants to
|
|
1951
|
+
* override with visibility:visible — this is a CSS guarantee that display:none
|
|
1952
|
+
* does NOT offer (Chrome's print engine ignores the specificity override for
|
|
1953
|
+
* display:none !important on body > *).
|
|
1954
|
+
*
|
|
1955
|
+
* Siblings are collapsed to height:0 so no blank space precedes the section.
|
|
1956
|
+
* position:absolute (not fixed) allows content to paginate across pages.
|
|
1957
|
+
*/
|
|
1958
|
+
body.ai-chat-printing {
|
|
1959
|
+
visibility: hidden !important;
|
|
1960
|
+
position: relative !important;
|
|
1922
1961
|
}
|
|
1923
1962
|
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1963
|
+
body.ai-chat-printing > *:not(#ai-cortex-print-section) {
|
|
1964
|
+
height: 0 !important;
|
|
1965
|
+
overflow: hidden !important;
|
|
1927
1966
|
}
|
|
1928
1967
|
|
|
1929
1968
|
body.ai-chat-printing #ai-cortex-print-section {
|
|
1969
|
+
visibility: visible !important;
|
|
1930
1970
|
display: block !important;
|
|
1971
|
+
position: absolute !important;
|
|
1972
|
+
top: 0 !important;
|
|
1973
|
+
left: 0 !important;
|
|
1974
|
+
width: 100% !important;
|
|
1975
|
+
padding: 0 !important;
|
|
1976
|
+
background: #ffffff;
|
|
1977
|
+
color: #1f2937;
|
|
1978
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
1979
|
+
font-size: 14px;
|
|
1980
|
+
line-height: 1.6;
|
|
1931
1981
|
}
|
|
1932
1982
|
|
|
1933
|
-
/*
|
|
1934
|
-
* Kill all CSS animations and transitions inside the print section.
|
|
1935
|
-
* Browsers reset animations during the print reflow, catching elements at
|
|
1936
|
-
* their `from` keyframe (opacity: 0, scale: 0) rather than their final state.
|
|
1937
|
-
* Disabling animations lets elements fall back to their base CSS styles,
|
|
1938
|
-
* which are always the fully-visible final appearance.
|
|
1939
|
-
*
|
|
1940
|
-
* print-color-adjust: exact forces Chrome to render background colours and
|
|
1941
|
-
* images — without it Chrome strips inline background fills (e.g. dot colours
|
|
1942
|
-
* on scatter/dot charts) and the dots appear as white outlines only.
|
|
1943
|
-
*/
|
|
1944
1983
|
body.ai-chat-printing #ai-cortex-print-section * {
|
|
1984
|
+
visibility: visible !important;
|
|
1945
1985
|
animation: none !important;
|
|
1946
1986
|
transition: none !important;
|
|
1987
|
+
/* Kill any opacity:0 left by a stopped animation */
|
|
1988
|
+
opacity: 1 !important;
|
|
1947
1989
|
-webkit-print-color-adjust: exact !important;
|
|
1948
1990
|
print-color-adjust: exact !important;
|
|
1949
1991
|
}
|
|
1950
1992
|
|
|
1951
|
-
/* Clean up the bubble for print */
|
|
1952
1993
|
body.ai-chat-printing #ai-cortex-print-section .ai-chat-bubble.assistant {
|
|
1953
|
-
max-width: none;
|
|
1954
|
-
width: 100
|
|
1955
|
-
border: none;
|
|
1956
|
-
border-radius: 0;
|
|
1957
|
-
padding: 0;
|
|
1958
|
-
background: #ffffff;
|
|
1959
|
-
color: #
|
|
1960
|
-
box-shadow: none;
|
|
1994
|
+
max-width: none !important;
|
|
1995
|
+
width: 100% !important;
|
|
1996
|
+
border: none !important;
|
|
1997
|
+
border-radius: 0 !important;
|
|
1998
|
+
padding: 0 !important;
|
|
1999
|
+
background: #ffffff !important;
|
|
2000
|
+
color: #1f2937 !important;
|
|
2001
|
+
box-shadow: none !important;
|
|
1961
2002
|
}
|
|
1962
2003
|
}
|
|
1963
2004
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orchid-ai",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Shared Orchid AI chat UI and visualization components",
|
|
5
5
|
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
6
7
|
"exports": {
|
|
7
8
|
".": "./src/index.js",
|
|
8
9
|
"./orchid-ai.css": "./orchid-ai.css",
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import React, { useRef, useEffect } from 'react';
|
|
2
2
|
import Message from './Message';
|
|
3
3
|
|
|
4
|
+
const DEFAULT_SUGGESTIONS = [
|
|
5
|
+
'Give me brief tips for navigating iLink.',
|
|
6
|
+
'What should I check before starting a dispatch?',
|
|
7
|
+
'How do I narrow down a search in Hermes command search (⌘K)?',
|
|
8
|
+
];
|
|
9
|
+
|
|
4
10
|
/**
|
|
5
|
-
*
|
|
11
|
+
* Orchid AI chat window (Markdown + optional orchid-ai-chart fenced blocks).
|
|
6
12
|
*/
|
|
7
13
|
export default function ChatWindow({
|
|
8
14
|
messages,
|
|
@@ -11,7 +17,13 @@ export default function ChatWindow({
|
|
|
11
17
|
onSuggestionClick,
|
|
12
18
|
aiEnabled,
|
|
13
19
|
organisationName,
|
|
20
|
+
appName = "Hermes Chat",
|
|
21
|
+
unavailableMessage,
|
|
22
|
+
emptyDescription,
|
|
23
|
+
suggestions = DEFAULT_SUGGESTIONS,
|
|
24
|
+
suggestionsDisabled = false,
|
|
14
25
|
}) {
|
|
26
|
+
const exportPrefix = appName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
15
27
|
const bottomRef = useRef(null);
|
|
16
28
|
|
|
17
29
|
useEffect(() => {
|
|
@@ -20,6 +32,8 @@ export default function ChatWindow({
|
|
|
20
32
|
|
|
21
33
|
const renderEmptyState = () => {
|
|
22
34
|
if (!aiEnabled) {
|
|
35
|
+
const msg = unavailableMessage ??
|
|
36
|
+
`${appName} needs an Anthropic API key on the server. Contact your administrator if this persists.`;
|
|
23
37
|
return (
|
|
24
38
|
<div className="ai-chat-empty">
|
|
25
39
|
<div className="ai-chat-empty-icon">
|
|
@@ -37,16 +51,14 @@ export default function ChatWindow({
|
|
|
37
51
|
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07" />
|
|
38
52
|
</svg>
|
|
39
53
|
</div>
|
|
40
|
-
<h2>
|
|
41
|
-
<p>
|
|
42
|
-
Hermes Chat needs an Anthropic API key on the server and an active organisation. Contact your administrator
|
|
43
|
-
if this persists.
|
|
44
|
-
</p>
|
|
54
|
+
<h2>{appName} unavailable</h2>
|
|
55
|
+
<p>{msg}</p>
|
|
45
56
|
</div>
|
|
46
57
|
);
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
const scope = organisationName || 'your organisation';
|
|
61
|
+
const description = emptyDescription ?? `Ask about ${scope} — shipments, schedules, data insights, or what to explore next.`;
|
|
50
62
|
|
|
51
63
|
return (
|
|
52
64
|
<div className="ai-chat-empty">
|
|
@@ -67,21 +79,23 @@ export default function ChatWindow({
|
|
|
67
79
|
</svg>
|
|
68
80
|
</div>
|
|
69
81
|
<h2>How can I help?</h2>
|
|
70
|
-
<p>{
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
<p>{description}</p>
|
|
83
|
+
{suggestions.length > 0 && (
|
|
84
|
+
<div className={`ai-chat-suggestions${suggestionsDisabled ? ' ai-chat-suggestions--disabled' : ''}`}>
|
|
85
|
+
<span>Try asking:</span>
|
|
86
|
+
<ul>
|
|
87
|
+
{suggestions.map((s) => (
|
|
88
|
+
<li
|
|
89
|
+
key={s}
|
|
90
|
+
onClick={suggestionsDisabled ? undefined : () => onSuggestionClick(s)}
|
|
91
|
+
aria-disabled={suggestionsDisabled || undefined}
|
|
92
|
+
>
|
|
93
|
+
{s}
|
|
94
|
+
</li>
|
|
95
|
+
))}
|
|
96
|
+
</ul>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
85
99
|
</div>
|
|
86
100
|
);
|
|
87
101
|
};
|
|
@@ -90,7 +104,7 @@ export default function ChatWindow({
|
|
|
90
104
|
<div className="ai-chat-window">
|
|
91
105
|
{messages?.length === 0 && !loading && renderEmptyState()}
|
|
92
106
|
{(messages ?? []).map((msg, i) => (
|
|
93
|
-
<Message key={i} role={msg.role} content={msg.content} truncated={msg.truncated} />
|
|
107
|
+
<Message key={i} role={msg.role} content={msg.content} truncated={msg.truncated} exportPrefix={exportPrefix} />
|
|
94
108
|
))}
|
|
95
109
|
{loading && (
|
|
96
110
|
<div className="ai-chat-message assistant">
|
|
@@ -9,7 +9,46 @@ const IS_DEV = process.env.NODE_ENV === "development";
|
|
|
9
9
|
|
|
10
10
|
const TITLE_RE = /<!--\s*title:\s*([^-][^>]*?)\s*-->/i;
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/** Split on http(s) URLs for lightweight linkify in user bubbles (plain text, not full markdown). */
|
|
13
|
+
const URL_INLINE_RE = /(https?:\/\/[^\s<>`]+)/gi;
|
|
14
|
+
|
|
15
|
+
function trimTrailingUrlPunctuation(href) {
|
|
16
|
+
return href.replace(/[),.;:!?'"\]}>]+$/g, "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function linkifyUserLine(line) {
|
|
20
|
+
const parts = String(line).split(URL_INLINE_RE);
|
|
21
|
+
return parts.map((part, i) => {
|
|
22
|
+
if (part === "") return null;
|
|
23
|
+
if (/^https?:\/\//i.test(part)) {
|
|
24
|
+
const href = trimTrailingUrlPunctuation(part);
|
|
25
|
+
return (
|
|
26
|
+
<a
|
|
27
|
+
key={i}
|
|
28
|
+
href={href}
|
|
29
|
+
target="_blank"
|
|
30
|
+
rel="noopener noreferrer"
|
|
31
|
+
className="ai-chat-user-link"
|
|
32
|
+
>
|
|
33
|
+
{part}
|
|
34
|
+
</a>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return <React.Fragment key={i}>{part}</React.Fragment>;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function UserBubbleContent({ content }) {
|
|
42
|
+
const lines = String(content).split("\n");
|
|
43
|
+
return lines.map((line, li) => (
|
|
44
|
+
<React.Fragment key={li}>
|
|
45
|
+
{li > 0 ? <br /> : null}
|
|
46
|
+
{linkifyUserLine(line)}
|
|
47
|
+
</React.Fragment>
|
|
48
|
+
));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function Message({ role, content, truncated, exportPrefix = "orchid-ai" }) {
|
|
13
52
|
const isUser = role === "user";
|
|
14
53
|
const [copied, setCopied] = useState(false);
|
|
15
54
|
const [isPrinting, setIsPrinting] = useState(false);
|
|
@@ -56,23 +95,21 @@ export default function Message({ role, content, truncated }) {
|
|
|
56
95
|
const pad = (v) => String(v).padStart(2, "0");
|
|
57
96
|
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}`;
|
|
58
97
|
|
|
98
|
+
setIsPrinting(true);
|
|
99
|
+
|
|
100
|
+
const clone = bubbleContent.cloneNode(true);
|
|
101
|
+
clone.querySelectorAll(".ai-chart-export-actions, .ai-chat-message-actions").forEach((n) => n.remove());
|
|
102
|
+
|
|
59
103
|
setIsPrinting(true);
|
|
60
104
|
const previousTitle = document.title;
|
|
61
|
-
document.title =
|
|
105
|
+
document.title = `${exportPrefix}-${slug}-${timestamp}`;
|
|
62
106
|
|
|
63
|
-
// Clone the bubble content into an isolated print section at the body root.
|
|
64
|
-
// This keeps the layout clean (no surrounding app chrome) while the CSS
|
|
65
|
-
// animation: none rule ensures chart elements render at their final visible
|
|
66
|
-
// state rather than being caught mid-animation by the print reflow.
|
|
67
107
|
let printSection = document.getElementById("ai-cortex-print-section");
|
|
68
108
|
if (!printSection) {
|
|
69
109
|
printSection = document.createElement("div");
|
|
70
110
|
printSection.id = "ai-cortex-print-section";
|
|
71
111
|
document.body.appendChild(printSection);
|
|
72
112
|
}
|
|
73
|
-
|
|
74
|
-
const clone = bubbleContent.cloneNode(true);
|
|
75
|
-
clone.querySelectorAll(".ai-chart-export-actions, .ai-chat-message-actions").forEach((n) => n.remove());
|
|
76
113
|
printSection.innerHTML = "";
|
|
77
114
|
printSection.appendChild(clone);
|
|
78
115
|
document.body.classList.add("ai-chat-printing");
|
|
@@ -147,6 +184,16 @@ export default function Message({ role, content, truncated }) {
|
|
|
147
184
|
</code>
|
|
148
185
|
);
|
|
149
186
|
},
|
|
187
|
+
a({ href, children, ...props }) {
|
|
188
|
+
if (typeof href !== "string" || !/^https?:\/\//i.test(href)) {
|
|
189
|
+
return <span {...props}>{children}</span>;
|
|
190
|
+
}
|
|
191
|
+
return (
|
|
192
|
+
<a {...props} href={href} target="_blank" rel="noopener noreferrer">
|
|
193
|
+
{children}
|
|
194
|
+
</a>
|
|
195
|
+
);
|
|
196
|
+
},
|
|
150
197
|
};
|
|
151
198
|
|
|
152
199
|
return (
|
|
@@ -157,12 +204,7 @@ export default function Message({ role, content, truncated }) {
|
|
|
157
204
|
<div className={`ai-chat-bubble ${role}`}>
|
|
158
205
|
<div className="ai-chat-message-content">
|
|
159
206
|
{isUser ? (
|
|
160
|
-
content
|
|
161
|
-
<React.Fragment key={i}>
|
|
162
|
-
{line}
|
|
163
|
-
{i < content.split("\n").length - 1 && <br />}
|
|
164
|
-
</React.Fragment>
|
|
165
|
-
))
|
|
207
|
+
<UserBubbleContent content={content} />
|
|
166
208
|
) : (
|
|
167
209
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>{renderContent}</ReactMarkdown>
|
|
168
210
|
)}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default status message strings. Export these so server-side code can import
|
|
5
|
+
* and reference the same values, making it easy to keep client and server in sync
|
|
6
|
+
* or swap them out per-app.
|
|
7
|
+
*
|
|
8
|
+
* Set showStatus: false on the hook to disable status display entirely.
|
|
9
|
+
*/
|
|
10
|
+
export const ORCHID_AI_DEFAULT_STATUS = {
|
|
11
|
+
thinking: 'Thinking',
|
|
12
|
+
compilingResponse: 'Compiling response',
|
|
13
|
+
lookingUpData: 'Looking up data',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* React hook for Orchid AI chat. Handles SSE streaming when the server responds
|
|
18
|
+
* with Content-Type: text/event-stream, falling back to plain JSON for non-streaming
|
|
19
|
+
* backends.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} opts
|
|
22
|
+
* @param {string} opts.endpoint - POST endpoint URL
|
|
23
|
+
* @param {(userMessage: string, history: Array, sendOptions?: object) => object} opts.buildBody - builds the request body
|
|
24
|
+
* @param {() => object} [opts.getHeaders] - returns extra request headers (e.g. CSRF token)
|
|
25
|
+
* @param {boolean} [opts.showStatus=true] - set false to suppress status text entirely
|
|
26
|
+
* @param {Array} [opts.initialMessages=[]] - seed the conversation (e.g. from localStorage)
|
|
27
|
+
*
|
|
28
|
+
* @returns {{ messages, loading, statusText, sendMessage, clearMessages }}
|
|
29
|
+
*/
|
|
30
|
+
export function useOrchidAiChat({ endpoint, buildBody, getHeaders, showStatus = true, initialMessages = [] }) {
|
|
31
|
+
const [messages, setMessages] = useState(initialMessages);
|
|
32
|
+
const [loading, setLoading] = useState(false);
|
|
33
|
+
const [statusText, setStatusText] = useState('');
|
|
34
|
+
|
|
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
|
+
const messagesRef = useRef(initialMessages);
|
|
38
|
+
|
|
39
|
+
// Keep latest callbacks in refs so sendMessage identity stays stable regardless
|
|
40
|
+
// of whether the parent re-creates buildBody/getHeaders each render.
|
|
41
|
+
const buildBodyRef = useRef(buildBody);
|
|
42
|
+
const getHeadersRef = useRef(getHeaders);
|
|
43
|
+
buildBodyRef.current = buildBody;
|
|
44
|
+
getHeadersRef.current = getHeaders;
|
|
45
|
+
|
|
46
|
+
const addMessage = useCallback((msg) => {
|
|
47
|
+
setMessages((prev) => {
|
|
48
|
+
const next = [...prev, msg];
|
|
49
|
+
messagesRef.current = next;
|
|
50
|
+
return next;
|
|
51
|
+
});
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const sendMessage = useCallback(
|
|
55
|
+
async (userMessage, sendOptions = {}) => {
|
|
56
|
+
const history = messagesRef.current.slice();
|
|
57
|
+
addMessage({ role: 'user', content: userMessage });
|
|
58
|
+
setLoading(true);
|
|
59
|
+
setStatusText('');
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(endpoint, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
...getHeadersRef.current?.(),
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(buildBodyRef.current(userMessage, history, sendOptions)),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
72
|
+
|
|
73
|
+
if (contentType.includes('text/event-stream')) {
|
|
74
|
+
const reader = response.body.getReader();
|
|
75
|
+
const decoder = new TextDecoder();
|
|
76
|
+
let buffer = '';
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done) break;
|
|
81
|
+
buffer += decoder.decode(value, { stream: true });
|
|
82
|
+
const lines = buffer.split('\n');
|
|
83
|
+
buffer = lines.pop() ?? '';
|
|
84
|
+
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (!line.startsWith('data: ')) continue;
|
|
87
|
+
try {
|
|
88
|
+
const event = JSON.parse(line.slice(6));
|
|
89
|
+
if (event.type === 'status' && showStatus) {
|
|
90
|
+
setStatusText(event.text);
|
|
91
|
+
} else if (event.type === 'done') {
|
|
92
|
+
addMessage({
|
|
93
|
+
role: 'assistant',
|
|
94
|
+
content: event.response,
|
|
95
|
+
truncated: event.truncated,
|
|
96
|
+
});
|
|
97
|
+
} else if (event.type === 'error') {
|
|
98
|
+
addMessage({
|
|
99
|
+
role: 'assistant',
|
|
100
|
+
content: event.error || 'Something went wrong.',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// ignore malformed SSE lines
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
// Plain JSON fallback for non-streaming backends
|
|
110
|
+
const data = await response.json();
|
|
111
|
+
if (response.ok) {
|
|
112
|
+
addMessage({ role: 'assistant', content: data.response, truncated: data.truncated });
|
|
113
|
+
} else {
|
|
114
|
+
addMessage({ role: 'assistant', content: data.error || 'Something went wrong.' });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
addMessage({
|
|
119
|
+
role: 'assistant',
|
|
120
|
+
content: 'Network error. Please check your connection and try again.',
|
|
121
|
+
});
|
|
122
|
+
} finally {
|
|
123
|
+
setLoading(false);
|
|
124
|
+
setStatusText('');
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
[endpoint, showStatus, addMessage]
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const clearMessages = useCallback(() => {
|
|
131
|
+
setMessages([]);
|
|
132
|
+
messagesRef.current = [];
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
return { messages, loading, statusText, sendMessage, clearMessages };
|
|
136
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
// ─── Chat UI ────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface ChatMessage {
|
|
6
|
+
role: 'user' | 'assistant';
|
|
7
|
+
content: string;
|
|
8
|
+
truncated?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ChatWindowProps {
|
|
12
|
+
messages: ChatMessage[];
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
statusText?: string;
|
|
15
|
+
onSuggestionClick?: (text: string) => void;
|
|
16
|
+
aiEnabled?: boolean;
|
|
17
|
+
organisationName?: string;
|
|
18
|
+
/** Any extra props are forwarded to the root element */
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ChatInputProps {
|
|
23
|
+
onSend: (text: string) => void;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
disabledReason?: string;
|
|
26
|
+
onDisabledClick?: () => void;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MessageProps {
|
|
31
|
+
role: 'user' | 'assistant';
|
|
32
|
+
content: string;
|
|
33
|
+
truncated?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const ChatWindow: React.FC<ChatWindowProps>;
|
|
37
|
+
export const ChatInput: React.FC<ChatInputProps>;
|
|
38
|
+
export const Message: React.FC<MessageProps>;
|
|
39
|
+
|
|
40
|
+
// ─── useOrchidAiChat ─────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/** Optional flags merged into the request by `buildBody` (e.g. web research for Hermes). */
|
|
43
|
+
export interface OrchidAiChatSendOptions {
|
|
44
|
+
allowWebResearch?: boolean;
|
|
45
|
+
urlsToCheck?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface OrchidAiChatOptions {
|
|
49
|
+
/** POST endpoint that accepts the chat message */
|
|
50
|
+
endpoint: string;
|
|
51
|
+
/**
|
|
52
|
+
* Builds the request body from the user message, history, and per-send options.
|
|
53
|
+
* The third argument is only present when callers use `sendMessage(text, options)`.
|
|
54
|
+
*/
|
|
55
|
+
buildBody: (
|
|
56
|
+
userMessage: string,
|
|
57
|
+
history: ChatMessage[],
|
|
58
|
+
sendOptions?: OrchidAiChatSendOptions
|
|
59
|
+
) => Record<string, unknown>;
|
|
60
|
+
/** Returns extra request headers (e.g. CSRF token) */
|
|
61
|
+
getHeaders?: () => Record<string, string>;
|
|
62
|
+
/**
|
|
63
|
+
* Whether to display status messages during processing.
|
|
64
|
+
* Set to false to suppress all status text. Defaults to true.
|
|
65
|
+
*/
|
|
66
|
+
showStatus?: boolean;
|
|
67
|
+
/** Initial transcript when mounting the hook */
|
|
68
|
+
initialMessages?: ChatMessage[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface OrchidAiChatState {
|
|
72
|
+
messages: ChatMessage[];
|
|
73
|
+
loading: boolean;
|
|
74
|
+
/** Live status text emitted by the server (e.g. "Thinking", "Compiling response") */
|
|
75
|
+
statusText: string;
|
|
76
|
+
sendMessage: (userMessage: string, sendOptions?: OrchidAiChatSendOptions) => Promise<void>;
|
|
77
|
+
clearMessages: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function useOrchidAiChat(options: OrchidAiChatOptions): OrchidAiChatState;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Default status message strings used by the Orchid AI backend services.
|
|
84
|
+
* Import these in server-side code to keep client and server labels in sync,
|
|
85
|
+
* or to override individual values per app.
|
|
86
|
+
*/
|
|
87
|
+
export const ORCHID_AI_DEFAULT_STATUS: {
|
|
88
|
+
readonly thinking: string;
|
|
89
|
+
readonly compilingResponse: string;
|
|
90
|
+
readonly lookingUpData: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ─── Visualizations ──────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export const AiVisualization: React.FC<{ data: unknown }>;
|
|
96
|
+
export const BarChart: React.FC<{ data: unknown }>;
|
|
97
|
+
export const LineChart: React.FC<{ data: unknown }>;
|
|
98
|
+
export const DotChart: React.FC<{ data: unknown }>;
|
|
99
|
+
export const ScatterPlot: React.FC<{ data: unknown }>;
|
|
100
|
+
export const HistogramChart: React.FC<{ data: unknown }>;
|
|
101
|
+
export const StackedBarChart: React.FC<{ data: unknown }>;
|
|
102
|
+
export const GroupedBarChart: React.FC<{ data: unknown }>;
|
|
103
|
+
export const DataTable: React.FC<{ data: unknown }>;
|
|
104
|
+
export const StatCards: React.FC<{ data: unknown }>;
|
|
105
|
+
export const Timeline: React.FC<{ data: unknown }>;
|
|
106
|
+
|
|
107
|
+
// ─── Chart schema utilities ───────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
export const CHART_BLOCK_LANGUAGE: string;
|
|
110
|
+
export const LEGACY_CHART_BLOCK_LANGUAGE: string;
|
|
111
|
+
export function isChartMarkdownLanguage(lang: string): boolean;
|
|
112
|
+
|
|
113
|
+
export const DOT_CHART_TYPE: string;
|
|
114
|
+
export const TABLE_TYPE: string;
|
|
115
|
+
export const BAR_CHART_TYPE: string;
|
|
116
|
+
export const STAT_CARDS_TYPE: string;
|
|
117
|
+
export const TIMELINE_TYPE: string;
|
|
118
|
+
export const LINE_CHART_TYPE: string;
|
|
119
|
+
export const STACKED_BAR_CHART_TYPE: string;
|
|
120
|
+
export const GROUPED_BAR_CHART_TYPE: string;
|
|
121
|
+
export const HISTOGRAM_TYPE: string;
|
|
122
|
+
export const SCATTER_PLOT_TYPE: string;
|
|
123
|
+
|
|
124
|
+
export function validateBarChartPayload(data: unknown): boolean;
|
|
125
|
+
export function validateLineChartPayload(data: unknown): boolean;
|
|
126
|
+
export function validateStackedBarChartPayload(data: unknown): boolean;
|
|
127
|
+
export function validateGroupedBarChartPayload(data: unknown): boolean;
|
|
128
|
+
export function validateHistogramPayload(data: unknown): boolean;
|
|
129
|
+
export function validateScatterPlotPayload(data: unknown): boolean;
|
|
130
|
+
export function validateStatCardsPayload(data: unknown): boolean;
|
|
131
|
+
export function validateTimelinePayload(data: unknown): boolean;
|
|
132
|
+
export function validateTablePayload(data: unknown): boolean;
|
|
133
|
+
|
|
134
|
+
export function parseChartBlock(block: string): unknown;
|
|
135
|
+
export function resolveChartBlock(block: string): unknown;
|
|
136
|
+
|
|
137
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
export const ORCHID_AI_VISUALIZATION_INSTRUCTIONS: string;
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Chat UI
|
|
2
2
|
export { default as ChatWindow } from './components/ChatWindow';
|
|
3
|
+
export { useOrchidAiChat, ORCHID_AI_DEFAULT_STATUS } from './hooks/useOrchidAiChat';
|
|
3
4
|
export { default as ChatInput } from './components/ChatInput';
|
|
4
5
|
export { default as Message } from './components/Message';
|
|
5
6
|
|