@devicai/ui 0.2.0 → 0.3.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 +31 -15
- package/dist/cjs/api/client.js.map +1 -1
- package/dist/cjs/components/AICommandBar/AICommandBar.js +314 -0
- package/dist/cjs/components/AICommandBar/AICommandBar.js.map +1 -0
- package/dist/cjs/components/AICommandBar/useAICommandBar.js +595 -0
- package/dist/cjs/components/AICommandBar/useAICommandBar.js.map +1 -0
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js +73 -5
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatMessages.js +5 -2
- package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/cjs/components/Feedback/FeedbackModal.js +87 -0
- package/dist/cjs/components/Feedback/FeedbackModal.js.map +1 -0
- package/dist/cjs/components/Feedback/MessageActions.js +74 -0
- package/dist/cjs/components/Feedback/MessageActions.js.map +1 -0
- package/dist/cjs/index.js +9 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/styles.css +1 -1
- package/dist/esm/api/client.d.ts +10 -2
- package/dist/esm/api/client.js +31 -15
- package/dist/esm/api/client.js.map +1 -1
- package/dist/esm/api/types.d.ts +25 -0
- package/dist/esm/components/AICommandBar/AICommandBar.d.ts +22 -0
- package/dist/esm/components/AICommandBar/AICommandBar.js +312 -0
- package/dist/esm/components/AICommandBar/AICommandBar.js.map +1 -0
- package/dist/esm/components/AICommandBar/AICommandBar.types.d.ts +321 -0
- package/dist/esm/components/AICommandBar/index.d.ts +3 -0
- package/dist/esm/components/AICommandBar/useAICommandBar.d.ts +57 -0
- package/dist/esm/components/AICommandBar/useAICommandBar.js +592 -0
- package/dist/esm/components/AICommandBar/useAICommandBar.js.map +1 -0
- package/dist/esm/components/AutocompleteInput/AutocompleteInput.d.ts +4 -0
- package/dist/esm/components/AutocompleteInput/AutocompleteInput.types.d.ts +50 -0
- package/dist/esm/components/AutocompleteInput/index.d.ts +4 -0
- package/dist/esm/components/AutocompleteInput/useAutocomplete.d.ts +29 -0
- package/dist/esm/components/ChatDrawer/ChatDrawer.d.ts +4 -2
- package/dist/esm/components/ChatDrawer/ChatDrawer.js +74 -6
- package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +36 -0
- package/dist/esm/components/ChatDrawer/ChatMessages.d.ts +2 -1
- package/dist/esm/components/ChatDrawer/ChatMessages.js +5 -2
- package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/esm/components/ChatDrawer/index.d.ts +1 -1
- package/dist/esm/components/Feedback/Feedback.types.d.ts +50 -0
- package/dist/esm/components/Feedback/FeedbackModal.d.ts +5 -0
- package/dist/esm/components/Feedback/FeedbackModal.js +85 -0
- package/dist/esm/components/Feedback/FeedbackModal.js.map +1 -0
- package/dist/esm/components/Feedback/MessageActions.d.ts +5 -0
- package/dist/esm/components/Feedback/MessageActions.js +72 -0
- package/dist/esm/components/Feedback/MessageActions.js.map +1 -0
- package/dist/esm/components/Feedback/index.d.ts +3 -0
- package/dist/esm/index.d.ts +6 -2
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
require('react/jsx-runtime');
|
|
5
|
+
var DevicContext = require('../../provider/DevicContext.js');
|
|
6
|
+
var client = require('../../api/client.js');
|
|
7
|
+
var usePolling = require('../../hooks/usePolling.js');
|
|
8
|
+
var useModelInterface = require('../../hooks/useModelInterface.js');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a shortcut string like "cmd+j" into its components
|
|
12
|
+
*/
|
|
13
|
+
function parseShortcut(shortcut) {
|
|
14
|
+
const parts = shortcut.toLowerCase().split('+');
|
|
15
|
+
const key = parts.pop() || '';
|
|
16
|
+
const modifiers = parts;
|
|
17
|
+
return { key, modifiers };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if a keyboard event matches a shortcut string
|
|
21
|
+
*/
|
|
22
|
+
function matchShortcut(event, shortcut) {
|
|
23
|
+
const { key, modifiers } = parseShortcut(shortcut);
|
|
24
|
+
const keyMatch = event.key.toLowerCase() === key;
|
|
25
|
+
const cmdMatch = modifiers.includes('cmd') === (event.metaKey || event.ctrlKey);
|
|
26
|
+
const shiftMatch = modifiers.includes('shift') === event.shiftKey;
|
|
27
|
+
const altMatch = modifiers.includes('alt') === event.altKey;
|
|
28
|
+
return keyMatch && cmdMatch && shiftMatch && altMatch;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Format a shortcut string for display
|
|
32
|
+
*/
|
|
33
|
+
function formatShortcut(shortcut) {
|
|
34
|
+
const isMac = typeof navigator !== 'undefined' && /Mac/.test(navigator.platform);
|
|
35
|
+
return shortcut
|
|
36
|
+
.replace(/cmd/gi, isMac ? '\u2318' : 'Ctrl')
|
|
37
|
+
.replace(/shift/gi, '\u21E7')
|
|
38
|
+
.replace(/alt/gi, isMac ? '\u2325' : 'Alt')
|
|
39
|
+
.replace(/\+/g, ' ')
|
|
40
|
+
.replace(/([a-z])/gi, (match) => match.toUpperCase());
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Format tool name to human-readable (fallback when no summary)
|
|
44
|
+
*/
|
|
45
|
+
function formatToolName(toolName) {
|
|
46
|
+
// Convert snake_case or camelCase to human-readable
|
|
47
|
+
return toolName
|
|
48
|
+
.replace(/_/g, ' ')
|
|
49
|
+
.replace(/([A-Z])/g, ' $1')
|
|
50
|
+
.trim()
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.replace(/^./, (c) => c.toUpperCase());
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Hook for managing AICommandBar state and behavior
|
|
56
|
+
*/
|
|
57
|
+
function useAICommandBar(options) {
|
|
58
|
+
const { assistantId, apiKey: propsApiKey, baseUrl: propsBaseUrl, tenantId, tenantMetadata, options: barOptions = {}, isVisible: controlledVisible, onVisibilityChange, onExecute = 'callback', chatDrawerRef, onResponse, modelInterfaceTools = [], onSubmit, onToolCall, onError, onOpen, onClose, } = options;
|
|
59
|
+
const { shortcut } = barOptions;
|
|
60
|
+
// Get context
|
|
61
|
+
const context = DevicContext.useOptionalDevicContext();
|
|
62
|
+
const apiKey = propsApiKey || context?.apiKey;
|
|
63
|
+
const baseUrl = propsBaseUrl || context?.baseUrl || 'https://api.devic.ai';
|
|
64
|
+
const resolvedTenantId = tenantId || context?.tenantId;
|
|
65
|
+
const resolvedTenantMetadata = { ...context?.tenantMetadata, ...tenantMetadata };
|
|
66
|
+
// Visibility state
|
|
67
|
+
const [internalVisible, setInternalVisible] = React.useState(false);
|
|
68
|
+
const isVisible = controlledVisible ?? internalVisible;
|
|
69
|
+
// Input state
|
|
70
|
+
const [inputValue, setInputValue] = React.useState('');
|
|
71
|
+
const inputRef = React.useRef(null);
|
|
72
|
+
// Processing state
|
|
73
|
+
const [isProcessing, setIsProcessing] = React.useState(false);
|
|
74
|
+
const [toolCalls, setToolCalls] = React.useState([]);
|
|
75
|
+
const [currentToolSummary, setCurrentToolSummary] = React.useState(null);
|
|
76
|
+
// Result state
|
|
77
|
+
const [result, setResult] = React.useState(null);
|
|
78
|
+
const [chatUid, setChatUid] = React.useState(null);
|
|
79
|
+
const [error, setError] = React.useState(null);
|
|
80
|
+
// Polling state
|
|
81
|
+
const [shouldPoll, setShouldPoll] = React.useState(false);
|
|
82
|
+
// History state
|
|
83
|
+
const enableHistory = barOptions.enableHistory !== false; // default true
|
|
84
|
+
const maxHistoryItems = barOptions.maxHistoryItems ?? 50;
|
|
85
|
+
const historyStorageKey = barOptions.historyStorageKey ?? 'devic-command-bar-history';
|
|
86
|
+
const showHistoryCommand = barOptions.showHistoryCommand !== false; // default true
|
|
87
|
+
const [history, setHistory] = React.useState(() => {
|
|
88
|
+
if (!enableHistory || typeof window === 'undefined')
|
|
89
|
+
return [];
|
|
90
|
+
try {
|
|
91
|
+
const stored = localStorage.getItem(historyStorageKey);
|
|
92
|
+
return stored ? JSON.parse(stored) : [];
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
const [historyIndex, setHistoryIndex] = React.useState(-1);
|
|
99
|
+
const [showingHistory, setShowingHistory] = React.useState(false);
|
|
100
|
+
const [tempInput, setTempInput] = React.useState(''); // Store current input when navigating history
|
|
101
|
+
// Commands state
|
|
102
|
+
const commands = barOptions.commands ?? [];
|
|
103
|
+
const [selectedCommandIndex, setSelectedCommandIndex] = React.useState(0);
|
|
104
|
+
// Built-in history command
|
|
105
|
+
const historyCommand = React.useMemo(() => ({
|
|
106
|
+
keyword: 'history',
|
|
107
|
+
description: 'Show command history',
|
|
108
|
+
message: '', // Special handling
|
|
109
|
+
}), []);
|
|
110
|
+
// All available commands (user commands + built-in)
|
|
111
|
+
const allCommands = React.useMemo(() => {
|
|
112
|
+
const userCommands = commands;
|
|
113
|
+
// Add history command if enabled and not overwritten
|
|
114
|
+
if (showHistoryCommand && !userCommands.some(c => c.keyword === 'history')) {
|
|
115
|
+
return [...userCommands, historyCommand];
|
|
116
|
+
}
|
|
117
|
+
return userCommands;
|
|
118
|
+
}, [commands, showHistoryCommand, historyCommand]);
|
|
119
|
+
// Detect if showing command suggestions
|
|
120
|
+
const isCommandMode = inputValue.startsWith('/');
|
|
121
|
+
const commandQuery = isCommandMode ? inputValue.slice(1).toLowerCase() : '';
|
|
122
|
+
const showingCommands = isCommandMode && !isProcessing && !result;
|
|
123
|
+
// Filter commands based on query
|
|
124
|
+
const filteredCommands = React.useMemo(() => {
|
|
125
|
+
if (!showingCommands)
|
|
126
|
+
return [];
|
|
127
|
+
if (commandQuery === '')
|
|
128
|
+
return allCommands;
|
|
129
|
+
return allCommands.filter(cmd => cmd.keyword.toLowerCase().includes(commandQuery) ||
|
|
130
|
+
cmd.description.toLowerCase().includes(commandQuery));
|
|
131
|
+
}, [showingCommands, commandQuery, allCommands]);
|
|
132
|
+
// Reset command selection when filtered list changes
|
|
133
|
+
React.useEffect(() => {
|
|
134
|
+
setSelectedCommandIndex(0);
|
|
135
|
+
}, [filteredCommands.length]);
|
|
136
|
+
// Callback refs
|
|
137
|
+
const onErrorRef = React.useRef(onError);
|
|
138
|
+
const onResponseRef = React.useRef(onResponse);
|
|
139
|
+
const onToolCallRef = React.useRef(onToolCall);
|
|
140
|
+
const onSubmitRef = React.useRef(onSubmit);
|
|
141
|
+
const onOpenRef = React.useRef(onOpen);
|
|
142
|
+
const onCloseRef = React.useRef(onClose);
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
onErrorRef.current = onError;
|
|
145
|
+
onResponseRef.current = onResponse;
|
|
146
|
+
onToolCallRef.current = onToolCall;
|
|
147
|
+
onSubmitRef.current = onSubmit;
|
|
148
|
+
onOpenRef.current = onOpen;
|
|
149
|
+
onCloseRef.current = onClose;
|
|
150
|
+
});
|
|
151
|
+
// API client
|
|
152
|
+
const clientRef = React.useRef(null);
|
|
153
|
+
if (!clientRef.current && apiKey) {
|
|
154
|
+
clientRef.current = new client.DevicApiClient({ apiKey, baseUrl });
|
|
155
|
+
}
|
|
156
|
+
React.useEffect(() => {
|
|
157
|
+
if (clientRef.current && apiKey) {
|
|
158
|
+
clientRef.current.setConfig({ apiKey, baseUrl });
|
|
159
|
+
}
|
|
160
|
+
}, [apiKey, baseUrl]);
|
|
161
|
+
// Model interface
|
|
162
|
+
const { toolSchemas, handleToolCalls: executeToolCalls, extractPendingToolCalls, } = useModelInterface.useModelInterface({
|
|
163
|
+
tools: modelInterfaceTools,
|
|
164
|
+
onToolExecute: onToolCall,
|
|
165
|
+
});
|
|
166
|
+
// Visibility controls
|
|
167
|
+
const open = React.useCallback(() => {
|
|
168
|
+
setInternalVisible(true);
|
|
169
|
+
onVisibilityChange?.(true);
|
|
170
|
+
onOpenRef.current?.();
|
|
171
|
+
// Focus input after visibility change
|
|
172
|
+
setTimeout(() => inputRef.current?.focus(), 50);
|
|
173
|
+
}, [onVisibilityChange]);
|
|
174
|
+
const close = React.useCallback(() => {
|
|
175
|
+
setInternalVisible(false);
|
|
176
|
+
onVisibilityChange?.(false);
|
|
177
|
+
onCloseRef.current?.();
|
|
178
|
+
}, [onVisibilityChange]);
|
|
179
|
+
const toggle = React.useCallback(() => {
|
|
180
|
+
if (isVisible) {
|
|
181
|
+
close();
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
open();
|
|
185
|
+
}
|
|
186
|
+
}, [isVisible, open, close]);
|
|
187
|
+
const focus = React.useCallback(() => {
|
|
188
|
+
inputRef.current?.focus();
|
|
189
|
+
}, []);
|
|
190
|
+
// Save history to localStorage
|
|
191
|
+
const saveHistory = React.useCallback((newHistory) => {
|
|
192
|
+
if (!enableHistory || typeof window === 'undefined')
|
|
193
|
+
return;
|
|
194
|
+
try {
|
|
195
|
+
localStorage.setItem(historyStorageKey, JSON.stringify(newHistory));
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Ignore localStorage errors
|
|
199
|
+
}
|
|
200
|
+
}, [enableHistory, historyStorageKey]);
|
|
201
|
+
// Add item to history
|
|
202
|
+
const addToHistory = React.useCallback((message) => {
|
|
203
|
+
if (!enableHistory || !message.trim() || message.startsWith('/'))
|
|
204
|
+
return;
|
|
205
|
+
setHistory(prev => {
|
|
206
|
+
// Don't add duplicates at the top
|
|
207
|
+
const filtered = prev.filter(item => item !== message);
|
|
208
|
+
const newHistory = [message, ...filtered].slice(0, maxHistoryItems);
|
|
209
|
+
saveHistory(newHistory);
|
|
210
|
+
return newHistory;
|
|
211
|
+
});
|
|
212
|
+
setHistoryIndex(-1);
|
|
213
|
+
}, [enableHistory, maxHistoryItems, saveHistory]);
|
|
214
|
+
// Clear history
|
|
215
|
+
const clearHistory = React.useCallback(() => {
|
|
216
|
+
setHistory([]);
|
|
217
|
+
setHistoryIndex(-1);
|
|
218
|
+
if (enableHistory && typeof window !== 'undefined') {
|
|
219
|
+
try {
|
|
220
|
+
localStorage.removeItem(historyStorageKey);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Ignore
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}, [enableHistory, historyStorageKey]);
|
|
227
|
+
// Navigate history
|
|
228
|
+
const navigateHistory = React.useCallback((direction) => {
|
|
229
|
+
if (!enableHistory || history.length === 0)
|
|
230
|
+
return;
|
|
231
|
+
if (direction === 'up') {
|
|
232
|
+
if (historyIndex === -1) {
|
|
233
|
+
// Save current input before navigating
|
|
234
|
+
setTempInput(inputValue);
|
|
235
|
+
setHistoryIndex(0);
|
|
236
|
+
setInputValue(history[0]);
|
|
237
|
+
}
|
|
238
|
+
else if (historyIndex < history.length - 1) {
|
|
239
|
+
const newIndex = historyIndex + 1;
|
|
240
|
+
setHistoryIndex(newIndex);
|
|
241
|
+
setInputValue(history[newIndex]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
if (historyIndex > 0) {
|
|
246
|
+
const newIndex = historyIndex - 1;
|
|
247
|
+
setHistoryIndex(newIndex);
|
|
248
|
+
setInputValue(history[newIndex]);
|
|
249
|
+
}
|
|
250
|
+
else if (historyIndex === 0) {
|
|
251
|
+
setHistoryIndex(-1);
|
|
252
|
+
setInputValue(tempInput);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}, [enableHistory, history, historyIndex, inputValue, tempInput]);
|
|
256
|
+
// Ref to hold submit function (to avoid circular dependency)
|
|
257
|
+
const submitRef = React.useRef();
|
|
258
|
+
// Select a command
|
|
259
|
+
const selectCommand = React.useCallback((command) => {
|
|
260
|
+
if (command.keyword === 'history') {
|
|
261
|
+
// Special handling for history command
|
|
262
|
+
setShowingHistory(true);
|
|
263
|
+
setInputValue('');
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// Send the command's message
|
|
267
|
+
setInputValue('');
|
|
268
|
+
submitRef.current?.(command.message);
|
|
269
|
+
}
|
|
270
|
+
}, []);
|
|
271
|
+
// Navigate commands
|
|
272
|
+
const navigateCommands = React.useCallback((direction) => {
|
|
273
|
+
if (filteredCommands.length === 0)
|
|
274
|
+
return;
|
|
275
|
+
if (direction === 'down') {
|
|
276
|
+
setSelectedCommandIndex(prev => prev < filteredCommands.length - 1 ? prev + 1 : 0);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
setSelectedCommandIndex(prev => prev > 0 ? prev - 1 : filteredCommands.length - 1);
|
|
280
|
+
}
|
|
281
|
+
}, [filteredCommands.length]);
|
|
282
|
+
// Register keyboard shortcut
|
|
283
|
+
React.useEffect(() => {
|
|
284
|
+
if (!shortcut)
|
|
285
|
+
return;
|
|
286
|
+
const handler = (e) => {
|
|
287
|
+
if (matchShortcut(e, shortcut)) {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
toggle();
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
window.addEventListener('keydown', handler);
|
|
293
|
+
return () => window.removeEventListener('keydown', handler);
|
|
294
|
+
}, [shortcut, toggle]);
|
|
295
|
+
// Process tool calls from realtime data
|
|
296
|
+
const processToolCalls = React.useCallback((messages) => {
|
|
297
|
+
const summaries = [];
|
|
298
|
+
const toolResponseMap = new Map();
|
|
299
|
+
// Collect tool responses
|
|
300
|
+
for (const msg of messages) {
|
|
301
|
+
if (msg.role === 'tool' && msg.tool_call_id) {
|
|
302
|
+
toolResponseMap.set(msg.tool_call_id, msg.content);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Collect tool calls from assistant messages
|
|
306
|
+
for (const msg of messages) {
|
|
307
|
+
if (msg.role === 'assistant' && msg.tool_calls?.length) {
|
|
308
|
+
for (const tc of msg.tool_calls) {
|
|
309
|
+
const hasResponse = toolResponseMap.has(tc.id);
|
|
310
|
+
let input;
|
|
311
|
+
try {
|
|
312
|
+
input = JSON.parse(tc.function.arguments || '{}');
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
input = {};
|
|
316
|
+
}
|
|
317
|
+
// Use message summary if available, otherwise format tool name
|
|
318
|
+
const summaryText = msg.summary || formatToolName(tc.function.name);
|
|
319
|
+
summaries.push({
|
|
320
|
+
id: tc.id,
|
|
321
|
+
name: tc.function.name,
|
|
322
|
+
status: hasResponse ? 'completed' : 'executing',
|
|
323
|
+
summary: summaryText,
|
|
324
|
+
input,
|
|
325
|
+
output: toolResponseMap.get(tc.id),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return summaries;
|
|
331
|
+
}, []);
|
|
332
|
+
// Handle pending client-side tool calls
|
|
333
|
+
const handlePendingToolCalls = React.useCallback(async (data) => {
|
|
334
|
+
if (!clientRef.current || !chatUid)
|
|
335
|
+
return;
|
|
336
|
+
const pendingCalls = data.pendingToolCalls || extractPendingToolCalls(data.chatHistory);
|
|
337
|
+
if (pendingCalls.length === 0)
|
|
338
|
+
return;
|
|
339
|
+
try {
|
|
340
|
+
const responses = await executeToolCalls(pendingCalls);
|
|
341
|
+
if (responses.length > 0) {
|
|
342
|
+
await clientRef.current.sendToolResponses(assistantId, chatUid, responses);
|
|
343
|
+
setShouldPoll(true);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
348
|
+
setError(error);
|
|
349
|
+
onErrorRef.current?.(error);
|
|
350
|
+
}
|
|
351
|
+
}, [chatUid, assistantId, executeToolCalls, extractPendingToolCalls]);
|
|
352
|
+
// Polling
|
|
353
|
+
usePolling.usePolling(shouldPoll ? chatUid : null, async () => {
|
|
354
|
+
if (!clientRef.current || !chatUid) {
|
|
355
|
+
throw new Error('Cannot poll without client or chatUid');
|
|
356
|
+
}
|
|
357
|
+
return clientRef.current.getRealtimeHistory(assistantId, chatUid);
|
|
358
|
+
}, {
|
|
359
|
+
interval: 1000,
|
|
360
|
+
enabled: shouldPoll,
|
|
361
|
+
stopStatuses: ['completed', 'error', 'waiting_for_tool_response'],
|
|
362
|
+
onUpdate: async (data) => {
|
|
363
|
+
// Update tool calls display
|
|
364
|
+
const summaries = processToolCalls(data.chatHistory);
|
|
365
|
+
setToolCalls(summaries);
|
|
366
|
+
// Update current tool summary
|
|
367
|
+
// Prefer showing an executing tool, but fall back to the last tool (completed or not)
|
|
368
|
+
if (summaries.length > 0) {
|
|
369
|
+
const lastExecuting = summaries.filter(s => s.status === 'executing').pop();
|
|
370
|
+
const lastTool = summaries[summaries.length - 1];
|
|
371
|
+
setCurrentToolSummary(lastExecuting?.summary || lastTool?.summary || null);
|
|
372
|
+
}
|
|
373
|
+
// Handle client-side tool calls
|
|
374
|
+
if (data.status === 'waiting_for_tool_response' || data.pendingToolCalls?.length) {
|
|
375
|
+
await handlePendingToolCalls(data);
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
onStop: (data) => {
|
|
379
|
+
setShouldPoll(false);
|
|
380
|
+
if (data?.status === 'error') {
|
|
381
|
+
setIsProcessing(false);
|
|
382
|
+
const err = new Error('Processing failed');
|
|
383
|
+
setError(err);
|
|
384
|
+
onErrorRef.current?.(err);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (data?.status === 'completed') {
|
|
388
|
+
setIsProcessing(false);
|
|
389
|
+
// Extract final assistant message
|
|
390
|
+
const assistantMessages = data.chatHistory.filter(m => m.role === 'assistant');
|
|
391
|
+
const lastAssistantMessage = assistantMessages[assistantMessages.length - 1];
|
|
392
|
+
if (lastAssistantMessage && chatUid) {
|
|
393
|
+
const commandResult = {
|
|
394
|
+
chatUid,
|
|
395
|
+
message: lastAssistantMessage,
|
|
396
|
+
toolCalls: processToolCalls(data.chatHistory),
|
|
397
|
+
};
|
|
398
|
+
setResult(commandResult);
|
|
399
|
+
// Handle execution mode
|
|
400
|
+
if (onExecute === 'openDrawer' && chatDrawerRef?.current) {
|
|
401
|
+
chatDrawerRef.current.setChatUid(chatUid);
|
|
402
|
+
chatDrawerRef.current.open();
|
|
403
|
+
// Close and reset the command bar when handing off to drawer
|
|
404
|
+
setResult(null);
|
|
405
|
+
setToolCalls([]);
|
|
406
|
+
setCurrentToolSummary(null);
|
|
407
|
+
setInternalVisible(false);
|
|
408
|
+
onVisibilityChange?.(false);
|
|
409
|
+
onCloseRef.current?.();
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
onResponseRef.current?.(commandResult);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
onError: (err) => {
|
|
418
|
+
setError(err);
|
|
419
|
+
setIsProcessing(false);
|
|
420
|
+
setShouldPoll(false);
|
|
421
|
+
onErrorRef.current?.(err);
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
// Submit message
|
|
425
|
+
const submit = React.useCallback(async (message) => {
|
|
426
|
+
const msg = message ?? inputValue;
|
|
427
|
+
if (!msg.trim())
|
|
428
|
+
return;
|
|
429
|
+
if (!clientRef.current) {
|
|
430
|
+
const err = new Error('API client not configured. Please provide an API key.');
|
|
431
|
+
setError(err);
|
|
432
|
+
onErrorRef.current?.(err);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// Add to history before processing
|
|
436
|
+
addToHistory(msg);
|
|
437
|
+
// Clear input and start processing
|
|
438
|
+
setInputValue('');
|
|
439
|
+
setIsProcessing(true);
|
|
440
|
+
setError(null);
|
|
441
|
+
setResult(null);
|
|
442
|
+
setToolCalls([]);
|
|
443
|
+
setCurrentToolSummary(null);
|
|
444
|
+
setShowingHistory(false);
|
|
445
|
+
setHistoryIndex(-1);
|
|
446
|
+
onSubmitRef.current?.(msg);
|
|
447
|
+
try {
|
|
448
|
+
const dto = {
|
|
449
|
+
message: msg,
|
|
450
|
+
chatUid: chatUid || undefined,
|
|
451
|
+
metadata: resolvedTenantMetadata,
|
|
452
|
+
tenantId: resolvedTenantId,
|
|
453
|
+
...(toolSchemas.length > 0 && { tools: toolSchemas }),
|
|
454
|
+
};
|
|
455
|
+
const response = await clientRef.current.sendMessageAsync(assistantId, dto);
|
|
456
|
+
if (response.chatUid && response.chatUid !== chatUid) {
|
|
457
|
+
setChatUid(response.chatUid);
|
|
458
|
+
}
|
|
459
|
+
setShouldPoll(true);
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
463
|
+
setError(error);
|
|
464
|
+
setIsProcessing(false);
|
|
465
|
+
onErrorRef.current?.(error);
|
|
466
|
+
}
|
|
467
|
+
}, [inputValue, chatUid, assistantId, resolvedTenantId, resolvedTenantMetadata, toolSchemas, addToHistory]);
|
|
468
|
+
// Update submit ref for use in selectCommand
|
|
469
|
+
submitRef.current = submit;
|
|
470
|
+
// Reset state
|
|
471
|
+
const reset = React.useCallback(() => {
|
|
472
|
+
setInputValue('');
|
|
473
|
+
setIsProcessing(false);
|
|
474
|
+
setToolCalls([]);
|
|
475
|
+
setCurrentToolSummary(null);
|
|
476
|
+
setResult(null);
|
|
477
|
+
setChatUid(null);
|
|
478
|
+
setError(null);
|
|
479
|
+
setShouldPoll(false);
|
|
480
|
+
}, []);
|
|
481
|
+
// Handle keyboard events
|
|
482
|
+
const handleKeyDown = React.useCallback((e) => {
|
|
483
|
+
// Handle Escape
|
|
484
|
+
if (e.key === 'Escape') {
|
|
485
|
+
e.preventDefault();
|
|
486
|
+
if (showingHistory) {
|
|
487
|
+
setShowingHistory(false);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (showingCommands) {
|
|
491
|
+
setInputValue('');
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// Reset if there's a result or if processing
|
|
495
|
+
if (result || isProcessing) {
|
|
496
|
+
reset();
|
|
497
|
+
}
|
|
498
|
+
close();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
// Handle arrow keys for commands
|
|
502
|
+
if (showingCommands && filteredCommands.length > 0) {
|
|
503
|
+
if (e.key === 'ArrowDown') {
|
|
504
|
+
e.preventDefault();
|
|
505
|
+
navigateCommands('down');
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (e.key === 'ArrowUp') {
|
|
509
|
+
e.preventDefault();
|
|
510
|
+
navigateCommands('up');
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
514
|
+
e.preventDefault();
|
|
515
|
+
const selectedCommand = filteredCommands[selectedCommandIndex];
|
|
516
|
+
if (selectedCommand) {
|
|
517
|
+
selectCommand(selectedCommand);
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (e.key === 'Tab') {
|
|
522
|
+
e.preventDefault();
|
|
523
|
+
const selectedCommand = filteredCommands[selectedCommandIndex];
|
|
524
|
+
if (selectedCommand) {
|
|
525
|
+
setInputValue('/' + selectedCommand.keyword + ' ');
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Handle arrow keys for history (only when not in command mode)
|
|
531
|
+
if (!showingCommands && !isProcessing) {
|
|
532
|
+
if (e.key === 'ArrowUp') {
|
|
533
|
+
e.preventDefault();
|
|
534
|
+
navigateHistory('up');
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (e.key === 'ArrowDown' && historyIndex >= 0) {
|
|
538
|
+
e.preventDefault();
|
|
539
|
+
navigateHistory('down');
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Handle Enter to submit
|
|
544
|
+
if (e.key === 'Enter' && !e.shiftKey && !isProcessing) {
|
|
545
|
+
e.preventDefault();
|
|
546
|
+
submit();
|
|
547
|
+
}
|
|
548
|
+
}, [
|
|
549
|
+
isProcessing,
|
|
550
|
+
result,
|
|
551
|
+
showingCommands,
|
|
552
|
+
showingHistory,
|
|
553
|
+
filteredCommands,
|
|
554
|
+
selectedCommandIndex,
|
|
555
|
+
historyIndex,
|
|
556
|
+
submit,
|
|
557
|
+
reset,
|
|
558
|
+
close,
|
|
559
|
+
navigateCommands,
|
|
560
|
+
navigateHistory,
|
|
561
|
+
selectCommand,
|
|
562
|
+
]);
|
|
563
|
+
return {
|
|
564
|
+
isVisible,
|
|
565
|
+
open,
|
|
566
|
+
close,
|
|
567
|
+
toggle,
|
|
568
|
+
inputValue,
|
|
569
|
+
setInputValue,
|
|
570
|
+
inputRef,
|
|
571
|
+
focus,
|
|
572
|
+
isProcessing,
|
|
573
|
+
toolCalls,
|
|
574
|
+
currentToolSummary,
|
|
575
|
+
result,
|
|
576
|
+
chatUid,
|
|
577
|
+
error,
|
|
578
|
+
history,
|
|
579
|
+
historyIndex,
|
|
580
|
+
showingHistory,
|
|
581
|
+
setShowingHistory,
|
|
582
|
+
showingCommands,
|
|
583
|
+
filteredCommands,
|
|
584
|
+
selectedCommandIndex,
|
|
585
|
+
selectCommand,
|
|
586
|
+
submit,
|
|
587
|
+
reset,
|
|
588
|
+
handleKeyDown,
|
|
589
|
+
clearHistory,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
exports.formatShortcut = formatShortcut;
|
|
594
|
+
exports.useAICommandBar = useAICommandBar;
|
|
595
|
+
//# sourceMappingURL=useAICommandBar.js.map
|