@alexkroman1/aai 0.3.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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli.js +3436 -0
  3. package/package.json +78 -0
  4. package/sdk/_internal_types.ts +89 -0
  5. package/sdk/_mock_ws.ts +172 -0
  6. package/sdk/_timeout.ts +24 -0
  7. package/sdk/builtin_tools.ts +309 -0
  8. package/sdk/capnweb.ts +341 -0
  9. package/sdk/define_agent.ts +70 -0
  10. package/sdk/direct_executor.ts +195 -0
  11. package/sdk/kv.ts +183 -0
  12. package/sdk/mod.ts +35 -0
  13. package/sdk/protocol.ts +313 -0
  14. package/sdk/runtime.ts +65 -0
  15. package/sdk/s2s.ts +271 -0
  16. package/sdk/server.ts +198 -0
  17. package/sdk/session.ts +438 -0
  18. package/sdk/system_prompt.ts +47 -0
  19. package/sdk/types.ts +406 -0
  20. package/sdk/vector.ts +133 -0
  21. package/sdk/winterc_server.ts +141 -0
  22. package/sdk/worker_entry.ts +99 -0
  23. package/sdk/worker_shim.ts +170 -0
  24. package/sdk/ws_handler.ts +190 -0
  25. package/templates/_shared/.env.example +5 -0
  26. package/templates/_shared/package.json +17 -0
  27. package/templates/code-interpreter/agent.ts +27 -0
  28. package/templates/code-interpreter/client.tsx +2 -0
  29. package/templates/dispatch-center/agent.ts +1536 -0
  30. package/templates/dispatch-center/client.tsx +504 -0
  31. package/templates/embedded-assets/agent.ts +49 -0
  32. package/templates/embedded-assets/client.tsx +2 -0
  33. package/templates/embedded-assets/knowledge.json +20 -0
  34. package/templates/health-assistant/agent.ts +160 -0
  35. package/templates/health-assistant/client.tsx +2 -0
  36. package/templates/infocom-adventure/agent.ts +164 -0
  37. package/templates/infocom-adventure/client.tsx +299 -0
  38. package/templates/math-buddy/agent.ts +21 -0
  39. package/templates/math-buddy/client.tsx +2 -0
  40. package/templates/memory-agent/agent.ts +74 -0
  41. package/templates/memory-agent/client.tsx +2 -0
  42. package/templates/night-owl/agent.ts +98 -0
  43. package/templates/night-owl/client.tsx +28 -0
  44. package/templates/personal-finance/agent.ts +26 -0
  45. package/templates/personal-finance/client.tsx +2 -0
  46. package/templates/simple/agent.ts +6 -0
  47. package/templates/simple/client.tsx +2 -0
  48. package/templates/smart-research/agent.ts +164 -0
  49. package/templates/smart-research/client.tsx +2 -0
  50. package/templates/support/README.md +62 -0
  51. package/templates/support/agent.ts +19 -0
  52. package/templates/support/client.tsx +2 -0
  53. package/templates/travel-concierge/agent.ts +29 -0
  54. package/templates/travel-concierge/client.tsx +2 -0
  55. package/templates/web-researcher/agent.ts +17 -0
  56. package/templates/web-researcher/client.tsx +2 -0
  57. package/ui/_components/app.tsx +37 -0
  58. package/ui/_components/chat_view.tsx +36 -0
  59. package/ui/_components/controls.tsx +32 -0
  60. package/ui/_components/error_banner.tsx +18 -0
  61. package/ui/_components/message_bubble.tsx +21 -0
  62. package/ui/_components/message_list.tsx +61 -0
  63. package/ui/_components/state_indicator.tsx +17 -0
  64. package/ui/_components/thinking_indicator.tsx +19 -0
  65. package/ui/_components/tool_call_block.tsx +110 -0
  66. package/ui/_components/tool_icons.tsx +101 -0
  67. package/ui/_components/transcript.tsx +20 -0
  68. package/ui/audio.ts +170 -0
  69. package/ui/components.ts +49 -0
  70. package/ui/components_mod.ts +37 -0
  71. package/ui/mod.ts +48 -0
  72. package/ui/mount.tsx +112 -0
  73. package/ui/mount_context.ts +19 -0
  74. package/ui/session.ts +456 -0
  75. package/ui/session_mod.ts +27 -0
  76. package/ui/signals.ts +111 -0
  77. package/ui/types.ts +50 -0
  78. package/ui/worklets/capture-processor.js +62 -0
  79. package/ui/worklets/playback-processor.js +110 -0
