mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08: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 { EditorHost } from '@blocksuite/affine/block-std';
|
||||||
import type { MindmapElementModel } from '@blocksuite/affine/blocks';
|
import type { MindmapElementModel } from '@blocksuite/affine/blocks';
|
||||||
|
|
||||||
import { createTextRenderer } from '../components/text-renderer';
|
import { createAIScrollableTextRenderer } from '../components/ai-scrollable-text-renderer';
|
||||||
import {
|
import {
|
||||||
createMindmapExecuteRenderer,
|
createMindmapExecuteRenderer,
|
||||||
createMindmapRenderer,
|
createMindmapRenderer,
|
||||||
@@ -52,5 +52,5 @@ export function actionToAnswerRenderer<
|
|||||||
return createImageRenderer(host, { height: 300 });
|
return createImageRenderer(host, { height: 300 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return createTextRenderer(host, { maxHeight: 320 });
|
return createAIScrollableTextRenderer(host, {}, 320, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
replaceWithMarkdown,
|
replaceWithMarkdown,
|
||||||
} from './actions/page-response';
|
} from './actions/page-response';
|
||||||
import type { AIItemConfig } from './components/ai-item/types';
|
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 { AIProvider } from './provider';
|
||||||
import { reportResponse } from './utils/action-reporter';
|
import { reportResponse } from './utils/action-reporter';
|
||||||
import { getAIPanelWidget } from './utils/ai-widgets';
|
import { getAIPanelWidget } from './utils/ai-widgets';
|
||||||
@@ -293,7 +293,7 @@ export function buildAIPanelConfig(
|
|||||||
const ctx = new AIContext();
|
const ctx = new AIContext();
|
||||||
const searchService = framework.get(AINetworkSearchService);
|
const searchService = framework.get(AINetworkSearchService);
|
||||||
return {
|
return {
|
||||||
answerRenderer: createTextRenderer(panel.host, { maxHeight: 320 }),
|
answerRenderer: createAIScrollableTextRenderer(panel.host, {}, 320, true),
|
||||||
finishStateConfig: buildFinishConfig(panel, 'chat', ctx),
|
finishStateConfig: buildFinishConfig(panel, 'chat', ctx),
|
||||||
generatingStateConfig: buildGeneratingConfig(),
|
generatingStateConfig: buildGeneratingConfig(),
|
||||||
errorStateConfig: buildErrorConfig(panel),
|
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 = {
|
export type TextRendererOptions = {
|
||||||
maxHeight?: number;
|
|
||||||
customHeading?: boolean;
|
customHeading?: boolean;
|
||||||
extensions?: ExtensionType[];
|
extensions?: ExtensionType[];
|
||||||
additionalMiddlewares?: TransformerMiddleware[];
|
additionalMiddlewares?: TransformerMiddleware[];
|
||||||
@@ -284,18 +283,12 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { maxHeight, customHeading } = this.options;
|
const { customHeading } = this.options;
|
||||||
const classes = classMap({
|
const classes = classMap({
|
||||||
'text-renderer-container': true,
|
'text-renderer-container': true,
|
||||||
'show-scrollbar': !!maxHeight,
|
|
||||||
'custom-heading': !!customHeading,
|
'custom-heading': !!customHeading,
|
||||||
});
|
});
|
||||||
return html`
|
return html`
|
||||||
<style>
|
|
||||||
.text-renderer-container {
|
|
||||||
max-height: ${maxHeight ? Math.max(maxHeight, 200) + 'px' : ''};
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class=${classes}>
|
<div class=${classes}>
|
||||||
${keyed(
|
${keyed(
|
||||||
this._doc,
|
this._doc,
|
||||||
@@ -332,7 +325,6 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
|||||||
// Apply min-height to prevent shrinking
|
// Apply min-height to prevent shrinking
|
||||||
this._container.style.minHeight = `${this._maxContainerHeight}px`;
|
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 { ChatPanelDocChip } from './chat-panel/components/doc-chip';
|
||||||
import { ChatPanelFileChip } from './chat-panel/components/file-chip';
|
import { ChatPanelFileChip } from './chat-panel/components/file-chip';
|
||||||
import { effects as componentAiItemEffects } from './components/ai-item';
|
import { effects as componentAiItemEffects } from './components/ai-item';
|
||||||
|
import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
|
||||||
import { AskAIButton } from './components/ask-ai-button';
|
import { AskAIButton } from './components/ask-ai-button';
|
||||||
import { AskAIIcon } from './components/ask-ai-icon';
|
import { AskAIIcon } from './components/ask-ai-icon';
|
||||||
import { AskAIPanel } from './components/ask-ai-panel';
|
import { AskAIPanel } from './components/ask-ai-panel';
|
||||||
@@ -107,6 +108,10 @@ export function registerAIEffects() {
|
|||||||
customElements.define('affine-ai-chat', AIChatBlockComponent);
|
customElements.define('affine-ai-chat', AIChatBlockComponent);
|
||||||
customElements.define('ai-chat-message', AIChatMessage);
|
customElements.define('ai-chat-message', AIChatMessage);
|
||||||
customElements.define('ai-chat-messages', AIChatMessages);
|
customElements.define('ai-chat-messages', AIChatMessages);
|
||||||
|
customElements.define(
|
||||||
|
'ai-scrollable-text-renderer',
|
||||||
|
AIScrollableTextRenderer
|
||||||
|
);
|
||||||
customElements.define('image-placeholder', ImagePlaceholder);
|
customElements.define('image-placeholder', ImagePlaceholder);
|
||||||
customElements.define('chat-image', ChatImage);
|
customElements.define('chat-image', ChatImage);
|
||||||
customElements.define('chat-images', ChatImages);
|
customElements.define('chat-images', ChatImages);
|
||||||
|
|||||||
Reference in New Issue
Block a user