import './chat/chat.js'; import './edgeless/edgeless.js'; import './copilot-service/index.js'; import { ShadowlessElement, WithDisposable } from '@cbi-blocksuite/block-std'; import { EdgelessComponentToolbar } from '@cbi-blocksuite/blocks'; import { css, html, type TemplateResult } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import { styleMap } from 'lit/directives/style-map.js'; import type { AffineEditorContainer } from '../../index.js'; import type { AllAction } from './chat/logic.js'; import { copilotConfig } from './copilot-service/copilot-config.js'; import { CreateNewService } from './copilot-service/index.js'; import { allKindService } from './copilot-service/service-base.js'; import type { AIEdgelessLogic } from './edgeless/logic.js'; import { AddCursorIcon, StarIcon } from './icons.js'; import { AILogic } from './logic.js'; import { getSurfaceElementFromEditor } from './utils/selection-utils.js'; @customElement('copilot-panel') export class CopilotPanel extends WithDisposable(ShadowlessElement) { static override styles = css` copilot-panel { width: 100%; font-family: var(--affine-font-family); overflow-y: scroll; overflow-x: visible; } .copilot-panel-setting-title { font-size: 14px; margin-top: 12px; margin-bottom: 4px; color: var(--affine-text-secondary-color); } .copilot-panel-key-input { width: 100%; border: 1px solid var(--affine-border-color, #e3e2e4); border-radius: 4px; padding: 8px; box-sizing: border-box; margin-bottom: 12px; } .copilot-panel-action-button { cursor: pointer; user-select: none; padding: 4px; background-color: var(--affine-primary-color); border-radius: 8px; color: white; height: 32px; border: 1px solid var(--affine-border-color, #e3e2e4); display: flex; align-items: center; justify-content: center; margin-top: 12px; } .copilot-panel-action-prompt { width: 100%; border: 1px solid var(--affine-border-color, #e3e2e4); border-radius: 4px; padding: 8px; box-sizing: border-box; margin-top: 8px; } .copilot-panel-action-description { font-size: 14px; margin-bottom: 8px; margin-top: 4px; color: var(--affine-text-secondary-color); } .copilot-panel-add-vendor-button { display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; } .copilot-panel-add-vendor-button:hover { background-color: var(--affine-hover-color); } .copilot-panel-vendor-item { display: flex; align-items: center; justify-content: center; font-size: 12px; padding: 2px 4px; border-radius: 4px; color: white; } .copilot-box { margin-bottom: 64px; } .service-provider-container { display: flex; align-items: center; justify-content: space-between; } .service-type { font-size: 14px; color: var(--affine-text-secondary-color); } `; @property({ attribute: false }) editor!: AffineEditorContainer; editorWithAI?: AIEdgelessLogic; aiLogic?: AILogic; get host() { return this.editor.host; } get logic() { if (!this.aiLogic) { this.aiLogic = new AILogic(() => this.host); } return this.aiLogic; } public override connectedCallback() { super.connectedCallback(); this.disposables.add( getSurfaceElementFromEditor(this.host).model.childrenUpdated.on(() => { this.requestUpdate(); }) ); } config = () => { const createNew = (type: string) => () => { const panel = new CreateNewService(); panel.type = type; panel.onSave = config => { copilotConfig.addVendor(config); this.requestUpdate(); panel.remove(); }; document.body.append(panel); }; return html`
${repeat(allKindService, v => { const list = copilotConfig.getVendorsByService(v); return html`
${v.title}
${AddCursorIcon}
${repeat(list, v => { const style = styleMap({ backgroundColor: v.impl.vendor.color, }); return html`
${v.vendor.name} ${v.impl.vendor.key} ${v.impl.name}
`; })}
`; })}
`; }; panels: Record< string, { render: () => TemplateResult; } > = { Chat: { render: () => { return html` `; }, }, Edgeless: { render: () => { return html` `; }, }, Config: { render: this.config, }, }; @state() currentPanel: keyof typeof this.panels = 'Chat'; override render() { const panel = this.panels[this.currentPanel]; return html`
${repeat(Object.keys(this.panels), key => { const changePanel = () => { this.currentPanel = key as keyof typeof this.panels; }; const style = styleMap({ 'background-color': this.currentPanel === key ? 'var(--affine-hover-color)' : 'transparent', padding: '4px 8px', 'border-radius': '8px', width: '91px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 500, color: key === this.currentPanel ? 'var(--affine-text-primary-color)' : 'var(--affine-text-secondary-color)', }); return html`
${key}
`; })}
${panel.render()}
`; } } declare global { interface HTMLElementTagNameMap { 'copilot-panel': CopilotPanel; } } export const affineFormatBarItemConfig = { type: 'custom' as const, render(): TemplateResult | null { const copilot = document.querySelector('copilot-panel'); if (!copilot) { return null; } const renderItem = (item: AllAction): TemplateResult => { if (item.type === 'group') { return html` ${item.name} ${repeat(item.children, renderItem)} `; } return html` ${item.name} `; }; return html`
${StarIcon} Ask AI
${repeat( copilot.aiLogic?.chat.docSelectionActionList ?? [], renderItem )}
`; }, }; EdgelessComponentToolbar.registerCustomRenderer({ render(): TemplateResult | undefined { const copilot = document.querySelector('copilot-panel'); if (!copilot) { return; } const renderItem = (item: AllAction): TemplateResult => { if (item.type === 'group') { return html` ${item.name} ${repeat(item.children, renderItem)} `; } if (item.hide?.() === true) { return html``; } return html` ${item.name} `; }; return html`
${StarIcon} Ask AI
${repeat( copilot.aiLogic?.chat.edgelessSelectionActionList ?? [], renderItem )}
`; }, });