mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
fix(core): extract a scrollable text renderer fot ai panel (#10469)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user