akumatus
2025-04-03 14:53:50 +00:00
parent 6cf182190c
commit 0aeb3041b5
9 changed files with 600 additions and 464 deletions

View File

@@ -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();

View File

@@ -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>`;
} }
} }

View File

@@ -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;
} }

View File

@@ -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;
};
}

View File

@@ -0,0 +1 @@
export * from './ai-chat-composer';

View File

@@ -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) {

View File

@@ -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(

View File

@@ -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}

View File

@@ -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;
}
`; `;