refactor(core): ai chat chips (#11341)

Support [BS-2583](https://linear.app/affine-design/issue/BS-2583).

### What changed?
- Move chat panel chip components to `/components` folder.
- Separate `chips` and `embeddingProgress` from ChatContextValue.
This commit is contained in:
akumatus
2025-04-01 09:15:16 +00:00
parent 9cb80205f8
commit b74d40d45f
17 changed files with 220 additions and 222 deletions

View File

@@ -13,8 +13,6 @@ import type { EditorHost } from '@blocksuite/affine/std';
import type { GfxModel } from '@blocksuite/affine/std/gfx';
import type { BlockModel } from '@blocksuite/affine/store';
import type { DocContext, FileContext } from '../chat-panel/chat-context';
export const translateLangs = [
'English',
'Spanish',
@@ -114,12 +112,28 @@ declare global {
type AIActionTextResponse<T extends AITextActionOptions> =
T['stream'] extends true ? TextStream : Promise<string>;
interface AIDocContextOption {
docId: string;
docTitle: string;
docContent: string;
tags: string;
createDate: string;
updatedDate: string;
}
interface AIFileContextOption {
blobId: string;
fileName: string;
fileType: string;
fileContent: string;
}
interface ChatOptions extends AITextActionOptions {
sessionId?: string;
isRootSession?: boolean;
contexts?: {
docs: DocContext[];
files: FileContext[];
docs: AIDocContextOption[];
files: AIFileContextOption[];
};
}

View File

@@ -1,12 +1,3 @@
import type { TagMeta } from '@affine/core/components/page-list';
import type {
SearchCollectionMenuAction,
SearchDocMenuAction,
SearchTagMenuAction,
} from '@affine/core/modules/search-menu/services';
import type { Collection } from '@affine/env/filter';
import type { LinkedMenuGroup } from '@blocksuite/affine/blocks/root';
import type { DocMeta, Store } from '@blocksuite/affine/store';
import type { Signal } from '@preact/signals-core';
export interface AppSidebarConfig {
@@ -25,52 +16,3 @@ export interface AINetworkSearchConfig {
enabled: Signal<boolean | undefined>;
setEnabled: (state: boolean) => void;
}
export interface DocDisplayConfig {
getIcon: (docId: string) => any;
getTitle: (docId: string) => string;
getTitleSignal: (docId: string) => {
signal: Signal<string>;
cleanup: () => void;
};
getDocMeta: (docId: string) => Partial<DocMeta> | null;
getDocPrimaryMode: (docId: string) => 'page' | 'edgeless';
getDoc: (docId: string) => Store | null;
getReferenceDocs: (docIds: string[]) => {
signal: Signal<
Array<{
docId: string;
title: string;
}>
>;
cleanup: () => void;
};
getTags: () => {
signal: Signal<TagMeta[]>;
cleanup: () => void;
};
getTagTitle: (tagId: string) => string;
getTagPageIds: (tagId: string) => string[];
getCollections: () => {
signal: Signal<Collection[]>;
cleanup: () => void;
};
}
export interface SearchMenuConfig {
getDocMenuGroup: (
query: string,
action: SearchDocMenuAction,
abortSignal: AbortSignal
) => LinkedMenuGroup;
getTagMenuGroup: (
query: string,
action: SearchTagMenuAction,
abortSignal: AbortSignal
) => LinkedMenuGroup;
getCollectionMenuGroup: (
query: string,
action: SearchCollectionMenuAction,
abortSignal: AbortSignal
) => LinkedMenuGroup;
}

View File

@@ -1,5 +1,3 @@
import type { Signal } from '@preact/signals-core';
import type { AIError } from '../provider';
export type ChatMessage = {
@@ -34,22 +32,6 @@ export type ChatStatus =
| 'idle'
| 'transmitting';
export interface DocContext {
docId: string;
docTitle: string;
docContent: string;
tags: string;
createDate: string;
updatedDate: string;
}
export interface FileContext {
blobId: string;
fileName: string;
fileType: string;
fileContent: string;
}
export type ChatContextValue = {
// history messages of the chat
items: ChatItem[];
@@ -61,10 +43,6 @@ export type ChatContextValue = {
markdown: string;
// images of the selected content or user uploaded
images: File[];
// chips of workspace doc or user uploaded file
chips: ChatChip[];
// the progress of the embedding
embeddingProgress: [number, number];
abortController: AbortController | null;
};
@@ -73,39 +51,3 @@ export type ChatBlockMessage = ChatMessage & {
userName?: string;
avatarUrl?: string;
};
export type ChipState = 'candidate' | 'processing' | 'finished' | 'failed';
export interface BaseChip {
/**
* candidate: the chip is a candidate for the chat
* processing: the chip is processing
* finished: the chip is successfully processed
* failed: the chip is failed to process
*/
state: ChipState;
tooltip?: string | null;
createdAt?: number | null;
}
export interface DocChip extends BaseChip {
docId: string;
markdown?: Signal<string> | null;
tokenCount?: number | null;
}
export interface FileChip extends BaseChip {
file: File;
fileId?: string | null;
blobId?: string | null;
}
export interface TagChip extends BaseChip {
tagId: string;
}
export interface CollectionChip extends BaseChip {
collectionId: string;
}
export type ChatChip = DocChip | FileChip | TagChip | CollectionChip;

View File

@@ -14,18 +14,17 @@ import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { ChatAbortIcon, ChatSendIcon } from '../_common/icons';
import type {
ChatChip,
DocDisplayConfig,
FileChip,
} from '../components/ai-chat-chips';
import { isDocChip, isFileChip } from '../components/ai-chat-chips';
import { type AIError, AIProvider } from '../provider';
import { reportResponse } from '../utils/action-reporter';
import { readBlobAsURL } from '../utils/image';
import type { AINetworkSearchConfig, DocDisplayConfig } from './chat-config';
import type {
ChatContextValue,
ChatMessage,
DocContext,
FileChip,
FileContext,
} from './chat-context';
import { isDocChip, isFileChip } from './components/utils';
import type { AINetworkSearchConfig } from './chat-config';
import type { ChatContextValue, ChatMessage } from './chat-context';
import { PROMPT_NAME_AFFINE_AI, PROMPT_NAME_NETWORK_SEARCH } from './const';
const MaximumImageCount = 32;
@@ -201,6 +200,9 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
@property({ attribute: false })
accessor chatContextValue!: ChatContextValue;
@property({ attribute: false })
accessor chips: ChatChip[] = [];
@property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>;
@@ -232,8 +234,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
private get _isNetworkDisabled() {
return (
!!this.chatContextValue.images.length ||
!!this.chatContextValue.chips.filter(chip => chip.state === 'finished')
.length
!!this.chips.filter(chip => chip.state === 'finished').length
);
}
@@ -575,7 +576,10 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
string,
{ docId: string; docContent: string }
>();
const fileContexts = new Map<string, FileContext>();
const fileContexts = new Map<
string,
BlockSuitePresets.AIFileContextOption
>();
const { files: matchedFiles = [], docs: matchedDocs = [] } =
(await AIProvider.context?.matchContext(contextId, userInput)) ?? {};
@@ -592,7 +596,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
if (context) {
context.fileContent += `\n${file.content}`;
} else {
const fileChip = this.chatContextValue.chips.find(
const fileChip = this.chips.find(
chip => isFileChip(chip) && chip.fileId === file.fileId
) as FileChip | undefined;
if (fileChip && fileChip.blobId) {
@@ -606,7 +610,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
}
});
this.chatContextValue.chips.forEach(chip => {
this.chips.forEach(chip => {
if (isDocChip(chip) && !!chip.markdown?.value) {
docContexts.set(chip.docId, {
docId: chip.docId,
@@ -615,7 +619,9 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
}
});
const docs: DocContext[] = Array.from(docContexts.values()).map(doc => {
const docs: BlockSuitePresets.AIDocContextOption[] = Array.from(
docContexts.values()
).map(doc => {
const docMeta = this.docDisplayConfig.getDocMeta(doc.docId);
const docTitle = this.docDisplayConfig.getTitle(doc.docId);
const tags = docMeta?.tags

View File

@@ -21,40 +21,38 @@ import { createRef, type Ref, ref } from 'lit/directives/ref.js';
import { styleMap } from 'lit/directives/style-map.js';
import { throttle } from 'lodash-es';
import type {
ChatChip,
CollectionChip,
DocChip,
DocDisplayConfig,
FileChip,
SearchMenuConfig,
TagChip,
} from '../components/ai-chat-chips';
import {
isCollectionChip,
isDocChip,
isTagChip,
} from '../components/ai-chat-chips';
import { AIProvider } from '../provider';
import { extractSelectedContent } from '../utils/extract';
import {
getSelectedImagesAsBlobs,
getSelectedTextContent,
} from '../utils/selection-utils';
import type {
AINetworkSearchConfig,
AppSidebarConfig,
DocDisplayConfig,
SearchMenuConfig,
} from './chat-config';
import type {
ChatChip,
ChatContextValue,
ChatItem,
CollectionChip,
DocChip,
FileChip,
TagChip,
} from './chat-context';
import type { AINetworkSearchConfig, AppSidebarConfig } from './chat-config';
import type { ChatContextValue, ChatItem } from './chat-context';
import type { ChatPanelMessages } from './chat-panel-messages';
import { isCollectionChip, isDocChip, isTagChip } from './components/utils';
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
quote: '',
images: [],
abortController: null,
items: [],
chips: [],
status: 'idle',
error: null,
markdown: '',
embeddingProgress: [0, 0],
};
export class ChatPanel extends SignalWatcher(
@@ -245,10 +243,7 @@ export class ChatPanel extends SignalWatcher(
return aTime - bTime;
});
this.chatContextValue = {
...this.chatContextValue,
chips,
};
this.updateChips(chips);
};
private readonly _initEmbeddingProgress = async () => {
@@ -307,6 +302,12 @@ export class ChatPanel extends SignalWatcher(
@state()
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
@state()
accessor chips: ChatChip[] = [];
@state()
accessor embeddingProgress: [number, number] = [0, 0];
private _chatSessionId: string | null | undefined = null;
private _chatContextId: string | null | undefined = null;
@@ -391,6 +392,16 @@ export class ChatPanel extends SignalWatcher(
}
};
private readonly _resetPanel = () => {
this._abortPoll();
this._chatSessionId = null;
this._chatContextId = null;
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
this.isLoading = true;
this.chips = [];
this.embeddingProgress = [0, 0];
};
private readonly _pollContextDocsAndFiles = async () => {
if (!this._chatSessionId || !this._chatContextId || !AIProvider.context) {
return;
@@ -444,7 +455,7 @@ export class ChatPanel extends SignalWatcher(
hashMap.set(file.id, file);
file.status && count[file.status]++;
});
const nextChips = this.chatContextValue.chips.map(chip => {
const nextChips = this.chips.map(chip => {
if (isTagChip(chip) || isCollectionChip(chip)) {
return chip;
}
@@ -460,10 +471,8 @@ export class ChatPanel extends SignalWatcher(
return chip;
});
const total = count.finished + count.processing + count.failed;
this.updateContext({
chips: nextChips,
embeddingProgress: [count.finished, total],
});
this.embeddingProgress = [count.finished, total];
this.updateChips(nextChips);
if (count.processing === 0) {
this._abortPoll();
}
@@ -476,12 +485,7 @@ export class ChatPanel extends SignalWatcher(
protected override updated(_changedProperties: PropertyValues) {
if (_changedProperties.has('doc')) {
this._abortPoll();
this._chatSessionId = null;
this._chatContextId = null;
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
this.isLoading = true;
this._resetPanel();
requestAnimationFrame(async () => {
await this._initPanel();
});
@@ -576,6 +580,10 @@ export class ChatPanel extends SignalWatcher(
this.chatContextValue = { ...this.chatContextValue, ...context };
};
updateChips = (chips: ChatChip[]) => {
this.chips = chips;
};
continueInChat = async () => {
const text = await getSelectedTextContent(this.host, 'plain-text');
const markdown = await getSelectedTextContent(this.host, 'markdown');
@@ -592,7 +600,7 @@ export class ChatPanel extends SignalWatcher(
const style = styleMap({
padding: width > 540 ? '8px 24px 0 24px' : '8px 12px 0 12px',
});
const [done, total] = this.chatContextValue.embeddingProgress;
const [done, total] = this.embeddingProgress;
const isEmbedding = total > 0 && done < total;
return html`<div class="chat-panel-container" style=${style}>
@@ -617,14 +625,15 @@ export class ChatPanel extends SignalWatcher(
></chat-panel-messages>
<chat-panel-chips
.host=${this.host}
.chatContextValue=${this.chatContextValue}
.chips=${this.chips}
.getContextId=${this._getContextId}
.updateContext=${this.updateContext}
.updateChips=${this.updateChips}
.pollContextDocsAndFiles=${this._pollContextDocsAndFiles}
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
></chat-panel-chips>
<chat-panel-input
.chips=${this.chips}
.chatContextValue=${this.chatContextValue}
.getSessionId=${this._getSessionId}
.getContextId=${this._getContextId}

View File

@@ -22,8 +22,7 @@ import { css, html, type TemplateResult } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import type { DocDisplayConfig, SearchMenuConfig } from '../chat-config';
import type { ChatChip } from '../chat-context';
import type { ChatChip, DocDisplayConfig, SearchMenuConfig } from './type';
enum AddPopoverMode {
Default = 'default',

View File

@@ -8,8 +8,7 @@ import { css, html } from 'lit';
import { property, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import type { DocDisplayConfig } from '../chat-config';
import type { DocChip } from '../chat-context';
import type { DocChip, DocDisplayConfig } from './type';
export class ChatPanelCandidatesPopover extends SignalWatcher(
WithDisposable(ShadowlessElement)

View File

@@ -12,16 +12,16 @@ import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { isEqual } from 'lodash-es';
import { AIProvider } from '../provider';
import type { DocDisplayConfig, SearchMenuConfig } from './chat-config';
import { AIProvider } from '../../provider';
import type {
ChatChip,
ChatContextValue,
CollectionChip,
DocChip,
DocDisplayConfig,
FileChip,
SearchMenuConfig,
TagChip,
} from './chat-context';
} from './type';
import {
estimateTokenCount,
getChipKey,
@@ -29,7 +29,7 @@ import {
isDocChip,
isFileChip,
isTagChip,
} from './components/utils';
} from './utils';
// 100k tokens limit for the docs context
const MAX_TOKEN_COUNT = 100000;
@@ -83,13 +83,13 @@ export class ChatPanelChips extends SignalWatcher(
accessor host!: EditorHost;
@property({ attribute: false })
accessor chatContextValue!: ChatContextValue;
accessor chips!: ChatChip[];
@property({ attribute: false })
accessor getContextId!: () => Promise<string | undefined>;
@property({ attribute: false })
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
accessor updateChips!: (chips: ChatChip[]) => void;
@property({ attribute: false })
accessor pollContextDocsAndFiles!: () => void;
@@ -134,9 +134,7 @@ export class ChatPanelChips extends SignalWatcher(
state: 'candidate',
}));
const moreCandidates = candidates.length > MAX_CANDIDATES;
const allChips = this.chatContextValue.chips.concat(
candidates.slice(0, MAX_CANDIDATES)
);
const allChips = this.chips.concat(candidates.slice(0, MAX_CANDIDATES));
const isCollapsed = this.isCollapsed && allChips.length > 1;
const chips = isCollapsed ? allChips.slice(0, 1) : allChips;
@@ -232,8 +230,7 @@ export class ChatPanelChips extends SignalWatcher(
this.isCollapsed = true;
}
// TODO only update when the chips are changed
if (_changedProperties.has('chatContextValue')) {
if (_changedProperties.has('chips')) {
this._updateReferenceDocs();
}
}
@@ -324,11 +321,9 @@ export class ChatPanelChips extends SignalWatcher(
private readonly _addChip = async (chip: ChatChip) => {
this.isCollapsed = false;
// remove the chip if it already exists
const chips = this._omitChip(this.chatContextValue.chips, chip);
this.updateContext({
chips: [...chips, chip],
});
if (chips.length < this.chatContextValue.chips.length) {
const chips = this._omitChip(this.chips, chip);
this.updateChips([...chips, chip]);
if (chips.length < this.chips.length) {
await this._removeFromContext(chip);
}
await this._addToContext(chip);
@@ -339,7 +334,7 @@ export class ChatPanelChips extends SignalWatcher(
chip: ChatChip,
options: Partial<DocChip | FileChip>
) => {
const index = this._findChipIndex(this.chatContextValue.chips, chip);
const index = this._findChipIndex(this.chips, chip);
if (index === -1) {
return;
}
@@ -347,21 +342,17 @@ export class ChatPanelChips extends SignalWatcher(
...chip,
...options,
};
this.updateContext({
chips: [
...this.chatContextValue.chips.slice(0, index),
nextChip,
...this.chatContextValue.chips.slice(index + 1),
],
});
this.updateChips([
...this.chips.slice(0, index),
nextChip,
...this.chips.slice(index + 1),
]);
};
private readonly _removeChip = async (chip: ChatChip) => {
const chips = this._omitChip(this.chatContextValue.chips, chip);
this.updateContext({
chips,
});
if (chips.length < this.chatContextValue.chips.length) {
const chips = this._omitChip(this.chips, chip);
this.updateChips(chips);
if (chips.length < this.chips.length) {
await this._removeFromContext(chip);
}
};
@@ -518,7 +509,7 @@ export class ChatPanelChips extends SignalWatcher(
newChip: DocChip,
newTokenCount: number
) => {
const estimatedTokens = this.chatContextValue.chips.reduce((acc, chip) => {
const estimatedTokens = this.chips.reduce((acc, chip) => {
if (isFileChip(chip) || isTagChip(chip) || isCollectionChip(chip)) {
return acc;
}
@@ -536,7 +527,7 @@ export class ChatPanelChips extends SignalWatcher(
};
private readonly _updateReferenceDocs = () => {
const docIds = this.chatContextValue.chips
const docIds = this.chips
.filter(isDocChip)
.filter(chip => chip.state !== 'candidate')
.map(chip => chip.docId);

View File

@@ -5,7 +5,7 @@ import { CloseIcon, PlusIcon } from '@blocksuite/icons/lit';
import { css, html, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
import type { ChipState } from '../chat-context';
import type { ChipState } from './type';
export class ChatPanelChip extends SignalWatcher(
WithDisposable(ShadowlessElement)

View File

@@ -5,7 +5,7 @@ import { CollectionsIcon } from '@blocksuite/icons/lit';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import type { CollectionChip } from '../chat-context';
import type { CollectionChip } from './type';
import { getChipIcon, getChipTooltip } from './utils';
export class ChatPanelCollectionChip extends SignalWatcher(

View File

@@ -7,8 +7,7 @@ import { property } from 'lit/decorators.js';
import throttle from 'lodash-es/throttle';
import { extractMarkdownFromDoc } from '../../utils/extract';
import type { DocDisplayConfig } from '../chat-config';
import type { DocChip } from '../chat-context';
import type { DocChip, DocDisplayConfig } from './type';
import { estimateTokenCount, getChipIcon, getChipTooltip } from './utils';
const EXTRACT_DOC_THROTTLE = 1000;

View File

@@ -4,7 +4,7 @@ import { ShadowlessElement } from '@blocksuite/affine/std';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import type { FileChip } from '../chat-context';
import type { FileChip } from './type';
import { getChipIcon, getChipTooltip } from './utils';
export class ChatPanelFileChip extends SignalWatcher(

View File

@@ -0,0 +1,2 @@
export * from './type';
export * from './utils';

View File

@@ -5,7 +5,7 @@ import { ShadowlessElement } from '@blocksuite/affine/std';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
import type { TagChip } from '../chat-context';
import type { TagChip } from './type';
import { getChipIcon, getChipTooltip } from './utils';
export class ChatPanelTagChip extends SignalWatcher(

View File

@@ -0,0 +1,95 @@
import type { TagMeta } from '@affine/core/components/page-list';
import type {
SearchCollectionMenuAction,
SearchDocMenuAction,
SearchTagMenuAction,
} from '@affine/core/modules/search-menu/services';
import type { Collection } from '@affine/env/filter';
import type { LinkedMenuGroup } from '@blocksuite/affine/blocks/root';
import type { DocMeta, Store } from '@blocksuite/affine/store';
import type { Signal } from '@preact/signals-core';
export type ChipState = 'candidate' | 'processing' | 'finished' | 'failed';
export interface BaseChip {
/**
* candidate: the chip is a candidate for the chat
* processing: the chip is processing
* finished: the chip is successfully processed
* failed: the chip is failed to process
*/
state: ChipState;
tooltip?: string | null;
createdAt?: number | null;
}
export interface DocChip extends BaseChip {
docId: string;
markdown?: Signal<string> | null;
tokenCount?: number | null;
}
export interface FileChip extends BaseChip {
file: File;
fileId?: string | null;
blobId?: string | null;
}
export interface TagChip extends BaseChip {
tagId: string;
}
export interface CollectionChip extends BaseChip {
collectionId: string;
}
export type ChatChip = DocChip | FileChip | TagChip | CollectionChip;
export interface DocDisplayConfig {
getIcon: (docId: string) => any;
getTitle: (docId: string) => string;
getTitleSignal: (docId: string) => {
signal: Signal<string>;
cleanup: () => void;
};
getDocMeta: (docId: string) => Partial<DocMeta> | null;
getDocPrimaryMode: (docId: string) => 'page' | 'edgeless';
getDoc: (docId: string) => Store | null;
getReferenceDocs: (docIds: string[]) => {
signal: Signal<
Array<{
docId: string;
title: string;
}>
>;
cleanup: () => void;
};
getTags: () => {
signal: Signal<TagMeta[]>;
cleanup: () => void;
};
getTagTitle: (tagId: string) => string;
getTagPageIds: (tagId: string) => string[];
getCollections: () => {
signal: Signal<Collection[]>;
cleanup: () => void;
};
}
export interface SearchMenuConfig {
getDocMenuGroup: (
query: string,
action: SearchDocMenuAction,
abortSignal: AbortSignal
) => LinkedMenuGroup;
getTagMenuGroup: (
query: string,
action: SearchTagMenuAction,
abortSignal: AbortSignal
) => LinkedMenuGroup;
getCollectionMenuGroup: (
query: string,
action: SearchCollectionMenuAction,
abortSignal: AbortSignal
) => LinkedMenuGroup;
}

View File

@@ -9,7 +9,7 @@ import type {
DocChip,
FileChip,
TagChip,
} from '../chat-context';
} from './type';
export function getChipTooltip(
state: ChipState,

View File

@@ -23,16 +23,8 @@ import { ActionMindmap } from './chat-panel/actions/mindmap';
import { ActionSlides } from './chat-panel/actions/slides';
import { ActionText } from './chat-panel/actions/text';
import { AILoading } from './chat-panel/ai-loading';
import { ChatPanelChips } from './chat-panel/chat-panel-chips';
import { ChatPanelInput } from './chat-panel/chat-panel-input';
import { ChatPanelMessages } from './chat-panel/chat-panel-messages';
import { ChatPanelAddPopover } from './chat-panel/components/add-popover';
import { ChatPanelCandidatesPopover } from './chat-panel/components/candidates-popover';
import { ChatPanelChip } from './chat-panel/components/chip';
import { ChatPanelCollectionChip } from './chat-panel/components/collection-chip';
import { ChatPanelDocChip } from './chat-panel/components/doc-chip';
import { ChatPanelFileChip } from './chat-panel/components/file-chip';
import { ChatPanelTagChip } from './chat-panel/components/tag-chip';
import { AssistantAvatar } from './chat-panel/content/assistant-avatar';
import { ChatContentImages } from './chat-panel/content/images';
import { ChatContentPureText } from './chat-panel/content/pure-text';
@@ -40,6 +32,14 @@ import { ChatContentRichText } from './chat-panel/content/rich-text';
import { ChatMessageAction } from './chat-panel/message/action';
import { ChatMessageAssistant } from './chat-panel/message/assistant';
import { ChatMessageUser } from './chat-panel/message/user';
import { ChatPanelAddPopover } from './components/ai-chat-chips/add-popover';
import { ChatPanelCandidatesPopover } from './components/ai-chat-chips/candidates-popover';
import { ChatPanelChips } from './components/ai-chat-chips/chat-panel-chips';
import { ChatPanelChip } from './components/ai-chat-chips/chip';
import { ChatPanelCollectionChip } from './components/ai-chat-chips/collection-chip';
import { ChatPanelDocChip } from './components/ai-chat-chips/doc-chip';
import { ChatPanelFileChip } from './components/ai-chat-chips/file-chip';
import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
import { effects as componentAiItemEffects } from './components/ai-item';
import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
import { AskAIButton } from './components/ask-ai-button';