@hef2024/llmasaservice-ui 0.16.8 → 0.16.9

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.
@@ -86,6 +86,9 @@ export interface AIChatPanelProps {
86
86
  totalContextTokens?: number;
87
87
  maxContextTokens?: number;
88
88
  enableContextDetailView?: boolean;
89
+
90
+ // Callback when a new conversation is created via API
91
+ onConversationCreated?: (conversationId: string) => void;
89
92
  }
90
93
 
91
94
  /**
@@ -658,7 +661,14 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
658
661
  totalContextTokens = 0,
659
662
  maxContextTokens = 8000,
660
663
  enableContextDetailView = false,
664
+ onConversationCreated,
661
665
  }) => {
666
+ // ============================================================================
667
+ // API URL
668
+ // ============================================================================
669
+ // Public API URL for dev and production (matches ChatPanel)
670
+ const publicAPIUrl = 'https://api.llmasaservice.io';
671
+
662
672
  // ============================================================================
663
673
  // State
664
674
  // ============================================================================
@@ -694,6 +704,33 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
694
704
  // Store the latest processed history for callbacks (doesn't trigger re-renders)
695
705
  const latestHistoryRef = useRef<Record<string, HistoryEntry>>(initialHistory);
696
706
 
707
+ // Sync new entries from initialHistory into local history state
708
+ // This allows parent components to inject messages (e.g., page-based agent suggestions)
709
+ useEffect(() => {
710
+ if (!initialHistory) return;
711
+
712
+ // Use functional update to access current history without adding it to deps
713
+ setHistory(prev => {
714
+ const currentKeys = Object.keys(prev);
715
+ const newEntries: Record<string, HistoryEntry> = {};
716
+ let hasNewEntries = false;
717
+
718
+ for (const [key, value] of Object.entries(initialHistory)) {
719
+ if (!currentKeys.includes(key)) {
720
+ newEntries[key] = value as HistoryEntry;
721
+ hasNewEntries = true;
722
+ }
723
+ }
724
+
725
+ // Merge new entries if any were found
726
+ if (hasNewEntries) {
727
+ return { ...prev, ...newEntries };
728
+ }
729
+
730
+ return prev; // No changes
731
+ });
732
+ }, [initialHistory]);
733
+
697
734
  // ============================================================================
698
735
  // useLLM Hook
699
736
  // ============================================================================
@@ -754,6 +791,74 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
754
791
  };
755
792
  }, []);
756
793
 
794
+ // Ensure a conversation exists before sending the first message
795
+ // This creates a conversation on the server and returns the conversation ID
796
+ const ensureConversation = useCallback(() => {
797
+ console.log('ensureConversation - called with:', {
798
+ currentConversation,
799
+ createConversationOnFirstChat,
800
+ project_id,
801
+ publicAPIUrl,
802
+ });
803
+ if (
804
+ (!currentConversation || currentConversation === '') &&
805
+ createConversationOnFirstChat
806
+ ) {
807
+ // Guard: Don't create conversation without a project_id
808
+ if (!project_id) {
809
+ console.error('ensureConversation - Cannot create conversation without project_id');
810
+ return Promise.resolve('');
811
+ }
812
+
813
+ const requestBody = {
814
+ project_id: project_id,
815
+ agentId: agent,
816
+ customerId: customer?.customer_id ?? null,
817
+ customerEmail: customer?.customer_user_email ?? null,
818
+ timezone: browserInfo?.userTimezone,
819
+ language: browserInfo?.userLanguage,
820
+ };
821
+ console.log('ensureConversation - Creating conversation with:', requestBody);
822
+ console.log('ensureConversation - API URL:', `${publicAPIUrl}/conversations`);
823
+
824
+ return fetch(`${publicAPIUrl}/conversations`, {
825
+ method: 'POST',
826
+ headers: {
827
+ 'Content-Type': 'application/json',
828
+ },
829
+ body: JSON.stringify(requestBody),
830
+ })
831
+ .then(async (res) => {
832
+ if (!res.ok) {
833
+ const errorText = await res.text();
834
+ throw new Error(
835
+ `HTTP error! status: ${res.status}, message: ${errorText}`
836
+ );
837
+ }
838
+ return res.json();
839
+ })
840
+ .then((newConvo) => {
841
+ console.log('ensureConversation - API response:', newConvo);
842
+ if (newConvo?.id) {
843
+ console.log('ensureConversation - New conversation ID:', newConvo.id);
844
+ setCurrentConversation(newConvo.id);
845
+ // NOTE: Don't call onConversationCreated here - it causes a re-render
846
+ // before send() is called. The caller should notify after send() starts.
847
+ return newConvo.id;
848
+ }
849
+ console.warn('ensureConversation - No ID in response');
850
+ return '';
851
+ })
852
+ .catch((error) => {
853
+ console.error('Error creating new conversation', error);
854
+ return '';
855
+ });
856
+ }
857
+ // If a currentConversation exists, return it in a resolved Promise.
858
+ console.log('ensureConversation - using existing conversation:', currentConversation);
859
+ return Promise.resolve(currentConversation);
860
+ }, [currentConversation, createConversationOnFirstChat, publicAPIUrl, project_id, agent, customer, browserInfo]);
861
+
757
862
  // Data with extras (matches ChatPanel's dataWithExtras)
758
863
  const dataWithExtras = useCallback(() => {
759
864
  return [
@@ -927,6 +1032,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
927
1032
  // Continue chat (send message) - matches ChatPanel behavior exactly
928
1033
  // promptText is now required - comes from the isolated ChatInput component
929
1034
  const continueChat = useCallback((promptText: string) => {
1035
+ console.log('AIChatPanel.continueChat called with:', promptText);
930
1036
  // Clear thinking blocks for new response
931
1037
  setThinkingBlocks([]);
932
1038
  setCurrentThinkingIndex(0);
@@ -957,73 +1063,88 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
957
1063
 
958
1064
  setIsLoading(true);
959
1065
 
960
- // Build messagesAndHistory from history (matches ChatPanel)
961
- const messagesAndHistory: { role: string; content: string }[] = [];
962
- Object.entries(history).forEach(([historyPrompt, historyEntry]) => {
963
- // Strip timestamp prefix from prompt before using it (matches ChatPanel)
964
- let promptForHistory = historyPrompt;
965
- const isoTimestampRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z:/;
966
- if (isoTimestampRegex.test(historyPrompt)) {
967
- const colonIndex = historyPrompt.indexOf(':', 19);
968
- promptForHistory = historyPrompt.substring(colonIndex + 1);
969
- } else if (/^\d+:/.test(historyPrompt)) {
970
- const colonIndex = historyPrompt.indexOf(':');
971
- promptForHistory = historyPrompt.substring(colonIndex + 1);
1066
+ // Ensure conversation exists before sending (matches ChatPanel)
1067
+ console.log('AIChatPanel.continueChat - about to call ensureConversation');
1068
+ ensureConversation().then((convId) => {
1069
+ console.log('AIChatPanel.continueChat - ensureConversation resolved with:', convId);
1070
+ // Build messagesAndHistory from history (matches ChatPanel)
1071
+ const messagesAndHistory: { role: string; content: string }[] = [];
1072
+ Object.entries(history).forEach(([historyPrompt, historyEntry]) => {
1073
+ // Strip timestamp prefix from prompt before using it (matches ChatPanel)
1074
+ let promptForHistory = historyPrompt;
1075
+ const isoTimestampRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z:/;
1076
+ if (isoTimestampRegex.test(historyPrompt)) {
1077
+ const colonIndex = historyPrompt.indexOf(':', 19);
1078
+ promptForHistory = historyPrompt.substring(colonIndex + 1);
1079
+ } else if (/^\d+:/.test(historyPrompt)) {
1080
+ const colonIndex = historyPrompt.indexOf(':');
1081
+ promptForHistory = historyPrompt.substring(colonIndex + 1);
1082
+ }
1083
+
1084
+ messagesAndHistory.push({ role: 'user', content: promptForHistory });
1085
+ messagesAndHistory.push({ role: 'assistant', content: historyEntry.content });
1086
+ });
1087
+
1088
+ // Generate unique key using ISO timestamp prefix + prompt (matches ChatPanel)
1089
+ const timestamp = new Date().toISOString();
1090
+ const promptKey = `${timestamp}:${promptToSend.trim()}`;
1091
+
1092
+ // Set history entry before sending (matches ChatPanel)
1093
+ setHistory((prevHistory) => ({
1094
+ ...prevHistory,
1095
+ [promptKey]: { content: '', callId: '' },
1096
+ }));
1097
+
1098
+ // Build the full prompt - only apply template for first message (matches ChatPanel)
1099
+ let fullPromptToSend = promptToSend.trim();
1100
+ if (Object.keys(history).length === 0 && promptTemplate) {
1101
+ fullPromptToSend = promptTemplate.replace('{{prompt}}', fullPromptToSend);
1102
+ }
1103
+
1104
+ // Add follow-on prompt
1105
+ if (followOnPrompt) {
1106
+ fullPromptToSend += `\n\n${followOnPrompt}`;
972
1107
  }
973
1108
 
974
- messagesAndHistory.push({ role: 'user', content: promptForHistory });
975
- messagesAndHistory.push({ role: 'assistant', content: historyEntry.content });
1109
+ const newController = new AbortController();
1110
+ setLastController(newController);
1111
+
1112
+ // Pass data array to send() for template replacement (e.g., {{Context}})
1113
+ // Pass service (group_id) and customer data just like ChatPanel does
1114
+ // Use convId from ensureConversation (matches ChatPanel)
1115
+ send(
1116
+ fullPromptToSend,
1117
+ messagesAndHistory,
1118
+ [
1119
+ ...dataWithExtras(),
1120
+ { key: '--messages', data: messagesAndHistory.length.toString() },
1121
+ ],
1122
+ true, // stream
1123
+ true, // includeHistory
1124
+ service, // group_id from agent config
1125
+ convId, // Use the conversation ID from ensureConversation
1126
+ newController
1127
+ );
1128
+
1129
+ setLastPrompt(promptToSend.trim());
1130
+ setLastMessages(messagesAndHistory);
1131
+ setLastKey(promptKey);
1132
+
1133
+ // Notify parent of new conversation ID AFTER send() has started
1134
+ // This prevents the component from being remounted before send() runs
1135
+ if (convId && onConversationCreated) {
1136
+ // Use setTimeout to ensure send() has fully started before triggering re-render
1137
+ setTimeout(() => {
1138
+ onConversationCreated(convId);
1139
+ }, 100);
1140
+ }
1141
+
1142
+ // Scroll to bottom after adding the new prompt to show it immediately
1143
+ // Use setTimeout to ensure the DOM has updated with the new history entry
1144
+ setTimeout(() => {
1145
+ scrollToBottom();
1146
+ }, 0);
976
1147
  });
977
-
978
- // Generate unique key using ISO timestamp prefix + prompt (matches ChatPanel)
979
- const timestamp = new Date().toISOString();
980
- const promptKey = `${timestamp}:${promptToSend.trim()}`;
981
-
982
- // Set history entry before sending (matches ChatPanel)
983
- setHistory((prevHistory) => ({
984
- ...prevHistory,
985
- [promptKey]: { content: '', callId: '' },
986
- }));
987
-
988
- // Build the full prompt - only apply template for first message (matches ChatPanel)
989
- let fullPromptToSend = promptToSend.trim();
990
- if (Object.keys(history).length === 0 && promptTemplate) {
991
- fullPromptToSend = promptTemplate.replace('{{prompt}}', fullPromptToSend);
992
- }
993
-
994
- // Add follow-on prompt
995
- if (followOnPrompt) {
996
- fullPromptToSend += `\n\n${followOnPrompt}`;
997
- }
998
-
999
- const newController = new AbortController();
1000
- setLastController(newController);
1001
-
1002
- // Pass data array to send() for template replacement (e.g., {{Context}})
1003
- // Pass service (group_id) and customer data just like ChatPanel does
1004
- send(
1005
- fullPromptToSend,
1006
- messagesAndHistory,
1007
- [
1008
- ...dataWithExtras(),
1009
- { key: '--messages', data: messagesAndHistory.length.toString() },
1010
- ],
1011
- true, // stream
1012
- true, // includeHistory
1013
- service, // group_id from agent config
1014
- currentConversation,
1015
- newController
1016
- );
1017
-
1018
- setLastPrompt(promptToSend.trim());
1019
- setLastMessages(messagesAndHistory);
1020
- setLastKey(promptKey);
1021
-
1022
- // Scroll to bottom after adding the new prompt to show it immediately
1023
- // Use setTimeout to ensure the DOM has updated with the new history entry
1024
- setTimeout(() => {
1025
- scrollToBottom();
1026
- }, 0);
1027
1148
  }, [
1028
1149
  idle,
1029
1150
  stop,
@@ -1038,9 +1159,10 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
1038
1159
  followOnPrompt,
1039
1160
  send,
1040
1161
  service,
1041
- currentConversation,
1162
+ ensureConversation,
1042
1163
  dataWithExtras,
1043
1164
  scrollToBottom,
1165
+ onConversationCreated,
1044
1166
  ]);
1045
1167
 
1046
1168
  // Handle suggestion click - directly sends like ChatPanel does
@@ -1095,27 +1217,29 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
1095
1217
 
1096
1218
  // Effect 1: Process response for DISPLAY ONLY (no callbacks here)
1097
1219
  // Updates state for rendering, stores in ref for later callback use
1220
+ // NOTE: We store RAW content (without action processing) in history state.
1221
+ // Actions are applied at RENDER time so that switching agents with different
1222
+ // actions will re-process the history correctly.
1098
1223
  useEffect(() => {
1099
1224
  if (!response || !lastKey || justReset) return;
1100
1225
 
1101
1226
  const { cleanedText, blocks } = processThinkingTags(response);
1102
- const processedContent = processActions(cleanedText);
1103
1227
 
1104
1228
  // Update display state
1105
1229
  setThinkingBlocks(blocks);
1106
1230
 
1107
- // Update history state for display AND store in ref for callbacks
1231
+ // Update history state with RAW content (actions applied at render time)
1108
1232
  setHistory((prev) => {
1109
1233
  const newHistory = { ...prev };
1110
1234
  newHistory[lastKey] = {
1111
- content: processedContent,
1235
+ content: cleanedText, // Store raw content, not processed
1112
1236
  callId: lastCallId || '',
1113
1237
  };
1114
1238
  // Keep ref in sync for callbacks (this doesn't trigger re-renders)
1115
1239
  latestHistoryRef.current = newHistory;
1116
1240
  return newHistory;
1117
1241
  });
1118
- }, [response, lastKey, lastCallId, processThinkingTags, processActions, justReset]);
1242
+ }, [response, lastKey, lastCallId, processThinkingTags, justReset]);
1119
1243
 
1120
1244
  // Effect 2: Handle response completion - SINGLE POINT for all completion logic
1121
1245
  // Triggers ONLY when idle transitions from false → true
@@ -1259,47 +1383,62 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
1259
1383
  // This prevents unwanted LLM calls when switching to loaded conversations
1260
1384
  const hasLoadedHistory = initialHistory && Object.keys(initialHistory).length > 0;
1261
1385
 
1386
+ // Don't proceed if project_id is not yet available
1387
+ if (!project_id) {
1388
+ return;
1389
+ }
1390
+
1262
1391
  if (initialPrompt && initialPrompt !== '' && initialPrompt !== lastPrompt && !hasLoadedHistory) {
1263
1392
  setIsLoading(true);
1264
1393
  setThinkingBlocks([]);
1265
1394
  setCurrentThinkingIndex(0);
1266
1395
  setUserHasScrolled(false); // Enable auto-scroll for new prompt
1267
1396
 
1268
- const controller = new AbortController();
1269
- setLastController(controller);
1270
-
1271
- // Generate timestamp-prefixed key (matches ChatPanel)
1272
- const timestamp = new Date().toISOString();
1273
- const promptKey = `${timestamp}:${initialPrompt}`;
1274
-
1275
- // Set history entry before sending (matches ChatPanel)
1276
- setHistory({ [promptKey]: { content: '', callId: '' } });
1277
-
1278
- // Build prompt with template
1279
- let fullPrompt = initialPrompt;
1280
- if (promptTemplate) {
1281
- fullPrompt = promptTemplate.replace('{{prompt}}', initialPrompt);
1282
- }
1283
-
1284
- send(
1285
- fullPrompt,
1286
- [],
1287
- [
1288
- ...dataWithExtras(),
1289
- { key: '--messages', data: '0' },
1290
- ],
1291
- true,
1292
- true,
1293
- service,
1294
- currentConversation,
1295
- controller
1296
- );
1297
-
1298
- setLastPrompt(initialPrompt);
1299
- setLastMessages([]);
1300
- setLastKey(promptKey);
1397
+ // Ensure conversation exists before sending (matches ChatPanel)
1398
+ ensureConversation().then((convId) => {
1399
+ const controller = new AbortController();
1400
+ setLastController(controller);
1401
+
1402
+ // Generate timestamp-prefixed key (matches ChatPanel)
1403
+ const timestamp = new Date().toISOString();
1404
+ const promptKey = `${timestamp}:${initialPrompt}`;
1405
+
1406
+ // Set history entry before sending (matches ChatPanel)
1407
+ setHistory({ [promptKey]: { content: '', callId: '' } });
1408
+
1409
+ // Build prompt with template
1410
+ let fullPrompt = initialPrompt;
1411
+ if (promptTemplate) {
1412
+ fullPrompt = promptTemplate.replace('{{prompt}}', initialPrompt);
1413
+ }
1414
+
1415
+ send(
1416
+ fullPrompt,
1417
+ [],
1418
+ [
1419
+ ...dataWithExtras(),
1420
+ { key: '--messages', data: '0' },
1421
+ ],
1422
+ true,
1423
+ true,
1424
+ service,
1425
+ convId, // Use conversation ID from ensureConversation
1426
+ controller
1427
+ );
1428
+
1429
+ setLastPrompt(initialPrompt);
1430
+ setLastMessages([]);
1431
+ setLastKey(promptKey);
1432
+
1433
+ // Notify parent of new conversation ID AFTER send() has started
1434
+ if (convId && onConversationCreated) {
1435
+ setTimeout(() => {
1436
+ onConversationCreated(convId);
1437
+ }, 100);
1438
+ }
1439
+ });
1301
1440
  }
1302
- }, [initialPrompt, initialHistory]);
1441
+ }, [initialPrompt, initialHistory, ensureConversation, promptTemplate, send, dataWithExtras, service, lastPrompt, project_id, onConversationCreated]);
1303
1442
 
1304
1443
  // ============================================================================
1305
1444
  // Render Helpers
@@ -1536,12 +1675,17 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
1536
1675
  {/* History */}
