@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.
- package/AICHATPANEL-PORT-INVENTORY.md +2 -0
- package/CONTROLLED-COLLAPSE-IMPLEMENTATION.md +274 -0
- package/DEBUG-ERROR-HANDLING.md +2 -0
- package/FIX-APPLIED.md +2 -0
- package/IMPLEMENTATION-COMPLETE.md +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +78 -37
- package/dist/index.mjs +78 -37
- package/docs/CHANGELOG-ERROR-HANDLING.md +2 -0
- package/docs/CONTROLLED-COLLAPSE-QUICK-START.md +147 -0
- package/docs/CONTROLLED-COLLAPSE-STATE.md +651 -0
- package/docs/CONVERSATION-HISTORY.md +2 -0
- package/docs/ERROR-HANDLING-413.md +2 -0
- package/docs/ERROR-HANDLING-SUMMARY.md +2 -0
- package/package.json +1 -1
- package/src/AIAgentPanel.tsx +119 -53
- package/src/components/ui/Button.tsx +2 -0
- package/src/components/ui/Dialog.tsx +2 -0
- package/src/components/ui/Input.tsx +2 -0
- package/src/components/ui/Select.tsx +2 -0
- package/src/components/ui/ToolInfoModal.tsx +2 -0
- package/src/components/ui/Tooltip.tsx +2 -0
- package/src/components/ui/index.ts +2 -0
package/src/AIAgentPanel.tsx
CHANGED
|
@@ -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 [
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
//
|
|
1178
|
-
const
|
|
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
|
-
//
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1867
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|