@hef2024/llmasaservice-ui 0.16.8 → 0.16.9
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/DEPLOYMENT.md +193 -0
- package/dist/index.css +1951 -1886
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +326 -104
- package/dist/index.mjs +327 -105
- package/package.json +1 -1
- package/src/AIAgentPanel.css +79 -1
- package/src/AIAgentPanel.tsx +236 -23
- package/src/AIChatPanel.tsx +252 -108
- package/src/AgentPanel.tsx +1 -3
- package/src/ChatPanel.tsx +1 -7
- package/src/components/ui/Button.tsx +2 -0
- package/src/components/ui/Dialog.tsx +2 -0
- package/src/components/ui/Input.tsx +2 -0
- package/src/components/ui/Select.tsx +2 -0
- package/src/components/ui/Tooltip.tsx +2 -0
- package/src/components/ui/index.ts +2 -0
- package/src/hooks/useAgentRegistry.ts +1 -4
package/package.json
CHANGED
package/src/AIAgentPanel.css
CHANGED
|
@@ -569,13 +569,18 @@
|
|
|
569
569
|
}
|
|
570
570
|
|
|
571
571
|
.ai-agent-panel__loading {
|
|
572
|
+
position: absolute;
|
|
573
|
+
top: 0;
|
|
574
|
+
left: 0;
|
|
575
|
+
right: 0;
|
|
576
|
+
bottom: 0;
|
|
572
577
|
display: flex;
|
|
573
578
|
flex-direction: column;
|
|
574
579
|
align-items: center;
|
|
575
580
|
justify-content: center;
|
|
576
|
-
flex: 1;
|
|
577
581
|
gap: 12px;
|
|
578
582
|
color: var(--ai-sidebar-text-muted);
|
|
583
|
+
background-color: var(--ai-chat-bg);
|
|
579
584
|
}
|
|
580
585
|
|
|
581
586
|
.ai-agent-panel__conversation-loading-overlay {
|
|
@@ -1352,3 +1357,76 @@
|
|
|
1352
1357
|
border-left: 1px solid var(--ai-sidebar-border);
|
|
1353
1358
|
}
|
|
1354
1359
|
|
|
1360
|
+
/* --------------------------------------------------------
|
|
1361
|
+
No History Mode - Hide sidebar completely
|
|
1362
|
+
-------------------------------------------------------- */
|
|
1363
|
+
.ai-agent-panel--no-history .ai-agent-panel__sidebar {
|
|
1364
|
+
display: none;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
.ai-agent-panel--no-history .ai-agent-panel__chat {
|
|
1368
|
+
flex: 1;
|
|
1369
|
+
border: none;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/* --------------------------------------------------------
|
|
1373
|
+
Context Change Notification
|
|
1374
|
+
-------------------------------------------------------- */
|
|
1375
|
+
.ai-agent-panel__context-notification {
|
|
1376
|
+
position: absolute;
|
|
1377
|
+
bottom: 70px;
|
|
1378
|
+
left: 50%;
|
|
1379
|
+
transform: translateX(-50%);
|
|
1380
|
+
z-index: 50;
|
|
1381
|
+
display: flex;
|
|
1382
|
+
align-items: center;
|
|
1383
|
+
gap: 8px;
|
|
1384
|
+
padding: 8px 16px;
|
|
1385
|
+
background-color: var(--ai-agent-badge-bg);
|
|
1386
|
+
color: var(--ai-agent-badge-text);
|
|
1387
|
+
border-radius: 20px;
|
|
1388
|
+
font-size: 13px;
|
|
1389
|
+
font-weight: 500;
|
|
1390
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
1391
|
+
animation: ai-context-notification-in 0.3s ease-out, ai-context-notification-out 0.3s ease-in 2.7s forwards;
|
|
1392
|
+
pointer-events: none;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
.ai-agent-panel__context-notification svg {
|
|
1396
|
+
flex-shrink: 0;
|
|
1397
|
+
animation: ai-sparkle-pulse 0.6s ease-in-out infinite alternate;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
@keyframes ai-context-notification-in {
|
|
1401
|
+
0% {
|
|
1402
|
+
opacity: 0;
|
|
1403
|
+
transform: translateX(-50%) translateY(10px) scale(0.9);
|
|
1404
|
+
}
|
|
1405
|
+
100% {
|
|
1406
|
+
opacity: 1;
|
|
1407
|
+
transform: translateX(-50%) translateY(0) scale(1);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
@keyframes ai-context-notification-out {
|
|
1412
|
+
0% {
|
|
1413
|
+
opacity: 1;
|
|
1414
|
+
transform: translateX(-50%) translateY(0) scale(1);
|
|
1415
|
+
}
|
|
1416
|
+
100% {
|
|
1417
|
+
opacity: 0;
|
|
1418
|
+
transform: translateX(-50%) translateY(10px) scale(0.9);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
@keyframes ai-sparkle-pulse {
|
|
1423
|
+
0% {
|
|
1424
|
+
opacity: 0.7;
|
|
1425
|
+
transform: scale(0.9);
|
|
1426
|
+
}
|
|
1427
|
+
100% {
|
|
1428
|
+
opacity: 1;
|
|
1429
|
+
transform: scale(1.1);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
package/src/AIAgentPanel.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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
1
|
import { LLMAsAServiceCustomer } from 'llmasaservice-client';
|
|
2
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
3
|
import './AIAgentPanel.css';
|
|
4
|
+
import AIChatPanel, { AgentOption } from './AIChatPanel';
|
|
5
|
+
import { Button, Dialog, DialogFooter, Input, ScrollArea, Tooltip } from './components/ui';
|
|
6
|
+
import { useAgentRegistry } from './hooks/useAgentRegistry';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Context section for agent awareness
|
|
@@ -42,6 +42,7 @@ export interface APIConversationSummary {
|
|
|
42
42
|
*/
|
|
43
43
|
export interface ActiveConversation {
|
|
44
44
|
conversationId: string;
|
|
45
|
+
stableKey: string; // Stable key for React component - never changes
|
|
45
46
|
agentId: string;
|
|
46
47
|
history: Record<string, { content: string; callId: string }>;
|
|
47
48
|
isLoading: boolean;
|
|
@@ -125,6 +126,9 @@ export interface AIAgentPanelProps {
|
|
|
125
126
|
|
|
126
127
|
// Conversation history settings
|
|
127
128
|
historyListLimit?: number;
|
|
129
|
+
|
|
130
|
+
// Enable/disable conversation history panel
|
|
131
|
+
showConversationHistory?: boolean;
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
// Icons
|
|
@@ -193,6 +197,13 @@ const SidebarIcon = () => (
|
|
|
193
197
|
</svg>
|
|
194
198
|
);
|
|
195
199
|
|
|
200
|
+
const SparkleIcon = () => (
|
|
201
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
202
|
+
<path d="M8 1v3M8 12v3M3 8H0M16 8h-3M12.95 3.05l-2.12 2.12M5.17 10.83l-2.12 2.12M12.95 12.95l-2.12-2.12M5.17 5.17L3.05 3.05" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
|
203
|
+
<circle cx="8" cy="8" r="2" fill="currentColor"/>
|
|
204
|
+
</svg>
|
|
205
|
+
);
|
|
206
|
+
|
|
196
207
|
/**
|
|
197
208
|
* AIAgentPanel - Cursor-inspired multi-agent panel
|
|
198
209
|
*/
|
|
@@ -351,6 +362,8 @@ interface ChatPanelWrapperProps {
|
|
|
351
362
|
totalContextTokens: number;
|
|
352
363
|
maxContextTokens: number;
|
|
353
364
|
enableContextDetailView: boolean;
|
|
365
|
+
// Conversation creation callback
|
|
366
|
+
onConversationCreated: (tempId: string, realId: string) => void;
|
|
354
367
|
}
|
|
355
368
|
|
|
356
369
|
// Remove React.memo temporarily to debug - ChatPanelWrapper needs to re-render when agentId changes
|
|
@@ -379,6 +392,7 @@ const ChatPanelWrapper = (({
|
|
|
379
392
|
totalContextTokens,
|
|
380
393
|
maxContextTokens,
|
|
381
394
|
enableContextDetailView,
|
|
395
|
+
onConversationCreated,
|
|
382
396
|
}) => {
|
|
383
397
|
const convAgentProfile = getAgent(activeConv.agentId);
|
|
384
398
|
const convAgentMetadata = convAgentProfile?.metadata;
|
|
@@ -399,6 +413,14 @@ const ChatPanelWrapper = (({
|
|
|
399
413
|
[handleLoadingChange, activeConv.conversationId]
|
|
400
414
|
);
|
|
401
415
|
|
|
416
|
+
// Callback when AIChatPanel creates a new conversation via API
|
|
417
|
+
const conversationCreatedCallback = useCallback(
|
|
418
|
+
(realConversationId: string) => {
|
|
419
|
+
onConversationCreated(activeConv.conversationId, realConversationId);
|
|
420
|
+
},
|
|
421
|
+
[onConversationCreated, activeConv.conversationId]
|
|
422
|
+
);
|
|
423
|
+
|
|
402
424
|
// Compute follow-on questions - MUST update when agent switches
|
|
403
425
|
// Don't use useMemo - compute fresh every render to ensure it always reflects current agent
|
|
404
426
|
// The computation is cheap (just string split), and this guarantees updates
|
|
@@ -472,6 +494,7 @@ const ChatPanelWrapper = (({
|
|
|
472
494
|
totalContextTokens={totalContextTokens}
|
|
473
495
|
maxContextTokens={maxContextTokens}
|
|
474
496
|
enableContextDetailView={enableContextDetailView}
|
|
497
|
+
onConversationCreated={conversationCreatedCallback}
|
|
475
498
|
/>
|
|
476
499
|
</div>
|
|
477
500
|
);
|
|
@@ -514,6 +537,7 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
514
537
|
followOnQuestions = [],
|
|
515
538
|
followOnPrompt = '',
|
|
516
539
|
historyListLimit = 50,
|
|
540
|
+
showConversationHistory = true,
|
|
517
541
|
}) => {
|
|
518
542
|
// Panel state
|
|
519
543
|
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
|
@@ -540,6 +564,7 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
540
564
|
const [showSearch, setShowSearch] = useState(false);
|
|
541
565
|
const [showHandoffDialog, setShowHandoffDialog] = useState(false);
|
|
542
566
|
const [suggestedAgent, setSuggestedAgent] = useState<string | null>(null);
|
|
567
|
+
const [handoffSource, setHandoffSource] = useState<'agent' | 'page'>('agent');
|
|
543
568
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
544
569
|
const resizeRef = useRef<HTMLDivElement>(null);
|
|
545
570
|
|
|
@@ -628,6 +653,14 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
628
653
|
const currentConversationIdRef = useRef<string | null>(currentConversationId);
|
|
629
654
|
currentConversationIdRef.current = currentConversationId;
|
|
630
655
|
|
|
656
|
+
// Context change notification state
|
|
657
|
+
const [showContextNotification, setShowContextNotification] = useState(false);
|
|
658
|
+
const prevContextRef = useRef<string | null>(null);
|
|
659
|
+
const contextNotificationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
660
|
+
|
|
661
|
+
// Track previous defaultAgent to avoid re-suggesting the same agent
|
|
662
|
+
const prevDefaultAgentRef = useRef<string | null>(null);
|
|
663
|
+
|
|
631
664
|
// Fetch first prompt from a conversation
|
|
632
665
|
const fetchFirstPrompt = useCallback(async (conversationId: string, agentIdForConversation?: string) => {
|
|
633
666
|
// Skip if already checked/loaded
|
|
@@ -774,6 +807,9 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
774
807
|
|
|
775
808
|
setConversationsLoading(true);
|
|
776
809
|
setConversationsError(null);
|
|
810
|
+
console.log("projectId", projectId);
|
|
811
|
+
console.log("customerId", customerId);
|
|
812
|
+
console.log("apiKey", apiKey);
|
|
777
813
|
|
|
778
814
|
try {
|
|
779
815
|
console.log('fetchConversations - customerId:', customerId);
|
|
@@ -942,6 +978,7 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
942
978
|
const next = new Map(prev);
|
|
943
979
|
next.set(conversationId, {
|
|
944
980
|
conversationId,
|
|
981
|
+
stableKey: conversationId, // Use real ID as stable key when loading from API
|
|
945
982
|
agentId: agentIdToUse,
|
|
946
983
|
history,
|
|
947
984
|
isLoading: false,
|
|
@@ -975,6 +1012,9 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
975
1012
|
|
|
976
1013
|
// Fetch conversations on mount and when agent changes
|
|
977
1014
|
useEffect(() => {
|
|
1015
|
+
// Skip fetching if conversation history is disabled
|
|
1016
|
+
if (!showConversationHistory) return;
|
|
1017
|
+
|
|
978
1018
|
// Only fetch if agents are loaded and we have necessary data
|
|
979
1019
|
if (!agentsLoading && currentAgentId && apiKey) {
|
|
980
1020
|
// Check if agent is ready (has projectId)
|
|
@@ -987,7 +1027,7 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
987
1027
|
fetchConversations(currentAgentId);
|
|
988
1028
|
}
|
|
989
1029
|
}
|
|
990
|
-
}, [agentsLoading, currentAgentId, apiKey, fetchConversations, getAgent]);
|
|
1030
|
+
}, [agentsLoading, currentAgentId, apiKey, fetchConversations, getAgent, showConversationHistory]);
|
|
991
1031
|
|
|
992
1032
|
// Start new conversation
|
|
993
1033
|
const handleNewConversation = useCallback(() => {
|
|
@@ -999,6 +1039,7 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
999
1039
|
const next = new Map(prev);
|
|
1000
1040
|
next.set(tempId, {
|
|
1001
1041
|
conversationId: tempId,
|
|
1042
|
+
stableKey: tempId, // Stable key never changes even when conversationId updates
|
|
1002
1043
|
agentId: currentAgentId,
|
|
1003
1044
|
history: {},
|
|
1004
1045
|
isLoading: false,
|
|
@@ -1202,6 +1243,125 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1202
1243
|
}, [context, sharedContextSections, pageContextSections, contextDataSources]);
|
|
1203
1244
|
|
|
1204
1245
|
|
|
1246
|
+
// Detect context changes and show notification
|
|
1247
|
+
useEffect(() => {
|
|
1248
|
+
// Create a stable string representation of the context for comparison
|
|
1249
|
+
const contextString = JSON.stringify(mergedContext.sections.map(s => ({ id: s.id, data: s.data })));
|
|
1250
|
+
|
|
1251
|
+
// Skip on first render
|
|
1252
|
+
if (prevContextRef.current === null) {
|
|
1253
|
+
prevContextRef.current = contextString;
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// Check if context actually changed
|
|
1258
|
+
if (prevContextRef.current !== contextString) {
|
|
1259
|
+
console.log('AIAgentPanel - Context changed, showing notification');
|
|
1260
|
+
prevContextRef.current = contextString;
|
|
1261
|
+
|
|
1262
|
+
// Clear any existing timeout before setting a new one
|
|
1263
|
+
if (contextNotificationTimeoutRef.current) {
|
|
1264
|
+
clearTimeout(contextNotificationTimeoutRef.current);
|
|
1265
|
+
contextNotificationTimeoutRef.current = null;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Show the notification
|
|
1269
|
+
setShowContextNotification(true);
|
|
1270
|
+
|
|
1271
|
+
// Auto-hide after 3 seconds
|
|
1272
|
+
contextNotificationTimeoutRef.current = setTimeout(() => {
|
|
1273
|
+
setShowContextNotification(false);
|
|
1274
|
+
contextNotificationTimeoutRef.current = null;
|
|
1275
|
+
}, 3000);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Note: No cleanup on every re-render - only clear timeout when context changes
|
|
1279
|
+
// This prevents race conditions when switching rapidly
|
|
1280
|
+
}, [mergedContext.sections]);
|
|
1281
|
+
|
|
1282
|
+
// Separate cleanup effect for unmount only
|
|
1283
|
+
useEffect(() => {
|
|
1284
|
+
return () => {
|
|
1285
|
+
if (contextNotificationTimeoutRef.current) {
|
|
1286
|
+
clearTimeout(contextNotificationTimeoutRef.current);
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
}, []);
|
|
1290
|
+
|
|
1291
|
+
// Auto-suggest agent switch when page context includes a defaultAgent
|
|
1292
|
+
// Injects a synthetic message with [SUGGEST_AGENT:...] into the chat history
|
|
1293
|
+
useEffect(() => {
|
|
1294
|
+
// Look for a defaultAgent in any page context section
|
|
1295
|
+
let foundDefaultAgent: string | null = null;
|
|
1296
|
+
|
|
1297
|
+
for (const section of pageContextSections) {
|
|
1298
|
+
if (section.data && typeof section.data === 'object' && 'defaultAgent' in section.data) {
|
|
1299
|
+
const defaultAgentValue = section.data.defaultAgent;
|
|
1300
|
+
if (typeof defaultAgentValue === 'string' && defaultAgentValue) {
|
|
1301
|
+
foundDefaultAgent = defaultAgentValue;
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Skip if no defaultAgent found or it's the same as last time we processed
|
|
1308
|
+
if (!foundDefaultAgent || foundDefaultAgent === prevDefaultAgentRef.current) {
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// Skip if it's already the current agent
|
|
1313
|
+
if (foundDefaultAgent === currentAgentId) {
|
|
1314
|
+
// Update ref so we don't re-process if user switches away and back
|
|
1315
|
+
prevDefaultAgentRef.current = foundDefaultAgent;
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Check if this agent is in our available agents list and is ready
|
|
1320
|
+
const isAvailable = agentIds.includes(foundDefaultAgent);
|
|
1321
|
+
const agentProfile = getAgent(foundDefaultAgent);
|
|
1322
|
+
const isReady = agentProfile?.status === 'ready';
|
|
1323
|
+
|
|
1324
|
+
if (isAvailable && isReady && currentConversationIdRef.current) {
|
|
1325
|
+
// Get the agent's display name
|
|
1326
|
+
const agentName = agentProfile?.metadata?.displayTitle ||
|
|
1327
|
+
localOverrides[foundDefaultAgent]?.localName ||
|
|
1328
|
+
foundDefaultAgent;
|
|
1329
|
+
|
|
1330
|
+
// Create a synthetic message with the SUGGEST_AGENT marker
|
|
1331
|
+
// This will be processed by the existing agent suggestion card rendering
|
|
1332
|
+
const suggestionMessage = `This page recommends a different agent for better assistance.\n\n[SUGGEST_AGENT:${foundDefaultAgent}|${agentName}|Recommended for this page]`;
|
|
1333
|
+
|
|
1334
|
+
// Create a unique key for the synthetic message (includes timestamp to allow multiple suggestions)
|
|
1335
|
+
// Using __system__: prefix tells AIChatPanel to hide the "user message" part
|
|
1336
|
+
const syntheticKey = `__system__:page_suggestion_${foundDefaultAgent}_${Date.now()}`;
|
|
1337
|
+
|
|
1338
|
+
// Update ref BEFORE injecting to prevent duplicate processing
|
|
1339
|
+
prevDefaultAgentRef.current = foundDefaultAgent;
|
|
1340
|
+
|
|
1341
|
+
// Inject the synthetic message into the current conversation's history
|
|
1342
|
+
setActiveConversations(prev => {
|
|
1343
|
+
const conversationId = currentConversationIdRef.current;
|
|
1344
|
+
if (!conversationId) return prev;
|
|
1345
|
+
|
|
1346
|
+
const existing = prev.get(conversationId);
|
|
1347
|
+
if (!existing) return prev;
|
|
1348
|
+
|
|
1349
|
+
const next = new Map(prev);
|
|
1350
|
+
next.set(conversationId, {
|
|
1351
|
+
...existing,
|
|
1352
|
+
history: {
|
|
1353
|
+
...existing.history,
|
|
1354
|
+
[syntheticKey]: {
|
|
1355
|
+
content: suggestionMessage,
|
|
1356
|
+
callId: `page-suggestion-${Date.now()}`,
|
|
1357
|
+
},
|
|
1358
|
+
},
|
|
1359
|
+
});
|
|
1360
|
+
return next;
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}, [pageContextSections, currentAgentId, agentIds, getAgent, localOverrides]);
|
|
1364
|
+
|
|
1205
1365
|
// Build data array for ChatPanel
|
|
1206
1366
|
const chatPanelData = useMemo(() => {
|
|
1207
1367
|
const contextData = mergedContext.sections.map((section) => ({
|
|
@@ -1337,6 +1497,7 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1337
1497
|
const suggestedId = handoffMatch[1];
|
|
1338
1498
|
if (suggestedId && suggestedId !== currentAgentId && agents.includes(suggestedId)) {
|
|
1339
1499
|
setSuggestedAgent(suggestedId);
|
|
1500
|
+
setHandoffSource('agent');
|
|
1340
1501
|
setShowHandoffDialog(true);
|
|
1341
1502
|
}
|
|
1342
1503
|
}
|
|
@@ -1380,13 +1541,43 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1380
1541
|
}
|
|
1381
1542
|
setShowHandoffDialog(false);
|
|
1382
1543
|
setSuggestedAgent(null);
|
|
1544
|
+
setHandoffSource('agent');
|
|
1383
1545
|
}, [suggestedAgent, handleAgentSwitch]);
|
|
1384
1546
|
|
|
1385
1547
|
const handleHandoffCancel = useCallback(() => {
|
|
1386
1548
|
setShowHandoffDialog(false);
|
|
1387
1549
|
setSuggestedAgent(null);
|
|
1550
|
+
setHandoffSource('agent');
|
|
1388
1551
|
}, []);
|
|
1389
1552
|
|
|
1553
|
+
// Handle conversation created - update temp ID to real ID from API
|
|
1554
|
+
const handleConversationCreated = useCallback((tempId: string, realId: string) => {
|
|
1555
|
+
console.log('Conversation created:', tempId, '->', realId);
|
|
1556
|
+
|
|
1557
|
+
// Update activeConversations - replace temp ID with real ID
|
|
1558
|
+
setActiveConversations(prev => {
|
|
1559
|
+
const existing = prev.get(tempId);
|
|
1560
|
+
if (existing) {
|
|
1561
|
+
const next = new Map(prev);
|
|
1562
|
+
next.delete(tempId);
|
|
1563
|
+
next.set(realId, {
|
|
1564
|
+
...existing,
|
|
1565
|
+
conversationId: realId,
|
|
1566
|
+
});
|
|
1567
|
+
return next;
|
|
1568
|
+
}
|
|
1569
|
+
return prev;
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
// Update currentConversationId if it was the temp one
|
|
1573
|
+
if (currentConversationIdRef.current === tempId) {
|
|
1574
|
+
setCurrentConversationId(realId);
|
|
1575
|
+
if (onConversationChange) {
|
|
1576
|
+
onConversationChange(realId);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}, [onConversationChange]);
|
|
1580
|
+
|
|
1390
1581
|
// Toggle collapse
|
|
1391
1582
|
const toggleCollapse = useCallback(() => {
|
|
1392
1583
|
setIsCollapsed((prev) => !prev);
|
|
@@ -1408,6 +1599,7 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1408
1599
|
isCollapsed ? 'ai-agent-panel--collapsed' : '',
|
|
1409
1600
|
position === 'left' ? 'ai-agent-panel--left' : 'ai-agent-panel--right',
|
|
1410
1601
|
sidebarPosition === 'right' ? 'ai-agent-panel--sidebar-right' : 'ai-agent-panel--sidebar-left',
|
|
1602
|
+
!showConversationHistory ? 'ai-agent-panel--no-history' : '',
|
|
1411
1603
|
].filter(Boolean).join(' ');
|
|
1412
1604
|
|
|
1413
1605
|
// Collapsed view
|
|
@@ -1433,18 +1625,20 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1433
1625
|
|
|
1434
1626
|
<div className="ai-agent-panel__collapsed-divider" />
|
|
1435
1627
|
|
|
1436
|
-
|
|
1437
|
-
<
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1628
|
+
{showConversationHistory && (
|
|
1629
|
+
<Tooltip content="Search" side="left">
|
|
1630
|
+
<Button
|
|
1631
|
+
variant="ghost"
|
|
1632
|
+
size="icon"
|
|
1633
|
+
onClick={() => {
|
|
1634
|
+
setIsCollapsed(false);
|
|
1635
|
+
setShowSearch(true);
|
|
1636
|
+
}}
|
|
1637
|
+
>
|
|
1638
|
+
<SearchIcon />
|
|
1639
|
+
</Button>
|
|
1640
|
+
</Tooltip>
|
|
1641
|
+
)}
|
|
1448
1642
|
|
|
1449
1643
|
<Tooltip content="New Chat" side="left">
|
|
1450
1644
|
<Button
|
|
@@ -1489,6 +1683,7 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1489
1683
|
const next = new Map(prev);
|
|
1490
1684
|
next.set(tempId, {
|
|
1491
1685
|
conversationId: tempId,
|
|
1686
|
+
stableKey: tempId, // Stable key never changes
|
|
1492
1687
|
agentId: agent.id,
|
|
1493
1688
|
history: {},
|
|
1494
1689
|
isLoading: false,
|
|
@@ -1543,7 +1738,8 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1543
1738
|
tabIndex={0}
|
|
1544
1739
|
/>
|
|
1545
1740
|
|
|
1546
|
-
{/* Sidebar */}
|
|
1741
|
+
{/* Sidebar - only show if conversation history is enabled */}
|
|
1742
|
+
{showConversationHistory && (
|
|
1547
1743
|
<div className={`ai-agent-panel__sidebar ${isHistoryCollapsed ? 'ai-agent-panel__sidebar--collapsed' : ''}`}>
|
|
1548
1744
|
{isHistoryCollapsed ? (
|
|
1549
1745
|
// Collapsed history bar
|
|
@@ -1609,6 +1805,7 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1609
1805
|
const next = new Map(prev);
|
|
1610
1806
|
next.set(tempId, {
|
|
1611
1807
|
conversationId: tempId,
|
|
1808
|
+
stableKey: tempId, // Stable key never changes
|
|
1612
1809
|
agentId: agent.id,
|
|
1613
1810
|
history: {},
|
|
1614
1811
|
isLoading: false,
|
|
@@ -1696,7 +1893,7 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1696
1893
|
</div>
|
|
1697
1894
|
{activeConversationsList.map((activeConv) => (
|
|
1698
1895
|
<div
|
|
1699
|
-
key={activeConv.
|
|
1896
|
+
key={activeConv.stableKey}
|
|
1700
1897
|
className={`ai-agent-panel__conversation ai-agent-panel__conversation--active-item ${
|
|
1701
1898
|
currentConversationId === activeConv.conversationId
|
|
1702
1899
|
? 'ai-agent-panel__conversation--current'
|
|
@@ -1795,13 +1992,22 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1795
1992
|
</>
|
|
1796
1993
|
)}
|
|
1797
1994
|
</div>
|
|
1995
|
+
)}
|
|
1798
1996
|
|
|
1799
1997
|
{/* Chat area */}
|
|
1800
1998
|
<div className="ai-agent-panel__chat">
|
|
1999
|
+
{/* Context change notification */}
|
|
2000
|
+
{showContextNotification && (
|
|
2001
|
+
<div className="ai-agent-panel__context-notification">
|
|
2002
|
+
<SparkleIcon />
|
|
2003
|
+
<span>Agent now has new context</span>
|
|
2004
|
+
</div>
|
|
2005
|
+
)}
|
|
2006
|
+
|
|
1801
2007
|
{/* Chat panels - one per active conversation, shown/hidden via CSS */}
|
|
1802
2008
|
{activeConversationsList.map((activeConv) => (
|
|
1803
2009
|
<ChatPanelWrapper
|
|
1804
|
-
key={`${activeConv.
|
|
2010
|
+
key={`${activeConv.stableKey}-${activeConv.agentId}`}
|
|
1805
2011
|
activeConv={activeConv}
|
|
1806
2012
|
currentConversationId={currentConversationId}
|
|
1807
2013
|
getAgent={getAgent}
|
|
@@ -1826,6 +2032,7 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1826
2032
|
totalContextTokens={mergedContext.totalTokens || 0}
|
|
1827
2033
|
maxContextTokens={maxContextTokens}
|
|
1828
2034
|
enableContextDetailView={enableContextDetailView}
|
|
2035
|
+
onConversationCreated={handleConversationCreated}
|
|
1829
2036
|
/>
|
|
1830
2037
|
))}
|
|
1831
2038
|
|
|
@@ -1862,9 +2069,15 @@ const AIAgentPanel: React.FC<AIAgentPanelProps> = ({
|
|
|
1862
2069
|
isOpen={showHandoffDialog}
|
|
1863
2070
|
onClose={handleHandoffCancel}
|
|
1864
2071
|
title="Switch Agent?"
|
|
1865
|
-
description={
|
|
1866
|
-
|
|
1867
|
-
|
|
2072
|
+
description={
|
|
2073
|
+
handoffSource === 'page'
|
|
2074
|
+
? `This page has a recommended agent: ${
|
|
2075
|
+
suggestedAgent ? getAgent(suggestedAgent)?.metadata?.displayTitle || suggestedAgent : 'another agent'
|
|
2076
|
+
}. Would you like to switch?`
|
|
2077
|
+
: `The current agent suggests transferring this conversation to ${
|
|
2078
|
+
suggestedAgent ? getAgent(suggestedAgent)?.metadata?.displayTitle || suggestedAgent : 'another agent'
|
|
2079
|
+
}.`
|
|
2080
|
+
}
|
|
1868
2081
|
>
|
|
1869
2082
|
<DialogFooter>
|
|
1870
2083
|
<Button variant="outline" onClick={handleHandoffCancel}>
|