@hef2024/llmasaservice-ui 0.20.0 → 0.20.2
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/AICHATPANEL-PORT-INVENTORY.md +1 -0
- package/DEBUG-ERROR-HANDLING.md +1 -0
- package/FIX-APPLIED.md +1 -0
- package/IMPLEMENTATION-COMPLETE.md +1 -0
- package/dist/index.css +511 -0
- package/dist/index.d.mts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +1181 -451
- package/dist/index.mjs +1012 -282
- package/docs/CHANGELOG-ERROR-HANDLING.md +1 -0
- package/docs/CONVERSATION-HISTORY.md +1 -0
- package/docs/ERROR-HANDLING-413.md +1 -0
- package/docs/ERROR-HANDLING-SUMMARY.md +1 -0
- package/package.json +2 -2
- package/src/AIAgentPanel.tsx +186 -9
- package/src/AIChatPanel.css +618 -0
- package/src/AIChatPanel.tsx +723 -38
- package/src/AgentPanel.tsx +3 -1
- package/src/ChatPanel.tsx +69 -29
- package/src/components/ui/Button.tsx +1 -0
- package/src/components/ui/Dialog.tsx +1 -0
- package/src/components/ui/Input.tsx +1 -0
- package/src/components/ui/Select.tsx +1 -0
- package/src/components/ui/ToolInfoModal.tsx +69 -0
- package/src/components/ui/Tooltip.tsx +1 -0
- package/src/components/ui/index.ts +1 -0
package/src/AIChatPanel.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import React, {
|
|
|
11
11
|
useRef,
|
|
12
12
|
useState,
|
|
13
13
|
} from 'react';
|
|
14
|
+
import ReactDOMServer from 'react-dom/server';
|
|
14
15
|
import { LLMAsAServiceCustomer, useLLM } from 'llmasaservice-client';
|
|
15
16
|
import ReactMarkdown from 'react-markdown';
|
|
16
17
|
import remarkGfm from 'remark-gfm';
|
|
@@ -19,6 +20,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
|
19
20
|
import materialDark from 'react-syntax-highlighter/dist/esm/styles/prism/material-dark.js';
|
|
20
21
|
import materialLight from 'react-syntax-highlighter/dist/esm/styles/prism/material-light.js';
|
|
21
22
|
import { Button, ScrollArea, Tooltip } from './components/ui';
|
|
23
|
+
import ToolInfoModal from './components/ui/ToolInfoModal';
|
|
22
24
|
import './AIChatPanel.css';
|
|
23
25
|
|
|
24
26
|
// ============================================================================
|
|
@@ -86,9 +88,34 @@ export interface AIChatPanelProps {
|
|
|
86
88
|
totalContextTokens?: number;
|
|
87
89
|
maxContextTokens?: number;
|
|
88
90
|
enableContextDetailView?: boolean;
|
|
91
|
+
disabledSectionIds?: Set<string>;
|
|
92
|
+
onToggleSection?: (sectionId: string, enabled: boolean) => void;
|
|
89
93
|
|
|
90
94
|
// Callback when a new conversation is created via API
|
|
91
95
|
onConversationCreated?: (conversationId: string) => void;
|
|
96
|
+
|
|
97
|
+
// UI Customization Props (from ChatPanel)
|
|
98
|
+
cssUrl?: string;
|
|
99
|
+
markdownClass?: string;
|
|
100
|
+
width?: string;
|
|
101
|
+
height?: string;
|
|
102
|
+
scrollToEnd?: boolean;
|
|
103
|
+
prismStyle?: any; // PrismStyle type from react-syntax-highlighter
|
|
104
|
+
|
|
105
|
+
// Email & Save Props
|
|
106
|
+
showSaveButton?: boolean;
|
|
107
|
+
showEmailButton?: boolean;
|
|
108
|
+
messages?: { role: "user" | "assistant"; content: string }[];
|
|
109
|
+
|
|
110
|
+
// Call-to-Action Props
|
|
111
|
+
showCallToAction?: boolean;
|
|
112
|
+
callToActionButtonText?: string;
|
|
113
|
+
callToActionEmailAddress?: string;
|
|
114
|
+
callToActionEmailSubject?: string;
|
|
115
|
+
|
|
116
|
+
// Customer Email Capture Props
|
|
117
|
+
customerEmailCaptureMode?: "HIDE" | "OPTIONAL" | "REQUIRED";
|
|
118
|
+
customerEmailCapturePlaceholder?: string;
|
|
92
119
|
}
|
|
93
120
|
|
|
94
121
|
/**
|
|
@@ -270,6 +297,8 @@ interface ChatInputProps {
|
|
|
270
297
|
totalContextTokens?: number;
|
|
271
298
|
maxContextTokens?: number;
|
|
272
299
|
enableContextDetailView?: boolean;
|
|
300
|
+
disabledSectionIds?: Set<string>;
|
|
301
|
+
onToggleSection?: (sectionId: string, enabled: boolean) => void;
|
|
273
302
|
onContextViewerToggle?: () => void;
|
|
274
303
|
}
|
|
275
304
|
|
|
@@ -288,11 +317,14 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
288
317
|
totalContextTokens = 0,
|
|
289
318
|
maxContextTokens = 8000,
|
|
290
319
|
enableContextDetailView = false,
|
|
320
|
+
disabledSectionIds = new Set(),
|
|
321
|
+
onToggleSection,
|
|
291
322
|
}) => {
|
|
292
323
|
const [inputValue, setInputValue] = useState('');
|
|
293
324
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
294
325
|
const [contextViewerOpen, setContextViewerOpen] = useState(false);
|
|
295
326
|
const [contextViewMode, setContextViewMode] = useState<'summary' | 'detail'>('summary');
|
|
327
|
+
const [expandedSectionId, setExpandedSectionId] = useState<string | null>(null);
|
|
296
328
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
297
329
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
298
330
|
const contextPopoverRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -338,6 +370,7 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
338
370
|
if (contextPopoverRef.current && !contextPopoverRef.current.contains(event.target as Node)) {
|
|
339
371
|
setContextViewerOpen(false);
|
|
340
372
|
setContextViewMode('summary');
|
|
373
|
+
setExpandedSectionId(null);
|
|
341
374
|
}
|
|
342
375
|
};
|
|
343
376
|
if (contextViewerOpen) {
|
|
@@ -441,6 +474,9 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
441
474
|
setContextViewerOpen(!contextViewerOpen);
|
|
442
475
|
if (!contextViewerOpen) {
|
|
443
476
|
setContextViewMode('summary');
|
|
477
|
+
setExpandedSectionId(null);
|
|
478
|
+
} else {
|
|
479
|
+
setExpandedSectionId(null);
|
|
444
480
|
}
|
|
445
481
|
}}
|
|
446
482
|
type="button"
|
|
@@ -463,7 +499,10 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
463
499
|
<span className="ai-chat-context-popover__title">Context</span>
|
|
464
500
|
<button
|
|
465
501
|
className="ai-chat-context-popover__close"
|
|
466
|
-
onClick={() =>
|
|
502
|
+
onClick={() => {
|
|
503
|
+
setContextViewerOpen(false);
|
|
504
|
+
setExpandedSectionId(null);
|
|
505
|
+
}}
|
|
467
506
|
type="button"
|
|
468
507
|
>
|
|
469
508
|
×
|
|
@@ -492,6 +531,7 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
492
531
|
className={`ai-chat-context-popover__section-item ${enableContextDetailView ? 'ai-chat-context-popover__section-item--clickable' : ''}`}
|
|
493
532
|
onClick={() => {
|
|
494
533
|
if (enableContextDetailView) {
|
|
534
|
+
setExpandedSectionId(section.id);
|
|
495
535
|
setContextViewMode('detail');
|
|
496
536
|
}
|
|
497
537
|
}}
|
|
@@ -507,7 +547,10 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
507
547
|
{enableContextDetailView && (
|
|
508
548
|
<button
|
|
509
549
|
className="ai-chat-context-popover__expand-btn"
|
|
510
|
-
onClick={() =>
|
|
550
|
+
onClick={() => {
|
|
551
|
+
setExpandedSectionId(null);
|
|
552
|
+
setContextViewMode('detail');
|
|
553
|
+
}}
|
|
511
554
|
type="button"
|
|
512
555
|
>
|
|
513
556
|
View details →
|
|
@@ -522,7 +565,10 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
522
565
|
<div className="ai-chat-context-popover__header">
|
|
523
566
|
<button
|
|
524
567
|
className="ai-chat-context-popover__back"
|
|
525
|
-
onClick={() =>
|
|
568
|
+
onClick={() => {
|
|
569
|
+
setContextViewMode('summary');
|
|
570
|
+
setExpandedSectionId(null);
|
|
571
|
+
}}
|
|
526
572
|
type="button"
|
|
527
573
|
>
|
|
528
574
|
← Back
|
|
@@ -530,7 +576,10 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
530
576
|
<span className="ai-chat-context-popover__title">Context Details</span>
|
|
531
577
|
<button
|
|
532
578
|
className="ai-chat-context-popover__close"
|
|
533
|
-
onClick={() =>
|
|
579
|
+
onClick={() => {
|
|
580
|
+
setContextViewerOpen(false);
|
|
581
|
+
setExpandedSectionId(null);
|
|
582
|
+
}}
|
|
534
583
|
type="button"
|
|
535
584
|
>
|
|
536
585
|
×
|
|
@@ -555,10 +604,35 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
555
604
|
<div className="ai-chat-context-popover__detail-sections">
|
|
556
605
|
{contextSections.map((section) => {
|
|
557
606
|
const format = detectFormat(section.data);
|
|
607
|
+
const isEnabled = !disabledSectionIds.has(section.id);
|
|
558
608
|
return (
|
|
559
|
-
<details
|
|
609
|
+
<details
|
|
610
|
+
key={section.id}
|
|
611
|
+
className={`ai-chat-context-popover__detail-section ${!isEnabled ? 'ai-chat-context-popover__detail-section--disabled' : ''}`}
|
|
612
|
+
open={expandedSectionId === section.id}
|
|
613
|
+
>
|
|
560
614
|
<summary className="ai-chat-context-popover__detail-section-header">
|
|
561
|
-
<
|
|
615
|
+
<div className="ai-chat-context-popover__detail-section-title-row">
|
|
616
|
+
<span className="ai-chat-context-popover__detail-section-title">{section.title}</span>
|
|
617
|
+
<label
|
|
618
|
+
className="ai-chat-context-toggle"
|
|
619
|
+
onClick={(e) => e.stopPropagation()}
|
|
620
|
+
title={isEnabled ? "Disable this context section" : "Enable this context section"}
|
|
621
|
+
>
|
|
622
|
+
<input
|
|
623
|
+
type="checkbox"
|
|
624
|
+
checked={isEnabled}
|
|
625
|
+
onChange={(e) => {
|
|
626
|
+
e.stopPropagation();
|
|
627
|
+
if (onToggleSection) {
|
|
628
|
+
onToggleSection(section.id, !isEnabled);
|
|
629
|
+
}
|
|
630
|
+
}}
|
|
631
|
+
className="ai-chat-context-toggle__input"
|
|
632
|
+
/>
|
|
633
|
+
<span className="ai-chat-context-toggle__slider"></span>
|
|
634
|
+
</label>
|
|
635
|
+
</div>
|
|
562
636
|
<span className="ai-chat-context-popover__detail-section-meta">
|
|
563
637
|
<code>{`{{${section.id}}}`}</code>
|
|
564
638
|
<span>~{section.tokens || Math.ceil(JSON.stringify(section.data).length / 4)}</span>
|
|
@@ -676,7 +750,28 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
676
750
|
totalContextTokens = 0,
|
|
677
751
|
maxContextTokens = 8000,
|
|
678
752
|
enableContextDetailView = false,
|
|
753
|
+
disabledSectionIds: propDisabledSectionIds,
|
|
754
|
+
onToggleSection: propOnToggleSection,
|
|
679
755
|
onConversationCreated,
|
|
756
|
+
// UI Customization Props
|
|
757
|
+
cssUrl,
|
|
758
|
+
markdownClass,
|
|
759
|
+
width,
|
|
760
|
+
height,
|
|
761
|
+
scrollToEnd = false,
|
|
762
|
+
prismStyle,
|
|
763
|
+
// Email & Save Props
|
|
764
|
+
showSaveButton = true,
|
|
765
|
+
showEmailButton = true,
|
|
766
|
+
messages = [],
|
|
767
|
+
// Call-to-Action Props
|
|
768
|
+
showCallToAction = false,
|
|
769
|
+
callToActionButtonText = 'Submit',
|
|
770
|
+
callToActionEmailAddress = '',
|
|
771
|
+
callToActionEmailSubject = 'Agent CTA submitted',
|
|
772
|
+
// Customer Email Capture Props
|
|
773
|
+
customerEmailCaptureMode = 'HIDE',
|
|
774
|
+
customerEmailCapturePlaceholder = 'Please enter your email...',
|
|
680
775
|
}) => {
|
|
681
776
|
// ============================================================================
|
|
682
777
|
// API URL
|
|
@@ -703,6 +798,45 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
703
798
|
const [feedbackCallId, setFeedbackCallId] = useState<{ callId: string; type: 'up' | 'down' } | null>(null);
|
|
704
799
|
const [error, setError] = useState<{ message: string; code?: string } | null>(null);
|
|
705
800
|
|
|
801
|
+
// Email & Save state
|
|
802
|
+
const [emailSent, setEmailSent] = useState(false);
|
|
803
|
+
|
|
804
|
+
// Tool Info Modal state
|
|
805
|
+
const [isToolInfoModalOpen, setIsToolInfoModalOpen] = useState(false);
|
|
806
|
+
const [toolInfoData, setToolInfoData] = useState<{ calls: any[]; responses: any[] } | null>(null);
|
|
807
|
+
|
|
808
|
+
// Call-to-Action state
|
|
809
|
+
const [callToActionSent, setCallToActionSent] = useState(false);
|
|
810
|
+
const [CTAClickedButNoEmail, setCTAClickedButNoEmail] = useState(false);
|
|
811
|
+
|
|
812
|
+
// Customer Email Capture state
|
|
813
|
+
const [emailInput, setEmailInput] = useState(customer?.customer_user_email ?? '');
|
|
814
|
+
const [emailInputSet, setEmailInputSet] = useState(false);
|
|
815
|
+
const [emailValid, setEmailValid] = useState(true);
|
|
816
|
+
const [showEmailPanel, setShowEmailPanel] = useState(customerEmailCaptureMode !== 'HIDE');
|
|
817
|
+
const [emailClickedButNoEmail, setEmailClickedButNoEmail] = useState(false);
|
|
818
|
+
|
|
819
|
+
// Tool Approval state (for MCP tools)
|
|
820
|
+
const [pendingToolRequests, setPendingToolRequests] = useState<any[]>([]);
|
|
821
|
+
const [sessionApprovedTools, setSessionApprovedTools] = useState<string[]>([]);
|
|
822
|
+
const [alwaysApprovedTools, setAlwaysApprovedTools] = useState<string[]>([]);
|
|
823
|
+
|
|
824
|
+
// Context section toggle state (disabled sections)
|
|
825
|
+
// Use internal state only if prop is not provided
|
|
826
|
+
const [internalDisabledSectionIds, setInternalDisabledSectionIds] = useState<Set<string>>(new Set());
|
|
827
|
+
const disabledSectionIds = propDisabledSectionIds ?? internalDisabledSectionIds;
|
|
828
|
+
|
|
829
|
+
// Email capture mode effect - like ChatPanel
|
|
830
|
+
useEffect(() => {
|
|
831
|
+
setShowEmailPanel(customerEmailCaptureMode !== 'HIDE');
|
|
832
|
+
|
|
833
|
+
if (customerEmailCaptureMode === 'REQUIRED') {
|
|
834
|
+
if (!isEmailAddress(emailInput)) {
|
|
835
|
+
setEmailValid(false);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}, [customerEmailCaptureMode, emailInput]);
|
|
839
|
+
|
|
706
840
|
// Refs
|
|
707
841
|
const bottomRef = useRef<HTMLDivElement | null>(null);
|
|
708
842
|
const responseAreaRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -722,6 +856,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
722
856
|
const latestHistoryRef = useRef<Record<string, HistoryEntry>>(initialHistory);
|
|
723
857
|
// Track if we've sent the initial prompt (prevents loops)
|
|
724
858
|
const initialPromptSentRef = useRef<boolean>(false);
|
|
859
|
+
// Track the last followOnPrompt to detect changes (for auto-submit trigger)
|
|
860
|
+
const lastFollowOnPromptRef = useRef<string>('');
|
|
725
861
|
|
|
726
862
|
// Sync new entries from initialHistory into local history state
|
|
727
863
|
// This allows parent components to inject messages (e.g., page-based agent suggestions)
|
|
@@ -796,9 +932,9 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
796
932
|
// ============================================================================
|
|
797
933
|
// Memoized Values
|
|
798
934
|
// ============================================================================
|
|
799
|
-
const
|
|
800
|
-
() => (theme === 'light' ? materialLight : materialDark),
|
|
801
|
-
[theme]
|
|
935
|
+
const effectivePrismStyle = useMemo(
|
|
936
|
+
() => prismStyle ?? (theme === 'light' ? materialLight : materialDark),
|
|
937
|
+
[prismStyle, theme]
|
|
802
938
|
);
|
|
803
939
|
|
|
804
940
|
// Browser info for context (matches ChatPanel)
|
|
@@ -811,6 +947,24 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
811
947
|
};
|
|
812
948
|
}, []);
|
|
813
949
|
|
|
950
|
+
// Handle toggling context sections on/off
|
|
951
|
+
const handleToggleSection = useCallback((sectionId: string, enabled: boolean) => {
|
|
952
|
+
// Use prop callback if provided, otherwise use internal state
|
|
953
|
+
if (propOnToggleSection) {
|
|
954
|
+
propOnToggleSection(sectionId, enabled);
|
|
955
|
+
} else {
|
|
956
|
+
setInternalDisabledSectionIds(prev => {
|
|
957
|
+
const next = new Set(prev);
|
|
958
|
+
if (enabled) {
|
|
959
|
+
next.delete(sectionId);
|
|
960
|
+
} else {
|
|
961
|
+
next.add(sectionId);
|
|
962
|
+
}
|
|
963
|
+
return next;
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}, [propOnToggleSection]);
|
|
967
|
+
|
|
814
968
|
// Ensure a conversation exists before sending the first message
|
|
815
969
|
// This creates a conversation on the server and returns the conversation ID
|
|
816
970
|
const ensureConversation = useCallback(() => {
|
|
@@ -907,6 +1061,215 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
907
1061
|
const currentAgentLabel = currentAgentInfo.label;
|
|
908
1062
|
const currentAgentAvatarUrl = currentAgentInfo.avatarUrl;
|
|
909
1063
|
|
|
1064
|
+
// ============================================================================
|
|
1065
|
+
// Helper Functions
|
|
1066
|
+
// ============================================================================
|
|
1067
|
+
|
|
1068
|
+
// Email validation helper
|
|
1069
|
+
const isEmailAddress = (email: string): boolean => {
|
|
1070
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1071
|
+
return emailRegex.test(email);
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
// Convert conversation history to standalone HTML file
|
|
1075
|
+
// Convert markdown to HTML - like ChatPanel
|
|
1076
|
+
const convertMarkdownToHTML = (markdown: string): string => {
|
|
1077
|
+
const html = ReactDOMServer.renderToStaticMarkup(
|
|
1078
|
+
<div className={markdownClass}>
|
|
1079
|
+
<ReactMarkdown
|
|
1080
|
+
remarkPlugins={[remarkGfm]}
|
|
1081
|
+
rehypePlugins={[rehypeRaw]}
|
|
1082
|
+
>
|
|
1083
|
+
{markdown}
|
|
1084
|
+
</ReactMarkdown>
|
|
1085
|
+
</div>
|
|
1086
|
+
);
|
|
1087
|
+
return html;
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// Convert conversation history to HTML - like ChatPanel
|
|
1091
|
+
const convertHistoryToHTML = (history: Record<string, HistoryEntry>): string => {
|
|
1092
|
+
const stylesheet = `
|
|
1093
|
+
<style>
|
|
1094
|
+
.conversation-history {
|
|
1095
|
+
font-family: Arial, sans-serif;
|
|
1096
|
+
line-height: 1.5;
|
|
1097
|
+
}
|
|
1098
|
+
.history-entry {
|
|
1099
|
+
margin-bottom: 15px;
|
|
1100
|
+
}
|
|
1101
|
+
.prompt-container, .response-container {
|
|
1102
|
+
margin-bottom: 10px;
|
|
1103
|
+
}
|
|
1104
|
+
.prompt, .response {
|
|
1105
|
+
display: block;
|
|
1106
|
+
margin: 5px 0;
|
|
1107
|
+
padding: 10px;
|
|
1108
|
+
border-radius: 5px;
|
|
1109
|
+
max-width: 80%;
|
|
1110
|
+
}
|
|
1111
|
+
.prompt {
|
|
1112
|
+
background-color: #efefef;
|
|
1113
|
+
margin-left: 0;
|
|
1114
|
+
}
|
|
1115
|
+
.response {
|
|
1116
|
+
background-color: #f0fcfd;
|
|
1117
|
+
margin-left: 25px;
|
|
1118
|
+
}
|
|
1119
|
+
</style>
|
|
1120
|
+
`;
|
|
1121
|
+
|
|
1122
|
+
let html = `
|
|
1123
|
+
<html>
|
|
1124
|
+
<head>
|
|
1125
|
+
${stylesheet}
|
|
1126
|
+
</head>
|
|
1127
|
+
<body>
|
|
1128
|
+
<h1>Conversation History (${new Date().toLocaleString()})</h1>
|
|
1129
|
+
<div class="conversation-history">
|
|
1130
|
+
`;
|
|
1131
|
+
|
|
1132
|
+
Object.entries(history).forEach(([prompt, response], index) => {
|
|
1133
|
+
if (hideInitialPrompt && index === 0) {
|
|
1134
|
+
html += `
|
|
1135
|
+
<div class="history-entry">
|
|
1136
|
+
<div class="response-container">
|
|
1137
|
+
<div class="response">${convertMarkdownToHTML(response.content)}</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
</div>
|
|
1140
|
+
`;
|
|
1141
|
+
} else {
|
|
1142
|
+
html += `
|
|
1143
|
+
<div class="history-entry">
|
|
1144
|
+
<div class="prompt-container">
|
|
1145
|
+
<div class="prompt">${convertMarkdownToHTML(
|
|
1146
|
+
formatPromptForDisplay(prompt)
|
|
1147
|
+
)}</div>
|
|
1148
|
+
</div>
|
|
1149
|
+
<div class="response-container">
|
|
1150
|
+
<div class="response">${convertMarkdownToHTML(response.content)}</div>
|
|
1151
|
+
</div>
|
|
1152
|
+
</div>
|
|
1153
|
+
`;
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
html += `
|
|
1158
|
+
</div>
|
|
1159
|
+
</body>
|
|
1160
|
+
</html>
|
|
1161
|
+
`;
|
|
1162
|
+
|
|
1163
|
+
return html;
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
// Save HTML to file - like ChatPanel
|
|
1167
|
+
const saveHTMLToFile = (html: string, filename: string) => {
|
|
1168
|
+
const blob = new Blob([html], { type: 'text/html' });
|
|
1169
|
+
const link = document.createElement('a');
|
|
1170
|
+
link.href = URL.createObjectURL(blob);
|
|
1171
|
+
link.download = filename;
|
|
1172
|
+
document.body.appendChild(link);
|
|
1173
|
+
link.click();
|
|
1174
|
+
document.body.removeChild(link);
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
// Download conversation as HTML file
|
|
1178
|
+
const saveAsHTMLFile = useCallback(() => {
|
|
1179
|
+
saveHTMLToFile(
|
|
1180
|
+
convertHistoryToHTML(history),
|
|
1181
|
+
`conversation-${new Date().toISOString()}.html`
|
|
1182
|
+
);
|
|
1183
|
+
interactionClicked(lastCallId || '', 'save');
|
|
1184
|
+
}, [history, lastCallId]);
|
|
1185
|
+
|
|
1186
|
+
const handleSendEmail = (to: string, from: string) => {
|
|
1187
|
+
sendConversationsViaEmail(to, `Conversation History from ${title}`, from);
|
|
1188
|
+
interactionClicked(lastCallId || '', 'email', to);
|
|
1189
|
+
setEmailSent(true);
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
const sendConversationsViaEmail = async (
|
|
1193
|
+
to: string,
|
|
1194
|
+
subject: string = `Conversation History from ${title}`,
|
|
1195
|
+
from: string = ''
|
|
1196
|
+
) => {
|
|
1197
|
+
fetch(`${publicAPIUrl}/share/email`, {
|
|
1198
|
+
method: 'POST',
|
|
1199
|
+
headers: {
|
|
1200
|
+
'Content-Type': 'application/json',
|
|
1201
|
+
},
|
|
1202
|
+
body: JSON.stringify({
|
|
1203
|
+
to: to,
|
|
1204
|
+
from: from,
|
|
1205
|
+
subject: subject,
|
|
1206
|
+
html: convertHistoryToHTML(history),
|
|
1207
|
+
project_id: project_id ?? '',
|
|
1208
|
+
customer: customer,
|
|
1209
|
+
history: history,
|
|
1210
|
+
title: title,
|
|
1211
|
+
}),
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
await interactionClicked(lastCallId || '', 'email', from);
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
// Send CTA email
|
|
1219
|
+
const sendCallToActionEmail = useCallback(async (from: string) => {
|
|
1220
|
+
try {
|
|
1221
|
+
await fetch(`${publicAPIUrl}/share/email`, {
|
|
1222
|
+
method: 'POST',
|
|
1223
|
+
headers: {
|
|
1224
|
+
'Content-Type': 'application/json',
|
|
1225
|
+
},
|
|
1226
|
+
body: JSON.stringify({
|
|
1227
|
+
to: callToActionEmailAddress,
|
|
1228
|
+
from: from,
|
|
1229
|
+
subject: `${callToActionEmailSubject} from ${from}`,
|
|
1230
|
+
html: convertHistoryToHTML(history),
|
|
1231
|
+
project_id: project_id ?? '',
|
|
1232
|
+
customer: customer,
|
|
1233
|
+
history: history,
|
|
1234
|
+
title: title,
|
|
1235
|
+
}),
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
await interactionClicked(lastCallId || '', 'cta', from);
|
|
1239
|
+
setCallToActionSent(true);
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
console.error('[AIChatPanel] Failed to send CTA email:', err);
|
|
1242
|
+
}
|
|
1243
|
+
}, [history, title, project_id, customer, lastCallId, publicAPIUrl, callToActionEmailAddress, callToActionEmailSubject]);
|
|
1244
|
+
|
|
1245
|
+
// Check if button should be disabled due to email capture requirements
|
|
1246
|
+
const isDisabledDueToNoEmail = useCallback(() => {
|
|
1247
|
+
if (customerEmailCaptureMode === 'REQUIRED' && !emailInputSet) {
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
return false;
|
|
1251
|
+
}, [customerEmailCaptureMode, emailInputSet]);
|
|
1252
|
+
|
|
1253
|
+
// Handle tool approval for MCP tools
|
|
1254
|
+
const handleToolApproval = useCallback((toolName: string, scope: 'once' | 'session' | 'always') => {
|
|
1255
|
+
if (scope === 'session' || scope === 'always') {
|
|
1256
|
+
setSessionApprovedTools((prev) => Array.from(new Set([...prev, toolName])));
|
|
1257
|
+
}
|
|
1258
|
+
if (scope === 'always') {
|
|
1259
|
+
setAlwaysApprovedTools((prev) => Array.from(new Set([...prev, toolName])));
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Remove approved tool from pending list
|
|
1263
|
+
setPendingToolRequests((prev) => prev.filter((r) => r.toolName !== toolName));
|
|
1264
|
+
|
|
1265
|
+
console.log(`[AIChatPanel] Tool "${toolName}" approved with scope: ${scope}`);
|
|
1266
|
+
}, []);
|
|
1267
|
+
|
|
1268
|
+
// Get unique tool names from pending requests
|
|
1269
|
+
const getUniqueToolNames = useCallback(() => {
|
|
1270
|
+
return Array.from(new Set(pendingToolRequests.map((r) => r.toolName)));
|
|
1271
|
+
}, [pendingToolRequests]);
|
|
1272
|
+
|
|
910
1273
|
// ============================================================================
|
|
911
1274
|
// Callbacks
|
|
912
1275
|
// ============================================================================
|
|
@@ -1226,11 +1589,6 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1226
1589
|
fullPromptToSend = promptTemplate.replace('{{prompt}}', fullPromptToSend);
|
|
1227
1590
|
}
|
|
1228
1591
|
|
|
1229
|
-
// Add follow-on prompt
|
|
1230
|
-
if (followOnPrompt) {
|
|
1231
|
-
fullPromptToSend += `\n\n${followOnPrompt}`;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
1592
|
const newController = new AbortController();
|
|
1235
1593
|
setLastController(newController);
|
|
1236
1594
|
|
|
@@ -1314,7 +1672,6 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1314
1672
|
clearFollowOnQuestionsNextPrompt,
|
|
1315
1673
|
history,
|
|
1316
1674
|
promptTemplate,
|
|
1317
|
-
followOnPrompt,
|
|
1318
1675
|
send,
|
|
1319
1676
|
service,
|
|
1320
1677
|
ensureConversation,
|
|
@@ -1447,12 +1804,13 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1447
1804
|
useEffect(() => {
|
|
1448
1805
|
// Only auto-scroll if:
|
|
1449
1806
|
// 1. We're actively streaming (!idle)
|
|
1450
|
-
// 2. User hasn't manually scrolled up during this response
|
|
1807
|
+
// 2. User hasn't manually scrolled up during this response (or scrollToEnd prop is true)
|
|
1451
1808
|
// 3. We have content to show (response exists)
|
|
1452
|
-
|
|
1809
|
+
const shouldAutoScroll = scrollToEnd || !userHasScrolled;
|
|
1810
|
+
if (!idle && shouldAutoScroll && response) {
|
|
1453
1811
|
scrollToBottom();
|
|
1454
1812
|
}
|
|
1455
|
-
}, [response, scrollToBottom, idle, userHasScrolled]); // Removed history dependency
|
|
1813
|
+
}, [response, scrollToBottom, idle, userHasScrolled, scrollToEnd]); // Removed history dependency
|
|
1456
1814
|
|
|
1457
1815
|
// Ref to track idle state for scroll handler (avoids stale closure)
|
|
1458
1816
|
const idleRef = useRef(idle);
|
|
@@ -1548,6 +1906,15 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1548
1906
|
}
|
|
1549
1907
|
}, [initialPrompt, continueChat]);
|
|
1550
1908
|
|
|
1909
|
+
// Auto-send followOnPrompt when it changes (like ChatPanel)
|
|
1910
|
+
// This allows parent components to programmatically submit prompts to the current conversation
|
|
1911
|
+
useEffect(() => {
|
|
1912
|
+
if (followOnPrompt && followOnPrompt !== '' && followOnPrompt !== lastFollowOnPromptRef.current) {
|
|
1913
|
+
lastFollowOnPromptRef.current = followOnPrompt;
|
|
1914
|
+
continueChat(followOnPrompt);
|
|
1915
|
+
}
|
|
1916
|
+
}, [followOnPrompt, continueChat]);
|
|
1917
|
+
|
|
1551
1918
|
// Monitor for errors from useLLM hook
|
|
1552
1919
|
useEffect(() => {
|
|
1553
1920
|
if (llmError && llmError.trim()) {
|
|
@@ -1594,6 +1961,50 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1594
1961
|
}
|
|
1595
1962
|
}, [llmError, lastKey, lastCallId]);
|
|
1596
1963
|
|
|
1964
|
+
// Dynamic CSS Injection
|
|
1965
|
+
useEffect(() => {
|
|
1966
|
+
// Clean up any previously added CSS from this component
|
|
1967
|
+
const existingLinks = document.querySelectorAll(
|
|
1968
|
+
'link[data-source="ai-chat-panel"]'
|
|
1969
|
+
);
|
|
1970
|
+
existingLinks.forEach((link) => link.parentNode?.removeChild(link));
|
|
1971
|
+
|
|
1972
|
+
const existingStyles = document.querySelectorAll(
|
|
1973
|
+
'style[data-source="ai-chat-panel"]'
|
|
1974
|
+
);
|
|
1975
|
+
existingStyles.forEach((style) => style.parentNode?.removeChild(style));
|
|
1976
|
+
|
|
1977
|
+
if (cssUrl) {
|
|
1978
|
+
if (cssUrl.startsWith('http://') || cssUrl.startsWith('https://')) {
|
|
1979
|
+
// If it's a URL, create a link element
|
|
1980
|
+
const link = document.createElement('link');
|
|
1981
|
+
link.href = cssUrl;
|
|
1982
|
+
link.rel = 'stylesheet';
|
|
1983
|
+
link.setAttribute('data-source', 'ai-chat-panel');
|
|
1984
|
+
document.head.appendChild(link);
|
|
1985
|
+
} else {
|
|
1986
|
+
// If it's a CSS string, create a style element
|
|
1987
|
+
const style = document.createElement('style');
|
|
1988
|
+
style.textContent = cssUrl;
|
|
1989
|
+
style.setAttribute('data-source', 'ai-chat-panel');
|
|
1990
|
+
document.head.appendChild(style);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// Clean up when component unmounts
|
|
1995
|
+
return () => {
|
|
1996
|
+
const links = document.querySelectorAll(
|
|
1997
|
+
'link[data-source="ai-chat-panel"]'
|
|
1998
|
+
);
|
|
1999
|
+
links.forEach((link) => link.parentNode?.removeChild(link));
|
|
2000
|
+
|
|
2001
|
+
const styles = document.querySelectorAll(
|
|
2002
|
+
'style[data-source="ai-chat-panel"]'
|
|
2003
|
+
);
|
|
2004
|
+
styles.forEach((style) => style.parentNode?.removeChild(style));
|
|
2005
|
+
};
|
|
2006
|
+
}, [cssUrl]);
|
|
2007
|
+
|
|
1597
2008
|
// ============================================================================
|
|
1598
2009
|
// Render Helpers
|
|
1599
2010
|
// ============================================================================
|
|
@@ -1603,7 +2014,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1603
2014
|
const match = /language-(\w+)/.exec(className || '');
|
|
1604
2015
|
return !inline && match ? (
|
|
1605
2016
|
<SyntaxHighlighter
|
|
1606
|
-
style={
|
|
2017
|
+
style={effectivePrismStyle}
|
|
1607
2018
|
language={match[1]}
|
|
1608
2019
|
PreTag="div"
|
|
1609
2020
|
{...props}
|
|
@@ -1615,7 +2026,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1615
2026
|
{children}
|
|
1616
2027
|
</code>
|
|
1617
2028
|
);
|
|
1618
|
-
}, [
|
|
2029
|
+
}, [effectivePrismStyle]);
|
|
1619
2030
|
|
|
1620
2031
|
// Agent suggestion card component for inline agent handoff
|
|
1621
2032
|
const AgentSuggestionCard = React.memo(({ agentId, agentName, reason }: {
|
|
@@ -1824,7 +2235,13 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1824
2235
|
const panelClasses = ['ai-chat-panel', theme === 'dark' ? 'dark-theme' : ''].filter(Boolean).join(' ');
|
|
1825
2236
|
|
|
1826
2237
|
return (
|
|
1827
|
-
<div
|
|
2238
|
+
<div
|
|
2239
|
+
className={panelClasses}
|
|
2240
|
+
style={{
|
|
2241
|
+
...(width && { width }),
|
|
2242
|
+
...(height && { height })
|
|
2243
|
+
}}
|
|
2244
|
+
>
|
|
1828
2245
|
{/* Title */}
|
|
1829
2246
|
{title && <div className="ai-chat-panel__title">{title}</div>}
|
|
1830
2247
|
|
|
@@ -1858,9 +2275,23 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1858
2275
|
{initialMessage && (
|
|
1859
2276
|
<div className="ai-chat-message ai-chat-message--assistant">
|
|
1860
2277
|
<div className="ai-chat-message__content">
|
|
1861
|
-
|
|
1862
|
-
{
|
|
1863
|
-
|
|
2278
|
+
{markdownClass ? (
|
|
2279
|
+
<div className={markdownClass}>
|
|
2280
|
+
<ReactMarkdown
|
|
2281
|
+
remarkPlugins={[remarkGfm]}
|
|
2282
|
+
rehypePlugins={[rehypeRaw]}
|
|
2283
|
+
>
|
|
2284
|
+
{initialMessage}
|
|
2285
|
+
</ReactMarkdown>
|
|
2286
|
+
</div>
|
|
2287
|
+
) : (
|
|
2288
|
+
<ReactMarkdown
|
|
2289
|
+
remarkPlugins={[remarkGfm]}
|
|
2290
|
+
rehypePlugins={[rehypeRaw]}
|
|
2291
|
+
>
|
|
2292
|
+
{initialMessage}
|
|
2293
|
+
</ReactMarkdown>
|
|
2294
|
+
)}
|
|
1864
2295
|
</div>
|
|
1865
2296
|
</div>
|
|
1866
2297
|
)}
|
|
@@ -1895,13 +2326,25 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1895
2326
|
{thinkingBlocks.length > 0 && renderThinkingBlocks()}
|
|
1896
2327
|
|
|
1897
2328
|
{processedContent ? (
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2329
|
+
markdownClass ? (
|
|
2330
|
+
<div className={markdownClass}>
|
|
2331
|
+
<ReactMarkdown
|
|
2332
|
+
remarkPlugins={[remarkGfm]}
|
|
2333
|
+
rehypePlugins={[rehypeRaw]}
|
|
2334
|
+
components={markdownComponents}
|
|
2335
|
+
>
|
|
2336
|
+
{processedContent}
|
|
2337
|
+
</ReactMarkdown>
|
|
2338
|
+
</div>
|
|
2339
|
+
) : (
|
|
2340
|
+
<ReactMarkdown
|
|
2341
|
+
remarkPlugins={[remarkGfm]}
|
|
2342
|
+
rehypePlugins={[rehypeRaw]}
|
|
2343
|
+
components={markdownComponents}
|
|
2344
|
+
>
|
|
2345
|
+
{processedContent}
|
|
2346
|
+
</ReactMarkdown>
|
|
2347
|
+
)
|
|
1905
2348
|
) : (
|
|
1906
2349
|
<div className="ai-chat-loading">
|
|
1907
2350
|
<span>Thinking</span>
|
|
@@ -1916,13 +2359,25 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1916
2359
|
) : (
|
|
1917
2360
|
<>
|
|
1918
2361
|
{isLastEntry && thinkingBlocks.length > 0 && renderThinkingBlocks()}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2362
|
+
{markdownClass ? (
|
|
2363
|
+
<div className={markdownClass}>
|
|
2364
|
+
<ReactMarkdown
|
|
2365
|
+
remarkPlugins={[remarkGfm]}
|
|
2366
|
+
rehypePlugins={[rehypeRaw]}
|
|
2367
|
+
components={markdownComponents}
|
|
2368
|
+
>
|
|
2369
|
+
{processedContent}
|
|
2370
|
+
</ReactMarkdown>
|
|
2371
|
+
</div>
|
|
2372
|
+
) : (
|
|
2373
|
+
<ReactMarkdown
|
|
2374
|
+
remarkPlugins={[remarkGfm]}
|
|
2375
|
+
rehypePlugins={[rehypeRaw]}
|
|
2376
|
+
components={markdownComponents}
|
|
2377
|
+
>
|
|
2378
|
+
{processedContent}
|
|
2379
|
+
</ReactMarkdown>
|
|
2380
|
+
)}
|
|
1926
2381
|
</>
|
|
1927
2382
|
)}
|
|
1928
2383
|
</div>
|
|
@@ -1974,6 +2429,27 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1974
2429
|
</svg>
|
|
1975
2430
|
)}
|
|
1976
2431
|
</button>
|
|
2432
|
+
|
|
2433
|
+
{/* Tool Info Button - show if entry has tool data */}
|
|
2434
|
+
{(entry.toolCalls || entry.toolResponses) && (
|
|
2435
|
+
<button
|
|
2436
|
+
className="ai-chat-action-button"
|
|
2437
|
+
onClick={() => {
|
|
2438
|
+
setToolInfoData({
|
|
2439
|
+
calls: entry.toolCalls || [],
|
|
2440
|
+
responses: entry.toolResponses || [],
|
|
2441
|
+
});
|
|
2442
|
+
setIsToolInfoModalOpen(true);
|
|
2443
|
+
}}
|
|
2444
|
+
title="View tool information"
|
|
2445
|
+
>
|
|
2446
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="ai-chat-icon-sm">
|
|
2447
|
+
<circle cx="12" cy="12" r="10" />
|
|
2448
|
+
<line x1="12" x2="12" y1="16" y2="12" />
|
|
2449
|
+
<line x1="12" x2="12.01" y1="8" y2="8" />
|
|
2450
|
+
</svg>
|
|
2451
|
+
</button>
|
|
2452
|
+
)}
|
|
1977
2453
|
</div>
|
|
1978
2454
|
)}
|
|
1979
2455
|
</div>
|
|
@@ -2001,6 +2477,109 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2001
2477
|
<div ref={bottomRef} />
|
|
2002
2478
|
</ScrollArea>
|
|
2003
2479
|
|
|
2480
|
+
{/* Tool Approval Panel */}
|
|
2481
|
+
{pendingToolRequests.length > 0 && (
|
|
2482
|
+
<div className="ai-chat-approve-tools-panel">
|
|
2483
|
+
<div className="ai-chat-approve-tools-header">
|
|
2484
|
+
Tool Approval Required
|
|
2485
|
+
</div>
|
|
2486
|
+
<div className="ai-chat-approve-tools-description">
|
|
2487
|
+
The AI wants to use the following tools:
|
|
2488
|
+
</div>
|
|
2489
|
+
{getUniqueToolNames().map((toolName) => (
|
|
2490
|
+
<div key={toolName} className="ai-chat-approve-tool-item">
|
|
2491
|
+
<div className="ai-chat-approve-tool-name">{toolName}</div>
|
|
2492
|
+
<div className="ai-chat-approve-tools-buttons">
|
|
2493
|
+
<Button
|
|
2494
|
+
size="sm"
|
|
2495
|
+
variant="outline"
|
|
2496
|
+
className="ai-chat-approve-tools-button"
|
|
2497
|
+
onClick={() => handleToolApproval(toolName, 'once')}
|
|
2498
|
+
>
|
|
2499
|
+
Once
|
|
2500
|
+
</Button>
|
|
2501
|
+
<Button
|
|
2502
|
+
size="sm"
|
|
2503
|
+
variant="outline"
|
|
2504
|
+
className="ai-chat-approve-tools-button"
|
|
2505
|
+
onClick={() => handleToolApproval(toolName, 'session')}
|
|
2506
|
+
>
|
|
2507
|
+
This Session
|
|
2508
|
+
</Button>
|
|
2509
|
+
<Button
|
|
2510
|
+
size="sm"
|
|
2511
|
+
variant="default"
|
|
2512
|
+
className="ai-chat-approve-tools-button"
|
|
2513
|
+
onClick={() => handleToolApproval(toolName, 'always')}
|
|
2514
|
+
>
|
|
2515
|
+
Always
|
|
2516
|
+
</Button>
|
|
2517
|
+
</div>
|
|
2518
|
+
</div>
|
|
2519
|
+
))}
|
|
2520
|
+
</div>
|
|
2521
|
+
)}
|
|
2522
|
+
|
|
2523
|
+
{/* Button Container - Save, Email, CTA */}
|
|
2524
|
+
{(showSaveButton || showEmailButton || showCallToAction) && (
|
|
2525
|
+
<div className="ai-chat-button-container">
|
|
2526
|
+
{showSaveButton && (
|
|
2527
|
+
<Button
|
|
2528
|
+
variant="outline"
|
|
2529
|
+
size="sm"
|
|
2530
|
+
onClick={saveAsHTMLFile}
|
|
2531
|
+
disabled={Object.keys(history).length === 0}
|
|
2532
|
+
className="ai-chat-save-button"
|
|
2533
|
+
>
|
|
2534
|
+
💾 Save
|
|
2535
|
+
</Button>
|
|
2536
|
+
)}
|
|
2537
|
+
|
|
2538
|
+
{showEmailButton && (
|
|
2539
|
+
<Button
|
|
2540
|
+
variant="outline"
|
|
2541
|
+
size="sm"
|
|
2542
|
+
onClick={() => {
|
|
2543
|
+
if (isEmailAddress(emailInput)) {
|
|
2544
|
+
setEmailInputSet(true);
|
|
2545
|
+
setEmailValid(true);
|
|
2546
|
+
handleSendEmail(emailInput, emailInput);
|
|
2547
|
+
setEmailClickedButNoEmail(false);
|
|
2548
|
+
} else {
|
|
2549
|
+
setShowEmailPanel(true);
|
|
2550
|
+
setEmailValid(false);
|
|
2551
|
+
setEmailClickedButNoEmail(true);
|
|
2552
|
+
}
|
|
2553
|
+
}}
|
|
2554
|
+
disabled={Object.keys(history).length === 0 || isDisabledDueToNoEmail()}
|
|
2555
|
+
className="ai-chat-email-button"
|
|
2556
|
+
>
|
|
2557
|
+
📧 Email Conversation{emailSent ? ' ✓' : ''}
|
|
2558
|
+
</Button>
|
|
2559
|
+
)}
|
|
2560
|
+
|
|
2561
|
+
{showCallToAction && (
|
|
2562
|
+
<Button
|
|
2563
|
+
variant={callToActionSent ? 'outline' : 'default'}
|
|
2564
|
+
size="sm"
|
|
2565
|
+
onClick={() => {
|
|
2566
|
+
if (customerEmailCaptureMode !== 'HIDE' && !emailInputSet) {
|
|
2567
|
+
setCTAClickedButNoEmail(true);
|
|
2568
|
+
setTimeout(() => setCTAClickedButNoEmail(false), 3000);
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
const fromEmail = emailInput || customer?.customer_user_email || '';
|
|
2572
|
+
sendCallToActionEmail(fromEmail);
|
|
2573
|
+
}}
|
|
2574
|
+
disabled={callToActionSent || Object.keys(history).length === 0}
|
|
2575
|
+
className="ai-chat-cta-button"
|
|
2576
|
+
>
|
|
2577
|
+
{callToActionSent ? '✓ Submitted' : callToActionButtonText}
|
|
2578
|
+
</Button>
|
|
2579
|
+
)}
|
|
2580
|
+
</div>
|
|
2581
|
+
)}
|
|
2582
|
+
|
|
2004
2583
|
{/* New Conversation Button */}
|
|
2005
2584
|
{showNewConversationButton && (
|
|
2006
2585
|
<div className="ai-chat-panel__new-conversation">
|
|
@@ -2015,6 +2594,103 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2015
2594
|
</div>
|
|
2016
2595
|
)}
|
|
2017
2596
|
|
|
2597
|
+
{/* Customer Email Capture Panel */}
|
|
2598
|
+
{showEmailPanel && (
|
|
2599
|
+
<>
|
|
2600
|
+
{!emailValid && (
|
|
2601
|
+
<div className="ai-chat-email-input-message">
|
|
2602
|
+
{isDisabledDueToNoEmail()
|
|
2603
|
+
? "Let's get started - please enter your email"
|
|
2604
|
+
: CTAClickedButNoEmail || emailClickedButNoEmail
|
|
2605
|
+
? 'Sure, we just need an email address to contact you'
|
|
2606
|
+
: 'Email address is invalid'}
|
|
2607
|
+
</div>
|
|
2608
|
+
)}
|
|
2609
|
+
<div className="ai-chat-email-input-container">
|
|
2610
|
+
<input
|
|
2611
|
+
type="email"
|
|
2612
|
+
name="email"
|
|
2613
|
+
id="email"
|
|
2614
|
+
className={
|
|
2615
|
+
emailValid
|
|
2616
|
+
? emailInputSet
|
|
2617
|
+
? 'ai-chat-email-input-set'
|
|
2618
|
+
: 'ai-chat-email-input'
|
|
2619
|
+
: 'ai-chat-email-input-invalid'
|
|
2620
|
+
}
|
|
2621
|
+
placeholder={customerEmailCapturePlaceholder}
|
|
2622
|
+
value={emailInput}
|
|
2623
|
+
onChange={(e) => {
|
|
2624
|
+
const newEmail = e.target.value;
|
|
2625
|
+
setEmailInput(newEmail);
|
|
2626
|
+
// Reset validation while typing
|
|
2627
|
+
if (!emailInputSet) {
|
|
2628
|
+
if (customerEmailCaptureMode === 'REQUIRED' && newEmail !== '') {
|
|
2629
|
+
setEmailValid(isEmailAddress(newEmail));
|
|
2630
|
+
} else {
|
|
2631
|
+
setEmailValid(true);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
}}
|
|
2635
|
+
onBlur={() => {
|
|
2636
|
+
// Auto-validate and set email when field loses focus
|
|
2637
|
+
if (emailInput && isEmailAddress(emailInput) && !emailInputSet) {
|
|
2638
|
+
setEmailInputSet(true);
|
|
2639
|
+
setEmailValid(true);
|
|
2640
|
+
interactionClicked('', 'emailcapture', emailInput);
|
|
2641
|
+
|
|
2642
|
+
// Handle pending actions
|
|
2643
|
+
if (CTAClickedButNoEmail) {
|
|
2644
|
+
sendCallToActionEmail(emailInput);
|
|
2645
|
+
setCTAClickedButNoEmail(false);
|
|
2646
|
+
}
|
|
2647
|
+
if (emailClickedButNoEmail) {
|
|
2648
|
+
handleSendEmail(emailInput, emailInput);
|
|
2649
|
+
setEmailClickedButNoEmail(false);
|
|
2650
|
+
}
|
|
2651
|
+
} else if (customerEmailCaptureMode === 'REQUIRED' && emailInput !== '') {
|
|
2652
|
+
setEmailValid(isEmailAddress(emailInput));
|
|
2653
|
+
}
|
|
2654
|
+
}}
|
|
2655
|
+
onKeyDown={(e) => {
|
|
2656
|
+
if (e.key === 'Enter') {
|
|
2657
|
+
if (isEmailAddress(emailInput)) {
|
|
2658
|
+
setEmailInputSet(true);
|
|
2659
|
+
setEmailValid(true);
|
|
2660
|
+
interactionClicked('', 'emailcapture', emailInput);
|
|
2661
|
+
|
|
2662
|
+
// Handle pending actions
|
|
2663
|
+
if (CTAClickedButNoEmail) {
|
|
2664
|
+
sendCallToActionEmail(emailInput);
|
|
2665
|
+
setCTAClickedButNoEmail(false);
|
|
2666
|
+
}
|
|
2667
|
+
if (emailClickedButNoEmail) {
|
|
2668
|
+
handleSendEmail(emailInput, emailInput);
|
|
2669
|
+
setEmailClickedButNoEmail(false);
|
|
2670
|
+
}
|
|
2671
|
+
} else {
|
|
2672
|
+
setEmailValid(false);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}}
|
|
2676
|
+
disabled={false}
|
|
2677
|
+
/>
|
|
2678
|
+
{emailInputSet && (
|
|
2679
|
+
<button
|
|
2680
|
+
className="ai-chat-email-edit-button"
|
|
2681
|
+
onClick={() => {
|
|
2682
|
+
setEmailInputSet(false);
|
|
2683
|
+
setEmailValid(true);
|
|
2684
|
+
}}
|
|
2685
|
+
title="Edit email"
|
|
2686
|
+
>
|
|
2687
|
+
✎
|
|
2688
|
+
</button>
|
|
2689
|
+
)}
|
|
2690
|
+
</div>
|
|
2691
|
+
</>
|
|
2692
|
+
)}
|
|
2693
|
+
|
|
2018
2694
|
{/* Input Area - Isolated component for performance */}
|
|
2019
2695
|
<ChatInput
|
|
2020
2696
|
placeholder={placeholder}
|
|
@@ -2031,6 +2707,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2031
2707
|
totalContextTokens={totalContextTokens}
|
|
2032
2708
|
maxContextTokens={maxContextTokens}
|
|
2033
2709
|
enableContextDetailView={enableContextDetailView}
|
|
2710
|
+
disabledSectionIds={disabledSectionIds}
|
|
2711
|
+
onToggleSection={handleToggleSection}
|
|
2034
2712
|
/>
|
|
2035
2713
|
|
|
2036
2714
|
{/* Footer */}
|
|
@@ -2064,6 +2742,13 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2064
2742
|
</div>
|
|
2065
2743
|
</div>
|
|
2066
2744
|
)}
|
|
2745
|
+
|
|
2746
|
+
{/* Modals */}
|
|
2747
|
+
<ToolInfoModal
|
|
2748
|
+
isOpen={isToolInfoModalOpen}
|
|
2749
|
+
onClose={() => setIsToolInfoModalOpen(false)}
|
|
2750
|
+
data={toolInfoData}
|
|
2751
|
+
/>
|
|
2067
2752
|
</div>
|
|
2068
2753
|
);
|
|
2069
2754
|
};
|