@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.
- package/LICENSE +21 -0
- package/dist/cli.js +3436 -0
- package/package.json +78 -0
- package/sdk/_internal_types.ts +89 -0
- package/sdk/_mock_ws.ts +172 -0
- package/sdk/_timeout.ts +24 -0
- package/sdk/builtin_tools.ts +309 -0
- package/sdk/capnweb.ts +341 -0
- package/sdk/define_agent.ts +70 -0
- package/sdk/direct_executor.ts +195 -0
- package/sdk/kv.ts +183 -0
- package/sdk/mod.ts +35 -0
- package/sdk/protocol.ts +313 -0
- package/sdk/runtime.ts +65 -0
- package/sdk/s2s.ts +271 -0
- package/sdk/server.ts +198 -0
- package/sdk/session.ts +438 -0
- package/sdk/system_prompt.ts +47 -0
- package/sdk/types.ts +406 -0
- package/sdk/vector.ts +133 -0
- package/sdk/winterc_server.ts +141 -0
- package/sdk/worker_entry.ts +99 -0
- package/sdk/worker_shim.ts +170 -0
- package/sdk/ws_handler.ts +190 -0
- package/templates/_shared/.env.example +5 -0
- package/templates/_shared/package.json +17 -0
- package/templates/code-interpreter/agent.ts +27 -0
- package/templates/code-interpreter/client.tsx +2 -0
- package/templates/dispatch-center/agent.ts +1536 -0
- package/templates/dispatch-center/client.tsx +504 -0
- package/templates/embedded-assets/agent.ts +49 -0
- package/templates/embedded-assets/client.tsx +2 -0
- package/templates/embedded-assets/knowledge.json +20 -0
- package/templates/health-assistant/agent.ts +160 -0
- package/templates/health-assistant/client.tsx +2 -0
- package/templates/infocom-adventure/agent.ts +164 -0
- package/templates/infocom-adventure/client.tsx +299 -0
- package/templates/math-buddy/agent.ts +21 -0
- package/templates/math-buddy/client.tsx +2 -0
- package/templates/memory-agent/agent.ts +74 -0
- package/templates/memory-agent/client.tsx +2 -0
- package/templates/night-owl/agent.ts +98 -0
- package/templates/night-owl/client.tsx +28 -0
- package/templates/personal-finance/agent.ts +26 -0
- package/templates/personal-finance/client.tsx +2 -0
- package/templates/simple/agent.ts +6 -0
- package/templates/simple/client.tsx +2 -0
- package/templates/smart-research/agent.ts +164 -0
- package/templates/smart-research/client.tsx +2 -0
- package/templates/support/README.md +62 -0
- package/templates/support/agent.ts +19 -0
- package/templates/support/client.tsx +2 -0
- package/templates/travel-concierge/agent.ts +29 -0
- package/templates/travel-concierge/client.tsx +2 -0
- package/templates/web-researcher/agent.ts +17 -0
- package/templates/web-researcher/client.tsx +2 -0
- package/ui/_components/app.tsx +37 -0
- package/ui/_components/chat_view.tsx +36 -0
- package/ui/_components/controls.tsx +32 -0
- package/ui/_components/error_banner.tsx +18 -0
- package/ui/_components/message_bubble.tsx +21 -0
- package/ui/_components/message_list.tsx +61 -0
- package/ui/_components/state_indicator.tsx +17 -0
- package/ui/_components/thinking_indicator.tsx +19 -0
- package/ui/_components/tool_call_block.tsx +110 -0
- package/ui/_components/tool_icons.tsx +101 -0
- package/ui/_components/transcript.tsx +20 -0
- package/ui/audio.ts +170 -0
- package/ui/components.ts +49 -0
- package/ui/components_mod.ts +37 -0
- package/ui/mod.ts +48 -0
- package/ui/mount.tsx +112 -0
- package/ui/mount_context.ts +19 -0
- package/ui/session.ts +456 -0
- package/ui/session_mod.ts +27 -0
- package/ui/signals.ts +111 -0
- package/ui/types.ts +50 -0
- package/ui/worklets/capture-processor.js +62 -0
- 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,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
|
+
});
|