@hef2024/llmasaservice-ui 0.25.2 → 0.26.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/dist/index.css +439 -2
- package/dist/index.d.mts +68 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.js +832 -159
- package/dist/index.mjs +832 -159
- package/hef2024-llmasaservice-ui-0.25.3.tgz +0 -0
- package/index.ts +5 -0
- package/package.json +12 -1
- package/src/AIAgentPanel.css +16 -2
- package/src/AIAgentPanel.tsx +122 -4
- package/src/AIChatPanel.css +497 -1
- package/src/AIChatPanel.tsx +1085 -223
package/src/AIChatPanel.tsx
CHANGED
|
@@ -52,6 +52,41 @@ export interface BeforeSendPayload {
|
|
|
52
52
|
messages: { role: string; content: string }[];
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export type CompactionAction = 'none' | 'compacted' | 'fallback_window';
|
|
56
|
+
|
|
57
|
+
export interface CompactionTokenUsage {
|
|
58
|
+
projectedBefore: number;
|
|
59
|
+
projectedAfter: number;
|
|
60
|
+
warnThreshold: number;
|
|
61
|
+
compactThreshold: number;
|
|
62
|
+
targetThreshold: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CompactContextInput {
|
|
66
|
+
prompt: string;
|
|
67
|
+
conversationId: string | null;
|
|
68
|
+
agentId?: string | null;
|
|
69
|
+
service?: string | null;
|
|
70
|
+
messages: { role: string; content: string }[];
|
|
71
|
+
data: { key: string; data: string }[];
|
|
72
|
+
maxContextTokens: number;
|
|
73
|
+
totalContextTokens: number;
|
|
74
|
+
warnRatio: number;
|
|
75
|
+
compactRatio: number;
|
|
76
|
+
targetRatio: number;
|
|
77
|
+
preserveTurns: number;
|
|
78
|
+
projectedTokens: CompactionTokenUsage;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CompactContextResult {
|
|
82
|
+
action: CompactionAction;
|
|
83
|
+
prompt: string;
|
|
84
|
+
messages: { role: string; content: string }[];
|
|
85
|
+
warning?: string | null;
|
|
86
|
+
tokenUsage?: Partial<CompactionTokenUsage> | null;
|
|
87
|
+
metadata?: Record<string, unknown> | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
55
90
|
export type TraceContextMode = 'standard' | 'full';
|
|
56
91
|
|
|
57
92
|
export interface LocalToolExecutorContext {
|
|
@@ -169,6 +204,8 @@ export interface AIChatPanelProps {
|
|
|
169
204
|
|
|
170
205
|
// Callback invoked before each send() call
|
|
171
206
|
onBeforeSend?: (payload: BeforeSendPayload) => Promise<void> | void;
|
|
207
|
+
compactContext?: (input: CompactContextInput) => Promise<CompactContextResult> | CompactContextResult;
|
|
208
|
+
compactionPreserveTurns?: number;
|
|
172
209
|
|
|
173
210
|
// UI Customization Props (from ChatPanel)
|
|
174
211
|
cssUrl?: string;
|
|
@@ -193,6 +230,7 @@ export interface AIChatPanelProps {
|
|
|
193
230
|
customerEmailCaptureMode?: "HIDE" | "OPTIONAL" | "REQUIRED";
|
|
194
231
|
customerEmailCapturePlaceholder?: string;
|
|
195
232
|
toolStatusLabelFormatter?: ToolStatusLabelFormatter;
|
|
233
|
+
composerAgentModeControl?: ComposerAgentModeControl;
|
|
196
234
|
}
|
|
197
235
|
|
|
198
236
|
/**
|
|
@@ -207,8 +245,122 @@ export interface ContextSection {
|
|
|
207
245
|
rawData?: string;
|
|
208
246
|
}
|
|
209
247
|
|
|
248
|
+
export interface ComposerAgentModeControl {
|
|
249
|
+
active?: boolean;
|
|
250
|
+
disabled?: boolean;
|
|
251
|
+
label?: string;
|
|
252
|
+
title?: string;
|
|
253
|
+
onActivate?: () => void;
|
|
254
|
+
}
|
|
255
|
+
|
|
210
256
|
export type ContextDataFormat = 'json' | 'toon' | 'markdown' | 'text';
|
|
211
257
|
|
|
258
|
+
type UserInputOption = {
|
|
259
|
+
value: string;
|
|
260
|
+
label: string;
|
|
261
|
+
description?: string;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
type UserInputQuestion = {
|
|
265
|
+
id: string;
|
|
266
|
+
header: string;
|
|
267
|
+
question: string;
|
|
268
|
+
required: boolean;
|
|
269
|
+
allowWriteIn: boolean;
|
|
270
|
+
writeInPlaceholder: string;
|
|
271
|
+
options: UserInputOption[];
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
type UserInputRequest = {
|
|
275
|
+
requestId: string;
|
|
276
|
+
title: string;
|
|
277
|
+
instructions: string;
|
|
278
|
+
submitLabel: string;
|
|
279
|
+
cancelLabel: string;
|
|
280
|
+
allowCancel: boolean;
|
|
281
|
+
questions: UserInputQuestion[];
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
type UserInputAnswer = {
|
|
285
|
+
id: string;
|
|
286
|
+
header: string;
|
|
287
|
+
question: string;
|
|
288
|
+
selectedOptionValue: string | null;
|
|
289
|
+
selectedOptionLabel: string | null;
|
|
290
|
+
writeIn: string | null;
|
|
291
|
+
answer: string;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
type UserInputCompletionPayload = {
|
|
295
|
+
status: 'submitted' | 'cancelled';
|
|
296
|
+
requestId: string;
|
|
297
|
+
answers?: UserInputAnswer[];
|
|
298
|
+
cancelledBy?: 'user' | 'stop' | 'reset' | 'unmount';
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const USER_INPUT_TOOL_NAMES = new Set(['request_user_input', 'ff_request_user_input']);
|
|
302
|
+
const MAX_USER_INPUT_QUESTIONS = 5;
|
|
303
|
+
const MAX_USER_INPUT_OPTIONS = 8;
|
|
304
|
+
const MAX_USER_INPUT_WRITE_IN_CHARS = 1000;
|
|
305
|
+
const DEFAULT_USER_INPUT_TITLE = 'Need your input';
|
|
306
|
+
const DEFAULT_USER_INPUT_INSTRUCTIONS = 'Answer these quick questions to continue.';
|
|
307
|
+
|
|
308
|
+
const DEFAULT_CONTEXT_WARN_RATIO = 0.7;
|
|
309
|
+
const DEFAULT_CONTEXT_COMPACT_RATIO = 0.9;
|
|
310
|
+
const DEFAULT_CONTEXT_TARGET_RATIO = 0.65;
|
|
311
|
+
const DEFAULT_COMPACTION_PRESERVE_TURNS = 8;
|
|
312
|
+
|
|
313
|
+
function toBoundedRatio(value: number, fallback: number): number {
|
|
314
|
+
if (!Number.isFinite(value)) return fallback;
|
|
315
|
+
if (value <= 0) return fallback;
|
|
316
|
+
if (value >= 1) return 1;
|
|
317
|
+
return value;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function estimateTokensFromText(text: string): number {
|
|
321
|
+
const normalized = typeof text === 'string' ? text : '';
|
|
322
|
+
if (!normalized) return 0;
|
|
323
|
+
return Math.ceil(normalized.length / 4);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function estimateTokensFromMessages(messages: Array<{ role: string; content: string }>): number {
|
|
327
|
+
if (!Array.isArray(messages) || messages.length === 0) return 0;
|
|
328
|
+
return messages.reduce((sum, message) => {
|
|
329
|
+
const role = typeof message?.role === 'string' ? message.role : '';
|
|
330
|
+
const content = typeof message?.content === 'string' ? message.content : '';
|
|
331
|
+
return sum + estimateTokensFromText(role) + estimateTokensFromText(content);
|
|
332
|
+
}, 0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function estimateTokensFromData(data: Array<{ key: string; data: string }>): number {
|
|
336
|
+
if (!Array.isArray(data) || data.length === 0) return 0;
|
|
337
|
+
return data.reduce((sum, entry) => {
|
|
338
|
+
const key = typeof entry?.key === 'string' ? entry.key : '';
|
|
339
|
+
const value = typeof entry?.data === 'string' ? entry.data : '';
|
|
340
|
+
return sum + estimateTokensFromText(key) + estimateTokensFromText(value);
|
|
341
|
+
}, 0);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function formatTokenCount(tokens: number): string {
|
|
345
|
+
const safeTokens = Number.isFinite(tokens) ? Math.max(0, Math.floor(tokens)) : 0;
|
|
346
|
+
if (safeTokens >= 1000) {
|
|
347
|
+
return `${(safeTokens / 1000).toFixed(1)}K`;
|
|
348
|
+
}
|
|
349
|
+
return safeTokens.toString();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function buildFallbackWindowMessages(
|
|
353
|
+
messages: Array<{ role: string; content: string }>,
|
|
354
|
+
preserveTurns: number,
|
|
355
|
+
): Array<{ role: string; content: string }> {
|
|
356
|
+
if (!Array.isArray(messages) || messages.length === 0) return [];
|
|
357
|
+
const keepMessageCount = Math.max(2, Math.floor(preserveTurns) * 2);
|
|
358
|
+
if (messages.length <= keepMessageCount) {
|
|
359
|
+
return messages;
|
|
360
|
+
}
|
|
361
|
+
return messages.slice(messages.length - keepMessageCount);
|
|
362
|
+
}
|
|
363
|
+
|
|
212
364
|
type ThinkingBlock = ResponseArtifactBlock;
|
|
213
365
|
|
|
214
366
|
interface ToolRequestMatch {
|
|
@@ -342,6 +494,158 @@ const shouldPreserveBoundaryDroppedStreamText = (
|
|
|
342
494
|
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
|
343
495
|
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
344
496
|
|
|
497
|
+
const toTrimmedString = (value: unknown): string =>
|
|
498
|
+
typeof value === 'string' ? value.trim() : '';
|
|
499
|
+
|
|
500
|
+
const toBooleanWithDefault = (value: unknown, fallback: boolean): boolean =>
|
|
501
|
+
typeof value === 'boolean' ? value : fallback;
|
|
502
|
+
|
|
503
|
+
const parseMaybeJsonString = (value: unknown): unknown => {
|
|
504
|
+
if (typeof value !== 'string') return value;
|
|
505
|
+
const trimmed = value.trim();
|
|
506
|
+
if (!trimmed) return value;
|
|
507
|
+
if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) return value;
|
|
508
|
+
try {
|
|
509
|
+
return JSON.parse(trimmed);
|
|
510
|
+
} catch (_error) {
|
|
511
|
+
return value;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const toArrayFromUnknown = (value: unknown): unknown[] => {
|
|
516
|
+
const parsed = parseMaybeJsonString(value);
|
|
517
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const toSlugValue = (value: string, fallback: string): string => {
|
|
521
|
+
const normalized = value
|
|
522
|
+
.toLowerCase()
|
|
523
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
524
|
+
.replace(/^-+|-+$/g, '');
|
|
525
|
+
return normalized || fallback;
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const normalizeUserInputOptionEntry = (
|
|
529
|
+
value: unknown,
|
|
530
|
+
optionIndex: number,
|
|
531
|
+
questionId: string,
|
|
532
|
+
): UserInputOption | null => {
|
|
533
|
+
const normalizedValue = parseMaybeJsonString(value);
|
|
534
|
+
if (typeof normalizedValue === 'string') {
|
|
535
|
+
const label = normalizedValue.trim();
|
|
536
|
+
if (!label) return null;
|
|
537
|
+
return {
|
|
538
|
+
label,
|
|
539
|
+
value: toSlugValue(label, `${questionId}-option-${optionIndex + 1}`),
|
|
540
|
+
description: '',
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (!isObjectRecord(normalizedValue)) return null;
|
|
544
|
+
const label =
|
|
545
|
+
toTrimmedString(normalizedValue.label) ||
|
|
546
|
+
toTrimmedString(normalizedValue.title) ||
|
|
547
|
+
toTrimmedString(normalizedValue.value);
|
|
548
|
+
if (!label) return null;
|
|
549
|
+
const explicitValue = toTrimmedString(normalizedValue.value);
|
|
550
|
+
return {
|
|
551
|
+
label,
|
|
552
|
+
value: explicitValue || toSlugValue(label, `${questionId}-option-${optionIndex + 1}`),
|
|
553
|
+
description: toTrimmedString(normalizedValue.description),
|
|
554
|
+
};
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const normalizeUserInputQuestionEntry = (
|
|
558
|
+
value: unknown,
|
|
559
|
+
questionIndex: number,
|
|
560
|
+
): UserInputQuestion | null => {
|
|
561
|
+
const normalizedValue = parseMaybeJsonString(value);
|
|
562
|
+
if (!isObjectRecord(normalizedValue)) return null;
|
|
563
|
+
const fallbackId = `question_${questionIndex + 1}`;
|
|
564
|
+
const id = toSlugValue(
|
|
565
|
+
toTrimmedString(normalizedValue.id) || toTrimmedString(normalizedValue.key) || fallbackId,
|
|
566
|
+
fallbackId,
|
|
567
|
+
);
|
|
568
|
+
const header = toTrimmedString(normalizedValue.header) || `Question ${questionIndex + 1}`;
|
|
569
|
+
const question =
|
|
570
|
+
toTrimmedString(normalizedValue.question) ||
|
|
571
|
+
toTrimmedString(normalizedValue.prompt) ||
|
|
572
|
+
toTrimmedString(normalizedValue.text);
|
|
573
|
+
if (!question) return null;
|
|
574
|
+
|
|
575
|
+
const rawOptions = toArrayFromUnknown(normalizedValue.options);
|
|
576
|
+
const normalizedOptions = rawOptions
|
|
577
|
+
.map((entry, optionIndex) => normalizeUserInputOptionEntry(entry, optionIndex, id))
|
|
578
|
+
.filter((entry): entry is UserInputOption => Boolean(entry))
|
|
579
|
+
.slice(0, MAX_USER_INPUT_OPTIONS);
|
|
580
|
+
if (normalizedOptions.length === 0) {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const parsedWriteInConfig = parseMaybeJsonString(normalizedValue.writeIn);
|
|
585
|
+
const writeInConfig = isObjectRecord(parsedWriteInConfig) ? parsedWriteInConfig : null;
|
|
586
|
+
const allowWriteIn =
|
|
587
|
+
toBooleanWithDefault(normalizedValue.allowWriteIn, false) ||
|
|
588
|
+
toBooleanWithDefault(writeInConfig?.enabled, false);
|
|
589
|
+
const writeInPlaceholder =
|
|
590
|
+
toTrimmedString(normalizedValue.writeInPlaceholder) ||
|
|
591
|
+
toTrimmedString(writeInConfig?.placeholder) ||
|
|
592
|
+
'Add details';
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
id,
|
|
596
|
+
header,
|
|
597
|
+
question,
|
|
598
|
+
required: toBooleanWithDefault(normalizedValue.required, true),
|
|
599
|
+
allowWriteIn,
|
|
600
|
+
writeInPlaceholder,
|
|
601
|
+
options: normalizedOptions,
|
|
602
|
+
};
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const normalizeUserInputRequest = (
|
|
606
|
+
args: Record<string, unknown>,
|
|
607
|
+
callId: string,
|
|
608
|
+
): UserInputRequest => {
|
|
609
|
+
const rawQuestions = toArrayFromUnknown(args.questions);
|
|
610
|
+
const fallbackQuestion =
|
|
611
|
+
toTrimmedString(args.question) || toTrimmedString(args.prompt) || toTrimmedString(args.text);
|
|
612
|
+
const fallbackOptions = toArrayFromUnknown(args.options);
|
|
613
|
+
const baseQuestions =
|
|
614
|
+
rawQuestions.length > 0
|
|
615
|
+
? rawQuestions
|
|
616
|
+
: fallbackQuestion
|
|
617
|
+
? [
|
|
618
|
+
{
|
|
619
|
+
id: 'question_1',
|
|
620
|
+
header: 'Question 1',
|
|
621
|
+
question: fallbackQuestion,
|
|
622
|
+
options: fallbackOptions,
|
|
623
|
+
allowWriteIn: toBooleanWithDefault(args.allowWriteIn, false),
|
|
624
|
+
writeInPlaceholder: toTrimmedString(args.writeInPlaceholder),
|
|
625
|
+
},
|
|
626
|
+
]
|
|
627
|
+
: [];
|
|
628
|
+
|
|
629
|
+
const questions = baseQuestions
|
|
630
|
+
.map((entry, index) => normalizeUserInputQuestionEntry(entry, index))
|
|
631
|
+
.filter((entry): entry is UserInputQuestion => Boolean(entry))
|
|
632
|
+
.slice(0, MAX_USER_INPUT_QUESTIONS);
|
|
633
|
+
|
|
634
|
+
if (questions.length === 0) {
|
|
635
|
+
throw new Error('request_user_input requires at least one valid question with options.');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
requestId: toTrimmedString(args.requestId) || toTrimmedString(args.id) || callId,
|
|
640
|
+
title: toTrimmedString(args.title) || DEFAULT_USER_INPUT_TITLE,
|
|
641
|
+
instructions: toTrimmedString(args.instructions) || DEFAULT_USER_INPUT_INSTRUCTIONS,
|
|
642
|
+
submitLabel: toTrimmedString(args.submitLabel) || 'Submit',
|
|
643
|
+
cancelLabel: toTrimmedString(args.cancelLabel) || 'Cancel',
|
|
644
|
+
allowCancel: toBooleanWithDefault(args.allowCancel, true),
|
|
645
|
+
questions,
|
|
646
|
+
};
|
|
647
|
+
};
|
|
648
|
+
|
|
345
649
|
const stringifyToolArgs = (value: unknown): string => {
|
|
346
650
|
if (typeof value === 'string') return value;
|
|
347
651
|
try {
|
|
@@ -1408,6 +1712,8 @@ interface ChatInputProps {
|
|
|
1408
1712
|
onSubmit: (text: string) => void;
|
|
1409
1713
|
onQueueSubmit: (text: string) => void;
|
|
1410
1714
|
onStop: () => void;
|
|
1715
|
+
userInputRequest?: UserInputRequest | null;
|
|
1716
|
+
onCompleteUserInputRequest?: (payload: UserInputCompletionPayload) => void;
|
|
1411
1717
|
queuedPrompts?: string[];
|
|
1412
1718
|
onClearQueuedPrompt: (index: number) => void;
|
|
1413
1719
|
agentOptions: AgentOption[];
|
|
@@ -1424,6 +1730,7 @@ interface ChatInputProps {
|
|
|
1424
1730
|
disabledSectionIds?: Set<string>;
|
|
1425
1731
|
onToggleSection?: (sectionId: string, enabled: boolean) => void;
|
|
1426
1732
|
onContextViewerToggle?: () => void;
|
|
1733
|
+
composerAgentModeControl?: ComposerAgentModeControl;
|
|
1427
1734
|
}
|
|
1428
1735
|
|
|
1429
1736
|
const ChatInput = React.memo<ChatInputProps>(({
|
|
@@ -1432,6 +1739,8 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
1432
1739
|
onSubmit,
|
|
1433
1740
|
onQueueSubmit,
|
|
1434
1741
|
onStop,
|
|
1742
|
+
userInputRequest = null,
|
|
1743
|
+
onCompleteUserInputRequest,
|
|
1435
1744
|
queuedPrompts = [],
|
|
1436
1745
|
onClearQueuedPrompt,
|
|
1437
1746
|
agentOptions,
|
|
@@ -1446,6 +1755,7 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
1446
1755
|
enableContextDetailView = false,
|
|
1447
1756
|
disabledSectionIds = new Set(),
|
|
1448
1757
|
onToggleSection,
|
|
1758
|
+
composerAgentModeControl,
|
|
1449
1759
|
}) => {
|
|
1450
1760
|
const [inputValue, setInputValue] = useState('');
|
|
1451
1761
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
@@ -1455,6 +1765,65 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
1455
1765
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
1456
1766
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
1457
1767
|
const contextPopoverRef = useRef<HTMLDivElement | null>(null);
|
|
1768
|
+
const [userInputSelections, setUserInputSelections] = useState<Record<string, string>>({});
|
|
1769
|
+
const [userInputWriteIns, setUserInputWriteIns] = useState<Record<string, string>>({});
|
|
1770
|
+
const [userInputValidationError, setUserInputValidationError] = useState('');
|
|
1771
|
+
const [userInputQuestionIndex, setUserInputQuestionIndex] = useState(0);
|
|
1772
|
+
const [agentModeChipExpanded, setAgentModeChipExpanded] = useState(false);
|
|
1773
|
+
const isAgentModeActionVisible = composerAgentModeControl !== undefined;
|
|
1774
|
+
const isAgentModeActive = Boolean(composerAgentModeControl?.active);
|
|
1775
|
+
const isAgentModeActionDisabled = Boolean(composerAgentModeControl?.disabled);
|
|
1776
|
+
const agentModeLabel = String(composerAgentModeControl?.label || 'Agent').trim() || 'Agent';
|
|
1777
|
+
const agentModeTitle =
|
|
1778
|
+
String(composerAgentModeControl?.title || '').trim() ||
|
|
1779
|
+
(isAgentModeActive ? 'Agent Mode active for this session' : 'Enable Agent Mode');
|
|
1780
|
+
|
|
1781
|
+
useEffect(() => {
|
|
1782
|
+
if (isAgentModeActionVisible) return;
|
|
1783
|
+
setAgentModeChipExpanded(false);
|
|
1784
|
+
}, [isAgentModeActionVisible]);
|
|
1785
|
+
|
|
1786
|
+
useEffect(() => {
|
|
1787
|
+
if (!isAgentModeActive) return;
|
|
1788
|
+
setAgentModeChipExpanded(true);
|
|
1789
|
+
}, [isAgentModeActive]);
|
|
1790
|
+
|
|
1791
|
+
const handleAgentModePillClick = useCallback(() => {
|
|
1792
|
+
if (!isAgentModeActionVisible || isAgentModeActionDisabled) return;
|
|
1793
|
+
if (isAgentModeActive) {
|
|
1794
|
+
setAgentModeChipExpanded(true);
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
setAgentModeChipExpanded(true);
|
|
1798
|
+
if (typeof composerAgentModeControl?.onActivate === 'function') {
|
|
1799
|
+
composerAgentModeControl.onActivate();
|
|
1800
|
+
}
|
|
1801
|
+
}, [
|
|
1802
|
+
composerAgentModeControl,
|
|
1803
|
+
isAgentModeActionDisabled,
|
|
1804
|
+
isAgentModeActionVisible,
|
|
1805
|
+
isAgentModeActive,
|
|
1806
|
+
]);
|
|
1807
|
+
|
|
1808
|
+
useEffect(() => {
|
|
1809
|
+
if (!userInputRequest) {
|
|
1810
|
+
setUserInputSelections({});
|
|
1811
|
+
setUserInputWriteIns({});
|
|
1812
|
+
setUserInputValidationError('');
|
|
1813
|
+
setUserInputQuestionIndex(0);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
const nextSelections: Record<string, string> = {};
|
|
1817
|
+
const nextWriteIns: Record<string, string> = {};
|
|
1818
|
+
userInputRequest.questions.forEach((question) => {
|
|
1819
|
+
nextSelections[question.id] = '';
|
|
1820
|
+
nextWriteIns[question.id] = '';
|
|
1821
|
+
});
|
|
1822
|
+
setUserInputSelections(nextSelections);
|
|
1823
|
+
setUserInputWriteIns(nextWriteIns);
|
|
1824
|
+
setUserInputValidationError('');
|
|
1825
|
+
setUserInputQuestionIndex(0);
|
|
1826
|
+
}, [userInputRequest]);
|
|
1458
1827
|
|
|
1459
1828
|
// Auto-resize textarea
|
|
1460
1829
|
const autoResize = useCallback(() => {
|
|
@@ -1562,12 +1931,221 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
1562
1931
|
const hasQueuedPrompts = normalizedQueuedPrompts.length > 0;
|
|
1563
1932
|
const shouldQueueSubmission = isBusy || hasQueuedPrompts;
|
|
1564
1933
|
const showStopAction = isBusy && !hasDraft;
|
|
1934
|
+
const composerLockedByUserInput = Boolean(userInputRequest);
|
|
1935
|
+
const totalUserInputQuestions = userInputRequest?.questions.length || 0;
|
|
1936
|
+
const boundedUserInputQuestionIndex =
|
|
1937
|
+
totalUserInputQuestions > 0
|
|
1938
|
+
? Math.min(Math.max(userInputQuestionIndex, 0), totalUserInputQuestions - 1)
|
|
1939
|
+
: 0;
|
|
1940
|
+
const activeUserInputQuestion =
|
|
1941
|
+
totalUserInputQuestions > 0 ? userInputRequest?.questions[boundedUserInputQuestionIndex] || null : null;
|
|
1942
|
+
const isFirstUserInputQuestion = boundedUserInputQuestionIndex <= 0;
|
|
1943
|
+
const isLastUserInputQuestion = totalUserInputQuestions > 0 && boundedUserInputQuestionIndex >= totalUserInputQuestions - 1;
|
|
1944
|
+
|
|
1945
|
+
const getQuestionAnswerState = useCallback((question: UserInputQuestion) => {
|
|
1946
|
+
const selectedValue = toTrimmedString(userInputSelections[question.id]);
|
|
1947
|
+
const writeInRaw = toTrimmedString(userInputWriteIns[question.id]);
|
|
1948
|
+
const writeIn = writeInRaw.slice(0, MAX_USER_INPUT_WRITE_IN_CHARS);
|
|
1949
|
+
const selectedOption = question.options.find((option) => option.value === selectedValue) || null;
|
|
1950
|
+
const hasAnswer = Boolean(selectedOption) || (question.allowWriteIn && writeIn.length > 0);
|
|
1951
|
+
return {
|
|
1952
|
+
selectedOption,
|
|
1953
|
+
writeIn,
|
|
1954
|
+
hasAnswer,
|
|
1955
|
+
};
|
|
1956
|
+
}, [userInputSelections, userInputWriteIns]);
|
|
1957
|
+
|
|
1958
|
+
const validateUserInputQuestion = useCallback((question: UserInputQuestion): boolean => {
|
|
1959
|
+
const answerState = getQuestionAnswerState(question);
|
|
1960
|
+
if (question.required && !answerState.hasAnswer) {
|
|
1961
|
+
setUserInputValidationError(`Please answer "${question.header}".`);
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
return true;
|
|
1965
|
+
}, [getQuestionAnswerState]);
|
|
1966
|
+
|
|
1967
|
+
const handleUserInputNext = useCallback(() => {
|
|
1968
|
+
if (!activeUserInputQuestion || isLastUserInputQuestion) return;
|
|
1969
|
+
if (!validateUserInputQuestion(activeUserInputQuestion)) return;
|
|
1970
|
+
setUserInputValidationError('');
|
|
1971
|
+
setUserInputQuestionIndex((prev) => Math.min(prev + 1, totalUserInputQuestions - 1));
|
|
1972
|
+
}, [activeUserInputQuestion, isLastUserInputQuestion, totalUserInputQuestions, validateUserInputQuestion]);
|
|
1973
|
+
|
|
1974
|
+
const handleUserInputBack = useCallback(() => {
|
|
1975
|
+
setUserInputValidationError('');
|
|
1976
|
+
setUserInputQuestionIndex((prev) => Math.max(prev - 1, 0));
|
|
1977
|
+
}, []);
|
|
1978
|
+
|
|
1979
|
+
const handleUserInputSubmit = useCallback(() => {
|
|
1980
|
+
if (!userInputRequest || typeof onCompleteUserInputRequest !== 'function') {
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
const answers: UserInputAnswer[] = [];
|
|
1985
|
+
for (let questionIndex = 0; questionIndex < userInputRequest.questions.length; questionIndex += 1) {
|
|
1986
|
+
const question = userInputRequest.questions[questionIndex]!;
|
|
1987
|
+
const answerState = getQuestionAnswerState(question);
|
|
1988
|
+
if (question.required && !answerState.hasAnswer) {
|
|
1989
|
+
setUserInputQuestionIndex(questionIndex);
|
|
1990
|
+
setUserInputValidationError(`Please answer "${question.header}".`);
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
answers.push({
|
|
1995
|
+
id: question.id,
|
|
1996
|
+
header: question.header,
|
|
1997
|
+
question: question.question,
|
|
1998
|
+
selectedOptionValue: answerState.selectedOption?.value || null,
|
|
1999
|
+
selectedOptionLabel: answerState.selectedOption?.label || null,
|
|
2000
|
+
writeIn: answerState.writeIn || null,
|
|
2001
|
+
answer: answerState.writeIn || answerState.selectedOption?.label || '',
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
setUserInputValidationError('');
|
|
2006
|
+
onCompleteUserInputRequest({
|
|
2007
|
+
status: 'submitted',
|
|
2008
|
+
requestId: userInputRequest.requestId,
|
|
2009
|
+
answers,
|
|
2010
|
+
});
|
|
2011
|
+
}, [getQuestionAnswerState, onCompleteUserInputRequest, userInputRequest]);
|
|
2012
|
+
|
|
2013
|
+
const handleUserInputCancel = useCallback(() => {
|
|
2014
|
+
if (!userInputRequest || typeof onCompleteUserInputRequest !== 'function') {
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
onCompleteUserInputRequest({
|
|
2018
|
+
status: 'cancelled',
|
|
2019
|
+
requestId: userInputRequest.requestId,
|
|
2020
|
+
cancelledBy: 'user',
|
|
2021
|
+
});
|
|
2022
|
+
}, [onCompleteUserInputRequest, userInputRequest]);
|
|
1565
2023
|
|
|
1566
2024
|
return (
|
|
1567
2025
|
<div
|
|
1568
2026
|
className={`ai-chat-panel__input-container ${dropdownOpen ? 'ai-chat-panel__input-container--dropdown-open' : ''}`}
|
|
1569
2027
|
ref={containerRef}
|
|
1570
2028
|
>
|
|
2029
|
+
{userInputRequest && (
|
|
2030
|
+
<div className="ai-chat-user-input-popover" role="dialog" aria-live="polite" aria-label={userInputRequest.title}>
|
|
2031
|
+
<div className="ai-chat-user-input-popover__header">
|
|
2032
|
+
<div className="ai-chat-user-input-popover__title">{userInputRequest.title}</div>
|
|
2033
|
+
<div className="ai-chat-user-input-popover__header-meta">
|
|
2034
|
+
{totalUserInputQuestions > 1 && (
|
|
2035
|
+
<span className="ai-chat-user-input-popover__progress">
|
|
2036
|
+
{boundedUserInputQuestionIndex + 1} of {totalUserInputQuestions}
|
|
2037
|
+
</span>
|
|
2038
|
+
)}
|
|
2039
|
+
{userInputRequest.allowCancel && (
|
|
2040
|
+
<button
|
|
2041
|
+
type="button"
|
|
2042
|
+
className="ai-chat-user-input-popover__cancel-inline"
|
|
2043
|
+
onClick={handleUserInputCancel}
|
|
2044
|
+
>
|
|
2045
|
+
{userInputRequest.cancelLabel}
|
|
2046
|
+
</button>
|
|
2047
|
+
)}
|
|
2048
|
+
</div>
|
|
2049
|
+
</div>
|
|
2050
|
+
{userInputRequest.instructions && (
|
|
2051
|
+
<div className="ai-chat-user-input-popover__instructions">{userInputRequest.instructions}</div>
|
|
2052
|
+
)}
|
|
2053
|
+
{activeUserInputQuestion && (
|
|
2054
|
+
<div className="ai-chat-user-input-popover__question">
|
|
2055
|
+
<div className="ai-chat-user-input-popover__question-header">
|
|
2056
|
+
<span className="ai-chat-user-input-popover__question-index">{boundedUserInputQuestionIndex + 1}</span>
|
|
2057
|
+
<span className="ai-chat-user-input-popover__question-title">{activeUserInputQuestion.header}</span>
|
|
2058
|
+
</div>
|
|
2059
|
+
<div className="ai-chat-user-input-popover__question-text">{activeUserInputQuestion.question}</div>
|
|
2060
|
+
<div className="ai-chat-user-input-popover__options">
|
|
2061
|
+
{activeUserInputQuestion.options.map((option) => (
|
|
2062
|
+
<label key={`${activeUserInputQuestion.id}:${option.value}`} className="ai-chat-user-input-popover__option">
|
|
2063
|
+
<input
|
|
2064
|
+
type="radio"
|
|
2065
|
+
name={`user-input-${activeUserInputQuestion.id}`}
|
|
2066
|
+
value={option.value}
|
|
2067
|
+
checked={(userInputSelections[activeUserInputQuestion.id] || '') === option.value}
|
|
2068
|
+
onChange={() => {
|
|
2069
|
+
setUserInputValidationError('');
|
|
2070
|
+
setUserInputSelections((prev) => ({
|
|
2071
|
+
...prev,
|
|
2072
|
+
[activeUserInputQuestion.id]: option.value,
|
|
2073
|
+
}));
|
|
2074
|
+
}}
|
|
2075
|
+
/>
|
|
2076
|
+
<span className="ai-chat-user-input-popover__option-content">
|
|
2077
|
+
<span className="ai-chat-user-input-popover__option-label">{option.label}</span>
|
|
2078
|
+
{option.description ? (
|
|
2079
|
+
<span className="ai-chat-user-input-popover__option-description">{option.description}</span>
|
|
2080
|
+
) : null}
|
|
2081
|
+
</span>
|
|
2082
|
+
</label>
|
|
2083
|
+
))}
|
|
2084
|
+
</div>
|
|
2085
|
+
{activeUserInputQuestion.allowWriteIn && (
|
|
2086
|
+
<textarea
|
|
2087
|
+
className="ai-chat-user-input-popover__writein"
|
|
2088
|
+
placeholder={activeUserInputQuestion.writeInPlaceholder}
|
|
2089
|
+
value={userInputWriteIns[activeUserInputQuestion.id] || ''}
|
|
2090
|
+
onChange={(event) => {
|
|
2091
|
+
const nextValue = event.target.value.slice(0, MAX_USER_INPUT_WRITE_IN_CHARS);
|
|
2092
|
+
setUserInputValidationError('');
|
|
2093
|
+
setUserInputWriteIns((prev) => ({
|
|
2094
|
+
...prev,
|
|
2095
|
+
[activeUserInputQuestion.id]: nextValue,
|
|
2096
|
+
}));
|
|
2097
|
+
}}
|
|
2098
|
+
rows={2}
|
|
2099
|
+
/>
|
|
2100
|
+
)}
|
|
2101
|
+
</div>
|
|
2102
|
+
)}
|
|
2103
|
+
{userInputValidationError ? (
|
|
2104
|
+
<div className="ai-chat-user-input-popover__error">{userInputValidationError}</div>
|
|
2105
|
+
) : null}
|
|
2106
|
+
<div className="ai-chat-user-input-popover__actions">
|
|
2107
|
+
<div className="ai-chat-user-input-popover__actions-left">
|
|
2108
|
+
{userInputRequest.allowCancel && (
|
|
2109
|
+
<button
|
|
2110
|
+
type="button"
|
|
2111
|
+
className="ai-chat-user-input-popover__button ai-chat-user-input-popover__button--secondary"
|
|
2112
|
+
onClick={handleUserInputCancel}
|
|
2113
|
+
>
|
|
2114
|
+
{userInputRequest.cancelLabel}
|
|
2115
|
+
</button>
|
|
2116
|
+
)}
|
|
2117
|
+
</div>
|
|
2118
|
+
<div className="ai-chat-user-input-popover__actions-right">
|
|
2119
|
+
{!isFirstUserInputQuestion && (
|
|
2120
|
+
<button
|
|
2121
|
+
type="button"
|
|
2122
|
+
className="ai-chat-user-input-popover__button ai-chat-user-input-popover__button--secondary"
|
|
2123
|
+
onClick={handleUserInputBack}
|
|
2124
|
+
>
|
|
2125
|
+
Back
|
|
2126
|
+
</button>
|
|
2127
|
+
)}
|
|
2128
|
+
{isLastUserInputQuestion ? (
|
|
2129
|
+
<button
|
|
2130
|
+
type="button"
|
|
2131
|
+
className="ai-chat-user-input-popover__button ai-chat-user-input-popover__button--primary"
|
|
2132
|
+
onClick={handleUserInputSubmit}
|
|
2133
|
+
>
|
|
2134
|
+
{userInputRequest.submitLabel}
|
|
2135
|
+
</button>
|
|
2136
|
+
) : (
|
|
2137
|
+
<button
|
|
2138
|
+
type="button"
|
|
2139
|
+
className="ai-chat-user-input-popover__button ai-chat-user-input-popover__button--primary"
|
|
2140
|
+
onClick={handleUserInputNext}
|
|
2141
|
+
>
|
|
2142
|
+
Next
|
|
2143
|
+
</button>
|
|
2144
|
+
)}
|
|
2145
|
+
</div>
|
|
2146
|
+
</div>
|
|
2147
|
+
</div>
|
|
2148
|
+
)}
|
|
1571
2149
|
{hasQueuedPrompts && (
|
|
1572
2150
|
<div className="ai-chat-queued-prompts">
|
|
1573
2151
|
{normalizedQueuedPrompts.map((queuedPrompt, index) => (
|
|
@@ -1603,8 +2181,9 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
1603
2181
|
<textarea
|
|
1604
2182
|
ref={textareaRef}
|
|
1605
2183
|
className="ai-chat-input"
|
|
1606
|
-
placeholder={placeholder}
|
|
2184
|
+
placeholder={composerLockedByUserInput ? 'Answer the question above to continue…' : placeholder}
|
|
1607
2185
|
value={inputValue}
|
|
2186
|
+
disabled={composerLockedByUserInput}
|
|
1608
2187
|
onChange={(e) => {
|
|
1609
2188
|
setInputValue(e.target.value);
|
|
1610
2189
|
setTimeout(autoResize, 0);
|
|
@@ -1621,62 +2200,83 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
1621
2200
|
|
|
1622
2201
|
{/* Footer row with agent selector and send button */}
|
|
1623
2202
|
<div className="ai-chat-panel__input-footer">
|
|
1624
|
-
|
|
1625
|
-
|
|
2203
|
+
<div className="ai-chat-input-footer__left">
|
|
2204
|
+
{agentOptions.length > 0 ? (
|
|
2205
|
+
<div className="ai-chat-agent-selector">
|
|
2206
|
+
<button
|
|
2207
|
+
className="ai-chat-agent-selector__trigger"
|
|
2208
|
+
onClick={() => setDropdownOpen(!dropdownOpen)}
|
|
2209
|
+
disabled={agentsLoading}
|
|
2210
|
+
>
|
|
2211
|
+
{currentAgentAvatarUrl ? (
|
|
2212
|
+
<img
|
|
2213
|
+
src={currentAgentAvatarUrl}
|
|
2214
|
+
alt={currentAgentLabel || 'Agent'}
|
|
2215
|
+
className="ai-chat-agent-selector__avatar"
|
|
2216
|
+
/>
|
|
2217
|
+
) : (
|
|
2218
|
+
<AgentIcon />
|
|
2219
|
+
)}
|
|
2220
|
+
<span className="ai-chat-agent-selector__label">
|
|
2221
|
+
{agentsLoading ? 'Loading...' : currentAgentLabel || 'Select agent'}
|
|
2222
|
+
</span>
|
|
2223
|
+
{dropdownOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
|
2224
|
+
</button>
|
|
2225
|
+
</div>
|
|
2226
|
+
) : null}
|
|
2227
|
+
{isAgentModeActionVisible && (
|
|
1626
2228
|
<button
|
|
1627
|
-
className=
|
|
1628
|
-
|
|
1629
|
-
|
|
2229
|
+
className={`ai-chat-agent-mode-trigger ${
|
|
2230
|
+
agentModeChipExpanded ? 'ai-chat-agent-mode-trigger--expanded' : ''
|
|
2231
|
+
} ${isAgentModeActive ? 'ai-chat-agent-mode-trigger--active' : ''}`}
|
|
2232
|
+
onClick={handleAgentModePillClick}
|
|
2233
|
+
type="button"
|
|
2234
|
+
title={agentModeTitle}
|
|
2235
|
+
aria-label={isAgentModeActive ? 'Agent active' : 'Enable Agent Mode'}
|
|
2236
|
+
aria-pressed={isAgentModeActive}
|
|
2237
|
+
disabled={isAgentModeActionDisabled}
|
|
1630
2238
|
>
|
|
1631
|
-
|
|
1632
|
-
<img
|
|
1633
|
-
src={currentAgentAvatarUrl}
|
|
1634
|
-
alt={currentAgentLabel || 'Agent'}
|
|
1635
|
-
className="ai-chat-agent-selector__avatar"
|
|
1636
|
-
/>
|
|
1637
|
-
) : (
|
|
2239
|
+
<span className="ai-chat-agent-mode-trigger__icon" aria-hidden="true">
|
|
1638
2240
|
<AgentIcon />
|
|
1639
|
-
)}
|
|
1640
|
-
<span className="ai-chat-agent-selector__label">
|
|
1641
|
-
{agentsLoading ? 'Loading...' : currentAgentLabel || 'Select agent'}
|
|
1642
2241
|
</span>
|
|
1643
|
-
|
|
2242
|
+
<span className="ai-chat-agent-mode-trigger__label">{agentModeLabel}</span>
|
|
1644
2243
|
</button>
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2244
|
+
)}
|
|
2245
|
+
{agentOptions.length === 0 && !isAgentModeActionVisible ? (
|
|
2246
|
+
<div className="ai-chat-panel__input-footer-spacer" />
|
|
2247
|
+
) : null}
|
|
2248
|
+
</div>
|
|
1649
2249
|
|
|
1650
|
-
{/* Context viewer pill - Cursor style */}
|
|
1651
2250
|
{contextSections.length > 0 && (
|
|
1652
2251
|
<div className="ai-chat-context-pill-wrapper">
|
|
1653
|
-
<
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
2252
|
+
<div className="ai-chat-context-pill-anchor">
|
|
2253
|
+
<button
|
|
2254
|
+
className={`ai-chat-context-pill ${contextViewerOpen ? 'ai-chat-context-pill--active' : ''} ${isOverLimit ? 'ai-chat-context-pill--warning' : ''}`}
|
|
2255
|
+
onClick={(e) => {
|
|
2256
|
+
e.preventDefault();
|
|
2257
|
+
e.stopPropagation();
|
|
2258
|
+
console.log('[ContextViewer] Button clicked, current state:', contextViewerOpen);
|
|
2259
|
+
setContextViewerOpen(!contextViewerOpen);
|
|
2260
|
+
if (!contextViewerOpen) {
|
|
2261
|
+
setContextViewMode('summary');
|
|
2262
|
+
setExpandedSectionId(null);
|
|
2263
|
+
} else {
|
|
2264
|
+
setExpandedSectionId(null);
|
|
2265
|
+
}
|
|
2266
|
+
}}
|
|
2267
|
+
type="button"
|
|
2268
|
+
title="View context"
|
|
2269
|
+
>
|
|
2270
|
+
<span className="ai-chat-context-pill__label">context: {contextSections.length} {contextSections.length === 1 ? 'section' : 'sections'}</span>
|
|
2271
|
+
</button>
|
|
1672
2272
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
2273
|
+
{/* Context popover - positioned relative to context pill */}
|
|
2274
|
+
{contextViewerOpen && (
|
|
2275
|
+
<div
|
|
2276
|
+
className={`ai-chat-context-popover ${contextViewMode === 'detail' ? 'ai-chat-context-popover--detail' : ''}`}
|
|
2277
|
+
ref={contextPopoverRef}
|
|
2278
|
+
onClick={(e) => e.stopPropagation()}
|
|
2279
|
+
>
|
|
1680
2280
|
{/* Summary view */}
|
|
1681
2281
|
{contextViewMode === 'summary' && (
|
|
1682
2282
|
<div className="ai-chat-context-popover__summary">
|
|
@@ -1834,16 +2434,27 @@ const ChatInput = React.memo<ChatInputProps>(({
|
|
|
1834
2434
|
</div>
|
|
1835
2435
|
</div>
|
|
1836
2436
|
)}
|
|
1837
|
-
|
|
1838
|
-
|
|
2437
|
+
</div>
|
|
2438
|
+
)}
|
|
2439
|
+
</div>
|
|
1839
2440
|
</div>
|
|
1840
2441
|
)}
|
|
1841
2442
|
|
|
1842
2443
|
<button
|
|
1843
|
-
className={`ai-chat-send-button ${
|
|
2444
|
+
className={`ai-chat-send-button ${
|
|
2445
|
+
!showStopAction && (!hasDraft || composerLockedByUserInput) ? 'ai-chat-send-button--disabled' : ''
|
|
2446
|
+
} ${showStopAction ? 'ai-chat-send-button--stop' : ''}`}
|
|
1844
2447
|
onClick={() => showStopAction ? onStop() : handleSubmit()}
|
|
1845
|
-
disabled={!showStopAction && !hasDraft}
|
|
1846
|
-
title={
|
|
2448
|
+
disabled={(!showStopAction && !hasDraft) || (!showStopAction && composerLockedByUserInput)}
|
|
2449
|
+
title={
|
|
2450
|
+
showStopAction
|
|
2451
|
+
? 'Stop response'
|
|
2452
|
+
: composerLockedByUserInput
|
|
2453
|
+
? 'Answer the question above to continue'
|
|
2454
|
+
: shouldQueueSubmission
|
|
2455
|
+
? 'Queue prompt'
|
|
2456
|
+
: 'Send prompt'
|
|
2457
|
+
}
|
|
1847
2458
|
>
|
|
1848
2459
|
{showStopAction ? <StopIcon /> : <ArrowUpIcon />}
|
|
1849
2460
|
</button>
|
|
@@ -1946,6 +2557,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1946
2557
|
onToggleSection: propOnToggleSection,
|
|
1947
2558
|
onConversationCreated,
|
|
1948
2559
|
onBeforeSend,
|
|
2560
|
+
compactContext,
|
|
2561
|
+
compactionPreserveTurns = DEFAULT_COMPACTION_PRESERVE_TURNS,
|
|
1949
2562
|
// UI Customization Props
|
|
1950
2563
|
cssUrl,
|
|
1951
2564
|
markdownClass,
|
|
@@ -1966,6 +2579,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1966
2579
|
customerEmailCaptureMode = 'HIDE',
|
|
1967
2580
|
customerEmailCapturePlaceholder = 'Please enter your email...',
|
|
1968
2581
|
toolStatusLabelFormatter,
|
|
2582
|
+
composerAgentModeControl,
|
|
1969
2583
|
}) => {
|
|
1970
2584
|
// ============================================================================
|
|
1971
2585
|
// API URL
|
|
@@ -2000,6 +2614,10 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2000
2614
|
const [copiedCallId, setCopiedCallId] = useState<string | null>(null);
|
|
2001
2615
|
const [feedbackCallId, setFeedbackCallId] = useState<{ callId: string; type: 'up' | 'down' } | null>(null);
|
|
2002
2616
|
const [error, setError] = useState<{ message: string; code?: string } | null>(null);
|
|
2617
|
+
const [compactionNotice, setCompactionNotice] = useState<{
|
|
2618
|
+
level: 'info' | 'warning' | 'success';
|
|
2619
|
+
message: string;
|
|
2620
|
+
} | null>(null);
|
|
2003
2621
|
const lastProcessedErrorRef = useRef<string | null>(null);
|
|
2004
2622
|
|
|
2005
2623
|
// Email & Save state
|
|
@@ -2031,6 +2649,9 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2031
2649
|
const [activeToolCalls, setActiveToolCalls] = useState<
|
|
2032
2650
|
Array<{ toolName: string; callId: string; args?: Record<string, unknown> }>
|
|
2033
2651
|
>([]);
|
|
2652
|
+
const [pendingUserInputRequest, setPendingUserInputRequest] = useState<UserInputRequest | null>(null);
|
|
2653
|
+
const pendingUserInputRequestRef = useRef<UserInputRequest | null>(null);
|
|
2654
|
+
const pendingUserInputResolverRef = useRef<((payload: UserInputCompletionPayload) => void) | null>(null);
|
|
2034
2655
|
const normalizeToolName = useCallback((toolName: string): string => {
|
|
2035
2656
|
return String(toolName ?? '').trim().toLowerCase();
|
|
2036
2657
|
}, []);
|
|
@@ -2043,6 +2664,31 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2043
2664
|
},
|
|
2044
2665
|
[normalizeToolName],
|
|
2045
2666
|
);
|
|
2667
|
+
const completePendingUserInputRequest = useCallback((payload: UserInputCompletionPayload) => {
|
|
2668
|
+
const resolver = pendingUserInputResolverRef.current;
|
|
2669
|
+
pendingUserInputResolverRef.current = null;
|
|
2670
|
+
pendingUserInputRequestRef.current = null;
|
|
2671
|
+
setPendingUserInputRequest(null);
|
|
2672
|
+
if (resolver) {
|
|
2673
|
+
resolver(payload);
|
|
2674
|
+
}
|
|
2675
|
+
}, []);
|
|
2676
|
+
useEffect(() => {
|
|
2677
|
+
pendingUserInputRequestRef.current = pendingUserInputRequest;
|
|
2678
|
+
}, [pendingUserInputRequest]);
|
|
2679
|
+
useEffect(() => {
|
|
2680
|
+
return () => {
|
|
2681
|
+
const resolver = pendingUserInputResolverRef.current;
|
|
2682
|
+
if (!resolver) return;
|
|
2683
|
+
pendingUserInputResolverRef.current = null;
|
|
2684
|
+
const pendingRequestId = pendingUserInputRequestRef.current?.requestId || 'request_user_input';
|
|
2685
|
+
resolver({
|
|
2686
|
+
status: 'cancelled',
|
|
2687
|
+
requestId: pendingRequestId,
|
|
2688
|
+
cancelledBy: 'unmount',
|
|
2689
|
+
});
|
|
2690
|
+
};
|
|
2691
|
+
}, []);
|
|
2046
2692
|
const alwaysApprovedToolsStorageKey = useMemo(() => {
|
|
2047
2693
|
const customerId =
|
|
2048
2694
|
(customer as any)?.customer_id ||
|
|
@@ -2864,6 +3510,51 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2864
3510
|
turnLockRef.current = false;
|
|
2865
3511
|
}, [hasInFlightTurnWork]);
|
|
2866
3512
|
|
|
3513
|
+
const runRequestUserInputTool = useCallback(
|
|
3514
|
+
async (toolCall: {
|
|
3515
|
+
toolName: string;
|
|
3516
|
+
callId: string;
|
|
3517
|
+
args: Record<string, unknown>;
|
|
3518
|
+
}): Promise<Record<string, unknown>> => {
|
|
3519
|
+
const normalizedToolName = normalizeToolName(toolCall.toolName);
|
|
3520
|
+
if (!USER_INPUT_TOOL_NAMES.has(normalizedToolName)) {
|
|
3521
|
+
throw new Error(`Unsupported user input tool: ${toolCall.toolName}`);
|
|
3522
|
+
}
|
|
3523
|
+
if (pendingUserInputResolverRef.current || pendingUserInputRequestRef.current) {
|
|
3524
|
+
throw new Error('A user input request is already pending. Wait for completion before requesting another.');
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
const request = normalizeUserInputRequest(
|
|
3528
|
+
isObjectRecord(toolCall.args) ? toolCall.args : {},
|
|
3529
|
+
toolCall.callId || toolCall.toolName,
|
|
3530
|
+
);
|
|
3531
|
+
pendingUserInputRequestRef.current = request;
|
|
3532
|
+
setPendingUserInputRequest(request);
|
|
3533
|
+
|
|
3534
|
+
const completion = await new Promise<UserInputCompletionPayload>((resolve) => {
|
|
3535
|
+
pendingUserInputResolverRef.current = resolve;
|
|
3536
|
+
});
|
|
3537
|
+
|
|
3538
|
+
if (completion.status === 'cancelled') {
|
|
3539
|
+
return {
|
|
3540
|
+
status: 'cancelled',
|
|
3541
|
+
requestId: request.requestId,
|
|
3542
|
+
cancelledBy: completion.cancelledBy || 'user',
|
|
3543
|
+
answers: [],
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
const answers = Array.isArray(completion.answers) ? completion.answers : [];
|
|
3548
|
+
return {
|
|
3549
|
+
status: 'ok',
|
|
3550
|
+
requestId: request.requestId,
|
|
3551
|
+
answers,
|
|
3552
|
+
answeredCount: answers.length,
|
|
3553
|
+
};
|
|
3554
|
+
},
|
|
3555
|
+
[normalizeToolName],
|
|
3556
|
+
);
|
|
3557
|
+
|
|
2867
3558
|
const processGivenToolRequests = useCallback(
|
|
2868
3559
|
async (requests: ToolRequestMatch[]) => {
|
|
2869
3560
|
const dedupeToolRequests = (input: ToolRequestMatch[]): ToolRequestMatch[] => {
|
|
@@ -3223,6 +3914,33 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3223
3914
|
localToolExecutors && typeof localToolExecutors[toolCall.toolName] === 'function'
|
|
3224
3915
|
? localToolExecutors[toolCall.toolName]
|
|
3225
3916
|
: null;
|
|
3917
|
+
const normalizedToolName = normalizeToolName(toolCall.toolName);
|
|
3918
|
+
|
|
3919
|
+
if (USER_INPUT_TOOL_NAMES.has(normalizedToolName)) {
|
|
3920
|
+
try {
|
|
3921
|
+
const requestResult = await runRequestUserInputTool({
|
|
3922
|
+
toolName: toolCall.toolName,
|
|
3923
|
+
callId: toolCall.callId,
|
|
3924
|
+
args: toolCall.args,
|
|
3925
|
+
});
|
|
3926
|
+
return {
|
|
3927
|
+
tool_call_id: toolCall.callId,
|
|
3928
|
+
tool_name: toolCall.toolName,
|
|
3929
|
+
result: JSON.stringify(requestResult),
|
|
3930
|
+
isError: false,
|
|
3931
|
+
};
|
|
3932
|
+
} catch (error) {
|
|
3933
|
+
return {
|
|
3934
|
+
tool_call_id: toolCall.callId,
|
|
3935
|
+
tool_name: toolCall.toolName,
|
|
3936
|
+
result:
|
|
3937
|
+
error instanceof Error
|
|
3938
|
+
? error.message
|
|
3939
|
+
: `Unhandled error calling ${toolCall.toolName}`,
|
|
3940
|
+
isError: true,
|
|
3941
|
+
};
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3226
3944
|
|
|
3227
3945
|
if (localExecutor) {
|
|
3228
3946
|
try {
|
|
@@ -3607,6 +4325,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3607
4325
|
idle,
|
|
3608
4326
|
stop,
|
|
3609
4327
|
lastController,
|
|
4328
|
+
normalizeToolName,
|
|
4329
|
+
runRequestUserInputTool,
|
|
3610
4330
|
waitForStreamIdle,
|
|
3611
4331
|
releaseTurnLockIfSettled,
|
|
3612
4332
|
],
|
|
@@ -4168,6 +4888,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4168
4888
|
|
|
4169
4889
|
// Clear any previous errors
|
|
4170
4890
|
setError(null);
|
|
4891
|
+
setCompactionNotice(null);
|
|
4171
4892
|
|
|
4172
4893
|
// Reset scroll tracking for new message - enable auto-scroll
|
|
4173
4894
|
setUserHasScrolled(false);
|
|
@@ -4209,189 +4930,279 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4209
4930
|
|
|
4210
4931
|
// Now proceed with API calls in the background (conversation creation + LLM call)
|
|
4211
4932
|
// Ensure conversation exists before sending (matches ChatPanel)
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4933
|
+
void (async () => {
|
|
4934
|
+
try {
|
|
4935
|
+
console.log('AIChatPanel.continueChat - about to call ensureConversation');
|
|
4936
|
+
const convId = await ensureConversation();
|
|
4937
|
+
console.log('AIChatPanel.continueChat - ensureConversation resolved with:', convId);
|
|
4938
|
+
|
|
4939
|
+
// Build messagesAndHistory from history (matches ChatPanel)
|
|
4940
|
+
// IMPORTANT: Exclude the current prompt (promptKey) since it's new and we're sending it now
|
|
4941
|
+
const historyForCall = latestHistoryRef.current || {};
|
|
4942
|
+
const messagesAndHistory: { role: string; content: string }[] = [];
|
|
4943
|
+
Object.entries(historyForCall).forEach(([historyPrompt, historyEntry]) => {
|
|
4944
|
+
if (historyPrompt === promptKey) return;
|
|
4945
|
+
const promptForHistory = normalizeHistoryPromptForContext(historyPrompt);
|
|
4946
|
+
const assistantContextContent = buildAssistantContextContent(historyPrompt, historyEntry);
|
|
4947
|
+
|
|
4948
|
+
messagesAndHistory.push({ role: 'user', content: promptForHistory });
|
|
4949
|
+
messagesAndHistory.push({ role: 'assistant', content: assistantContextContent });
|
|
4950
|
+
});
|
|
4951
|
+
|
|
4952
|
+
let fullPromptToSend = promptToSend;
|
|
4953
|
+
if (messagesAndHistory.length === 0 && promptTemplate) {
|
|
4954
|
+
fullPromptToSend = promptTemplate.replace('{{prompt}}', fullPromptToSend);
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4957
|
+
const newController = new AbortController();
|
|
4958
|
+
setLastController(newController);
|
|
4959
|
+
|
|
4960
|
+
if (onBeforeSend) {
|
|
4961
|
+
try {
|
|
4962
|
+
await Promise.resolve(
|
|
4963
|
+
onBeforeSend({
|
|
4964
|
+
prompt: promptToSend,
|
|
4965
|
+
conversationId: convId || null,
|
|
4966
|
+
agentId: agent,
|
|
4967
|
+
service,
|
|
4968
|
+
messages: messagesAndHistory,
|
|
4969
|
+
}),
|
|
4970
|
+
);
|
|
4971
|
+
} catch (error) {
|
|
4972
|
+
console.warn('[AIChatPanel] onBeforeSend callback failed:', error);
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4976
|
+
const warnRatio = toBoundedRatio(DEFAULT_CONTEXT_WARN_RATIO, DEFAULT_CONTEXT_WARN_RATIO);
|
|
4977
|
+
const compactRatio = toBoundedRatio(DEFAULT_CONTEXT_COMPACT_RATIO, DEFAULT_CONTEXT_COMPACT_RATIO);
|
|
4978
|
+
const targetRatio = toBoundedRatio(DEFAULT_CONTEXT_TARGET_RATIO, DEFAULT_CONTEXT_TARGET_RATIO);
|
|
4979
|
+
const maxTokens = Math.max(1, Number.isFinite(Number(maxContextTokens)) ? Math.floor(Number(maxContextTokens)) : 8000);
|
|
4980
|
+
const dataEntries = dataWithExtras();
|
|
4981
|
+
const projectedBefore =
|
|
4982
|
+
estimateTokensFromText(fullPromptToSend)
|
|
4983
|
+
+ estimateTokensFromMessages(messagesAndHistory)
|
|
4984
|
+
+ Math.max(Number(totalContextTokens) || 0, estimateTokensFromData(dataEntries));
|
|
4985
|
+
const warnThreshold = Math.floor(maxTokens * warnRatio);
|
|
4986
|
+
const compactThreshold = Math.floor(maxTokens * compactRatio);
|
|
4987
|
+
const targetThreshold = Math.floor(maxTokens * targetRatio);
|
|
4988
|
+
const preserveTurnsForCompaction =
|
|
4989
|
+
Number.isFinite(Number(compactionPreserveTurns)) && Number(compactionPreserveTurns) > 0
|
|
4990
|
+
? Math.floor(Number(compactionPreserveTurns))
|
|
4991
|
+
: DEFAULT_COMPACTION_PRESERVE_TURNS;
|
|
4992
|
+
const canDropOlderTurns =
|
|
4993
|
+
buildFallbackWindowMessages(messagesAndHistory, preserveTurnsForCompaction).length
|
|
4994
|
+
< messagesAndHistory.length;
|
|
4995
|
+
|
|
4996
|
+
let sendPrompt = fullPromptToSend;
|
|
4997
|
+
let sendMessages = messagesAndHistory;
|
|
4998
|
+
|
|
4999
|
+
if (projectedBefore >= warnThreshold && projectedBefore < compactThreshold) {
|
|
5000
|
+
const nearLimitMessage = 'Context is approaching the configured limit. A later turn may auto-compact.';
|
|
5001
|
+
setCompactionNotice({
|
|
5002
|
+
level: 'warning',
|
|
5003
|
+
message: nearLimitMessage,
|
|
5004
|
+
});
|
|
5005
|
+
}
|
|
4238
5006
|
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
5007
|
+
if (compactContext && projectedBefore >= compactThreshold && canDropOlderTurns) {
|
|
5008
|
+
setCompactionNotice({
|
|
5009
|
+
level: 'info',
|
|
5010
|
+
message: 'Automatically compacting context',
|
|
5011
|
+
});
|
|
5012
|
+
const compactionInput: CompactContextInput = {
|
|
5013
|
+
prompt: fullPromptToSend,
|
|
4243
5014
|
conversationId: convId || null,
|
|
4244
5015
|
agentId: agent,
|
|
4245
5016
|
service,
|
|
4246
5017
|
messages: messagesAndHistory,
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
return {
|
|
4293
|
-
...prev,
|
|
4294
|
-
[promptKey]: {
|
|
4295
|
-
...existingEntry,
|
|
4296
|
-
content: 'Response canceled',
|
|
4297
|
-
callId: lastCallId || existingEntry.callId || '',
|
|
4298
|
-
},
|
|
4299
|
-
};
|
|
4300
|
-
});
|
|
5018
|
+
data: dataEntries,
|
|
5019
|
+
maxContextTokens: maxTokens,
|
|
5020
|
+
totalContextTokens: Number(totalContextTokens) || 0,
|
|
5021
|
+
warnRatio,
|
|
5022
|
+
compactRatio,
|
|
5023
|
+
targetRatio,
|
|
5024
|
+
preserveTurns: preserveTurnsForCompaction,
|
|
5025
|
+
projectedTokens: {
|
|
5026
|
+
projectedBefore,
|
|
5027
|
+
projectedAfter: projectedBefore,
|
|
5028
|
+
warnThreshold,
|
|
5029
|
+
compactThreshold,
|
|
5030
|
+
targetThreshold,
|
|
5031
|
+
},
|
|
5032
|
+
};
|
|
5033
|
+
|
|
5034
|
+
try {
|
|
5035
|
+
const compactResult = await Promise.resolve(compactContext(compactionInput));
|
|
5036
|
+
if (compactResult && typeof compactResult === 'object') {
|
|
5037
|
+
if (typeof compactResult.prompt === 'string' && compactResult.prompt.trim()) {
|
|
5038
|
+
sendPrompt = compactResult.prompt;
|
|
5039
|
+
}
|
|
5040
|
+
if (Array.isArray(compactResult.messages)) {
|
|
5041
|
+
sendMessages = compactResult.messages;
|
|
5042
|
+
}
|
|
5043
|
+
|
|
5044
|
+
if (compactResult.action === 'compacted') {
|
|
5045
|
+
const usageBefore = Number(compactResult.tokenUsage?.projectedBefore);
|
|
5046
|
+
const usageAfter = Number(compactResult.tokenUsage?.projectedAfter);
|
|
5047
|
+
const hasUsageNumbers = Number.isFinite(usageBefore) && Number.isFinite(usageAfter);
|
|
5048
|
+
const compactedMessage = hasUsageNumbers
|
|
5049
|
+
? `Context compacted for this send (${formatTokenCount(usageBefore)} -> ${formatTokenCount(usageAfter)} tokens).`
|
|
5050
|
+
: 'Older turns were compacted to keep this thread within context limits.';
|
|
5051
|
+
setCompactionNotice({
|
|
5052
|
+
level: 'success',
|
|
5053
|
+
message: compactResult.warning || compactedMessage,
|
|
5054
|
+
});
|
|
5055
|
+
} else if (compactResult.action === 'fallback_window') {
|
|
5056
|
+
setCompactionNotice({
|
|
5057
|
+
level: 'warning',
|
|
5058
|
+
message: compactResult.warning || 'Compaction fallback applied. Sending only the most recent turns.',
|
|
5059
|
+
});
|
|
5060
|
+
} else {
|
|
5061
|
+
setCompactionNotice(null);
|
|
5062
|
+
}
|
|
4301
5063
|
}
|
|
4302
|
-
}
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
5064
|
+
} catch (error) {
|
|
5065
|
+
console.warn('[AIChatPanel] compactContext failed, applying local fallback window', error);
|
|
5066
|
+
sendMessages = buildFallbackWindowMessages(messagesAndHistory, preserveTurnsForCompaction);
|
|
5067
|
+
setCompactionNotice({
|
|
5068
|
+
level: 'warning',
|
|
5069
|
+
message: 'Compaction service unavailable. Sending a reduced recent-window context.',
|
|
4308
5070
|
});
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
|
|
5074
|
+
send(
|
|
5075
|
+
sendPrompt,
|
|
5076
|
+
sendMessages,
|
|
5077
|
+
[
|
|
5078
|
+
...dataEntries,
|
|
5079
|
+
{ key: '--messages', data: sendMessages.length.toString() },
|
|
5080
|
+
],
|
|
5081
|
+
true, // stream
|
|
5082
|
+
true, // includeHistory
|
|
5083
|
+
service, // group_id from agent config
|
|
5084
|
+
convId, // Use the conversation ID from ensureConversation
|
|
5085
|
+
newController,
|
|
5086
|
+
undefined, // onComplete
|
|
5087
|
+
(errorMsg: string) => {
|
|
5088
|
+
// Error callback - handle errors immediately
|
|
5089
|
+
console.log('[AIChatPanel] Error callback triggered:', errorMsg);
|
|
5090
|
+
|
|
5091
|
+
// Check if this is a user-initiated abort
|
|
5092
|
+
const isAbortError = errorMsg.toLowerCase().includes('abort')
|
|
5093
|
+
|| errorMsg.toLowerCase().includes('canceled')
|
|
5094
|
+
|| errorMsg.toLowerCase().includes('cancelled');
|
|
5095
|
+
|
|
5096
|
+
if (isAbortError) {
|
|
5097
|
+
if (suppressAbortHistoryUpdateRef.current) {
|
|
5098
|
+
setIsLoading(false);
|
|
5099
|
+
releaseTurnLockIfSettled();
|
|
5100
|
+
return;
|
|
5101
|
+
}
|
|
5102
|
+
// User canceled the request - don't show error banner
|
|
5103
|
+
console.log('[AIChatPanel] Request was aborted by user');
|
|
5104
|
+
|
|
5105
|
+
// Update history to show cancellation
|
|
5106
|
+
if (promptKey) {
|
|
5107
|
+
setHistory((prev) => {
|
|
5108
|
+
const existingEntry = prev[promptKey] || { content: '', callId: '' };
|
|
5109
|
+
return {
|
|
5110
|
+
...prev,
|
|
5111
|
+
[promptKey]: {
|
|
5112
|
+
...existingEntry,
|
|
5113
|
+
content: 'Response canceled',
|
|
5114
|
+
callId: lastCallId || existingEntry.callId || '',
|
|
5115
|
+
},
|
|
5116
|
+
};
|
|
5117
|
+
});
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
// Detect 413 Content Too Large error
|
|
5121
|
+
else if (errorMsg.includes('413') || errorMsg.toLowerCase().includes('content too large')) {
|
|
5122
|
+
setError({
|
|
5123
|
+
message: 'The context is too large to process. Please start a new conversation or reduce the amount of context.',
|
|
5124
|
+
code: '413',
|
|
4322
5125
|
});
|
|
5126
|
+
|
|
5127
|
+
if (promptKey) {
|
|
5128
|
+
setHistory((prev) => {
|
|
5129
|
+
const existingEntry = prev[promptKey] || { content: '', callId: '' };
|
|
5130
|
+
return {
|
|
5131
|
+
...prev,
|
|
5132
|
+
[promptKey]: {
|
|
5133
|
+
...existingEntry,
|
|
5134
|
+
content: `Error: ${errorMsg}`,
|
|
5135
|
+
callId: lastCallId || existingEntry.callId || '',
|
|
5136
|
+
},
|
|
5137
|
+
};
|
|
5138
|
+
});
|
|
5139
|
+
}
|
|
4323
5140
|
}
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
code: 'NETWORK_ERROR',
|
|
4330
|
-
});
|
|
4331
|
-
|
|
4332
|
-
// Update history to show error
|
|
4333
|
-
if (promptKey) {
|
|
4334
|
-
setHistory((prev) => {
|
|
4335
|
-
const existingEntry = prev[promptKey] || { content: '', callId: '' };
|
|
4336
|
-
return {
|
|
4337
|
-
...prev,
|
|
4338
|
-
[promptKey]: {
|
|
4339
|
-
...existingEntry,
|
|
4340
|
-
content: `Error: ${errorMsg}`,
|
|
4341
|
-
callId: lastCallId || existingEntry.callId || '',
|
|
4342
|
-
},
|
|
4343
|
-
};
|
|
5141
|
+
// Detect other network errors
|
|
5142
|
+
else if (errorMsg.toLowerCase().includes('network error') || errorMsg.toLowerCase().includes('fetch')) {
|
|
5143
|
+
setError({
|
|
5144
|
+
message: 'Network error. Please check your connection and try again.',
|
|
5145
|
+
code: 'NETWORK_ERROR',
|
|
4344
5146
|
});
|
|
5147
|
+
|
|
5148
|
+
if (promptKey) {
|
|
5149
|
+
setHistory((prev) => {
|
|
5150
|
+
const existingEntry = prev[promptKey] || { content: '', callId: '' };
|
|
5151
|
+
return {
|
|
5152
|
+
...prev,
|
|
5153
|
+
[promptKey]: {
|
|
5154
|
+
...existingEntry,
|
|
5155
|
+
content: `Error: ${errorMsg}`,
|
|
5156
|
+
callId: lastCallId || existingEntry.callId || '',
|
|
5157
|
+
},
|
|
5158
|
+
};
|
|
5159
|
+
});
|
|
5160
|
+
}
|
|
4345
5161
|
}
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
code: 'UNKNOWN_ERROR',
|
|
4352
|
-
});
|
|
4353
|
-
|
|
4354
|
-
// Update history to show error
|
|
4355
|
-
if (promptKey) {
|
|
4356
|
-
setHistory((prev) => {
|
|
4357
|
-
const existingEntry = prev[promptKey] || { content: '', callId: '' };
|
|
4358
|
-
return {
|
|
4359
|
-
...prev,
|
|
4360
|
-
[promptKey]: {
|
|
4361
|
-
...existingEntry,
|
|
4362
|
-
content: `Error: ${errorMsg}`,
|
|
4363
|
-
callId: lastCallId || existingEntry.callId || '',
|
|
4364
|
-
},
|
|
4365
|
-
};
|
|
5162
|
+
// Generic error
|
|
5163
|
+
else {
|
|
5164
|
+
setError({
|
|
5165
|
+
message: errorMsg,
|
|
5166
|
+
code: 'UNKNOWN_ERROR',
|
|
4366
5167
|
});
|
|
5168
|
+
|
|
5169
|
+
if (promptKey) {
|
|
5170
|
+
setHistory((prev) => {
|
|
5171
|
+
const existingEntry = prev[promptKey] || { content: '', callId: '' };
|
|
5172
|
+
return {
|
|
5173
|
+
...prev,
|
|
5174
|
+
[promptKey]: {
|
|
5175
|
+
...existingEntry,
|
|
5176
|
+
content: `Error: ${errorMsg}`,
|
|
5177
|
+
callId: lastCallId || existingEntry.callId || '',
|
|
5178
|
+
},
|
|
5179
|
+
};
|
|
5180
|
+
});
|
|
5181
|
+
}
|
|
4367
5182
|
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
5183
|
+
|
|
5184
|
+
setIsLoading(false);
|
|
5185
|
+
releaseTurnLockIfSettled();
|
|
5186
|
+
},
|
|
5187
|
+
);
|
|
5188
|
+
|
|
5189
|
+
setLastMessages(sendMessages);
|
|
5190
|
+
|
|
5191
|
+
if (convId && onConversationCreated) {
|
|
5192
|
+
setTimeout(() => {
|
|
5193
|
+
onConversationCreated(convId);
|
|
5194
|
+
}, 100);
|
|
4373
5195
|
}
|
|
4374
|
-
)
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
setTimeout(() => {
|
|
4383
|
-
onConversationCreated(convId);
|
|
4384
|
-
}, 100);
|
|
5196
|
+
} catch (error) {
|
|
5197
|
+
console.error('[AIChatPanel] Failed to send prompt:', error);
|
|
5198
|
+
setError({
|
|
5199
|
+
message: error instanceof Error ? error.message : 'Failed to send prompt',
|
|
5200
|
+
code: 'UNKNOWN_ERROR',
|
|
5201
|
+
});
|
|
5202
|
+
setIsLoading(false);
|
|
5203
|
+
releaseTurnLockIfSettled();
|
|
4385
5204
|
}
|
|
4386
|
-
})
|
|
4387
|
-
console.error('[AIChatPanel] Failed to send prompt:', error);
|
|
4388
|
-
setError({
|
|
4389
|
-
message: error instanceof Error ? error.message : 'Failed to send prompt',
|
|
4390
|
-
code: 'UNKNOWN_ERROR',
|
|
4391
|
-
});
|
|
4392
|
-
setIsLoading(false);
|
|
4393
|
-
releaseTurnLockIfSettled();
|
|
4394
|
-
});
|
|
5205
|
+
})();
|
|
4395
5206
|
}, [
|
|
4396
5207
|
clearFollowOnQuestionsNextPrompt,
|
|
4397
5208
|
promptTemplate,
|
|
@@ -4406,6 +5217,10 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4406
5217
|
scrollToBottom,
|
|
4407
5218
|
onConversationCreated,
|
|
4408
5219
|
onBeforeSend,
|
|
5220
|
+
compactContext,
|
|
5221
|
+
compactionPreserveTurns,
|
|
5222
|
+
maxContextTokens,
|
|
5223
|
+
totalContextTokens,
|
|
4409
5224
|
hasInFlightTurnWork,
|
|
4410
5225
|
queuePromptForLater,
|
|
4411
5226
|
releaseTurnLockIfSettled,
|
|
@@ -4431,8 +5246,15 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4431
5246
|
|
|
4432
5247
|
// Handle stop - stable callback for ChatInput
|
|
4433
5248
|
const handleStop = useCallback(() => {
|
|
5249
|
+
if (pendingUserInputRequest) {
|
|
5250
|
+
completePendingUserInputRequest({
|
|
5251
|
+
status: 'cancelled',
|
|
5252
|
+
requestId: pendingUserInputRequest.requestId,
|
|
5253
|
+
cancelledBy: 'stop',
|
|
5254
|
+
});
|
|
5255
|
+
}
|
|
4434
5256
|
stop(lastController);
|
|
4435
|
-
}, [
|
|
5257
|
+
}, [completePendingUserInputRequest, lastController, pendingUserInputRequest, stop]);
|
|
4436
5258
|
|
|
4437
5259
|
// Reset conversation
|
|
4438
5260
|
const handleNewConversation = useCallback(() => {
|
|
@@ -4447,9 +5269,17 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4447
5269
|
if (!idle) {
|
|
4448
5270
|
stop(lastController);
|
|
4449
5271
|
}
|
|
5272
|
+
if (pendingUserInputRequest) {
|
|
5273
|
+
completePendingUserInputRequest({
|
|
5274
|
+
status: 'cancelled',
|
|
5275
|
+
requestId: pendingUserInputRequest.requestId,
|
|
5276
|
+
cancelledBy: 'reset',
|
|
5277
|
+
});
|
|
5278
|
+
}
|
|
4450
5279
|
|
|
4451
5280
|
setResponse('');
|
|
4452
5281
|
setHistory({});
|
|
5282
|
+
setCompactionNotice(null);
|
|
4453
5283
|
latestHistoryRef.current = {}; // Keep ref in sync
|
|
4454
5284
|
hasNotifiedCompletionRef.current = true; // Prevent stale notifications
|
|
4455
5285
|
queuedPromptsRef.current = [];
|
|
@@ -4471,6 +5301,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4471
5301
|
setThinkingBlocks([]);
|
|
4472
5302
|
setCurrentThinkingIndex(0);
|
|
4473
5303
|
setCollapsedBlocks(new Set());
|
|
5304
|
+
setPendingUserInputRequest(null);
|
|
4474
5305
|
// Note: activeThinkingBlock is computed via useMemo from response
|
|
4475
5306
|
hasAutoCollapsedRef.current = false;
|
|
4476
5307
|
prevBlockCountRef.current = 0;
|
|
@@ -4485,7 +5316,16 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4485
5316
|
setJustReset(false);
|
|
4486
5317
|
responseAreaRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
4487
5318
|
}, 100);
|
|
4488
|
-
}, [
|
|
5319
|
+
}, [
|
|
5320
|
+
completePendingUserInputRequest,
|
|
5321
|
+
followOnQuestions,
|
|
5322
|
+
idle,
|
|
5323
|
+
lastController,
|
|
5324
|
+
newConversationConfirm,
|
|
5325
|
+
pendingUserInputRequest,
|
|
5326
|
+
setResponse,
|
|
5327
|
+
stop,
|
|
5328
|
+
]);
|
|
4489
5329
|
|
|
4490
5330
|
// ============================================================================
|
|
4491
5331
|
// Effects - CLEAN DESIGN: No overlapping effects, explicit triggers only
|
|
@@ -5539,6 +6379,19 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
5539
6379
|
</button>
|
|
5540
6380
|
</div>
|
|
5541
6381
|
)}
|
|
6382
|
+
|
|
6383
|
+
{compactionNotice && compactionNotice.level !== 'info' && (
|
|
6384
|
+
<div className={`ai-chat-compaction-banner ai-chat-compaction-banner--${compactionNotice.level}`}>
|
|
6385
|
+
<div className="ai-chat-compaction-banner__message">{compactionNotice.message}</div>
|
|
6386
|
+
<button
|
|
6387
|
+
className="ai-chat-compaction-banner__close"
|
|
6388
|
+
onClick={() => setCompactionNotice(null)}
|
|
6389
|
+
aria-label="Dismiss context notice"
|
|
6390
|
+
>
|
|
6391
|
+
<CloseIcon />
|
|
6392
|
+
</button>
|
|
6393
|
+
</div>
|
|
6394
|
+
)}
|
|
5542
6395
|
|
|
5543
6396
|
{/* Messages Area */}
|
|
5544
6397
|
<ScrollArea className="ai-chat-panel__messages" ref={responseAreaRef}>
|
|
@@ -5919,6 +6772,12 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
5919
6772
|
<div ref={bottomRef} />
|
|
5920
6773
|
</ScrollArea>
|
|
5921
6774
|
|
|
6775
|
+
{compactionNotice?.level === 'info' && (
|
|
6776
|
+
<div className="ai-chat-compaction-inline-status" role="status" aria-live="polite">
|
|
6777
|
+
<span>{compactionNotice.message}</span>
|
|
6778
|
+
</div>
|
|
6779
|
+
)}
|
|
6780
|
+
|
|
5922
6781
|
{/* Button Container - Save, Email, CTA */}
|
|
5923
6782
|
{(showSaveButton || showEmailButton || showCallToAction) && (
|
|
5924
6783
|
<div className="ai-chat-button-container">
|
|
@@ -6097,6 +6956,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
6097
6956
|
onSubmit={continueChat}
|
|
6098
6957
|
onQueueSubmit={handleQueuePrompt}
|
|
6099
6958
|
onStop={handleStop}
|
|
6959
|
+
userInputRequest={pendingUserInputRequest}
|
|
6960
|
+
onCompleteUserInputRequest={completePendingUserInputRequest}
|
|
6100
6961
|
queuedPrompts={queuedPrompts}
|
|
6101
6962
|
onClearQueuedPrompt={handleClearQueuedPrompt}
|
|
6102
6963
|
agentOptions={agentOptions}
|
|
@@ -6111,6 +6972,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
6111
6972
|
enableContextDetailView={enableContextDetailView}
|
|
6112
6973
|
disabledSectionIds={disabledSectionIds}
|
|
6113
6974
|
onToggleSection={handleToggleSection}
|
|
6975
|
+
composerAgentModeControl={composerAgentModeControl}
|
|
6114
6976
|
/>
|
|
6115
6977
|
|
|
6116
6978
|
{/* Footer */}
|