@actioneer/ads 1.0.7 → 1.0.9
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/DESIGN.md +119 -91
- package/README.md +19 -1
- package/components/ai-elements/chart.js +20 -20
- package/components/ai-elements/inline-citation.js +50 -50
- package/components/ai-elements/message.js +1 -1
- package/components/ai-elements/tool.js +5 -5
- package/components/alert.d.ts +3 -2
- package/components/alert.js +25 -25
- package/components/answer.js +1 -1
- package/components/assistant-ui/action-bar.js +1 -1
- package/components/assistant-ui/attachment.js +68 -49
- package/components/assistant-ui/composer-add-context.js +1 -1
- package/components/assistant-ui/composer-harness.js +5 -4
- package/components/assistant-ui/composer-trigger-popover.js +1 -1
- package/components/assistant-ui/composer.js +13 -13
- package/components/assistant-ui/message-timing.js +6 -6
- package/components/assistant-ui/selection-toolbar.js +5 -5
- package/components/assistant-ui/sources.js +15 -15
- package/components/assistant-ui/tooltip-icon-button.js +18 -14
- package/components/audio-waveform.d.ts +21 -8
- package/components/audio-waveform.js +371 -94
- package/components/autocomplete.js +9 -9
- package/components/breadcrumbs.js +36 -42
- package/components/button.d.ts +16 -1
- package/components/button.js +86 -73
- package/components/card.js +1 -1
- package/components/chart.d.ts +31 -4
- package/components/chart.js +900 -666
- package/components/chip.d.ts +2 -1
- package/components/chip.js +56 -51
- package/components/command-palette.js +12 -12
- package/components/connector-card.d.ts +1 -2
- package/components/connector-card.js +17 -17
- package/components/date-range-picker.js +1 -2
- package/components/drawer.js +9 -10
- package/components/dropdown.js +1 -1
- package/components/empty-state.d.ts +2 -0
- package/components/empty-state.js +17 -15
- package/components/field.js +9 -10
- package/components/file-dropzone.d.ts +6 -3
- package/components/file-dropzone.js +72 -60
- package/components/filters.d.ts +37 -0
- package/components/filters.js +129 -0
- package/components/index.d.ts +1 -0
- package/components/index.js +447 -444
- package/components/infinite-canvas.js +9 -9
- package/components/modal.d.ts +2 -1
- package/components/modal.js +102 -91
- package/components/page-body.js +8 -8
- package/components/popover.d.ts +6 -1
- package/components/popover.js +75 -65
- package/components/progress-circle.d.ts +1 -1
- package/components/progress-circle.js +57 -48
- package/components/segmented.d.ts +1 -0
- package/components/segmented.js +34 -36
- package/components/select.js +2 -2
- package/components/selection-action-bar.js +32 -31
- package/components/setting-row.js +10 -10
- package/components/sidebar.d.ts +15 -0
- package/components/sidebar.js +186 -149
- package/components/slider.js +5 -5
- package/components/stepper.d.ts +1 -1
- package/components/stepper.js +1 -1
- package/components/switch.js +10 -10
- package/components/table.d.ts +10 -2
- package/components/table.js +380 -366
- package/components/tabs.d.ts +6 -1
- package/components/tabs.js +32 -29
- package/components/timeline.js +3 -3
- package/components/toast.js +53 -56
- package/components/ui/button.js +4 -4
- package/components/ui/dialog.js +1 -1
- package/components/viz-charts.js +263 -221
- package/demo/components/add-to-artifact.js +3 -2
- package/examples/README.md +31 -0
- package/examples/agents-screen.tsx +477 -0
- package/examples/campaign-analysis-screen.tsx +138 -0
- package/examples/campaigns-screen.tsx +439 -0
- package/examples/settings-screen.tsx +90 -0
- package/examples/workspace-header.tsx +71 -0
- package/lib/icons.d.ts +8 -0
- package/lib/icons.js +171 -118
- package/llms.txt +93 -29
- package/manifest.json +1660 -113
- package/package.json +1 -1
- package/styles.css +55 -68
- package/theme.css +50 -59
|
@@ -35,6 +35,7 @@ import "react-aria-components";
|
|
|
35
35
|
import "../../components/autocomplete.js";
|
|
36
36
|
import "../../components/slider.js";
|
|
37
37
|
import "../../components/segmented.js";
|
|
38
|
+
import "../../components/filters.js";
|
|
38
39
|
import { Modal as q, ModalHeader as J, ModalTitle as Q, ModalBody as x, ModalFooter as g } from "../../components/modal.js";
|
|
39
40
|
import "../../components/drawer.js";
|
|
40
41
|
import "../../components/popover.js";
|
|
@@ -106,7 +107,7 @@ import "../../components/assistant-ui/quote.js";
|
|
|
106
107
|
import "../../components/assistant-ui/sources.js";
|
|
107
108
|
import "../../components/assistant-ui/tool-fallback.js";
|
|
108
109
|
import "../../components/assistant-ui/tool-group.js";
|
|
109
|
-
function
|
|
110
|
+
function ii({
|
|
110
111
|
block: i,
|
|
111
112
|
trigger: v = "button",
|
|
112
113
|
className: A,
|
|
@@ -358,5 +359,5 @@ function ei({
|
|
|
358
359
|
] });
|
|
359
360
|
}
|
|
360
361
|
export {
|
|
361
|
-
|
|
362
|
+
ii as AddToArtifact
|
|
362
363
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ADS sample screens
|
|
2
|
+
|
|
3
|
+
Full screens built only from `@actioneer/ads` exports. Open them to see how the
|
|
4
|
+
components are meant to be composed, or copy one into your app as a starting
|
|
5
|
+
point and swap the sample data for your own.
|
|
6
|
+
|
|
7
|
+
Before rendering any of them, finish the package setup in the main README:
|
|
8
|
+
import `@actioneer/ads/styles.css` right after `tailwindcss`, and wrap your app
|
|
9
|
+
root in `MotionConfig`, `ToastProvider` and `TooltipProvider`.
|
|
10
|
+
|
|
11
|
+
| File | Export | What it shows |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `agents-screen.tsx` | `AgentsScreen` | A full list page: `PageHeader` with a primary action, a summary strip, `Tabs` with counts leading a `SearchDisclosure`, a filter `Dropdown`, a sortable `Table` with clickable rows and a pinned action column, per-row actions with `Tooltip`s, and filtered `EmptyState`s. |
|
|
14
|
+
| `settings-screen.tsx` | `SettingsScreen` | A settings card: `Card` header with a status `Badge`, an `InputField`, a `SettingList` of `SettingRow`s with `Switch` and `Button` controls, and one primary action. |
|
|
15
|
+
| `workspace-header.tsx` | `WorkspaceHeaderScreen` | A page header with a status `Badge` and trailing actions, then `Tabs` with counts whose panels hold `Card`s. |
|
|
16
|
+
| `campaigns-screen.tsx` | `CampaignsScreen` | The collection-screen recipe: `BodyPageHeader` with one primary action, counted `Tabs` leading `SearchDisclosure` + `Filters`, a multi-select `Table` (hover-revealed checkboxes, cohort child rows, `isActive` open row) whose page size fills the panel with `Pagination` at the bottom, a shell-level `Drawer` with status under the title and Performance / Details sections, and the fixed-height New campaign `Modal`. |
|
|
17
|
+
| `campaign-analysis-screen.tsx` | `CampaignAnalysisScreen` | The record-analytics recipe: a 16px `Breadcrumbs` title with the primary action on the same row, record `Tabs`, outlined stat cards with `Progress`, and outlined `BarChart` / `LineChart` cards with header legends and printed values. |
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// app/agents/page.tsx
|
|
21
|
+
"use client";
|
|
22
|
+
|
|
23
|
+
import { AgentsScreen } from "./agents-screen"; // copied from node_modules/@actioneer/ads/examples
|
|
24
|
+
|
|
25
|
+
export default function Page() {
|
|
26
|
+
return <AgentsScreen />;
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Icons come from
|
|
31
|
+
`@actioneer/ads/lib/icons`; never install a second icon package.
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents screen — a full list page built only from ADS exports.
|
|
3
|
+
*
|
|
4
|
+
* What it shows: a PageHeader with a primary action, a summary strip, Tabs
|
|
5
|
+
* with counts leading a trailing SearchDisclosure, a filter Dropdown, a
|
|
6
|
+
* sortable Table whose rows open a record and keep a pinned action column,
|
|
7
|
+
* per-row Dropdown actions with Tooltips, and filtered EmptyStates.
|
|
8
|
+
*
|
|
9
|
+
* Copy it into your app and replace the sample AGENTS data with your own.
|
|
10
|
+
*/
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import {
|
|
13
|
+
Avatar,
|
|
14
|
+
Button,
|
|
15
|
+
Dropdown,
|
|
16
|
+
DropdownContent,
|
|
17
|
+
DropdownItem,
|
|
18
|
+
DropdownTrigger,
|
|
19
|
+
EmptyState,
|
|
20
|
+
PageHeader,
|
|
21
|
+
SearchDisclosure,
|
|
22
|
+
Tab,
|
|
23
|
+
TabList,
|
|
24
|
+
Table,
|
|
25
|
+
TableBody,
|
|
26
|
+
TableCell,
|
|
27
|
+
TableColumn,
|
|
28
|
+
TableHeader,
|
|
29
|
+
TableRow,
|
|
30
|
+
Tabs,
|
|
31
|
+
Tooltip,
|
|
32
|
+
type SortDescriptor,
|
|
33
|
+
} from "@actioneer/ads";
|
|
34
|
+
import {
|
|
35
|
+
Close,
|
|
36
|
+
Copy,
|
|
37
|
+
Eye,
|
|
38
|
+
Filter,
|
|
39
|
+
MoreHorizontal,
|
|
40
|
+
PhoneIncoming,
|
|
41
|
+
Search,
|
|
42
|
+
Trash,
|
|
43
|
+
} from "@actioneer/ads/lib/icons";
|
|
44
|
+
|
|
45
|
+
export type AgentStatus = "deployed" | "not-deployed";
|
|
46
|
+
export type AgentUsage = "all" | "deployed" | "not-deployed" | "hotlines" | "campaigns";
|
|
47
|
+
export type ManualFilter = "original" | "duplicated" | "has-calls" | "no-calls";
|
|
48
|
+
|
|
49
|
+
export interface AgentRecord {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
type: string;
|
|
53
|
+
updated: string;
|
|
54
|
+
updatedSort: number;
|
|
55
|
+
status: AgentStatus;
|
|
56
|
+
lineage?: string;
|
|
57
|
+
hotlines: number;
|
|
58
|
+
campaigns: number;
|
|
59
|
+
steps: number;
|
|
60
|
+
calls: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const AGENTS: AgentRecord[] = [
|
|
64
|
+
{ id: "simran", name: "Simran", type: "Voice Agent", updated: "Aug 25, 2026", updatedSort: 20260825, status: "deployed", hotlines: 0, campaigns: 1, steps: 0, calls: 0 },
|
|
65
|
+
{ id: "vivaan", name: "Vivaan", type: "Voice Agent", updated: "Aug 26, 2026", updatedSort: 20260826, status: "not-deployed", lineage: "Simran", hotlines: 0, campaigns: 0, steps: 0, calls: 0 },
|
|
66
|
+
{ id: "manish", name: "Manish", type: "Voice Agent", updated: "Aug 26, 2026", updatedSort: 20260826, status: "not-deployed", lineage: "Esha", hotlines: 0, campaigns: 0, steps: 0, calls: 0 },
|
|
67
|
+
{ id: "esha", name: "Esha", type: "Voice Agent", updated: "Aug 26, 2026", updatedSort: 20260826, status: "not-deployed", lineage: "Simran", hotlines: 0, campaigns: 0, steps: 0, calls: 0 },
|
|
68
|
+
{ id: "ananya", name: "Ananya", type: "KYC completion support", updated: "Aug 6, 2026", updatedSort: 20260806, status: "deployed", hotlines: 0, campaigns: 5, steps: 28, calls: 96 },
|
|
69
|
+
{ id: "aarohi", name: "Aarohi", type: "EMI due-date reminder", updated: "Aug 6, 2026", updatedSort: 20260806, status: "deployed", hotlines: 0, campaigns: 4, steps: 28, calls: 96 },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export const USAGE_TABS = [
|
|
73
|
+
{ value: "all", label: "All", count: 6 },
|
|
74
|
+
{ value: "deployed", label: "Deployed", count: 3 },
|
|
75
|
+
{ value: "not-deployed", label: "Not yet deployed", count: 3 },
|
|
76
|
+
{ value: "hotlines", label: "On hotlines", count: 0 },
|
|
77
|
+
{ value: "campaigns", label: "In campaigns", count: 3 },
|
|
78
|
+
] as const;
|
|
79
|
+
|
|
80
|
+
export const MANUAL_FILTERS = [
|
|
81
|
+
{ value: "original", label: "Original agents" },
|
|
82
|
+
{ value: "duplicated", label: "Duplicated agents" },
|
|
83
|
+
{ value: "has-calls", label: "Has calls" },
|
|
84
|
+
{ value: "no-calls", label: "No calls" },
|
|
85
|
+
] as const;
|
|
86
|
+
|
|
87
|
+
export function ScreenFrame({ children }: { children: React.ReactNode }) {
|
|
88
|
+
return <div className="min-h-screen bg-card p-5 text-foreground">{children}</div>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function AgentStatusAvatar({ name, status }: { name: string; status: AgentStatus }) {
|
|
92
|
+
const label = status === "deployed" ? "Deployed" : "Not yet deployed";
|
|
93
|
+
return (
|
|
94
|
+
<span className="relative inline-flex shrink-0">
|
|
95
|
+
<Avatar name={name} size="sm" />
|
|
96
|
+
<span
|
|
97
|
+
aria-hidden="true"
|
|
98
|
+
className={status === "deployed"
|
|
99
|
+
? "absolute bottom-0 right-0 size-2 rounded-full bg-success ring-1 ring-card"
|
|
100
|
+
: "absolute bottom-0 right-0 size-2 rounded-full bg-wg6 ring-1 ring-card"}
|
|
101
|
+
/>
|
|
102
|
+
<span className="sr-only">{label}</span>
|
|
103
|
+
</span>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function AgentIdentity({ agent }: { agent: AgentRecord }) {
|
|
108
|
+
return (
|
|
109
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
110
|
+
<AgentStatusAvatar name={agent.name} status={agent.status} />
|
|
111
|
+
<div className="min-w-0">
|
|
112
|
+
<div className="truncate text-m font-medium text-foreground">{agent.name}</div>
|
|
113
|
+
{agent.lineage && (
|
|
114
|
+
<div className="truncate text-s text-secondary">
|
|
115
|
+
Copy of {agent.lineage}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function AgentSummaryStripContent() {
|
|
124
|
+
const items = [
|
|
125
|
+
{ label: "Deployed", value: "3", detail: "of 6 agents" },
|
|
126
|
+
{ label: "In campaigns", value: "3", detail: "ready to engage" },
|
|
127
|
+
{ label: "Call activity", value: "192", detail: "all-time calls" },
|
|
128
|
+
];
|
|
129
|
+
return (
|
|
130
|
+
<section
|
|
131
|
+
aria-label="Agent summary"
|
|
132
|
+
className="grid min-h-20 grid-cols-1 gap-4 rounded-m border border-table-stroke px-4 py-5 sm:grid-cols-3 sm:gap-0"
|
|
133
|
+
>
|
|
134
|
+
{items.map((item, index) => (
|
|
135
|
+
<div
|
|
136
|
+
key={item.label}
|
|
137
|
+
className={index === 0 ? "min-w-0 sm:pr-6" : "min-w-0 border-table-stroke sm:border-l sm:px-6"}
|
|
138
|
+
>
|
|
139
|
+
<p className="text-s font-medium uppercase tracking-wide text-table-header-foreground">
|
|
140
|
+
{item.label}
|
|
141
|
+
</p>
|
|
142
|
+
<p className="mt-2 flex items-baseline gap-2">
|
|
143
|
+
<span className="text-xl font-medium leading-none tabular-nums text-foreground">{item.value}</span>
|
|
144
|
+
<span className="text-m text-muted-foreground">{item.detail}</span>
|
|
145
|
+
</p>
|
|
146
|
+
</div>
|
|
147
|
+
))}
|
|
148
|
+
</section>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function AgentRowActions({
|
|
153
|
+
name,
|
|
154
|
+
defaultOpen = false,
|
|
155
|
+
focusTrigger = false,
|
|
156
|
+
}: {
|
|
157
|
+
name: string;
|
|
158
|
+
defaultOpen?: boolean;
|
|
159
|
+
focusTrigger?: boolean;
|
|
160
|
+
}) {
|
|
161
|
+
return (
|
|
162
|
+
<Dropdown defaultOpen={defaultOpen}>
|
|
163
|
+
<Tooltip content={`Actions for ${name}`}>
|
|
164
|
+
<span className="inline-flex">
|
|
165
|
+
<DropdownTrigger>
|
|
166
|
+
<Button
|
|
167
|
+
autoFocus={focusTrigger}
|
|
168
|
+
variant="ghost"
|
|
169
|
+
size="icon-sm"
|
|
170
|
+
aria-label={`Actions for ${name}`}
|
|
171
|
+
className="enabled:hover:bg-card enabled:hover:text-foreground enabled:active:bg-card enabled:active:text-foreground"
|
|
172
|
+
>
|
|
173
|
+
<MoreHorizontal aria-hidden="true" />
|
|
174
|
+
</Button>
|
|
175
|
+
</DropdownTrigger>
|
|
176
|
+
</span>
|
|
177
|
+
</Tooltip>
|
|
178
|
+
<DropdownContent align="end" autoFocusFirstItem={defaultOpen}>
|
|
179
|
+
<DropdownItem startContent={<Eye aria-hidden="true" />}>See details</DropdownItem>
|
|
180
|
+
<DropdownItem startContent={<Copy aria-hidden="true" />}>Duplicate</DropdownItem>
|
|
181
|
+
<DropdownItem danger startContent={<Trash aria-hidden="true" />}>Delete</DropdownItem>
|
|
182
|
+
</DropdownContent>
|
|
183
|
+
</Dropdown>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function matchesUsage(agent: AgentRecord, usage: AgentUsage) {
|
|
188
|
+
if (usage === "deployed") return agent.status === "deployed";
|
|
189
|
+
if (usage === "not-deployed") return agent.status === "not-deployed";
|
|
190
|
+
if (usage === "hotlines") return agent.hotlines > 0;
|
|
191
|
+
if (usage === "campaigns") return agent.campaigns > 0;
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function matchesManualFilter(agent: AgentRecord, filter: ManualFilter) {
|
|
196
|
+
if (filter === "original") return !agent.lineage;
|
|
197
|
+
if (filter === "duplicated") return Boolean(agent.lineage);
|
|
198
|
+
if (filter === "has-calls") return agent.calls > 0;
|
|
199
|
+
return agent.calls === 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function sortAgents(agents: AgentRecord[], descriptor?: SortDescriptor) {
|
|
203
|
+
if (!descriptor) return agents;
|
|
204
|
+
const direction = descriptor.direction === "ascending" ? 1 : -1;
|
|
205
|
+
const value = (agent: AgentRecord): string | number => {
|
|
206
|
+
if (descriptor.column === "type") return agent.type;
|
|
207
|
+
if (descriptor.column === "updated") return agent.updatedSort;
|
|
208
|
+
if (descriptor.column === "hotlines") return agent.hotlines;
|
|
209
|
+
if (descriptor.column === "campaigns") return agent.campaigns;
|
|
210
|
+
if (descriptor.column === "steps") return agent.steps;
|
|
211
|
+
if (descriptor.column === "calls") return agent.calls;
|
|
212
|
+
return agent.name;
|
|
213
|
+
};
|
|
214
|
+
return [...agents].sort((left, right) => {
|
|
215
|
+
const a = value(left);
|
|
216
|
+
const b = value(right);
|
|
217
|
+
const result = typeof a === "number" && typeof b === "number"
|
|
218
|
+
? a - b
|
|
219
|
+
: String(a).localeCompare(String(b));
|
|
220
|
+
return result * direction;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function AgentsFilterMenu({
|
|
225
|
+
filters,
|
|
226
|
+
onAdd,
|
|
227
|
+
}: {
|
|
228
|
+
filters: ManualFilter[];
|
|
229
|
+
onAdd: (filter: ManualFilter) => void;
|
|
230
|
+
}) {
|
|
231
|
+
return (
|
|
232
|
+
<Dropdown>
|
|
233
|
+
<Tooltip content="Add filter">
|
|
234
|
+
<span className="inline-flex">
|
|
235
|
+
<DropdownTrigger>
|
|
236
|
+
<Button
|
|
237
|
+
variant="outline"
|
|
238
|
+
size="icon-sm"
|
|
239
|
+
aria-label="Add filter"
|
|
240
|
+
data-active={filters.length > 0 || undefined}
|
|
241
|
+
className="aria-expanded:text-action-primary-text data-[active=true]:text-action-primary-text"
|
|
242
|
+
>
|
|
243
|
+
<Filter aria-hidden="true" />
|
|
244
|
+
{filters.length > 0 && (
|
|
245
|
+
<span aria-hidden="true" className="absolute -right-1 -top-1 size-2 rounded-full bg-danger" />
|
|
246
|
+
)}
|
|
247
|
+
</Button>
|
|
248
|
+
</DropdownTrigger>
|
|
249
|
+
</span>
|
|
250
|
+
</Tooltip>
|
|
251
|
+
<DropdownContent align="end" autoFocusFirstItem>
|
|
252
|
+
{MANUAL_FILTERS.map((filter) => (
|
|
253
|
+
<DropdownItem
|
|
254
|
+
key={filter.value}
|
|
255
|
+
disabled={filters.includes(filter.value)}
|
|
256
|
+
onSelect={() => onAdd(filter.value)}
|
|
257
|
+
>
|
|
258
|
+
{filter.label}
|
|
259
|
+
</DropdownItem>
|
|
260
|
+
))}
|
|
261
|
+
</DropdownContent>
|
|
262
|
+
</Dropdown>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function AgentsEmptyState({
|
|
267
|
+
title,
|
|
268
|
+
description,
|
|
269
|
+
hotline = false,
|
|
270
|
+
action,
|
|
271
|
+
}: {
|
|
272
|
+
title: string;
|
|
273
|
+
description?: string;
|
|
274
|
+
hotline?: boolean;
|
|
275
|
+
action?: "view-all" | "clear";
|
|
276
|
+
}) {
|
|
277
|
+
return (
|
|
278
|
+
<div className="rounded-m border border-table-stroke bg-card">
|
|
279
|
+
<EmptyState
|
|
280
|
+
icon={hotline ? <PhoneIncoming aria-hidden="true" /> : <Search aria-hidden="true" />}
|
|
281
|
+
title={title}
|
|
282
|
+
description={description}
|
|
283
|
+
action={action === "view-all"
|
|
284
|
+
? <Button size="sm" variant="outline">View all agents</Button>
|
|
285
|
+
: action === "clear"
|
|
286
|
+
? <Button size="sm" variant="outline">Clear filters</Button>
|
|
287
|
+
: undefined}
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function AgentsComposition() {
|
|
294
|
+
const toolbarRef = React.useRef<HTMLDivElement>(null);
|
|
295
|
+
const [usage, setUsage] = React.useState<AgentUsage>("all");
|
|
296
|
+
const [search, setSearch] = React.useState("");
|
|
297
|
+
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
298
|
+
const [manualFilters, setManualFilters] = React.useState<ManualFilter[]>([]);
|
|
299
|
+
const [sortDescriptor, setSortDescriptor] = React.useState<SortDescriptor>();
|
|
300
|
+
const [toolbarIsConstrained, setToolbarIsConstrained] = React.useState(false);
|
|
301
|
+
|
|
302
|
+
React.useEffect(() => {
|
|
303
|
+
const toolbar = toolbarRef.current;
|
|
304
|
+
if (!toolbar) return;
|
|
305
|
+
|
|
306
|
+
const update = () => setToolbarIsConstrained(toolbar.clientWidth < 820);
|
|
307
|
+
update();
|
|
308
|
+
|
|
309
|
+
const observer = new ResizeObserver(update);
|
|
310
|
+
observer.observe(toolbar);
|
|
311
|
+
return () => observer.disconnect();
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
314
|
+
const filtered = AGENTS.filter((agent) => {
|
|
315
|
+
const query = search.trim().toLowerCase();
|
|
316
|
+
const matchesQuery = !query || `${agent.name} ${agent.type} ${agent.lineage ?? ""}`.toLowerCase().includes(query);
|
|
317
|
+
return matchesQuery && matchesUsage(agent, usage) && manualFilters.every((filter) => matchesManualFilter(agent, filter));
|
|
318
|
+
});
|
|
319
|
+
const rows = sortAgents(filtered, sortDescriptor);
|
|
320
|
+
const hasManualQuery = Boolean(search.trim() || manualFilters.length > 0);
|
|
321
|
+
const compactTabs = searchOpen || toolbarIsConstrained;
|
|
322
|
+
|
|
323
|
+
const clearQuery = () => {
|
|
324
|
+
setSearch("");
|
|
325
|
+
setManualFilters([]);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
let emptyTitle = "No agents match";
|
|
329
|
+
let emptyDescription: string | undefined = hasManualQuery ? "No agents match the applied search or manual filters." : undefined;
|
|
330
|
+
let emptyHotline = false;
|
|
331
|
+
if (!hasManualQuery && usage === "hotlines") {
|
|
332
|
+
emptyTitle = "No agents on hotlines";
|
|
333
|
+
emptyDescription = undefined;
|
|
334
|
+
emptyHotline = true;
|
|
335
|
+
} else if (!hasManualQuery && usage === "deployed") {
|
|
336
|
+
emptyTitle = "No deployed agents";
|
|
337
|
+
} else if (!hasManualQuery && usage === "not-deployed") {
|
|
338
|
+
emptyTitle = "All agents are deployed";
|
|
339
|
+
} else if (!hasManualQuery && usage === "campaigns") {
|
|
340
|
+
emptyTitle = "No agents in campaigns";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<ScreenFrame>
|
|
345
|
+
<PageHeader
|
|
346
|
+
title="Agents"
|
|
347
|
+
actions={<Button size="sm">New agent</Button>}
|
|
348
|
+
/>
|
|
349
|
+
|
|
350
|
+
<div className="mt-10">
|
|
351
|
+
<AgentSummaryStripContent />
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<div className="mt-10">
|
|
355
|
+
<div ref={toolbarRef} className="flex w-full items-center gap-2">
|
|
356
|
+
<Tabs
|
|
357
|
+
className="min-w-0 flex-1 gap-0"
|
|
358
|
+
value={usage}
|
|
359
|
+
onValueChange={(value) => setUsage(value as AgentUsage)}
|
|
360
|
+
>
|
|
361
|
+
<TabList aria-label="Filter agents by usage" className="min-w-max">
|
|
362
|
+
{USAGE_TABS.map((tab, index) =>
|
|
363
|
+
compactTabs && index > 2 ? null : (
|
|
364
|
+
<Tab key={tab.value} value={tab.value} count={tab.count}>
|
|
365
|
+
{tab.label}
|
|
366
|
+
</Tab>
|
|
367
|
+
),
|
|
368
|
+
)}
|
|
369
|
+
{compactTabs && (
|
|
370
|
+
<button
|
|
371
|
+
type="button"
|
|
372
|
+
aria-label="Show 2 more usage filters"
|
|
373
|
+
onClick={() => setSearchOpen(false)}
|
|
374
|
+
className="relative -mb-px inline-flex h-8 items-center justify-center whitespace-nowrap text-l font-normal text-secondary outline-none transition-colors hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring"
|
|
375
|
+
>
|
|
376
|
+
+2 more
|
|
377
|
+
</button>
|
|
378
|
+
)}
|
|
379
|
+
</TabList>
|
|
380
|
+
</Tabs>
|
|
381
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
382
|
+
<SearchDisclosure
|
|
383
|
+
value={search}
|
|
384
|
+
onValueChange={setSearch}
|
|
385
|
+
open={searchOpen}
|
|
386
|
+
onOpenChange={setSearchOpen}
|
|
387
|
+
placeholder="Search agents"
|
|
388
|
+
inputLabel="Search agents by name or type"
|
|
389
|
+
triggerLabel="Search agents"
|
|
390
|
+
/>
|
|
391
|
+
<AgentsFilterMenu
|
|
392
|
+
filters={manualFilters}
|
|
393
|
+
onAdd={(filter) => setManualFilters((current) => current.includes(filter) ? current : [...current, filter])}
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
{manualFilters.length > 0 && (
|
|
399
|
+
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
400
|
+
{manualFilters.map((filter) => {
|
|
401
|
+
const label = MANUAL_FILTERS.find((item) => item.value === filter)?.label ?? filter;
|
|
402
|
+
return (
|
|
403
|
+
<Button
|
|
404
|
+
key={filter}
|
|
405
|
+
variant="ghost"
|
|
406
|
+
size="xs"
|
|
407
|
+
aria-label={`Remove filter ${label}`}
|
|
408
|
+
onClick={() => setManualFilters((current) => current.filter((item) => item !== filter))}
|
|
409
|
+
endContent={<Close aria-hidden="true" />}
|
|
410
|
+
className="bg-action-primary-soft text-s text-action-primary-text [&>span]:gap-2"
|
|
411
|
+
>
|
|
412
|
+
{label}
|
|
413
|
+
</Button>
|
|
414
|
+
);
|
|
415
|
+
})}
|
|
416
|
+
<Button
|
|
417
|
+
variant="ghost"
|
|
418
|
+
size="xs"
|
|
419
|
+
onClick={() => setManualFilters([])}
|
|
420
|
+
className="text-s enabled:hover:bg-muted enabled:hover:text-foreground enabled:active:bg-muted enabled:active:text-foreground"
|
|
421
|
+
>
|
|
422
|
+
Clear
|
|
423
|
+
</Button>
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
426
|
+
|
|
427
|
+
<section aria-label="Agents" className="mt-6">
|
|
428
|
+
{rows.length === 0 ? (
|
|
429
|
+
<EmptyState
|
|
430
|
+
icon={emptyHotline ? <PhoneIncoming aria-hidden="true" /> : <Search aria-hidden="true" />}
|
|
431
|
+
title={emptyTitle}
|
|
432
|
+
description={emptyDescription}
|
|
433
|
+
action={hasManualQuery
|
|
434
|
+
? <Button size="sm" variant="outline" onClick={clearQuery}>Clear filters</Button>
|
|
435
|
+
: <Button size="sm" variant="outline" onClick={() => setUsage("all")}>View all agents</Button>}
|
|
436
|
+
/>
|
|
437
|
+
) : (
|
|
438
|
+
<Table
|
|
439
|
+
aria-label="Your agents"
|
|
440
|
+
sortDescriptor={sortDescriptor}
|
|
441
|
+
onSortChange={setSortDescriptor}
|
|
442
|
+
>
|
|
443
|
+
<TableHeader>
|
|
444
|
+
<TableRow>
|
|
445
|
+
<TableColumn sortKey="agent">Agent</TableColumn>
|
|
446
|
+
<TableColumn sortKey="type">Type</TableColumn>
|
|
447
|
+
<TableColumn sortKey="updated" align="right">Updated</TableColumn>
|
|
448
|
+
<TableColumn sortKey="hotlines" align="right">Hotlines</TableColumn>
|
|
449
|
+
<TableColumn sortKey="campaigns" align="right">Campaigns</TableColumn>
|
|
450
|
+
<TableColumn sortKey="steps" align="right">Steps</TableColumn>
|
|
451
|
+
<TableColumn sortKey="calls" align="right">Calls</TableColumn>
|
|
452
|
+
<TableColumn allowsSorting={false} align="right" aria-label="Actions" />
|
|
453
|
+
</TableRow>
|
|
454
|
+
</TableHeader>
|
|
455
|
+
<TableBody>
|
|
456
|
+
{rows.map((agent) => (
|
|
457
|
+
<TableRow key={agent.id}>
|
|
458
|
+
<TableCell><AgentIdentity agent={agent} /></TableCell>
|
|
459
|
+
<TableCell className="text-secondary">{agent.type}</TableCell>
|
|
460
|
+
<TableCell align="right" className="tabular-nums text-secondary" sortValue={agent.updatedSort}>{agent.updated}</TableCell>
|
|
461
|
+
<TableCell align="right" className="tabular-nums">{agent.hotlines}</TableCell>
|
|
462
|
+
<TableCell align="right" className="tabular-nums">{agent.campaigns}</TableCell>
|
|
463
|
+
<TableCell align="right" className="tabular-nums">{agent.steps}</TableCell>
|
|
464
|
+
<TableCell align="right" className="tabular-nums">{agent.calls}</TableCell>
|
|
465
|
+
<TableCell align="right"><AgentRowActions name={agent.name} /></TableCell>
|
|
466
|
+
</TableRow>
|
|
467
|
+
))}
|
|
468
|
+
</TableBody>
|
|
469
|
+
</Table>
|
|
470
|
+
)}
|
|
471
|
+
</section>
|
|
472
|
+
</div>
|
|
473
|
+
</ScreenFrame>
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export { AgentsComposition as AgentsScreen };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Campaign analysis screen — the ADS record/analytics-page recipe, built only
|
|
3
|
+
* from ADS exports.
|
|
4
|
+
*
|
|
5
|
+
* Follows principles `leading-back-navigation`, `analytics-surfaces` and
|
|
6
|
+
* `status-language` (see `get_principles`):
|
|
7
|
+
*
|
|
8
|
+
* - The 16px Breadcrumbs trail (parent / record) IS the page title; the one
|
|
9
|
+
* primary action sits at the right end of the same row. No back arrow and
|
|
10
|
+
* no repeated H1.
|
|
11
|
+
* - Every stat and every chart sits in its own outlined card (1px WG3 border,
|
|
12
|
+
* radius-l, 20px padding, flat), 16px apart.
|
|
13
|
+
* - A stat card stacks label → number + scope 4px apart, then a Progress bar
|
|
14
|
+
* 12px below that fills the card.
|
|
15
|
+
* - A chart card puts its title + scope subtitle on the left and the legend at
|
|
16
|
+
* the right of the same row (`legendPlacement="header"`), and prints its
|
|
17
|
+
* values on the chart — never hover-only.
|
|
18
|
+
*
|
|
19
|
+
* Copy it and replace the sample STATS / SEGMENT / REACH data with your own.
|
|
20
|
+
*/
|
|
21
|
+
import * as React from "react";
|
|
22
|
+
import {
|
|
23
|
+
BarChart,
|
|
24
|
+
BreadcrumbItem,
|
|
25
|
+
Breadcrumbs,
|
|
26
|
+
Button,
|
|
27
|
+
LineChart,
|
|
28
|
+
Progress,
|
|
29
|
+
Tab,
|
|
30
|
+
TabList,
|
|
31
|
+
Tabs,
|
|
32
|
+
} from "@actioneer/ads";
|
|
33
|
+
|
|
34
|
+
export const STATS = [
|
|
35
|
+
{ label: "Calls analysed", value: 3930, of: "of 4,800 calls", percent: 82, color: "primary" as const },
|
|
36
|
+
{ label: "Recipients reached", value: 864, of: "of 4,800 people", percent: 18, color: "primary" as const },
|
|
37
|
+
// The one number that needs attention is the only amber one.
|
|
38
|
+
{ label: "Asked for a callback", value: 144, of: "of 864 calls", percent: 17, color: "warning" as const },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export const SEGMENT = [
|
|
42
|
+
{ day: "Oct 19", netNew: 3200, already: 1900 },
|
|
43
|
+
{ day: "Oct 20", netNew: 420, already: 3080 },
|
|
44
|
+
{ day: "Oct 21", netNew: 360, already: 3240 },
|
|
45
|
+
{ day: "Oct 22", netNew: 440, already: 3460 },
|
|
46
|
+
{ day: "Oct 23", netNew: 380, already: 3520 },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const REACH = [
|
|
50
|
+
{ day: "Oct 19", reached: 12, booked: 2 },
|
|
51
|
+
{ day: "Oct 20", reached: 14, booked: 3 },
|
|
52
|
+
{ day: "Oct 21", reached: 15, booked: 4 },
|
|
53
|
+
{ day: "Oct 22", reached: 17, booked: 4 },
|
|
54
|
+
{ day: "Oct 23", reached: 18, booked: 5 },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const TABS = ["Script", "Workflow", "Analysis", "Call logs", "Guardrails"];
|
|
58
|
+
|
|
59
|
+
/** Outlined container for one stat or one chart (principle `analytics-surfaces`). */
|
|
60
|
+
function DataCard({ children, label }: { children: React.ReactNode; label: string }) {
|
|
61
|
+
return (
|
|
62
|
+
<section aria-label={label} className="flex min-w-0 flex-col rounded-l border border-tertiary bg-card p-5">
|
|
63
|
+
{children}
|
|
64
|
+
</section>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function CampaignAnalysisScreen() {
|
|
69
|
+
const [tab, setTab] = React.useState("Analysis");
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="flex h-screen bg-background p-3 text-foreground">
|
|
73
|
+
<main aria-label="Q4 renewal reminders" className="flex min-w-0 flex-1 flex-col overflow-hidden rounded-l bg-card">
|
|
74
|
+
<div className="px-6 pt-6">
|
|
75
|
+
<Breadcrumbs actions={<Button>Pause campaign</Button>}>
|
|
76
|
+
<BreadcrumbItem href="#campaigns">Voice campaigns</BreadcrumbItem>
|
|
77
|
+
<BreadcrumbItem isCurrent>Q4 renewal reminders</BreadcrumbItem>
|
|
78
|
+
</Breadcrumbs>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="px-6 pt-5">
|
|
82
|
+
<Tabs value={tab} onValueChange={setTab}>
|
|
83
|
+
<TabList aria-label="Campaign sections">
|
|
84
|
+
{TABS.map((item) => (
|
|
85
|
+
<Tab key={item} value={item}>{item}</Tab>
|
|
86
|
+
))}
|
|
87
|
+
</TabList>
|
|
88
|
+
</Tabs>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-6 pt-6 pb-6">
|
|
92
|
+
<div role="list" aria-label="Campaign progress" className="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-4">
|
|
93
|
+
{STATS.map((stat) => (
|
|
94
|
+
<div role="listitem" key={stat.label} className="contents">
|
|
95
|
+
<DataCard label={stat.label}>
|
|
96
|
+
<p className="text-m text-secondary">{stat.label}</p>
|
|
97
|
+
<p className="mt-1 flex items-baseline gap-2">
|
|
98
|
+
<span className="text-3xl tabular-nums text-foreground">{stat.value.toLocaleString("en-IN")}</span>
|
|
99
|
+
<span className="text-m text-secondary">{stat.of}</span>
|
|
100
|
+
</p>
|
|
101
|
+
<Progress className="mt-3" value={stat.percent} color={stat.color} label={stat.label} />
|
|
102
|
+
</DataCard>
|
|
103
|
+
</div>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(24rem,1fr))] gap-4">
|
|
108
|
+
<DataCard label="Who was in the segment">
|
|
109
|
+
<BarChart
|
|
110
|
+
title="Who was in the segment"
|
|
111
|
+
subtitle="4,800 net new numbers · Oct 19 – 23"
|
|
112
|
+
legendPlacement="header"
|
|
113
|
+
data={SEGMENT}
|
|
114
|
+
categoryKey="day"
|
|
115
|
+
series={["netNew", "already"]}
|
|
116
|
+
seriesLabels={{ netNew: "Net new", already: "Already in campaign" }}
|
|
117
|
+
stacked
|
|
118
|
+
height={360}
|
|
119
|
+
/>
|
|
120
|
+
</DataCard>
|
|
121
|
+
<DataCard label="Did we reach them?">
|
|
122
|
+
<LineChart
|
|
123
|
+
title="Did we reach them?"
|
|
124
|
+
subtitle="Share of 4,800 people · by day"
|
|
125
|
+
legendPlacement="header"
|
|
126
|
+
data={REACH}
|
|
127
|
+
categoryKey="day"
|
|
128
|
+
series={["reached", "booked"]}
|
|
129
|
+
seriesLabels={{ reached: "Reached", booked: "Booked" }}
|
|
130
|
+
height={360}
|
|
131
|
+
/>
|
|
132
|
+
</DataCard>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</main>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|