mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
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:
@@ -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[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
@@ -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(
|
||||
@@ -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;
|
||||
@@ -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(
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './type';
|
||||
export * from './utils';
|
||||
@@ -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(
|
||||
@@ -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;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
DocChip,
|
||||
FileChip,
|
||||
TagChip,
|
||||
} from '../chat-context';
|
||||
} from './type';
|
||||
|
||||
export function getChipTooltip(
|
||||
state: ChipState,
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user