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`