Files
iios/apps/agent-demo/src/App.tsx
T
maaz519 9c6e2c76e8 fix(support): agent-demo self-seeds (join default queue + go online); tickets always get a queue
Root cause of 'waiting for escalations': no queue/membership meant tickets were
never assigned. Now: createTicket find-or-creates a default queue; new joinDefault
endpoint + useGoOnline hook; agent-demo joins+goes-AVAILABLE on load (assignPending
picks up any waiting ticket). smoke-support self-seeds (no seed script needed).
Also: vitest singleFork to end cross-file DB races (45 tests deterministic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:39:14 +05:30

107 lines
3.5 KiB
TypeScript

import { useEffect, useState } from 'react';
import {
SupportProvider,
useAssignedTickets,
useGoOnline,
useThread,
useMessages,
type Ticket,
} from '@insignia/iios-support-web';
const SERVICE = 'http://localhost:3200';
const APP_ID = 'portal-demo';
const AGENT_ID = 'agent1';
async function devToken(userId: string): Promise<string> {
const r = await fetch(`${SERVICE}/v1/dev/token`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ appId: APP_ID, userId, name: userId }),
});
if (!r.ok) throw new Error(`devToken ${r.status} (service running with IIOS_DEV_TOKENS=1?)`);
return ((await r.json()) as { token: string }).token;
}
function AgentChat({ threadId }: { threadId: string }) {
const { open } = useThread();
useEffect(() => {
void open(threadId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [threadId]);
const { messages, send } = useMessages(threadId);
const [text, setText] = useState('');
return (
<div style={{ flex: 1, padding: 12, fontFamily: 'sans-serif' }}>
<h3 style={{ marginTop: 0 }}>Thread {threadId.slice(0, 8)}</h3>
<div style={{ height: 280, overflow: 'auto', background: '#fafafa', padding: 8 }}>
{messages.map((m) => (
<div key={m.id} style={{ padding: '2px 0' }}>
<b>{m.senderActorId.slice(0, 6)}:</b> {m.content}
</div>
))}
</div>
<input
value={text}
placeholder="reply as agent + Enter"
style={{ width: '100%', padding: 6, marginTop: 8 }}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && text.trim()) {
void send(text.trim());
setText('');
}
}}
/>
</div>
);
}
function AgentInner() {
const goOnline = useGoOnline();
const { tickets } = useAssignedTickets();
const [threadId, setThreadId] = useState<string | null>(null);
useEffect(() => {
void goOnline(); // join default queue + go AVAILABLE
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div style={{ display: 'flex', fontFamily: 'sans-serif' }}>
<div style={{ width: 240, borderRight: '1px solid #ddd', padding: 12 }}>
<b>Assigned tickets ({tickets.length})</b>
{tickets.length === 0 && <div style={{ color: '#999', marginTop: 6 }}>waiting for escalations</div>}
{tickets.map((t: Ticket) => {
const tid = t.threadLinks?.[0]?.threadId ?? null;
return (
<div key={t.id} style={{ padding: '6px 0', borderBottom: '1px solid #eee' }}>
<div style={{ fontWeight: 600 }}>{t.subject}</div>
<div style={{ color: '#999' }}>{t.state}</div>
<button disabled={!tid} onClick={() => setThreadId(tid)}>
open thread
</button>
</div>
);
})}
</div>
{threadId ? <AgentChat threadId={threadId} /> : <div style={{ flex: 1, padding: 12 }}>Select a ticket.</div>}
</div>
);
}
export function App() {
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
void devToken(AGENT_ID).then(setToken);
}, []);
if (!token) return <div style={{ padding: 16, fontFamily: 'sans-serif' }}>loading agent</div>;
return (
<div style={{ padding: 16 }}>
<h2 style={{ fontFamily: 'sans-serif' }}>IIOS P4 Agent Dashboard ({AGENT_ID})</h2>
<SupportProvider serviceUrl={SERVICE} token={token}>
<AgentInner />
</SupportProvider>
</div>
);
}