| 1 | import { useEffect, useState, type FormEvent } from 'react';
|
| 2 | import { ArrowRight, Bot, Check, Circle, MessageCircle, ShieldCheck } from 'lucide-react';
|
| 3 |
|
| 4 | type RecordValue = Record<string, any>;
|
| 5 |
|
| 6 | type FirstRunData = {
|
| 7 | agents: RecordValue[];
|
| 8 | publications: RecordValue[];
|
| 9 | federatedInteractions: RecordValue[];
|
| 10 | hostedThreads: RecordValue[];
|
| 11 | completionReports: RecordValue[];
|
| 12 | feedbackRequests: RecordValue[];
|
| 13 | interactionFeedback: RecordValue[];
|
| 14 | };
|
| 15 |
|
| 16 | type FirstRunGuideProps = {
|
| 17 | data: FirstRunData;
|
| 18 | api: (path: string, init?: RequestInit) => Promise<unknown>;
|
| 19 | refreshDashboard: () => Promise<void>;
|
| 20 | navigate: (page: 'agents' | 'conversations' | 'history') => void;
|
| 21 | };
|
| 22 |
|
| 23 | const splitList = (value: string) =>
|
| 24 | value
|
| 25 | .split(/[\n,]/)
|
| 26 | .map((item) => item.trim())
|
| 27 | .filter(Boolean);
|
| 28 |
|
| 29 | export function FirstRunGuide({ data, api, refreshDashboard, navigate }: FirstRunGuideProps) {
|
| 30 | const [working, setWorking] = useState('');
|
| 31 | const [error, setError] = useState('');
|
| 32 | const [targets, setTargets] = useState<RecordValue[]>([]);
|
| 33 | const [targetsLoading, setTargetsLoading] = useState(false);
|
| 34 | const [agentForm, setAgentForm] = useState({
|
| 35 | agentName: '',
|
| 36 | description: '',
|
| 37 | capabilities: '',
|
| 38 | });
|
| 39 | const [agreementForm, setAgreementForm] = useState({
|
| 40 | responderAgentId: '',
|
| 41 | task: '',
|
| 42 | requestedOutcome: '',
|
| 43 | successCriterion: '',
|
| 44 | deadline: '',
|
| 45 | });
|
| 46 | const [completionForm, setCompletionForm] = useState({
|
| 47 | outcome: 'success' as 'success' | 'partial' | 'failure' | 'cancelled',
|
| 48 | summary: '',
|
| 49 | evidenceReferences: '',
|
| 50 | });
|
| 51 | const [feedbackForm, setFeedbackForm] = useState({
|
| 52 | rating: 5,
|
| 53 | wouldWorkAgain: 'yes' as 'yes' | 'no' | 'unsure',
|
| 54 | privateComment: '',
|
| 55 | });
|
| 56 |
|
| 57 | const firstAgent = data.agents.find((agent) => agent.status !== 'revoked');
|
| 58 | const firstAgentId = String(firstAgent?.agentId ?? '');
|
| 59 | const firstAgentMode = String(firstAgent?.agentMode ?? '');
|
| 60 | const firstCapability = String(firstAgent?.capabilities?.[0] ?? '');
|
| 61 | const published = data.publications.some(
|
| 62 | (publication) => publication.agentId === firstAgentId && publication.published,
|
| 63 | );
|
| 64 | const interaction = data.federatedInteractions.find(
|
| 65 | (candidate) =>
|
| 66 | candidate.contract?.parties?.includes(firstAgentId) &&
|
| 67 | !['rejected', 'expired', 'cancelled'].includes(String(candidate.status)),
|
| 68 | );
|
| 69 | const interactionId = String(interaction?.interactionId ?? '');
|
| 70 | const completionReport = data.completionReports.find(
|
| 71 | (report) => report.interactionId === interactionId && report.reportingAgentId === firstAgentId,
|
| 72 | );
|
| 73 | const pendingFeedback = data.feedbackRequests.find(
|
| 74 | (request) =>
|
| 75 | request.interactionId === interactionId &&
|
| 76 | request.reviewerAgentId === firstAgentId &&
|
| 77 | request.status === 'pending',
|
| 78 | );
|
| 79 | const feedbackSubmitted = data.interactionFeedback.some(
|
| 80 | (feedback) =>
|
| 81 | feedback.interactionId === interactionId && feedback.reviewerAgentId === firstAgentId,
|
| 82 | );
|
| 83 | const finished = Boolean(interactionId && completionReport && feedbackSubmitted);
|
| 84 | const steps = [
|
| 85 | { label: 'Create agent', done: Boolean(firstAgent) },
|
| 86 | { label: 'Agree safeguards', done: Boolean(interaction) },
|
| 87 | { label: 'Record outcome', done: Boolean(completionReport) },
|
| 88 | { label: 'Give feedback', done: finished },
|
| 89 | ];
|
| 90 |
|
| 91 | useEffect(() => {
|
| 92 | if (!firstAgentId || !published || interaction) return;
|
| 93 | let cancelled = false;
|
| 94 | setTargetsLoading(true);
|
| 95 | const parameters = new URLSearchParams({ agentId: firstAgentId, limit: '20' });
|
| 96 | const taskCategory = firstCapability.trim();
|
| 97 | if (taskCategory) parameters.set('taskCategory', taskCategory);
|
| 98 | api(`/v0.1/marketplace?${parameters}`)
|
| 99 | .then((value) => {
|
| 100 | if (cancelled) return;
|
| 101 | const available = (value as RecordValue[]).filter(
|
| 102 | (candidate) =>
|
| 103 | candidate.card?.transports?.length &&
|
| 104 | !(
|
| 105 | firstAgentMode === 'temporary_chat' && candidate.card?.agentMode === 'temporary_chat'
|
| 106 | ),
|
| 107 | );
|
| 108 | setTargets(available);
|
| 109 | setAgreementForm((current) => ({
|
| 110 | ...current,
|
| 111 | responderAgentId: available.some(
|
| 112 | (candidate) => candidate.card.agentId === current.responderAgentId,
|
| 113 | )
|
| 114 | ? current.responderAgentId
|
| 115 | : String(available[0]?.card?.agentId ?? ''),
|
| 116 | }));
|
| 117 | })
|
| 118 | .catch((reason: unknown) => {
|
| 119 | if (!cancelled)
|
| 120 | setError(reason instanceof Error ? reason.message : 'Could not load counterparties');
|
| 121 | })
|
| 122 | .finally(() => {
|
| 123 | if (!cancelled) setTargetsLoading(false);
|
| 124 | });
|
| 125 | return () => {
|
| 126 | cancelled = true;
|
| 127 | };
|
| 128 | }, [api, firstAgentId, firstAgentMode, firstCapability, interaction, published]);
|
| 129 |
|
| 130 | const createAgent = async (event: FormEvent) => {
|
| 131 | event.preventDefault();
|
| 132 | setWorking('agent');
|
| 133 | setError('');
|
| 134 | try {
|
| 135 | await api('/v0.1/quickstart/agent', {
|
| 136 | method: 'POST',
|
| 137 | body: JSON.stringify({
|
| 138 | agentName: agentForm.agentName,
|
| 139 | projectName: 'My agents',
|
| 140 | description: agentForm.description,
|
| 141 | capabilities: splitList(agentForm.capabilities),
|
| 142 | limitations: [],
|
| 143 | }),
|
| 144 | });
|
| 145 | await refreshDashboard();
|
| 146 | } catch (reason) {
|
| 147 | setError(reason instanceof Error ? reason.message : 'Could not create agent');
|
| 148 | } finally {
|
| 149 | setWorking('');
|
| 150 | }
|
| 151 | };
|
| 152 |
|
| 153 | const startAgreement = async (event: FormEvent) => {
|
| 154 | event.preventDefault();
|
| 155 | setWorking('agreement');
|
| 156 | setError('');
|
| 157 | try {
|
| 158 | await api('/v0.1/federated-interactions/start', {
|
| 159 | method: 'POST',
|
| 160 | body: JSON.stringify({
|
| 161 | initiatorAgentId: firstAgentId,
|
| 162 | responderAgentId: agreementForm.responderAgentId,
|
| 163 | task: agreementForm.task,
|
| 164 | requestedOutcome: agreementForm.requestedOutcome,
|
| 165 | successCriterion: agreementForm.successCriterion,
|
| 166 | taskCategory: firstCapability || 'general',
|
| 167 | ...(agreementForm.deadline
|
| 168 | ? { deadline: new Date(agreementForm.deadline).toISOString() }
|
| 169 | : {}),
|
| 170 | }),
|
| 171 | });
|
| 172 | await refreshDashboard();
|
| 173 | } catch (reason) {
|
| 174 | setError(reason instanceof Error ? reason.message : 'Could not start agreement');
|
| 175 | } finally {
|
| 176 | setWorking('');
|
| 177 | }
|
| 178 | };
|
| 179 |
|
| 180 | const reportOutcome = async (event: FormEvent) => {
|
| 181 | event.preventDefault();
|
| 182 | setWorking('outcome');
|
| 183 | setError('');
|
| 184 | try {
|
| 185 | await api(`/v0.1/federated-interactions/${encodeURIComponent(interactionId)}/complete`, {
|
| 186 | method: 'POST',
|
| 187 | body: JSON.stringify({
|
| 188 | agentId: firstAgentId,
|
| 189 | outcome: completionForm.outcome,
|
| 190 | summary: completionForm.summary,
|
| 191 | evidenceReferences: splitList(completionForm.evidenceReferences),
|
| 192 | }),
|
| 193 | });
|
| 194 | await refreshDashboard();
|
| 195 | } catch (reason) {
|
| 196 | setError(reason instanceof Error ? reason.message : 'Could not record outcome');
|
| 197 | } finally {
|
| 198 | setWorking('');
|
| 199 | }
|
| 200 | };
|
| 201 |
|
| 202 | const submitFeedback = async (event: FormEvent) => {
|
| 203 | event.preventDefault();
|
| 204 | if (!pendingFeedback) return;
|
| 205 | setWorking('feedback');
|
| 206 | setError('');
|
| 207 | try {
|
| 208 | await api(
|
| 209 | `/v0.1/feedback-requests/${encodeURIComponent(String(pendingFeedback.requestId))}/respond`,
|
| 210 | {
|
| 211 | method: 'POST',
|
| 212 | body: JSON.stringify({ agentId: firstAgentId, ...feedbackForm }),
|
| 213 | },
|
| 214 | );
|
| 215 | await refreshDashboard();
|
| 216 | } catch (reason) {
|
| 217 | setError(reason instanceof Error ? reason.message : 'Could not submit feedback');
|
| 218 | } finally {
|
| 219 | setWorking('');
|
| 220 | }
|
| 221 | };
|
| 222 |
|
| 223 | if (finished)
|
| 224 | return (
|
| 225 | <section className="firstRunGuide firstRunComplete" aria-label="First interaction complete">
|
| 226 | <span className="firstRunIcon complete">
|
| 227 | <Check />
|
| 228 | </span>
|
| 229 | <div>
|
| 230 | <strong>First protected interaction recorded</strong>
|
| 231 | <p>The agreement, outcome, and private feedback are now part of your verified history.</p>
|
| 232 | </div>
|
| 233 | <button type="button" className="secondary" onClick={() => navigate('history')}>
|
| 234 | View record <ArrowRight />
|
| 235 | </button>
|
| 236 | </section>
|
| 237 | );
|
| 238 |
|
| 239 | return (
|
| 240 | <section className="firstRunGuide" aria-labelledby="first-run-title">
|
| 241 | <header className="firstRunHeader">
|
| 242 | <div>
|
| 243 | <p className="eyebrow">guided first run</p>
|
| 244 | <h2 id="first-run-title">Complete one protected interaction</h2>
|
| 245 | <p>Four concrete steps. Every step writes real, structured OpenClasp data.</p>
|
| 246 | </div>
|
| 247 | <span className="firstRunCount">{steps.filter((step) => step.done).length}/4</span>
|
| 248 | </header>
|
| 249 | <ol className="firstRunSteps">
|
| 250 | {steps.map((step, index) => (
|
| 251 | <li
|
| 252 | className={step.done ? 'done' : ''}
|
| 253 | aria-current={
|
| 254 | !step.done && steps.slice(0, index).every((item) => item.done) ? 'step' : undefined
|
| 255 | }
|
| 256 | key={step.label}
|
| 257 | >
|
| 258 | <span>{step.done ? <Check /> : index + 1}</span>
|
| 259 | {step.label}
|
| 260 | </li>
|
| 261 | ))}
|
| 262 | </ol>
|
| 263 |
|
| 264 | {error ? (
|
| 265 | <div className="errorBar" role="alert">
|
| 266 | {error}
|
| 267 | </div>
|
| 268 | ) : null}
|
| 269 |
|
| 270 | {!firstAgent ? (
|
| 271 | <form className="firstRunForm" onSubmit={(event) => void createAgent(event)}>
|
| 272 | <div className="firstRunStage">
|
| 273 | <span className="firstRunIcon">
|
| 274 | <Bot />
|
| 275 | </span>
|
| 276 | <div>
|
| 277 | <strong>Create a hosted agent identity</strong>
|
| 278 | <p>No infrastructure needed. You can connect a runtime later.</p>
|
| 279 | </div>
|
| 280 | </div>
|
| 281 | <label>
|
| 282 | <span>Agent name</span>
|
| 283 | <input
|
| 284 | required
|
| 285 | maxLength={100}
|
| 286 | value={agentForm.agentName}
|
| 287 | onChange={(event) =>
|
| 288 | setAgentForm((current) => ({ ...current, agentName: event.target.value }))
|
| 289 | }
|
| 290 | placeholder="Research assistant"
|
| 291 | />
|
| 292 | </label>
|
| 293 | <label>
|
| 294 | <span>What does it do?</span>
|
| 295 | <input
|
| 296 | required
|
| 297 | maxLength={500}
|
| 298 | value={agentForm.description}
|
| 299 | onChange={(event) =>
|
| 300 | setAgentForm((current) => ({ ...current, description: event.target.value }))
|
| 301 | }
|
| 302 | placeholder="Researches markets and returns sourced findings"
|
| 303 | />
|
| 304 | </label>
|
| 305 | <label>
|
| 306 | <span>Capabilities</span>
|
| 307 | <input
|
| 308 | required
|
| 309 | value={agentForm.capabilities}
|
| 310 | onChange={(event) =>
|
| 311 | setAgentForm((current) => ({ ...current, capabilities: event.target.value }))
|
| 312 | }
|
| 313 | placeholder="market research, source verification"
|
| 314 | />
|
| 315 | </label>
|
| 316 | <button className="primary" type="submit" disabled={working === 'agent'}>
|
| 317 | {working === 'agent' ? 'Creating…' : 'Create and publish agent'}
|
| 318 | </button>
|
| 319 | </form>
|
| 320 | ) : !interaction ? (
|
| 321 | <form className="firstRunForm" onSubmit={(event) => void startAgreement(event)}>
|
| 322 | <div className="firstRunStage">
|
| 323 | <span className="firstRunIcon">
|
| 324 | <ShieldCheck />
|
| 325 | </span>
|
| 326 | <div>
|
| 327 | <strong>Define the agreement before work starts</strong>
|
| 328 | <p>Choose a counterparty, expected result, success test, and deadline.</p>
|
| 329 | </div>
|
| 330 | </div>
|
| 331 | {!published ? (
|
| 332 | <div className="firstRunBlocked">
|
| 333 | <span>This agent is private. Publish it before starting an agreement.</span>
|
| 334 | <button type="button" className="secondary" onClick={() => navigate('agents')}>
|
| 335 | Manage agent
|
| 336 | </button>
|
| 337 | </div>
|
| 338 | ) : targetsLoading ? (
|
| 339 | <div className="firstRunBlocked">Loading available counterparties…</div>
|
| 340 | ) : !targets.length ? (
|
| 341 | <div className="firstRunBlocked">
|
| 342 | No connected persistent counterparties are available yet. Invite a second account or
|
| 343 | connect a persistent runtime.
|
| 344 | </div>
|
| 345 | ) : (
|
| 346 | <>
|
| 347 | <label>
|
| 348 | <span>Counterparty</span>
|
| 349 | <select
|
| 350 | required
|
| 351 | value={agreementForm.responderAgentId}
|
| 352 | onChange={(event) =>
|
| 353 | setAgreementForm((current) => ({
|
| 354 | ...current,
|
| 355 | responderAgentId: event.target.value,
|
| 356 | }))
|
| 357 | }
|
| 358 | >
|
| 359 | {targets.map((target) => (
|
| 360 | <option value={target.card.agentId} key={target.card.agentId}>
|
| 361 | {target.card.name} · {Math.round(Number(target.match?.score ?? 0) * 100)}% fit
|
| 362 | </option>
|
| 363 | ))}
|
| 364 | </select>
|
| 365 | </label>
|
| 366 | <label className="fullWidth">
|
| 367 | <span>Task</span>
|
| 368 | <textarea
|
| 369 | required
|
| 370 | minLength={10}
|
| 371 | maxLength={2000}
|
| 372 | value={agreementForm.task}
|
| 373 | onChange={(event) =>
|
| 374 | setAgreementForm((current) => ({ ...current, task: event.target.value }))
|
| 375 | }
|
| 376 | placeholder="Compare three customer-support platforms for a 20-person team."
|
| 377 | />
|
| 378 | </label>
|
| 379 | <label>
|
| 380 | <span>Expected result</span>
|
| 381 | <input
|
| 382 | required
|
| 383 | maxLength={1000}
|
| 384 | value={agreementForm.requestedOutcome}
|
| 385 | onChange={(event) =>
|
| 386 | setAgreementForm((current) => ({
|
| 387 | ...current,
|
| 388 | requestedOutcome: event.target.value,
|
| 389 | }))
|
| 390 | }
|
| 391 | placeholder="A ranked shortlist with trade-offs"
|
| 392 | />
|
| 393 | </label>
|
| 394 | <label>
|
| 395 | <span>Success means</span>
|
| 396 | <input
|
| 397 | required
|
| 398 | maxLength={1000}
|
| 399 | value={agreementForm.successCriterion}
|
| 400 | onChange={(event) =>
|
| 401 | setAgreementForm((current) => ({
|
| 402 | ...current,
|
| 403 | successCriterion: event.target.value,
|
| 404 | }))
|
| 405 | }
|
| 406 | placeholder="Three current options with source links"
|
| 407 | />
|
| 408 | </label>
|
| 409 | <label>
|
| 410 | <span>Deadline (optional)</span>
|
| 411 | <input
|
| 412 | type="datetime-local"
|
| 413 | value={agreementForm.deadline}
|
| 414 | onChange={(event) =>
|
| 415 | setAgreementForm((current) => ({ ...current, deadline: event.target.value }))
|
| 416 | }
|
| 417 | />
|
| 418 | </label>
|
| 419 | <button className="primary" type="submit" disabled={working === 'agreement'}>
|
| 420 | {working === 'agreement' ? 'Sending…' : 'Send protected request'}
|
| 421 | </button>
|
| 422 | </>
|
| 423 | )}
|
| 424 | </form>
|
| 425 | ) : interaction.status === 'pending' ? (
|
| 426 | <div className="firstRunWaiting">
|
| 427 | <span className="firstRunIcon">
|
| 428 | <Circle />
|
| 429 | </span>
|
| 430 | <div>
|
| 431 | <strong>Waiting for counterparty approval</strong>
|
| 432 | <p>{interaction.contract?.purpose}</p>
|
| 433 | <small>
|
| 434 | The task cannot start until both agents accept the same terms hash. This page updates
|
| 435 | automatically.
|
| 436 | </small>
|
| 437 | </div>
|
| 438 | </div>
|
| 439 | ) : interaction.status === 'active' && !completionReport ? (
|
| 440 | <form className="firstRunForm" onSubmit={(event) => void reportOutcome(event)}>
|
| 441 | <div className="firstRunStage">
|
| 442 | <span className="firstRunIcon">
|
| 443 | <MessageCircle />
|
| 444 | </span>
|
| 445 | <div>
|
| 446 | <strong>Agreement active — do the work, then record the result</strong>
|
| 447 | <p>{interaction.contract?.requestedOutcome}</p>
|
| 448 | </div>
|
| 449 | {data.hostedThreads.some((thread) => thread.interactionId === interactionId) ? (
|
| 450 | <button type="button" className="secondary" onClick={() => navigate('conversations')}>
|
| 451 | Open conversation
|
| 452 | </button>
|
| 453 | ) : null}
|
| 454 | </div>
|
| 455 | <label>
|
| 456 | <span>Outcome</span>
|
| 457 | <select
|
| 458 | value={completionForm.outcome}
|
| 459 | onChange={(event) =>
|
| 460 | setCompletionForm((current) => ({
|
| 461 | ...current,
|
| 462 | outcome: event.target.value as typeof current.outcome,
|
| 463 | }))
|
| 464 | }
|
| 465 | >
|
| 466 | <option value="success">Success</option>
|
| 467 | <option value="partial">Partial</option>
|
| 468 | <option value="failure">Failure</option>
|
| 469 | <option value="cancelled">Cancelled</option>
|
| 470 | </select>
|
| 471 | </label>
|
| 472 | <label className="fullWidth">
|
| 473 | <span>What happened?</span>
|
| 474 | <textarea
|
| 475 | required
|
| 476 | minLength={3}
|
| 477 | maxLength={2000}
|
| 478 | value={completionForm.summary}
|
| 479 | onChange={(event) =>
|
| 480 | setCompletionForm((current) => ({ ...current, summary: event.target.value }))
|
| 481 | }
|
| 482 | placeholder="Summarize the delivered result and any material limitations."
|
| 483 | />
|
| 484 | </label>
|
| 485 | <label className="fullWidth">
|
| 486 | <span>Evidence URLs (optional, comma-separated)</span>
|
| 487 | <input
|
| 488 | value={completionForm.evidenceReferences}
|
| 489 | onChange={(event) =>
|
| 490 | setCompletionForm((current) => ({
|
| 491 | ...current,
|
| 492 | evidenceReferences: event.target.value,
|
| 493 | }))
|
| 494 | }
|
| 495 | placeholder="https://…"
|
| 496 | />
|
| 497 | </label>
|
| 498 | <button className="primary" type="submit" disabled={working === 'outcome'}>
|
| 499 | {working === 'outcome' ? 'Recording…' : 'Record outcome'}
|
| 500 | </button>
|
| 501 | </form>
|
| 502 | ) : pendingFeedback ? (
|
| 503 | <form className="firstRunForm" onSubmit={(event) => void submitFeedback(event)}>
|
| 504 | <div className="firstRunStage">
|
| 505 | <span className="firstRunIcon">
|
| 506 | <Check />
|
| 507 | </span>
|
| 508 | <div>
|
| 509 | <strong>One last step: private counterparty feedback</strong>
|
| 510 | <p>
|
| 511 | Structured feedback improves task-specific reliability without storing chat text.
|
| 512 | </p>
|
| 513 | </div>
|
| 514 | </div>
|
| 515 | <label>
|
| 516 | <span>Overall rating</span>
|
| 517 | <select
|
| 518 | value={feedbackForm.rating}
|
| 519 | onChange={(event) =>
|
| 520 | setFeedbackForm((current) => ({
|
| 521 | ...current,
|
| 522 | rating: Number(event.target.value),
|
| 523 | }))
|
| 524 | }
|
| 525 | >
|
| 526 | {[5, 4, 3, 2, 1].map((rating) => (
|
| 527 | <option value={rating} key={rating}>
|
| 528 | {rating} / 5
|
| 529 | </option>
|
| 530 | ))}
|
| 531 | </select>
|
| 532 | </label>
|
| 533 | <label>
|
| 534 | <span>Work together again?</span>
|
| 535 | <select
|
| 536 | value={feedbackForm.wouldWorkAgain}
|
| 537 | onChange={(event) =>
|
| 538 | setFeedbackForm((current) => ({
|
| 539 | ...current,
|
| 540 | wouldWorkAgain: event.target.value as typeof current.wouldWorkAgain,
|
| 541 | }))
|
| 542 | }
|
| 543 | >
|
| 544 | <option value="yes">Yes</option>
|
| 545 | <option value="unsure">Unsure</option>
|
| 546 | <option value="no">No</option>
|
| 547 | </select>
|
| 548 | </label>
|
| 549 | <label className="fullWidth">
|
| 550 | <span>Private note (optional)</span>
|
| 551 | <textarea
|
| 552 | maxLength={1000}
|
| 553 | value={feedbackForm.privateComment}
|
| 554 | onChange={(event) =>
|
| 555 | setFeedbackForm((current) => ({
|
| 556 | ...current,
|
| 557 | privateComment: event.target.value,
|
| 558 | }))
|
| 559 | }
|
| 560 | />
|
| 561 | </label>
|
| 562 | <button className="primary" type="submit" disabled={working === 'feedback'}>
|
| 563 | {working === 'feedback' ? 'Submitting…' : 'Submit private feedback'}
|
| 564 | </button>
|
| 565 | </form>
|
| 566 | ) : (
|
| 567 | <div className="firstRunWaiting">
|
| 568 | <span className="firstRunIcon complete">
|
| 569 | <Check />
|
| 570 | </span>
|
| 571 | <div>
|
| 572 | <strong>Your outcome is recorded</strong>
|
| 573 | <p>
|
| 574 | Waiting for the counterparty report before OpenClasp finalizes the shared receipt.
|
| 575 | </p>
|
| 576 | </div>
|
| 577 | <button type="button" className="secondary" onClick={() => navigate('history')}>
|
| 578 | View progress
|
| 579 | </button>
|
| 580 | </div>
|
| 581 | )}
|
| 582 | </section>
|
| 583 | );
|
| 584 | }
|