@hef2024/llmasaservice-ui 0.20.3 → 0.22.0

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.
@@ -96,6 +96,8 @@ export interface AIAgentPanelProps {
96
96
  theme?: 'light' | 'dark';
97
97
  collapsible?: boolean;
98
98
  defaultCollapsed?: boolean;
99
+ isCollapsed?: boolean;
100
+ onCollapsedChange?: (isCollapsed: boolean) => void;
99
101
  defaultWidth?: number;
100
102
  minWidth?: number;
101
103
  maxWidth?: number;
@@ -618,6 +620,8 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
618
620
  theme = 'light',
619
621
  collapsible = true,
620
622
  defaultCollapsed = false,
623
+ isCollapsed: controlledIsCollapsed,
624
+ onCollapsedChange,
621
625
  defaultWidth = 720,
622
626
  minWidth = 520,
623
627
  maxWidth = 1200,
@@ -660,8 +664,36 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
660
664
  customerEmailCaptureMode,
661
665
  customerEmailCapturePlaceholder,
662
666
  }, ref) => {
667
+ // Dev mode warnings for prop conflicts
668
+ useEffect(() => {
669
+ if (process.env.NODE_ENV === 'development') {
670
+ // Warn if isCollapsed is provided without onCollapsedChange (user can't interact)
671
+ if (controlledIsCollapsed !== undefined && !onCollapsedChange) {
672
+ console.warn(
673
+ 'AIAgentPanel: You provided `isCollapsed` prop without `onCollapsedChange`. ' +
674
+ 'This will render a read-only collapsed state. To allow user interaction, provide both props.'
675
+ );
676
+ }
677
+
678
+ // Warn if both isCollapsed and defaultCollapsed are provided (conflicting props)
679
+ if (controlledIsCollapsed !== undefined && defaultCollapsed !== false) {
680
+ console.warn(
681
+ 'AIAgentPanel: You provided both `isCollapsed` and `defaultCollapsed` props. ' +
682
+ 'When using controlled mode (isCollapsed), the defaultCollapsed prop is ignored. ' +
683
+ 'Remove defaultCollapsed to avoid confusion.'
684
+ );
685
+ }
686
+ }
687
+ }, [controlledIsCollapsed, onCollapsedChange, defaultCollapsed]);
688
+
663
689
  // Panel state - only start collapsed if collapsible is true
664
- const [isCollapsed, setIsCollapsed] = useState(collapsible && defaultCollapsed);
690
+ const [uncontrolledIsCollapsed, setUncontrolledIsCollapsed] = useState(collapsible && defaultCollapsed);
691
+
692
+ // Determine if controlled
693
+ const isControlled = controlledIsCollapsed !== undefined;
694
+
695
+ // Use controlled value if provided, otherwise use internal state
696
+ const isCollapsed = isControlled ? controlledIsCollapsed : uncontrolledIsCollapsed;
665
697
  const [isHistoryCollapsed, setIsHistoryCollapsed] = useState(() => {
666
698
  if (typeof window !== 'undefined') {
667
699
  const saved = localStorage.getItem('ai-agent-panel-history-collapsed');
@@ -1092,28 +1124,6 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
1092
1124
  }, [apiKey, customerId, getAgent, fetchFirstPrompt]);
1093
1125
 
1094
1126
  // Helper to strip context/template data from a prompt - keeps only the user's actual message
1095
- const stripContextFromPrompt = useCallback((prompt: string): string => {
1096
- let cleanPrompt = prompt;
1097
-
1098
- // Strip ---context--- block and everything after it
1099
- const contextIndex = cleanPrompt.indexOf('---context---');
1100
- if (contextIndex !== -1) {
1101
- cleanPrompt = cleanPrompt.substring(0, contextIndex).trim();
1102
- }
1103
-
1104
- // Strip timestamp prefix (ISO format: 2024-01-01T00:00:00.000Z:)
1105
- const isoTimestampRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z:/;
1106
- if (isoTimestampRegex.test(cleanPrompt)) {
1107
- const colonIndex = cleanPrompt.indexOf(':', 19);
1108
- cleanPrompt = cleanPrompt.substring(colonIndex + 1);
1109
- } else if (/^\d+:/.test(cleanPrompt)) {
1110
- const colonIndex = cleanPrompt.indexOf(':');
1111
- cleanPrompt = cleanPrompt.substring(colonIndex + 1);
1112
- }
1113
-
1114
- return cleanPrompt.trim();
1115
- }, []);
1116
-
1117
1127
  // Load a specific conversation's transcript (or switch to it if already active)
1118
1128
  const loadConversationTranscript = useCallback(async (conversationId: string, agentIdForConversation?: string, title?: string) => {
1119
1129
  // Check if conversation is already in activeConversations (use ref to avoid dependency)
@@ -1160,40 +1170,75 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
1160
1170
  console.log('loadConversationTranscript - API response:', payload);
1161
1171
 
1162
1172
  // The /calls endpoint returns an array of calls
1163
- // Build history from all calls in chronological order
1164
- // IMPORTANT: Strip context/template data from prompts to prevent token bloat
1173
+ // Use the messages property from the LAST call, which contains the full conversation history
1165
1174
  const history: Record<string, { content: string; callId: string }> = {};
1166
1175
  let firstPrompt: string | null = null;
1167
1176
 
1168
1177
  if (Array.isArray(payload) && payload.length > 0) {
1169
- // Parse all calls in order
1170
- payload.forEach((call: any, index: number) => {
1171
- if (call.prompt && call.response) {
1172
- // Get the raw prompt text
1173
- const rawPrompt = typeof call.prompt === 'string'
1174
- ? call.prompt
1175
- : call.prompt[0]?.text || '';
1178
+ // Get the last call - it contains the full conversation history in the messages property
1179
+ const lastCall = payload[payload.length - 1];
1180
+ const callId = lastCall.id || '';
1181
+ const timestamp = lastCall.createdAt || lastCall.created_at || new Date().toISOString();
1182
+
1183
+ if (lastCall.messages) {
1184
+ try {
1185
+ // Parse the messages JSON string
1186
+ const messages = JSON.parse(lastCall.messages) as { role: string; content: string }[];
1187
+ console.log('loadConversationTranscript - parsed messages:', messages);
1176
1188
 
1177
- // Strip context/template data - keep only the user's actual message
1178
- const cleanedPrompt = stripContextFromPrompt(rawPrompt);
1189
+ // Filter out system messages and special __system__: user messages
1190
+ const relevantMessages = messages.filter(msg =>
1191
+ msg.role !== 'system' &&
1192
+ !(msg.role === 'user' && msg.content.startsWith('__system__:'))
1193
+ );
1194
+ console.log('loadConversationTranscript - filtered messages:', relevantMessages);
1179
1195
 
1180
- // Extract first prompt for display in list
1181
- if (index === 0 && cleanedPrompt) {
1182
- firstPrompt = cleanedPrompt.length > 60 ? cleanedPrompt.slice(0, 57) + '...' : cleanedPrompt;
1196
+ // Pair user/assistant messages to build history
1197
+ // Note: The messages array contains the INPUT to the LLM (history + new prompt)
1198
+ // The LAST user message needs to be paired with the call's response field
1199
+ for (let i = 0; i < relevantMessages.length; i++) {
1200
+ const msg = relevantMessages[i];
1201
+
1202
+ if (!msg) continue;
1203
+
1204
+ if (msg.role === 'user') {
1205
+ // Extract first prompt for conversation title
1206
+ if (!firstPrompt) {
1207
+ firstPrompt = msg.content.length > 60 ? msg.content.slice(0, 57) + '...' : msg.content;
1208
+ }
1209
+
1210
+ // Look for the next assistant message in the array
1211
+ const nextMsg = relevantMessages[i + 1];
1212
+
1213
+ if (nextMsg && nextMsg.role === 'assistant') {
1214
+ // This is a historical turn - pair the user message with the assistant message from the array
1215
+ const historyKey = `${timestamp}:${msg.content}`;
1216
+ history[historyKey] = {
1217
+ content: nextMsg.content,
1218
+ callId: callId,
1219
+ };
1220
+
1221
+ // Skip the assistant message we just processed
1222
+ i++;
1223
+ } else {
1224
+ // This is the LAST user message - pair it with the response field
1225
+ if (lastCall.response) {
1226
+ const historyKey = `${timestamp}:${msg.content}`;
1227
+ history[historyKey] = {
1228
+ content: lastCall.response,
1229
+ callId: callId,
1230
+ };
1231
+ }
1232
+ }
1233
+ }
1183
1234
  }
1184
-
1185
- // Use timestamp-prefixed key to maintain order and uniqueness
1186
- const timestamp = call.createdAt || call.created_at || new Date().toISOString();
1187
- const historyKey = `${timestamp}:${cleanedPrompt}`;
1188
-
1189
- history[historyKey] = {
1190
- content: call.response,
1191
- callId: call.id || '',
1192
- };
1235
+ } catch (err) {
1236
+ console.error('loadConversationTranscript - failed to parse messages property:', err);
1193
1237
  }
1194
- });
1238
+ }
1195
1239
  }
1196
1240
 
1241
+ console.log('loadConversationTranscript - created', Object.keys(history).length, 'history entries');
1197
1242
  console.log('loadConversationTranscript - parsed history:', history);
1198
1243
 
1199
1244
  // Update first prompt in state if we found one
@@ -1234,7 +1279,7 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
1234
1279
  // Clear loading state on error
1235
1280
  setLoadingConversationId(null);
1236
1281
  }
1237
- }, [apiKey, currentAgentId, getAgent, onConversationChange, stripContextFromPrompt]);
1282
+ }, [apiKey, currentAgentId, getAgent, onConversationChange]);
1238
1283
 
1239
1284
 
1240
1285
  // Refresh conversations callback
@@ -1863,8 +1908,17 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
1863
1908
  // Toggle collapse - only if collapsible is enabled
1864
1909
  const toggleCollapse = useCallback(() => {
1865
1910
  if (!collapsible) return;
1866
- setIsCollapsed((prev) => !prev);
1867
- }, [collapsible]);
1911
+
1912
+ const newValue = !isCollapsed;
1913
+
1914
+ // Update internal state if uncontrolled
1915
+ if (!isControlled) {
1916
+ setUncontrolledIsCollapsed(newValue);
1917
+ }
1918
+
1919
+ // Call callback if provided
1920
+ onCollapsedChange?.(newValue);
1921
+ }, [collapsible, isCollapsed, isControlled, onCollapsedChange]);
1868
1922
 
