orbitx 0.0.1 → 0.0.3

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/.env.example ADDED
@@ -0,0 +1 @@
1
+ CHROME_PATH="C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
package/Readme.md ADDED
@@ -0,0 +1,81 @@
1
+ <!-- [![OrbitX Logo](./orbitx.png)](#) -->
2
+ # OrbitX
3
+
4
+ Better DOCS Coming soon Sorry
5
+
6
+ Example of Simple Agent
7
+ ```ts
8
+ const ollamaProvider = new OllamaProvider("gemma3:latest");
9
+
10
+ const agent = new SimpleAgent({
11
+ aiProvider: ollamaProvider,
12
+ instruction: "",
13
+ tools: [GetCurrentTimeTool]
14
+ });
15
+
16
+ let oldRole: "assistant" | "tool" | undefined;
17
+ agent.run("what is current time?", (chunk) => {
18
+ if (oldRole != chunk.role) {
19
+ process.stdout.write(`\n${chunk.role}: `);
20
+ oldRole = chunk.role;
21
+ }
22
+ process.stdout.write(chunk.content);
23
+ if (chunk.done) {
24
+ process.stdout.write("\n\n");
25
+ oldRole = undefined;
26
+ }
27
+ });
28
+ ```
29
+
30
+ Example of Research Agent
31
+ ```ts
32
+ const ollamaProvider = new OllamaProvider("gemma3:latest");
33
+
34
+ const agent = new ResearcherAgent({ aiProvider: ollamaProvider });
35
+
36
+ let oldRole: "assistant" | "tool" | undefined;
37
+ agent.run("How artificial intelligence is changing the job market", (chunk) => {
38
+ if (oldRole != chunk.role) {
39
+ process.stdout.write(`\n${chunk.role}: `);
40
+ oldRole = chunk.role;
41
+ }
42
+ process.stdout.write(chunk.content);
43
+ if (chunk.done) {
44
+ process.stdout.write("\n\n");
45
+ oldRole = undefined;
46
+ }
47
+ });
48
+ ```
49
+
50
+
51
+
52
+ Writing from base
53
+ ```ts
54
+ const ollamaProvider = new OllamaProvider("gemma3:latest");
55
+
56
+ const connection = new MCPConnection();
57
+
58
+ const mcpServer = new MCPServer(connection);
59
+ mcpServer.registerTool(GetCurrentTimeTool);
60
+ const mcpClient = new MCPClient("DEFAULT_ENV", connection);
61
+
62
+ const agent = new BaseAgent({
63
+ aiProvider: ollamaProvider,
64
+ instruction: "",
65
+ mcpClient,
66
+ allowedTools: [GetCurrentTimeTool]
67
+ });
68
+
69
+ let oldRole: "assistant" | "tool" | undefined;
70
+ agent.run("what is current time?", (chunk) => {
71
+ if (oldRole != chunk.role) {
72
+ process.stdout.write(`\n${chunk.role}: `);
73
+ oldRole = chunk.role;
74
+ }
75
+ process.stdout.write(chunk.content);
76
+ if (chunk.done) {
77
+ process.stdout.write("\n\n");
78
+ oldRole = undefined;
79
+ }
80
+ });
81
+ ```
package/orbitx.png ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orbitx",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "An AI SDK providing agents, swarms, planners, skills, MCP tools, workflows, and multi-provider AI integrations.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -14,6 +14,21 @@
14
14
  "Skills",
15
15
  "Tools"
16
16
  ],
17
+ "type": "module",
17
18
  "author": "ali39",
18
- "license": "MIT"
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "express": "^5.2.1",
22
+ "ollama": "^0.6.3",
23
+ "openai": "^6.45.0",
24
+ "puppeteer-core": "^24.43.1",
25
+ "socket.io": "^4.8.3"
26
+ },
27
+ "devDependencies": {
28
+ "@types/express": "^5.0.6",
29
+ "@types/node": "^26.0.0",
30
+ "nodemon": "^3.1.14",
31
+ "tsx": "^4.22.4",
32
+ "typescript": "^6.0.3"
33
+ }
19
34
  }
