feat(web): add web demo and polish Lit web components UI and demo tooling
This commit is contained in:
@@ -3,3 +3,5 @@ dist_*/
|
||||
.nogit/
|
||||
coverage/
|
||||
*.tgz
|
||||
demo/demo.web.js
|
||||
demo/demo.web.js.map
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[ 233ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:8765/favicon.ico:0
|
||||
@@ -0,0 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-07 - 0.1.0 - feat(web)
|
||||
add web demo and polish Lit web components UI and demo tooling
|
||||
|
||||
- Add web demo artifacts and build tooling: demo.web.html, demo.web.ts, demo.cli.ts, and demo/build.web.ts (esbuild) plus demoWeb and demoCli npm scripts
|
||||
- Enhance web components (ts_web): smartchat-window, smartchat-message, smartchat-input — new header/status, avatars, timestamps, improved streaming/thinking indicators, responsive textarea input, and refined styles/animations
|
||||
- Add esbuild to devDependencies to support demo bundling/serving
|
||||
- Update .gitignore to exclude demo build outputs
|
||||
- Add README with usage docs and examples
|
||||
|
||||
## 2026-03-06 - 0.0.1 - initial
|
||||
Scaffold initial @push.rocks/smartchat package with core, CLI, and web layers.
|
||||
|
||||
- Three-layer architecture built on @push.rocks/smartagent.
|
||||
- ts/: ChatSession wrapper around runAgent() with conversation state management.
|
||||
- ts_cli/: Ink-based terminal chat TUI implemented using React.createElement (no JSX).
|
||||
- ts_web/: Lit web components (smartchat-window, smartchat-message, smartchat-input).
|
||||
@@ -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();
|
||||
}
|
||||
+5
-2
@@ -24,7 +24,9 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --verbose --timeout 120)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany)"
|
||||
"build": "(tsbuild tsfolders --allowimplicitany)",
|
||||
"demoCli": "tsx demo/demo.cli.ts",
|
||||
"demoWeb": "tsx demo/build.web.ts --serve"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.3.0",
|
||||
@@ -32,7 +34,8 @@
|
||||
"@git.zone/tstest": "^3.3.0",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0"
|
||||
"@types/react": "^19.0.0",
|
||||
"esbuild": "^0.27.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartagent": "^3.0.1",
|
||||
|
||||
Generated
+3
@@ -48,6 +48,9 @@ importers:
|
||||
'@types/react':
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.14
|
||||
esbuild:
|
||||
specifier: ^0.27.3
|
||||
version: 0.27.3
|
||||
|
||||
packages:
|
||||
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
# @push.rocks/smartchat
|
||||
|
||||
Interactive chat interfaces for AI agents — CLI TUI and web components, built on [`@push.rocks/smartagent`](https://code.foss.global/push.rocks/smartagent).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pnpm install @push.rocks/smartchat
|
||||
```
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Overview
|
||||
|
||||
`@push.rocks/smartchat` gives you everything you need to build conversational AI interfaces — in the terminal and on the web. It ships three entry points:
|
||||
|
||||
| Entry Point | Import Path | What You Get |
|
||||
|---|---|---|
|
||||
| **Core** | `@push.rocks/smartchat` | `ChatSession` — headless conversation state management |
|
||||
| **CLI** | `@push.rocks/smartchat/cli` | `startChat()` — full-featured terminal TUI (ink-based) |
|
||||
| **Web** | `@push.rocks/smartchat/web` | `<smartchat-window>` and friends — Lit-based web components |
|
||||
|
||||
Under the hood, every turn calls [`@push.rocks/smartagent`](https://code.foss.global/push.rocks/smartagent)'s `runAgent()`, giving you streaming tokens, tool calling, and multi-step agentic loops out of the box.
|
||||
|
||||
## Usage
|
||||
|
||||
### 🧠 Core — `ChatSession`
|
||||
|
||||
`ChatSession` manages in-memory conversation history and wraps the agent loop. It's headless — bring your own UI or use it from scripts, bots, and backend services.
|
||||
|
||||
```typescript
|
||||
import { ChatSession } from '@push.rocks/smartchat';
|
||||
import type { IChatSessionOptions, IChatCallbacks, IChatUsage } from '@push.rocks/smartchat';
|
||||
|
||||
// Create a session with your model and optional tools
|
||||
const session = new ChatSession(
|
||||
{
|
||||
model: myLanguageModel, // LanguageModelV3 from @push.rocks/smartai
|
||||
system: 'You are a helpful coding assistant.',
|
||||
tools: myToolSet, // optional ToolSet
|
||||
maxSteps: 20, // max agentic steps per turn (default: 20)
|
||||
},
|
||||
{
|
||||
onToken: (delta) => process.stdout.write(delta),
|
||||
onToolCall: (name, input) => console.log(`🔧 ${name}`),
|
||||
onToolResult: (name, result) => console.log(`✅ ${name}`),
|
||||
onTurnComplete: (result) => console.log(`Done — ${result.usage.totalTokens} tokens`),
|
||||
onError: (err) => console.error(err),
|
||||
}
|
||||
);
|
||||
|
||||
// Send a message and get the result
|
||||
const result = await session.send('Explain the builder pattern in TypeScript');
|
||||
console.log(result.text);
|
||||
|
||||
// Inspect state
|
||||
console.log(session.getUsage()); // { inputTokens, outputTokens, totalTokens, turns }
|
||||
console.log(session.getMessages()); // full conversation history
|
||||
console.log(session.isBusy()); // false (turn complete)
|
||||
|
||||
// Manage conversation
|
||||
session.clear(); // reset history + usage
|
||||
session.setMessages(savedHistory); // restore a saved session
|
||||
session.updateOptions({ system: 'New system prompt' });
|
||||
session.updateCallbacks({ onToken: null });
|
||||
```
|
||||
|
||||
#### `IChatSessionOptions`
|
||||
|
||||
| Property | Type | Description |
|
||||
|---|---|---|
|
||||
| `model` | `LanguageModelV3` | The language model to use (required) |
|
||||
| `system` | `string` | System prompt for the agent |
|
||||
| `tools` | `ToolSet` | Tools available to the agent |
|
||||
| `maxSteps` | `number` | Max agentic steps per turn (default: `20`) |
|
||||
|
||||
#### `IChatCallbacks`
|
||||
|
||||
| Callback | Signature | When It Fires |
|
||||
|---|---|---|
|
||||
| `onToken` | `(delta: string) => void` | Each streamed text token |
|
||||
| `onToolCall` | `(name: string, input: unknown) => void` | Tool call starts |
|
||||
| `onToolResult` | `(name: string, result: string) => void` | Tool call completes |
|
||||
| `onTurnComplete` | `(result: IAgentRunResult) => void` | Turn finishes |
|
||||
| `onError` | `(error: Error) => void` | Error occurs |
|
||||
|
||||
#### `IChatUsage`
|
||||
|
||||
| Field | Type |
|
||||
|---|---|
|
||||
| `inputTokens` | `number` |
|
||||
| `outputTokens` | `number` |
|
||||
| `totalTokens` | `number` |
|
||||
| `turns` | `number` |
|
||||
|
||||
---
|
||||
|
||||
### 💻 CLI — Terminal TUI
|
||||
|
||||
Launch a beautiful, interactive chat right in your terminal. Built with [ink](https://github.com/vadimdemedes/ink) (React for CLIs).
|
||||
|
||||
```typescript
|
||||
import { startChat } from '@push.rocks/smartchat/cli';
|
||||
import type { IStartChatOptions } from '@push.rocks/smartchat/cli';
|
||||
|
||||
await startChat({
|
||||
model: myLanguageModel,
|
||||
system: 'You are a friendly assistant.',
|
||||
tools: myToolSet,
|
||||
maxSteps: 30,
|
||||
modelName: 'Claude Opus', // displayed in the header & status bar
|
||||
});
|
||||
```
|
||||
|
||||
#### Features
|
||||
|
||||
- ⚡ **Real-time streaming** — tokens appear as they're generated
|
||||
- 🔧 **Tool call visibility** — see tool invocations inline
|
||||
- 📊 **Status bar** — model name, token usage, turn count
|
||||
- ⌨️ **Keyboard shortcuts**:
|
||||
- `Ctrl+C` — exit
|
||||
- `Ctrl+L` — clear conversation
|
||||
- 💬 **Slash commands**:
|
||||
- `/clear` — reset the conversation
|
||||
- `/quit` or `/exit` — exit the TUI
|
||||
|
||||
#### `IStartChatOptions`
|
||||
|
||||
| Property | Type | Description |
|
||||
|---|---|---|
|
||||
| `model` | `LanguageModelV3` | The language model (required) |
|
||||
| `system` | `string` | System prompt |
|
||||
| `tools` | `ToolSet` | Tools for the agent |
|
||||
| `maxSteps` | `number` | Max agentic steps per turn |
|
||||
| `modelName` | `string` | Display name shown in header & status bar |
|
||||
|
||||
---
|
||||
|
||||
### 🌐 Web — Lit Web Components
|
||||
|
||||
Drop a full chat UI into any web app. Three custom elements, fully themeable with CSS custom properties.
|
||||
|
||||
```typescript
|
||||
import '@push.rocks/smartchat/web';
|
||||
import { ChatSession } from '@push.rocks/smartchat';
|
||||
|
||||
// Create a session
|
||||
const session = new ChatSession({
|
||||
model: myLanguageModel,
|
||||
system: 'You are a helpful assistant.',
|
||||
});
|
||||
|
||||
// Get the element and wire it up
|
||||
const chatWindow = document.querySelector('smartchat-window');
|
||||
chatWindow.chatSession = session;
|
||||
```
|
||||
|
||||
```html
|
||||
<smartchat-window placeholder="Ask me anything..."></smartchat-window>
|
||||
```
|
||||
|
||||
#### Components
|
||||
|
||||
| Element | Description |
|
||||
|---|---|
|
||||
| `<smartchat-window>` | Full chat window — message list + input area. Pass a `ChatSession` via the `chatSession` property. |
|
||||
| `<smartchat-message>` | Single message bubble. Attributes: `role` (`user` / `assistant` / `tool`), `content`, `toolName`. |
|
||||
| `<smartchat-input>` | Input field + send button. Fires `send` custom event. Attributes: `disabled`, `placeholder`. |
|
||||
|
||||
#### 🎨 Theming with CSS Custom Properties
|
||||
|
||||
Every visual aspect is customizable:
|
||||
|
||||
```css
|
||||
smartchat-window {
|
||||
/* Layout */
|
||||
--smartchat-bg: #111827;
|
||||
--smartchat-text: #e5e7eb;
|
||||
--smartchat-font: system-ui, -apple-system, sans-serif;
|
||||
--smartchat-radius: 12px;
|
||||
--smartchat-border: #1f2937;
|
||||
--smartchat-muted: #6b7280;
|
||||
|
||||
/* User messages */
|
||||
--smartchat-user-bubble: #2563eb;
|
||||
--smartchat-user-text: #fff;
|
||||
|
||||
/* Assistant messages */
|
||||
--smartchat-assistant-bubble: #374151;
|
||||
--smartchat-assistant-text: #e5e7eb;
|
||||
|
||||
/* Tool messages */
|
||||
--smartchat-tool-bubble: #1e293b;
|
||||
--smartchat-tool-text: #94a3b8;
|
||||
--smartchat-tool-accent: #6366f1;
|
||||
|
||||
/* Input area */
|
||||
--smartchat-input-bg: #1f2937;
|
||||
--smartchat-input-border: #374151;
|
||||
--smartchat-input-focus: #6366f1;
|
||||
--smartchat-input-text: #e5e7eb;
|
||||
--smartchat-input-placeholder: #6b7280;
|
||||
|
||||
/* Send button */
|
||||
--smartchat-send-bg: #6366f1;
|
||||
--smartchat-send-text: #fff;
|
||||
|
||||
/* Streaming cursor */
|
||||
--smartchat-cursor: #6366f1;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
@push.rocks/smartchat
|
||||
├── ts/ → Core module (ChatSession, interfaces)
|
||||
├── ts_cli/ → CLI TUI (ink + React)
|
||||
└── ts_web/ → Web components (Lit)
|
||||
```
|
||||
|
||||
- **Core** (`ts/`) is headless and dependency-light. CLI and Web layers both import from Core.
|
||||
- **CLI** (`ts_cli/`) uses [ink](https://github.com/vadimdemedes/ink) with `React.createElement` (no JSX).
|
||||
- **Web** (`ts_web/`) uses [Lit](https://lit.dev/) with `static properties` (no decorators).
|
||||
- The agentic loop is powered by [`@push.rocks/smartagent`](https://code.foss.global/push.rocks/smartagent), which orchestrates multi-step tool calling with any `LanguageModelV3` from [`@push.rocks/smartai`](https://code.foss.global/push.rocks/smartai).
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartchat',
|
||||
version: '0.1.0',
|
||||
description: 'Interactive chat interfaces for AI agents — CLI TUI and web components, built on @push.rocks/smartagent.'
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartchat',
|
||||
version: '0.1.0',
|
||||
description: 'Interactive chat interfaces for AI agents — CLI TUI and web components, built on @push.rocks/smartagent.'
|
||||
}
|
||||
+101
-33
@@ -16,51 +16,100 @@ export class SmartchatInput extends LitElement {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
.input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: var(--smartchat-input-bg, #1f2937);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--smartchat-input-border, #374151);
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.input-row:focus-within {
|
||||
border-color: var(--smartchat-input-focus, #6366f1);
|
||||
.input-wrap:focus-within {
|
||||
border-color: rgba(99, 102, 241, 0.45);
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
input {
|
||||
.input-wrap.disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--smartchat-input-text, #e5e7eb);
|
||||
color: #e4e4e7;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
resize: none;
|
||||
min-height: 22px;
|
||||
max-height: 110px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--smartchat-input-placeholder, #6b7280);
|
||||
textarea::placeholder {
|
||||
color: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--smartchat-send-bg, #6366f1);
|
||||
color: var(--smartchat-send-text, #fff);
|
||||
.send-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 6px 16px;
|
||||
background: linear-gradient(135deg, #6366f1, #7c3aed);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.12s ease, opacity 0.12s ease, box-shadow 0.12s ease;
|
||||
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
.send-btn:hover:not(:disabled) {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 3px 12px rgba(99, 102, 241, 0.45);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
.send-btn:active:not(:disabled) {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.25;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.send-btn svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 5px 2px 0;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 10.5px;
|
||||
color: rgba(255, 255, 255, 0.13);
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0 3px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
font-family: inherit;
|
||||
font-size: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -71,13 +120,17 @@ export class SmartchatInput extends LitElement {
|
||||
}
|
||||
|
||||
private handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !e.shiftKey && this.value.trim()) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
|
||||
private handleInput(e: Event) {
|
||||
this.value = (e.target as HTMLInputElement).value;
|
||||
const textarea = e.target as HTMLTextAreaElement;
|
||||
this.value = textarea.value;
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 110) + 'px';
|
||||
}
|
||||
|
||||
private submit() {
|
||||
@@ -90,25 +143,40 @@ export class SmartchatInput extends LitElement {
|
||||
}),
|
||||
);
|
||||
this.value = '';
|
||||
const input = this.shadowRoot?.querySelector('input');
|
||||
if (input) input.value = '';
|
||||
const textarea = this.shadowRoot?.querySelector('textarea');
|
||||
if (textarea) {
|
||||
textarea.value = '';
|
||||
textarea.style.height = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const canSend = !this.disabled && !!this.value.trim();
|
||||
return html`
|
||||
<div class="input-row">
|
||||
<input
|
||||
type="text"
|
||||
<div class="input-wrap ${this.disabled ? 'disabled' : ''}">
|
||||
<textarea
|
||||
rows="1"
|
||||
.value=${this.value}
|
||||
@input=${this.handleInput}
|
||||
@keydown=${this.handleKeydown}
|
||||
placeholder=${this.disabled ? 'Waiting for response...' : this.placeholder}
|
||||
?disabled=${this.disabled}
|
||||
/>
|
||||
<button @click=${this.submit} ?disabled=${this.disabled}>
|
||||
Send
|
||||
></textarea>
|
||||
<button
|
||||
class="send-btn"
|
||||
@click=${this.submit}
|
||||
?disabled=${!canSend}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="19" x2="12" y2="5"></line>
|
||||
<polyline points="5 12 12 5 19 12"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hint">
|
||||
<span class="hint-text"><kbd>Enter</kbd> send · <kbd>Shift+Enter</kbd> new line</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
+138
-31
@@ -5,54 +5,138 @@ export class SmartchatMessage extends LitElement {
|
||||
declare role: 'user' | 'assistant' | 'tool';
|
||||
declare content: string;
|
||||
declare toolName: string;
|
||||
declare timestamp: number;
|
||||
|
||||
static properties = {
|
||||
role: { type: String },
|
||||
content: { type: String },
|
||||
toolName: { type: String },
|
||||
timestamp: { type: Number },
|
||||
};
|
||||
|
||||
static styles: CSSResult = css`
|
||||
:host {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
animation: msg-in 0.3s ease-out both;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
max-width: 85%;
|
||||
word-wrap: break-word;
|
||||
@keyframes msg-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Message Row ── */
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.row.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.row.assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* ── Avatar ── */
|
||||
.avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.avatar.user {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
color: #fff;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.avatar.assistant {
|
||||
background: linear-gradient(135deg, #6366f1, #7c3aed);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Bubble ── */
|
||||
.bubble {
|
||||
padding: 10px 14px;
|
||||
border-radius: 18px;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
max-width: min(75%, 480px);
|
||||
}
|
||||
|
||||
.user {
|
||||
background: var(--smartchat-user-bubble, #2563eb);
|
||||
color: var(--smartchat-user-text, #fff);
|
||||
margin-left: auto;
|
||||
border-bottom-right-radius: 2px;
|
||||
.bubble.user {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
.assistant {
|
||||
background: var(--smartchat-assistant-bubble, #374151);
|
||||
color: var(--smartchat-assistant-text, #e5e7eb);
|
||||
margin-right: auto;
|
||||
border-bottom-left-radius: 2px;
|
||||
.bubble.assistant {
|
||||
background: var(--smartchat-assistant-bg, #1e1e2e);
|
||||
border: 1px solid var(--smartchat-assistant-border, rgba(255, 255, 255, 0.08));
|
||||
color: var(--smartchat-assistant-text, #d1d5db);
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
|
||||
.tool {
|
||||
background: var(--smartchat-tool-bubble, #1e293b);
|
||||
color: var(--smartchat-tool-text, #94a3b8);
|
||||
margin-right: auto;
|
||||
font-size: 0.85em;
|
||||
font-family: monospace;
|
||||
border-left: 3px solid var(--smartchat-tool-accent, #6366f1);
|
||||
/* ── Tool ── */
|
||||
.tool-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding: 2px 0 2px 38px;
|
||||
}
|
||||
|
||||
.tool-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border: 1px solid rgba(99, 102, 241, 0.15);
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-family: 'SF Mono', 'Fira Code', ui-monospace, monospace;
|
||||
color: #a5b4fc;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
color: var(--smartchat-tool-accent, #6366f1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Timestamp ── */
|
||||
.time-row {
|
||||
padding: 1px 38px 6px;
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.time-row.user {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.time-row.assistant {
|
||||
text-align: left;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -61,16 +145,39 @@ export class SmartchatMessage extends LitElement {
|
||||
this.role = 'user';
|
||||
this.content = '';
|
||||
this.toolName = '';
|
||||
this.timestamp = 0;
|
||||
}
|
||||
|
||||
private formatTime(ts: number): string {
|
||||
if (!ts) return '';
|
||||
const d = new Date(ts);
|
||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (this.role === 'tool') {
|
||||
return html`
|
||||
<div class="message ${this.role}">
|
||||
${this.role === 'tool' && this.toolName
|
||||
? html`<div class="tool-name">${this.toolName}</div>`
|
||||
: ''}
|
||||
<div>${this.content}</div>
|
||||
<div class="tool-row">
|
||||
<div class="tool-pill">
|
||||
<span class="tool-icon">⚡</span>
|
||||
<span class="tool-name">${this.toolName || 'tool'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="row ${this.role}">
|
||||
<div class="avatar ${this.role}">
|
||||
${this.role === 'user' ? '⬆' : '✦'}
|
||||
</div>
|
||||
<div class="bubble ${this.role}">
|
||||
${this.content}
|
||||
</div>
|
||||
</div>
|
||||
${this.timestamp
|
||||
? html`<div class="time-row ${this.role}">${this.formatTime(this.timestamp)}</div>`
|
||||
: ''}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
+253
-33
@@ -7,11 +7,13 @@ interface IDisplayMessage {
|
||||
role: 'user' | 'assistant' | 'tool';
|
||||
content: string;
|
||||
toolName?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export class SmartchatWindow extends LitElement {
|
||||
declare chatSession: ChatSession;
|
||||
declare placeholder: string;
|
||||
declare headerTitle: string;
|
||||
private messages: IDisplayMessage[] = [];
|
||||
private busy = false;
|
||||
private streamingText = '';
|
||||
@@ -19,6 +21,7 @@ export class SmartchatWindow extends LitElement {
|
||||
static properties = {
|
||||
chatSession: { attribute: false },
|
||||
placeholder: { type: String },
|
||||
headerTitle: { type: String, attribute: 'header-title' },
|
||||
};
|
||||
|
||||
static styles: CSSResult = css`
|
||||
@@ -26,63 +29,244 @@ export class SmartchatWindow extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--smartchat-bg, #111827);
|
||||
color: var(--smartchat-text, #e5e7eb);
|
||||
font-family: var(--smartchat-font, system-ui, -apple-system, sans-serif);
|
||||
border-radius: var(--smartchat-radius, 12px);
|
||||
background: var(--smartchat-bg, #0c0c14);
|
||||
color: var(--smartchat-text, #e4e4e7);
|
||||
font-family: var(--smartchat-font, 'Inter', system-ui, -apple-system, sans-serif);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
border-radius: var(--smartchat-radius, 16px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
/* ── Header ── */
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 20px;
|
||||
background: var(--smartchat-header-bg, rgba(255, 255, 255, 0.025));
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
.header-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6366f1, #7c3aed);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
color: var(--smartchat-muted, #6b7280);
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 10px rgba(99, 102, 241, 0.35);
|
||||
}
|
||||
|
||||
.streaming {
|
||||
padding: 8px 12px;
|
||||
background: var(--smartchat-assistant-bubble, #374151);
|
||||
color: var(--smartchat-assistant-text, #e5e7eb);
|
||||
border-radius: 8px;
|
||||
border-bottom-left-radius: 2px;
|
||||
max-width: 85%;
|
||||
.header-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #f0f0f3;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.header-status {
|
||||
font-size: 11px;
|
||||
color: #71717a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 6px rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
.status-dot.busy {
|
||||
background: #f59e0b;
|
||||
box-shadow: 0 0 6px rgba(245, 158, 11, 0.5);
|
||||
animation: pulse-dot 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
/* ── Message List ── */
|
||||
.message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
scroll-behavior: smooth;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.message-list::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.message-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.message-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.message-list::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
/* ── Empty State ── */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
gap: 14px;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.12), rgba(124, 58, 237, 0.06));
|
||||
border: 1px solid rgba(99, 102, 241, 0.12);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
color: #818cf8;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #e4e4e7;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
font-size: 13px;
|
||||
color: #52525b;
|
||||
max-width: 260px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Streaming ── */
|
||||
.streaming-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 4px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.stream-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6366f1, #7c3aed);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stream-bubble {
|
||||
background: #1e1e2e;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: #d1d5db;
|
||||
padding: 10px 14px;
|
||||
border-radius: 18px;
|
||||
border-bottom-left-radius: 6px;
|
||||
max-width: min(75%, 480px);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
height: 1em;
|
||||
background: var(--smartchat-cursor, #6366f1);
|
||||
animation: blink 1s step-end infinite;
|
||||
height: 1.1em;
|
||||
background: #818cf8;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
animation: cursor-blink 0.8s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
@keyframes cursor-blink {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* ── Thinking Indicator ── */
|
||||
.thinking-bubble {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 12px 18px;
|
||||
background: #1e1e2e;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 18px;
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
|
||||
.thinking-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #6366f1;
|
||||
animation: thinking-bounce 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.thinking-dot:nth-child(2) { animation-delay: 0.16s; }
|
||||
.thinking-dot:nth-child(3) { animation-delay: 0.32s; }
|
||||
|
||||
@keyframes thinking-bounce {
|
||||
0%, 60%, 100% {
|
||||
transform: translateY(0);
|
||||
opacity: 0.3;
|
||||
}
|
||||
30% {
|
||||
transform: translateY(-5px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Input Area ── */
|
||||
.input-area {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--smartchat-border, #1f2937);
|
||||
padding: 12px 20px 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: var(--smartchat-input-area-bg, rgba(255, 255, 255, 0.015));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.placeholder = 'Type a message...';
|
||||
this.headerTitle = 'SmartChat';
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
@@ -97,7 +281,7 @@ export class SmartchatWindow extends LitElement {
|
||||
onToolCall: (name: string) => {
|
||||
this.messages = [
|
||||
...this.messages,
|
||||
{ role: 'tool', content: '', toolName: name },
|
||||
{ role: 'tool', content: '', toolName: name, timestamp: Date.now() },
|
||||
];
|
||||
this.requestUpdate();
|
||||
this.scrollToBottom();
|
||||
@@ -119,7 +303,7 @@ export class SmartchatWindow extends LitElement {
|
||||
if (this.busy) return;
|
||||
|
||||
const text = e.detail.text;
|
||||
this.messages = [...this.messages, { role: 'user', content: text }];
|
||||
this.messages = [...this.messages, { role: 'user', content: text, timestamp: Date.now() }];
|
||||
this.busy = true;
|
||||
this.streamingText = '';
|
||||
this.requestUpdate();
|
||||
@@ -128,12 +312,12 @@ export class SmartchatWindow extends LitElement {
|
||||
try {
|
||||
const result = await this.chatSession.send(text);
|
||||
this.streamingText = '';
|
||||
this.messages = [...this.messages, { role: 'assistant', content: result.text }];
|
||||
this.messages = [...this.messages, { role: 'assistant', content: result.text, timestamp: Date.now() }];
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
this.messages = [
|
||||
...this.messages,
|
||||
{ role: 'assistant', content: `Error: ${errMsg}` },
|
||||
{ role: 'assistant', content: `Error: ${errMsg}`, timestamp: Date.now() },
|
||||
];
|
||||
} finally {
|
||||
this.busy = false;
|
||||
@@ -144,9 +328,28 @@ export class SmartchatWindow extends LitElement {
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="header-icon">✦</div>
|
||||
<div class="header-info">
|
||||
<div class="header-title">${this.headerTitle}</div>
|
||||
<div class="header-status">
|
||||
<span class="status-dot ${this.busy ? 'busy' : ''}"></span>
|
||||
${this.busy ? 'Thinking...' : 'Online'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-list">
|
||||
${this.messages.length === 0 && !this.busy
|
||||
? html`<div class="empty-state">Start a conversation...</div>`
|
||||
? html`
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">✦</div>
|
||||
<div class="empty-title">Start a conversation</div>
|
||||
<div class="empty-subtitle">
|
||||
Send a message below to begin chatting.
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${this.messages.map(
|
||||
(msg) => html`
|
||||
@@ -154,17 +357,34 @@ export class SmartchatWindow extends LitElement {
|
||||
role=${msg.role}
|
||||
content=${msg.content}
|
||||
.toolName=${msg.toolName ?? ''}
|
||||
.timestamp=${msg.timestamp ?? 0}
|
||||
></smartchat-message>
|
||||
`,
|
||||
)}
|
||||
${this.busy && this.streamingText
|
||||
? html`
|
||||
<div class="streaming">
|
||||
<div class="streaming-row">
|
||||
<div class="stream-avatar">✦</div>
|
||||
<div class="stream-bubble">
|
||||
${this.streamingText}<span class="cursor"></span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${this.busy && !this.streamingText
|
||||
? html`
|
||||
<div class="streaming-row">
|
||||
<div class="stream-avatar">✦</div>
|
||||
<div class="thinking-bubble">
|
||||
<span class="thinking-dot"></span>
|
||||
<span class="thinking-dot"></span>
|
||||
<span class="thinking-dot"></span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
<smartchat-input
|
||||
?disabled=${this.busy}
|
||||
|
||||
Reference in New Issue
Block a user