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` +