@@ -0,0 +1,160 @@
1
+ import { defineAgent } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+
4
+ function first(field: unknown): string | undefined {
5
+ return Array.isArray(field) ? field[0] : undefined;
6
+ }
7
+
8
+ async function lookupDrug(
9
+ name: string,
10
+ ): Promise<Record<string, unknown>> {
11
+ const q = encodeURIComponent(name.toLowerCase());
12
+ let raw: Record<string, unknown>;
13
+ try {
14
+ const resp = await fetch(
15
+ `https://api.fda.gov/drug/label.json?search=openfda.generic_name:"${q}"+openfda.brand_name:"${q}"&limit=1`,
16
+ );
17
+ raw = resp.ok ? await resp.json() : { error: `Drug not found: ${name}` };
18
+ } catch {
19
+ raw = { error: `Drug not found: ${name}` };
20
+ }
21
+
22
+ if ("error" in raw) return raw;
23
+ const results = raw.results as Record<string, unknown>[] | undefined;
24
+ if (!results?.length) return { error: `No FDA data found for: ${name}` };
25
+
26
+ const drug = results[0]!;
27
+ const openfda = (drug.openfda ?? {}) as Record<string, string[]>;
28
+ return {
29
+ name: openfda.generic_name?.[0] ?? name,
30
+ brand_names: openfda.brand_name ?? [],
31
+ purpose: first(drug.purpose) ?? first(drug.indications_and_usage) ?? "N/A",
32
+ warnings: first(drug.warnings)?.slice(0, 500) ?? "N/A",
33
+ dosage: first(drug.dosage_and_administration)?.slice(0, 500) ?? "N/A",
34
+ side_effects: first(drug.adverse_reactions)?.slice(0, 500) ?? "N/A",
35
+ manufacturer: openfda.manufacturer_name?.[0] ?? "N/A",
36
+ };
37
+ }
38
+
39
+ type RxCui = {
40
+ name: string;
41
+ rxcui: string;
42
+ };
43
+
44
+ async function resolveRxCui(
45
+ name: string,
46
+ ): Promise<RxCui | null> {
47
+ let raw: { idGroup: { rxnormId?: string[] } } | null;
48
+ try {
49
+ const resp = await fetch(
50
+ `https://rxnav.nlm.nih.gov/REST/rxcui.json?name=${
51
+ encodeURIComponent(name)
52
+ }`,
53
+ );
54
+ raw = resp.ok ? await resp.json() : null;
55
+ } catch {
56
+ raw = null;
57
+ }
58
+ if (!raw) return null;
59
+ const id = raw.idGroup.rxnormId?.[0];
60
+ return id ? { name, rxcui: id } : null;
61
+ }
62
+
63
+ async function checkInteractions(
64
+ drugs: string,
65
+ ): Promise<Record<string, unknown>> {
66
+ const names = drugs.split(",").map((d) => d.trim().toLowerCase());
67
+
68
+ const resolved = (await Promise.all(names.map((n) => resolveRxCui(n))))
69
+ .filter((r): r is RxCui => r !== null);
70
+
71
+ if (resolved.length < 2) {
72
+ return {
73
+ error: `Could not resolve all drug names. Found: ${
74
+ resolved.map((r) => r.name).join(", ") || "none"
75
+ }`,
76
+ };
77
+ }
78
+
79
+ const rxcuiList = resolved.map((r) => r.rxcui).join("+");
80
+ let raw: Record<string, unknown>;
81
+ try {
82
+ const resp = await fetch(
83
+ `https://rxnav.nlm.nih.gov/REST/interaction/list.json?rxcuis=${rxcuiList}`,
84
+ );
85
+ raw = resp.ok ? await resp.json() : { error: "Interaction lookup failed" };
86
+ } catch {
87
+ raw = { error: "Interaction lookup failed" };
88
+ }
89
+
90
+ if ("error" in raw) return raw;
91
+
92
+ type InteractionGroup = {
93
+ fullInteractionType?: {
94
+ interactionPair?: { description: string; severity: string }[];
95
+ }[];
96
+ };
97
+
98
+ const groups = (raw.fullInteractionTypeGroup ?? []) as InteractionGroup[];
99
+ const interactions = groups
100
+ .flatMap((g) => g.fullInteractionType ?? [])
101
+ .flatMap((t) => t.interactionPair ?? [])
102
+ .map(({ description, severity }) => ({ description, severity }));
103
+
104
+ return {
105
+ drugs: resolved.map(({ name, rxcui }) => ({ name, rxcui })),
106
+ interactions_found: interactions.length,
107
+ interactions: interactions.slice(0, 5),
108
+ };
109
+ }
110
+
111
+ export default defineAgent({
112
+ name: "Dr. Sage",
113
+ instructions:
114
+ `You are Dr. Sage, a friendly health information assistant. You help people \
115
+ understand symptoms, look up medication details, check drug interactions, and calculate \
116
+ basic health metrics.
117
+
118
+ Rules:
119
+ - You are NOT a doctor and cannot diagnose or prescribe. Always remind users to consult \
120
+ a healthcare provider for medical decisions.
121
+ - Be clear and calm when discussing symptoms — avoid alarming language
122
+ - When discussing medications, always mention common side effects
123
+ - Use plain language first, then mention the medical term
124
+ - Keep responses concise — this is a voice conversation
125
+ - If symptoms sound urgent (chest pain, difficulty breathing, sudden numbness), \
126
+ advise calling emergency services immediately
127
+ - Use web_search to look up current symptom information when needed
128
+ - Use medication_lookup to get details on a single medication
129
+ - Use check_drug_interaction to check interactions between multiple drugs
130
+
131
+ Use run_code for health calculations:
132
+ - BMI: weight_kg / (height_m * height_m). Categories: <18.5 underweight, 18.5-25 normal, 25-30 overweight, >30 obese
133
+ Unit conversions: 1 lb = 0.453592 kg, 1 in = 0.0254 m, 1 ft = 0.3048 m, 1 cm = 0.01 m
134
+ - Weight-based dosage: dose_mg = weight_kg * dose_per_kg. Always note this is an estimate.`,
135
+ greeting:
136
+ "Hey, I'm Dr. Sage. Try asking me something like, what are the side effects of ibuprofen, can I take aspirin and warfarin together, or calculate my BMI. Just remember, I'm not a real doctor, so always check with your healthcare provider.",
137
+ builtinTools: ["web_search", "run_code"],
138
+ tools: {
139
+ medication_lookup: {
140
+ description:
141
+ "Look up detailed information about a single medication, including purpose, warnings, dosage, side effects, and manufacturer. Works with both generic and brand names.",
142
+ parameters: z.object({
143
+ name: z.string().describe(
144
+ "Medication name (generic or brand, e.g. 'ibuprofen' or 'Advil')",
145
+ ),
146
+ }),
147
+ execute: ({ name }) => lookupDrug(name),
148
+ },
149
+ check_drug_interaction: {
150
+ description:
151
+ "Check for known interactions between two or more medications. Resolves drug names via RxNorm and returns interaction details with severity levels.",
152
+ parameters: z.object({
153
+ drugs: z.string().describe(
154
+ "Comma-separated medication names (e.g. 'ibuprofen, warfarin')",
155
+ ),
156
+ }),
157
+ execute: ({ drugs }) => checkInteractions(drugs),
158
+ },
159
+ },
160
+ });
@@ -0,0 +1,2 @@
1
+ import { App, mount } from "@alexkroman1/aai/ui";
2
+ mount(App);
@@ -0,0 +1,164 @@
1
+ import { defineAgent } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import type { ToolContext } from "@alexkroman1/aai";
4
+
5
+ type GameState = {
6
+ inventory: string[];
7
+ currentRoom: string;
8
+ score: number;
9
+ moves: number;
10
+ flags: Record<string, boolean>;
11
+ history: string[];
12
+ };
13
+
14
+ function s(ctx: ToolContext<GameState>): GameState {
15
+ return ctx.state;
16
+ }
17
+
18
+ export default defineAgent({
19
+ name: "Infocom Adventure",
20
+ greeting:
21
+ "Welcome to the great underground empire. You are standing in an open field west of a white house, with a boarded front door. There is a small mailbox here. What would you like to do?",
22
+
23
+ state: (): GameState => ({
24
+ inventory: [],
25
+ currentRoom: "West of House",
26
+ score: 0,
27
+ moves: 0,
28
+ flags: {},
29
+ history: [],
30
+ }),
31
+
32
+ instructions:
33
+ `You are a classic Infocom-style text adventure game engine, simulating ZORK I: The Great Underground Empire.
34
+
35
+ You ARE the game. You maintain the world state, describe rooms, handle puzzles, manage inventory, track score, and respond to player commands — all faithfully recreating the Zork experience.
36
+
37
+ GAME WORLD RULES:
38
+ - Follow the geography, puzzles, and items of Zork I as closely as you can recall
39
+ - The map includes: West of House, North of House, Behind House, South of House, Kitchen, Living Room, Attic, Cellar, the Great Underground Empire (Troll Room, Flood Control Dam, Loud Room, etc.), the maze, Hades, and more
40
+ - Key items: brass lantern, elvish sword, jeweled egg, gold coffin, platinum bar, jade figurine, sapphire bracelet, trunk of jewels, crystal trident, etc.
41
+ - Key encounters: troll, thief, cyclops, spirits, vampire bat
42
+ - Puzzles work as they do in Zork: the dam, the coal mine, the Egyptian room, the mirror rooms, Hades, the maze, etc.
43
+ - Score increases when the player collects treasures and places them in the trophy case in the living room
44
+ - The brass lantern has limited battery life underground
45
+
46
+ VOICE-FIRST RESPONSE RULES:
47
+ - Describe rooms vividly but concisely — two to four sentences max
48
+ - For movement, describe the new room immediately
49
+ - For failed actions, give brief, witty responses in the Infocom style ("There is a wall in the way." or "You can't eat that.")
50
+ - Read inventory as a spoken list
51
+ - Announce score changes
52
+ - Keep the classic dry humor of Infocom games
53
+ - Never use visual formatting — no bullets, no bold, no lists with dashes
54
+ - Use "First... Then... Finally..." for sequences
55
+ - Use directional words naturally: "To the north you see..." not "N: forest"
56
+
57
+ COMMAND INTERPRETATION:
58
+ - Players speak naturally. Translate their voice into classic adventure commands
59
+ - "go north" / "head north" / "walk north" = north
60
+ - "pick up the sword" / "grab the sword" / "take sword" = take sword
61
+ - "what do I have" / "check my stuff" / "inventory" = inventory
62
+ - "where am I" / "look around" / "describe the room" = look
63
+ - "hit the troll" / "fight the troll" / "attack troll" = attack troll with sword
64
+ - "what's my score" = score
65
+ - Accept natural conversational commands and map them to game actions
66
+
67
+ Use the game state tools to track inventory, location, score, and flags. Use game_state_get to read the current state, game_state_move to change rooms, game_state_take to pick up items, game_state_drop to drop items, game_state_score to add points, game_state_flag to set game flags, and game_state_history to log commands. Always update state when the player takes an item, moves rooms, or triggers an event. Check state before responding to ensure consistency.
68
+
69
+ ATMOSPHERE:
70
+ - Underground areas should feel dark and foreboding when the lantern is present, and terrifying in pitch blackness
71
+ - The thief should appear randomly and steal items
72
+ - The troll blocks the passage until defeated
73
+ - Convey a sense of mystery and danger
74
+ - Keep the wry, understated humor that made Infocom games legendary`,
75
+
76
+ tools: {
77
+ game_state_get: {
78
+ description:
79
+ "Read the current game state including inventory, current room, score, moves, flags, and recent history.",
80
+ execute: (_args, ctx) => {
81
+ const g = s(ctx);
82
+ return {
83
+ currentRoom: g.currentRoom,
84
+ inventory: g.inventory,
85
+ score: g.score,
86
+ moves: g.moves,
87
+ flags: g.flags,
88
+ recentHistory: g.history.slice(-5),
89
+ };
90
+ },
91
+ },
92
+ game_state_move: {
93
+ description:
94
+ "Move the player to a new room and increment the move counter.",
95
+ parameters: z.object({
96
+ value: z.string().describe("Room name to move to"),
97
+ }),
98
+ execute: ({ value }, ctx) => {
99
+ const g = s(ctx);
100
+ g.currentRoom = value;
101
+ g.moves++;
102
+ return { currentRoom: g.currentRoom, moves: g.moves };
103
+ },
104
+ },
105
+ game_state_take: {
106
+ description: "Add an item to the player's inventory.",
107
+ parameters: z.object({
108
+ value: z.string().describe("Item name to take"),
109
+ }),
110
+ execute: ({ value }, ctx) => {
111
+ const g = s(ctx);
112
+ if (!g.inventory.includes(value)) g.inventory.push(value);
113
+ return { inventory: g.inventory };
114
+ },
115
+ },
116
+ game_state_drop: {
117
+ description: "Remove an item from the player's inventory.",
118
+ parameters: z.object({
119
+ value: z.string().describe("Item name to drop"),
120
+ }),
121
+ execute: ({ value }, ctx) => {
122
+ const g = s(ctx);
123
+ g.inventory = g.inventory.filter((i) => i !== value);
124
+ return { inventory: g.inventory };
125
+ },
126
+ },
127
+ game_state_score: {
128
+ description: "Add points to the player's score.",
129
+ parameters: z.object({
130
+ value: z.number().describe("Points to add"),
131
+ }),
132
+ execute: ({ value }, ctx) => {
133
+ const g = s(ctx);
134
+ g.score += value;
135
+ return { score: g.score };
136
+ },
137
+ },
138
+ game_state_flag: {
139
+ description:
140
+ "Set a game flag to true, used for tracking puzzle and event state.",
141
+ parameters: z.object({
142
+ value: z.string().describe("Flag name to set"),
143
+ }),
144
+ execute: ({ value }, ctx) => {
145
+ const g = s(ctx);
146
+ g.flags[value] = true;
147
+ return { flags: g.flags };
148
+ },
149
+ },
150
+ game_state_history: {
151
+ description:
152
+ "Log a player command to the history and increment the move counter.",
153
+ parameters: z.object({
154
+ value: z.string().describe("Command text to log"),
155
+ }),
156
+ execute: ({ value }, ctx) => {
157
+ const g = s(ctx);
158
+ g.history.push(value);
159
+ g.moves++;
160
+ return { moves: g.moves, recentHistory: g.history.slice(-5) };
161
+ },
162
+ },
163
+ },
164
+ });
@@ -0,0 +1,299 @@
1
+ import { mount, useSession } from "@alexkroman1/aai/ui";
2
+ import type { Message } from "@alexkroman1/aai/ui";
3
+ import { useEffect, useRef } from "preact/hooks";
4
+
5
+ const CSS = `
6
+ @keyframes ic-flicker {
7
+ 0% { opacity: 0.97; }
8
+ 5% { opacity: 0.95; }
9
+ 10% { opacity: 0.98; }
10
+ 15% { opacity: 0.96; }
11
+ 20% { opacity: 0.99; }
12
+ 50% { opacity: 0.96; }
13
+ 80% { opacity: 0.98; }
14
+ 100% { opacity: 0.97; }
15
+ }
16
+ @keyframes ic-scanline {
17
+ 0% { transform: translateY(-100%); }
18
+ 100% { transform: translateY(100vh); }
19
+ }
20
+ @keyframes ic-blink {
21
+ 0%, 49% { opacity: 1; }
22
+ 50%, 100% { opacity: 0; }
23
+ }
24
+ @keyframes ic-boot {
25
+ 0% { opacity: 0; transform: scaleY(0.01); }
26
+ 30% { opacity: 1; transform: scaleY(0.01); }
27
+ 60% { transform: scaleY(1); }
28
+ 100% { transform: scaleY(1); opacity: 1; }
29
+ }
30
+ @keyframes ic-pulse {
31
+ 0%, 100% { box-shadow: 0 0 8px rgba(0, 255, 65, 0.3); }
32
+ 50% { box-shadow: 0 0 20px rgba(0, 255, 65, 0.6); }
33
+ }
34
+ .ic-crt::before {
35
+ content: "";
36
+ position: absolute;
37
+ inset: 0;
38
+ background: repeating-linear-gradient(
39
+ 0deg,
40
+ rgba(0, 0, 0, 0.15) 0px,
41
+ rgba(0, 0, 0, 0.15) 1px,
42
+ transparent 1px,
43
+ transparent 3px
44
+ );
45
+ pointer-events: none;
46
+ z-index: 10;
47
+ }
48
+ .ic-crt::after {
49
+ content: "";
50
+ position: absolute;
51
+ top: 0;
52
+ left: 0;
53
+ right: 0;
54
+ height: 4px;
55
+ background: rgba(0, 255, 65, 0.08);
56
+ animation: ic-scanline 8s linear infinite;
57
+ pointer-events: none;
58
+ z-index: 11;
59
+ }
60
+ .ic-messages::-webkit-scrollbar { width: 6px; }
61
+ .ic-messages::-webkit-scrollbar-track { background: #001a00; }
62
+ .ic-messages::-webkit-scrollbar-thumb { background: #00ff41; }
63
+ .ic-user-msg::before { content: "> "; color: #00ccff; }
64
+ .ic-transcript::before { content: "> "; color: #007a1e; }
65
+ `;
66
+
67
+ const ASCII_LOGO = `
68
+ ____ ___ ____ _ __
69
+ /__ |/ _ \\| _ \\| |/ /
70
+ / /| | | | |_) | ' /
71
+ / / | |_| | _ <| . \\
72
+ /_/ \\___/|_| \\_\\_|\\_\\
73
+ `;
74
+
75
+ function InfocomAdventure() {
76
+ const { session, started, running, start, toggle, reset } = useSession();
77
+ const bottom = useRef<HTMLDivElement>(null);
78
+
79
+ useEffect(() => {
80
+ bottom.current?.scrollIntoView({ behavior: "smooth" });
81
+ }, [session.messages.value.length, session.userUtterance.value]);
82
+
83
+ const stateVal = session.state.value;
84
+ const stateLabel = stateVal === "listening"
85
+ ? "Listening"
86
+ : stateVal === "speaking"
87
+ ? "Narrating"
88
+ : stateVal === "thinking"
89
+ ? "Thinking"
90
+ : stateVal === "connecting"
91
+ ? "Connecting"
92
+ : stateVal === "ready"
93
+ ? "Ready"
94
+ : "Idle";
95
+
96
+ const msgCount =
97
+ session.messages.value.filter((m: Message) => m.role === "user").length;
98
+
99
+ const dotColor = stateVal === "listening"
100
+ ? "#00ff41"
101
+ : stateVal === "speaking"
102
+ ? "#ffaa00"
103
+ : stateVal === "thinking"
104
+ ? "#00ccff"
105
+ : "#003300";
106
+
107
+ if (!started.value) {
108
+ return (
109
+ <>
110
+ <style>{CSS}</style>
111
+ <div
112
+ class="ic-crt fixed inset-0 overflow-hidden"
113
+ style={{
114
+ background: "#000800",
115
+ color: "#00ff41",
116
+ fontFamily: "monospace",
117
+ fontSize: "15px",
118
+ lineHeight: 1.6,
119
+ animation: "ic-flicker 4s infinite",
120
+ }}
121
+ >
122
+ <div
123
+ class="flex flex-col items-center justify-center h-full text-center p-10"
124
+ style={{ animation: "ic-boot 1.5s ease-out" }}
125
+ >
126
+ <div
127
+ class="text-[11px] whitespace-pre mb-8"
128
+ style={{ textShadow: "0 0 10px rgba(0, 255, 65, 0.5)" }}
129
+ >
130
+ {ASCII_LOGO}
131
+ </div>
132
+ <div class="text-[13px] mb-2" style={{ color: "#00aa2a" }}>
133
+ INFOCOM INTERACTIVE FICTION
134
+ </div>
135
+ <div class="text-[13px] mb-2" style={{ color: "#00aa2a" }}>
136
+ Copyright (c) 1980 Infocom, Inc.
137
+ </div>
138
+ <div class="text-[13px] mb-2" style={{ color: "#00aa2a" }}>
139
+ All rights reserved.
140
+ </div>
141
+ <div class="text-[13px] mt-4" style={{ color: "#00ff41" }}>
142
+ VOICE-ENABLED EDITION
143
+ </div>
144
+ <div class="text-[13px] mt-6" style={{ color: "#00aa2a" }}>
145
+ Release 88 / Serial No. 840726
146
+ </div>
147
+ <button
148
+ type="button"
149
+ class="mt-10 px-12 py-3.5 bg-transparent cursor-pointer uppercase tracking-[3px] font-mono text-base"
150
+ style={{
151
+ color: "#00ff41",
152
+ border: "1px solid #00ff41",
153
+ animation: "ic-pulse 2s ease-in-out infinite",
154
+ }}
155
+ onClick={start}
156
+ >
157
+ Begin Adventure
158
+ </button>
159
+ </div>
160
+ <div
161
+ class="fixed inset-0 pointer-events-none z-[12]"
162
+ style={{
163
+ background:
164
+ "radial-gradient(ellipse at center, transparent 60%, rgba(0, 0, 0, 0.4) 100%)",
165
+ }}
166
+ />
167
+ </div>
168
+ </>
169
+ );
170
+ }
171
+
172
+ return (
173
+ <>
174
+ <style>{CSS}</style>
175
+ <div
176
+ class="ic-crt fixed inset-0 overflow-hidden"
177
+ style={{
178
+ background: "#000800",
179
+ color: "#00ff41",
180
+ fontFamily: "monospace",
181
+ fontSize: "15px",
182
+ lineHeight: 1.6,
183
+ animation: "ic-flicker 4s infinite",
184
+ }}
185
+ >
186
+ <div class="flex flex-col h-full">
187
+ {/* Status bar */}
188
+ <div
189
+ class="flex items-center justify-between px-5 py-2 text-[13px] font-bold tracking-wider shrink-0"
190
+ style={{ background: "#00ff41", color: "#000800" }}
191
+ >
192
+ <div class="flex gap-6">
193
+ <span>ZORK I</span>
194
+ <span>Moves: {msgCount}</span>
195
+ </div>
196
+ <span>Voice Adventure</span>
197
+ </div>
198
+
199
+ {session.error.value && (
200
+ <div
201
+ class="px-5 py-2 text-xs"
202
+ style={{ background: "#3a0000", color: "#ff4141" }}
203
+ >
204
+ ERROR: {session.error.value.message}
205
+ </div>
206
+ )}
207
+
208
+ {/* Messages */}
209
+ <div
210
+ class="ic-messages flex-1 overflow-y-auto p-5"
211
+ style={{
212
+ scrollbarWidth: "thin",
213
+ scrollbarColor: "#00ff41 #001a00",
214
+ }}
215
+ >
216
+ {session.messages.value.map((msg: Message, i: number) => (
217
+ <div
218
+ key={i}
219
+ class={`mb-4 ${msg.role === "user" ? "ic-user-msg" : ""}`}
220
+ style={{
221
+ textShadow: msg.role === "user"
222
+ ? "0 0 5px rgba(0, 204, 255, 0.3)"
223
+ : "0 0 5px rgba(0, 255, 65, 0.3)",
224
+ color: msg.role === "user" ? "#00ccff" : "#00ff41",
225
+ }}
226
+ >
227
+ {msg.text}
228
+ </div>
229
+ ))}
230
+ {session.userUtterance.value !== null && (
231
+ <div
232
+ class="ic-transcript italic"
233
+ style={{
234
+ color: "#007a1e",
235
+ textShadow: "0 0 5px rgba(0, 255, 65, 0.15)",
236
+ }}
237
+ >
238
+ {session.userUtterance.value || "..."}
239
+ </div>
240
+ )}
241
+ <div ref={bottom} />
242
+ </div>
243
+
244
+ {/* Footer controls */}
245
+ <div
246
+ class="flex items-center justify-between px-5 py-2 shrink-0 gap-3"
247
+ style={{
248
+ borderTop: "1px solid #003300",
249
+ background: "#001100",
250
+ }}
251
+ >
252
+ <div
253
+ class="flex items-center gap-2.5 text-xs uppercase tracking-wider"
254
+ style={{ color: "#00aa2a" }}
255
+ >
256
+ <div
257
+ class="w-2 h-2 rounded-full"
258
+ style={{
259
+ background: dotColor,
260
+ boxShadow: dotColor !== "#003300"
261
+ ? `0 0 6px ${dotColor}`
262
+ : "none",
263
+ }}
264
+ />
265
+ <span>{stateLabel}</span>
266
+ </div>
267
+ <div class="flex gap-2">
268
+ <button
269
+ type="button"
270
+ class="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
271
+ style={{ color: "#00aa2a", border: "1px solid #003300" }}
272
+ onClick={toggle}
273
+ >
274
+ {running.value ? "[P]ause" : "[R]esume"}
275
+ </button>
276
+ <button
277
+ type="button"
278
+ class="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
279
+ style={{ color: "#00aa2a", border: "1px solid #003300" }}
280
+ onClick={reset}
281
+ >
282
+ [Q]uit
283
+ </button>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ <div
288
+ class="fixed inset-0 pointer-events-none z-[12]"
289
+ style={{
290
+ background:
291
+ "radial-gradient(ellipse at center, transparent 60%, rgba(0, 0, 0, 0.4) 100%)",
292
+ }}
293
+ />
294
+ </div>
295
+ </>
296
+ );
297
+ }
298
+
299
+ mount(InfocomAdventure);
@@ -0,0 +1,21 @@
1
+ import { defineAgent } from "@alexkroman1/aai";
2
+
3
+ export default defineAgent({
4
+ name: "Math Buddy",
5
+ instructions:
6
+ `You are Math Buddy, a friendly math assistant. You help with calculations,
7
+ unit conversions, dice rolls, and random number generation. Keep answers short and clear.
8
+ When doing multi-step math, show your work briefly.
9
+
10
+ Use run_code for ALL calculations. Write JavaScript using console.log() for output.
11
+
12
+ Examples:
13
+ - Math expressions: console.log((12 + 8) * 3) or console.log(Math.sqrt(144))
14
+ - Unit conversions: convert using known factors (1 km = 0.621371 mi, 1 lb = 0.453592 kg, etc.)
15
+ - Temperature: C to F = (c * 9/5) + 32, F to C = (f - 32) * 5/9, C to K = c + 273.15
16
+ - Dice rolls: console.log(Array.from({length: N}, () => Math.floor(Math.random() * sides) + 1))
17
+ - Random numbers: console.log(Math.floor(Math.random() * (max - min + 1)) + min)`,
18
+ greeting:
19
+ "Hey, I'm Math Buddy. Try asking me something like, what's 127 times 849, convert 5 miles to kilometers, or roll 3 twenty-sided dice.",
20
+ builtinTools: ["run_code"],
21
+ });
@@ -0,0 +1,2 @@
1
+ import { App, mount } from "@alexkroman1/aai/ui";
2
+ mount(App);