ai-design-system 0.1.46 → 0.1.47
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/components/ai-elements/conversation.tsx +1 -1
- package/components/blocks/AIConversation/AIConversation.tsx +9 -4
- package/components/blocks/InboxPanel/InboxPanel.tsx +12 -1
- package/components/blocks/PromptInput/PromptInput.tsx +3 -1
- package/components/blocks/SectionLayout/SectionLayout.tsx +2 -2
- package/components/composites/ApprovalCard/ApprovalCard.tsx +70 -42
- package/components/composites/OrchestratorMessage/OrchestratorMessage.tsx +2 -1
- package/components/composites/SpecialistMessage/SpecialistMessage.tsx +14 -6
- package/components/features/RefinementPanel/RefinementPanel.tsx +30 -25
- package/components/features/RefinementPanel/useRefinementPanel.d.ts +13 -8
- package/components/features/RefinementPanel/useRefinementPanel.mock.ts +6 -0
- package/dist/index.cjs +110 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +110 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
|
|
11
11
|
|
|
12
12
|
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
|
13
13
|
<StickToBottom
|
|
14
|
-
className={cn("relative flex-1 overflow-
|
|
14
|
+
className={cn("relative flex-1 overflow-hidden", className)}
|
|
15
15
|
initial="smooth"
|
|
16
16
|
resize="smooth"
|
|
17
17
|
role="log"
|
|
@@ -92,9 +92,14 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
92
92
|
[emptyState?.description]
|
|
93
93
|
)
|
|
94
94
|
|
|
95
|
+
const toMessageString = (content: unknown): string =>
|
|
96
|
+
typeof content === 'string' ? content : ''
|
|
97
|
+
|
|
95
98
|
const renderedMessages = React.useMemo(
|
|
96
99
|
() =>
|
|
97
100
|
messages.map((message) => {
|
|
101
|
+
const contentStr = toMessageString(message.content)
|
|
102
|
+
|
|
98
103
|
// Render based on role field
|
|
99
104
|
if (message.role === "user") {
|
|
100
105
|
return (
|
|
@@ -102,7 +107,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
102
107
|
key={message.id}
|
|
103
108
|
message={{
|
|
104
109
|
id: message.id,
|
|
105
|
-
content:
|
|
110
|
+
content: contentStr,
|
|
106
111
|
avatarSrc: message.avatarSrc,
|
|
107
112
|
avatarName: message.avatarName,
|
|
108
113
|
}}
|
|
@@ -122,7 +127,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
122
127
|
(tc) => tc.name !== "task" && tc.name !== "ask_user"
|
|
123
128
|
) || []
|
|
124
129
|
|
|
125
|
-
const hasContent =
|
|
130
|
+
const hasContent = contentStr.trim() !== ""
|
|
126
131
|
if (!hasContent && directToolCalls.length === 0 && subAgents.length === 0 && !message.isLoading) {
|
|
127
132
|
return null;
|
|
128
133
|
}
|
|
@@ -132,7 +137,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
132
137
|
key={message.id}
|
|
133
138
|
message={{
|
|
134
139
|
id: message.id,
|
|
135
|
-
content:
|
|
140
|
+
content: contentStr,
|
|
136
141
|
avatarSrc: message.avatarSrc,
|
|
137
142
|
avatarName: message.avatarName,
|
|
138
143
|
isLoading: message.isLoading,
|
|
@@ -170,7 +175,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
170
175
|
message={{
|
|
171
176
|
id: message.id,
|
|
172
177
|
name: message.avatarName || "Agent",
|
|
173
|
-
content:
|
|
178
|
+
content: contentStr,
|
|
174
179
|
toolCalls: message.toolCalls?.filter((tc) => tc.name !== "task"),
|
|
175
180
|
status: "completed",
|
|
176
181
|
}}
|
|
@@ -13,6 +13,8 @@ export interface InboxPanelProps {
|
|
|
13
13
|
isLoading?: boolean
|
|
14
14
|
emptyMessage?: string
|
|
15
15
|
className?: string
|
|
16
|
+
title?: string
|
|
17
|
+
headerAction?: React.ReactNode
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export const InboxPanel = React.memo<InboxPanelProps>(
|
|
@@ -26,10 +28,19 @@ export const InboxPanel = React.memo<InboxPanelProps>(
|
|
|
26
28
|
isLoading = false,
|
|
27
29
|
emptyMessage,
|
|
28
30
|
className,
|
|
31
|
+
title,
|
|
32
|
+
headerAction,
|
|
29
33
|
}) => {
|
|
30
34
|
return (
|
|
31
35
|
<section className={`flex min-h-0 flex-1 flex-col overflow-hidden ${className ?? ""}`}>
|
|
32
|
-
|
|
36
|
+
{title || headerAction ? (
|
|
37
|
+
<div className="flex items-center justify-between px-4 pt-4 pb-2">
|
|
38
|
+
{title ? <h2 className="text-sm font-semibold text-muted-foreground">{title}</h2> : <div />}
|
|
39
|
+
{headerAction ? <div>{headerAction}</div> : null}
|
|
40
|
+
</div>
|
|
41
|
+
) : null}
|
|
42
|
+
|
|
43
|
+
<div className="px-4 pb-2">
|
|
33
44
|
<Input
|
|
34
45
|
aria-label="Search inbox items"
|
|
35
46
|
className="h-8"
|
|
@@ -29,6 +29,7 @@ export interface PromptInputBlockProps
|
|
|
29
29
|
event: FormEvent<HTMLFormElement>
|
|
30
30
|
) => void | Promise<void>;
|
|
31
31
|
dialog?: ReactNode;
|
|
32
|
+
loading?: boolean;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
@@ -39,6 +40,7 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
39
40
|
onChange,
|
|
40
41
|
onSubmit,
|
|
41
42
|
dialog,
|
|
43
|
+
loading = false,
|
|
42
44
|
...props
|
|
43
45
|
}) => {
|
|
44
46
|
const handleSubmit = React.useCallback(
|
|
@@ -80,7 +82,7 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
80
82
|
<PromptInputTools>
|
|
81
83
|
{}
|
|
82
84
|
</PromptInputTools>
|
|
83
|
-
<PromptInputSubmit disabled={disabled} />
|
|
85
|
+
<PromptInputSubmit disabled={disabled} status={loading ? "submitted" : undefined} />
|
|
84
86
|
</PromptInputFooter>
|
|
85
87
|
</AIPromptInput>
|
|
86
88
|
);
|
|
@@ -28,11 +28,11 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
|
|
|
28
28
|
...section,
|
|
29
29
|
resizable,
|
|
30
30
|
content: (
|
|
31
|
-
<div className="h-full min-h-0 flex flex-col overflow-
|
|
31
|
+
<div className="h-full min-h-0 flex flex-col overflow-hidden">
|
|
32
32
|
{section.header && (
|
|
33
33
|
<AppHeader {...section.header} />
|
|
34
34
|
)}
|
|
35
|
-
<div className="min-h-0 flex-1 overflow-hidden">
|
|
35
|
+
<div className="min-h-0 flex-1 overflow-hidden flex flex-col">
|
|
36
36
|
{section.content}
|
|
37
37
|
</div>
|
|
38
38
|
</div>
|
|
@@ -98,6 +98,10 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
98
98
|
const [rejectReason, setRejectReason] = useState("");
|
|
99
99
|
const [showRejectInput, setShowRejectInput] = useState(false);
|
|
100
100
|
|
|
101
|
+
const hideSkipButton = (actionRequest.args?.hide_skip_button as boolean) ?? false;
|
|
102
|
+
const submitButtonText = (actionRequest.args?.button_submit_text as string) || 'Continue';
|
|
103
|
+
const skipButtonText = (actionRequest.args?.button_skip_text as string) || 'Skip';
|
|
104
|
+
|
|
101
105
|
// Question states
|
|
102
106
|
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
|
103
107
|
const [selectedAnswers, setSelectedAnswers] = useState<Record<number, string | string[]>>({});
|
|
@@ -117,13 +121,15 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
117
121
|
const isQuestionAction =
|
|
118
122
|
actionRequest.name === "ask_question" ||
|
|
119
123
|
(actionRequest.args &&
|
|
120
|
-
(typeof actionRequest.args.question === "string" ||
|
|
121
|
-
Array.isArray(actionRequest.args.questions)));
|
|
124
|
+
((typeof actionRequest.args.question === "string") ||
|
|
125
|
+
(Array.isArray(actionRequest.args.questions) && actionRequest.args.questions.length > 0)));
|
|
122
126
|
|
|
123
127
|
// Parse questions
|
|
124
128
|
let questions: Question[] = [];
|
|
125
129
|
if (actionRequest.args && Array.isArray(actionRequest.args.questions)) {
|
|
126
130
|
questions = actionRequest.args.questions as Question[];
|
|
131
|
+
} else if (actionRequest.args && actionRequest.args.questions && typeof actionRequest.args.questions === "object") {
|
|
132
|
+
questions = Object.values(actionRequest.args.questions) as Question[];
|
|
127
133
|
} else if (actionRequest.args && typeof actionRequest.args.question === "string") {
|
|
128
134
|
questions = [
|
|
129
135
|
{
|
|
@@ -144,19 +150,37 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
144
150
|
// Render interactive question view
|
|
145
151
|
if (isQuestionAction && currentQuestion) {
|
|
146
152
|
const handleContinue = () => {
|
|
153
|
+
const qIdx = currentQuestionIndex;
|
|
154
|
+
const selIdx = selectedOptionIndices[qIdx];
|
|
155
|
+
const hasOtherSelected = selIdx === options.length;
|
|
156
|
+
const hasRegularSelection = selIdx !== null && selIdx !== undefined && !hasOtherSelected;
|
|
157
|
+
const hasMultiSelectItems = isMultiSelect && Array.isArray(selectedAnswers[qIdx]) && (selectedAnswers[qIdx] as string[]).length > 0;
|
|
158
|
+
|
|
159
|
+
const isApprovalType = actionRequest.args?.type === 'approval';
|
|
160
|
+
const isOtherEmpty = hasOtherSelected && !otherTexts[qIdx]?.trim();
|
|
161
|
+
const hasNoValidSelection = (!hasRegularSelection && !hasMultiSelectItems && !hasOtherSelected) || isOtherEmpty;
|
|
162
|
+
|
|
163
|
+
let answer: string | string[] = "Approved";
|
|
164
|
+
|
|
165
|
+
if (hasOtherSelected) {
|
|
166
|
+
answer = otherTexts[qIdx] || "Approved";
|
|
167
|
+
} else if (isMultiSelect) {
|
|
168
|
+
answer = (selectedAnswers[qIdx] as string[]).join(", ") || "Approved";
|
|
169
|
+
} else if (hasRegularSelection) {
|
|
170
|
+
answer = selectedAnswers[qIdx] as string || "Approved";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!isApprovalType && hasNoValidSelection) return;
|
|
174
|
+
|
|
147
175
|
if (currentQuestionIndex === questions.length - 1) {
|
|
148
176
|
const finalAnswers = questions.map((q, idx) => {
|
|
149
|
-
if (
|
|
150
|
-
return otherTexts[idx] || "";
|
|
151
|
-
}
|
|
177
|
+
if (idx === qIdx) return answer;
|
|
152
178
|
return selectedAnswers[idx] || "";
|
|
153
179
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
onEdit({ ...actionRequest.args, answers: finalAnswers });
|
|
157
|
-
}
|
|
158
|
-
onApprove();
|
|
180
|
+
if (onEdit) onEdit({ ...actionRequest.args, answers: finalAnswers });
|
|
181
|
+
else onApprove();
|
|
159
182
|
} else {
|
|
183
|
+
setSelectedAnswers((prev) => ({ ...prev, [qIdx]: answer }));
|
|
160
184
|
setCurrentQuestionIndex((prev) => prev + 1);
|
|
161
185
|
}
|
|
162
186
|
};
|
|
@@ -297,17 +321,19 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
297
321
|
</div>
|
|
298
322
|
|
|
299
323
|
<ConfirmationActions>
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
onReject
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
324
|
+
{!hideSkipButton && (
|
|
325
|
+
<ConfirmationAction
|
|
326
|
+
variant="outline"
|
|
327
|
+
type="button"
|
|
328
|
+
onClick={() => {
|
|
329
|
+
if (onReject) {
|
|
330
|
+
onReject("Skipped question");
|
|
331
|
+
}
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
{skipButtonText}
|
|
335
|
+
</ConfirmationAction>
|
|
336
|
+
)}
|
|
311
337
|
|
|
312
338
|
<ConfirmationAction
|
|
313
339
|
type="button"
|
|
@@ -318,7 +344,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
318
344
|
{currentQuestionIndex === questions.length - 1 ? (
|
|
319
345
|
<>
|
|
320
346
|
<Check className="h-4 w-4 mr-2" />
|
|
321
|
-
|
|
347
|
+
{submitButtonText}
|
|
322
348
|
</>
|
|
323
349
|
) : (
|
|
324
350
|
<>
|
|
@@ -423,24 +449,26 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
423
449
|
)}
|
|
424
450
|
|
|
425
451
|
<div className="mt-3 space-y-3">
|
|
426
|
-
{Object.entries(actionRequest.args)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
452
|
+
{Object.entries(actionRequest.args)
|
|
453
|
+
.filter(([_, value]) => typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
454
|
+
.map(([key, value]) => {
|
|
455
|
+
const strValue = formatValue(value);
|
|
456
|
+
const isMultiline = strValue.includes("\n");
|
|
457
|
+
return (
|
|
458
|
+
<div key={key} className="space-y-1">
|
|
459
|
+
<div className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
|
|
460
|
+
{key}
|
|
461
|
+
</div>
|
|
462
|
+
{isMultiline ? (
|
|
463
|
+
<pre className="text-xs text-card-foreground whitespace-pre-wrap break-all font-mono bg-muted/50 rounded-md px-2.5 py-1.5 border border-border">
|
|
464
|
+
{strValue}
|
|
465
|
+
</pre>
|
|
466
|
+
) : (
|
|
467
|
+
<div className="text-sm text-card-foreground break-all">{strValue}</div>
|
|
468
|
+
)}
|
|
433
469
|
</div>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
{strValue}
|
|
437
|
-
</pre>
|
|
438
|
-
) : (
|
|
439
|
-
<div className="text-sm text-card-foreground break-all">{strValue}</div>
|
|
440
|
-
)}
|
|
441
|
-
</div>
|
|
442
|
-
);
|
|
443
|
-
})}
|
|
470
|
+
);
|
|
471
|
+
})}
|
|
444
472
|
</div>
|
|
445
473
|
</div>
|
|
446
474
|
|
|
@@ -489,7 +517,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
489
517
|
</>
|
|
490
518
|
) : (
|
|
491
519
|
<>
|
|
492
|
-
{canReject && (
|
|
520
|
+
{canReject && !hideSkipButton && (
|
|
493
521
|
<ConfirmationAction
|
|
494
522
|
type="button"
|
|
495
523
|
variant="outline"
|
|
@@ -497,7 +525,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
497
525
|
disabled={isProcessing}
|
|
498
526
|
>
|
|
499
527
|
<X className="h-4 w-4 mr-2" />
|
|
500
|
-
|
|
528
|
+
{skipButtonText}
|
|
501
529
|
</ConfirmationAction>
|
|
502
530
|
)}
|
|
503
531
|
{canEdit && (
|
|
@@ -518,7 +546,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
518
546
|
disabled={isProcessing}
|
|
519
547
|
>
|
|
520
548
|
<Check className="h-4 w-4 mr-2" />
|
|
521
|
-
|
|
549
|
+
{submitButtonText}
|
|
522
550
|
</ConfirmationAction>
|
|
523
551
|
</>
|
|
524
552
|
)}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
MessageAvatar,
|
|
6
6
|
MessageTypingIndicator,
|
|
7
7
|
} from "@/components/ai-elements/message"
|
|
8
|
+
import { Response } from "@/components/ai-elements/response"
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* OrchestratorMessage Block
|
|
@@ -63,7 +64,7 @@ export const OrchestratorMessage = React.memo<OrchestratorMessageProps>(
|
|
|
63
64
|
</MessageContent>
|
|
64
65
|
) : hasContent && (
|
|
65
66
|
<MessageContent variant="contained">
|
|
66
|
-
{message.content}
|
|
67
|
+
<Response mode={message.isLoading ? "streaming" : "static"} isAnimating={!!message.isLoading}>{message.content}</Response>
|
|
67
68
|
</MessageContent>
|
|
68
69
|
)}
|
|
69
70
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@/components/ai-elements/plan"
|
|
10
10
|
import { ToolCallDisplay, type ToolCall } from "@/components/composites/ToolCallDisplay"
|
|
11
11
|
import { cn } from "@/lib/utils"
|
|
12
|
-
import {
|
|
12
|
+
import { Response } from "@/components/ai-elements/response"
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* SpecialistMessage Block
|
|
@@ -65,10 +65,17 @@ export const SpecialistMessage = React.memo<SpecialistMessageProps>(
|
|
|
65
65
|
[message.toolCalls]
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
+
const description = React.useMemo(() => {
|
|
69
|
+
if (message.description) return message.description;
|
|
70
|
+
if (message.status === "active") return "Thinking...";
|
|
71
|
+
return undefined;
|
|
72
|
+
}, [message.description, message.status]);
|
|
73
|
+
|
|
68
74
|
return (
|
|
69
75
|
<Plan
|
|
70
76
|
defaultOpen={collapsible ? defaultOpen : true}
|
|
71
77
|
className={cn(isNested && "ml-8")}
|
|
78
|
+
isStreaming={message.status === "active"}
|
|
72
79
|
>
|
|
73
80
|
<PlanHeader>
|
|
74
81
|
<div>
|
|
@@ -76,8 +83,8 @@ export const SpecialistMessage = React.memo<SpecialistMessageProps>(
|
|
|
76
83
|
{message.icon && message.icon}
|
|
77
84
|
<PlanTitle>{message.name}</PlanTitle>
|
|
78
85
|
</div>
|
|
79
|
-
{
|
|
80
|
-
<PlanDescription>{
|
|
86
|
+
{description && (
|
|
87
|
+
<PlanDescription>{description}</PlanDescription>
|
|
81
88
|
)}
|
|
82
89
|
</div>
|
|
83
90
|
{collapsible && <PlanTrigger />}
|
|
@@ -86,12 +93,13 @@ export const SpecialistMessage = React.memo<SpecialistMessageProps>(
|
|
|
86
93
|
<PlanContent>
|
|
87
94
|
{/* Message content */}
|
|
88
95
|
{hasContent && (
|
|
89
|
-
<
|
|
96
|
+
<Response
|
|
90
97
|
className="size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
|
|
91
|
-
mode="static"
|
|
98
|
+
mode={message.status === "active" ? "streaming" : "static"}
|
|
99
|
+
isAnimating={message.status === "active"}
|
|
92
100
|
>
|
|
93
101
|
{message.content}
|
|
94
|
-
</
|
|
102
|
+
</Response>
|
|
95
103
|
)}
|
|
96
104
|
|
|
97
105
|
{/* Tool calls */}
|
|
@@ -89,6 +89,10 @@ export interface RefinementPanelProps {
|
|
|
89
89
|
* Processing state for HITL approval
|
|
90
90
|
*/
|
|
91
91
|
isApprovalProcessing?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Loading state — shows spinner on send button while streaming
|
|
94
|
+
*/
|
|
95
|
+
loading?: boolean;
|
|
92
96
|
/**
|
|
93
97
|
* Placeholder text for input
|
|
94
98
|
*/
|
|
@@ -115,6 +119,7 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
|
|
|
115
119
|
onApprovalReject,
|
|
116
120
|
onApprovalEdit,
|
|
117
121
|
isApprovalProcessing = false,
|
|
122
|
+
loading = false,
|
|
118
123
|
placeholder = "Ask a question or describe a task...",
|
|
119
124
|
className,
|
|
120
125
|
}) => {
|
|
@@ -146,26 +151,20 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
|
|
|
146
151
|
|
|
147
152
|
// Handle approve for HITL approval request
|
|
148
153
|
const handleApprovalApprove = React.useCallback(() => {
|
|
149
|
-
setApprovalCardState("approval-responded");
|
|
150
|
-
setApprovalCardApproval({ approved: true });
|
|
151
154
|
onApprovalApprove?.();
|
|
152
155
|
}, [onApprovalApprove]);
|
|
153
156
|
|
|
154
|
-
// Handle reject for HITL approval request
|
|
155
|
-
const handleApprovalReject = React.useCallback((reason: string) => {
|
|
156
|
-
setApprovalCardState("approval-responded");
|
|
157
|
-
setApprovalCardApproval({ approved: false });
|
|
158
|
-
onApprovalReject?.(reason);
|
|
159
|
-
}, [onApprovalReject]);
|
|
160
|
-
|
|
161
157
|
// Extract pending ask_user tool call from messages
|
|
162
158
|
const pendingAskUser = React.useMemo(() => {
|
|
159
|
+
console.log("[RefinementPanel] Messages length:", messages.length);
|
|
163
160
|
// Look from the end
|
|
164
161
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
165
162
|
const msg = messages[i];
|
|
166
|
-
if (msg.toolCalls) {
|
|
163
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
164
|
+
console.log(`[RefinementPanel] Message ${i} (${msg.id}) has ${msg.toolCalls.length} tool calls`, msg.toolCalls.map(tc => `${tc.name} [${tc.status}]`));
|
|
167
165
|
const askUserTc = msg.toolCalls.find(tc => tc.name === "ask_user" && tc.status === "pending");
|
|
168
166
|
if (askUserTc) {
|
|
167
|
+
console.log("[RefinementPanel] Found pending ask_user:", askUserTc);
|
|
169
168
|
return askUserTc;
|
|
170
169
|
}
|
|
171
170
|
}
|
|
@@ -176,6 +175,7 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
|
|
|
176
175
|
const activeApprovalRequest = React.useMemo(() => {
|
|
177
176
|
if (approvalRequest) return approvalRequest;
|
|
178
177
|
if (pendingAskUser) {
|
|
178
|
+
console.log("[RefinementPanel] ask_user args:", JSON.stringify(pendingAskUser.args));
|
|
179
179
|
return {
|
|
180
180
|
name: pendingAskUser.name,
|
|
181
181
|
args: pendingAskUser.args,
|
|
@@ -184,21 +184,27 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
|
|
|
184
184
|
return undefined;
|
|
185
185
|
}, [approvalRequest, pendingAskUser]);
|
|
186
186
|
|
|
187
|
+
// Handle reject for HITL approval request
|
|
188
|
+
const handleApprovalReject = React.useCallback((reason: string) => {
|
|
189
|
+
if (activeApprovalRequest?.name === "ask_user") {
|
|
190
|
+
const dummyEvent = { preventDefault: () => {} } as React.FormEvent<HTMLFormElement>;
|
|
191
|
+
onSubmit({ text: "Skipped", files: [] }, dummyEvent);
|
|
192
|
+
} else {
|
|
193
|
+
onApprovalReject?.(reason);
|
|
194
|
+
}
|
|
195
|
+
}, [onApprovalReject, activeApprovalRequest, onSubmit]);
|
|
196
|
+
|
|
187
197
|
// Handle edit for HITL approval request
|
|
188
198
|
const handleApprovalEdit = React.useCallback((editedArgs: Record<string, unknown>) => {
|
|
189
|
-
|
|
190
|
-
setApprovalCardApproval({ approved: true });
|
|
191
|
-
onApprovalEdit?.(editedArgs);
|
|
192
|
-
|
|
193
|
-
// If there's a pendingAskUser and answers are provided, submit them!
|
|
194
|
-
if (pendingAskUser && editedArgs.answers) {
|
|
199
|
+
if (activeApprovalRequest?.name === "ask_user" && editedArgs.answers) {
|
|
195
200
|
const answers = Array.isArray(editedArgs.answers) ? editedArgs.answers : [editedArgs.answers];
|
|
196
201
|
const text = answers.join(', ');
|
|
197
|
-
// Submit the text response
|
|
198
202
|
const dummyEvent = { preventDefault: () => {} } as React.FormEvent<HTMLFormElement>;
|
|
199
203
|
onSubmit({ text, files: [] }, dummyEvent);
|
|
204
|
+
} else {
|
|
205
|
+
onApprovalEdit?.(editedArgs);
|
|
200
206
|
}
|
|
201
|
-
}, [onApprovalEdit,
|
|
207
|
+
}, [onApprovalEdit, activeApprovalRequest, onSubmit]);
|
|
202
208
|
|
|
203
209
|
// Reset file change state when fileChanges are cleared
|
|
204
210
|
React.useEffect(() => {
|
|
@@ -253,18 +259,17 @@ export const RefinementPanel = React.memo<RefinementPanelProps>(
|
|
|
253
259
|
|
|
254
260
|
return (
|
|
255
261
|
<div className={`relative flex h-full flex-col ${className || ""}`}>
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
/>
|
|
262
|
-
</div>
|
|
262
|
+
<AIConversation
|
|
263
|
+
messages={messages}
|
|
264
|
+
showAvatars={true}
|
|
265
|
+
className="flex-1 min-h-0"
|
|
266
|
+
/>
|
|
263
267
|
<div className="sticky bottom-0 z-10 bg-background border-t">
|
|
264
268
|
<PromptInput
|
|
265
269
|
dialog={dialog}
|
|
266
270
|
placeholder={placeholder}
|
|
267
271
|
onSubmit={onSubmit}
|
|
272
|
+
loading={loading}
|
|
268
273
|
/>
|
|
269
274
|
</div>
|
|
270
275
|
</div>
|
|
@@ -43,14 +43,11 @@ export interface UseRefinementPanelReturn {
|
|
|
43
43
|
/** Review configuration for HITL approval */
|
|
44
44
|
reviewConfig?: ReviewConfig;
|
|
45
45
|
|
|
46
|
-
/** Handle approval of HITL approval request */
|
|
47
|
-
|
|
46
|
+
/** Handle approval of HITL approval request (ask_user or review_content approve) */
|
|
47
|
+
handleAskUserApprove?: () => Promise<void> | void;
|
|
48
48
|
|
|
49
49
|
/** Handle rejection of HITL approval request */
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/** Handle edit of HITL approval request */
|
|
53
|
-
handleApprovalEdit?: (editedArgs: Record<string, unknown>) => Promise<void> | void;
|
|
50
|
+
handleAskUserReject?: (reason: string) => Promise<void> | void;
|
|
54
51
|
|
|
55
52
|
/** Processing state for HITL approval */
|
|
56
53
|
isApprovalProcessing?: boolean;
|
|
@@ -68,11 +65,19 @@ export interface UseRefinementPanelReturn {
|
|
|
68
65
|
* @example
|
|
69
66
|
* ```tsx
|
|
70
67
|
* // Real application implementation
|
|
71
|
-
* export function useRefinementPanel(): UseRefinementPanelReturn {
|
|
68
|
+
* export function useRefinementPanel(options?: { threadId?: string }): UseRefinementPanelReturn {
|
|
69
|
+
* const { threadId } = options || {};
|
|
72
70
|
* const [messages, setMessages] = useState<RefinementMessage[]>([]);
|
|
73
71
|
* const [fileChanges, setFileChanges] = useState<FileChangeData[]>([]);
|
|
74
72
|
* const [loading, setLoading] = useState(false);
|
|
75
73
|
*
|
|
74
|
+
* useEffect(() => {
|
|
75
|
+
* if (!threadId) return;
|
|
76
|
+
* fetch(`/api/v1/chat/threads/${threadId}/history`)
|
|
77
|
+
* .then((res) => res.json())
|
|
78
|
+
* .then((data) => setMessages(data.messages));
|
|
79
|
+
* }, [threadId]);
|
|
80
|
+
*
|
|
76
81
|
* const handleSubmit = async (prompt: string) => {
|
|
77
82
|
* setLoading(true);
|
|
78
83
|
* try {
|
|
@@ -90,4 +95,4 @@ export interface UseRefinementPanelReturn {
|
|
|
90
95
|
* }
|
|
91
96
|
* ```
|
|
92
97
|
*/
|
|
93
|
-
export function useRefinementPanel(): UseRefinementPanelReturn;
|
|
98
|
+
export function useRefinementPanel(options?: { threadId?: string }): UseRefinementPanelReturn;
|
|
@@ -22,6 +22,7 @@ export interface UseRefinementPanelReturn {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface UseRefinementPanelOptions {
|
|
25
|
+
threadId?: string;
|
|
25
26
|
initialMessages?: RefinementMessage[];
|
|
26
27
|
reviewMessages?: RefinementMessage[];
|
|
27
28
|
reviewFileChanges?: FileChangeData[];
|
|
@@ -34,6 +35,7 @@ export function useRefinementPanelMock(
|
|
|
34
35
|
options: UseRefinementPanelOptions = {}
|
|
35
36
|
): UseRefinementPanelReturn {
|
|
36
37
|
const {
|
|
38
|
+
threadId,
|
|
37
39
|
initialMessages = [],
|
|
38
40
|
reviewMessages = [],
|
|
39
41
|
reviewFileChanges = [],
|
|
@@ -42,6 +44,10 @@ export function useRefinementPanelMock(
|
|
|
42
44
|
apiDelay = 800,
|
|
43
45
|
} = options;
|
|
44
46
|
|
|
47
|
+
if (threadId) {
|
|
48
|
+
console.log(`[useRefinementPanelMock] threadId=${threadId} — history would be fetched from API in production`);
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
const [messages, setMessages] = useState<RefinementMessage[]>(initialMessages);
|
|
46
52
|
const [fileChanges, setFileChanges] = useState<FileChangeData[]>([]);
|
|
47
53
|
const [loading, setLoading] = useState(false);
|