@echothink-ui/agent 0.1.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/README.md +5 -0
- package/dist/components/AgentApprovalGate.d.ts +10 -0
- package/dist/components/AgentContextViewer.d.ts +7 -0
- package/dist/components/AgentGeneratedArtifactPanel.d.ts +8 -0
- package/dist/components/AgentHandoffPanel.d.ts +22 -0
- package/dist/components/AgentInterruptionPanel.d.ts +13 -0
- package/dist/components/AgentMemoryPanel.d.ts +9 -0
- package/dist/components/AgentMessageList.d.ts +8 -0
- package/dist/components/AgentPlanDiff.d.ts +7 -0
- package/dist/components/AgentPlanPreview.d.ts +6 -0
- package/dist/components/AgentPromptBox.d.ts +15 -0
- package/dist/components/AgentRunControls.d.ts +10 -0
- package/dist/components/AgentSafetyPanel.d.ts +6 -0
- package/dist/components/AgentStateBadge.d.ts +6 -0
- package/dist/components/AgentThinkingChain.d.ts +8 -0
- package/dist/components/AgentThinkingPanel.d.ts +8 -0
- package/dist/components/AgentToolCallLog.d.ts +7 -0
- package/dist/components/AgentTraceViewer.d.ts +6 -0
- package/dist/components/AppDomainAgentPanel.d.ts +17 -0
- package/dist/components/ChatAgentRail.d.ts +24 -0
- package/dist/components/ScopeAttachmentPanel.d.ts +7 -0
- package/dist/components/utils.d.ts +20 -0
- package/dist/index.cjs +2709 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +2433 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +2666 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/package.json +45 -0
- package/src/components/AgentApprovalGate.tsx +165 -0
- package/src/components/AgentContextViewer.tsx +161 -0
- package/src/components/AgentGeneratedArtifactPanel.tsx +224 -0
- package/src/components/AgentHandoffPanel.tsx +154 -0
- package/src/components/AgentInterruptionPanel.tsx +85 -0
- package/src/components/AgentMemoryPanel.tsx +167 -0
- package/src/components/AgentMessageList.tsx +209 -0
- package/src/components/AgentPlanDiff.tsx +149 -0
- package/src/components/AgentPlanPreview.tsx +106 -0
- package/src/components/AgentPromptBox.tsx +163 -0
- package/src/components/AgentRunControls.tsx +221 -0
- package/src/components/AgentSafetyPanel.tsx +113 -0
- package/src/components/AgentStateBadge.tsx +30 -0
- package/src/components/AgentThinkingChain.tsx +151 -0
- package/src/components/AgentThinkingPanel.tsx +56 -0
- package/src/components/AgentToolCallLog.tsx +262 -0
- package/src/components/AgentTraceViewer.tsx +218 -0
- package/src/components/AppDomainAgentPanel.tsx +66 -0
- package/src/components/ChatAgentRail.tsx +192 -0
- package/src/components/ScopeAttachmentPanel.tsx +130 -0
- package/src/components/utils.ts +186 -0
- package/src/index.test.tsx +212 -0
- package/src/index.tsx +88 -0
- package/src/styles.css +2902 -0
- package/src/types.ts +158 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Button, InlineNotification, Surface, Textarea } from "@echothink-ui/core";
|
|
4
|
+
import { ExternalLinkIcon } from "@echothink-ui/icons";
|
|
5
|
+
import type { SurfaceComponentProps } from "@echothink-ui/core";
|
|
6
|
+
import type { AgentApprovalAction, AgentCitation } from "../types";
|
|
7
|
+
import { riskSeverity } from "./utils";
|
|
8
|
+
|
|
9
|
+
export interface AgentApprovalGateProps extends Omit<SurfaceComponentProps, "children"> {
|
|
10
|
+
action: AgentApprovalAction;
|
|
11
|
+
evidence?: AgentCitation[];
|
|
12
|
+
onApprove?: (reason?: string) => void;
|
|
13
|
+
onReject?: (reason?: string) => void;
|
|
14
|
+
state?: "idle" | "pending" | "approved" | "rejected";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function AgentApprovalGate({
|
|
18
|
+
action,
|
|
19
|
+
evidence = [],
|
|
20
|
+
onApprove,
|
|
21
|
+
onReject,
|
|
22
|
+
state = "idle",
|
|
23
|
+
title = "Approval required",
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: AgentApprovalGateProps) {
|
|
27
|
+
const [reason, setReason] = React.useState("");
|
|
28
|
+
const severity = riskSeverity(action.riskLevel);
|
|
29
|
+
const locked = state === "pending" || state === "approved" || state === "rejected";
|
|
30
|
+
const stateLabel = approvalStateLabel(state);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Surface
|
|
34
|
+
{...props}
|
|
35
|
+
className={clsx("eth-agent-approval-gate", `eth-agent-approval-gate--${state}`, className)}
|
|
36
|
+
data-eth-component="AgentApprovalGate"
|
|
37
|
+
severity={severity}
|
|
38
|
+
status={approvalStatus(state)}
|
|
39
|
+
title={title}
|
|
40
|
+
subtitle={action.label}
|
|
41
|
+
>
|
|
42
|
+
<div className="eth-agent-approval-gate__layout">
|
|
43
|
+
<InlineNotification
|
|
44
|
+
severity={severity}
|
|
45
|
+
title={`${riskLabel(action.riskLevel)} risk action`}
|
|
46
|
+
>
|
|
47
|
+
{action.description}
|
|
48
|
+
</InlineNotification>
|
|
49
|
+
|
|
50
|
+
<dl className="eth-agent-approval-gate__summary" aria-label="Approval action details">
|
|
51
|
+
<div>
|
|
52
|
+
<dt>Action</dt>
|
|
53
|
+
<dd>{action.id}</dd>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<dt>Risk</dt>
|
|
57
|
+
<dd>{riskLabel(action.riskLevel)}</dd>
|
|
58
|
+
</div>
|
|
59
|
+
<div>
|
|
60
|
+
<dt>Status</dt>
|
|
61
|
+
<dd>{stateLabel}</dd>
|
|
62
|
+
</div>
|
|
63
|
+
</dl>
|
|
64
|
+
|
|
65
|
+
{evidence.length ? (
|
|
66
|
+
<section className="eth-agent-approval-gate__evidence">
|
|
67
|
+
<div className="eth-agent-approval-gate__section-header">
|
|
68
|
+
<h3>Evidence</h3>
|
|
69
|
+
<span>
|
|
70
|
+
{evidence.length} reference{evidence.length === 1 ? "" : "s"}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="eth-agent-approval-gate__evidence-list">
|
|
74
|
+
{evidence.map((item) => (
|
|
75
|
+
<EvidenceItem key={item.id} item={item} />
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
) : null}
|
|
80
|
+
|
|
81
|
+
<Textarea
|
|
82
|
+
className="eth-agent-approval-gate__reason"
|
|
83
|
+
aria-label="Approval reason"
|
|
84
|
+
disabled={locked}
|
|
85
|
+
labelText="Approval rationale"
|
|
86
|
+
placeholder="Optional reason"
|
|
87
|
+
rows={2}
|
|
88
|
+
value={reason}
|
|
89
|
+
onChange={(event) => setReason(event.currentTarget.value)}
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
<div className="eth-agent-approval-gate__actions">
|
|
93
|
+
<Button
|
|
94
|
+
density="default"
|
|
95
|
+
intent="success"
|
|
96
|
+
loading={state === "pending"}
|
|
97
|
+
disabled={locked}
|
|
98
|
+
onClick={() => onApprove?.(reason || undefined)}
|
|
99
|
+
>
|
|
100
|
+
{state === "approved" ? "Approved" : "Approve"}
|
|
101
|
+
</Button>
|
|
102
|
+
<Button
|
|
103
|
+
density="default"
|
|
104
|
+
intent="danger"
|
|
105
|
+
disabled={locked}
|
|
106
|
+
onClick={() => onReject?.(reason || undefined)}
|
|
107
|
+
>
|
|
108
|
+
{state === "rejected" ? "Rejected" : "Reject"}
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</Surface>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function EvidenceItem({ item }: { item: AgentCitation }) {
|
|
117
|
+
const content = (
|
|
118
|
+
<>
|
|
119
|
+
<span className="eth-agent-approval-gate__evidence-copy">
|
|
120
|
+
<span className="eth-agent-approval-gate__evidence-label">{item.label}</span>
|
|
121
|
+
{item.description ? (
|
|
122
|
+
<span className="eth-agent-approval-gate__evidence-description">{item.description}</span>
|
|
123
|
+
) : null}
|
|
124
|
+
</span>
|
|
125
|
+
{item.meta ? (
|
|
126
|
+
<span className="eth-agent-approval-gate__evidence-meta">{item.meta}</span>
|
|
127
|
+
) : null}
|
|
128
|
+
{item.href ? (
|
|
129
|
+
<ExternalLinkIcon className="eth-agent-approval-gate__evidence-icon" size={16} />
|
|
130
|
+
) : null}
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (item.href) {
|
|
135
|
+
return (
|
|
136
|
+
<a
|
|
137
|
+
className="eth-agent-approval-gate__evidence-item eth-agent-approval-gate__evidence-item--link"
|
|
138
|
+
href={item.href}
|
|
139
|
+
>
|
|
140
|
+
{content}
|
|
141
|
+
</a>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return <div className="eth-agent-approval-gate__evidence-item">{content}</div>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function riskLabel(riskLevel: AgentApprovalAction["riskLevel"]) {
|
|
149
|
+
if (!riskLevel) return "Low";
|
|
150
|
+
return riskLevel.charAt(0).toUpperCase() + riskLevel.slice(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function approvalStateLabel(state: AgentApprovalGateProps["state"]) {
|
|
154
|
+
if (state === "pending") return "Submitting";
|
|
155
|
+
if (state === "approved") return "Approved";
|
|
156
|
+
if (state === "rejected") return "Rejected";
|
|
157
|
+
return "Awaiting decision";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function approvalStatus(state: AgentApprovalGateProps["state"]) {
|
|
161
|
+
if (state === "pending") return "running";
|
|
162
|
+
if (state === "approved") return "completed";
|
|
163
|
+
if (state === "rejected") return "blocked";
|
|
164
|
+
return "approval-required";
|
|
165
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Badge, EmptyState, Surface } from "@echothink-ui/core";
|
|
4
|
+
import {
|
|
5
|
+
AgentRunningIcon,
|
|
6
|
+
ChevronRightIcon,
|
|
7
|
+
DocumentIcon,
|
|
8
|
+
ExternalLinkIcon,
|
|
9
|
+
MessageIcon,
|
|
10
|
+
SearchIcon,
|
|
11
|
+
type EthIconProps
|
|
12
|
+
} from "@echothink-ui/icons";
|
|
13
|
+
import type { SurfaceComponentProps } from "@echothink-ui/core";
|
|
14
|
+
import type { AgentContextSource } from "../types";
|
|
15
|
+
|
|
16
|
+
export interface AgentContextViewerProps extends Omit<
|
|
17
|
+
SurfaceComponentProps,
|
|
18
|
+
"children" | "onSelect"
|
|
19
|
+
> {
|
|
20
|
+
sources: AgentContextSource[];
|
|
21
|
+
onSelect?: (source: AgentContextSource) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function AgentContextViewer({
|
|
25
|
+
sources,
|
|
26
|
+
onSelect,
|
|
27
|
+
title = "Context sources",
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}: AgentContextViewerProps) {
|
|
31
|
+
const sourceListLabel = typeof title === "string" ? title : "Context sources";
|
|
32
|
+
const sortedSources = React.useMemo(
|
|
33
|
+
() => [...sources].sort((a, b) => (b.relevance ?? -1) - (a.relevance ?? -1)),
|
|
34
|
+
[sources]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Surface
|
|
39
|
+
{...props}
|
|
40
|
+
className={clsx("eth-agent-context-viewer", className)}
|
|
41
|
+
data-eth-component="AgentContextViewer"
|
|
42
|
+
title={title}
|
|
43
|
+
>
|
|
44
|
+
{!sortedSources.length ? (
|
|
45
|
+
<EmptyState title="No context sources" />
|
|
46
|
+
) : (
|
|
47
|
+
<ul className="eth-agent-context-viewer__sources" aria-label={sourceListLabel}>
|
|
48
|
+
{sortedSources.map((source) => {
|
|
49
|
+
const SourceIcon = sourceIcon(source.type);
|
|
50
|
+
const relevance = source.relevance == null ? null : formatRelevance(source.relevance);
|
|
51
|
+
const relevanceValue =
|
|
52
|
+
source.relevance == null ? null : normalizedRelevance(source.relevance);
|
|
53
|
+
const isInteractive = Boolean(source.href || onSelect);
|
|
54
|
+
|
|
55
|
+
const content = (
|
|
56
|
+
<>
|
|
57
|
+
<span className="eth-agent-context-viewer__kind-icon" aria-hidden="true">
|
|
58
|
+
<SourceIcon size={18} />
|
|
59
|
+
</span>
|
|
60
|
+
<span className="eth-agent-context-viewer__main">
|
|
61
|
+
<span className="eth-agent-context-viewer__topline">
|
|
62
|
+
<strong className="eth-agent-context-viewer__label">{source.label}</strong>
|
|
63
|
+
<Badge severity="neutral">{typeLabel(source.type)}</Badge>
|
|
64
|
+
</span>
|
|
65
|
+
{source.excerpt ? (
|
|
66
|
+
<span className="eth-agent-context-viewer__excerpt">{source.excerpt}</span>
|
|
67
|
+
) : null}
|
|
68
|
+
</span>
|
|
69
|
+
<span className="eth-agent-context-viewer__aside">
|
|
70
|
+
{relevance != null && relevanceValue != null ? (
|
|
71
|
+
<span
|
|
72
|
+
className="eth-agent-context-viewer__relevance"
|
|
73
|
+
aria-label={`Relevance ${relevance}`}
|
|
74
|
+
>
|
|
75
|
+
<span className="eth-agent-context-viewer__relevance-value">
|
|
76
|
+
Relevance {relevance}
|
|
77
|
+
</span>
|
|
78
|
+
<span
|
|
79
|
+
className="eth-agent-context-viewer__relevance-meter"
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
>
|
|
82
|
+
<span style={{ inlineSize: `${relevanceValue}%` }} />
|
|
83
|
+
</span>
|
|
84
|
+
</span>
|
|
85
|
+
) : (
|
|
86
|
+
<span className="eth-agent-context-viewer__unranked">Unranked</span>
|
|
87
|
+
)}
|
|
88
|
+
{source.href ? (
|
|
89
|
+
<ExternalLinkIcon className="eth-agent-context-viewer__indicator" size={16} />
|
|
90
|
+
) : isInteractive ? (
|
|
91
|
+
<ChevronRightIcon className="eth-agent-context-viewer__indicator" size={16} />
|
|
92
|
+
) : null}
|
|
93
|
+
</span>
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<li key={source.id}>
|
|
99
|
+
{source.href ? (
|
|
100
|
+
<a
|
|
101
|
+
className={clsx(
|
|
102
|
+
"eth-agent-context-viewer__source",
|
|
103
|
+
"eth-agent-context-viewer__source--interactive"
|
|
104
|
+
)}
|
|
105
|
+
href={source.href}
|
|
106
|
+
onClick={() => onSelect?.(source)}
|
|
107
|
+
>
|
|
108
|
+
{content}
|
|
109
|
+
</a>
|
|
110
|
+
) : onSelect ? (
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
className={clsx(
|
|
114
|
+
"eth-agent-context-viewer__source",
|
|
115
|
+
"eth-agent-context-viewer__source--interactive"
|
|
116
|
+
)}
|
|
117
|
+
onClick={() => onSelect(source)}
|
|
118
|
+
>
|
|
119
|
+
{content}
|
|
120
|
+
</button>
|
|
121
|
+
) : (
|
|
122
|
+
<article className="eth-agent-context-viewer__source">{content}</article>
|
|
123
|
+
)}
|
|
124
|
+
</li>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</ul>
|
|
128
|
+
)}
|
|
129
|
+
</Surface>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sourceIcon(type: AgentContextSource["type"]): React.ComponentType<EthIconProps> {
|
|
134
|
+
switch (type) {
|
|
135
|
+
case "document":
|
|
136
|
+
return DocumentIcon;
|
|
137
|
+
case "query":
|
|
138
|
+
return SearchIcon;
|
|
139
|
+
case "tool-result":
|
|
140
|
+
return AgentRunningIcon;
|
|
141
|
+
case "message":
|
|
142
|
+
default:
|
|
143
|
+
return MessageIcon;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function typeLabel(type: AgentContextSource["type"]) {
|
|
148
|
+
return type
|
|
149
|
+
.split("-")
|
|
150
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
151
|
+
.join(" ");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatRelevance(relevance: number) {
|
|
155
|
+
return `${normalizedRelevance(relevance)}%`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizedRelevance(relevance: number) {
|
|
159
|
+
const normalized = relevance <= 1 ? relevance * 100 : relevance;
|
|
160
|
+
return Math.min(100, Math.max(0, Math.round(normalized)));
|
|
161
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Drawer, EmptyState, LinkButton, Surface } from "@echothink-ui/core";
|
|
4
|
+
import type { SurfaceComponentProps } from "@echothink-ui/core";
|
|
5
|
+
import type { AgentGeneratedArtifact } from "../types";
|
|
6
|
+
import { formatDateTime } from "./utils";
|
|
7
|
+
|
|
8
|
+
export interface AgentGeneratedArtifactPanelProps extends Omit<
|
|
9
|
+
SurfaceComponentProps,
|
|
10
|
+
"children" | "onSelect"
|
|
11
|
+
> {
|
|
12
|
+
artifacts: AgentGeneratedArtifact[];
|
|
13
|
+
onSelect?: (artifact: AgentGeneratedArtifact) => void;
|
|
14
|
+
selectedId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function AgentGeneratedArtifactPanel({
|
|
18
|
+
artifacts,
|
|
19
|
+
onSelect,
|
|
20
|
+
selectedId,
|
|
21
|
+
density,
|
|
22
|
+
title = "Generated artifacts",
|
|
23
|
+
className,
|
|
24
|
+
...props
|
|
25
|
+
}: AgentGeneratedArtifactPanelProps) {
|
|
26
|
+
const [internalSelectedId, setInternalSelectedId] = React.useState<string | undefined>(
|
|
27
|
+
selectedId ?? artifacts[0]?.id
|
|
28
|
+
);
|
|
29
|
+
const [previewOpen, setPreviewOpen] = React.useState(false);
|
|
30
|
+
const selectedArtifact =
|
|
31
|
+
artifacts.find((artifact) => artifact.id === (selectedId ?? internalSelectedId)) ??
|
|
32
|
+
artifacts[0];
|
|
33
|
+
const compact = density === "compact";
|
|
34
|
+
|
|
35
|
+
const selectArtifact = (artifact: AgentGeneratedArtifact) => {
|
|
36
|
+
setInternalSelectedId(artifact.id);
|
|
37
|
+
setPreviewOpen(true);
|
|
38
|
+
onSelect?.(artifact);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<Surface
|
|
44
|
+
{...props}
|
|
45
|
+
density={density}
|
|
46
|
+
className={clsx("eth-agent-generated-artifact-panel", className)}
|
|
47
|
+
data-eth-component="AgentGeneratedArtifactPanel"
|
|
48
|
+
title={title}
|
|
49
|
+
>
|
|
50
|
+
{!artifacts.length ? (
|
|
51
|
+
<EmptyState title="No artifacts generated" />
|
|
52
|
+
) : (
|
|
53
|
+
<div
|
|
54
|
+
className="eth-agent-generated-artifact-panel__layout"
|
|
55
|
+
style={{
|
|
56
|
+
gridTemplateColumns: compact ? "1fr" : "minmax(220px, 320px) minmax(0, 1fr)"
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<ArtifactList
|
|
60
|
+
artifacts={artifacts}
|
|
61
|
+
selectedId={selectedArtifact?.id}
|
|
62
|
+
onSelect={selectArtifact}
|
|
63
|
+
/>
|
|
64
|
+
{!compact ? <ArtifactPreview artifact={selectedArtifact} /> : null}
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</Surface>
|
|
68
|
+
{compact ? (
|
|
69
|
+
<Drawer
|
|
70
|
+
open={previewOpen && Boolean(selectedArtifact)}
|
|
71
|
+
title={selectedArtifact?.label ?? "Artifact preview"}
|
|
72
|
+
onClose={() => setPreviewOpen(false)}
|
|
73
|
+
>
|
|
74
|
+
<ArtifactPreview artifact={selectedArtifact} />
|
|
75
|
+
</Drawer>
|
|
76
|
+
) : null}
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ArtifactList({
|
|
82
|
+
artifacts,
|
|
83
|
+
selectedId,
|
|
84
|
+
onSelect
|
|
85
|
+
}: {
|
|
86
|
+
artifacts: AgentGeneratedArtifact[];
|
|
87
|
+
selectedId?: string;
|
|
88
|
+
onSelect: (artifact: AgentGeneratedArtifact) => void;
|
|
89
|
+
}) {
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
className="eth-agent-generated-artifact-panel__list"
|
|
93
|
+
role="group"
|
|
94
|
+
aria-label="Generated artifacts"
|
|
95
|
+
>
|
|
96
|
+
{artifacts.map((artifact) => {
|
|
97
|
+
const selected = selectedId === artifact.id;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<button
|
|
101
|
+
key={artifact.id}
|
|
102
|
+
type="button"
|
|
103
|
+
aria-pressed={selected}
|
|
104
|
+
className={clsx(
|
|
105
|
+
"eth-agent-generated-artifact-panel__item",
|
|
106
|
+
selected && "eth-agent-generated-artifact-panel__item--selected"
|
|
107
|
+
)}
|
|
108
|
+
onClick={() => onSelect(artifact)}
|
|
109
|
+
>
|
|
110
|
+
<span className="eth-agent-generated-artifact-panel__item-topline">
|
|
111
|
+
<span className="eth-agent-generated-artifact-panel__item-title">
|
|
112
|
+
{artifact.label}
|
|
113
|
+
</span>
|
|
114
|
+
<span className="eth-agent-generated-artifact-panel__kind">
|
|
115
|
+
{artifactKindLabel(artifact.kind)}
|
|
116
|
+
</span>
|
|
117
|
+
</span>
|
|
118
|
+
<span className="eth-agent-generated-artifact-panel__item-meta">
|
|
119
|
+
{artifactMeta(artifact).join(" · ")}
|
|
120
|
+
</span>
|
|
121
|
+
<span className="eth-agent-generated-artifact-panel__item-status">
|
|
122
|
+
{artifactPreviewStatus(artifact)}
|
|
123
|
+
</span>
|
|
124
|
+
</button>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ArtifactPreview({ artifact }: { artifact?: AgentGeneratedArtifact }) {
|
|
132
|
+
const headingId = React.useId();
|
|
133
|
+
if (!artifact) return <EmptyState title="Select an artifact" />;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<section className="eth-agent-generated-artifact-panel__preview" aria-labelledby={headingId}>
|
|
137
|
+
<header className="eth-agent-generated-artifact-panel__preview-header">
|
|
138
|
+
<div className="eth-agent-generated-artifact-panel__preview-heading">
|
|
139
|
+
<h3 id={headingId}>{artifact.label}</h3>
|
|
140
|
+
<div className="eth-agent-generated-artifact-panel__preview-meta">
|
|
141
|
+
{artifactMeta(artifact).map((item) => (
|
|
142
|
+
<span key={item}>{item}</span>
|
|
143
|
+
))}
|
|
144
|
+
{artifact.mimeType ? <span>{artifact.mimeType}</span> : null}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
{artifact.href ? (
|
|
148
|
+
<LinkButton href={artifact.href} density="compact" intent="secondary">
|
|
149
|
+
Open
|
|
150
|
+
</LinkButton>
|
|
151
|
+
) : null}
|
|
152
|
+
</header>
|
|
153
|
+
|
|
154
|
+
<div className="eth-agent-generated-artifact-panel__preview-body">
|
|
155
|
+
<ArtifactPreviewBody artifact={artifact} />
|
|
156
|
+
</div>
|
|
157
|
+
</section>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function ArtifactPreviewBody({ artifact }: { artifact: AgentGeneratedArtifact }) {
|
|
162
|
+
const content = artifact.content?.trim();
|
|
163
|
+
|
|
164
|
+
if (artifact.kind === "image" && artifact.previewUrl) {
|
|
165
|
+
return <img alt={artifact.label} src={artifact.previewUrl} />;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (artifact.kind === "code" && content) {
|
|
169
|
+
return (
|
|
170
|
+
<pre>
|
|
171
|
+
<code>{content}</code>
|
|
172
|
+
</pre>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (artifact.previewUrl) {
|
|
177
|
+
return <iframe title={artifact.label} src={artifact.previewUrl} />;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (content) {
|
|
181
|
+
return <div className="eth-agent-generated-artifact-panel__text-preview">{content}</div>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div className="eth-agent-generated-artifact-panel__empty-preview" role="status">
|
|
186
|
+
<p className="eth-agent-generated-artifact-panel__empty-title">Preview unavailable</p>
|
|
187
|
+
<p className="eth-agent-generated-artifact-panel__empty-description">
|
|
188
|
+
{artifact.href
|
|
189
|
+
? "Open the artifact to inspect the generated output."
|
|
190
|
+
: "Artifact saved without an inline preview."}
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function artifactMeta(artifact: AgentGeneratedArtifact) {
|
|
197
|
+
return [artifact.size, formatDateTime(artifact.createdAt)].filter((item): item is string =>
|
|
198
|
+
Boolean(item)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function artifactKindLabel(kind: AgentGeneratedArtifact["kind"]) {
|
|
203
|
+
switch (kind) {
|
|
204
|
+
case "chart":
|
|
205
|
+
return "Chart";
|
|
206
|
+
case "code":
|
|
207
|
+
return "Code";
|
|
208
|
+
case "doc":
|
|
209
|
+
return "Doc";
|
|
210
|
+
case "file":
|
|
211
|
+
return "File";
|
|
212
|
+
case "image":
|
|
213
|
+
return "Image";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function artifactPreviewStatus(artifact: AgentGeneratedArtifact) {
|
|
218
|
+
if (artifact.kind === "image" && artifact.previewUrl) return "Inline image preview";
|
|
219
|
+
if (artifact.kind === "code" && artifact.content?.trim()) return "Inline code preview";
|
|
220
|
+
if (artifact.previewUrl) return "Embedded preview";
|
|
221
|
+
if (artifact.content?.trim()) return "Inline text preview";
|
|
222
|
+
if (artifact.href) return "Open externally";
|
|
223
|
+
return "No inline preview";
|
|
224
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import {
|
|
4
|
+
Badge,
|
|
5
|
+
Button,
|
|
6
|
+
EmptyState,
|
|
7
|
+
FormField,
|
|
8
|
+
StatusDot,
|
|
9
|
+
Surface,
|
|
10
|
+
Textarea
|
|
11
|
+
} from "@echothink-ui/core";
|
|
12
|
+
import type { SurfaceComponentProps } from "@echothink-ui/core";
|
|
13
|
+
import type { AgentHandoffTarget } from "../types";
|
|
14
|
+
|
|
15
|
+
export interface AgentHandoffPanelProps
|
|
16
|
+
extends Omit<SurfaceComponentProps, "children" | "onChange" | "onSubmit"> {
|
|
17
|
+
itemRef?: string;
|
|
18
|
+
agentRef?: string;
|
|
19
|
+
assigneeOptions?: AgentHandoffTarget[];
|
|
20
|
+
selectedAssigneeId?: string;
|
|
21
|
+
reason?: string;
|
|
22
|
+
reasonSchema?: { label?: string; placeholder?: string; required?: boolean };
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
onAssigneeChange?: (id: string) => void;
|
|
25
|
+
onReasonChange?: (reason: string) => void;
|
|
26
|
+
onSubmit?: (handoff: { assigneeId?: string; reason: string }) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function AgentHandoffPanel({
|
|
30
|
+
itemRef,
|
|
31
|
+
agentRef,
|
|
32
|
+
assigneeOptions = [],
|
|
33
|
+
selectedAssigneeId,
|
|
34
|
+
reason,
|
|
35
|
+
reasonSchema,
|
|
36
|
+
disabled,
|
|
37
|
+
onAssigneeChange,
|
|
38
|
+
onReasonChange,
|
|
39
|
+
onSubmit,
|
|
40
|
+
title = "Agent handoff",
|
|
41
|
+
className,
|
|
42
|
+
...props
|
|
43
|
+
}: AgentHandoffPanelProps) {
|
|
44
|
+
const [internalAssigneeId, setInternalAssigneeId] = React.useState(selectedAssigneeId);
|
|
45
|
+
const [internalReason, setInternalReason] = React.useState(reason ?? "");
|
|
46
|
+
const assigneeId = selectedAssigneeId ?? internalAssigneeId;
|
|
47
|
+
const handoffReason = reason ?? internalReason;
|
|
48
|
+
const reasonRequired = reasonSchema?.required ?? true;
|
|
49
|
+
const canSubmit = Boolean(assigneeId) && (!reasonRequired || Boolean(handoffReason.trim()));
|
|
50
|
+
|
|
51
|
+
const selectAssignee = (id: string) => {
|
|
52
|
+
setInternalAssigneeId(id);
|
|
53
|
+
onAssigneeChange?.(id);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const changeReason = (value: string) => {
|
|
57
|
+
setInternalReason(value);
|
|
58
|
+
onReasonChange?.(value);
|
|
59
|
+
};
|
|
60
|
+
const reasonId = React.useId();
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Surface
|
|
64
|
+
{...props}
|
|
65
|
+
className={clsx("eth-agent-handoff-panel", className)}
|
|
66
|
+
data-eth-component="AgentHandoffPanel"
|
|
67
|
+
title={title}
|
|
68
|
+
subtitle={[agentRef, itemRef].filter(Boolean).join(" · ") || undefined}
|
|
69
|
+
>
|
|
70
|
+
<div className="eth-agent-handoff-panel__body">
|
|
71
|
+
{!assigneeOptions.length ? (
|
|
72
|
+
<EmptyState title="No handoff targets" />
|
|
73
|
+
) : (
|
|
74
|
+
<div
|
|
75
|
+
className="eth-agent-handoff-panel__target-list"
|
|
76
|
+
role="listbox"
|
|
77
|
+
aria-label="Handoff targets"
|
|
78
|
+
>
|
|
79
|
+
{assigneeOptions.map((target) => (
|
|
80
|
+
<button
|
|
81
|
+
key={target.id}
|
|
82
|
+
type="button"
|
|
83
|
+
role="option"
|
|
84
|
+
aria-selected={assigneeId === target.id}
|
|
85
|
+
className={clsx(
|
|
86
|
+
"eth-agent-handoff-panel__target",
|
|
87
|
+
assigneeId === target.id && "eth-agent-handoff-panel__target--selected"
|
|
88
|
+
)}
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
onClick={() => selectAssignee(target.id)}
|
|
91
|
+
>
|
|
92
|
+
<span className="eth-agent-handoff-panel__target-main">
|
|
93
|
+
<span className="eth-agent-handoff-panel__target-copy">
|
|
94
|
+
<strong className="eth-agent-handoff-panel__target-label">
|
|
95
|
+
{target.label}
|
|
96
|
+
</strong>
|
|
97
|
+
{target.description ? (
|
|
98
|
+
<span className="eth-agent-handoff-panel__target-description">
|
|
99
|
+
{target.description}
|
|
100
|
+
</span>
|
|
101
|
+
) : null}
|
|
102
|
+
</span>
|
|
103
|
+
<span className="eth-agent-handoff-panel__target-meta">
|
|
104
|
+
<Badge severity="neutral">{target.type}</Badge>
|
|
105
|
+
{target.status ? (
|
|
106
|
+
<StatusDot
|
|
107
|
+
status={target.status}
|
|
108
|
+
label={formatHandoffStatus(target.status)}
|
|
109
|
+
/>
|
|
110
|
+
) : null}
|
|
111
|
+
</span>
|
|
112
|
+
</span>
|
|
113
|
+
</button>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
<FormField
|
|
119
|
+
id={reasonId}
|
|
120
|
+
label={reasonSchema?.label ?? "Handoff reason"}
|
|
121
|
+
required={reasonRequired}
|
|
122
|
+
>
|
|
123
|
+
<Textarea
|
|
124
|
+
className="eth-agent-handoff-panel__reason"
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
placeholder={reasonSchema?.placeholder ?? "Explain what the next owner should do."}
|
|
127
|
+
required={reasonRequired}
|
|
128
|
+
rows={3}
|
|
129
|
+
value={handoffReason}
|
|
130
|
+
onChange={(event) => changeReason(event.currentTarget.value)}
|
|
131
|
+
/>
|
|
132
|
+
</FormField>
|
|
133
|
+
|
|
134
|
+
<div className="eth-agent-handoff-panel__actions">
|
|
135
|
+
<Button
|
|
136
|
+
disabled={disabled || !canSubmit}
|
|
137
|
+
onClick={() => onSubmit?.({ assigneeId, reason: handoffReason })}
|
|
138
|
+
>
|
|
139
|
+
Handoff
|
|
140
|
+
</Button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</Surface>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatHandoffStatus(status: AgentHandoffTarget["status"]) {
|
|
148
|
+
return status
|
|
149
|
+
? status
|
|
150
|
+
.split("-")
|
|
151
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
152
|
+
.join(" ")
|
|
153
|
+
: undefined;
|
|
154
|
+
}
|