From 7c8ba13aada3223f043bbfd73370c7086bc33016 Mon Sep 17 00:00:00 2001 From: donteatfriedrice Date: Thu, 27 Feb 2025 07:00:16 +0000 Subject: [PATCH] fix(core): extract a scrollable text renderer fot ai panel (#10469) --- .../blocksuite/ai/actions/answer-renderer.ts | 4 +- .../core/src/blocksuite/ai/ai-panel.ts | 4 +- .../components/ai-scrollable-text-renderer.ts | 133 ++++++++++++++++++ .../blocksuite/ai/components/text-renderer.ts | 10 +- .../core/src/blocksuite/ai/effects.ts | 5 + 5 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 packages/frontend/core/src/blocksuite/ai/components/ai-scrollable-text-renderer.ts diff --git a/packages/frontend/core/src/blocksuite/ai/actions/answer-renderer.ts b/packages/frontend/core/src/blocksuite/ai/actions/answer-renderer.ts index eee74853b..ae2f36f0d 100644 --- a/packages/frontend/core/src/blocksuite/ai/actions/answer-renderer.ts +++ b/packages/frontend/core/src/blocksuite/ai/actions/answer-renderer.ts @@ -1,7 +1,7 @@ import type { EditorHost } from '@blocksuite/affine/block-std'; import type { MindmapElementModel } from '@blocksuite/affine/blocks'; -import { createTextRenderer } from '../components/text-renderer'; +import { createAIScrollableTextRenderer } from '../components/ai-scrollable-text-renderer'; import { createMindmapExecuteRenderer, createMindmapRenderer, @@ -52,5 +52,5 @@ export function actionToAnswerRenderer< return createImageRenderer(host, { height: 300 }); } - return createTextRenderer(host, { maxHeight: 320 }); + return createAIScrollableTextRenderer(host, {}, 320, true); } diff --git a/packages/frontend/core/src/blocksuite/ai/ai-panel.ts b/packages/frontend/core/src/blocksuite/ai/ai-panel.ts index 3cb656028..8adeb658b 100644 --- a/packages/frontend/core/src/blocksuite/ai/ai-panel.ts +++ b/packages/frontend/core/src/blocksuite/ai/ai-panel.ts @@ -33,7 +33,7 @@ import { replaceWithMarkdown, } from './actions/page-response'; import type { AIItemConfig } from './components/ai-item/types'; -import { createTextRenderer } from './components/text-renderer'; +import { createAIScrollableTextRenderer } from './components/ai-scrollable-text-renderer'; import { AIProvider } from './provider'; import { reportResponse } from './utils/action-reporter'; import { getAIPanelWidget } from './utils/ai-widgets'; @@ -293,7 +293,7 @@ export function buildAIPanelConfig( const ctx = new AIContext(); const searchService = framework.get(AINetworkSearchService); return { - answerRenderer: createTextRenderer(panel.host, { maxHeight: 320 }), + answerRenderer: createAIScrollableTextRenderer(panel.host, {}, 320, true), finishStateConfig: buildFinishConfig(panel, 'chat', ctx), generatingStateConfig: buildGeneratingConfig(), errorStateConfig: buildErrorConfig(panel), diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-scrollable-text-renderer.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-scrollable-text-renderer.ts new file mode 100644 index 000000000..efa6d54fd --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-scrollable-text-renderer.ts @@ -0,0 +1,133 @@ +import { + type EditorHost, + ShadowlessElement, +} from '@blocksuite/affine/block-std'; +import { scrollbarStyle } from '@blocksuite/affine/blocks'; +import { throttle, WithDisposable } from '@blocksuite/affine/global/utils'; +import type { PropertyValues } from 'lit'; +import { css, html } from 'lit'; +import { property, query } from 'lit/decorators.js'; + +import type { + AffineAIPanelState, + AffineAIPanelWidgetConfig, +} from '../widgets/ai-panel/type'; +import type { TextRendererOptions } from './text-renderer'; + +export class AIScrollableTextRenderer extends WithDisposable( + ShadowlessElement +) { + static override styles = css` + .ai-scrollable-text-renderer { + overflow-y: auto; + } + + ${scrollbarStyle('.ai-scrollable-text-renderer')}; + `; + + private _lastScrollHeight = 0; + + private readonly _scrollToEnd = () => { + requestAnimationFrame(() => { + if (!this._scrollableTextRenderer) { + return; + } + const scrollHeight = this._scrollableTextRenderer.scrollHeight || 0; + + if (scrollHeight > this._lastScrollHeight) { + this._lastScrollHeight = scrollHeight; + // Scroll when scroll height greater than maxheight + this._scrollableTextRenderer?.scrollTo({ + top: scrollHeight, + }); + } + }); + }; + + private readonly _throttledScrollToEnd = throttle(this._scrollToEnd, 300); + + private readonly _onWheel = (e: WheelEvent) => { + e.stopPropagation(); + if (this.state === 'generating') { + e.preventDefault(); + } + }; + + protected override updated(_changedProperties: PropertyValues) { + if ( + this.autoScroll && + _changedProperties.has('answer') && + (this.state === 'generating' || this.state === 'finished') + ) { + this._throttledScrollToEnd(); + } + } + + override render() { + const { host, answer, state, textRendererOptions } = this; + + return html` +
+ +
`; + } + + @property({ attribute: false }) + accessor answer!: string; + + @property({ attribute: false }) + accessor host: EditorHost | null = null; + + @property({ attribute: false }) + accessor state: AffineAIPanelState | undefined = undefined; + + @property({ attribute: false }) + accessor textRendererOptions!: TextRendererOptions; + + @property({ attribute: false }) + accessor maxHeight = 320; + + @property({ attribute: false }) + accessor autoScroll = true; + + @query('.ai-scrollable-text-renderer') + accessor _scrollableTextRenderer: HTMLDivElement | null = null; +} + +export const createAIScrollableTextRenderer: ( + host: EditorHost, + textRendererOptions: TextRendererOptions, + maxHeight: number, + autoScroll: boolean +) => AffineAIPanelWidgetConfig['answerRenderer'] = ( + host, + textRendererOptions, + maxHeight, + autoScroll +) => { + return (answer, state) => { + return html``; + }; +}; + +declare global { + interface HTMLElementTagNameMap { + 'ai-scrollable-text-renderer': AIScrollableTextRenderer; + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts index 094d7a313..6fbc92611 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts @@ -88,7 +88,6 @@ const customHeadingStyles = css` `; export type TextRendererOptions = { - maxHeight?: number; customHeading?: boolean; extensions?: ExtensionType[]; additionalMiddlewares?: TransformerMiddleware[]; @@ -284,18 +283,12 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) { return nothing; } - const { maxHeight, customHeading } = this.options; + const { customHeading } = this.options; const classes = classMap({ 'text-renderer-container': true, - 'show-scrollbar': !!maxHeight, 'custom-heading': !!customHeading, }); return html` -
${keyed( this._doc, @@ -332,7 +325,6 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) { // Apply min-height to prevent shrinking this._container.style.minHeight = `${this._maxContainerHeight}px`; } - this._container.scrollTop = this._container.scrollHeight; }); } diff --git a/packages/frontend/core/src/blocksuite/ai/effects.ts b/packages/frontend/core/src/blocksuite/ai/effects.ts index e1bb284f9..28e73e62b 100644 --- a/packages/frontend/core/src/blocksuite/ai/effects.ts +++ b/packages/frontend/core/src/blocksuite/ai/effects.ts @@ -31,6 +31,7 @@ import { ChatPanelChip } from './chat-panel/components/chip'; import { ChatPanelDocChip } from './chat-panel/components/doc-chip'; import { ChatPanelFileChip } from './chat-panel/components/file-chip'; import { effects as componentAiItemEffects } from './components/ai-item'; +import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer'; import { AskAIButton } from './components/ask-ai-button'; import { AskAIIcon } from './components/ask-ai-icon'; import { AskAIPanel } from './components/ask-ai-panel'; @@ -107,6 +108,10 @@ export function registerAIEffects() { customElements.define('affine-ai-chat', AIChatBlockComponent); customElements.define('ai-chat-message', AIChatMessage); customElements.define('ai-chat-messages', AIChatMessages); + customElements.define( + 'ai-scrollable-text-renderer', + AIScrollableTextRenderer + ); customElements.define('image-placeholder', ImagePlaceholder); customElements.define('chat-image', ChatImage); customElements.define('chat-images', ChatImages);