@hef2024/llmasaservice-ui 0.24.2 → 0.24.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.css +256 -62
- package/dist/index.d.mts +158 -115
- package/dist/index.d.ts +158 -115
- package/dist/index.js +3099 -568
- package/dist/index.mjs +3099 -568
- package/index.ts +6 -1
- package/package.json +1 -1
- package/src/AIAgentPanel.tsx +647 -207
- package/src/AIChatPanel.css +286 -37
- package/src/AIChatPanel.tsx +2871 -358
- package/src/AgentPanel.tsx +4 -0
- package/src/ChatPanel.tsx +254 -104
- package/src/hooks/useAgentRegistry.ts +2 -1
- package/src/mcpAuth.ts +36 -0
- package/src/toolArgsParser.ts +346 -0
package/src/AIAgentPanel.tsx
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { LLMAsAServiceCustomer } from 'llmasaservice-client';
|
|
2
2
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import './AIAgentPanel.css';
|
|
4
|
-
import AIChatPanel, {
|
|
4
|
+
import AIChatPanel, {
|
|
5
|
+
AgentOption,
|
|
6
|
+
BeforeSendPayload,
|
|
7
|
+
LocalToolExecutor,
|
|
8
|
+
TraceContextMode,
|
|
9
|
+
} from './AIChatPanel';
|
|
5
10
|
import { Button, Dialog, DialogFooter, Input, ScrollArea, Tooltip } from './components/ui';
|
|
6
11
|
import { useAgentRegistry } from './hooks/useAgentRegistry';
|
|
12
|
+
import { MCPAuthHeaderResolver } from './mcpAuth';
|
|
7
13
|
|
|
8
14
|
/**
|
|
9
15
|
* Context section for agent awareness
|
|
@@ -58,6 +64,7 @@ export interface ActiveConversation {
|
|
|
58
64
|
stableKey: string; // Stable key for React component - never changes
|
|
59
65
|
agentId: string;
|
|
60
66
|
history: Record<string, { content: string; callId: string }>;
|
|
67
|
+
transcriptLoaded: boolean; // Prevent re-fetch loops for conversations with empty history
|
|
61
68
|
isLoading: boolean;
|
|
62
69
|
title: string;
|
|
63
70
|
conversationInitialPrompt?: string; // Per-conversation initial prompt for programmatic start
|
|
@@ -126,6 +133,7 @@ export interface AIAgentPanelProps {
|
|
|
126
133
|
// Callbacks
|
|
127
134
|
onAgentSwitch?: (fromAgent: string, toAgent: string) => void;
|
|
128
135
|
onConversationChange?: (conversationId: string) => void;
|
|
136
|
+
onBeforeSend?: (payload: BeforeSendPayload) => Promise<void> | void;
|
|
129
137
|
historyChangedCallback?: (history: Record<string, { content: string; callId: string }>) => void;
|
|
130
138
|
responseCompleteCallback?: (callId: string, prompt: string, response: string) => void;
|
|
131
139
|
thumbsUpClick?: (callId: string) => void;
|
|
@@ -176,6 +184,10 @@ export interface AIAgentPanelProps {
|
|
|
176
184
|
callToActionEmailSubject?: string;
|
|
177
185
|
customerEmailCaptureMode?: "HIDE" | "OPTIONAL" | "REQUIRED";
|
|
178
186
|
customerEmailCapturePlaceholder?: string;
|
|
187
|
+
resolveMcpAuthHeaders?: MCPAuthHeaderResolver;
|
|
188
|
+
localToolExecutors?: Record<string, LocalToolExecutor>;
|
|
189
|
+
traceContextMode?: TraceContextMode;
|
|
190
|
+
autoApproveTools?: boolean | string[];
|
|
179
191
|
}
|
|
180
192
|
|
|
181
193
|
// Icons
|
|
@@ -259,16 +271,22 @@ const normalizeConversationListPayload = (payload: any): APIConversationSummary[
|
|
|
259
271
|
if (!payload) return [];
|
|
260
272
|
|
|
261
273
|
const conversations = Array.isArray(payload) ? payload : payload.conversations || [];
|
|
262
|
-
|
|
263
|
-
|
|
274
|
+
|
|
275
|
+
const normalized = conversations.map((conv: any, index: number) => {
|
|
264
276
|
// Get title, but filter out generic "Conversation" from API
|
|
265
277
|
let title = conv.title && conv.title !== 'Conversation' ? conv.title : '';
|
|
266
278
|
if (!title) {
|
|
267
279
|
title = conv.summary || extractTitleFromConversation(conv);
|
|
268
280
|
}
|
|
281
|
+
|
|
282
|
+
const resolvedConversationId =
|
|
283
|
+
conv.conversationId ||
|
|
284
|
+
conv.id ||
|
|
285
|
+
conv.conversation_id ||
|
|
286
|
+
`conversation-${index}-${Date.now()}`;
|
|
269
287
|
|
|
270
288
|
return {
|
|
271
|
-
conversationId:
|
|
289
|
+
conversationId: resolvedConversationId,
|
|
272
290
|
title,
|
|
273
291
|
summary: conv.summary,
|
|
274
292
|
createdAt: conv.createdAt || conv.created_at || conv.timestamp || new Date().toISOString(),
|
|
@@ -277,6 +295,25 @@ const normalizeConversationListPayload = (payload: any): APIConversationSummary[
|
|
|
277
295
|
messageCount: conv.messageCount || conv.message_count,
|
|
278
296
|
};
|
|
279
297
|
});
|
|
298
|
+
|
|
299
|
+
// Dedupe by conversationId in case upstream returns duplicate rows.
|
|
300
|
+
// Keep the most recently updated entry for each ID.
|
|
301
|
+
const dedupedById = new Map<string, APIConversationSummary>();
|
|
302
|
+
for (const conv of normalized) {
|
|
303
|
+
const existing = dedupedById.get(conv.conversationId);
|
|
304
|
+
if (!existing) {
|
|
305
|
+
dedupedById.set(conv.conversationId, conv);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const existingUpdated = new Date(existing.updatedAt).getTime();
|
|
310
|
+
const incomingUpdated = new Date(conv.updatedAt).getTime();
|
|
311
|
+
if (incomingUpdated >= existingUpdated) {
|
|
312
|
+
dedupedById.set(conv.conversationId, conv);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return Array.from(dedupedById.values());
|
|
280
317
|
};
|
|
281
318
|
|
|
282
319
|
// Extract title from conversation data
|
|
@@ -376,6 +413,333 @@ const groupConversationsByTime = (conversations: APIConversationSummary[], showA
|
|
|
376
413
|
.map(([label, conversations]) => ({ label, conversations, count: conversations.length }));
|
|
377
414
|
};
|
|
378
415
|
|
|
416
|
+
interface TranscriptMessage {
|
|
417
|
+
role: string;
|
|
418
|
+
content: string;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
interface TranscriptTurn {
|
|
422
|
+
prompt: string;
|
|
423
|
+
response: string;
|
|
424
|
+
callId: string;
|
|
425
|
+
timestampMs: number;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const toRecord = (value: unknown): Record<string, unknown> | null => {
|
|
429
|
+
if (typeof value === 'object' && value !== null) {
|
|
430
|
+
return value as Record<string, unknown>;
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const toStringValue = (value: unknown): string => {
|
|
436
|
+
if (typeof value === 'string') return value;
|
|
437
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
438
|
+
return '';
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const normalizeContentText = (value: unknown): string => {
|
|
442
|
+
if (typeof value === 'string') return value;
|
|
443
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
444
|
+
if (Array.isArray(value)) {
|
|
445
|
+
return value
|
|
446
|
+
.map(item => normalizeContentText(item))
|
|
447
|
+
.filter(Boolean)
|
|
448
|
+
.join('\n')
|
|
449
|
+
.trim();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const record = toRecord(value);
|
|
453
|
+
if (!record) return '';
|
|
454
|
+
|
|
455
|
+
const directText = toStringValue(record.text);
|
|
456
|
+
if (directText) return directText;
|
|
457
|
+
|
|
458
|
+
const directValue = toStringValue(record.value);
|
|
459
|
+
if (directValue) return directValue;
|
|
460
|
+
|
|
461
|
+
const directContent = toStringValue(record.content);
|
|
462
|
+
if (directContent) return directContent;
|
|
463
|
+
|
|
464
|
+
if (Array.isArray(record.content)) {
|
|
465
|
+
return normalizeContentText(record.content);
|
|
466
|
+
}
|
|
467
|
+
if (Array.isArray(record.parts)) {
|
|
468
|
+
return normalizeContentText(record.parts);
|
|
469
|
+
}
|
|
470
|
+
if (record.content && typeof record.content === 'object') {
|
|
471
|
+
return normalizeContentText(record.content);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return '';
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const parseCallMessages = (rawMessages: unknown): TranscriptMessage[] => {
|
|
478
|
+
let parsed: unknown = rawMessages;
|
|
479
|
+
if (typeof rawMessages === 'string') {
|
|
480
|
+
try {
|
|
481
|
+
parsed = JSON.parse(rawMessages);
|
|
482
|
+
} catch {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const parsedRecord = toRecord(parsed);
|
|
488
|
+
if (parsedRecord && Array.isArray(parsedRecord.messages)) {
|
|
489
|
+
parsed = parsedRecord.messages;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (!Array.isArray(parsed)) {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const normalized: TranscriptMessage[] = [];
|
|
497
|
+
for (const message of parsed) {
|
|
498
|
+
const messageRecord = toRecord(message);
|
|
499
|
+
if (!messageRecord) continue;
|
|
500
|
+
|
|
501
|
+
const role = toStringValue(messageRecord.role).toLowerCase().trim();
|
|
502
|
+
if (!role) continue;
|
|
503
|
+
|
|
504
|
+
const content = normalizeContentText(
|
|
505
|
+
messageRecord.content ?? messageRecord.text ?? messageRecord.message
|
|
506
|
+
).trim();
|
|
507
|
+
if (!content) continue;
|
|
508
|
+
|
|
509
|
+
normalized.push({ role, content });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return normalized;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const shouldSkipTranscriptMessage = (message: TranscriptMessage): boolean => {
|
|
516
|
+
return message.role === 'system' || (message.role === 'user' && message.content.startsWith('__system__:'));
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const parseTimestampMs = (value: unknown): number | null => {
|
|
520
|
+
const timestamp = toStringValue(value).trim();
|
|
521
|
+
if (!timestamp) return null;
|
|
522
|
+
const parsed = Date.parse(timestamp);
|
|
523
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const getCallTimestampMs = (call: Record<string, unknown>, fallbackIndex: number): number => {
|
|
527
|
+
const timestampCandidates = [
|
|
528
|
+
call.createdAt,
|
|
529
|
+
call.created_at,
|
|
530
|
+
call.timestamp,
|
|
531
|
+
call.updatedAt,
|
|
532
|
+
call.updated_at,
|
|
533
|
+
];
|
|
534
|
+
|
|
535
|
+
for (const candidate of timestampCandidates) {
|
|
536
|
+
const parsed = parseTimestampMs(candidate);
|
|
537
|
+
if (parsed !== null) return parsed;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Deterministic fallback to preserve ordering when timestamps are absent
|
|
541
|
+
return fallbackIndex;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const getCallId = (call: Record<string, unknown>): string => {
|
|
545
|
+
const id = toStringValue(call.id);
|
|
546
|
+
if (id) return id;
|
|
547
|
+
return toStringValue(call.callId);
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const normalizeCallsPayload = (payload: unknown): Record<string, unknown>[] => {
|
|
551
|
+
const payloadRecord = toRecord(payload);
|
|
552
|
+
const calls = Array.isArray(payload)
|
|
553
|
+
? payload
|
|
554
|
+
: payloadRecord && Array.isArray(payloadRecord.calls)
|
|
555
|
+
? (payloadRecord.calls as unknown[])
|
|
556
|
+
: [];
|
|
557
|
+
|
|
558
|
+
return calls
|
|
559
|
+
.map(call => toRecord(call))
|
|
560
|
+
.filter((call): call is Record<string, unknown> => call !== null);
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const turnsEqual = (left: TranscriptTurn, right: TranscriptTurn): boolean => {
|
|
564
|
+
return left.prompt === right.prompt && left.response === right.response;
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const getPrefixMatchLength = (existing: TranscriptTurn[], incoming: TranscriptTurn[]): number => {
|
|
568
|
+
const max = Math.min(existing.length, incoming.length);
|
|
569
|
+
let matched = 0;
|
|
570
|
+
while (matched < max && turnsEqual(existing[matched]!, incoming[matched]!)) {
|
|
571
|
+
matched += 1;
|
|
572
|
+
}
|
|
573
|
+
return matched;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const getSuffixPrefixOverlap = (existing: TranscriptTurn[], incoming: TranscriptTurn[]): number => {
|
|
577
|
+
const max = Math.min(existing.length, incoming.length);
|
|
578
|
+
for (let overlap = max; overlap > 0; overlap -= 1) {
|
|
579
|
+
let matches = true;
|
|
580
|
+
for (let idx = 0; idx < overlap; idx += 1) {
|
|
581
|
+
const left = existing[existing.length - overlap + idx]!;
|
|
582
|
+
const right = incoming[idx]!;
|
|
583
|
+
if (!turnsEqual(left, right)) {
|
|
584
|
+
matches = false;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (matches) return overlap;
|
|
589
|
+
}
|
|
590
|
+
return 0;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const mergeTranscriptTurns = (existing: TranscriptTurn[], incoming: TranscriptTurn[]): TranscriptTurn[] => {
|
|
594
|
+
if (incoming.length === 0) return existing;
|
|
595
|
+
if (existing.length === 0) return [...incoming];
|
|
596
|
+
|
|
597
|
+
const prefixMatchLength = getPrefixMatchLength(existing, incoming);
|
|
598
|
+
if (prefixMatchLength > 0) {
|
|
599
|
+
return [...existing, ...incoming.slice(prefixMatchLength)];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const overlap = getSuffixPrefixOverlap(existing, incoming);
|
|
603
|
+
if (overlap > 0) {
|
|
604
|
+
return [...existing, ...incoming.slice(overlap)];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return [...existing, ...incoming];
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const buildTurnsFromMessages = (
|
|
611
|
+
messages: TranscriptMessage[],
|
|
612
|
+
fallbackResponse: string,
|
|
613
|
+
callId: string,
|
|
614
|
+
timestampMs: number,
|
|
615
|
+
): TranscriptTurn[] => {
|
|
616
|
+
const turns: TranscriptTurn[] = [];
|
|
617
|
+
|
|
618
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
619
|
+
const message = messages[index];
|
|
620
|
+
if (!message || message.role !== 'user') continue;
|
|
621
|
+
|
|
622
|
+
const prompt = message.content.trim();
|
|
623
|
+
if (!prompt) continue;
|
|
624
|
+
|
|
625
|
+
let response = '';
|
|
626
|
+
let reachedNextUser = false;
|
|
627
|
+
for (let lookAhead = index + 1; lookAhead < messages.length; lookAhead += 1) {
|
|
628
|
+
const nextMessage = messages[lookAhead];
|
|
629
|
+
if (!nextMessage) continue;
|
|
630
|
+
if (nextMessage.role === 'assistant') {
|
|
631
|
+
response = nextMessage.content.trim();
|
|
632
|
+
index = lookAhead;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
if (nextMessage.role === 'user') {
|
|
636
|
+
reachedNextUser = true;
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Only pair fallback response with the final unresolved user message for this call.
|
|
642
|
+
if (!response && !reachedNextUser) {
|
|
643
|
+
response = fallbackResponse;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (!response) continue;
|
|
647
|
+
|
|
648
|
+
turns.push({
|
|
649
|
+
prompt,
|
|
650
|
+
response,
|
|
651
|
+
callId,
|
|
652
|
+
timestampMs,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return turns;
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const buildTranscriptTurnsFromCalls = (calls: Record<string, unknown>[]): TranscriptTurn[] => {
|
|
660
|
+
const orderedCalls = calls
|
|
661
|
+
.map((call, index) => ({
|
|
662
|
+
call,
|
|
663
|
+
index,
|
|
664
|
+
timestampMs: getCallTimestampMs(call, index),
|
|
665
|
+
}))
|
|
666
|
+
.sort((left, right) => {
|
|
667
|
+
if (left.timestampMs === right.timestampMs) return left.index - right.index;
|
|
668
|
+
return left.timestampMs - right.timestampMs;
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
let mergedTurns: TranscriptTurn[] = [];
|
|
672
|
+
|
|
673
|
+
for (const orderedCall of orderedCalls) {
|
|
674
|
+
const { call, timestampMs } = orderedCall;
|
|
675
|
+
const callId = getCallId(call);
|
|
676
|
+
const fallbackResponse = normalizeContentText(call.response).trim();
|
|
677
|
+
|
|
678
|
+
const parsedMessages = parseCallMessages(call.messages).filter(
|
|
679
|
+
message => !shouldSkipTranscriptMessage(message)
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
let callTurns = buildTurnsFromMessages(
|
|
683
|
+
parsedMessages,
|
|
684
|
+
fallbackResponse,
|
|
685
|
+
callId,
|
|
686
|
+
timestampMs,
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
if (callTurns.length === 0) {
|
|
690
|
+
const fallbackPrompt = normalizeContentText(call.prompt).trim();
|
|
691
|
+
if (fallbackPrompt && fallbackResponse) {
|
|
692
|
+
callTurns = [
|
|
693
|
+
{
|
|
694
|
+
prompt: fallbackPrompt,
|
|
695
|
+
response: fallbackResponse,
|
|
696
|
+
callId,
|
|
697
|
+
timestampMs,
|
|
698
|
+
},
|
|
699
|
+
];
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
mergedTurns = mergeTranscriptTurns(mergedTurns, callTurns);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return mergedTurns;
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const buildHistoryFromTranscriptTurns = (
|
|
710
|
+
turns: TranscriptTurn[],
|
|
711
|
+
): Record<string, { content: string; callId: string }> => {
|
|
712
|
+
const history: Record<string, { content: string; callId: string }> = {};
|
|
713
|
+
const keyUsageByPrompt = new Map<string, number>();
|
|
714
|
+
|
|
715
|
+
turns.forEach((turn, index) => {
|
|
716
|
+
const prompt = turn.prompt.trim();
|
|
717
|
+
const response = turn.response.trim();
|
|
718
|
+
if (!prompt || !response) return;
|
|
719
|
+
|
|
720
|
+
const baseTimestampMs = Number.isFinite(turn.timestampMs) ? turn.timestampMs : index;
|
|
721
|
+
const keySeed = `${baseTimestampMs}:${prompt}`;
|
|
722
|
+
const keyUsageCount = keyUsageByPrompt.get(keySeed) ?? 0;
|
|
723
|
+
keyUsageByPrompt.set(keySeed, keyUsageCount + 1);
|
|
724
|
+
|
|
725
|
+
const uniqueTimestamp = new Date(baseTimestampMs + keyUsageCount).toISOString();
|
|
726
|
+
const historyKey = `${uniqueTimestamp}:${prompt}`;
|
|
727
|
+
history[historyKey] = {
|
|
728
|
+
content: response,
|
|
729
|
+
callId: turn.callId,
|
|
730
|
+
};
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
return history;
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const truncatePromptForTitle = (prompt: string): string => {
|
|
737
|
+
if (prompt.length > 60) {
|
|
738
|
+
return `${prompt.slice(0, 57)}...`;
|
|
739
|
+
}
|
|
740
|
+
return prompt;
|
|
741
|
+
};
|
|
742
|
+
|
|
379
743
|
// Empty arrays/objects to prevent recreating on every render
|
|
380
744
|
const EMPTY_ARRAY: never[] = [];
|
|
381
745
|
const EMPTY_HISTORY: Record<string, { content: string; callId: string }> = {};
|
|
@@ -416,6 +780,7 @@ interface ChatPanelWrapperProps {
|
|
|
416
780
|
onToggleSection: (sectionId: string, enabled: boolean) => void;
|
|
417
781
|
// Conversation creation callback
|
|
418
782
|
onConversationCreated: (tempId: string, realId: string) => void;
|
|
783
|
+
onBeforeSend?: (payload: BeforeSendPayload) => Promise<void> | void;
|
|
419
784
|
// Per-conversation initial prompt
|
|
420
785
|
conversationInitialPrompt?: string;
|
|
421
786
|
// New props from ChatPanel port
|
|
@@ -434,6 +799,10 @@ interface ChatPanelWrapperProps {
|
|
|
434
799
|
callToActionEmailSubject?: string;
|
|
435
800
|
customerEmailCaptureMode?: "HIDE" | "OPTIONAL" | "REQUIRED";
|
|
436
801
|
customerEmailCapturePlaceholder?: string;
|
|
802
|
+
resolveMcpAuthHeaders?: MCPAuthHeaderResolver;
|
|
803
|
+
localToolExecutors?: Record<string, LocalToolExecutor>;
|
|
804
|
+
traceContextMode?: TraceContextMode;
|
|
805
|
+
autoApproveTools?: boolean | string[];
|
|
437
806
|
}
|
|
438
807
|
|
|
439
808
|
// Remove React.memo temporarily to debug - ChatPanelWrapper needs to re-render when agentId changes
|
|
@@ -468,6 +837,7 @@ const ChatPanelWrapper = (({
|
|
|
468
837
|
disabledSectionIds,
|
|
469
838
|
onToggleSection,
|
|
470
839
|
onConversationCreated,
|
|
840
|
+
onBeforeSend,
|
|
471
841
|
conversationInitialPrompt,
|
|
472
842
|
// New props from ChatPanel port
|
|
473
843
|
cssUrl,
|
|
@@ -485,6 +855,10 @@ const ChatPanelWrapper = (({
|
|
|
485
855
|
callToActionEmailSubject,
|
|
486
856
|
customerEmailCaptureMode,
|
|
487
857
|
customerEmailCapturePlaceholder,
|
|
858
|
+
resolveMcpAuthHeaders,
|
|
859
|
+
localToolExecutors,
|
|
860
|
+
traceContextMode,
|
|
861
|
+
autoApproveTools,
|
|
488
862
|
}) => {
|
|
489
863
|
const convAgentProfile = getAgent(activeConv.agentId);
|
|
490
864
|
const convAgentMetadata = convAgentProfile?.metadata;
|
|
@@ -513,6 +887,18 @@ const ChatPanelWrapper = (({
|
|
|
513
887
|
},
|
|
514
888
|
[onConversationCreated, activeConv.conversationId]
|
|
515
889
|
);
|
|
890
|
+
|
|
891
|
+
const beforeSendCallback = useCallback(
|
|
892
|
+
(payload: BeforeSendPayload) => {
|
|
893
|
+
if (!onBeforeSend) return;
|
|
894
|
+
return onBeforeSend({
|
|
895
|
+
...payload,
|
|
896
|
+
conversationId: payload.conversationId || activeConv.conversationId,
|
|
897
|
+
agentId: payload.agentId || activeConv.agentId,
|
|
898
|
+
});
|
|
899
|
+
},
|
|
900
|
+
[onBeforeSend, activeConv.conversationId, activeConv.agentId]
|
|
901
|
+
);
|
|
516
902
|
|
|
517
903
|
// Compute follow-on questions - MUST update when agent switches
|
|
518
904
|
// Don't use useMemo - compute fresh every render to ensure it always reflects current agent
|
|
@@ -595,6 +981,7 @@ const ChatPanelWrapper = (({
|
|
|
595
981
|
disabledSectionIds={disabledSectionIds}
|
|
596
982
|
onToggleSection={onToggleSection}
|
|
597
983
|
onConversationCreated={conversationCreatedCallback}
|
|
984
|
+
onBeforeSend={beforeSendCallback}
|
|
598
985
|
cssUrl={cssUrl}
|
|
599
986
|
markdownClass={markdownClass}
|
|
600
987
|
width={width}
|
|
@@ -610,6 +997,10 @@ const ChatPanelWrapper = (({
|
|
|
610
997
|
callToActionEmailSubject={callToActionEmailSubject ?? convAgentMetadata.displayCallToActionEmailSubject ?? 'Agent CTA submitted'}
|
|
611
998
|
customerEmailCaptureMode={customerEmailCaptureMode ?? convAgentMetadata?.customerEmailCaptureMode ?? 'HIDE'}
|
|
612
999
|
customerEmailCapturePlaceholder={customerEmailCapturePlaceholder ?? convAgentMetadata?.customerEmailCapturePlaceholder ?? 'Please enter your email...'}
|
|
1000
|
+
resolveMcpAuthHeaders={resolveMcpAuthHeaders}
|
|
1001
|
+
localToolExecutors={localToolExecutors}
|
|
1002
|
+
traceContextMode={traceContextMode}
|
|
1003
|
+
autoApproveTools={autoApproveTools}
|
|
613
1004
|
/>
|
|
614
1005
|
</div>
|
|
615
1006
|
);
|
|
@@ -668,6 +1059,7 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
668
1059
|
enableContextDetailView = false,
|
|
669
1060
|
onAgentSwitch,
|
|
670
1061
|
onConversationChange,
|
|
1062
|
+
onBeforeSend,
|
|
671
1063
|
historyChangedCallback,
|
|
672
1064
|
responseCompleteCallback,
|
|
673
1065
|
thumbsUpClick,
|
|
@@ -700,6 +1092,10 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
700
1092
|
callToActionEmailSubject,
|
|
701
1093
|
customerEmailCaptureMode,
|
|
702
1094
|
customerEmailCapturePlaceholder,
|
|
1095
|
+
resolveMcpAuthHeaders,
|
|
1096
|
+
localToolExecutors,
|
|
1097
|
+
traceContextMode = 'standard',
|
|
1098
|
+
autoApproveTools,
|
|
703
1099
|
}, ref) => {
|
|
704
1100
|
// Dev mode warnings for prop conflicts
|
|
705
1101
|
useEffect(() => {
|
|
@@ -731,13 +1127,8 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
731
1127
|
|
|
732
1128
|
// Use controlled value if provided, otherwise use internal state
|
|
733
1129
|
const isCollapsed = isControlled ? controlledIsCollapsed : uncontrolledIsCollapsed;
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const saved = localStorage.getItem('ai-agent-panel-history-collapsed');
|
|
737
|
-
return saved === 'true';
|
|
738
|
-
}
|
|
739
|
-
return false;
|
|
740
|
-
});
|
|
1130
|
+
// Keep history closed by default; users can explicitly open it per session.
|
|
1131
|
+
const [isHistoryCollapsed, setIsHistoryCollapsed] = useState(true);
|
|
741
1132
|
const [panelWidth, setPanelWidth] = useState(() => {
|
|
742
1133
|
if (typeof window !== 'undefined') {
|
|
743
1134
|
const savedWidth = localStorage.getItem('ai-agent-panel-width');
|
|
@@ -877,9 +1268,17 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
877
1268
|
const [conversationsError, setConversationsError] = useState<string | null>(null);
|
|
878
1269
|
const [searchQuery, setSearchQuery] = useState('');
|
|
879
1270
|
|
|
1271
|
+
const controlledConversationId =
|
|
1272
|
+
typeof conversation === 'string' && conversation.trim() !== ''
|
|
1273
|
+
? conversation.trim()
|
|
1274
|
+
: null;
|
|
1275
|
+
const isConversationControlled = controlledConversationId !== null;
|
|
1276
|
+
|
|
880
1277
|
// Multi-conversation state
|
|
881
1278
|
const [activeConversations, setActiveConversations] = useState<Map<string, ActiveConversation>>(new Map());
|
|
882
|
-
const [currentConversationId, setCurrentConversationId] = useState<string | null>(
|
|
1279
|
+
const [currentConversationId, setCurrentConversationId] = useState<string | null>(
|
|
1280
|
+
controlledConversationId
|
|
1281
|
+
);
|
|
883
1282
|
|
|
884
1283
|
// Store first prompts for conversations (conversationId -> firstPrompt)
|
|
885
1284
|
const [conversationFirstPrompts, setConversationFirstPrompts] = useState<Record<string, string>>({});
|
|
@@ -911,6 +1310,7 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
911
1310
|
|
|
912
1311
|
// Ref to track if fetch is in progress (prevents duplicate calls)
|
|
913
1312
|
const fetchInProgressRef = useRef(false);
|
|
1313
|
+
const loadingTranscriptIdsRef = useRef<Set<string>>(new Set());
|
|
914
1314
|
|
|
915
1315
|
// Ref to track the last agent we fetched for
|
|
916
1316
|
const lastFetchedAgentRef = useRef<string | null>(null);
|
|
@@ -932,32 +1332,62 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
932
1332
|
const currentConversationIdRef = useRef<string | null>(currentConversationId);
|
|
933
1333
|
currentConversationIdRef.current = currentConversationId;
|
|
934
1334
|
|
|
935
|
-
//
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1335
|
+
// Centralized conversation selection flow.
|
|
1336
|
+
// In controlled mode, we only update local selection for prop-driven updates (notify=false).
|
|
1337
|
+
const commitConversationSelection = useCallback(
|
|
1338
|
+
(conversationId: string | null, notifyChange: boolean) => {
|
|
1339
|
+
const shouldSyncLocalState =
|
|
1340
|
+
!isConversationControlled ||
|
|
1341
|
+
!notifyChange ||
|
|
1342
|
+
controlledConversationId === conversationId;
|
|
1343
|
+
|
|
1344
|
+
if (shouldSyncLocalState) {
|
|
1345
|
+
setCurrentConversationId(conversationId);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (notifyChange && conversationId && onConversationChange) {
|
|
1349
|
+
const shouldNotifyParent =
|
|
1350
|
+
!isConversationControlled || controlledConversationId !== conversationId;
|
|
1351
|
+
if (shouldNotifyParent) {
|
|
1352
|
+
onConversationChange(conversationId);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
[controlledConversationId, isConversationControlled, onConversationChange]
|
|
1357
|
+
);
|
|
1358
|
+
|
|
1359
|
+
const createDraftConversation = useCallback(
|
|
1360
|
+
(agentId: string, conversationInitialPrompt?: string): string => {
|
|
939
1361
|
const tempId = `new-${Date.now()}`;
|
|
940
|
-
|
|
1362
|
+
|
|
941
1363
|
setActiveConversations(prev => {
|
|
942
1364
|
const next = new Map(prev);
|
|
943
1365
|
next.set(tempId, {
|
|
944
1366
|
conversationId: tempId,
|
|
945
1367
|
stableKey: tempId,
|
|
946
|
-
agentId
|
|
1368
|
+
agentId,
|
|
947
1369
|
history: {},
|
|
1370
|
+
transcriptLoaded: true,
|
|
948
1371
|
isLoading: false,
|
|
949
1372
|
title: 'New conversation',
|
|
950
|
-
conversationInitialPrompt
|
|
1373
|
+
conversationInitialPrompt,
|
|
951
1374
|
});
|
|
952
1375
|
return next;
|
|
953
1376
|
});
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1377
|
+
|
|
1378
|
+
return tempId;
|
|
1379
|
+
},
|
|
1380
|
+
[]
|
|
1381
|
+
);
|
|
1382
|
+
|
|
1383
|
+
// Imperative handle for programmatic control
|
|
1384
|
+
React.useImperativeHandle(ref, () => ({
|
|
1385
|
+
startNewConversation: (prompt: string, agent?: string) => {
|
|
1386
|
+
const targetAgent = agent || currentAgentId;
|
|
1387
|
+
const tempId = createDraftConversation(targetAgent, prompt);
|
|
1388
|
+
commitConversationSelection(tempId, true);
|
|
959
1389
|
}
|
|
960
|
-
}), [
|
|
1390
|
+
}), [commitConversationSelection, createDraftConversation, currentAgentId]);
|
|
961
1391
|
|
|
962
1392
|
// Context change notification state
|
|
963
1393
|
const [showContextNotification, setShowContextNotification] = useState(false);
|
|
@@ -1162,15 +1592,25 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
1162
1592
|
|
|
1163
1593
|
// Helper to strip context/template data from a prompt - keeps only the user's actual message
|
|
1164
1594
|
// Load a specific conversation's transcript (or switch to it if already active)
|
|
1165
|
-
const loadConversationTranscript = useCallback(async (
|
|
1595
|
+
const loadConversationTranscript = useCallback(async (
|
|
1596
|
+
conversationId: string,
|
|
1597
|
+
agentIdForConversation?: string,
|
|
1598
|
+
title?: string,
|
|
1599
|
+
notifyConversationChange: boolean = true,
|
|
1600
|
+
) => {
|
|
1166
1601
|
// Check if conversation is already in activeConversations (use ref to avoid dependency)
|
|
1167
1602
|
const existingActive = activeConversationsRef.current.get(conversationId);
|
|
1168
|
-
if (
|
|
1603
|
+
if (
|
|
1604
|
+
existingActive &&
|
|
1605
|
+
(existingActive.transcriptLoaded || Object.keys(existingActive.history || {}).length > 0)
|
|
1606
|
+
) {
|
|
1169
1607
|
// Just switch to it, no API call needed
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1608
|
+
commitConversationSelection(conversationId, notifyConversationChange);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
if (loadingTranscriptIdsRef.current.has(conversationId)) {
|
|
1613
|
+
commitConversationSelection(conversationId, notifyConversationChange);
|
|
1174
1614
|
return;
|
|
1175
1615
|
}
|
|
1176
1616
|
|
|
@@ -1184,6 +1624,9 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
1184
1624
|
return;
|
|
1185
1625
|
}
|
|
1186
1626
|
|
|
1627
|
+
loadingTranscriptIdsRef.current.add(conversationId);
|
|
1628
|
+
setConversationsError(null);
|
|
1629
|
+
|
|
1187
1630
|
// Set loading state
|
|
1188
1631
|
setLoadingConversationId(conversationId);
|
|
1189
1632
|
|
|
@@ -1200,83 +1643,41 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
1200
1643
|
});
|
|
1201
1644
|
|
|
1202
1645
|
if (!response.ok) {
|
|
1646
|
+
if (response.status === 404) {
|
|
1647
|
+
// Allow externally supplied conversation/session IDs that have not yet produced calls.
|
|
1648
|
+
const conversationTitle =
|
|
1649
|
+
title || conversationFirstPrompts[conversationId] || 'New conversation';
|
|
1650
|
+
setActiveConversations(prev => {
|
|
1651
|
+
const next = new Map(prev);
|
|
1652
|
+
next.set(conversationId, {
|
|
1653
|
+
conversationId,
|
|
1654
|
+
stableKey: conversationId,
|
|
1655
|
+
agentId: agentIdToUse,
|
|
1656
|
+
history: {},
|
|
1657
|
+
transcriptLoaded: true,
|
|
1658
|
+
isLoading: false,
|
|
1659
|
+
title: conversationTitle,
|
|
1660
|
+
});
|
|
1661
|
+
return next;
|
|
1662
|
+
});
|
|
1663
|
+
commitConversationSelection(conversationId, notifyConversationChange);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1203
1666
|
throw new Error(`Failed to load conversation (${response.status})`);
|
|
1204
1667
|
}
|
|
1205
1668
|
|
|
1206
1669
|
const payload = await response.json();
|
|
1207
1670
|
console.log('loadConversationTranscript - API response:', payload);
|
|
1208
|
-
|
|
1209
|
-
// The /calls endpoint returns an array of calls
|
|
1210
|
-
// Use the messages property from the LAST call, which contains the full conversation history
|
|
1211
|
-
const history: Record<string, { content: string; callId: string }> = {};
|
|
1212
|
-
let firstPrompt: string | null = null;
|
|
1213
|
-
|
|
1214
|
-
if (Array.isArray(payload) && payload.length > 0) {
|
|
1215
|
-
// Get the last call - it contains the full conversation history in the messages property
|
|
1216
|
-
const lastCall = payload[payload.length - 1];
|
|
1217
|
-
const callId = lastCall.id || '';
|
|
1218
|
-
const timestamp = lastCall.createdAt || lastCall.created_at || new Date().toISOString();
|
|
1219
|
-
|
|
1220
|
-
if (lastCall.messages) {
|
|
1221
|
-
try {
|
|
1222
|
-
// Parse the messages JSON string
|
|
1223
|
-
const messages = JSON.parse(lastCall.messages) as { role: string; content: string }[];
|
|
1224
|
-
console.log('loadConversationTranscript - parsed messages:', messages);
|
|
1225
|
-
|
|
1226
|
-
// Filter out system messages and special __system__: user messages
|
|
1227
|
-
const relevantMessages = messages.filter(msg =>
|
|
1228
|
-
msg.role !== 'system' &&
|
|
1229
|
-
!(msg.role === 'user' && msg.content.startsWith('__system__:'))
|
|
1230
|
-
);
|
|
1231
|
-
console.log('loadConversationTranscript - filtered messages:', relevantMessages);
|
|
1232
|
-
|
|
1233
|
-
// Pair user/assistant messages to build history
|
|
1234
|
-
// Note: The messages array contains the INPUT to the LLM (history + new prompt)
|
|
1235
|
-
// The LAST user message needs to be paired with the call's response field
|
|
1236
|
-
for (let i = 0; i < relevantMessages.length; i++) {
|
|
1237
|
-
const msg = relevantMessages[i];
|
|
1238
|
-
|
|
1239
|
-
if (!msg) continue;
|
|
1240
|
-
|
|
1241
|
-
if (msg.role === 'user') {
|
|
1242
|
-
// Extract first prompt for conversation title
|
|
1243
|
-
if (!firstPrompt) {
|
|
1244
|
-
firstPrompt = msg.content.length > 60 ? msg.content.slice(0, 57) + '...' : msg.content;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
// Look for the next assistant message in the array
|
|
1248
|
-
const nextMsg = relevantMessages[i + 1];
|
|
1249
|
-
|
|
1250
|
-
if (nextMsg && nextMsg.role === 'assistant') {
|
|
1251
|
-
// This is a historical turn - pair the user message with the assistant message from the array
|
|
1252
|
-
const historyKey = `${timestamp}:${msg.content}`;
|
|
1253
|
-
history[historyKey] = {
|
|
1254
|
-
content: nextMsg.content,
|
|
1255
|
-
callId: callId,
|
|
1256
|
-
};
|
|
1257
|
-
|
|
1258
|
-
// Skip the assistant message we just processed
|
|
1259
|
-
i++;
|
|
1260
|
-
} else {
|
|
1261
|
-
// This is the LAST user message - pair it with the response field
|
|
1262
|
-
if (lastCall.response) {
|
|
1263
|
-
const historyKey = `${timestamp}:${msg.content}`;
|
|
1264
|
-
history[historyKey] = {
|
|
1265
|
-
content: lastCall.response,
|
|
1266
|
-
callId: callId,
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
} catch (err) {
|
|
1273
|
-
console.error('loadConversationTranscript - failed to parse messages property:', err);
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
1671
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1672
|
+
const calls = normalizeCallsPayload(payload);
|
|
1673
|
+
const transcriptTurns = buildTranscriptTurnsFromCalls(calls);
|
|
1674
|
+
const history = buildHistoryFromTranscriptTurns(transcriptTurns);
|
|
1675
|
+
const firstPrompt = transcriptTurns.length > 0
|
|
1676
|
+
? truncatePromptForTitle(transcriptTurns[0]!.prompt)
|
|
1677
|
+
: null;
|
|
1678
|
+
|
|
1679
|
+
console.log('loadConversationTranscript - parsed calls:', calls.length);
|
|
1680
|
+
console.log('loadConversationTranscript - created history entries:', Object.keys(history).length);
|
|
1280
1681
|
|
|
1281
1682
|
// Update first prompt in state if we found one
|
|
1282
1683
|
if (firstPrompt) {
|
|
@@ -1295,28 +1696,106 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
1295
1696
|
stableKey: conversationId, // Use real ID as stable key when loading from API
|
|
1296
1697
|
agentId: agentIdToUse,
|
|
1297
1698
|
history,
|
|
1699
|
+
transcriptLoaded: true,
|
|
1298
1700
|
isLoading: false,
|
|
1299
1701
|
title: conversationTitle,
|
|
1300
1702
|
});
|
|
1301
1703
|
return next;
|
|
1302
1704
|
});
|
|
1303
1705
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
if (onConversationChange) {
|
|
1307
|
-
onConversationChange(conversationId);
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// Clear loading state on success
|
|
1311
|
-
setLoadingConversationId(null);
|
|
1706
|
+
commitConversationSelection(conversationId, notifyConversationChange);
|
|
1312
1707
|
} catch (error: any) {
|
|
1313
1708
|
console.error('Failed to load conversation:', error);
|
|
1314
1709
|
setConversationsError(error.message || 'Failed to load conversation');
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
setLoadingConversationId(null);
|
|
1710
|
+
} finally {
|
|
1711
|
+
loadingTranscriptIdsRef.current.delete(conversationId);
|
|
1712
|
+
setLoadingConversationId(prev => (prev === conversationId ? null : prev));
|
|
1713
|
+
}
|
|
1714
|
+
}, [apiKey, commitConversationSelection, conversationFirstPrompts, currentAgentId, getAgent]);
|
|
1715
|
+
|
|
1716
|
+
// Controlled conversation mode: keep panel selection in sync with external conversation prop.
|
|
1717
|
+
useEffect(() => {
|
|
1718
|
+
if (!isConversationControlled) return;
|
|
1719
|
+
|
|
1720
|
+
const targetConversationId = controlledConversationId;
|
|
1721
|
+
if (!targetConversationId) {
|
|
1722
|
+
if (currentConversationIdRef.current !== null) {
|
|
1723
|
+
setCurrentConversationId(null);
|
|
1724
|
+
}
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
if (targetConversationId.startsWith('new-')) {
|
|
1729
|
+
setActiveConversations(prev => {
|
|
1730
|
+
if (prev.has(targetConversationId)) {
|
|
1731
|
+
return prev;
|
|
1732
|
+
}
|
|
1733
|
+
const next = new Map(prev);
|
|
1734
|
+
next.set(targetConversationId, {
|
|
1735
|
+
conversationId: targetConversationId,
|
|
1736
|
+
stableKey: targetConversationId,
|
|
1737
|
+
agentId: currentAgentId,
|
|
1738
|
+
history: {},
|
|
1739
|
+
transcriptLoaded: true,
|
|
1740
|
+
isLoading: false,
|
|
1741
|
+
title: 'New conversation',
|
|
1742
|
+
});
|
|
1743
|
+
return next;
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
if (currentConversationIdRef.current !== targetConversationId) {
|
|
1747
|
+
setCurrentConversationId(targetConversationId);
|
|
1748
|
+
}
|
|
1749
|
+
return;
|
|
1318
1750
|
}
|
|
1319
|
-
|
|
1751
|
+
|
|
1752
|
+
if (loadingConversationId === targetConversationId) {
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const existingActive = activeConversationsRef.current.get(targetConversationId);
|
|
1757
|
+
if (
|
|
1758
|
+
existingActive &&
|
|
1759
|
+
existingActive.transcriptLoaded &&
|
|
1760
|
+
currentConversationIdRef.current === targetConversationId
|
|
1761
|
+
) {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
const apiConversation = apiConversations.find(
|
|
1766
|
+
conv => conv.conversationId === targetConversationId
|
|
1767
|
+
);
|
|
1768
|
+
const targetAgentId =
|
|
1769
|
+
apiConversation?.agentId || existingActive?.agentId || currentAgentId;
|
|
1770
|
+
|
|
1771
|
+
if (!targetAgentId) {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
if (targetAgentId !== currentAgentId) {
|
|
1776
|
+
setCurrentAgentId(targetAgentId);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
const targetTitle =
|
|
1780
|
+
conversationFirstPrompts[targetConversationId] ||
|
|
1781
|
+
apiConversation?.title ||
|
|
1782
|
+
existingActive?.title;
|
|
1783
|
+
|
|
1784
|
+
void loadConversationTranscript(
|
|
1785
|
+
targetConversationId,
|
|
1786
|
+
targetAgentId,
|
|
1787
|
+
targetTitle,
|
|
1788
|
+
false,
|
|
1789
|
+
);
|
|
1790
|
+
}, [
|
|
1791
|
+
apiConversations,
|
|
1792
|
+
controlledConversationId,
|
|
1793
|
+
conversationFirstPrompts,
|
|
1794
|
+
currentAgentId,
|
|
1795
|
+
isConversationControlled,
|
|
1796
|
+
loadConversationTranscript,
|
|
1797
|
+
loadingConversationId,
|
|
1798
|
+
]);
|
|
1320
1799
|
|
|
1321
1800
|
|
|
1322
1801
|
// Refresh conversations callback
|
|
@@ -1345,35 +1824,21 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
1345
1824
|
|
|
1346
1825
|
// Start new conversation
|
|
1347
1826
|
const handleNewConversation = useCallback(() => {
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
// Add to active conversations with empty history
|
|
1352
|
-
setActiveConversations(prev => {
|
|
1353
|
-
const next = new Map(prev);
|
|
1354
|
-
next.set(tempId, {
|
|
1355
|
-
conversationId: tempId,
|
|
1356
|
-
stableKey: tempId, // Stable key never changes even when conversationId updates
|
|
1357
|
-
agentId: currentAgentId,
|
|
1358
|
-
history: {},
|
|
1359
|
-
isLoading: false,
|
|
1360
|
-
title: 'New conversation',
|
|
1361
|
-
});
|
|
1362
|
-
return next;
|
|
1363
|
-
});
|
|
1364
|
-
|
|
1365
|
-
setCurrentConversationId(tempId);
|
|
1366
|
-
}, [currentAgentId]);
|
|
1827
|
+
const tempId = createDraftConversation(currentAgentId);
|
|
1828
|
+
commitConversationSelection(tempId, true);
|
|
1829
|
+
}, [commitConversationSelection, createDraftConversation, currentAgentId]);
|
|
1367
1830
|
|
|
1368
1831
|
// Auto-start a new conversation when none exist and agent is ready
|
|
1369
1832
|
useEffect(() => {
|
|
1833
|
+
if (isConversationControlled) return;
|
|
1834
|
+
|
|
1370
1835
|
const agentProfile = getAgent(currentAgentId);
|
|
1371
1836
|
const isAgentReady = agentProfile?.metadata?.projectId;
|
|
1372
1837
|
|
|
1373
1838
|
if (isAgentReady && !agentsLoading && activeConversations.size === 0) {
|
|
1374
1839
|
handleNewConversation();
|
|
1375
1840
|
}
|
|
1376
|
-
}, [currentAgentId, agentsLoading, activeConversations.size, getAgent, handleNewConversation]);
|
|
1841
|
+
}, [currentAgentId, agentsLoading, activeConversations.size, getAgent, handleNewConversation, isConversationControlled]);
|
|
1377
1842
|
|
|
1378
1843
|
// Close an active conversation
|
|
1379
1844
|
const handleCloseConversation = useCallback((conversationId: string, e?: React.MouseEvent) => {
|
|
@@ -1391,9 +1856,13 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
1391
1856
|
if (currentConversationIdRef.current === conversationId) {
|
|
1392
1857
|
const remaining = Array.from(activeConversationsRef.current.keys()).filter(id => id !== conversationId);
|
|
1393
1858
|
const nextId = remaining.length > 0 ? remaining[0] ?? null : null;
|
|
1394
|
-
|
|
1859
|
+
if (nextId) {
|
|
1860
|
+
commitConversationSelection(nextId, true);
|
|
1861
|
+
} else if (!isConversationControlled) {
|
|
1862
|
+
setCurrentConversationId(null);
|
|
1863
|
+
}
|
|
1395
1864
|
}
|
|
1396
|
-
}, []);
|
|
1865
|
+
}, [commitConversationSelection, isConversationControlled]);
|
|
1397
1866
|
|
|
1398
1867
|
// Select conversation
|
|
1399
1868
|
const handleSelectConversation = useCallback((conversationId: string) => {
|
|
@@ -1830,6 +2299,7 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
1830
2299
|
next.set(targetConversationId, {
|
|
1831
2300
|
...existing,
|
|
1832
2301
|
history,
|
|
2302
|
+
transcriptLoaded: true,
|
|
1833
2303
|
title,
|
|
1834
2304
|
});
|
|
1835
2305
|
return next;
|
|
@@ -1941,18 +2411,21 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
1941
2411
|
|
|
1942
2412
|
// Update currentConversationId if it was the temp one
|
|
1943
2413
|
if (currentConversationIdRef.current === tempId) {
|
|
1944
|
-
|
|
1945
|
-
if (onConversationChange) {
|
|
1946
|
-
onConversationChange(realId);
|
|
1947
|
-
}
|
|
2414
|
+
commitConversationSelection(realId, true);
|
|
1948
2415
|
}
|
|
1949
|
-
}, [
|
|
2416
|
+
}, [commitConversationSelection]);
|
|
1950
2417
|
|
|
1951
2418
|
// Toggle collapse - only if collapsible is enabled
|
|
1952
2419
|
const toggleCollapse = useCallback(() => {
|
|
1953
2420
|
if (!collapsible) return;
|
|
1954
2421
|
|
|
1955
2422
|
const newValue = !isCollapsed;
|
|
2423
|
+
|
|
2424
|
+
// Expanding the main panel should not implicitly reopen conversation history.
|
|
2425
|
+
if (!newValue) {
|
|
2426
|
+
setIsHistoryCollapsed(true);
|
|
2427
|
+
setShowSearch(false);
|
|
2428
|
+
}
|
|
1956
2429
|
|
|
1957
2430
|
// Update internal state if uncontrolled
|
|
1958
2431
|
if (!isControlled) {
|
|
@@ -1965,11 +2438,7 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
1965
2438
|
|
|
1966
2439
|
// Toggle history collapse
|
|
1967
2440
|
const toggleHistoryCollapse = useCallback(() => {
|
|
1968
|
-
setIsHistoryCollapsed((prev) =>
|
|
1969
|
-
const next = !prev;
|
|
1970
|
-
localStorage.setItem('ai-agent-panel-history-collapsed', String(next));
|
|
1971
|
-
return next;
|
|
1972
|
-
});
|
|
2441
|
+
setIsHistoryCollapsed((prev) => !prev);
|
|
1973
2442
|
}, []);
|
|
1974
2443
|
|
|
1975
2444
|
// Panel classes
|
|
@@ -2055,38 +2524,23 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
2055
2524
|
variant={agent.id === currentAgentId ? 'secondary' : 'ghost'}
|
|
2056
2525
|
size="icon"
|
|
2057
2526
|
title={agent.name}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
}
|
|
2063
|
-
onCollapsedChange?.(false);
|
|
2064
|
-
if (hasActiveConversation && activeConvForAgent) {
|
|
2065
|
-
// Switch to the existing conversation
|
|
2066
|
-
setCurrentConversationId(activeConvForAgent.conversationId);
|
|
2067
|
-
setCurrentAgentId(agent.id);
|
|
2068
|
-
if (onConversationChange) {
|
|
2069
|
-
onConversationChange(activeConvForAgent.conversationId);
|
|
2527
|
+
onClick={() => {
|
|
2528
|
+
// Expand panel in controlled or uncontrolled mode
|
|
2529
|
+
if (!isControlled) {
|
|
2530
|
+
setUncontrolledIsCollapsed(false);
|
|
2070
2531
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
});
|
|
2084
|
-
return next;
|
|
2085
|
-
});
|
|
2086
|
-
setCurrentConversationId(tempId);
|
|
2087
|
-
setCurrentAgentId(agent.id);
|
|
2088
|
-
}
|
|
2089
|
-
}}
|
|
2532
|
+
onCollapsedChange?.(false);
|
|
2533
|
+
if (hasActiveConversation && activeConvForAgent) {
|
|
2534
|
+
// Switch to the existing conversation
|
|
2535
|
+
setCurrentAgentId(agent.id);
|
|
2536
|
+
commitConversationSelection(activeConvForAgent.conversationId, true);
|
|
2537
|
+
} else {
|
|
2538
|
+
// Start a new conversation with this agent
|
|
2539
|
+
const tempId = createDraftConversation(agent.id);
|
|
2540
|
+
commitConversationSelection(tempId, true);
|
|
2541
|
+
setCurrentAgentId(agent.id);
|
|
2542
|
+
}
|
|
2543
|
+
}}
|
|
2090
2544
|
className={`ai-agent-panel__agent-icon ${hasActiveConversation ? 'ai-agent-panel__agent-icon--active' : ''}`}
|
|
2091
2545
|
>
|
|
2092
2546
|
{agent.avatarUrl ? (
|
|
@@ -2184,27 +2638,12 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
2184
2638
|
onClick={() => {
|
|
2185
2639
|
if (hasActiveConversation && activeConvForAgent) {
|
|
2186
2640
|
// Switch to the existing conversation
|
|
2187
|
-
setCurrentConversationId(activeConvForAgent.conversationId);
|
|
2188
2641
|
setCurrentAgentId(agent.id);
|
|
2189
|
-
|
|
2190
|
-
onConversationChange(activeConvForAgent.conversationId);
|
|
2191
|
-
}
|
|
2642
|
+
commitConversationSelection(activeConvForAgent.conversationId, true);
|
|
2192
2643
|
} else {
|
|
2193
2644
|
// Start a new conversation with this agent
|
|
2194
|
-
const tempId =
|
|
2195
|
-
|
|
2196
|
-
const next = new Map(prev);
|
|
2197
|
-
next.set(tempId, {
|
|
2198
|
-
conversationId: tempId,
|
|
2199
|
-
stableKey: tempId, // Stable key never changes
|
|
2200
|
-
agentId: agent.id,
|
|
2201
|
-
history: {},
|
|
2202
|
-
isLoading: false,
|
|
2203
|
-
title: 'New conversation',
|
|
2204
|
-
});
|
|
2205
|
-
return next;
|
|
2206
|
-
});
|
|
2207
|
-
setCurrentConversationId(tempId);
|
|
2645
|
+
const tempId = createDraftConversation(agent.id);
|
|
2646
|
+
commitConversationSelection(tempId, true);
|
|
2208
2647
|
setCurrentAgentId(agent.id);
|
|
2209
2648
|
}
|
|
2210
2649
|
}}
|
|
@@ -2292,10 +2731,7 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
2292
2731
|
: ''
|
|
2293
2732
|
}`}
|
|
2294
2733
|
onClick={() => {
|
|
2295
|
-
|
|
2296
|
-
if (onConversationChange) {
|
|
2297
|
-
onConversationChange(activeConv.conversationId);
|
|
2298
|
-
}
|
|
2734
|
+
commitConversationSelection(activeConv.conversationId, true);
|
|
2299
2735
|
}}
|
|
2300
2736
|
>
|
|
2301
2737
|
<div className="ai-agent-panel__conversation-content">
|
|
@@ -2353,12 +2789,12 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
2353
2789
|
{expandedSections[group.label] ? '▼' : '▶'}
|
|
2354
2790
|
</span>
|
|
2355
2791
|
</div>
|
|
2356
|
-
{expandedSections[group.label] && group.conversations.length > 0 && group.conversations.map((conv) => {
|
|
2792
|
+
{expandedSections[group.label] && group.conversations.length > 0 && group.conversations.map((conv, convIndex) => {
|
|
2357
2793
|
// Check if this conversation is already active
|
|
2358
2794
|
const isActive = activeConversations.has(conv.conversationId);
|
|
2359
2795
|
return (
|
|
2360
2796
|
<div
|
|
2361
|
-
key={conv.conversationId}
|
|
2797
|
+
key={`${group.label}-${conv.conversationId}-${convIndex}`}
|
|
2362
2798
|
className={`ai-agent-panel__conversation ${
|
|
2363
2799
|
currentConversationId === conv.conversationId
|
|
2364
2800
|
? 'ai-agent-panel__conversation--current'
|
|
@@ -2411,7 +2847,7 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
2411
2847
|
{/* Chat panels - one per active conversation, shown/hidden via CSS */}
|
|
2412
2848
|
{activeConversationsList.map((activeConv) => (
|
|
2413
2849
|
<ChatPanelWrapper
|
|
2414
|
-
key={
|
|
2850
|
+
key={activeConv.stableKey}
|
|
2415
2851
|
activeConv={activeConv}
|
|
2416
2852
|
currentConversationId={currentConversationId}
|
|
2417
2853
|
getAgent={getAgent}
|
|
@@ -2442,6 +2878,7 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
2442
2878
|
disabledSectionIds={currentDisabledSections}
|
|
2443
2879
|
onToggleSection={handleContextSectionToggle}
|
|
2444
2880
|
onConversationCreated={handleConversationCreated}
|
|
2881
|
+
onBeforeSend={onBeforeSend}
|
|
2445
2882
|
conversationInitialPrompt={activeConv.conversationInitialPrompt}
|
|
2446
2883
|
cssUrl={cssUrl}
|
|
2447
2884
|
markdownClass={markdownClass}
|
|
@@ -2455,10 +2892,14 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
2455
2892
|
showCallToAction={showCallToAction}
|
|
2456
2893
|
callToActionButtonText={callToActionButtonText}
|
|
2457
2894
|
callToActionEmailAddress={callToActionEmailAddress}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2895
|
+
callToActionEmailSubject={callToActionEmailSubject}
|
|
2896
|
+
customerEmailCaptureMode={customerEmailCaptureMode}
|
|
2897
|
+
customerEmailCapturePlaceholder={customerEmailCapturePlaceholder}
|
|
2898
|
+
resolveMcpAuthHeaders={resolveMcpAuthHeaders}
|
|
2899
|
+
localToolExecutors={localToolExecutors}
|
|
2900
|
+
traceContextMode={traceContextMode}
|
|
2901
|
+
autoApproveTools={autoApproveTools}
|
|
2902
|
+
/>
|
|
2462
2903
|
))}
|
|
2463
2904
|
|
|
2464
2905
|
{/* Conversation loading overlay */}
|
|
@@ -2520,4 +2961,3 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
|
|
|
2520
2961
|
AIAgentPanel.displayName = 'AIAgentPanel';
|
|
2521
2962
|
|
|
2522
2963
|
export default AIAgentPanel;
|
|
2523
|
-
|