@alexkroman1/aai-cli 5.3.0 → 5.5.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/README.md +51 -0
- package/dist/{_agent-D8zBb3M5.mjs → _agent-DMyOab9_.mjs} +2 -2
- package/dist/{_bundler-DFS4xxqE.mjs → _bundler-Cjaxa2wi.mjs} +1 -1
- package/dist/{_config-D3F9km8X.mjs → _config-5AEqhh-O.mjs} +2 -2
- package/dist/{_dev-server-BBUWo0sb.mjs → _dev-server-vV05Fnki.mjs} +3 -3
- package/dist/{_init-DU-sXH6S.mjs → _init-BZ9t_Kz-.mjs} +3 -56
- package/dist/{_slug-api-19R5kZ0U.mjs → _slug-api-DaqQJHk8.mjs} +1 -1
- package/dist/_templates-Bv8CR800.mjs +69 -0
- package/dist/_templates.d.ts +6 -0
- package/dist/{build-Xq1xxoSg.mjs → build-D_PgQOD4.mjs} +1 -1
- package/dist/cli.mjs +59 -17
- package/dist/{client-bundler-BWDkUeEP.mjs → client-bundler-yiWoXrgb.mjs} +3 -0
- package/dist/client-bundler.d.ts +9 -0
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-kSXFLxek.mjs → delete-DXilFBb1.mjs} +1 -1
- package/dist/{deploy-Ci0X1gp2.mjs → deploy-1eaXcfUw.mjs} +3 -3
- package/dist/{dev-g7sGt8qX.mjs → dev-gVNdGFYY.mjs} +1 -1
- package/dist/{init-BjeK8crW.mjs → init-DoU4_txp.mjs} +3 -3
- package/dist/login-C59ZHzuO.mjs +109 -0
- package/dist/login.d.ts +34 -0
- package/dist/scaffold/CLAUDE.md +190 -120
- package/dist/scaffold/package.json +3 -3
- package/dist/{secret-DOva9OGk.mjs → secret-CGAIAbUx.mjs} +1 -1
- package/dist/{storage-vM6HjDZZ.mjs → storage-CnhOayhm.mjs} +1 -1
- package/dist/templates/code-interpreter/agent.ts +1 -2
- package/dist/templates/dispatch-center/agent.test.ts +3 -3
- package/dist/templates/dispatch-center/agent.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +7 -2
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_create.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_get.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_triage.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +1 -1
- package/dist/templates/embedded-assets/agent.ts +2 -3
- package/dist/templates/health-assistant/agent.ts +3 -4
- package/dist/templates/infocom-adventure/agent.ts +10 -12
- package/dist/templates/infocom-adventure/client.tsx +5 -5
- package/dist/templates/math-buddy/agent.ts +7 -6
- package/dist/templates/night-owl/agent.ts +2 -3
- package/dist/templates/personal-finance/agent.ts +1 -2
- package/dist/templates/pipeline-simple/agent.test.ts +13 -13
- package/dist/templates/pipeline-simple/agent.ts +5 -6
- package/dist/templates/pizza-ordering/agent.test.ts +13 -11
- package/dist/templates/pizza-ordering/agent.ts +5 -6
- package/dist/templates/retail/address.ts +29 -0
- package/dist/templates/retail/agent.test.ts +1030 -0
- package/dist/templates/retail/agent.ts +71 -0
- package/dist/templates/retail/authenticate.ts +34 -0
- package/dist/templates/retail/client.tsx +459 -0
- package/dist/templates/retail/refund.ts +19 -0
- package/dist/templates/retail/registry.test.ts +182 -0
- package/dist/templates/retail/resolve.test.ts +158 -0
- package/dist/templates/retail/resolve.ts +158 -0
- package/dist/templates/retail/seed.json +4260 -0
- package/dist/templates/retail/seed.test.ts +224 -0
- package/dist/templates/retail/shared.test.ts +192 -0
- package/dist/templates/retail/shared.ts +430 -0
- package/dist/templates/retail/store.test.ts +256 -0
- package/dist/templates/retail/store.ts +243 -0
- package/dist/templates/retail/swap.test.ts +186 -0
- package/dist/templates/retail/swap.ts +138 -0
- package/dist/templates/retail/system-prompt.md +111 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +82 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +83 -0
- package/dist/templates/retail/tools/find_user_id_by_email.ts +32 -0
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +38 -0
- package/dist/templates/retail/tools/get_item_details.ts +29 -0
- package/dist/templates/retail/tools/get_order_details.ts +41 -0
- package/dist/templates/retail/tools/get_product_details.ts +33 -0
- package/dist/templates/retail/tools/get_user_details.ts +44 -0
- package/dist/templates/retail/tools/list_all_product_types.ts +24 -0
- package/dist/templates/retail/tools/modify_pending_order_address.ts +46 -0
- package/dist/templates/retail/tools/modify_pending_order_items.ts +102 -0
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +103 -0
- package/dist/templates/retail/tools/modify_user_address.ts +38 -0
- package/dist/templates/retail/tools/return_delivered_order_items.ts +101 -0
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +22 -0
- package/dist/templates/simple/agent.test.ts +18 -0
- package/dist/templates/simple/agent.ts +3 -0
- package/dist/templates/solo-rpg/agent.test.ts +2 -2
- package/dist/templates/solo-rpg/agent.ts +1 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +1 -1
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +1 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +1 -1
- package/dist/templates/web-researcher/agent.ts +1 -2
- package/dist/typecheck.d.ts +18 -0
- package/dist/typecheck.mjs +8 -0
- package/dist/worker-bundler.d.ts +9 -1
- package/dist/worker-bundler.mjs +8 -0
- package/package.json +3 -3
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { agent } from "@alexkroman1/aai";
|
|
2
|
+
import { assemblyAIStt } from "@alexkroman1/aai/stt";
|
|
3
|
+
import type { StateSlot } from "./shared.ts";
|
|
4
|
+
import { storeView } from "./shared.ts";
|
|
5
|
+
import { createDefaultState } from "./store.ts";
|
|
6
|
+
import systemPrompt from "./system-prompt.md?raw";
|
|
7
|
+
import { cancelPendingOrder } from "./tools/cancel_pending_order.ts";
|
|
8
|
+
import { exchangeDeliveredOrderItems } from "./tools/exchange_delivered_order_items.ts";
|
|
9
|
+
import { findUserIdByEmail } from "./tools/find_user_id_by_email.ts";
|
|
10
|
+
import { findUserIdByNameZip } from "./tools/find_user_id_by_name_zip.ts";
|
|
11
|
+
import { getItemDetails } from "./tools/get_item_details.ts";
|
|
12
|
+
import { getOrderDetails } from "./tools/get_order_details.ts";
|
|
13
|
+
import { getProductDetails } from "./tools/get_product_details.ts";
|
|
14
|
+
import { getUserDetails } from "./tools/get_user_details.ts";
|
|
15
|
+
import { listAllProductTypes } from "./tools/list_all_product_types.ts";
|
|
16
|
+
import { modifyPendingOrderAddress } from "./tools/modify_pending_order_address.ts";
|
|
17
|
+
import { modifyPendingOrderItems } from "./tools/modify_pending_order_items.ts";
|
|
18
|
+
import { modifyPendingOrderPayment } from "./tools/modify_pending_order_payment.ts";
|
|
19
|
+
import { modifyUserAddress } from "./tools/modify_user_address.ts";
|
|
20
|
+
import { returnDeliveredOrderItems } from "./tools/return_delivered_order_items.ts";
|
|
21
|
+
import { transferToHumanAgents } from "./tools/transfer_to_human_agents.ts";
|
|
22
|
+
|
|
23
|
+
export default agent({
|
|
24
|
+
name: "Retail Support",
|
|
25
|
+
|
|
26
|
+
// No provider spread: pipeline mode is the default, and an unset stage is
|
|
27
|
+
// filled from the all-AssemblyAI pipeline at parse time. Only `stt` is
|
|
28
|
+
// overridden below; `llm` and `tts` take the defaults.
|
|
29
|
+
|
|
30
|
+
// The store lives in ctx.state, one pristine copy per session — callers must
|
|
31
|
+
// not see each other's cancellations. Wrapped in the `StateSlot` shape
|
|
32
|
+
// `store.ts`'s `getState`/`shared.ts`'s `storeView` both key off — `agent()`
|
|
33
|
+
// infers its session-state type parameter from BOTH `state` and `syncState`,
|
|
34
|
+
// and `StateSlot`'s `retail` field is optional (a "weak type"), so handing
|
|
35
|
+
// it a bare `RetailState` with no properties in common fails that inference
|
|
36
|
+
// rather than an ordinary structural check.
|
|
37
|
+
state: (): StateSlot => ({ retail: createDefaultState() }),
|
|
38
|
+
|
|
39
|
+
// One projection pushed after every tool call. It is a projection, not a
|
|
40
|
+
// flag, because the state holds all six seeded customers and only the
|
|
41
|
+
// authenticated one may reach the browser.
|
|
42
|
+
syncState: storeView,
|
|
43
|
+
|
|
44
|
+
// Callers read order numbers and ten-digit item numbers in bursts with
|
|
45
|
+
// pauses inside one utterance, so end-of-turn silence has to be longer than
|
|
46
|
+
// the default or "W seven six seven … eight oh seven two" splits in two.
|
|
47
|
+
stt: assemblyAIStt({ minTurnSilenceMs: 2200 }),
|
|
48
|
+
|
|
49
|
+
systemPrompt,
|
|
50
|
+
greeting:
|
|
51
|
+
"Thanks for calling. Before I can look anything up I'll need to find your account — " +
|
|
52
|
+
"what's the email address on it?",
|
|
53
|
+
|
|
54
|
+
tools: {
|
|
55
|
+
cancel_pending_order: cancelPendingOrder,
|
|
56
|
+
exchange_delivered_order_items: exchangeDeliveredOrderItems,
|
|
57
|
+
find_user_id_by_email: findUserIdByEmail,
|
|
58
|
+
find_user_id_by_name_zip: findUserIdByNameZip,
|
|
59
|
+
get_item_details: getItemDetails,
|
|
60
|
+
get_order_details: getOrderDetails,
|
|
61
|
+
get_product_details: getProductDetails,
|
|
62
|
+
get_user_details: getUserDetails,
|
|
63
|
+
list_all_product_types: listAllProductTypes,
|
|
64
|
+
modify_pending_order_address: modifyPendingOrderAddress,
|
|
65
|
+
modify_pending_order_items: modifyPendingOrderItems,
|
|
66
|
+
modify_pending_order_payment: modifyPendingOrderPayment,
|
|
67
|
+
modify_user_address: modifyUserAddress,
|
|
68
|
+
return_delivered_order_items: returnDeliveredOrderItems,
|
|
69
|
+
transfer_to_human_agents: transferToHumanAgents,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RetailState, User } from "./shared.ts";
|
|
2
|
+
import type { ErrorResult } from "./store.ts";
|
|
3
|
+
|
|
4
|
+
export interface AuthResult {
|
|
5
|
+
user_id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
order_count: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Latch the session onto one customer.
|
|
12
|
+
*
|
|
13
|
+
* Switching to a different customer is refused. tau2 keeps "you can only help
|
|
14
|
+
* one user per conversation" in the prompt, which leaves the ownership guard on
|
|
15
|
+
* every other tool trivially bypassable — just re-authenticate as whoever's
|
|
16
|
+
* order you want. Re-running a finder for the SAME customer succeeds, because a
|
|
17
|
+
* caller repeating their email should not hit an error.
|
|
18
|
+
*/
|
|
19
|
+
export function authenticateAs(state: RetailState, user: User): AuthResult | ErrorResult {
|
|
20
|
+
const current = state.authenticatedUserId;
|
|
21
|
+
if (current && current !== user.user_id) {
|
|
22
|
+
return {
|
|
23
|
+
error:
|
|
24
|
+
"This conversation is already helping another customer, and you can help only " +
|
|
25
|
+
"one customer per conversation. Ask the caller to start a new call.",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
state.authenticatedUserId = user.user_id;
|
|
29
|
+
return {
|
|
30
|
+
user_id: user.user_id,
|
|
31
|
+
name: `${user.name.first_name} ${user.name.last_name}`,
|
|
32
|
+
order_count: user.orders.length,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
+
import type { ChatMessage } from "@alexkroman1/aai-ui";
|
|
3
|
+
import { client, useAgentState, useSession } from "@alexkroman1/aai-ui";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { useEffect, useRef } from "react";
|
|
6
|
+
import type {
|
|
7
|
+
OrderStatus,
|
|
8
|
+
OrderView,
|
|
9
|
+
PaymentMethodView,
|
|
10
|
+
StoreView,
|
|
11
|
+
SwapOptionView,
|
|
12
|
+
} from "./shared.ts";
|
|
13
|
+
import { DEMO_PERSONAS, storeView } from "./shared.ts";
|
|
14
|
+
|
|
15
|
+
const CSS = `
|
|
16
|
+
@keyframes rt-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
|
|
17
|
+
@keyframes rt-slide-in { from { transform: translateY(8px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
18
|
+
.rt-scroll::-webkit-scrollbar { width: 6px; }
|
|
19
|
+
.rt-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
20
|
+
.rt-scroll::-webkit-scrollbar-thumb { background: #d4d4d8; border-radius: 3px; }
|
|
21
|
+
@media (max-width: 900px) {
|
|
22
|
+
/* Both rows bounded: an auto row sizes to the sidebar's full content, which
|
|
23
|
+
would push the chat (and its Hold/End controls) past the viewport. */
|
|
24
|
+
.rt-main { grid-template-columns: 1fr !important; grid-template-rows: minmax(0, 1fr) minmax(0, 40%) !important; }
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
// `satisfies`-pinned to the shared union, so a new order status is a compile
|
|
29
|
+
// error here rather than a silently grey badge.
|
|
30
|
+
const statusColors: Record<string, string> = {
|
|
31
|
+
pending: "#b45309",
|
|
32
|
+
"pending (item modified)": "#7c3aed",
|
|
33
|
+
processed: "#0369a1",
|
|
34
|
+
delivered: "#15803d",
|
|
35
|
+
cancelled: "#71717a",
|
|
36
|
+
"return requested": "#c2410c",
|
|
37
|
+
"exchange requested": "#a16207",
|
|
38
|
+
} satisfies Record<OrderStatus, string>;
|
|
39
|
+
|
|
40
|
+
// The sidebar before the first tool call, derived from the projection itself so
|
|
41
|
+
// a new StoreView field can't miss the pre-first-call render.
|
|
42
|
+
const EMPTY_VIEW: StoreView = storeView({});
|
|
43
|
+
|
|
44
|
+
function stateColor(state: string): string {
|
|
45
|
+
if (state === "listening" || state === "ready") return "#16a34a";
|
|
46
|
+
if (state === "thinking") return "#ca8a04";
|
|
47
|
+
if (state === "speaking") return "#2563eb";
|
|
48
|
+
return "#a1a1aa";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function Panel({ title, children }: { title: string; children: ReactNode }) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="rounded-lg p-3" style={{ background: "#ffffff", border: "1px solid #e4e4e7" }}>
|
|
54
|
+
<div
|
|
55
|
+
className="text-[10px] font-semibold uppercase tracking-[1.3px] mb-2"
|
|
56
|
+
style={{ color: "#71717a" }}
|
|
57
|
+
>
|
|
58
|
+
{title}
|
|
59
|
+
</div>
|
|
60
|
+
{children}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function MethodRow({ method }: { method: PaymentMethodView }) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="flex justify-between items-center py-0.5 text-xs">
|
|
68
|
+
<span style={{ color: "#52525b" }}>{method.label}</span>
|
|
69
|
+
{method.balance === undefined ? (
|
|
70
|
+
<span className="text-[10px] uppercase tracking-wider" style={{ color: "#a1a1aa" }}>
|
|
71
|
+
on file
|
|
72
|
+
</span>
|
|
73
|
+
) : (
|
|
74
|
+
<span className="font-semibold tabular-nums" style={{ color: "#15803d" }}>
|
|
75
|
+
${method.balance.toFixed(2)}
|
|
76
|
+
</span>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function OrderCard({ order, focused }: { order: OrderView; focused: boolean }) {
|
|
83
|
+
const color = statusColors[order.status] ?? "#71717a";
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className="rounded-md p-2.5 mb-2"
|
|
87
|
+
style={{
|
|
88
|
+
background: focused ? "#fafafa" : "#ffffff",
|
|
89
|
+
animation: "rt-slide-in 0.25s ease-out",
|
|
90
|
+
border: `1px solid ${focused ? color : "#e4e4e7"}`,
|
|
91
|
+
borderLeft: `3px solid ${color}`,
|
|
92
|
+
boxShadow: focused ? `0 0 0 2px ${color}22` : "none",
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<div className="flex justify-between items-center gap-2 mb-1">
|
|
96
|
+
<span className="text-xs font-semibold tabular-nums" style={{ color: "#18181b" }}>
|
|
97
|
+
{order.orderId}
|
|
98
|
+
</span>
|
|
99
|
+
<span
|
|
100
|
+
className="text-[9px] px-1.5 py-0.5 rounded font-semibold uppercase whitespace-nowrap"
|
|
101
|
+
style={{ background: `${color}1a`, color }}
|
|
102
|
+
>
|
|
103
|
+
{order.status}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="text-[11px] mb-1 tabular-nums" style={{ color: "#71717a" }}>
|
|
107
|
+
${order.total.toFixed(2)} · {order.items.length} item
|
|
108
|
+
{order.items.length === 1 ? "" : "s"}
|
|
109
|
+
</div>
|
|
110
|
+
{order.items.slice(0, 3).map((item) => (
|
|
111
|
+
<div
|
|
112
|
+
key={`${item.itemId}-${item.name}`}
|
|
113
|
+
className="text-[11px]"
|
|
114
|
+
style={{ color: "#52525b" }}
|
|
115
|
+
>
|
|
116
|
+
· {item.name}{" "}
|
|
117
|
+
<span style={{ color: "#a1a1aa" }}>{Object.values(item.options).join(", ")}</span>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
{order.items.length > 3 && (
|
|
121
|
+
<div className="text-[11px]" style={{ color: "#a1a1aa" }}>
|
|
122
|
+
· and {order.items.length - 3} more
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Character select. Rendered before authentication, because a user with no
|
|
130
|
+
* email to give cannot start the conversation at all. */
|
|
131
|
+
function PersonaList() {
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex flex-col gap-2">
|
|
134
|
+
<div className="text-[11px] leading-snug" style={{ color: "#3f3f46" }}>
|
|
135
|
+
Pick someone to be, then read their email to the agent.
|
|
136
|
+
</div>
|
|
137
|
+
{DEMO_PERSONAS.map((persona) => (
|
|
138
|
+
<div
|
|
139
|
+
key={persona.email}
|
|
140
|
+
className="rounded-md p-2"
|
|
141
|
+
style={{ background: "#ffffff", border: "1px solid #e4e4e7" }}
|
|
142
|
+
>
|
|
143
|
+
<div className="text-xs font-semibold" style={{ color: "#18181b" }}>
|
|
144
|
+
{persona.name}
|
|
145
|
+
</div>
|
|
146
|
+
<div className="text-[11px] break-all" style={{ color: "#2563eb" }}>
|
|
147
|
+
{persona.email}
|
|
148
|
+
</div>
|
|
149
|
+
<div className="text-[10px] tabular-nums" style={{ color: "#a1a1aa" }}>
|
|
150
|
+
zip {persona.zip}
|
|
151
|
+
</div>
|
|
152
|
+
<div className="text-[10px] leading-snug mt-1" style={{ color: "#71717a" }}>
|
|
153
|
+
{persona.hint}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** What the focused order's items could become — the words a user needs in order
|
|
162
|
+
* to ask for an exchange or an item change. */
|
|
163
|
+
function SwapOptions({ option }: { option: SwapOptionView }) {
|
|
164
|
+
return (
|
|
165
|
+
<div className="mb-2.5">
|
|
166
|
+
<div className="text-[11px] font-semibold" style={{ color: "#18181b" }}>
|
|
167
|
+
{option.itemName}
|
|
168
|
+
</div>
|
|
169
|
+
<div className="text-[10px] mb-1" style={{ color: "#a1a1aa" }}>
|
|
170
|
+
now: {Object.values(option.currentOptions).join(", ")}
|
|
171
|
+
</div>
|
|
172
|
+
{option.alternatives.length === 0 ? (
|
|
173
|
+
<div className="text-[10px]" style={{ color: "#a1a1aa" }}>
|
|
174
|
+
No other option is in stock.
|
|
175
|
+
</div>
|
|
176
|
+
) : (
|
|
177
|
+
option.alternatives.map((alternative) => (
|
|
178
|
+
<div
|
|
179
|
+
key={alternative.itemId}
|
|
180
|
+
className="flex justify-between gap-2 text-[10px] py-0.5"
|
|
181
|
+
style={{ color: "#52525b" }}
|
|
182
|
+
>
|
|
183
|
+
<span>{Object.values(alternative.options).join(", ")}</span>
|
|
184
|
+
<span className="tabular-nums whitespace-nowrap" style={{ color: "#71717a" }}>
|
|
185
|
+
${alternative.price.toFixed(2)}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
))
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function App() {
|
|
195
|
+
const session = useSession();
|
|
196
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
197
|
+
// The agent's own store, projected by `syncState` after every tool call.
|
|
198
|
+
const view = useAgentState<StoreView>() ?? EMPTY_VIEW;
|
|
199
|
+
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
202
|
+
}, [session.messages]);
|
|
203
|
+
|
|
204
|
+
const lastAction = view.activity.at(-1);
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<>
|
|
208
|
+
<style>{CSS}</style>
|
|
209
|
+
<div
|
|
210
|
+
className="flex flex-col h-dvh overflow-hidden m-0 p-0"
|
|
211
|
+
style={{ background: "#f4f4f5", color: "#18181b" }}
|
|
212
|
+
>
|
|
213
|
+
<div
|
|
214
|
+
className="flex items-center justify-between px-5 py-3 gap-4 flex-wrap shrink-0"
|
|
215
|
+
style={{ background: "#ffffff", borderBottom: "1px solid #e4e4e7" }}
|
|
216
|
+
>
|
|
217
|
+
<div className="flex items-center gap-2.5 text-base font-semibold">
|
|
218
|
+
<span style={{ color: "#2563eb" }}>◆</span>
|
|
219
|
+
Retail Support
|
|
220
|
+
<span
|
|
221
|
+
className="w-2 h-2 rounded-full inline-block"
|
|
222
|
+
style={{
|
|
223
|
+
background: stateColor(session.state),
|
|
224
|
+
animation:
|
|
225
|
+
session.state === "listening"
|
|
226
|
+
? "rt-pulse 1.5s ease-in-out infinite"
|
|
227
|
+
: session.state === "thinking"
|
|
228
|
+
? "rt-pulse 0.8s ease-in-out infinite"
|
|
229
|
+
: "none",
|
|
230
|
+
}}
|
|
231
|
+
title={session.state}
|
|
232
|
+
/>
|
|
233
|
+
<span className="text-[11px] font-normal" style={{ color: "#a1a1aa" }}>
|
|
234
|
+
{session.state}
|
|
235
|
+
</span>
|
|
236
|
+
</div>
|
|
237
|
+
<span
|
|
238
|
+
className="px-2.5 py-1 rounded text-[10px] font-semibold uppercase tracking-wider"
|
|
239
|
+
style={
|
|
240
|
+
view.customer
|
|
241
|
+
? { background: "#dcfce7", color: "#15803d" }
|
|
242
|
+
: { background: "#f4f4f5", color: "#71717a" }
|
|
243
|
+
}
|
|
244
|
+
>
|
|
245
|
+
{view.customer ? `Verified · ${view.customer.name}` : "Not yet identified"}
|
|
246
|
+
</span>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div
|
|
250
|
+
className="rt-main flex-1 grid overflow-hidden"
|
|
251
|
+
style={{ gridTemplateColumns: "1fr 340px" }}
|
|
252
|
+
>
|
|
253
|
+
{/* Conversation */}
|
|
254
|
+
<div
|
|
255
|
+
className="flex flex-col overflow-hidden"
|
|
256
|
+
style={{ borderRight: "1px solid #e4e4e7" }}
|
|
257
|
+
>
|
|
258
|
+
<div className="rt-scroll flex-1 overflow-y-auto p-4 flex flex-col gap-2">
|
|
259
|
+
{session.messages.length === 0 && (
|
|
260
|
+
<div className="text-center p-10 text-[13px]" style={{ color: "#a1a1aa" }}>
|
|
261
|
+
Press start, then read one of the emails from “Who to be” to the agent.
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
{session.messages.map((message: ChatMessage, index: number) => (
|
|
265
|
+
<div
|
|
266
|
+
key={index}
|
|
267
|
+
className="rounded-lg text-[13px] max-w-[80%] px-3.5 py-2.5"
|
|
268
|
+
style={{
|
|
269
|
+
lineHeight: 1.55,
|
|
270
|
+
alignSelf: message.role === "assistant" ? "flex-start" : "flex-end",
|
|
271
|
+
background: message.role === "assistant" ? "#ffffff" : "#2563eb",
|
|
272
|
+
color: message.role === "assistant" ? "#18181b" : "#ffffff",
|
|
273
|
+
border: message.role === "assistant" ? "1px solid #e4e4e7" : "none",
|
|
274
|
+
animation: "rt-slide-in 0.2s ease-out",
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
{message.content}
|
|
278
|
+
</div>
|
|
279
|
+
))}
|
|
280
|
+
<div ref={messagesEndRef} />
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{session.userTranscript !== null && (
|
|
284
|
+
<div
|
|
285
|
+
className="flex items-center px-4 py-2 text-xs italic min-h-8"
|
|
286
|
+
style={{ background: "#fafafa", borderTop: "1px solid #e4e4e7", color: "#71717a" }}
|
|
287
|
+
>
|
|
288
|
+
<span
|
|
289
|
+
className="w-2 h-2 rounded-full inline-block mr-2"
|
|
290
|
+
style={{ background: "#16a34a", animation: "rt-pulse 1.5s ease-in-out infinite" }}
|
|
291
|
+
/>
|
|
292
|
+
{session.userTranscript === "" ? "…" : session.userTranscript}
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
{session.error && (
|
|
296
|
+
<div
|
|
297
|
+
className="px-4 py-2 text-xs"
|
|
298
|
+
style={{ background: "#fef2f2", color: "#b91c1c", borderTop: "1px solid #fecaca" }}
|
|
299
|
+
>
|
|
300
|
+
{session.error.message} ({session.error.code})
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
|
|
304
|
+
<div
|
|
305
|
+
className="flex items-center gap-2 px-4 py-3"
|
|
306
|
+
style={{ background: "#ffffff", borderTop: "1px solid #e4e4e7" }}
|
|
307
|
+
>
|
|
308
|
+
{!session.started ? (
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer text-white"
|
|
312
|
+
style={{ background: "#2563eb" }}
|
|
313
|
+
onClick={() => session.start()}
|
|
314
|
+
>
|
|
315
|
+
Start call
|
|
316
|
+
</button>
|
|
317
|
+
) : (
|
|
318
|
+
<>
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer"
|
|
322
|
+
style={{
|
|
323
|
+
background: session.running ? "#e4e4e7" : "#2563eb",
|
|
324
|
+
color: session.running ? "#18181b" : "#ffffff",
|
|
325
|
+
}}
|
|
326
|
+
onClick={() => session.toggle()}
|
|
327
|
+
>
|
|
328
|
+
{session.running ? "Hold" : "Resume"}
|
|
329
|
+
</button>
|
|
330
|
+
{/* end() hangs up and flips `started` back, so the UI
|
|
331
|
+
returns to "Start call" and the next start is a brand-new
|
|
332
|
+
session (fresh store, greeting included). reset() would
|
|
333
|
+
keep the call live — the buttons never toggle back. */}
|
|
334
|
+
<button
|
|
335
|
+
type="button"
|
|
336
|
+
className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer text-white"
|
|
337
|
+
style={{ background: "#dc2626" }}
|
|
338
|
+
onClick={() => session.end()}
|
|
339
|
+
>
|
|
340
|
+
End
|
|
341
|
+
</button>
|
|
342
|
+
</>
|
|
343
|
+
)}
|
|
344
|
+
<div className="flex-1" />
|
|
345
|
+
<span className="text-[10px] tabular-nums" style={{ color: "#a1a1aa" }}>
|
|
346
|
+
{view.productCount} products
|
|
347
|
+
</span>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{/* Sidebar: the customer file */}
|
|
352
|
+
<div
|
|
353
|
+
className="rt-scroll overflow-y-auto p-3 flex flex-col gap-3"
|
|
354
|
+
style={{ background: "#f4f4f5" }}
|
|
355
|
+
>
|
|
356
|
+
{!view.customer && (
|
|
357
|
+
<Panel title="Who to be">
|
|
358
|
+
<PersonaList />
|
|
359
|
+
</Panel>
|
|
360
|
+
)}
|
|
361
|
+
|
|
362
|
+
<Panel title="Customer">
|
|
363
|
+
{view.customer ? (
|
|
364
|
+
<>
|
|
365
|
+
<div className="text-sm font-semibold">{view.customer.name}</div>
|
|
366
|
+
<div className="text-[11px] break-all" style={{ color: "#71717a" }}>
|
|
367
|
+
{view.customer.email}
|
|
368
|
+
</div>
|
|
369
|
+
<div className="text-[11px] mt-1" style={{ color: "#71717a" }}>
|
|
370
|
+
{view.customer.address.address1}
|
|
371
|
+
{view.customer.address.address2 ? `, ${view.customer.address.address2}` : ""}
|
|
372
|
+
<br />
|
|
373
|
+
{view.customer.address.city} {view.customer.address.state}{" "}
|
|
374
|
+
{view.customer.address.zip}
|
|
375
|
+
</div>
|
|
376
|
+
</>
|
|
377
|
+
) : (
|
|
378
|
+
<div className="text-xs" style={{ color: "#a1a1aa" }}>
|
|
379
|
+
Nothing is looked up until the caller is identified.
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
</Panel>
|
|
383
|
+
|
|
384
|
+
{view.customer && (
|
|
385
|
+
<Panel title="Payment methods">
|
|
386
|
+
{view.customer.paymentMethods.map((method) => (
|
|
387
|
+
<MethodRow key={method.id} method={method} />
|
|
388
|
+
))}
|
|
389
|
+
</Panel>
|
|
390
|
+
)}
|
|
391
|
+
|
|
392
|
+
{view.orders.length > 0 && (
|
|
393
|
+
<Panel title={`Orders (${view.orders.length})`}>
|
|
394
|
+
{view.orders.map((order) => (
|
|
395
|
+
<OrderCard
|
|
396
|
+
key={order.orderId}
|
|
397
|
+
order={order}
|
|
398
|
+
focused={order.orderId === view.focus.orderId}
|
|
399
|
+
/>
|
|
400
|
+
))}
|
|
401
|
+
</Panel>
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{view.swapOptions.length > 0 && (
|
|
405
|
+
<Panel title={`Swap options · ${view.focus.orderId ?? ""}`}>
|
|
406
|
+
{view.swapOptions.map((option) => (
|
|
407
|
+
<SwapOptions key={option.itemId} option={option} />
|
|
408
|
+
))}
|
|
409
|
+
</Panel>
|
|
410
|
+
)}
|
|
411
|
+
|
|
412
|
+
<Panel title="You can say">
|
|
413
|
+
{view.scriptBullets.map((bullet) => (
|
|
414
|
+
<div
|
|
415
|
+
key={bullet}
|
|
416
|
+
className="text-[11px] leading-relaxed mb-1"
|
|
417
|
+
style={{ color: "#3f3f46" }}
|
|
418
|
+
>
|
|
419
|
+
· {bullet}
|
|
420
|
+
</div>
|
|
421
|
+
))}
|
|
422
|
+
</Panel>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
{/* The footer strip. `callSeq` is monotonic, so this moves on EVERY tool
|
|
427
|
+
call — including a repeated read that changed nothing else. */}
|
|
428
|
+
<div
|
|
429
|
+
className="flex items-center justify-between px-5 py-2 text-[10px] shrink-0"
|
|
430
|
+
style={{ background: "#ffffff", borderTop: "1px solid #e4e4e7", color: "#71717a" }}
|
|
431
|
+
>
|
|
432
|
+
<span className="truncate">
|
|
433
|
+
{lastAction ? (
|
|
434
|
+
<>
|
|
435
|
+
<span style={{ color: "#2563eb" }}>{lastAction.tool}</span> — {lastAction.summary}
|
|
436
|
+
</>
|
|
437
|
+
) : (
|
|
438
|
+
"No tool calls yet"
|
|
439
|
+
)}
|
|
440
|
+
</span>
|
|
441
|
+
<span className="tabular-nums whitespace-nowrap ml-3">
|
|
442
|
+
{view.callSeq} call{view.callSeq === 1 ? "" : "s"}
|
|
443
|
+
</span>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
</>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
client({
|
|
451
|
+
component: App,
|
|
452
|
+
theme: {
|
|
453
|
+
bg: "#f4f4f5",
|
|
454
|
+
primary: "#2563eb",
|
|
455
|
+
text: "#18181b",
|
|
456
|
+
surface: "#ffffff",
|
|
457
|
+
border: "#e4e4e7",
|
|
458
|
+
},
|
|
459
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { User } from "./shared.ts";
|
|
2
|
+
import { isGiftCard, money } from "./store.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Credit a refund. A gift card is topped up in place and the money is back
|
|
6
|
+
* immediately; every other method takes 5–7 business days, which is a statement
|
|
7
|
+
* about the outside world and so changes no state here.
|
|
8
|
+
*/
|
|
9
|
+
export function creditRefund(user: User, methodId: string, amount: number): { immediate: boolean } {
|
|
10
|
+
const method = user.payment_methods[methodId];
|
|
11
|
+
if (method && isGiftCard(method)) {
|
|
12
|
+
method.balance = money(method.balance + amount);
|
|
13
|
+
return { immediate: true };
|
|
14
|
+
}
|
|
15
|
+
return { immediate: false };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const REFUND_DELAY_NOTE = "The refund will appear in 5 to 7 business days.";
|
|
19
|
+
export const REFUND_IMMEDIATE_NOTE = "The refund is on the gift card immediately.";
|