mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-19 23:37:15 +08:00
feat(core): add ai chat composer (#11443)
Close [BS-2583](https://linear.app/affine-design/issue/BS-2583). 
This commit is contained in:
@@ -150,6 +150,9 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getSessionId!: () => Promise<string | undefined>;
|
accessor getSessionId!: () => Promise<string | undefined>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor createSessionId!: () => Promise<string | undefined>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
||||||
|
|
||||||
@@ -347,7 +350,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
retry = async () => {
|
retry = async () => {
|
||||||
const { doc } = this.host;
|
const { doc } = this.host;
|
||||||
try {
|
try {
|
||||||
const sessionId = await this.getSessionId();
|
const sessionId = await this.createSessionId();
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import './chat-panel-messages';
|
import './chat-panel-messages';
|
||||||
|
|
||||||
import type {
|
import type { ContextEmbedStatus } from '@affine/graphql';
|
||||||
ContextEmbedStatus,
|
|
||||||
CopilotContextDoc,
|
|
||||||
CopilotContextFile,
|
|
||||||
CopilotDocType,
|
|
||||||
} from '@affine/graphql';
|
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { NotificationProvider } from '@blocksuite/affine/shared/services';
|
|
||||||
import type { SpecBuilder } from '@blocksuite/affine/shared/utils';
|
import type { SpecBuilder } from '@blocksuite/affine/shared/utils';
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
import type { Store } from '@blocksuite/affine/store';
|
import type { Store } from '@blocksuite/affine/store';
|
||||||
import { HelpIcon, InformationIcon } from '@blocksuite/icons/lit';
|
import { HelpIcon } from '@blocksuite/icons/lit';
|
||||||
import { type Signal, signal } from '@preact/signals-core';
|
import { type Signal, signal } from '@preact/signals-core';
|
||||||
import { css, html, type PropertyValues } from 'lit';
|
import { css, html, type PropertyValues } from 'lit';
|
||||||
import { property, state } from 'lit/decorators.js';
|
import { property, state } from 'lit/decorators.js';
|
||||||
@@ -21,18 +15,8 @@ import { styleMap } from 'lit/directives/style-map.js';
|
|||||||
import { throttle } from 'lodash-es';
|
import { throttle } from 'lodash-es';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ChatChip,
|
|
||||||
CollectionChip,
|
|
||||||
DocChip,
|
|
||||||
DocDisplayConfig,
|
DocDisplayConfig,
|
||||||
FileChip,
|
|
||||||
SearchMenuConfig,
|
SearchMenuConfig,
|
||||||
TagChip,
|
|
||||||
} from '../components/ai-chat-chips';
|
|
||||||
import {
|
|
||||||
isCollectionChip,
|
|
||||||
isDocChip,
|
|
||||||
isTagChip,
|
|
||||||
} from '../components/ai-chat-chips';
|
} from '../components/ai-chat-chips';
|
||||||
import type { AINetworkSearchConfig } from '../components/ai-chat-input';
|
import type { AINetworkSearchConfig } from '../components/ai-chat-input';
|
||||||
import { type HistoryMessage } from '../components/ai-chat-messages';
|
import { type HistoryMessage } from '../components/ai-chat-messages';
|
||||||
@@ -125,20 +109,9 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
.chat-panel-hints :nth-child(2) {
|
.chat-panel-hints :nth-child(2) {
|
||||||
color: var(--affine-text-secondary-color);
|
color: var(--affine-text-secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-panel-footer {
|
|
||||||
margin: 8px 0px;
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--affine-text-secondary-color);
|
|
||||||
font-size: 12px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
private readonly _chatMessages: Ref<ChatPanelMessages> =
|
private readonly _chatMessagesRef: Ref<ChatPanelMessages> =
|
||||||
createRef<ChatPanelMessages>();
|
createRef<ChatPanelMessages>();
|
||||||
|
|
||||||
// request counter to track the latest request
|
// request counter to track the latest request
|
||||||
@@ -163,9 +136,8 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
|
|
||||||
const messages: HistoryMessage[] = actions ? [...actions] : [];
|
const messages: HistoryMessage[] = actions ? [...actions] : [];
|
||||||
|
|
||||||
const history = histories?.find(
|
const sessionId = await this._getSessionId();
|
||||||
history => history.sessionId === this._chatSessionId
|
const history = histories?.find(history => history.sessionId === sessionId);
|
||||||
);
|
|
||||||
if (history) {
|
if (history) {
|
||||||
messages.push(...history.messages);
|
messages.push(...history.messages);
|
||||||
AIProvider.LAST_ROOT_SESSION_ID = history.sessionId;
|
AIProvider.LAST_ROOT_SESSION_ID = history.sessionId;
|
||||||
@@ -182,98 +154,38 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
this._scrollToEnd();
|
this._scrollToEnd();
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _initChips = async () => {
|
private readonly _updateEmbeddingProgress = (
|
||||||
// context not initialized
|
count: Record<ContextEmbedStatus, number>
|
||||||
if (!this._chatSessionId || !this._chatContextId) {
|
) => {
|
||||||
return;
|
const total = count.finished + count.processing + count.failed;
|
||||||
}
|
this.embeddingProgress = [count.finished, total];
|
||||||
|
|
||||||
// context initialized, show the chips
|
|
||||||
const {
|
|
||||||
docs = [],
|
|
||||||
files = [],
|
|
||||||
tags = [],
|
|
||||||
collections = [],
|
|
||||||
} = (await AIProvider.context?.getContextDocsAndFiles(
|
|
||||||
this.doc.workspace.id,
|
|
||||||
this._chatSessionId,
|
|
||||||
this._chatContextId
|
|
||||||
)) || {};
|
|
||||||
|
|
||||||
const docChips: DocChip[] = docs.map(doc => ({
|
|
||||||
docId: doc.id,
|
|
||||||
state: doc.status || 'processing',
|
|
||||||
tooltip: doc.error,
|
|
||||||
createdAt: doc.createdAt,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const fileChips: FileChip[] = await Promise.all(
|
|
||||||
files.map(async file => {
|
|
||||||
const blob = await this.host.doc.blobSync.get(file.blobId);
|
|
||||||
return {
|
|
||||||
file: new File(blob ? [blob] : [], file.name),
|
|
||||||
blobId: file.blobId,
|
|
||||||
fileId: file.id,
|
|
||||||
state: blob ? file.status : 'failed',
|
|
||||||
tooltip: blob ? file.error : 'File not found in blob storage',
|
|
||||||
createdAt: file.createdAt,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const tagChips: TagChip[] = tags.map(tag => ({
|
|
||||||
tagId: tag.id,
|
|
||||||
state: 'finished',
|
|
||||||
createdAt: tag.createdAt,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const collectionChips: CollectionChip[] = collections.map(collection => ({
|
|
||||||
collectionId: collection.id,
|
|
||||||
state: 'finished',
|
|
||||||
createdAt: collection.createdAt,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const chips: ChatChip[] = [
|
|
||||||
...docChips,
|
|
||||||
...fileChips,
|
|
||||||
...tagChips,
|
|
||||||
...collectionChips,
|
|
||||||
].sort((a, b) => {
|
|
||||||
const aTime = a.createdAt ?? Date.now();
|
|
||||||
const bTime = b.createdAt ?? Date.now();
|
|
||||||
return aTime - bTime;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateChips(chips);
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _initEmbeddingProgress = async () => {
|
|
||||||
await this._pollContextDocsAndFiles();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _getSessionId = async () => {
|
private readonly _getSessionId = async () => {
|
||||||
if (this._chatSessionId) {
|
if (this._sessionId) {
|
||||||
return this._chatSessionId;
|
return this._sessionId;
|
||||||
}
|
}
|
||||||
this._chatSessionId = await AIProvider.session?.createSession(
|
const sessions = (
|
||||||
|
(await AIProvider.session?.getSessions(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
this.doc.id,
|
||||||
|
{ action: false }
|
||||||
|
)) || []
|
||||||
|
).filter(session => !session.parentSessionId);
|
||||||
|
const sessionId = sessions.at(-1)?.id;
|
||||||
|
this._sessionId = sessionId;
|
||||||
|
return this._sessionId;
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _createSessionId = async () => {
|
||||||
|
if (this._sessionId) {
|
||||||
|
return this._sessionId;
|
||||||
|
}
|
||||||
|
this._sessionId = await AIProvider.session?.createSession(
|
||||||
this.doc.workspace.id,
|
this.doc.workspace.id,
|
||||||
this.doc.id
|
this.doc.id
|
||||||
);
|
);
|
||||||
return this._chatSessionId;
|
return this._sessionId;
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _getContextId = async () => {
|
|
||||||
if (this._chatContextId) {
|
|
||||||
return this._chatContextId;
|
|
||||||
}
|
|
||||||
const sessionId = await this._getSessionId();
|
|
||||||
if (sessionId) {
|
|
||||||
this._chatContextId = await AIProvider.context?.createContext(
|
|
||||||
this.doc.workspace.id,
|
|
||||||
sessionId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this._chatContextId;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -298,192 +210,55 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
accessor previewSpecBuilder!: SpecBuilder;
|
accessor previewSpecBuilder!: SpecBuilder;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor isLoading = true;
|
accessor isLoading = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor chips: ChatChip[] = [];
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor embeddingProgress: [number, number] = [0, 0];
|
accessor embeddingProgress: [number, number] = [0, 0];
|
||||||
|
|
||||||
private _chatSessionId: string | null | undefined = null;
|
private _isInitialized = false;
|
||||||
|
|
||||||
private _chatContextId: string | null | undefined = null;
|
// always use getSessionId to get the sessionId
|
||||||
|
private _sessionId: string | undefined = undefined;
|
||||||
|
|
||||||
private _isOpen: Signal<boolean | undefined> = signal(false);
|
private _isSidebarOpen: Signal<boolean | undefined> = signal(false);
|
||||||
|
|
||||||
private _width: Signal<number | undefined> = signal(undefined);
|
private _sidebarWidth: Signal<number | undefined> = signal(undefined);
|
||||||
|
|
||||||
private _pollAbortController: AbortController | null = null;
|
|
||||||
|
|
||||||
private readonly _scrollToEnd = () => {
|
private readonly _scrollToEnd = () => {
|
||||||
if (!this._wheelTriggered) {
|
if (!this._wheelTriggered) {
|
||||||
this._chatMessages.value?.scrollToEnd();
|
this._chatMessagesRef.value?.scrollToEnd();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _throttledScrollToEnd = throttle(this._scrollToEnd, 600);
|
private readonly _throttledScrollToEnd = throttle(this._scrollToEnd, 600);
|
||||||
|
|
||||||
private readonly _cleanupHistories = async () => {
|
|
||||||
const notification = this.host.std.getOptional(NotificationProvider);
|
|
||||||
if (!notification) return;
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
await notification.confirm({
|
|
||||||
title: 'Clear History',
|
|
||||||
message:
|
|
||||||
'Are you sure you want to clear all history? This action will permanently delete all content, including all chat logs and data, and cannot be undone.',
|
|
||||||
confirmText: 'Confirm',
|
|
||||||
cancelText: 'Cancel',
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
const actionIds = this.chatContextValue.messages
|
|
||||||
.filter(item => 'sessionId' in item)
|
|
||||||
.map(item => item.sessionId);
|
|
||||||
await AIProvider.histories?.cleanup(
|
|
||||||
this.doc.workspace.id,
|
|
||||||
this.doc.id,
|
|
||||||
[
|
|
||||||
...(this._chatSessionId ? [this._chatSessionId] : []),
|
|
||||||
...(actionIds || []),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
notification.toast('History cleared');
|
|
||||||
await this._updateHistory();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
notification.toast('Failed to clear history');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _initPanel = async () => {
|
private readonly _initPanel = async () => {
|
||||||
try {
|
try {
|
||||||
if (!this._isOpen.value) return;
|
if (!this._isSidebarOpen.value) return;
|
||||||
|
if (this.isLoading) return;
|
||||||
const userId = (await AIProvider.userInfo)?.id;
|
const userId = (await AIProvider.userInfo)?.id;
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const sessions = (
|
await this._updateHistory();
|
||||||
(await AIProvider.session?.getSessions(
|
|
||||||
this.doc.workspace.id,
|
|
||||||
this.doc.id,
|
|
||||||
{ action: false }
|
|
||||||
)) || []
|
|
||||||
).filter(session => !session.parentSessionId);
|
|
||||||
|
|
||||||
if (sessions && sessions.length) {
|
|
||||||
this._chatSessionId = sessions.at(-1)?.id;
|
|
||||||
await this._updateHistory();
|
|
||||||
}
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
if (this._chatSessionId) {
|
this._isInitialized = true;
|
||||||
this._chatContextId = await AIProvider.context?.getContextId(
|
|
||||||
this.doc.workspace.id,
|
|
||||||
this._chatSessionId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await this._initChips();
|
|
||||||
await this._initEmbeddingProgress();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _resetPanel = () => {
|
private readonly _resetPanel = () => {
|
||||||
this._abortPoll();
|
this._sessionId = undefined;
|
||||||
this._chatSessionId = null;
|
|
||||||
this._chatContextId = null;
|
|
||||||
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
||||||
this.isLoading = true;
|
this.isLoading = false;
|
||||||
this.chips = [];
|
this._isInitialized = false;
|
||||||
this.embeddingProgress = [0, 0];
|
this.embeddingProgress = [0, 0];
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _pollContextDocsAndFiles = async () => {
|
|
||||||
if (!this._chatSessionId || !this._chatContextId || !AIProvider.context) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this._pollAbortController) {
|
|
||||||
// already polling, reset timer
|
|
||||||
this._abortPoll();
|
|
||||||
}
|
|
||||||
this._pollAbortController = new AbortController();
|
|
||||||
await AIProvider.context.pollContextDocsAndFiles(
|
|
||||||
this.doc.workspace.id,
|
|
||||||
this._chatSessionId,
|
|
||||||
this._chatContextId,
|
|
||||||
this._onPoll,
|
|
||||||
this._pollAbortController.signal
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _onPoll = (
|
|
||||||
result?: BlockSuitePresets.AIDocsAndFilesContext
|
|
||||||
) => {
|
|
||||||
if (!result) {
|
|
||||||
this._abortPoll();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
docs: sDocs = [],
|
|
||||||
files = [],
|
|
||||||
tags = [],
|
|
||||||
collections = [],
|
|
||||||
} = result;
|
|
||||||
const docs = [
|
|
||||||
...sDocs,
|
|
||||||
...tags.flatMap(tag => tag.docs),
|
|
||||||
...collections.flatMap(collection => collection.docs),
|
|
||||||
];
|
|
||||||
const hashMap = new Map<
|
|
||||||
string,
|
|
||||||
CopilotContextDoc | CopilotDocType | CopilotContextFile
|
|
||||||
>();
|
|
||||||
const count: Record<ContextEmbedStatus, number> = {
|
|
||||||
finished: 0,
|
|
||||||
processing: 0,
|
|
||||||
failed: 0,
|
|
||||||
};
|
|
||||||
docs.forEach(doc => {
|
|
||||||
hashMap.set(doc.id, doc);
|
|
||||||
doc.status && count[doc.status]++;
|
|
||||||
});
|
|
||||||
files.forEach(file => {
|
|
||||||
hashMap.set(file.id, file);
|
|
||||||
file.status && count[file.status]++;
|
|
||||||
});
|
|
||||||
const nextChips = this.chips.map(chip => {
|
|
||||||
if (isTagChip(chip) || isCollectionChip(chip)) {
|
|
||||||
return chip;
|
|
||||||
}
|
|
||||||
const id = isDocChip(chip) ? chip.docId : chip.fileId;
|
|
||||||
const item = id && hashMap.get(id);
|
|
||||||
if (item && item.status) {
|
|
||||||
return {
|
|
||||||
...chip,
|
|
||||||
state: item.status,
|
|
||||||
tooltip: 'error' in item ? item.error : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return chip;
|
|
||||||
});
|
|
||||||
const total = count.finished + count.processing + count.failed;
|
|
||||||
this.embeddingProgress = [count.finished, total];
|
|
||||||
this.updateChips(nextChips);
|
|
||||||
if (count.processing === 0) {
|
|
||||||
this._abortPoll();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _abortPoll = () => {
|
|
||||||
this._pollAbortController?.abort();
|
|
||||||
this._pollAbortController = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
protected override updated(_changedProperties: PropertyValues) {
|
protected override updated(_changedProperties: PropertyValues) {
|
||||||
if (_changedProperties.has('doc')) {
|
if (_changedProperties.has('doc')) {
|
||||||
this._resetPanel();
|
this._resetPanel();
|
||||||
@@ -515,7 +290,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override firstUpdated(): void {
|
protected override firstUpdated(): void {
|
||||||
const chatMessages = this._chatMessages.value;
|
const chatMessages = this._chatMessagesRef.value;
|
||||||
if (chatMessages) {
|
if (chatMessages) {
|
||||||
chatMessages.updateComplete
|
chatMessages.updateComplete
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -561,16 +336,16 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isOpen = this.appSidebarConfig.isOpen();
|
const isOpen = this.appSidebarConfig.isOpen();
|
||||||
this._isOpen = isOpen.signal;
|
this._isSidebarOpen = isOpen.signal;
|
||||||
this._disposables.add(isOpen.cleanup);
|
this._disposables.add(isOpen.cleanup);
|
||||||
|
|
||||||
const width = this.appSidebarConfig.getWidth();
|
const width = this.appSidebarConfig.getWidth();
|
||||||
this._width = width.signal;
|
this._sidebarWidth = width.signal;
|
||||||
this._disposables.add(width.cleanup);
|
this._disposables.add(width.cleanup);
|
||||||
|
|
||||||
this._disposables.add(
|
this._disposables.add(
|
||||||
this._isOpen.subscribe(isOpen => {
|
this._isSidebarOpen.subscribe(isOpen => {
|
||||||
if (isOpen && this.isLoading) {
|
if (isOpen && !this._isInitialized) {
|
||||||
this._initPanel().catch(console.error);
|
this._initPanel().catch(console.error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -581,10 +356,6 @@ 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');
|
||||||
@@ -597,7 +368,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
};
|
};
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const width = this._width.value || 0;
|
const width = this._sidebarWidth.value || 0;
|
||||||
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',
|
||||||
});
|
});
|
||||||
@@ -622,38 +393,34 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<chat-panel-messages
|
<chat-panel-messages
|
||||||
${ref(this._chatMessages)}
|
${ref(this._chatMessagesRef)}
|
||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
.getSessionId=${this._getSessionId}
|
.getSessionId=${this._getSessionId}
|
||||||
|
.createSessionId=${this._createSessionId}
|
||||||
.updateContext=${this.updateContext}
|
.updateContext=${this.updateContext}
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.isLoading=${this.isLoading}
|
.isLoading=${this.isLoading || !this._isInitialized}
|
||||||
.previewSpecBuilder=${this.previewSpecBuilder}
|
.previewSpecBuilder=${this.previewSpecBuilder}
|
||||||
></chat-panel-messages>
|
></chat-panel-messages>
|
||||||
<chat-panel-chips
|
<ai-chat-composer
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
.chips=${this.chips}
|
.doc=${this.doc}
|
||||||
.getContextId=${this._getContextId}
|
|
||||||
.updateChips=${this.updateChips}
|
|
||||||
.pollContextDocsAndFiles=${this._pollContextDocsAndFiles}
|
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
|
||||||
.searchMenuConfig=${this.searchMenuConfig}
|
|
||||||
></chat-panel-chips>
|
|
||||||
<ai-chat-input
|
|
||||||
.chips=${this.chips}
|
|
||||||
.chatContextValue=${this.chatContextValue}
|
|
||||||
.getSessionId=${this._getSessionId}
|
.getSessionId=${this._getSessionId}
|
||||||
.getContextId=${this._getContextId}
|
.createSessionId=${this._createSessionId}
|
||||||
|
.createChatSessionId=${this._createSessionId}
|
||||||
|
.chatContextValue=${this.chatContextValue}
|
||||||
|
.updateContext=${this.updateContext}
|
||||||
|
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
||||||
|
.onHistoryCleared=${this._updateHistory}
|
||||||
|
.isVisible=${this._isSidebarOpen}
|
||||||
.networkSearchConfig=${this.networkSearchConfig}
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.updateContext=${this.updateContext}
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
.host=${this.host}
|
.trackOptions=${{
|
||||||
.cleanupHistories=${this._cleanupHistories}
|
where: 'chat-panel',
|
||||||
></ai-chat-input>
|
control: 'chat-send',
|
||||||
<div class="chat-panel-footer">
|
}}
|
||||||
${InformationIcon()}
|
></ai-chat-composer>
|
||||||
<div>AI outputs can be misleading or wrong</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
accessor chips!: ChatChip[];
|
accessor chips!: ChatChip[];
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getContextId!: () => Promise<string | undefined>;
|
accessor createContextId!: () => Promise<string | undefined>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateChips!: (chips: ChatChip[]) => void;
|
accessor updateChips!: (chips: ChatChip[]) => void;
|
||||||
@@ -378,7 +378,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
|
|
||||||
private readonly _addDocToContext = async (chip: DocChip) => {
|
private readonly _addDocToContext = async (chip: DocChip) => {
|
||||||
try {
|
try {
|
||||||
const contextId = await this.getContextId();
|
const contextId = await this.createContextId();
|
||||||
if (!contextId || !AIProvider.context) {
|
if (!contextId || !AIProvider.context) {
|
||||||
throw new Error('Context not found');
|
throw new Error('Context not found');
|
||||||
}
|
}
|
||||||
@@ -396,7 +396,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
|
|
||||||
private readonly _addFileToContext = async (chip: FileChip) => {
|
private readonly _addFileToContext = async (chip: FileChip) => {
|
||||||
try {
|
try {
|
||||||
const contextId = await this.getContextId();
|
const contextId = await this.createContextId();
|
||||||
if (!contextId || !AIProvider.context) {
|
if (!contextId || !AIProvider.context) {
|
||||||
throw new Error('Context not found');
|
throw new Error('Context not found');
|
||||||
}
|
}
|
||||||
@@ -420,7 +420,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
|
|
||||||
private readonly _addTagToContext = async (chip: TagChip) => {
|
private readonly _addTagToContext = async (chip: TagChip) => {
|
||||||
try {
|
try {
|
||||||
const contextId = await this.getContextId();
|
const contextId = await this.createContextId();
|
||||||
if (!contextId || !AIProvider.context) {
|
if (!contextId || !AIProvider.context) {
|
||||||
throw new Error('Context not found');
|
throw new Error('Context not found');
|
||||||
}
|
}
|
||||||
@@ -444,7 +444,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
|
|
||||||
private readonly _addCollectionToContext = async (chip: CollectionChip) => {
|
private readonly _addCollectionToContext = async (chip: CollectionChip) => {
|
||||||
try {
|
try {
|
||||||
const contextId = await this.getContextId();
|
const contextId = await this.createContextId();
|
||||||
if (!contextId || !AIProvider.context) {
|
if (!contextId || !AIProvider.context) {
|
||||||
throw new Error('Context not found');
|
throw new Error('Context not found');
|
||||||
}
|
}
|
||||||
@@ -474,7 +474,7 @@ export class ChatPanelChips extends SignalWatcher(
|
|||||||
chip: ChatChip
|
chip: ChatChip
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const contextId = await this.getContextId();
|
const contextId = await this.createContextId();
|
||||||
if (!contextId || !AIProvider.context) {
|
if (!contextId || !AIProvider.context) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,409 @@
|
|||||||
|
import type {
|
||||||
|
ContextEmbedStatus,
|
||||||
|
CopilotContextDoc,
|
||||||
|
CopilotContextFile,
|
||||||
|
CopilotDocType,
|
||||||
|
} from '@affine/graphql';
|
||||||
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
|
import { NotificationProvider } from '@blocksuite/affine/shared/services';
|
||||||
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
|
import type { Store } from '@blocksuite/affine/store';
|
||||||
|
import { InformationIcon } from '@blocksuite/icons/lit';
|
||||||
|
import { type Signal, signal } from '@preact/signals-core';
|
||||||
|
import { css, html, type PropertyValues } from 'lit';
|
||||||
|
import { property, state } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
import { AIProvider } from '../../provider';
|
||||||
|
import type {
|
||||||
|
ChatChip,
|
||||||
|
CollectionChip,
|
||||||
|
DocChip,
|
||||||
|
DocDisplayConfig,
|
||||||
|
FileChip,
|
||||||
|
SearchMenuConfig,
|
||||||
|
TagChip,
|
||||||
|
} from '../ai-chat-chips';
|
||||||
|
import { isCollectionChip, isDocChip, isTagChip } from '../ai-chat-chips';
|
||||||
|
import type {
|
||||||
|
AIChatInputContext,
|
||||||
|
AINetworkSearchConfig,
|
||||||
|
} from '../ai-chat-input';
|
||||||
|
|
||||||
|
export class AIChatComposer extends SignalWatcher(
|
||||||
|
WithDisposable(ShadowlessElement)
|
||||||
|
) {
|
||||||
|
static override styles = css`
|
||||||
|
.chat-panel-footer {
|
||||||
|
margin: 8px 0px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--affine-text-secondary-color);
|
||||||
|
font-size: 12px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor host!: EditorHost;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor doc!: Store;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor getSessionId!: () => Promise<string | undefined>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor createSessionId!: () => Promise<string | undefined>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor createChatSessionId!: () => Promise<string | undefined>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor chatContextValue!: AIChatInputContext;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor onHistoryCleared: (() => void) | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor isVisible: Signal<boolean | undefined> = signal(false);
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor updateEmbeddingProgress!: (
|
||||||
|
count: Record<ContextEmbedStatus, number>
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor docDisplayConfig!: DocDisplayConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor searchMenuConfig!: SearchMenuConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor onChatSuccess: (() => void) | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor trackOptions!: BlockSuitePresets.TrackerOptions;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor portalContainer: HTMLElement | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor chips: ChatChip[] = [];
|
||||||
|
|
||||||
|
private _isInitialized = false;
|
||||||
|
|
||||||
|
private _isLoading = false;
|
||||||
|
|
||||||
|
private _contextId: string | undefined = undefined;
|
||||||
|
|
||||||
|
private _pollAbortController: AbortController | null = null;
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<chat-panel-chips
|
||||||
|
.host=${this.host}
|
||||||
|
.chips=${this.chips}
|
||||||
|
.createContextId=${this._createContextId}
|
||||||
|
.updateChips=${this.updateChips}
|
||||||
|
.pollContextDocsAndFiles=${this._pollContextDocsAndFiles}
|
||||||
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
|
.portalContainer=${this.portalContainer}
|
||||||
|
></chat-panel-chips>
|
||||||
|
<ai-chat-input
|
||||||
|
.host=${this.host}
|
||||||
|
.chips=${this.chips}
|
||||||
|
.getSessionId=${this.getSessionId}
|
||||||
|
.createSessionId=${this.createChatSessionId}
|
||||||
|
.getContextId=${this._getContextId}
|
||||||
|
.chatContextValue=${this.chatContextValue}
|
||||||
|
.updateContext=${this.updateContext}
|
||||||
|
.networkSearchConfig=${this.networkSearchConfig}
|
||||||
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
|
.cleanupHistories=${this._cleanupHistories}
|
||||||
|
.onChatSuccess=${this.onChatSuccess}
|
||||||
|
.trackOptions=${this.trackOptions}
|
||||||
|
></ai-chat-input>
|
||||||
|
<div class="chat-panel-footer">
|
||||||
|
${InformationIcon()}
|
||||||
|
<div>AI outputs can be misleading or wrong</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
if (!this.doc) throw new Error('doc is required');
|
||||||
|
|
||||||
|
this._disposables.add(
|
||||||
|
AIProvider.slots.userInfo.subscribe(() => {
|
||||||
|
this._initComposer().catch(console.error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this._disposables.add(
|
||||||
|
this.isVisible.subscribe(isVisible => {
|
||||||
|
if (isVisible && !this._isInitialized) {
|
||||||
|
this._initComposer().catch(console.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override updated(_changedProperties: PropertyValues) {
|
||||||
|
if (_changedProperties.has('doc')) {
|
||||||
|
this._resetComposer();
|
||||||
|
requestAnimationFrame(async () => {
|
||||||
|
await this._initComposer();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _getContextId = async () => {
|
||||||
|
if (this._contextId) {
|
||||||
|
return this._contextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = await this.getSessionId();
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
const contextId = await AIProvider.context?.getContextId(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
sessionId
|
||||||
|
);
|
||||||
|
this._contextId = contextId;
|
||||||
|
return this._contextId;
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _createContextId = async () => {
|
||||||
|
if (this._contextId) {
|
||||||
|
return this._contextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = await this.createSessionId();
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
this._contextId = await AIProvider.context?.createContext(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
sessionId
|
||||||
|
);
|
||||||
|
return this._contextId;
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _initChips = async () => {
|
||||||
|
// context not initialized
|
||||||
|
const sessionId = await this.getSessionId();
|
||||||
|
const contextId = await this._getContextId();
|
||||||
|
if (!sessionId || !contextId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// context initialized, show the chips
|
||||||
|
const {
|
||||||
|
docs = [],
|
||||||
|
files = [],
|
||||||
|
tags = [],
|
||||||
|
collections = [],
|
||||||
|
} = (await AIProvider.context?.getContextDocsAndFiles(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
sessionId,
|
||||||
|
contextId
|
||||||
|
)) || {};
|
||||||
|
|
||||||
|
const docChips: DocChip[] = docs.map(doc => ({
|
||||||
|
docId: doc.id,
|
||||||
|
state: doc.status || 'processing',
|
||||||
|
tooltip: doc.error,
|
||||||
|
createdAt: doc.createdAt,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const fileChips: FileChip[] = await Promise.all(
|
||||||
|
files.map(async file => {
|
||||||
|
const blob = await this.host.doc.blobSync.get(file.blobId);
|
||||||
|
return {
|
||||||
|
file: new File(blob ? [blob] : [], file.name),
|
||||||
|
blobId: file.blobId,
|
||||||
|
fileId: file.id,
|
||||||
|
state: blob ? file.status : 'failed',
|
||||||
|
tooltip: blob ? file.error : 'File not found in blob storage',
|
||||||
|
createdAt: file.createdAt,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const tagChips: TagChip[] = tags.map(tag => ({
|
||||||
|
tagId: tag.id,
|
||||||
|
state: 'finished',
|
||||||
|
createdAt: tag.createdAt,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const collectionChips: CollectionChip[] = collections.map(collection => ({
|
||||||
|
collectionId: collection.id,
|
||||||
|
state: 'finished',
|
||||||
|
createdAt: collection.createdAt,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const chips: ChatChip[] = [
|
||||||
|
...docChips,
|
||||||
|
...fileChips,
|
||||||
|
...tagChips,
|
||||||
|
...collectionChips,
|
||||||
|
].sort((a, b) => {
|
||||||
|
const aTime = a.createdAt ?? Date.now();
|
||||||
|
const bTime = b.createdAt ?? Date.now();
|
||||||
|
return aTime - bTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateChips(chips);
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly updateChips = (chips: ChatChip[]) => {
|
||||||
|
this.chips = chips;
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _pollContextDocsAndFiles = async () => {
|
||||||
|
const sessionId = await this.getSessionId();
|
||||||
|
const contextId = await this._getContextId();
|
||||||
|
if (!sessionId || !contextId || !AIProvider.context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._pollAbortController) {
|
||||||
|
// already polling, reset timer
|
||||||
|
this._abortPoll();
|
||||||
|
}
|
||||||
|
this._pollAbortController = new AbortController();
|
||||||
|
await AIProvider.context.pollContextDocsAndFiles(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
sessionId,
|
||||||
|
contextId,
|
||||||
|
this._onPoll,
|
||||||
|
this._pollAbortController.signal
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _onPoll = (
|
||||||
|
result?: BlockSuitePresets.AIDocsAndFilesContext
|
||||||
|
) => {
|
||||||
|
if (!result) {
|
||||||
|
this._abortPoll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
docs: sDocs = [],
|
||||||
|
files = [],
|
||||||
|
tags = [],
|
||||||
|
collections = [],
|
||||||
|
} = result;
|
||||||
|
const docs = [
|
||||||
|
...sDocs,
|
||||||
|
...tags.flatMap(tag => tag.docs),
|
||||||
|
...collections.flatMap(collection => collection.docs),
|
||||||
|
];
|
||||||
|
const hashMap = new Map<
|
||||||
|
string,
|
||||||
|
CopilotContextDoc | CopilotDocType | CopilotContextFile
|
||||||
|
>();
|
||||||
|
const count: Record<ContextEmbedStatus, number> = {
|
||||||
|
finished: 0,
|
||||||
|
processing: 0,
|
||||||
|
failed: 0,
|
||||||
|
};
|
||||||
|
docs.forEach(doc => {
|
||||||
|
hashMap.set(doc.id, doc);
|
||||||
|
doc.status && count[doc.status]++;
|
||||||
|
});
|
||||||
|
files.forEach(file => {
|
||||||
|
hashMap.set(file.id, file);
|
||||||
|
file.status && count[file.status]++;
|
||||||
|
});
|
||||||
|
const nextChips = this.chips.map(chip => {
|
||||||
|
if (isTagChip(chip) || isCollectionChip(chip)) {
|
||||||
|
return chip;
|
||||||
|
}
|
||||||
|
const id = isDocChip(chip) ? chip.docId : chip.fileId;
|
||||||
|
const item = id && hashMap.get(id);
|
||||||
|
if (item && item.status) {
|
||||||
|
return {
|
||||||
|
...chip,
|
||||||
|
state: item.status,
|
||||||
|
tooltip: 'error' in item ? item.error : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return chip;
|
||||||
|
});
|
||||||
|
this.updateChips(nextChips);
|
||||||
|
this.updateEmbeddingProgress(count);
|
||||||
|
if (count.processing === 0) {
|
||||||
|
this._abortPoll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _abortPoll = () => {
|
||||||
|
this._pollAbortController?.abort();
|
||||||
|
this._pollAbortController = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _cleanupHistories = async () => {
|
||||||
|
const sessionId = await this.getSessionId();
|
||||||
|
const notification = this.host.std.getOptional(NotificationProvider);
|
||||||
|
if (!notification) return;
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
await notification.confirm({
|
||||||
|
title: 'Clear History',
|
||||||
|
message:
|
||||||
|
'Are you sure you want to clear all history? This action will permanently delete all content, including all chat logs and data, and cannot be undone.',
|
||||||
|
confirmText: 'Confirm',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
const actionIds = this.chatContextValue.messages
|
||||||
|
.filter(item => 'sessionId' in item)
|
||||||
|
.map(item => item.sessionId);
|
||||||
|
await AIProvider.histories?.cleanup(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
this.doc.id,
|
||||||
|
[...(sessionId ? [sessionId] : []), ...(actionIds || [])]
|
||||||
|
);
|
||||||
|
notification.toast('History cleared');
|
||||||
|
this.onHistoryCleared?.();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
notification.toast('Failed to clear history');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _initComposer = async () => {
|
||||||
|
if (!this.isVisible.value) return;
|
||||||
|
if (this._isLoading) return;
|
||||||
|
|
||||||
|
const userId = (await AIProvider.userInfo)?.id;
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
this._isLoading = true;
|
||||||
|
await this._initChips();
|
||||||
|
const isProcessing = this.chips.some(chip => chip.state === 'processing');
|
||||||
|
if (isProcessing) {
|
||||||
|
await this._pollContextDocsAndFiles();
|
||||||
|
}
|
||||||
|
this._isLoading = false;
|
||||||
|
this._isInitialized = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _resetComposer = () => {
|
||||||
|
this._abortPoll();
|
||||||
|
this.chips = [];
|
||||||
|
this._contextId = undefined;
|
||||||
|
this._isLoading = false;
|
||||||
|
this._isInitialized = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './ai-chat-composer';
|
||||||
@@ -222,6 +222,9 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getSessionId!: () => Promise<string | undefined>;
|
accessor getSessionId!: () => Promise<string | undefined>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor createSessionId!: () => Promise<string | undefined>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getContextId!: () => Promise<string | undefined>;
|
accessor getContextId!: () => Promise<string | undefined>;
|
||||||
|
|
||||||
@@ -241,13 +244,10 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
accessor isRootSession: boolean = true;
|
accessor isRootSession: boolean = true;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onChatSuccess = () => null;
|
accessor onChatSuccess: (() => void) | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor trackOptions: BlockSuitePresets.TrackerOptions = {
|
accessor trackOptions!: BlockSuitePresets.TrackerOptions;
|
||||||
control: 'chat-send',
|
|
||||||
where: 'chat-panel',
|
|
||||||
};
|
|
||||||
|
|
||||||
@property({ attribute: 'data-testid', reflect: true })
|
@property({ attribute: 'data-testid', reflect: true })
|
||||||
accessor testId = 'chat-panel-input-container';
|
accessor testId = 'chat-panel-input-container';
|
||||||
@@ -284,7 +284,7 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _updatePromptName(promptName: string) {
|
private async _updatePromptName(promptName: string) {
|
||||||
const sessionId = await this.getSessionId();
|
const sessionId = await this.createSessionId();
|
||||||
if (sessionId && AIProvider.session) {
|
if (sessionId && AIProvider.session) {
|
||||||
await AIProvider.session.updateSession(sessionId, promptName);
|
await AIProvider.session.updateSession(sessionId, promptName);
|
||||||
}
|
}
|
||||||
@@ -542,7 +542,7 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
// otherwise, the unauthorized error can not be rendered properly
|
// otherwise, the unauthorized error can not be rendered properly
|
||||||
await this._updatePromptName(promptName);
|
await this._updatePromptName(promptName);
|
||||||
|
|
||||||
const sessionId = await this.getSessionId();
|
const sessionId = await this.createSessionId();
|
||||||
const contexts = await this._getMatchedContexts(userInput);
|
const contexts = await this._getMatchedContexts(userInput);
|
||||||
if (abortController.signal.aborted) {
|
if (abortController.signal.aborted) {
|
||||||
return;
|
return;
|
||||||
@@ -570,7 +570,7 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updateContext({ status: 'success' });
|
this.updateContext({ status: 'success' });
|
||||||
this.onChatSuccess();
|
this.onChatSuccess?.();
|
||||||
// update message id from server
|
// update message id from server
|
||||||
await this._postUpdateMessages();
|
await this._postUpdateMessages();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { ChatPanelCollectionChip } from './components/ai-chat-chips/collection-c
|
|||||||
import { ChatPanelDocChip } from './components/ai-chat-chips/doc-chip';
|
import { ChatPanelDocChip } from './components/ai-chat-chips/doc-chip';
|
||||||
import { ChatPanelFileChip } from './components/ai-chat-chips/file-chip';
|
import { ChatPanelFileChip } from './components/ai-chat-chips/file-chip';
|
||||||
import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
|
import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
|
||||||
|
import { AIChatComposer } from './components/ai-chat-composer';
|
||||||
import { AIChatInput } from './components/ai-chat-input/ai-chat-input';
|
import { AIChatInput } from './components/ai-chat-input/ai-chat-input';
|
||||||
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';
|
||||||
@@ -98,6 +99,7 @@ export function registerAIEffects() {
|
|||||||
customElements.define('chat-panel-messages', ChatPanelMessages);
|
customElements.define('chat-panel-messages', ChatPanelMessages);
|
||||||
customElements.define('chat-panel', ChatPanel);
|
customElements.define('chat-panel', ChatPanel);
|
||||||
customElements.define('ai-chat-input', AIChatInput);
|
customElements.define('ai-chat-input', AIChatInput);
|
||||||
|
customElements.define('ai-chat-composer', AIChatComposer);
|
||||||
customElements.define('chat-panel-chips', ChatPanelChips);
|
customElements.define('chat-panel-chips', ChatPanelChips);
|
||||||
customElements.define('chat-panel-add-popover', ChatPanelAddPopover);
|
customElements.define('chat-panel-add-popover', ChatPanelAddPopover);
|
||||||
customElements.define(
|
customElements.define(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ContextEmbedStatus } from '@affine/graphql';
|
||||||
import {
|
import {
|
||||||
CanvasElementType,
|
CanvasElementType,
|
||||||
EdgelessCRUDIdentifier,
|
EdgelessCRUDIdentifier,
|
||||||
@@ -6,12 +7,11 @@ import {
|
|||||||
import { ConnectorMode } from '@blocksuite/affine/model';
|
import { ConnectorMode } from '@blocksuite/affine/model';
|
||||||
import {
|
import {
|
||||||
DocModeProvider,
|
DocModeProvider,
|
||||||
NotificationProvider,
|
|
||||||
TelemetryProvider,
|
TelemetryProvider,
|
||||||
} from '@blocksuite/affine/shared/services';
|
} from '@blocksuite/affine/shared/services';
|
||||||
import type { SpecBuilder } from '@blocksuite/affine/shared/utils';
|
import type { Signal, SpecBuilder } from '@blocksuite/affine/shared/utils';
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
import { InformationIcon } from '@blocksuite/icons/lit';
|
import { signal } from '@preact/signals-core';
|
||||||
import { html, LitElement, nothing } from 'lit';
|
import { html, LitElement, nothing } from 'lit';
|
||||||
import { property, query, state } from 'lit/decorators.js';
|
import { property, query, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
} from '../_common/chat-actions-handle';
|
} from '../_common/chat-actions-handle';
|
||||||
import { type AIChatBlockModel } from '../blocks';
|
import { type AIChatBlockModel } from '../blocks';
|
||||||
import type {
|
import type {
|
||||||
ChatChip,
|
|
||||||
DocDisplayConfig,
|
DocDisplayConfig,
|
||||||
SearchMenuConfig,
|
SearchMenuConfig,
|
||||||
} from '../components/ai-chat-chips';
|
} from '../components/ai-chat-chips';
|
||||||
@@ -45,33 +44,33 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
return this.host.std.get(DocModeProvider);
|
return this.host.std.get(DocModeProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get parentSessionId() {
|
private get _sessionId() {
|
||||||
return this.parentModel.props.sessionId;
|
return this.blockModel.props.sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get historyMessagesString() {
|
private get historyMessagesString() {
|
||||||
return this.parentModel.props.messages;
|
return this.blockModel.props.messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get parentChatBlockId() {
|
private get blockId() {
|
||||||
return this.parentModel.id;
|
return this.blockModel.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get parentRootDocId() {
|
private get rootDocId() {
|
||||||
return this.parentModel.props.rootDocId;
|
return this.blockModel.props.rootDocId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get parentRootWorkspaceId() {
|
private get rootWorkspaceId() {
|
||||||
return this.parentModel.props.rootWorkspaceId;
|
return this.blockModel.props.rootWorkspaceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _textRendererOptions: TextRendererOptions = {};
|
private _textRendererOptions: TextRendererOptions = {};
|
||||||
|
|
||||||
private _chatSessionId: string | null | undefined = null;
|
private _forkBlockId: string | undefined = undefined;
|
||||||
|
|
||||||
private _chatContextId: string | null | undefined = null;
|
private _forkSessionId: string | undefined = undefined;
|
||||||
|
|
||||||
private _chatBlockId: string | null | undefined = null;
|
accessor isComposerVisible: Signal<boolean | undefined> = signal(true);
|
||||||
|
|
||||||
private readonly _deserializeHistoryChatMessages = (
|
private readonly _deserializeHistoryChatMessages = (
|
||||||
historyMessagesString: string
|
historyMessagesString: string
|
||||||
@@ -137,45 +136,40 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
abortController: null,
|
abortController: null,
|
||||||
messages: [],
|
messages: [],
|
||||||
});
|
});
|
||||||
this._chatSessionId = null;
|
this._forkBlockId = undefined;
|
||||||
this._chatContextId = null;
|
this._forkSessionId = undefined;
|
||||||
this._chatBlockId = null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _getSessionId = async () => {
|
private readonly _getSessionId = async () => {
|
||||||
// If has not forked a chat session, fork a new one
|
return this._sessionId;
|
||||||
if (!this._chatSessionId) {
|
|
||||||
const latestMessage = this._historyMessages.at(-1);
|
|
||||||
if (!latestMessage) return;
|
|
||||||
|
|
||||||
const forkSessionId = await AIProvider.forkChat?.({
|
|
||||||
workspaceId: this.host.doc.workspace.id,
|
|
||||||
docId: this.host.doc.id,
|
|
||||||
sessionId: this.parentSessionId,
|
|
||||||
latestMessageId: latestMessage.id,
|
|
||||||
});
|
|
||||||
this._chatSessionId = forkSessionId;
|
|
||||||
}
|
|
||||||
return this._chatSessionId;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _getContextId = async () => {
|
private readonly _createSessionId = async () => {
|
||||||
if (this._chatContextId) {
|
return this._sessionId;
|
||||||
return this._chatContextId;
|
};
|
||||||
|
|
||||||
|
private readonly _createForkSessionId = async () => {
|
||||||
|
if (this._forkSessionId) {
|
||||||
|
return this._forkSessionId;
|
||||||
}
|
}
|
||||||
const sessionId = await this._getSessionId();
|
|
||||||
if (sessionId) {
|
const lastMessage = this._historyMessages.at(-1);
|
||||||
this._chatContextId = await AIProvider.context?.createContext(
|
if (!lastMessage) return;
|
||||||
this.host.doc.workspace.id,
|
|
||||||
sessionId
|
const { doc } = this.host;
|
||||||
);
|
const forkSessionId = await AIProvider.forkChat?.({
|
||||||
}
|
workspaceId: doc.workspace.id,
|
||||||
return this._chatContextId;
|
docId: doc.id,
|
||||||
|
sessionId: this._sessionId,
|
||||||
|
latestMessageId: lastMessage.id,
|
||||||
|
});
|
||||||
|
this._forkSessionId = forkSessionId;
|
||||||
|
return this._forkSessionId;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _onChatSuccess = async () => {
|
private readonly _onChatSuccess = async () => {
|
||||||
if (!this._chatBlockId) {
|
if (!this._forkBlockId) {
|
||||||
await this.createAIChatBlock();
|
await this._createForkChatBlock();
|
||||||
}
|
}
|
||||||
// Update new chat block messages if there are contents returned from AI
|
// Update new chat block messages if there are contents returned from AI
|
||||||
await this.updateChatBlockMessages();
|
await this.updateChatBlockMessages();
|
||||||
@@ -184,7 +178,7 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
/**
|
/**
|
||||||
* Create a new AI chat block based on the current session and history messages
|
* Create a new AI chat block based on the current session and history messages
|
||||||
*/
|
*/
|
||||||
createAIChatBlock = async () => {
|
private readonly _createForkChatBlock = async () => {
|
||||||
// Only create AI chat block in edgeless mode
|
// Only create AI chat block in edgeless mode
|
||||||
const mode = this._modeService.getEditorMode();
|
const mode = this._modeService.getEditorMode();
|
||||||
if (mode !== 'edgeless') {
|
if (mode !== 'edgeless') {
|
||||||
@@ -192,12 +186,12 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If there is already a chat block, do not create a new one
|
// If there is already a chat block, do not create a new one
|
||||||
if (this._chatBlockId) {
|
if (this._forkBlockId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no session id or chat messages, do not create a new chat block
|
// If there is no session id or chat messages, do not create a new chat block
|
||||||
if (!this._chatSessionId || !this.chatContext.messages.length) {
|
if (!this._forkSessionId || !this.chatContext.messages.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,43 +205,42 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get fork session messages
|
// Get fork session messages
|
||||||
const { parentRootWorkspaceId, parentRootDocId } = this;
|
const { rootWorkspaceId, rootDocId } = this;
|
||||||
const messages = await this._constructBranchChatBlockMessages(
|
const messages = await this._constructBranchChatBlockMessages(
|
||||||
parentRootWorkspaceId,
|
rootWorkspaceId,
|
||||||
parentRootDocId,
|
rootDocId,
|
||||||
this._chatSessionId
|
this._forkSessionId
|
||||||
);
|
);
|
||||||
if (!messages.length) {
|
if (!messages.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bound = calcChildBound(this.parentModel, this.host.std);
|
const bound = calcChildBound(this.blockModel, this.host.std);
|
||||||
|
|
||||||
const crud = this.host.std.get(EdgelessCRUDIdentifier);
|
const crud = this.host.std.get(EdgelessCRUDIdentifier);
|
||||||
const aiChatBlockId = crud.addBlock(
|
const forkBlockId = crud.addBlock(
|
||||||
'affine:embed-ai-chat',
|
'affine:embed-ai-chat',
|
||||||
{
|
{
|
||||||
xywh: bound.serialize(),
|
xywh: bound.serialize(),
|
||||||
messages: JSON.stringify(messages),
|
messages: JSON.stringify(messages),
|
||||||
sessionId: this._chatSessionId,
|
sessionId: this._forkSessionId,
|
||||||
rootWorkspaceId: parentRootWorkspaceId,
|
rootWorkspaceId: rootWorkspaceId,
|
||||||
rootDocId: parentRootDocId,
|
rootDocId: rootDocId,
|
||||||
},
|
},
|
||||||
surfaceBlock.id
|
surfaceBlock.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!aiChatBlockId) {
|
if (!forkBlockId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._forkBlockId = forkBlockId;
|
||||||
this._chatBlockId = aiChatBlockId;
|
|
||||||
|
|
||||||
// Connect the parent chat block to the AI chat block
|
// Connect the parent chat block to the AI chat block
|
||||||
crud.addElement(CanvasElementType.CONNECTOR, {
|
crud.addElement(CanvasElementType.CONNECTOR, {
|
||||||
mode: ConnectorMode.Curve,
|
mode: ConnectorMode.Curve,
|
||||||
controllers: [],
|
controllers: [],
|
||||||
source: { id: this.parentChatBlockId },
|
source: { id: this.blockId },
|
||||||
target: { id: aiChatBlockId },
|
target: { id: forkBlockId },
|
||||||
});
|
});
|
||||||
|
|
||||||
const telemetryService = this.host.std.getOptional(TelemetryProvider);
|
const telemetryService = this.host.std.getOptional(TelemetryProvider);
|
||||||
@@ -265,20 +258,20 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
* Update the current chat messages with the new message
|
* Update the current chat messages with the new message
|
||||||
*/
|
*/
|
||||||
updateChatBlockMessages = async () => {
|
updateChatBlockMessages = async () => {
|
||||||
if (!this._chatBlockId || !this._chatSessionId) {
|
if (!this._forkBlockId || !this._forkSessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { doc } = this.host;
|
const { doc } = this.host;
|
||||||
const chatBlock = doc.getBlock(this._chatBlockId);
|
const chatBlock = doc.getBlock(this._forkBlockId);
|
||||||
if (!chatBlock) return;
|
if (!chatBlock) return;
|
||||||
|
|
||||||
// Get fork session messages
|
// Get fork session messages
|
||||||
const { parentRootWorkspaceId, parentRootDocId } = this;
|
const { rootWorkspaceId, rootDocId } = this;
|
||||||
const messages = await this._constructBranchChatBlockMessages(
|
const messages = await this._constructBranchChatBlockMessages(
|
||||||
parentRootWorkspaceId,
|
rootWorkspaceId,
|
||||||
parentRootDocId,
|
rootDocId,
|
||||||
this._chatSessionId
|
this._forkSessionId
|
||||||
);
|
);
|
||||||
if (!messages.length) {
|
if (!messages.length) {
|
||||||
return;
|
return;
|
||||||
@@ -292,58 +285,35 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
this.chatContext = { ...this.chatContext, ...context };
|
this.chatContext = { ...this.chatContext, ...context };
|
||||||
};
|
};
|
||||||
|
|
||||||
updateChips = (chips: ChatChip[]) => {
|
private readonly _updateEmbeddingProgress = (
|
||||||
this.chips = chips;
|
count: Record<ContextEmbedStatus, number>
|
||||||
|
) => {
|
||||||
|
const total = count.finished + count.processing + count.failed;
|
||||||
|
this.embeddingProgress = [count.finished, total];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean current chat messages and delete the newly created AI chat block
|
* Clean current chat messages and delete the newly created AI chat block
|
||||||
*/
|
*/
|
||||||
cleanCurrentChatHistories = async () => {
|
private readonly _onHistoryCleared = async () => {
|
||||||
const notificationService = this.host.std.getOptional(NotificationProvider);
|
const { _forkBlockId, host } = this;
|
||||||
if (!notificationService) return;
|
if (_forkBlockId) {
|
||||||
|
const surface = getSurfaceBlock(host.doc);
|
||||||
const { _chatBlockId, _chatSessionId } = this;
|
const crud = host.std.get(EdgelessCRUDIdentifier);
|
||||||
if (!_chatBlockId && !_chatSessionId) {
|
const chatBlock = host.doc.getBlock(_forkBlockId)?.model;
|
||||||
return;
|
if (chatBlock) {
|
||||||
}
|
const connectors = surface?.getConnectors(chatBlock.id);
|
||||||
|
host.doc.transact(() => {
|
||||||
if (
|
// Delete the AI chat block
|
||||||
await notificationService.confirm({
|
crud.removeElement(_forkBlockId);
|
||||||
title: 'Clear History',
|
// Delete the connectors
|
||||||
message:
|
connectors?.forEach(connector => {
|
||||||
'Are you sure you want to clear all history? This action will permanently delete all content, including all chat logs and data, and cannot be undone.',
|
crud.removeElement(connector.id);
|
||||||
confirmText: 'Confirm',
|
|
||||||
cancelText: 'Cancel',
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
const { doc } = this.host;
|
|
||||||
if (_chatSessionId) {
|
|
||||||
await AIProvider.histories?.cleanup(doc.workspace.id, doc.id, [
|
|
||||||
_chatSessionId,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_chatBlockId) {
|
|
||||||
const surface = getSurfaceBlock(doc);
|
|
||||||
const crud = this.host.std.get(EdgelessCRUDIdentifier);
|
|
||||||
const chatBlock = doc.getBlock(_chatBlockId)?.model;
|
|
||||||
if (chatBlock) {
|
|
||||||
const connectors = surface?.getConnectors(chatBlock.id);
|
|
||||||
doc.transact(() => {
|
|
||||||
// Delete the AI chat block
|
|
||||||
crud.removeElement(_chatBlockId);
|
|
||||||
// Delete the connectors
|
|
||||||
connectors?.forEach(connector => {
|
|
||||||
crud.removeElement(connector.id);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationService.toast('History cleared');
|
|
||||||
this._resetContext();
|
|
||||||
}
|
}
|
||||||
|
this._resetContext();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -351,8 +321,8 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
*/
|
*/
|
||||||
retry = async () => {
|
retry = async () => {
|
||||||
const { doc } = this.host;
|
const { doc } = this.host;
|
||||||
const { _chatBlockId, _chatSessionId } = this;
|
const { _forkBlockId, _forkSessionId } = this;
|
||||||
if (!_chatBlockId || !_chatSessionId) {
|
if (!_forkBlockId || !_forkSessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +339,7 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
this.updateContext({ messages, status: 'loading', error: null });
|
this.updateContext({ messages, status: 'loading', error: null });
|
||||||
|
|
||||||
const stream = AIProvider.actions.chat?.({
|
const stream = AIProvider.actions.chat?.({
|
||||||
sessionId: _chatSessionId,
|
sessionId: _forkSessionId,
|
||||||
retry: true,
|
retry: true,
|
||||||
docId: doc.id,
|
docId: doc.id,
|
||||||
workspaceId: doc.workspace.id,
|
workspaceId: doc.workspace.id,
|
||||||
@@ -484,12 +454,8 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
this._historyMessages = this._deserializeHistoryChatMessages(
|
this._historyMessages = this._deserializeHistoryChatMessages(
|
||||||
this.historyMessagesString
|
this.historyMessagesString
|
||||||
);
|
);
|
||||||
const { parentRootWorkspaceId, parentRootDocId, parentSessionId } = this;
|
const { rootWorkspaceId, rootDocId, _sessionId } = this;
|
||||||
queryHistoryMessages(
|
queryHistoryMessages(rootWorkspaceId, rootDocId, _sessionId)
|
||||||
parentRootWorkspaceId,
|
|
||||||
parentRootDocId,
|
|
||||||
parentSessionId
|
|
||||||
)
|
|
||||||
.then(messages => {
|
.then(messages => {
|
||||||
this._historyMessages = this._historyMessages.map((message, idx) => {
|
this._historyMessages = this._historyMessages.map((message, idx) => {
|
||||||
return {
|
return {
|
||||||
@@ -522,7 +488,6 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
const latestHistoryMessage = _historyMessages[_historyMessages.length - 1];
|
const latestHistoryMessage = _historyMessages[_historyMessages.length - 1];
|
||||||
const latestMessageCreatedAt = latestHistoryMessage.createdAt;
|
const latestMessageCreatedAt = latestHistoryMessage.createdAt;
|
||||||
const {
|
const {
|
||||||
cleanCurrentChatHistories,
|
|
||||||
chatContext,
|
chatContext,
|
||||||
updateContext,
|
updateContext,
|
||||||
networkSearchConfig,
|
networkSearchConfig,
|
||||||
@@ -543,26 +508,27 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
${this.CurrentMessages(currentChatMessages)}
|
${this.CurrentMessages(currentChatMessages)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ai-chat-input
|
<ai-chat-composer
|
||||||
.host=${host}
|
.host=${host}
|
||||||
.chips=${this.chips}
|
.doc=${this.host.doc}
|
||||||
.getSessionId=${this._getSessionId}
|
.getSessionId=${this._getSessionId}
|
||||||
.getContextId=${this._getContextId}
|
.createSessionId=${this._createSessionId}
|
||||||
.cleanupHistories=${cleanCurrentChatHistories}
|
.createChatSessionId=${this._createForkSessionId}
|
||||||
.chatContextValue=${chatContext}
|
.chatContextValue=${chatContext}
|
||||||
.updateContext=${updateContext}
|
.updateContext=${updateContext}
|
||||||
|
.onHistoryCleared=${this._onHistoryCleared}
|
||||||
|
.isVisible=${this.isComposerVisible}
|
||||||
|
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
|
||||||
.networkSearchConfig=${networkSearchConfig}
|
.networkSearchConfig=${networkSearchConfig}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
|
.searchMenuConfig=${this.searchMenuConfig}
|
||||||
.onChatSuccess=${this._onChatSuccess}
|
.onChatSuccess=${this._onChatSuccess}
|
||||||
.trackOptions=${{
|
.trackOptions=${{
|
||||||
where: 'ai-chat-block',
|
where: 'ai-chat-block',
|
||||||
control: 'chat-send',
|
control: 'chat-send',
|
||||||
}}
|
}}
|
||||||
></ai-chat-input>
|
.portalContainer=${this.parentElement}
|
||||||
<div class="peek-view-footer">
|
></ai-chat-composer>
|
||||||
${InformationIcon()}
|
|
||||||
<div>AI outputs can be misleading or wrong</div>
|
|
||||||
</div>
|
|
||||||
</div> `;
|
</div> `;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,7 +536,7 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
accessor _chatMessagesContainer!: HTMLDivElement;
|
accessor _chatMessagesContainer!: HTMLDivElement;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor parentModel!: AIChatBlockModel;
|
accessor blockModel!: AIChatBlockModel;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host!: EditorHost;
|
accessor host!: EditorHost;
|
||||||
@@ -600,7 +566,7 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor chips: ChatChip[] = [];
|
accessor embeddingProgress: [number, number] = [0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -610,7 +576,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AIChatBlockPeekViewTemplate = (
|
export const AIChatBlockPeekViewTemplate = (
|
||||||
parentModel: AIChatBlockModel,
|
blockModel: AIChatBlockModel,
|
||||||
host: EditorHost,
|
host: EditorHost,
|
||||||
previewSpecBuilder: SpecBuilder,
|
previewSpecBuilder: SpecBuilder,
|
||||||
docDisplayConfig: DocDisplayConfig,
|
docDisplayConfig: DocDisplayConfig,
|
||||||
@@ -618,7 +584,7 @@ export const AIChatBlockPeekViewTemplate = (
|
|||||||
networkSearchConfig: AINetworkSearchConfig
|
networkSearchConfig: AINetworkSearchConfig
|
||||||
) => {
|
) => {
|
||||||
return html`<ai-chat-block-peek-view
|
return html`<ai-chat-block-peek-view
|
||||||
.parentModel=${parentModel}
|
.blockModel=${blockModel}
|
||||||
.host=${host}
|
.host=${host}
|
||||||
.previewSpecBuilder=${previewSpecBuilder}
|
.previewSpecBuilder=${previewSpecBuilder}
|
||||||
.networkSearchConfig=${networkSearchConfig}
|
.networkSearchConfig=${networkSearchConfig}
|
||||||
|
|||||||
@@ -57,16 +57,4 @@ export const PeekViewStyles = css`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peek-view-footer {
|
|
||||||
margin-top: 8px;
|
|
||||||
width: 100%;
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--affine-text-secondary-color);
|
|
||||||
font-size: var(--affine-font-xs);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user