1537
1676
  {Object.entries(history).map(([prompt, entry], index) => {
1538
1677
  const isLastEntry = index === Object.keys(history).length - 1;
1678
+ // Check if this is a system message (injected by page context, etc.)
1679
+ const isSystemMessage = prompt.startsWith('__system__:');
1680
+ // Process thinking tags first, then apply actions at render time
1681
+ // This ensures actions are always applied with current props (e.g., after agent switch)
1539
1682
  const { cleanedText } = processThinkingTags(entry.content);
1683
+ const processedContent = processActions(cleanedText);
1540
1684
 
1541
1685
  return (
1542
1686
  <div key={index} className="ai-chat-entry">
1543
- {/* User Message */}
1544
- {!(hideInitialPrompt && index === 0) && (
1687
+ {/* User Message - hidden for initial prompt or system messages */}
1688
+ {!(hideInitialPrompt && index === 0) && !isSystemMessage && (
1545
1689
  <div className="ai-chat-message ai-chat-message--user">
1546
1690
  <div className="ai-chat-message__content">
1547
1691
  {formatPromptForDisplay(prompt)}
@@ -1557,13 +1701,13 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
1557
1701
  <div className="ai-chat-streaming">
1558
1702
  {thinkingBlocks.length > 0 && renderThinkingBlocks()}
1559
1703
 
1560
- {cleanedText ? (
1704
+ {processedContent ? (
1561
1705
  <ReactMarkdown
1562
1706
  remarkPlugins={[remarkGfm]}
1563
1707
  rehypePlugins={[rehypeRaw]}
1564
1708
  components={markdownComponents}
1565
1709
  >
1566
- {cleanedText}
1710
+ {processedContent}
1567
1711
  </ReactMarkdown>
1568
1712
  ) : (
1569
1713
  <div className="ai-chat-loading">
@@ -1584,7 +1728,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
1584
1728
  rehypePlugins={[rehypeRaw]}
1585
1729
  components={markdownComponents}
1586
1730
  >
1587
- {cleanedText}
1731
+ {processedContent}
1588
1732
  </ReactMarkdown>
1589
1733
  </>
1590
1734
  )}
@@ -159,9 +159,7 @@ const AgentPanel: React.FC<AgentPanelProps & ExtraProps> = ({
159
159
  useEffect(() => {
160
160
  const fetchAgentData = async () => {
161
161
  try {
162
- const fetchUrl = url.endsWith("dev")
163
- ? `https://8ftw8droff.execute-api.us-east-1.amazonaws.com/dev/agents/${agent}`
164
- : `https://api.llmasaservice.io/agents/${agent}`;
162
+ const fetchUrl = `https://api.llmasaservice.io/agents/${agent}`;
165
163
 
166
164
  const response = await fetch(fetchUrl, {
167
165
  method: "GET",
package/src/ChatPanel.tsx CHANGED
@@ -597,13 +597,7 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
597
597
  };
598
598
 
599
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
- }
600
+ const publicAPIUrl = "https://api.llmasaservice.io";
607
601
 
608
602
  const [toolList, setToolList] = useState<any[]>([]);
609
603
  const [toolsLoading, setToolsLoading] = useState(false);
@@ -55,3 +55,5 @@ export default Button;
55
55
 
56
56
 
57
57
 
58
+
59
+
@@ -151,3 +151,5 @@ export default Dialog;
151
151
 
152
152
 
153
153
 
154
+
155
+
@@ -31,3 +31,5 @@ export default Input;
31
31
 
32
32
 
33
33
 
34
+
35
+
@@ -154,3 +154,5 @@ export default Select;
154
154
 
155
155
 
156
156
 
157
+
158
+
@@ -71,3 +71,5 @@ export default Tooltip;
71
71
 
72
72
 
73
73
 
74
+
75
+
@@ -18,3 +18,5 @@ export type { DialogProps, DialogFooterProps } from './Dialog';
18
18
 
19
19
 
20
20
 
21
+
22
+
@@ -69,10 +69,7 @@ interface UseAgentRegistryOptions {
69
69
  }
70
70
 
71
71
  const resolveApiEndpoint = (baseUrl: string, agentId: string): string => {
72
- const apiRoot = baseUrl.endsWith('dev')
73
- ? 'https://8ftw8droff.execute-api.us-east-1.amazonaws.com/dev'
74
- : 'https://api.llmasaservice.io';
75
- return `${apiRoot}/agents/${agentId}`;
72
+ return `https://api.llmasaservice.io/agents/${agentId}`;
76
73
  };
77
74
 
78
75
  /**