fix(core): use patched preview spec builder in ai chat (#10090)

[BS-2526](https://linear.app/affine-design/issue/BS-2526/chat-panel-里的-footnote-popup-需要支持交互)
This commit is contained in:
donteatfriedrice
2025-02-11 15:11:54 +00:00
parent 54d194afe7
commit 19f0eb1931
11 changed files with 227 additions and 27 deletions

View File

@@ -2,12 +2,14 @@ import './action-wrapper';
import type { EditorHost } from '@blocksuite/affine/block-std';
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import type { SpecBuilder } from '@blocksuite/affine/blocks';
import { WithDisposable } from '@blocksuite/affine/global/utils';
import { html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { createTextRenderer } from '../../../_common';
import { renderImages } from '../components/images';
export class ChatText extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor host!: EditorHost;
@@ -21,14 +23,17 @@ export class ChatText extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor state: 'finished' | 'generating' = 'finished';
@property({ attribute: false })
accessor previewSpecBuilder!: SpecBuilder;
protected override render() {
const { attachments, text, host } = this;
return html`${attachments && attachments.length > 0
? renderImages(attachments)
: nothing}${createTextRenderer(host, { customHeading: true })(
text,
this.state
)} `;
: nothing}${createTextRenderer(host, {
customHeading: true,
extensions: this.previewSpecBuilder.value,
})(text, this.state)} `;
}
}

View File

@@ -6,6 +6,7 @@ import {
FeatureFlagService,
isInsidePageEditor,
PaymentRequiredError,
type SpecBuilder,
UnauthorizedError,
} from '@blocksuite/affine/blocks';
import { WithDisposable } from '@blocksuite/affine/global/utils';
@@ -129,6 +130,9 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
@property({ attribute: false })
accessor previewSpecBuilder!: SpecBuilder;
@query('.chat-panel-messages')
accessor messagesContainer: HTMLDivElement | null = null;
@@ -316,6 +320,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
.attachments=${item.attachments}
.text=${item.content}
.state=${state}
.previewSpecBuilder=${this.previewSpecBuilder}
></chat-text>
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
${this.renderEditorActions(item, isLast)}`;

View File

@@ -3,7 +3,10 @@ import './chat-panel-messages';
import type { EditorHost } from '@blocksuite/affine/block-std';
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { NotificationProvider } from '@blocksuite/affine/blocks';
import {
NotificationProvider,
type SpecBuilder,
} from '@blocksuite/affine/blocks';
import { debounce, WithDisposable } from '@blocksuite/affine/global/utils';
import type { Store } from '@blocksuite/affine/store';
import { css, html, type PropertyValues } from 'lit';
@@ -172,6 +175,9 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false })
accessor previewSpecBuilder!: SpecBuilder;
@state()
accessor isLoading = false;
@@ -315,6 +321,7 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
.updateContext=${this.updateContext}
.host=${this.host}
.isLoading=${this.isLoading}
.previewSpecBuilder=${this.previewSpecBuilder}
></chat-panel-messages>
<chat-panel-chips
.host=${this.host}

View File

@@ -7,6 +7,7 @@ import {
EdgelessCRUDIdentifier,
getSurfaceBlock,
NotificationProvider,
type SpecBuilder,
TelemetryProvider,
} from '@blocksuite/affine/blocks';
import { html, LitElement, nothing } from 'lit';
@@ -19,6 +20,7 @@ import {
type ChatMessage,
ChatMessagesSchema,
} from '../../../blocks';
import type { TextRendererOptions } from '../../_common/components/text-renderer';
import {
ChatBlockPeekViewActions,
constructUserInfoWithMessages,
@@ -384,6 +386,9 @@ export class AIChatBlockPeekView extends LitElement {
userName: message.userName,
avatarUrl: message.avatarUrl,
};
const textRendererOptions: TextRendererOptions = {
extensions: this.previewSpecBuilder.value,
};
return html`<div class=${messageClasses}>
<ai-chat-message
@@ -393,6 +398,7 @@ export class AIChatBlockPeekView extends LitElement {
.attachments=${attachments}
.messageRole=${role}
.userInfo=${userInfo}
.textRendererOptions=${textRendererOptions}
></ai-chat-message>
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
${shouldRenderCopyMore
@@ -473,12 +479,16 @@ export class AIChatBlockPeekView extends LitElement {
} = this;
const { messages: currentChatMessages } = chatContext;
const textRendererOptions: TextRendererOptions = {
extensions: this.previewSpecBuilder.value,
};
return html`<div class="ai-chat-block-peek-view-container">
<div class="ai-chat-messages-container">
<ai-chat-messages
.host=${host}
.messages=${_historyMessages}
.textRendererOptions=${textRendererOptions}
></ai-chat-messages>
<date-time .date=${latestMessageCreatedAt}></date-time>
<div class="new-chat-messages-container">
@@ -511,6 +521,9 @@ export class AIChatBlockPeekView extends LitElement {
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor previewSpecBuilder!: SpecBuilder;
@state()
accessor _historyMessages: ChatMessage[] = [];
@@ -534,10 +547,12 @@ declare global {
export const AIChatBlockPeekViewTemplate = (
parentModel: AIChatBlockModel,
host: EditorHost
host: EditorHost,
previewSpecBuilder: SpecBuilder
) => {
return html`<ai-chat-block-peek-view
.parentModel=${parentModel}
.host=${host}
.previewSpecBuilder=${previewSpecBuilder}
></ai-chat-block-peek-view>`;
};

View File

@@ -1,7 +1,27 @@
import { AIChatBlockSpec } from '@affine/core/blocksuite/presets/blocks/ai-chat-block';
import { SpecProvider } from '@blocksuite/affine/blocks';
import { PeekViewService } from '@affine/core/modules/peek-view';
import { AppThemeService } from '@affine/core/modules/theme';
import {
type BlockStdScope,
LifeCycleWatcher,
StdIdentifier,
} from '@blocksuite/affine/block-std';
import {
ColorScheme,
createSignalFromObservable,
type Signal,
type SpecBuilder,
SpecProvider,
type ThemeExtension,
ThemeExtensionIdentifier,
} from '@blocksuite/affine/blocks';
import type { Container } from '@blocksuite/affine/global/di';
import type { ExtensionType } from '@blocksuite/affine/store';
import type { FrameworkProvider } from '@toeverything/infra';
import type { Observable } from 'rxjs';
import { buildDocDisplayMetaExtension } from './custom/root-block';
import { patchPeekViewService } from './custom/spec-patchers';
import { getFontConfigExtension } from './font-extension';
const CustomSpecs: ExtensionType[] = [
@@ -18,3 +38,73 @@ export function effects() {
// Patch edgeless preview spec for blocksuite surface-ref and embed-synced-doc
patchPreviewSpec('edgeless:preview', CustomSpecs);
}
export function getPagePreviewThemeExtension(framework: FrameworkProvider) {
class AffinePagePreviewThemeExtension
extends LifeCycleWatcher
implements ThemeExtension
{
static override readonly key = 'affine-page-preview-theme';
readonly theme: Signal<ColorScheme>;
readonly disposables: (() => void)[] = [];
static override setup(di: Container) {
super.setup(di);
di.override(ThemeExtensionIdentifier, AffinePagePreviewThemeExtension, [
StdIdentifier,
]);
}
constructor(std: BlockStdScope) {
super(std);
const theme$: Observable<ColorScheme> = framework
.get(AppThemeService)
.appTheme.theme$.map(theme => {
return theme === ColorScheme.Dark
? ColorScheme.Dark
: ColorScheme.Light;
});
const { signal, cleanup } = createSignalFromObservable<ColorScheme>(
theme$,
ColorScheme.Light
);
this.theme = signal;
this.disposables.push(cleanup);
}
getAppTheme() {
return this.theme;
}
getEdgelessTheme() {
return this.theme;
}
override unmounted() {
this.dispose();
}
dispose() {
this.disposables.forEach(dispose => dispose());
}
}
return AffinePagePreviewThemeExtension;
}
export function createPageModePreviewSpecs(
framework: FrameworkProvider
): SpecBuilder {
const specProvider = SpecProvider.getInstance();
const pagePreviewSpec = specProvider.getSpec('page:preview');
// Enable theme extension, doc display meta extension and peek view service
const peekViewService = framework.get(PeekViewService);
pagePreviewSpec.extend([
getPagePreviewThemeExtension(framework),
buildDocDisplayMetaExtension(framework),
patchPeekViewService(peekViewService),
]);
return pagePreviewSpec;
}

View File

@@ -1,4 +1,5 @@
import { ChatPanel } from '@affine/core/blocksuite/presets/ai';
import { createPageModePreviewSpecs } from '@affine/core/components/blocksuite/block-suite-editor/specs/preview';
import { AINetworkSearchService } from '@affine/core/modules/ai-button/services/network-search';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { DocSearchMenuService } from '@affine/core/modules/doc-search-menu/services';
@@ -83,6 +84,8 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
);
},
};
chatPanelRef.current.previewSpecBuilder =
createPageModePreviewSpecs(framework);
} else {
chatPanelRef.current.host = editor.host;
chatPanelRef.current.doc = editor.doc;

View File

@@ -0,0 +1,29 @@
import { toReactNode } from '@affine/component';
import { AIChatBlockPeekViewTemplate } from '@affine/core/blocksuite/presets/ai';
import type { EditorHost } from '@blocksuite/affine/block-std';
import { useFramework } from '@toeverything/infra';
import { useMemo } from 'react';
import type { AIChatBlockModel } from '../../../../blocksuite/blocks/ai-chat-block/ai-chat-model';
import { createPageModePreviewSpecs } from '../../../../components/blocksuite/block-suite-editor/specs/preview';
export type AIChatBlockPeekViewProps = {
model: AIChatBlockModel;
host: EditorHost;
};
export const AIChatBlockPeekView = ({
model,
host,
}: AIChatBlockPeekViewProps) => {
const framework = useFramework();
return useMemo(() => {
const previewSpecBuilder = createPageModePreviewSpecs(framework);
const template = AIChatBlockPeekViewTemplate(
model,
host,
previewSpecBuilder
);
return toReactNode(template);
}, [framework, model, host]);
};

View File

@@ -1,11 +1,11 @@
import { toReactNode } from '@affine/component';
import { AIChatBlockPeekViewTemplate } from '@affine/core/blocksuite/presets/ai';
import { BlockComponent } from '@blocksuite/affine/block-std';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ActivePeekView } from '../entities/peek-view';
import { PeekViewService } from '../services/peek-view';
import { AIChatBlockPeekView } from './ai-chat-block-peek-view';
import { AttachmentPreviewPeekView } from './attachment-preview';
import { DocPeekPreview } from './doc-preview';
import { ImagePreviewPeekView } from './image-preview';
@@ -46,8 +46,7 @@ function renderPeekView({ info }: ActivePeekView, animating?: boolean) {
}
if (info.type === 'ai-chat-block') {
const template = AIChatBlockPeekViewTemplate(info.model, info.host);
return toReactNode(template);
return <AIChatBlockPeekView model={info.model} host={info.host} />;
}
return null; // unreachable