Files
smartchat/ts_web/smartchat-window.ts
T

180 lines
4.6 KiB
TypeScript
Raw Normal View History

import { LitElement, html, css } from './plugins.js';
import type { CSSResult, TemplateResult, ChatSession, IAgentRunResult } from './plugins.js';
import './smartchat-message.js';
import './smartchat-input.js';
interface IDisplayMessage {
role: 'user' | 'assistant' | 'tool';
content: string;
toolName?: string;
}
export class SmartchatWindow extends LitElement {
declare chatSession: ChatSession;
declare placeholder: string;
private messages: IDisplayMessage[] = [];
private busy = false;
private streamingText = '';
static properties = {
chatSession: { attribute: false },
placeholder: { type: String },
};
static styles: CSSResult = css`
:host {
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);
overflow: hidden;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 4px;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
color: var(--smartchat-muted, #6b7280);
font-size: 14px;
}
.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%;
white-space: pre-wrap;
}
.cursor {
display: inline-block;
width: 2px;
height: 1em;
background: var(--smartchat-cursor, #6366f1);
animation: blink 1s step-end infinite;
vertical-align: text-bottom;
}
@keyframes blink {
50% { opacity: 0; }
}
.input-area {
padding: 12px 16px;
border-top: 1px solid var(--smartchat-border, #1f2937);
}
`;
constructor() {
super();
this.placeholder = 'Type a message...';
}
connectedCallback() {
super.connectedCallback();
if (this.chatSession) {
this.chatSession.updateCallbacks({
onToken: (delta: string) => {
this.streamingText += delta;
this.requestUpdate();
this.scrollToBottom();
},
onToolCall: (name: string) => {
this.messages = [
...this.messages,
{ role: 'tool', content: '', toolName: name },
];
this.requestUpdate();
this.scrollToBottom();
},
});
}
}
private scrollToBottom() {
requestAnimationFrame(() => {
const list = this.shadowRoot?.querySelector('.message-list');
if (list) {
list.scrollTop = list.scrollHeight;
}
});
}
private async handleSend(e: CustomEvent<{ text: string }>) {
if (this.busy) return;
const text = e.detail.text;
this.messages = [...this.messages, { role: 'user', content: text }];
this.busy = true;
this.streamingText = '';
this.requestUpdate();
this.scrollToBottom();
try {
const result = await this.chatSession.send(text);
this.streamingText = '';
this.messages = [...this.messages, { role: 'assistant', content: result.text }];
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
this.messages = [
...this.messages,
{ role: 'assistant', content: `Error: ${errMsg}` },
];
} finally {
this.busy = false;
this.requestUpdate();
this.scrollToBottom();
}
}
render(): TemplateResult {
return html`
<div class="message-list">
${this.messages.length === 0 && !this.busy
? html`<div class="empty-state">Start a conversation...</div>`
: ''}
${this.messages.map(
(msg) => html`
<smartchat-message
role=${msg.role}
content=${msg.content}
.toolName=${msg.toolName ?? ''}
></smartchat-message>
`,
)}
${this.busy && this.streamingText
? html`
<div class="streaming">
${this.streamingText}<span class="cursor"></span>
</div>
`
: ''}
</div>
<div class="input-area">
<smartchat-input
?disabled=${this.busy}
placeholder=${this.placeholder}
@send=${this.handleSend}
></smartchat-input>
</div>
`;
}
}
customElements.define('smartchat-window', SmartchatWindow);