feat(web): add web demo and polish Lit web components UI and demo tooling
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user