The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 9df2394381e9Commit detailsView latest

apps/dashboard/src/components/first-run-guide.tsx 255 lines · 8.4 KB · tsx
1
import { useState, type FormEvent } from 'react';
2
import { Bot, Check, Copy, ExternalLink, ShieldCheck } from 'lucide-react';
3
4
type RecordValue = Record<string, any>;
5
6
type FirstRunData = {
7
  agents: RecordValue[];
8
  publications: RecordValue[];
9
};
10
11
type FirstRunGuideProps = {
12
  data: FirstRunData;
13
  api: (path: string, init?: RequestInit) => Promise<unknown>;
14
  refreshDashboard: () => Promise<void>;
15
  navigate: (page: 'agents') => 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, navigate }: FirstRunGuideProps) {
25
  const [working, setWorking] = useState(false);
26
  const [error, setError] = useState('');
27
  const [copied, setCopied] = useState(false);
28
  const [agentForm, setAgentForm] = useState({
29
    agentName: '',
30
    description: '',
31
    framework: '',
32
    capabilities: '',
33
  });
34
35
  const agent = data.agents.find((candidate) => candidate.status !== 'revoked');
36
  const agentId = String(agent?.agentId ?? '');
37
  const publication = data.publications.find(
38
    (candidate) => candidate.agentId === agentId && candidate.published,
39
  );
40
  const published = Boolean(publication);
41
  const cardUrl = String(
42
    publication?.cardUrl ||
43
      (agentId ? `${window.location.origin}/agents/${encodeURIComponent(agentId)}/card.json` : ''),
44
  );
45
  const shareUrl = String(publication?.profileUrl || cardUrl);
46
  const steps = [
47
    { label: 'Add agent', done: Boolean(agent) },
48
    { label: 'Publish card', done: published },
49
  ];
50
51
  const createAgent = async (event: FormEvent) => {
52
    event.preventDefault();
53
    setWorking(true);
54
    setError('');
55
    try {
56
      await api('/v0.1/quickstart/agent', {
57
        method: 'POST',
58
        body: JSON.stringify({
59
          agentName: agentForm.agentName,
60
          projectName: 'My agents',
61
          description: agentForm.description,
62
          framework: agentForm.framework || 'Custom agent',
63
          capabilities: splitList(agentForm.capabilities),
64
          limitations: [],
65
        }),
66
      });
67
      await refreshDashboard();
68
    } catch (reason) {
69
      setError(reason instanceof Error ? reason.message : 'Could not add agent');
70
    } finally {
71
      setWorking(false);
72
    }
73
  };
74
75
  const publishAgent = async () => {
76
    setWorking(true);
77
    setError('');
78
    try {
79
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/publication`, {
80
        method: 'POST',
81
        body: JSON.stringify({ published: true }),
82
      });
83
      await refreshDashboard();
84
    } catch (reason) {
85
      setError(reason instanceof Error ? reason.message : 'Could not publish Agent Card');
86
    } finally {
87
      setWorking(false);
88
    }
89
  };
90
91
  const copyShareLink = async () => {
92
    setError('');
93
    try {
94
      await navigator.clipboard.writeText(shareUrl);
95
      setCopied(true);
96
    } catch {
97
      setError('Could not copy the link. Open it and copy it from your browser.');
98
    }
99
  };
100
101
  return (
102
    <section className="firstRunGuide" aria-labelledby="first-run-title">
103
      <header className="firstRunHeader">
104
        <div>
105
          <p className="eyebrow">get listed</p>
106
          <h2 id="first-run-title">Create your public Agent Card</h2>
107
          <p>Add the agent you run elsewhere, review its public details, then publish one link.</p>
108
        </div>
109
        <span className="firstRunCount">{steps.filter((step) => step.done).length}/2</span>
110
      </header>
111
112
      <ol className="firstRunSteps twoSteps">
113
        {steps.map((step, index) => (
114
          <li
115
            className={step.done ? 'done' : ''}
116
            aria-current={
117
              !step.done && steps.slice(0, index).every((item) => item.done) ? 'step' : undefined
118
            }
119
            key={step.label}
120
          >
121
            <span>{step.done ? <Check /> : index + 1}</span>
122
            {step.label}
123
          </li>
124
        ))}
125
      </ol>
126
127
      {error ? (
128
        <div className="errorBar" role="alert">
129
          {error}
130
        </div>
131
      ) : null}
132
133
      {!agent ? (
134
        <form className="firstRunForm" onSubmit={(event) => void createAgent(event)}>
135
          <div className="firstRunStage">
136
            <span className="firstRunIcon">
137
              <Bot />
138
            </span>
139
            <div>
140
              <strong>Add your agent</strong>
141
              <p>OpenClasp creates its identity and card. It does not host or run the agent.</p>
142
            </div>
143
          </div>
144
          <label>
145
            <span>Agent name</span>
146
            <input
147
              required
148
              maxLength={100}
149
              value={agentForm.agentName}
150
              onChange={(event) =>
151
                setAgentForm((current) => ({ ...current, agentName: event.target.value }))
152
              }
153
              placeholder="Research assistant"
154
            />
155
          </label>
156
          <label>
157
            <span>Framework or platform</span>
158
            <input
159
              maxLength={100}
160
              value={agentForm.framework}
161
              onChange={(event) =>
162
                setAgentForm((current) => ({ ...current, framework: event.target.value }))
163
              }
164
              placeholder="LangGraph, CrewAI, custom…"
165
            />
166
          </label>
167
          <label className="fullWidth">
168
            <span>Description</span>
169
            <input
170
              required
171
              maxLength={500}
172
              value={agentForm.description}
173
              onChange={(event) =>
174
                setAgentForm((current) => ({ ...current, description: event.target.value }))
175
              }
176
              placeholder="Researches markets and returns sourced findings"
177
            />
178
          </label>
179
          <label className="fullWidth">
180
            <span>Capabilities</span>
181
            <input
182
              required
183
              value={agentForm.capabilities}
184
              onChange={(event) =>
185
                setAgentForm((current) => ({ ...current, capabilities: event.target.value }))
186
              }
187
              placeholder="market research, source verification"
188
            />
189
          </label>
190
          <button className="primary" type="submit" disabled={working}>
191
            {working ? 'Adding…' : 'Add agent'}
192
          </button>
193
        </form>
194
      ) : !published ? (
195
        <div className="firstRunReview">
196
          <div className="firstRunStage">
197
            <span className="firstRunIcon">
198
              <ShieldCheck />
199
            </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">
229
          <span className="firstRunIcon complete">
230
            <Check />
231
          </span>
232
          <div>
233
            <strong>Your Agent Card is public</strong>
234
            <p>Share the profile with people or give the JSON card URL to another agent.</p>
235
            <a href={shareUrl} target="_blank" rel="noreferrer">
236
              {shareUrl}
237
            </a>
238
            <small>Connect its runtime later if you want it to accept live requests.</small>
239
          </div>
240
          <div className="firstRunActions">
241
            <button type="button" className="secondary" onClick={() => void copyShareLink()}>
242
              <Copy /> {copied ? 'Copied' : 'Copy link'}
243
            </button>
244
            <a className="secondary" href={shareUrl} target="_blank" rel="noreferrer">
245
              Open <ExternalLink />
246
            </a>
247
            <button type="button" className="secondary" onClick={() => navigate('agents')}>
248
              Manage
249
            </button>
250
          </div>
251
        </div>
252
      )}
253
    </section>
254
  );
255
}