@hef2024/llmasaservice-ui 0.16.8
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/README.md +162 -0
- package/dist/index.css +3239 -0
- package/dist/index.d.mts +521 -0
- package/dist/index.d.ts +521 -0
- package/dist/index.js +5885 -0
- package/dist/index.mjs +5851 -0
- package/index.ts +28 -0
- package/package.json +70 -0
- package/src/AIAgentPanel.css +1354 -0
- package/src/AIAgentPanel.tsx +1883 -0
- package/src/AIChatPanel.css +1618 -0
- package/src/AIChatPanel.tsx +1725 -0
- package/src/AgentPanel.tsx +323 -0
- package/src/ChatPanel.css +1093 -0
- package/src/ChatPanel.tsx +3583 -0
- package/src/ChatStatus.tsx +40 -0
- package/src/EmailModal.tsx +56 -0
- package/src/ToolInfoModal.tsx +49 -0
- package/src/components/ui/Button.tsx +57 -0
- package/src/components/ui/Dialog.tsx +153 -0
- package/src/components/ui/Input.tsx +33 -0
- package/src/components/ui/ScrollArea.tsx +29 -0
- package/src/components/ui/Select.tsx +156 -0
- package/src/components/ui/Tooltip.tsx +73 -0
- package/src/components/ui/index.ts +20 -0
- package/src/hooks/useAgentRegistry.ts +349 -0
- package/src/hooks/useConversationStore.ts +313 -0
- package/src/mcpClient.ts +107 -0
- package/tsconfig.json +108 -0
- package/types/declarations.d.ts +22 -0
|
@@ -0,0 +1,3583 @@
|
|
|
1
|
+
import { LLMAsAServiceCustomer, useLLM } from "llmasaservice-client";
|
|
2
|
+
import React, {
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import ReactMarkdown from "react-markdown";
|
|
10
|
+
|
|
11
|
+
import ReactDOMServer from "react-dom/server";
|
|
12
|
+
import "./ChatPanel.css";
|
|
13
|
+
import remarkGfm from "remark-gfm";
|
|
14
|
+
import rehypeRaw from "rehype-raw";
|
|
15
|
+
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
16
|
+
import PrismStyle from "react-syntax-highlighter";
|
|
17
|
+
import materialDark from "react-syntax-highlighter/dist/esm/styles/prism/material-dark.js";
|
|
18
|
+
import materialLight from "react-syntax-highlighter/dist/esm/styles/prism/material-light.js";
|
|
19
|
+
import EmailModal from "./EmailModal";
|
|
20
|
+
import ToolInfoModal from "./ToolInfoModal";
|
|
21
|
+
|
|
22
|
+
export interface ChatPanelProps {
|
|
23
|
+
project_id: string;
|
|
24
|
+
initialPrompt?: string;
|
|
25
|
+
initialMessage?: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
hideInitialPrompt?: boolean;
|
|
29
|
+
customer?: LLMAsAServiceCustomer;
|
|
30
|
+
messages?: { role: "user" | "assistant"; content: string }[];
|
|
31
|
+
data?: { key: string; data: string }[];
|
|
32
|
+
thumbsUpClick?: (callId: string) => void;
|
|
33
|
+
thumbsDownClick?: (callId: string) => void;
|
|
34
|
+
theme?: "light" | "dark";
|
|
35
|
+
cssUrl?: string;
|
|
36
|
+
markdownClass?: string;
|
|
37
|
+
width?: string;
|
|
38
|
+
height?: string;
|
|
39
|
+
url?: string | null;
|
|
40
|
+
scrollToEnd?: boolean;
|
|
41
|
+
prismStyle?: PrismStyle;
|
|
42
|
+
service?: string | null;
|
|
43
|
+
historyChangedCallback?: (history: {
|
|
44
|
+
[key: string]: { content: string; callId: string };
|
|
45
|
+
}) => void;
|
|
46
|
+
responseCompleteCallback?: (
|
|
47
|
+
callId: string,
|
|
48
|
+
prompt: string,
|
|
49
|
+
response: string
|
|
50
|
+
) => void;
|
|
51
|
+
promptTemplate?: string;
|
|
52
|
+
actions?: {
|
|
53
|
+
pattern: string;
|
|
54
|
+
type?: string;
|
|
55
|
+
markdown?: string;
|
|
56
|
+
callback?: (match: string, groups: any[]) => void;
|
|
57
|
+
clickCode?: string;
|
|
58
|
+
style?: string;
|
|
59
|
+
}[];
|
|
60
|
+
showSaveButton?: boolean;
|
|
61
|
+
showEmailButton?: boolean;
|
|
62
|
+
showNewConversationButton?: boolean;
|
|
63
|
+
followOnQuestions?: string[];
|
|
64
|
+
clearFollowOnQuestionsNextPrompt?: boolean;
|
|
65
|
+
followOnPrompt?: string;
|
|
66
|
+
showPoweredBy?: boolean;
|
|
67
|
+
agent?: string | null;
|
|
68
|
+
conversation?: string | null;
|
|
69
|
+
showCallToAction?: boolean;
|
|
70
|
+
callToActionButtonText?: string;
|
|
71
|
+
callToActionEmailAddress?: string;
|
|
72
|
+
callToActionEmailSubject?: string;
|
|
73
|
+
initialHistory?: {
|
|
74
|
+
[prompt: string]: { content: string; callId: string };
|
|
75
|
+
};
|
|
76
|
+
hideRagContextInPrompt?: boolean;
|
|
77
|
+
createConversationOnFirstChat?: boolean;
|
|
78
|
+
customerEmailCaptureMode?: "HIDE" | "OPTIONAL" | "REQUIRED";
|
|
79
|
+
customerEmailCapturePlaceholder?: string;
|
|
80
|
+
mcpServers?: [];
|
|
81
|
+
progressiveActions?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface HistoryEntry {
|
|
85
|
+
content: string;
|
|
86
|
+
callId: string;
|
|
87
|
+
toolCalls?: [];
|
|
88
|
+
toolResponses?: [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ExtraProps extends React.HTMLAttributes<HTMLElement> {
|
|
92
|
+
inline?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
|
|
96
|
+
project_id,
|
|
97
|
+
initialPrompt = "",
|
|
98
|
+
title = "",
|
|
99
|
+
placeholder = "Type a message",
|
|
100
|
+
hideInitialPrompt = true,
|
|
101
|
+
customer = {},
|
|
102
|
+
messages = [],
|
|
103
|
+
data = [],
|
|
104
|
+
thumbsUpClick,
|
|
105
|
+
thumbsDownClick,
|
|
106
|
+
theme = "light",
|
|
107
|
+
cssUrl = "",
|
|
108
|
+
markdownClass = null,
|
|
109
|
+
width = "300px",
|
|
110
|
+
height = "100vh",
|
|
111
|
+
url = null,
|
|
112
|
+
scrollToEnd = false,
|
|
113
|
+
initialMessage = "",
|
|
114
|
+
prismStyle = theme === "light" ? materialLight : materialDark,
|
|
115
|
+
service = null,
|
|
116
|
+
historyChangedCallback = null,
|
|
117
|
+
responseCompleteCallback = null,
|
|
118
|
+
promptTemplate = "",
|
|
119
|
+
actions,
|
|
120
|
+
showSaveButton = true,
|
|
121
|
+
showEmailButton = true,
|
|
122
|
+
showNewConversationButton = true,
|
|
123
|
+
followOnQuestions = [],
|
|
124
|
+
clearFollowOnQuestionsNextPrompt = false,
|
|
125
|
+
followOnPrompt = "",
|
|
126
|
+
showPoweredBy = true,
|
|
127
|
+
agent = null,
|
|
128
|
+
conversation = null,
|
|
129
|
+
showCallToAction = false,
|
|
130
|
+
callToActionButtonText = "Submit",
|
|
131
|
+
callToActionEmailAddress = "",
|
|
132
|
+
callToActionEmailSubject = "Agent CTA submitted",
|
|
133
|
+
initialHistory = {},
|
|
134
|
+
hideRagContextInPrompt = true,
|
|
135
|
+
createConversationOnFirstChat = true,
|
|
136
|
+
customerEmailCaptureMode = "HIDE",
|
|
137
|
+
customerEmailCapturePlaceholder = "Please enter your email...",
|
|
138
|
+
mcpServers,
|
|
139
|
+
progressiveActions = true,
|
|
140
|
+
}) => {
|
|
141
|
+
const isEmailAddress = (email: string): boolean => {
|
|
142
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
143
|
+
return emailRegex.test(email);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const [nextPrompt, setNextPrompt] = useState("");
|
|
147
|
+
const [lastController, setLastController] = useState(new AbortController());
|
|
148
|
+
const [lastMessages, setLastMessages] = useState<any[]>([]);
|
|
149
|
+
const [history, setHistory] = useState<{ [prompt: string]: HistoryEntry }>(
|
|
150
|
+
initialHistory
|
|
151
|
+
);
|
|
152
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
153
|
+
const [lastPrompt, setLastPrompt] = useState<string | null>(null);
|
|
154
|
+
const [lastKey, setLastKey] = useState<string | null>(null);
|
|
155
|
+
const [hasScroll, setHasScroll] = useState(false);
|
|
156
|
+
const bottomRef = useRef<HTMLDivElement | null>(null);
|
|
157
|
+
const bottomPanelRef = useRef<HTMLDivElement | null>(null);
|
|
158
|
+
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
159
|
+
const historyCallbackRef = useRef(historyChangedCallback);
|
|
160
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
161
|
+
const [isEmailModalOpen, setIsEmailModalOpen] = useState(false);
|
|
162
|
+
const [isToolInfoModalOpen, setIsToolInfoModalOpen] = useState(false);
|
|
163
|
+
const [toolInfoData, setToolInfoData] = useState<{
|
|
164
|
+
calls: any[];
|
|
165
|
+
responses: any[];
|
|
166
|
+
} | null>(null);
|
|
167
|
+
|
|
168
|
+
const [currentConversation, setCurrentConversation] = useState<string | null>(
|
|
169
|
+
conversation
|
|
170
|
+
);
|
|
171
|
+
const [emailInput, setEmailInput] = useState(
|
|
172
|
+
(customer as LLMAsAServiceCustomer)?.customer_user_email ?? ""
|
|
173
|
+
);
|
|
174
|
+
const [emailInputSet, setEmailInputSet] = useState(
|
|
175
|
+
isEmailAddress(emailInput)
|
|
176
|
+
);
|
|
177
|
+
const [emailValid, setEmailValid] = useState(true);
|
|
178
|
+
const [showEmailPanel, setShowEmailPanel] = useState(
|
|
179
|
+
customerEmailCaptureMode !== "HIDE"
|
|
180
|
+
);
|
|
181
|
+
const [callToActionSent, setCallToActionSent] = useState(false);
|
|
182
|
+
const [CTAClickedButNoEmail, setCTAClickedButNoEmail] = useState(false);
|
|
183
|
+
const [emailSent, setEmailSent] = useState(false);
|
|
184
|
+
const [emailClickedButNoEmail, setEmailClickedButNoEmail] = useState(false);
|
|
185
|
+
const [currentCustomer, setCurrentCustomer] = useState<LLMAsAServiceCustomer>(
|
|
186
|
+
customer as LLMAsAServiceCustomer
|
|
187
|
+
);
|
|
188
|
+
const [justReset, setJustReset] = useState(false);
|
|
189
|
+
const [newConversationConfirm, setNewConversationConfirm] = useState(false);
|
|
190
|
+
const [allActions, setAllActions] = useState<
|
|
191
|
+
{
|
|
192
|
+
pattern: string;
|
|
193
|
+
type?: string;
|
|
194
|
+
markdown?: string;
|
|
195
|
+
callback?: (match: string, groups: any[]) => void;
|
|
196
|
+
clickCode?: string;
|
|
197
|
+
style?: string;
|
|
198
|
+
actionType?: string;
|
|
199
|
+
}[]
|
|
200
|
+
>([]);
|
|
201
|
+
|
|
202
|
+
const [pendingToolRequests, setPendingToolRequests] = useState<
|
|
203
|
+
{
|
|
204
|
+
match: string;
|
|
205
|
+
groups: any[];
|
|
206
|
+
toolName: string;
|
|
207
|
+
}[]
|
|
208
|
+
>([]);
|
|
209
|
+
|
|
210
|
+
const [followOnQuestionsState, setFollowOnQuestionsState] =
|
|
211
|
+
useState(followOnQuestions);
|
|
212
|
+
|
|
213
|
+
// new per‐tool approval state
|
|
214
|
+
const [sessionApprovedTools, setSessionApprovedTools] = useState<string[]>(
|
|
215
|
+
[]
|
|
216
|
+
);
|
|
217
|
+
const [alwaysApprovedTools, setAlwaysApprovedTools] = useState<string[]>([]);
|
|
218
|
+
|
|
219
|
+
// State for tracking thinking content and navigation
|
|
220
|
+
const [thinkingBlocks, setThinkingBlocks] = useState<
|
|
221
|
+
Array<{ type: "reasoning" | "searching"; content: string; index: number }>
|
|
222
|
+
>([]);
|
|
223
|
+
const [currentThinkingIndex, setCurrentThinkingIndex] = useState(0);
|
|
224
|
+
|
|
225
|
+
// State for tracking user-resized textarea height
|
|
226
|
+
const [userResizedHeight, setUserResizedHeight] = useState<number | null>(null);
|
|
227
|
+
|
|
228
|
+
// State for pending button attachments
|
|
229
|
+
const [pendingButtonAttachments, setPendingButtonAttachments] = useState<
|
|
230
|
+
Array<{
|
|
231
|
+
buttonId: string;
|
|
232
|
+
action: any;
|
|
233
|
+
match: string;
|
|
234
|
+
groups: any[];
|
|
235
|
+
}>
|
|
236
|
+
>([]);
|
|
237
|
+
|
|
238
|
+
// Progressive actions support
|
|
239
|
+
const actionMatchRegistry = useRef<Map<string, string>>(new Map()); // key => stable buttonId
|
|
240
|
+
const deferredActionsRef = useRef<
|
|
241
|
+
Map<string, { action: any; match: string; groups: any[] }>
|
|
242
|
+
>(new Map()); // buttonId => metadata (filled during streaming)
|
|
243
|
+
const finalizedButtonsRef = useRef<Set<string>>(new Set()); // track activated buttonIds
|
|
244
|
+
const actionSequenceRef = useRef<number>(0); // incremental id source
|
|
245
|
+
const lastProcessedResponseRef = useRef<string>(""); // detect unchanged streaming chunks
|
|
246
|
+
const finalizedForCallRef = useRef<string | null>(null); // lastCallId for which finalization happened
|
|
247
|
+
|
|
248
|
+
// Keep historyChangedCallback ref in sync (avoids infinite loops from unstable references)
|
|
249
|
+
historyCallbackRef.current = historyChangedCallback;
|
|
250
|
+
|
|
251
|
+
// Persistent button action registry for event delegation fallback
|
|
252
|
+
const buttonActionRegistry = useRef<
|
|
253
|
+
Map<string, { action: any; match: string; groups: any[] }>
|
|
254
|
+
>(new Map());
|
|
255
|
+
|
|
256
|
+
// load “always” approvals
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
const stored = localStorage.getItem("alwaysApprovedTools");
|
|
259
|
+
if (stored) setAlwaysApprovedTools(JSON.parse(stored));
|
|
260
|
+
}, []);
|
|
261
|
+
|
|
262
|
+
// persist “always” approvals
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
localStorage.setItem(
|
|
265
|
+
"alwaysApprovedTools",
|
|
266
|
+
JSON.stringify(alwaysApprovedTools)
|
|
267
|
+
);
|
|
268
|
+
}, [alwaysApprovedTools]);
|
|
269
|
+
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
if (followOnQuestions !== followOnQuestionsState) {
|
|
272
|
+
setFollowOnQuestionsState(followOnQuestions);
|
|
273
|
+
}
|
|
274
|
+
}, [followOnQuestions]);
|
|
275
|
+
|
|
276
|
+
// Cleanup button registry on unmount
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
return () => {
|
|
279
|
+
buttonActionRegistry.current.clear();
|
|
280
|
+
};
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
283
|
+
// Periodic cleanup of orphaned registry entries
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
const cleanupInterval = setInterval(() => {
|
|
286
|
+
const registryKeys = Array.from(buttonActionRegistry.current.keys());
|
|
287
|
+
const orphanedKeys: string[] = [];
|
|
288
|
+
|
|
289
|
+
registryKeys.forEach((buttonId) => {
|
|
290
|
+
const button = document.getElementById(buttonId);
|
|
291
|
+
if (!button) {
|
|
292
|
+
orphanedKeys.push(buttonId);
|
|
293
|
+
buttonActionRegistry.current.delete(buttonId);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (orphanedKeys.length > 0) {
|
|
298
|
+
}
|
|
299
|
+
}, 10000); // Cleanup every 10 seconds
|
|
300
|
+
|
|
301
|
+
return () => clearInterval(cleanupInterval);
|
|
302
|
+
}, []);
|
|
303
|
+
|
|
304
|
+
const [iframeUrl, setIframeUrl] = useState<string | null>(null);
|
|
305
|
+
const responseAreaRef = useRef(null);
|
|
306
|
+
|
|
307
|
+
// Memoized regex patterns to avoid recreation on every render
|
|
308
|
+
const THINKING_PATTERNS = useMemo(
|
|
309
|
+
() => ({
|
|
310
|
+
reasoning: /<reasoning>([\s\S]*?)<\/reasoning>/gi,
|
|
311
|
+
searching: /<searching>([\s\S]*?)<\/searching>/gi,
|
|
312
|
+
}),
|
|
313
|
+
[]
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Memoized regex instances for better performance
|
|
317
|
+
const reasoningRegex = useMemo(
|
|
318
|
+
() => new RegExp(THINKING_PATTERNS.reasoning.source, "gi"),
|
|
319
|
+
[THINKING_PATTERNS.reasoning.source]
|
|
320
|
+
);
|
|
321
|
+
const searchingRegex = useMemo(
|
|
322
|
+
() => new RegExp(THINKING_PATTERNS.searching.source, "gi"),
|
|
323
|
+
[THINKING_PATTERNS.searching.source]
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Memoized content cleaning function
|
|
327
|
+
const cleanContentForDisplay = useCallback((content: string): string => {
|
|
328
|
+
let cleaned = content
|
|
329
|
+
.replace(/\*\*(.*?)\*\*/g, "$1") // Remove bold
|
|
330
|
+
.replace(/\*(.*?)\*/g, "$1") // Remove italics
|
|
331
|
+
.replace(/\n+/g, " ") // Replace newlines with spaces
|
|
332
|
+
.replace(/\s+/g, " ") // Normalize whitespace
|
|
333
|
+
.trim();
|
|
334
|
+
|
|
335
|
+
// Limit length to keep UI clean
|
|
336
|
+
if (cleaned.length > 100) {
|
|
337
|
+
cleaned = cleaned.substring(0, 100) + "...";
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return cleaned || "Thinking";
|
|
341
|
+
}, []);
|
|
342
|
+
|
|
343
|
+
// Optimized function to extract thinking blocks in order
|
|
344
|
+
const processThinkingTags = useCallback(
|
|
345
|
+
(
|
|
346
|
+
text: string
|
|
347
|
+
): {
|
|
348
|
+
cleanedText: string;
|
|
349
|
+
thinkingBlocks: Array<{
|
|
350
|
+
type: "reasoning" | "searching";
|
|
351
|
+
content: string;
|
|
352
|
+
index: number;
|
|
353
|
+
}>;
|
|
354
|
+
lastThinkingContent: string;
|
|
355
|
+
} => {
|
|
356
|
+
if (!text) {
|
|
357
|
+
return {
|
|
358
|
+
cleanedText: "",
|
|
359
|
+
thinkingBlocks: [],
|
|
360
|
+
lastThinkingContent: "Thinking",
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Remove zero-width space characters from keepalive before processing
|
|
365
|
+
// This prevents them from interfering with thinking block extraction
|
|
366
|
+
const processedText = text.replace(/\u200B/g, "");
|
|
367
|
+
|
|
368
|
+
const allMatches: Array<{
|
|
369
|
+
content: string;
|
|
370
|
+
index: number;
|
|
371
|
+
type: "reasoning" | "searching";
|
|
372
|
+
}> = [];
|
|
373
|
+
|
|
374
|
+
// Reset regex state for fresh matching
|
|
375
|
+
reasoningRegex.lastIndex = 0;
|
|
376
|
+
searchingRegex.lastIndex = 0;
|
|
377
|
+
|
|
378
|
+
// Process reasoning blocks
|
|
379
|
+
let reasoningMatch;
|
|
380
|
+
while ((reasoningMatch = reasoningRegex.exec(processedText)) !== null) {
|
|
381
|
+
const content = reasoningMatch[1]?.trim();
|
|
382
|
+
if (content) {
|
|
383
|
+
allMatches.push({
|
|
384
|
+
content,
|
|
385
|
+
index: reasoningMatch.index,
|
|
386
|
+
type: "reasoning",
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Process searching blocks
|
|
392
|
+
let searchingMatch;
|
|
393
|
+
while ((searchingMatch = searchingRegex.exec(processedText)) !== null) {
|
|
394
|
+
const content = searchingMatch[1]?.trim();
|
|
395
|
+
if (content) {
|
|
396
|
+
allMatches.push({
|
|
397
|
+
content,
|
|
398
|
+
index: searchingMatch.index,
|
|
399
|
+
type: "searching",
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Sort by index to preserve original order
|
|
405
|
+
const thinkingBlocks = allMatches.sort((a, b) => a.index - b.index);
|
|
406
|
+
|
|
407
|
+
// Clean the text by removing thinking tags
|
|
408
|
+
let cleanedText = processedText
|
|
409
|
+
.replace(THINKING_PATTERNS.reasoning, "")
|
|
410
|
+
.replace(THINKING_PATTERNS.searching, "")
|
|
411
|
+
.trim();
|
|
412
|
+
|
|
413
|
+
// Get last thinking content
|
|
414
|
+
let lastThinkingContent = "Thinking";
|
|
415
|
+
if (thinkingBlocks.length > 0) {
|
|
416
|
+
const lastBlock = thinkingBlocks[thinkingBlocks.length - 1];
|
|
417
|
+
if (lastBlock?.content) {
|
|
418
|
+
lastThinkingContent = cleanContentForDisplay(lastBlock.content);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
cleanedText,
|
|
424
|
+
thinkingBlocks,
|
|
425
|
+
lastThinkingContent,
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
[
|
|
429
|
+
THINKING_PATTERNS.reasoning,
|
|
430
|
+
THINKING_PATTERNS.searching,
|
|
431
|
+
reasoningRegex,
|
|
432
|
+
searchingRegex,
|
|
433
|
+
cleanContentForDisplay,
|
|
434
|
+
]
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// Memoized render function for thinking blocks with navigation
|
|
438
|
+
const renderThinkingBlocks = useCallback((): React.ReactElement | null => {
|
|
439
|
+
if (thinkingBlocks.length === 0) return null;
|
|
440
|
+
|
|
441
|
+
const currentBlock = thinkingBlocks[currentThinkingIndex];
|
|
442
|
+
if (!currentBlock) return null;
|
|
443
|
+
|
|
444
|
+
const icon = currentBlock.type === "reasoning" ? "🤔" : "🔍";
|
|
445
|
+
const baseTitle =
|
|
446
|
+
currentBlock.type === "reasoning" ? "Reasoning" : "Searching";
|
|
447
|
+
|
|
448
|
+
// Extract title from **[title]** at the beginning of content and strip formatting
|
|
449
|
+
const extractTitleAndContent = (
|
|
450
|
+
text: string
|
|
451
|
+
): { displayTitle: string; content: string } => {
|
|
452
|
+
// Handle potential whitespace at the beginning and be more flexible with the pattern
|
|
453
|
+
const trimmedText = text.trim();
|
|
454
|
+
const titleMatch = trimmedText.match(/^\*\*\[(.*?)\]\*\*/);
|
|
455
|
+
if (titleMatch) {
|
|
456
|
+
const extractedTitle = titleMatch[1];
|
|
457
|
+
// Remove the title pattern and any following whitespace/newlines
|
|
458
|
+
const remainingContent = trimmedText
|
|
459
|
+
.replace(/^\*\*\[.*?\]\*\*\s*\n?/, "")
|
|
460
|
+
.replace(/\*\*(.*?)\*\*/g, "$1")
|
|
461
|
+
.trim();
|
|
462
|
+
return {
|
|
463
|
+
displayTitle: `${baseTitle}: ${extractedTitle}`,
|
|
464
|
+
content: remainingContent,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
// If no title found, just strip bold formatting
|
|
468
|
+
return {
|
|
469
|
+
displayTitle: baseTitle,
|
|
470
|
+
content: trimmedText.replace(/\*\*(.*?)\*\*/g, "$1"),
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const { displayTitle, content } = extractTitleAndContent(
|
|
475
|
+
currentBlock.content
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<div className="thinking-block-container">
|
|
480
|
+
<div className={`thinking-section ${currentBlock.type}-section`}>
|
|
481
|
+
<div className="thinking-header">
|
|
482
|
+
{icon} {displayTitle}
|
|
483
|
+
{thinkingBlocks.length > 1 && (
|
|
484
|
+
<div className="thinking-navigation">
|
|
485
|
+
<button
|
|
486
|
+
onClick={() =>
|
|
487
|
+
setCurrentThinkingIndex(
|
|
488
|
+
Math.max(0, currentThinkingIndex - 1)
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
disabled={currentThinkingIndex === 0}
|
|
492
|
+
className="thinking-nav-btn"
|
|
493
|
+
>
|
|
494
|
+
←
|
|
495
|
+
</button>
|
|
496
|
+
<span className="thinking-counter">
|
|
497
|
+
{currentThinkingIndex + 1} / {thinkingBlocks.length}
|
|
498
|
+
</span>
|
|
499
|
+
<button
|
|
500
|
+
onClick={() =>
|
|
501
|
+
setCurrentThinkingIndex(
|
|
502
|
+
Math.min(
|
|
503
|
+
thinkingBlocks.length - 1,
|
|
504
|
+
currentThinkingIndex + 1
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
disabled={currentThinkingIndex === thinkingBlocks.length - 1}
|
|
509
|
+
className="thinking-nav-btn"
|
|
510
|
+
>
|
|
511
|
+
→
|
|
512
|
+
</button>
|
|
513
|
+
</div>
|
|
514
|
+
)}
|
|
515
|
+
</div>
|
|
516
|
+
<div className="thinking-content">{content}</div>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
);
|
|
520
|
+
}, [thinkingBlocks, currentThinkingIndex]);
|
|
521
|
+
|
|
522
|
+
const getBrowserInfo = () => {
|
|
523
|
+
try {
|
|
524
|
+
return {
|
|
525
|
+
currentTimeUTC: new Date().toISOString(),
|
|
526
|
+
userTimezone:
|
|
527
|
+
(typeof Intl !== "undefined" &&
|
|
528
|
+
Intl.DateTimeFormat().resolvedOptions().timeZone) ||
|
|
529
|
+
"unknown",
|
|
530
|
+
userLanguage:
|
|
531
|
+
(typeof navigator !== "undefined" &&
|
|
532
|
+
(navigator.language || navigator.language)) ||
|
|
533
|
+
"unknown",
|
|
534
|
+
};
|
|
535
|
+
} catch (e) {
|
|
536
|
+
console.warn("Error getting browser info:", e);
|
|
537
|
+
return {
|
|
538
|
+
currentTimeUTC: new Date().toISOString(),
|
|
539
|
+
userTimezone: "unknown",
|
|
540
|
+
userLanguage: "unknown",
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const browserInfo = useMemo(() => getBrowserInfo(), []);
|
|
546
|
+
|
|
547
|
+
const dataWithExtras = () => {
|
|
548
|
+
return [
|
|
549
|
+
...data,
|
|
550
|
+
{ key: "--customer_id", data: currentCustomer?.customer_id ?? "" },
|
|
551
|
+
{
|
|
552
|
+
key: "--customer_name",
|
|
553
|
+
data: currentCustomer?.customer_name ?? "",
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
key: "--customer_user_id",
|
|
557
|
+
data: currentCustomer?.customer_user_id ?? "",
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
key: "--customer_user_name",
|
|
561
|
+
data: currentCustomer?.customer_user_name ?? "",
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
key: "--customer_user_email",
|
|
565
|
+
data: currentCustomer?.customer_user_email ?? "",
|
|
566
|
+
},
|
|
567
|
+
{ key: "--email", data: emailInput ?? "" },
|
|
568
|
+
{ key: "--emailValid", data: emailValid ? "true" : "false" },
|
|
569
|
+
{
|
|
570
|
+
key: "--emailInputSet",
|
|
571
|
+
data: emailInputSet ? "true" : "false",
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
key: "--emailPanelShowing",
|
|
575
|
+
data: showEmailPanel ? "true" : "false",
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
key: "--callToActionSent",
|
|
579
|
+
data: callToActionSent ? "true" : "false",
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
key: "--CTAClickedButNoEmail",
|
|
583
|
+
data: CTAClickedButNoEmail ? "true" : "false",
|
|
584
|
+
},
|
|
585
|
+
{ key: "--emailSent", data: emailSent ? "true" : "false" },
|
|
586
|
+
{
|
|
587
|
+
key: "--emailClickedButNoEmail",
|
|
588
|
+
data: emailClickedButNoEmail ? "true" : "false",
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
key: "--currentTimeUTC",
|
|
592
|
+
data: browserInfo?.currentTimeUTC,
|
|
593
|
+
},
|
|
594
|
+
{ key: "--userTimezone", data: browserInfo?.userTimezone },
|
|
595
|
+
{ key: "--userLanguage", data: browserInfo?.userLanguage },
|
|
596
|
+
];
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// public api url for dev and production
|
|
600
|
+
let publicAPIUrl = "https://api.llmasaservice.io";
|
|
601
|
+
if (
|
|
602
|
+
window.location.hostname === "localhost" ||
|
|
603
|
+
window.location.hostname === "dev.llmasaservice.io"
|
|
604
|
+
) {
|
|
605
|
+
publicAPIUrl = "https://8ftw8droff.execute-api.us-east-1.amazonaws.com/dev";
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const [toolList, setToolList] = useState<any[]>([]);
|
|
609
|
+
const [toolsLoading, setToolsLoading] = useState(false);
|
|
610
|
+
const [toolsFetchError, setToolsFetchError] = useState(false);
|
|
611
|
+
|
|
612
|
+
// mcp servers are passed in in the mcpServers prop. Fetch tools for each one.
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
//console.log("MCP servers", mcpServers);
|
|
615
|
+
|
|
616
|
+
const fetchAndSetTools = async () => {
|
|
617
|
+
if (!mcpServers || mcpServers.length === 0) {
|
|
618
|
+
setToolList([]);
|
|
619
|
+
setToolsLoading(false);
|
|
620
|
+
setToolsFetchError(false);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
setToolsLoading(true);
|
|
625
|
+
setToolsFetchError(false);
|
|
626
|
+
|
|
627
|
+
try {
|
|
628
|
+
// Create an array of promises, one for each fetch call
|
|
629
|
+
const fetchPromises = (mcpServers ?? []).map(async (m: any) => {
|
|
630
|
+
const urlToFetch = `${publicAPIUrl}/tools/${encodeURIComponent(
|
|
631
|
+
m.url
|
|
632
|
+
)}`;
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
const response = await fetch(urlToFetch);
|
|
636
|
+
if (!response.ok) {
|
|
637
|
+
console.error(
|
|
638
|
+
`Error fetching tools from ${m.url}: ${response.status} ${response.statusText}`
|
|
639
|
+
);
|
|
640
|
+
const errorBody = await response.text();
|
|
641
|
+
console.error(`Error body: ${errorBody}`);
|
|
642
|
+
throw new Error(
|
|
643
|
+
`HTTP ${response.status}: ${response.statusText}`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const toolsFromServer = await response.json();
|
|
647
|
+
if (Array.isArray(toolsFromServer)) {
|
|
648
|
+
return toolsFromServer.map((tool) => ({
|
|
649
|
+
...tool,
|
|
650
|
+
url: m.url,
|
|
651
|
+
accessToken: m.accessToken || "",
|
|
652
|
+
headers: {},
|
|
653
|
+
}));
|
|
654
|
+
} else {
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
} catch (fetchError) {
|
|
658
|
+
console.error(
|
|
659
|
+
`Network or parsing error fetching tools from ${m.url}:`,
|
|
660
|
+
fetchError
|
|
661
|
+
);
|
|
662
|
+
throw fetchError; // Re-throw to be caught by outer try-catch
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Wait for all fetch calls to complete
|
|
667
|
+
const results = await Promise.all(fetchPromises);
|
|
668
|
+
|
|
669
|
+
const allTools = results.flat();
|
|
670
|
+
|
|
671
|
+
//console.log("Merged tools from all servers:", allTools);
|
|
672
|
+
setToolList(allTools);
|
|
673
|
+
setToolsFetchError(false);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
console.error(
|
|
676
|
+
"An error occurred while processing tool fetches:",
|
|
677
|
+
error
|
|
678
|
+
);
|
|
679
|
+
setToolList([]); // Clear tools on overall error
|
|
680
|
+
setToolsFetchError(true);
|
|
681
|
+
} finally {
|
|
682
|
+
setToolsLoading(false);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
fetchAndSetTools();
|
|
687
|
+
}, [mcpServers, publicAPIUrl]);
|
|
688
|
+
|
|
689
|
+
const { send, response, idle, stop, lastCallId, setResponse } = useLLM({
|
|
690
|
+
project_id: project_id,
|
|
691
|
+
customer: currentCustomer,
|
|
692
|
+
url: url,
|
|
693
|
+
agent: agent,
|
|
694
|
+
tools: toolList.map((item) => {
|
|
695
|
+
// remove the url from the tool list
|
|
696
|
+
return {
|
|
697
|
+
name: item.name,
|
|
698
|
+
description: item.description,
|
|
699
|
+
parameters: item.parameters,
|
|
700
|
+
};
|
|
701
|
+
}) as [],
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Centralized action processing function to eliminate duplication
|
|
705
|
+
const processActionsOnContent = useCallback((
|
|
706
|
+
content: string,
|
|
707
|
+
context: {
|
|
708
|
+
type: 'history' | 'streaming';
|
|
709
|
+
historyIndex?: number;
|
|
710
|
+
actionIndex?: number;
|
|
711
|
+
matchOffset?: number;
|
|
712
|
+
isProgressive?: boolean;
|
|
713
|
+
isIdle?: boolean;
|
|
714
|
+
}
|
|
715
|
+
): {
|
|
716
|
+
processedContent: string;
|
|
717
|
+
buttonAttachments: Array<{
|
|
718
|
+
buttonId: string;
|
|
719
|
+
action: any;
|
|
720
|
+
match: string;
|
|
721
|
+
groups: any[];
|
|
722
|
+
}>;
|
|
723
|
+
} => {
|
|
724
|
+
let workingContent = content;
|
|
725
|
+
const buttonAttachments: Array<{
|
|
726
|
+
buttonId: string;
|
|
727
|
+
action: any;
|
|
728
|
+
match: string;
|
|
729
|
+
groups: any[];
|
|
730
|
+
}> = [];
|
|
731
|
+
|
|
732
|
+
// 1. Remove tool JSON patterns (display only)
|
|
733
|
+
allActions
|
|
734
|
+
.filter((a) => a.actionType === "tool")
|
|
735
|
+
.forEach((action) => {
|
|
736
|
+
try {
|
|
737
|
+
const regex = new RegExp(action.pattern, "gmi");
|
|
738
|
+
workingContent = workingContent.replace(regex, "");
|
|
739
|
+
} catch (e) {
|
|
740
|
+
console.warn("Invalid tool action regex", action.pattern, e);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// 2. Remove thinking tags (for history processing)
|
|
745
|
+
if (context.type === 'history') {
|
|
746
|
+
const { cleanedText } = processThinkingTags(workingContent);
|
|
747
|
+
workingContent = cleanedText;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// 3. Apply non-tool, non-response actions (markdown/html/button transformations)
|
|
751
|
+
const filteredActions = allActions.filter((a) => a.type !== "response" && a.actionType !== "tool");
|
|
752
|
+
console.log(`DEBUG: ${context.type} processing - filtered actions:`, filteredActions.length, 'of', allActions.length);
|
|
753
|
+
|
|
754
|
+
filteredActions.forEach((action, actionIndex) => {
|
|
755
|
+
try {
|
|
756
|
+
const regex = new RegExp(action.pattern, "gmi");
|
|
757
|
+
const matches = workingContent.match(regex);
|
|
758
|
+
|
|
759
|
+
if (matches) {
|
|
760
|
+
console.log(`${context.type === 'history' ? 'History' : 'Streaming'} processing: Found matches for pattern "${action.pattern}":`, matches, 'in content:', workingContent.substring(0, 100));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
workingContent = workingContent.replace(
|
|
764
|
+
regex,
|
|
765
|
+
(match: string, ...groups: any[]) => {
|
|
766
|
+
// The match parameter is the full match, groups are the capture groups
|
|
767
|
+
const actualMatch = match;
|
|
768
|
+
// Remove the last two elements (offset and full string) to get just capture groups
|
|
769
|
+
const restGroups = groups.slice(0, -2);
|
|
770
|
+
|
|
771
|
+
// Generate buttonId based on context
|
|
772
|
+
let buttonId: string;
|
|
773
|
+
if (context.type === 'history') {
|
|
774
|
+
const matchIndex = restGroups[restGroups.length - 2];
|
|
775
|
+
buttonId = `button-init-${context.historyIndex}-${actionIndex}-${matchIndex}`;
|
|
776
|
+
} else {
|
|
777
|
+
// Streaming context
|
|
778
|
+
const offset = groups[groups.length - 2];
|
|
779
|
+
const matchOffset = typeof offset === "number" ? offset : 0;
|
|
780
|
+
const key = `${actionIndex}:${matchOffset}`;
|
|
781
|
+
|
|
782
|
+
// Derive or allocate stable buttonId
|
|
783
|
+
let existingButtonId = actionMatchRegistry.current.get(key);
|
|
784
|
+
if (!existingButtonId) {
|
|
785
|
+
existingButtonId = `button-stable-${actionSequenceRef.current++}`;
|
|
786
|
+
actionMatchRegistry.current.set(key, existingButtonId);
|
|
787
|
+
}
|
|
788
|
+
buttonId = existingButtonId;
|
|
789
|
+
|
|
790
|
+
// If already finalized and progressive mode active, skip reinsertion
|
|
791
|
+
if (context.isProgressive && finalizedButtonsRef.current.has(buttonId)) {
|
|
792
|
+
return match;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Replace tokens in template
|
|
797
|
+
const substituteTemplate = (template: string) => {
|
|
798
|
+
let html = template.replace(/\$match/gim, actualMatch);
|
|
799
|
+
restGroups.forEach((g, gi) => {
|
|
800
|
+
html = html.replace(new RegExp(`\\$${gi + 1}`, "gmi"), g || '');
|
|
801
|
+
});
|
|
802
|
+
return html;
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// Build HTML output
|
|
806
|
+
let html = actualMatch;
|
|
807
|
+
if (action.type === "button" || action.type === "callback") {
|
|
808
|
+
// For history: always active buttons
|
|
809
|
+
// For streaming: placeholder (disabled) during streaming, active when idle
|
|
810
|
+
if (context.type === 'history') {
|
|
811
|
+
html = `<br /><button id="${buttonId}" ${
|
|
812
|
+
action.style ? 'class="' + action.style + '"' : ""
|
|
813
|
+
}>${action.markdown ?? actualMatch}</button>`;
|
|
814
|
+
} else {
|
|
815
|
+
// Streaming context with progressive actions
|
|
816
|
+
if (context.isProgressive && !context.isIdle) {
|
|
817
|
+
html = `<br /><button id="${buttonId}" data-pending="true" ${
|
|
818
|
+
action.style ? 'class="' + action.style + '"' : ""
|
|
819
|
+
}>${substituteTemplate(action.markdown ?? actualMatch)}</button>`;
|
|
820
|
+
} else {
|
|
821
|
+
html = `<br /><button id="${buttonId}" ${
|
|
822
|
+
action.style ? 'class="' + action.style + '"' : ""
|
|
823
|
+
}>${substituteTemplate(action.markdown ?? actualMatch)}</button>`;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
} else if (action.type === "markdown" || action.type === "html") {
|
|
827
|
+
html = context.type === 'history'
|
|
828
|
+
? (action.markdown ?? "")
|
|
829
|
+
: substituteTemplate(action.markdown ?? "");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Apply token substitution for history context
|
|
833
|
+
if (context.type === 'history') {
|
|
834
|
+
html = html.replace(new RegExp("\\$match", "gmi"), actualMatch);
|
|
835
|
+
restGroups.forEach((group: string, gi: number) => {
|
|
836
|
+
html = html.replace(
|
|
837
|
+
new RegExp(`\\$${gi + 1}`, "gmi"),
|
|
838
|
+
group || ''
|
|
839
|
+
);
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Queue button attachment context if needed
|
|
844
|
+
if (action.type === "button" || action.type === "callback") {
|
|
845
|
+
if (context.type === 'history') {
|
|
846
|
+
buttonAttachments.push({
|
|
847
|
+
buttonId,
|
|
848
|
+
action,
|
|
849
|
+
match: actualMatch,
|
|
850
|
+
groups: restGroups,
|
|
851
|
+
});
|
|
852
|
+
// Also add to registry for fallback event delegation
|
|
853
|
+
buttonActionRegistry.current.set(buttonId, {
|
|
854
|
+
action,
|
|
855
|
+
match: actualMatch,
|
|
856
|
+
groups: restGroups,
|
|
857
|
+
});
|
|
858
|
+
} else {
|
|
859
|
+
// Streaming context
|
|
860
|
+
if (!finalizedButtonsRef.current.has(buttonId)) {
|
|
861
|
+
deferredActionsRef.current.set(buttonId, {
|
|
862
|
+
action,
|
|
863
|
+
match: actualMatch,
|
|
864
|
+
groups: restGroups,
|
|
865
|
+
});
|
|
866
|
+
// If NOT progressive (legacy behavior) attach immediately
|
|
867
|
+
if (!context.isProgressive) {
|
|
868
|
+
buttonAttachments.push({
|
|
869
|
+
buttonId,
|
|
870
|
+
action,
|
|
871
|
+
match: actualMatch,
|
|
872
|
+
groups: restGroups,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return html;
|
|
880
|
+
}
|
|
881
|
+
);
|
|
882
|
+
} catch (e) {
|
|
883
|
+
console.warn("Invalid action regex", action.pattern, e);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
processedContent: workingContent,
|
|
889
|
+
buttonAttachments,
|
|
890
|
+
};
|
|
891
|
+
}, [allActions, processThinkingTags, progressiveActions, idle]);
|
|
892
|
+
|
|
893
|
+
useEffect(() => {
|
|
894
|
+
setShowEmailPanel(customerEmailCaptureMode !== "HIDE");
|
|
895
|
+
|
|
896
|
+
if (customerEmailCaptureMode === "REQUIRED") {
|
|
897
|
+
if (!isEmailAddress(emailInput)) {
|
|
898
|
+
setEmailValid(false);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}, [customerEmailCaptureMode]);
|
|
902
|
+
|
|
903
|
+
useEffect(() => {
|
|
904
|
+
// do any response actions
|
|
905
|
+
if (lastCallId && lastCallId !== "" && idle && response) {
|
|
906
|
+
allActions
|
|
907
|
+
.filter((a) => a.type === "response")
|
|
908
|
+
.forEach((action) => {
|
|
909
|
+
if (action.type === "response" && action.pattern) {
|
|
910
|
+
const regex = new RegExp(action.pattern, "gi");
|
|
911
|
+
const matches = regex.exec(response);
|
|
912
|
+
/*
|
|
913
|
+
console.log(
|
|
914
|
+
"action match",
|
|
915
|
+
matches,
|
|
916
|
+
action.pattern,
|
|
917
|
+
action.callback
|
|
918
|
+
);
|
|
919
|
+
*/
|
|
920
|
+
if (matches && action.callback) {
|
|
921
|
+
action.callback(matches[0], matches.slice(1));
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// call the connected responseCompleteCallback
|
|
928
|
+
if (responseCompleteCallback) {
|
|
929
|
+
if (lastCallId && lastCallId !== "" && idle)
|
|
930
|
+
responseCompleteCallback(lastCallId, lastPrompt ?? "", response);
|
|
931
|
+
}
|
|
932
|
+
}, [idle]);
|
|
933
|
+
|
|
934
|
+
useEffect(() => {
|
|
935
|
+
if (Object.keys(initialHistory).length === 0) return;
|
|
936
|
+
setHistory(initialHistory);
|
|
937
|
+
}, [initialHistory]);
|
|
938
|
+
|
|
939
|
+
useEffect(() => {
|
|
940
|
+
if (!conversation || conversation === "") return;
|
|
941
|
+
setCurrentConversation(conversation);
|
|
942
|
+
setHistory(initialHistory);
|
|
943
|
+
}, [conversation]);
|
|
944
|
+
|
|
945
|
+
useEffect(() => {
|
|
946
|
+
// Clean up any previously added CSS from this component
|
|
947
|
+
const existingLinks = document.querySelectorAll(
|
|
948
|
+
'link[data-source="llmasaservice-ui"]'
|
|
949
|
+
);
|
|
950
|
+
existingLinks.forEach((link) => link.parentNode?.removeChild(link));
|
|
951
|
+
|
|
952
|
+
const existingStyles = document.querySelectorAll(
|
|
953
|
+
'style[data-source="llmasaservice-ui"]'
|
|
954
|
+
);
|
|
955
|
+
existingStyles.forEach((style) => style.parentNode?.removeChild(style));
|
|
956
|
+
|
|
957
|
+
if (cssUrl) {
|
|
958
|
+
if (cssUrl.startsWith("http://") || cssUrl.startsWith("https://")) {
|
|
959
|
+
// If it's a URL, create a link element
|
|
960
|
+
const link = document.createElement("link");
|
|
961
|
+
link.href = cssUrl;
|
|
962
|
+
link.rel = "stylesheet";
|
|
963
|
+
// Add a data attribute to identify and remove this link later if needed
|
|
964
|
+
link.setAttribute("data-source", "llmasaservice-ui");
|
|
965
|
+
document.head.appendChild(link);
|
|
966
|
+
//console.log("Added CSS link", link);
|
|
967
|
+
} else {
|
|
968
|
+
// If it's a CSS string, create a style element
|
|
969
|
+
const style = document.createElement("style");
|
|
970
|
+
style.textContent = cssUrl;
|
|
971
|
+
// Add a data attribute to identify and remove this style later if needed
|
|
972
|
+
style.setAttribute("data-source", "llmasaservice-ui");
|
|
973
|
+
document.head.appendChild(style);
|
|
974
|
+
//console.log("Added inline CSS");
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Clean up when component unmounts
|
|
979
|
+
return () => {
|
|
980
|
+
const links = document.querySelectorAll(
|
|
981
|
+
'link[data-source="llmasaservice-ui"]'
|
|
982
|
+
);
|
|
983
|
+
links.forEach((link) => link.parentNode?.removeChild(link));
|
|
984
|
+
|
|
985
|
+
const styles = document.querySelectorAll(
|
|
986
|
+
'style[data-source="llmasaservice-ui"]'
|
|
987
|
+
);
|
|
988
|
+
styles.forEach((style) => style.parentNode?.removeChild(style));
|
|
989
|
+
};
|
|
990
|
+
}, [cssUrl]);
|
|
991
|
+
|
|
992
|
+
const extractValue = (
|
|
993
|
+
match: string,
|
|
994
|
+
groups: any[] = [],
|
|
995
|
+
extraArgs: string[] = []
|
|
996
|
+
): string => {
|
|
997
|
+
// Rule 1: If there are no extraArgs and no groups, use match.
|
|
998
|
+
if (
|
|
999
|
+
(!extraArgs || extraArgs.length === 0) &&
|
|
1000
|
+
(!groups || groups.length === 0)
|
|
1001
|
+
) {
|
|
1002
|
+
return match;
|
|
1003
|
+
}
|
|
1004
|
+
// Rule 2: If there are no extraArgs but groups exist, use groups[0].
|
|
1005
|
+
if ((!extraArgs || extraArgs.length === 0) && groups && groups.length > 0) {
|
|
1006
|
+
return groups[0];
|
|
1007
|
+
}
|
|
1008
|
+
// Rule 3: If there are extraArgs, use the first one as a template.
|
|
1009
|
+
if (extraArgs && extraArgs.length > 0) {
|
|
1010
|
+
const template = extraArgs[0] ?? "";
|
|
1011
|
+
return template.replace(/\$(\d+)/g, (_, index) => {
|
|
1012
|
+
const i = parseInt(index, 10);
|
|
1013
|
+
return groups[i] !== undefined ? groups[i] : "";
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
return "";
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
const openUrlActionCallback = useCallback(
|
|
1020
|
+
(match: string, groups: any[], ...extraArgs: string[]) => {
|
|
1021
|
+
const url = extractValue(match, groups, extraArgs);
|
|
1022
|
+
if (url?.startsWith("http") || url?.startsWith("mailto")) {
|
|
1023
|
+
window.open(url, "_blank");
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
[]
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
const copyToClipboardCallback = useCallback(
|
|
1030
|
+
(match: string, groups: any[], ...extraArgs: string[]) => {
|
|
1031
|
+
const val = extractValue(match, groups, extraArgs);
|
|
1032
|
+
navigator.clipboard.writeText(val);
|
|
1033
|
+
},
|
|
1034
|
+
[]
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
const showAlertCallback = useCallback(
|
|
1038
|
+
(match: string, groups: any[], ...extraArgs: string[]) => {
|
|
1039
|
+
alert(extractValue(match, groups, extraArgs));
|
|
1040
|
+
},
|
|
1041
|
+
[]
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
const sendFollowOnPromptCallback = useCallback(
|
|
1045
|
+
(match: string, groups: any[], ...extraArgs: string[]) => {
|
|
1046
|
+
const val = extractValue(match, groups, extraArgs);
|
|
1047
|
+
if (val && val !== followOnPrompt) {
|
|
1048
|
+
continueChat(val);
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
[followOnPrompt]
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
const setFollowUpQuestionsCallback = useCallback(
|
|
1055
|
+
(match: string, groups: any[], ...extraArgs: string[]) => {
|
|
1056
|
+
const val = extractValue(match, groups, extraArgs).split("|");
|
|
1057
|
+
setFollowOnQuestionsState(val);
|
|
1058
|
+
},
|
|
1059
|
+
[followOnQuestions]
|
|
1060
|
+
);
|
|
1061
|
+
|
|
1062
|
+
const openIframeCallback = useCallback(
|
|
1063
|
+
(match: string, groups: any[], ...extraArgs: string[]) => {
|
|
1064
|
+
const url = extractValue(match, groups, extraArgs);
|
|
1065
|
+
if (url?.startsWith("http")) {
|
|
1066
|
+
setIframeUrl(url);
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
[]
|
|
1070
|
+
);
|
|
1071
|
+
|
|
1072
|
+
const anthropic_toolAction = {
|
|
1073
|
+
pattern:
|
|
1074
|
+
'\\{"type":"tool_use","id":"([^"]+)","name":"([^"]+)","input":(\\{[\\s\\S]+?\\}),"service":"([^"]+)"\\}',
|
|
1075
|
+
type: "markdown",
|
|
1076
|
+
markdown: "<br />*Tool use requested: $2*",
|
|
1077
|
+
actionType: "tool",
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
const openAI_toolAction = {
|
|
1081
|
+
pattern:
|
|
1082
|
+
'\\{"id":"([^"]+)","type":"function","function":\\{"name":"([^"]+)","arguments":"((?:\\\\.|[^"\\\\])*)"\\},"service":"([^"]+)"\\}',
|
|
1083
|
+
type: "markdown",
|
|
1084
|
+
markdown: "<br />*Tool use requested: $2*",
|
|
1085
|
+
actionType: "tool",
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
// google doesn't return an id, so we just grab functioCall
|
|
1089
|
+
const google_toolAction = {
|
|
1090
|
+
pattern:
|
|
1091
|
+
'^\\{\\s*"(functionCall)"\\s*:\\s*\\{\\s*"name"\\s*:\\s*"([^"]+)"\\s*,\\s*"args"\\s*:\\s*(\\{[\\s\\S]+?\\})\\s*\\}(?:\\s*,\\s*"thoughtSignature"\\s*:\\s*"[^"]*")?\\s*,\\s*"service"\\s*:\\s*"([^"]+)"\\s*\\}$',
|
|
1092
|
+
type: "markdown",
|
|
1093
|
+
markdown: "<br />*Tool use requested: $2*",
|
|
1094
|
+
actionType: "tool",
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
type ActionCallback = (
|
|
1098
|
+
match: string,
|
|
1099
|
+
groups: any[],
|
|
1100
|
+
...extraArgs: string[]
|
|
1101
|
+
) => void;
|
|
1102
|
+
|
|
1103
|
+
const callbackMapping: Record<string, ActionCallback> = useMemo(
|
|
1104
|
+
() => ({
|
|
1105
|
+
openUrlActionCallback,
|
|
1106
|
+
copyToClipboardCallback,
|
|
1107
|
+
showAlertCallback,
|
|
1108
|
+
sendFollowOnPromptCallback,
|
|
1109
|
+
setFollowUpQuestionsCallback,
|
|
1110
|
+
openIframeCallback,
|
|
1111
|
+
}),
|
|
1112
|
+
[
|
|
1113
|
+
openUrlActionCallback,
|
|
1114
|
+
copyToClipboardCallback,
|
|
1115
|
+
showAlertCallback,
|
|
1116
|
+
sendFollowOnPromptCallback,
|
|
1117
|
+
setFollowUpQuestionsCallback,
|
|
1118
|
+
openIframeCallback,
|
|
1119
|
+
]
|
|
1120
|
+
);
|
|
1121
|
+
const parseCallbackString = (callbackStr: string) => {
|
|
1122
|
+
const regex = /^(\w+)(?:\((.+)\))?$/;
|
|
1123
|
+
const match = callbackStr.match(regex);
|
|
1124
|
+
if (match) {
|
|
1125
|
+
const name = match[1];
|
|
1126
|
+
// If there are args, split by comma and trim whitespace.
|
|
1127
|
+
const args = match[2] ? match[2].split(",").map((arg) => arg.trim()) : [];
|
|
1128
|
+
return { name, args };
|
|
1129
|
+
}
|
|
1130
|
+
return null;
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
const getActionsArraySafely = (actionsString: string) => {
|
|
1134
|
+
let actions: any[] = [];
|
|
1135
|
+
if (actionsString && actionsString !== "") {
|
|
1136
|
+
try {
|
|
1137
|
+
actions = JSON.parse(actionsString);
|
|
1138
|
+
if (!Array.isArray(actions)) {
|
|
1139
|
+
throw new Error("Parsed actions is not an array");
|
|
1140
|
+
}
|
|
1141
|
+
// Map string callbacks to actual functions using callbackMapping and parsing args if needed
|
|
1142
|
+
actions = actions
|
|
1143
|
+
.map((action) => {
|
|
1144
|
+
if (typeof action.callback === "string") {
|
|
1145
|
+
const parsed = parseCallbackString(action.callback);
|
|
1146
|
+
if (parsed && parsed.name && callbackMapping[parsed.name]) {
|
|
1147
|
+
// Wrap the callback so that it receives the original match & groups plus extra args
|
|
1148
|
+
const mappedCallback = callbackMapping[parsed.name];
|
|
1149
|
+
if (mappedCallback) {
|
|
1150
|
+
return {
|
|
1151
|
+
...action,
|
|
1152
|
+
callback: (match: string, groups: any[]) =>
|
|
1153
|
+
mappedCallback(match, groups, ...parsed.args),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
} else {
|
|
1157
|
+
// Optionally provide a no-op fallback or skip the action:
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
} else {
|
|
1161
|
+
return action;
|
|
1162
|
+
}
|
|
1163
|
+
})
|
|
1164
|
+
.filter(Boolean); // removes null entries
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
console.error("Error parsing actions string:", error);
|
|
1167
|
+
actions = [];
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return actions;
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
useEffect(() => {
|
|
1174
|
+
// DEBUG: Log when actions prop changes
|
|
1175
|
+
console.log("ChatPanel received actions prop change:", {
|
|
1176
|
+
actions,
|
|
1177
|
+
actionsType: typeof actions,
|
|
1178
|
+
actionsLength: Array.isArray(actions) ? actions.length : "not array"
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
const actionsString =
|
|
1182
|
+
typeof actions === "string" ? actions : JSON.stringify(actions);
|
|
1183
|
+
const processedActions = getActionsArraySafely(actionsString);
|
|
1184
|
+
console.log("DEBUG: Setting allActions:", {
|
|
1185
|
+
actionsString,
|
|
1186
|
+
processedActions,
|
|
1187
|
+
totalActions: [...processedActions, anthropic_toolAction, openAI_toolAction, google_toolAction].length
|
|
1188
|
+
});
|
|
1189
|
+
setAllActions([
|
|
1190
|
+
...processedActions,
|
|
1191
|
+
anthropic_toolAction,
|
|
1192
|
+
openAI_toolAction,
|
|
1193
|
+
google_toolAction,
|
|
1194
|
+
]);
|
|
1195
|
+
}, [actions]);
|
|
1196
|
+
|
|
1197
|
+
// Process existing (initial) history entries so they receive the same formatting
|
|
1198
|
+
// as streamed responses (tool JSON removal, thinking tag removal, action markdown/button injection)
|
|
1199
|
+
useEffect(() => {
|
|
1200
|
+
if (!allActions || allActions.length === 0) return; // wait for actions
|
|
1201
|
+
setHistory((prevHistory) => {
|
|
1202
|
+
if (!prevHistory || Object.keys(prevHistory).length === 0)
|
|
1203
|
+
return prevHistory;
|
|
1204
|
+
|
|
1205
|
+
let changed = false;
|
|
1206
|
+
const updated: typeof prevHistory = { ...prevHistory };
|
|
1207
|
+
const newButtonAttachments: Array<{
|
|
1208
|
+
buttonId: string;
|
|
1209
|
+
action: any;
|
|
1210
|
+
match: string;
|
|
1211
|
+
groups: any[];
|
|
1212
|
+
}> = [];
|
|
1213
|
+
|
|
1214
|
+
Object.entries(prevHistory).forEach(([prompt, entry], historyIndex) => {
|
|
1215
|
+
if (!entry || !entry.content) {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Use centralized action processing function
|
|
1220
|
+
const { processedContent, buttonAttachments } = processActionsOnContent(
|
|
1221
|
+
entry.content,
|
|
1222
|
+
{
|
|
1223
|
+
type: 'history',
|
|
1224
|
+
historyIndex,
|
|
1225
|
+
}
|
|
1226
|
+
);
|
|
1227
|
+
|
|
1228
|
+
if (processedContent !== entry.content) {
|
|
1229
|
+
updated[prompt] = { ...entry, content: processedContent } as any;
|
|
1230
|
+
changed = true;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Add button attachments to the queue
|
|
1234
|
+
newButtonAttachments.push(...buttonAttachments);
|
|
1235
|
+
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
if (newButtonAttachments.length > 0) {
|
|
1239
|
+
// Defer adding button attachments until after history state applied
|
|
1240
|
+
setPendingButtonAttachments((prev) => [
|
|
1241
|
+
...prev,
|
|
1242
|
+
...newButtonAttachments,
|
|
1243
|
+
]);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return changed ? updated : prevHistory;
|
|
1247
|
+
});
|
|
1248
|
+
}, [allActions, processThinkingTags, initialHistory]);
|
|
1249
|
+
|
|
1250
|
+
const pendingToolRequestsRef = useRef(pendingToolRequests);
|
|
1251
|
+
|
|
1252
|
+
useEffect(() => {
|
|
1253
|
+
pendingToolRequestsRef.current = pendingToolRequests;
|
|
1254
|
+
}, [pendingToolRequests]);
|
|
1255
|
+
|
|
1256
|
+
const processGivenToolRequests = async (
|
|
1257
|
+
requests: typeof pendingToolRequests
|
|
1258
|
+
) => {
|
|
1259
|
+
if (!requests || requests.length === 0)
|
|
1260
|
+
requests = pendingToolRequestsRef.current;
|
|
1261
|
+
|
|
1262
|
+
if (requests.length === 0) return;
|
|
1263
|
+
|
|
1264
|
+
//console.log("processGivenToolRequests", requests);
|
|
1265
|
+
setIsLoading(true);
|
|
1266
|
+
|
|
1267
|
+
const toolsToProcess = [...requests];
|
|
1268
|
+
setPendingToolRequests([]);
|
|
1269
|
+
try {
|
|
1270
|
+
// Start with base messages including the user's original question
|
|
1271
|
+
const newMessages = [
|
|
1272
|
+
{
|
|
1273
|
+
role: "user",
|
|
1274
|
+
content: [
|
|
1275
|
+
{
|
|
1276
|
+
type: "text",
|
|
1277
|
+
text: lastKey,
|
|
1278
|
+
},
|
|
1279
|
+
],
|
|
1280
|
+
},
|
|
1281
|
+
];
|
|
1282
|
+
|
|
1283
|
+
// Add a single assistant message with ALL tool calls
|
|
1284
|
+
const toolCallsMessage = {
|
|
1285
|
+
role: "assistant",
|
|
1286
|
+
content: [],
|
|
1287
|
+
tool_calls: [],
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
// Parse all tool calls first
|
|
1291
|
+
const toolCallsPromises = toolsToProcess.map(async (req) => {
|
|
1292
|
+
if (!req) return null;
|
|
1293
|
+
|
|
1294
|
+
try {
|
|
1295
|
+
return {
|
|
1296
|
+
req,
|
|
1297
|
+
parsedToolCall: JSON.parse(req.match),
|
|
1298
|
+
};
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
console.error("Failed to parse tool call:", e);
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
// Wait for all tool calls to be parsed
|
|
1306
|
+
const parsedToolCalls = await Promise.all(toolCallsPromises);
|
|
1307
|
+
|
|
1308
|
+
// Add all tool calls to the assistant message
|
|
1309
|
+
parsedToolCalls.forEach((item) => {
|
|
1310
|
+
if (item && item.parsedToolCall) {
|
|
1311
|
+
(toolCallsMessage.tool_calls as any[]).push(item.parsedToolCall);
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
// Add the assistant message with all tool calls
|
|
1316
|
+
newMessages.push(toolCallsMessage);
|
|
1317
|
+
|
|
1318
|
+
const finalToolCalls = toolCallsMessage.tool_calls;
|
|
1319
|
+
|
|
1320
|
+
const toolResponsePromises = parsedToolCalls.map(async (item) => {
|
|
1321
|
+
if (!item || !item.req) return null;
|
|
1322
|
+
|
|
1323
|
+
const req = item.req;
|
|
1324
|
+
//console.log(`Processing tool ${req.toolName}`);
|
|
1325
|
+
|
|
1326
|
+
const mcpTool = toolList.find((tool) => tool.name === req.toolName);
|
|
1327
|
+
|
|
1328
|
+
if (!mcpTool) {
|
|
1329
|
+
console.error(`Tool ${req.toolName} not found in tool list`);
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
try {
|
|
1334
|
+
let args;
|
|
1335
|
+
try {
|
|
1336
|
+
args = JSON.parse(req.groups[2]);
|
|
1337
|
+
} catch (e) {
|
|
1338
|
+
try {
|
|
1339
|
+
args = JSON.parse(req.groups[2].replace(/\\"/g, '"'));
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
console.error("Failed to parse tool arguments:", err);
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const body = {
|
|
1347
|
+
tool: req.groups[1],
|
|
1348
|
+
args: args,
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
const result = await fetch(
|
|
1352
|
+
`${publicAPIUrl}/tools/${encodeURIComponent(mcpTool.url)}`,
|
|
1353
|
+
{
|
|
1354
|
+
method: "POST",
|
|
1355
|
+
headers: {
|
|
1356
|
+
"Content-Type": "application/json",
|
|
1357
|
+
"x-mcp-access-token":
|
|
1358
|
+
mcpTool.accessToken && mcpTool.accessToken !== ""
|
|
1359
|
+
? mcpTool.accessToken
|
|
1360
|
+
: "",
|
|
1361
|
+
"x-project-id": project_id,
|
|
1362
|
+
},
|
|
1363
|
+
body: JSON.stringify(body),
|
|
1364
|
+
}
|
|
1365
|
+
);
|
|
1366
|
+
|
|
1367
|
+
if (!result.ok) {
|
|
1368
|
+
console.error(
|
|
1369
|
+
`Error calling tool ${req.toolName}: ${result.status} ${result.statusText}`
|
|
1370
|
+
);
|
|
1371
|
+
const errorBody = await result.text();
|
|
1372
|
+
console.error(`Error body: ${errorBody}`);
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
let resultData;
|
|
1377
|
+
try {
|
|
1378
|
+
resultData = await result.json();
|
|
1379
|
+
} catch (jsonError) {
|
|
1380
|
+
console.error(
|
|
1381
|
+
`Error parsing JSON response for tool ${req.toolName}:`,
|
|
1382
|
+
jsonError
|
|
1383
|
+
);
|
|
1384
|
+
// Attempt to read as text for debugging if JSON fails
|
|
1385
|
+
try {
|
|
1386
|
+
const textBody = await result.text(); // Note: This consumes the body if json() failed early
|
|
1387
|
+
console.error("Response body (text):", textBody);
|
|
1388
|
+
} catch (textError) {
|
|
1389
|
+
console.error(
|
|
1390
|
+
"Failed to read response body as text either:",
|
|
1391
|
+
textError
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
return null; // Exit if JSON parsing failed
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
if (
|
|
1398
|
+
resultData &&
|
|
1399
|
+
resultData.content &&
|
|
1400
|
+
resultData.content.length > 0
|
|
1401
|
+
) {
|
|
1402
|
+
const textResult = resultData.content[0]?.text;
|
|
1403
|
+
return {
|
|
1404
|
+
role: "tool",
|
|
1405
|
+
content: [
|
|
1406
|
+
{
|
|
1407
|
+
type: "text",
|
|
1408
|
+
text: textResult,
|
|
1409
|
+
},
|
|
1410
|
+
],
|
|
1411
|
+
tool_call_id: req.groups[0],
|
|
1412
|
+
};
|
|
1413
|
+
} else {
|
|
1414
|
+
console.error(`No content returned from tool ${req.toolName}`);
|
|
1415
|
+
return null;
|
|
1416
|
+
}
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
console.error(`Error processing tool ${req.toolName}:`, error);
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
// Wait for all tool responses
|
|
1424
|
+
const toolResponses = await Promise.all(toolResponsePromises);
|
|
1425
|
+
const finalToolResponses = toolResponses.filter(Boolean); // Filter out null
|
|
1426
|
+
|
|
1427
|
+
if (lastKey) {
|
|
1428
|
+
setHistory((prev) => {
|
|
1429
|
+
const existingEntry = prev[lastKey] || {};
|
|
1430
|
+
|
|
1431
|
+
return {
|
|
1432
|
+
...prev,
|
|
1433
|
+
[lastKey]: {
|
|
1434
|
+
...existingEntry,
|
|
1435
|
+
toolCalls: [
|
|
1436
|
+
...((existingEntry as any).toolCalls || []),
|
|
1437
|
+
...finalToolCalls,
|
|
1438
|
+
],
|
|
1439
|
+
toolResponses: [
|
|
1440
|
+
...((existingEntry as any).toolResponses || []),
|
|
1441
|
+
...finalToolResponses,
|
|
1442
|
+
],
|
|
1443
|
+
},
|
|
1444
|
+
} as any;
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
finalToolResponses.forEach((response) => {
|
|
1449
|
+
if (response) {
|
|
1450
|
+
newMessages.push(response);
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
send(
|
|
1455
|
+
"",
|
|
1456
|
+
newMessages as any,
|
|
1457
|
+
[
|
|
1458
|
+
...dataWithExtras(),
|
|
1459
|
+
{
|
|
1460
|
+
key: "--messages",
|
|
1461
|
+
data: newMessages.length.toString(),
|
|
1462
|
+
},
|
|
1463
|
+
],
|
|
1464
|
+
true,
|
|
1465
|
+
true,
|
|
1466
|
+
service,
|
|
1467
|
+
currentConversation,
|
|
1468
|
+
lastController
|
|
1469
|
+
);
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
console.error("Error in processing all tools:", error);
|
|
1472
|
+
setIsLoading(false);
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
useEffect(() => {
|
|
1477
|
+
if (response && response.length > 0) {
|
|
1478
|
+
setIsLoading(false);
|
|
1479
|
+
|
|
1480
|
+
// Step 1: Detect tool requests from the original response BEFORE any cleaning
|
|
1481
|
+
const toolRequests: { match: string; groups: any[]; toolName: string }[] =
|
|
1482
|
+
[];
|
|
1483
|
+
|
|
1484
|
+
if (allActions && allActions.length > 0) {
|
|
1485
|
+
allActions
|
|
1486
|
+
.filter((a) => a.actionType === "tool")
|
|
1487
|
+
.forEach((action) => {
|
|
1488
|
+
const regex = new RegExp(action.pattern, "gmi");
|
|
1489
|
+
let match;
|
|
1490
|
+
// Use original response for tool detection
|
|
1491
|
+
while ((match = regex.exec(response)) !== null) {
|
|
1492
|
+
toolRequests.push({
|
|
1493
|
+
match: match[0],
|
|
1494
|
+
groups: Array.from(match).slice(1),
|
|
1495
|
+
toolName: match[2] ?? "tool", // Tool name should always in the 2nd capture group
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// Set tool requests immediately after detection
|
|
1502
|
+
if (toolRequests.length > 0) {
|
|
1503
|
+
//console.log("toolRequests", toolRequests);
|
|
1504
|
+
setPendingToolRequests(toolRequests);
|
|
1505
|
+
} else {
|
|
1506
|
+
setPendingToolRequests([]);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Step 2: Remove tool JSON patterns from response for display
|
|
1510
|
+
let responseWithoutTools = response;
|
|
1511
|
+
if (allActions && allActions.length > 0) {
|
|
1512
|
+
allActions
|
|
1513
|
+
.filter((a) => a.actionType === "tool")
|
|
1514
|
+
.forEach((action) => {
|
|
1515
|
+
const regex = new RegExp(action.pattern, "gmi");
|
|
1516
|
+
responseWithoutTools = responseWithoutTools.replace(regex, "");
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Step 3: Process thinking tags on the response without tool JSON
|
|
1521
|
+
const { cleanedText, thinkingBlocks: newThinkingBlocks } =
|
|
1522
|
+
processThinkingTags(responseWithoutTools);
|
|
1523
|
+
|
|
1524
|
+
// Replace the blocks entirely (don't append) to avoid duplicates during streaming
|
|
1525
|
+
setThinkingBlocks(newThinkingBlocks);
|
|
1526
|
+
// Always show the latest (last) thinking block
|
|
1527
|
+
setCurrentThinkingIndex(Math.max(0, newThinkingBlocks.length - 1));
|
|
1528
|
+
|
|
1529
|
+
// Step 4: Process other non-tool actions on the cleaned response with two-phase option
|
|
1530
|
+
const { processedContent: newResponse, buttonAttachments } = processActionsOnContent(
|
|
1531
|
+
cleanedText,
|
|
1532
|
+
{
|
|
1533
|
+
type: 'streaming',
|
|
1534
|
+
isProgressive: progressiveActions,
|
|
1535
|
+
isIdle: idle,
|
|
1536
|
+
}
|
|
1537
|
+
);
|
|
1538
|
+
|
|
1539
|
+
// Handle button attachments for non-progressive mode
|
|
1540
|
+
if (!progressiveActions && buttonAttachments.length > 0) {
|
|
1541
|
+
setPendingButtonAttachments((prev) => [...prev, ...buttonAttachments]);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// Store the cleaned response (without reasoning/searching tags and without tool JSON)
|
|
1545
|
+
setHistory((prevHistory) => {
|
|
1546
|
+
// Get any existing tool data from the previous state
|
|
1547
|
+
const existingEntry = prevHistory[lastKey ?? ""] || {
|
|
1548
|
+
content: "",
|
|
1549
|
+
callId: "",
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
const updatedHistory = {
|
|
1553
|
+
...prevHistory,
|
|
1554
|
+
[lastKey ?? ""]: {
|
|
1555
|
+
...existingEntry, // This preserves toolCalls and toolResponses
|
|
1556
|
+
content: newResponse, // Store cleaned response without thinking tags or tool JSON
|
|
1557
|
+
callId: lastCallId,
|
|
1558
|
+
},
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
return updatedHistory;
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
}, [
|
|
1565
|
+
response,
|
|
1566
|
+
allActions,
|
|
1567
|
+
lastKey,
|
|
1568
|
+
lastCallId,
|
|
1569
|
+
messages.length,
|
|
1570
|
+
lastPrompt,
|
|
1571
|
+
lastMessages,
|
|
1572
|
+
initialPrompt,
|
|
1573
|
+
processThinkingTags,
|
|
1574
|
+
progressiveActions,
|
|
1575
|
+
idle,
|
|
1576
|
+
]);
|
|
1577
|
+
|
|
1578
|
+
// Finalization effect: when idle transitions true, convert placeholders to active buttons and queue attachments once
|
|
1579
|
+
useEffect(() => {
|
|
1580
|
+
if (!progressiveActions) return; // legacy path unchanged
|
|
1581
|
+
if (!idle) return; // only finalize at end
|
|
1582
|
+
if (!lastKey) return;
|
|
1583
|
+
if (finalizedForCallRef.current === lastCallId) return; // already finalized
|
|
1584
|
+
|
|
1585
|
+
// Replace data-pending buttons in the stored history content for the lastKey
|
|
1586
|
+
setHistory((prev) => {
|
|
1587
|
+
const entry = prev[lastKey];
|
|
1588
|
+
if (!entry) return prev;
|
|
1589
|
+
let content = entry.content;
|
|
1590
|
+
// Simple replace: remove data-pending attribute
|
|
1591
|
+
content = content.replace(/data-pending="true"/g, "");
|
|
1592
|
+
|
|
1593
|
+
const updated = {
|
|
1594
|
+
...prev,
|
|
1595
|
+
[lastKey]: { ...entry, content },
|
|
1596
|
+
};
|
|
1597
|
+
return updated;
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
// Queue attachments for all deferred actions not yet finalized
|
|
1601
|
+
const attachments: Array<{
|
|
1602
|
+
buttonId: string;
|
|
1603
|
+
action: any;
|
|
1604
|
+
match: string;
|
|
1605
|
+
groups: any[];
|
|
1606
|
+
}> = [];
|
|
1607
|
+
deferredActionsRef.current.forEach((meta, buttonId) => {
|
|
1608
|
+
if (!finalizedButtonsRef.current.has(buttonId)) {
|
|
1609
|
+
attachments.push({ buttonId, ...meta });
|
|
1610
|
+
finalizedButtonsRef.current.add(buttonId);
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
if (attachments.length > 0) {
|
|
1615
|
+
setPendingButtonAttachments((prev) => [...prev, ...attachments]);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// Move deferred into active registry for delegation
|
|
1619
|
+
attachments.forEach(({ buttonId, action, match, groups }) => {
|
|
1620
|
+
buttonActionRegistry.current.set(buttonId, { action, match, groups });
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
finalizedForCallRef.current = lastCallId;
|
|
1624
|
+
}, [idle, progressiveActions, lastCallId, lastKey]);
|
|
1625
|
+
|
|
1626
|
+
// More reliable button attachment with retry mechanism and MutationObserver
|
|
1627
|
+
const attachButtonHandlers = useCallback(
|
|
1628
|
+
(
|
|
1629
|
+
attachments: Array<{
|
|
1630
|
+
buttonId: string;
|
|
1631
|
+
action: any;
|
|
1632
|
+
match: string;
|
|
1633
|
+
groups: any[];
|
|
1634
|
+
}>,
|
|
1635
|
+
retryCount = 0
|
|
1636
|
+
) => {
|
|
1637
|
+
if (attachments.length === 0) return;
|
|
1638
|
+
let attachedCount = 0;
|
|
1639
|
+
let notFoundCount = 0;
|
|
1640
|
+
let alreadyAttachedCount = 0;
|
|
1641
|
+
const stillPending: typeof attachments = [];
|
|
1642
|
+
|
|
1643
|
+
// First, let's see what buttons actually exist in the DOM
|
|
1644
|
+
const allButtonsInDOM = document.querySelectorAll("button");
|
|
1645
|
+
const buttonIdsInDOM = Array.from(allButtonsInDOM)
|
|
1646
|
+
.map((btn) => btn.id)
|
|
1647
|
+
.filter((id) => id);
|
|
1648
|
+
|
|
1649
|
+
attachments.forEach(({ buttonId, action, match, groups }) => {
|
|
1650
|
+
const button = document.getElementById(buttonId) as HTMLButtonElement;
|
|
1651
|
+
|
|
1652
|
+
if (button) {
|
|
1653
|
+
if (!button.onclick) {
|
|
1654
|
+
button.onclick = () => {
|
|
1655
|
+
if (action.callback) {
|
|
1656
|
+
action.callback(match, groups);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (action.clickCode) {
|
|
1660
|
+
try {
|
|
1661
|
+
const func = new Function("match", action.clickCode);
|
|
1662
|
+
func(match);
|
|
1663
|
+
// Note: interactionClicked will be available when this closure executes
|
|
1664
|
+
if (typeof interactionClicked === "function") {
|
|
1665
|
+
interactionClicked(lastCallId, "action");
|
|
1666
|
+
}
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
console.error("Error executing clickCode:", error);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
attachedCount++;
|
|
1673
|
+
} else {
|
|
1674
|
+
alreadyAttachedCount++;
|
|
1675
|
+
}
|
|
1676
|
+
} else {
|
|
1677
|
+
notFoundCount++;
|
|
1678
|
+
stillPending.push({ buttonId, action, match, groups });
|
|
1679
|
+
// Only register in fallback for buttons that failed direct attachment
|
|
1680
|
+
buttonActionRegistry.current.set(buttonId, { action, match, groups });
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
// If there are still pending buttons and we haven't exceeded retry limit, try again
|
|
1685
|
+
if (stillPending.length > 0 && retryCount < 5) {
|
|
1686
|
+
setTimeout(() => {
|
|
1687
|
+
attachButtonHandlers(stillPending, retryCount + 1);
|
|
1688
|
+
}, 200);
|
|
1689
|
+
} else if (stillPending.length > 0) {
|
|
1690
|
+
console.warn(
|
|
1691
|
+
"Failed to attach all buttons after max retries:",
|
|
1692
|
+
stillPending.map((p) => p.buttonId)
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
[lastCallId]
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
// Handle button attachments after history updates
|
|
1700
|
+
useEffect(() => {
|
|
1701
|
+
if (pendingButtonAttachments.length > 0) {
|
|
1702
|
+
// Use requestAnimationFrame to ensure DOM is ready, then add a small delay
|
|
1703
|
+
requestAnimationFrame(() => {
|
|
1704
|
+
setTimeout(() => {
|
|
1705
|
+
attachButtonHandlers([...pendingButtonAttachments]);
|
|
1706
|
+
setPendingButtonAttachments([]);
|
|
1707
|
+
}, 100);
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
}, [pendingButtonAttachments, attachButtonHandlers]);
|
|
1711
|
+
|
|
1712
|
+
// Additional effect to catch buttons that might be added through other means
|
|
1713
|
+
useEffect(() => {
|
|
1714
|
+
if (typeof window === "undefined") return;
|
|
1715
|
+
|
|
1716
|
+
const observer = new MutationObserver((mutations) => {
|
|
1717
|
+
let newButtonsFound = false;
|
|
1718
|
+
|
|
1719
|
+
mutations.forEach((mutation) => {
|
|
1720
|
+
if (mutation.type === "childList") {
|
|
1721
|
+
mutation.addedNodes.forEach((node) => {
|
|
1722
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1723
|
+
const element = node as Element;
|
|
1724
|
+
|
|
1725
|
+
// Check if the added node is a button with an action ID or contains such buttons
|
|
1726
|
+
const actionButtons = element.id?.startsWith("button-")
|
|
1727
|
+
? [element as HTMLButtonElement]
|
|
1728
|
+
: Array.from(element.querySelectorAll('button[id^="button-"]'));
|
|
1729
|
+
|
|
1730
|
+
if (actionButtons.length > 0) {
|
|
1731
|
+
newButtonsFound = true;
|
|
1732
|
+
|
|
1733
|
+
// Check if any of these buttons don't have click handlers
|
|
1734
|
+
actionButtons.forEach((button) => {
|
|
1735
|
+
if (!(button as HTMLButtonElement).onclick) {
|
|
1736
|
+
console.log(
|
|
1737
|
+
"Found unattached button via MutationObserver:",
|
|
1738
|
+
button.id
|
|
1739
|
+
);
|
|
1740
|
+
// Note: We can't directly attach here because we don't have the action context
|
|
1741
|
+
// This is mainly for debugging to see if buttons are being added after our attachment
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
// Start observing only the response area, not the entire document
|
|
1752
|
+
const responseArea = responseAreaRef.current;
|
|
1753
|
+
if (responseArea) {
|
|
1754
|
+
observer.observe(responseArea, {
|
|
1755
|
+
childList: true,
|
|
1756
|
+
subtree: true,
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
return () => observer.disconnect();
|
|
1761
|
+
}, []);
|
|
1762
|
+
|
|
1763
|
+
// Fallback event delegation system for button clicks
|
|
1764
|
+
useEffect(() => {
|
|
1765
|
+
if (typeof window === "undefined") return;
|
|
1766
|
+
|
|
1767
|
+
const handleDelegatedClick = (event: Event) => {
|
|
1768
|
+
const target = event.target as HTMLElement;
|
|
1769
|
+
|
|
1770
|
+
// Check if the clicked element is a button with an action ID
|
|
1771
|
+
if (
|
|
1772
|
+
target.tagName === "BUTTON" &&
|
|
1773
|
+
target.id &&
|
|
1774
|
+
target.id.startsWith("button-")
|
|
1775
|
+
) {
|
|
1776
|
+
const buttonData = buttonActionRegistry.current.get(target.id);
|
|
1777
|
+
|
|
1778
|
+
if (buttonData) {
|
|
1779
|
+
const { action, match, groups } = buttonData;
|
|
1780
|
+
|
|
1781
|
+
// Only handle if the button doesn't already have an onclick handler
|
|
1782
|
+
const button = target as HTMLButtonElement;
|
|
1783
|
+
if (!button.onclick) {
|
|
1784
|
+
// Prevent default and stop propagation to avoid conflicts
|
|
1785
|
+
event.preventDefault();
|
|
1786
|
+
event.stopPropagation();
|
|
1787
|
+
|
|
1788
|
+
if (action.callback) {
|
|
1789
|
+
action.callback(match, groups);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
if (action.clickCode) {
|
|
1793
|
+
try {
|
|
1794
|
+
const func = new Function("match", action.clickCode);
|
|
1795
|
+
func(match);
|
|
1796
|
+
if (typeof interactionClicked === "function") {
|
|
1797
|
+
interactionClicked(lastCallId, "action");
|
|
1798
|
+
}
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
console.error(
|
|
1801
|
+
"Error executing clickCode via delegation:",
|
|
1802
|
+
error
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// Remove from registry after successful execution to prevent memory leaks
|
|
1808
|
+
buttonActionRegistry.current.delete(target.id);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
// Add the delegated event listener to the document (using bubble phase instead of capture)
|
|
1815
|
+
document.addEventListener("click", handleDelegatedClick, false);
|
|
1816
|
+
|
|
1817
|
+
return () => {
|
|
1818
|
+
document.removeEventListener("click", handleDelegatedClick, false);
|
|
1819
|
+
};
|
|
1820
|
+
}, [lastCallId]);
|
|
1821
|
+
|
|
1822
|
+
// Debug function to check DOM state - you can call this from browser console
|
|
1823
|
+
useEffect(() => {
|
|
1824
|
+
if (typeof window !== "undefined") {
|
|
1825
|
+
(window as any).debugChatPanelButtons = () => {
|
|
1826
|
+
const allButtons = document.querySelectorAll('button[id^="button-"]');
|
|
1827
|
+
const allButtonsAny = document.querySelectorAll("button");
|
|
1828
|
+
const buttonInfo = Array.from(allButtons).map((button) => ({
|
|
1829
|
+
id: button.id,
|
|
1830
|
+
hasOnclick: !!(button as HTMLButtonElement).onclick,
|
|
1831
|
+
textContent: button.textContent?.substring(0, 50),
|
|
1832
|
+
visible: (button as HTMLElement).offsetParent !== null,
|
|
1833
|
+
inDOM: document.contains(button),
|
|
1834
|
+
parentElement: button.parentElement?.tagName,
|
|
1835
|
+
outerHTML: button.outerHTML.substring(0, 200),
|
|
1836
|
+
inRegistry: buttonActionRegistry.current.has(button.id),
|
|
1837
|
+
}));
|
|
1838
|
+
|
|
1839
|
+
return buttonInfo;
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
}, [pendingButtonAttachments, history]);
|
|
1843
|
+
|
|
1844
|
+
function hasVerticalScrollbar(element: any) {
|
|
1845
|
+
return element.scrollHeight > element.clientHeight;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// Auto-resize textarea based on content
|
|
1849
|
+
const autoResizeTextarea = useCallback(() => {
|
|
1850
|
+
if (textareaRef.current) {
|
|
1851
|
+
const textarea = textareaRef.current;
|
|
1852
|
+
// Reset height to auto to get the actual scrollHeight
|
|
1853
|
+
textarea.style.height = "auto";
|
|
1854
|
+
// Set height to scrollHeight, but with min and max constraints
|
|
1855
|
+
const minHeight = 40; // Minimum height in pixels
|
|
1856
|
+
const maxHeight = 120; // Maximum height in pixels (about 4 lines)
|
|
1857
|
+
|
|
1858
|
+
// Use user's resized height as minimum if they've manually resized
|
|
1859
|
+
const effectiveMinHeight = userResizedHeight ? Math.max(userResizedHeight, minHeight) : minHeight;
|
|
1860
|
+
|
|
1861
|
+
// If user has manually resized, don't enforce max height constraint
|
|
1862
|
+
const effectiveMaxHeight = userResizedHeight ? Number.MAX_SAFE_INTEGER : maxHeight;
|
|
1863
|
+
|
|
1864
|
+
const newHeight = Math.min(
|
|
1865
|
+
Math.max(textarea.scrollHeight, effectiveMinHeight),
|
|
1866
|
+
effectiveMaxHeight
|
|
1867
|
+
);
|
|
1868
|
+
textarea.style.height = `${newHeight}px`;
|
|
1869
|
+
}
|
|
1870
|
+
}, [userResizedHeight]);
|
|
1871
|
+
|
|
1872
|
+
// Auto-resize textarea when nextPrompt changes or component mounts
|
|
1873
|
+
useEffect(() => {
|
|
1874
|
+
autoResizeTextarea();
|
|
1875
|
+
}, [nextPrompt, autoResizeTextarea]);
|
|
1876
|
+
|
|
1877
|
+
// Detect manual textarea resizing by user
|
|
1878
|
+
useEffect(() => {
|
|
1879
|
+
if (!textareaRef.current) return;
|
|
1880
|
+
|
|
1881
|
+
const textarea = textareaRef.current;
|
|
1882
|
+
|
|
1883
|
+
// Store the height before any potential user interaction
|
|
1884
|
+
let heightBeforeInteraction = textarea.clientHeight;
|
|
1885
|
+
let userIsInteracting = false;
|
|
1886
|
+
|
|
1887
|
+
const handleMouseDown = () => {
|
|
1888
|
+
heightBeforeInteraction = textarea.clientHeight;
|
|
1889
|
+
userIsInteracting = true;
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
const handleMouseUp = () => {
|
|
1893
|
+
if (userIsInteracting) {
|
|
1894
|
+
const currentHeight = textarea.clientHeight;
|
|
1895
|
+
// If the height changed significantly during user interaction, consider it a manual resize
|
|
1896
|
+
if (Math.abs(currentHeight - heightBeforeInteraction) > 5) {
|
|
1897
|
+
setUserResizedHeight(currentHeight);
|
|
1898
|
+
}
|
|
1899
|
+
userIsInteracting = false;
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
|
|
1903
|
+
// Also handle the case where user drags outside the textarea
|
|
1904
|
+
const handleGlobalMouseUp = () => {
|
|
1905
|
+
if (userIsInteracting) {
|
|
1906
|
+
handleMouseUp();
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
|
|
1910
|
+
// Add event listeners
|
|
1911
|
+
textarea.addEventListener('mousedown', handleMouseDown);
|
|
1912
|
+
textarea.addEventListener('mouseup', handleMouseUp);
|
|
1913
|
+
document.addEventListener('mouseup', handleGlobalMouseUp);
|
|
1914
|
+
|
|
1915
|
+
// Cleanup
|
|
1916
|
+
return () => {
|
|
1917
|
+
textarea.removeEventListener('mousedown', handleMouseDown);
|
|
1918
|
+
textarea.removeEventListener('mouseup', handleMouseUp);
|
|
1919
|
+
document.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
1920
|
+
};
|
|
1921
|
+
}, []);
|
|
1922
|
+
|
|
1923
|
+
// initial prompt change. Reset the chat history and get this response
|
|
1924
|
+
useEffect(() => {
|
|
1925
|
+
if (initialPrompt && initialPrompt !== "") {
|
|
1926
|
+
if (initialPrompt !== lastPrompt) {
|
|
1927
|
+
setIsLoading(true);
|
|
1928
|
+
|
|
1929
|
+
// Clear thinking blocks for new response
|
|
1930
|
+
setThinkingBlocks([]);
|
|
1931
|
+
setCurrentThinkingIndex(0);
|
|
1932
|
+
|
|
1933
|
+
ensureConversation().then((convId) => {
|
|
1934
|
+
if (lastController) stop(lastController);
|
|
1935
|
+
const controller = new AbortController();
|
|
1936
|
+
send(
|
|
1937
|
+
initialPrompt,
|
|
1938
|
+
messages,
|
|
1939
|
+
[
|
|
1940
|
+
...dataWithExtras(),
|
|
1941
|
+
{
|
|
1942
|
+
key: "--messages",
|
|
1943
|
+
data: messages.length.toString(),
|
|
1944
|
+
},
|
|
1945
|
+
],
|
|
1946
|
+
true,
|
|
1947
|
+
true,
|
|
1948
|
+
service,
|
|
1949
|
+
convId,
|
|
1950
|
+
controller
|
|
1951
|
+
);
|
|
1952
|
+
|
|
1953
|
+
// Store the context in component state
|
|
1954
|
+
setLastPrompt(initialPrompt);
|
|
1955
|
+
setLastMessages(messages);
|
|
1956
|
+
setLastKey(initialPrompt);
|
|
1957
|
+
setLastController(controller);
|
|
1958
|
+
setHistory({});
|
|
1959
|
+
|
|
1960
|
+
// Clear button registry for new conversation
|
|
1961
|
+
buttonActionRegistry.current.clear();
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}, [initialPrompt]);
|
|
1966
|
+
|
|
1967
|
+
useEffect(() => {
|
|
1968
|
+
if (scrollToEnd) {
|
|
1969
|
+
if (window.top !== window.self) {
|
|
1970
|
+
const responseArea = responseAreaRef.current as any;
|
|
1971
|
+
responseArea.scrollTo({
|
|
1972
|
+
top: responseArea.scrollHeight,
|
|
1973
|
+
behavior: "smooth",
|
|
1974
|
+
});
|
|
1975
|
+
} else {
|
|
1976
|
+
// If the ChatPanel is not within an iframe
|
|
1977
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
|
|
1978
|
+
}
|
|
1979
|
+
} else {
|
|
1980
|
+
const responseArea = responseAreaRef.current as any;
|
|
1981
|
+
if (responseArea) {
|
|
1982
|
+
setHasScroll(hasVerticalScrollbar(responseArea));
|
|
1983
|
+
const handleScroll = () => {
|
|
1984
|
+
const isScrolledToBottom =
|
|
1985
|
+
responseArea.scrollHeight - responseArea.scrollTop ===
|
|
1986
|
+
responseArea.clientHeight;
|
|
1987
|
+
setIsAtBottom(isScrolledToBottom);
|
|
1988
|
+
};
|
|
1989
|
+
handleScroll();
|
|
1990
|
+
responseArea.addEventListener("scroll", handleScroll);
|
|
1991
|
+
return () => responseArea.removeEventListener("scroll", handleScroll);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}, [response, history]);
|
|
1995
|
+
|
|
1996
|
+
// Use ref to avoid infinite loops from unstable callback references
|
|
1997
|
+
useEffect(() => {
|
|
1998
|
+
if (historyCallbackRef.current) {
|
|
1999
|
+
historyCallbackRef.current(history);
|
|
2000
|
+
}
|
|
2001
|
+
}, [history]);
|
|
2002
|
+
|
|
2003
|
+
useEffect(() => {
|
|
2004
|
+
//console.log("followOnPromptChanged detected");
|
|
2005
|
+
if (followOnPrompt && followOnPrompt !== "") {
|
|
2006
|
+
continueChat(followOnPrompt);
|
|
2007
|
+
}
|
|
2008
|
+
}, [followOnPrompt]);
|
|
2009
|
+
|
|
2010
|
+
const setCookie = (name: string, value: string, days: number = 30) => {
|
|
2011
|
+
const date = new Date();
|
|
2012
|
+
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
2013
|
+
const expires = `; expires=${date.toUTCString()}`;
|
|
2014
|
+
const sameSite = "; SameSite=Lax"; // Add SameSite attribute for security
|
|
2015
|
+
const secure = window.location.protocol === "https:" ? "; Secure" : "";
|
|
2016
|
+
document.cookie = `${name}=${value}${expires}; path=/${sameSite}${secure}`;
|
|
2017
|
+
};
|
|
2018
|
+
|
|
2019
|
+
const getCookie = (name: string): string | undefined => {
|
|
2020
|
+
const value = `; ${document.cookie}`;
|
|
2021
|
+
const parts = value.split(`; ${name}=`);
|
|
2022
|
+
if (parts.length === 2) {
|
|
2023
|
+
return parts.pop()?.split(";").shift();
|
|
2024
|
+
}
|
|
2025
|
+
return undefined;
|
|
2026
|
+
};
|
|
2027
|
+
|
|
2028
|
+
const ensureConversation = () => {
|
|
2029
|
+
if (
|
|
2030
|
+
(!currentConversation || currentConversation === "") &&
|
|
2031
|
+
createConversationOnFirstChat
|
|
2032
|
+
) {
|
|
2033
|
+
const browserInfo = getBrowserInfo();
|
|
2034
|
+
|
|
2035
|
+
return fetch(`${publicAPIUrl}/conversations`, {
|
|
2036
|
+
method: "POST",
|
|
2037
|
+
headers: {
|
|
2038
|
+
"Content-Type": "application/json",
|
|
2039
|
+
},
|
|
2040
|
+
body: JSON.stringify({
|
|
2041
|
+
project_id: project_id ?? "",
|
|
2042
|
+
agentId: agent,
|
|
2043
|
+
customerId: currentCustomer?.customer_id ?? null,
|
|
2044
|
+
customerEmail: currentCustomer?.customer_user_email ?? null,
|
|
2045
|
+
timezone: browserInfo?.userTimezone,
|
|
2046
|
+
language: browserInfo?.userLanguage,
|
|
2047
|
+
}),
|
|
2048
|
+
})
|
|
2049
|
+
.then(async (res) => {
|
|
2050
|
+
if (!res.ok) {
|
|
2051
|
+
const errorText = await res.text();
|
|
2052
|
+
throw new Error(
|
|
2053
|
+
`HTTP error! status: ${res.status}, message: ${errorText}`
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
return res.json();
|
|
2057
|
+
})
|
|
2058
|
+
.then((newConvo) => {
|
|
2059
|
+
if (newConvo?.id) {
|
|
2060
|
+
//console.log("new conversation created", newConvo.id);
|
|
2061
|
+
setCurrentConversation(newConvo.id);
|
|
2062
|
+
return newConvo.id;
|
|
2063
|
+
}
|
|
2064
|
+
return "";
|
|
2065
|
+
})
|
|
2066
|
+
.catch((error) => {
|
|
2067
|
+
console.error("Error creating new conversation", error);
|
|
2068
|
+
return "";
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
// If a currentConversation exists, return it in a resolved Promise.
|
|
2072
|
+
return Promise.resolve(currentConversation);
|
|
2073
|
+
};
|
|
2074
|
+
|
|
2075
|
+
useEffect(() => {
|
|
2076
|
+
const isEmpty = (value: string | undefined | null): boolean => {
|
|
2077
|
+
return !value || value.trim() === "" || value.trim() === "undefined";
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2080
|
+
// First, try to save any new values to cookies
|
|
2081
|
+
let updatedValues = { ...currentCustomer };
|
|
2082
|
+
let needsUpdate = false;
|
|
2083
|
+
|
|
2084
|
+
if (
|
|
2085
|
+
!isEmpty(currentCustomer?.customer_user_email) &&
|
|
2086
|
+
isEmailAddress(currentCustomer.customer_user_email ?? "")
|
|
2087
|
+
) {
|
|
2088
|
+
setCookie(
|
|
2089
|
+
"llmasaservice-panel-customer-user-email",
|
|
2090
|
+
currentCustomer?.customer_user_email ?? ""
|
|
2091
|
+
);
|
|
2092
|
+
// Only update email state if it's different from the cookie-derived value
|
|
2093
|
+
if (emailInput !== currentCustomer.customer_user_email) {
|
|
2094
|
+
setEmailInput(currentCustomer.customer_user_email ?? "");
|
|
2095
|
+
setEmailInputSet(true);
|
|
2096
|
+
setEmailValid(true);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
if (isEmpty(currentCustomer?.customer_user_email)) {
|
|
2101
|
+
const cookieEmail = getCookie("llmasaservice-panel-customer-user-email");
|
|
2102
|
+
if (!isEmpty(cookieEmail) && isEmailAddress(cookieEmail ?? "")) {
|
|
2103
|
+
updatedValues.customer_user_email = cookieEmail;
|
|
2104
|
+
needsUpdate = true;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// if the customer_id is not set, but the email is set, use the email as the customer_id
|
|
2109
|
+
if (
|
|
2110
|
+
isEmpty(currentCustomer?.customer_id) &&
|
|
2111
|
+
!isEmpty(updatedValues.customer_user_email) &&
|
|
2112
|
+
isEmailAddress(updatedValues.customer_user_email ?? "")
|
|
2113
|
+
) {
|
|
2114
|
+
updatedValues.customer_id = updatedValues.customer_user_email ?? "";
|
|
2115
|
+
needsUpdate = true;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// Only update state if the derived values are actually different
|
|
2119
|
+
if (
|
|
2120
|
+
needsUpdate &&
|
|
2121
|
+
(updatedValues.customer_id !== currentCustomer.customer_id ||
|
|
2122
|
+
updatedValues.customer_user_email !==
|
|
2123
|
+
currentCustomer.customer_user_email)
|
|
2124
|
+
) {
|
|
2125
|
+
// update the customer id and email in the conversation
|
|
2126
|
+
ensureConversation().then((convId) => {
|
|
2127
|
+
if (convId && convId !== "") {
|
|
2128
|
+
/*
|
|
2129
|
+
console.log(
|
|
2130
|
+
"updating conversation with customer id and email",
|
|
2131
|
+
convId,
|
|
2132
|
+
updatedValues
|
|
2133
|
+
);
|
|
2134
|
+
*/
|
|
2135
|
+
|
|
2136
|
+
fetch(`${publicAPIUrl}/conversations/${convId}`, {
|
|
2137
|
+
method: "POST",
|
|
2138
|
+
headers: {
|
|
2139
|
+
"Content-Type": "application/json",
|
|
2140
|
+
},
|
|
2141
|
+
body: JSON.stringify({
|
|
2142
|
+
project_id: project_id ?? "",
|
|
2143
|
+
customerId: updatedValues.customer_id,
|
|
2144
|
+
customerEmail: updatedValues.customer_user_email,
|
|
2145
|
+
}),
|
|
2146
|
+
}).catch((error) =>
|
|
2147
|
+
console.error("Failed to update conversation:", error)
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
|
|
2152
|
+
setCurrentCustomer(updatedValues);
|
|
2153
|
+
}
|
|
2154
|
+
}, [currentCustomer, project_id, agent, publicAPIUrl, emailInput]);
|
|
2155
|
+
|
|
2156
|
+
const continueChat = (suggestion?: string) => {
|
|
2157
|
+
//console.log("continueChat called");
|
|
2158
|
+
|
|
2159
|
+
// Clear thinking blocks for new response
|
|
2160
|
+
setThinkingBlocks([]);
|
|
2161
|
+
setCurrentThinkingIndex(0);
|
|
2162
|
+
|
|
2163
|
+
// Auto-set email if valid before proceeding
|
|
2164
|
+
if (emailInput && isEmailAddress(emailInput) && !emailInputSet) {
|
|
2165
|
+
const newId =
|
|
2166
|
+
currentCustomer?.customer_id &&
|
|
2167
|
+
currentCustomer.customer_id !== "" &&
|
|
2168
|
+
currentCustomer.customer_id !== currentCustomer?.customer_user_email
|
|
2169
|
+
? currentCustomer.customer_id
|
|
2170
|
+
: emailInput;
|
|
2171
|
+
|
|
2172
|
+
setEmailInputSet(true);
|
|
2173
|
+
setEmailValid(true);
|
|
2174
|
+
setCurrentCustomer({
|
|
2175
|
+
customer_id: newId,
|
|
2176
|
+
customer_user_email: emailInput,
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// wait till new conversation created....
|
|
2181
|
+
ensureConversation().then((convId) => {
|
|
2182
|
+
//console.log("current customer", currentCustomer);
|
|
2183
|
+
|
|
2184
|
+
if (!idle) {
|
|
2185
|
+
stop(lastController);
|
|
2186
|
+
|
|
2187
|
+
setHistory((prevHistory) => {
|
|
2188
|
+
return {
|
|
2189
|
+
...prevHistory,
|
|
2190
|
+
[lastKey ?? ""]: {
|
|
2191
|
+
content:
|
|
2192
|
+
processThinkingTags(response).cleanedText +
|
|
2193
|
+
"\n\n(response cancelled)",
|
|
2194
|
+
callId: lastCallId,
|
|
2195
|
+
},
|
|
2196
|
+
};
|
|
2197
|
+
});
|
|
2198
|
+
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
if (clearFollowOnQuestionsNextPrompt) {
|
|
2203
|
+
setFollowOnQuestionsState([]);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
if (
|
|
2207
|
+
(suggestion && suggestion !== "") ||
|
|
2208
|
+
(nextPrompt && nextPrompt !== "")
|
|
2209
|
+
) {
|
|
2210
|
+
setIsLoading(true);
|
|
2211
|
+
|
|
2212
|
+
// build the chat input from history
|
|
2213
|
+
// For new conversations (when history is empty), start with an empty array
|
|
2214
|
+
// to prevent carrying over previous conversation context
|
|
2215
|
+
const messagesAndHistory =
|
|
2216
|
+
Object.keys(history).length === 0 ? [] : messages;
|
|
2217
|
+
Object.entries(history).forEach(([prompt, response]) => {
|
|
2218
|
+
// Strip timestamp prefix from prompt before using it
|
|
2219
|
+
let promptToSend = prompt;
|
|
2220
|
+
if (prompt.includes(":")) {
|
|
2221
|
+
// Check if it starts with an ISO timestamp pattern
|
|
2222
|
+
const isoTimestampRegex =
|
|
2223
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z:/;
|
|
2224
|
+
if (isoTimestampRegex.test(prompt)) {
|
|
2225
|
+
const colonIndex = prompt.indexOf(":", 19); // Skip the colons in the timestamp part
|
|
2226
|
+
promptToSend = prompt.substring(colonIndex + 1);
|
|
2227
|
+
}
|
|
2228
|
+
// Also handle legacy numeric timestamps for backward compatibility
|
|
2229
|
+
else if (/^\d+:/.test(prompt)) {
|
|
2230
|
+
const colonIndex = prompt.indexOf(":");
|
|
2231
|
+
promptToSend = prompt.substring(colonIndex + 1);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
if (promptTemplate && promptTemplate !== "") {
|
|
2236
|
+
promptToSend = promptTemplate.replace("{{prompt}}", promptToSend);
|
|
2237
|
+
for (let i = 0; i < data.length; i++) {
|
|
2238
|
+
promptToSend = promptToSend.replace(
|
|
2239
|
+
"{{" + data[i]?.key + "}}",
|
|
2240
|
+
data[i]?.data ?? ""
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
messagesAndHistory.push({ role: "user", content: promptToSend });
|
|
2246
|
+
messagesAndHistory.push({
|
|
2247
|
+
role: "assistant",
|
|
2248
|
+
content: response.content,
|
|
2249
|
+
});
|
|
2250
|
+
});
|
|
2251
|
+
|
|
2252
|
+
let nextPromptToSend = suggestion ?? nextPrompt;
|
|
2253
|
+
|
|
2254
|
+
// Generate a unique key using ISO timestamp prefix + prompt content
|
|
2255
|
+
// This ensures chronological order and virtually guarantees uniqueness
|
|
2256
|
+
const timestamp = new Date().toISOString();
|
|
2257
|
+
let promptKey = `${timestamp}:${nextPromptToSend ?? ""}`;
|
|
2258
|
+
|
|
2259
|
+
// set the history prompt with the about to be sent prompt
|
|
2260
|
+
setHistory((prevHistory) => {
|
|
2261
|
+
return {
|
|
2262
|
+
...prevHistory,
|
|
2263
|
+
[promptKey ?? ""]: { content: "", callId: "" },
|
|
2264
|
+
};
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
// if this is the first user message, use the template. otherwise it is a follow-on question(s)
|
|
2268
|
+
if (
|
|
2269
|
+
(initialPrompt &&
|
|
2270
|
+
initialPrompt !== "" &&
|
|
2271
|
+
Object.keys(history).length === 1) ||
|
|
2272
|
+
((!initialPrompt || initialPrompt === "") &&
|
|
2273
|
+
Object.keys(history).length === 0)
|
|
2274
|
+
) {
|
|
2275
|
+
if (promptTemplate && promptTemplate !== "") {
|
|
2276
|
+
nextPromptToSend = promptTemplate.replace(
|
|
2277
|
+
"{{prompt}}",
|
|
2278
|
+
nextPromptToSend
|
|
2279
|
+
);
|
|
2280
|
+
for (let i = 0; i < data.length; i++) {
|
|
2281
|
+
nextPromptToSend = nextPromptToSend.replace(
|
|
2282
|
+
"{{" + data[i]?.key + "}}",
|
|
2283
|
+
data[i]?.data ?? ""
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
//console.log("Sending for conversation", convId);
|
|
2290
|
+
|
|
2291
|
+
const controller = new AbortController();
|
|
2292
|
+
send(
|
|
2293
|
+
nextPromptToSend,
|
|
2294
|
+
messagesAndHistory,
|
|
2295
|
+
[
|
|
2296
|
+
...dataWithExtras(),
|
|
2297
|
+
{
|
|
2298
|
+
key: "--messages",
|
|
2299
|
+
data: messagesAndHistory.length.toString(),
|
|
2300
|
+
},
|
|
2301
|
+
],
|
|
2302
|
+
true,
|
|
2303
|
+
true,
|
|
2304
|
+
service,
|
|
2305
|
+
convId,
|
|
2306
|
+
controller
|
|
2307
|
+
);
|
|
2308
|
+
|
|
2309
|
+
setLastPrompt(nextPromptToSend);
|
|
2310
|
+
setLastMessages(messagesAndHistory);
|
|
2311
|
+
setLastKey(promptKey);
|
|
2312
|
+
setLastController(controller);
|
|
2313
|
+
setNextPrompt("");
|
|
2314
|
+
}
|
|
2315
|
+
});
|
|
2316
|
+
};
|
|
2317
|
+
|
|
2318
|
+
const replaceHistory = (newHistory: {
|
|
2319
|
+
[prompt: string]: { content: string; callId: string };
|
|
2320
|
+
}) => {
|
|
2321
|
+
setHistory(newHistory);
|
|
2322
|
+
};
|
|
2323
|
+
|
|
2324
|
+
function copyToClipboard(text: string) {
|
|
2325
|
+
navigator.clipboard.writeText(text);
|
|
2326
|
+
interactionClicked(lastCallId, "copy");
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
const scrollToBottom = () => {
|
|
2330
|
+
if (window.top !== window.self) {
|
|
2331
|
+
const responseArea = responseAreaRef.current as any;
|
|
2332
|
+
responseArea.scrollTo({
|
|
2333
|
+
top: responseArea.scrollHeight,
|
|
2334
|
+
behavior: "smooth",
|
|
2335
|
+
});
|
|
2336
|
+
} else {
|
|
2337
|
+
// If the ChatPanel is not within an iframe
|
|
2338
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
|
|
2342
|
+
const CodeBlock = ({ node, className, children, style, ...props }: any) => {
|
|
2343
|
+
const match = /language-(\w+)/.exec(className || "");
|
|
2344
|
+
|
|
2345
|
+
return match ? (
|
|
2346
|
+
<>
|
|
2347
|
+
<div
|
|
2348
|
+
style={{
|
|
2349
|
+
border: 0,
|
|
2350
|
+
padding: 0,
|
|
2351
|
+
height: "16px",
|
|
2352
|
+
display: "flex",
|
|
2353
|
+
justifyContent: "space-between",
|
|
2354
|
+
alignItems: "center",
|
|
2355
|
+
}}
|
|
2356
|
+
>
|
|
2357
|
+
<span>{match ? match[1] : "Unknown"}</span>
|
|
2358
|
+
<button
|
|
2359
|
+
onClick={() => copyToClipboard(children)}
|
|
2360
|
+
className="copy-button"
|
|
2361
|
+
>
|
|
2362
|
+
<svg
|
|
2363
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2364
|
+
viewBox="0 0 320 320"
|
|
2365
|
+
fill="currentColor"
|
|
2366
|
+
className="icon-svg"
|
|
2367
|
+
>
|
|
2368
|
+
<path
|
|
2369
|
+
d="M35,270h45v45c0,8.284,6.716,15,15,15h200c8.284,0,15-6.716,15-15V75c0-8.284-6.716-15-15-15h-45V15
|
|
2370
|
+
c0-8.284-6.716-15-15-15H35c-8.284,0-15,6.716-15,15v240C20,263.284,26.716,270,35,270z M280,300H110V90h170V300z M50,30h170v30H95
|
|
2371
|
+
c-8.284,0-15,6.716-15,15v165H50V30z"
|
|
2372
|
+
/>
|
|
2373
|
+
<path d="M155,120c-8.284,0-15,6.716-15,15s6.716,15,15,15h80c8.284,0,15-6.716,15-15s-6.716-15-15-15H155z" />
|
|
2374
|
+
<path d="M235,180h-80c-8.284,0-15,6.716-15,15s6.716,15,15,15h80c8.284,0,15-6.716,15-15S243.284,180,235,180z" />
|
|
2375
|
+
<path
|
|
2376
|
+
d="M235,240h-80c-8.284,0-15,6.716-15,15c0,8.284,6.716,15,15,15h80c8.284,0,15-6.716,15-15C250,246.716,243.284,240,235,240z
|
|
2377
|
+
"
|
|
2378
|
+
/>
|
|
2379
|
+
</svg>
|
|
2380
|
+
</button>
|
|
2381
|
+
</div>
|
|
2382
|
+
<SyntaxHighlighter
|
|
2383
|
+
style={prismStyle}
|
|
2384
|
+
PreTag="div"
|
|
2385
|
+
language={match[1]}
|
|
2386
|
+
{...props}
|
|
2387
|
+
>
|
|
2388
|
+
{String(children).replace(/\n$/, "")}
|
|
2389
|
+
</SyntaxHighlighter>
|
|
2390
|
+
</>
|
|
2391
|
+
) : (
|
|
2392
|
+
<code className={className ? className : ""} {...props}>
|
|
2393
|
+
{children}
|
|
2394
|
+
</code>
|
|
2395
|
+
);
|
|
2396
|
+
};
|
|
2397
|
+
|
|
2398
|
+
// links should always open in a new tab
|
|
2399
|
+
const CustomLink = ({ href, children, ...props }: any) => {
|
|
2400
|
+
return (
|
|
2401
|
+
<a href={href} target="_blank" rel="noopener noreferrer" {...props}>
|
|
2402
|
+
{children}
|
|
2403
|
+
</a>
|
|
2404
|
+
);
|
|
2405
|
+
};
|
|
2406
|
+
|
|
2407
|
+
const convertMarkdownToHTML = (markdown: string): string => {
|
|
2408
|
+
const html = ReactDOMServer.renderToStaticMarkup(
|
|
2409
|
+
<ReactMarkdown
|
|
2410
|
+
className={markdownClass}
|
|
2411
|
+
remarkPlugins={[remarkGfm]}
|
|
2412
|
+
rehypePlugins={[rehypeRaw]}
|
|
2413
|
+
>
|
|
2414
|
+
{markdown}
|
|
2415
|
+
</ReactMarkdown>
|
|
2416
|
+
);
|
|
2417
|
+
return html;
|
|
2418
|
+
};
|
|
2419
|
+
|
|
2420
|
+
const convertHistoryToHTML = (history: {
|
|
2421
|
+
[prompt: string]: { callId: string; content: string };
|
|
2422
|
+
}): string => {
|
|
2423
|
+
const stylesheet = `
|
|
2424
|
+
<style>
|
|
2425
|
+
.conversation-history {
|
|
2426
|
+
font-family: Arial, sans-serif;
|
|
2427
|
+
line-height: 1.5; /* Slightly increase line height for readability */
|
|
2428
|
+
}
|
|
2429
|
+
.history-entry {
|
|
2430
|
+
margin-bottom: 15px;
|
|
2431
|
+
}
|
|
2432
|
+
.prompt-container, .response-container {
|
|
2433
|
+
margin-bottom: 10px; /* Adjusted spacing */
|
|
2434
|
+
}
|
|
2435
|
+
.prompt, .response {
|
|
2436
|
+
display: block; /* Ensure they take up the full row */
|
|
2437
|
+
margin: 5px 0; /* Add vertical spacing */
|
|
2438
|
+
padding: 10px; /* Increase padding for better spacing */
|
|
2439
|
+
border-radius: 5px;
|
|
2440
|
+
max-width: 80%; /* Keep width constrained */
|
|
2441
|
+
}
|
|
2442
|
+
.prompt {
|
|
2443
|
+
background-color: #efefef;
|
|
2444
|
+
margin-left: 0; /* Align to the left */
|
|
2445
|
+
}
|
|
2446
|
+
.response {
|
|
2447
|
+
background-color: #f0fcfd;
|
|
2448
|
+
margin-left: 25px; /* Indent slightly for visual differentiation */
|
|
2449
|
+
}
|
|
2450
|
+
</style>
|
|
2451
|
+
`;
|
|
2452
|
+
|
|
2453
|
+
let html = `
|
|
2454
|
+
<html>
|
|
2455
|
+
<head>
|
|
2456
|
+
${stylesheet}
|
|
2457
|
+
</head>
|
|
2458
|
+
<body>
|
|
2459
|
+
<h1>Conversation History (${new Date().toLocaleString()})</h1>
|
|
2460
|
+
<div class="conversation-history">
|
|
2461
|
+
`;
|
|
2462
|
+
|
|
2463
|
+
Object.entries(history).forEach(([prompt, response], index) => {
|
|
2464
|
+
if (hideInitialPrompt && index === 0) {
|
|
2465
|
+
html += `
|
|
2466
|
+
<div class="history-entry">
|
|
2467
|
+
<div class="response-container">
|
|
2468
|
+
<div class="response">${convertMarkdownToHTML(
|
|
2469
|
+
response.content
|
|
2470
|
+
)}</div>
|
|
2471
|
+
</div>
|
|
2472
|
+
</div>
|
|
2473
|
+
`;
|
|
2474
|
+
} else {
|
|
2475
|
+
html += `
|
|
2476
|
+
<div class="history-entry">
|
|
2477
|
+
<div class="prompt-container">
|
|
2478
|
+
<div class="prompt">${convertMarkdownToHTML(
|
|
2479
|
+
formatPromptForDisplay(prompt)
|
|
2480
|
+
)}</div>
|
|
2481
|
+
</div>
|
|
2482
|
+
<div class="response-container">
|
|
2483
|
+
<div class="response">${convertMarkdownToHTML(response.content)}</div>
|
|
2484
|
+
</div>
|
|
2485
|
+
</div>
|
|
2486
|
+
`;
|
|
2487
|
+
}
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
html += `
|
|
2491
|
+
</div>
|
|
2492
|
+
</body>
|
|
2493
|
+
</html>
|
|
2494
|
+
`;
|
|
2495
|
+
|
|
2496
|
+
return html;
|
|
2497
|
+
};
|
|
2498
|
+
|
|
2499
|
+
const saveHTMLToFile = (html: string, filename: string) => {
|
|
2500
|
+
const blob = new Blob([html], { type: "text/html" });
|
|
2501
|
+
const link = document.createElement("a");
|
|
2502
|
+
link.href = URL.createObjectURL(blob);
|
|
2503
|
+
link.download = filename;
|
|
2504
|
+
document.body.appendChild(link);
|
|
2505
|
+
link.click();
|
|
2506
|
+
document.body.removeChild(link);
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
const handleSendEmail = (to: string, from: string) => {
|
|
2510
|
+
sendConversationsViaEmail(to, `Conversation History from ${title}`, from);
|
|
2511
|
+
interactionClicked(lastCallId, "email", to);
|
|
2512
|
+
setEmailSent(true);
|
|
2513
|
+
};
|
|
2514
|
+
|
|
2515
|
+
const handleSuggestionClick = async (suggestion: string) => {
|
|
2516
|
+
continueChat(suggestion);
|
|
2517
|
+
await interactionClicked(lastCallId, "suggestion");
|
|
2518
|
+
};
|
|
2519
|
+
|
|
2520
|
+
const sendConversationsViaEmail = async (
|
|
2521
|
+
to: string,
|
|
2522
|
+
subject: string = `Conversation History from ${title}`,
|
|
2523
|
+
from: string = ""
|
|
2524
|
+
) => {
|
|
2525
|
+
fetch(`${publicAPIUrl}/share/email`, {
|
|
2526
|
+
method: "POST",
|
|
2527
|
+
headers: {
|
|
2528
|
+
"Content-Type": "application/json",
|
|
2529
|
+
},
|
|
2530
|
+
body: JSON.stringify({
|
|
2531
|
+
to: to,
|
|
2532
|
+
from: from,
|
|
2533
|
+
subject: subject,
|
|
2534
|
+
html: convertHistoryToHTML(history),
|
|
2535
|
+
project_id: project_id ?? "",
|
|
2536
|
+
customer: currentCustomer,
|
|
2537
|
+
history: history,
|
|
2538
|
+
title: title,
|
|
2539
|
+
}),
|
|
2540
|
+
});
|
|
2541
|
+
|
|
2542
|
+
await interactionClicked(lastCallId, "email", from);
|
|
2543
|
+
};
|
|
2544
|
+
|
|
2545
|
+
const sendCallToActionEmail = async (from: string) => {
|
|
2546
|
+
const r = await fetch(`${publicAPIUrl}/share/email`, {
|
|
2547
|
+
method: "POST",
|
|
2548
|
+
headers: {
|
|
2549
|
+
"Content-Type": "application/json",
|
|
2550
|
+
},
|
|
2551
|
+
body: JSON.stringify({
|
|
2552
|
+
to: callToActionEmailAddress,
|
|
2553
|
+
from: from,
|
|
2554
|
+
subject: `${callToActionEmailSubject} from ${from}`,
|
|
2555
|
+
html: convertHistoryToHTML(history),
|
|
2556
|
+
project_id: project_id ?? "",
|
|
2557
|
+
customer: currentCustomer,
|
|
2558
|
+
history: history,
|
|
2559
|
+
title: title,
|
|
2560
|
+
}),
|
|
2561
|
+
});
|
|
2562
|
+
|
|
2563
|
+
await interactionClicked(lastCallId, "cta", from);
|
|
2564
|
+
|
|
2565
|
+
setCallToActionSent(true);
|
|
2566
|
+
};
|
|
2567
|
+
|
|
2568
|
+
const interactionClicked = async (
|
|
2569
|
+
callId: string,
|
|
2570
|
+
action: string,
|
|
2571
|
+
emailaddress: string = "",
|
|
2572
|
+
comment: string = ""
|
|
2573
|
+
) => {
|
|
2574
|
+
console.log(`Interaction clicked: ${action} for callId: ${callId}`);
|
|
2575
|
+
|
|
2576
|
+
ensureConversation().then((convId) => {
|
|
2577
|
+
// special case where no call made yet, and they click on Suggestion/CTA/Email/Save
|
|
2578
|
+
if (!callId || callId === "") callId = convId;
|
|
2579
|
+
|
|
2580
|
+
const email =
|
|
2581
|
+
emailaddress && emailaddress !== ""
|
|
2582
|
+
? emailaddress
|
|
2583
|
+
: isEmailAddress(currentCustomer?.customer_user_email ?? "")
|
|
2584
|
+
? currentCustomer?.customer_user_email
|
|
2585
|
+
: isEmailAddress(currentCustomer?.customer_id ?? "")
|
|
2586
|
+
? currentCustomer?.customer_id
|
|
2587
|
+
: "";
|
|
2588
|
+
|
|
2589
|
+
fetch(`${publicAPIUrl}/feedback/${callId}/${action}`, {
|
|
2590
|
+
method: "POST",
|
|
2591
|
+
headers: {
|
|
2592
|
+
"Content-Type": "application/json",
|
|
2593
|
+
},
|
|
2594
|
+
body: JSON.stringify({
|
|
2595
|
+
project_id: project_id ?? "",
|
|
2596
|
+
conversation_id: convId ?? "",
|
|
2597
|
+
email: email,
|
|
2598
|
+
comment: comment,
|
|
2599
|
+
}),
|
|
2600
|
+
});
|
|
2601
|
+
});
|
|
2602
|
+
};
|
|
2603
|
+
|
|
2604
|
+
const formatPromptForDisplay = (prompt: string): string => {
|
|
2605
|
+
if (!prompt) {
|
|
2606
|
+
return "";
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// Strip timestamp prefix if present (format: "ISO_timestamp:prompt")
|
|
2610
|
+
let cleanedPrompt = prompt;
|
|
2611
|
+
if (prompt.includes(":")) {
|
|
2612
|
+
// Check if it starts with an ISO timestamp pattern (YYYY-MM-DDTHH:mm:ss.sssZ)
|
|
2613
|
+
const isoTimestampRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z:/;
|
|
2614
|
+
if (isoTimestampRegex.test(prompt)) {
|
|
2615
|
+
const colonIndex = prompt.indexOf(":", 19); // Skip the colons in the timestamp part
|
|
2616
|
+
cleanedPrompt = prompt.substring(colonIndex + 1);
|
|
2617
|
+
}
|
|
2618
|
+
// Also handle legacy numeric timestamps for backward compatibility
|
|
2619
|
+
else if (/^\d+:/.test(prompt)) {
|
|
2620
|
+
const colonIndex = prompt.indexOf(":");
|
|
2621
|
+
cleanedPrompt = prompt.substring(colonIndex + 1);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
if (hideRagContextInPrompt && cleanedPrompt.includes("CONTEXT:")) {
|
|
2626
|
+
const parts = cleanedPrompt.split("CONTEXT:");
|
|
2627
|
+
const withoutContext = parts.length > 0 ? (parts[0] as string) : "";
|
|
2628
|
+
|
|
2629
|
+
// Remove the optional chaining since withoutContext is always a string
|
|
2630
|
+
if (withoutContext.includes("PROMPT:")) {
|
|
2631
|
+
const promptParts = withoutContext.split("PROMPT:");
|
|
2632
|
+
return promptParts.length > 1
|
|
2633
|
+
? (promptParts[1] || "").trim()
|
|
2634
|
+
: withoutContext.trim();
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
return withoutContext.trim();
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
return cleanedPrompt;
|
|
2641
|
+
};
|
|
2642
|
+
|
|
2643
|
+
const isDisabledDueToNoEmail = () => {
|
|
2644
|
+
const valid = isEmailAddress(emailInput);
|
|
2645
|
+
if (valid) return false;
|
|
2646
|
+
if (customerEmailCaptureMode === "REQUIRED") return true;
|
|
2647
|
+
return false;
|
|
2648
|
+
};
|
|
2649
|
+
|
|
2650
|
+
// helper to dedupe tool names
|
|
2651
|
+
const getUniqueToolNames = (reqs: typeof pendingToolRequests) =>
|
|
2652
|
+
Array.from(new Set(reqs.map((r) => r.toolName)));
|
|
2653
|
+
|
|
2654
|
+
// called by each button
|
|
2655
|
+
const handleToolApproval = (
|
|
2656
|
+
toolName: string,
|
|
2657
|
+
scope: "once" | "session" | "always"
|
|
2658
|
+
) => {
|
|
2659
|
+
if (scope === "session" || scope === "always") {
|
|
2660
|
+
setSessionApprovedTools((p) => Array.from(new Set([...p, toolName])));
|
|
2661
|
+
}
|
|
2662
|
+
if (scope === "always") {
|
|
2663
|
+
setAlwaysApprovedTools((p) => Array.from(new Set([...p, toolName])));
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// process and remove just this tool’s calls
|
|
2667
|
+
const requestsToRun = pendingToolRequests.filter(
|
|
2668
|
+
(r) => r.toolName === toolName
|
|
2669
|
+
);
|
|
2670
|
+
processGivenToolRequests(requestsToRun);
|
|
2671
|
+
setPendingToolRequests((p) => p.filter((r) => r.toolName !== toolName));
|
|
2672
|
+
};
|
|
2673
|
+
|
|
2674
|
+
// auto‐process pending tools that were previously approved (session or always)
|
|
2675
|
+
useEffect(() => {
|
|
2676
|
+
if (pendingToolRequests.length === 0) return;
|
|
2677
|
+
const toAuto = pendingToolRequests.filter(
|
|
2678
|
+
(r) =>
|
|
2679
|
+
sessionApprovedTools.includes(r.toolName) ||
|
|
2680
|
+
alwaysApprovedTools.includes(r.toolName)
|
|
2681
|
+
);
|
|
2682
|
+
if (toAuto.length > 0) {
|
|
2683
|
+
processGivenToolRequests(toAuto);
|
|
2684
|
+
setPendingToolRequests((prev) =>
|
|
2685
|
+
prev.filter(
|
|
2686
|
+
(r) =>
|
|
2687
|
+
!sessionApprovedTools.includes(r.toolName) &&
|
|
2688
|
+
!alwaysApprovedTools.includes(r.toolName)
|
|
2689
|
+
)
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
}, [pendingToolRequests, sessionApprovedTools, alwaysApprovedTools]);
|
|
2693
|
+
|
|
2694
|
+
return (
|
|
2695
|
+
<>
|
|
2696
|
+
<div
|
|
2697
|
+
style={{ width: width, height: height }}
|
|
2698
|
+
className={"llm-panel" + (theme === "light" ? "" : " dark-theme")}
|
|
2699
|
+
>
|
|
2700
|
+
{title && title !== "" ? <div className="title">{title}</div> : null}
|
|
2701
|
+
<div className="responseArea" ref={responseAreaRef}>
|
|
2702
|
+
{initialMessage && initialMessage !== "" ? (
|
|
2703
|
+
<div className="history-entry">
|
|
2704
|
+
<div className="response">
|
|
2705
|
+
<ReactMarkdown
|
|
2706
|
+
className={markdownClass}
|
|
2707
|
+
remarkPlugins={[remarkGfm]}
|
|
2708
|
+
rehypePlugins={[rehypeRaw]}
|
|
2709
|
+
>
|
|
2710
|
+
{initialMessage}
|
|
2711
|
+
</ReactMarkdown>
|
|
2712
|
+
</div>
|
|
2713
|
+
</div>
|
|
2714
|
+
) : null}
|
|
2715
|
+
|
|
2716
|
+
{Object.entries(history).map(([prompt, historyEntry], index) => {
|
|
2717
|
+
const isLastEntry = index === Object.keys(history).length - 1;
|
|
2718
|
+
const hasToolData = !!(
|
|
2719
|
+
(historyEntry?.toolCalls?.length || 0) > 0 ||
|
|
2720
|
+
(historyEntry?.toolResponses?.length || 0) > 0
|
|
2721
|
+
);
|
|
2722
|
+
|
|
2723
|
+
return (
|
|
2724
|
+
<div className="history-entry" key={index}>
|
|
2725
|
+
{hideInitialPrompt && index === 0 ? null : (
|
|
2726
|
+
<div className="prompt">{formatPromptForDisplay(prompt)}</div>
|
|
2727
|
+
)}
|
|
2728
|
+
|
|
2729
|
+
<div className="response">
|
|
2730
|
+
{/* Show streaming response with thinking blocks displayed separately */}
|
|
2731
|
+
{index === Object.keys(history).length - 1 &&
|
|
2732
|
+
(isLoading || !idle) &&
|
|
2733
|
+
!justReset ? (
|
|
2734
|
+
<div className="streaming-response">
|
|
2735
|
+
{/* Display current thinking block or thinking message */}
|
|
2736
|
+
{(() => {
|
|
2737
|
+
const { cleanedText } = processThinkingTags(
|
|
2738
|
+
response || ""
|
|
2739
|
+
);
|
|
2740
|
+
|
|
2741
|
+
// If we have thinking blocks, show the current one
|
|
2742
|
+
if (thinkingBlocks.length > 0) {
|
|
2743
|
+
const isOnLastBlock =
|
|
2744
|
+
currentThinkingIndex === thinkingBlocks.length - 1;
|
|
2745
|
+
const hasMainContent =
|
|
2746
|
+
cleanedText && cleanedText.trim().length > 0;
|
|
2747
|
+
const shouldShowLoading =
|
|
2748
|
+
isOnLastBlock && !hasMainContent;
|
|
2749
|
+
|
|
2750
|
+
return (
|
|
2751
|
+
<div>
|
|
2752
|
+
{renderThinkingBlocks()}
|
|
2753
|
+
{/* Show animated thinking if we're showing the last block and no main content yet */}
|
|
2754
|
+
{shouldShowLoading && (
|
|
2755
|
+
<div className="loading-text">
|
|
2756
|
+
Thinking...
|
|
2757
|
+
<div className="dot"></div>
|
|
2758
|
+
<div className="dot"></div>
|
|
2759
|
+
<div className="dot"></div>
|
|
2760
|
+
</div>
|
|
2761
|
+
)}
|
|
2762
|
+
</div>
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// If no thinking blocks yet but no main content, show generic thinking
|
|
2767
|
+
if (!cleanedText || cleanedText.length === 0) {
|
|
2768
|
+
return (
|
|
2769
|
+
<div className="loading-text">
|
|
2770
|
+
Thinking...
|
|
2771
|
+
<div className="dot"></div>
|
|
2772
|
+
<div className="dot"></div>
|
|
2773
|
+
<div className="dot"></div>
|
|
2774
|
+
</div>
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
return null;
|
|
2779
|
+
})()}
|
|
2780
|
+
|
|
2781
|
+
{/* Display the main content (processed with actions) */}
|
|
2782
|
+
{(() => {
|
|
2783
|
+
// Get the processed content that includes action buttons from history
|
|
2784
|
+
// During streaming, use the most recent history entry if it exists
|
|
2785
|
+
if (lastKey && history[lastKey] && history[lastKey].content) {
|
|
2786
|
+
return (
|
|
2787
|
+
<ReactMarkdown
|
|
2788
|
+
className={markdownClass}
|
|
2789
|
+
remarkPlugins={[remarkGfm]}
|
|
2790
|
+
rehypePlugins={[rehypeRaw]}
|
|
2791
|
+
components={{ /*a: CustomLink,*/ code: CodeBlock }}
|
|
2792
|
+
>
|
|
2793
|
+
{history[lastKey].content}
|
|
2794
|
+
</ReactMarkdown>
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// Fallback to cleaned text if no processed history exists yet
|
|
2799
|
+
const { cleanedText } = processThinkingTags(
|
|
2800
|
+
response || ""
|
|
2801
|
+
);
|
|
2802
|
+
return cleanedText && cleanedText.length > 0 ? (
|
|
2803
|
+
<ReactMarkdown
|
|
2804
|
+
className={markdownClass}
|
|
2805
|
+
remarkPlugins={[remarkGfm]}
|
|
2806
|
+
rehypePlugins={[rehypeRaw]}
|
|
2807
|
+
components={{ /*a: CustomLink,*/ code: CodeBlock }}
|
|
2808
|
+
>
|
|
2809
|
+
{cleanedText}
|
|
2810
|
+
</ReactMarkdown>
|
|
2811
|
+
) : null;
|
|
2812
|
+
})()}
|
|
2813
|
+
</div>
|
|
2814
|
+
) : (
|
|
2815
|
+
<div>
|
|
2816
|
+
{/* For completed responses, show stored thinking blocks if this is the last entry */}
|
|
2817
|
+
{isLastEntry &&
|
|
2818
|
+
thinkingBlocks.length > 0 &&
|
|
2819
|
+
renderThinkingBlocks()}
|
|
2820
|
+
|
|
2821
|
+
{/* Show the main content (cleaned of thinking tags) */}
|
|
2822
|
+
<ReactMarkdown
|
|
2823
|
+
className={markdownClass}
|
|
2824
|
+
remarkPlugins={[remarkGfm]}
|
|
2825
|
+
rehypePlugins={[rehypeRaw]}
|
|
2826
|
+
components={{ /*a: CustomLink,*/ code: CodeBlock }}
|
|
2827
|
+
>
|
|
2828
|
+
{processThinkingTags(historyEntry.content).cleanedText}
|
|
2829
|
+
</ReactMarkdown>
|
|
2830
|
+
</div>
|
|
2831
|
+
)}
|
|
2832
|
+
|
|
2833
|
+
{isLastEntry && pendingToolRequests.length > 0 && (
|
|
2834
|
+
<div className="approve-tools-panel">
|
|
2835
|
+
{getUniqueToolNames(pendingToolRequests).map(
|
|
2836
|
+
(toolName) => {
|
|
2837
|
+
const tool = toolList.find(
|
|
2838
|
+
(t) => t.name === toolName
|
|
2839
|
+
);
|
|
2840
|
+
return (
|
|
2841
|
+
<div key={toolName} className="approve-tool-item">
|
|
2842
|
+
<div className="approve-tools-header">
|
|
2843
|
+
Tool “{toolName}” requires approval <br />
|
|
2844
|
+
</div>
|
|
2845
|
+
|
|
2846
|
+
<div className="approve-tools-buttons">
|
|
2847
|
+
<button
|
|
2848
|
+
className="approve-tools-button"
|
|
2849
|
+
onClick={() =>
|
|
2850
|
+
handleToolApproval(toolName, "once")
|
|
2851
|
+
}
|
|
2852
|
+
disabled={isLoading}
|
|
2853
|
+
>
|
|
2854
|
+
Approve Once
|
|
2855
|
+
</button>
|
|
2856
|
+
<button
|
|
2857
|
+
className="approve-tools-button"
|
|
2858
|
+
onClick={() =>
|
|
2859
|
+
handleToolApproval(toolName, "session")
|
|
2860
|
+
}
|
|
2861
|
+
disabled={isLoading}
|
|
2862
|
+
>
|
|
2863
|
+
Approve This Chat
|
|
2864
|
+
</button>
|
|
2865
|
+
<button
|
|
2866
|
+
className="approve-tools-button"
|
|
2867
|
+
onClick={() =>
|
|
2868
|
+
handleToolApproval(toolName, "always")
|
|
2869
|
+
}
|
|
2870
|
+
disabled={isLoading}
|
|
2871
|
+
>
|
|
2872
|
+
Approve Always
|
|
2873
|
+
</button>
|
|
2874
|
+
<br />
|
|
2875
|
+
</div>
|
|
2876
|
+
|
|
2877
|
+
{tool?.description && (
|
|
2878
|
+
<div className="approve-tools-description">
|
|
2879
|
+
{tool.description}
|
|
2880
|
+
</div>
|
|
2881
|
+
)}
|
|
2882
|
+
</div>
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
)}
|
|
2886
|
+
</div>
|
|
2887
|
+
)}
|
|
2888
|
+
|
|
2889
|
+
{idle && !isLoading && pendingToolRequests.length === 0 && (
|
|
2890
|
+
<div className="button-container">
|
|
2891
|
+
<button
|
|
2892
|
+
className="copy-button"
|
|
2893
|
+
onClick={() => {
|
|
2894
|
+
copyToClipboard(historyEntry.content);
|
|
2895
|
+
}}
|
|
2896
|
+
disabled={isDisabledDueToNoEmail()}
|
|
2897
|
+
>
|
|
2898
|
+
<svg
|
|
2899
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2900
|
+
viewBox="0 0 320 320"
|
|
2901
|
+
fill="currentColor"
|
|
2902
|
+
className="icon-svg"
|
|
2903
|
+
>
|
|
2904
|
+
<path
|
|
2905
|
+
d="M35,270h45v45c0,8.284,6.716,15,15,15h200c8.284,0,15-6.716,15-15V75c0-8.284-6.716-15-15-15h-45V15
|
|
2906
|
+
c0-8.284-6.716-15-15-15H35c-8.284,0-15,6.716-15,15v240C20,263.284,26.716,270,35,270z M280,300H110V90h170V300z M50,30h170v30H95
|
|
2907
|
+
c-8.284,0-15,6.716-15,15v165H50V30z"
|
|
2908
|
+
/>
|
|
2909
|
+
<path d="M155,120c-8.284,0-15,6.716-15,15s6.716,15,15,15h80c8.284,0,15-6.716,15-15s-6.716-15-15-15H155z" />
|
|
2910
|
+
<path d="M235,180h-80c-8.284,0-15,6.716-15,15s6.716,15,15,15h80c8.284,0,15-6.716,15-15S243.284,180,235,180z" />
|
|
2911
|
+
<path
|
|
2912
|
+
d="M235,240h-80c-8.284,0-15,6.716-15,15c0,8.284,6.716,15,15,15h80c8.284,0,15-6.716,15-15C250,246.716,243.284,240,235,240z
|
|
2913
|
+
"
|
|
2914
|
+
/>
|
|
2915
|
+
</svg>
|
|
2916
|
+
</button>
|
|
2917
|
+
|
|
2918
|
+
<button
|
|
2919
|
+
className="thumbs-button"
|
|
2920
|
+
onClick={() => {
|
|
2921
|
+
if (thumbsUpClick) thumbsUpClick(historyEntry.callId);
|
|
2922
|
+
interactionClicked(historyEntry.callId, "thumbsup");
|
|
2923
|
+
}}
|
|
2924
|
+
disabled={isDisabledDueToNoEmail()}
|
|
2925
|
+
>
|
|
2926
|
+
<svg
|
|
2927
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2928
|
+
viewBox="0 0 20 20"
|
|
2929
|
+
fill="currentColor"
|
|
2930
|
+
className="icon-svg"
|
|
2931
|
+
>
|
|
2932
|
+
<path d="M20.22 9.55C19.79 9.04 19.17 8.75 18.5 8.75H14.47V6C14.47 4.48 13.24 3.25 11.64 3.25C10.94 3.25 10.31 3.67 10.03 4.32L7.49 10.25H5.62C4.31 10.25 3.25 11.31 3.25 12.62V18.39C3.25 19.69 4.32 20.75 5.62 20.75H17.18C18.27 20.75 19.2 19.97 19.39 18.89L20.71 11.39C20.82 10.73 20.64 10.06 20.21 9.55H20.22ZM5.62 19.25C5.14 19.25 4.75 18.86 4.75 18.39V12.62C4.75 12.14 5.14 11.75 5.62 11.75H7.23V19.25H5.62ZM17.92 18.63C17.86 18.99 17.55 19.25 17.18 19.25H8.74V11.15L11.41 4.9C11.45 4.81 11.54 4.74 11.73 4.74C12.42 4.74 12.97 5.3 12.97 5.99V10.24H18.5C18.73 10.24 18.93 10.33 19.07 10.5C19.21 10.67 19.27 10.89 19.23 11.12L17.91 18.62L17.92 18.63Z" />
|
|
2933
|
+
</svg>
|
|
2934
|
+
</button>
|
|
2935
|
+
|
|
2936
|
+
<button
|
|
2937
|
+
className="thumbs-button"
|
|
2938
|
+
onClick={() => {
|
|
2939
|
+
if (thumbsDownClick)
|
|
2940
|
+
thumbsDownClick(historyEntry.callId);
|
|
2941
|
+
interactionClicked(historyEntry.callId, "thumbsdown");
|
|
2942
|
+
}}
|
|
2943
|
+
disabled={isDisabledDueToNoEmail()}
|
|
2944
|
+
>
|
|
2945
|
+
<svg
|
|
2946
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2947
|
+
viewBox="0 0 20 20"
|
|
2948
|
+
fill="currentColor"
|
|
2949
|
+
className="icon-svg"
|
|
2950
|
+
>
|
|
2951
|
+
<path d="M18.38 3.25H6.81C5.72 3.25 4.79 4.03 4.6 5.11L3.29 12.61C3.18 13.27 3.36 13.94 3.78 14.45C4.21 14.96 4.83 15.25 5.5 15.25H9.53V18C9.53 19.52 10.76 20.75 12.36 20.75C13.06 20.75 13.69 20.33 13.97 19.68L16.51 13.75H18.39C19.7 13.75 20.76 12.69 20.76 11.38V5.61C20.76 4.31 19.7 3.25 18.39 3.25H18.38ZM15.26 12.85L12.59 19.1C12.55 19.19 12.46 19.26 12.27 19.26C11.58 19.26 11.03 18.7 11.03 18.01V13.76H5.5C5.27 13.76 5.07 13.67 4.93 13.5C4.78 13.33 4.73 13.11 4.77 12.88L6.08 5.38C6.14 5.02 6.45001 4.76 6.82 4.76H15.26V12.85ZM19.25 11.38C19.25 11.86 18.86 12.25 18.38 12.25H16.77V4.75H18.38C18.86 4.75 19.25 5.14 19.25 5.61V11.38Z" />
|
|
2952
|
+
</svg>
|
|
2953
|
+
</button>
|
|
2954
|
+
|
|
2955
|
+
{(idle || hasToolData) && (
|
|
2956
|
+
<button
|
|
2957
|
+
className="copy-button"
|
|
2958
|
+
title="Show Tool Call/Response JSON"
|
|
2959
|
+
onClick={() => {
|
|
2960
|
+
const historyEntry = history[prompt];
|
|
2961
|
+
setToolInfoData({
|
|
2962
|
+
calls: historyEntry?.toolCalls ?? [],
|
|
2963
|
+
responses: historyEntry?.toolResponses ?? [],
|
|
2964
|
+
});
|
|
2965
|
+
setIsToolInfoModalOpen(true);
|
|
2966
|
+
}}
|
|
2967
|
+
>
|
|
2968
|
+
<svg
|
|
2969
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2970
|
+
viewBox="0 0 24 24"
|
|
2971
|
+
fill="currentColor"
|
|
2972
|
+
className="icon-svg"
|
|
2973
|
+
>
|
|
2974
|
+
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z" />
|
|
2975
|
+
</svg>
|
|
2976
|
+
</button>
|
|
2977
|
+
)}
|
|
2978
|
+
</div>
|
|
2979
|
+
)}
|
|
2980
|
+
</div>
|
|
2981
|
+
</div>
|
|
2982
|
+
);
|
|
2983
|
+
})}
|
|
2984
|
+
<ToolInfoModal
|
|
2985
|
+
isOpen={isToolInfoModalOpen}
|
|
2986
|
+
onClose={() => setIsToolInfoModalOpen(false)}
|
|
2987
|
+
data={toolInfoData}
|
|
2988
|
+
/>
|
|
2989
|
+
|
|
2990
|
+
{followOnQuestionsState &&
|
|
2991
|
+
followOnQuestionsState.length > 0 &&
|
|
2992
|
+
idle &&
|
|
2993
|
+
!isLoading &&
|
|
2994
|
+
pendingToolRequests.length === 0 && (
|
|
2995
|
+
<div className="suggestions-container">
|
|
2996
|
+
{followOnQuestionsState.map((question, index) => (
|
|
2997
|
+
<button
|
|
2998
|
+
key={index}
|
|
2999
|
+
className="suggestion-button"
|
|
3000
|
+
onClick={() => handleSuggestionClick(question)}
|
|
3001
|
+
disabled={!idle || isDisabledDueToNoEmail()}
|
|
3002
|
+
>
|
|
3003
|
+
{question}
|
|
3004
|
+
</button>
|
|
3005
|
+
))}
|
|
3006
|
+
</div>
|
|
3007
|
+
)}
|
|
3008
|
+
|
|
3009
|
+
<div ref={bottomRef} />
|
|
3010
|
+
|
|
3011
|
+
{hasScroll && !isAtBottom && (
|
|
3012
|
+
<button className="scroll-button" onClick={scrollToBottom}>
|
|
3013
|
+
↓
|
|
3014
|
+
</button>
|
|
3015
|
+
)}
|
|
3016
|
+
</div>
|
|
3017
|
+
{showEmailPanel && (
|
|
3018
|
+
<>
|
|
3019
|
+
{!emailValid && (
|
|
3020
|
+
<div className="email-input-message">
|
|
3021
|
+
{isDisabledDueToNoEmail()
|
|
3022
|
+
? "Let's get started - please enter your email"
|
|
3023
|
+
: CTAClickedButNoEmail || emailClickedButNoEmail
|
|
3024
|
+
? "Sure, we just need an email address to contact you"
|
|
3025
|
+
: "Email address is invalid"}
|
|
3026
|
+
</div>
|
|
3027
|
+
)}
|
|
3028
|
+
<div className="email-input-container">
|
|
3029
|
+
<input
|
|
3030
|
+
type="email"
|
|
3031
|
+
name="email"
|
|
3032
|
+
id="email"
|
|
3033
|
+
className={
|
|
3034
|
+
emailValid
|
|
3035
|
+
? emailInputSet
|
|
3036
|
+
? "email-input-set"
|
|
3037
|
+
: "email-input"
|
|
3038
|
+
: "email-input-invalid"
|
|
3039
|
+
}
|
|
3040
|
+
placeholder={customerEmailCapturePlaceholder}
|
|
3041
|
+
value={emailInput}
|
|
3042
|
+
onChange={(e) => {
|
|
3043
|
+
const newEmail = e.target.value;
|
|
3044
|
+
setEmailInput(newEmail);
|
|
3045
|
+
|
|
3046
|
+
// Reset validation state while typing
|
|
3047
|
+
if (!emailInputSet) {
|
|
3048
|
+
if (
|
|
3049
|
+
customerEmailCaptureMode === "REQUIRED" &&
|
|
3050
|
+
newEmail !== ""
|
|
3051
|
+
) {
|
|
3052
|
+
setEmailValid(isEmailAddress(newEmail));
|
|
3053
|
+
} else {
|
|
3054
|
+
setEmailValid(true);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}}
|
|
3058
|
+
onBlur={() => {
|
|
3059
|
+
// Auto-validate and set email when field loses focus
|
|
3060
|
+
if (
|
|
3061
|
+
emailInput &&
|
|
3062
|
+
isEmailAddress(emailInput) &&
|
|
3063
|
+
!emailInputSet
|
|
3064
|
+
) {
|
|
3065
|
+
const newId =
|
|
3066
|
+
currentCustomer?.customer_id &&
|
|
3067
|
+
currentCustomer.customer_id !== "" &&
|
|
3068
|
+
currentCustomer.customer_id !==
|
|
3069
|
+
currentCustomer?.customer_user_email
|
|
3070
|
+
? currentCustomer.customer_id
|
|
3071
|
+
: emailInput;
|
|
3072
|
+
|
|
3073
|
+
setEmailInputSet(true);
|
|
3074
|
+
setEmailValid(true);
|
|
3075
|
+
setCurrentCustomer({
|
|
3076
|
+
customer_id: newId,
|
|
3077
|
+
customer_user_email: emailInput,
|
|
3078
|
+
});
|
|
3079
|
+
|
|
3080
|
+
// Handle pending actions
|
|
3081
|
+
if (CTAClickedButNoEmail) {
|
|
3082
|
+
sendCallToActionEmail(emailInput);
|
|
3083
|
+
setCTAClickedButNoEmail(false);
|
|
3084
|
+
}
|
|
3085
|
+
if (emailClickedButNoEmail) {
|
|
3086
|
+
handleSendEmail(emailInput, emailInput);
|
|
3087
|
+
setEmailClickedButNoEmail(false);
|
|
3088
|
+
}
|
|
3089
|
+
} else if (
|
|
3090
|
+
customerEmailCaptureMode === "REQUIRED" &&
|
|
3091
|
+
emailInput !== ""
|
|
3092
|
+
) {
|
|
3093
|
+
setEmailValid(isEmailAddress(emailInput));
|
|
3094
|
+
}
|
|
3095
|
+
}}
|
|
3096
|
+
disabled={false}
|
|
3097
|
+
/>
|
|
3098
|
+
{emailInputSet && (
|
|
3099
|
+
<button
|
|
3100
|
+
className="email-input-button"
|
|
3101
|
+
onClick={() => {
|
|
3102
|
+
setEmailInputSet(false);
|
|
3103
|
+
setEmailValid(true);
|
|
3104
|
+
}}
|
|
3105
|
+
title="Edit email"
|
|
3106
|
+
>
|
|
3107
|
+
✎
|
|
3108
|
+
</button>
|
|
3109
|
+
)}
|
|
3110
|
+
</div>
|
|
3111
|
+
</>
|
|
3112
|
+
)}
|
|
3113
|
+
<div className="button-container-actions">
|
|
3114
|
+
{showNewConversationButton && (
|
|
3115
|
+
<button
|
|
3116
|
+
className={`save-button new-conversation-button ${
|
|
3117
|
+
newConversationConfirm ? "confirm-state" : ""
|
|
3118
|
+
}`}
|
|
3119
|
+
onClick={() => {
|
|
3120
|
+
if (!newConversationConfirm) {
|
|
3121
|
+
// First click - show confirmation state
|
|
3122
|
+
setNewConversationConfirm(true);
|
|
3123
|
+
|
|
3124
|
+
// Auto-reset confirmation after 3 seconds
|
|
3125
|
+
setTimeout(() => {
|
|
3126
|
+
setNewConversationConfirm(false);
|
|
3127
|
+
}, 3000);
|
|
3128
|
+
} else {
|
|
3129
|
+
// Second click - proceed with reset
|
|
3130
|
+
setNewConversationConfirm(false);
|
|
3131
|
+
|
|
3132
|
+
// Stop any current request first
|
|
3133
|
+
if (!idle) {
|
|
3134
|
+
stop(lastController);
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
// Clear the current response from useLLM hook
|
|
3138
|
+
setResponse("");
|
|
3139
|
+
|
|
3140
|
+
// Reset conversation state while preserving customer info
|
|
3141
|
+
setHistory({});
|
|
3142
|
+
setLastMessages([]);
|
|
3143
|
+
setNextPrompt("");
|
|
3144
|
+
setLastPrompt(null);
|
|
3145
|
+
setLastKey(null);
|
|
3146
|
+
setIsLoading(false);
|
|
3147
|
+
setCurrentConversation(null);
|
|
3148
|
+
setCallToActionSent(false);
|
|
3149
|
+
setEmailSent(false);
|
|
3150
|
+
setCTAClickedButNoEmail(false);
|
|
3151
|
+
setEmailClickedButNoEmail(false);
|
|
3152
|
+
setFollowOnQuestionsState(followOnQuestions);
|
|
3153
|
+
setSessionApprovedTools([]);
|
|
3154
|
+
setThinkingBlocks([]);
|
|
3155
|
+
setCurrentThinkingIndex(0);
|
|
3156
|
+
setPendingButtonAttachments([]);
|
|
3157
|
+
setPendingToolRequests([]);
|
|
3158
|
+
setIsEmailModalOpen(false);
|
|
3159
|
+
setIsToolInfoModalOpen(false);
|
|
3160
|
+
setToolInfoData(null);
|
|
3161
|
+
setJustReset(true);
|
|
3162
|
+
|
|
3163
|
+
// Create a new AbortController for future requests
|
|
3164
|
+
setLastController(new AbortController());
|
|
3165
|
+
|
|
3166
|
+
// Clear button registry for new conversation
|
|
3167
|
+
buttonActionRegistry.current.clear();
|
|
3168
|
+
|
|
3169
|
+
// Force a small delay to ensure all state updates are processed
|
|
3170
|
+
setTimeout(() => {
|
|
3171
|
+
setJustReset(false);
|
|
3172
|
+
// Scroll to top after state updates
|
|
3173
|
+
const responseArea = responseAreaRef.current as any;
|
|
3174
|
+
if (responseArea) {
|
|
3175
|
+
responseArea.scrollTo({
|
|
3176
|
+
top: 0,
|
|
3177
|
+
behavior: "smooth",
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
}, 100);
|
|
3181
|
+
}
|
|
3182
|
+
}}
|
|
3183
|
+
title={
|
|
3184
|
+
newConversationConfirm
|
|
3185
|
+
? "Click again to confirm reset"
|
|
3186
|
+
: "Start a new conversation"
|
|
3187
|
+
}
|
|
3188
|
+
>
|
|
3189
|
+
{newConversationConfirm ? "Click to Confirm" : "New Conversation"}
|
|
3190
|
+
</button>
|
|
3191
|
+
)}
|
|
3192
|
+
{showSaveButton && (
|
|
3193
|
+
<button
|
|
3194
|
+
className="save-button"
|
|
3195
|
+
onClick={() => {
|
|
3196
|
+
saveHTMLToFile(
|
|
3197
|
+
convertHistoryToHTML(history),
|
|
3198
|
+
`conversation-${new Date().toISOString()}.html`
|
|
3199
|
+
);
|
|
3200
|
+
interactionClicked(lastCallId, "save");
|
|
3201
|
+
}}
|
|
3202
|
+
disabled={isDisabledDueToNoEmail()}
|
|
3203
|
+
>
|
|
3204
|
+
Save Conversation
|
|
3205
|
+
</button>
|
|
3206
|
+
)}
|
|
3207
|
+
{showEmailButton && (
|
|
3208
|
+
<button
|
|
3209
|
+
className="save-button"
|
|
3210
|
+
onClick={() => {
|
|
3211
|
+
if (isEmailAddress(emailInput)) {
|
|
3212
|
+
setEmailInputSet(true);
|
|
3213
|
+
setEmailValid(true);
|
|
3214
|
+
handleSendEmail(emailInput, emailInput);
|
|
3215
|
+
setEmailClickedButNoEmail(false);
|
|
3216
|
+
} else {
|
|
3217
|
+
setShowEmailPanel(true);
|
|
3218
|
+
setEmailValid(false);
|
|
3219
|
+
setEmailClickedButNoEmail(true);
|
|
3220
|
+
}
|
|
3221
|
+
}}
|
|
3222
|
+
disabled={isDisabledDueToNoEmail()}
|
|
3223
|
+
>
|
|
3224
|
+
{"Email Conversation" + (emailSent ? " ✓" : "")}
|
|
3225
|
+
</button>
|
|
3226
|
+
)}
|
|
3227
|
+
|
|
3228
|
+
{showCallToAction && (
|
|
3229
|
+
<button
|
|
3230
|
+
className="save-button"
|
|
3231
|
+
onClick={() => {
|
|
3232
|
+
if (isEmailAddress(emailInput)) {
|
|
3233
|
+
setEmailInputSet(true);
|
|
3234
|
+
setEmailValid(true);
|
|
3235
|
+
sendCallToActionEmail(emailInput);
|
|
3236
|
+
setCTAClickedButNoEmail(false);
|
|
3237
|
+
} else {
|
|
3238
|
+
setShowEmailPanel(true);
|
|
3239
|
+
setEmailValid(false);
|
|
3240
|
+
setCTAClickedButNoEmail(true);
|
|
3241
|
+
}
|
|
3242
|
+
}}
|
|
3243
|
+
disabled={isDisabledDueToNoEmail()}
|
|
3244
|
+
>
|
|
3245
|
+
{callToActionButtonText + (callToActionSent ? " ✓" : "")}
|
|
3246
|
+
</button>
|
|
3247
|
+
)}
|
|
3248
|
+
</div>
|
|
3249
|
+
|
|
3250
|
+
<EmailModal
|
|
3251
|
+
isOpen={isEmailModalOpen}
|
|
3252
|
+
defaultEmail={emailInput ?? ""}
|
|
3253
|
+
onClose={() => setIsEmailModalOpen(false)}
|
|
3254
|
+
onSend={handleSendEmail}
|
|
3255
|
+
/>
|
|
3256
|
+
|
|
3257
|
+
<div className="input-container">
|
|
3258
|
+
<textarea
|
|
3259
|
+
ref={textareaRef}
|
|
3260
|
+
className={`chat-input${userResizedHeight ? ' user-resized' : ''}`}
|
|
3261
|
+
placeholder={placeholder}
|
|
3262
|
+
value={nextPrompt}
|
|
3263
|
+
onChange={(e) => {
|
|
3264
|
+
setNextPrompt(e.target.value);
|
|
3265
|
+
// Auto-resize on content change
|
|
3266
|
+
setTimeout(autoResizeTextarea, 0);
|
|
3267
|
+
}}
|
|
3268
|
+
onKeyDown={(e) => {
|
|
3269
|
+
if (e.key === "Enter") {
|
|
3270
|
+
if (e.shiftKey) {
|
|
3271
|
+
// Shift+Enter: Allow new line (default behavior)
|
|
3272
|
+
return;
|
|
3273
|
+
} else {
|
|
3274
|
+
// Enter alone: Send message
|
|
3275
|
+
if (nextPrompt.trim() !== "") {
|
|
3276
|
+
e.preventDefault();
|
|
3277
|
+
continueChat();
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
}}
|
|
3282
|
+
onDoubleClick={() => {
|
|
3283
|
+
// Double-click to reset to default auto-resize behavior
|
|
3284
|
+
if (userResizedHeight) {
|
|
3285
|
+
setUserResizedHeight(null);
|
|
3286
|
+
// Trigger auto-resize to return to content-based height
|
|
3287
|
+
setTimeout(autoResizeTextarea, 0);
|
|
3288
|
+
}
|
|
3289
|
+
}}
|
|
3290
|
+
disabled={isDisabledDueToNoEmail()}
|
|
3291
|
+
title={userResizedHeight ? "Double-click to reset to auto-resize" : "Drag bottom-right corner to manually resize"}
|
|
3292
|
+
></textarea>
|
|
3293
|
+
<button
|
|
3294
|
+
className="send-button"
|
|
3295
|
+
onClick={() => continueChat()}
|
|
3296
|
+
disabled={isDisabledDueToNoEmail()}
|
|
3297
|
+
>
|
|
3298
|
+
{idle ? (
|
|
3299
|
+
<svg
|
|
3300
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3301
|
+
viewBox="0 0 24 24"
|
|
3302
|
+
strokeWidth="1"
|
|
3303
|
+
stroke="currentColor"
|
|
3304
|
+
fill="none"
|
|
3305
|
+
strokeLinecap="round"
|
|
3306
|
+
strokeLinejoin="round"
|
|
3307
|
+
className="icon-svg-large"
|
|
3308
|
+
>
|
|
3309
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
|
3310
|
+
<path d="M10 14l11 -11"></path>
|
|
3311
|
+
<path d="M21 3l-6.5 18a.55 .55 0 0 1 -1 0l-3.5 -7l-7 -3.5a.55 .55 0 0 1 0 -1l18 -6.5"></path>
|
|
3312
|
+
</svg>
|
|
3313
|
+
) : (
|
|
3314
|
+
<svg
|
|
3315
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3316
|
+
viewBox="0 0 24 24"
|
|
3317
|
+
strokeWidth="1"
|
|
3318
|
+
stroke="currentColor"
|
|
3319
|
+
fill="currentColor"
|
|
3320
|
+
className="icon-svg-large"
|
|
3321
|
+
>
|
|
3322
|
+
<path d="M8 8h16v16H8z" />
|
|
3323
|
+
</svg>
|
|
3324
|
+
)}
|
|
3325
|
+
</button>
|
|
3326
|
+
</div>
|
|
3327
|
+
{showPoweredBy && (
|
|
3328
|
+
<div
|
|
3329
|
+
className={`footer-container ${
|
|
3330
|
+
mcpServers && mcpServers.length > 0 ? "with-tools" : "no-tools"
|
|
3331
|
+
}`}
|
|
3332
|
+
>
|
|
3333
|
+
{/* Tool status indicator - only show when tools are configured */}
|
|
3334
|
+
{mcpServers && mcpServers.length > 0 && (
|
|
3335
|
+
<div className="footer-left">
|
|
3336
|
+
<div className="tool-status">
|
|
3337
|
+
<span
|
|
3338
|
+
className={`tool-status-dot ${
|
|
3339
|
+
toolsLoading
|
|
3340
|
+
? "loading"
|
|
3341
|
+
: toolsFetchError
|
|
3342
|
+
? "error"
|
|
3343
|
+
: "ready"
|
|
3344
|
+
}`}
|
|
3345
|
+
title={
|
|
3346
|
+
!toolsLoading && !toolsFetchError && toolList.length > 0
|
|
3347
|
+
? toolList
|
|
3348
|
+
.map(
|
|
3349
|
+
(tool) =>
|
|
3350
|
+
`${tool.name}: ${
|
|
3351
|
+
tool.description || "No description"
|
|
3352
|
+
}`
|
|
3353
|
+
)
|
|
3354
|
+
.join("\n")
|
|
3355
|
+
: ""
|
|
3356
|
+
}
|
|
3357
|
+
></span>
|
|
3358
|
+
<span
|
|
3359
|
+
className="tool-status-text"
|
|
3360
|
+
title={
|
|
3361
|
+
!toolsLoading && !toolsFetchError && toolList.length > 0
|
|
3362
|
+
? toolList
|
|
3363
|
+
.map(
|
|
3364
|
+
(tool) =>
|
|
3365
|
+
`${tool.name}: ${
|
|
3366
|
+
tool.description || "No description"
|
|
3367
|
+
}`
|
|
3368
|
+
)
|
|
3369
|
+
.join("\n")
|
|
3370
|
+
: ""
|
|
3371
|
+
}
|
|
3372
|
+
>
|
|
3373
|
+
{toolsLoading
|
|
3374
|
+
? "tools loading..."
|
|
3375
|
+
: toolsFetchError
|
|
3376
|
+
? "tool fetch failed"
|
|
3377
|
+
: toolList.length > 0
|
|
3378
|
+
? `${toolList.length} tools ready`
|
|
3379
|
+
: "no tools found"}
|
|
3380
|
+
</span>
|
|
3381
|
+
</div>
|
|
3382
|
+
</div>
|
|
3383
|
+
)}
|
|
3384
|
+
|
|
3385
|
+
<div
|
|
3386
|
+
className={`footer-right ${
|
|
3387
|
+
mcpServers && mcpServers.length > 0 ? "" : "footer-center"
|
|
3388
|
+
}`}
|
|
3389
|
+
>
|
|
3390
|
+
<div className="powered-by">
|
|
3391
|
+
<svg
|
|
3392
|
+
width="16"
|
|
3393
|
+
height="16"
|
|
3394
|
+
viewBox="0 0 72 72"
|
|
3395
|
+
fill="none"
|
|
3396
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3397
|
+
>
|
|
3398
|
+
<ellipse
|
|
3399
|
+
cx="14.0868"
|
|
3400
|
+
cy="59.2146"
|
|
3401
|
+
rx="7.8261"
|
|
3402
|
+
ry="7.7854"
|
|
3403
|
+
fill="#2487D8"
|
|
3404
|
+
/>
|
|
3405
|
+
<ellipse
|
|
3406
|
+
cx="24.9013"
|
|
3407
|
+
cy="43.0776"
|
|
3408
|
+
rx="6.11858"
|
|
3409
|
+
ry="6.08676"
|
|
3410
|
+
fill="#2487D8"
|
|
3411
|
+
/>
|
|
3412
|
+
<ellipse
|
|
3413
|
+
cx="45.391"
|
|
3414
|
+
cy="43.0776"
|
|
3415
|
+
rx="6.11858"
|
|
3416
|
+
ry="6.08676"
|
|
3417
|
+
fill="#2487D8"
|
|
3418
|
+
/>
|
|
3419
|
+
<ellipse
|
|
3420
|
+
cx="65.8813"
|
|
3421
|
+
cy="43.0776"
|
|
3422
|
+
rx="6.11858"
|
|
3423
|
+
ry="6.08676"
|
|
3424
|
+
fill="#2487D8"
|
|
3425
|
+
/>
|
|
3426
|
+
<ellipse
|
|
3427
|
+
cx="13.9444"
|
|
3428
|
+
cy="30.4795"
|
|
3429
|
+
rx="4.83795"
|
|
3430
|
+
ry="4.81279"
|
|
3431
|
+
fill="#2487D8"
|
|
3432
|
+
/>
|
|
3433
|
+
<ellipse
|
|
3434
|
+
cx="34.7193"
|
|
3435
|
+
cy="30.4795"
|
|
3436
|
+
rx="4.83795"
|
|
3437
|
+
ry="4.81279"
|
|
3438
|
+
fill="#2487D8"
|
|
3439
|
+
/>
|
|
3440
|
+
<ellipse
|
|
3441
|
+
cx="55.4942"
|
|
3442
|
+
cy="30.4795"
|
|
3443
|
+
rx="4.83795"
|
|
3444
|
+
ry="4.81279"
|
|
3445
|
+
fill="#2487D8"
|
|
3446
|
+
/>
|
|
3447
|
+
<ellipse
|
|
3448
|
+
cx="3.27273"
|
|
3449
|
+
cy="20.4293"
|
|
3450
|
+
rx="3.27273"
|
|
3451
|
+
ry="3.25571"
|
|
3452
|
+
fill="#2487D8"
|
|
3453
|
+
/>
|
|
3454
|
+
<ellipse
|
|
3455
|
+
cx="24.9011"
|
|
3456
|
+
cy="20.4293"
|
|
3457
|
+
rx="3.27273"
|
|
3458
|
+
ry="3.25571"
|
|
3459
|
+
fill="#2487D8"
|
|
3460
|
+
/>
|
|
3461
|
+
<ellipse
|
|
3462
|
+
cx="45.3914"
|
|
3463
|
+
cy="20.4293"
|
|
3464
|
+
rx="3.27273"
|
|
3465
|
+
ry="3.25571"
|
|
3466
|
+
fill="#2487D8"
|
|
3467
|
+
/>
|
|
3468
|
+
<ellipse
|
|
3469
|
+
cx="12.2373"
|
|
3470
|
+
cy="13.4931"
|
|
3471
|
+
rx="1.70751"
|
|
3472
|
+
ry="1.69863"
|
|
3473
|
+
fill="#2487D8"
|
|
3474
|
+
/>
|
|
3475
|
+
<ellipse
|
|
3476
|
+
cx="33.0122"
|
|
3477
|
+
cy="13.4931"
|
|
3478
|
+
rx="1.70751"
|
|
3479
|
+
ry="1.69863"
|
|
3480
|
+
fill="#2487D8"
|
|
3481
|
+
/>
|
|
3482
|
+
<ellipse
|
|
3483
|
+
cx="53.5019"
|
|
3484
|
+
cy="13.4931"
|
|
3485
|
+
rx="1.70751"
|
|
3486
|
+
ry="1.69863"
|
|
3487
|
+
fill="#2487D8"
|
|
3488
|
+
/>
|
|
3489
|
+
<ellipse
|
|
3490
|
+
cx="19.3517"
|
|
3491
|
+
cy="6.13242"
|
|
3492
|
+
rx="1.13834"
|
|
3493
|
+
ry="1.13242"
|
|
3494
|
+
fill="#2487D8"
|
|
3495
|
+
/>
|
|
3496
|
+
<ellipse
|
|
3497
|
+
cx="40.1266"
|
|
3498
|
+
cy="6.13242"
|
|
3499
|
+
rx="1.13834"
|
|
3500
|
+
ry="1.13242"
|
|
3501
|
+
fill="#2487D8"
|
|
3502
|
+
/>
|
|
3503
|
+
<ellipse
|
|
3504
|
+
cx="60.901"
|
|
3505
|
+
cy="6.13242"
|
|
3506
|
+
rx="1.13834"
|
|
3507
|
+
ry="1.13242"
|
|
3508
|
+
fill="#2487D8"
|
|
3509
|
+
/>
|
|
3510
|
+
<ellipse
|
|
3511
|
+
cx="34.8617"
|
|
3512
|
+
cy="59.2146"
|
|
3513
|
+
rx="7.8261"
|
|
3514
|
+
ry="7.7854"
|
|
3515
|
+
fill="#2487D8"
|
|
3516
|
+
/>
|
|
3517
|
+
<ellipse
|
|
3518
|
+
cx="55.6366"
|
|
3519
|
+
cy="59.2146"
|
|
3520
|
+
rx="7.8261"
|
|
3521
|
+
ry="7.7854"
|
|
3522
|
+
fill="#ED7D31"
|
|
3523
|
+
/>
|
|
3524
|
+
</svg>{" "}
|
|
3525
|
+
brought to you by
|
|
3526
|
+
<a
|
|
3527
|
+
href="https://llmasaservice.io"
|
|
3528
|
+
target="_blank"
|
|
3529
|
+
rel="noopener noreferrer"
|
|
3530
|
+
>
|
|
3531
|
+
llmasaservice.io
|
|
3532
|
+
</a>
|
|
3533
|
+
</div>
|
|
3534
|
+
</div>
|
|
3535
|
+
</div>
|
|
3536
|
+
)}
|
|
3537
|
+
</div>
|
|
3538
|
+
<div ref={bottomPanelRef} />
|
|
3539
|
+
{/* Modal with iframe */}
|
|
3540
|
+
{iframeUrl && (
|
|
3541
|
+
<div
|
|
3542
|
+
style={{
|
|
3543
|
+
position: "fixed",
|
|
3544
|
+
top: 0,
|
|
3545
|
+
left: 0,
|
|
3546
|
+
width: "100vw",
|
|
3547
|
+
height: "100vh",
|
|
3548
|
+
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
3549
|
+
display: "flex",
|
|
3550
|
+
justifyContent: "center",
|
|
3551
|
+
alignItems: "center",
|
|
3552
|
+
zIndex: 9999,
|
|
3553
|
+
}}
|
|
3554
|
+
>
|
|
3555
|
+
<div style={{ position: "relative", width: "95vw", height: "95vh" }}>
|
|
3556
|
+
<button
|
|
3557
|
+
onClick={() => setIframeUrl(null)}
|
|
3558
|
+
style={{
|
|
3559
|
+
position: "absolute",
|
|
3560
|
+
top: 10,
|
|
3561
|
+
right: 10,
|
|
3562
|
+
zIndex: 10000,
|
|
3563
|
+
background: "#fff",
|
|
3564
|
+
border: "none",
|
|
3565
|
+
padding: "5px 5px",
|
|
3566
|
+
cursor: "pointer",
|
|
3567
|
+
}}
|
|
3568
|
+
>
|
|
3569
|
+
Close
|
|
3570
|
+
</button>
|
|
3571
|
+
<iframe
|
|
3572
|
+
src={iframeUrl}
|
|
3573
|
+
title="Dynamic Iframe"
|
|
3574
|
+
style={{ width: "100%", height: "100%", border: "none" }}
|
|
3575
|
+
/>
|
|
3576
|
+
</div>
|
|
3577
|
+
</div>
|
|
3578
|
+
)}
|
|
3579
|
+
</>
|
|
3580
|
+
);
|
|
3581
|
+
};
|
|
3582
|
+
|
|
3583
|
+
export default ChatPanel;
|