@hef2024/llmasaservice-ui 0.19.1 → 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 +831 -0
- package/DEBUG-ERROR-HANDLING.md +131 -0
- package/FIX-APPLIED.md +235 -0
- package/IMPLEMENTATION-COMPLETE.md +247 -0
- package/README.md +1 -0
- package/dist/index.css +635 -1
- package/dist/index.d.mts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +1241 -454
- package/dist/index.mjs +1083 -296
- package/docs/CHANGELOG-ERROR-HANDLING.md +248 -0
- package/docs/CONVERSATION-HISTORY.md +1 -0
- package/docs/ERROR-HANDLING-413.md +254 -0
- package/docs/ERROR-HANDLING-SUMMARY.md +132 -0
- package/package.json +2 -2
- package/src/AIAgentPanel.tsx +97 -1
- package/src/AIChatPanel.css +656 -1
- package/src/AIChatPanel.tsx +914 -69
- package/src/AgentPanel.tsx +3 -1
- package/src/ChatPanel.css +111 -0
- package/src/ChatPanel.tsx +169 -33
- 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
|
/**
|
|
@@ -235,6 +260,21 @@ const LLMAsAServiceLogo = () => (
|
|
|
235
260
|
</svg>
|
|
236
261
|
);
|
|
237
262
|
|
|
263
|
+
const AlertCircleIcon = () => (
|
|
264
|
+
<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">
|
|
265
|
+
<circle cx="12" cy="12" r="10" />
|
|
266
|
+
<line x1="12" x2="12" y1="8" y2="12" />
|
|
267
|
+
<line x1="12" x2="12.01" y1="16" y2="16" />
|
|
268
|
+
</svg>
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const CloseIcon = () => (
|
|
272
|
+
<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">
|
|
273
|
+
<line x1="18" x2="6" y1="6" y2="18" />
|
|
274
|
+
<line x1="6" x2="18" y1="6" y2="18" />
|
|
275
|
+
</svg>
|
|
276
|
+
);
|
|
277
|
+
|
|
238
278
|
// ============================================================================
|
|
239
279
|
// Isolated Input Component - Prevents full re-renders on every keystroke
|
|
240
280
|
// ============================================================================
|
|
@@ -662,6 +702,25 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
662
702
|
maxContextTokens = 8000,
|
|
663
703
|
enableContextDetailView = false,
|
|
664
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...',
|
|
665
724
|
}) => {
|
|
666
725
|
// ============================================================================
|
|
667
726
|
// API URL
|
|
@@ -685,6 +744,42 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
685
744
|
const [newConversationConfirm, setNewConversationConfirm] = useState(false);
|
|
686
745
|
const [justReset, setJustReset] = useState(false);
|
|
687
746
|
const [copiedCallId, setCopiedCallId] = useState<string | null>(null);
|
|
747
|
+
const [feedbackCallId, setFeedbackCallId] = useState<{ callId: string; type: 'up' | 'down' } | null>(null);
|
|
748
|
+
const [error, setError] = useState<{ message: string; code?: string } | null>(null);
|
|
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]);
|
|
688
783
|
|
|
689
784
|
// Refs
|
|
690
785
|
const bottomRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -705,6 +800,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
705
800
|
const latestHistoryRef = useRef<Record<string, HistoryEntry>>(initialHistory);
|
|
706
801
|
// Track if we've sent the initial prompt (prevents loops)
|
|
707
802
|
const initialPromptSentRef = useRef<boolean>(false);
|
|
803
|
+
// Track the last followOnPrompt to detect changes (for auto-submit trigger)
|
|
804
|
+
const lastFollowOnPromptRef = useRef<string>('');
|
|
708
805
|
|
|
709
806
|
// Sync new entries from initialHistory into local history state
|
|
710
807
|
// This allows parent components to inject messages (e.g., page-based agent suggestions)
|
|
@@ -752,6 +849,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
752
849
|
lastCallId,
|
|
753
850
|
stop,
|
|
754
851
|
setResponse,
|
|
852
|
+
error: llmError,
|
|
755
853
|
} = llmResult;
|
|
756
854
|
|
|
757
855
|
// Tool-related properties (may not exist on all versions of useLLM)
|
|
@@ -778,9 +876,9 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
778
876
|
// ============================================================================
|
|
779
877
|
// Memoized Values
|
|
780
878
|
// ============================================================================
|
|
781
|
-
const
|
|
782
|
-
() => (theme === 'light' ? materialLight : materialDark),
|
|
783
|
-
[theme]
|
|
879
|
+
const effectivePrismStyle = useMemo(
|
|
880
|
+
() => prismStyle ?? (theme === 'light' ? materialLight : materialDark),
|
|
881
|
+
[prismStyle, theme]
|
|
784
882
|
);
|
|
785
883
|
|
|
786
884
|
// Browser info for context (matches ChatPanel)
|
|
@@ -889,6 +987,215 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
889
987
|
const currentAgentLabel = currentAgentInfo.label;
|
|
890
988
|
const currentAgentAvatarUrl = currentAgentInfo.avatarUrl;
|
|
891
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
|
+
|
|
892
1199
|
// ============================================================================
|
|
893
1200
|
// Callbacks
|
|
894
1201
|
// ============================================================================
|
|
@@ -989,6 +1296,50 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
989
1296
|
return displayPrompt;
|
|
990
1297
|
}, [hideRagContextInPrompt]);
|
|
991
1298
|
|
|
1299
|
+
// Built-in interaction tracking - reports to LLMAsAService API
|
|
1300
|
+
const interactionClicked = useCallback(async (
|
|
1301
|
+
callId: string,
|
|
1302
|
+
action: string,
|
|
1303
|
+
emailaddress: string = "",
|
|
1304
|
+
comment: string = ""
|
|
1305
|
+
) => {
|
|
1306
|
+
console.log(`[AIChatPanel] Interaction: ${action} for callId: ${callId}`);
|
|
1307
|
+
|
|
1308
|
+
// Ensure conversation exists
|
|
1309
|
+
const convId = currentConversation || await ensureConversation();
|
|
1310
|
+
|
|
1311
|
+
// Use the callId parameter or fallback to conversation ID
|
|
1312
|
+
const finalCallId = callId || convId;
|
|
1313
|
+
|
|
1314
|
+
// Get email from customer data
|
|
1315
|
+
const isEmailAddress = (str: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
|
|
1316
|
+
const email = emailaddress && emailaddress !== ""
|
|
1317
|
+
? emailaddress
|
|
1318
|
+
: isEmailAddress(customer?.customer_user_email ?? "")
|
|
1319
|
+
? customer?.customer_user_email
|
|
1320
|
+
: isEmailAddress(customer?.customer_id ?? "")
|
|
1321
|
+
? customer?.customer_id
|
|
1322
|
+
: "";
|
|
1323
|
+
|
|
1324
|
+
// Send feedback to API
|
|
1325
|
+
try {
|
|
1326
|
+
await fetch(`${publicAPIUrl}/feedback/${finalCallId}/${action}`, {
|
|
1327
|
+
method: "POST",
|
|
1328
|
+
headers: {
|
|
1329
|
+
"Content-Type": "application/json",
|
|
1330
|
+
},
|
|
1331
|
+
body: JSON.stringify({
|
|
1332
|
+
project_id: project_id ?? "",
|
|
1333
|
+
conversation_id: convId ?? "",
|
|
1334
|
+
email: email,
|
|
1335
|
+
comment: comment,
|
|
1336
|
+
}),
|
|
1337
|
+
});
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
console.error('[AIChatPanel] Failed to send feedback:', err);
|
|
1340
|
+
}
|
|
1341
|
+
}, [currentConversation, ensureConversation, customer, project_id, publicAPIUrl]);
|
|
1342
|
+
|
|
992
1343
|
// Copy to clipboard
|
|
993
1344
|
const copyToClipboard = useCallback(async (text: string, callId: string) => {
|
|
994
1345
|
try {
|
|
@@ -1003,10 +1354,46 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1003
1354
|
await navigator.clipboard.writeText(cleanText);
|
|
1004
1355
|
setCopiedCallId(callId);
|
|
1005
1356
|
setTimeout(() => setCopiedCallId(null), 2000);
|
|
1357
|
+
|
|
1358
|
+
// Report to API (built-in)
|
|
1359
|
+
await interactionClicked(callId, "copy");
|
|
1006
1360
|
} catch (err) {
|
|
1007
1361
|
console.error('Failed to copy:', err);
|
|
1008
1362
|
}
|
|
1009
|
-
}, []);
|
|
1363
|
+
}, [interactionClicked]);
|
|
1364
|
+
|
|
1365
|
+
// Handle thumbs up/down with visual feedback
|
|
1366
|
+
const handleThumbsUp = useCallback(async (callId: string) => {
|
|
1367
|
+
console.log('[AIChatPanel] Thumbs up clicked:', callId);
|
|
1368
|
+
|
|
1369
|
+
// Show visual feedback
|
|
1370
|
+
setFeedbackCallId({ callId, type: 'up' });
|
|
1371
|
+
setTimeout(() => setFeedbackCallId(null), 2000);
|
|
1372
|
+
|
|
1373
|
+
// Report to API (built-in)
|
|
1374
|
+
await interactionClicked(callId, "thumbsup");
|
|
1375
|
+
|
|
1376
|
+
// Call external callback if provided
|
|
1377
|
+
if (thumbsUpClick) {
|
|
1378
|
+
thumbsUpClick(callId);
|
|
1379
|
+
}
|
|
1380
|
+
}, [thumbsUpClick, interactionClicked]);
|
|
1381
|
+
|
|
1382
|
+
const handleThumbsDown = useCallback(async (callId: string) => {
|
|
1383
|
+
console.log('[AIChatPanel] Thumbs down clicked:', callId);
|
|
1384
|
+
|
|
1385
|
+
// Show visual feedback
|
|
1386
|
+
setFeedbackCallId({ callId, type: 'down' });
|
|
1387
|
+
setTimeout(() => setFeedbackCallId(null), 2000);
|
|
1388
|
+
|
|
1389
|
+
// Report to API (built-in)
|
|
1390
|
+
await interactionClicked(callId, "thumbsdown");
|
|
1391
|
+
|
|
1392
|
+
// Call external callback if provided
|
|
1393
|
+
if (thumbsDownClick) {
|
|
1394
|
+
thumbsDownClick(callId);
|
|
1395
|
+
}
|
|
1396
|
+
}, [thumbsDownClick, interactionClicked]);
|
|
1010
1397
|
|
|
1011
1398
|
// Scroll to bottom - throttled using RAF to prevent layout thrashing
|
|
1012
1399
|
const scrollToBottom = useCallback(() => {
|
|
@@ -1039,6 +1426,9 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1039
1426
|
setThinkingBlocks([]);
|
|
1040
1427
|
setCurrentThinkingIndex(0);
|
|
1041
1428
|
|
|
1429
|
+
// Clear any previous errors
|
|
1430
|
+
setError(null);
|
|
1431
|
+
|
|
1042
1432
|
// Reset scroll tracking for new message - enable auto-scroll
|
|
1043
1433
|
setUserHasScrolled(false);
|
|
1044
1434
|
|
|
@@ -1125,11 +1515,6 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1125
1515
|
fullPromptToSend = promptTemplate.replace('{{prompt}}', fullPromptToSend);
|
|
1126
1516
|
}
|
|
1127
1517
|
|
|
1128
|
-
// Add follow-on prompt
|
|
1129
|
-
if (followOnPrompt) {
|
|
1130
|
-
fullPromptToSend += `\n\n${followOnPrompt}`;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
1518
|
const newController = new AbortController();
|
|
1134
1519
|
setLastController(newController);
|
|
1135
1520
|
|
|
@@ -1147,7 +1532,48 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1147
1532
|
true, // includeHistory
|
|
1148
1533
|
service, // group_id from agent config
|
|
1149
1534
|
convId, // Use the conversation ID from ensureConversation
|
|
1150
|
-
newController
|
|
1535
|
+
newController,
|
|
1536
|
+
undefined, // onComplete
|
|
1537
|
+
(errorMsg: string) => {
|
|
1538
|
+
// Error callback - handle errors immediately
|
|
1539
|
+
console.log('[AIChatPanel] Error callback triggered:', errorMsg);
|
|
1540
|
+
|
|
1541
|
+
// Detect 413 Content Too Large error
|
|
1542
|
+
if (errorMsg.includes('413') || errorMsg.toLowerCase().includes('content too large')) {
|
|
1543
|
+
setError({
|
|
1544
|
+
message: 'The context is too large to process. Please start a new conversation or reduce the amount of context.',
|
|
1545
|
+
code: '413',
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
// Detect other network errors
|
|
1549
|
+
else if (errorMsg.toLowerCase().includes('network error') || errorMsg.toLowerCase().includes('fetch')) {
|
|
1550
|
+
setError({
|
|
1551
|
+
message: 'Network error. Please check your connection and try again.',
|
|
1552
|
+
code: 'NETWORK_ERROR',
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
// Generic error
|
|
1556
|
+
else {
|
|
1557
|
+
setError({
|
|
1558
|
+
message: errorMsg,
|
|
1559
|
+
code: 'UNKNOWN_ERROR',
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Reset loading state
|
|
1564
|
+
setIsLoading(false);
|
|
1565
|
+
|
|
1566
|
+
// Update history to show error
|
|
1567
|
+
if (promptKey) {
|
|
1568
|
+
setHistory((prev) => ({
|
|
1569
|
+
...prev,
|
|
1570
|
+
[promptKey]: {
|
|
1571
|
+
content: `Error: ${errorMsg}`,
|
|
1572
|
+
callId: lastCallId || '',
|
|
1573
|
+
},
|
|
1574
|
+
}));
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1151
1577
|
);
|
|
1152
1578
|
|
|
1153
1579
|
setLastMessages(messagesAndHistory);
|
|
@@ -1172,7 +1598,6 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1172
1598
|
clearFollowOnQuestionsNextPrompt,
|
|
1173
1599
|
history,
|
|
1174
1600
|
promptTemplate,
|
|
1175
|
-
followOnPrompt,
|
|
1176
1601
|
send,
|
|
1177
1602
|
service,
|
|
1178
1603
|
ensureConversation,
|
|
@@ -1221,6 +1646,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1221
1646
|
setJustReset(true);
|
|
1222
1647
|
setLastController(new AbortController());
|
|
1223
1648
|
setUserHasScrolled(false);
|
|
1649
|
+
setError(null); // Clear any errors
|
|
1224
1650
|
|
|
1225
1651
|
setTimeout(() => {
|
|
1226
1652
|
setJustReset(false);
|
|
@@ -1271,6 +1697,9 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1271
1697
|
if (wasStreaming && isNowIdle && !hasNotifiedCompletionRef.current) {
|
|
1272
1698
|
hasNotifiedCompletionRef.current = true;
|
|
1273
1699
|
|
|
1700
|
+
// Reset loading state on completion
|
|
1701
|
+
setIsLoading(false);
|
|
1702
|
+
|
|
1274
1703
|
// Get the latest values from refs (stable, not from closure)
|
|
1275
1704
|
const currentHistory = latestHistoryRef.current;
|
|
1276
1705
|
const currentLastKey = lastKeyRef.current;
|
|
@@ -1301,12 +1730,13 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1301
1730
|
useEffect(() => {
|
|
1302
1731
|
// Only auto-scroll if:
|
|
1303
1732
|
// 1. We're actively streaming (!idle)
|
|
1304
|
-
// 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)
|
|
1305
1734
|
// 3. We have content to show (response exists)
|
|
1306
|
-
|
|
1735
|
+
const shouldAutoScroll = scrollToEnd || !userHasScrolled;
|
|
1736
|
+
if (!idle && shouldAutoScroll && response) {
|
|
1307
1737
|
scrollToBottom();
|
|
1308
1738
|
}
|
|
1309
|
-
}, [response, scrollToBottom, idle, userHasScrolled]); // Removed history dependency
|
|
1739
|
+
}, [response, scrollToBottom, idle, userHasScrolled, scrollToEnd]); // Removed history dependency
|
|
1310
1740
|
|
|
1311
1741
|
// Ref to track idle state for scroll handler (avoids stale closure)
|
|
1312
1742
|
const idleRef = useRef(idle);
|
|
@@ -1402,6 +1832,105 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1402
1832
|
}
|
|
1403
1833
|
}, [initialPrompt, continueChat]);
|
|
1404
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
|
+
|
|
1844
|
+
// Monitor for errors from useLLM hook
|
|
1845
|
+
useEffect(() => {
|
|
1846
|
+
if (llmError && llmError.trim()) {
|
|
1847
|
+
console.log('[AIChatPanel] Error detected:', llmError);
|
|
1848
|
+
|
|
1849
|
+
// Parse error message to detect specific error types
|
|
1850
|
+
const errorMessage = llmError;
|
|
1851
|
+
|
|
1852
|
+
// Detect 413 Content Too Large error
|
|
1853
|
+
if (errorMessage.includes('413') || errorMessage.toLowerCase().includes('content too large')) {
|
|
1854
|
+
setError({
|
|
1855
|
+
message: 'The context is too large to process. Please start a new conversation or reduce the amount of context.',
|
|
1856
|
+
code: '413',
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
// Detect other network errors
|
|
1860
|
+
else if (errorMessage.toLowerCase().includes('network error') || errorMessage.toLowerCase().includes('fetch')) {
|
|
1861
|
+
setError({
|
|
1862
|
+
message: 'Network error. Please check your connection and try again.',
|
|
1863
|
+
code: 'NETWORK_ERROR',
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
// Generic error
|
|
1867
|
+
else {
|
|
1868
|
+
setError({
|
|
1869
|
+
message: errorMessage,
|
|
1870
|
+
code: 'UNKNOWN_ERROR',
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// Reset loading state
|
|
1875
|
+
setIsLoading(false);
|
|
1876
|
+
|
|
1877
|
+
// Update history to show error
|
|
1878
|
+
if (lastKey) {
|
|
1879
|
+
setHistory((prev) => ({
|
|
1880
|
+
...prev,
|
|
1881
|
+
[lastKey]: {
|
|
1882
|
+
content: `Error: ${errorMessage}`,
|
|
1883
|
+
callId: lastCallId || '',
|
|
1884
|
+
},
|
|
1885
|
+
}));
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}, [llmError, lastKey, lastCallId]);
|
|
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
|
+
|
|
1405
1934
|
// ============================================================================
|
|
1406
1935
|
// Render Helpers
|
|
1407
1936
|
// ============================================================================
|
|
@@ -1411,7 +1940,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1411
1940
|
const match = /language-(\w+)/.exec(className || '');
|
|
1412
1941
|
return !inline && match ? (
|
|
1413
1942
|
<SyntaxHighlighter
|
|
1414
|
-
style={
|
|
1943
|
+
style={effectivePrismStyle}
|
|
1415
1944
|
language={match[1]}
|
|
1416
1945
|
PreTag="div"
|
|
1417
1946
|
{...props}
|
|
@@ -1423,14 +1952,23 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1423
1952
|
{children}
|
|
1424
1953
|
</code>
|
|
1425
1954
|
);
|
|
1426
|
-
}, [
|
|
1955
|
+
}, [effectivePrismStyle]);
|
|
1427
1956
|
|
|
1428
1957
|
// Agent suggestion card component for inline agent handoff
|
|
1429
|
-
const AgentSuggestionCard =
|
|
1958
|
+
const AgentSuggestionCard = React.memo(({ agentId, agentName, reason }: {
|
|
1430
1959
|
agentId: string;
|
|
1431
1960
|
agentName: string;
|
|
1432
1961
|
reason: string;
|
|
1433
1962
|
}) => {
|
|
1963
|
+
// Auto-scroll when the agent suggestion card appears
|
|
1964
|
+
useEffect(() => {
|
|
1965
|
+
// Small delay to ensure the card is fully rendered in the DOM
|
|
1966
|
+
const timer = setTimeout(() => {
|
|
1967
|
+
scrollToBottom();
|
|
1968
|
+
}, 100);
|
|
1969
|
+
return () => clearTimeout(timer);
|
|
1970
|
+
}, []); // Empty deps - only run on mount
|
|
1971
|
+
|
|
1434
1972
|
if (!agentId || !onAgentChange) return null;
|
|
1435
1973
|
|
|
1436
1974
|
// Validate agent ID - must be a valid UUID or exist in agentOptions
|
|
@@ -1547,15 +2085,15 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1547
2085
|
onAgentChange(agentId);
|
|
1548
2086
|
// Scroll to bottom after a brief delay to let React re-render
|
|
1549
2087
|
setTimeout(() => {
|
|
1550
|
-
|
|
1551
|
-
},
|
|
2088
|
+
scrollToBottom();
|
|
2089
|
+
}, 100);
|
|
1552
2090
|
}}
|
|
1553
2091
|
>
|
|
1554
2092
|
Switch
|
|
1555
2093
|
</Button>
|
|
1556
2094
|
</span>
|
|
1557
2095
|
);
|
|
1558
|
-
}
|
|
2096
|
+
});
|
|
1559
2097
|
|
|
1560
2098
|
// Markdown components including custom agent-suggestion element
|
|
1561
2099
|
const markdownComponents = useMemo(() => ({
|
|
@@ -1623,19 +2161,63 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1623
2161
|
const panelClasses = ['ai-chat-panel', theme === 'dark' ? 'dark-theme' : ''].filter(Boolean).join(' ');
|
|
1624
2162
|
|
|
1625
2163
|
return (
|
|
1626
|
-
<div
|
|
2164
|
+
<div
|
|
2165
|
+
className={panelClasses}
|
|
2166
|
+
style={{
|
|
2167
|
+
...(width && { width }),
|
|
2168
|
+
...(height && { height })
|
|
2169
|
+
}}
|
|
2170
|
+
>
|
|
1627
2171
|
{/* Title */}
|
|
1628
2172
|
{title && <div className="ai-chat-panel__title">{title}</div>}
|
|
1629
2173
|
|
|
2174
|
+
{/* Error Banner */}
|
|
2175
|
+
{error && (
|
|
2176
|
+
<div className="ai-chat-error-banner">
|
|
2177
|
+
<div className="ai-chat-error-banner__icon">
|
|
2178
|
+
<AlertCircleIcon />
|
|
2179
|
+
</div>
|
|
2180
|
+
<div className="ai-chat-error-banner__content">
|
|
2181
|
+
<div className="ai-chat-error-banner__message">{error.message}</div>
|
|
2182
|
+
{error.code === '413' && (
|
|
2183
|
+
<div className="ai-chat-error-banner__hint">
|
|
2184
|
+
Try starting a new conversation or reducing the amount of information being sent.
|
|
2185
|
+
</div>
|
|
2186
|
+
)}
|
|
2187
|
+
</div>
|
|
2188
|
+
<button
|
|
2189
|
+
className="ai-chat-error-banner__close"
|
|
2190
|
+
onClick={() => setError(null)}
|
|
2191
|
+
aria-label="Dismiss error"
|
|
2192
|
+
>
|
|
2193
|
+
<CloseIcon />
|
|
2194
|
+
</button>
|
|
2195
|
+
</div>
|
|
2196
|
+
)}
|
|
2197
|
+
|
|
1630
2198
|
{/* Messages Area */}
|
|
1631
2199
|
<ScrollArea className="ai-chat-panel__messages" ref={responseAreaRef}>
|
|
1632
2200
|
{/* Initial Message */}
|
|
1633
2201
|
{initialMessage && (
|
|
1634
2202
|
<div className="ai-chat-message ai-chat-message--assistant">
|
|
1635
2203
|
<div className="ai-chat-message__content">
|
|
1636
|
-
|
|
1637
|
-
{
|
|
1638
|
-
|
|
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
|
+
)}
|
|
1639
2221
|
</div>
|
|
1640
2222
|
</div>
|
|
1641
2223
|
)}
|
|
@@ -1670,13 +2252,25 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1670
2252
|
{thinkingBlocks.length > 0 && renderThinkingBlocks()}
|
|
1671
2253
|
|
|
1672
2254
|
{processedContent ? (
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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
|
+
)
|
|
1680
2274
|
) : (
|
|
1681
2275
|
<div className="ai-chat-loading">
|
|
1682
2276
|
<span>Thinking</span>
|
|
@@ -1691,52 +2285,96 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1691
2285
|
) : (
|
|
1692
2286
|
<>
|
|
1693
2287
|
{isLastEntry && thinkingBlocks.length > 0 && renderThinkingBlocks()}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
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
|
+
)}
|
|
1701
2307
|
</>
|
|
1702
2308
|
)}
|
|
1703
2309
|
</div>
|
|
1704
2310
|
|
|
1705
2311
|
{/* Action Buttons */}
|
|
1706
|
-
{
|
|
2312
|
+
{(!isLastEntry || !isLoading) && (
|
|
1707
2313
|
<div className="ai-chat-message__actions">
|
|
1708
|
-
<
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
<
|
|
1715
|
-
|
|
1716
|
-
|
|
2314
|
+
<button
|
|
2315
|
+
className="ai-chat-action-button"
|
|
2316
|
+
onClick={() => copyToClipboard(entry.content, entry.callId)}
|
|
2317
|
+
title={copiedCallId === entry.callId ? 'Copied!' : 'Copy'}
|
|
2318
|
+
>
|
|
2319
|
+
{copiedCallId === entry.callId ? (
|
|
2320
|
+
<span style={{ fontSize: '11px', fontWeight: 500 }}>Copied!</span>
|
|
2321
|
+
) : (
|
|
2322
|
+
<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">
|
|
2323
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
2324
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
2325
|
+
</svg>
|
|
2326
|
+
)}
|
|
2327
|
+
</button>
|
|
1717
2328
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
2329
|
+
<button
|
|
2330
|
+
className="ai-chat-action-button"
|
|
2331
|
+
onClick={() => handleThumbsUp(entry.callId)}
|
|
2332
|
+
title={feedbackCallId?.callId === entry.callId && feedbackCallId?.type === 'up' ? 'Thanks!' : 'Good response'}
|
|
2333
|
+
style={feedbackCallId?.callId === entry.callId && feedbackCallId?.type === 'up' ? { color: '#22c55e' } : undefined}
|
|
2334
|
+
>
|
|
2335
|
+
{feedbackCallId?.callId === entry.callId && feedbackCallId?.type === 'up' ? (
|
|
2336
|
+
<span style={{ fontSize: '11px', fontWeight: 500 }}>Thanks!</span>
|
|
2337
|
+
) : (
|
|
2338
|
+
<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">
|
|
2339
|
+
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />
|
|
2340
|
+
</svg>
|
|
2341
|
+
)}
|
|
2342
|
+
</button>
|
|
1729
2343
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
2344
|
+
<button
|
|
2345
|
+
className="ai-chat-action-button"
|
|
2346
|
+
onClick={() => handleThumbsDown(entry.callId)}
|
|
2347
|
+
title={feedbackCallId?.callId === entry.callId && feedbackCallId?.type === 'down' ? 'Thanks!' : 'Poor response'}
|
|
2348
|
+
style={feedbackCallId?.callId === entry.callId && feedbackCallId?.type === 'down' ? { color: '#ef4444' } : undefined}
|
|
2349
|
+
>
|
|
2350
|
+
{feedbackCallId?.callId === entry.callId && feedbackCallId?.type === 'down' ? (
|
|
2351
|
+
<span style={{ fontSize: '11px', fontWeight: 500 }}>Thanks!</span>
|
|
2352
|
+
) : (
|
|
2353
|
+
<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">
|
|
2354
|
+
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" />
|
|
2355
|
+
</svg>
|
|
2356
|
+
)}
|
|
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>
|
|
1740
2378
|
)}
|
|
1741
2379
|
</div>
|
|
1742
2380
|
)}
|
|
@@ -1765,6 +2403,109 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1765
2403
|
<div ref={bottomRef} />
|
|
1766
2404
|
</ScrollArea>
|
|
1767
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
|
+
|
|
1768
2509
|
{/* New Conversation Button */}
|
|
1769
2510
|
{showNewConversationButton && (
|
|
1770
2511
|
<div className="ai-chat-panel__new-conversation">
|
|
@@ -1779,6 +2520,103 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1779
2520
|
</div>
|
|
1780
2521
|
)}
|
|
1781
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
|
+
|
|
1782
2620
|
{/* Input Area - Isolated component for performance */}
|
|
1783
2621
|
<ChatInput
|
|
1784
2622
|
placeholder={placeholder}
|
|
@@ -1828,6 +2666,13 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1828
2666
|
</div>
|
|
1829
2667
|
</div>
|
|
1830
2668
|
)}
|
|
2669
|
+
|
|
2670
|
+
{/* Modals */}
|
|
2671
|
+
<ToolInfoModal
|
|
2672
|
+
isOpen={isToolInfoModalOpen}
|
|
2673
|
+
onClose={() => setIsToolInfoModalOpen(false)}
|
|
2674
|
+
data={toolInfoData}
|
|
2675
|
+
/>
|
|
1831
2676
|
</div>
|
|
1832
2677
|
);
|
|
1833
2678
|
};
|