@alexkroman1/aai 0.7.10 → 0.7.12

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,324 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
2
+ import { useState } from "preact/hooks";
3
+ import { ChatView, mount, useSession, useToolResult } from "@alexkroman1/aai/ui";
4
+
5
+ interface Pizza {
6
+ id: number;
7
+ size: string;
8
+ crust: string;
9
+ toppings: string[];
10
+ quantity: number;
11
+ }
12
+
13
+ interface OrderInfo {
14
+ pizzas: Pizza[];
15
+ total: string;
16
+ orderPlaced: boolean;
17
+ orderNumber?: number;
18
+ estimatedMinutes?: number;
19
+ }
20
+
21
+ const SIZE_PRICES: Record<string, number> = {
22
+ small: 8.99,
23
+ medium: 11.99,
24
+ large: 14.99,
25
+ };
26
+ const CRUST_PRICES: Record<string, number> = {
27
+ thin: 0,
28
+ regular: 0,
29
+ thick: 1.0,
30
+ stuffed: 2.0,
31
+ };
32
+ const TOPPING_PRICES: Record<string, number> = {
33
+ pepperoni: 1.5,
34
+ sausage: 1.5,
35
+ mushrooms: 1.0,
36
+ onions: 1.0,
37
+ green_peppers: 1.0,
38
+ black_olives: 1.0,
39
+ bacon: 2.0,
40
+ ham: 1.5,
41
+ pineapple: 1.0,
42
+ jalapenos: 1.0,
43
+ extra_cheese: 1.5,
44
+ spinach: 1.0,
45
+ tomatoes: 1.0,
46
+ anchovies: 1.5,
47
+ chicken: 2.0,
48
+ };
49
+
50
+ function pizzaPrice(p: Pizza): number {
51
+ const base = SIZE_PRICES[p.size] ?? 11.99;
52
+ const crust = CRUST_PRICES[p.crust] ?? 0;
53
+ const toppings = p.toppings.reduce(
54
+ (s, t) => s + (TOPPING_PRICES[t] ?? 1.0),
55
+ 0,
56
+ );
57
+ return (base + crust + toppings) * p.quantity;
58
+ }
59
+
60
+ function PizzaIcon({ size }: { size: string }) {
61
+ const dim = size === "small" ? 36 : size === "large" ? 52 : 44;
62
+ return (
63
+ <svg
64
+ width={dim}
65
+ height={dim}
66
+ viewBox="0 0 100 100"
67
+ style={{ flexShrink: 0 }}
68
+ >
69
+ <circle cx="50" cy="50" r="48" fill="#F4C542" stroke="#D4A017" stroke-width="3" />
70
+ <circle cx="50" cy="50" r="42" fill="#E8A025" />
71
+ <circle cx="35" cy="35" r="7" fill="#C0392B" opacity="0.9" />
72
+ <circle cx="60" cy="30" r="6" fill="#C0392B" opacity="0.9" />
73
+ <circle cx="55" cy="55" r="7" fill="#C0392B" opacity="0.9" />
74
+ <circle cx="30" cy="58" r="6" fill="#C0392B" opacity="0.9" />
75
+ <circle cx="65" cy="65" r="5" fill="#C0392B" opacity="0.9" />
76
+ <circle cx="45" cy="68" r="4" fill="#27AE60" opacity="0.7" />
77
+ <circle cx="70" cy="42" r="4" fill="#27AE60" opacity="0.7" />
78
+ </svg>
79
+ );
80
+ }
81
+
82
+ function OrderPanel({ order }: { order: OrderInfo }) {
83
+ if (order.orderPlaced) {
84
+ return (
85
+ <div class="flex flex-col items-center gap-4 p-6 text-center">
86
+ <div style={{ fontSize: "48px" }}>&#10003;</div>
87
+ <h2 class="text-lg font-bold text-aai-text">Order Placed</h2>
88
+ {order.orderNumber && (
89
+ <p class="text-aai-text opacity-70">
90
+ Order #{order.orderNumber}
91
+ </p>
92
+ )}
93
+ <p class="text-aai-primary font-bold text-xl">{order.total}</p>
94
+ {order.estimatedMinutes && (
95
+ <p class="text-aai-text opacity-60 text-sm">
96
+ Ready in ~{order.estimatedMinutes} minutes
97
+ </p>
98
+ )}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ if (order.pizzas.length === 0) {
104
+ return (
105
+ <div class="flex flex-col items-center gap-3 p-6 text-center opacity-50">
106
+ <PizzaIcon size="large" />
107
+ <p class="text-aai-text text-sm">
108
+ Your order is empty. Tell me what you'd like.
109
+ </p>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ return (
115
+ <div class="flex flex-col gap-3 p-4">
116
+ <h3 class="text-sm font-bold text-aai-text opacity-60 uppercase tracking-wide">
117
+ Your Order
118
+ </h3>
119
+ {order.pizzas.map((p) => (
120
+ <div
121
+ key={p.id}
122
+ class="flex items-center gap-3 p-3 rounded-lg"
123
+ style={{ background: "var(--color-aai-surface)" }}
124
+ >
125
+ <PizzaIcon size={p.size} />
126
+ <div class="flex-1 min-w-0">
127
+ <p class="text-aai-text text-sm font-medium">
128
+ {p.quantity > 1 ? `${p.quantity}x ` : ""}
129
+ {p.size.charAt(0).toUpperCase() + p.size.slice(1)}{" "}
130
+ {p.crust} crust
131
+ </p>
132
+ <p class="text-aai-text opacity-60 text-xs truncate">
133
+ {p.toppings.length > 0
134
+ ? p.toppings.map((t) => t.replace("_", " ")).join(", ")
135
+ : "cheese only"}
136
+ </p>
137
+ </div>
138
+ <p class="text-aai-primary text-sm font-bold whitespace-nowrap">
139
+ ${pizzaPrice(p).toFixed(2)}
140
+ </p>
141
+ </div>
142
+ ))}
143
+ <div
144
+ class="flex justify-between items-center pt-3 mt-1"
145
+ style={{ borderTop: "1px solid var(--color-aai-border)" }}
146
+ >
147
+ <span class="text-aai-text font-bold">Total</span>
148
+ <span class="text-aai-primary font-bold text-lg">{order.total}</span>
149
+ </div>
150
+ </div>
151
+ );
152
+ }
153
+
154
+ function PizzaAgent() {
155
+ const { started, running, start, toggle, reset } = useSession();
156
+ const [order, setOrder] = useState<OrderInfo>({
157
+ pizzas: [],
158
+ total: "$0.00",
159
+ orderPlaced: false,
160
+ });
161
+
162
+ useToolResult((toolName, result: any) => {
163
+ switch (toolName) {
164
+ case "add_pizza":
165
+ if (result.added) {
166
+ setOrder((prev) => ({
167
+ ...prev,
168
+ pizzas: [...prev.pizzas, result.added],
169
+ total: result.orderTotal,
170
+ }));
171
+ }
172
+ break;
173
+ case "remove_pizza":
174
+ if (result.removed) {
175
+ setOrder((prev) => ({
176
+ ...prev,
177
+ pizzas: prev.pizzas.filter(
178
+ (p: Pizza) => p.id !== result.removed.id,
179
+ ),
180
+ total: result.orderTotal,
181
+ }));
182
+ }
183
+ break;
184
+ case "update_pizza":
185
+ if (result.updated) {
186
+ setOrder((prev) => ({
187
+ ...prev,
188
+ pizzas: prev.pizzas.map((p: Pizza) =>
189
+ p.id === result.updated.id ? result.updated : p,
190
+ ),
191
+ total: result.orderTotal,
192
+ }));
193
+ }
194
+ break;
195
+ case "view_order":
196
+ if (result.pizzas) {
197
+ const total =
198
+ result.orderTotal ||
199
+ `$${result.pizzas.reduce((s: number, p: Pizza) => s + pizzaPrice(p), 0).toFixed(2)}`;
200
+ setOrder((prev) => ({ ...prev, total }));
201
+ }
202
+ break;
203
+ case "place_order":
204
+ if (result.orderNumber) {
205
+ setOrder((prev) => ({
206
+ ...prev,
207
+ orderPlaced: true,
208
+ orderNumber: result.orderNumber,
209
+ total: result.total,
210
+ estimatedMinutes: result.estimatedMinutes,
211
+ }));
212
+ }
213
+ break;
214
+ }
215
+ });
216
+
217
+ if (!started.value) {
218
+ return (
219
+ <div
220
+ class="flex items-center justify-center h-screen"
221
+ style={{ background: "var(--color-aai-bg)" }}
222
+ >
223
+ <style>
224
+ {`
225
+ @keyframes float {
226
+ 0%, 100% { transform: translateY(0px); }
227
+ 50% { transform: translateY(-10px); }
228
+ }
229
+ .pizza-float { animation: float 3s ease-in-out infinite; }
230
+ `}
231
+ </style>
232
+ <div class="flex flex-col items-center gap-6">
233
+ <div class="pizza-float">
234
+ <svg width="120" height="120" viewBox="0 0 100 100">
235
+ <circle cx="50" cy="50" r="48" fill="#F4C542" stroke="#D4A017" stroke-width="3" />
236
+ <circle cx="50" cy="50" r="42" fill="#E8A025" />
237
+ <circle cx="35" cy="35" r="7" fill="#C0392B" opacity="0.9" />
238
+ <circle cx="60" cy="30" r="6" fill="#C0392B" opacity="0.9" />
239
+ <circle cx="55" cy="55" r="7" fill="#C0392B" opacity="0.9" />
240
+ <circle cx="30" cy="58" r="6" fill="#C0392B" opacity="0.9" />
241
+ <circle cx="65" cy="65" r="5" fill="#C0392B" opacity="0.9" />
242
+ <circle cx="45" cy="68" r="4" fill="#27AE60" opacity="0.7" />
243
+ <circle cx="70" cy="42" r="4" fill="#27AE60" opacity="0.7" />
244
+ </svg>
245
+ </div>
246
+ <h1 class="text-2xl font-bold text-aai-text">Pizza Palace</h1>
247
+ <p class="text-aai-text opacity-60 text-sm">
248
+ Voice-powered pizza ordering
249
+ </p>
250
+ <button
251
+ class="px-8 py-3 rounded-full text-white border-none cursor-pointer font-medium text-base"
252
+ style={{ background: "var(--color-aai-primary)" }}
253
+ onClick={start}
254
+ >
255
+ Start Ordering
256
+ </button>
257
+ </div>
258
+ </div>
259
+ );
260
+ }
261
+
262
+ return (
263
+ <div class="flex h-screen" style={{ background: "var(--color-aai-bg)" }}>
264
+ <div
265
+ class="w-80 flex-shrink-0 flex flex-col overflow-y-auto"
266
+ style={{
267
+ borderRight: "1px solid var(--color-aai-border)",
268
+ background: "var(--color-aai-bg)",
269
+ }}
270
+ >
271
+ <div class="p-4 flex items-center gap-3" style={{ borderBottom: "1px solid var(--color-aai-border)" }}>
272
+ <PizzaIcon size="small" />
273
+ <h2 class="text-base font-bold text-aai-text">Pizza Palace</h2>
274
+ </div>
275
+ <div class="flex-1">
276
+ <OrderPanel order={order} />
277
+ </div>
278
+ <div
279
+ class="p-3 flex gap-2"
280
+ style={{ borderTop: "1px solid var(--color-aai-border)" }}
281
+ >
282
+ <button
283
+ class="flex-1 py-2 rounded-lg text-sm border-none cursor-pointer text-white"
284
+ style={{ background: running.value ? "#C0392B" : "var(--color-aai-primary)" }}
285
+ onClick={toggle}
286
+ >
287
+ {running.value ? "Pause" : "Resume"}
288
+ </button>
289
+ <button
290
+ class="py-2 px-4 rounded-lg text-sm cursor-pointer text-aai-text"
291
+ style={{
292
+ background: "var(--color-aai-surface)",
293
+ border: "1px solid var(--color-aai-border)",
294
+ }}
295
+ onClick={() => {
296
+ reset();
297
+ setOrder({
298
+ pizzas: [],
299
+ total: "$0.00",
300
+ orderPlaced: false,
301
+ });
302
+ }}
303
+ >
304
+ New Order
305
+ </button>
306
+ </div>
307
+ </div>
308
+ <div class="flex-1 min-w-0">
309
+ <ChatView />
310
+ </div>
311
+ </div>
312
+ );
313
+ }
314
+
315
+ mount(PizzaAgent, {
316
+ title: "Pizza Palace",
317
+ theme: {
318
+ bg: "#1a1008",
319
+ primary: "#E8A025",
320
+ text: "#f5f0e8",
321
+ surface: "#2a1f10",
322
+ border: "#3d2e18",
323
+ },
324
+ });
package/ui/styles.css CHANGED
@@ -1,6 +1,7 @@
1
1
  @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap");
2
2
  @import "tailwindcss";
3
3
  @source "./";
4
+ @source "../dist/ui/";
4
5
 
5
6
  @theme {
6
7
  --color-aai-bg: #101010;