feat(initial): scaffold @push.rocks/smartchat with core, CLI, and web layers
Three-layer architecture built on @push.rocks/smartagent: - ts/ — ChatSession wrapping runAgent() with conversation state management - ts_cli/ — ink-based terminal chat TUI (React.createElement, no JSX) - ts_web/ — Lit web components (smartchat-window, smartchat-message, smartchat-input)
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user