180 lines
4.6 KiB
TypeScript
180 lines
4.6 KiB
TypeScript
|
|
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);
|