@hef2024/llmasaservice-ui 0.20.0 → 0.20.1
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 +445 -0
- package/dist/index.d.mts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +1042 -438
- package/dist/index.mjs +873 -269
- 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 +97 -1
- package/src/AIChatPanel.css +536 -0
- package/src/AIChatPanel.tsx +641 -32
- 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 +66 -0
- package/src/components/ui/Tooltip.tsx +1 -0
- package/src/components/ui/index.ts +1 -0
- package/src/hooks/useAgentRegistry.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
|
// ============================================================================
|
|
@@ -89,6 +91,29 @@ export interface AIChatPanelProps {
|
|
|
89
91
|
|
|
90
92
|
// Callback when a new conversation is created via API
|
|
91
93
|
onConversationCreated?: (conversationId: string) => void;
|
|
94
|
+
|
|
95
|
+
// UI Customization Props (from ChatPanel)
|
|
96
|
+
cssUrl?: string;
|
|
97
|
+
markdownClass?: string;
|
|
98
|
+
width?: string;
|
|
99
|
+
height?: string;
|
|
100
|
+
scrollToEnd?: boolean;
|
|
101
|
+
prismStyle?: any; // PrismStyle type from react-syntax-highlighter
|
|
102
|
+
|
|
103
|
+
// Email & Save Props
|
|
104
|
+
showSaveButton?: boolean;
|
|
105
|
+
showEmailButton?: boolean;
|
|
106
|
+
messages?: { role: "user" | "assistant"; content: string }[];
|
|
107
|
+
|
|
108
|
+
// Call-to-Action Props
|
|
109
|
+
showCallToAction?: boolean;
|
|
110
|
+
callToActionButtonText?: string;
|
|
111
|
+
callToActionEmailAddress?: string;
|
|
112
|
+
callToActionEmailSubject?: string;
|
|
113
|
+
|
|
114
|
+
// Customer Email Capture Props
|
|
115
|
+
customerEmailCaptureMode?: "HIDE" | "OPTIONAL" | "REQUIRED";
|
|
116
|
+
customerEmailCapturePlaceholder?: string;
|
|
92
117
|
}
|
|
93
118
|
|
|
94
119
|
/**
|
|
@@ -677,6 +702,25 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
677
702
|
maxContextTokens = 8000,
|
|
678
703
|
enableContextDetailView = false,
|
|
679
704
|
onConversationCreated,
|
|
705
|
+
// UI Customization Props
|
|
706
|
+
cssUrl,
|
|
707
|
+
markdownClass,
|
|
708
|
+
width,
|
|
709
|
+
height,
|
|
710
|
+
scrollToEnd = false,
|
|
711
|
+
prismStyle,
|
|
712
|
+
// Email & Save Props
|
|
713
|
+
showSaveButton = true,
|
|
714
|
+
showEmailButton = true,
|
|
715
|
+
messages = [],
|
|
716
|
+
// Call-to-Action Props
|
|
717
|
+
showCallToAction = false,
|
|
718
|
+
callToActionButtonText = 'Submit',
|
|
719
|
+
callToActionEmailAddress = '',
|
|
720
|
+
callToActionEmailSubject = 'Agent CTA submitted',
|
|
721
|
+
// Customer Email Capture Props
|
|
722
|
+
customerEmailCaptureMode = 'HIDE',
|
|
723
|
+
customerEmailCapturePlaceholder = 'Please enter your email...',
|
|
680
724
|
}) => {
|
|
681
725
|
// ============================================================================
|
|
682
726
|
// API URL
|
|
@@ -703,6 +747,40 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
703
747
|
const [feedbackCallId, setFeedbackCallId] = useState<{ callId: string; type: 'up' | 'down' } | null>(null);
|
|
704
748
|
const [error, setError] = useState<{ message: string; code?: string } | null>(null);
|
|
705
749
|
|
|
750
|
+
// Email & Save state
|
|
751
|
+
const [emailSent, setEmailSent] = useState(false);
|
|
752
|
+
|
|
753
|
+
// Tool Info Modal state
|
|
754
|
+
const [isToolInfoModalOpen, setIsToolInfoModalOpen] = useState(false);
|
|
755
|
+
const [toolInfoData, setToolInfoData] = useState<{ calls: any[]; responses: any[] } | null>(null);
|
|
756
|
+
|
|
757
|
+
// Call-to-Action state
|
|
758
|
+
const [callToActionSent, setCallToActionSent] = useState(false);
|
|
759
|
+
const [CTAClickedButNoEmail, setCTAClickedButNoEmail] = useState(false);
|
|
760
|
+
|
|
761
|
+
// Customer Email Capture state
|
|
762
|
+
const [emailInput, setEmailInput] = useState(customer?.customer_user_email ?? '');
|
|
763
|
+
const [emailInputSet, setEmailInputSet] = useState(false);
|
|
764
|
+
const [emailValid, setEmailValid] = useState(true);
|
|
765
|
+
const [showEmailPanel, setShowEmailPanel] = useState(customerEmailCaptureMode !== 'HIDE');
|
|
766
|
+
const [emailClickedButNoEmail, setEmailClickedButNoEmail] = useState(false);
|
|
767
|
+
|
|
768
|
+
// Tool Approval state (for MCP tools)
|
|
769
|
+
const [pendingToolRequests, setPendingToolRequests] = useState<any[]>([]);
|
|
770
|
+
const [sessionApprovedTools, setSessionApprovedTools] = useState<string[]>([]);
|
|
771
|
+
const [alwaysApprovedTools, setAlwaysApprovedTools] = useState<string[]>([]);
|
|
772
|
+
|
|
773
|
+
// Email capture mode effect - like ChatPanel
|
|
774
|
+
useEffect(() => {
|
|
775
|
+
setShowEmailPanel(customerEmailCaptureMode !== 'HIDE');
|
|
776
|
+
|
|
777
|
+
if (customerEmailCaptureMode === 'REQUIRED') {
|
|
778
|
+
if (!isEmailAddress(emailInput)) {
|
|
779
|
+
setEmailValid(false);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}, [customerEmailCaptureMode, emailInput]);
|
|
783
|
+
|
|
706
784
|
// Refs
|
|
707
785
|
const bottomRef = useRef<HTMLDivElement | null>(null);
|
|
708
786
|
const responseAreaRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -722,6 +800,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
722
800
|
const latestHistoryRef = useRef<Record<string, HistoryEntry>>(initialHistory);
|
|
723
801
|
// Track if we've sent the initial prompt (prevents loops)
|
|
724
802
|
const initialPromptSentRef = useRef<boolean>(false);
|
|
803
|
+
// Track the last followOnPrompt to detect changes (for auto-submit trigger)
|
|
804
|
+
const lastFollowOnPromptRef = useRef<string>('');
|
|
725
805
|
|
|
726
806
|
// Sync new entries from initialHistory into local history state
|
|
727
807
|
// This allows parent components to inject messages (e.g., page-based agent suggestions)
|
|
@@ -796,9 +876,9 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
796
876
|
// ============================================================================
|
|
797
877
|
// Memoized Values
|
|
798
878
|
// ============================================================================
|
|
799
|
-
const
|
|
800
|
-
() => (theme === 'light' ? materialLight : materialDark),
|
|
801
|
-
[theme]
|
|
879
|
+
const effectivePrismStyle = useMemo(
|
|
880
|
+
() => prismStyle ?? (theme === 'light' ? materialLight : materialDark),
|
|
881
|
+
[prismStyle, theme]
|
|
802
882
|
);
|
|
803
883
|
|
|
804
884
|
// Browser info for context (matches ChatPanel)
|
|
@@ -907,6 +987,215 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
907
987
|
const currentAgentLabel = currentAgentInfo.label;
|
|
908
988
|
const currentAgentAvatarUrl = currentAgentInfo.avatarUrl;
|
|
909
989
|
|
|
990
|
+
// ============================================================================
|
|
991
|
+
// Helper Functions
|
|
992
|
+
// ============================================================================
|
|
993
|
+
|
|
994
|
+
// Email validation helper
|
|
995
|
+
const isEmailAddress = (email: string): boolean => {
|
|
996
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
997
|
+
return emailRegex.test(email);
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
// Convert conversation history to standalone HTML file
|
|
1001
|
+
// Convert markdown to HTML - like ChatPanel
|
|
1002
|
+
const convertMarkdownToHTML = (markdown: string): string => {
|
|
1003
|
+
const html = ReactDOMServer.renderToStaticMarkup(
|
|
1004
|
+
<div className={markdownClass}>
|
|
1005
|
+
<ReactMarkdown
|
|
1006
|
+
remarkPlugins={[remarkGfm]}
|
|
1007
|
+
rehypePlugins={[rehypeRaw]}
|
|
1008
|
+
>
|
|
1009
|
+
{markdown}
|
|
1010
|
+
</ReactMarkdown>
|
|
1011
|
+
</div>
|
|
1012
|
+
);
|
|
1013
|
+
return html;
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
// Convert conversation history to HTML - like ChatPanel
|
|
1017
|
+
const convertHistoryToHTML = (history: Record<string, HistoryEntry>): string => {
|
|
1018
|
+
const stylesheet = `
|
|
1019
|
+
<style>
|
|
1020
|
+
.conversation-history {
|
|
1021
|
+
font-family: Arial, sans-serif;
|
|
1022
|
+
line-height: 1.5;
|
|
1023
|
+
}
|
|
1024
|
+
.history-entry {
|
|
1025
|
+
margin-bottom: 15px;
|
|
1026
|
+
}
|
|
1027
|
+
.prompt-container, .response-container {
|
|
1028
|
+
margin-bottom: 10px;
|
|
1029
|
+
}
|
|
1030
|
+
.prompt, .response {
|
|
1031
|
+
display: block;
|
|
1032
|
+
margin: 5px 0;
|
|
1033
|
+
padding: 10px;
|
|
1034
|
+
border-radius: 5px;
|
|
1035
|
+
max-width: 80%;
|
|
1036
|
+
}
|
|
1037
|
+
.prompt {
|
|
1038
|
+
background-color: #efefef;
|
|
1039
|
+
margin-left: 0;
|
|
1040
|
+
}
|
|
1041
|
+
.response {
|
|
1042
|
+
background-color: #f0fcfd;
|
|
1043
|
+
margin-left: 25px;
|
|
1044
|
+
}
|
|
1045
|
+
</style>
|
|
1046
|
+
`;
|
|
1047
|
+
|
|
1048
|
+
let html = `
|
|
1049
|
+
<html>
|
|
1050
|
+
<head>
|
|
1051
|
+
${stylesheet}
|
|
1052
|
+
</head>
|
|
1053
|
+
<body>
|
|
1054
|
+
<h1>Conversation History (${new Date().toLocaleString()})</h1>
|
|
1055
|
+
<div class="conversation-history">
|
|
1056
|
+
`;
|
|
1057
|
+
|
|
1058
|
+
Object.entries(history).forEach(([prompt, response], index) => {
|
|
1059
|
+
if (hideInitialPrompt && index === 0) {
|
|
1060
|
+
html += `
|
|
1061
|
+
<div class="history-entry">
|
|
1062
|
+
<div class="response-container">
|
|
1063
|
+
<div class="response">${convertMarkdownToHTML(response.content)}</div>
|
|
1064
|
+
</div>
|
|
1065
|
+
</div>
|
|
1066
|
+
`;
|
|
1067
|
+
} else {
|
|
1068
|
+
html += `
|
|
1069
|
+
<div class="history-entry">
|
|
1070
|
+
<div class="prompt-container">
|
|
1071
|
+
<div class="prompt">${convertMarkdownToHTML(
|
|
1072
|
+
formatPromptForDisplay(prompt)
|
|
1073
|
+
)}</div>
|
|
1074
|
+
</div>
|
|
1075
|
+
<div class="response-container">
|
|
1076
|
+
<div class="response">${convertMarkdownToHTML(response.content)}</div>
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
`;
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
html += `
|
|
1084
|
+
</div>
|
|
1085
|
+
</body>
|
|
1086
|
+
</html>
|
|
1087
|
+
`;
|
|
1088
|
+
|
|
1089
|
+
return html;
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
// Save HTML to file - like ChatPanel
|
|
1093
|
+
const saveHTMLToFile = (html: string, filename: string) => {
|
|
1094
|
+
const blob = new Blob([html], { type: 'text/html' });
|
|
1095
|
+
const link = document.createElement('a');
|
|
1096
|
+
link.href = URL.createObjectURL(blob);
|
|
1097
|
+
link.download = filename;
|
|
1098
|
+
document.body.appendChild(link);
|
|
1099
|
+
link.click();
|
|
1100
|
+
document.body.removeChild(link);
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
// Download conversation as HTML file
|
|
1104
|
+
const saveAsHTMLFile = useCallback(() => {
|
|
1105
|
+
saveHTMLToFile(
|
|
1106
|
+
convertHistoryToHTML(history),
|
|
1107
|
+
`conversation-${new Date().toISOString()}.html`
|
|
1108
|
+
);
|
|
1109
|
+
interactionClicked(lastCallId || '', 'save');
|
|
1110
|
+
}, [history, lastCallId]);
|
|
1111
|
+
|
|
1112
|
+
const handleSendEmail = (to: string, from: string) => {
|
|
1113
|
+
sendConversationsViaEmail(to, `Conversation History from ${title}`, from);
|
|
1114
|
+
interactionClicked(lastCallId || '', 'email', to);
|
|
1115
|
+
setEmailSent(true);
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
const sendConversationsViaEmail = async (
|
|
1119
|
+
to: string,
|
|
1120
|
+
subject: string = `Conversation History from ${title}`,
|
|
1121
|
+
from: string = ''
|
|
1122
|
+
) => {
|
|
1123
|
+
fetch(`${publicAPIUrl}/share/email`, {
|
|
1124
|
+
method: 'POST',
|
|
1125
|
+
headers: {
|
|
1126
|
+
'Content-Type': 'application/json',
|
|
1127
|
+
},
|
|
1128
|
+
body: JSON.stringify({
|
|
1129
|
+
to: to,
|
|
1130
|
+
from: from,
|
|
1131
|
+
subject: subject,
|
|
1132
|
+
html: convertHistoryToHTML(history),
|
|
1133
|
+
project_id: project_id ?? '',
|
|
1134
|
+
customer: customer,
|
|
1135
|
+
history: history,
|
|
1136
|
+
title: title,
|
|
1137
|
+
}),
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
await interactionClicked(lastCallId || '', 'email', from);
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
// Send CTA email
|
|
1145
|
+
const sendCallToActionEmail = useCallback(async (from: string) => {
|
|
1146
|
+
try {
|
|
1147
|
+
await fetch(`${publicAPIUrl}/share/email`, {
|
|
1148
|
+
method: 'POST',
|
|
1149
|
+
headers: {
|
|
1150
|
+
'Content-Type': 'application/json',
|
|
1151
|
+
},
|
|
1152
|
+
body: JSON.stringify({
|
|
1153
|
+
to: callToActionEmailAddress,
|
|
1154
|
+
from: from,
|
|
1155
|
+
subject: `${callToActionEmailSubject} from ${from}`,
|
|
1156
|
+
html: convertHistoryToHTML(history),
|
|
1157
|
+
project_id: project_id ?? '',
|
|
1158
|
+
customer: customer,
|
|
1159
|
+
history: history,
|
|
1160
|
+
title: title,
|
|
1161
|
+
}),
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
await interactionClicked(lastCallId || '', 'cta', from);
|
|
1165
|
+
setCallToActionSent(true);
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
console.error('[AIChatPanel] Failed to send CTA email:', err);
|
|
1168
|
+
}
|
|
1169
|
+
}, [history, title, project_id, customer, lastCallId, publicAPIUrl, callToActionEmailAddress, callToActionEmailSubject]);
|
|
1170
|
+
|
|
1171
|
+
// Check if button should be disabled due to email capture requirements
|
|
1172
|
+
const isDisabledDueToNoEmail = useCallback(() => {
|
|
1173
|
+
if (customerEmailCaptureMode === 'REQUIRED' && !emailInputSet) {
|
|
1174
|
+
return true;
|
|
1175
|
+
}
|
|
1176
|
+
return false;
|
|
1177
|
+
}, [customerEmailCaptureMode, emailInputSet]);
|
|
1178
|
+
|
|
1179
|
+
// Handle tool approval for MCP tools
|
|
1180
|
+
const handleToolApproval = useCallback((toolName: string, scope: 'once' | 'session' | 'always') => {
|
|
1181
|
+
if (scope === 'session' || scope === 'always') {
|
|
1182
|
+
setSessionApprovedTools((prev) => Array.from(new Set([...prev, toolName])));
|
|
1183
|
+
}
|
|
1184
|
+
if (scope === 'always') {
|
|
1185
|
+
setAlwaysApprovedTools((prev) => Array.from(new Set([...prev, toolName])));
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Remove approved tool from pending list
|
|
1189
|
+
setPendingToolRequests((prev) => prev.filter((r) => r.toolName !== toolName));
|
|
1190
|
+
|
|
1191
|
+
console.log(`[AIChatPanel] Tool "${toolName}" approved with scope: ${scope}`);
|
|
1192
|
+
}, []);
|
|
1193
|
+
|
|
1194
|
+
// Get unique tool names from pending requests
|
|
1195
|
+
const getUniqueToolNames = useCallback(() => {
|
|
1196
|
+
return Array.from(new Set(pendingToolRequests.map((r) => r.toolName)));
|
|
1197
|
+
}, [pendingToolRequests]);
|
|
1198
|
+
|
|
910
1199
|
// ============================================================================
|
|
911
1200
|
// Callbacks
|
|
912
1201
|
// ============================================================================
|
|
@@ -1226,11 +1515,6 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1226
1515
|
fullPromptToSend = promptTemplate.replace('{{prompt}}', fullPromptToSend);
|
|
1227
1516
|
}
|
|
1228
1517
|
|
|
1229
|
-
// Add follow-on prompt
|
|
1230
|
-
if (followOnPrompt) {
|
|
1231
|
-
fullPromptToSend += `\n\n${followOnPrompt}`;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
1518
|
const newController = new AbortController();
|
|
1235
1519
|
setLastController(newController);
|
|
1236
1520
|
|
|
@@ -1314,7 +1598,6 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1314
1598
|
clearFollowOnQuestionsNextPrompt,
|
|
1315
1599
|
history,
|
|
1316
1600
|
promptTemplate,
|
|
1317
|
-
followOnPrompt,
|
|
1318
1601
|
send,
|
|
1319
1602
|
service,
|
|
1320
1603
|
ensureConversation,
|
|
@@ -1447,12 +1730,13 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1447
1730
|
useEffect(() => {
|
|
1448
1731
|
// Only auto-scroll if:
|
|
1449
1732
|
// 1. We're actively streaming (!idle)
|
|
1450
|
-
// 2. User hasn't manually scrolled up during this response
|
|
1733
|
+
// 2. User hasn't manually scrolled up during this response (or scrollToEnd prop is true)
|
|
1451
1734
|
// 3. We have content to show (response exists)
|
|
1452
|
-
|
|
1735
|
+
const shouldAutoScroll = scrollToEnd || !userHasScrolled;
|
|
1736
|
+
if (!idle && shouldAutoScroll && response) {
|
|
1453
1737
|
scrollToBottom();
|
|
1454
1738
|
}
|
|
1455
|
-
}, [response, scrollToBottom, idle, userHasScrolled]); // Removed history dependency
|
|
1739
|
+
}, [response, scrollToBottom, idle, userHasScrolled, scrollToEnd]); // Removed history dependency
|
|
1456
1740
|
|
|
1457
1741
|
// Ref to track idle state for scroll handler (avoids stale closure)
|
|
1458
1742
|
const idleRef = useRef(idle);
|
|
@@ -1548,6 +1832,15 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1548
1832
|
}
|
|
1549
1833
|
}, [initialPrompt, continueChat]);
|
|
1550
1834
|
|
|
1835
|
+
// Auto-send followOnPrompt when it changes (like ChatPanel)
|
|
1836
|
+
// This allows parent components to programmatically submit prompts to the current conversation
|
|
1837
|
+
useEffect(() => {
|
|
1838
|
+
if (followOnPrompt && followOnPrompt !== '' && followOnPrompt !== lastFollowOnPromptRef.current) {
|
|
1839
|
+
lastFollowOnPromptRef.current = followOnPrompt;
|
|
1840
|
+
continueChat(followOnPrompt);
|
|
1841
|
+
}
|
|
1842
|
+
}, [followOnPrompt, continueChat]);
|
|
1843
|
+
|
|
1551
1844
|
// Monitor for errors from useLLM hook
|
|
1552
1845
|
useEffect(() => {
|
|
1553
1846
|
if (llmError && llmError.trim()) {
|
|
@@ -1594,6 +1887,50 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1594
1887
|
}
|
|
1595
1888
|
}, [llmError, lastKey, lastCallId]);
|
|
1596
1889
|
|
|
1890
|
+
// Dynamic CSS Injection
|
|
1891
|
+
useEffect(() => {
|
|
1892
|
+
// Clean up any previously added CSS from this component
|
|
1893
|
+
const existingLinks = document.querySelectorAll(
|
|
1894
|
+
'link[data-source="ai-chat-panel"]'
|
|
1895
|
+
);
|
|
1896
|
+
existingLinks.forEach((link) => link.parentNode?.removeChild(link));
|
|
1897
|
+
|
|
1898
|
+
const existingStyles = document.querySelectorAll(
|
|
1899
|
+
'style[data-source="ai-chat-panel"]'
|
|
1900
|
+
);
|
|
1901
|
+
existingStyles.forEach((style) => style.parentNode?.removeChild(style));
|
|
1902
|
+
|
|
1903
|
+
if (cssUrl) {
|
|
1904
|
+
if (cssUrl.startsWith('http://') || cssUrl.startsWith('https://')) {
|
|
1905
|
+
// If it's a URL, create a link element
|
|
1906
|
+
const link = document.createElement('link');
|
|
1907
|
+
link.href = cssUrl;
|
|
1908
|
+
link.rel = 'stylesheet';
|
|
1909
|
+
link.setAttribute('data-source', 'ai-chat-panel');
|
|
1910
|
+
document.head.appendChild(link);
|
|
1911
|
+
} else {
|
|
1912
|
+
// If it's a CSS string, create a style element
|
|
1913
|
+
const style = document.createElement('style');
|
|
1914
|
+
style.textContent = cssUrl;
|
|
1915
|
+
style.setAttribute('data-source', 'ai-chat-panel');
|
|
1916
|
+
document.head.appendChild(style);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// Clean up when component unmounts
|
|
1921
|
+
return () => {
|
|
1922
|
+
const links = document.querySelectorAll(
|
|
1923
|
+
'link[data-source="ai-chat-panel"]'
|
|
1924
|
+
);
|
|
1925
|
+
links.forEach((link) => link.parentNode?.removeChild(link));
|
|
1926
|
+
|
|
1927
|
+
const styles = document.querySelectorAll(
|
|
1928
|
+
'style[data-source="ai-chat-panel"]'
|
|
1929
|
+
);
|
|
1930
|
+
styles.forEach((style) => style.parentNode?.removeChild(style));
|
|
1931
|
+
};
|
|
1932
|
+
}, [cssUrl]);
|
|
1933
|
+
|
|
1597
1934
|
// ============================================================================
|
|
1598
1935
|
// Render Helpers
|
|
1599
1936
|
// ============================================================================
|
|
@@ -1603,7 +1940,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1603
1940
|
const match = /language-(\w+)/.exec(className || '');
|
|
1604
1941
|
return !inline && match ? (
|
|
1605
1942
|
<SyntaxHighlighter
|
|
1606
|
-
style={
|
|
1943
|
+
style={effectivePrismStyle}
|
|
1607
1944
|
language={match[1]}
|
|
1608
1945
|
PreTag="div"
|
|
1609
1946
|
{...props}
|
|
@@ -1615,7 +1952,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1615
1952
|
{children}
|
|
1616
1953
|
</code>
|
|
1617
1954
|
);
|
|
1618
|
-
}, [
|
|
1955
|
+
}, [effectivePrismStyle]);
|
|
1619
1956
|
|
|
1620
1957
|
// Agent suggestion card component for inline agent handoff
|
|
1621
1958
|
const AgentSuggestionCard = React.memo(({ agentId, agentName, reason }: {
|
|
@@ -1824,7 +2161,13 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1824
2161
|
const panelClasses = ['ai-chat-panel', theme === 'dark' ? 'dark-theme' : ''].filter(Boolean).join(' ');
|
|
1825
2162
|
|
|
1826
2163
|
return (
|
|
1827
|
-
<div
|
|
2164
|
+
<div
|
|
2165
|
+
className={panelClasses}
|
|
2166
|
+
style={{
|
|
2167
|
+
...(width && { width }),
|
|
2168
|
+
...(height && { height })
|
|
2169
|
+
}}
|
|
2170
|
+
>
|
|
1828
2171
|
{/* Title */}
|
|
1829
2172
|
{title && <div className="ai-chat-panel__title">{title}</div>}
|
|
1830
2173
|
|
|
@@ -1858,9 +2201,23 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1858
2201
|
{initialMessage && (
|
|
1859
2202
|
<div className="ai-chat-message ai-chat-message--assistant">
|
|
1860
2203
|
<div className="ai-chat-message__content">
|
|
1861
|
-
|
|
1862
|
-
{
|
|
1863
|
-
|
|
2204
|
+
{markdownClass ? (
|
|
2205
|
+
<div className={markdownClass}>
|
|
2206
|
+
<ReactMarkdown
|
|
2207
|
+
remarkPlugins={[remarkGfm]}
|
|
2208
|
+
rehypePlugins={[rehypeRaw]}
|
|
2209
|
+
>
|
|
2210
|
+
{initialMessage}
|
|
2211
|
+
</ReactMarkdown>
|
|
2212
|
+
</div>
|
|
2213
|
+
) : (
|
|
2214
|
+
<ReactMarkdown
|
|
2215
|
+
remarkPlugins={[remarkGfm]}
|
|
2216
|
+
rehypePlugins={[rehypeRaw]}
|
|
2217
|
+
>
|
|
2218
|
+
{initialMessage}
|
|
2219
|
+
</ReactMarkdown>
|
|
2220
|
+
)}
|
|
1864
2221
|
</div>
|
|
1865
2222
|
</div>
|
|
1866
2223
|
)}
|
|
@@ -1895,13 +2252,25 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1895
2252
|
{thinkingBlocks.length > 0 && renderThinkingBlocks()}
|
|
1896
2253
|
|
|
1897
2254
|
{processedContent ? (
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2255
|
+
markdownClass ? (
|
|
2256
|
+
<div className={markdownClass}>
|
|
2257
|
+
<ReactMarkdown
|
|
2258
|
+
remarkPlugins={[remarkGfm]}
|
|
2259
|
+
rehypePlugins={[rehypeRaw]}
|
|
2260
|
+
components={markdownComponents}
|
|
2261
|
+
>
|
|
2262
|
+
{processedContent}
|
|
2263
|
+
</ReactMarkdown>
|
|
2264
|
+
</div>
|
|
2265
|
+
) : (
|
|
2266
|
+
<ReactMarkdown
|
|
2267
|
+
remarkPlugins={[remarkGfm]}
|
|
2268
|
+
rehypePlugins={[rehypeRaw]}
|
|
2269
|
+
components={markdownComponents}
|
|
2270
|
+
>
|
|
2271
|
+
{processedContent}
|
|
2272
|
+
</ReactMarkdown>
|
|
2273
|
+
)
|
|
1905
2274
|
) : (
|
|
1906
2275
|
<div className="ai-chat-loading">
|
|
1907
2276
|
<span>Thinking</span>
|
|
@@ -1916,13 +2285,25 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1916
2285
|
) : (
|
|
1917
2286
|
<>
|
|
1918
2287
|
{isLastEntry && thinkingBlocks.length > 0 && renderThinkingBlocks()}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2288
|
+
{markdownClass ? (
|
|
2289
|
+
<div className={markdownClass}>
|
|
2290
|
+
<ReactMarkdown
|
|
2291
|
+
remarkPlugins={[remarkGfm]}
|
|
2292
|
+
rehypePlugins={[rehypeRaw]}
|
|
2293
|
+
components={markdownComponents}
|
|
2294
|
+
>
|
|
2295
|
+
{processedContent}
|
|
2296
|
+
</ReactMarkdown>
|
|
2297
|
+
</div>
|
|
2298
|
+
) : (
|
|
2299
|
+
<ReactMarkdown
|
|
2300
|
+
remarkPlugins={[remarkGfm]}
|
|
2301
|
+
rehypePlugins={[rehypeRaw]}
|
|
2302
|
+
components={markdownComponents}
|
|
2303
|
+
>
|
|
2304
|
+
{processedContent}
|
|
2305
|
+
</ReactMarkdown>
|
|
2306
|
+
)}
|
|
1926
2307
|
</>
|
|
1927
2308
|
)}
|
|
1928
2309
|
</div>
|
|
@@ -1974,6 +2355,27 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1974
2355
|
</svg>
|
|
1975
2356
|
)}
|
|
1976
2357
|
</button>
|
|
2358
|
+
|
|
2359
|
+
{/* Tool Info Button - show if entry has tool data */}
|
|
2360
|
+
{(entry.toolCalls || entry.toolResponses) && (
|
|
2361
|
+
<button
|
|
2362
|
+
className="ai-chat-action-button"
|
|
2363
|
+
onClick={() => {
|
|
2364
|
+
setToolInfoData({
|
|
2365
|
+
calls: entry.toolCalls || [],
|
|
2366
|
+
responses: entry.toolResponses || [],
|
|
2367
|
+
});
|
|
2368
|
+
setIsToolInfoModalOpen(true);
|
|
2369
|
+
}}
|
|
2370
|
+
title="View tool information"
|
|
2371
|
+
>
|
|
2372
|
+
<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">
|
|
2373
|
+
<circle cx="12" cy="12" r="10" />
|
|
2374
|
+
<line x1="12" x2="12" y1="16" y2="12" />
|
|
2375
|
+
<line x1="12" x2="12.01" y1="8" y2="8" />
|
|
2376
|
+
</svg>
|
|
2377
|
+
</button>
|
|
2378
|
+
)}
|
|
1977
2379
|
</div>
|
|
1978
2380
|
)}
|
|
1979
2381
|
</div>
|
|
@@ -2001,6 +2403,109 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2001
2403
|
<div ref={bottomRef} />
|
|
2002
2404
|
</ScrollArea>
|
|
2003
2405
|
|
|
2406
|
+
{/* Tool Approval Panel */}
|
|
2407
|
+
{pendingToolRequests.length > 0 && (
|
|
2408
|
+
<div className="ai-chat-approve-tools-panel">
|
|
2409
|
+
<div className="ai-chat-approve-tools-header">
|
|
2410
|
+
Tool Approval Required
|
|
2411
|
+
</div>
|
|
2412
|
+
<div className="ai-chat-approve-tools-description">
|
|
2413
|
+
The AI wants to use the following tools:
|
|
2414
|
+
</div>
|
|
2415
|
+
{getUniqueToolNames().map((toolName) => (
|
|
2416
|
+
<div key={toolName} className="ai-chat-approve-tool-item">
|
|
2417
|
+
<div className="ai-chat-approve-tool-name">{toolName}</div>
|
|
2418
|
+
<div className="ai-chat-approve-tools-buttons">
|
|
2419
|
+
<Button
|
|
2420
|
+
size="sm"
|
|
2421
|
+
variant="outline"
|
|
2422
|
+
className="ai-chat-approve-tools-button"
|
|
2423
|
+
onClick={() => handleToolApproval(toolName, 'once')}
|
|
2424
|
+
>
|
|
2425
|
+
Once
|
|
2426
|
+
</Button>
|
|
2427
|
+
<Button
|
|
2428
|
+
size="sm"
|
|
2429
|
+
variant="outline"
|
|
2430
|
+
className="ai-chat-approve-tools-button"
|
|
2431
|
+
onClick={() => handleToolApproval(toolName, 'session')}
|
|
2432
|
+
>
|
|
2433
|
+
This Session
|
|
2434
|
+
</Button>
|
|
2435
|
+
<Button
|
|
2436
|
+
size="sm"
|
|
2437
|
+
variant="default"
|
|
2438
|
+
className="ai-chat-approve-tools-button"
|
|
2439
|
+
onClick={() => handleToolApproval(toolName, 'always')}
|
|
2440
|
+
>
|
|
2441
|
+
Always
|
|
2442
|
+
</Button>
|
|
2443
|
+
</div>
|
|
2444
|
+
</div>
|
|
2445
|
+
))}
|
|
2446
|
+
</div>
|
|
2447
|
+
)}
|
|
2448
|
+
|
|
2449
|
+
{/* Button Container - Save, Email, CTA */}
|
|
2450
|
+
{(showSaveButton || showEmailButton || showCallToAction) && (
|
|
2451
|
+
<div className="ai-chat-button-container">
|
|
2452
|
+
{showSaveButton && (
|
|
2453
|
+
<Button
|
|
2454
|
+
variant="outline"
|
|
2455
|
+
size="sm"
|
|
2456
|
+
onClick={saveAsHTMLFile}
|
|
2457
|
+
disabled={Object.keys(history).length === 0}
|
|
2458
|
+
className="ai-chat-save-button"
|
|
2459
|
+
>
|
|
2460
|
+
💾 Save
|
|
2461
|
+
</Button>
|
|
2462
|
+
)}
|
|
2463
|
+
|
|
2464
|
+
{showEmailButton && (
|
|
2465
|
+
<Button
|
|
2466
|
+
variant="outline"
|
|
2467
|
+
size="sm"
|
|
2468
|
+
onClick={() => {
|
|
2469
|
+
if (isEmailAddress(emailInput)) {
|
|
2470
|
+
setEmailInputSet(true);
|
|
2471
|
+
setEmailValid(true);
|
|
2472
|
+
handleSendEmail(emailInput, emailInput);
|
|
2473
|
+
setEmailClickedButNoEmail(false);
|
|
2474
|
+
} else {
|
|
2475
|
+
setShowEmailPanel(true);
|
|
2476
|
+
setEmailValid(false);
|
|
2477
|
+
setEmailClickedButNoEmail(true);
|
|
2478
|
+
}
|
|
2479
|
+
}}
|
|
2480
|
+
disabled={Object.keys(history).length === 0 || isDisabledDueToNoEmail()}
|
|
2481
|
+
className="ai-chat-email-button"
|
|
2482
|
+
>
|
|
2483
|
+
📧 Email Conversation{emailSent ? ' ✓' : ''}
|
|
2484
|
+
</Button>
|
|
2485
|
+
)}
|
|
2486
|
+
|
|
2487
|
+
{showCallToAction && (
|
|
2488
|
+
<Button
|
|
2489
|
+
variant={callToActionSent ? 'outline' : 'default'}
|
|
2490
|
+
size="sm"
|
|
2491
|
+
onClick={() => {
|
|
2492
|
+
if (customerEmailCaptureMode !== 'HIDE' && !emailInputSet) {
|
|
2493
|
+
setCTAClickedButNoEmail(true);
|
|
2494
|
+
setTimeout(() => setCTAClickedButNoEmail(false), 3000);
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
const fromEmail = emailInput || customer?.customer_user_email || '';
|
|
2498
|
+
sendCallToActionEmail(fromEmail);
|
|
2499
|
+
}}
|
|
2500
|
+
disabled={callToActionSent || Object.keys(history).length === 0}
|
|
2501
|
+
className="ai-chat-cta-button"
|
|
2502
|
+
>
|
|
2503
|
+
{callToActionSent ? '✓ Submitted' : callToActionButtonText}
|
|
2504
|
+
</Button>
|
|
2505
|
+
)}
|
|
2506
|
+
</div>
|
|
2507
|
+
)}
|
|
2508
|
+
|
|
2004
2509
|
{/* New Conversation Button */}
|
|
2005
2510
|
{showNewConversationButton && (
|
|
2006
2511
|
<div className="ai-chat-panel__new-conversation">
|
|
@@ -2015,6 +2520,103 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2015
2520
|
</div>
|
|
2016
2521
|
)}
|
|
2017
2522
|
|
|
2523
|
+
{/* Customer Email Capture Panel */}
|
|
2524
|
+
{showEmailPanel && (
|
|
2525
|
+
<>
|
|
2526
|
+
{!emailValid && (
|
|
2527
|
+
<div className="ai-chat-email-input-message">
|
|
2528
|
+
{isDisabledDueToNoEmail()
|
|
2529
|
+
? "Let's get started - please enter your email"
|
|
2530
|
+
: CTAClickedButNoEmail || emailClickedButNoEmail
|
|
2531
|
+
? 'Sure, we just need an email address to contact you'
|
|
2532
|
+
: 'Email address is invalid'}
|
|
2533
|
+
</div>
|
|
2534
|
+
)}
|
|
2535
|
+
<div className="ai-chat-email-input-container">
|
|
2536
|
+
<input
|
|
2537
|
+
type="email"
|
|
2538
|
+
name="email"
|
|
2539
|
+
id="email"
|
|
2540
|
+
className={
|
|
2541
|
+
emailValid
|
|
2542
|
+
? emailInputSet
|
|
2543
|
+
? 'ai-chat-email-input-set'
|
|
2544
|
+
: 'ai-chat-email-input'
|
|
2545
|
+
: 'ai-chat-email-input-invalid'
|
|
2546
|
+
}
|
|
2547
|
+
placeholder={customerEmailCapturePlaceholder}
|
|
2548
|
+
value={emailInput}
|
|
2549
|
+
onChange={(e) => {
|
|
2550
|
+
const newEmail = e.target.value;
|
|
2551
|
+
setEmailInput(newEmail);
|
|
2552
|
+
// Reset validation while typing
|
|
2553
|
+
if (!emailInputSet) {
|
|
2554
|
+
if (customerEmailCaptureMode === 'REQUIRED' && newEmail !== '') {
|
|
2555
|
+
setEmailValid(isEmailAddress(newEmail));
|
|
2556
|
+
} else {
|
|
2557
|
+
setEmailValid(true);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}}
|
|
2561
|
+
onBlur={() => {
|
|
2562
|
+
// Auto-validate and set email when field loses focus
|
|
2563
|
+
if (emailInput && isEmailAddress(emailInput) && !emailInputSet) {
|
|
2564
|
+
setEmailInputSet(true);
|
|
2565
|
+
setEmailValid(true);
|
|
2566
|
+
interactionClicked('', 'emailcapture', emailInput);
|
|
2567
|
+
|
|
2568
|
+
// Handle pending actions
|
|
2569
|
+
if (CTAClickedButNoEmail) {
|
|
2570
|
+
sendCallToActionEmail(emailInput);
|
|
2571
|
+
setCTAClickedButNoEmail(false);
|
|
2572
|
+
}
|
|
2573
|
+
if (emailClickedButNoEmail) {
|
|
2574
|
+
handleSendEmail(emailInput, emailInput);
|
|
2575
|
+
setEmailClickedButNoEmail(false);
|
|
2576
|
+
}
|
|
2577
|
+
} else if (customerEmailCaptureMode === 'REQUIRED' && emailInput !== '') {
|
|
2578
|
+
setEmailValid(isEmailAddress(emailInput));
|
|
2579
|
+
}
|
|
2580
|
+
}}
|
|
2581
|
+
onKeyDown={(e) => {
|
|
2582
|
+
if (e.key === 'Enter') {
|
|
2583
|
+
if (isEmailAddress(emailInput)) {
|
|
2584
|
+
setEmailInputSet(true);
|
|
2585
|
+
setEmailValid(true);
|
|
2586
|
+
interactionClicked('', 'emailcapture', emailInput);
|
|
2587
|
+
|
|
2588
|
+
// Handle pending actions
|
|
2589
|
+
if (CTAClickedButNoEmail) {
|
|
2590
|
+
sendCallToActionEmail(emailInput);
|
|
2591
|
+
setCTAClickedButNoEmail(false);
|
|
2592
|
+
}
|
|
2593
|
+
if (emailClickedButNoEmail) {
|
|
2594
|
+
handleSendEmail(emailInput, emailInput);
|
|
2595
|
+
setEmailClickedButNoEmail(false);
|
|
2596
|
+
}
|
|
2597
|
+
} else {
|
|
2598
|
+
setEmailValid(false);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
}}
|
|
2602
|
+
disabled={false}
|
|
2603
|
+
/>
|
|
2604
|
+
{emailInputSet && (
|
|
2605
|
+
<button
|
|
2606
|
+
className="ai-chat-email-edit-button"
|
|
2607
|
+
onClick={() => {
|
|
2608
|
+
setEmailInputSet(false);
|
|
2609
|
+
setEmailValid(true);
|
|
2610
|
+
}}
|
|
2611
|
+
title="Edit email"
|
|
2612
|
+
>
|
|
2613
|
+
✎
|
|
2614
|
+
</button>
|
|
2615
|
+
)}
|
|
2616
|
+
</div>
|
|
2617
|
+
</>
|
|
2618
|
+
)}
|
|
2619
|
+
|
|
2018
2620
|
{/* Input Area - Isolated component for performance */}
|
|
2019
2621
|
<ChatInput
|
|
2020
2622
|
placeholder={placeholder}
|
|
@@ -2064,6 +2666,13 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2064
2666
|
</div>
|
|
2065
2667
|
</div>
|
|
2066
2668
|
)}
|
|
2669
|
+
|
|
2670
|
+
{/* Modals */}
|
|
2671
|
+
<ToolInfoModal
|
|
2672
|
+
isOpen={isToolInfoModalOpen}
|
|
2673
|
+
onClose={() => setIsToolInfoModalOpen(false)}
|
|
2674
|
+
data={toolInfoData}
|
|
2675
|
+
/>
|
|
2067
2676
|
</div>
|
|
2068
2677
|
);
|
|
2069
2678
|
};
|