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

Viewing files at commit 8135358a54bcCommit detailsView latest

apps/dashboard/src/components/first-run-guide.tsx 251 lines · 8.2 KB · tsx
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
}