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; timestamp?: number; } export class SmartchatWindow extends LitElement { declare chatSession: ChatSession; declare placeholder: string; declare headerTitle: string; private messages: IDisplayMessage[] = []; private busy = false; private streamingText = ''; static properties = { chatSession: { attribute: false }, placeholder: { type: String }, headerTitle: { type: String, attribute: 'header-title' }, }; static styles: CSSResult = css` :host { display: flex; flex-direction: column; height: 100%; 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; } /* ── Header ── */ .header { display: flex; 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; } .header-icon { width: 34px; height: 34px; border-radius: 50%; background: linear-gradient(135deg, #6366f1, #7c3aed); display: flex; align-items: center; justify-content: center; font-size: 15px; color: #fff; flex-shrink: 0; box-shadow: 0 2px 10px rgba(99, 102, 241, 0.35); } .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: 1.1em; background: #818cf8; vertical-align: text-bottom; margin-left: 1px; animation: cursor-blink 0.8s step-end infinite; } @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 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() { 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, timestamp: Date.now() }, ]; 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, timestamp: Date.now() }]; 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, timestamp: Date.now() }]; } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); this.messages = [ ...this.messages, { role: 'assistant', content: `Error: ${errMsg}`, timestamp: Date.now() }, ]; } finally { this.busy = false; this.requestUpdate(); this.scrollToBottom(); } } render(): TemplateResult { return html`
${this.headerTitle}
${this.busy ? 'Thinking...' : 'Online'}
${this.messages.length === 0 && !this.busy ? html`
Start a conversation
Send a message below to begin chatting.
` : ''} ${this.messages.map( (msg) => html` `, )} ${this.busy && this.streamingText ? html`
${this.streamingText}
` : ''} ${this.busy && !this.streamingText ? html`
` : ''}
`; } } customElements.define('smartchat-window', SmartchatWindow);