feat(web): add web demo and polish Lit web components UI and demo tooling

This commit is contained in:
2026-03-07 08:15:23 +00:00
parent dd04edb420
commit 54f9cea7f9
15 changed files with 994 additions and 100 deletions
+41
View File
@@ -0,0 +1,41 @@
import * as esbuild from 'esbuild';
// Shim unused Node.js built-ins that get pulled in by dependencies.
const nodeShimPlugin: esbuild.Plugin = {
name: 'node-builtins-shim',
setup(build) {
// Mark node built-ins as empty modules — they are imported but not actually used at runtime.
const builtins = ['path', 'fs', 'os', 'crypto', 'stream', 'util', 'events', 'http', 'https', 'net', 'tls', 'child_process', 'url', 'assert', 'buffer', 'tty', 'readline'];
const filter = new RegExp(`^(${builtins.join('|')})$`);
build.onResolve({ filter }, (args) => ({
path: args.path,
namespace: 'node-shim',
}));
build.onLoad({ filter: /.*/, namespace: 'node-shim' }, () => ({
contents: 'export default {};',
loader: 'js',
}));
},
};
const buildOptions: esbuild.BuildOptions = {
entryPoints: ['demo/demo.web.ts'],
bundle: true,
format: 'esm',
platform: 'browser',
outdir: 'demo',
sourcemap: true,
target: 'es2022',
logLevel: 'info',
plugins: [nodeShimPlugin],
};
const serve = process.argv.includes('--serve');
if (serve) {
const ctx = await esbuild.context(buildOptions);
const { host, port } = await ctx.serve({ servedir: 'demo' });
console.log(`\n 🌐 Web demo running at http://localhost:${port}/demo.web.html\n`);
} else {
await esbuild.build(buildOptions);
}
+21
View File
@@ -0,0 +1,21 @@
import { getModel } from '@push.rocks/smartai';
import { startChat } from '../ts_cli/index.js';
const apiKey = process.env.ANTHROPIC_TOKEN;
if (!apiKey) {
console.error('Missing ANTHROPIC_TOKEN environment variable.');
console.error('Usage: ANTHROPIC_TOKEN=sk-... tsx demo/demo.cli.ts');
process.exit(1);
}
const model = getModel({
provider: 'anthropic',
model: 'claude-sonnet-4-5-20250929',
apiKey,
});
await startChat({
model,
system: 'You are a friendly, concise assistant. Keep responses short unless asked for detail.',
modelName: 'Claude Sonnet',
});
+96
View File
@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>smartchat — Web Demo</title>
<!-- Put your Anthropic API key here -->
<meta name="anthropic-token" content="YOUR_KEY_HERE">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: #050508;
color: #e4e4e7;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
position: relative;
}
/* Subtle gradient background */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 600px 400px at 30% 20%, rgba(99, 102, 241, 0.06), transparent),
radial-gradient(ellipse 500px 300px at 70% 80%, rgba(139, 92, 246, 0.04), transparent);
pointer-events: none;
z-index: 0;
}
.page-header {
text-align: center;
margin-bottom: 20px;
position: relative;
z-index: 1;
}
.page-title {
font-size: 13px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.3);
margin-bottom: 4px;
}
.page-subtitle {
font-size: 11px;
color: rgba(255, 255, 255, 0.15);
}
smartchat-window {
width: 100%;
max-width: 680px;
height: min(75vh, 640px);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 16px;
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.03),
0 8px 40px rgba(0, 0, 0, 0.4),
0 2px 12px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 1;
}
</style>
<script type="module" src="./demo.web.js"></script>
</head>
<body>
<div class="page-header">
<div class="page-title">smartchat</div>
<div class="page-subtitle">Web Component Demo</div>
</div>
<smartchat-window
placeholder="Ask me anything..."
header-title="SmartChat AI"
></smartchat-window>
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
import '../ts_web/index.js';
import { ChatSession } from '../ts/index.js';
import { getModel } from '@push.rocks/smartai';
// Read API key from a meta tag so it doesn't need to be hardcoded.
// Set it in the HTML: <meta name="anthropic-token" content="sk-...">
const metaToken = document.querySelector<HTMLMetaElement>('meta[name="anthropic-token"]');
const apiKey = metaToken?.content;
const init = () => {
const chatWindow = document.querySelector('smartchat-window') as any;
if (!chatWindow) return;
if (!apiKey || apiKey === 'YOUR_KEY_HERE') {
// Mock mode — show sample messages to preview the UI
chatWindow.headerTitle = 'SmartChat AI (Preview)';
const mockMessages = [
{ role: 'user', content: 'Hey! Can you explain what TypeScript generics are?', timestamp: Date.now() - 60000 },
{ role: 'assistant', content: 'Generics let you write reusable code that works with multiple types while keeping type safety.\n\nFor example, instead of writing separate functions for arrays of strings and numbers, you write one:\n\n```ts\nfunction first<T>(items: T[]): T {\n return items[0];\n}\n```\n\nThe `<T>` is a type parameter — TypeScript infers the actual type when you call the function. So `first([1, 2, 3])` returns a `number`, and `first(["a", "b"])` returns a `string`.', timestamp: Date.now() - 45000 },
{ role: 'user', content: 'That makes sense! Can generics have constraints?', timestamp: Date.now() - 30000 },
{ role: 'tool', content: '', toolName: 'search_docs', timestamp: Date.now() - 25000 },
{ role: 'assistant', content: 'Yes! Use `extends` to constrain what types are allowed:\n\n```ts\nfunction getLength<T extends { length: number }>(item: T): number {\n return item.length;\n}\n```\n\nThis accepts strings, arrays, or any object with a `.length` property — but rejects numbers or booleans at compile time.', timestamp: Date.now() - 20000 },
];
// Inject mock messages by setting internal state
(chatWindow as any).messages = mockMessages;
(chatWindow as any).requestUpdate();
return;
}
const model = getModel({
provider: 'anthropic',
model: 'claude-sonnet-4-5-20250929',
apiKey,
});
const session = new ChatSession({
model,
system: 'You are a friendly, concise assistant. Keep responses short unless asked for detail.',
});
chatWindow.chatSession = session;
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}