@nxisai/agent-harness 0.7.0 → 0.9.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.
- package/LICENSE +201 -0
- package/README.md +17 -5
- package/dist/a2ui/cards.d.ts +3 -0
- package/dist/a2ui/cards.d.ts.map +1 -1
- package/dist/a2ui/cards.js +11 -2
- package/dist/a2ui/cards.js.map +1 -1
- package/dist/harness/cta.d.ts +27 -0
- package/dist/harness/cta.d.ts.map +1 -0
- package/dist/harness/cta.js +43 -0
- package/dist/harness/cta.js.map +1 -0
- package/dist/harness/harness.d.ts +33 -0
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +84 -2
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/recommend.d.ts +46 -0
- package/dist/harness/recommend.d.ts.map +1 -0
- package/dist/harness/recommend.js +144 -0
- package/dist/harness/recommend.js.map +1 -0
- package/dist/harness/records.d.ts +70 -0
- package/dist/harness/records.d.ts.map +1 -0
- package/dist/harness/records.js +209 -0
- package/dist/harness/records.js.map +1 -0
- package/dist/harness/renderer.d.ts +11 -1
- package/dist/harness/renderer.d.ts.map +1 -1
- package/dist/harness/router/coordinator.d.ts +6 -0
- package/dist/harness/router/coordinator.d.ts.map +1 -1
- package/dist/harness/router/coordinator.js +14 -0
- package/dist/harness/router/coordinator.js.map +1 -1
- package/dist/harness/tools/action-tools.d.ts +25 -0
- package/dist/harness/tools/action-tools.d.ts.map +1 -1
- package/dist/harness/tools/action-tools.js +39 -0
- package/dist/harness/tools/action-tools.js.map +1 -1
- package/dist/harness/tools/inbound.d.ts +88 -0
- package/dist/harness/tools/inbound.d.ts.map +1 -0
- package/dist/harness/tools/inbound.js +156 -0
- package/dist/harness/tools/inbound.js.map +1 -0
- package/dist/harness/tools/site-pages.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/ui/agent-fullscreen.d.ts.map +1 -1
- package/dist/ui/agent-fullscreen.js +154 -76
- package/dist/ui/agent-fullscreen.js.map +1 -1
- package/dist/ui/result-pick.d.ts +52 -0
- package/dist/ui/result-pick.d.ts.map +1 -0
- package/dist/ui/result-pick.js +196 -0
- package/dist/ui/result-pick.js.map +1 -0
- package/package.json +1 -1
- package/src/a2ui/cards.ts +14 -2
- package/src/harness/cta.ts +54 -0
- package/src/harness/harness.ts +93 -2
- package/src/harness/recommend.ts +194 -0
- package/src/harness/records.ts +237 -0
- package/src/harness/renderer.ts +12 -1
- package/src/harness/router/coordinator.ts +14 -0
- package/src/harness/tools/action-tools.ts +41 -0
- package/src/harness/tools/inbound.ts +189 -0
- package/src/harness/tools/site-pages.ts +1 -1
- package/src/index.ts +16 -0
- package/src/ui/agent-fullscreen.ts +162 -66
- package/src/ui/result-pick.ts +233 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search results as an answer: a recommended pick with its reasons, the
|
|
3
|
+
* others to compare against, or, when there is nothing to recommend, a short
|
|
4
|
+
* list that opens up per result.
|
|
5
|
+
*
|
|
6
|
+
* The pick comes from the harness (`recommend`): the model chose it and wrote
|
|
7
|
+
* the lead and reasons, and only reasons the record backs survived. Those are
|
|
8
|
+
* the only lines marked with a tick. Comparing another result shows that
|
|
9
|
+
* result's own facts, plainly, without a tick: the agent did not recommend it.
|
|
10
|
+
*
|
|
11
|
+
* Plain DOM, every string set as text.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { ResultPick } from '../harness/recommend.js';
|
|
16
|
+
|
|
17
|
+
/** One result, as the answer shows it. */
|
|
18
|
+
export interface AnswerCard {
|
|
19
|
+
title: string;
|
|
20
|
+
stars?: number;
|
|
21
|
+
reviews?: number;
|
|
22
|
+
status?: { open: boolean; label: string };
|
|
23
|
+
phone?: string;
|
|
24
|
+
page?: { url: string; label: string };
|
|
25
|
+
/** What the record states, short: hours, rating, services, since when. */
|
|
26
|
+
facts: string[];
|
|
27
|
+
/** A site form that can be sent to it, and what to ask the agent to start it. */
|
|
28
|
+
form?: { label: string; message: string };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What the answer can ask the conversation to do. */
|
|
32
|
+
export interface AnswerHandlers {
|
|
33
|
+
/** Send a message as the visitor: "Get a quote from Hatzel & Sons Plumbing". */
|
|
34
|
+
ask(text: string): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Child = Node | string | null | undefined | false;
|
|
38
|
+
|
|
39
|
+
function h<K extends keyof HTMLElementTagNameMap>(tag: K, attrs: Record<string, string | undefined> = {}, ...children: Child[]): HTMLElementTagNameMap[K] {
|
|
40
|
+
const el = document.createElement(tag);
|
|
41
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
42
|
+
if (v === undefined) continue;
|
|
43
|
+
if (k === 'class') el.className = v;
|
|
44
|
+
else el.setAttribute(k, v);
|
|
45
|
+
}
|
|
46
|
+
for (const c of children) if (c) el.append(c);
|
|
47
|
+
return el;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PALETTE = ['#0E4F5C', '#1D4ED8', '#6D28D9', '#9A3412', '#0F766E', '#B45309', '#334155'];
|
|
51
|
+
|
|
52
|
+
/** Up to two letters, and a colour that stays the same for a name. */
|
|
53
|
+
function avatar(name: string, size: 'lg' | 'sm'): HTMLElement {
|
|
54
|
+
const words = name.split(/\s+/).filter((w) => /^[\p{L}\p{N}]/u.test(w));
|
|
55
|
+
const ini = (words.slice(0, 2).map((w) => w[0]).join('') || '?').toUpperCase();
|
|
56
|
+
let hash = 0;
|
|
57
|
+
for (const ch of name) hash = (hash * 31 + ch.charCodeAt(0)) >>> 0;
|
|
58
|
+
const el = h('span', { class: `ra-avatar ${size}`, 'aria-hidden': 'true' }, ini);
|
|
59
|
+
el.style.background = PALETTE[hash % PALETTE.length];
|
|
60
|
+
return el;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** "★ 4.7 (412) · Open now · 24/7". */
|
|
64
|
+
function meta(c: AnswerCard): HTMLElement {
|
|
65
|
+
const line = h('span', { class: 'ra-meta' });
|
|
66
|
+
if (c.stars !== undefined) {
|
|
67
|
+
line.append(h('span', {}, `★ `, h('strong', {}, String(c.stars))));
|
|
68
|
+
if (c.reviews !== undefined) line.append(h('span', { class: 'ra-faint' }, ` (${c.reviews})`));
|
|
69
|
+
}
|
|
70
|
+
if (c.status) {
|
|
71
|
+
if (line.childNodes.length) line.append(' · ');
|
|
72
|
+
line.append(h('span', { class: c.status.open ? 'ra-open' : '' }, c.status.label));
|
|
73
|
+
}
|
|
74
|
+
return line;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function actions(c: AnswerCard, handlers: AnswerHandlers, primary: boolean): HTMLElement {
|
|
78
|
+
const row = h('div', { class: 'ra-actions' });
|
|
79
|
+
const tel = c.phone ? c.phone.replace(/[^\d+]/g, '') : '';
|
|
80
|
+
if (tel) row.append(h('a', { class: `ra-btn ${primary ? 'primary' : ''}`, href: `tel:${tel}` }, `Call ${c.phone}`));
|
|
81
|
+
if (c.form) {
|
|
82
|
+
const b = h('button', { type: 'button', class: `ra-btn ${primary && !tel ? 'primary' : ''}` }, c.form.label);
|
|
83
|
+
b.addEventListener('click', () => handlers.ask(c.form!.message));
|
|
84
|
+
row.append(b);
|
|
85
|
+
}
|
|
86
|
+
if (c.page) row.append(h('a', { class: 'ra-btn', href: c.page.url }, 'Their page'));
|
|
87
|
+
return row;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Draw the answer.
|
|
92
|
+
*
|
|
93
|
+
* @param cards - The results, in order.
|
|
94
|
+
* @param pick - The recommendation, when the harness made one.
|
|
95
|
+
*/
|
|
96
|
+
export function resultsAnswer(cards: AnswerCard[], pick: ResultPick | undefined, handlers: AnswerHandlers): HTMLElement {
|
|
97
|
+
const wrap = h('div', { class: 'msg assistant results-answer' });
|
|
98
|
+
const valid = pick && cards[pick.index] ? pick : undefined;
|
|
99
|
+
if (!valid) {
|
|
100
|
+
wrap.append(list(cards, handlers, 0));
|
|
101
|
+
return wrap;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let showing = valid.index;
|
|
105
|
+
const body = h('div', { class: 'ra-body' });
|
|
106
|
+
const draw = () => {
|
|
107
|
+
const c = cards[showing];
|
|
108
|
+
const mine = showing === valid.index;
|
|
109
|
+
const box = h('div', { class: `ra-pick ${mine ? 'is-pick' : ''}` });
|
|
110
|
+
const head = h('div', { class: 'ra-head' }, avatar(c.title, 'lg'), h('div', { class: 'ra-who' }, h('strong', {}, c.title), meta(c)));
|
|
111
|
+
box.append(head);
|
|
112
|
+
// Ticks only for what the agent recommended, and could back from the record.
|
|
113
|
+
const lines = mine ? valid.reasons : c.facts;
|
|
114
|
+
if (lines.length) {
|
|
115
|
+
const ul = h('ul', { class: `ra-reasons ${mine ? 'ticked' : ''}` });
|
|
116
|
+
lines.forEach((l) => ul.append(h('li', {}, h('span', { class: 'ra-mark', 'aria-hidden': 'true' }, mine ? '✓' : '•'), h('span', {}, l))));
|
|
117
|
+
box.append(ul);
|
|
118
|
+
}
|
|
119
|
+
box.append(actions(c, handlers, true));
|
|
120
|
+
|
|
121
|
+
const lead = mine ? h('p', { class: 'ra-lead' }, valid.lead) : h('p', { class: 'ra-lead' }, `Comparing ${c.title} with my pick, ${cards[valid.index].title}. `);
|
|
122
|
+
if (!mine) {
|
|
123
|
+
const back = h('button', { type: 'button', class: 'ra-link' }, 'Back to my pick');
|
|
124
|
+
back.addEventListener('click', () => {
|
|
125
|
+
showing = valid.index;
|
|
126
|
+
draw();
|
|
127
|
+
});
|
|
128
|
+
lead.append(back);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const others = cards.map((o, i) => ({ o, i })).filter(({ i }) => i !== showing).slice(0, 3);
|
|
132
|
+
const also = h('div', { class: 'ra-also' }, h('div', { class: 'ra-eyebrow' }, mine ? 'Also good · tap to compare' : 'Others'));
|
|
133
|
+
for (const { o, i } of others) {
|
|
134
|
+
const row = h('button', { type: 'button', class: 'ra-row' }, avatar(o.title, 'sm'), h('span', { class: 'ra-who' }, h('strong', {}, o.title), meta(o)), h('span', { class: 'ra-chev', 'aria-hidden': 'true' }, '›'));
|
|
135
|
+
row.addEventListener('click', () => {
|
|
136
|
+
showing = i;
|
|
137
|
+
draw();
|
|
138
|
+
});
|
|
139
|
+
also.append(row);
|
|
140
|
+
}
|
|
141
|
+
body.replaceChildren(lead, box, ...(others.length ? [also] : []));
|
|
142
|
+
if (cards.length > 4) {
|
|
143
|
+
const all = h('button', { type: 'button', class: 'ra-link' }, `Show all ${cards.length}`);
|
|
144
|
+
all.addEventListener('click', () => body.replaceChildren(list(cards, handlers, valid.index)));
|
|
145
|
+
body.append(all);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
draw();
|
|
149
|
+
wrap.append(body);
|
|
150
|
+
return wrap;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The plain list: one line per result, each opening to its facts and actions. */
|
|
154
|
+
function list(cards: AnswerCard[], handlers: AnswerHandlers, open: number | null): HTMLElement {
|
|
155
|
+
const box = h('div', { class: 'ra-list' });
|
|
156
|
+
const SHOWN = 5;
|
|
157
|
+
let all = cards.length <= SHOWN + 1;
|
|
158
|
+
let openIndex = open;
|
|
159
|
+
const draw = () => {
|
|
160
|
+
box.replaceChildren();
|
|
161
|
+
cards.slice(0, all ? cards.length : SHOWN).forEach((c, i) => {
|
|
162
|
+
const expanded = openIndex === i;
|
|
163
|
+
const item = h('div', { class: 'ra-item' });
|
|
164
|
+
const id = `ra-${i}-${Math.random().toString(36).slice(2, 7)}`;
|
|
165
|
+
const row = h('button', { type: 'button', class: 'ra-row plain', 'aria-expanded': String(expanded), 'aria-controls': id }, avatar(c.title, 'sm'), h('span', { class: 'ra-who' }, h('strong', {}, c.title), meta(c)), h('span', { class: 'ra-chev', 'aria-hidden': 'true' }, expanded ? '−' : '+'));
|
|
166
|
+
row.addEventListener('click', () => {
|
|
167
|
+
openIndex = expanded ? null : i;
|
|
168
|
+
draw();
|
|
169
|
+
});
|
|
170
|
+
item.append(row);
|
|
171
|
+
if (expanded) {
|
|
172
|
+
const more = h('div', { class: 'ra-more', id });
|
|
173
|
+
if (c.facts.length) {
|
|
174
|
+
const ul = h('ul', { class: 'ra-reasons' });
|
|
175
|
+
c.facts.forEach((f) => ul.append(h('li', {}, h('span', { class: 'ra-mark', 'aria-hidden': 'true' }, '•'), h('span', {}, f))));
|
|
176
|
+
more.append(ul);
|
|
177
|
+
}
|
|
178
|
+
more.append(actions(c, handlers, false));
|
|
179
|
+
item.append(more);
|
|
180
|
+
}
|
|
181
|
+
box.append(item);
|
|
182
|
+
});
|
|
183
|
+
if (!all) {
|
|
184
|
+
const more = h('button', { type: 'button', class: 'ra-showmore' }, `Show ${cards.length - SHOWN} more`);
|
|
185
|
+
more.addEventListener('click', () => {
|
|
186
|
+
all = true;
|
|
187
|
+
draw();
|
|
188
|
+
});
|
|
189
|
+
box.append(more);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
draw();
|
|
193
|
+
return box;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Styles, on the widget's tokens. */
|
|
197
|
+
export const RESULTS_ANSWER_CSS = `
|
|
198
|
+
.results-answer { align-self: flex-start; width: min(100%, 440px); box-sizing: border-box; padding: 12px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: 16px 16px 16px 4px; font-size: 14px; line-height: 1.5; color: var(--ink); white-space: normal; }
|
|
199
|
+
.ra-body { display: flex; flex-direction: column; gap: 12px; }
|
|
200
|
+
.ra-lead { margin: 0; }
|
|
201
|
+
.ra-pick { background: var(--bg); border: 1px solid var(--border-strong); border-radius: 14px; padding: 12px; display: flex; flex-direction: column; gap: 10px; }
|
|
202
|
+
.ra-pick.is-pick { border: 1.5px solid var(--blue); box-shadow: 0 0 0 4px var(--blue-tint); }
|
|
203
|
+
.ra-head { display: flex; align-items: center; gap: 10px; }
|
|
204
|
+
.ra-avatar { flex-shrink: 0; display: grid; place-items: center; color: #fff; font-weight: 700; letter-spacing: .02em; }
|
|
205
|
+
.ra-avatar.lg { width: 40px; height: 40px; border-radius: 12px; font-size: 13px; }
|
|
206
|
+
.ra-avatar.sm { width: 30px; height: 30px; border-radius: 9px; font-size: 11px; }
|
|
207
|
+
.ra-who { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex-grow: 1; text-align: left; }
|
|
208
|
+
.ra-who strong { font-size: 14.5px; font-weight: 650; }
|
|
209
|
+
.ra-meta { font-size: 12.5px; color: var(--ink-2); }
|
|
210
|
+
.ra-meta strong { color: var(--ink); font-size: 12.5px; font-weight: 600; }
|
|
211
|
+
.ra-faint { color: var(--ink-3); }
|
|
212
|
+
.ra-open { color: var(--green-strong); font-weight: 600; }
|
|
213
|
+
.ra-reasons { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--ink-2); }
|
|
214
|
+
.ra-reasons li { display: flex; gap: 8px; align-items: flex-start; }
|
|
215
|
+
.ra-mark { width: 18px; height: 18px; flex-shrink: 0; border-radius: 50%; display: grid; place-items: center; font-size: 11px; font-weight: 700; color: var(--ink-3); }
|
|
216
|
+
.ra-reasons.ticked .ra-mark { background: var(--green-tint); color: var(--green-strong); }
|
|
217
|
+
.ra-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
218
|
+
.ra-btn { min-height: 40px; padding: 0 14px; border-radius: var(--radius-sm); border: 1px solid var(--border-strong); background: var(--bg); color: var(--ink); font: 600 13.5px var(--sans); display: inline-flex; align-items: center; justify-content: center; text-decoration: none; cursor: pointer; }
|
|
219
|
+
.ra-btn.primary { flex-grow: 1; border: 0; background: var(--blue); color: #fff; }
|
|
220
|
+
.ra-btn.primary:hover { background: var(--blue-strong); }
|
|
221
|
+
.ra-also { display: flex; flex-direction: column; gap: 6px; }
|
|
222
|
+
.ra-eyebrow { font-family: var(--mono); font-size: 10.5px; font-weight: 500; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3); }
|
|
223
|
+
.ra-row { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg); color: var(--ink); font: inherit; cursor: pointer; }
|
|
224
|
+
.ra-row:hover { border-color: var(--border-strong); }
|
|
225
|
+
.ra-row.plain { border: 0; border-radius: 0; padding: 10px 12px; }
|
|
226
|
+
.ra-chev { color: var(--ink-3); font-size: 17px; line-height: 1; }
|
|
227
|
+
.ra-link { align-self: flex-start; border: 0; background: none; padding: 0; font: 600 13px var(--sans); color: var(--blue-strong); cursor: pointer; }
|
|
228
|
+
.ra-list { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
|
|
229
|
+
.ra-item + .ra-item { border-top: 1px solid var(--border); }
|
|
230
|
+
.ra-more { padding: 0 12px 12px 52px; display: flex; flex-direction: column; gap: 8px; }
|
|
231
|
+
.ra-showmore { width: 100%; min-height: 40px; border: 0; border-top: 1px solid var(--border); background: var(--bg); color: var(--blue-strong); font: 600 13px var(--sans); cursor: pointer; }
|
|
232
|
+
.ra-btn:focus-visible, .ra-row:focus-visible, .ra-link:focus-visible, .ra-showmore:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
|
|
233
|
+
`;
|