fix(core): extract a scrollable text renderer fot ai panel (#10469)

This commit is contained in:
donteatfriedrice
2025-02-27 07:00:16 +00:00
parent b3821ad619
commit 7c8ba13aad
5 changed files with 143 additions and 13 deletions

View File

@@ -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);
}

View File

@@ -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),

View File

@@ -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` <style>
.ai-scrollable-text-renderer {
max-height: ${this.maxHeight}px;
}
</style>
<div class="ai-scrollable-text-renderer" @wheel=${this._onWheel}>
<text-renderer
.host=${host}
.answer=${answer}
.state=${state}
.options=${textRendererOptions}
></text-renderer>
</div>`;
}
@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`<ai-scrollable-text-renderer
.host=${host}
.answer=${answer}
.state=${state}
.textRendererOptions=${textRendererOptions}
.maxHeight=${maxHeight}
.autoScroll=${autoScroll}
></ai-scrollable-text-renderer>`;
};
};
declare global {
interface HTMLElementTagNameMap {
'ai-scrollable-text-renderer': AIScrollableTextRenderer;
}
}

View File

@@ -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`
<style>
.text-renderer-container {
max-height: ${maxHeight ? Math.max(maxHeight, 200) + 'px' : ''};
}
</style>
<div class=${classes}>
${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;
});
}

View File

@@ -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);