@hef2024/llmasaservice-ui 0.16.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -0
- package/dist/index.css +3239 -0
- package/dist/index.d.mts +521 -0
- package/dist/index.d.ts +521 -0
- package/dist/index.js +5885 -0
- package/dist/index.mjs +5851 -0
- package/index.ts +28 -0
- package/package.json +70 -0
- package/src/AIAgentPanel.css +1354 -0
- package/src/AIAgentPanel.tsx +1883 -0
- package/src/AIChatPanel.css +1618 -0
- package/src/AIChatPanel.tsx +1725 -0
- package/src/AgentPanel.tsx +323 -0
- package/src/ChatPanel.css +1093 -0
- package/src/ChatPanel.tsx +3583 -0
- package/src/ChatStatus.tsx +40 -0
- package/src/EmailModal.tsx +56 -0
- package/src/ToolInfoModal.tsx +49 -0
- package/src/components/ui/Button.tsx +57 -0
- package/src/components/ui/Dialog.tsx +153 -0
- package/src/components/ui/Input.tsx +33 -0
- package/src/components/ui/ScrollArea.tsx +29 -0
- package/src/components/ui/Select.tsx +156 -0
- package/src/components/ui/Tooltip.tsx +73 -0
- package/src/components/ui/index.ts +20 -0
- package/src/hooks/useAgentRegistry.ts +349 -0
- package/src/hooks/useConversationStore.ts +313 -0
- package/src/mcpClient.ts +107 -0
- package/tsconfig.json +108 -0
- package/types/declarations.d.ts +22 -0
|
@@ -0,0 +1,1883 @@
|
|
|
1
|
+
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
|
2
|
+
import AIChatPanel, { AgentOption } from './AIChatPanel';
|
|
3
|
+
import { useAgentRegistry } from './hooks/useAgentRegistry';
|
|
4
|
+
import { Button, Input, ScrollArea, Dialog, DialogFooter, Tooltip } from './components/ui';
|
|
5
|
+
import { LLMAsAServiceCustomer } from 'llmasaservice-client';
|
|
6
|
+
import './AIAgentPanel.css';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Context section for agent awareness
|
|
10
|
+
*/
|
|
11
|
+
export interface ContextSection {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
data: Record<string, unknown>;
|
|
15
|
+
tokens?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Agent context passed to the panel
|
|
20
|
+
*/
|
|
21
|
+
export interface AgentContext {
|
|
22
|
+
route?: string;
|
|
23
|
+
sections: ContextSection[];
|
|
24
|
+
totalTokens?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* API Conversation Summary from LLMAsAService
|
|
29
|
+
*/
|
|
30
|
+
export interface APIConversationSummary {
|
|
31
|
+
conversationId: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
summary?: string;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
updatedAt: string;
|
|
36
|
+
agentId?: string;
|
|
37
|
+
messageCount?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Active conversation state for multi-conversation support
|
|
42
|
+
*/
|
|
43
|
+
export interface ActiveConversation {
|
|
44
|
+
conversationId: string;
|
|
45
|
+
agentId: string;
|
|
46
|
+
history: Record<string, { content: string; callId: string }>;
|
|
47
|
+
isLoading: boolean;
|
|
48
|
+
title: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Agent configuration with optional local overrides
|
|
53
|
+
*/
|
|
54
|
+
export interface AgentConfig {
|
|
55
|
+
id: string;
|
|
56
|
+
localName?: string;
|
|
57
|
+
avatarUrl?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Props for AIAgentPanel
|
|
62
|
+
*/
|
|
63
|
+
export interface AIAgentPanelProps {
|
|
64
|
+
// Agent Configuration - can be string IDs or full config objects
|
|
65
|
+
agents: (string | AgentConfig)[];
|
|
66
|
+
defaultAgent?: string;
|
|
67
|
+
|
|
68
|
+
// Customer ID - REQUIRED for conversation history
|
|
69
|
+
customerId: string;
|
|
70
|
+
|
|
71
|
+
// API Key for authenticated endpoints (conversations, etc.)
|
|
72
|
+
apiKey?: string;
|
|
73
|
+
|
|
74
|
+
// Context Management
|
|
75
|
+
context?: AgentContext | null;
|
|
76
|
+
contextDataSources?: Record<string, unknown>;
|
|
77
|
+
sharedContextSections?: ContextSection[];
|
|
78
|
+
pageContextSections?: ContextSection[];
|
|
79
|
+
onContextChange?: (context: AgentContext) => void;
|
|
80
|
+
maxContextTokens?: number;
|
|
81
|
+
data?: { key: string; data: string }[];
|
|
82
|
+
|
|
83
|
+
// UI Configuration
|
|
84
|
+
theme?: 'light' | 'dark';
|
|
85
|
+
defaultCollapsed?: boolean;
|
|
86
|
+
defaultWidth?: number;
|
|
87
|
+
minWidth?: number;
|
|
88
|
+
maxWidth?: number;
|
|
89
|
+
position?: 'left' | 'right';
|
|
90
|
+
sidebarPosition?: 'left' | 'right';
|
|
91
|
+
|
|
92
|
+
// Agent Suggestions (inline agent handoff cards)
|
|
93
|
+
enableAgentSuggestions?: boolean;
|
|
94
|
+
|
|
95
|
+
// Context Viewer Configuration
|
|
96
|
+
enableContextDetailView?: boolean;
|
|
97
|
+
|
|
98
|
+
// Callbacks
|
|
99
|
+
onAgentSwitch?: (fromAgent: string, toAgent: string) => void;
|
|
100
|
+
onConversationChange?: (conversationId: string) => void;
|
|
101
|
+
historyChangedCallback?: (history: Record<string, { content: string; callId: string }>) => void;
|
|
102
|
+
responseCompleteCallback?: (callId: string, prompt: string, response: string) => void;
|
|
103
|
+
thumbsUpClick?: (callId: string) => void;
|
|
104
|
+
thumbsDownClick?: (callId: string) => void;
|
|
105
|
+
|
|
106
|
+
// Standard props
|
|
107
|
+
customer?: LLMAsAServiceCustomer;
|
|
108
|
+
url?: string;
|
|
109
|
+
showPoweredBy?: boolean;
|
|
110
|
+
conversation?: string | null;
|
|
111
|
+
|
|
112
|
+
// Actions/Regex
|
|
113
|
+
actions?: {
|
|
114
|
+
pattern: string;
|
|
115
|
+
type?: string;
|
|
116
|
+
markdown?: string;
|
|
117
|
+
callback?: (match: string, groups: any[]) => void;
|
|
118
|
+
clickCode?: string;
|
|
119
|
+
style?: string;
|
|
120
|
+
}[];
|
|
121
|
+
|
|
122
|
+
// Follow-on
|
|
123
|
+
followOnQuestions?: string[];
|
|
124
|
+
followOnPrompt?: string;
|
|
125
|
+
|
|
126
|
+
// Conversation history settings
|
|
127
|
+
historyListLimit?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Icons
|
|
131
|
+
const SearchIcon = () => (
|
|
132
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
133
|
+
<path d="M7.333 12.667A5.333 5.333 0 1 0 7.333 2a5.333 5.333 0 0 0 0 10.667ZM14 14l-2.9-2.9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
134
|
+
</svg>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const PlusIcon = () => (
|
|
138
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
139
|
+
<path d="M8 3.333v9.334M3.333 8h9.334" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
140
|
+
</svg>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const ChevronLeftIcon = () => (
|
|
144
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
145
|
+
<path d="M10 12L6 8l4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
146
|
+
</svg>
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const ChevronRightIcon = () => (
|
|
150
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
151
|
+
<path d="M6 12l4-4-4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
152
|
+
</svg>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const MessageIcon = () => (
|
|
156
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
157
|
+
<path d="M14 10a1.333 1.333 0 0 1-1.333 1.333H4L2 14V3.333A1.333 1.333 0 0 1 3.333 2h9.334A1.333 1.333 0 0 1 14 3.333V10Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
158
|
+
</svg>
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const TrashIcon = () => (
|
|
162
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
163
|
+
<path d="M1.75 3.5h10.5M4.667 3.5V2.333a1.167 1.167 0 0 1 1.166-1.166h2.334a1.167 1.167 0 0 1 1.166 1.166V3.5m1.75 0v8.167a1.167 1.167 0 0 1-1.166 1.166H4.083a1.167 1.167 0 0 1-1.166-1.166V3.5h8.166Z" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
164
|
+
</svg>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const ContextIcon = () => (
|
|
168
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
169
|
+
<path d="M2 4h12M2 8h12M2 12h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
170
|
+
</svg>
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const CloseIcon = () => (
|
|
174
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
175
|
+
<path d="M9 3L3 9M3 3l6 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
176
|
+
</svg>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const LoadingDotIcon = () => (
|
|
180
|
+
<span className="ai-agent-panel__loading-dot" />
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const HistoryIcon = () => (
|
|
184
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
185
|
+
<path d="M8 4v4l2.5 1.5M14 8A6 6 0 1 1 2 8a6 6 0 0 1 12 0Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
186
|
+
</svg>
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const SidebarIcon = () => (
|
|
190
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
191
|
+
<rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" strokeWidth="1.5"/>
|
|
192
|
+
<path d="M6 2v12" stroke="currentColor" strokeWidth="1.5"/>
|
|
193
|
+
</svg>
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* AIAgentPanel - Cursor-inspired multi-agent panel
|
|
198
|
+
*/
|
|
199
|
+
// Normalize conversation list payload from API
|
|
200
|
+
const normalizeConversationListPayload = (payload: any): APIConversationSummary[] => {
|
|
201
|
+
if (!payload) return [];
|
|
202
|
+
|
|
203
|
+
const conversations = Array.isArray(payload) ? payload : payload.conversations || [];
|
|
204
|
+
|
|
205
|
+
return conversations.map((conv: any) => {
|
|
206
|
+
// Get title, but filter out generic "Conversation" from API
|
|
207
|
+
let title = conv.title && conv.title !== 'Conversation' ? conv.title : '';
|
|
208
|
+
if (!title) {
|
|
209
|
+
title = conv.summary || extractTitleFromConversation(conv);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
conversationId: conv.conversationId || conv.id || conv.conversation_id,
|
|
214
|
+
title,
|
|
215
|
+
summary: conv.summary,
|
|
216
|
+
createdAt: conv.createdAt || conv.created_at || conv.timestamp || new Date().toISOString(),
|
|
217
|
+
updatedAt: conv.updatedAt || conv.updated_at || conv.lastUpdated || conv.createdAt || new Date().toISOString(),
|
|
218
|
+
agentId: conv.agentId || conv.agent_id,
|
|
219
|
+
messageCount: conv.messageCount || conv.message_count,
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Extract title from conversation data
|
|
225
|
+
const extractTitleFromConversation = (conv: any): string => {
|
|
226
|
+
// Try to get the first user message as the title
|
|
227
|
+
if (conv.messages && Array.isArray(conv.messages) && conv.messages.length > 0) {
|
|
228
|
+
const firstUserMessage = conv.messages.find((m: any) => m.role === 'user');
|
|
229
|
+
if (firstUserMessage?.content) {
|
|
230
|
+
const content = typeof firstUserMessage.content === 'string'
|
|
231
|
+
? firstUserMessage.content
|
|
232
|
+
: firstUserMessage.content[0]?.text || '';
|
|
233
|
+
return content.length > 60 ? content.slice(0, 57) + '...' : content;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Try prompts object (keys are the prompts)
|
|
238
|
+
if (conv.prompts && typeof conv.prompts === 'object') {
|
|
239
|
+
const firstPrompt = Object.keys(conv.prompts)[0];
|
|
240
|
+
if (firstPrompt) {
|
|
241
|
+
return firstPrompt.length > 60 ? firstPrompt.slice(0, 57) + '...' : firstPrompt;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Try calls array - first call's prompt
|
|
246
|
+
if (conv.calls && Array.isArray(conv.calls) && conv.calls.length > 0) {
|
|
247
|
+
const firstCall = conv.calls[0];
|
|
248
|
+
if (firstCall.prompt) {
|
|
249
|
+
return firstCall.prompt.length > 60 ? firstCall.prompt.slice(0, 57) + '...' : firstCall.prompt;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Try firstPrompt field (in case API provides it directly)
|
|
254
|
+
if (conv.firstPrompt || conv.first_prompt) {
|
|
255
|
+
const prompt = conv.firstPrompt || conv.first_prompt;
|
|
256
|
+
return prompt.length > 60 ? prompt.slice(0, 57) + '...' : prompt;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Fall back to a human-readable timestamp
|
|
260
|
+
const timestamp = conv.createdAt || conv.created_at || conv.timestamp;
|
|
261
|
+
if (timestamp) {
|
|
262
|
+
const date = new Date(timestamp);
|
|
263
|
+
const now = new Date();
|
|
264
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
265
|
+
const chatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
266
|
+
|
|
267
|
+
if (chatDate.getTime() === today.getTime()) {
|
|
268
|
+
// Today - show time
|
|
269
|
+
return `Chat from ${date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}`;
|
|
270
|
+
} else {
|
|
271
|
+
// Other day - show date
|
|
272
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
273
|
+
if (chatDate.getTime() === yesterday.getTime()) {
|
|
274
|
+
return `Chat from yesterday`;
|
|
275
|
+
}
|
|
276
|
+
return `Chat from ${date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return 'New chat';
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// Group conversations by time
|
|
284
|
+
const groupConversationsByTime = (conversations: APIConversationSummary[], showAllGroups: boolean = false): { label: string; conversations: APIConversationSummary[]; count: number }[] => {
|
|
285
|
+
const now = new Date();
|
|
286
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
287
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
288
|
+
const thisWeek = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
289
|
+
const thisMonth = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
290
|
+
|
|
291
|
+
const groups: Record<string, APIConversationSummary[]> = {
|
|
292
|
+
'Today': [],
|
|
293
|
+
'Yesterday': [],
|
|
294
|
+
'This Week': [],
|
|
295
|
+
'This Month': [],
|
|
296
|
+
'Older': [],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
conversations
|
|
300
|
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
301
|
+
.forEach(conv => {
|
|
302
|
+
const date = new Date(conv.updatedAt);
|
|
303
|
+
if (date >= today) {
|
|
304
|
+
groups['Today']!.push(conv);
|
|
305
|
+
} else if (date >= yesterday) {
|
|
306
|
+
groups['Yesterday']!.push(conv);
|
|
307
|
+
} else if (date >= thisWeek) {
|
|
308
|
+
groups['This Week']!.push(conv);
|
|
309
|
+
} else if (date >= thisMonth) {
|
|
310
|
+
groups['This Month']!.push(conv);
|
|
311
|
+
} else {
|
|
312
|
+
groups['Older']!.push(conv);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return Object.entries(groups)
|
|
317
|
+
.filter(([_, convs]) => showAllGroups || convs.length > 0)
|
|
318
|
+
.map(([label, conversations]) => ({ label, conversations, count: conversations.length }));
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Empty arrays/objects to prevent recreating on every render
|
|
322
|
+
const EMPTY_ARRAY: never[] = [];
|
|
323
|
+
const EMPTY_HISTORY: Record<string, { content: string; callId: string }> = {};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Memoized wrapper for AIChatPanel to prevent unnecessary re-renders
|
|
327
|
+
*/
|
|
328
|
+
interface ChatPanelWrapperProps {
|
|
329
|
+
activeConv: ActiveConversation;
|
|
330
|
+
currentConversationId: string | null;
|
|
331
|
+
getAgent: (id: string) => any;
|
|
332
|
+
url: string;
|
|
333
|
+
theme: 'light' | 'dark';
|
|
334
|
+
chatPanelData: { key: string; data: string }[];
|
|
335
|
+
handleHistoryChanged: (history: Record<string, { content: string; callId: string }>, conversationId?: string) => void;
|
|
336
|
+
handleLoadingChange: (isLoading: boolean, conversationId?: string) => void;
|
|
337
|
+
responseCompleteCallback?: (callId: string, prompt: string, response: string) => void;
|
|
338
|
+
thumbsUpClick?: (callId: string) => void;
|
|
339
|
+
thumbsDownClick?: (callId: string) => void;
|
|
340
|
+
effectiveCustomer: any;
|
|
341
|
+
showPoweredBy: boolean;
|
|
342
|
+
actions: any[];
|
|
343
|
+
followOnQuestions: string[];
|
|
344
|
+
followOnPrompt: string;
|
|
345
|
+
agentOptions: any[];
|
|
346
|
+
currentAgentId: string;
|
|
347
|
+
handleAgentSwitch: (agentId: string) => void;
|
|
348
|
+
agentsLoading: boolean;
|
|
349
|
+
// Context viewer props
|
|
350
|
+
contextSections: ContextSection[];
|
|
351
|
+
totalContextTokens: number;
|
|
352
|
+
maxContextTokens: number;
|
|
353
|
+
enableContextDetailView: boolean;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Remove React.memo temporarily to debug - ChatPanelWrapper needs to re-render when agentId changes
|
|
357
|
+
const ChatPanelWrapper = (({
|
|
358
|
+
activeConv,
|
|
359
|
+
currentConversationId,
|
|
360
|
+
getAgent,
|
|
361
|
+
url,
|
|
362
|
+
theme,
|
|
363
|
+
chatPanelData,
|
|
364
|
+
handleHistoryChanged,
|
|
365
|
+
handleLoadingChange,
|
|
366
|
+
responseCompleteCallback,
|
|
367
|
+
thumbsUpClick,
|
|
368
|
+
thumbsDownClick,
|
|
369
|
+
effectiveCustomer,
|
|
370
|
+
showPoweredBy,
|
|
371
|
+
actions,
|
|
372
|
+
followOnQuestions,
|
|
373
|
+
followOnPrompt,
|
|
374
|
+
agentOptions,
|
|
375
|
+
currentAgentId,
|
|
376
|
+
handleAgentSwitch,
|
|
377
|
+
agentsLoading,
|
|
378
|
+
contextSections,
|
|
379
|
+
totalContextTokens,
|
|
380
|
+
maxContextTokens,
|
|
381
|
+
enableContextDetailView,
|
|
382
|
+
}) => {
|
|
383
|
+
const convAgentProfile = getAgent(activeConv.agentId);
|
|
384
|
+
const convAgentMetadata = convAgentProfile?.metadata;
|
|
385
|
+
const isVisible = currentConversationId === activeConv.conversationId;
|
|
386
|
+
|
|
387
|
+
// Memoize callbacks to prevent recreating on every render
|
|
388
|
+
const historyCallback = useCallback(
|
|
389
|
+
(history: Record<string, { content: string; callId: string }>) => {
|
|
390
|
+
handleHistoryChanged(history, activeConv.conversationId);
|
|
391
|
+
},
|
|
392
|
+
[handleHistoryChanged, activeConv.conversationId]
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const loadingCallback = useCallback(
|
|
396
|
+
(loading: boolean) => {
|
|
397
|
+
handleLoadingChange(loading, activeConv.conversationId);
|
|
398
|
+
},
|
|
399
|
+
[handleLoadingChange, activeConv.conversationId]
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// Compute follow-on questions - MUST update when agent switches
|
|
403
|
+
// Don't use useMemo - compute fresh every render to ensure it always reflects current agent
|
|
404
|
+
// The computation is cheap (just string split), and this guarantees updates
|
|
405
|
+
const agentStatus = convAgentProfile?.status;
|
|
406
|
+
const promptsString = convAgentMetadata?.displayFollowOnPrompts || '';
|
|
407
|
+
|
|
408
|
+
// Compute fresh every render - no memoization to avoid stale data
|
|
409
|
+
let effectiveFollowOnQuestions: string[];
|
|
410
|
+
if (followOnQuestions.length > 0) {
|
|
411
|
+
effectiveFollowOnQuestions = [...followOnQuestions];
|
|
412
|
+
} else {
|
|
413
|
+
const prompts = promptsString ? promptsString.split('|').filter((p: string) => p.trim()) : [];
|
|
414
|
+
effectiveFollowOnQuestions = [...prompts];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Memoize mcpServers
|
|
418
|
+
const mcpServers = useMemo(() => {
|
|
419
|
+
return convAgentProfile?.mcpServers || EMPTY_ARRAY;
|
|
420
|
+
}, [convAgentProfile?.mcpServers]);
|
|
421
|
+
|
|
422
|
+
if (!convAgentMetadata) return null;
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<div
|
|
426
|
+
className="ai-agent-panel__chat-wrapper"
|
|
427
|
+
style={{ display: isVisible ? 'flex' : 'none' }}
|
|
428
|
+
>
|
|
429
|
+
<AIChatPanel
|
|
430
|
+
project_id={convAgentMetadata.projectId}
|
|
431
|
+
service={convAgentMetadata.groupId || null}
|
|
432
|
+
url={url}
|
|
433
|
+
title=""
|
|
434
|
+
theme={theme}
|
|
435
|
+
promptTemplate={convAgentMetadata.displayPromptTemplate || '{{prompt}}'}
|
|
436
|
+
initialMessage={
|
|
437
|
+
convAgentMetadata.displayStartMessageOrPrompt === 'message'
|
|
438
|
+
? convAgentMetadata.displayInitialMessageOrPrompt
|
|
439
|
+
: undefined
|
|
440
|
+
}
|
|
441
|
+
initialPrompt={
|
|
442
|
+
convAgentMetadata.displayStartMessageOrPrompt === 'prompt'
|
|
443
|
+
? convAgentMetadata.displayInitialMessageOrPrompt
|
|
444
|
+
: undefined
|
|
445
|
+
}
|
|
446
|
+
placeholder={convAgentMetadata.displayPlaceholder || 'Type a message...'}
|
|
447
|
+
hideInitialPrompt={convAgentMetadata.displayHideInitialPrompt ?? true}
|
|
448
|
+
data={chatPanelData}
|
|
449
|
+
agent={activeConv.agentId}
|
|
450
|
+
conversation={activeConv.conversationId.startsWith('new-') ? undefined : activeConv.conversationId}
|
|
451
|
+
initialHistory={activeConv.history || EMPTY_HISTORY}
|
|
452
|
+
historyChangedCallback={historyCallback}
|
|
453
|
+
onLoadingChange={loadingCallback}
|
|
454
|
+
responseCompleteCallback={responseCompleteCallback}
|
|
455
|
+
thumbsUpClick={thumbsUpClick}
|
|
456
|
+
thumbsDownClick={thumbsDownClick}
|
|
457
|
+
customer={effectiveCustomer}
|
|
458
|
+
showPoweredBy={showPoweredBy}
|
|
459
|
+
showNewConversationButton={false}
|
|
460
|
+
createConversationOnFirstChat={convAgentMetadata.createConversationOnFirstChat ?? true}
|
|
461
|
+
mcpServers={mcpServers}
|
|
462
|
+
actions={actions}
|
|
463
|
+
followOnQuestions={effectiveFollowOnQuestions}
|
|
464
|
+
followOnPrompt={followOnPrompt}
|
|
465
|
+
progressiveActions={true}
|
|
466
|
+
hideRagContextInPrompt={true}
|
|
467
|
+
agentOptions={agentOptions}
|
|
468
|
+
currentAgentId={currentAgentId}
|
|
469
|
+
onAgentChange={handleAgentSwitch}
|
|
470
|
+
agentsLoading={agentsLoading}
|
|
471
|
+
contextSections={contextSections}
|
|
472
|
+
totalContextTokens={totalContextTokens}
|
|
473
|
+
maxContextTokens={maxContextTokens}
|
|
474
|
+
enableContextDetailView={enableContextDetailView}
|
|
475
|
+
/>
|
|
476
|
+
</div>
|
|
477
|
+
);
|
|
478
|
+
}) as React.FC<ChatPanelWrapperProps>;
|
|
479
|
+
|
|
480
|
+
ChatPanelWrapper.displayName = 'ChatPanelWrapper';
|
|
481
|
+
|
|
482
|
+
const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
483
|
+
agents,
|
|
484
|
+
defaultAgent,
|
|
485
|
+
customerId,
|
|
486
|
+
apiKey,
|
|
487
|
+
context,
|
|
488
|
+
contextDataSources,
|
|
489
|
+
sharedContextSections = [],
|
|
490
|
+
pageContextSections = [],
|
|
491
|
+
onContextChange,
|
|
492
|
+
maxContextTokens = 8000,
|
|
493
|
+
data = [],
|
|
494
|
+
theme = 'light',
|
|
495
|
+
defaultCollapsed = false,
|
|
496
|
+
defaultWidth = 720,
|
|
497
|
+
minWidth = 520,
|
|
498
|
+
maxWidth = 1200,
|
|
499
|
+
position = 'right',
|
|
500
|
+
sidebarPosition = 'left',
|
|
501
|
+
enableAgentSuggestions = true,
|
|
502
|
+
enableContextDetailView = false,
|
|
503
|
+
onAgentSwitch,
|
|
504
|
+
onConversationChange,
|
|
505
|
+
historyChangedCallback,
|
|
506
|
+
responseCompleteCallback,
|
|
507
|
+
thumbsUpClick,
|
|
508
|
+
thumbsDownClick,
|
|
509
|
+
customer,
|
|
510
|
+
url = 'https://chat.llmasaservice.io',
|
|
511
|
+
showPoweredBy = true,
|
|
512
|
+
conversation,
|
|
513
|
+
actions = [],
|
|
514
|
+
followOnQuestions = [],
|
|
515
|
+
followOnPrompt = '',
|
|
516
|
+
historyListLimit = 50,
|
|
517
|
+
}) => {
|
|
518
|
+
// Panel state
|
|
519
|
+
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
|
520
|
+
const [isHistoryCollapsed, setIsHistoryCollapsed] = useState(() => {
|
|
521
|
+
if (typeof window !== 'undefined') {
|
|
522
|
+
const saved = localStorage.getItem('ai-agent-panel-history-collapsed');
|
|
523
|
+
return saved === 'true';
|
|
524
|
+
}
|
|
525
|
+
return false;
|
|
526
|
+
});
|
|
527
|
+
const [panelWidth, setPanelWidth] = useState(() => {
|
|
528
|
+
if (typeof window !== 'undefined') {
|
|
529
|
+
const savedWidth = localStorage.getItem('ai-agent-panel-width');
|
|
530
|
+
if (savedWidth) {
|
|
531
|
+
const width = parseInt(savedWidth, 10);
|
|
532
|
+
if (width >= minWidth && width <= maxWidth) {
|
|
533
|
+
return width;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return defaultWidth;
|
|
538
|
+
});
|
|
539
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
540
|
+
const [showSearch, setShowSearch] = useState(false);
|
|
541
|
+
const [showHandoffDialog, setShowHandoffDialog] = useState(false);
|
|
542
|
+
const [suggestedAgent, setSuggestedAgent] = useState<string | null>(null);
|
|
543
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
544
|
+
const resizeRef = useRef<HTMLDivElement>(null);
|
|
545
|
+
|
|
546
|
+
// Normalize agents prop to extract IDs and local overrides
|
|
547
|
+
const { agentIds, localOverrides } = useMemo(() => {
|
|
548
|
+
const ids: string[] = [];
|
|
549
|
+
const overrides: Record<string, { localName?: string; avatarUrl?: string }> = {};
|
|
550
|
+
|
|
551
|
+
for (const agent of agents) {
|
|
552
|
+
if (typeof agent === 'string') {
|
|
553
|
+
ids.push(agent);
|
|
554
|
+
} else {
|
|
555
|
+
ids.push(agent.id);
|
|
556
|
+
if (agent.localName || agent.avatarUrl) {
|
|
557
|
+
overrides[agent.id] = {
|
|
558
|
+
localName: agent.localName,
|
|
559
|
+
avatarUrl: agent.avatarUrl,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return { agentIds: ids, localOverrides: overrides };
|
|
566
|
+
}, [agents]);
|
|
567
|
+
|
|
568
|
+
// Current agent state
|
|
569
|
+
const [currentAgentId, setCurrentAgentId] = useState<string>(
|
|
570
|
+
defaultAgent || agentIds[0] || ''
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
// API-based conversation state
|
|
574
|
+
const [apiConversations, setApiConversations] = useState<APIConversationSummary[]>([]);
|
|
575
|
+
const [conversationsLoading, setConversationsLoading] = useState(false);
|
|
576
|
+
const [conversationsError, setConversationsError] = useState<string | null>(null);
|
|
577
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
578
|
+
|
|
579
|
+
// Multi-conversation state
|
|
580
|
+
const [activeConversations, setActiveConversations] = useState<Map<string, ActiveConversation>>(new Map());
|
|
581
|
+
const [currentConversationId, setCurrentConversationId] = useState<string | null>(conversation || null);
|
|
582
|
+
|
|
583
|
+
// Store first prompts for conversations (conversationId -> firstPrompt)
|
|
584
|
+
const [conversationFirstPrompts, setConversationFirstPrompts] = useState<Record<string, string>>({});
|
|
585
|
+
const [loadingPrompts, setLoadingPrompts] = useState<Set<string>>(new Set());
|
|
586
|
+
|
|
587
|
+
// Loading state for conversation transcript
|
|
588
|
+
const [loadingConversationId, setLoadingConversationId] = useState<string | null>(null);
|
|
589
|
+
|
|
590
|
+
// Expand/collapse state for time sections
|
|
591
|
+
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
|
|
592
|
+
'Today': true,
|
|
593
|
+
'Yesterday': true,
|
|
594
|
+
'This Week': false,
|
|
595
|
+
'This Month': false,
|
|
596
|
+
'Older': false,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Agent registry hook
|
|
600
|
+
const {
|
|
601
|
+
agents: agentProfiles,
|
|
602
|
+
isLoading: agentsLoading,
|
|
603
|
+
getAgent,
|
|
604
|
+
buildAgentAwarenessInstructions,
|
|
605
|
+
agentList,
|
|
606
|
+
} = useAgentRegistry(agentIds, { url, localOverrides });
|
|
607
|
+
|
|
608
|
+
// Ref to track if fetch is in progress (prevents duplicate calls)
|
|
609
|
+
const fetchInProgressRef = useRef(false);
|
|
610
|
+
|
|
611
|
+
// Ref to track the last agent we fetched for
|
|
612
|
+
const lastFetchedAgentRef = useRef<string | null>(null);
|
|
613
|
+
|
|
614
|
+
// Ref to track which conversations we've checked for prompts
|
|
615
|
+
const checkedPromptsRef = useRef<Set<string>>(new Set());
|
|
616
|
+
|
|
617
|
+
// Ref to track which conversations are currently being fetched (prevents duplicate requests)
|
|
618
|
+
const fetchingPromptsRef = useRef<Set<string>>(new Set());
|
|
619
|
+
|
|
620
|
+
// Ref to track failed requests with timestamps (prevents immediate retries on errors)
|
|
621
|
+
const failedPromptsRef = useRef<Map<string, { timestamp: number; status?: number }>>(new Map());
|
|
622
|
+
|
|
623
|
+
// Ref to access activeConversations without causing dependency issues
|
|
624
|
+
const activeConversationsRef = useRef<Map<string, ActiveConversation>>(activeConversations);
|
|
625
|
+
activeConversationsRef.current = activeConversations;
|
|
626
|
+
|
|
627
|
+
// Ref to access currentConversationId without causing dependency issues
|
|
628
|
+
const currentConversationIdRef = useRef<string | null>(currentConversationId);
|
|
629
|
+
currentConversationIdRef.current = currentConversationId;
|
|
630
|
+
|
|
631
|
+
// Fetch first prompt from a conversation
|
|
632
|
+
const fetchFirstPrompt = useCallback(async (conversationId: string, agentIdForConversation?: string) => {
|
|
633
|
+
// Skip if already checked/loaded
|
|
634
|
+
if (checkedPromptsRef.current.has(conversationId)) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Skip if currently being fetched (prevents duplicate requests)
|
|
639
|
+
if (fetchingPromptsRef.current.has(conversationId)) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Check if this request recently failed with a server error (502, 503, 504)
|
|
644
|
+
// Don't retry server errors immediately - wait at least 30 seconds
|
|
645
|
+
const failedRequest = failedPromptsRef.current.get(conversationId);
|
|
646
|
+
if (failedRequest) {
|
|
647
|
+
const timeSinceFailure = Date.now() - failedRequest.timestamp;
|
|
648
|
+
const isServerError = failedRequest.status && failedRequest.status >= 500 && failedRequest.status < 600;
|
|
649
|
+
|
|
650
|
+
// For server errors, wait 30 seconds before retrying
|
|
651
|
+
// For other errors, wait 5 seconds
|
|
652
|
+
const cooldownPeriod = isServerError ? 30000 : 5000;
|
|
653
|
+
|
|
654
|
+
if (timeSinceFailure < cooldownPeriod) {
|
|
655
|
+
return; // Still in cooldown period, don't retry
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Mark as currently fetching
|
|
660
|
+
fetchingPromptsRef.current.add(conversationId);
|
|
661
|
+
|
|
662
|
+
// Mark as loading
|
|
663
|
+
setLoadingPrompts(prev => new Set(prev).add(conversationId));
|
|
664
|
+
|
|
665
|
+
// Use the conversation's agentId if available, otherwise fall back to currentAgentId
|
|
666
|
+
const agentIdToUse = agentIdForConversation || currentAgentId;
|
|
667
|
+
const agentProfile = getAgent(agentIdToUse);
|
|
668
|
+
const projectId = agentProfile?.metadata?.projectId;
|
|
669
|
+
|
|
670
|
+
if (!apiKey || !projectId) {
|
|
671
|
+
fetchingPromptsRef.current.delete(conversationId);
|
|
672
|
+
setLoadingPrompts(prev => {
|
|
673
|
+
const next = new Set(prev);
|
|
674
|
+
next.delete(conversationId);
|
|
675
|
+
return next;
|
|
676
|
+
});
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
const url = `https://api.llmasaservice.io/conversations/${conversationId}/calls`;
|
|
682
|
+
|
|
683
|
+
const response = await fetch(url, {
|
|
684
|
+
headers: {
|
|
685
|
+
'x-api-key': apiKey,
|
|
686
|
+
'x-project-id': projectId,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
if (!response.ok) {
|
|
691
|
+
// Track failed requests with status code
|
|
692
|
+
failedPromptsRef.current.set(conversationId, {
|
|
693
|
+
timestamp: Date.now(),
|
|
694
|
+
status: response.status,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// For server errors (5xx), don't retry immediately - the error will be logged but we won't spam
|
|
698
|
+
if (response.status >= 500 && response.status < 600) {
|
|
699
|
+
// Remove from fetching set but don't mark as checked, so it can retry after cooldown
|
|
700
|
+
fetchingPromptsRef.current.delete(conversationId);
|
|
701
|
+
setLoadingPrompts(prev => {
|
|
702
|
+
const next = new Set(prev);
|
|
703
|
+
next.delete(conversationId);
|
|
704
|
+
return next;
|
|
705
|
+
});
|
|
706
|
+
// Silently fail for server errors - don't log to avoid console spam
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
throw new Error(`Failed to fetch calls (${response.status})`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Success - mark as checked and clear any failure record
|
|
714
|
+
checkedPromptsRef.current.add(conversationId);
|
|
715
|
+
failedPromptsRef.current.delete(conversationId);
|
|
716
|
+
|
|
717
|
+
const payload = await response.json();
|
|
718
|
+
|
|
719
|
+
// Extract first prompt from calls array
|
|
720
|
+
if (Array.isArray(payload) && payload.length > 0) {
|
|
721
|
+
const firstCall = payload[0];
|
|
722
|
+
if (firstCall.prompt) {
|
|
723
|
+
const promptText = typeof firstCall.prompt === 'string'
|
|
724
|
+
? firstCall.prompt
|
|
725
|
+
: firstCall.prompt[0]?.text || '';
|
|
726
|
+
|
|
727
|
+
if (promptText) {
|
|
728
|
+
// Truncate to 60 characters
|
|
729
|
+
const truncated = promptText.length > 60 ? promptText.slice(0, 57) + '...' : promptText;
|
|
730
|
+
setConversationFirstPrompts(prev => ({
|
|
731
|
+
...prev,
|
|
732
|
+
[conversationId]: truncated,
|
|
733
|
+
}));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
} catch (error: any) {
|
|
738
|
+
// Only log non-server errors to avoid console spam
|
|
739
|
+
if (!error.message || !error.message.includes('50')) {
|
|
740
|
+
console.error('Failed to fetch first prompt:', error);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Track failed requests
|
|
744
|
+
failedPromptsRef.current.set(conversationId, {
|
|
745
|
+
timestamp: Date.now(),
|
|
746
|
+
});
|
|
747
|
+
} finally {
|
|
748
|
+
// Always remove from fetching set
|
|
749
|
+
fetchingPromptsRef.current.delete(conversationId);
|
|
750
|
+
setLoadingPrompts(prev => {
|
|
751
|
+
const next = new Set(prev);
|
|
752
|
+
next.delete(conversationId);
|
|
753
|
+
return next;
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}, [apiKey, currentAgentId, getAgent]);
|
|
757
|
+
|
|
758
|
+
// Fetch conversations from API
|
|
759
|
+
const fetchConversations = useCallback(async (agentId: string, signal?: AbortSignal) => {
|
|
760
|
+
// Get the agent profile to access projectId
|
|
761
|
+
const agentProfile = getAgent(agentId);
|
|
762
|
+
const projectId = agentProfile?.metadata?.projectId;
|
|
763
|
+
|
|
764
|
+
if (!agentId || !apiKey || !projectId) {
|
|
765
|
+
setApiConversations([]);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Prevent duplicate fetches
|
|
770
|
+
if (fetchInProgressRef.current) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
fetchInProgressRef.current = true;
|
|
774
|
+
|
|
775
|
+
setConversationsLoading(true);
|
|
776
|
+
setConversationsError(null);
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
console.log('fetchConversations - customerId:', customerId);
|
|
780
|
+
const url = `https://api.llmasaservice.io/conversations?customer_id=${customerId}`;
|
|
781
|
+
console.log('fetchConversations - URL:', url);
|
|
782
|
+
|
|
783
|
+
const response = await fetch(url, {
|
|
784
|
+
signal,
|
|
785
|
+
headers: {
|
|
786
|
+
'x-api-key': apiKey,
|
|
787
|
+
'x-project-id': projectId,
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
if (!response.ok) {
|
|
792
|
+
throw new Error(`Failed to fetch conversations (${response.status})`);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const payload = await response.json();
|
|
796
|
+
const normalized = normalizeConversationListPayload(payload);
|
|
797
|
+
|
|
798
|
+
if (!signal?.aborted) {
|
|
799
|
+
setApiConversations(normalized);
|
|
800
|
+
|
|
801
|
+
// Fetch first prompts for Today and Yesterday conversations
|
|
802
|
+
const now = new Date();
|
|
803
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
804
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
805
|
+
|
|
806
|
+
normalized.forEach(conv => {
|
|
807
|
+
const date = new Date(conv.updatedAt);
|
|
808
|
+
if (date >= yesterday && !checkedPromptsRef.current.has(conv.conversationId)) {
|
|
809
|
+
// Fetch first prompt for Today and Yesterday conversations
|
|
810
|
+
fetchFirstPrompt(conv.conversationId, conv.agentId);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
} catch (error: any) {
|
|
815
|
+
if (!signal?.aborted) {
|
|
816
|
+
setConversationsError(error.message || 'Failed to load conversations');
|
|
817
|
+
}
|
|
818
|
+
} finally {
|
|
819
|
+
fetchInProgressRef.current = false;
|
|
820
|
+
if (!signal?.aborted) {
|
|
821
|
+
setConversationsLoading(false);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}, [apiKey, customerId, getAgent, fetchFirstPrompt]);
|
|
825
|
+
|
|
826
|
+
// Helper to strip context/template data from a prompt - keeps only the user's actual message
|
|
827
|
+
const stripContextFromPrompt = useCallback((prompt: string): string => {
|
|
828
|
+
let cleanPrompt = prompt;
|
|
829
|
+
|
|
830
|
+
// Strip ---context--- block and everything after it
|
|
831
|
+
const contextIndex = cleanPrompt.indexOf('---context---');
|
|
832
|
+
if (contextIndex !== -1) {
|
|
833
|
+
cleanPrompt = cleanPrompt.substring(0, contextIndex).trim();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Strip timestamp prefix (ISO format: 2024-01-01T00:00:00.000Z:)
|
|
837
|
+
const isoTimestampRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z:/;
|
|
838
|
+
if (isoTimestampRegex.test(cleanPrompt)) {
|
|
839
|
+
const colonIndex = cleanPrompt.indexOf(':', 19);
|
|
840
|
+
cleanPrompt = cleanPrompt.substring(colonIndex + 1);
|
|
841
|
+
} else if (/^\d+:/.test(cleanPrompt)) {
|
|
842
|
+
const colonIndex = cleanPrompt.indexOf(':');
|
|
843
|
+
cleanPrompt = cleanPrompt.substring(colonIndex + 1);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return cleanPrompt.trim();
|
|
847
|
+
}, []);
|
|
848
|
+
|
|
849
|
+
// Load a specific conversation's transcript (or switch to it if already active)
|
|
850
|
+
const loadConversationTranscript = useCallback(async (conversationId: string, agentIdForConversation?: string, title?: string) => {
|
|
851
|
+
// Check if conversation is already in activeConversations (use ref to avoid dependency)
|
|
852
|
+
const existingActive = activeConversationsRef.current.get(conversationId);
|
|
853
|
+
if (existingActive) {
|
|
854
|
+
// Just switch to it, no API call needed
|
|
855
|
+
setCurrentConversationId(conversationId);
|
|
856
|
+
if (onConversationChange) {
|
|
857
|
+
onConversationChange(conversationId);
|
|
858
|
+
}
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Use the conversation's agentId if available, otherwise fall back to currentAgentId
|
|
863
|
+
const agentIdToUse = agentIdForConversation || currentAgentId;
|
|
864
|
+
const agentProfile = getAgent(agentIdToUse);
|
|
865
|
+
const projectId = agentProfile?.metadata?.projectId;
|
|
866
|
+
|
|
867
|
+
if (!apiKey || !projectId) {
|
|
868
|
+
setConversationsError('Missing API key or project ID');
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Set loading state
|
|
873
|
+
setLoadingConversationId(conversationId);
|
|
874
|
+
|
|
875
|
+
try {
|
|
876
|
+
console.log('loadConversationTranscript - conversationId:', conversationId);
|
|
877
|
+
const url = `https://api.llmasaservice.io/conversations/${conversationId}/calls`;
|
|
878
|
+
console.log('loadConversationTranscript - URL:', url);
|
|
879
|
+
|
|
880
|
+
const response = await fetch(url, {
|
|
881
|
+
headers: {
|
|
882
|
+
'x-api-key': apiKey,
|
|
883
|
+
'x-project-id': projectId,
|
|
884
|
+
},
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
if (!response.ok) {
|
|
888
|
+
throw new Error(`Failed to load conversation (${response.status})`);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const payload = await response.json();
|
|
892
|
+
console.log('loadConversationTranscript - API response:', payload);
|
|
893
|
+
|
|
894
|
+
// The /calls endpoint returns an array of calls
|
|
895
|
+
// Build history from all calls in chronological order
|
|
896
|
+
// IMPORTANT: Strip context/template data from prompts to prevent token bloat
|
|
897
|
+
const history: Record<string, { content: string; callId: string }> = {};
|
|
898
|
+
let firstPrompt: string | null = null;
|
|
899
|
+
|
|
900
|
+
if (Array.isArray(payload) && payload.length > 0) {
|
|
901
|
+
// Parse all calls in order
|
|
902
|
+
payload.forEach((call: any, index: number) => {
|
|
903
|
+
if (call.prompt && call.response) {
|
|
904
|
+
// Get the raw prompt text
|
|
905
|
+
const rawPrompt = typeof call.prompt === 'string'
|
|
906
|
+
? call.prompt
|
|
907
|
+
: call.prompt[0]?.text || '';
|
|
908
|
+
|
|
909
|
+
// Strip context/template data - keep only the user's actual message
|
|
910
|
+
const cleanedPrompt = stripContextFromPrompt(rawPrompt);
|
|
911
|
+
|
|
912
|
+
// Extract first prompt for display in list
|
|
913
|
+
if (index === 0 && cleanedPrompt) {
|
|
914
|
+
firstPrompt = cleanedPrompt.length > 60 ? cleanedPrompt.slice(0, 57) + '...' : cleanedPrompt;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Use timestamp-prefixed key to maintain order and uniqueness
|
|
918
|
+
const timestamp = call.createdAt || call.created_at || new Date().toISOString();
|
|
919
|
+
const historyKey = `${timestamp}:${cleanedPrompt}`;
|
|
920
|
+
|
|
921
|
+
history[historyKey] = {
|
|
922
|
+
content: call.response,
|
|
923
|
+
callId: call.id || '',
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
console.log('loadConversationTranscript - parsed history:', history);
|
|
930
|
+
|
|
931
|
+
// Update first prompt in state if we found one
|
|
932
|
+
if (firstPrompt) {
|
|
933
|
+
setConversationFirstPrompts(prev => ({
|
|
934
|
+
...prev,
|
|
935
|
+
[conversationId]: firstPrompt!,
|
|
936
|
+
}));
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Add to active conversations
|
|
940
|
+
const conversationTitle = title || firstPrompt || 'Conversation';
|
|
941
|
+
setActiveConversations(prev => {
|
|
942
|
+
const next = new Map(prev);
|
|
943
|
+
next.set(conversationId, {
|
|
944
|
+
conversationId,
|
|
945
|
+
agentId: agentIdToUse,
|
|
946
|
+
history,
|
|
947
|
+
isLoading: false,
|
|
948
|
+
title: conversationTitle,
|
|
949
|
+
});
|
|
950
|
+
return next;
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
setCurrentConversationId(conversationId);
|
|
954
|
+
|
|
955
|
+
if (onConversationChange) {
|
|
956
|
+
onConversationChange(conversationId);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Clear loading state on success
|
|
960
|
+
setLoadingConversationId(null);
|
|
961
|
+
} catch (error: any) {
|
|
962
|
+
console.error('Failed to load conversation:', error);
|
|
963
|
+
setConversationsError(error.message || 'Failed to load conversation');
|
|
964
|
+
|
|
965
|
+
// Clear loading state on error
|
|
966
|
+
setLoadingConversationId(null);
|
|
967
|
+
}
|
|
968
|
+
}, [apiKey, currentAgentId, getAgent, onConversationChange, stripContextFromPrompt]);
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
// Refresh conversations callback
|
|
972
|
+
const handleRefreshConversations = useCallback(() => {
|
|
973
|
+
fetchConversations(currentAgentId);
|
|
974
|
+
}, [currentAgentId, fetchConversations]);
|
|
975
|
+
|
|
976
|
+
// Fetch conversations on mount and when agent changes
|
|
977
|
+
useEffect(() => {
|
|
978
|
+
// Only fetch if agents are loaded and we have necessary data
|
|
979
|
+
if (!agentsLoading && currentAgentId && apiKey) {
|
|
980
|
+
// Check if agent is ready (has projectId)
|
|
981
|
+
const agentProfile = getAgent(currentAgentId);
|
|
982
|
+
const projectId = agentProfile?.metadata?.projectId;
|
|
983
|
+
|
|
984
|
+
// Only fetch once per agent, and only if agent is ready
|
|
985
|
+
if (projectId && lastFetchedAgentRef.current !== currentAgentId) {
|
|
986
|
+
lastFetchedAgentRef.current = currentAgentId;
|
|
987
|
+
fetchConversations(currentAgentId);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}, [agentsLoading, currentAgentId, apiKey, fetchConversations, getAgent]);
|
|
991
|
+
|
|
992
|
+
// Start new conversation
|
|
993
|
+
const handleNewConversation = useCallback(() => {
|
|
994
|
+
// Generate a temporary ID for the new conversation
|
|
995
|
+
const tempId = `new-${Date.now()}`;
|
|
996
|
+
|
|
997
|
+
// Add to active conversations with empty history
|
|
998
|
+
setActiveConversations(prev => {
|
|
999
|
+
const next = new Map(prev);
|
|
1000
|
+
next.set(tempId, {
|
|
1001
|
+
conversationId: tempId,
|
|
1002
|
+
agentId: currentAgentId,
|
|
1003
|
+
history: {},
|
|
1004
|
+
isLoading: false,
|
|
1005
|
+
title: 'New conversation',
|
|
1006
|
+
});
|
|
1007
|
+
return next;
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
setCurrentConversationId(tempId);
|
|
1011
|
+
}, [currentAgentId]);
|
|
1012
|
+
|
|
1013
|
+
// Auto-start a new conversation when none exist and agent is ready
|
|
1014
|
+
useEffect(() => {
|
|
1015
|
+
const agentProfile = getAgent(currentAgentId);
|
|
1016
|
+
const isAgentReady = agentProfile?.metadata?.projectId;
|
|
1017
|
+
|
|
1018
|
+
if (isAgentReady && !agentsLoading && activeConversations.size === 0) {
|
|
1019
|
+
handleNewConversation();
|
|
1020
|
+
}
|
|
1021
|
+
}, [currentAgentId, agentsLoading, activeConversations.size, getAgent, handleNewConversation]);
|
|
1022
|
+
|
|
1023
|
+
// Close an active conversation
|
|
1024
|
+
const handleCloseConversation = useCallback((conversationId: string, e?: React.MouseEvent) => {
|
|
1025
|
+
if (e) {
|
|
1026
|
+
e.stopPropagation();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
setActiveConversations(prev => {
|
|
1030
|
+
const next = new Map(prev);
|
|
1031
|
+
next.delete(conversationId);
|
|
1032
|
+
return next;
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
// If closing the current conversation, switch to another active one or null
|
|
1036
|
+
if (currentConversationIdRef.current === conversationId) {
|
|
1037
|
+
const remaining = Array.from(activeConversationsRef.current.keys()).filter(id => id !== conversationId);
|
|
1038
|
+
const nextId = remaining.length > 0 ? remaining[0] ?? null : null;
|
|
1039
|
+
setCurrentConversationId(nextId);
|
|
1040
|
+
}
|
|
1041
|
+
}, []);
|
|
1042
|
+
|
|
1043
|
+
// Select conversation
|
|
1044
|
+
const handleSelectConversation = useCallback((conversationId: string) => {
|
|
1045
|
+
loadConversationTranscript(conversationId);
|
|
1046
|
+
}, [loadConversationTranscript]);
|
|
1047
|
+
|
|
1048
|
+
// Toggle section expansion
|
|
1049
|
+
const toggleSection = useCallback((sectionLabel: string) => {
|
|
1050
|
+
setExpandedSections(prev => {
|
|
1051
|
+
const isExpanding = !prev[sectionLabel];
|
|
1052
|
+
|
|
1053
|
+
// If expanding, fetch first prompts for conversations in this section
|
|
1054
|
+
if (isExpanding) {
|
|
1055
|
+
// Use a ref to get current conversations to avoid dependency issues
|
|
1056
|
+
const conversationsToCheck = apiConversations;
|
|
1057
|
+
const now = new Date();
|
|
1058
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1059
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
1060
|
+
const thisWeek = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
1061
|
+
const thisMonth = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
1062
|
+
|
|
1063
|
+
// Fetch prompts for conversations in this section
|
|
1064
|
+
conversationsToCheck.forEach(conv => {
|
|
1065
|
+
const date = new Date(conv.updatedAt);
|
|
1066
|
+
let shouldFetch = false;
|
|
1067
|
+
|
|
1068
|
+
if (sectionLabel === 'Today' && date >= today) {
|
|
1069
|
+
shouldFetch = true;
|
|
1070
|
+
} else if (sectionLabel === 'Yesterday' && date >= yesterday && date < today) {
|
|
1071
|
+
shouldFetch = true;
|
|
1072
|
+
} else if (sectionLabel === 'This Week' && date >= thisWeek && date < yesterday) {
|
|
1073
|
+
shouldFetch = true;
|
|
1074
|
+
} else if (sectionLabel === 'This Month' && date >= thisMonth && date < thisWeek) {
|
|
1075
|
+
shouldFetch = true;
|
|
1076
|
+
} else if (sectionLabel === 'Older' && date < thisMonth) {
|
|
1077
|
+
shouldFetch = true;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (shouldFetch && !checkedPromptsRef.current.has(conv.conversationId)) {
|
|
1081
|
+
fetchFirstPrompt(conv.conversationId, conv.agentId);
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return {
|
|
1087
|
+
...prev,
|
|
1088
|
+
[sectionLabel]: isExpanding,
|
|
1089
|
+
};
|
|
1090
|
+
});
|
|
1091
|
+
}, [apiConversations, fetchFirstPrompt]);
|
|
1092
|
+
|
|
1093
|
+
// Group conversations
|
|
1094
|
+
const groupedConversations = useMemo(() => {
|
|
1095
|
+
let filtered = apiConversations;
|
|
1096
|
+
|
|
1097
|
+
// Filter by search query
|
|
1098
|
+
if (searchQuery) {
|
|
1099
|
+
const query = searchQuery.toLowerCase();
|
|
1100
|
+
filtered = filtered.filter(conv =>
|
|
1101
|
+
conv.title?.toLowerCase().includes(query) ||
|
|
1102
|
+
conv.summary?.toLowerCase().includes(query)
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Group all conversations, show all groups even if empty
|
|
1107
|
+
return groupConversationsByTime(filtered, true);
|
|
1108
|
+
}, [apiConversations, searchQuery]);
|
|
1109
|
+
|
|
1110
|
+
// Build effective customer object with required customerId
|
|
1111
|
+
const effectiveCustomer = useMemo(() => {
|
|
1112
|
+
return {
|
|
1113
|
+
...customer,
|
|
1114
|
+
customer_id: customerId,
|
|
1115
|
+
};
|
|
1116
|
+
}, [customer, customerId]);
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
// Resize start handler - attaches listeners directly
|
|
1120
|
+
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
|
1121
|
+
e.preventDefault();
|
|
1122
|
+
setIsResizing(true);
|
|
1123
|
+
|
|
1124
|
+
let currentWidth = panelWidth;
|
|
1125
|
+
|
|
1126
|
+
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
1127
|
+
const newWidth = position === 'right'
|
|
1128
|
+
? window.innerWidth - moveEvent.clientX
|
|
1129
|
+
: moveEvent.clientX;
|
|
1130
|
+
|
|
1131
|
+
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
|
1132
|
+
currentWidth = newWidth;
|
|
1133
|
+
setPanelWidth(newWidth);
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const handleMouseUp = () => {
|
|
1138
|
+
setIsResizing(false);
|
|
1139
|
+
localStorage.setItem('ai-agent-panel-width', currentWidth.toString());
|
|
1140
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
1141
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
1145
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
1146
|
+
}, [panelWidth, minWidth, maxWidth, position]);
|
|
1147
|
+
|
|
1148
|
+
// Get current agent profile
|
|
1149
|
+
const currentAgentProfile = useMemo(() => {
|
|
1150
|
+
return getAgent(currentAgentId);
|
|
1151
|
+
}, [currentAgentId, getAgent]);
|
|
1152
|
+
|
|
1153
|
+
const currentAgentMetadata = currentAgentProfile?.metadata;
|
|
1154
|
+
|
|
1155
|
+
// Get current active conversation
|
|
1156
|
+
const currentActiveConversation = useMemo(() => {
|
|
1157
|
+
if (!currentConversationId) return null;
|
|
1158
|
+
return activeConversations.get(currentConversationId) || null;
|
|
1159
|
+
}, [currentConversationId, activeConversations]);
|
|
1160
|
+
|
|
1161
|
+
// Get list of active conversations sorted by most recent
|
|
1162
|
+
const activeConversationsList = useMemo(() => {
|
|
1163
|
+
return Array.from(activeConversations.values());
|
|
1164
|
+
}, [activeConversations]);
|
|
1165
|
+
|
|
1166
|
+
// Build agent select options
|
|
1167
|
+
const agentOptions: AgentOption[] = useMemo(() => {
|
|
1168
|
+
return agentList.map((agent) => ({
|
|
1169
|
+
value: agent.id,
|
|
1170
|
+
label: agent.name,
|
|
1171
|
+
description: agent.description,
|
|
1172
|
+
avatarUrl: agent.avatarUrl,
|
|
1173
|
+
}));
|
|
1174
|
+
}, [agentList]);
|
|
1175
|
+
|
|
1176
|
+
// Merge context from all sources
|
|
1177
|
+
const mergedContext = useMemo(() => {
|
|
1178
|
+
const sections: ContextSection[] = [
|
|
1179
|
+
...sharedContextSections,
|
|
1180
|
+
...pageContextSections,
|
|
1181
|
+
...(context?.sections || []),
|
|
1182
|
+
];
|
|
1183
|
+
|
|
1184
|
+
// Add context data sources
|
|
1185
|
+
if (contextDataSources) {
|
|
1186
|
+
Object.entries(contextDataSources).forEach(([key, value]) => {
|
|
1187
|
+
sections.push({
|
|
1188
|
+
id: key,
|
|
1189
|
+
title: key,
|
|
1190
|
+
data: typeof value === 'object' ? (value as Record<string, unknown>) : { value },
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const totalTokens = sections.reduce((sum, s) => sum + (s.tokens || 0), 0);
|
|
1196
|
+
|
|
1197
|
+
return {
|
|
1198
|
+
route: context?.route,
|
|
1199
|
+
sections,
|
|
1200
|
+
totalTokens,
|
|
1201
|
+
};
|
|
1202
|
+
}, [context, sharedContextSections, pageContextSections, contextDataSources]);
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
// Build data array for ChatPanel
|
|
1206
|
+
const chatPanelData = useMemo(() => {
|
|
1207
|
+
const contextData = mergedContext.sections.map((section) => ({
|
|
1208
|
+
key: section.id,
|
|
1209
|
+
data: JSON.stringify(section.data),
|
|
1210
|
+
}));
|
|
1211
|
+
|
|
1212
|
+
// Add agent awareness instructions (only if agent suggestions are enabled)
|
|
1213
|
+
if (enableAgentSuggestions) {
|
|
1214
|
+
const awarenessInstructions = buildAgentAwarenessInstructions(currentAgentId);
|
|
1215
|
+
if (awarenessInstructions) {
|
|
1216
|
+
contextData.push({
|
|
1217
|
+
key: 'agent_awareness',
|
|
1218
|
+
data: awarenessInstructions,
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
return [...data, ...contextData];
|
|
1224
|
+
}, [data, mergedContext.sections, buildAgentAwarenessInstructions, currentAgentId, enableAgentSuggestions]);
|
|
1225
|
+
|
|
1226
|
+
// Handle agent switch - updates the agent for the current conversation without starting a new one
|
|
1227
|
+
const handleAgentSwitch = useCallback(
|
|
1228
|
+
(newAgentId: string) => {
|
|
1229
|
+
const oldAgentId = currentAgentId;
|
|
1230
|
+
setCurrentAgentId(newAgentId);
|
|
1231
|
+
|
|
1232
|
+
if (onAgentSwitch) {
|
|
1233
|
+
onAgentSwitch(oldAgentId, newAgentId);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Update the current conversation's agent ID (don't start a new conversation)
|
|
1237
|
+
if (currentConversationIdRef.current) {
|
|
1238
|
+
setActiveConversations(prev => {
|
|
1239
|
+
const existing = prev.get(currentConversationIdRef.current!);
|
|
1240
|
+
if (existing) {
|
|
1241
|
+
const next = new Map(prev);
|
|
1242
|
+
next.set(currentConversationIdRef.current!, {
|
|
1243
|
+
...existing,
|
|
1244
|
+
agentId: newAgentId,
|
|
1245
|
+
});
|
|
1246
|
+
return next;
|
|
1247
|
+
}
|
|
1248
|
+
return prev;
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
[currentAgentId, onAgentSwitch]
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
// Handle conversation select from sidebar
|
|
1256
|
+
const handleConversationSelect = useCallback(
|
|
1257
|
+
(conv: APIConversationSummary) => {
|
|
1258
|
+
// Determine which agent to use - prefer conversation's agent, but fall back if not available
|
|
1259
|
+
let agentIdToUse = conv.agentId || currentAgentId;
|
|
1260
|
+
|
|
1261
|
+
// Check if the conversation's agent exists
|
|
1262
|
+
const targetAgent = getAgent(agentIdToUse);
|
|
1263
|
+
if (!targetAgent && conv.agentId) {
|
|
1264
|
+
// Conversation's agent not found, fall back to current agent
|
|
1265
|
+
console.warn('Agent not found for conversation:', conv.agentId, 'Falling back to current agent:', currentAgentId);
|
|
1266
|
+
agentIdToUse = currentAgentId;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Final check - ensure we have a valid agent
|
|
1270
|
+
const finalAgent = getAgent(agentIdToUse);
|
|
1271
|
+
if (!finalAgent) {
|
|
1272
|
+
console.error('No valid agent available. Current:', currentAgentId, 'Available agents:', agentList.map(a => a.id));
|
|
1273
|
+
setConversationsError('No agent available');
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Get title from first prompts or conv title
|
|
1278
|
+
const title = conversationFirstPrompts[conv.conversationId] || conv.title || 'Conversation';
|
|
1279
|
+
|
|
1280
|
+
// Switch to the agent for this conversation if different
|
|
1281
|
+
if (agentIdToUse !== currentAgentId) {
|
|
1282
|
+
setCurrentAgentId(agentIdToUse);
|
|
1283
|
+
// Wait for state update, then load transcript
|
|
1284
|
+
setTimeout(() => {
|
|
1285
|
+
loadConversationTranscript(conv.conversationId, agentIdToUse, title);
|
|
1286
|
+
}, 0);
|
|
1287
|
+
} else {
|
|
1288
|
+
// Same agent, load immediately
|
|
1289
|
+
loadConversationTranscript(conv.conversationId, agentIdToUse, title);
|
|
1290
|
+
}
|
|
1291
|
+
},
|
|
1292
|
+
[currentAgentId, loadConversationTranscript, getAgent, agentList, conversationFirstPrompts]
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
// Handle history changes from ChatPanel
|
|
1296
|
+
const handleHistoryChanged = useCallback(
|
|
1297
|
+
(history: Record<string, { content: string; callId: string }>, conversationId?: string) => {
|
|
1298
|
+
const targetConversationId = conversationId || currentConversationIdRef.current;
|
|
1299
|
+
|
|
1300
|
+
// Update active conversation history
|
|
1301
|
+
if (targetConversationId) {
|
|
1302
|
+
setActiveConversations(prev => {
|
|
1303
|
+
const existing = prev.get(targetConversationId);
|
|
1304
|
+
if (existing) {
|
|
1305
|
+
const next = new Map(prev);
|
|
1306
|
+
// Derive title from first prompt if conversation is new
|
|
1307
|
+
let title = existing.title;
|
|
1308
|
+
if (title === 'New conversation' && Object.keys(history).length > 0) {
|
|
1309
|
+
const firstPrompt = Object.keys(history)[0];
|
|
1310
|
+
if (firstPrompt) {
|
|
1311
|
+
// Strip timestamp prefix
|
|
1312
|
+
let cleanPrompt = firstPrompt;
|
|
1313
|
+
const isoMatch = cleanPrompt.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z:(.+)/);
|
|
1314
|
+
if (isoMatch && isoMatch[1]) {
|
|
1315
|
+
cleanPrompt = isoMatch[1];
|
|
1316
|
+
}
|
|
1317
|
+
title = cleanPrompt.length > 60 ? cleanPrompt.slice(0, 57) + '...' : cleanPrompt;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
next.set(targetConversationId, {
|
|
1321
|
+
...existing,
|
|
1322
|
+
history,
|
|
1323
|
+
title,
|
|
1324
|
+
});
|
|
1325
|
+
return next;
|
|
1326
|
+
}
|
|
1327
|
+
return prev;
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Check for agent handoff suggestion
|
|
1332
|
+
const lastEntry = Object.entries(history).pop();
|
|
1333
|
+
if (lastEntry) {
|
|
1334
|
+
const content = lastEntry[1].content;
|
|
1335
|
+
const handoffMatch = content.match(/\[SUGGEST_AGENT:([^\]]+)\]/);
|
|
1336
|
+
if (handoffMatch) {
|
|
1337
|
+
const suggestedId = handoffMatch[1];
|
|
1338
|
+
if (suggestedId && suggestedId !== currentAgentId && agents.includes(suggestedId)) {
|
|
1339
|
+
setSuggestedAgent(suggestedId);
|
|
1340
|
+
setShowHandoffDialog(true);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (historyChangedCallback) {
|
|
1346
|
+
historyChangedCallback(history);
|
|
1347
|
+
}
|
|
1348
|
+
},
|
|
1349
|
+
[
|
|
1350
|
+
currentAgentId,
|
|
1351
|
+
historyChangedCallback,
|
|
1352
|
+
agents,
|
|
1353
|
+
]
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
// Handle loading state changes from ChatPanel
|
|
1357
|
+
const handleLoadingChange = useCallback((isLoading: boolean, conversationId?: string) => {
|
|
1358
|
+
const targetConversationId = conversationId || currentConversationIdRef.current;
|
|
1359
|
+
|
|
1360
|
+
if (targetConversationId) {
|
|
1361
|
+
setActiveConversations(prev => {
|
|
1362
|
+
const existing = prev.get(targetConversationId);
|
|
1363
|
+
if (existing && existing.isLoading !== isLoading) {
|
|
1364
|
+
const next = new Map(prev);
|
|
1365
|
+
next.set(targetConversationId, {
|
|
1366
|
+
...existing,
|
|
1367
|
+
isLoading,
|
|
1368
|
+
});
|
|
1369
|
+
return next;
|
|
1370
|
+
}
|
|
1371
|
+
return prev;
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
}, []);
|
|
1375
|
+
|
|
1376
|
+
// Handle handoff confirmation
|
|
1377
|
+
const handleHandoffConfirm = useCallback(() => {
|
|
1378
|
+
if (suggestedAgent) {
|
|
1379
|
+
handleAgentSwitch(suggestedAgent);
|
|
1380
|
+
}
|
|
1381
|
+
setShowHandoffDialog(false);
|
|
1382
|
+
setSuggestedAgent(null);
|
|
1383
|
+
}, [suggestedAgent, handleAgentSwitch]);
|
|
1384
|
+
|
|
1385
|
+
const handleHandoffCancel = useCallback(() => {
|
|
1386
|
+
setShowHandoffDialog(false);
|
|
1387
|
+
setSuggestedAgent(null);
|
|
1388
|
+
}, []);
|
|
1389
|
+
|
|
1390
|
+
// Toggle collapse
|
|
1391
|
+
const toggleCollapse = useCallback(() => {
|
|
1392
|
+
setIsCollapsed((prev) => !prev);
|
|
1393
|
+
}, []);
|
|
1394
|
+
|
|
1395
|
+
// Toggle history collapse
|
|
1396
|
+
const toggleHistoryCollapse = useCallback(() => {
|
|
1397
|
+
setIsHistoryCollapsed((prev) => {
|
|
1398
|
+
const next = !prev;
|
|
1399
|
+
localStorage.setItem('ai-agent-panel-history-collapsed', String(next));
|
|
1400
|
+
return next;
|
|
1401
|
+
});
|
|
1402
|
+
}, []);
|
|
1403
|
+
|
|
1404
|
+
// Panel classes
|
|
1405
|
+
const panelClasses = [
|
|
1406
|
+
'ai-agent-panel',
|
|
1407
|
+
theme === 'dark' ? 'dark-theme' : '',
|
|
1408
|
+
isCollapsed ? 'ai-agent-panel--collapsed' : '',
|
|
1409
|
+
position === 'left' ? 'ai-agent-panel--left' : 'ai-agent-panel--right',
|
|
1410
|
+
sidebarPosition === 'right' ? 'ai-agent-panel--sidebar-right' : 'ai-agent-panel--sidebar-left',
|
|
1411
|
+
].filter(Boolean).join(' ');
|
|
1412
|
+
|
|
1413
|
+
// Collapsed view
|
|
1414
|
+
if (isCollapsed) {
|
|
1415
|
+
return (
|
|
1416
|
+
<div className={panelClasses} ref={panelRef}>
|
|
1417
|
+
<div
|
|
1418
|
+
className="ai-agent-panel__collapsed-bar"
|
|
1419
|
+
onClick={(e) => {
|
|
1420
|
+
// Only expand if clicking on the bar itself, not on buttons
|
|
1421
|
+
if (e.target === e.currentTarget) {
|
|
1422
|
+
toggleCollapse();
|
|
1423
|
+
}
|
|
1424
|
+
}}
|
|
1425
|
+
title="Click to expand"
|
|
1426
|
+
>
|
|
1427
|
+
{/* Expand button at top for discoverability */}
|
|
1428
|
+
<Tooltip content="Expand Panel" side="left">
|
|
1429
|
+
<Button variant="ghost" size="icon" onClick={toggleCollapse}>
|
|
1430
|
+
{position === 'right' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
|
|
1431
|
+
</Button>
|
|
1432
|
+
</Tooltip>
|
|
1433
|
+
|
|
1434
|
+
<div className="ai-agent-panel__collapsed-divider" />
|
|
1435
|
+
|
|
1436
|
+
<Tooltip content="Search" side="left">
|
|
1437
|
+
<Button
|
|
1438
|
+
variant="ghost"
|
|
1439
|
+
size="icon"
|
|
1440
|
+
onClick={() => {
|
|
1441
|
+
setIsCollapsed(false);
|
|
1442
|
+
setShowSearch(true);
|
|
1443
|
+
}}
|
|
1444
|
+
>
|
|
1445
|
+
<SearchIcon />
|
|
1446
|
+
</Button>
|
|
1447
|
+
</Tooltip>
|
|
1448
|
+
|
|
1449
|
+
<Tooltip content="New Chat" side="left">
|
|
1450
|
+
<Button
|
|
1451
|
+
variant="ghost"
|
|
1452
|
+
size="icon"
|
|
1453
|
+
onClick={() => {
|
|
1454
|
+
setIsCollapsed(false);
|
|
1455
|
+
handleNewConversation();
|
|
1456
|
+
}}
|
|
1457
|
+
>
|
|
1458
|
+
<PlusIcon />
|
|
1459
|
+
</Button>
|
|
1460
|
+
</Tooltip>
|
|
1461
|
+
|
|
1462
|
+
<div className="ai-agent-panel__collapsed-divider" />
|
|
1463
|
+
|
|
1464
|
+
{agentList.map((agent) => {
|
|
1465
|
+
// Check if this agent has an active conversation
|
|
1466
|
+
const activeConvForAgent = activeConversationsList.find(
|
|
1467
|
+
(conv) => conv.agentId === agent.id
|
|
1468
|
+
);
|
|
1469
|
+
const hasActiveConversation = !!activeConvForAgent;
|
|
1470
|
+
|
|
1471
|
+
return (
|
|
1472
|
+
<Tooltip key={agent.id} content={agent.name} side="left">
|
|
1473
|
+
<Button
|
|
1474
|
+
variant={agent.id === currentAgentId ? 'secondary' : 'ghost'}
|
|
1475
|
+
size="icon"
|
|
1476
|
+
onClick={() => {
|
|
1477
|
+
setIsCollapsed(false);
|
|
1478
|
+
if (hasActiveConversation && activeConvForAgent) {
|
|
1479
|
+
// Switch to the existing conversation
|
|
1480
|
+
setCurrentConversationId(activeConvForAgent.conversationId);
|
|
1481
|
+
setCurrentAgentId(agent.id);
|
|
1482
|
+
if (onConversationChange) {
|
|
1483
|
+
onConversationChange(activeConvForAgent.conversationId);
|
|
1484
|
+
}
|
|
1485
|
+
} else {
|
|
1486
|
+
// Start a new conversation with this agent
|
|
1487
|
+
const tempId = `new-${Date.now()}`;
|
|
1488
|
+
setActiveConversations(prev => {
|
|
1489
|
+
const next = new Map(prev);
|
|
1490
|
+
next.set(tempId, {
|
|
1491
|
+
conversationId: tempId,
|
|
1492
|
+
agentId: agent.id,
|
|
1493
|
+
history: {},
|
|
1494
|
+
isLoading: false,
|
|
1495
|
+
title: 'New conversation',
|
|
1496
|
+
});
|
|
1497
|
+
return next;
|
|
1498
|
+
});
|
|
1499
|
+
setCurrentConversationId(tempId);
|
|
1500
|
+
setCurrentAgentId(agent.id);
|
|
1501
|
+
}
|
|
1502
|
+
}}
|
|
1503
|
+
className={`ai-agent-panel__agent-icon ${hasActiveConversation ? 'ai-agent-panel__agent-icon--active' : ''}`}
|
|
1504
|
+
>
|
|
1505
|
+
{agent.avatarUrl ? (
|
|
1506
|
+
<img
|
|
1507
|
+
src={agent.avatarUrl}
|
|
1508
|
+
alt={agent.name}
|
|
1509
|
+
className="ai-agent-panel__agent-avatar"
|
|
1510
|
+
/>
|
|
1511
|
+
) : (
|
|
1512
|
+
agent.name.charAt(0).toUpperCase()
|
|
1513
|
+
)}
|
|
1514
|
+
{hasActiveConversation && (
|
|
1515
|
+
<span className="ai-agent-panel__agent-active-indicator" />
|
|
1516
|
+
)}
|
|
1517
|
+
</Button>
|
|
1518
|
+
</Tooltip>
|
|
1519
|
+
);
|
|
1520
|
+
})}
|
|
1521
|
+
|
|
1522
|
+
<div className="ai-agent-panel__collapsed-spacer" />
|
|
1523
|
+
</div>
|
|
1524
|
+
</div>
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Expanded view
|
|
1529
|
+
return (
|
|
1530
|
+
<div
|
|
1531
|
+
className={panelClasses}
|
|
1532
|
+
ref={panelRef}
|
|
1533
|
+
style={{ width: `${panelWidth}px` }}
|
|
1534
|
+
>
|
|
1535
|
+
{/* Resize handle */}
|
|
1536
|
+
<div
|
|
1537
|
+
ref={resizeRef}
|
|
1538
|
+
className={`ai-agent-panel__resize-handle ai-agent-panel__resize-handle--${position}`}
|
|
1539
|
+
onMouseDown={handleResizeStart}
|
|
1540
|
+
role="separator"
|
|
1541
|
+
aria-orientation="vertical"
|
|
1542
|
+
aria-label="Resize panel"
|
|
1543
|
+
tabIndex={0}
|
|
1544
|
+
/>
|
|
1545
|
+
|
|
1546
|
+
{/* Sidebar */}
|
|
1547
|
+
<div className={`ai-agent-panel__sidebar ${isHistoryCollapsed ? 'ai-agent-panel__sidebar--collapsed' : ''}`}>
|
|
1548
|
+
{isHistoryCollapsed ? (
|
|
1549
|
+
// Collapsed history bar
|
|
1550
|
+
<div
|
|
1551
|
+
className="ai-agent-panel__history-collapsed-bar"
|
|
1552
|
+
onClick={(e) => {
|
|
1553
|
+
// Only expand if clicking on the bar itself, not on buttons
|
|
1554
|
+
if (e.target === e.currentTarget) {
|
|
1555
|
+
toggleHistoryCollapse();
|
|
1556
|
+
}
|
|
1557
|
+
}}
|
|
1558
|
+
title="Click to expand history"
|
|
1559
|
+
>
|
|
1560
|
+
{/* Expand button at top for discoverability */}
|
|
1561
|
+
<Tooltip content="Expand History" side={sidebarPosition === 'left' ? 'right' : 'left'}>
|
|
1562
|
+
<Button
|
|
1563
|
+
variant="ghost"
|
|
1564
|
+
size="icon"
|
|
1565
|
+
onClick={toggleHistoryCollapse}
|
|
1566
|
+
>
|
|
1567
|
+
<ChevronRightIcon />
|
|
1568
|
+
</Button>
|
|
1569
|
+
</Tooltip>
|
|
1570
|
+
|
|
1571
|
+
<div className="ai-agent-panel__collapsed-divider" />
|
|
1572
|
+
|
|
1573
|
+
<Tooltip content="New Chat" side={sidebarPosition === 'left' ? 'right' : 'left'}>
|
|
1574
|
+
<Button
|
|
1575
|
+
variant="ghost"
|
|
1576
|
+
size="icon"
|
|
1577
|
+
onClick={handleNewConversation}
|
|
1578
|
+
>
|
|
1579
|
+
<PlusIcon />
|
|
1580
|
+
</Button>
|
|
1581
|
+
</Tooltip>
|
|
1582
|
+
|
|
1583
|
+
<div className="ai-agent-panel__collapsed-divider" />
|
|
1584
|
+
|
|
1585
|
+
{agentList.map((agent) => {
|
|
1586
|
+
// Check if this agent has an active conversation
|
|
1587
|
+
const activeConvForAgent = activeConversationsList.find(
|
|
1588
|
+
(conv) => conv.agentId === agent.id
|
|
1589
|
+
);
|
|
1590
|
+
const hasActiveConversation = !!activeConvForAgent;
|
|
1591
|
+
|
|
1592
|
+
return (
|
|
1593
|
+
<Tooltip key={agent.id} content={agent.name} side={sidebarPosition === 'left' ? 'right' : 'left'}>
|
|
1594
|
+
<Button
|
|
1595
|
+
variant={agent.id === currentAgentId ? 'secondary' : 'ghost'}
|
|
1596
|
+
size="icon"
|
|
1597
|
+
onClick={() => {
|
|
1598
|
+
if (hasActiveConversation && activeConvForAgent) {
|
|
1599
|
+
// Switch to the existing conversation
|
|
1600
|
+
setCurrentConversationId(activeConvForAgent.conversationId);
|
|
1601
|
+
setCurrentAgentId(agent.id);
|
|
1602
|
+
if (onConversationChange) {
|
|
1603
|
+
onConversationChange(activeConvForAgent.conversationId);
|
|
1604
|
+
}
|
|
1605
|
+
} else {
|
|
1606
|
+
// Start a new conversation with this agent
|
|
1607
|
+
const tempId = `new-${Date.now()}`;
|
|
1608
|
+
setActiveConversations(prev => {
|
|
1609
|
+
const next = new Map(prev);
|
|
1610
|
+
next.set(tempId, {
|
|
1611
|
+
conversationId: tempId,
|
|
1612
|
+
agentId: agent.id,
|
|
1613
|
+
history: {},
|
|
1614
|
+
isLoading: false,
|
|
1615
|
+
title: 'New conversation',
|
|
1616
|
+
});
|
|
1617
|
+
return next;
|
|
1618
|
+
});
|
|
1619
|
+
setCurrentConversationId(tempId);
|
|
1620
|
+
setCurrentAgentId(agent.id);
|
|
1621
|
+
}
|
|
1622
|
+
}}
|
|
1623
|
+
className={`ai-agent-panel__agent-icon ${hasActiveConversation ? 'ai-agent-panel__agent-icon--active' : ''}`}
|
|
1624
|
+
>
|
|
1625
|
+
{agent.avatarUrl ? (
|
|
1626
|
+
<img
|
|
1627
|
+
src={agent.avatarUrl}
|
|
1628
|
+
alt={agent.name}
|
|
1629
|
+
className="ai-agent-panel__agent-avatar"
|
|
1630
|
+
/>
|
|
1631
|
+
) : (
|
|
1632
|
+
agent.name.charAt(0).toUpperCase()
|
|
1633
|
+
)}
|
|
1634
|
+
{hasActiveConversation && (
|
|
1635
|
+
<span className="ai-agent-panel__agent-active-indicator" />
|
|
1636
|
+
)}
|
|
1637
|
+
</Button>
|
|
1638
|
+
</Tooltip>
|
|
1639
|
+
);
|
|
1640
|
+
})}
|
|
1641
|
+
|
|
1642
|
+
<div className="ai-agent-panel__history-collapsed-spacer" />
|
|
1643
|
+
</div>
|
|
1644
|
+
) : (
|
|
1645
|
+
<>
|
|
1646
|
+
{/* Header */}
|
|
1647
|
+
<div className="ai-agent-panel__header">
|
|
1648
|
+
<div className="ai-agent-panel__header-actions">
|
|
1649
|
+
{showSearch ? (
|
|
1650
|
+
<Input
|
|
1651
|
+
placeholder="Search conversations..."
|
|
1652
|
+
value={searchQuery}
|
|
1653
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
1654
|
+
icon={<SearchIcon />}
|
|
1655
|
+
autoFocus
|
|
1656
|
+
onBlur={() => {
|
|
1657
|
+
if (!searchQuery) setShowSearch(false);
|
|
1658
|
+
}}
|
|
1659
|
+
/>
|
|
1660
|
+
) : (
|
|
1661
|
+
<>
|
|
1662
|
+
<Button
|
|
1663
|
+
variant="ghost"
|
|
1664
|
+
size="icon"
|
|
1665
|
+
onClick={() => setShowSearch(true)}
|
|
1666
|
+
>
|
|
1667
|
+
<SearchIcon />
|
|
1668
|
+
</Button>
|
|
1669
|
+
<Button
|
|
1670
|
+
variant="ghost"
|
|
1671
|
+
size="icon"
|
|
1672
|
+
onClick={handleNewConversation}
|
|
1673
|
+
>
|
|
1674
|
+
<PlusIcon />
|
|
1675
|
+
</Button>
|
|
1676
|
+
</>
|
|
1677
|
+
)}
|
|
1678
|
+
</div>
|
|
1679
|
+
<Tooltip content="Collapse History" side="bottom">
|
|
1680
|
+
<Button variant="ghost" size="icon" onClick={toggleHistoryCollapse}>
|
|
1681
|
+
<SidebarIcon />
|
|
1682
|
+
</Button>
|
|
1683
|
+
</Tooltip>
|
|
1684
|
+
<Button variant="ghost" size="icon" onClick={toggleCollapse}>
|
|
1685
|
+
{position === 'right' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
|
1686
|
+
</Button>
|
|
1687
|
+
</div>
|
|
1688
|
+
|
|
1689
|
+
{/* Conversation list */}
|
|
1690
|
+
<ScrollArea className="ai-agent-panel__conversations">
|
|
1691
|
+
{/* Active Conversations Section */}
|
|
1692
|
+
{activeConversationsList.length > 0 && (
|
|
1693
|
+
<div className="ai-agent-panel__group ai-agent-panel__group--active">
|
|
1694
|
+
<div className="ai-agent-panel__group-label">
|
|
1695
|
+
<span>Active ({activeConversationsList.length})</span>
|
|
1696
|
+
</div>
|
|
1697
|
+
{activeConversationsList.map((activeConv) => (
|
|
1698
|
+
<div
|
|
1699
|
+
key={activeConv.conversationId}
|
|
1700
|
+
className={`ai-agent-panel__conversation ai-agent-panel__conversation--active-item ${
|
|
1701
|
+
currentConversationId === activeConv.conversationId
|
|
1702
|
+
? 'ai-agent-panel__conversation--current'
|
|
1703
|
+
: ''
|
|
1704
|
+
}`}
|
|
1705
|
+
onClick={() => {
|
|
1706
|
+
setCurrentConversationId(activeConv.conversationId);
|
|
1707
|
+
if (onConversationChange) {
|
|
1708
|
+
onConversationChange(activeConv.conversationId);
|
|
1709
|
+
}
|
|
1710
|
+
}}
|
|
1711
|
+
>
|
|
1712
|
+
<div className="ai-agent-panel__conversation-content">
|
|
1713
|
+
<div className="ai-agent-panel__conversation-title">
|
|
1714
|
+
{activeConv.isLoading && <LoadingDotIcon />}
|
|
1715
|
+
<span>{activeConv.title}</span>
|
|
1716
|
+
</div>
|
|
1717
|
+
</div>
|
|
1718
|
+
<button
|
|
1719
|
+
className="ai-agent-panel__conversation-close"
|
|
1720
|
+
onClick={(e) => handleCloseConversation(activeConv.conversationId, e)}
|
|
1721
|
+
title="Close conversation"
|
|
1722
|
+
>
|
|
1723
|
+
<CloseIcon />
|
|
1724
|
+
</button>
|
|
1725
|
+
</div>
|
|
1726
|
+
))}
|
|
1727
|
+
</div>
|
|
1728
|
+
)}
|
|
1729
|
+
|
|
1730
|
+
{/* History Section */}
|
|
1731
|
+
{conversationsLoading ? (
|
|
1732
|
+
<div className="ai-agent-panel__loading">
|
|
1733
|
+
<div className="ai-agent-panel__loading-spinner" />
|
|
1734
|
+
<span>Loading conversations...</span>
|
|
1735
|
+
</div>
|
|
1736
|
+
) : conversationsError ? (
|
|
1737
|
+
<div className="ai-agent-panel__empty">
|
|
1738
|
+
<p>Error: {conversationsError}</p>
|
|
1739
|
+
<Button variant="secondary" size="sm" onClick={handleRefreshConversations}>
|
|
1740
|
+
Retry
|
|
1741
|
+
</Button>
|
|
1742
|
+
</div>
|
|
1743
|
+
) : groupedConversations.length === 0 && activeConversationsList.length === 0 ? (
|
|
1744
|
+
<div className="ai-agent-panel__empty">
|
|
1745
|
+
<MessageIcon />
|
|
1746
|
+
<p>No conversations yet</p>
|
|
1747
|
+
<p className="ai-agent-panel__empty-hint">
|
|
1748
|
+
Start chatting to create your first conversation
|
|
1749
|
+
</p>
|
|
1750
|
+
</div>
|
|
1751
|
+
) : (
|
|
1752
|
+
<>
|
|
1753
|
+
{activeConversationsList.length > 0 && groupedConversations.some(g => g.count > 0) && (
|
|
1754
|
+
<div className="ai-agent-panel__group-divider" />
|
|
1755
|
+
)}
|
|
1756
|
+
{groupedConversations.map((group) => (
|
|
1757
|
+
<div key={group.label} className="ai-agent-panel__group">
|
|
1758
|
+
<div
|
|
1759
|
+
className="ai-agent-panel__group-label ai-agent-panel__group-label--clickable"
|
|
1760
|
+
onClick={() => toggleSection(group.label)}
|
|
1761
|
+
>
|
|
1762
|
+
<span>{group.label} {group.count > 0 && `(${group.count})`}</span>
|
|
1763
|
+
<span className="ai-agent-panel__group-chevron">
|
|
1764
|
+
{expandedSections[group.label] ? '▼' : '▶'}
|
|
1765
|
+
</span>
|
|
1766
|
+
</div>
|
|
1767
|
+
{expandedSections[group.label] && group.conversations.length > 0 && group.conversations.map((conv) => {
|
|
1768
|
+
// Check if this conversation is already active
|
|
1769
|
+
const isActive = activeConversations.has(conv.conversationId);
|
|
1770
|
+
return (
|
|
1771
|
+
<div
|
|
1772
|
+
key={conv.conversationId}
|
|
1773
|
+
className={`ai-agent-panel__conversation ${
|
|
1774
|
+
currentConversationId === conv.conversationId
|
|
1775
|
+
? 'ai-agent-panel__conversation--current'
|
|
1776
|
+
: ''
|
|
1777
|
+
} ${isActive ? 'ai-agent-panel__conversation--in-active' : ''}`}
|
|
1778
|
+
onClick={() => handleConversationSelect(conv)}
|
|
1779
|
+
>
|
|
1780
|
+
<div className="ai-agent-panel__conversation-content">
|
|
1781
|
+
<div className="ai-agent-panel__conversation-title">
|
|
1782
|
+
{isActive && <span className="ai-agent-panel__active-badge">●</span>}
|
|
1783
|
+
{conversationFirstPrompts[conv.conversationId] || conv.title}
|
|
1784
|
+
</div>
|
|
1785
|
+
</div>
|
|
1786
|
+
</div>
|
|
1787
|
+
);
|
|
1788
|
+
})}
|
|
1789
|
+
</div>
|
|
1790
|
+
))}
|
|
1791
|
+
</>
|
|
1792
|
+
)}
|
|
1793
|
+
</ScrollArea>
|
|
1794
|
+
|
|
1795
|
+
</>
|
|
1796
|
+
)}
|
|
1797
|
+
</div>
|
|
1798
|
+
|
|
1799
|
+
{/* Chat area */}
|
|
1800
|
+
<div className="ai-agent-panel__chat">
|
|
1801
|
+
{/* Chat panels - one per active conversation, shown/hidden via CSS */}
|
|
1802
|
+
{activeConversationsList.map((activeConv) => (
|
|
1803
|
+
<ChatPanelWrapper
|
|
1804
|
+
key={`${activeConv.conversationId}-${activeConv.agentId}`}
|
|
1805
|
+
activeConv={activeConv}
|
|
1806
|
+
currentConversationId={currentConversationId}
|
|
1807
|
+
getAgent={getAgent}
|
|
1808
|
+
url={url}
|
|
1809
|
+
theme={theme}
|
|
1810
|
+
chatPanelData={chatPanelData}
|
|
1811
|
+
handleHistoryChanged={handleHistoryChanged}
|
|
1812
|
+
handleLoadingChange={handleLoadingChange}
|
|
1813
|
+
responseCompleteCallback={responseCompleteCallback}
|
|
1814
|
+
thumbsUpClick={thumbsUpClick}
|
|
1815
|
+
thumbsDownClick={thumbsDownClick}
|
|
1816
|
+
effectiveCustomer={effectiveCustomer}
|
|
1817
|
+
showPoweredBy={showPoweredBy}
|
|
1818
|
+
actions={actions}
|
|
1819
|
+
followOnQuestions={followOnQuestions}
|
|
1820
|
+
followOnPrompt={followOnPrompt}
|
|
1821
|
+
agentOptions={agentOptions}
|
|
1822
|
+
currentAgentId={currentAgentId}
|
|
1823
|
+
handleAgentSwitch={handleAgentSwitch}
|
|
1824
|
+
agentsLoading={agentsLoading}
|
|
1825
|
+
contextSections={mergedContext.sections}
|
|
1826
|
+
totalContextTokens={mergedContext.totalTokens || 0}
|
|
1827
|
+
maxContextTokens={maxContextTokens}
|
|
1828
|
+
enableContextDetailView={enableContextDetailView}
|
|
1829
|
+
/>
|
|
1830
|
+
))}
|
|
1831
|
+
|
|
1832
|
+
{/* Conversation loading overlay */}
|
|
1833
|
+
{loadingConversationId && (
|
|
1834
|
+
<div className="ai-agent-panel__conversation-loading-overlay">
|
|
1835
|
+
<div className="ai-agent-panel__loading-spinner" />
|
|
1836
|
+
<p>Loading conversation...</p>
|
|
1837
|
+
</div>
|
|
1838
|
+
)}
|
|
1839
|
+
|
|
1840
|
+
{/* Empty state when no conversation is selected */}
|
|
1841
|
+
{currentAgentMetadata && activeConversationsList.length === 0 && !loadingConversationId && (
|
|
1842
|
+
<div className="ai-agent-panel__empty-chat">
|
|
1843
|
+
<MessageIcon />
|
|
1844
|
+
<p>Select a conversation or start a new one</p>
|
|
1845
|
+
<Button variant="default" size="sm" onClick={handleNewConversation}>
|
|
1846
|
+
New Conversation
|
|
1847
|
+
</Button>
|
|
1848
|
+
</div>
|
|
1849
|
+
)}
|
|
1850
|
+
|
|
1851
|
+
{/* Agent loading state */}
|
|
1852
|
+
{agentsLoading && !currentAgentMetadata && (
|
|
1853
|
+
<div className="ai-agent-panel__loading">
|
|
1854
|
+
<div className="ai-agent-panel__loading-spinner" />
|
|
1855
|
+
<p>Loading agent...</p>
|
|
1856
|
+
</div>
|
|
1857
|
+
)}
|
|
1858
|
+
</div>
|
|
1859
|
+
|
|
1860
|
+
{/* Handoff confirmation dialog */}
|
|
1861
|
+
<Dialog
|
|
1862
|
+
isOpen={showHandoffDialog}
|
|
1863
|
+
onClose={handleHandoffCancel}
|
|
1864
|
+
title="Switch Agent?"
|
|
1865
|
+
description={`The current agent suggests transferring this conversation to ${
|
|
1866
|
+
suggestedAgent ? getAgent(suggestedAgent)?.metadata?.displayTitle || suggestedAgent : 'another agent'
|
|
1867
|
+
}.`}
|
|
1868
|
+
>
|
|
1869
|
+
<DialogFooter>
|
|
1870
|
+
<Button variant="outline" onClick={handleHandoffCancel}>
|
|
1871
|
+
Stay with current agent
|
|
1872
|
+
</Button>
|
|
1873
|
+
<Button onClick={handleHandoffConfirm}>
|
|
1874
|
+
Switch agent
|
|
1875
|
+
</Button>
|
|
1876
|
+
</DialogFooter>
|
|
1877
|
+
</Dialog>
|
|
1878
|
+
</div>
|
|
1879
|
+
);
|
|
1880
|
+
};
|
|
1881
|
+
|
|
1882
|
+
export default AIAgentPanel;
|
|
1883
|
+
|