WorkspaceAgent builder
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises, from Astro sites to React apps and AI products.
Book a callAgent builder
Describe an agent in a sentence and it drafts the rest: who it is, what it may do, what it knows, when it runs. Test it, save it, publish it. Free and MIT licensed, as HTML, a typed React component or an Astro component, in two styles with light and dark mode.
Draft the agent, then press Test to send it a sample email.
Install
Add it to your project with the CLI.
HTML, CSS and JavaScript
npx designme add agent-builderReact
npx designme add agent-builder --reactAstro
npx designme add agent-builder --astroThe CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro islands. The Astro version lands in src/components/designme too, with the same markup, CSS and script as the HTML version. See Astro.
shadcn CLI, React
npx shadcn@latest add https://designme.dev/r/shadcn/agent-builder.json shadcn CLI, Astro
npx shadcn@latest add https://designme.dev/r/shadcn/astro/agent-builder.json In a project set up for shadcn/ui, its CLI adds the same files to your components folder, under designme/, with the shared core. To give the components your shadcn or Tailwind v4 theme, add the theme bridge.
About
What it is for and what it does.
A builder for a new agent that starts from one sentence. Type what the agent should do and press Draft: the agent fills in every section in turn (its name and purpose, its avatar's colour and shape, its instructions, the model and effort, what it may do in each tool, what it knows, and what starts it) and marks each one Drafted until someone changes it. The avatar is Dotto, a dot-matrix face on a dark screen in a bezel of the agent's colour, and it shows wherever the agent does. Beside the form, a test chat sends it a sample and shows what it would do. A bar at the foot saves it and publishes it, and it goes Live.
- To set up an agent without filling in a long form from scratch.
- When each tool needs its own permission: read only, ask first, or act on its own.
- When people should try the agent on a sample before it goes live.
- A one-line description and Draft; once drafted the button says Draft again and steps back to an outline.
- While drafting, the section in hand shimmers and the ones after it wait, faded.
- A Drafted tag in the accent's tint on every section the agent filled, until that section is changed.
- Identity: the avatar, a colour picker (the accent, blue, violet, orange and green; a colour the accent already is shows once) and a shape picker (squircle or circle), then a name and a purpose. The avatar starts in the accent, as a squircle. Instructions, Model and effort, Tools with Read, Ask or Act each, Knowledge as tags, and Trigger (Chat, Schedule or Event, with a line saying when) follow.
- The avatar also sits beside the test chat's title, on each of the agent's replies and beside Live once it is published, and a new colour or shape reaches all of them at once. While the agent reads the test sample it thinks (its eyes move cell by cell over a loading row); otherwise it idles, blinking a row of dots.
- A name is required: without one the field is marked, a line says so, and Save and Publish are disabled.
- A save bar with the state in words (Not saved, Drafted not saved, Unsaved changes, Saved, Live with the avatar), Test, Save and Publish, the one primary button. On a phone the state takes its own line over the buttons.
- The test chat shows the sample, the agent reading it, then its tags and reply.
- Two panes side by side on a wide card; on a narrow one the test chat goes under the form.
- Every field has a label; the description field's label is a real label element.
- The name field has aria-invalid and aria-describedby pointing at the line that says what is missing.
- Effort, each tool's permission and the trigger are segmented switches: groups of buttons with aria-pressed and a label naming the tool.
- The colour and shape pickers are radio groups with a label each: the arrow keys move and pick, Home and End go to the first and last, and only the picked option is in the tab order. Every option is named (Accent, Violet, Squircle, Circle) and has a tooltip.
- The avatar beside the name is an image labelled with its colour and shape; the other avatars are decorative, next to the agent's name or Live. With reduced motion it holds still, in a pose that still shows idle or thinking.
- The test chat is a polite live region, so the agent's reply is read out.
Usage
Import it in React and drive it from your own state, or place it in an Astro page.
React
import { AgentBuilder } from '@/components/designme/agent-builder/AgentBuilder';React example
The demo above as a React component, 63 lines you can copy.
'use client';
// The animated preview from designme.dev, rebuilt from the React component: one sentence, the fields fill in (the avatar takes a
// colour and a shape), one test email while the avatar thinks, save, publish.
import { useRef, useState } from 'react';
import { AgentBuilder, type AgentBuilderProps, type AgentDraft, type AgentSection } from './AgentBuilder';
import { useStory, useWait } from '../lib/useScript';
const EMPTY: AgentDraft = { name: '', purpose: '', color: 'accent', instructions: '', model: 'Haiku 5', effort: 'med', trigger: 'chat', triggerDetail: 'When someone messages it',
tools: [{ key: 'gmail', label: 'Gmail', brand: 'gmail', level: 'read' }, { key: 'linear', label: 'Linear', brand: 'linear', level: 'read' }, { key: 'slack', label: 'Slack', brand: 'slack', level: 'read' }], knowledge: [] };
const DRAFT: Record<AgentSection, Partial<AgentDraft>> = {
id: { name: 'Support triage', purpose: 'Sorts incoming support emails and routes them to the right team' },
ins: { instructions: 'Read each new email. Tag it Billing, Bug or How-to, set its priority, and route it to that team. Draft a reply for billing refunds but never send money without asking.' },
model: { model: 'Sonnet 5', effort: 'med' },
tools: { tools: [{ key: 'gmail', label: 'Gmail', brand: 'gmail', level: 'act' }, { key: 'linear', label: 'Linear', brand: 'linear', level: 'act' }, { key: 'slack', label: 'Slack', brand: 'slack', level: 'ask' }] },
know: { knowledge: [{ label: 'Refund policy.pdf', icon: 'file-text' }, { label: 'Help centre', icon: 'globe' }, { label: 'Team routing.csv', icon: 'table' }] },
trig: { trigger: 'event', triggerDetail: 'When an email arrives at support@' }
};
const TRIG = { chat: 'When someone messages it', sched: 'Every weekday at 9:00', event: 'When an email arrives at support@' };
export default function AgentBuilderDemo() {
const ref = useRef<HTMLDivElement>(null);
const wait = useWait();
const [desc, setDesc] = useState('');
const [value, setValue] = useState(EMPTY);
const [drafting, setDrafting] = useState<AgentSection | null>(null);
const [drafted, setDrafted] = useState<AgentSection[]>([]);
const [status, setStatus] = useState<AgentBuilderProps['status']>('empty');
const [publishing, setPublishing] = useState(false);
const [test, setTest] = useState<AgentBuilderProps['test']>();
const draft = async () => {
for (const k of Object.keys(DRAFT) as AgentSection[]) {
setDrafting(k); await wait(550); setValue(v => ({ ...v, ...DRAFT[k] }));
// it names the agent, then picks its avatar's colour and shape
if (k === 'id') { await wait(350); setValue(v => ({ ...v, color: 'violet' })); await wait(350); setValue(v => ({ ...v, shape: 'circle' })); }
setDrafted(d => d.includes(k) ? d : [...d, k]);
}
setDrafting(null); setStatus('dirty');
};
const runTest = async () => {
setTest({ email: { subject: 'I was charged twice for October', body: 'Hi, my card shows two charges of $48 on Oct 3. Can you refund one? Maya', from: '[email protected] · sample' }, reply: 'busy' });
await wait(2000);
setTest(t => t && { ...t, reply: { tags: [{ label: 'Billing' }, { label: 'High priority', tone: 'warn' }, { label: 'Routed to Billing' }], text: 'Duplicate charge of $48 on Oct 3. I drafted a refund reply and will ask before sending it, as the instructions say.' } });
};
const publish = async () => { setPublishing(true); await wait(800); setPublishing(false); setStatus('live'); };
useStory(ref, async ({ wait, press, taken }) => {
await wait(1000);
const text = 'Triage support emails';
taken(() => setDesc(''));
for (let i = 1; i <= text.length; i++) { setDesc(text.slice(0, i)); await wait(60); }
await wait(500); press('.dm-abx-askr .dm-pill');
await wait(5000); press('.dm-abx-bar .dm-pill:first-of-type');
await wait(4000); press('.dm-abx-bar .dm-pill:nth-of-type(2)');
await wait(1000); press('.dm-abx-bar .dm-primary');
});
return (
<div ref={ref} style={{ width: '100%' }}>
<AgentBuilder value={value} description={desc} onDescriptionChange={setDesc} onDraft={() => { void draft(); }} drafting={drafting} drafted={drafted} status={status}
onChange={(v, section) => { setValue({ ...v, triggerDetail: TRIG[v.trigger] }); setDrafted(d => d.filter(x => x !== section)); if (status !== 'empty') setStatus('dirty'); }}
onSave={() => setStatus('saved')} onPublish={publish} publishing={publishing} onTest={() => { void runTest(); }} test={test} />
</div>
);
}
Astro
In a page in src/pages. It plays the demo above; change its agent-builder.js to show your own data. Attributes like class go on its root element.
---
import AgentBuilder from '../components/designme/agent-builder/AgentBuilder.astro';
---
<AgentBuilder />
<AgentBuilder data-style="soft" class="my-class" />Props
Everything AgentBuilder takes.
AgentBuilderProps
| Prop | Type | What it does |
|---|---|---|
valuerequired | AgentDraft | |
onChangerequired | (value: AgentDraft, section: AgentSection) => void | |
descriptionrequired | string | The one sentence it starts from |
onDescriptionChangerequired | (text: string) => void | |
onDraftrequired | () => void | Asks the agent to draft every field from the description |
drafting | AgentSection | null | The section being drafted now, while drafting; the ones after it wait |
drafted | AgentSection[] | Sections the agent drafted and nobody has changed since: they carry a Drafted tag |
statusrequired | 'empty' | 'dirty' | 'saved' | 'live' | empty: nothing drafted yet; dirty: unsaved changes; saved; live |
onSaverequired | () => void | |
onPublishrequired | () => void | Promise<unknown> | |
publishing | boolean | |
onTest | () => void | |
test | { email: { subject: string; body: string; from?: string }; reply?: 'busy' | { tags: { label: string; tone?: 'warn' }[]; text: ReactNode } } | The test chat: what went in, and the agent's reply, or 'busy' while it works (the avatar thinks meanwhile) |
models | string[] | |
colors | { value: string; label: string; color: string }[] | The avatar colours to pick from |
className | string |
AgentMascotProps
| Prop | Type | What it does |
|---|---|---|
color | string | Any CSS colour; the bezel takes it, the screen and the dots are mixed from it. Leave it out for the accent |
shape | MascotShape | |
state | MascotState | idle blinks a row of dots; thinking moves the eyes cell by cell and runs a loading row. Still poses with reduced motion |
size | number | In px; 24 by default |
label | string | Names it for screen readers; without one it is decorative |
className | string |
Code
The full source, as the CLI installs it.
AgentBuilder.tsxReact · 235 lines
'use client';
import { useId, useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode, type Ref } from 'react';
import { Brand, type BrandName } from '../lib/brands';
import { cx } from '../lib/cx';
import { useIsoLayoutEffect } from '../lib/hooks';
import { Icon, type IconName } from '../lib/icons';
import { Seg, Spin, Swap, Tip } from '../lib/motion';
import '../designme.css';
import './agent-builder.css';
export type AgentSection = 'id' | 'ins' | 'model' | 'tools' | 'know' | 'trig';
export type ToolLevel = 'read' | 'ask' | 'act';
export type MascotShape = 'circle' | 'squircle';
export type MascotState = 'idle' | 'thinking';
/* ---------- Dotto, the agent's mascot ---------- */
// each outline as [bezel, screen], in a 64 by 64 box; a mascot draws only the one in use
const SHAPES: Record<MascotShape, [string, string]> = {
circle: ['M4 32A28 28 0 1 0 60 32A28 28 0 1 0 4 32Z', 'M10.5 32A21.5 21.5 0 1 0 53.5 32A21.5 21.5 0 1 0 10.5 32Z'],
squircle: ['M60 32C60 58.5 58.5 60 32 60S4 58.5 4 32 5.5 4 32 4 60 5.5 60 32Z', 'M53.5 32C53.5 52.3 52.3 53.5 32 53.5S10.5 52.3 10.5 32 11.7 10.5 32 10.5 53.5 11.7 53.5 32Z'],
};
const SHAPE_NAMES: Record<MascotShape, string> = { circle: 'Circle', squircle: 'Squircle' };
type Cell = [number, number];
const GRID: Cell[] = Array.from({ length: 25 }, (_, i) => [i % 5, Math.floor(i / 5)]);
const dots = (cells: Cell[], cls?: string) => cells.map(([x, y]) => <circle key={`${x}-${y}`} className={cls} cx={19 + x * 6.5} cy={19 + y * 6.5} r={cls ? 2.75 : 1.3} />);
export interface AgentMascotProps {
/** Any CSS colour; the bezel takes it, the screen and the dots are mixed from it. Leave it out for the accent */
color?: string;
shape?: MascotShape;
/** idle blinks a row of dots; thinking moves the eyes cell by cell and runs a loading row. Still poses with reduced motion */
state?: MascotState;
/** In px; 24 by default */
size?: number;
/** Names it for screen readers; without one it is decorative */
label?: string;
className?: string;
}
/** Dotto: a dark LED screen in a bezel of the agent's colour, with a 5 by 5 grid of dots drawing its eyes and smile. */
export function AgentMascot({ color, shape = 'squircle', state = 'idle', size, label, className }: AgentMascotProps) {
const [bz, sc] = SHAPES[shape];
const style = { ...(color ? { '--dm-m': color } : {}), ...(size ? { width: size, height: size } : {}) } as CSSProperties;
return (
<svg className={cx('dm-abx-mascot', className)} viewBox="0 0 64 64" data-shape={shape} data-state={state} style={style}
{...(label ? { role: 'img', 'aria-label': label } : { 'aria-hidden': true })} focusable="false">
<path className="dm-bz" d={bz} /><path className="dm-sc" d={sc} />
<g className="dm-dg">{dots(GRID)}</g>
<g className="dm-lk">{dots([[1, 1], [3, 1]], 'dm-on dm-tp')}{dots([[1, 2], [3, 2]], 'dm-on')}</g>
<g className="dm-id">{dots([[0, 3], [4, 3], [1, 4], [2, 4], [3, 4]], 'dm-on')}</g>
<g className="dm-th">{dots([[1, 4]], 'dm-on dm-ld dm-l1')}{dots([[2, 4]], 'dm-on dm-ld dm-l2')}{dots([[3, 4]], 'dm-on dm-ld dm-l3')}</g>
</svg>
);
}
/** The colours the builder offers: the accent, then four of the palette's. A colour the accent already is shows once */
export const AGENT_COLORS: { value: string; label: string; color: string }[] = [
{ value: 'accent', label: 'Accent', color: 'var(--dm-accent)' },
{ value: 'blue', label: 'Blue', color: '#116ff8' },
{ value: 'violet', label: 'Violet', color: '#7c4dff' },
{ value: 'orange', label: 'Orange', color: '#f76700' },
{ value: 'green', label: 'Green', color: '#00a876' }
];
/** A radio group: a click picks, the arrow keys move and pick (and wrap), Home and End go to the ends; only the picked one is
in the tab order */
function Radios<T extends string>({ options, value, onChange, label, className, children, item, rootRef }: {
options: { value: T; label: string; hidden?: boolean }[]; value: T; onChange: (v: T) => void; label: string; className?: string;
children?: ReactNode; rootRef?: Ref<HTMLDivElement>; item: (o: { value: T; label: string }) => { className?: string; style?: CSSProperties; content?: ReactNode };
}) {
const shown = options.filter(o => !o.hidden);
const key = (e: KeyboardEvent<HTMLDivElement>) => {
const i = shown.findIndex(o => o.value === value), d = ({ ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 } as Record<string, number>)[e.key];
const to = d ? shown[(i + d + shown.length) % shown.length] : e.key === 'Home' ? shown[0] : e.key === 'End' ? shown[shown.length - 1] : null;
if (!to) return;
e.preventDefault(); onChange(to.value);
(e.currentTarget.querySelector(`[data-v="${to.value}"]`) as HTMLElement | null)?.focus();
};
return (
<div className={className} role="radiogroup" aria-label={label} onKeyDown={key} ref={rootRef}>
{children}
{shown.map(o => { const { className: c, style, content } = item(o);
return <Tip key={o.value} label={o.label}><button type="button" role="radio" aria-checked={o.value === value} tabIndex={o.value === value ? 0 : -1} aria-label={o.label}
data-v={o.value} className={c} style={style} onClick={() => onChange(o.value)}>{content}</button></Tip>; })}
</div>
);
}
export interface AgentDraft {
name: string;
purpose: string;
/** The avatar's colour: one of `colors` by its value ('accent', 'blue', 'violet', 'orange', 'green' by default) */
color: string;
/** The avatar's outline. Leave it out for a squircle */
shape?: MascotShape;
instructions: string;
model: string;
effort: 'low' | 'med' | 'high';
tools: { key: string; label: string; brand?: BrandName; level: ToolLevel }[];
knowledge: { label: string; icon?: IconName }[];
trigger: 'chat' | 'sched' | 'event';
/** A line beside the trigger, like "When an email arrives at support@" */
triggerDetail?: string;
}
export interface AgentBuilderProps {
value: AgentDraft;
onChange: (value: AgentDraft, section: AgentSection) => void;
/** The one sentence it starts from */
description: string;
onDescriptionChange: (text: string) => void;
/** Asks the agent to draft every field from the description */
onDraft: () => void;
/** The section being drafted now, while drafting; the ones after it wait */
drafting?: AgentSection | null;
/** Sections the agent drafted and nobody has changed since: they carry a Drafted tag */
drafted?: AgentSection[];
/** empty: nothing drafted yet; dirty: unsaved changes; saved; live */
status: 'empty' | 'dirty' | 'saved' | 'live';
onSave: () => void;
onPublish: () => void | Promise<unknown>;
publishing?: boolean;
onTest?: () => void;
/** The test chat: what went in, and the agent's reply, or 'busy' while it works (the avatar thinks meanwhile) */
test?: { email: { subject: string; body: string; from?: string }; reply?: 'busy' | { tags: { label: string; tone?: 'warn' }[]; text: ReactNode } };
models?: string[];
/** The avatar colours to pick from */
colors?: { value: string; label: string; color: string }[];
className?: string;
}
const SECTIONS: AgentSection[] = ['id', 'ins', 'model', 'tools', 'know', 'trig'];
const TRIG = [{ value: 'chat' as const, label: 'Chat' }, { value: 'sched' as const, label: 'Schedule' }, { value: 'event' as const, label: 'Event' }];
/** Describe an agent in one sentence and the agent drafts the rest: identity (its avatar, Dotto, in a colour and shape you pick),
instructions, model and effort, tools with a permission each, knowledge and trigger. A test chat beside it, and a bar to save and
publish. */
export function AgentBuilder({ value: v, onChange, description, onDescriptionChange, onDraft, drafting = null, drafted = [], status, onSave, onPublish, publishing = false, onTest, test,
models = ['Haiku 5', 'Sonnet 5', 'Opus 5.5'], colors = AGENT_COLORS, className }: AgentBuilderProps) {
const errId = useId(), descId = useId();
const ref = useRef<HTMLDivElement>(null), shpRef = useRef<HTMLDivElement>(null);
// the colours the accent already is, read once it is placed
const [dupes, setDupes] = useState<string[]>([]);
useIsoLayoutEffect(() => {
const el = ref.current; if (!el) return;
const ink = (c: string) => { const t = document.createElement('i'); t.hidden = true; t.style.color = c; el.appendChild(t); const x = getComputedStyle(t).color; t.remove(); return x; };
const acc = ink('var(--dm-accent)');
setDupes(colors.filter(c => c.value !== 'accent' && ink(c.color) === acc).map(c => c.value));
}, [colors]);
const shape = v.shape ?? 'squircle';
const pick = colors.find(c => c.value === v.color && !dupes.includes(c.value)) ?? colors[0];
const mood: MascotState = test?.reply === 'busy' ? 'thinking' : 'idle';
const dotto = (extra?: Partial<AgentMascotProps>) => <AgentMascot color={pick.color} shape={shape} state={mood} {...extra} />;
// the shape switch's knob sits under the picked outline
const [knob, setKnob] = useState<CSSProperties>({});
useIsoLayoutEffect(() => {
const el = shpRef.current; if (!el) return;
const place = () => { const b = el.querySelector<HTMLElement>('[aria-checked="true"]'); if (b) setKnob({ left: b.offsetLeft, width: b.offsetWidth }); };
place(); const ro = new ResizeObserver(place); ro.observe(el); return () => ro.disconnect();
}, [shape]);
const set = (section: AgentSection, patch: Partial<AgentDraft>) => onChange({ ...v, ...patch }, section);
const at = drafting ? SECTIONS.indexOf(drafting) : -1;
const sec = (k: AgentSection) => cx('dm-abx-s', at >= 0 && SECTIONS.indexOf(k) > at && 'dm-wait', drafting === k && 'dm-busy');
const head = (k: AgentSection, label: string, extra?: ReactNode) => (
<div className="dm-abx-sh"><span>{label}</span>{drafted.includes(k) && drafting !== k && <span className="dm-abx-dt dm-enter"><Icon name="spark" size="xs" />Drafted</span>}{extra}</div>
);
const bad = status !== 'empty' && !v.name.trim();
return (
<div data-dm="" ref={ref} className={cx('dm-abx dm-panel', className)} style={{ '--dm-m': pick.color } as CSSProperties}>
<div className="dm-abx-g">
<div className="dm-abx-f">
<div className="dm-abx-ask"><label className="dm-abx-l" htmlFor={descId}>Describe your agent</label>
<div className="dm-abx-askr"><input className="dm-abx-in" id={descId} autoComplete="off" placeholder="What should it do?" value={description} onChange={e => onDescriptionChange(e.target.value)} />
<button type="button" className={cx('dm-pill dm-sm', status === 'empty' ? 'dm-primary' : 'dm-line')} disabled={!!drafting || !description.trim()} onClick={onDraft}>
{drafting ? <><Spin />Drafting</> : status === 'empty' ? <><Icon name="spark" size="xs" />Draft</> : <><Icon name="refresh" size="xs" />Draft again</>}</button></div>
</div>
<div className={sec('id')} role="group" aria-label="Identity">{head('id', 'Identity')}
<div className="dm-abx-idr">
<span className="dm-abx-av" role="img" aria-label={`Avatar: ${pick.label}, ${SHAPE_NAMES[shape].toLowerCase()}`}>{dotto()}</span>
<input className="dm-abx-in" aria-label="Name" placeholder="Name" autoComplete="off" value={v.name} aria-invalid={bad} aria-describedby={errId} onChange={e => set('id', { name: e.target.value })} />
<div className="dm-abx-look">
<Radios className="dm-abx-sw" label="Colour" value={pick.value} onChange={c => set('id', { color: c })}
options={colors.map(c => ({ value: c.value, label: c.label, hidden: dupes.includes(c.value) }))}
item={o => ({ style: { '--dm-s': colors.find(c => c.value === o.value)!.color } as CSSProperties })} />
<Radios className="dm-seg2 dm-abx-shp" label="Shape" value={shape} onChange={s => set('id', { shape: s })} rootRef={shpRef}
options={(Object.keys(SHAPES) as MascotShape[]).map(s => ({ value: s, label: SHAPE_NAMES[s] }))}
item={o => ({ content: <svg viewBox="0 0 64 64" aria-hidden="true" focusable="false"><path d={SHAPES[o.value][0]} /></svg> })}>
<span className="dm-knob" style={knob} />
</Radios>
</div>
</div>
<span className="dm-abx-err" id={errId} hidden={!bad}><Icon name="alert" size="xs" />Give it a name to save it</span>
<input className="dm-abx-in" aria-label="Purpose" placeholder="What it is for" autoComplete="off" value={v.purpose} onChange={e => set('id', { purpose: e.target.value })} />
</div>
<div className={sec('ins')} role="group" aria-label="Instructions">{head('ins', 'Instructions')}<textarea className="dm-abx-ta" rows={3} aria-label="Instructions" placeholder="How it should work" value={v.instructions} onChange={e => set('ins', { instructions: e.target.value })} /></div>
<div className={sec('model')} role="group" aria-label="Model and effort">{head('model', 'Model and effort')}
<div className="dm-abx-mr"><select className="dm-abx-in dm-abx-sel" aria-label="Model" value={v.model} onChange={e => set('model', { model: e.target.value })}>{models.map(m => <option key={m}>{m}</option>)}</select>
<Seg className="dm-abx-eff" label="Effort" options={[{ value: 'low', label: 'Low' }, { value: 'med', label: 'Medium' }, { value: 'high', label: 'High' }]} value={v.effort} onChange={e => set('model', { effort: e })} /></div>
</div>
<div className={sec('tools')} role="group" aria-label="Tools">{head('tools', 'Tools', <span className="dm-muted">What it may do in each</span>)}
<div className="dm-abx-tools">{v.tools.map(t => (
<div key={t.key} className="dm-abx-tl">{t.brand ? <Brand name={t.brand} /> : <Icon name="bolt" />}<span className="dm-n">{t.label}</span>
<Seg label={`${t.label}: what it may do`} options={[{ value: 'read', label: 'Read' }, { value: 'ask', label: 'Ask' }, { value: 'act', label: 'Act' }]} value={t.level}
onChange={l => set('tools', { tools: v.tools.map(x => x.key === t.key ? { ...x, level: l } : x) })} /></div>))}</div>
</div>
<div className={sec('know')} role="group" aria-label="Knowledge">{head('know', 'Knowledge')}
<div className="dm-abx-kn">{v.knowledge.length ? v.knowledge.map(k => <span key={k.label} className="dm-tag">{k.icon && <Icon name={k.icon} size="xs" />}{k.label}</span>) : <span className="dm-muted" style={{ fontSize: 12 }}>Nothing yet</span>}</div>
</div>
<div className={sec('trig')} role="group" aria-label="Trigger">{head('trig', 'Trigger')}
<div className="dm-abx-tr"><Seg className="dm-abx-trs" label="Starts on" options={TRIG} value={v.trigger} onChange={t => set('trig', { trigger: t })} />{v.triggerDetail && <Swap className="dm-muted dm-abx-trd">{v.triggerDetail}</Swap>}</div>
</div>
<div className="dm-abx-bar">
<span className="dm-abx-st dm-muted">{status === 'live' ? <>{dotto()}<span className="dm-tag dm-ok">Live</span></> : status === 'saved' ? <><Icon name="check" size="xs" />Saved</> : status === 'dirty' ? (drafted.length ? 'Drafted, not saved' : 'Unsaved changes') : 'Not saved'}</span>
<span className="dm-sp" />
{onTest && <button type="button" className="dm-pill dm-line dm-sm" disabled={status === 'empty' || test?.reply === 'busy'} onClick={onTest}><Icon name="play" size="xs" />Test</button>}
<button type="button" className="dm-pill dm-line dm-sm" disabled={status === 'empty' || status === 'saved' || status === 'live' || bad} onClick={onSave}>Save</button>
<button type="button" className="dm-pill dm-primary dm-sm" disabled={status === 'empty' || status === 'live' || bad || publishing} onClick={() => { void onPublish(); }}>{publishing ? <><Spin />Publishing</> : status === 'live' ? 'Published' : 'Publish'}</button>
</div>
</div>
<div className="dm-abx-c">
<div className="dm-abx-ch">{dotto()}<div className="dm-abx-cht"><b>Test</b><Swap className="dm-muted dm-abx-cs">{!test ? 'Try it before it goes live' : test.reply === 'busy' || !test.reply ? 'Testing with a sample email' : 'The test passed'}</Swap></div></div>
<div className="dm-abx-log" aria-live="polite">
{!test ? <p className="dm-muted dm-abx-empty">Draft the agent, then press Test to send it a sample email.</p> : <>
<div className="dm-abx-em dm-enter"><b>{test.email.subject}</b><span>{test.email.body}</span>{test.email.from && <span className="dm-muted">{test.email.from}</span>}</div>
{test.reply === 'busy' ? <div className="dm-abx-rp dm-enter"><span className="dm-k">{dotto()}<span className="dm-shim">{v.name || 'The agent'} is reading it</span></span></div>
: test.reply ? <div className="dm-abx-rp dm-enter"><span className="dm-k">{dotto()}{v.name || 'The agent'}</span>
<span className="dm-tags">{test.reply.tags.map(t => <span key={t.label} className={cx('dm-tag', t.tone === 'warn' && 'dm-warn')}>{t.label}</span>)}</span><span>{test.reply.text}</span></div> : null}
</>}
</div>
</div>
</div>
</div>
);
}
agent-builder.cssReact · 104 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Agent builder ---------- */
/* a form on the left, a test chat on the right (under it on a narrow card). Every field the agent drafted carries a Drafted tag
until someone changes it; a bar at the foot of the form saves, tests and publishes */
.dm-abx{overflow:hidden;container-type:inline-size}
.dm-abx-g{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr)}
.dm-abx-f{display:flex;flex-direction:column;gap:var(--dm-s-3);padding:var(--dm-s-4);min-width:0}
.dm-abx-l,.dm-abx-sh{font-size:12px;color:var(--dm-muted)}
.dm-abx-ask{display:flex;flex-direction:column;gap:6px;padding-bottom:var(--dm-s-3);border-bottom:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-abx-askr{display:flex;gap:var(--dm-s-2)}
.dm-abx-askr .dm-abx-in{flex:1}
.dm-abx-in,.dm-abx-ta{min-width:0;height:32px;padding:0 10px;border:0;border-radius:var(--dm-r-field);outline:0;background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line) 50%,transparent);color:var(--dm-fg);font:inherit;font-size:13px;transition:box-shadow var(--dm-t-fast)}
.dm-abx-ta{height:auto;min-height:76px;padding:var(--dm-s-2) 10px;resize:none;line-height:1.5;border-radius:var(--dm-r-box);field-sizing:content}
.dm-abx-in:focus,.dm-abx-ta:focus{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line-strong) 50%,transparent)}
.dm-abx-in[aria-invalid="true"]{box-shadow:inset 0 0 0 1px var(--dm-bad)}
.dm-abx-in::placeholder,.dm-abx-ta::placeholder{color:var(--dm-muted)}
@supports (corner-shape:squircle){.dm-abx-in{border-radius:var(--dm-r-field-sq)}.dm-abx-ta{border-radius:var(--dm-r-box-sq)}}
.dm-abx-sel{appearance:none;-webkit-appearance:none;padding-right:28px;background:var(--dm-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") right 9px center/12px no-repeat;cursor:pointer}
.dm-abx-s{display:flex;flex-direction:column;gap:6px;transition:opacity .3s}
.dm-abx-sh{display:flex;align-items:center;gap:var(--dm-s-2);min-height:20px}
.dm-abx-sh>span:first-child{color:var(--dm-fg2);font-weight:500}
.dm-abx-sh .dm-muted{margin-left:auto;font-size:11.5px}
.dm-abx-s.dm-wait{opacity:.45}
.dm-abx-s.dm-busy .dm-abx-sh>span:first-child{animation:dm-shim 2.2s linear infinite;background:linear-gradient(90deg,var(--dm-fg2) 0%,var(--dm-fg2) 38%,var(--dm-faint) 50%,var(--dm-fg2) 62%,var(--dm-fg2) 100%);background-size:260% 100%;-webkit-background-clip:text;background-clip:text;color:transparent}
/* drafted by the agent, until someone edits it: the accent's tint with the accent as text */
.dm-abx-dt{display:inline-flex;align-items:center;gap:4px;height:18px;padding:0 6px;border-radius:var(--dm-r-tag);background:var(--dm-accent-bg);color:var(--dm-accent-fg,var(--dm-accent));font-size:11px;font-weight:500}
@supports (corner-shape:squircle){.dm-abx-dt{border-radius:var(--dm-r-tag-sq)}}
/* identity: the agent's avatar (Dotto) beside its name, with its colour and shape picked under the name */
.dm-abx-idr{display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--dm-s-2) var(--dm-s-3);align-items:center}
.dm-abx-av{grid-row:span 2;width:64px;height:64px;display:grid;place-items:center}
.dm-abx-av .dm-abx-mascot{width:64px;height:64px}
.dm-abx-look{display:flex;align-items:center;justify-content:space-between;gap:var(--dm-s-2) var(--dm-s-3);flex-wrap:wrap;min-width:0}
/* colour: swatches, a radio group; the one picked has a grey ring */
.dm-abx-sw{display:flex;gap:var(--dm-s-2)}
.dm-abx-sw button{width:20px;height:20px;border-radius:calc(6px * var(--dm-rk));background:var(--dm-s);box-shadow:inset 0 0 0 1px rgba(0,0,0,.08);transition:transform .15s var(--dm-ease),box-shadow var(--dm-t-fast)}
@media (hover:hover) and (pointer:fine){.dm-abx-sw button:hover{transform:scale(1.08)}}
.dm-abx-sw button[aria-checked="true"]{box-shadow:0 0 0 2px var(--dm-surface),0 0 0 3px color-mix(in srgb,var(--dm-pick-on) 70%,transparent)}
.dm-abx-sw button:focus-visible{outline-offset:4px;border-radius:calc(6px * var(--dm-rk))}
/* shape: a segmented switch of outlines, a radio group */
.dm-abx-shp button{width:30px;height:24px;padding:0;display:grid;place-items:center}
.dm-abx-shp button[aria-checked="true"]{color:var(--dm-fg)}
.dm-abx-shp svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.5;overflow:visible}
.dm-abx-shp svg path{vector-effect:non-scaling-stroke}
:root:not([data-style="soft"]) .dm-abx-sw button:not(:where([data-style="soft"],[data-style="soft"] *)),:root:not([data-style="soft"]) .dm-abx-sw button:focus-visible:not(:where([data-style="soft"],[data-style="soft"] *)){border-radius:0}
/* Dotto, the agent's mascot: a white screen (owner, 4 Nov: dark on a dark bezel was hard to see) in a bezel of the agent's colour (--dm-m, the accent unless one is picked), a 5 by 5
grid of dots drawing its eyes and smile. Idle blinks the top row of dots; thinking moves the eyes cell by cell and runs a
loading row. With reduced motion each state is a still pose */
.dm-abx-mascot{--dm-m-c:var(--dm-m,var(--dm-accent));--dm-m-ink:color-mix(in oklab,var(--dm-m-c) 18%,#0a0a10);--dm-m-glow:color-mix(in oklab,var(--dm-m-c) 82%,white);display:block;flex:none;width:24px;height:24px;overflow:visible}
.dm-abx-mascot .dm-bz{fill:var(--dm-m-c);transition:fill var(--dm-t-fast)}
.dm-abx-mascot .dm-sc{fill:#fff;transition:fill var(--dm-t-fast)}
.dm-abx-mascot .dm-dg{fill:var(--dm-m-c);opacity:.22}
/* the lit dots are the agent's colour taken darker, so they read on the white screen in every colour, lime included */
.dm-abx-mascot .dm-on{fill:color-mix(in oklab,var(--dm-m-c) 72%,#0a0a10)}
/* in dark mode the screen is black and the lit dots glow in the agent's colour */
:root[data-theme="dark"] .dm-abx-mascot .dm-sc{fill:#0b0b0e}
:root[data-theme="dark"] .dm-abx-mascot .dm-on{fill:var(--dm-m-glow)}
:root[data-theme="dark"] .dm-abx-mascot .dm-dg{opacity:.3}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .dm-abx-mascot .dm-sc{fill:#0b0b0e}:root:not([data-theme="light"]) .dm-abx-mascot .dm-on{fill:var(--dm-m-glow)}:root:not([data-theme="light"]) .dm-abx-mascot .dm-dg{opacity:.3}}
.dm-abx-mascot .dm-th,.dm-abx-mascot[data-state="thinking"] .dm-id{display:none}
.dm-abx-mascot[data-state="thinking"] .dm-th{display:inline}
.dm-abx-mascot[data-state="thinking"] .dm-lk{transform:translate(0,-6.5px)}
@media (prefers-reduced-motion:no-preference){
.dm-abx-mascot .dm-tp{animation:dm-abxblink 5.2s step-end infinite}
.dm-abx-mascot[data-state="thinking"] .dm-lk{animation:dm-abxlook 2.7s step-end infinite}
.dm-abx-mascot[data-state="thinking"] .dm-ld{animation:dm-abxload .96s step-end infinite}
.dm-abx-mascot[data-state="thinking"] .dm-l2{animation-delay:-.64s}
.dm-abx-mascot[data-state="thinking"] .dm-l3{animation-delay:-.32s}
}
@keyframes dm-abxblink{0%{opacity:1}90%{opacity:0}94%{opacity:1}}
@keyframes dm-abxlook{0%{transform:translate(0,-6.5px)}33.3%{transform:translate(6.5px,-6.5px)}66.6%{transform:translate(-6.5px,-6.5px)}}
@keyframes dm-abxload{0%{opacity:1}33.3%{opacity:.18}}
.dm-abx-err{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dm-bad)}
.dm-abx-mr{display:flex;gap:var(--dm-s-2);flex-wrap:wrap}
.dm-abx-mr .dm-abx-sel{flex:1;min-width:120px}
.dm-abx-eff button,.dm-abx-trs button,.dm-abx-tl .dm-seg2 button{font-size:12px;height:28px;padding:0 10px}
.dm-abx-tools{display:flex;flex-direction:column;gap:2px}
.dm-abx-tl{display:flex;align-items:center;gap:var(--dm-s-2);padding:2px 0;font-size:13px}
.dm-abx-tl .dm-brand{width:16px;height:16px}
.dm-abx-tl .dm-n{flex:1;min-width:0}
.dm-abx-tl .dm-seg2 button{height:24px;padding:0 9px;font-size:11.5px}
.dm-abx-kn{display:flex;flex-wrap:wrap;gap:6px}
.dm-abx-tr{display:flex;align-items:center;gap:var(--dm-s-2);flex-wrap:wrap}
.dm-abx-trd{font-size:12px}
.dm-abx-bar{display:flex;align-items:center;gap:6px;margin-top:var(--dm-s-1);padding-top:var(--dm-s-3);border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-abx-st{font-size:12.5px;display:flex;align-items:center;gap:6px}
.dm-abx-c{display:flex;flex-direction:column;min-width:0;background:var(--dm-row);border-left:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-abx-ch{display:flex;align-items:center;gap:var(--dm-s-3);padding:var(--dm-s-4) var(--dm-s-4) var(--dm-s-2);font-size:13.5px;line-height:1.4}
.dm-abx-ch .dm-abx-mascot{width:32px;height:32px}
.dm-abx-cht{display:flex;flex-direction:column;min-width:0}
.dm-abx-ch b{font-weight:500}
.dm-abx-ch .dm-muted{font-size:12px}
.dm-abx-log{flex:1;display:flex;flex-direction:column;gap:var(--dm-s-3);padding:var(--dm-s-2) var(--dm-s-4) var(--dm-s-4)}
.dm-abx-empty{margin:auto 0;text-align:center;font-size:12.5px;line-height:1.5}
.dm-abx-em{align-self:flex-end;max-width:92%;display:flex;flex-direction:column;gap:2px;padding:9px var(--dm-s-3);border-radius:var(--dm-r-card);background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-line) 50%,transparent);font-size:12.5px;line-height:1.5;color:var(--dm-fg2)}
.dm-abx-em b{font-weight:500;color:var(--dm-fg);font-size:13px}
.dm-abx-em .dm-muted{font-size:11.5px}
.dm-abx-rp{display:flex;flex-direction:column;gap:6px;font-size:13px;line-height:1.5}
.dm-abx-rp .dm-k{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dm-muted)}
.dm-abx-rp .dm-tags{display:flex;gap:4px;flex-wrap:wrap}
.dm-abx-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@container (max-width:600px){.dm-abx-g{grid-template-columns:minmax(0,1fr)}.dm-abx-c{border-left:0;border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent);min-height:220px}}
/* on a phone the state takes its own line over the buttons, so Live with its avatar never pushes Publish out of the card */
@container (max-width:340px){.dm-abx-bar{flex-wrap:wrap;row-gap:var(--dm-s-2)}.dm-abx-st{flex-basis:100%}}
AgentBuilder.astroAstro · 49 lines
---
// Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync.
// Agent builder: Describe an agent in a sentence and it drafts the rest: who it is, what it may do, what it knows, when it runs. Test it, save it, publish it.
// The markup, CSS and script are designme.dev's own, run by the shared core in ../core, which mounts the component when it scrolls
// into view and plays its demo. To show your own data, change agent-builder.js. The markup is kept exactly as it is (is:raw).
import type { HTMLAttributes } from 'astro/types';
import '../core/designme.css';
import './agent-builder.css';
export interface Props extends HTMLAttributes<'div'> {
/** "soft" draws it in the Soft style. Leave it out for Blueprint, or for the style set on <html> (data-style="soft") */
'data-style'?: 'soft';
}
const { class: className, ...rest } = Astro.props;
---
<div class:list={['card', 'wide', className]} data-c="agent-builder" {...rest}>
<div class="stage-in"><Fragment is:raw>
<div class="abx panel"><div class="abx-g">
<div class="abx-f">
<div class="abx-ask"><label class="abx-l" for="abx-desc">Describe your agent</label><div class="abx-askr"><input class="abx-in" id="abx-desc" autocomplete="off" placeholder="What should it do?"><button class="pill primary sm abx-go"><svg class="i xs"><use href="#i-spark"/></svg>Draft</button></div></div>
<div class="abx-s" data-s="id" role="group" aria-label="Identity"><div class="abx-sh"><span>Identity</span></div>
<div class="abx-idr"><span class="abx-av"></span><input class="abx-in abx-name" aria-label="Name" placeholder="Name" autocomplete="off">
<div class="abx-look"><div class="abx-sw" role="radiogroup" aria-label="Colour"></div><div class="seg2 abx-shp" role="radiogroup" aria-label="Shape"></div></div></div>
<span class="abx-err" hidden><svg class="i xs"><use href="#i-alert"/></svg>Give it a name to save it</span>
<input class="abx-in abx-purp" aria-label="Purpose" placeholder="What it is for" autocomplete="off">
</div>
<div class="abx-s" data-s="ins" role="group" aria-label="Instructions"><div class="abx-sh"><span>Instructions</span></div><textarea class="abx-ta" rows="3" aria-label="Instructions" placeholder="How it should work"></textarea></div>
<div class="abx-s" data-s="model" role="group" aria-label="Model and effort"><div class="abx-sh"><span>Model and effort</span></div>
<div class="abx-mr"><select class="abx-in abx-sel" aria-label="Model"><option>Haiku 5</option><option>Sonnet 5</option><option>Opus 5.5</option></select><div class="seg2 abx-eff" role="group" aria-label="Effort"><button aria-pressed="false" data-v="low">Low</button><button aria-pressed="true" data-v="med">Medium</button><button aria-pressed="false" data-v="high">High</button></div></div>
</div>
<div class="abx-s" data-s="tools" role="group" aria-label="Tools"><div class="abx-sh"><span>Tools</span><span class="muted">What it may do in each</span></div><div class="abx-tools"></div></div>
<div class="abx-s" data-s="know" role="group" aria-label="Knowledge"><div class="abx-sh"><span>Knowledge</span></div><div class="abx-kn"></div></div>
<div class="abx-s" data-s="trig" role="group" aria-label="Trigger"><div class="abx-sh"><span>Trigger</span></div>
<div class="abx-tr"><div class="seg2 abx-trs" role="group" aria-label="Starts on"><button aria-pressed="true" data-v="chat">Chat</button><button aria-pressed="false" data-v="sched">Schedule</button><button aria-pressed="false" data-v="event">Event</button></div><span class="muted abx-trd">When someone messages it</span></div>
</div>
<div class="abx-bar"><span class="abx-st muted">Not saved</span><span class="sp"></span><button class="pill line sm abx-test"><svg class="i xs"><use href="#i-play"/></svg>Test</button><button class="pill line sm abx-save">Save</button><button class="pill primary sm abx-pub">Publish</button></div>
</div>
<div class="abx-c">
<div class="abx-ch"><div class="abx-cht"><b>Test</b><span class="muted abx-cs">Try it before it goes live</span></div></div>
<div class="abx-log" aria-live="polite"><p class="muted abx-empty">Draft the agent, then press Test to send it a sample email.</p></div>
</div>
</div><p class="abx-sr" role="status"></p>
</div>
</Fragment></div>
</div>
<script>
import './agent-builder.js';
</script>
agent-builder.htmlHTML · 31 lines
<div class="card wide" data-c="agent-builder">
<div class="stage-in">
<div class="abx panel"><div class="abx-g">
<div class="abx-f">
<div class="abx-ask"><label class="abx-l" for="abx-desc">Describe your agent</label><div class="abx-askr"><input class="abx-in" id="abx-desc" autocomplete="off" placeholder="What should it do?"><button class="pill primary sm abx-go"><svg class="i xs"><use href="#i-spark"/></svg>Draft</button></div></div>
<div class="abx-s" data-s="id" role="group" aria-label="Identity"><div class="abx-sh"><span>Identity</span></div>
<div class="abx-idr"><span class="abx-av"></span><input class="abx-in abx-name" aria-label="Name" placeholder="Name" autocomplete="off">
<div class="abx-look"><div class="abx-sw" role="radiogroup" aria-label="Colour"></div><div class="seg2 abx-shp" role="radiogroup" aria-label="Shape"></div></div></div>
<span class="abx-err" hidden><svg class="i xs"><use href="#i-alert"/></svg>Give it a name to save it</span>
<input class="abx-in abx-purp" aria-label="Purpose" placeholder="What it is for" autocomplete="off">
</div>
<div class="abx-s" data-s="ins" role="group" aria-label="Instructions"><div class="abx-sh"><span>Instructions</span></div><textarea class="abx-ta" rows="3" aria-label="Instructions" placeholder="How it should work"></textarea></div>
<div class="abx-s" data-s="model" role="group" aria-label="Model and effort"><div class="abx-sh"><span>Model and effort</span></div>
<div class="abx-mr"><select class="abx-in abx-sel" aria-label="Model"><option>Haiku 5</option><option>Sonnet 5</option><option>Opus 5.5</option></select><div class="seg2 abx-eff" role="group" aria-label="Effort"><button aria-pressed="false" data-v="low">Low</button><button aria-pressed="true" data-v="med">Medium</button><button aria-pressed="false" data-v="high">High</button></div></div>
</div>
<div class="abx-s" data-s="tools" role="group" aria-label="Tools"><div class="abx-sh"><span>Tools</span><span class="muted">What it may do in each</span></div><div class="abx-tools"></div></div>
<div class="abx-s" data-s="know" role="group" aria-label="Knowledge"><div class="abx-sh"><span>Knowledge</span></div><div class="abx-kn"></div></div>
<div class="abx-s" data-s="trig" role="group" aria-label="Trigger"><div class="abx-sh"><span>Trigger</span></div>
<div class="abx-tr"><div class="seg2 abx-trs" role="group" aria-label="Starts on"><button aria-pressed="true" data-v="chat">Chat</button><button aria-pressed="false" data-v="sched">Schedule</button><button aria-pressed="false" data-v="event">Event</button></div><span class="muted abx-trd">When someone messages it</span></div>
</div>
<div class="abx-bar"><span class="abx-st muted">Not saved</span><span class="sp"></span><button class="pill line sm abx-test"><svg class="i xs"><use href="#i-play"/></svg>Test</button><button class="pill line sm abx-save">Save</button><button class="pill primary sm abx-pub">Publish</button></div>
</div>
<div class="abx-c">
<div class="abx-ch"><div class="abx-cht"><b>Test</b><span class="muted abx-cs">Try it before it goes live</span></div></div>
<div class="abx-log" aria-live="polite"><p class="muted abx-empty">Draft the agent, then press Test to send it a sample email.</p></div>
</div>
</div><p class="abx-sr" role="status"></p>
</div>
</div>
</div>
agent-builder.cssHTML · 103 lines
/* ---------- Agent builder ---------- */
/* a form on the left, a test chat on the right (under it on a narrow card). Every field the agent drafted carries a Drafted tag
until someone changes it; a bar at the foot of the form saves, tests and publishes */
.abx{overflow:hidden;container-type:inline-size}
.abx-g{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr)}
.abx-f{display:flex;flex-direction:column;gap:var(--dm-s-3);padding:var(--dm-s-4);min-width:0}
.abx-l,.abx-sh{font-size:12px;color:var(--dm-muted)}
.abx-ask{display:flex;flex-direction:column;gap:6px;padding-bottom:var(--dm-s-3);border-bottom:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.abx-askr{display:flex;gap:var(--dm-s-2)}
.abx-askr .abx-in{flex:1}
.abx-in,.abx-ta{min-width:0;height:32px;padding:0 10px;border:0;border-radius:var(--dm-r-field);outline:0;background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line) 50%,transparent);color:var(--dm-fg);font:inherit;font-size:13px;transition:box-shadow var(--dm-t-fast)}
.abx-ta{height:auto;min-height:76px;padding:var(--dm-s-2) 10px;resize:none;line-height:1.5;border-radius:var(--dm-r-box);field-sizing:content}
.abx-in:focus,.abx-ta:focus{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line-strong) 50%,transparent)}
.abx-in[aria-invalid="true"]{box-shadow:inset 0 0 0 1px var(--dm-bad)}
.abx-in::placeholder,.abx-ta::placeholder{color:var(--dm-muted)}
@supports (corner-shape:squircle){.abx-in{border-radius:var(--dm-r-field-sq)}.abx-ta{border-radius:var(--dm-r-box-sq)}}
.abx-sel{appearance:none;-webkit-appearance:none;padding-right:28px;background:var(--dm-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") right 9px center/12px no-repeat;cursor:pointer}
.abx-s{display:flex;flex-direction:column;gap:6px;transition:opacity .3s}
.abx-sh{display:flex;align-items:center;gap:var(--dm-s-2);min-height:20px}
.abx-sh>span:first-child{color:var(--dm-fg2);font-weight:500}
.abx-sh .muted{margin-left:auto;font-size:11.5px}
.abx-s.wait{opacity:.45}
.abx-s.busy .abx-sh>span:first-child{animation:shim 2.2s linear infinite;background:linear-gradient(90deg,var(--dm-fg2) 0%,var(--dm-fg2) 38%,var(--dm-faint) 50%,var(--dm-fg2) 62%,var(--dm-fg2) 100%);background-size:260% 100%;-webkit-background-clip:text;background-clip:text;color:transparent}
/* drafted by the agent, until someone edits it: the accent's tint with the accent as text */
.abx-dt{display:inline-flex;align-items:center;gap:4px;height:18px;padding:0 6px;border-radius:var(--dm-r-tag);background:var(--dm-accent-bg);color:var(--dm-accent-fg,var(--dm-accent));font-size:11px;font-weight:500}
@supports (corner-shape:squircle){.abx-dt{border-radius:var(--dm-r-tag-sq)}}
/* identity: the agent's avatar (Dotto) beside its name, with its colour and shape picked under the name */
.abx-idr{display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--dm-s-2) var(--dm-s-3);align-items:center}
.abx-av{grid-row:span 2;width:64px;height:64px;display:grid;place-items:center}
.abx-av .abx-mascot{width:64px;height:64px}
.abx-look{display:flex;align-items:center;justify-content:space-between;gap:var(--dm-s-2) var(--dm-s-3);flex-wrap:wrap;min-width:0}
/* colour: swatches, a radio group; the one picked has a grey ring */
.abx-sw{display:flex;gap:var(--dm-s-2)}
.abx-sw button{width:20px;height:20px;border-radius:calc(6px * var(--dm-rk));background:var(--dm-s);box-shadow:inset 0 0 0 1px rgba(0,0,0,.08);transition:transform .15s var(--dm-ease),box-shadow var(--dm-t-fast)}
@media (hover:hover) and (pointer:fine){.abx-sw button:hover{transform:scale(1.08)}}
.abx-sw button[aria-checked="true"]{box-shadow:0 0 0 2px var(--dm-surface),0 0 0 3px color-mix(in srgb,var(--dm-pick-on) 70%,transparent)}
.abx-sw button:focus-visible{outline-offset:4px;border-radius:calc(6px * var(--dm-rk))}
/* shape: a segmented switch of outlines, a radio group */
.abx-shp button{width:30px;height:24px;padding:0;display:grid;place-items:center}
.abx-shp button[aria-checked="true"]{color:var(--dm-fg)}
.abx-shp svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.5;overflow:visible}
.abx-shp svg path{vector-effect:non-scaling-stroke}
:root:not([data-style="soft"]) .abx-sw button:not(:where([data-style="soft"],[data-style="soft"] *)),:root:not([data-style="soft"]) .abx-sw button:focus-visible:not(:where([data-style="soft"],[data-style="soft"] *)){border-radius:0}
/* Dotto, the agent's mascot: a white screen (owner, 4 Nov: dark on a dark bezel was hard to see) in a bezel of the agent's colour (--dm-m, the accent unless one is picked), a 5 by 5
grid of dots drawing its eyes and smile. Idle blinks the top row of dots; thinking moves the eyes cell by cell and runs a
loading row. With reduced motion each state is a still pose */
.abx-mascot{--dm-m-c:var(--dm-m,var(--dm-accent));--dm-m-ink:color-mix(in oklab,var(--dm-m-c) 18%,#0a0a10);--dm-m-glow:color-mix(in oklab,var(--dm-m-c) 82%,white);display:block;flex:none;width:24px;height:24px;overflow:visible}
.abx-mascot .bz{fill:var(--dm-m-c);transition:fill var(--dm-t-fast)}
.abx-mascot .sc{fill:#fff;transition:fill var(--dm-t-fast)}
.abx-mascot .dg{fill:var(--dm-m-c);opacity:.22}
/* the lit dots are the agent's colour taken darker, so they read on the white screen in every colour, lime included */
.abx-mascot .on{fill:color-mix(in oklab,var(--dm-m-c) 72%,#0a0a10)}
/* in dark mode the screen is black and the lit dots glow in the agent's colour */
:root[data-theme="dark"] .abx-mascot .sc{fill:#0b0b0e}
:root[data-theme="dark"] .abx-mascot .on{fill:var(--dm-m-glow)}
:root[data-theme="dark"] .abx-mascot .dg{opacity:.3}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .abx-mascot .sc{fill:#0b0b0e}:root:not([data-theme="light"]) .abx-mascot .on{fill:var(--dm-m-glow)}:root:not([data-theme="light"]) .abx-mascot .dg{opacity:.3}}
.abx-mascot .th,.abx-mascot[data-state="thinking"] .id{display:none}
.abx-mascot[data-state="thinking"] .th{display:inline}
.abx-mascot[data-state="thinking"] .lk{transform:translate(0,-6.5px)}
@media (prefers-reduced-motion:no-preference){
.abx-mascot .tp{animation:abxblink 5.2s step-end infinite}
.abx-mascot[data-state="thinking"] .lk{animation:abxlook 2.7s step-end infinite}
.abx-mascot[data-state="thinking"] .ld{animation:abxload .96s step-end infinite}
.abx-mascot[data-state="thinking"] .l2{animation-delay:-.64s}
.abx-mascot[data-state="thinking"] .l3{animation-delay:-.32s}
}
@keyframes abxblink{0%{opacity:1}90%{opacity:0}94%{opacity:1}}
@keyframes abxlook{0%{transform:translate(0,-6.5px)}33.3%{transform:translate(6.5px,-6.5px)}66.6%{transform:translate(-6.5px,-6.5px)}}
@keyframes abxload{0%{opacity:1}33.3%{opacity:.18}}
.abx-err{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dm-bad)}
.abx-mr{display:flex;gap:var(--dm-s-2);flex-wrap:wrap}
.abx-mr .abx-sel{flex:1;min-width:120px}
.abx-eff button,.abx-trs button,.abx-tl .seg2 button{font-size:12px;height:28px;padding:0 10px}
.abx-tools{display:flex;flex-direction:column;gap:2px}
.abx-tl{display:flex;align-items:center;gap:var(--dm-s-2);padding:2px 0;font-size:13px}
.abx-tl .brand{width:16px;height:16px}
.abx-tl .n{flex:1;min-width:0}
.abx-tl .seg2 button{height:24px;padding:0 9px;font-size:11.5px}
.abx-kn{display:flex;flex-wrap:wrap;gap:6px}
.abx-tr{display:flex;align-items:center;gap:var(--dm-s-2);flex-wrap:wrap}
.abx-trd{font-size:12px}
.abx-bar{display:flex;align-items:center;gap:6px;margin-top:var(--dm-s-1);padding-top:var(--dm-s-3);border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.abx-st{font-size:12.5px;display:flex;align-items:center;gap:6px}
.abx-c{display:flex;flex-direction:column;min-width:0;background:var(--dm-row);border-left:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.abx-ch{display:flex;align-items:center;gap:var(--dm-s-3);padding:var(--dm-s-4) var(--dm-s-4) var(--dm-s-2);font-size:13.5px;line-height:1.4}
.abx-ch .abx-mascot{width:32px;height:32px}
.abx-cht{display:flex;flex-direction:column;min-width:0}
.abx-ch b{font-weight:500}
.abx-ch .muted{font-size:12px}
.abx-log{flex:1;display:flex;flex-direction:column;gap:var(--dm-s-3);padding:var(--dm-s-2) var(--dm-s-4) var(--dm-s-4)}
.abx-empty{margin:auto 0;text-align:center;font-size:12.5px;line-height:1.5}
.abx-em{align-self:flex-end;max-width:92%;display:flex;flex-direction:column;gap:2px;padding:9px var(--dm-s-3);border-radius:var(--dm-r-card);background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-line) 50%,transparent);font-size:12.5px;line-height:1.5;color:var(--dm-fg2)}
.abx-em b{font-weight:500;color:var(--dm-fg);font-size:13px}
.abx-em .muted{font-size:11.5px}
.abx-rp{display:flex;flex-direction:column;gap:6px;font-size:13px;line-height:1.5}
.abx-rp .k{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dm-muted)}
.abx-rp .tags{display:flex;gap:4px;flex-wrap:wrap}
.abx-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@container (max-width:600px){.abx-g{grid-template-columns:minmax(0,1fr)}.abx-c{border-left:0;border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent);min-height:220px}}
/* on a phone the state takes its own line over the buttons, so Live with its avatar never pushes Publish out of the card */
@container (max-width:340px){.abx-bar{flex-wrap:wrap;row-gap:var(--dm-s-2)}.abx-st{flex-basis:100%}}
agent-builder.jsHTML · 127 lines
DM.def('agent-builder', (r, c) => {
r.innerHTML = r.innerHTML.replace(/\{(spark|alert|play)\}/g, (m, k) => I(k, 'xs'));
const $r = s => $(s, r);
const root = $r('.abx'), desc = $r('#abx-desc'), go = $r('.abx-go'), name = $r('.abx-name'), purp = $r('.abx-purp'), ins = $r('.abx-ta'), sel = $r('.abx-sel'), err = $r('.abx-err');
const tools = $r('.abx-tools'), kn = $r('.abx-kn'), trd = $r('.abx-trd'), st = $r('.abx-st'), test = $r('.abx-test'), save = $r('.abx-save'), pub = $r('.abx-pub'), log = $r('.abx-log'), cs = $r('.abx-cs'), sr = $r('.abx-sr');
const S = k => $(`.abx-s[data-s="${k}"]`, r);
// Dotto, the agent's avatar: a dark LED screen in a bezel of the agent's colour, a 5 by 5 grid of dots drawing its eyes and
// smile. Each one carries only the outline in use; its colour is --m on the builder, so every Dotto in it changes at once
const SHAPES = { circle:['M4 32A28 28 0 1 0 60 32A28 28 0 1 0 4 32Z', 'M10.5 32A21.5 21.5 0 1 0 53.5 32A21.5 21.5 0 1 0 10.5 32Z'], squircle:['M60 32C60 58.5 58.5 60 32 60S4 58.5 4 32 5.5 4 32 4 60 5.5 60 32Z', 'M53.5 32C53.5 52.3 52.3 53.5 32 53.5S10.5 52.3 10.5 32 11.7 10.5 32 10.5 53.5 11.7 53.5 32Z'] };
const SHAPE_N = { circle:'Circle', squircle:'Squircle' };
const COLS = [['accent', 'Accent', 'var(--dm-accent)'], ['blue', 'Blue', '#116ff8'], ['violet', 'Violet', '#7c4dff'], ['orange', 'Orange', '#f76700'], ['green', 'Green', '#00a876']];
// it starts as an accent squircle (owner, 4 Nov: squircle and circle only)
const look = { color:'accent', shape:'squircle' };
let mood = 'idle';
const dots = (cls, cells) => cells.map(([x, y]) => `<circle${cls ? ` class="${cls}"` : ''} cx="${19 + x * 6.5}" cy="${19 + y * 6.5}" r="${cls ? 2.75 : 1.3}"/>`).join('');
const GRID = [...Array(25)].map((_, i) => [i % 5, i / 5 | 0]);
const mascot = () => { const [bz, sc] = SHAPES[look.shape];
return `<svg class="abx-mascot" viewBox="0 0 64 64" data-shape="${look.shape}" data-state="${mood}" aria-hidden="true" focusable="false"><path class="bz" d="${bz}"/><path class="sc" d="${sc}"/>`
+ `<g class="dg">${dots('', GRID)}</g><g class="lk">${dots('on tp', [[1, 1], [3, 1]])}${dots('on', [[1, 2], [3, 2]])}</g>`
+ `<g class="id">${dots('on', [[0, 3], [4, 3], [1, 4], [2, 4], [3, 4]])}</g><g class="th">${dots('on ld l1', [[1, 4]])}${dots('on ld l2', [[2, 4]])}${dots('on ld l3', [[3, 4]])}</g></svg>`; };
const av = $r('.abx-av'), sw = $r('.abx-sw'), shp = $r('.abx-shp');
av.innerHTML = mascot(); av.setAttribute('role', 'img');
$r('.abx-ch').insertAdjacentHTML('afterbegin', mascot());
// a change of colour, shape or mood reaches every Dotto in the builder: the avatar, the test chat's header and replies, Live
function paint(){
root.style.setProperty('--dm-m', COLS.find(x => x[0] === look.color)[2]);
const [bz, sc] = SHAPES[look.shape];
$$('.abx-mascot', r).forEach(m => { m.dataset.shape = look.shape; m.dataset.state = mood; $('.bz', m).setAttribute('d', bz); $('.sc', m).setAttribute('d', sc); });
av.setAttribute('aria-label', `Avatar: ${COLS.find(x => x[0] === look.color)[1]}, ${SHAPE_N[look.shape].toLowerCase()}`);
}
// the pickers are radio groups: a click picks, the arrow keys move and pick (and wrap), Home and End go to the ends, and only
// the picked one is in the tab order
function radios(g, on){
const all = () => $$('[role="radio"]', g), shown = () => all().filter(b => !b.hidden);
const pick = (v, by) => { const t = all().find(b => b.dataset.v === v); if (!t || t.hidden) v = shown()[0].dataset.v;
all().forEach(b => { const y = b.dataset.v === v; b.setAttribute('aria-checked', String(y)); b.tabIndex = y ? 0 : -1; }); on(v, by); };
c.on(g, 'click', e => { const b = e.target.closest('[role="radio"]'); if (b) pick(b.dataset.v, true); });
c.on(g, 'keydown', e => { const l = shown(), i = l.indexOf(document.activeElement), d = { ArrowRight:1, ArrowDown:1, ArrowLeft:-1, ArrowUp:-1 }[e.key];
const to = d ? l[(i + d + l.length) % l.length] : e.key === 'Home' ? l[0] : e.key === 'End' ? l[l.length - 1] : null;
if (to){ e.preventDefault(); to.focus(); pick(to.dataset.v, true); } });
return v => pick(v, false);
}
sw.innerHTML = COLS.map(([v, n, x]) => `<button role="radio" aria-checked="false" tabindex="-1" aria-label="${n}" data-tip="${n}" data-v="${v}" style="--dm-s:${x}"></button>`).join('');
// a colour the accent already is shows once, as the accent
const ink = v => { const t = document.createElement('i'); t.hidden = true; t.style.color = v; root.appendChild(t); const x = getComputedStyle(t).color; t.remove(); return x; };
const acc = ink('var(--dm-accent)');
COLS.slice(1).forEach(([v, , x]) => { if (ink(x) === acc) $(`[data-v="${v}"]`, sw).hidden = true; });
shp.innerHTML = Object.entries(SHAPE_N).map(([v, n]) => `<button role="radio" aria-checked="false" tabindex="-1" aria-label="${n}" data-tip="${n}" data-v="${v}"><svg viewBox="0 0 64 64" aria-hidden="true" focusable="false"><path d="${SHAPES[v][0]}"/></svg></button>`).join('');
// the shape switch slides its knob like the other switches; its first placing is instant
const knob = document.createElement('span'); knob.className = 'knob'; shp.prepend(knob);
const place = () => { const b = $('[aria-checked="true"]', shp); if (b){ knob.style.left = b.offsetLeft + 'px'; knob.style.width = b.offsetWidth + 'px'; } };
const setColor = radios(sw, (v, by) => { look.color = v; paint(); if (by) edited('id'); });
const setShape = radios(shp, (v, by) => { look.shape = v; place(); paint(); if (by) edited('id'); });
setColor(look.color); setShape(look.shape);
requestAnimationFrame(() => { knob.style.transition = 'none'; place(); void knob.offsetWidth; knob.style.transition = ''; });
new ResizeObserver(place).observe(shp);
const think = on => { mood = on ? 'thinking' : 'idle'; paint(); };
// tools, each with what the agent may do there: read, ask first, or act
const T = [['gmail', 'Gmail'], ['linear', 'Linear'], ['slack', 'Slack']];
tools.innerHTML = T.map(([k, n]) => `<div class="abx-tl" data-t="${k}">${BRAND[k]}<span class="n">${n}</span><div class="seg2" role="group" aria-label="${n}: what it may do"><button aria-pressed="true" data-v="read">Read</button><button aria-pressed="false" data-v="ask">Ask</button><button aria-pressed="false" data-v="act">Act</button></div></div>`).join('');
const segs = {}; $$('.abx-tl', tools).forEach(t => segs[t.dataset.t] = seg($('.seg2', t), () => edited('tools')));
const setSeg = (el, v) => { const b = $(`button[data-v="${v}"]`, el); $$('button', el).forEach(x => x.setAttribute('aria-pressed', x === b)); };
const TRIG = { chat:'When someone messages it', sched:'Every weekday at 9:00', event:'When an email arrives at support@' };
seg($r('.abx-trs'), v => { swap(trd, TRIG[v]); edited('trig'); });
seg($r('.abx-eff'), () => edited('model'));
kn.innerHTML = '<span class="muted" style="font-size:12px">Nothing yet</span>';
// what the person has changed since it was drafted: the Drafted tag goes, and the form has unsaved changes
const tag = k => $('.abx-dt', S(k));
let state = 'empty';
const status = (t, cls) => { st.innerHTML = t; st.className = 'abx-st ' + (cls || 'muted'); };
function edited(k){ const t = tag(k); if (t) t.remove(); if (state !== 'empty'){ state = 'dirty'; status('Unsaved changes'); } valid(); }
[[name, 'id'], [purp, 'id'], [ins, 'ins'], [sel, 'model']].forEach(([el, k]) => c.on(el, 'input', () => edited(k)));
c.on(sel, 'change', () => edited('model'));
// a name is needed to save or publish
err.id = 'abx-err-' + Math.random().toString(36).slice(2, 8); name.setAttribute('aria-describedby', err.id);
function valid(){ const ok = !!name.value.trim() || state === 'empty'; err.hidden = ok; name.setAttribute('aria-invalid', String(!ok)); save.disabled = !ok || state === 'empty' || state === 'saved' || state === 'live'; pub.disabled = !ok || state === 'empty' || state === 'live'; return ok; }
valid(); test.disabled = true;
// drafting: the agent fills each section in turn from the one sentence, and marks it Drafted
const D = {
// it names the agent, then picks its colour and its shape
id:async () => { name.value = 'Support triage'; purp.value = 'Sorts incoming support emails and routes them to the right team'; await c.wait(350); setColor('violet'); await c.wait(350); setShape('circle'); },
ins:() => { ins.value = 'Read each new email. Tag it Billing, Bug or How-to, set its priority, and route it to that team. Draft a reply for billing refunds but never send money without asking.'; },
model:() => { sel.value = 'Sonnet 5'; setSeg($r('.abx-eff'), 'med'); },
tools:() => { setSeg($('.seg2', $('[data-t="gmail"]', tools)), 'act'); setSeg($('.seg2', $('[data-t="linear"]', tools)), 'act'); setSeg($('.seg2', $('[data-t="slack"]', tools)), 'ask'); },
know:() => { kn.innerHTML = `<span class="tag">${I('file-text','xs')}Refund policy.pdf</span><span class="tag">${I('globe','xs')}Help centre</span><span class="tag">${I('table','xs')}Team routing.csv</span>`; },
trig:() => { $$('.abx-trs button', r).forEach(b => b.setAttribute('aria-pressed', b.dataset.v === 'event')); $r('.abx-trs').dispatchEvent(new Event('resize')); swap(trd, TRIG.event); }
};
let drafting = false;
go.onclick = () => c.run(async () => {
if (drafting) return; if (!desc.value.trim()){ desc.focus(); return; }
drafting = true; go.disabled = true; go.innerHTML = '<span class="spin"></span>Drafting'; sr.textContent = 'Drafting the agent';
const keys = Object.keys(D); keys.forEach(k => S(k).classList.add('wait'));
for (const k of keys){ const s = S(k); s.classList.remove('wait'); s.classList.add('busy'); await c.wait(550); await D[k](); s.classList.remove('busy');
if (!tag(k)){ const t = html(`<span class="abx-dt">${I('spark','xs')}Drafted</span>`); $('.abx-sh', s).insertBefore(t, $('.abx-sh .muted', s)); enter(t); } }
// the trigger switch was redrawn from code, so its knob is placed again
const tk = $('.abx-trs .knob', r), tb = $('.abx-trs [aria-pressed="true"]', r); if (tk && tb){ tk.style.left = tb.offsetLeft + 'px'; tk.style.width = tb.offsetWidth + 'px'; }
$$('.abx-tl .seg2', tools).forEach(g => { const k = $('.knob', g), b = $('[aria-pressed="true"]', g); if (k && b){ k.style.left = b.offsetLeft + 'px'; k.style.width = b.offsetWidth + 'px'; } });
const ek = $('.abx-eff .knob', r), eb = $('.abx-eff [aria-pressed="true"]', r); if (ek && eb){ ek.style.left = eb.offsetLeft + 'px'; ek.style.width = eb.offsetWidth + 'px'; }
state = 'dirty'; status('Drafted, not saved'); go.disabled = false; go.className = 'pill line sm abx-go'; go.innerHTML = `${I('refresh','xs')}Draft again`; test.disabled = false; drafting = false; valid(); sr.textContent = 'Drafted. Check the fields, then test it';
});
// a test: a sample email goes in, and the agent says what it would do with it
test.onclick = () => c.run(async () => {
test.disabled = true; swap(cs, 'Testing with a sample email');
log.innerHTML = `<div class="abx-em"><b>I was charged twice for October</b><span>Hi, my card shows two charges of $48 on Oct 3. Can you refund one? Maya</span><span class="muted">[email protected] · sample</span></div>`; enter(log.firstElementChild);
await c.wait(700);
// Dotto thinks while the agent replies
think(true);
const rp = html(`<div class="abx-rp"><span class="k">${mascot()}<span class="shim">Support triage is reading it</span></span></div>`); log.appendChild(rp); enter(rp);
await c.wait(1300);
think(false);
rp.innerHTML = `<span class="k">${mascot()}Support triage</span><span class="tags"><span class="tag">Billing</span><span class="tag warn">High priority</span><span class="tag">Routed to Billing</span></span><span>Duplicate charge of $48 on Oct 3. I drafted a refund reply and will ask before sending it, as the instructions say.</span>`;
enter(rp); swap(cs, 'The test passed'); test.disabled = false; sr.textContent = 'Test done: Billing, high priority, routed to Billing';
});
save.onclick = () => { if (!valid()) { name.focus(); return; } state = 'saved'; status(`${I('check','xs')}Saved`); sr.textContent = 'Saved'; valid(); };
pub.onclick = () => c.run(async () => { if (!valid()) { name.focus(); return; } pub.innerHTML = '<span class="spin"></span>Publishing'; await c.wait(800); state = 'live'; status(`${mascot()}<span class="tag ok">Live</span>`, 'muted'); pub.textContent = 'Published'; valid(); sr.textContent = 'Published. The agent is live'; });
// the preview's story: one sentence, the fields fill in, one test email, save, publish
if (r === c.st) c.story(async s => {
await s.beat(2);
await s.point(desc, false); await s.type(desc, 'Triage support emails', { n:.12 }); await s.beat(1);
await s.tap(go); s.away();
await s.until(() => !drafting && state === 'dirty'); await s.beat(3);
await s.tap(test); s.away(); await s.until(() => $('.abx-rp .tags', r)); await s.beat(4);
await s.tap(save); await s.beat(2);
await s.tap(pub); s.away(); await s.until(() => state === 'live'); await s.beat(6);
});
});
Questions
What people ask about it.
- Does it draft the fields itself?
- No.
onDraftis yours: ask your model for the fields, then setdraftingto the section in hand and updatevalueanddraftedas each one arrives. - How do I know which section someone changed?
onChangegets the new value and the section it came from (id,ins,model,tools,knowortrig), so you can drop that section fromdrafted.- What do Read, Ask and Act mean?
- They are the permission the agent has in that tool: Read only looks, Ask asks a person before changing anything, and Act may change things on its own. The component shows the choice; enforcing it is up to your agent.
- Can I use the avatar on its own?
- In React, yes:
AgentMascotcomes from the same file asAgentBuilder, withcolor(any CSS colour, the accent if left out),shape(squircleorcircle),state(idleorthinking),sizein px and an optionallabel. InAgentDraft,colornames one of the builder'scolorsandshapemay be left out for the style's own. In the HTML and Astro versions the builder's script draws it.
More in Workspace
Navigation, search, settings, models and automations around the agent.