feat(core): remove chat-panel component's dependency on doc (#12975)

Close [AI-259](https://linear.app/affine-design/issue/AI-259)
Close [AI-243](https://linear.app/affine-design/issue/AI-243)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced a unified AI chat content component to manage and display
chat interactions.
* Added new chat block message components for improved chat message
rendering.

* **Refactor**
* Simplified and unified session management across all AI chat
components, now passing full session objects instead of session IDs.
* Updated component and property names for clarity and consistency
(e.g., chat message and block message components).
* Consolidated chat history and actions retrieval for a more streamlined
chat experience.
* Removed redundant session ID getters and replaced them with direct
session object usage.
* Streamlined chat panel and composer components by removing internal
message and context state management.

* **Bug Fixes**
* Improved handling of chat session state and loading, reducing
redundant state properties.
* Enhanced event handling to prevent errors when chat parameters are
missing.

* **Tests**
* Removed outdated chat clearing test cases to align with new chat state
management.

* **Chores**
* Updated import paths and reorganized module exports for better
maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wu Yue
2025-07-01 19:20:20 +08:00
committed by GitHub
parent d49a069351
commit 6e9487a9e1
28 changed files with 669 additions and 649 deletions

View File

@@ -1,4 +1,3 @@
import { ChatHistoryOrder } from '@affine/graphql';
import { EdgelessCRUDIdentifier } from '@blocksuite/affine/blocks/surface'; import { EdgelessCRUDIdentifier } from '@blocksuite/affine/blocks/surface';
import { import {
Bound, Bound,
@@ -69,14 +68,15 @@ export type ChatAction = {
export async function queryHistoryMessages( export async function queryHistoryMessages(
workspaceId: string, workspaceId: string,
docId: string, forkSessionId: string,
forkSessionId: string docId?: string
) { ) {
// Get fork session messages // Get fork session messages
const histories = await AIProvider.histories?.chats(workspaceId, docId, { const histories = await AIProvider.histories?.chats(
sessionId: forkSessionId, workspaceId,
messageOrder: ChatHistoryOrder.asc, forkSessionId,
}); docId
);
if (!histories || !histories.length) { if (!histories || !histories.length) {
return []; return [];
@@ -117,8 +117,8 @@ export async function constructRootChatBlockMessages(
const userInfo = await AIProvider.userInfo; const userInfo = await AIProvider.userInfo;
const forkMessages = (await queryHistoryMessages( const forkMessages = (await queryHistoryMessages(
doc.workspace.id, doc.workspace.id,
doc.id, forkSessionId,
forkSessionId doc.id
)) as ChatMessage[]; )) as ChatMessage[];
return constructUserInfoWithMessages(forkMessages, userInfo); return constructUserInfoWithMessages(forkMessages, userInfo);
} }

View File

@@ -1,5 +1,4 @@
import type { import type {
ChatHistoryOrder,
ContextMatchedDocChunk, ContextMatchedDocChunk,
ContextMatchedFileChunk, ContextMatchedFileChunk,
ContextWorkspaceEmbeddingStatus, ContextWorkspaceEmbeddingStatus,
@@ -400,15 +399,12 @@ declare global {
// non chat histories // non chat histories
actions: ( actions: (
workspaceId: string, workspaceId: string,
docId?: string docId: string
) => Promise<AIHistory[] | undefined>; ) => Promise<AIHistory[] | undefined>;
chats: ( chats: (
workspaceId: string, workspaceId: string,
docId?: string, sessionId: string,
options?: { docId?: string
sessionId?: string;
messageOrder?: ChatHistoryOrder;
}
) => Promise<AIHistory[] | undefined>; ) => Promise<AIHistory[] | undefined>;
cleanup: ( cleanup: (
workspaceId: string, workspaceId: string,

View File

@@ -49,12 +49,12 @@ export class AIChatBlockComponent extends BlockComponent<AIChatBlockModel> {
return html`<div class="affine-ai-chat-block-container"> return html`<div class="affine-ai-chat-block-container">
<div class="ai-chat-messages-container"> <div class="ai-chat-messages-container">
<ai-chat-messages <ai-chat-block-messages
.host=${this.host} .host=${this.host}
.messages=${messages} .messages=${messages}
.textRendererOptions=${this._textRendererOptions} .textRendererOptions=${this._textRendererOptions}
.withMask=${true} .withMask=${true}
></ai-chat-messages> ></ai-chat-block-messages>
</div> </div>
<div class="ai-chat-block-button"> <div class="ai-chat-block-button">
${ChatWithAIIcon} <span>AI chat block</span> ${ChatWithAIIcon} <span>AI chat block</span>

View File

@@ -11,7 +11,7 @@ import {
} from '../../../components/ai-chat-messages'; } from '../../../components/ai-chat-messages';
import { UserInfoTemplate } from './user-info'; import { UserInfoTemplate } from './user-info';
export class AIChatMessage extends LitElement { export class AIChatBlockMessage extends LitElement {
static override styles = css` static override styles = css`
.ai-chat-message { .ai-chat-message {
display: flex; display: flex;
@@ -99,7 +99,7 @@ export class AIChatMessage extends LitElement {
accessor textRendererOptions: TextRendererOptions = {}; accessor textRendererOptions: TextRendererOptions = {};
} }
export class AIChatMessages extends LitElement { export class AIChatBlockMessages extends LitElement {
static override styles = css` static override styles = css`
:host { :host {
width: 100%; width: 100%;
@@ -123,11 +123,11 @@ export class AIChatMessages extends LitElement {
message => message.id || message.createdAt, message => message.id || message.createdAt,
message => { message => {
return html` return html`
<ai-chat-message <ai-chat-block-message
.host=${this.host} .host=${this.host}
.textRendererOptions=${this.textRendererOptions} .textRendererOptions=${this.textRendererOptions}
.message=${message} .message=${message}
></ai-chat-message> ></ai-chat-block-message>
`; `;
} }
)} )}
@@ -146,7 +146,7 @@ export class AIChatMessages extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'ai-chat-message': AIChatMessage; 'ai-chat-block-message': AIChatBlockMessage;
'ai-chat-messages': AIChatMessages; 'ai-chat-block-messages': AIChatBlockMessages;
} }
} }

View File

@@ -1,5 +1,3 @@
import './chat-panel-messages';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql'; import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
@@ -12,9 +10,8 @@ import { CenterPeekIcon } from '@blocksuite/icons/lit';
import { type Signal, signal } from '@preact/signals-core'; import { type Signal, signal } from '@preact/signals-core';
import { css, html, nothing, type PropertyValues } from 'lit'; import { css, html, nothing, type PropertyValues } from 'lit';
import { property, state } from 'lit/decorators.js'; import { property, state } from 'lit/decorators.js';
import { createRef, type Ref, ref } from 'lit/directives/ref.js'; import { keyed } from 'lit/directives/keyed.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { throttle } from 'lodash-es';
import type { import type {
DocDisplayConfig, DocDisplayConfig,
@@ -25,31 +22,9 @@ import type {
AIPlaygroundConfig, AIPlaygroundConfig,
AIReasoningConfig, AIReasoningConfig,
} from '../components/ai-chat-input'; } from '../components/ai-chat-input';
import {
type ChatAction,
type ChatMessage,
type HistoryMessage,
} from '../components/ai-chat-messages';
import { createPlaygroundModal } from '../components/playground/modal'; import { createPlaygroundModal } from '../components/playground/modal';
import { AIProvider } from '../provider'; import { AIProvider } from '../provider';
import { extractSelectedContent } from '../utils/extract';
import {
getSelectedImagesAsBlobs,
getSelectedTextContent,
} from '../utils/selection-utils';
import type { AppSidebarConfig } from './chat-config'; import type { AppSidebarConfig } from './chat-config';
import type { ChatContextValue } from './chat-context';
import type { ChatPanelMessages } from './chat-panel-messages';
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
quote: '',
images: [],
abortController: null,
messages: [],
status: 'idle',
error: null,
markdown: '',
};
export class ChatPanel extends SignalWatcher( export class ChatPanel extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
@@ -58,24 +33,10 @@ export class ChatPanel extends SignalWatcher(
chat-panel { chat-panel {
width: 100%; width: 100%;
user-select: text; user-select: text;
}
.chat-panel-container { .chat-panel-container {
display: flex; height: 100%;
flex-direction: column; }
height: 100%;
}
.chat-panel-title {
background: var(--affine-background-primary-color);
position: relative;
padding: 8px 0px;
width: 100%;
height: 36px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1;
.chat-panel-title-text { .chat-panel-title-text {
font-size: 14px; font-size: 14px;
@@ -83,153 +44,40 @@ export class ChatPanel extends SignalWatcher(
color: var(--affine-text-secondary-color); color: var(--affine-text-secondary-color);
} }
svg { .chat-panel-playground {
width: 18px; cursor: pointer;
height: 18px; padding: 2px;
color: var(--affine-text-secondary-color); margin-left: 8px;
margin-right: auto;
display: flex;
justify-content: center;
align-items: center;
}
.chat-panel-playground:hover svg {
color: ${unsafeCSSVarV2('icon/activated')};
} }
} }
chat-panel-messages {
flex: 1;
overflow-y: hidden;
}
.chat-panel-hints {
margin: 0 4px;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--affine-border-color);
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.chat-panel-hints :first-child {
color: var(--affine-text-primary-color);
}
.chat-panel-hints :nth-child(2) {
color: var(--affine-text-secondary-color);
}
.chat-panel-playground {
cursor: pointer;
padding: 2px;
margin-left: 8px;
margin-right: auto;
display: flex;
justify-content: center;
align-items: center;
}
.chat-panel-playground:hover svg {
color: ${unsafeCSSVarV2('icon/activated')};
}
`; `;
private readonly _chatMessagesRef: Ref<ChatPanelMessages> =
createRef<ChatPanelMessages>();
// request counter to track the latest request
private _updateHistoryCounter = 0;
private _wheelTriggered = false;
private readonly _updateHistory = async () => {
const { doc } = this;
const currentRequest = ++this._updateHistoryCounter;
const [histories, actions] = await Promise.all([
AIProvider.histories?.chats(doc.workspace.id, doc.id),
AIProvider.histories?.actions(doc.workspace.id, doc.id),
]);
// Check if this is still the latest request
if (currentRequest !== this._updateHistoryCounter) {
return;
}
const chatActions = (actions || []) as ChatAction[];
const messages: HistoryMessage[] = chatActions;
const sessionId = await this._getSessionId();
const history = histories?.find(history => history.sessionId === sessionId);
if (history) {
const chatMessages = (history.messages || []) as ChatMessage[];
messages.push(...chatMessages);
}
this.chatContextValue = {
...this.chatContextValue,
messages: messages.sort(
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
),
};
this._scrollToEnd();
};
private readonly _updateEmbeddingProgress = (
count: Record<ContextEmbedStatus, number>
) => {
const total = count.finished + count.processing + count.failed;
this.embeddingProgress = [count.finished, total];
};
private readonly _getSessionId = async () => {
if (this.session) {
return this.session.id;
}
const sessions = (
(await AIProvider.session?.getSessions(
this.doc.workspace.id,
this.doc.id,
{ action: false }
)) || []
).filter(session => !session.parentSessionId);
this.session = sessions.at(-1);
return this.session?.id;
};
private readonly _createSessionId = async () => {
if (this.session) {
return this.session.id;
}
const sessionId = await AIProvider.session?.createSession({
docId: this.doc.id,
workspaceId: this.doc.workspace.id,
promptName: 'Chat With AFFiNE AI',
});
if (sessionId) {
this.session = await AIProvider.session?.getSession(
this.doc.workspace.id,
sessionId
);
}
return sessionId;
};
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host!: EditorHost;
@property({ attribute: false }) @property({ attribute: false })
accessor doc!: Store; accessor doc!: Store;
@property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig;
@property({ attribute: false })
accessor reasoningConfig!: AIReasoningConfig;
@property({ attribute: false }) @property({ attribute: false })
accessor playgroundConfig!: AIPlaygroundConfig; accessor playgroundConfig!: AIPlaygroundConfig;
@property({ attribute: false }) @property({ attribute: false })
accessor appSidebarConfig!: AppSidebarConfig; accessor appSidebarConfig!: AppSidebarConfig;
@property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig;
@property({ attribute: false })
accessor reasoningConfig!: AIReasoningConfig;
@property({ attribute: false }) @property({ attribute: false })
accessor searchMenuConfig!: SearchMenuConfig; accessor searchMenuConfig!: SearchMenuConfig;
@@ -246,56 +94,74 @@ export class ChatPanel extends SignalWatcher(
accessor affineWorkspaceDialogService!: WorkspaceDialogService; accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@state() @state()
accessor isLoading = false; accessor session: CopilotSessionType | null | undefined;
@state()
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
@state() @state()
accessor embeddingProgress: [number, number] = [0, 0]; accessor embeddingProgress: [number, number] = [0, 0];
@state() private isSidebarOpen: Signal<boolean | undefined> = signal(false);
accessor session: CopilotSessionType | undefined = undefined;
private _isInitialized = false; private sidebarWidth: Signal<number | undefined> = signal(undefined);
private _isSidebarOpen: Signal<boolean | undefined> = signal(false); private readonly initSession = async () => {
if (this.session) {
private _sidebarWidth: Signal<number | undefined> = signal(undefined); return this.session;
private readonly _scrollToEnd = () => {
if (!this._wheelTriggered) {
this._chatMessagesRef.value?.scrollToEnd();
} }
const sessions = (
(await AIProvider.session?.getSessions(
this.doc.workspace.id,
this.doc.id,
{ action: false }
)) || []
).filter(session => !session.parentSessionId);
const session = sessions.at(-1);
this.session = session ?? null;
return session;
}; };
private readonly _throttledScrollToEnd = throttle(this._scrollToEnd, 600); private readonly createSession = async () => {
if (this.session) {
return this.session;
}
const sessionId = await AIProvider.session?.createSession({
docId: this.doc.id,
workspaceId: this.doc.workspace.id,
promptName: 'Chat With AFFiNE AI',
});
if (sessionId) {
const session = await AIProvider.session?.getSession(
this.doc.workspace.id,
sessionId
);
this.session = session ?? null;
}
return this.session;
};
private readonly _initPanel = async () => { private readonly initPanel = async () => {
try { try {
if (!this._isSidebarOpen.value) return; if (!this.isSidebarOpen.value) {
if (this.isLoading) return; return;
const userId = (await AIProvider.userInfo)?.id; }
if (!userId) return; await this.initSession();
this.isLoading = true;
await this._updateHistory();
this.isLoading = false;
this._isInitialized = true;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
}; };
private readonly _resetPanel = () => { private readonly resetPanel = () => {
this.session = undefined; this.session = undefined;
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
this.isLoading = false;
this._isInitialized = false;
this.embeddingProgress = [0, 0]; this.embeddingProgress = [0, 0];
}; };
private readonly _openPlayground = () => { private readonly updateEmbeddingProgress = (
count: Record<ContextEmbedStatus, number>
) => {
const total = count.finished + count.processing + count.failed;
this.embeddingProgress = [count.finished, total];
};
private readonly openPlayground = () => {
const playgroundContent = html` const playgroundContent = html`
<playground-content <playground-content
.host=${this.host} .host=${this.host}
@@ -314,48 +180,10 @@ export class ChatPanel extends SignalWatcher(
createPlaygroundModal(playgroundContent, 'AI Playground'); createPlaygroundModal(playgroundContent, 'AI Playground');
}; };
protected override willUpdate(_changedProperties: PropertyValues) { protected override updated(changedProperties: PropertyValues) {
if (_changedProperties.has('doc')) { if (changedProperties.has('doc')) {
this._resetPanel(); this.resetPanel();
requestAnimationFrame(async () => { this.initPanel().catch(console.error);
await this._initPanel();
});
}
}
protected override updated(_changedProperties: PropertyValues) {
if (this.chatContextValue.status === 'loading') {
// reset the wheel triggered flag when the status is loading
this._wheelTriggered = false;
}
if (
_changedProperties.has('chatContextValue') &&
(this.chatContextValue.status === 'loading' ||
this.chatContextValue.status === 'error' ||
this.chatContextValue.status === 'success')
) {
setTimeout(this._scrollToEnd, 500);
}
if (
_changedProperties.has('chatContextValue') &&
this.chatContextValue.status === 'transmitting'
) {
this._throttledScrollToEnd();
}
}
protected override firstUpdated(): void {
const chatMessages = this._chatMessagesRef.value;
if (chatMessages) {
chatMessages.updateComplete
.then(() => {
chatMessages.getScrollContainer()?.addEventListener('wheel', () => {
this._wheelTriggered = true;
});
})
.catch(console.error);
} }
} }
@@ -363,135 +191,80 @@ export class ChatPanel extends SignalWatcher(
super.connectedCallback(); super.connectedCallback();
if (!this.doc) throw new Error('doc is required'); if (!this.doc) throw new Error('doc is required');
this._disposables.add(
AIProvider.slots.actions.subscribe(({ event }) => {
const { status } = this.chatContextValue;
if (
event === 'finished' &&
(status === 'idle' || status === 'success')
) {
this._updateHistory().catch(console.error);
}
})
);
this._disposables.add( this._disposables.add(
AIProvider.slots.userInfo.subscribe(() => { AIProvider.slots.userInfo.subscribe(() => {
this._initPanel().catch(console.error); this.resetPanel();
}) this.initPanel().catch(console.error);
);
this._disposables.add(
AIProvider.slots.requestOpenWithChat.subscribe(({ host }) => {
if (this.host === host) {
extractSelectedContent(host)
.then(context => {
if (!context) return;
this.updateContext(context);
})
.catch(console.error);
}
}) })
); );
const isOpen = this.appSidebarConfig.isOpen(); const isOpen = this.appSidebarConfig.isOpen();
this._isSidebarOpen = 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._sidebarWidth = width.signal; this.sidebarWidth = width.signal;
this._disposables.add(width.cleanup); this._disposables.add(width.cleanup);
this._disposables.add( this._disposables.add(
this._isSidebarOpen.subscribe(isOpen => { this.isSidebarOpen.subscribe(() => {
if (isOpen && !this._isInitialized) { if (this.session === undefined) {
this._initPanel().catch(console.error); this.initPanel().catch(console.error);
} }
}) })
); );
} }
updateContext = (context: Partial<ChatContextValue>) => {
this.chatContextValue = { ...this.chatContextValue, ...context };
};
continueInChat = async () => {
const text = await getSelectedTextContent(this.host, 'plain-text');
const markdown = await getSelectedTextContent(this.host, 'markdown');
const images = await getSelectedImagesAsBlobs(this.host);
this.updateContext({
quote: text,
markdown,
images,
});
};
override render() { override render() {
const width = this._sidebarWidth.value || 0; const isInitialized = this.session !== undefined;
if (!isInitialized) {
return nothing;
}
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',
}); });
const [done, total] = this.embeddingProgress; const [done, total] = this.embeddingProgress;
const isEmbedding = total > 0 && done < total; const isEmbedding = total > 0 && done < total;
const title = html`
<div class="chat-panel-title-text">
${isEmbedding
? html`<span data-testid="chat-panel-embedding-progress"
>Embedding ${done}/${total}</span
>`
: 'AFFiNE AI'}
</div>
${this.playgroundConfig.visible.value
? html`
<div class="chat-panel-playground" @click=${this.openPlayground}>
${CenterPeekIcon()}
</div>
`
: nothing}
`;
return html`<div class="chat-panel-container" style=${style}> return html`<div class="chat-panel-container" style=${style}>
<div class="chat-panel-title"> ${keyed(
<div class="chat-panel-title-text"> this.doc.id,
${isEmbedding html`<ai-chat-content
? html`<span data-testid="chat-panel-embedding-progress" .chatTitle=${title}
>Embedding ${done}/${total}</span
>`
: 'AFFiNE AI'}
</div>
${this.playgroundConfig.visible.value
? html`
<div class="chat-panel-playground" @click=${this._openPlayground}>
${CenterPeekIcon()}
</div>
`
: nothing}
<ai-history-clear
.host=${this.host} .host=${this.host}
.doc=${this.doc} .session=${this.session}
.getSessionId=${this._getSessionId} .createSession=${this.createSession}
.onHistoryCleared=${this._updateHistory} .workspaceId=${this.doc.workspace.id}
.chatContextValue=${this.chatContextValue} .docId=${this.doc.id}
></ai-history-clear> .networkSearchConfig=${this.networkSearchConfig}
</div> .reasoningConfig=${this.reasoningConfig}
<chat-panel-messages .searchMenuConfig=${this.searchMenuConfig}
${ref(this._chatMessagesRef)} .docDisplayConfig=${this.docDisplayConfig}
.chatContextValue=${this.chatContextValue} .extensions=${this.extensions}
.getSessionId=${this._getSessionId} .affineFeatureFlagService=${this.affineFeatureFlagService}
.createSessionId=${this._createSessionId} .affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.updateContext=${this.updateContext} .updateEmbeddingProgress=${this.updateEmbeddingProgress}
.host=${this.host} .width=${this.sidebarWidth}
.isLoading=${this.isLoading} ></ai-chat-content>`
.extensions=${this.extensions} )}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.panelWidth=${this._sidebarWidth}
></chat-panel-messages>
<ai-chat-composer
.host=${this.host}
.doc=${this.doc}
.session=${this.session}
.getSessionId=${this._getSessionId}
.createSessionId=${this._createSessionId}
.chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext}
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
.isVisible=${this._isSidebarOpen}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.playgroundConfig=${this.playgroundConfig}
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.trackOptions=${{
where: 'chat-panel',
control: 'chat-send',
}}
.panelWidth=${this._sidebarWidth}
></ai-chat-composer>
</div>`; </div>`;
} }
} }

View File

@@ -1,4 +1,5 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { CopilotSessionType } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils'; import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
@@ -53,7 +54,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
accessor affineFeatureFlagService!: FeatureFlagService; accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false }) @property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>; accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor retry!: () => void; accessor retry!: () => void;
@@ -62,7 +63,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
accessor testId = 'chat-message-assistant'; accessor testId = 'chat-message-assistant';
@property({ attribute: false }) @property({ attribute: false })
accessor panelWidth!: Signal<number | undefined>; accessor width: Signal<number | undefined> | undefined;
get state() { get state() {
const { isLast, status } = this; const { isLast, status } = this;
@@ -117,7 +118,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
.answer=${answer} .answer=${answer}
.host=${this.host} .host=${this.host}
.state=${this.state} .state=${this.state}
.width=${this.panelWidth} .width=${this.width}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
></chat-content-stream-objects>`; ></chat-content-stream-objects>`;
@@ -134,7 +135,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
} }
private renderEditorActions() { private renderEditorActions() {
const { item, isLast, status } = this; const { item, isLast, status, host, session } = this;
if (!isChatMessage(item) || item.role !== 'assistant') return nothing; if (!isChatMessage(item) || item.role !== 'assistant') return nothing;
@@ -146,7 +147,6 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
) )
return nothing; return nothing;
const { host } = this;
const { content, streamObjects, id: messageId } = item; const { content, streamObjects, id: messageId } = item;
const markdown = streamObjects?.length const markdown = streamObjects?.length
? mergeStreamContent(streamObjects) ? mergeStreamContent(streamObjects)
@@ -159,10 +159,10 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
return html` return html`
<chat-copy-more <chat-copy-more
.host=${host} .host=${host}
.session=${session}
.actions=${actions} .actions=${actions}
.content=${markdown} .content=${markdown}
.isLast=${isLast} .isLast=${isLast}
.getSessionId=${this.getSessionId}
.messageId=${messageId} .messageId=${messageId}
.withMargin=${true} .withMargin=${true}
.retry=${() => this.retry()} .retry=${() => this.retry()}
@@ -171,8 +171,8 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
? html`<chat-action-list ? html`<chat-action-list
.actions=${actions} .actions=${actions}
.host=${host} .host=${host}
.session=${session}
.content=${markdown} .content=${markdown}
.getSessionId=${this.getSessionId}
.messageId=${messageId ?? undefined} .messageId=${messageId ?? undefined}
.withMargin=${true} .withMargin=${true}
></chat-action-list>` ></chat-action-list>`

View File

@@ -12,9 +12,7 @@ import type {
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
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 { css, html } from 'lit';
import { type Signal, signal } from '@preact/signals-core';
import { css, html, type PropertyValues } from 'lit';
import { property, state } from 'lit/decorators.js'; import { property, state } from 'lit/decorators.js';
import { AIProvider } from '../../provider'; import { AIProvider } from '../../provider';
@@ -56,16 +54,13 @@ export class AIChatComposer extends SignalWatcher(
accessor host!: EditorHost; accessor host!: EditorHost;
@property({ attribute: false }) @property({ attribute: false })
accessor doc!: Store; accessor workspaceId!: string;
@property({ attribute: false }) @property({ attribute: false })
accessor session!: CopilotSessionType | undefined; accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>; accessor createSession!: () => Promise<CopilotSessionType | undefined>;
@property({ attribute: false })
accessor createSessionId!: () => Promise<string | undefined>;
@property({ attribute: false }) @property({ attribute: false })
accessor chatContextValue!: AIChatInputContext; accessor chatContextValue!: AIChatInputContext;
@@ -73,9 +68,6 @@ export class AIChatComposer extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor updateContext!: (context: Partial<AIChatInputContext>) => void; accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
@property({ attribute: false })
accessor isVisible: Signal<boolean | undefined> = signal(false);
@property({ attribute: false }) @property({ attribute: false })
accessor updateEmbeddingProgress!: ( accessor updateEmbeddingProgress!: (
count: Record<ContextEmbedStatus, number> count: Record<ContextEmbedStatus, number>
@@ -102,9 +94,6 @@ export class AIChatComposer extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor portalContainer: HTMLElement | null = null; accessor portalContainer: HTMLElement | null = null;
@property({ attribute: false })
accessor panelWidth: Signal<number | undefined> = signal(undefined);
@property({ attribute: false }) @property({ attribute: false })
accessor affineWorkspaceDialogService!: WorkspaceDialogService; accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@@ -114,10 +103,6 @@ export class AIChatComposer extends SignalWatcher(
@state() @state()
accessor embeddingCompleted = false; accessor embeddingCompleted = false;
private _isInitialized = false;
private _isLoading = false;
private _contextId: string | undefined = undefined; private _contextId: string | undefined = undefined;
private _pollAbortController: AbortController | null = null; private _pollAbortController: AbortController | null = null;
@@ -141,9 +126,7 @@ export class AIChatComposer extends SignalWatcher(
.host=${this.host} .host=${this.host}
.chips=${this.chips} .chips=${this.chips}
.session=${this.session} .session=${this.session}
.getSessionId=${this.getSessionId} .createSession=${this.createSession}
.createSessionId=${this.createSessionId}
.getContextId=${this._getContextId}
.chatContextValue=${this.chatContextValue} .chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext} .updateContext=${this.updateContext}
.networkSearchConfig=${this.networkSearchConfig} .networkSearchConfig=${this.networkSearchConfig}
@@ -151,7 +134,6 @@ export class AIChatComposer extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig} .docDisplayConfig=${this.docDisplayConfig}
.onChatSuccess=${this.onChatSuccess} .onChatSuccess=${this.onChatSuccess}
.trackOptions=${this.trackOptions} .trackOptions=${this.trackOptions}
.panelWidth=${this.panelWidth}
.addImages=${this.addImages} .addImages=${this.addImages}
></ai-chat-input> ></ai-chat-input>
<div class="chat-panel-footer"> <div class="chat-panel-footer">
@@ -173,25 +155,7 @@ export class AIChatComposer extends SignalWatcher(
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (!this.doc) throw new Error('doc is required'); this._initComposer().catch(console.error);
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);
}
if (!isVisible) {
this._abortPoll();
this._abortPollEmbeddingStatus();
}
})
);
} }
override disconnectedCallback() { override disconnectedCallback() {
@@ -200,25 +164,16 @@ export class AIChatComposer extends SignalWatcher(
this._abortPollEmbeddingStatus(); this._abortPollEmbeddingStatus();
} }
protected override willUpdate(_changedProperties: PropertyValues) {
if (_changedProperties.has('doc')) {
this._resetComposer();
requestAnimationFrame(async () => {
await this._initComposer();
});
}
}
private readonly _getContextId = async () => { private readonly _getContextId = async () => {
if (this._contextId) { if (this._contextId) {
return this._contextId; return this._contextId;
} }
const sessionId = await this.getSessionId(); const sessionId = this.session?.id;
if (!sessionId) return; if (!sessionId) return;
const contextId = await AIProvider.context?.getContextId( const contextId = await AIProvider.context?.getContextId(
this.doc.workspace.id, this.workspaceId,
sessionId sessionId
); );
this._contextId = contextId; this._contextId = contextId;
@@ -230,11 +185,11 @@ export class AIChatComposer extends SignalWatcher(
return this._contextId; return this._contextId;
} }
const sessionId = await this.createSessionId(); const sessionId = (await this.createSession())?.id;
if (!sessionId) return; if (!sessionId) return;
this._contextId = await AIProvider.context?.createContext( this._contextId = await AIProvider.context?.createContext(
this.doc.workspace.id, this.workspaceId,
sessionId sessionId
); );
return this._contextId; return this._contextId;
@@ -242,7 +197,7 @@ export class AIChatComposer extends SignalWatcher(
private readonly _initChips = async () => { private readonly _initChips = async () => {
// context not initialized // context not initialized
const sessionId = await this.getSessionId(); const sessionId = this.session?.id;
const contextId = await this._getContextId(); const contextId = await this._getContextId();
if (!sessionId || !contextId) { if (!sessionId || !contextId) {
return; return;
@@ -255,7 +210,7 @@ export class AIChatComposer extends SignalWatcher(
tags = [], tags = [],
collections = [], collections = [],
} = (await AIProvider.context?.getContextDocsAndFiles( } = (await AIProvider.context?.getContextDocsAndFiles(
this.doc.workspace.id, this.workspaceId,
sessionId, sessionId,
contextId contextId
)) || {}; )) || {};
@@ -319,7 +274,7 @@ export class AIChatComposer extends SignalWatcher(
}; };
private readonly _pollContextDocsAndFiles = async () => { private readonly _pollContextDocsAndFiles = async () => {
const sessionId = await this.getSessionId(); const sessionId = this.session?.id;
const contextId = await this._getContextId(); const contextId = await this._getContextId();
if (!sessionId || !contextId || !AIProvider.context) { if (!sessionId || !contextId || !AIProvider.context) {
return; return;
@@ -330,7 +285,7 @@ export class AIChatComposer extends SignalWatcher(
} }
this._pollAbortController = new AbortController(); this._pollAbortController = new AbortController();
await AIProvider.context.pollContextDocsAndFiles( await AIProvider.context.pollContextDocsAndFiles(
this.doc.workspace.id, this.workspaceId,
sessionId, sessionId,
contextId, contextId,
this._onPoll, this._onPoll,
@@ -436,13 +391,9 @@ export class AIChatComposer extends SignalWatcher(
}; };
private readonly _initComposer = async () => { private readonly _initComposer = async () => {
if (!this.isVisible.value) return;
if (this._isLoading) return;
const userId = (await AIProvider.userInfo)?.id; const userId = (await AIProvider.userInfo)?.id;
if (!userId) return; if (!userId || !this.session) return;
this._isLoading = true;
await this._initChips(); await this._initChips();
const needPoll = this.chips.some( const needPoll = this.chips.some(
chip => chip =>
@@ -452,16 +403,5 @@ export class AIChatComposer extends SignalWatcher(
await this._pollContextDocsAndFiles(); await this._pollContextDocsAndFiles();
} }
await this._pollEmbeddingStatus(); await this._pollEmbeddingStatus();
this._isLoading = false;
this._isInitialized = true;
};
private readonly _resetComposer = () => {
this._abortPoll();
this._abortPollEmbeddingStatus();
this.chips = [];
this._contextId = undefined;
this._isLoading = false;
this._isInitialized = false;
}; };
} }

View File

@@ -0,0 +1,321 @@
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
import type { ExtensionType } from '@blocksuite/affine/store';
import { type Signal } from '@preact/signals-core';
import { css, html, type PropertyValues, type TemplateResult } from 'lit';
import { property, state } from 'lit/decorators.js';
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
import { throttle } from 'lodash-es';
import { type AIChatParams, AIProvider } from '../../provider/ai-provider';
import { extractSelectedContent } from '../../utils/extract';
import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
import type {
AINetworkSearchConfig,
AIReasoningConfig,
} from '../ai-chat-input';
import {
type AIChatMessages,
type ChatAction,
type ChatMessage,
type HistoryMessage,
isChatMessage,
} from '../ai-chat-messages';
import type { ChatContextValue } from './type';
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
quote: '',
images: [],
abortController: null,
messages: [],
status: 'idle',
error: null,
markdown: '',
};
export class AIChatContent extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css`
ai-chat-content {
display: flex;
flex-direction: column;
height: 100%;
.ai-chat-title {
background: var(--affine-background-primary-color);
position: relative;
padding: 8px 0px;
width: 100%;
height: 36px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1;
svg {
width: 18px;
height: 18px;
color: var(--affine-text-secondary-color);
}
}
ai-chat-messages {
flex: 1;
overflow-y: hidden;
}
}
`;
@property({ attribute: false })
accessor chatTitle!: TemplateResult<1>;
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false })
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
@property({ attribute: false })
accessor workspaceId!: string;
@property({ attribute: false })
accessor docId: string | undefined;
@property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig;
@property({ attribute: false })
accessor reasoningConfig!: AIReasoningConfig;
@property({ attribute: false })
accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false })
accessor extensions!: ExtensionType[];
@property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@property({ attribute: false })
accessor updateEmbeddingProgress!: (
count: Record<ContextEmbedStatus, number>
) => void;
@property({ attribute: false })
accessor width: Signal<number | undefined> | undefined;
@state()
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
@state()
accessor isHistoryLoading = false;
private readonly chatMessagesRef: Ref<AIChatMessages> =
createRef<AIChatMessages>();
// request counter to track the latest request
private updateHistoryCounter = 0;
private wheelTriggered = false;
private readonly updateHistory = async () => {
const currentRequest = ++this.updateHistoryCounter;
if (!AIProvider.histories) {
return;
}
const sessionId = this.session?.id;
const [histories, actions] = await Promise.all([
sessionId
? AIProvider.histories.chats(this.workspaceId, sessionId, this.docId)
: Promise.resolve([]),
this.docId
? AIProvider.histories.actions(this.workspaceId, this.docId)
: Promise.resolve([]),
]);
// Check if this is still the latest request
if (currentRequest !== this.updateHistoryCounter) {
return;
}
const messages: HistoryMessage[] = this.chatContextValue.messages.slice();
const chatActions = (actions || []) as ChatAction[];
messages.push(...chatActions);
const chatMessages = (histories?.[0]?.messages || []) as ChatMessage[];
messages.push(...chatMessages);
this.updateContext({
messages: messages.sort(
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
),
});
this.scrollToEnd();
};
private readonly updateActions = async () => {
if (!this.docId || !AIProvider.histories) {
return;
}
const actions = await AIProvider.histories.actions(
this.workspaceId,
this.docId
);
if (actions && actions.length) {
const chatMessages = this.chatContextValue.messages.filter(message =>
isChatMessage(message)
);
const chatActions = actions as ChatAction[];
const messages: HistoryMessage[] = [...chatMessages, ...chatActions];
this.updateContext({
messages: messages.sort(
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
),
});
}
};
private readonly updateContext = (context: Partial<ChatContextValue>) => {
this.chatContextValue = { ...this.chatContextValue, ...context };
};
private readonly scrollToEnd = () => {
if (!this.wheelTriggered) {
this.chatMessagesRef.value?.scrollToEnd();
}
};
private readonly _throttledScrollToEnd = throttle(this.scrollToEnd, 600);
private readonly initChatContent = async () => {
this.isHistoryLoading = true;
await this.updateHistory();
this.isHistoryLoading = false;
};
protected override firstUpdated(): void {
const chatMessages = this.chatMessagesRef.value;
if (chatMessages) {
chatMessages.updateComplete
.then(() => {
chatMessages.getScrollContainer()?.addEventListener('wheel', () => {
this.wheelTriggered = true;
});
})
.catch(console.error);
}
}
protected override updated(changedProperties: PropertyValues) {
if (this.chatContextValue.status === 'loading') {
// reset the wheel triggered flag when the status is loading
this.wheelTriggered = false;
}
if (
changedProperties.has('chatContextValue') &&
(this.chatContextValue.status === 'loading' ||
this.chatContextValue.status === 'error' ||
this.chatContextValue.status === 'success')
) {
setTimeout(this.scrollToEnd, 500);
}
if (
changedProperties.has('chatContextValue') &&
this.chatContextValue.status === 'transmitting'
) {
this._throttledScrollToEnd();
}
}
override connectedCallback() {
super.connectedCallback();
this.initChatContent().catch(console.error);
this._disposables.add(
AIProvider.slots.actions.subscribe(({ event }) => {
const { status } = this.chatContextValue;
if (
event === 'finished' &&
(status === 'idle' || status === 'success')
) {
this.updateActions().catch(console.error);
}
})
);
this._disposables.add(
AIProvider.slots.requestOpenWithChat.subscribe(
(params: AIChatParams | null) => {
if (!params) {
return;
}
if (this.host === params.host) {
extractSelectedContent(params.host)
.then(context => {
if (!context) return;
this.updateContext(context);
})
.catch(console.error);
}
}
)
);
}
override render() {
return html` <div class="ai-chat-title">${this.chatTitle}</div>
<ai-chat-messages
${ref(this.chatMessagesRef)}
.host=${this.host}
.session=${this.session}
.createSession=${this.createSession}
.chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext}
.isHistoryLoading=${this.isHistoryLoading}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.width=${this.width}
></ai-chat-messages>
<ai-chat-composer
.host=${this.host}
.workspaceId=${this.workspaceId}
.session=${this.session}
.createSession=${this.createSession}
.chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext}
.updateEmbeddingProgress=${this.updateEmbeddingProgress}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.trackOptions=${{
where: 'chat-panel',
control: 'chat-send',
}}
></ai-chat-composer>`;
}
}

View File

@@ -0,0 +1,2 @@
export * from './ai-chat-content';
export * from './type';

View File

@@ -1,8 +1,5 @@
import type { import type { AIError } from '../../provider';
ChatStatus, import type { ChatStatus, HistoryMessage } from '../ai-chat-messages';
HistoryMessage,
} from '../components/ai-chat-messages';
import type { AIError } from '../provider';
export type ChatContextValue = { export type ChatContextValue = {
// history messages of the chat // history messages of the chat

View File

@@ -6,14 +6,13 @@ import { openFilesWith } 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 { ArrowUpBigIcon, CloseIcon, ImageIcon } from '@blocksuite/icons/lit'; import { ArrowUpBigIcon, CloseIcon, ImageIcon } from '@blocksuite/icons/lit';
import { type Signal, signal } from '@preact/signals-core';
import { css, html, nothing } from 'lit'; import { css, html, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js'; import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { ChatAbortIcon } from '../../_common/icons'; import { ChatAbortIcon } from '../../_common/icons';
import { type AIError, AIProvider } from '../../provider'; import { type AIError, AIProvider, type AISendParams } from '../../provider';
import { reportResponse } from '../../utils/action-reporter'; import { reportResponse } from '../../utils/action-reporter';
import { readBlobAsURL } from '../../utils/image'; import { readBlobAsURL } from '../../utils/image';
import { mergeStreamObjects } from '../../utils/stream-objects'; import { mergeStreamObjects } from '../../utils/stream-objects';
@@ -285,7 +284,7 @@ export class AIChatInput extends SignalWatcher(
accessor host!: EditorHost; accessor host!: EditorHost;
@property({ attribute: false }) @property({ attribute: false })
accessor session!: CopilotSessionType | undefined; accessor session!: CopilotSessionType | null | undefined;
@query('image-preview-grid') @query('image-preview-grid')
accessor imagePreviewGrid: HTMLDivElement | null = null; accessor imagePreviewGrid: HTMLDivElement | null = null;
@@ -309,13 +308,7 @@ export class AIChatInput extends SignalWatcher(
accessor chips: ChatChip[] = []; accessor chips: ChatChip[] = [];
@property({ attribute: false }) @property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>; accessor createSession!: () => Promise<CopilotSessionType | undefined>;
@property({ attribute: false })
accessor createSessionId!: () => Promise<string | undefined>;
@property({ attribute: false })
accessor getContextId!: () => Promise<string | undefined>;
@property({ attribute: false }) @property({ attribute: false })
accessor updateContext!: (context: Partial<AIChatInputContext>) => void; accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
@@ -341,9 +334,6 @@ export class AIChatInput extends SignalWatcher(
@property({ attribute: 'data-testid', reflect: true }) @property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-panel-input-container'; accessor testId = 'chat-panel-input-container';
@property({ attribute: false })
accessor panelWidth: Signal<number | undefined> = signal(undefined);
@property({ attribute: false }) @property({ attribute: false })
accessor addImages!: (images: File[]) => void; accessor addImages!: (images: File[]) => void;
@@ -366,9 +356,15 @@ export class AIChatInput extends SignalWatcher(
super.connectedCallback(); super.connectedCallback();
this._disposables.add( this._disposables.add(
AIProvider.slots.requestSendWithChat.subscribe( AIProvider.slots.requestSendWithChat.subscribe(
({ input, context, host }) => { (params: AISendParams | null) => {
if (!params) {
return;
}
const { input, context, host } = params;
if (this.host === host) { if (this.host === host) {
context && this.updateContext(context); if (context) {
this.updateContext(context);
}
setTimeout(() => { setTimeout(() => {
this.send(input).catch(console.error); this.send(input).catch(console.error);
}, 0); }, 0);
@@ -591,7 +587,7 @@ export class AIChatInput extends SignalWatcher(
// optimistic update messages // optimistic update messages
await this._preUpdateMessages(userInput, attachments); await this._preUpdateMessages(userInput, attachments);
const sessionId = await this.createSessionId(); const sessionId = (await this.createSession())?.id;
let contexts = await this._getMatchedContexts(); let contexts = await this._getMatchedContexts();
if (abortController.signal.aborted) { if (abortController.signal.aborted) {
return; return;
@@ -678,11 +674,13 @@ export class AIChatInput extends SignalWatcher(
}; };
private readonly _postUpdateMessages = async () => { private readonly _postUpdateMessages = async () => {
const sessionId = this.session?.id;
if (!sessionId || !AIProvider.histories) return;
const { messages } = this.chatContextValue; const { messages } = this.chatContextValue;
const last = messages[messages.length - 1] as ChatMessage; const last = messages[messages.length - 1] as ChatMessage;
if (!last.id) { if (!last.id) {
const sessionId = await this.getSessionId(); const historyIds = await AIProvider.histories.ids(
const historyIds = await AIProvider.histories?.ids(
this.host.store.workspace.id, this.host.store.workspace.id,
this.host.store.id, this.host.store.id,
{ sessionId } { sessionId }

View File

@@ -50,7 +50,7 @@ export class ChatInputPreference extends SignalWatcher(
`; `;
@property({ attribute: false }) @property({ attribute: false })
accessor session!: CopilotSessionType | undefined; accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor onModelChange: ((modelId: string) => void) | undefined; accessor onModelChange: ((modelId: string) => void) | undefined;

View File

@@ -1,3 +1,4 @@
import type { CopilotSessionType } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { import {
DocModeProvider, DocModeProvider,
@@ -13,25 +14,21 @@ import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { AffineIcon } from '../_common/icons'; import { AffineIcon } from '../../_common/icons';
import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const';
import { AIPreloadConfig } from '../../chat-panel/preload-config';
import { type AIError, AIProvider, UnauthorizedError } from '../../provider';
import { mergeStreamObjects } from '../../utils/stream-objects';
import { type ChatContextValue } from '../ai-chat-content/type';
import type { import type {
AINetworkSearchConfig, AINetworkSearchConfig,
AIReasoningConfig, AIReasoningConfig,
} from '../components/ai-chat-input'; } from '../ai-chat-input';
import { import { isChatAction, isChatMessage, StreamObjectSchema } from './type';
isChatAction,
isChatMessage,
StreamObjectSchema,
} from '../components/ai-chat-messages';
import { type AIError, AIProvider, UnauthorizedError } from '../provider';
import { mergeStreamObjects } from '../utils/stream-objects';
import { type ChatContextValue } from './chat-context';
import { HISTORY_IMAGE_ACTIONS } from './const';
import { AIPreloadConfig } from './preload-config';
export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { export class AIChatMessages extends WithDisposable(ShadowlessElement) {
static override styles = css` static override styles = css`
chat-panel-messages { ai-chat-messages {
position: relative; position: relative;
} }
@@ -147,16 +144,16 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
accessor host!: EditorHost; accessor host!: EditorHost;
@property({ attribute: false }) @property({ attribute: false })
accessor isLoading!: boolean; accessor isHistoryLoading!: boolean;
@property({ attribute: false }) @property({ attribute: false })
accessor chatContextValue!: ChatContextValue; accessor chatContextValue!: ChatContextValue;
@property({ attribute: false }) @property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>; accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor createSessionId!: () => Promise<string | undefined>; accessor createSession!: () => Promise<CopilotSessionType | undefined>;
@property({ attribute: false }) @property({ attribute: false })
accessor updateContext!: (context: Partial<ChatContextValue>) => void; accessor updateContext!: (context: Partial<ChatContextValue>) => void;
@@ -174,7 +171,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
accessor reasoningConfig!: AIReasoningConfig; accessor reasoningConfig!: AIReasoningConfig;
@property({ attribute: false }) @property({ attribute: false })
accessor panelWidth!: Signal<number | undefined>; accessor width: Signal<number | undefined> | undefined;
@query('.chat-panel-messages-container') @query('.chat-panel-messages-container')
accessor messagesContainer: HTMLDivElement | null = null; accessor messagesContainer: HTMLDivElement | null = null;
@@ -202,7 +199,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
} }
private _renderAIOnboarding() { private _renderAIOnboarding() {
return this.isLoading || return this.isHistoryLoading ||
!this.host?.store.get(FeatureFlagService).getFlag('enable_ai_onboarding') !this.host?.store.get(FeatureFlagService).getFlag('enable_ai_onboarding')
? nothing ? nothing
: html`<div class="onboarding-wrapper" data-testid="ai-onboarding"> : html`<div class="onboarding-wrapper" data-testid="ai-onboarding">
@@ -241,7 +238,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
protected override render() { protected override render() {
const { messages, status, error } = this.chatContextValue; const { messages, status, error } = this.chatContextValue;
const { isLoading } = this; const { isHistoryLoading } = this;
const filteredItems = messages.filter(item => { const filteredItems = messages.filter(item => {
return ( return (
isChatMessage(item) || isChatMessage(item) ||
@@ -268,12 +265,15 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
data-testid="chat-panel-messages-placeholder" data-testid="chat-panel-messages-placeholder"
> >
${AffineIcon( ${AffineIcon(
isLoading isHistoryLoading
? 'var(--affine-icon-secondary)' ? 'var(--affine-icon-secondary)'
: 'var(--affine-primary-color)' : 'var(--affine-primary-color)'
)} )}
<div class="messages-placeholder-title" data-loading=${isLoading}> <div
${this.isLoading class="messages-placeholder-title"
data-loading=${isHistoryLoading}
>
${this.isHistoryLoading
? html`<span data-testid="chat-panel-loading-state" ? html`<span data-testid="chat-panel-loading-state"
>AFFiNE AI is loading history...</span >AFFiNE AI is loading history...</span
>` >`
@@ -295,15 +295,15 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
} else if (isChatMessage(item) && item.role === 'assistant') { } else if (isChatMessage(item) && item.role === 'assistant') {
return html`<chat-message-assistant return html`<chat-message-assistant
.host=${this.host} .host=${this.host}
.session=${this.session}
.item=${item} .item=${item}
.isLast=${isLast} .isLast=${isLast}
.status=${isLast ? status : 'idle'} .status=${isLast ? status : 'idle'}
.error=${isLast ? error : null} .error=${isLast ? error : null}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.getSessionId=${this.getSessionId}
.retry=${() => this.retry()} .retry=${() => this.retry()}
.panelWidth=${this.panelWidth} .width=${this.width}
></chat-message-assistant>`; ></chat-message-assistant>`;
} else if (isChatAction(item)) { } else if (isChatAction(item)) {
return html`<chat-message-action return html`<chat-message-action
@@ -365,7 +365,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
} }
protected override updated(_changedProperties: PropertyValues) { protected override updated(_changedProperties: PropertyValues) {
if (_changedProperties.has('isLoading')) { if (_changedProperties.has('isHistoryLoading')) {
this.canScrollDown = false; this.canScrollDown = false;
} }
} }
@@ -382,7 +382,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
retry = async () => { retry = async () => {
try { try {
const sessionId = await this.createSessionId(); const sessionId = (await this.createSession())?.id;
if (!sessionId) return; if (!sessionId) return;
if (!AIProvider.actions.chat) return; if (!AIProvider.actions.chat) return;
@@ -448,9 +448,3 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
} }
}; };
} }
declare global {
interface HTMLElementTagNameMap {
'chat-panel-messages': ChatPanelMessages;
}
}

View File

@@ -1 +1,2 @@
export * from './ai-chat-messages';
export * from './type'; export * from './type';

View File

@@ -1,3 +1,4 @@
import type { CopilotSessionType } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { NotificationProvider } from '@blocksuite/affine/shared/services'; import { NotificationProvider } from '@blocksuite/affine/shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
@@ -7,15 +8,15 @@ import type { Store } from '@blocksuite/affine/store';
import { css, html } from 'lit'; import { css, html } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import type { ChatContextValue } from '../../chat-panel/chat-context';
import { AIProvider } from '../../provider'; import { AIProvider } from '../../provider';
import type { ChatContextValue } from '../ai-chat-content';
export class AIHistoryClear extends WithDisposable(ShadowlessElement) { export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor chatContextValue!: ChatContextValue; accessor chatContextValue!: ChatContextValue;
@property({ attribute: false }) @property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>; accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host!: EditorHost;
@@ -41,15 +42,16 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
return ( return (
this.chatContextValue.status === 'loading' || this.chatContextValue.status === 'loading' ||
this.chatContextValue.status === 'transmitting' || this.chatContextValue.status === 'transmitting' ||
!this.chatContextValue.messages.length !this.chatContextValue.messages.length ||
!this.session
); );
} }
private readonly _cleanupHistories = async () => { private readonly _cleanupHistories = async () => {
if (this._isHistoryClearDisabled) { if (this._isHistoryClearDisabled || !this.session) {
return; return;
} }
const sessionId = await this.getSessionId(); const sessionId = this.session.id;
const notification = this.host.std.getOptional(NotificationProvider); const notification = this.host.std.getOptional(NotificationProvider);
if (!notification) return; if (!notification) return;
try { try {

View File

@@ -1,3 +1,4 @@
import type { CopilotSessionType } from '@affine/graphql';
import type { ImageSelection } from '@blocksuite/affine/shared/selection'; import type { ImageSelection } from '@blocksuite/affine/shared/selection';
import { NotificationProvider } from '@blocksuite/affine/shared/services'; import { NotificationProvider } from '@blocksuite/affine/shared/services';
import type { import type {
@@ -81,7 +82,7 @@ export class ChatActionList extends LitElement {
accessor content: string = ''; accessor content: string = '';
@property({ attribute: false }) @property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>; accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor messageId: string | undefined = undefined; accessor messageId: string | undefined = undefined;
@@ -138,7 +139,7 @@ export class ChatActionList extends LitElement {
blocks: this._currentBlockSelections, blocks: this._currentBlockSelections,
images: this._currentImageSelections, images: this._currentImageSelections,
}; };
const sessionId = await this.getSessionId(); const sessionId = this.session?.id;
const success = await action.handler( const success = await action.handler(
host, host,
content, content,

View File

@@ -1,3 +1,4 @@
import type { CopilotSessionType } from '@affine/graphql';
import { Tooltip } from '@blocksuite/affine/components/toolbar'; import { Tooltip } from '@blocksuite/affine/components/toolbar';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { noop } from '@blocksuite/affine/global/utils'; import { noop } from '@blocksuite/affine/global/utils';
@@ -110,10 +111,10 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
accessor actions: ChatAction[] = []; accessor actions: ChatAction[] = [];
@property({ attribute: false }) @property({ attribute: false })
accessor content!: string; accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>; accessor content!: string;
@property({ attribute: false }) @property({ attribute: false })
accessor messageId: string | undefined = undefined; accessor messageId: string | undefined = undefined;
@@ -221,7 +222,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
}; };
return html`<div return html`<div
@click=${async () => { @click=${async () => {
const sessionId = await this.getSessionId(); const sessionId = this.session?.id;
const success = await action.handler( const success = await action.handler(
host, host,
content, content,

View File

@@ -6,23 +6,22 @@ import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std';
import type { ExtensionType, Store } from '@blocksuite/affine/store'; import type { ExtensionType, Store } from '@blocksuite/affine/store';
import { DeleteIcon, NewPageIcon } from '@blocksuite/icons/lit'; import { DeleteIcon, NewPageIcon } from '@blocksuite/icons/lit';
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';
import { createRef, type Ref, ref } from 'lit/directives/ref.js'; import { createRef, type Ref, ref } from 'lit/directives/ref.js';
import { throttle } from 'lodash-es'; import { throttle } from 'lodash-es';
import type { AppSidebarConfig } from '../../chat-panel/chat-config'; import type { AppSidebarConfig } from '../../chat-panel/chat-config';
import type { ChatContextValue } from '../../chat-panel/chat-context';
import type { ChatPanelMessages } from '../../chat-panel/chat-panel-messages';
import { AIProvider } from '../../provider'; import { AIProvider } from '../../provider';
import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips'; import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
import type { ChatContextValue } from '../ai-chat-content';
import type { import type {
AINetworkSearchConfig, AINetworkSearchConfig,
AIPlaygroundConfig, AIPlaygroundConfig,
AIReasoningConfig, AIReasoningConfig,
} from '../ai-chat-input'; } from '../ai-chat-input';
import { import {
type AIChatMessages,
type ChatAction, type ChatAction,
type ChatMessage, type ChatMessage,
type HistoryMessage, type HistoryMessage,
@@ -74,7 +73,7 @@ export class PlaygroundChat extends SignalWatcher(
} }
} }
chat-panel-messages { ai-chat-messages {
flex: 1; flex: 1;
overflow-y: hidden; overflow-y: hidden;
} }
@@ -129,6 +128,9 @@ export class PlaygroundChat extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor doc!: Store; accessor doc!: Store;
@property({ attribute: false })
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig; accessor networkSearchConfig!: AINetworkSearchConfig;
@@ -153,9 +155,6 @@ export class PlaygroundChat extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService; accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor session: CopilotSessionType | undefined = undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor addChat!: () => Promise<void>; accessor addChat!: () => Promise<void>;
@@ -168,10 +167,8 @@ export class PlaygroundChat extends SignalWatcher(
@state() @state()
accessor embeddingProgress: [number, number] = [0, 0]; accessor embeddingProgress: [number, number] = [0, 0];
private readonly _isVisible: Signal<boolean | undefined> = signal(true); private readonly _chatMessagesRef: Ref<AIChatMessages> =
createRef<AIChatMessages>();
private readonly _chatMessagesRef: Ref<ChatPanelMessages> =
createRef<ChatPanelMessages>();
// request counter to track the latest request // request counter to track the latest request
private _updateHistoryCounter = 0; private _updateHistoryCounter = 0;
@@ -185,22 +182,29 @@ export class PlaygroundChat extends SignalWatcher(
this.isLoading = false; this.isLoading = false;
}; };
private readonly _getSessionId = async () => { private readonly _createSession = async () => {
return this.session?.id; return this.session;
};
private readonly _createSessionId = async () => {
return this.session?.id;
}; };
private readonly _updateHistory = async () => { private readonly _updateHistory = async () => {
const { doc } = this; if (!AIProvider.histories) {
return;
}
const currentRequest = ++this._updateHistoryCounter; const currentRequest = ++this._updateHistoryCounter;
const sessionId = this.session?.id;
const [histories, actions] = await Promise.all([ const [histories, actions] = await Promise.all([
AIProvider.histories?.chats(doc.workspace.id, doc.id), sessionId
AIProvider.histories?.actions(doc.workspace.id, doc.id), ? AIProvider.histories.chats(
this.doc.workspace.id,
sessionId,
this.doc.id
)
: Promise.resolve([]),
this.doc.id
? AIProvider.histories.actions(this.doc.workspace.id, this.doc.id)
: Promise.resolve([]),
]); ]);
// Check if this is still the latest request // Check if this is still the latest request
@@ -211,12 +215,8 @@ export class PlaygroundChat extends SignalWatcher(
const chatActions = (actions || []) as ChatAction[]; const chatActions = (actions || []) as ChatAction[];
const messages: HistoryMessage[] = chatActions; const messages: HistoryMessage[] = chatActions;
const sessionId = await this._getSessionId(); const chatMessages = (histories?.[0]?.messages || []) as ChatMessage[];
const history = histories?.find(history => history.sessionId === sessionId); messages.push(...chatMessages);
if (history) {
const chatMessages = (history.messages || []) as ChatMessage[];
messages.push(...chatMessages);
}
this.chatContextValue = { this.chatContextValue = {
...this.chatContextValue, ...this.chatContextValue,
@@ -289,35 +289,33 @@ export class PlaygroundChat extends SignalWatcher(
<ai-history-clear <ai-history-clear
.host=${this.host} .host=${this.host}
.doc=${this.doc} .doc=${this.doc}
.getSessionId=${this._getSessionId} .session=${this.session}
.onHistoryCleared=${this._updateHistory} .onHistoryCleared=${this._updateHistory}
.chatContextValue=${this.chatContextValue} .chatContextValue=${this.chatContextValue}
></ai-history-clear> ></ai-history-clear>
<div class="chat-panel-delete">${DeleteIcon()}</div> <div class="chat-panel-delete">${DeleteIcon()}</div>
</div> </div>
<chat-panel-messages <ai-chat-messages
${ref(this._chatMessagesRef)} ${ref(this._chatMessagesRef)}
.chatContextValue=${this.chatContextValue}
.getSessionId=${this._getSessionId}
.createSessionId=${this._createSessionId}
.updateContext=${this.updateContext}
.host=${this.host} .host=${this.host}
.isLoading=${this.isLoading} .isHistoryLoading=${this.isLoading}
.chatContextValue=${this.chatContextValue}
.session=${this.session}
.createSession=${this._createSession}
.updateContext=${this.updateContext}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.networkSearchConfig=${this.networkSearchConfig} .networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig} .reasoningConfig=${this.reasoningConfig}
></chat-panel-messages> ></ai-chat-messages>
<ai-chat-composer <ai-chat-composer
.host=${this.host} .host=${this.host}
.doc=${this.doc} .workspaceId=${this.doc.workspace.id}
.session=${this.session} .session=${this.session}
.getSessionId=${this._getSessionId} .createSession=${this._createSession}
.createSessionId=${this._createSessionId}
.chatContextValue=${this.chatContextValue} .chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext} .updateContext=${this.updateContext}
.updateEmbeddingProgress=${this._updateEmbeddingProgress} .updateEmbeddingProgress=${this._updateEmbeddingProgress}
.isVisible=${this._isVisible}
.networkSearchConfig=${this.networkSearchConfig} .networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig} .reasoningConfig=${this.reasoningConfig}
.playgroundConfig=${this.playgroundConfig} .playgroundConfig=${this.playgroundConfig}

View File

@@ -327,6 +327,7 @@ export class PlaygroundContent extends SignalWatcher(
<playground-chat <playground-chat
.host=${this.host} .host=${this.host}
.doc=${this.doc} .doc=${this.doc}
.session=${session}
.networkSearchConfig=${this.networkSearchConfig} .networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig} .reasoningConfig=${this.reasoningConfig}
.playgroundConfig=${this.playgroundConfig} .playgroundConfig=${this.playgroundConfig}
@@ -335,7 +336,6 @@ export class PlaygroundContent extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig} .docDisplayConfig=${this.docDisplayConfig}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.session=${session}
.addChat=${this.addChat} .addChat=${this.addChat}
></playground-chat> ></playground-chat>
</div> </div>

View File

@@ -2,8 +2,8 @@ import { AIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-block';
import { EdgelessAIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-edgeless-block'; import { EdgelessAIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-edgeless-block';
import { LitTranscriptionBlock } from './blocks/ai-chat-block/ai-transcription-block'; import { LitTranscriptionBlock } from './blocks/ai-chat-block/ai-transcription-block';
import { import {
AIChatMessage, AIChatBlockMessage,
AIChatMessages, AIChatBlockMessages,
} from './blocks/ai-chat-block/components/ai-chat-messages'; } from './blocks/ai-chat-block/components/ai-chat-messages';
import { import {
ChatImage, ChatImage,
@@ -20,7 +20,6 @@ import { ActionMindmap } from './chat-panel/actions/mindmap';
import { ActionSlides } from './chat-panel/actions/slides'; import { ActionSlides } from './chat-panel/actions/slides';
import { ActionText } from './chat-panel/actions/text'; import { ActionText } from './chat-panel/actions/text';
import { AILoading } from './chat-panel/ai-loading'; import { AILoading } from './chat-panel/ai-loading';
import { ChatPanelMessages } from './chat-panel/chat-panel-messages';
import { ChatMessageAction } from './chat-panel/message/action'; import { ChatMessageAction } from './chat-panel/message/action';
import { ChatMessageAssistant } from './chat-panel/message/assistant'; import { ChatMessageAssistant } from './chat-panel/message/assistant';
import { ChatMessageUser } from './chat-panel/message/user'; import { ChatMessageUser } from './chat-panel/message/user';
@@ -33,9 +32,11 @@ 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 { AIChatComposer } from './components/ai-chat-composer';
import { AIChatContent } from './components/ai-chat-content';
import { AIChatInput } from './components/ai-chat-input'; import { AIChatInput } from './components/ai-chat-input';
import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embedding-status-tooltip'; import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embedding-status-tooltip';
import { ChatInputPreference } from './components/ai-chat-input/preference-popup'; import { ChatInputPreference } from './components/ai-chat-input/preference-popup';
import { AIChatMessages } from './components/ai-chat-messages/ai-chat-messages';
import { AIHistoryClear } from './components/ai-history-clear'; import { AIHistoryClear } from './components/ai-history-clear';
import { effects as componentAiItemEffects } from './components/ai-item'; import { effects as componentAiItemEffects } from './components/ai-item';
import { AssistantAvatar } from './components/ai-message-content/assistant-avatar'; import { AssistantAvatar } from './components/ai-message-content/assistant-avatar';
@@ -104,7 +105,8 @@ export function registerAIEffects() {
customElements.define('action-slides', ActionSlides); customElements.define('action-slides', ActionSlides);
customElements.define('action-text', ActionText); customElements.define('action-text', ActionText);
customElements.define('ai-loading', AILoading); customElements.define('ai-loading', AILoading);
customElements.define('chat-panel-messages', ChatPanelMessages); customElements.define('ai-chat-content', AIChatContent);
customElements.define('ai-chat-messages', AIChatMessages);
customElements.define('chat-panel', ChatPanel); customElements.define('chat-panel', ChatPanel);
customElements.define('ai-chat-input', AIChatInput); customElements.define('ai-chat-input', AIChatInput);
customElements.define( customElements.define(
@@ -135,8 +137,8 @@ export function registerAIEffects() {
EdgelessAIChatBlockComponent EdgelessAIChatBlockComponent
); );
customElements.define('affine-ai-chat', AIChatBlockComponent); customElements.define('affine-ai-chat', AIChatBlockComponent);
customElements.define('ai-chat-message', AIChatMessage); customElements.define('ai-chat-block-message', AIChatBlockMessage);
customElements.define('ai-chat-messages', AIChatMessages); customElements.define('ai-chat-block-messages', AIChatBlockMessages);
customElements.define( customElements.define(
'ai-scrollable-text-renderer', 'ai-scrollable-text-renderer',
AIScrollableTextRenderer AIScrollableTextRenderer

View File

@@ -1,6 +1,6 @@
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { ContextEmbedStatus } from '@affine/graphql'; import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
import { import {
CanvasElementType, CanvasElementType,
EdgelessCRUDIdentifier, EdgelessCRUDIdentifier,
@@ -12,9 +12,7 @@ import {
DocModeProvider, DocModeProvider,
TelemetryProvider, TelemetryProvider,
} from '@blocksuite/affine/shared/services'; } from '@blocksuite/affine/shared/services';
import type { Signal } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import { signal } from '@preact/signals-core';
import { html, LitElement, nothing, type PropertyValues } from 'lit'; import { html, LitElement, nothing, type PropertyValues } 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';
@@ -94,10 +92,6 @@ export class AIChatBlockPeekView extends LitElement {
private _forkBlockId: string | undefined = undefined; private _forkBlockId: string | undefined = undefined;
private _forkSessionId: string | undefined = undefined;
accessor isComposerVisible: Signal<boolean | undefined> = signal(true);
private readonly _deserializeHistoryChatMessages = ( private readonly _deserializeHistoryChatMessages = (
historyMessagesString: string historyMessagesString: string
) => { ) => {
@@ -117,14 +111,14 @@ export class AIChatBlockPeekView extends LitElement {
private readonly _constructBranchChatBlockMessages = async ( private readonly _constructBranchChatBlockMessages = async (
rootWorkspaceId: string, rootWorkspaceId: string,
rootDocId: string, forkSessionId: string,
forkSessionId: string docId?: string
) => { ) => {
const currentUserInfo = await AIProvider.userInfo; const currentUserInfo = await AIProvider.userInfo;
const forkMessages = (await queryHistoryMessages( const forkMessages = (await queryHistoryMessages(
rootWorkspaceId, rootWorkspaceId,
rootDocId, forkSessionId,
forkSessionId docId
)) as ChatMessage[]; )) as ChatMessage[];
const forkLength = forkMessages.length; const forkLength = forkMessages.length;
const historyLength = this._historyMessages.length; const historyLength = this._historyMessages.length;
@@ -163,18 +157,20 @@ export class AIChatBlockPeekView extends LitElement {
messages: [], messages: [],
}); });
this._forkBlockId = undefined; this._forkBlockId = undefined;
this._forkSessionId = undefined;
}; };
private readonly _getSessionId = async () => { private readonly initSession = async () => {
return this._forkSessionId ?? this._sessionId; const session = await AIProvider.session?.getSession(
this.rootWorkspaceId,
this._sessionId
);
this.session = session ?? null;
}; };
private readonly _createSessionId = async () => { private readonly createForkSession = async () => {
if (this._forkSessionId) { if (this.forkSession) {
return this._forkSessionId; return this.forkSession;
} }
const lastMessage = this._historyMessages.at(-1); const lastMessage = this._historyMessages.at(-1);
if (!lastMessage) return; if (!lastMessage) return;
@@ -185,8 +181,14 @@ export class AIChatBlockPeekView extends LitElement {
sessionId: this._sessionId, sessionId: this._sessionId,
latestMessageId: lastMessage.id, latestMessageId: lastMessage.id,
}); });
this._forkSessionId = forkSessionId; if (forkSessionId) {
return this._forkSessionId; const session = await AIProvider.session?.getSession(
this.rootWorkspaceId,
forkSessionId
);
this.forkSession = session ?? null;
}
return this.forkSession;
}; };
private readonly _onChatSuccess = async () => { private readonly _onChatSuccess = async () => {
@@ -213,7 +215,8 @@ export class AIChatBlockPeekView extends LitElement {
} }
// 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._forkSessionId || !this.chatContext.messages.length) { const forkSessionId = this.forkSession?.id;
if (!forkSessionId || !this.chatContext.messages.length) {
return; return;
} }
@@ -230,8 +233,8 @@ export class AIChatBlockPeekView extends LitElement {
const { rootWorkspaceId, rootDocId } = this; const { rootWorkspaceId, rootDocId } = this;
const messages = await this._constructBranchChatBlockMessages( const messages = await this._constructBranchChatBlockMessages(
rootWorkspaceId, rootWorkspaceId,
rootDocId, forkSessionId,
this._forkSessionId rootDocId
); );
if (!messages.length) { if (!messages.length) {
return; return;
@@ -245,7 +248,7 @@ export class AIChatBlockPeekView extends LitElement {
{ {
xywh: bound.serialize(), xywh: bound.serialize(),
messages: JSON.stringify(messages), messages: JSON.stringify(messages),
sessionId: this._forkSessionId, sessionId: forkSessionId,
rootWorkspaceId: rootWorkspaceId, rootWorkspaceId: rootWorkspaceId,
rootDocId: rootDocId, rootDocId: rootDocId,
}, },
@@ -280,7 +283,8 @@ 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._forkBlockId || !this._forkSessionId) { const forkSessionId = this.forkSession?.id;
if (!this._forkBlockId || !forkSessionId) {
return; return;
} }
@@ -292,8 +296,8 @@ export class AIChatBlockPeekView extends LitElement {
const { rootWorkspaceId, rootDocId } = this; const { rootWorkspaceId, rootDocId } = this;
const messages = await this._constructBranchChatBlockMessages( const messages = await this._constructBranchChatBlockMessages(
rootWorkspaceId, rootWorkspaceId,
rootDocId, forkSessionId,
this._forkSessionId rootDocId
); );
if (!messages.length) { if (!messages.length) {
return; return;
@@ -355,8 +359,8 @@ export class AIChatBlockPeekView extends LitElement {
*/ */
retry = async () => { retry = async () => {
try { try {
const { _forkBlockId, _forkSessionId } = this; const forkSessionId = this.forkSession?.id;
if (!_forkBlockId || !_forkSessionId) return; if (!this._forkBlockId || !forkSessionId) return;
if (!AIProvider.actions.chat) return; if (!AIProvider.actions.chat) return;
const abortController = new AbortController(); const abortController = new AbortController();
@@ -376,7 +380,7 @@ export class AIChatBlockPeekView extends LitElement {
const { store } = this.host; const { store } = this.host;
const stream = await AIProvider.actions.chat({ const stream = await AIProvider.actions.chat({
sessionId: _forkSessionId, sessionId: forkSessionId,
retry: true, retry: true,
docId: store.id, docId: store.id,
workspaceId: store.workspace.id, workspaceId: store.workspace.id,
@@ -461,20 +465,20 @@ export class AIChatBlockPeekView extends LitElement {
} }
return html`<div class=${messageClasses}> return html`<div class=${messageClasses}>
<ai-chat-message <ai-chat-block-message
.host=${host} .host=${host}
.state=${messageState} .state=${messageState}
.message=${message} .message=${message}
.textRendererOptions=${this._textRendererOptions} .textRendererOptions=${this._textRendererOptions}
></ai-chat-message> ></ai-chat-block-message>
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing} ${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
${shouldRenderCopyMore ${shouldRenderCopyMore
? html` <chat-copy-more ? html` <chat-copy-more
.host=${host} .host=${host}
.session=${this.forkSession}
.actions=${actions} .actions=${actions}
.content=${markdown} .content=${markdown}
.isLast=${isLastReply} .isLast=${isLastReply}
.getSessionId=${this._getSessionId}
.messageId=${message.id ?? undefined} .messageId=${message.id ?? undefined}
.retry=${() => this.retry()} .retry=${() => this.retry()}
></chat-copy-more>` ></chat-copy-more>`
@@ -482,9 +486,9 @@ export class AIChatBlockPeekView extends LitElement {
${shouldRenderActions ${shouldRenderActions
? html`<chat-action-list ? html`<chat-action-list
.host=${host} .host=${host}
.session=${this.forkSession}
.actions=${actions} .actions=${actions}
.content=${markdown} .content=${markdown}
.getSessionId=${this._getSessionId}
.messageId=${message.id ?? undefined} .messageId=${message.id ?? undefined}
.layoutDirection=${'horizontal'} .layoutDirection=${'horizontal'}
></chat-action-list>` ></chat-action-list>`
@@ -496,6 +500,7 @@ export class AIChatBlockPeekView extends LitElement {
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.initSession().catch(console.error);
const extensions = this.host.std const extensions = this.host.std
.get(ViewExtensionManagerIdentifier) .get(ViewExtensionManagerIdentifier)
.get('preview-page'); .get('preview-page');
@@ -507,8 +512,8 @@ export class AIChatBlockPeekView extends LitElement {
this._historyMessages = this._deserializeHistoryChatMessages( this._historyMessages = this._deserializeHistoryChatMessages(
this.historyMessagesString this.historyMessagesString
); );
const { rootWorkspaceId, rootDocId, _sessionId } = this; const { rootWorkspaceId, _sessionId } = this;
queryHistoryMessages(rootWorkspaceId, rootDocId, _sessionId) queryHistoryMessages(rootWorkspaceId, _sessionId)
.then(messages => { .then(messages => {
this._historyMessages = this._historyMessages.map((message, idx) => { this._historyMessages = this._historyMessages.map((message, idx) => {
return { return {
@@ -566,17 +571,17 @@ export class AIChatBlockPeekView extends LitElement {
<ai-history-clear <ai-history-clear
.host=${this.host} .host=${this.host}
.doc=${this.host.store} .doc=${this.host.store}
.getSessionId=${this._getSessionId} .session=${this.forkSession}
.onHistoryCleared=${this._onHistoryCleared} .onHistoryCleared=${this._onHistoryCleared}
.chatContextValue=${chatContext} .chatContextValue=${chatContext}
></ai-history-clear> ></ai-history-clear>
</div> </div>
<div class="ai-chat-messages-container"> <div class="ai-chat-messages-container">
<ai-chat-messages <ai-chat-block-messages
.host=${host} .host=${host}
.messages=${_historyMessages} .messages=${_historyMessages}
.textRendererOptions=${_textRendererOptions} .textRendererOptions=${_textRendererOptions}
></ai-chat-messages> ></ai-chat-block-messages>
<date-time .date=${latestMessageCreatedAt}></date-time> <date-time .date=${latestMessageCreatedAt}></date-time>
<div class="new-chat-messages-container"> <div class="new-chat-messages-container">
${this.CurrentMessages(currentChatMessages)} ${this.CurrentMessages(currentChatMessages)}
@@ -584,12 +589,11 @@ export class AIChatBlockPeekView extends LitElement {
</div> </div>
<ai-chat-composer <ai-chat-composer
.host=${host} .host=${host}
.doc=${this.host.store} .workspaceId=${this.rootWorkspaceId}
.getSessionId=${this._getSessionId} .session=${this.forkSession ?? this.session}
.createSessionId=${this._createSessionId} .createSession=${this.createForkSession}
.chatContextValue=${chatContext} .chatContextValue=${chatContext}
.updateContext=${updateContext} .updateContext=${updateContext}
.isVisible=${this.isComposerVisible}
.updateEmbeddingProgress=${this._updateEmbeddingProgress} .updateEmbeddingProgress=${this._updateEmbeddingProgress}
.networkSearchConfig=${networkSearchConfig} .networkSearchConfig=${networkSearchConfig}
.docDisplayConfig=${this.docDisplayConfig} .docDisplayConfig=${this.docDisplayConfig}
@@ -647,6 +651,12 @@ export class AIChatBlockPeekView extends LitElement {
@state() @state()
accessor embeddingProgress: [number, number] = [0, 0]; accessor embeddingProgress: [number, number] = [0, 0];
@state()
accessor session: CopilotSessionType | null | undefined;
@state()
accessor forkSession: CopilotSessionType | null | undefined;
} }
declare global { declare global {

View File

@@ -1,8 +1,8 @@
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import { captureException } from '@sentry/react'; import { captureException } from '@sentry/react';
import { Subject } from 'rxjs'; import { BehaviorSubject, Subject } from 'rxjs';
import type { ChatContextValue } from '../chat-panel/chat-context'; import type { ChatContextValue } from '../components/ai-chat-content';
import { import {
PaymentRequiredError, PaymentRequiredError,
RequestTimeoutError, RequestTimeoutError,
@@ -133,8 +133,8 @@ export class AIProvider {
// use case: when user selects "continue in chat" in an ask ai result panel // use case: when user selects "continue in chat" in an ask ai result panel
// do we need to pass the context to the chat panel? // do we need to pass the context to the chat panel?
/* eslint-disable rxjs/finnish */ /* eslint-disable rxjs/finnish */
requestOpenWithChat: new Subject<AIChatParams>(), requestOpenWithChat: new BehaviorSubject<AIChatParams | null>(null),
requestSendWithChat: new Subject<AISendParams>(), requestSendWithChat: new BehaviorSubject<AISendParams | null>(null),
requestInsertTemplate: new Subject<{ requestInsertTemplate: new Subject<{
template: string; template: string;
mode: 'page' | 'edgeless'; mode: 'page' | 'edgeless';

View File

@@ -2,7 +2,6 @@ import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onb
import type { AuthAccountInfo, AuthService } from '@affine/core/modules/cloud'; import type { AuthAccountInfo, AuthService } from '@affine/core/modules/cloud';
import type { GlobalDialogService } from '@affine/core/modules/dialogs'; import type { GlobalDialogService } from '@affine/core/modules/dialogs';
import { import {
type ChatHistoryOrder,
ContextCategories, ContextCategories,
type ContextWorkspaceEmbeddingStatus, type ContextWorkspaceEmbeddingStatus,
type getCopilotHistoriesQuery, type getCopilotHistoriesQuery,
@@ -742,7 +741,7 @@ Could you make a new website based on these notes and send back just the html fi
AIProvider.provide('histories', { AIProvider.provide('histories', {
actions: async ( actions: async (
workspaceId: string, workspaceId: string,
docId?: string docId: string
): Promise<BlockSuitePresets.AIHistory[]> => { ): Promise<BlockSuitePresets.AIHistory[]> => {
// @ts-expect-error - 'action' is missing in server impl // @ts-expect-error - 'action' is missing in server impl
return ( return (
@@ -754,14 +753,15 @@ Could you make a new website based on these notes and send back just the html fi
}, },
chats: async ( chats: async (
workspaceId: string, workspaceId: string,
docId?: string, sessionId: string,
options?: { docId?: string
sessionId?: string;
messageOrder?: ChatHistoryOrder;
}
): Promise<BlockSuitePresets.AIHistory[]> => { ): Promise<BlockSuitePresets.AIHistory[]> => {
// @ts-expect-error - 'action' is missing in server impl // @ts-expect-error - 'action' is missing in server impl
return (await client.getHistories(workspaceId, docId, options)) ?? []; return (
(await client.getHistories(workspaceId, docId, {
sessionId,
})) ?? []
);
}, },
cleanup: async ( cleanup: async (
workspaceId: string, workspaceId: string,

View File

@@ -23,7 +23,7 @@ import type { EditorHost } from '@blocksuite/affine/std';
import type { BlockModel, Store } from '@blocksuite/affine/store'; import type { BlockModel, Store } from '@blocksuite/affine/store';
import { Slice, toDraftModel } from '@blocksuite/affine/store'; import { Slice, toDraftModel } from '@blocksuite/affine/store';
import type { ChatContextValue } from '../chat-panel/chat-context'; import type { ChatContextValue } from '../components/ai-chat-content';
import { import {
getSelectedImagesAsBlobs, getSelectedImagesAsBlobs,
getSelectedTextContent, getSelectedTextContent,

View File

@@ -1,6 +1,6 @@
import { Scrollable } from '@affine/component'; import { Scrollable } from '@affine/component';
import { PageDetailLoading } from '@affine/component/page-detail-skeleton'; import { PageDetailLoading } from '@affine/component/page-detail-skeleton';
import type { ChatPanel } from '@affine/core/blocksuite/ai'; import type { AIChatParams, ChatPanel } from '@affine/core/blocksuite/ai';
import { AIProvider } from '@affine/core/blocksuite/ai'; import { AIProvider } from '@affine/core/blocksuite/ai';
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor'; import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer'; import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer';
@@ -119,7 +119,10 @@ const DetailPageImpl = memo(function DetailPageImpl() {
useEffect(() => { useEffect(() => {
const disposables: Subscription[] = []; const disposables: Subscription[] = [];
const openHandler = () => { const openHandler = (params: AIChatParams | null) => {
if (!params) {
return;
}
workbench.openSidebar(); workbench.openSidebar();
view.activeSidebarTab('chat'); view.activeSidebarTab('chat');
}; };

View File

@@ -1,6 +1,6 @@
import { Scrollable } from '@affine/component'; import { Scrollable } from '@affine/component';
import { PageDetailLoading } from '@affine/component/page-detail-skeleton'; import { PageDetailLoading } from '@affine/component/page-detail-skeleton';
import { AIProvider } from '@affine/core/blocksuite/ai'; import { type AIChatParams, AIProvider } from '@affine/core/blocksuite/ai';
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor'; import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer'; import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer';
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary'; import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
@@ -127,7 +127,10 @@ function DocPeekPreviewEditor({
useEffect(() => { useEffect(() => {
const disposables: Subscription[] = []; const disposables: Subscription[] = [];
const openHandler = () => { const openHandler = (params: AIChatParams | null) => {
if (!params) {
return;
}
if (doc) { if (doc) {
workbench.openDoc(doc.id); workbench.openDoc(doc.id);
peekView.close(); peekView.close();

View File

@@ -343,25 +343,6 @@ test.describe('AIBasic/Chat', () => {
]); ]);
}); });
test('should support clearing chat', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
await utils.chatPanel.clearChat(page);
await utils.chatPanel.waitForHistory(page, []);
});
test('should support copying answer', async ({ test('should support copying answer', async ({
loggedInPage: page, loggedInPage: page,
utils, utils,

View File

@@ -460,9 +460,6 @@ test.describe('AISettings/Embedding', () => {
await utils.settings.closeSettingsPanel(page); await utils.settings.closeSettingsPanel(page);
// Clear history
await utils.chatPanel.clearChat(page);
// Ignored docs should not be used for embedding // Ignored docs should not be used for embedding
await utils.chatPanel.makeChat( await utils.chatPanel.makeChat(
page, page,