a2a-source-edit: write frontend/src/App.jsx
This commit is contained in:
@@ -1,18 +1,31 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
callSkill,
|
||||
loadAgentConfig,
|
||||
loadSession,
|
||||
sampleArgs,
|
||||
signInUrl,
|
||||
} from "./a2a.js";
|
||||
import { callSkill, loadAgentConfig, loadSession, signInUrl } from "./a2a.js";
|
||||
|
||||
const SUCCESS_FIXTURE = `From: alex@company.example
|
||||
Subject: Concern about team behavior
|
||||
Body: I want to report behavior that feels like harassment and retaliation after I raised a concern. What should I do next?
|
||||
---
|
||||
From: priya@company.example
|
||||
Subject: Benefits enrollment for dependent
|
||||
Body: I need to add a dependent to my insurance and want to understand eligibility and enrollment timing.
|
||||
---
|
||||
From: marco@company.example
|
||||
Subject: PTO request next month
|
||||
Body: I would like to take leave from May 6 to May 10. Can People approve this?`;
|
||||
|
||||
const DEFAULT_POLICY = `Benefits questions: be empathetic, explain that enrollment and eligibility depend on plan documents, and route exceptions to People Ops.
|
||||
Leave requests: acknowledge receipt, ask for dates if missing, and mention manager/People approval before commitments.
|
||||
Payroll or personal data: mark urgent, avoid exposing private data, and route to the secure HRIS/payroll channel.
|
||||
Employee relations or harassment: mark urgent, avoid investigation promises, and escalate to the designated People partner.`;
|
||||
|
||||
export function App() {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [session, setSession] = useState(null);
|
||||
const [selectedSkillName, setSelectedSkillName] = useState("");
|
||||
const [argsText, setArgsText] = useState("{}");
|
||||
const [inboxText, setInboxText] = useState(SUCCESS_FIXTURE);
|
||||
const [policyText, setPolicyText] = useState(DEFAULT_POLICY);
|
||||
const [maxMessages, setMaxMessages] = useState(6);
|
||||
const [result, setResult] = useState(null);
|
||||
const [mailbox, setMailbox] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [running, setRunning] = useState(false);
|
||||
|
||||
@@ -20,46 +33,30 @@ export function App() {
|
||||
loadAgentConfig()
|
||||
.then((data) => {
|
||||
setConfig(data);
|
||||
const firstSkill = data.skills?.[0];
|
||||
if (firstSkill) {
|
||||
setSelectedSkillName(firstSkill.name);
|
||||
setArgsText(JSON.stringify(sampleArgs(firstSkill), null, 2));
|
||||
}
|
||||
return loadSession(data).catch(() => null);
|
||||
})
|
||||
.then((sessionData) => setSession(sessionData))
|
||||
.catch((err) => setError(err.message || String(err)));
|
||||
}, []);
|
||||
|
||||
const selectedSkill = useMemo(
|
||||
() => config?.skills?.find((skill) => skill.name === selectedSkillName),
|
||||
[config, selectedSkillName],
|
||||
);
|
||||
|
||||
const needsSignIn = Boolean(
|
||||
config?.auth?.invokeRequiresSession && session && !session.authenticated,
|
||||
);
|
||||
const needsSignIn = Boolean(config?.auth?.invokeRequiresSession && session && !session.authenticated);
|
||||
const signInHref = useMemo(
|
||||
() => (config && needsSignIn ? signInUrl(config) : null),
|
||||
[config, needsSignIn],
|
||||
);
|
||||
|
||||
function selectSkill(skill) {
|
||||
setSelectedSkillName(skill.name);
|
||||
setArgsText(JSON.stringify(sampleArgs(skill), null, 2));
|
||||
setResult(null);
|
||||
setError(null);
|
||||
}
|
||||
|
||||
async function runSkill(event) {
|
||||
async function runTriage(event) {
|
||||
event.preventDefault();
|
||||
if (!config || !selectedSkill) return;
|
||||
if (!config) return;
|
||||
setRunning(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
try {
|
||||
const args = JSON.parse(argsText || "{}");
|
||||
const data = await callSkill(config, selectedSkill.name, args);
|
||||
const data = await callSkill(config, "triage_people_inbox", {
|
||||
inbox_text: inboxText,
|
||||
policy_text: policyText,
|
||||
max_messages: Number(maxMessages),
|
||||
});
|
||||
setResult(data);
|
||||
} catch (err) {
|
||||
setError(err.message || String(err));
|
||||
@@ -68,91 +65,149 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkMailbox() {
|
||||
if (!config) return;
|
||||
setError(null);
|
||||
try {
|
||||
const data = await callSkill(config, "mailbox_status", { limit: 5, unseen_only: false });
|
||||
setMailbox(data);
|
||||
} catch (err) {
|
||||
setError(err.message || String(err));
|
||||
}
|
||||
}
|
||||
|
||||
function downloadReport() {
|
||||
if (!result?.artifact?.content_text) return;
|
||||
const blob = new Blob([result.artifact.content_text], {
|
||||
type: result.artifact.media_type || "application/json",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = result.artifact.name || "people-team-inbox-triage.json";
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<main className="loading">
|
||||
<p>{error || "Loading A2A agent contract..."}</p>
|
||||
</main>
|
||||
);
|
||||
return <main className="loading">{error || "Loading People Inbox Assistant..."}</main>;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<aside className="sidebar">
|
||||
<main className="product-shell" data-testid="agent-app">
|
||||
<section className="hero-card">
|
||||
<p className="eyebrow">Production proof v116 · People teams</p>
|
||||
<div className="hero-grid">
|
||||
<div>
|
||||
<p className="eyebrow">A2A packed app</p>
|
||||
<h1>production-proof-v116-high-utili-7255-2</h1>
|
||||
<p className="agent-meta">
|
||||
{config.agent.name} v{config.agent.version}
|
||||
<h1>Shared inbox triage with approval-ready HR replies</h1>
|
||||
<p className="lede">
|
||||
Paste a few People inbox messages, keep or edit the policy guidance, and get a prioritized queue plus grounded drafts in under two minutes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="session-card">
|
||||
<span>{session?.authenticated ? "Signed in" : needsSignIn ? "Sign-in required to run" : "Ready"}</span>
|
||||
<strong>{session?.user?.email || session?.org?.slug || config.agent?.name}</strong>
|
||||
{needsSignIn && signInHref ? <a href={signInHref}>Sign in to run</a> : null}
|
||||
<button className="ghost" type="button" onClick={checkMailbox}>Check mailbox setup</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav className="skill-list" aria-label="Agent skills">
|
||||
{(config.skills || []).map((skill) => (
|
||||
<button
|
||||
className={skill.name === selectedSkillName ? "skill active" : "skill"}
|
||||
key={skill.name}
|
||||
onClick={() => selectSkill(skill)}
|
||||
type="button"
|
||||
>
|
||||
<span>{skill.name}</span>
|
||||
<small>{skill.stream ? "streaming" : "request"}</small>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="session">
|
||||
<span>{session?.authenticated ? "Signed in" : needsSignIn ? "Signed out" : "Local session"}</span>
|
||||
<strong>{session?.user?.email || session?.org?.slug || "dev@example.local"}</strong>
|
||||
{/* Skills that spend the caller's LLM budget need a signed-in
|
||||
visitor even when the page itself is public, so offer the way in
|
||||
rather than failing at the first click. */}
|
||||
{needsSignIn && signInHref ? (
|
||||
<a className="signin" href={signInHref}>Sign in to run</a>
|
||||
{mailbox ? (
|
||||
<section className="notice">
|
||||
<strong>Mailbox:</strong> {mailbox.status} — {mailbox.message || mailbox.address || `${mailbox.messages?.length || 0} recent messages`}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="workbench">
|
||||
<header className="toolbar">
|
||||
<div>
|
||||
<p className="eyebrow">Skill runner</p>
|
||||
<h2>{selectedSkill?.name || "No skills found"}</h2>
|
||||
</div>
|
||||
<a href={config.docs.base} rel="noreferrer" target="_blank">
|
||||
Docs
|
||||
</a>
|
||||
</header>
|
||||
|
||||
{selectedSkill ? (
|
||||
<form className="runner" onSubmit={runSkill}>
|
||||
<form className="workflow" onSubmit={runTriage}>
|
||||
<label>
|
||||
Arguments JSON
|
||||
<span>Inbox messages</span>
|
||||
<textarea
|
||||
spellCheck="false"
|
||||
value={argsText}
|
||||
onChange={(event) => setArgsText(event.target.value)}
|
||||
data-testid="agent-input"
|
||||
value={inboxText}
|
||||
onChange={(event) => setInboxText(event.target.value)}
|
||||
maxLength={20000}
|
||||
/>
|
||||
</label>
|
||||
<button disabled={running} type="submit">
|
||||
{running ? "Running..." : `Run ${selectedSkill.name}`}
|
||||
<label>
|
||||
<span>Policy guidance</span>
|
||||
<textarea
|
||||
className="policy"
|
||||
value={policyText}
|
||||
onChange={(event) => setPolicyText(event.target.value)}
|
||||
maxLength={12000}
|
||||
/>
|
||||
</label>
|
||||
<div className="actions">
|
||||
<label className="small-field">
|
||||
Max messages
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="12"
|
||||
value={maxMessages}
|
||||
onChange={(event) => setMaxMessages(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button data-testid="agent-submit" disabled={running || needsSignIn} type="submit">
|
||||
{running ? "Triaging..." : "Triage inbox & draft replies"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<p className="empty">Add a `@a2a.tool` to your agent to make it callable here.</p>
|
||||
)}
|
||||
|
||||
<section className="panels">
|
||||
<div className="panel">
|
||||
<h3>Input schema</h3>
|
||||
<pre>{JSON.stringify(selectedSkill?.input_schema || {}, null, 2)}</pre>
|
||||
{error ? <section className="error">{error}</section> : null}
|
||||
|
||||
{result ? (
|
||||
<section className="results" data-testid="agent-result">
|
||||
<div className="result-header">
|
||||
<div>
|
||||
<p className="eyebrow">Structured result</p>
|
||||
<h2>{result.summary || result.status}</h2>
|
||||
</div>
|
||||
<div className="panel">
|
||||
<h3>Result</h3>
|
||||
<pre>{error || JSON.stringify(result, null, 2) || "No run yet."}</pre>
|
||||
{result.artifact ? (
|
||||
<button data-testid="agent-download" type="button" onClick={downloadReport}>
|
||||
Download {result.artifact.name}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="boundary">{result.approval_boundary}</p>
|
||||
{result.receipt ? (
|
||||
<p className={result.receipt.persisted ? "receipt ok" : "receipt warn"}>
|
||||
Receipt {result.receipt.receipt_id}: {result.receipt.message}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="queue">
|
||||
{(result.queue || []).map((item) => (
|
||||
<article className={`queue-card ${item.priority}`} key={item.message_id}>
|
||||
<div className="queue-top">
|
||||
<span className="pill">{item.priority}</span>
|
||||
<span>{item.category}</span>
|
||||
</div>
|
||||
<h3>{item.subject}</h3>
|
||||
<p><strong>From:</strong> {item.sender}</p>
|
||||
<p><strong>Why:</strong> {item.reason}</p>
|
||||
<p><strong>Next:</strong> {item.next_action}</p>
|
||||
<details open>
|
||||
<summary>Approval-ready draft</summary>
|
||||
<div className="draft">
|
||||
<p><strong>To:</strong> {item.draft.to}</p>
|
||||
<p><strong>Subject:</strong> {item.draft.subject}</p>
|
||||
<pre>{item.draft.body}</pre>
|
||||
<p><strong>Approval reason:</strong> {item.draft.approval_reason}</p>
|
||||
<ul>
|
||||
{(item.draft.policy_basis || []).map((basis) => <li key={basis}>{basis}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user