1869
1923
  // Toggle history collapse
1870
1924
  const toggleHistoryCollapse = useCallback(() => {
@@ -1914,7 +1968,11 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
1914
1968
  variant="ghost"
1915
1969
  size="icon"
1916
1970
  onClick={() => {
1917
- setIsCollapsed(false);
1971
+ // Expand panel in controlled or uncontrolled mode
1972
+ if (!isControlled) {
1973
+ setUncontrolledIsCollapsed(false);
1974
+ }
1975
+ onCollapsedChange?.(false);
1918
1976
  setShowSearch(true);
1919
1977
  }}
1920
1978
  >
@@ -1928,7 +1986,11 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
1928
1986
  variant="ghost"
1929
1987
  size="icon"
1930
1988
  onClick={() => {
1931
- setIsCollapsed(false);
1989
+ // Expand panel in controlled or uncontrolled mode
1990
+ if (!isControlled) {
1991
+ setUncontrolledIsCollapsed(false);
1992
+ }
1993
+ onCollapsedChange?.(false);
1932
1994
  handleNewConversation();
1933
1995
  }}
1934
1996
  >
@@ -1951,7 +2013,11 @@ const AIAgentPanel = React.forwardRef<AIAgentPanelHandle, AIAgentPanelProps>(({
1951
2013
  variant={agent.id === currentAgentId ? 'secondary' : 'ghost'}
1952
2014
  size="icon"
1953
2015
  onClick={() => {
1954
- setIsCollapsed(false);
2016
+ // Expand panel in controlled or uncontrolled mode
2017
+ if (!isControlled) {
2018
+ setUncontrolledIsCollapsed(false);
2019
+ }
2020
+ onCollapsedChange?.(false);
1955
2021
  if (hasActiveConversation && activeConvForAgent) {
1956
2022
  // Switch to the existing conversation
1957
2023
  setCurrentConversationId(activeConvForAgent.conversationId);
@@ -60,3 +60,5 @@ export default Button;
60
60
 
61
61
 
62
62
 
63
+
64
+
@@ -156,3 +156,5 @@ export default Dialog;
156
156
 
157
157
 
158
158
 
159
+
160
+
@@ -36,3 +36,5 @@ export default Input;
36
36
 
37
37
 
38
38
 
39
+
40
+
@@ -159,3 +159,5 @@ export default Select;
159
159
 
160
160
 
161
161
 
162
+
163
+
@@ -67,3 +67,5 @@ export default ToolInfoModal;
67
67
 
68
68
 
69
69
 
70
+
71
+
@@ -76,3 +76,5 @@ export default Tooltip;
76
76
 
77
77
 
78
78
 
79
+
80
+
@@ -23,3 +23,5 @@ export type { DialogProps, DialogFooterProps } from './Dialog';
23
23
 
24
24
 
25
25
 
26
+
27
+