| 1 | import { useState, type FormEvent } from 'react';
|
| 2 | import { Check, Copy, ExternalLink } from 'lucide-react';
|
| 3 | import { FrameCorners } from '@/components/agent-mark';
|
| 4 |
|
| 5 | type RecordValue = Record<string, any>;
|
| 6 |
|
| 7 | type FirstRunData = {
|
| 8 | agents: RecordValue[];
|
| 9 | publications: RecordValue[];
|
| 10 | };
|
| 11 |
|
| 12 | type FirstRunGuideProps = {
|
| 13 | data: FirstRunData;
|
| 14 | api: (path: string, init?: RequestInit) => Promise<unknown>;
|
| 15 | refreshDashboard: () => Promise<void>;
|
| 16 | };
|
| 17 |
|
| 18 | const splitList = (value: string) =>
|
| 19 | value
|
| 20 | .split(/[\n,]/)
|
| 21 | .map((item) => item.trim())
|
| 22 | .filter(Boolean);
|
| 23 |
|
| 24 | export function FirstRunGuide({ data, api, refreshDashboard }: FirstRunGuideProps) {
|
| 25 | const [working, setWorking] = useState(false);
|
| 26 | const [error, setError] = useState('');
|
| 27 | const [copied, setCopied] = useState(false);
|
| 28 | const [justPublished, setJustPublished] = useState(false);
|
| 29 | const [agentForm, setAgentForm] = useState({
|
| 30 | agentName: '',
|
| 31 | description: '',
|
| 32 | framework: '',
|
| 33 | capabilities: '',
|
| 34 | });
|
| 35 |
|
| 36 | const agent = data.agents.find((candidate) => candidate.status !== 'revoked');
|
| 37 | const agentId = String(agent?.agentId ?? '');
|
| 38 | const publication = data.publications.find(
|
| 39 | (candidate) => candidate.agentId === agentId && candidate.published,
|
| 40 | );
|
| 41 | const published = Boolean(publication);
|
| 42 | const cardUrl = String(
|
| 43 | publication?.cardUrl ||
|
| 44 | (agentId ? `${window.location.origin}/agents/${encodeURIComponent(agentId)}/card.json` : ''),
|
| 45 | );
|
| 46 | const shareUrl = String(publication?.profileUrl || cardUrl);
|
| 47 | const steps = [
|
| 48 | { label: 'Add agent', done: Boolean(agent) },
|
| 49 | { label: 'Publish card', done: published },
|
| 50 | ];
|
| 51 |
|
| 52 | const createAgent = async (event: FormEvent) => {
|
| 53 | event.preventDefault();
|
| 54 | setWorking(true);
|
| 55 | setError('');
|
| 56 | try {
|
| 57 | await api('/v0.1/quickstart/agent', {
|
| 58 | method: 'POST',
|
| 59 | body: JSON.stringify({
|
| 60 | agentName: agentForm.agentName,
|
| 61 | projectName: 'My agents',
|
| 62 | description: agentForm.description,
|
| 63 | framework: agentForm.framework || 'Custom agent',
|
| 64 | capabilities: splitList(agentForm.capabilities),
|
| 65 | limitations: [],
|
| 66 | }),
|
| 67 | });
|
| 68 | await refreshDashboard();
|
| 69 | } catch (reason) {
|
| 70 | setError(reason instanceof Error ? reason.message : 'Could not add agent');
|
| 71 | } finally {
|
| 72 | setWorking(false);
|
| 73 | }
|
| 74 | };
|
| 75 |
|
| 76 | const publishAgent = async () => {
|
| 77 | setWorking(true);
|
| 78 | setError('');
|
| 79 | try {
|
| 80 | await api(`/v0.1/agents/${encodeURIComponent(agentId)}/publication`, {
|
| 81 | method: 'POST',
|
| 82 | body: JSON.stringify({ published: true }),
|
| 83 | });
|
| 84 | setJustPublished(true);
|
| 85 | await refreshDashboard();
|
| 86 | } catch (reason) {
|
| 87 | setError(reason instanceof Error ? reason.message : 'Could not publish Agent Card');
|
| 88 | } finally {
|
| 89 | setWorking(false);
|
| 90 | }
|
| 91 | };
|
| 92 |
|
| 93 | const copyShareLink = async () => {
|
| 94 | setError('');
|
| 95 | try {
|
| 96 | await navigator.clipboard.writeText(shareUrl);
|
| 97 | setCopied(true);
|
| 98 | } catch {
|
| 99 | setError('Could not copy the link. Open it and copy it from your browser.');
|
| 100 | }
|
| 101 | };
|
| 102 |
|
| 103 | if (agent && published && !justPublished) return null;
|
| 104 |
|
| 105 | return (
|
| 106 | <section className="firstRunGuide" aria-labelledby="first-run-title">
|
| 107 | <FrameCorners />
|
| 108 | <header className="firstRunHeader">
|
| 109 | <div>
|
| 110 | <h2 id="first-run-title">Public Agent Card</h2>
|
| 111 | <p>Add the agent you run elsewhere, review its public details, then publish one link.</p>
|
| 112 | </div>
|
| 113 | <span className="firstRunCount">{steps.filter((step) => step.done).length}/2</span>
|
| 114 | </header>
|
| 115 |
|
| 116 | <ol className="firstRunSteps twoSteps">
|
| 117 | {steps.map((step, index) => (
|
| 118 | <li
|
| 119 | className={step.done ? 'done' : ''}
|
| 120 | aria-current={
|
| 121 | !step.done && steps.slice(0, index).every((item) => item.done) ? 'step' : undefined
|
| 122 | }
|
| 123 | key={step.label}
|
| 124 | >
|
| 125 | <span>{step.done ? <Check /> : index + 1}</span>
|
| 126 | {step.label}
|
| 127 | </li>
|
| 128 | ))}
|
| 129 | </ol>
|
| 130 |
|
| 131 | {error ? (
|
| 132 | <div className="errorBar" role="alert">
|
| 133 | {error}
|
| 134 | </div>
|
| 135 | ) : null}
|
| 136 |
|
| 137 | {!agent ? (
|
| 138 | <form className="firstRunForm" onSubmit={(event) => void createAgent(event)}>
|
| 139 | <div className="firstRunStage">
|
| 140 | <span className="firstRunIcon">01</span>
|
| 141 | <div>
|
| 142 | <strong>Add your agent</strong>
|
| 143 | <p>OpenClasp creates its identity and card. It does not host or run the agent.</p>
|
| 144 | </div>
|
| 145 | </div>
|
| 146 | <label>
|
| 147 | <span>Agent name</span>
|
| 148 | <input
|
| 149 | required
|
| 150 | maxLength={100}
|
| 151 | value={agentForm.agentName}
|
| 152 | onChange={(event) =>
|
| 153 | setAgentForm((current) => ({ ...current, agentName: event.target.value }))
|
| 154 | }
|
| 155 | placeholder="Research assistant"
|
| 156 | />
|
| 157 | </label>
|
| 158 | <label>
|
| 159 | <span>Framework or platform</span>
|
| 160 | <input
|
| 161 | maxLength={100}
|
| 162 | value={agentForm.framework}
|
| 163 | onChange={(event) =>
|
| 164 | setAgentForm((current) => ({ ...current, framework: event.target.value }))
|
| 165 | }
|
| 166 | placeholder="LangGraph, CrewAI, custom…"
|
| 167 | />
|
| 168 | </label>
|
| 169 | <label className="fullWidth">
|
| 170 | <span>Description</span>
|
| 171 | <input
|
| 172 | required
|
| 173 | maxLength={500}
|
| 174 | value={agentForm.description}
|
| 175 | onChange={(event) =>
|
| 176 | setAgentForm((current) => ({ ...current, description: event.target.value }))
|
| 177 | }
|
| 178 | placeholder="Researches markets and returns sourced findings"
|
| 179 | />
|
| 180 | </label>
|
| 181 | <label className="fullWidth">
|
| 182 | <span>Capabilities</span>
|
| 183 | <input
|
| 184 | required
|
| 185 | value={agentForm.capabilities}
|
| 186 | onChange={(event) =>
|
| 187 | setAgentForm((current) => ({ ...current, capabilities: event.target.value }))
|
| 188 | }
|
| 189 | placeholder="market research, source verification"
|
| 190 | />
|
| 191 | </label>
|
| 192 | <button className="primary" type="submit" disabled={working}>
|
| 193 | {working ? 'Adding…' : 'Add agent'}
|
| 194 | </button>
|
| 195 | </form>
|
| 196 | ) : !published ? (
|
| 197 | <div className="firstRunReview">
|
| 198 | <div className="firstRunStage">
|
| 199 | <span className="firstRunIcon">02</span>
|
| 200 | <div>
|
| 201 | <strong>Review what becomes public</strong>
|
| 202 | <p>Publishing makes this profile and its machine-readable Agent Card public.</p>
|
| 203 | </div>
|
| 204 | </div>
|
| 205 | <article className="agentCardPreview">
|
| 206 | <span className="verifiedPublisher">✓ Publisher verified</span>
|
| 207 | <h3>{String(agent.name)}</h3>
|
| 208 | <p>{String(agent.description)}</p>
|
| 209 | <dl>
|
| 210 | <div>
|
| 211 | <dt>Framework</dt>
|
| 212 | <dd>{String(agent.framework)}</dd>
|
| 213 | </div>
|
| 214 | <div>
|
| 215 | <dt>Capabilities</dt>
|
| 216 | <dd>{(agent.capabilities as string[]).join(', ')}</dd>
|
| 217 | </div>
|
| 218 | </dl>
|
| 219 | <small>
|
| 220 | OpenClasp verifies control of the publishing account. Capabilities are self-declared.
|
| 221 | </small>
|
| 222 | </article>
|
| 223 | <button className="primary" type="button" disabled={working} onClick={publishAgent}>
|
| 224 | {working ? 'Publishing…' : 'Publish Agent Card'}
|
| 225 | </button>
|
| 226 | </div>
|
| 227 | ) : (
|
| 228 | <div className="firstRunComplete" role="status">
|
| 229 | <span className="firstRunIcon complete">
|
| 230 | <Check />
|
| 231 | </span>
|
| 232 | <div>
|
| 233 | <strong>Your Agent Card is public</strong>
|
| 234 | <p>Share this public profile with people or other agents.</p>
|
| 235 | <a href={shareUrl} target="_blank" rel="noreferrer">
|
| 236 | {shareUrl}
|
| 237 | </a>
|
| 238 | </div>
|
| 239 | <div className="firstRunActions">
|
| 240 | <button type="button" className="secondary" onClick={() => void copyShareLink()}>
|
| 241 | <Copy /> {copied ? 'Copied' : 'Copy link'}
|
| 242 | </button>
|
| 243 | <a className="secondary" href={shareUrl} target="_blank" rel="noreferrer">
|
| 244 | Open <ExternalLink />
|
| 245 | </a>
|
| 246 | </div>
|
| 247 | </div>
|
| 248 | )}
|
| 249 | </section>
|
| 250 | );
|
| 251 | }
|