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 { GfxModel } from '@blocksuite/affine/std/gfx';
import type { BlockModel } from '@blocksuite/affine/store'; import type { BlockModel } from '@blocksuite/affine/store';
import type { DocContext, FileContext } from '../chat-panel/chat-context';
export const translateLangs = [ export const translateLangs = [
'English', 'English',
'Spanish', 'Spanish',
@@ -114,12 +112,28 @@ declare global {
type AIActionTextResponse<T extends AITextActionOptions> = type AIActionTextResponse<T extends AITextActionOptions> =
T['stream'] extends true ? TextStream : Promise<string>; 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 { interface ChatOptions extends AITextActionOptions {
sessionId?: string; sessionId?: string;
isRootSession?: boolean; isRootSession?: boolean;
contexts?: { contexts?: {
docs: DocContext[]; docs: AIDocContextOption[];
files: FileContext[]; 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'; import type { Signal } from '@preact/signals-core';
export interface AppSidebarConfig { export interface AppSidebarConfig {
@@ -25,52 +16,3 @@ export interface AINetworkSearchConfig {
enabled: Signal<boolean | undefined>; enabled: Signal<boolean | undefined>;
setEnabled: (state: boolean) => void; 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'; import type { AIError } from '../provider';
export type ChatMessage = { export type ChatMessage = {
@@ -34,22 +32,6 @@ export type ChatStatus =
| 'idle' | 'idle'
| 'transmitting'; | '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 = { export type ChatContextValue = {
// history messages of the chat // history messages of the chat
items: ChatItem[]; items: ChatItem[];
@@ -61,10 +43,6 @@ export type ChatContextValue = {
markdown: string; markdown: string;
// images of the selected content or user uploaded // images of the selected content or user uploaded
images: File[]; images: File[];
// chips of workspace doc or user uploaded file
chips: ChatChip[];
// the progress of the embedding
embeddingProgress: [number, number];
abortController: AbortController | null; abortController: AbortController | null;
}; };
@@ -73,39 +51,3 @@ export type ChatBlockMessage = ChatMessage & {
userName?: string; userName?: string;
avatarUrl?: 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 { repeat } from 'lit/directives/repeat.js';
import { ChatAbortIcon, ChatSendIcon } from '../_common/icons'; 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 { type AIError, AIProvider } from '../provider';
import { reportResponse } from '../utils/action-reporter'; import { reportResponse } from '../utils/action-reporter';
import { readBlobAsURL } from '../utils/image'; import { readBlobAsURL } from '../utils/image';
import type { AINetworkSearchConfig, DocDisplayConfig } from './chat-config'; import type { AINetworkSearchConfig } from './chat-config';
import type { import type { ChatContextValue, ChatMessage } from './chat-context';
ChatContextValue,
ChatMessage,
DocContext,
FileChip,
FileContext,
} from './chat-context';
import { isDocChip, isFileChip } from './components/utils';
import { PROMPT_NAME_AFFINE_AI, PROMPT_NAME_NETWORK_SEARCH } from './const'; import { PROMPT_NAME_AFFINE_AI, PROMPT_NAME_NETWORK_SEARCH } from './const';
const MaximumImageCount = 32; const MaximumImageCount = 32;
@@ -201,6 +200,9 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
@property({ attribute: false }) @property({ attribute: false })
accessor chatContextValue!: ChatContextValue; accessor chatContextValue!: ChatContextValue;
@property({ attribute: false })
accessor chips: ChatChip[] = [];
@property({ attribute: false }) @property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>; accessor getSessionId!: () => Promise<string | undefined>;
@@ -232,8 +234,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
private get _isNetworkDisabled() { private get _isNetworkDisabled() {
return ( return (
!!this.chatContextValue.images.length || !!this.chatContextValue.images.length ||
!!this.chatContextValue.chips.filter(chip => chip.state === 'finished') !!this.chips.filter(chip => chip.state === 'finished').length
.length
); );
} }
@@ -575,7 +576,10 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
string, string,
{ docId: string; docContent: string } { docId: string; docContent: string }
>(); >();
const fileContexts = new Map<string, FileContext>(); const fileContexts = new Map<
string,
BlockSuitePresets.AIFileContextOption
>();
const { files: matchedFiles = [], docs: matchedDocs = [] } = const { files: matchedFiles = [], docs: matchedDocs = [] } =
(await AIProvider.context?.matchContext(contextId, userInput)) ?? {}; (await AIProvider.context?.matchContext(contextId, userInput)) ?? {};
@@ -592,7 +596,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
if (context) { if (context) {
context.fileContent += `\n${file.content}`; context.fileContent += `\n${file.content}`;
} else { } else {
const fileChip = this.chatContextValue.chips.find( const fileChip = this.chips.find(
chip => isFileChip(chip) && chip.fileId === file.fileId chip => isFileChip(chip) && chip.fileId === file.fileId
) as FileChip | undefined; ) as FileChip | undefined;
if (fileChip && fileChip.blobId) { 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) { if (isDocChip(chip) && !!chip.markdown?.value) {
docContexts.set(chip.docId, { docContexts.set(chip.docId, {
docId: 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 docMeta = this.docDisplayConfig.getDocMeta(doc.docId);
const docTitle = this.docDisplayConfig.getTitle(doc.docId); const docTitle = this.docDisplayConfig.getTitle(doc.docId);
const tags = docMeta?.tags 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 { styleMap } from 'lit/directives/style-map.js';
import { throttle } from 'lodash-es'; 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 { AIProvider } from '../provider';
import { extractSelectedContent } from '../utils/extract'; import { extractSelectedContent } from '../utils/extract';
import { import {
getSelectedImagesAsBlobs, getSelectedImagesAsBlobs,
getSelectedTextContent, getSelectedTextContent,
} from '../utils/selection-utils'; } from '../utils/selection-utils';
import type { import type { AINetworkSearchConfig, AppSidebarConfig } from './chat-config';
AINetworkSearchConfig, import type { ChatContextValue, ChatItem } from './chat-context';
AppSidebarConfig,
DocDisplayConfig,
SearchMenuConfig,
} from './chat-config';
import type {
ChatChip,
ChatContextValue,
ChatItem,
CollectionChip,
DocChip,
FileChip,
TagChip,
} from './chat-context';
import type { ChatPanelMessages } from './chat-panel-messages'; import type { ChatPanelMessages } from './chat-panel-messages';
import { isCollectionChip, isDocChip, isTagChip } from './components/utils';
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = { const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
quote: '', quote: '',
images: [], images: [],
abortController: null, abortController: null,
items: [], items: [],
chips: [],
status: 'idle', status: 'idle',
error: null, error: null,
markdown: '', markdown: '',
embeddingProgress: [0, 0],
}; };
export class ChatPanel extends SignalWatcher( export class ChatPanel extends SignalWatcher(
@@ -245,10 +243,7 @@ export class ChatPanel extends SignalWatcher(
return aTime - bTime; return aTime - bTime;
}); });
this.chatContextValue = { this.updateChips(chips);
...this.chatContextValue,
chips,
};
}; };
private readonly _initEmbeddingProgress = async () => { private readonly _initEmbeddingProgress = async () => {
@@ -307,6 +302,12 @@ export class ChatPanel extends SignalWatcher(
@state() @state()
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE; 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 _chatSessionId: string | null | undefined = null;
private _chatContextId: 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 () => { private readonly _pollContextDocsAndFiles = async () => {
if (!this._chatSessionId || !this._chatContextId || !AIProvider.context) { if (!this._chatSessionId || !this._chatContextId || !AIProvider.context) {
return; return;
@@ -444,7 +455,7 @@ export class ChatPanel extends SignalWatcher(
hashMap.set(file.id, file); hashMap.set(file.id, file);
file.status && count[file.status]++; file.status && count[file.status]++;
}); });
const nextChips = this.chatContextValue.chips.map(chip => { const nextChips = this.chips.map(chip => {
if (isTagChip(chip) || isCollectionChip(chip)) { if (isTagChip(chip) || isCollectionChip(chip)) {
return chip; return chip;
} }
@@ -460,10 +471,8 @@ export class ChatPanel extends SignalWatcher(
return chip; return chip;
}); });
const total = count.finished + count.processing + count.failed; const total = count.finished + count.processing + count.failed;
this.updateContext({ this.embeddingProgress = [count.finished, total];
chips: nextChips, this.updateChips(nextChips);
embeddingProgress: [count.finished, total],
});
if (count.processing === 0) { if (count.processing === 0) {
this._abortPoll(); this._abortPoll();
} }
@@ -476,12 +485,7 @@ export class ChatPanel extends SignalWatcher(
protected override updated(_changedProperties: PropertyValues) { protected override updated(_changedProperties: PropertyValues) {
if (_changedProperties.has('doc')) { if (_changedProperties.has('doc')) {
this._abortPoll(); this._resetPanel();
this._chatSessionId = null;
this._chatContextId = null;
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
this.isLoading = true;
requestAnimationFrame(async () => { requestAnimationFrame(async () => {
await this._initPanel(); await this._initPanel();
}); });
@@ -576,6 +580,10 @@ export class ChatPanel extends SignalWatcher(
this.chatContextValue = { ...this.chatContextValue, ...context }; this.chatContextValue = { ...this.chatContextValue, ...context };
}; };
updateChips = (chips: ChatChip[]) => {
this.chips = chips;
};
continueInChat = async () => { continueInChat = async () => {
const text = await getSelectedTextContent(this.host, 'plain-text'); const text = await getSelectedTextContent(this.host, 'plain-text');
const markdown = await getSelectedTextContent(this.host, 'markdown'); const markdown = await getSelectedTextContent(this.host, 'markdown');
@@ -592,7 +600,7 @@ export class ChatPanel extends SignalWatcher(
const style = styleMap({ const style = styleMap({
padding: width > 540 ? '8px 24px 0 24px' : '8px 12px 0 12px', 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; const isEmbedding = total > 0 && done < total;
return html`<div class="chat-panel-container" style=${style}> return html`<div class="chat-panel-container" style=${style}>
@@ -617,14 +625,15 @@ export class ChatPanel extends SignalWatcher(
></chat-panel-messages> ></chat-panel-messages>
<chat-panel-chips <chat-panel-chips
.host=${this.host} .host=${this.host}
.chatContextValue=${this.chatContextValue} .chips=${this.chips}
.getContextId=${this._getContextId} .getContextId=${this._getContextId}
.updateContext=${this.updateContext} .updateChips=${this.updateChips}
.pollContextDocsAndFiles=${this._pollContextDocsAndFiles} .pollContextDocsAndFiles=${this._pollContextDocsAndFiles}
.docDisplayConfig=${this.docDisplayConfig} .docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig} .searchMenuConfig=${this.searchMenuConfig}
></chat-panel-chips> ></chat-panel-chips>
<chat-panel-input <chat-panel-input
.chips=${this.chips}
.chatContextValue=${this.chatContextValue} .chatContextValue=${this.chatContextValue}
.getSessionId=${this._getSessionId} .getSessionId=${this._getSessionId}
.getContextId=${this._getContextId} .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 { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import type { DocDisplayConfig, SearchMenuConfig } from '../chat-config'; import type { ChatChip, DocDisplayConfig, SearchMenuConfig } from './type';
import type { ChatChip } from '../chat-context';
enum AddPopoverMode { enum AddPopoverMode {
Default = 'default', Default = 'default',

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { ShadowlessElement } from '@blocksuite/affine/std';
import { html } from 'lit'; import { html } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import type { FileChip } from '../chat-context'; import type { FileChip } from './type';
import { getChipIcon, getChipTooltip } from './utils'; import { getChipIcon, getChipTooltip } from './utils';
export class ChatPanelFileChip extends SignalWatcher( 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 { css, html } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import type { TagChip } from '../chat-context'; import type { TagChip } from './type';
import { getChipIcon, getChipTooltip } from './utils'; import { getChipIcon, getChipTooltip } from './utils';
export class ChatPanelTagChip extends SignalWatcher( 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, DocChip,
FileChip, FileChip,
TagChip, TagChip,
} from '../chat-context'; } from './type';
export function getChipTooltip( export function getChipTooltip(
state: ChipState, state: ChipState,

View File

@@ -23,16 +23,8 @@ import { ActionMindmap } from './chat-panel/actions/mindmap';
import { ActionSlides } from './chat-panel/actions/slides'; import { ActionSlides } from './chat-panel/actions/slides';
import { ActionText } from './chat-panel/actions/text'; import { ActionText } from './chat-panel/actions/text';
import { AILoading } from './chat-panel/ai-loading'; import { AILoading } from './chat-panel/ai-loading';
import { ChatPanelChips } from './chat-panel/chat-panel-chips';
import { ChatPanelInput } from './chat-panel/chat-panel-input'; import { ChatPanelInput } from './chat-panel/chat-panel-input';
import { ChatPanelMessages } from './chat-panel/chat-panel-messages'; 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 { AssistantAvatar } from './chat-panel/content/assistant-avatar';
import { ChatContentImages } from './chat-panel/content/images'; import { ChatContentImages } from './chat-panel/content/images';
import { ChatContentPureText } from './chat-panel/content/pure-text'; 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 { ChatMessageAction } from './chat-panel/message/action';
import { ChatMessageAssistant } from './chat-panel/message/assistant'; import { ChatMessageAssistant } from './chat-panel/message/assistant';
import { ChatMessageUser } from './chat-panel/message/user'; 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 { effects as componentAiItemEffects } from './components/ai-item';
import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer'; import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
import { AskAIButton } from './components/ask-ai-button'; import { AskAIButton } from './components/ask-ai-button';