import { ShadowlessElement, WithDisposable } from '@cbi-blocksuite/block-std'; import { css, html, nothing, type PropertyValues } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import { styleMap } from 'lit/directives/style-map.js'; import { ChatServiceKind, EmbeddingServiceKind, } from '../copilot-service/service-base.js'; import { ChatFeatureKey } from '../doc/api.js'; import type { AILogic } from '../logic.js'; import type { ChatMessage, ChatReactiveData, EmbeddedDoc } from './logic.js'; @customElement('copilot-chat-panel') export class CopilotChatPanel extends WithDisposable(ShadowlessElement) implements ChatReactiveData { static override styles = css` copilot-chat-panel { margin-top: 12px; display: flex; flex-direction: column; width: 100%; font-family: var(--affine-font-family); height: 100%; gap: 4px; overflow: auto; } .copilot-chat-prompt-container { border-top: 0.5px solid var(--affine-border-color); height: 189px; padding: 8px; display: flex; gap: 10px; } .copilot-chat-prompt { flex: 1; border: none; border-radius: 4px; padding: 4px 8px; outline: none; background-color: transparent; } .send-button { height: 32px; border-radius: 50%; width: 32px; background-color: var(--affine-primary-color); color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; } .sync-workspace-button { border: 1px solid var(--affine-border-color); height: 32px; border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; } .synced-doc-list { margin-bottom: 14px; color: var(--affine-text-secondary-color); font-size: 12px; } .history-item { position: relative; max-width: calc(100% - 78px); display: flex; flex-direction: column; padding: 10px; border-radius: 8px; font-size: 14px; border: 0.5px solid var(--affine-border-color); background-color: var(--affine-background-primary-color); white-space: pre-wrap; line-height: 22px; color: var(--affine-text-primary-color); } .history-refs { font-size: 12px; color: var(--affine-text-secondary-color); } `; @property({ attribute: false }) logic!: AILogic; get chat() { return this.logic.chat; } get host() { return this.logic.getHost(); } @query('.chat-messages-container') chatMessagesContainer!: HTMLDivElement; protected override updated(_changedProperties: PropertyValues) { super.updated(_changedProperties); if ( _changedProperties.has('history') || _changedProperties.has('tempMessage') ) { this.chatMessagesContainer.scrollTop = this.chatMessagesContainer.scrollHeight; } } @state() tempMessage?: string; @state() history: ChatMessage[] = []; @state() currentRequest?: number; get loading(): boolean { return this.currentRequest != null; } @state() value = ''; @state() syncedDocs: EmbeddedDoc[] = []; @state() surfaceSelection = false; @state() docSelection = false; public override connectedCallback() { super.connectedCallback(); this.logic.chat.reactiveData = this; this.disposables.add( this.host.doc.collection.slots.docUpdated.on(() => { this.requestUpdate(); }) ); this.checkSelection(); this.disposables.add( this.host.selection.slots.changed.on(() => { this.checkSelection(); }) ); } checkSelection() { this.surfaceSelection = this.host.selection.value.some( v => v.type === 'surface' ); this.docSelection = this.host.selection.value.some(v => v.type === 'block'); } addSelectionBackground = async () => { if (this.surfaceSelection) { await this.chat.selectShapesForBackground(); } if (this.docSelection) { await this.chat.selectTextForBackground(); } this.requestUpdate(); }; renderMessage = (message: ChatMessage) => { if (message.role === 'system') { return null; } if (message.role === 'user') { const style = styleMap({ alignSelf: 'flex-end', }); return html`
${repeat(message.content, item => { if (item.type === 'text') { return html`
${item.text}
`; } if (item.type === 'image_url') { return html`
`; } return null; })}
`; } if (message.role === 'assistant') { const style = styleMap({ alignItems: 'flex-start', backgroundColor: 'var(--affine-blue-100)', }); return html`
${message.content}
${message.sources?.length ? html`
sources:
${repeat(message.sources, ref => { const doc = this.host.doc.collection.getDoc(ref.id); if (!doc) { return; } const title = doc.meta?.title || 'Untitled'; const jumpTo = () => { this.host.doc = doc; }; return html` ${title}`; })}
` : null}
replace
insert below
`; } return null; }; toolbar() { // const lastMessage = this.history[this.history.length - 1]; // return html`
// ${this.loading // ? html`
// ${StopIcon} Stop //
` // : nothing} // ${!this.loading && lastMessage.role === 'assistant' // ? html`
// Longer //
//
// Shorter //
` // : nothing} //
`; } @query('.copilot-chat-panel-chat-input') input!: HTMLInputElement; protected override render(): unknown { const getAnswer = async () => { this.input.focus(); const text = this.input.value; this.input.value = ''; await this.chat.genAnswer(text); }; const keydown = async (e: KeyboardEvent) => { e.stopPropagation(); if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey && !e.isComposing) { e.preventDefault(); await getAnswer(); } }; const sendButtonStyle = styleMap({ opacity: !this.loading ? '1' : '0.5', }); return html`
Embedding Service
Chat Service
${repeat(this.history, this.renderMessage)} ${this.tempMessage ? this.renderMessage({ role: 'assistant', content: this.tempMessage, sources: [], }) : nothing}
${this.toolbar()}
`; } } declare global { interface HTMLElementTagNameMap { 'copilot-chat-panel': CopilotChatPanel; } }