a2a-source-edit: write frontend/src/App.jsx
This commit is contained in:
@@ -1,18 +1,31 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import { callSkill, loadAgentConfig, loadSession, signInUrl } from "./a2a.js";
|
||||||
callSkill,
|
|
||||||
loadAgentConfig,
|
const SUCCESS_FIXTURE = `From: alex@company.example
|
||||||
loadSession,
|
Subject: Concern about team behavior
|
||||||
sampleArgs,
|
Body: I want to report behavior that feels like harassment and retaliation after I raised a concern. What should I do next?
|
||||||
signInUrl,
|
---
|
||||||
} from "./a2a.js";
|
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() {
|
export function App() {
|
||||||
const [config, setConfig] = useState(null);
|
const [config, setConfig] = useState(null);
|
||||||
const [session, setSession] = useState(null);
|
const [session, setSession] = useState(null);
|
||||||
const [selectedSkillName, setSelectedSkillName] = useState("");
|
const [inboxText, setInboxText] = useState(SUCCESS_FIXTURE);
|
||||||
const [argsText, setArgsText] = useState("{}");
|
const [policyText, setPolicyText] = useState(DEFAULT_POLICY);
|
||||||
|
const [maxMessages, setMaxMessages] = useState(6);
|
||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
|
const [mailbox, setMailbox] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
|
|
||||||
@@ -20,46 +33,30 @@ export function App() {
|
|||||||
loadAgentConfig()
|
loadAgentConfig()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setConfig(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);
|
return loadSession(data).catch(() => null);
|
||||||
})
|
})
|
||||||
.then((sessionData) => setSession(sessionData))
|
.then((sessionData) => setSession(sessionData))
|
||||||
.catch((err) => setError(err.message || String(err)));
|
.catch((err) => setError(err.message || String(err)));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const selectedSkill = useMemo(
|
const needsSignIn = Boolean(config?.auth?.invokeRequiresSession && session && !session.authenticated);
|
||||||
() => config?.skills?.find((skill) => skill.name === selectedSkillName),
|
|
||||||
[config, selectedSkillName],
|
|
||||||
);
|
|
||||||
|
|
||||||
const needsSignIn = Boolean(
|
|
||||||
config?.auth?.invokeRequiresSession && session && !session.authenticated,
|
|
||||||
);
|
|
||||||
const signInHref = useMemo(
|
const signInHref = useMemo(
|
||||||
() => (config && needsSignIn ? signInUrl(config) : null),
|
() => (config && needsSignIn ? signInUrl(config) : null),
|
||||||
[config, needsSignIn],
|
[config, needsSignIn],
|
||||||
);
|
);
|
||||||
|
|
||||||
function selectSkill(skill) {
|
async function runTriage(event) {
|
||||||
setSelectedSkillName(skill.name);
|
|
||||||
setArgsText(JSON.stringify(sampleArgs(skill), null, 2));
|
|
||||||
setResult(null);
|
|
||||||
setError(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runSkill(event) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!config || !selectedSkill) return;
|
if (!config) return;
|
||||||
setRunning(true);
|
setRunning(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
try {
|
try {
|
||||||
const args = JSON.parse(argsText || "{}");
|
const data = await callSkill(config, "triage_people_inbox", {
|
||||||
const data = await callSkill(config, selectedSkill.name, args);
|
inbox_text: inboxText,
|
||||||
|
policy_text: policyText,
|
||||||
|
max_messages: Number(maxMessages),
|
||||||
|
});
|
||||||
setResult(data);
|
setResult(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || String(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) {
|
if (!config) {
|
||||||
return (
|
return <main className="loading">{error || "Loading People Inbox Assistant..."}</main>;
|
||||||
<main className="loading">
|
|
||||||
<p>{error || "Loading A2A agent contract..."}</p>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="app-shell">
|
<main className="product-shell" data-testid="agent-app">
|
||||||
<aside className="sidebar">
|
<section className="hero-card">
|
||||||
<div>
|
<p className="eyebrow">Production proof v116 · People teams</p>
|
||||||
<p className="eyebrow">A2A packed app</p>
|
<div className="hero-grid">
|
||||||
<h1>production-proof-v116-high-utili-7255-2</h1>
|
|
||||||
<p className="agent-meta">
|
|
||||||
{config.agent.name} v{config.agent.version}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section className="workbench">
|
|
||||||
<header className="toolbar">
|
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Skill runner</p>
|
<h1>Shared inbox triage with approval-ready HR replies</h1>
|
||||||
<h2>{selectedSkill?.name || "No skills found"}</h2>
|
<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>
|
||||||
<a href={config.docs.base} rel="noreferrer" target="_blank">
|
<div className="session-card">
|
||||||
Docs
|
<span>{session?.authenticated ? "Signed in" : needsSignIn ? "Sign-in required to run" : "Ready"}</span>
|
||||||
</a>
|
<strong>{session?.user?.email || session?.org?.slug || config.agent?.name}</strong>
|
||||||
</header>
|
{needsSignIn && signInHref ? <a href={signInHref}>Sign in to run</a> : null}
|
||||||
|
<button className="ghost" type="button" onClick={checkMailbox}>Check mailbox setup</button>
|
||||||
{selectedSkill ? (
|
|
||||||
<form className="runner" onSubmit={runSkill}>
|
|
||||||
<label>
|
|
||||||
Arguments JSON
|
|
||||||
<textarea
|
|
||||||
spellCheck="false"
|
|
||||||
value={argsText}
|
|
||||||
onChange={(event) => setArgsText(event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button disabled={running} type="submit">
|
|
||||||
{running ? "Running..." : `Run ${selectedSkill.name}`}
|
|
||||||
</button>
|
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="panel">
|
</div>
|
||||||
<h3>Result</h3>
|
</section>
|
||||||
<pre>{error || JSON.stringify(result, null, 2) || "No run yet."}</pre>
|
|
||||||
|
{mailbox ? (
|
||||||
|
<section className="notice">
|
||||||
|
<strong>Mailbox:</strong> {mailbox.status} — {mailbox.message || mailbox.address || `${mailbox.messages?.length || 0} recent messages`}
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<form className="workflow" onSubmit={runTriage}>
|
||||||
|
<label>
|
||||||
|
<span>Inbox messages</span>
|
||||||
|
<textarea
|
||||||
|
data-testid="agent-input"
|
||||||
|
value={inboxText}
|
||||||
|
onChange={(event) => setInboxText(event.target.value)}
|
||||||
|
maxLength={20000}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{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>
|
||||||
|
{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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user