@@ -0,0 +1,397 @@
1
+ <!doctype html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <script src="static/tailwindcss.js"></script>
8
+ <style>
9
+ * {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ body {
14
+ margin: 0;
15
+ font-family: 'Inter', system-ui, sans-serif;
16
+ }
17
+
18
+ ::-webkit-scrollbar {
19
+ width: 6px;
20
+ }
21
+
22
+ ::-webkit-scrollbar-track {
23
+ background: transparent;
24
+ }
25
+
26
+ ::-webkit-scrollbar-thumb {
27
+ background: #d1d5db;
28
+ border-radius: 3px;
29
+ }
30
+
31
+ .dark ::-webkit-scrollbar-thumb {
32
+ background: #374151;
33
+ }
34
+
35
+ .tool-val {
36
+ max-width: 260px;
37
+ overflow: hidden;
38
+ text-overflow: ellipsis;
39
+ white-space: nowrap;
40
+ }
41
+ </style>
42
+ <script>
43
+ tailwind.config = {
44
+ darkMode: 'class',
45
+ theme: {
46
+ extend: {
47
+ fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] },
48
+ colors: {
49
+ surface: { 0: '#f5f5f4', 1: '#fafaf9', 2: '#ffffff' },
50
+ accent: { bg: '#eff6ff', text: '#1d4ed8', border: '#bfdbfe' },
51
+ pro: { bg: '#f3f0ff', text: '#6d28d9' },
52
+ success: { bg: '#f0fdf4', text: '#15803d', border: '#bbf7d0' },
53
+ warning: { bg: '#fffbeb', text: '#b45309', border: '#fde68a' },
54
+ danger: { bg: '#fff1f2', text: '#be123c', border: '#fecdd3' },
55
+ },
56
+ animation: {
57
+ 'fade-up': 'fadeUp 0.18s ease-out',
58
+ 'blink': 'blink 1s step-start infinite',
59
+ },
60
+ keyframes: {
61
+ fadeUp: { '0%': { opacity: 0, transform: 'translateY(6px)' }, '100%': { opacity: 1, transform: 'translateY(0)' } },
62
+ blink: { '0%,100%': { opacity: 1 }, '50%': { opacity: 0 } },
63
+ }
64
+ }
65
+ }
66
+ }
67
+ </script>
68
+ </head>
69
+
70
+ <body class="bg-stone-100 dark:bg-zinc-900 min-h-screen flex flex-col">
71
+
72
+ <!-- Header -->
73
+ <header
74
+ class="sticky top-0 z-10 bg-white dark:bg-zinc-800 border-b border-stone-200 dark:border-zinc-700 px-4 py-3 flex items-center justify-between">
75
+ <div class="flex items-center gap-2.5">
76
+ <div class="w-7 h-7 rounded-full bg-violet-100 dark:bg-violet-900 flex items-center justify-center">
77
+ <svg class="w-4 h-4 text-violet-600 dark:text-violet-300" viewBox="0 0 24 24" fill="none"
78
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
79
+ <path d="M12 2L2 7l10 5 10-5-10-5z" />
80
+ <path d="M2 17l10 5 10-5" />
81
+ <path d="M2 12l10 5 10-5" />
82
+ </svg>
83
+ </div>
84
+ <span class="font-semibold text-sm text-zinc-800 dark:text-zinc-100">Agent Thread</span>
85
+ <span id="status-dot" class="w-2 h-2 rounded-full bg-emerald-400"></span>
86
+ </div>
87
+ <button onclick="toggleDark()"
88
+ class="p-1.5 rounded-lg hover:bg-stone-100 dark:hover:bg-zinc-700 text-zinc-500 dark:text-zinc-400 transition-colors">
89
+ <svg id="icon-sun" class="w-4 h-4 hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor"
90
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
91
+ <circle cx="12" cy="12" r="5" />
92
+ <line x1="12" y1="1" x2="12" y2="3" />
93
+ <line x1="12" y1="21" x2="12" y2="23" />
94
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
95
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
96
+ <line x1="1" y1="12" x2="3" y2="12" />
97
+ <line x1="21" y1="12" x2="23" y2="12" />
98
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
99
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
100
+ </svg>
101
+ <svg id="icon-moon" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
102
+ stroke-linecap="round" stroke-linejoin="round">
103
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
104
+ </svg>
105
+ </button>
106
+ </header>
107
+
108
+ <!-- Thread -->
109
+ <main id="thread" class="flex-1 overflow-y-auto px-4 py-4 flex flex-col gap-3 max-w-3xl mx-auto w-full"></main>
110
+
111
+ <!-- Typing indicator placeholder -->
112
+ <div id="typing" class="hidden max-w-3xl mx-auto w-full px-4 pb-2">
113
+ <div class="flex items-center gap-2 text-xs text-zinc-400 dark:text-zinc-500">
114
+ <div class="flex gap-1">
115
+ <span class="w-1.5 h-1.5 rounded-full bg-zinc-400 dark:bg-zinc-500 animate-bounce"
116
+ style="animation-delay:0ms"></span>
117
+ <span class="w-1.5 h-1.5 rounded-full bg-zinc-400 dark:bg-zinc-500 animate-bounce"
118
+ style="animation-delay:120ms"></span>
119
+ <span class="w-1.5 h-1.5 rounded-full bg-zinc-400 dark:bg-zinc-500 animate-bounce"
120
+ style="animation-delay:240ms"></span>
121
+ </div>
122
+ Agent is working…
123
+ </div>
124
+ </div>
125
+
126
+ <!-- Input -->
127
+ <footer class="sticky bottom-0 bg-white dark:bg-zinc-800 border-t border-stone-200 dark:border-zinc-700 px-4 py-3">
128
+ <div class="max-w-3xl mx-auto flex gap-2">
129
+ <input id="user-input" type="text" placeholder="Send a message…"
130
+ class="flex-1 text-sm rounded-xl border border-stone-200 dark:border-zinc-600 bg-stone-50 dark:bg-zinc-700 text-zinc-800 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-500 px-4 py-2.5 outline-none focus:ring-2 focus:ring-violet-400 focus:border-transparent transition"
131
+ onkeydown="if(event.key==='Enter')sendUserMessage()" />
132
+ <button onclick="sendUserMessage()"
133
+ class="px-4 py-2.5 rounded-xl bg-violet-600 hover:bg-violet-700 active:scale-95 text-white text-sm font-medium transition-all flex items-center gap-1.5">
134
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
135
+ stroke-linecap="round" stroke-linejoin="round">
136
+ <line x1="22" y1="2" x2="11" y2="13" />
137
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
138
+ </svg>
139
+ Send
140
+ </button>
141
+ </div>
142
+ </footer>
143
+
144
+
145
+ <script>
146
+ /* ── ICONS ─────────────────────────────────────────── */
147
+ const ICONS = {
148
+ search: `<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
149
+ globe: `<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
150
+ code: `<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>`,
151
+ file: `<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
152
+ check: `<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
153
+ cross: `<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
154
+ clock: `<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`,
155
+ agent: `<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`,
156
+ };
157
+
158
+ const TOOL_META = {
159
+ 'web-search': { icon: ICONS.search, label: 'Web search', color: 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/40' },
160
+ 'read-web-page': { icon: ICONS.globe, label: 'Fetch page', color: 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/40' },
161
+ 'run-code': { icon: ICONS.code, label: 'Run code', color: 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-900/40' },
162
+ 'read-file': { icon: ICONS.file, label: 'Read file', color: 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-900/40' },
163
+ };
164
+ function toolMeta(name) { return TOOL_META[name] || { icon: ICONS.code, label: name, color: 'text-zinc-500 bg-zinc-100 dark:bg-zinc-700' }; }
165
+
166
+ /* ── STATE ─────────────────────────────────────────── */
167
+ // Each entry: { role: 'user'|'assistant'|'tool', content }
168
+ // tool content is a JSON string
169
+ let messageQueue = [];
170
+ let isGenerating = false;
171
+ let currentGroupEl = null; // the current agent-block div
172
+ let currentGroupItems = []; // items in current agent block
173
+
174
+ /* ── DEMO SCRIPT ───────────────────────────────────── */
175
+ // Pre-loaded conversation that plays automatically
176
+ const DEMO_SCRIPT = [
177
+ { role: 'user', content: 'Find me the best price for a flight from Birjand to Tehran.' },
178
+ { role: 'assistant', content: 'On it — starting with a broad price search.' },
179
+ { role: 'tool', content: JSON.stringify({ tool: 'web-search', inputs: { query: 'best price flights Birjand to Tehran' }, output: '10 results returned', status: 'ok' }) },
180
+ { role: 'assistant', content: "Got results. Let me open the top comparison site for live pricing." },
181
+ { role: 'tool', content: JSON.stringify({ tool: 'read-web-page', inputs: { url: 'https://zbordirect.com/en/low-cost/from-iran/to-iran/birjand-tehran', outputType: 'text' }, error: 'Navigation timeout of 50000 ms exceeded', status: 'err' }) },
182
+ { role: 'assistant', content: 'That site timed out. Trying Trip.com instead.' },
183
+ { role: 'tool', content: JSON.stringify({ tool: 'read-web-page', inputs: { url: 'https://us.trip.com/flights/birjand-to-tehran/airfares-xbj-thr/', outputType: 'text' }, error: 'Navigation timeout of 50000 ms exceeded', status: 'err' }) },
184
+ { role: 'assistant', content: 'Still failing. Let me search for more reliable comparison sites first.' },
185
+ { role: 'tool', content: JSON.stringify({ tool: 'web-search', inputs: { query: 'reliable flight comparison Iran Birjand Tehran' }, output: 'Skyscanner, Jetcost, Kayak, Wego found', status: 'ok' }) },
186
+ { role: 'assistant', content: 'Found better options. Reading Skyscanner now.' },
187
+ { role: 'tool', content: JSON.stringify({ tool: 'read-web-page', inputs: { url: 'https://www.skyscanner.com.my/routes/xbj/thra/birjand-to-tehran.html', outputType: 'text' }, output: 'Page loaded — 3 airlines, cheapest from ~$42', status: 'ok' }) },
188
+ { role: 'assistant', content: "Here's what I found: Iran Air and Mahan Air both operate direct flights (≈90 min). Cheapest one-way is **~$42** on Iran Air, rising to ~$65 on Mahan Air. Book at least 2 weeks out for the best fares." },
189
+ { role: 'user', content: 'What about train options between the two cities?' },
190
+ { role: 'assistant', content: "Let me look up train schedules from Birjand to Tehran." },
191
+ { role: 'tool', content: JSON.stringify({ tool: 'web-search', inputs: { query: 'train Birjand to Tehran schedule Raja Rail' }, output: '6 results found', status: 'ok' }) },
192
+ { role: 'assistant', content: 'Fetching the rail operator timetable.' },
193
+ { role: 'tool', content: JSON.stringify({ tool: 'read-web-page', inputs: { url: 'https://www.raja.ir/en/trains', outputType: 'text' }, output: 'Timetable loaded — 2 weekly departures', status: 'ok' }) },
194
+ { role: 'assistant', content: "Train option: Raja Rail runs **twice weekly** (Tues & Sat), departing 18:00, arriving ~08:30 next day — roughly 14.5 hours. Tickets start at **~450,000 IRR** (~$1.50 at free-market rate) in second class. Much cheaper than flying but significantly longer." },
195
+ ];
196
+
197
+ /* ── DOM HELPERS ──────────────────────────────────── */
198
+ const thread = document.getElementById('thread');
199
+ const typingEl = document.getElementById('typing');
200
+
201
+ function scrollToBottom() {
202
+ thread.scrollTop = thread.scrollHeight;
203
+ }
204
+
205
+ function showTyping(show) {
206
+ typingEl.classList.toggle('hidden', !show);
207
+ if (show) typingEl.scrollIntoView({ behavior: 'smooth', block: 'end' });
208
+ }
209
+
210
+ function trunc(s, n) { return s && s.length > n ? s.slice(0, n) + '…' : s; }
211
+
212
+ function md(text) {
213
+ return (text || '')
214
+ .replace(/\*\*(.+?)\*\*/g, '<strong class="font-semibold">$1</strong>')
215
+ .replace(/\n/g, '<br>');
216
+ }
217
+
218
+ /* ── RENDER: USER BUBBLE ─────────────────────────── */
219
+ function appendUserBubble(content) {
220
+ // Close any open agent group
221
+ currentGroupEl = null;
222
+ currentGroupItems = [];
223
+
224
+ const wrap = document.createElement('div');
225
+ wrap.className = 'flex justify-end animate-fade-up';
226
+ wrap.innerHTML = `
227
+ <div class="max-w-[72%] bg-violet-600 text-white text-sm rounded-2xl rounded-br-sm px-4 py-2.5 leading-relaxed shadow-sm">
228
+ ${md(content)}
229
+ </div>`;
230
+ thread.appendChild(wrap);
231
+ scrollToBottom();
232
+ }
233
+
234
+ /* ── RENDER: AGENT TEXT inside group ─────────────── */
235
+ function getOrCreateAgentGroup() {
236
+ if (!currentGroupEl) {
237
+ const wrap = document.createElement('div');
238
+ wrap.className = 'flex gap-2.5 animate-fade-up';
239
+
240
+ const avatarCol = document.createElement('div');
241
+ avatarCol.className = 'flex flex-col items-center pt-1 gap-1';
242
+ avatarCol.innerHTML = `
243
+ <div class="w-7 h-7 rounded-full bg-violet-100 dark:bg-violet-900/60 flex items-center justify-center text-violet-600 dark:text-violet-300 flex-shrink-0">
244
+ ${ICONS.agent}
245
+ </div>
246
+ <div class="agent-line w-px flex-1 bg-stone-200 dark:bg-zinc-700 min-h-0"></div>`;
247
+
248
+ const bodyCol = document.createElement('div');
249
+ bodyCol.className = 'flex-1 min-w-0 flex flex-col gap-1.5 pb-1';
250
+ bodyCol.dataset.agentBody = '1';
251
+
252
+ wrap.appendChild(avatarCol);
253
+ wrap.appendChild(bodyCol);
254
+ thread.appendChild(wrap);
255
+ currentGroupEl = bodyCol;
256
+ }
257
+ return currentGroupEl;
258
+ }
259
+
260
+ function appendAgentText(content) {
261
+ const body = getOrCreateAgentGroup();
262
+ const p = document.createElement('p');
263
+ p.className = 'text-sm text-zinc-700 dark:text-zinc-200 leading-relaxed';
264
+ p.innerHTML = md(content);
265
+ body.appendChild(p);
266
+ scrollToBottom();
267
+ }
268
+
269
+ /* ── RENDER: TOOL CARD inside group ──────────────── */
270
+ function appendToolCard(data) {
271
+ const body = getOrCreateAgentGroup();
272
+ const meta = toolMeta(data.tool);
273
+ const st = data.status;
274
+
275
+ let badgeHtml, resultHtml = '';
276
+ if (st === 'ok') {
277
+ badgeHtml = `<span class="inline-flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-emerald-50 dark:bg-emerald-900/40 text-emerald-600 dark:text-emerald-400">${ICONS.check} Done</span>`;
278
+ if (data.output) resultHtml = `<p class="text-[11px] text-zinc-400 dark:text-zinc-500 mt-1 leading-relaxed">${trunc(data.output, 100)}</p>`;
279
+ } else if (st === 'err') {
280
+ badgeHtml = `<span class="inline-flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-red-50 dark:bg-red-900/40 text-red-500 dark:text-red-400">${ICONS.cross} Failed</span>`;
281
+ if (data.error) resultHtml = `<p class="text-[11px] font-mono text-red-400 dark:text-red-500 mt-1 leading-relaxed">${data.error}</p>`;
282
+ } else {
283
+ badgeHtml = `<span class="inline-flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-zinc-100 dark:bg-zinc-700 text-zinc-400 dark:text-zinc-500">${ICONS.clock} Running</span>`;
284
+ }
285
+
286
+ const kvHtml = Object.entries(data.inputs || {}).map(([k, v]) => `
287
+ <span class="text-[10px] text-zinc-400 dark:text-zinc-500">${k}</span>
288
+ <span class="tool-val text-[10px] font-mono text-zinc-500 dark:text-zinc-400 bg-stone-100 dark:bg-zinc-800 border border-stone-200 dark:border-zinc-700 rounded px-1.5 py-0.5">${trunc(String(v), 55)}</span>`
289
+ ).join('');
290
+
291
+ const card = document.createElement('div');
292
+ card.className = 'rounded-xl border border-stone-200 dark:border-zinc-700 bg-white dark:bg-zinc-800/60 overflow-hidden';
293
+ card.innerHTML = `
294
+ <div class="flex items-center gap-2 px-3 py-2 border-b border-stone-100 dark:border-zinc-700/60">
295
+ <div class="w-5 h-5 rounded-md flex items-center justify-center flex-shrink-0 ${meta.color}">${meta.icon}</div>
296
+ <span class="text-xs font-medium text-zinc-600 dark:text-zinc-300">${meta.label}</span>
297
+ <div class="ml-auto">${badgeHtml}</div>
298
+ </div>
299
+ <div class="px-3 py-2 flex flex-wrap items-center gap-x-2 gap-y-1">
300
+ ${kvHtml}
301
+ ${resultHtml}
302
+ </div>`;
303
+ body.appendChild(card);
304
+ scrollToBottom();
305
+ }
306
+
307
+ /* ── MESSAGE PROCESSOR ────────────────────────────── */
308
+ async function processMessage(msg) {
309
+ await delay(60);
310
+ if (msg.role === 'user') {
311
+ currentGroupEl = null; // reset group
312
+ appendUserBubble(msg.content);
313
+ } else if (msg.role === 'assistant') {
314
+ await delay(300);
315
+ appendAgentText(msg.content);
316
+ } else if (msg.role === 'tool') {
317
+ let data;
318
+ try { data = JSON.parse(msg.content); } catch { data = {}; }
319
+ await delay(200);
320
+ appendToolCard(data);
321
+ }
322
+ }
323
+
324
+ function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
325
+
326
+ async function drainQueue() {
327
+ if (isGenerating) return;
328
+ isGenerating = true;
329
+ showTyping(true);
330
+ while (messageQueue.length > 0) {
331
+ const msg = messageQueue.shift();
332
+ await processMessage(msg);
333
+ await delay(msg.role === 'tool' ? 350 : 180);
334
+ }
335
+ showTyping(false);
336
+ isGenerating = false;
337
+ }
338
+
339
+ function enqueue(msgs) {
340
+ messageQueue.push(...msgs);
341
+ drainQueue();
342
+ }
343
+
344
+ /* ── USER INPUT ───────────────────────────────────── */
345
+ function sendUserMessage() {
346
+ const input = document.getElementById('user-input');
347
+ const text = input.value.trim();
348
+ if (!text) return;
349
+ input.value = '';
350
+
351
+ // Immediately show user bubble
352
+ currentGroupEl = null;
353
+ appendUserBubble(text);
354
+
355
+ // Simulate agent reply
356
+ const reply = generateAgentReply(text);
357
+ messageQueue.push(...reply);
358
+ drainQueue();
359
+ }
360
+
361
+ function generateAgentReply(userText) {
362
+ // Simple mock — builds a short tool + response sequence
363
+ const q = userText.toLowerCase();
364
+ const searchQuery = q.slice(0, 50);
365
+ return [
366
+ { role: 'assistant', content: `Understood. Let me look that up for you.` },
367
+ { role: 'tool', content: JSON.stringify({ tool: 'web-search', inputs: { query: searchQuery }, output: 'Results found', status: 'ok' }) },
368
+ { role: 'assistant', content: `I searched for <strong>"${userText}"</strong>. In a real setup the agent would process results here and give you a detailed answer. Try connecting a real API to replace this stub.` },
369
+ ];
370
+ }
371
+
372
+ /* ── DARK MODE ────────────────────────────────────── */
373
+ function toggleDark() {
374
+ document.documentElement.classList.toggle('dark');
375
+ document.getElementById('icon-sun').classList.toggle('hidden');
376
+ document.getElementById('icon-moon').classList.toggle('hidden');
377
+ }
378
+
379
+ /* ── BOOT: play demo script ───────────────────────── */
380
+ window.addEventListener('DOMContentLoaded', () => {
381
+ // Stagger demo messages with realistic delays
382
+ const staggered = DEMO_SCRIPT.map((msg, i) => {
383
+ const base = msg.role === 'user' ? 600 : msg.role === 'tool' ? 450 : 300;
384
+ return { msg, delay: i * base };
385
+ });
386
+
387
+ staggered.forEach(({ msg, delay: d }) => {
388
+ setTimeout(() => {
389
+ messageQueue.push(msg);
390
+ if (!isGenerating) drainQueue();
391
+ }, d);
392
+ });
393
+ });
394
+ </script>
395
+ </body>
396
+
397
+ </html>
@@ -0,0 +1,25 @@
1
+ export interface Message {
2
+ role: "user" | "system" | "assistant" | "tool";
3
+ content?: string;
4
+ }
5
+
6
+ export interface ChatResponse {
7
+ content: string;
8
+ inputTokens: number;
9
+ outputTokens: number;
10
+ }
11
+
12
+ export type StreamCallback = (chunk: {
13
+ role: "assistant" | "tool";
14
+ content: string;
15
+ done: boolean;
16
+ }) => void;
17
+
18
+ export abstract class AIProvider {
19
+ abstract chat(
20
+ messages: Message[],
21
+ streamCallback?: StreamCallback
22
+ ): Promise<ChatResponse>;
23
+ }
24
+
25
+ export default AIProvider;