@sansavision/yousaid-react 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ import type { AgentRunView, HumanHandoff, PendingApprovalView, RealtimeStatus, RoomCanvasView, RoomDetail, RoomMemberView, RoomRealtimeEvent } from "@sansavision/yousaid";
2
+ /**
3
+ * Live Room events over the realtime WebSocket: ordered by sequence, deduped,
4
+ * resumed from the last processed sequence after reconnects, and rebuilt from
5
+ * a snapshot when the client fell far behind. Falls back to HTTP catch-up
6
+ * polling where WebSocket is unavailable.
7
+ * @experimental
8
+ */
9
+ export declare function useRoomEvents(roomId: string | null, options?: {
10
+ onEvent?: (event: RoomRealtimeEvent) => void;
11
+ pollMs?: number;
12
+ }): {
13
+ events: RoomRealtimeEvent[];
14
+ status: RealtimeStatus;
15
+ lastSequence: number;
16
+ };
17
+ /**
18
+ * One agent run, polled until it reaches a terminal state, with cancel.
19
+ * @experimental
20
+ */
21
+ export declare function useAgentRun(runId: string | null, options?: {
22
+ pollMs?: number;
23
+ }): {
24
+ run: AgentRunView | null;
25
+ loading: boolean;
26
+ error: string;
27
+ refresh: () => Promise<AgentRunView | null>;
28
+ cancel: () => Promise<void>;
29
+ };
30
+ /**
31
+ * Actions a teammate is reviewing in Rooms you can see. Customers only see that a review is pending.
32
+ * @experimental
33
+ */
34
+ export declare function usePendingApprovals(roomId?: string, options?: {
35
+ pollMs?: number;
36
+ }): {
37
+ approvals: PendingApprovalView[];
38
+ loading: boolean;
39
+ error: string;
40
+ refresh: () => Promise<void>;
41
+ };
42
+ /**
43
+ * The customer-visible Room Canvas. Internal fields never reach the SDK.
44
+ * @experimental
45
+ */
46
+ export declare function useRoomCanvas(roomId: string | null): {
47
+ canvas: RoomCanvasView | null;
48
+ loading: boolean;
49
+ error: string;
50
+ refresh: () => Promise<void>;
51
+ };
52
+ /**
53
+ * Customers in the Room (staff identities are never exposed to the SDK).
54
+ * @experimental
55
+ */
56
+ export declare function useRoomMembers(roomId: string | null): {
57
+ members: RoomMemberView[];
58
+ memberCount: number;
59
+ loading: boolean;
60
+ error: string;
61
+ refresh: () => Promise<void>;
62
+ };
63
+ /**
64
+ * "Talk to a person": request, show the acknowledgement and SLA, or withdraw.
65
+ * @experimental
66
+ */
67
+ export declare function useRequestHuman(roomId: string | null): {
68
+ handoff: HumanHandoff | null;
69
+ active: boolean;
70
+ pending: boolean;
71
+ error: string;
72
+ request: (reason?: string) => Promise<HumanHandoff | null>;
73
+ withdraw: () => Promise<void>;
74
+ refresh: () => Promise<void>;
75
+ };
76
+ /** @experimental */
77
+ export type CanvasEntry = {
78
+ key: string;
79
+ label: string;
80
+ kind: "text";
81
+ value: string;
82
+ } | {
83
+ key: string;
84
+ label: string;
85
+ kind: "list";
86
+ value: string[];
87
+ } | {
88
+ key: string;
89
+ label: string;
90
+ kind: "pairs";
91
+ value: Array<[string, string]>;
92
+ };
93
+ /** @experimental */
94
+ export declare const DEFAULT_CANVAS_LABELS: Record<string, string>;
95
+ /**
96
+ * Turn Canvas content into labeled, renderable entries. Empty fields are skipped; unknown fields keep a readable label.
97
+ * @experimental
98
+ */
99
+ export declare function canvasEntries(content: Record<string, unknown>, labels?: Record<string, string>): CanvasEntry[];
100
+ /**
101
+ * Renders Canvas fields as readable sections, never raw JSON. Themeable through --ys-* variables and className.
102
+ * @experimental
103
+ */
104
+ export declare function RoomCanvas({ detail, canvas, labels, className, title, emptyMessage, }: {
105
+ detail?: Pick<RoomDetail, "canvas">;
106
+ canvas?: Pick<RoomCanvasView, "content" | "version"> | null;
107
+ labels?: Record<string, string>;
108
+ className?: string;
109
+ title?: string;
110
+ emptyMessage?: string;
111
+ }): import("react").JSX.Element;
112
+ /** @experimental */
113
+ export type MentionAgent = {
114
+ alias: string;
115
+ label: string;
116
+ description: string;
117
+ };
118
+ /** @experimental */
119
+ export declare const DEFAULT_MENTION_AGENTS: MentionAgent[];
120
+ /** @experimental */
121
+ export declare function filterAgents(agents: MentionAgent[], query: string): MentionAgent[];
122
+ /**
123
+ * Keyboard model for the mention listbox: returns the next active index, a selection, or a close request.
124
+ * @experimental
125
+ */
126
+ export declare function mentionMenuKey(key: string, activeIndex: number, count: number): {
127
+ activeIndex: number;
128
+ select?: boolean;
129
+ close?: boolean;
130
+ handled: boolean;
131
+ };
132
+ /**
133
+ * Interactive @agent picker: a filterable listbox with arrow-key, Home/End,
134
+ * Enter/Tab and Escape handling. Controlled (activeIndex + onActiveIndexChange,
135
+ * for use inside a composer that keeps focus) or uncontrolled.
136
+ * @experimental
137
+ */
138
+ export declare function AgentMentionMenu({ agents, query, onSelect, onClose, activeIndex: controlledIndex, onActiveIndexChange, id, className, }: {
139
+ agents?: MentionAgent[];
140
+ query?: string;
141
+ onSelect?: (agent: MentionAgent) => void;
142
+ onClose?: () => void;
143
+ activeIndex?: number;
144
+ onActiveIndexChange?: (index: number) => void;
145
+ id?: string;
146
+ className?: string;
147
+ }): import("react").JSX.Element;
148
+ /**
149
+ * A persistent, honest way to reach a person. Shows the acknowledgement and expected response time once requested.
150
+ * @experimental
151
+ */
152
+ export declare function RequestHumanButton({ roomId, className }: {
153
+ roomId: string;
154
+ className?: string;
155
+ }): import("react").JSX.Element;
156
+ /** @experimental */
157
+ export declare const roomAgentStyles = ".ys-canvas{display:grid;gap:8px}.ys-canvas h4{margin:0;font-size:12px;font-weight:750}.ys-canvas dl{margin:0;display:grid;gap:10px}.ys-canvas dt{color:var(--ys-muted,#aaa6b2);font-size:10px;text-transform:uppercase;letter-spacing:.08em}.ys-canvas dd{margin:3px 0 0}.ys-canvas dd p{margin:0}.ys-canvas ul{margin:0;padding-left:18px}.ys-canvas__empty{margin:0;color:var(--ys-muted,#aaa6b2)}.ys-mentions{list-style:none;margin:6px 0 0;padding:4px;border:1px solid var(--ys-line,#2c2b34);border-radius:12px;background:var(--ys-panel,#19191f);max-width:360px}.ys-mentions li{display:grid;gap:2px;padding:8px 10px;border-radius:9px;cursor:pointer}.ys-mentions li.is-active{background:var(--ys-inset,#212129)}.ys-mentions small{color:var(--ys-muted,#aaa6b2)}.ys-mentions--empty{margin:6px 0 0;color:var(--ys-muted,#aaa6b2);font-size:12px}.ys-human{display:grid;gap:6px;margin-top:10px}.ys-human>button,.ys-human form button{justify-self:start;border:1px solid var(--ys-line,#2c2b34);border-radius:10px;background:transparent;color:var(--ys-text,#f4f1eb);padding:8px 11px;font-weight:700;cursor:pointer}.ys-human.is-requested{padding:10px;border-radius:12px;background:#18342c;color:var(--ys-mint,#6eddbb)}.ys-human p{margin:0}.ys-human form{display:grid;gap:8px}.ys-human form span{display:flex;gap:8px}.ys-human textarea{width:100%;min-height:60px;border:1px solid var(--ys-line,#2c2b34);border-radius:10px;background:#0e0e13;color:var(--ys-text,#f4f1eb);padding:8px}";
158
+ //# sourceMappingURL=rooms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rooms.d.ts","sourceRoot":"","sources":["../src/rooms.tsx"],"names":[],"mappings":"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"}
package/dist/rooms.js ADDED
@@ -0,0 +1,442 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useId, useRef, useState, } from "react";
4
+ import { useYouSaid } from "./context.js";
5
+ /* Headless hooks (PRD 12.3). Each hook works without the bundled components. */
6
+ const MAX_EVENTS = 500;
7
+ const TERMINAL_RUN_STATES = new Set(["completed", "failed", "cancelled", "approval_needed"]);
8
+ /**
9
+ * Live Room events over the realtime WebSocket: ordered by sequence, deduped,
10
+ * resumed from the last processed sequence after reconnects, and rebuilt from
11
+ * a snapshot when the client fell far behind. Falls back to HTTP catch-up
12
+ * polling where WebSocket is unavailable.
13
+ * @experimental
14
+ */
15
+ export function useRoomEvents(roomId, options = {}) {
16
+ const client = useYouSaid();
17
+ const [events, setEvents] = useState([]);
18
+ const [status, setStatus] = useState("idle");
19
+ const [lastSequence, setLastSequence] = useState(0);
20
+ const onEvent = useRef(options.onEvent);
21
+ onEvent.current = options.onEvent;
22
+ useEffect(() => {
23
+ if (!roomId)
24
+ return;
25
+ setEvents([]);
26
+ setLastSequence(0);
27
+ const handlers = {
28
+ onEvent: (event) => {
29
+ setEvents((current) => [...current, event].slice(-MAX_EVENTS));
30
+ setLastSequence(event.sequence);
31
+ onEvent.current?.(event);
32
+ },
33
+ onSnapshot: (snapshot) => {
34
+ setEvents(snapshot.events.slice(-MAX_EVENTS));
35
+ setLastSequence(snapshot.sequence);
36
+ },
37
+ onStatus: setStatus,
38
+ };
39
+ const realtime = client.realtime(roomId, handlers);
40
+ if (typeof globalThis.WebSocket === "function") {
41
+ realtime.start();
42
+ return () => realtime.stop();
43
+ }
44
+ let stopped = false;
45
+ let timer;
46
+ const poll = async () => {
47
+ try {
48
+ let page = await client.roomEvents(roomId, realtime.lastSequence);
49
+ realtime.ingest(page);
50
+ while (page.hasMore && !stopped) {
51
+ page = await client.roomEvents(roomId, page.nextSequence);
52
+ realtime.ingest(page);
53
+ }
54
+ setStatus("open");
55
+ }
56
+ catch {
57
+ setStatus("reconnecting");
58
+ }
59
+ if (!stopped)
60
+ timer = setTimeout(poll, options.pollMs ?? 5_000);
61
+ };
62
+ void poll();
63
+ return () => {
64
+ stopped = true;
65
+ if (timer)
66
+ clearTimeout(timer);
67
+ };
68
+ }, [client, roomId, options.pollMs]);
69
+ return { events, status, lastSequence };
70
+ }
71
+ /**
72
+ * One agent run, polled until it reaches a terminal state, with cancel.
73
+ * @experimental
74
+ */
75
+ export function useAgentRun(runId, options = {}) {
76
+ const client = useYouSaid();
77
+ const [run, setRun] = useState(null);
78
+ const [error, setError] = useState("");
79
+ const [loading, setLoading] = useState(Boolean(runId));
80
+ const refresh = useCallback(async () => {
81
+ if (!runId)
82
+ return null;
83
+ try {
84
+ const next = await client.getAgentRun(runId);
85
+ setRun(next);
86
+ setError("");
87
+ return next;
88
+ }
89
+ catch (reason) {
90
+ setError(reason instanceof Error ? reason.message : "The agent run is unavailable.");
91
+ return null;
92
+ }
93
+ finally {
94
+ setLoading(false);
95
+ }
96
+ }, [client, runId]);
97
+ useEffect(() => {
98
+ if (!runId) {
99
+ setRun(null);
100
+ setLoading(false);
101
+ return;
102
+ }
103
+ let stopped = false;
104
+ let timer;
105
+ const tick = async () => {
106
+ const next = await refresh();
107
+ if (!stopped && (!next || !TERMINAL_RUN_STATES.has(next.status)))
108
+ timer = setTimeout(tick, options.pollMs ?? 1_500);
109
+ };
110
+ setLoading(true);
111
+ void tick();
112
+ return () => { stopped = true; if (timer)
113
+ clearTimeout(timer); };
114
+ }, [runId, refresh, options.pollMs]);
115
+ const cancel = useCallback(async () => {
116
+ if (!runId)
117
+ return;
118
+ try {
119
+ setRun(await client.cancelAgentRun(runId));
120
+ }
121
+ catch (reason) {
122
+ setError(reason instanceof Error ? reason.message : "The run could not be cancelled.");
123
+ }
124
+ }, [client, runId]);
125
+ return { run, loading, error, refresh, cancel };
126
+ }
127
+ /**
128
+ * Actions a teammate is reviewing in Rooms you can see. Customers only see that a review is pending.
129
+ * @experimental
130
+ */
131
+ export function usePendingApprovals(roomId, options = {}) {
132
+ const client = useYouSaid();
133
+ const [approvals, setApprovals] = useState([]);
134
+ const [loading, setLoading] = useState(true);
135
+ const [error, setError] = useState("");
136
+ const refresh = useCallback(async () => {
137
+ try {
138
+ setApprovals(await client.listPendingApprovals(roomId));
139
+ setError("");
140
+ }
141
+ catch (reason) {
142
+ setError(reason instanceof Error ? reason.message : "Approvals could not be loaded.");
143
+ }
144
+ finally {
145
+ setLoading(false);
146
+ }
147
+ }, [client, roomId]);
148
+ useEffect(() => {
149
+ void refresh();
150
+ if (!options.pollMs)
151
+ return;
152
+ const timer = setInterval(() => void refresh(), options.pollMs);
153
+ return () => clearInterval(timer);
154
+ }, [refresh, options.pollMs]);
155
+ return { approvals, loading, error, refresh };
156
+ }
157
+ /**
158
+ * The customer-visible Room Canvas. Internal fields never reach the SDK.
159
+ * @experimental
160
+ */
161
+ export function useRoomCanvas(roomId) {
162
+ const client = useYouSaid();
163
+ const [canvas, setCanvas] = useState(null);
164
+ const [loading, setLoading] = useState(Boolean(roomId));
165
+ const [error, setError] = useState("");
166
+ const refresh = useCallback(async () => {
167
+ if (!roomId) {
168
+ setCanvas(null);
169
+ setLoading(false);
170
+ return;
171
+ }
172
+ setLoading(true);
173
+ try {
174
+ setCanvas(await client.getRoomCanvas(roomId));
175
+ setError("");
176
+ }
177
+ catch (reason) {
178
+ setError(reason instanceof Error ? reason.message : "The Canvas could not be loaded.");
179
+ }
180
+ finally {
181
+ setLoading(false);
182
+ }
183
+ }, [client, roomId]);
184
+ useEffect(() => { void refresh(); }, [refresh]);
185
+ return { canvas, loading, error, refresh };
186
+ }
187
+ /**
188
+ * Customers in the Room (staff identities are never exposed to the SDK).
189
+ * @experimental
190
+ */
191
+ export function useRoomMembers(roomId) {
192
+ const client = useYouSaid();
193
+ const [members, setMembers] = useState([]);
194
+ const [memberCount, setMemberCount] = useState(0);
195
+ const [loading, setLoading] = useState(Boolean(roomId));
196
+ const [error, setError] = useState("");
197
+ const refresh = useCallback(async () => {
198
+ if (!roomId) {
199
+ setMembers([]);
200
+ setMemberCount(0);
201
+ setLoading(false);
202
+ return;
203
+ }
204
+ setLoading(true);
205
+ try {
206
+ const result = await client.listRoomMembers(roomId);
207
+ setMembers(result.members);
208
+ setMemberCount(result.memberCount);
209
+ setError("");
210
+ }
211
+ catch (reason) {
212
+ setError(reason instanceof Error ? reason.message : "Members could not be loaded.");
213
+ }
214
+ finally {
215
+ setLoading(false);
216
+ }
217
+ }, [client, roomId]);
218
+ useEffect(() => { void refresh(); }, [refresh]);
219
+ return { members, memberCount, loading, error, refresh };
220
+ }
221
+ /**
222
+ * "Talk to a person": request, show the acknowledgement and SLA, or withdraw.
223
+ * @experimental
224
+ */
225
+ export function useRequestHuman(roomId) {
226
+ const client = useYouSaid();
227
+ const [handoff, setHandoff] = useState(null);
228
+ const [pending, setPending] = useState(false);
229
+ const [error, setError] = useState("");
230
+ const refresh = useCallback(async () => {
231
+ if (!roomId) {
232
+ setHandoff(null);
233
+ return;
234
+ }
235
+ try {
236
+ setHandoff(await client.getHumanHandoff(roomId));
237
+ }
238
+ catch { /* the button stays available; a request surfaces its own error */ }
239
+ }, [client, roomId]);
240
+ useEffect(() => { void refresh(); }, [refresh]);
241
+ const request = useCallback(async (reason = "") => {
242
+ if (!roomId)
243
+ return null;
244
+ setPending(true);
245
+ setError("");
246
+ try {
247
+ const next = await client.requestHuman(roomId, reason);
248
+ setHandoff(next);
249
+ return next;
250
+ }
251
+ catch (reason) {
252
+ setError(reason instanceof Error ? reason.message : "Your request could not be sent. Try again.");
253
+ return null;
254
+ }
255
+ finally {
256
+ setPending(false);
257
+ }
258
+ }, [client, roomId]);
259
+ const withdraw = useCallback(async () => {
260
+ if (!roomId)
261
+ return;
262
+ setPending(true);
263
+ setError("");
264
+ try {
265
+ await client.withdrawHumanHandoff(roomId);
266
+ setHandoff(null);
267
+ }
268
+ catch (reason) {
269
+ setError(reason instanceof Error ? reason.message : "The request could not be withdrawn.");
270
+ }
271
+ finally {
272
+ setPending(false);
273
+ }
274
+ }, [client, roomId]);
275
+ return { handoff, active: Boolean(handoff && (handoff.status === "open" || handoff.status === "acknowledged")), pending, error, request, withdraw, refresh };
276
+ }
277
+ /** @experimental */
278
+ export const DEFAULT_CANVAS_LABELS = {
279
+ purpose: "Why this Room exists",
280
+ outcome: "Done means",
281
+ currentState: "Where things stand",
282
+ confirmedFacts: "Confirmed",
283
+ hypotheses: "Working theories",
284
+ openQuestions: "Open questions",
285
+ affected: "Who is affected",
286
+ decisions: "Decisions",
287
+ promises: "Commitments",
288
+ actions: "Next actions",
289
+ release: "Release",
290
+ verification: "Verification",
291
+ canonicalAnswer: "Answer",
292
+ };
293
+ const CANVAS_ORDER = Object.keys(DEFAULT_CANVAS_LABELS);
294
+ function humanize(key) {
295
+ const spaced = key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim().toLowerCase();
296
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
297
+ }
298
+ function itemText(value) {
299
+ if (typeof value === "string")
300
+ return value;
301
+ if (typeof value === "number" || typeof value === "boolean")
302
+ return String(value);
303
+ if (value && typeof value === "object") {
304
+ const record = value;
305
+ const primary = record.title ?? record.statement ?? record.text ?? record.summary ?? record.name;
306
+ if (typeof primary === "string") {
307
+ const status = typeof record.status === "string" ? ` (${record.status.replaceAll("_", " ")})` : "";
308
+ return `${primary}${status}`;
309
+ }
310
+ return Object.entries(record).filter(([, item]) => item !== null && item !== undefined && item !== "").map(([key, item]) => `${humanize(key)}: ${itemText(item)}`).join(", ");
311
+ }
312
+ return "";
313
+ }
314
+ /**
315
+ * Turn Canvas content into labeled, renderable entries. Empty fields are skipped; unknown fields keep a readable label.
316
+ * @experimental
317
+ */
318
+ export function canvasEntries(content, labels = {}) {
319
+ const merged = { ...DEFAULT_CANVAS_LABELS, ...labels };
320
+ const keys = [...CANVAS_ORDER.filter((key) => key in content), ...Object.keys(content).filter((key) => !CANVAS_ORDER.includes(key) && key !== "fieldVisibility").sort()];
321
+ const entries = [];
322
+ for (const key of keys) {
323
+ const value = content[key];
324
+ const label = merged[key] ?? humanize(key);
325
+ if (value === null || value === undefined || value === "")
326
+ continue;
327
+ if (Array.isArray(value)) {
328
+ const items = value.map(itemText).filter(Boolean);
329
+ if (items.length)
330
+ entries.push({ key, label, kind: "list", value: items });
331
+ continue;
332
+ }
333
+ if (typeof value === "object") {
334
+ const record = value;
335
+ if (key === "verification") {
336
+ if (record.attempted !== true && !record.result)
337
+ continue;
338
+ entries.push({ key, label, kind: "text", value: record.attempted === true ? `Checked${record.result ? `: ${String(record.result).replaceAll("_", " ")}` : ""}${typeof record.note === "string" && record.note ? `. ${record.note}` : ""}` : String(record.result) });
339
+ continue;
340
+ }
341
+ const pairs = Object.entries(record).filter(([, item]) => item !== null && item !== undefined && item !== "").map(([name, item]) => [humanize(name), itemText(item)]).filter(([, item]) => item);
342
+ if (pairs.length)
343
+ entries.push({ key, label, kind: "pairs", value: pairs });
344
+ continue;
345
+ }
346
+ entries.push({ key, label, kind: "text", value: itemText(value) });
347
+ }
348
+ return entries;
349
+ }
350
+ /**
351
+ * Renders Canvas fields as readable sections, never raw JSON. Themeable through --ys-* variables and className.
352
+ * @experimental
353
+ */
354
+ export function RoomCanvas({ detail, canvas, labels, className = "", title = "Room outcome", emptyMessage = "The team has not filled in the Canvas yet.", }) {
355
+ const content = canvas?.content ?? detail?.canvas?.content ?? {};
356
+ const version = canvas?.version ?? detail?.canvas?.version;
357
+ const entries = canvasEntries(content, labels);
358
+ const headingId = useId();
359
+ return (_jsxs("section", { className: `ys-canvas ${className}`.trim(), "aria-labelledby": headingId, "data-canvas-version": version ?? undefined, children: [_jsx("h4", { id: headingId, children: title }), entries.length ? (_jsx("dl", { children: entries.map((entry) => (_jsxs("div", { "data-canvas-field": entry.key, children: [_jsx("dt", { children: entry.label }), _jsx("dd", { children: entry.kind === "text" ? (_jsx("p", { children: entry.value })) : entry.kind === "list" ? (_jsx("ul", { children: entry.value.map((item, index) => _jsx("li", { children: item }, `${entry.key}-${index}`)) })) : (_jsx("ul", { children: entry.value.map(([name, item]) => _jsxs("li", { children: [_jsx("strong", { children: name }), " ", item] }, `${entry.key}-${name}`)) })) })] }, entry.key))) })) : (_jsx("p", { className: "ys-canvas__empty", children: emptyMessage }))] }));
360
+ }
361
+ /** @experimental */
362
+ export const DEFAULT_MENTION_AGENTS = [
363
+ { alias: "support", label: "@support", description: "Answers from approved help and this Room" },
364
+ { alias: "product", label: "@product", description: "Summarizes needs, trade-offs and open questions" },
365
+ { alias: "incident", label: "@incident", description: "Structures what is known about an outage or bug" },
366
+ ];
367
+ /** @experimental */
368
+ export function filterAgents(agents, query) {
369
+ const needle = query.trim().toLowerCase().replace(/^@/, "");
370
+ return agents.filter((agent) => !needle || agent.alias.startsWith(needle) || agent.label.toLowerCase().includes(needle));
371
+ }
372
+ /**
373
+ * Keyboard model for the mention listbox: returns the next active index, a selection, or a close request.
374
+ * @experimental
375
+ */
376
+ export function mentionMenuKey(key, activeIndex, count) {
377
+ if (!count)
378
+ return { activeIndex: 0, handled: key === "Escape", ...(key === "Escape" ? { close: true } : {}) };
379
+ if (key === "ArrowDown")
380
+ return { activeIndex: (activeIndex + 1) % count, handled: true };
381
+ if (key === "ArrowUp")
382
+ return { activeIndex: (activeIndex - 1 + count) % count, handled: true };
383
+ if (key === "Home")
384
+ return { activeIndex: 0, handled: true };
385
+ if (key === "End")
386
+ return { activeIndex: count - 1, handled: true };
387
+ if (key === "Enter" || key === "Tab")
388
+ return { activeIndex, select: true, handled: true };
389
+ if (key === "Escape")
390
+ return { activeIndex, close: true, handled: true };
391
+ return { activeIndex, handled: false };
392
+ }
393
+ /**
394
+ * Interactive @agent picker: a filterable listbox with arrow-key, Home/End,
395
+ * Enter/Tab and Escape handling. Controlled (activeIndex + onActiveIndexChange,
396
+ * for use inside a composer that keeps focus) or uncontrolled.
397
+ * @experimental
398
+ */
399
+ export function AgentMentionMenu({ agents = DEFAULT_MENTION_AGENTS, query = "", onSelect, onClose, activeIndex: controlledIndex, onActiveIndexChange, id, className = "", }) {
400
+ const generatedId = useId();
401
+ const listId = id ?? `ys-mentions-${generatedId}`;
402
+ const matches = filterAgents(agents, query);
403
+ const [localIndex, setLocalIndex] = useState(0);
404
+ const activeIndex = Math.min(controlledIndex ?? localIndex, Math.max(0, matches.length - 1));
405
+ const setIndex = (index) => { setLocalIndex(index); onActiveIndexChange?.(index); };
406
+ function onKeyDown(event) {
407
+ const result = mentionMenuKey(event.key, activeIndex, matches.length);
408
+ if (!result.handled)
409
+ return;
410
+ event.preventDefault();
411
+ if (result.close) {
412
+ onClose?.();
413
+ return;
414
+ }
415
+ if (result.select && matches[activeIndex]) {
416
+ onSelect?.(matches[activeIndex]);
417
+ return;
418
+ }
419
+ setIndex(result.activeIndex);
420
+ }
421
+ if (!matches.length)
422
+ return _jsxs("p", { className: `ys-mentions ys-mentions--empty ${className}`.trim(), role: "status", children: ["No agent matches @", query, "."] });
423
+ return (_jsx("ul", { id: listId, role: "listbox", "aria-label": "Mention an agent", "aria-activedescendant": `${listId}-${matches[activeIndex]?.alias ?? ""}`, tabIndex: 0, className: `ys-mentions ${className}`.trim(), onKeyDown: onKeyDown, children: matches.map((agent, index) => (_jsxs("li", { id: `${listId}-${agent.alias}`, role: "option", "aria-selected": index === activeIndex, className: index === activeIndex ? "is-active" : "", onMouseEnter: () => setIndex(index), onMouseDown: (event) => { event.preventDefault(); onSelect?.(agent); }, children: [_jsx("strong", { children: agent.label }), _jsx("small", { children: agent.description })] }, agent.alias))) }));
424
+ }
425
+ /**
426
+ * A persistent, honest way to reach a person. Shows the acknowledgement and expected response time once requested.
427
+ * @experimental
428
+ */
429
+ export function RequestHumanButton({ roomId, className = "" }) {
430
+ const human = useRequestHuman(roomId);
431
+ const [asking, setAsking] = useState(false);
432
+ const [reason, setReason] = useState("");
433
+ if (human.active && human.handoff) {
434
+ return (_jsxs("div", { className: `ys-human is-requested ${className}`.trim(), role: "status", "aria-live": "polite", children: [_jsx("strong", { children: human.handoff.acknowledged ? "A teammate has your request" : "A person has been asked to help" }), _jsx("p", { children: human.handoff.message }), _jsx("button", { type: "button", onClick: () => void human.withdraw(), disabled: human.pending, children: "Withdraw request" }), human.error ? _jsx("p", { role: "alert", children: human.error }) : null] }));
435
+ }
436
+ return (_jsxs("div", { className: `ys-human ${className}`.trim(), children: [asking ? (_jsxs("form", { onSubmit: (event) => { event.preventDefault(); void human.request(reason).then((result) => { if (result) {
437
+ setAsking(false);
438
+ setReason("");
439
+ } }); }, children: [_jsxs("label", { children: ["What should the team know? (optional)", _jsx("textarea", { value: reason, maxLength: 1_000, onChange: (event) => setReason(event.target.value) })] }), _jsxs("span", { children: [_jsx("button", { type: "submit", disabled: human.pending, children: human.pending ? "Sending…" : "Ask for a person" }), _jsx("button", { type: "button", onClick: () => setAsking(false), children: "Cancel" })] })] })) : (_jsx("button", { type: "button", onClick: () => setAsking(true), "aria-haspopup": "dialog", children: "Talk to a person" })), human.error ? _jsx("p", { role: "alert", children: human.error }) : null] }));
440
+ }
441
+ /** @experimental */
442
+ export const roomAgentStyles = `.ys-canvas{display:grid;gap:8px}.ys-canvas h4{margin:0;font-size:12px;font-weight:750}.ys-canvas dl{margin:0;display:grid;gap:10px}.ys-canvas dt{color:var(--ys-muted,#aaa6b2);font-size:10px;text-transform:uppercase;letter-spacing:.08em}.ys-canvas dd{margin:3px 0 0}.ys-canvas dd p{margin:0}.ys-canvas ul{margin:0;padding-left:18px}.ys-canvas__empty{margin:0;color:var(--ys-muted,#aaa6b2)}.ys-mentions{list-style:none;margin:6px 0 0;padding:4px;border:1px solid var(--ys-line,#2c2b34);border-radius:12px;background:var(--ys-panel,#19191f);max-width:360px}.ys-mentions li{display:grid;gap:2px;padding:8px 10px;border-radius:9px;cursor:pointer}.ys-mentions li.is-active{background:var(--ys-inset,#212129)}.ys-mentions small{color:var(--ys-muted,#aaa6b2)}.ys-mentions--empty{margin:6px 0 0;color:var(--ys-muted,#aaa6b2);font-size:12px}.ys-human{display:grid;gap:6px;margin-top:10px}.ys-human>button,.ys-human form button{justify-self:start;border:1px solid var(--ys-line,#2c2b34);border-radius:10px;background:transparent;color:var(--ys-text,#f4f1eb);padding:8px 11px;font-weight:700;cursor:pointer}.ys-human.is-requested{padding:10px;border-radius:12px;background:#18342c;color:var(--ys-mint,#6eddbb)}.ys-human p{margin:0}.ys-human form{display:grid;gap:8px}.ys-human form span{display:flex;gap:8px}.ys-human textarea{width:100%;min-height:60px;border:1px solid var(--ys-line,#2c2b34);border-radius:10px;background:#0e0e13;color:var(--ys-text,#f4f1eb);padding:8px}`;
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@sansavision/yousaid-react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "React 19 components and hooks for YouSaid: provider, launcher, inbox and lifecycle receipts.",
5
+ "private": false,
6
+ "license": "UNLICENSED",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "sideEffects": false,
24
+ "scripts": {
25
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=19"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "react": {
32
+ "optional": true
33
+ }
34
+ },
35
+ "dependencies": {
36
+ "@sansavision/yousaid": "^0.2.0"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/Sansa-Organisation/yousaid.git",
41
+ "directory": "packages/react"
42
+ },
43
+ "homepage": "https://yousaid.dev/docs/sdk",
44
+ "engines": {
45
+ "node": ">=20.0.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }