@devicai/ui 0.1.0 → 0.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/dist/cjs/api/client.js +19 -2
- package/dist/cjs/api/client.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js +132 -22
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatInput.js +12 -12
- package/dist/cjs/components/ChatDrawer/ChatInput.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatMessages.js +134 -29
- package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ConversationSelector.js +93 -0
- package/dist/cjs/components/ChatDrawer/ConversationSelector.js.map +1 -0
- package/dist/cjs/components/ChatDrawer/ErrorBoundary.js +25 -0
- package/dist/cjs/components/ChatDrawer/ErrorBoundary.js.map +1 -0
- package/dist/cjs/hooks/useDevicChat.js +54 -27
- package/dist/cjs/hooks/useDevicChat.js.map +1 -1
- package/dist/cjs/hooks/useModelInterface.js +6 -6
- package/dist/cjs/hooks/usePolling.js +64 -30
- package/dist/cjs/hooks/usePolling.js.map +1 -1
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/provider/DevicContext.js +4 -4
- package/dist/cjs/provider/DevicProvider.js +2 -2
- package/dist/cjs/styles.css +1 -1
- package/dist/esm/api/client.d.ts +10 -2
- package/dist/esm/api/client.js +19 -2
- package/dist/esm/api/client.js.map +1 -1
- package/dist/esm/api/types.d.ts +15 -0
- package/dist/esm/components/ChatDrawer/ChatDrawer.d.ts +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.js +123 -13
- package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +119 -5
- package/dist/esm/components/ChatDrawer/ChatInput.d.ts +2 -1
- package/dist/esm/components/ChatDrawer/ChatInput.js +2 -2
- package/dist/esm/components/ChatDrawer/ChatInput.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatMessages.d.ts +1 -4
- package/dist/esm/components/ChatDrawer/ChatMessages.js +133 -28
- package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ConversationSelector.d.ts +2 -0
- package/dist/esm/components/ChatDrawer/ConversationSelector.js +91 -0
- package/dist/esm/components/ChatDrawer/ConversationSelector.js.map +1 -0
- package/dist/esm/components/ChatDrawer/ErrorBoundary.d.ts +16 -0
- package/dist/esm/components/ChatDrawer/ErrorBoundary.js +23 -0
- package/dist/esm/components/ChatDrawer/ErrorBoundary.js.map +1 -0
- package/dist/esm/components/ChatDrawer/index.d.ts +2 -1
- package/dist/esm/hooks/useDevicChat.js +37 -10
- package/dist/esm/hooks/useDevicChat.js.map +1 -1
- package/dist/esm/hooks/usePolling.js +46 -12
- package/dist/esm/hooks/usePolling.js.map +1 -1
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/styles.css +1 -1
- package/package.json +10 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
-
import { useRef, useEffect } from 'react';
|
|
3
|
-
import
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import React, { useRef, useEffect, useState } from 'react';
|
|
3
|
+
import Markdown from 'markdown-to-jsx';
|
|
4
4
|
|
|
5
|
+
console.log('[devic-ui] ChatMessages: DEV-BUILD-005');
|
|
5
6
|
/**
|
|
6
7
|
* Format timestamp to readable time
|
|
7
8
|
*/
|
|
@@ -11,41 +12,145 @@ function formatTime(timestamp) {
|
|
|
11
12
|
minute: '2-digit',
|
|
12
13
|
});
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Groups consecutive tool-call assistant messages (no text content)
|
|
17
|
+
* into { toolMessages, isActive } groups, interleaved with regular messages.
|
|
18
|
+
*/
|
|
19
|
+
function groupMessages(messages, isLoading) {
|
|
20
|
+
const result = [];
|
|
21
|
+
let currentToolGroup = [];
|
|
22
|
+
const flushToolGroup = (isActive) => {
|
|
23
|
+
if (currentToolGroup.length > 0) {
|
|
24
|
+
result.push({ type: 'toolGroup', toolMessages: [...currentToolGroup], isActive });
|
|
25
|
+
currentToolGroup = [];
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
for (let i = 0; i < messages.length; i++) {
|
|
29
|
+
const msg = messages[i];
|
|
30
|
+
// Skip developer and tool response messages
|
|
31
|
+
if (msg.role === 'developer' || msg.role === 'tool') {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const hasToolCalls = msg.tool_calls && msg.tool_calls.length > 0;
|
|
35
|
+
const hasText = !!msg.content?.message;
|
|
36
|
+
const hasFiles = msg.content?.files && msg.content.files.length > 0;
|
|
37
|
+
if (hasToolCalls) {
|
|
38
|
+
// If message has both text and tool_calls, show text first
|
|
39
|
+
if (hasText || hasFiles) {
|
|
40
|
+
// Flush any prior tool group before inserting the text message
|
|
41
|
+
const remainingMeaningful = messages.slice(i + 1).some((m) => (m.role === 'assistant' && m.content?.message) || m.role === 'user');
|
|
42
|
+
flushToolGroup(isLoading && !remainingMeaningful);
|
|
43
|
+
result.push({ type: 'message', message: msg });
|
|
44
|
+
}
|
|
45
|
+
// Always accumulate the tool call
|
|
46
|
+
currentToolGroup.push(msg);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Regular message → flush any accumulated tool group first
|
|
50
|
+
const remainingMeaningful = messages.slice(i).some((m) => (m.role === 'assistant' && m.content?.message) || m.role === 'user');
|
|
51
|
+
flushToolGroup(isLoading && !remainingMeaningful);
|
|
52
|
+
if (hasText || hasFiles) {
|
|
53
|
+
result.push({ type: 'message', message: msg });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Flush remaining tool group (is active if still loading)
|
|
58
|
+
flushToolGroup(isLoading);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Collapsible tool actions group
|
|
63
|
+
*/
|
|
64
|
+
function ToolGroup({ toolMessages, isActive, allMessages, toolRenderers, toolIcons, }) {
|
|
65
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
66
|
+
const shouldCollapse = toolMessages.length > 3 && !isActive;
|
|
67
|
+
// Auto-collapse when transitioning from active to completed
|
|
68
|
+
const wasActiveRef = useRef(isActive);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (wasActiveRef.current && !isActive && toolMessages.length > 3) {
|
|
71
|
+
setIsCollapsed(true);
|
|
72
|
+
}
|
|
73
|
+
wasActiveRef.current = isActive;
|
|
74
|
+
}, [isActive, toolMessages.length]);
|
|
75
|
+
const lastIndex = toolMessages.length - 1;
|
|
76
|
+
const renderToolItem = (msg, opts) => {
|
|
77
|
+
const toolCall = msg.tool_calls?.[0];
|
|
78
|
+
const toolName = toolCall?.function?.name;
|
|
79
|
+
const summaryText = msg.summary || toolName || (opts.active ? 'Processing...' : 'Completed');
|
|
80
|
+
// Custom renderer for completed tools
|
|
81
|
+
if (!opts.active && toolName && toolRenderers?.[toolName] && allMessages) {
|
|
82
|
+
const toolResponse = allMessages.find((m) => m.role === 'tool' && m.tool_call_id === toolCall.id);
|
|
83
|
+
let input = {};
|
|
84
|
+
try {
|
|
85
|
+
input = JSON.parse(toolCall.function.arguments);
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
const output = toolResponse?.content?.data ?? toolResponse?.content?.message;
|
|
89
|
+
return toolRenderers[toolName](input, output);
|
|
90
|
+
}
|
|
91
|
+
const icon = opts.showSpinner
|
|
92
|
+
? jsx(SpinnerIcon, {})
|
|
93
|
+
: (toolName && toolIcons?.[toolName]) || jsx(ToolDoneIcon, {});
|
|
94
|
+
return (jsxs(Fragment, { children: [jsx("span", { className: "devic-tool-activity-icon", children: icon }), jsx("span", { className: `devic-tool-activity-text ${opts.active ? 'devic-glow-text' : ''}`, children: summaryText })] }));
|
|
95
|
+
};
|
|
96
|
+
// If active, show all items; last one gets the glow treatment
|
|
97
|
+
if (isActive) {
|
|
98
|
+
return (jsx("div", { className: "devic-tool-group", children: toolMessages.map((msg, idx) => {
|
|
99
|
+
const isLast = idx === lastIndex;
|
|
100
|
+
return (jsx("div", { className: `devic-tool-activity ${isLast ? 'devic-tool-activity--active' : ''}`, children: renderToolItem(msg, { active: isLast, showSpinner: isLast }) }, msg.uid));
|
|
101
|
+
}) }));
|
|
102
|
+
}
|
|
103
|
+
// Completed: collapse if > 3 actions
|
|
104
|
+
if (shouldCollapse && isCollapsed) {
|
|
105
|
+
return (jsx("div", { className: "devic-tool-group", children: jsxs("button", { className: "devic-tool-collapse-btn", onClick: () => setIsCollapsed(false), type: "button", children: [jsx(ToolDoneIcon, {}), jsxs("span", { children: [toolMessages.length, " actions"] }), jsx(ChevronDownIcon, {})] }) }));
|
|
106
|
+
}
|
|
107
|
+
return (jsxs("div", { className: "devic-tool-group", children: [shouldCollapse && (jsxs("button", { className: "devic-tool-collapse-btn", onClick: () => setIsCollapsed(true), type: "button", children: [jsxs("span", { children: [toolMessages.length, " actions"] }), jsx(ChevronUpIcon, {})] })), jsx("div", { className: "devic-tool-group-items", "data-expanded": "true", children: toolMessages.map((msg) => (jsx("div", { className: "devic-tool-activity", children: renderToolItem(msg, {}) }, msg.uid))) })] }));
|
|
108
|
+
}
|
|
14
109
|
/**
|
|
15
110
|
* Messages list component
|
|
16
111
|
*/
|
|
17
|
-
|
|
112
|
+
const markdownOverrides = {
|
|
113
|
+
table: {
|
|
114
|
+
component: ({ children, ...props }) => React.createElement('div', { className: 'markdown-table' }, React.createElement('table', props, children)),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
function ChatMessages({ messages, allMessages, isLoading, welcomeMessage, suggestedMessages, onSuggestedClick, toolRenderers, toolIcons, loadingIndicator, }) {
|
|
18
118
|
const containerRef = useRef(null);
|
|
19
|
-
const
|
|
119
|
+
const prevLengthRef = useRef(messages.length);
|
|
20
120
|
// Auto-scroll to bottom when new messages arrive
|
|
21
121
|
useEffect(() => {
|
|
22
|
-
if (
|
|
122
|
+
if (containerRef.current) {
|
|
23
123
|
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
|
24
124
|
}
|
|
25
|
-
|
|
26
|
-
}, [messages.length]);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
status: 'completed',
|
|
35
|
-
timestamp: m.timestamp,
|
|
36
|
-
})))
|
|
37
|
-
: [];
|
|
38
|
-
return (jsxs("div", { className: "devic-messages-container", ref: containerRef, children: [messages.length === 0 && (welcomeMessage || suggestedMessages?.length) && (jsxs("div", { className: "devic-welcome", children: [welcomeMessage && (jsx("p", { className: "devic-welcome-text", children: welcomeMessage })), suggestedMessages && suggestedMessages.length > 0 && (jsx("div", { className: "devic-suggested-messages", children: suggestedMessages.map((msg, idx) => (jsx("button", { className: "devic-suggested-btn", onClick: () => onSuggestedClick?.(msg), children: msg }, idx))) }))] })), messages.map((message) => {
|
|
39
|
-
// Skip tool messages in main display (shown in timeline)
|
|
40
|
-
if (message.role === 'tool' && showToolTimeline) {
|
|
41
|
-
return null;
|
|
125
|
+
prevLengthRef.current = messages.length;
|
|
126
|
+
}, [messages.length, isLoading]);
|
|
127
|
+
const grouped = groupMessages(messages, isLoading);
|
|
128
|
+
// Show loading dots only if there's no active tool group at the end
|
|
129
|
+
const lastGroup = grouped[grouped.length - 1];
|
|
130
|
+
const showLoadingDots = isLoading && !(lastGroup?.type === 'toolGroup' && lastGroup.isActive);
|
|
131
|
+
return (jsxs("div", { className: "devic-messages-container", ref: containerRef, children: [messages.length === 0 && !isLoading && (welcomeMessage || suggestedMessages?.length) && (jsxs("div", { className: "devic-welcome", children: [welcomeMessage && (jsx("p", { className: "devic-welcome-text", children: welcomeMessage })), suggestedMessages && suggestedMessages.length > 0 && (jsx("div", { className: "devic-suggested-messages", children: suggestedMessages.map((msg, idx) => (jsx("button", { className: "devic-suggested-btn", onClick: () => onSuggestedClick?.(msg), children: msg }, idx))) }))] })), grouped.map((item) => {
|
|
132
|
+
if (item.type === 'toolGroup') {
|
|
133
|
+
return (jsx(ToolGroup, { toolMessages: item.toolMessages, isActive: item.isActive, allMessages: allMessages, toolRenderers: toolRenderers, toolIcons: toolIcons }, `tg-${item.toolMessages[0].uid}`));
|
|
42
134
|
}
|
|
43
|
-
|
|
44
|
-
|
|
135
|
+
const message = item.message;
|
|
136
|
+
const messageText = message.content?.message;
|
|
137
|
+
const hasFiles = message.content?.files && message.content.files.length > 0;
|
|
138
|
+
return (jsxs("div", { className: "devic-message", "data-role": message.role, children: [jsxs("div", { className: "devic-message-bubble", children: [messageText && message.role === 'assistant' ? (jsx(Markdown, { options: { overrides: markdownOverrides }, children: messageText })) : (messageText), hasFiles && (jsx("div", { className: "devic-message-files", children: message.content.files.map((file, fileIdx) => (jsxs("div", { className: "devic-message-file", children: [jsx(FileIcon, {}), jsx("span", { children: file.name })] }, fileIdx))) }))] }), jsx("span", { className: "devic-message-time", children: formatTime(message.timestamp) })] }, message.uid));
|
|
139
|
+
}), showLoadingDots && (loadingIndicator ? (jsx("div", { className: "devic-loading", children: loadingIndicator })) : (jsxs("div", { className: "devic-loading", children: [jsx("span", { className: "devic-loading-dot" }), jsx("span", { className: "devic-loading-dot" }), jsx("span", { className: "devic-loading-dot" })] })))] }));
|
|
140
|
+
}
|
|
141
|
+
/* ── Icons ── */
|
|
142
|
+
function SpinnerIcon() {
|
|
143
|
+
return (jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", className: "devic-spinner", children: jsx("path", { d: "M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" }) }));
|
|
144
|
+
}
|
|
145
|
+
function ToolDoneIcon() {
|
|
146
|
+
return (jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "20,6 9,17 4,12" }) }));
|
|
147
|
+
}
|
|
148
|
+
function ChevronDownIcon() {
|
|
149
|
+
return (jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "6,9 12,15 18,9" }) }));
|
|
150
|
+
}
|
|
151
|
+
function ChevronUpIcon() {
|
|
152
|
+
return (jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "6,15 12,9 18,15" }) }));
|
|
45
153
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Simple file icon
|
|
48
|
-
*/
|
|
49
154
|
function FileIcon() {
|
|
50
155
|
return (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), jsx("polyline", { points: "14,2 14,8 20,8" })] }));
|
|
51
156
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatMessages.js","sources":["../../../../src/components/ChatDrawer/ChatMessages.tsx"],"sourcesContent":["import React, { useEffect, useRef } from 'react';\nimport type { ChatMessagesProps } from './ChatDrawer.types';\nimport { ToolTimeline } from './ToolTimeline';\n\n/**\n * Format timestamp to readable time\n */\nfunction formatTime(timestamp: number): string {\n return new Date(timestamp).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\n/**\n * Messages list component\n */\nexport function ChatMessages({\n messages,\n isLoading,\n welcomeMessage,\n suggestedMessages,\n onSuggestedClick,\n showToolTimeline = true,\n}: ChatMessagesProps): JSX.Element {\n const containerRef = useRef<HTMLDivElement>(null);\n const lastMessageCountRef = useRef(messages.length);\n\n // Auto-scroll to bottom when new messages arrive\n useEffect(() => {\n if (messages.length > lastMessageCountRef.current && containerRef.current) {\n containerRef.current.scrollTop = containerRef.current.scrollHeight;\n }\n lastMessageCountRef.current = messages.length;\n }, [messages.length]);\n\n // Extract tool calls for timeline\n const toolCalls = showToolTimeline\n ? messages\n .filter((m) => m.tool_calls?.length)\n .flatMap((m) =>\n m.tool_calls!.map((tc) => ({\n id: tc.id,\n name: tc.function.name,\n status: 'completed' as const,\n timestamp: m.timestamp,\n }))\n )\n : [];\n\n return (\n <div className=\"devic-messages-container\" ref={containerRef}>\n {messages.length === 0 && (welcomeMessage || suggestedMessages?.length) && (\n <div className=\"devic-welcome\">\n {welcomeMessage && (\n <p className=\"devic-welcome-text\">{welcomeMessage}</p>\n )}\n {suggestedMessages && suggestedMessages.length > 0 && (\n <div className=\"devic-suggested-messages\">\n {suggestedMessages.map((msg, idx) => (\n <button\n key={idx}\n className=\"devic-suggested-btn\"\n onClick={() => onSuggestedClick?.(msg)}\n >\n {msg}\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n\n {messages.map((message) => {\n // Skip tool messages in main display (shown in timeline)\n if (message.role === 'tool' && showToolTimeline) {\n return null;\n }\n\n return (\n <div\n key={message.uid}\n className=\"devic-message\"\n data-role={message.role}\n >\n <div className=\"devic-message-bubble\">\n {message.content.message}\n {message.content.files && message.content.files.length > 0 && (\n <div className=\"devic-message-files\">\n {message.content.files.map((file, idx) => (\n <div key={idx} className=\"devic-message-file\">\n <FileIcon />\n <span>{file.name}</span>\n </div>\n ))}\n </div>\n )}\n </div>\n <span className=\"devic-message-time\">\n {formatTime(message.timestamp)}\n </span>\n </div>\n );\n })}\n\n {showToolTimeline && toolCalls.length > 0 && (\n <ToolTimeline toolCalls={toolCalls} />\n )}\n\n {isLoading && (\n <div className=\"devic-loading\">\n <span className=\"devic-loading-dot\"></span>\n <span className=\"devic-loading-dot\"></span>\n <span className=\"devic-loading-dot\"></span>\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Simple file icon\n */\nfunction FileIcon(): JSX.Element {\n return (\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" />\n <polyline points=\"14,2 14,8 20,8\" />\n </svg>\n );\n}\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatMessages.js","sources":["../../../../src/components/ChatDrawer/ChatMessages.tsx"],"sourcesContent":["import React, { useState, useEffect, useRef } from 'react';\nimport Markdown from 'markdown-to-jsx';\nimport type { ChatMessagesProps } from './ChatDrawer.types';\nimport type { ChatMessage } from '../../api/types';\n\nconsole.log('[devic-ui] ChatMessages: DEV-BUILD-005');\n\n/**\n * Format timestamp to readable time\n */\nfunction formatTime(timestamp: number): string {\n return new Date(timestamp).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\n/**\n * Groups consecutive tool-call assistant messages (no text content)\n * into { toolMessages, isActive } groups, interleaved with regular messages.\n */\nfunction groupMessages(\n messages: ChatMessage[],\n isLoading: boolean\n): Array<\n | { type: 'message'; message: ChatMessage }\n | { type: 'toolGroup'; toolMessages: ChatMessage[]; isActive: boolean }\n> {\n const result: Array<\n | { type: 'message'; message: ChatMessage }\n | { type: 'toolGroup'; toolMessages: ChatMessage[]; isActive: boolean }\n > = [];\n\n let currentToolGroup: ChatMessage[] = [];\n\n const flushToolGroup = (isActive: boolean) => {\n if (currentToolGroup.length > 0) {\n result.push({ type: 'toolGroup', toolMessages: [...currentToolGroup], isActive });\n currentToolGroup = [];\n }\n };\n\n for (let i = 0; i < messages.length; i++) {\n const msg = messages[i];\n\n // Skip developer and tool response messages\n if (msg.role === 'developer' || msg.role === 'tool') {\n continue;\n }\n\n const hasToolCalls = msg.tool_calls && msg.tool_calls.length > 0;\n const hasText = !!msg.content?.message;\n const hasFiles = msg.content?.files && msg.content.files.length > 0;\n\n if (hasToolCalls) {\n // If message has both text and tool_calls, show text first\n if (hasText || hasFiles) {\n // Flush any prior tool group before inserting the text message\n const remainingMeaningful = messages.slice(i + 1).some(\n (m) => (m.role === 'assistant' && m.content?.message) || m.role === 'user'\n );\n flushToolGroup(isLoading && !remainingMeaningful);\n result.push({ type: 'message', message: msg });\n }\n // Always accumulate the tool call\n currentToolGroup.push(msg);\n } else {\n // Regular message → flush any accumulated tool group first\n const remainingMeaningful = messages.slice(i).some(\n (m) => (m.role === 'assistant' && m.content?.message) || m.role === 'user'\n );\n flushToolGroup(isLoading && !remainingMeaningful);\n\n if (hasText || hasFiles) {\n result.push({ type: 'message', message: msg });\n }\n }\n }\n\n // Flush remaining tool group (is active if still loading)\n flushToolGroup(isLoading);\n\n return result;\n}\n\n/**\n * Collapsible tool actions group\n */\nfunction ToolGroup({\n toolMessages,\n isActive,\n allMessages,\n toolRenderers,\n toolIcons,\n}: {\n toolMessages: ChatMessage[];\n isActive: boolean;\n allMessages?: ChatMessage[];\n toolRenderers?: Record<string, (input: any, output: any) => React.ReactNode>;\n toolIcons?: Record<string, React.ReactNode>;\n}): JSX.Element {\n const [isCollapsed, setIsCollapsed] = useState(false);\n const shouldCollapse = toolMessages.length > 3 && !isActive;\n\n // Auto-collapse when transitioning from active to completed\n const wasActiveRef = useRef(isActive);\n useEffect(() => {\n if (wasActiveRef.current && !isActive && toolMessages.length > 3) {\n setIsCollapsed(true);\n }\n wasActiveRef.current = isActive;\n }, [isActive, toolMessages.length]);\n\n const lastIndex = toolMessages.length - 1;\n\n const renderToolItem = (msg: ChatMessage, opts: { active?: boolean; showSpinner?: boolean }) => {\n const toolCall = msg.tool_calls?.[0];\n const toolName = toolCall?.function?.name;\n const summaryText = msg.summary || toolName || (opts.active ? 'Processing...' : 'Completed');\n\n // Custom renderer for completed tools\n if (!opts.active && toolName && toolRenderers?.[toolName] && allMessages) {\n const toolResponse = allMessages.find(\n (m) => m.role === 'tool' && m.tool_call_id === toolCall!.id\n );\n let input: any = {};\n try { input = JSON.parse(toolCall!.function.arguments); } catch {}\n const output = toolResponse?.content?.data ?? toolResponse?.content?.message;\n return toolRenderers[toolName](input, output);\n }\n\n const icon = opts.showSpinner\n ? <SpinnerIcon />\n : (toolName && toolIcons?.[toolName]) || <ToolDoneIcon />;\n\n return (\n <>\n <span className=\"devic-tool-activity-icon\">{icon}</span>\n <span className={`devic-tool-activity-text ${opts.active ? 'devic-glow-text' : ''}`}>\n {summaryText}\n </span>\n </>\n );\n };\n\n // If active, show all items; last one gets the glow treatment\n if (isActive) {\n return (\n <div className=\"devic-tool-group\">\n {toolMessages.map((msg, idx) => {\n const isLast = idx === lastIndex;\n return (\n <div\n key={msg.uid}\n className={`devic-tool-activity ${isLast ? 'devic-tool-activity--active' : ''}`}\n >\n {renderToolItem(msg, { active: isLast, showSpinner: isLast })}\n </div>\n );\n })}\n </div>\n );\n }\n\n // Completed: collapse if > 3 actions\n if (shouldCollapse && isCollapsed) {\n return (\n <div className=\"devic-tool-group\">\n <button\n className=\"devic-tool-collapse-btn\"\n onClick={() => setIsCollapsed(false)}\n type=\"button\"\n >\n <ToolDoneIcon />\n <span>{toolMessages.length} actions</span>\n <ChevronDownIcon />\n </button>\n </div>\n );\n }\n\n return (\n <div className=\"devic-tool-group\">\n {shouldCollapse && (\n <button\n className=\"devic-tool-collapse-btn\"\n onClick={() => setIsCollapsed(true)}\n type=\"button\"\n >\n <span>{toolMessages.length} actions</span>\n <ChevronUpIcon />\n </button>\n )}\n <div className=\"devic-tool-group-items\" data-expanded=\"true\">\n {toolMessages.map((msg) => (\n <div key={msg.uid} className=\"devic-tool-activity\">\n {renderToolItem(msg, {})}\n </div>\n ))}\n </div>\n </div>\n );\n}\n\n/**\n * Messages list component\n */\nconst markdownOverrides = {\n table: {\n component: ({ children, ...props }: any) =>\n React.createElement('div', { className: 'markdown-table' },\n React.createElement('table', props, children)\n ),\n },\n};\n\nexport function ChatMessages({\n messages,\n allMessages,\n isLoading,\n welcomeMessage,\n suggestedMessages,\n onSuggestedClick,\n toolRenderers,\n toolIcons,\n loadingIndicator,\n}: ChatMessagesProps): JSX.Element {\n const containerRef = useRef<HTMLDivElement>(null);\n const prevLengthRef = useRef(messages.length);\n\n // Auto-scroll to bottom when new messages arrive\n useEffect(() => {\n if (containerRef.current) {\n containerRef.current.scrollTop = containerRef.current.scrollHeight;\n }\n prevLengthRef.current = messages.length;\n }, [messages.length, isLoading]);\n\n const grouped = groupMessages(messages, isLoading);\n\n // Show loading dots only if there's no active tool group at the end\n const lastGroup = grouped[grouped.length - 1];\n const showLoadingDots = isLoading && !(lastGroup?.type === 'toolGroup' && lastGroup.isActive);\n\n return (\n <div className=\"devic-messages-container\" ref={containerRef}>\n {messages.length === 0 && !isLoading && (welcomeMessage || suggestedMessages?.length) && (\n <div className=\"devic-welcome\">\n {welcomeMessage && (\n <p className=\"devic-welcome-text\">{welcomeMessage}</p>\n )}\n {suggestedMessages && suggestedMessages.length > 0 && (\n <div className=\"devic-suggested-messages\">\n {suggestedMessages.map((msg, idx) => (\n <button\n key={idx}\n className=\"devic-suggested-btn\"\n onClick={() => onSuggestedClick?.(msg)}\n >\n {msg}\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n\n {grouped.map((item) => {\n if (item.type === 'toolGroup') {\n return (\n <ToolGroup\n key={`tg-${item.toolMessages[0].uid}`}\n toolMessages={item.toolMessages}\n isActive={item.isActive}\n allMessages={allMessages}\n toolRenderers={toolRenderers}\n toolIcons={toolIcons}\n />\n );\n }\n\n const message = item.message;\n const messageText = message.content?.message;\n const hasFiles = message.content?.files && message.content.files.length > 0;\n\n return (\n <div\n key={message.uid}\n className=\"devic-message\"\n data-role={message.role}\n >\n <div className=\"devic-message-bubble\">\n {messageText && message.role === 'assistant' ? (\n <Markdown options={{ overrides: markdownOverrides }}>{messageText}</Markdown>\n ) : (\n messageText\n )}\n {hasFiles && (\n <div className=\"devic-message-files\">\n {message.content!.files!.map((file, fileIdx) => (\n <div key={fileIdx} className=\"devic-message-file\">\n <FileIcon />\n <span>{file.name}</span>\n </div>\n ))}\n </div>\n )}\n </div>\n <span className=\"devic-message-time\">\n {formatTime(message.timestamp)}\n </span>\n </div>\n );\n })}\n\n {showLoadingDots && (\n loadingIndicator ? (\n <div className=\"devic-loading\">{loadingIndicator}</div>\n ) : (\n <div className=\"devic-loading\">\n <span className=\"devic-loading-dot\"></span>\n <span className=\"devic-loading-dot\"></span>\n <span className=\"devic-loading-dot\"></span>\n </div>\n )\n )}\n </div>\n );\n}\n\n/* ── Icons ── */\n\nfunction SpinnerIcon(): JSX.Element {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n className=\"devic-spinner\"\n >\n <path d=\"M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83\" />\n </svg>\n );\n}\n\nfunction ToolDoneIcon(): JSX.Element {\n return (\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <polyline points=\"20,6 9,17 4,12\" />\n </svg>\n );\n}\n\nfunction ChevronDownIcon(): JSX.Element {\n return (\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n );\n}\n\nfunction ChevronUpIcon(): JSX.Element {\n return (\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <polyline points=\"6,15 12,9 18,15\" />\n </svg>\n );\n}\n\nfunction FileIcon(): JSX.Element {\n return (\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" />\n <polyline points=\"14,2 14,8 20,8\" />\n </svg>\n );\n}\n"],"names":["_jsx","_jsxs","_Fragment"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { ConversationSelectorProps } from './ChatDrawer.types';
|
|
2
|
+
export declare function ConversationSelector({ assistantId, currentChatUid, onSelect, onNewChat, apiKey: propsApiKey, baseUrl: propsBaseUrl, tenantId: propsTenantId, }: ConversationSelectorProps): JSX.Element;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
|
+
import { useOptionalDevicContext } from '../../provider/DevicContext.js';
|
|
4
|
+
import { DevicApiClient } from '../../api/client.js';
|
|
5
|
+
|
|
6
|
+
function ConversationSelector({ assistantId, currentChatUid, onSelect, onNewChat, apiKey: propsApiKey, baseUrl: propsBaseUrl, tenantId: propsTenantId, }) {
|
|
7
|
+
const context = useOptionalDevicContext();
|
|
8
|
+
const apiKey = propsApiKey || context?.apiKey;
|
|
9
|
+
const baseUrl = propsBaseUrl || context?.baseUrl || 'https://api.devic.ai';
|
|
10
|
+
const tenantId = propsTenantId || context?.tenantId;
|
|
11
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
12
|
+
const [conversations, setConversations] = useState([]);
|
|
13
|
+
const [search, setSearch] = useState('');
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
const dropdownRef = useRef(null);
|
|
16
|
+
const clientRef = useRef(null);
|
|
17
|
+
if (!clientRef.current && apiKey) {
|
|
18
|
+
clientRef.current = new DevicApiClient({ apiKey, baseUrl });
|
|
19
|
+
}
|
|
20
|
+
// Update client config when props/context change
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (clientRef.current && apiKey) {
|
|
23
|
+
clientRef.current.setConfig({ apiKey, baseUrl });
|
|
24
|
+
}
|
|
25
|
+
else if (!clientRef.current && apiKey) {
|
|
26
|
+
clientRef.current = new DevicApiClient({ apiKey, baseUrl });
|
|
27
|
+
}
|
|
28
|
+
}, [apiKey, baseUrl]);
|
|
29
|
+
const fetchConversations = useCallback(async () => {
|
|
30
|
+
if (!clientRef.current)
|
|
31
|
+
return;
|
|
32
|
+
setLoading(true);
|
|
33
|
+
try {
|
|
34
|
+
const list = await clientRef.current.listConversations(assistantId, { tenantId });
|
|
35
|
+
setConversations(list);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// silently fail
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
}, [assistantId, tenantId]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (isOpen) {
|
|
46
|
+
fetchConversations();
|
|
47
|
+
}
|
|
48
|
+
}, [isOpen, fetchConversations]);
|
|
49
|
+
// Close on outside click
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!isOpen)
|
|
52
|
+
return;
|
|
53
|
+
const handler = (e) => {
|
|
54
|
+
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
|
55
|
+
setIsOpen(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
document.addEventListener('mousedown', handler);
|
|
59
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
60
|
+
}, [isOpen]);
|
|
61
|
+
const currentConv = conversations.find((c) => c.chatUID === currentChatUid);
|
|
62
|
+
const currentName = currentConv
|
|
63
|
+
? currentConv.name || formatDate(currentConv.creationTimestampMs)
|
|
64
|
+
: 'New chat';
|
|
65
|
+
const filtered = conversations.filter((c) => !search || (c.name || '').toLowerCase().includes(search.toLowerCase()));
|
|
66
|
+
return (jsxs("div", { className: "devic-conversation-selector", ref: dropdownRef, children: [jsxs("button", { className: "devic-conversation-selector-trigger", onClick: () => setIsOpen(!isOpen), type: "button", children: [jsx("span", { className: "devic-conversation-selector-label", children: currentName }), jsx(ChevronIcon, { open: isOpen })] }), isOpen && (jsxs("div", { className: "devic-conversation-dropdown", children: [jsx("div", { className: "devic-conversation-search-wrapper", children: jsx("input", { className: "devic-conversation-search", type: "text", placeholder: "Search conversations...", value: search, onChange: (e) => setSearch(e.target.value), autoFocus: true }) }), jsxs("div", { className: "devic-conversation-list", children: [loading && (jsx("div", { className: "devic-conversation-loading", children: "Loading..." })), !loading && filtered.length === 0 && (jsx("div", { className: "devic-conversation-empty", children: "No conversations" })), !loading &&
|
|
67
|
+
filtered.map((conv) => (jsxs("button", { className: "devic-conversation-item", "data-active": conv.chatUID === currentChatUid, type: "button", onClick: () => {
|
|
68
|
+
onSelect(conv.chatUID);
|
|
69
|
+
setIsOpen(false);
|
|
70
|
+
}, children: [conv.chatUID === currentChatUid && (jsx("span", { className: "devic-conversation-item-check", children: jsx(CheckIcon, {}) })), jsx("span", { className: "devic-conversation-item-name", children: conv.name || formatDate(conv.creationTimestampMs) }), jsx("span", { className: "devic-conversation-item-date", children: formatDate(conv.lastEditTimestampMs || conv.creationTimestampMs) })] }, conv.chatUID)))] }), jsx("button", { className: "devic-conversation-new", type: "button", onClick: () => {
|
|
71
|
+
onNewChat();
|
|
72
|
+
setIsOpen(false);
|
|
73
|
+
}, children: "+ Start a new chat" })] }))] }));
|
|
74
|
+
}
|
|
75
|
+
function ChevronIcon({ open }) {
|
|
76
|
+
return (jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { transform: open ? 'rotate(180deg)' : undefined, transition: '0.2s' }, children: jsx("polyline", { points: "6 9 12 15 18 9" }) }));
|
|
77
|
+
}
|
|
78
|
+
function CheckIcon() {
|
|
79
|
+
return (jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "20,6 9,17 4,12" }) }));
|
|
80
|
+
}
|
|
81
|
+
function formatDate(ms) {
|
|
82
|
+
const d = new Date(ms);
|
|
83
|
+
const now = new Date();
|
|
84
|
+
if (d.toDateString() === now.toDateString()) {
|
|
85
|
+
return d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
86
|
+
}
|
|
87
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { ConversationSelector };
|
|
91
|
+
//# sourceMappingURL=ConversationSelector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConversationSelector.js","sources":["../../../../src/components/ChatDrawer/ConversationSelector.tsx"],"sourcesContent":["import React, { useState, useEffect, useRef, useCallback } from 'react';\nimport { useOptionalDevicContext } from '../../provider';\nimport { DevicApiClient } from '../../api/client';\nimport type { ConversationSummary } from '../../api/types';\nimport type { ConversationSelectorProps } from './ChatDrawer.types';\n\nexport function ConversationSelector({\n assistantId,\n currentChatUid,\n onSelect,\n onNewChat,\n apiKey: propsApiKey,\n baseUrl: propsBaseUrl,\n tenantId: propsTenantId,\n}: ConversationSelectorProps): JSX.Element {\n const context = useOptionalDevicContext();\n const apiKey = propsApiKey || context?.apiKey;\n const baseUrl = propsBaseUrl || context?.baseUrl || 'https://api.devic.ai';\n const tenantId = propsTenantId || context?.tenantId;\n\n const [isOpen, setIsOpen] = useState(false);\n const [conversations, setConversations] = useState<ConversationSummary[]>([]);\n const [search, setSearch] = useState('');\n const [loading, setLoading] = useState(false);\n const dropdownRef = useRef<HTMLDivElement>(null);\n const clientRef = useRef<DevicApiClient | null>(null);\n\n if (!clientRef.current && apiKey) {\n clientRef.current = new DevicApiClient({ apiKey, baseUrl });\n }\n\n // Update client config when props/context change\n useEffect(() => {\n if (clientRef.current && apiKey) {\n clientRef.current.setConfig({ apiKey, baseUrl });\n } else if (!clientRef.current && apiKey) {\n clientRef.current = new DevicApiClient({ apiKey, baseUrl });\n }\n }, [apiKey, baseUrl]);\n\n const fetchConversations = useCallback(async () => {\n if (!clientRef.current) return;\n setLoading(true);\n try {\n const list = await clientRef.current.listConversations(assistantId, { tenantId });\n setConversations(list);\n } catch {\n // silently fail\n } finally {\n setLoading(false);\n }\n }, [assistantId, tenantId]);\n\n useEffect(() => {\n if (isOpen) {\n fetchConversations();\n }\n }, [isOpen, fetchConversations]);\n\n // Close on outside click\n useEffect(() => {\n if (!isOpen) return;\n const handler = (e: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {\n setIsOpen(false);\n }\n };\n document.addEventListener('mousedown', handler);\n return () => document.removeEventListener('mousedown', handler);\n }, [isOpen]);\n\n const currentConv = conversations.find((c) => c.chatUID === currentChatUid);\n const currentName = currentConv\n ? currentConv.name || formatDate(currentConv.creationTimestampMs)\n : 'New chat';\n\n const filtered = conversations.filter((c) =>\n !search || (c.name || '').toLowerCase().includes(search.toLowerCase())\n );\n\n return (\n <div className=\"devic-conversation-selector\" ref={dropdownRef}>\n <button\n className=\"devic-conversation-selector-trigger\"\n onClick={() => setIsOpen(!isOpen)}\n type=\"button\"\n >\n <span className=\"devic-conversation-selector-label\">{currentName}</span>\n <ChevronIcon open={isOpen} />\n </button>\n\n {isOpen && (\n <div className=\"devic-conversation-dropdown\">\n <div className=\"devic-conversation-search-wrapper\">\n <input\n className=\"devic-conversation-search\"\n type=\"text\"\n placeholder=\"Search conversations...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n autoFocus\n />\n </div>\n\n <div className=\"devic-conversation-list\">\n {loading && (\n <div className=\"devic-conversation-loading\">Loading...</div>\n )}\n {!loading && filtered.length === 0 && (\n <div className=\"devic-conversation-empty\">No conversations</div>\n )}\n {!loading &&\n filtered.map((conv) => (\n <button\n key={conv.chatUID}\n className=\"devic-conversation-item\"\n data-active={conv.chatUID === currentChatUid}\n type=\"button\"\n onClick={() => {\n onSelect(conv.chatUID);\n setIsOpen(false);\n }}\n >\n {conv.chatUID === currentChatUid && (\n <span className=\"devic-conversation-item-check\">\n <CheckIcon />\n </span>\n )}\n <span className=\"devic-conversation-item-name\">\n {conv.name || formatDate(conv.creationTimestampMs)}\n </span>\n <span className=\"devic-conversation-item-date\">\n {formatDate(conv.lastEditTimestampMs || conv.creationTimestampMs)}\n </span>\n </button>\n ))}\n </div>\n\n <button\n className=\"devic-conversation-new\"\n type=\"button\"\n onClick={() => {\n onNewChat();\n setIsOpen(false);\n }}\n >\n + Start a new chat\n </button>\n </div>\n )}\n </div>\n );\n}\n\nfunction ChevronIcon({ open }: { open: boolean }): JSX.Element {\n return (\n <svg\n width=\"12\"\n height=\"12\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n style={{ transform: open ? 'rotate(180deg)' : undefined, transition: '0.2s' }}\n >\n <polyline points=\"6 9 12 15 18 9\" />\n </svg>\n );\n}\n\nfunction CheckIcon(): JSX.Element {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <polyline points=\"20,6 9,17 4,12\" />\n </svg>\n );\n}\n\nfunction formatDate(ms: number): string {\n const d = new Date(ms);\n const now = new Date();\n if (d.toDateString() === now.toDateString()) {\n return d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });\n }\n return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });\n}\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
import type { ReactNode, ErrorInfo } from 'react';
|
|
3
|
+
interface Props {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
fallback?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
interface State {
|
|
8
|
+
hasError: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare class ChatDrawerErrorBoundary extends Component<Props, State> {
|
|
11
|
+
state: State;
|
|
12
|
+
static getDerivedStateFromError(): State;
|
|
13
|
+
componentDidCatch(error: Error, info: ErrorInfo): void;
|
|
14
|
+
render(): ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
|
|
3
|
+
class ChatDrawerErrorBoundary extends Component {
|
|
4
|
+
constructor() {
|
|
5
|
+
super(...arguments);
|
|
6
|
+
this.state = { hasError: false };
|
|
7
|
+
}
|
|
8
|
+
static getDerivedStateFromError() {
|
|
9
|
+
return { hasError: true };
|
|
10
|
+
}
|
|
11
|
+
componentDidCatch(error, info) {
|
|
12
|
+
console.error('[devic-ui] ChatDrawer error:', error, info.componentStack);
|
|
13
|
+
}
|
|
14
|
+
render() {
|
|
15
|
+
if (this.state.hasError) {
|
|
16
|
+
return this.props.fallback ?? null;
|
|
17
|
+
}
|
|
18
|
+
return this.props.children;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { ChatDrawerErrorBoundary };
|
|
23
|
+
//# sourceMappingURL=ErrorBoundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ErrorBoundary.js","sources":["../../../../src/components/ChatDrawer/ErrorBoundary.tsx"],"sourcesContent":["import React, { Component } from 'react';\nimport type { ReactNode, ErrorInfo } from 'react';\n\ninterface Props {\n children: ReactNode;\n fallback?: ReactNode;\n}\n\ninterface State {\n hasError: boolean;\n}\n\nexport class ChatDrawerErrorBoundary extends Component<Props, State> {\n state: State = { hasError: false };\n\n static getDerivedStateFromError(): State {\n return { hasError: true };\n }\n\n componentDidCatch(error: Error, info: ErrorInfo): void {\n console.error('[devic-ui] ChatDrawer error:', error, info.componentStack);\n }\n\n render(): ReactNode {\n if (this.state.hasError) {\n return this.props.fallback ?? null;\n }\n return this.props.children;\n }\n}\n"],"names":[],"mappings":";;AAYM,MAAO,uBAAwB,SAAQ,SAAuB,CAAA;AAApE,IAAA,WAAA,GAAA;;AACE,QAAA,IAAA,CAAA,KAAK,GAAU,EAAE,QAAQ,EAAE,KAAK,EAAE;IAgBpC;AAdE,IAAA,OAAO,wBAAwB,GAAA;AAC7B,QAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3B;IAEA,iBAAiB,CAAC,KAAY,EAAE,IAAe,EAAA;QAC7C,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,KAAK,EAAE,IAAI,CAAC,cAAc,CAAC;IAC3E;IAEA,MAAM,GAAA;AACJ,QAAA,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE;AACvB,YAAA,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,IAAI;QACpC;AACA,QAAA,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ;IAC5B;AACD;;;;"}
|
|
@@ -2,4 +2,5 @@ export { ChatDrawer } from './ChatDrawer';
|
|
|
2
2
|
export { ChatMessages } from './ChatMessages';
|
|
3
3
|
export { ChatInput } from './ChatInput';
|
|
4
4
|
export { ToolTimeline } from './ToolTimeline';
|
|
5
|
-
export
|
|
5
|
+
export { ConversationSelector } from './ConversationSelector';
|
|
6
|
+
export type { ChatDrawerProps, ChatDrawerOptions, ChatMessagesProps, ChatInputProps, ToolTimelineProps, AllowedFileTypes, ConversationSelectorProps, } from './ChatDrawer.types';
|
|
@@ -5,6 +5,7 @@ import { DevicApiClient } from '../api/client.js';
|
|
|
5
5
|
import { usePolling } from './usePolling.js';
|
|
6
6
|
import { useModelInterface } from './useModelInterface.js';
|
|
7
7
|
|
|
8
|
+
console.log('[devic-ui] Version: DEV-BUILD-001 (2026-01-26 18:20)');
|
|
8
9
|
/**
|
|
9
10
|
* Main hook for managing chat with a Devic assistant
|
|
10
11
|
*
|
|
@@ -69,18 +70,38 @@ function useDevicChat(options) {
|
|
|
69
70
|
tools: modelInterfaceTools,
|
|
70
71
|
onToolExecute: onToolCall,
|
|
71
72
|
});
|
|
72
|
-
// Polling hook
|
|
73
|
+
// Polling hook - uses callbacks for side effects, return value not needed
|
|
74
|
+
console.log('[useDevicChat] Render - shouldPoll:', shouldPoll, 'chatUid:', chatUid);
|
|
73
75
|
usePolling(shouldPoll ? chatUid : null, async () => {
|
|
76
|
+
console.log('[useDevicChat] fetchFn called, chatUid:', chatUid);
|
|
74
77
|
if (!clientRef.current || !chatUid) {
|
|
75
78
|
throw new Error('Cannot poll without client or chatUid');
|
|
76
79
|
}
|
|
77
|
-
|
|
80
|
+
const result = await clientRef.current.getRealtimeHistory(assistantId, chatUid);
|
|
81
|
+
console.log('[useDevicChat] getRealtimeHistory result:', result);
|
|
82
|
+
return result;
|
|
78
83
|
}, {
|
|
79
84
|
interval: pollingInterval,
|
|
80
85
|
enabled: shouldPoll,
|
|
81
86
|
stopStatuses: ['completed', 'error', 'waiting_for_tool_response'],
|
|
82
87
|
onUpdate: async (data) => {
|
|
83
|
-
|
|
88
|
+
console.log('[useDevicChat] onUpdate called, status:', data.status);
|
|
89
|
+
// Merge realtime data with optimistic messages
|
|
90
|
+
setMessages((prev) => {
|
|
91
|
+
const realtimeUIDs = new Set(data.chatHistory.map((m) => m.uid));
|
|
92
|
+
const realtimeUserMessages = new Set(data.chatHistory
|
|
93
|
+
.filter((m) => m.role === 'user')
|
|
94
|
+
.map((m) => m.content?.message));
|
|
95
|
+
// Keep optimistic messages not yet in realtime data
|
|
96
|
+
const optimistic = prev.filter((m) => {
|
|
97
|
+
if (realtimeUIDs.has(m.uid))
|
|
98
|
+
return false;
|
|
99
|
+
if (m.role === 'user' && realtimeUserMessages.has(m.content?.message))
|
|
100
|
+
return false;
|
|
101
|
+
return true;
|
|
102
|
+
});
|
|
103
|
+
return [...data.chatHistory, ...optimistic];
|
|
104
|
+
});
|
|
84
105
|
setStatus(data.status);
|
|
85
106
|
// Notify about new messages
|
|
86
107
|
const lastMessage = data.chatHistory[data.chatHistory.length - 1];
|
|
@@ -92,20 +113,22 @@ function useDevicChat(options) {
|
|
|
92
113
|
await handlePendingToolCalls(data);
|
|
93
114
|
}
|
|
94
115
|
},
|
|
95
|
-
onStop:
|
|
96
|
-
|
|
116
|
+
onStop: (data) => {
|
|
117
|
+
console.log('[useDevicChat] onStop called, status:', data?.status);
|
|
97
118
|
setShouldPoll(false);
|
|
98
119
|
if (data?.status === 'error') {
|
|
120
|
+
setIsLoading(false);
|
|
99
121
|
const err = new Error('Chat processing failed');
|
|
100
122
|
setError(err);
|
|
101
123
|
onErrorRef.current?.(err);
|
|
102
124
|
}
|
|
103
|
-
else if (data?.status === '
|
|
104
|
-
|
|
105
|
-
await handlePendingToolCalls(data);
|
|
125
|
+
else if (data?.status === 'completed') {
|
|
126
|
+
setIsLoading(false);
|
|
106
127
|
}
|
|
128
|
+
// Note: waiting_for_tool_response is handled in onUpdate to avoid double execution
|
|
107
129
|
},
|
|
108
130
|
onError: (err) => {
|
|
131
|
+
console.error('[useDevicChat] onError called:', err);
|
|
109
132
|
setError(err);
|
|
110
133
|
setIsLoading(false);
|
|
111
134
|
setShouldPoll(false);
|
|
@@ -180,13 +203,17 @@ function useDevicChat(options) {
|
|
|
180
203
|
...(toolSchemas.length > 0 && { tools: toolSchemas }),
|
|
181
204
|
};
|
|
182
205
|
// Send message in async mode
|
|
206
|
+
console.log('[useDevicChat] Sending message async...');
|
|
183
207
|
const response = await clientRef.current.sendMessageAsync(assistantId, dto);
|
|
208
|
+
console.log('[useDevicChat] sendMessageAsync response:', response);
|
|
184
209
|
// Update chat UID if this is a new chat
|
|
185
210
|
if (response.chatUid && response.chatUid !== chatUid) {
|
|
211
|
+
console.log('[useDevicChat] Setting chatUid:', response.chatUid);
|
|
186
212
|
setChatUid(response.chatUid);
|
|
187
213
|
onChatCreatedRef.current?.(response.chatUid);
|
|
188
214
|
}
|
|
189
215
|
// Start polling for results
|
|
216
|
+
console.log('[useDevicChat] Setting shouldPoll to true');
|
|
190
217
|
setShouldPoll(true);
|
|
191
218
|
}
|
|
192
219
|
catch (err) {
|
|
@@ -226,7 +253,7 @@ function useDevicChat(options) {
|
|
|
226
253
|
setIsLoading(true);
|
|
227
254
|
setError(null);
|
|
228
255
|
try {
|
|
229
|
-
const history = await clientRef.current.getChatHistory(assistantId, loadChatUid);
|
|
256
|
+
const history = await clientRef.current.getChatHistory(assistantId, loadChatUid, { tenantId: resolvedTenantId });
|
|
230
257
|
setMessages(history.chatContent);
|
|
231
258
|
setChatUid(loadChatUid);
|
|
232
259
|
setStatus('completed');
|
|
@@ -239,7 +266,7 @@ function useDevicChat(options) {
|
|
|
239
266
|
finally {
|
|
240
267
|
setIsLoading(false);
|
|
241
268
|
}
|
|
242
|
-
}, [assistantId]);
|
|
269
|
+
}, [assistantId, resolvedTenantId]);
|
|
243
270
|
return {
|
|
244
271
|
messages,
|
|
245
272
|
chatUid,
|