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 { 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[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -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)
|
||||||
@@ -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);
|
||||||
@@ -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)
|
||||||
@@ -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(
|
||||||
@@ -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;
|
||||||
@@ -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(
|
||||||
@@ -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 { 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(
|
||||||
@@ -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,
|
DocChip,
|
||||||
FileChip,
|
FileChip,
|
||||||
TagChip,
|
TagChip,
|
||||||
} from '../chat-context';
|
} from './type';
|
||||||
|
|
||||||
export function getChipTooltip(
|
export function getChipTooltip(
|
||||||
state: ChipState,
|
state: ChipState,
|
||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user