@stigmer/react-ui 0.0.34
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/LICENSE +190 -0
- package/README.md +70 -0
- package/execution/components/ApprovalControls.d.ts +10 -0
- package/execution/components/ApprovalControls.d.ts.map +1 -0
- package/execution/components/ApprovalControls.js +19 -0
- package/execution/components/ApprovalControls.js.map +1 -0
- package/execution/components/ExecutionStatus.d.ts +8 -0
- package/execution/components/ExecutionStatus.d.ts.map +1 -0
- package/execution/components/ExecutionStatus.js +13 -0
- package/execution/components/ExecutionStatus.js.map +1 -0
- package/execution/components/ExecutionStream.d.ts +16 -0
- package/execution/components/ExecutionStream.d.ts.map +1 -0
- package/execution/components/ExecutionStream.js +47 -0
- package/execution/components/ExecutionStream.js.map +1 -0
- package/execution/components/MessageEntry.d.ts +17 -0
- package/execution/components/MessageEntry.d.ts.map +1 -0
- package/execution/components/MessageEntry.js +44 -0
- package/execution/components/MessageEntry.js.map +1 -0
- package/execution/components/MessageInput.d.ts +10 -0
- package/execution/components/MessageInput.d.ts.map +1 -0
- package/execution/components/MessageInput.js +28 -0
- package/execution/components/MessageInput.js.map +1 -0
- package/execution/components/OutputBlock.d.ts +9 -0
- package/execution/components/OutputBlock.d.ts.map +1 -0
- package/execution/components/OutputBlock.js +15 -0
- package/execution/components/OutputBlock.js.map +1 -0
- package/execution/components/SubAgentCard.d.ts +11 -0
- package/execution/components/SubAgentCard.d.ts.map +1 -0
- package/execution/components/SubAgentCard.js +19 -0
- package/execution/components/SubAgentCard.js.map +1 -0
- package/execution/components/ToolCallCard.d.ts +12 -0
- package/execution/components/ToolCallCard.d.ts.map +1 -0
- package/execution/components/ToolCallCard.js +26 -0
- package/execution/components/ToolCallCard.js.map +1 -0
- package/execution/helpers.d.ts +34 -0
- package/execution/helpers.d.ts.map +1 -0
- package/execution/helpers.js +163 -0
- package/execution/helpers.js.map +1 -0
- package/execution/hooks/useAgentExecution.d.ts +24 -0
- package/execution/hooks/useAgentExecution.d.ts.map +1 -0
- package/execution/hooks/useAgentExecution.js +112 -0
- package/execution/hooks/useAgentExecution.js.map +1 -0
- package/execution/hooks/useApproval.d.ts +16 -0
- package/execution/hooks/useApproval.d.ts.map +1 -0
- package/execution/hooks/useApproval.js +25 -0
- package/execution/hooks/useApproval.js.map +1 -0
- package/execution/hooks/useExecutionService.d.ts +8 -0
- package/execution/hooks/useExecutionService.d.ts.map +1 -0
- package/execution/hooks/useExecutionService.js +14 -0
- package/execution/hooks/useExecutionService.js.map +1 -0
- package/execution/index.d.ts +17 -0
- package/execution/index.d.ts.map +1 -0
- package/execution/index.js +26 -0
- package/execution/index.js.map +1 -0
- package/execution/services/execution-service.d.ts +23 -0
- package/execution/services/execution-service.d.ts.map +1 -0
- package/execution/services/execution-service.js +70 -0
- package/execution/services/execution-service.js.map +1 -0
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -0
- package/index.js +2 -0
- package/index.js.map +1 -0
- package/internal/ui/badge.d.ts +8 -0
- package/internal/ui/badge.d.ts.map +1 -0
- package/internal/ui/badge.js +46 -0
- package/internal/ui/badge.js.map +1 -0
- package/internal/ui/button.d.ts +9 -0
- package/internal/ui/button.d.ts.map +1 -0
- package/internal/ui/button.js +48 -0
- package/internal/ui/button.js.map +1 -0
- package/internal/ui/collapsible.d.ts +6 -0
- package/internal/ui/collapsible.d.ts.map +1 -0
- package/internal/ui/collapsible.js +28 -0
- package/internal/ui/collapsible.js.map +1 -0
- package/internal/ui/textarea.d.ts +4 -0
- package/internal/ui/textarea.d.ts.map +1 -0
- package/internal/ui/textarea.js +19 -0
- package/internal/ui/textarea.js.map +1 -0
- package/package.json +54 -0
- package/src/execution/components/ApprovalControls.tsx +99 -0
- package/src/execution/components/ExecutionStatus.tsx +36 -0
- package/src/execution/components/ExecutionStream.tsx +167 -0
- package/src/execution/components/MessageEntry.tsx +143 -0
- package/src/execution/components/MessageInput.tsx +70 -0
- package/src/execution/components/OutputBlock.tsx +47 -0
- package/src/execution/components/SubAgentCard.tsx +135 -0
- package/src/execution/components/ToolCallCard.tsx +155 -0
- package/src/execution/helpers.ts +199 -0
- package/src/execution/hooks/useAgentExecution.ts +126 -0
- package/src/execution/hooks/useApproval.ts +55 -0
- package/src/execution/hooks/useExecutionService.ts +15 -0
- package/src/execution/index.ts +53 -0
- package/src/execution/services/execution-service.ts +125 -0
- package/src/index.ts +1 -0
- package/src/internal/ui/badge.tsx +52 -0
- package/src/internal/ui/button.tsx +60 -0
- package/src/internal/ui/collapsible.tsx +21 -0
- package/src/internal/ui/textarea.tsx +18 -0
- package/src/styles.css +43 -0
- package/styles.css +2 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
5
|
+
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
6
|
+
import type { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
7
|
+
import { ExecutionStatus } from "./ExecutionStatus";
|
|
8
|
+
import { MessageEntry } from "./MessageEntry";
|
|
9
|
+
import { HumanMessageBubble } from "./MessageEntry";
|
|
10
|
+
import { MessageInput } from "./MessageInput";
|
|
11
|
+
import {
|
|
12
|
+
buildSubAgentIndex,
|
|
13
|
+
isTerminalPhase,
|
|
14
|
+
} from "../helpers";
|
|
15
|
+
import { cn } from "@stigmer/theme";
|
|
16
|
+
import { Button } from "../../internal/ui/button";
|
|
17
|
+
import { ArrowDown, AlertCircle } from "lucide-react";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Props
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
interface ExecutionStreamProps {
|
|
24
|
+
execution: AgentExecution | null;
|
|
25
|
+
phase: ExecutionPhase;
|
|
26
|
+
isConnected: boolean;
|
|
27
|
+
error: string | null;
|
|
28
|
+
onApproval?: (
|
|
29
|
+
toolCallId: string,
|
|
30
|
+
action: ApprovalAction,
|
|
31
|
+
comment?: string,
|
|
32
|
+
) => Promise<void>;
|
|
33
|
+
isApprovalSubmitting?: boolean;
|
|
34
|
+
onSendMessage?: (message: string) => void;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Component
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export function ExecutionStream(props: ExecutionStreamProps) {
|
|
43
|
+
const {
|
|
44
|
+
execution,
|
|
45
|
+
phase,
|
|
46
|
+
error,
|
|
47
|
+
onApproval,
|
|
48
|
+
isApprovalSubmitting = false,
|
|
49
|
+
onSendMessage,
|
|
50
|
+
className,
|
|
51
|
+
} = props;
|
|
52
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
53
|
+
const bottomRef = useRef<HTMLDivElement>(null);
|
|
54
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
55
|
+
|
|
56
|
+
const messages = execution?.status?.messages ?? [];
|
|
57
|
+
const subAgentIndex = useMemo(
|
|
58
|
+
() => (execution ? buildSubAgentIndex(execution) : new Map()),
|
|
59
|
+
[execution],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// ── Scroll-lock: auto-scroll when at bottom ──
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (isAtBottom) {
|
|
65
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
66
|
+
}
|
|
67
|
+
}, [messages.length, isAtBottom]);
|
|
68
|
+
|
|
69
|
+
const handleScroll = useCallback(() => {
|
|
70
|
+
const el = scrollRef.current;
|
|
71
|
+
if (!el) return;
|
|
72
|
+
const threshold = 48;
|
|
73
|
+
const atBottom =
|
|
74
|
+
el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
|
75
|
+
setIsAtBottom(atBottom);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const scrollToBottom = useCallback(() => {
|
|
79
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
80
|
+
setIsAtBottom(true);
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const isTerminal = isTerminalPhase(phase);
|
|
84
|
+
const canSendMessage = isTerminal && !!onSendMessage;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className={cn("flex flex-col", className)}>
|
|
88
|
+
{/* ── Header ── */}
|
|
89
|
+
<div className="flex items-center justify-between border-b px-4 py-2">
|
|
90
|
+
<ExecutionStatus phase={phase} />
|
|
91
|
+
{execution?.status?.startedAt && (
|
|
92
|
+
<time
|
|
93
|
+
dateTime={execution.status.startedAt}
|
|
94
|
+
className="text-xs text-muted-foreground"
|
|
95
|
+
>
|
|
96
|
+
{new Date(execution.status.startedAt).toLocaleTimeString()}
|
|
97
|
+
</time>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* ── Stream content ── */}
|
|
102
|
+
<div
|
|
103
|
+
ref={scrollRef}
|
|
104
|
+
onScroll={handleScroll}
|
|
105
|
+
className="relative flex-1 overflow-y-auto"
|
|
106
|
+
>
|
|
107
|
+
<div className="space-y-4 p-4">
|
|
108
|
+
{/* Show spec.message as the initial user message if no HUMAN message exists yet */}
|
|
109
|
+
{messages.length === 0 && execution?.spec?.message && (
|
|
110
|
+
<HumanMessageBubble content={execution.spec.message} />
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{messages.map((msg, index) => (
|
|
114
|
+
<MessageEntry
|
|
115
|
+
key={index}
|
|
116
|
+
message={msg}
|
|
117
|
+
subAgentIndex={subAgentIndex}
|
|
118
|
+
onApproval={onApproval}
|
|
119
|
+
isApprovalSubmitting={isApprovalSubmitting}
|
|
120
|
+
/>
|
|
121
|
+
))}
|
|
122
|
+
|
|
123
|
+
{/* Error banner */}
|
|
124
|
+
{error && (
|
|
125
|
+
<div className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
|
|
126
|
+
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
|
127
|
+
<p>{error}</p>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Execution error from status */}
|
|
132
|
+
{execution?.status?.error && isTerminal && (
|
|
133
|
+
<div className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
|
|
134
|
+
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
|
135
|
+
<p>{execution.status.error}</p>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
<div ref={bottomRef} />
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Scroll-to-bottom FAB */}
|
|
143
|
+
{!isAtBottom && (
|
|
144
|
+
<Button
|
|
145
|
+
size="icon"
|
|
146
|
+
variant="secondary"
|
|
147
|
+
onClick={scrollToBottom}
|
|
148
|
+
className="absolute bottom-4 right-4 z-10 rounded-full shadow-md"
|
|
149
|
+
aria-label="Scroll to bottom"
|
|
150
|
+
>
|
|
151
|
+
<ArrowDown className="size-4" />
|
|
152
|
+
</Button>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* ── Message input ── */}
|
|
157
|
+
{canSendMessage && (
|
|
158
|
+
<div className="border-t p-4">
|
|
159
|
+
<MessageInput
|
|
160
|
+
onSend={onSendMessage}
|
|
161
|
+
placeholder="Send a follow-up message..."
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
2
|
+
import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
|
|
3
|
+
import type { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
4
|
+
import { OutputBlock } from "./OutputBlock";
|
|
5
|
+
import { ToolCallCard } from "./ToolCallCard";
|
|
6
|
+
import { SubAgentCard } from "./SubAgentCard";
|
|
7
|
+
import {
|
|
8
|
+
isHumanMessage,
|
|
9
|
+
isAiMessage,
|
|
10
|
+
isSystemMessage,
|
|
11
|
+
} from "../helpers";
|
|
12
|
+
import { User, BotMessageSquare, Info } from "lucide-react";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Props
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export interface MessageEntryProps {
|
|
19
|
+
message: AgentMessage;
|
|
20
|
+
subAgentIndex: Map<string, SubAgentExecution>;
|
|
21
|
+
onApproval?: (
|
|
22
|
+
toolCallId: string,
|
|
23
|
+
action: ApprovalAction,
|
|
24
|
+
comment?: string,
|
|
25
|
+
) => Promise<void>;
|
|
26
|
+
isApprovalSubmitting?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Message dispatcher
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export function MessageEntry({
|
|
34
|
+
message,
|
|
35
|
+
subAgentIndex,
|
|
36
|
+
onApproval,
|
|
37
|
+
isApprovalSubmitting,
|
|
38
|
+
}: MessageEntryProps) {
|
|
39
|
+
if (isHumanMessage(message.type)) {
|
|
40
|
+
return <HumanMessageBubble content={message.content} />;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isAiMessage(message.type)) {
|
|
44
|
+
return (
|
|
45
|
+
<AiMessageBlock
|
|
46
|
+
message={message}
|
|
47
|
+
subAgentIndex={subAgentIndex}
|
|
48
|
+
onApproval={onApproval}
|
|
49
|
+
isApprovalSubmitting={isApprovalSubmitting}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isSystemMessage(message.type)) {
|
|
55
|
+
return <SystemMessageBlock content={message.content} />;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MESSAGE_TOOL: tool results are rendered inline with the tool call card,
|
|
59
|
+
// so we don't render a separate block for them.
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Message type blocks
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export function HumanMessageBubble({ content }: { content: string }) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex items-start gap-3">
|
|
70
|
+
<div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-muted">
|
|
71
|
+
<User className="size-3.5 text-muted-foreground" />
|
|
72
|
+
</div>
|
|
73
|
+
<div className="min-w-0 flex-1 rounded-lg bg-muted px-3 py-2 text-sm">
|
|
74
|
+
<p className="whitespace-pre-wrap">{content}</p>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function AiMessageBlock({
|
|
81
|
+
message,
|
|
82
|
+
subAgentIndex,
|
|
83
|
+
onApproval,
|
|
84
|
+
isApprovalSubmitting,
|
|
85
|
+
}: {
|
|
86
|
+
message: AgentMessage;
|
|
87
|
+
subAgentIndex: Map<string, SubAgentExecution>;
|
|
88
|
+
onApproval?: (
|
|
89
|
+
toolCallId: string,
|
|
90
|
+
action: ApprovalAction,
|
|
91
|
+
comment?: string,
|
|
92
|
+
) => Promise<void>;
|
|
93
|
+
isApprovalSubmitting?: boolean;
|
|
94
|
+
}) {
|
|
95
|
+
return (
|
|
96
|
+
<div className="flex items-start gap-3">
|
|
97
|
+
<div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10">
|
|
98
|
+
<BotMessageSquare className="size-3.5 text-primary" />
|
|
99
|
+
</div>
|
|
100
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
101
|
+
{message.content && (
|
|
102
|
+
<OutputBlock
|
|
103
|
+
content={message.content}
|
|
104
|
+
isStreaming={message.isStreaming}
|
|
105
|
+
model={message.model}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{message.toolCalls.map((tc) => {
|
|
110
|
+
const subAgent = subAgentIndex.get(tc.id);
|
|
111
|
+
if (subAgent) {
|
|
112
|
+
return (
|
|
113
|
+
<SubAgentCard
|
|
114
|
+
key={tc.id}
|
|
115
|
+
subAgent={subAgent}
|
|
116
|
+
onApproval={onApproval}
|
|
117
|
+
isApprovalSubmitting={isApprovalSubmitting}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return (
|
|
122
|
+
<ToolCallCard
|
|
123
|
+
key={tc.id}
|
|
124
|
+
toolCall={tc}
|
|
125
|
+
onApproval={onApproval}
|
|
126
|
+
isApprovalSubmitting={isApprovalSubmitting}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
})}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function SystemMessageBlock({ content }: { content: string }) {
|
|
136
|
+
if (!content) return null;
|
|
137
|
+
return (
|
|
138
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
139
|
+
<Info className="size-3 shrink-0" />
|
|
140
|
+
<p>{content}</p>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, type KeyboardEvent } from "react";
|
|
4
|
+
import { Textarea } from "../../internal/ui/textarea";
|
|
5
|
+
import { Button } from "../../internal/ui/button";
|
|
6
|
+
import { cn } from "@stigmer/theme";
|
|
7
|
+
import { SendHorizontal, Loader2 } from "lucide-react";
|
|
8
|
+
|
|
9
|
+
interface MessageInputProps {
|
|
10
|
+
onSend: (message: string) => void;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
isLoading?: boolean;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function MessageInput({
|
|
18
|
+
onSend,
|
|
19
|
+
disabled = false,
|
|
20
|
+
isLoading = false,
|
|
21
|
+
placeholder = "Send a message...",
|
|
22
|
+
className,
|
|
23
|
+
}: MessageInputProps) {
|
|
24
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
25
|
+
|
|
26
|
+
const handleSend = useCallback(() => {
|
|
27
|
+
const value = textareaRef.current?.value.trim();
|
|
28
|
+
if (!value || disabled || isLoading) return;
|
|
29
|
+
onSend(value);
|
|
30
|
+
if (textareaRef.current) {
|
|
31
|
+
textareaRef.current.value = "";
|
|
32
|
+
}
|
|
33
|
+
}, [onSend, disabled, isLoading]);
|
|
34
|
+
|
|
35
|
+
const handleKeyDown = useCallback(
|
|
36
|
+
(e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
37
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
handleSend();
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[handleSend],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className={cn("flex items-end gap-2", className)}>
|
|
47
|
+
<Textarea
|
|
48
|
+
ref={textareaRef}
|
|
49
|
+
placeholder={placeholder}
|
|
50
|
+
disabled={disabled || isLoading}
|
|
51
|
+
onKeyDown={handleKeyDown}
|
|
52
|
+
className="min-h-10 resize-none"
|
|
53
|
+
rows={1}
|
|
54
|
+
/>
|
|
55
|
+
<Button
|
|
56
|
+
size="icon"
|
|
57
|
+
onClick={handleSend}
|
|
58
|
+
disabled={disabled || isLoading}
|
|
59
|
+
className="shrink-0"
|
|
60
|
+
aria-label="Send message"
|
|
61
|
+
>
|
|
62
|
+
{isLoading ? (
|
|
63
|
+
<Loader2 className="size-4 animate-spin" />
|
|
64
|
+
) : (
|
|
65
|
+
<SendHorizontal className="size-4" />
|
|
66
|
+
)}
|
|
67
|
+
</Button>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
import ReactMarkdown from "react-markdown";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import { cn } from "@stigmer/theme";
|
|
7
|
+
|
|
8
|
+
interface OutputBlockProps {
|
|
9
|
+
content: string;
|
|
10
|
+
isStreaming?: boolean;
|
|
11
|
+
model?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const OutputBlock = memo(function OutputBlock({
|
|
16
|
+
content,
|
|
17
|
+
isStreaming = false,
|
|
18
|
+
model,
|
|
19
|
+
className,
|
|
20
|
+
}: OutputBlockProps) {
|
|
21
|
+
if (!content && !isStreaming) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className={cn("relative", className)}>
|
|
25
|
+
<div className="prose prose-sm dark:prose-invert max-w-none break-words [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs [&_code]:rounded [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-xs [&_code]:before:content-none [&_code]:after:content-none [&_table]:text-xs [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1">
|
|
26
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
27
|
+
{content}
|
|
28
|
+
</ReactMarkdown>
|
|
29
|
+
{isStreaming && <StreamingCursor />}
|
|
30
|
+
</div>
|
|
31
|
+
{model && !isStreaming && (
|
|
32
|
+
<p className="mt-1.5 text-[10px] text-muted-foreground/60">
|
|
33
|
+
{model}
|
|
34
|
+
</p>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function StreamingCursor() {
|
|
41
|
+
return (
|
|
42
|
+
<span
|
|
43
|
+
className="ml-0.5 inline-block h-4 w-0.5 animate-pulse bg-foreground align-text-bottom"
|
|
44
|
+
aria-label="Generating..."
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Badge } from "../../internal/ui/badge";
|
|
5
|
+
import {
|
|
6
|
+
Collapsible,
|
|
7
|
+
CollapsibleContent,
|
|
8
|
+
CollapsibleTrigger,
|
|
9
|
+
} from "../../internal/ui/collapsible";
|
|
10
|
+
import { OutputBlock } from "./OutputBlock";
|
|
11
|
+
import { ToolCallCard } from "./ToolCallCard";
|
|
12
|
+
import {
|
|
13
|
+
subAgentStatusLabel,
|
|
14
|
+
subAgentStatusVariant,
|
|
15
|
+
formatDuration,
|
|
16
|
+
isAiMessage,
|
|
17
|
+
} from "../helpers";
|
|
18
|
+
import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
|
|
19
|
+
import type { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
20
|
+
import { SubAgentStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
21
|
+
import { cn } from "@stigmer/theme";
|
|
22
|
+
import { ChevronRight, Bot, Loader2 } from "lucide-react";
|
|
23
|
+
|
|
24
|
+
interface SubAgentCardProps {
|
|
25
|
+
subAgent: SubAgentExecution;
|
|
26
|
+
onApproval?: (
|
|
27
|
+
toolCallId: string,
|
|
28
|
+
action: ApprovalAction,
|
|
29
|
+
comment?: string,
|
|
30
|
+
) => Promise<void>;
|
|
31
|
+
isApprovalSubmitting?: boolean;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SubAgentCard({
|
|
36
|
+
subAgent,
|
|
37
|
+
onApproval,
|
|
38
|
+
isApprovalSubmitting = false,
|
|
39
|
+
className,
|
|
40
|
+
}: SubAgentCardProps) {
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const isActive = subAgent.status === SubAgentStatus.SUB_AGENT_IN_PROGRESS;
|
|
43
|
+
const duration = formatDuration(subAgent.startedAt, subAgent.completedAt);
|
|
44
|
+
const aiMessages = subAgent.messages.filter((m) => isAiMessage(m.type));
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Collapsible open={open} onOpenChange={setOpen}>
|
|
48
|
+
<div
|
|
49
|
+
className={cn(
|
|
50
|
+
"rounded-lg border border-dashed bg-card text-card-foreground text-sm",
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
<CollapsibleTrigger className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/50 transition-colors rounded-t-lg">
|
|
55
|
+
<ChevronRight
|
|
56
|
+
className={cn(
|
|
57
|
+
"size-3.5 shrink-0 text-muted-foreground transition-transform",
|
|
58
|
+
open && "rotate-90",
|
|
59
|
+
)}
|
|
60
|
+
/>
|
|
61
|
+
{isActive ? (
|
|
62
|
+
<Loader2 className="size-3.5 shrink-0 animate-spin text-muted-foreground" />
|
|
63
|
+
) : (
|
|
64
|
+
<Bot className="size-3.5 shrink-0 text-muted-foreground" />
|
|
65
|
+
)}
|
|
66
|
+
<span className="truncate font-medium text-xs">
|
|
67
|
+
{subAgent.subject || subAgent.name}
|
|
68
|
+
</span>
|
|
69
|
+
<Badge
|
|
70
|
+
variant={subAgentStatusVariant(subAgent.status)}
|
|
71
|
+
className="ml-auto shrink-0 text-[10px]"
|
|
72
|
+
>
|
|
73
|
+
{subAgentStatusLabel(subAgent.status)}
|
|
74
|
+
</Badge>
|
|
75
|
+
{duration && (
|
|
76
|
+
<span className="shrink-0 text-[10px] text-muted-foreground">
|
|
77
|
+
{duration}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
{subAgent.toolCalls.length > 0 && (
|
|
81
|
+
<span className="shrink-0 text-[10px] text-muted-foreground">
|
|
82
|
+
{subAgent.toolCalls.length} tool{subAgent.toolCalls.length !== 1 && "s"}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
</CollapsibleTrigger>
|
|
86
|
+
|
|
87
|
+
<CollapsibleContent>
|
|
88
|
+
<div className="border-t px-3 py-2 space-y-3">
|
|
89
|
+
{subAgent.input && (
|
|
90
|
+
<div>
|
|
91
|
+
<p className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
92
|
+
Task
|
|
93
|
+
</p>
|
|
94
|
+
<p className="text-xs text-muted-foreground">{subAgent.input}</p>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{aiMessages.map((msg, i) => (
|
|
99
|
+
<OutputBlock
|
|
100
|
+
key={i}
|
|
101
|
+
content={msg.content}
|
|
102
|
+
isStreaming={msg.isStreaming}
|
|
103
|
+
model={msg.model}
|
|
104
|
+
/>
|
|
105
|
+
))}
|
|
106
|
+
|
|
107
|
+
{subAgent.toolCalls.map((tc) => (
|
|
108
|
+
<ToolCallCard
|
|
109
|
+
key={tc.id}
|
|
110
|
+
toolCall={tc}
|
|
111
|
+
onApproval={onApproval}
|
|
112
|
+
isApprovalSubmitting={isApprovalSubmitting}
|
|
113
|
+
/>
|
|
114
|
+
))}
|
|
115
|
+
|
|
116
|
+
{subAgent.error && (
|
|
117
|
+
<p className="text-xs text-destructive">{subAgent.error}</p>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{subAgent.output && (
|
|
121
|
+
<div>
|
|
122
|
+
<p className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
123
|
+
Result
|
|
124
|
+
</p>
|
|
125
|
+
<pre className="overflow-x-auto rounded bg-muted p-2 text-[11px] leading-relaxed whitespace-pre-wrap">
|
|
126
|
+
{subAgent.output}
|
|
127
|
+
</pre>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</CollapsibleContent>
|
|
132
|
+
</div>
|
|
133
|
+
</Collapsible>
|
|
134
|
+
);
|
|
135
|
+
}
|