feat(core): poll context docs and files embedding status (#10843)
Close [BS-2791](https://linear.app/affine-design/issue/BS-2791). ### What Changed? - Add status filed to `CopilotContextDoc` to querying document embedding processing progress. - Change `ChipState` from `success` to `finished` to better align with backend server status. - Add `pollContextDocsAndFiles` API for embedding status polling. ### About Polling - Set the minimum interval to 1 second and the maximum interval to 30 seconds - Use exponential backoff and increase the interval by 50% after each poll - Make sure the interval does not exceed the maximum
This commit is contained in:
@@ -242,6 +242,11 @@ declare global {
|
|||||||
): AIActionTextResponse<T>;
|
): AIActionTextResponse<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AIDocsAndFilesContext = {
|
||||||
|
docs: CopilotContextDoc[];
|
||||||
|
files: CopilotContextFile[];
|
||||||
|
};
|
||||||
|
|
||||||
interface AIContextService {
|
interface AIContextService {
|
||||||
createContext: (
|
createContext: (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
@@ -274,13 +279,14 @@ declare global {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
contextId: string
|
contextId: string
|
||||||
) => Promise<
|
) => Promise<AIDocsAndFilesContext | undefined>;
|
||||||
| {
|
pollContextDocsAndFiles: (
|
||||||
docs: Array<CopilotContextDoc>;
|
workspaceId: string,
|
||||||
files: Array<CopilotContextFile>;
|
sessionId: string,
|
||||||
}
|
contextId: string,
|
||||||
| undefined
|
onPoll: (result: AIDocsAndFilesContext | undefined) => void,
|
||||||
>;
|
abortSignal: AbortSignal
|
||||||
|
) => Promise<void>;
|
||||||
matchContext: (
|
matchContext: (
|
||||||
contextId: string,
|
contextId: string,
|
||||||
content: string,
|
content: string,
|
||||||
|
|||||||
@@ -70,13 +70,13 @@ export type ChatBlockMessage = ChatMessage & {
|
|||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChipState = 'candidate' | 'processing' | 'success' | 'failed';
|
export type ChipState = 'candidate' | 'processing' | 'finished' | 'failed';
|
||||||
|
|
||||||
export interface BaseChip {
|
export interface BaseChip {
|
||||||
/**
|
/**
|
||||||
* candidate: the chip is a candidate for the chat
|
* candidate: the chip is a candidate for the chat
|
||||||
* processing: the chip is processing
|
* processing: the chip is processing
|
||||||
* success: the chip is successfully processed
|
* finished: the chip is successfully processed
|
||||||
* failed: the chip is failed to process
|
* failed: the chip is failed to process
|
||||||
*/
|
*/
|
||||||
state: ChipState;
|
state: ChipState;
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor pollContextDocsAndFiles!: () => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor docDisplayConfig!: DocDisplayConfig;
|
accessor docDisplayConfig!: DocDisplayConfig;
|
||||||
|
|
||||||
@@ -201,6 +204,7 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
await this._removeFromContext(chip);
|
await this._removeFromContext(chip);
|
||||||
}
|
}
|
||||||
await this._addToContext(chip);
|
await this._addToContext(chip);
|
||||||
|
this.pollContextDocsAndFiles();
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _updateChip = (
|
private readonly _updateChip = (
|
||||||
@@ -229,19 +233,24 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
|
|
||||||
private readonly _removeChip = async (chip: ChatChip) => {
|
private readonly _removeChip = async (chip: ChatChip) => {
|
||||||
if (isDocChip(chip)) {
|
if (isDocChip(chip)) {
|
||||||
await this._removeFromContext(chip);
|
const removed = await this._removeFromContext(chip);
|
||||||
this.updateContext({
|
if (removed) {
|
||||||
chips: this.chatContextValue.chips.filter(item => {
|
this.updateContext({
|
||||||
return !isDocChip(item) || item.docId !== chip.docId;
|
chips: this.chatContextValue.chips.filter(item => {
|
||||||
}),
|
return !isDocChip(item) || item.docId !== chip.docId;
|
||||||
});
|
}),
|
||||||
} else {
|
});
|
||||||
await this._removeFromContext(chip);
|
}
|
||||||
this.updateContext({
|
}
|
||||||
chips: this.chatContextValue.chips.filter(item => {
|
if (isFileChip(chip)) {
|
||||||
return !isFileChip(item) || item.file !== chip.file;
|
const removed = await this._removeFromContext(chip);
|
||||||
}),
|
if (removed) {
|
||||||
});
|
this.updateContext({
|
||||||
|
chips: this.chatContextValue.chips.filter(item => {
|
||||||
|
return !isFileChip(item) || item.file !== chip.file;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -255,7 +264,8 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
contextId,
|
contextId,
|
||||||
docId: chip.docId,
|
docId: chip.docId,
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
|
if (isFileChip(chip)) {
|
||||||
try {
|
try {
|
||||||
const blobId = await this.host.doc.blobSync.set(chip.file);
|
const blobId = await this.host.doc.blobSync.set(chip.file);
|
||||||
const contextFile = await AIProvider.context.addContextFile(chip.file, {
|
const contextFile = await AIProvider.context.addContextFile(chip.file, {
|
||||||
@@ -263,7 +273,7 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
blobId,
|
blobId,
|
||||||
});
|
});
|
||||||
this._updateChip(chip, {
|
this._updateChip(chip, {
|
||||||
state: 'success',
|
state: contextFile.status,
|
||||||
blobId: contextFile.blobId,
|
blobId: contextFile.blobId,
|
||||||
fileId: contextFile.id,
|
fileId: contextFile.id,
|
||||||
});
|
});
|
||||||
@@ -276,22 +286,26 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _removeFromContext = async (chip: ChatChip) => {
|
private readonly _removeFromContext = async (
|
||||||
|
chip: ChatChip
|
||||||
|
): Promise<boolean> => {
|
||||||
const contextId = await this.getContextId();
|
const contextId = await this.getContextId();
|
||||||
if (!contextId || !AIProvider.context) {
|
if (!contextId || !AIProvider.context) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (isDocChip(chip)) {
|
if (isDocChip(chip)) {
|
||||||
await AIProvider.context.removeContextDoc({
|
return await AIProvider.context.removeContextDoc({
|
||||||
contextId,
|
contextId,
|
||||||
docId: chip.docId,
|
docId: chip.docId,
|
||||||
});
|
});
|
||||||
} else if (isFileChip(chip) && chip.fileId) {
|
}
|
||||||
await AIProvider.context.removeContextFile({
|
if (isFileChip(chip) && chip.fileId) {
|
||||||
|
return await AIProvider.context.removeContextFile({
|
||||||
contextId,
|
contextId,
|
||||||
fileId: chip.fileId,
|
fileId: chip.fileId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _checkTokenLimit = (
|
private readonly _checkTokenLimit = (
|
||||||
@@ -305,7 +319,7 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
|
|||||||
if (chip.docId === newChip.docId) {
|
if (chip.docId === newChip.docId) {
|
||||||
return acc + newTokenCount;
|
return acc + newTokenCount;
|
||||||
}
|
}
|
||||||
if (chip.markdown?.value && chip.state === 'success') {
|
if (chip.markdown?.value && chip.state === 'finished') {
|
||||||
const tokenCount =
|
const tokenCount =
|
||||||
chip.tokenCount ?? estimateTokenCount(chip.markdown.value);
|
chip.tokenCount ?? estimateTokenCount(chip.markdown.value);
|
||||||
return acc + tokenCount;
|
return acc + tokenCount;
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
private get _isNetworkDisabled() {
|
private get _isNetworkDisabled() {
|
||||||
return (
|
return (
|
||||||
!!this.chatContextValue.images.length ||
|
!!this.chatContextValue.images.length ||
|
||||||
!!this.chatContextValue.chips.filter(chip => chip.state === 'success')
|
!!this.chatContextValue.chips.filter(chip => chip.state === 'finished')
|
||||||
.length
|
.length
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -561,7 +561,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
: [];
|
: [];
|
||||||
const contexts = this.chatContextValue.chips.reduce(
|
const contexts = this.chatContextValue.chips.reduce(
|
||||||
(acc, chip, index) => {
|
(acc, chip, index) => {
|
||||||
if (chip.state !== 'success') {
|
if (chip.state !== 'finished') {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
if (isDocChip(chip) && !!chip.markdown?.value) {
|
if (isDocChip(chip) && !!chip.markdown?.value) {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export class ChatPanelDocChip extends SignalWatcher(
|
|||||||
const markdown = this.chip.markdown ?? new Signal<string>('');
|
const markdown = this.chip.markdown ?? new Signal<string>('');
|
||||||
markdown.value = value;
|
markdown.value = value;
|
||||||
this.updateChip(this.chip, {
|
this.updateChip(this.chip, {
|
||||||
state: 'success',
|
state: 'finished',
|
||||||
markdown,
|
markdown,
|
||||||
tokenCount,
|
tokenCount,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import './chat-panel-input';
|
import './chat-panel-input';
|
||||||
import './chat-panel-messages';
|
import './chat-panel-messages';
|
||||||
|
|
||||||
|
import type { CopilotContextDoc, CopilotContextFile } from '@affine/graphql';
|
||||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
@@ -35,7 +36,7 @@ import type {
|
|||||||
FileChip,
|
FileChip,
|
||||||
} from './chat-context';
|
} from './chat-context';
|
||||||
import type { ChatPanelMessages } from './chat-panel-messages';
|
import type { ChatPanelMessages } from './chat-panel-messages';
|
||||||
import { isDocContext } from './components/utils';
|
import { isDocChip, isDocContext } from './components/utils';
|
||||||
|
|
||||||
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
|
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
|
||||||
quote: '',
|
quote: '',
|
||||||
@@ -174,7 +175,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
this._scrollToEnd();
|
this._scrollToEnd();
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _updateChips = async () => {
|
private readonly _initChips = async () => {
|
||||||
// context not initialized, show candidate chip
|
// context not initialized, show candidate chip
|
||||||
if (!this._chatSessionId || !this._chatContextId) {
|
if (!this._chatSessionId || !this._chatContextId) {
|
||||||
return;
|
return;
|
||||||
@@ -191,12 +192,16 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
);
|
);
|
||||||
|
let allDone = true;
|
||||||
const chips: ChatChip[] = await Promise.all(
|
const chips: ChatChip[] = await Promise.all(
|
||||||
list.map(async item => {
|
list.map(async item => {
|
||||||
|
if (item.status === 'processing') {
|
||||||
|
allDone = false;
|
||||||
|
}
|
||||||
if (isDocContext(item)) {
|
if (isDocContext(item)) {
|
||||||
return {
|
return {
|
||||||
docId: item.id,
|
docId: item.id,
|
||||||
state: 'processing',
|
state: item.status || 'processing',
|
||||||
} as DocChip;
|
} as DocChip;
|
||||||
}
|
}
|
||||||
const file = await this.host.doc.blobSync.get(item.blobId);
|
const file = await this.host.doc.blobSync.get(item.blobId);
|
||||||
@@ -212,7 +217,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
file: new File([file], item.name),
|
file: new File([file], item.name),
|
||||||
blobId: item.blobId,
|
blobId: item.blobId,
|
||||||
fileId: item.id,
|
fileId: item.id,
|
||||||
state: item.status === 'finished' ? 'success' : item.status,
|
state: item.status,
|
||||||
tooltip: item.error,
|
tooltip: item.error,
|
||||||
} as FileChip;
|
} as FileChip;
|
||||||
}
|
}
|
||||||
@@ -223,6 +228,10 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
...this.chatContextValue,
|
...this.chatContextValue,
|
||||||
chips,
|
chips,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!allDone) {
|
||||||
|
await this._pollContextDocsAndFiles();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _getSessionId = async () => {
|
private readonly _getSessionId = async () => {
|
||||||
@@ -285,6 +294,8 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
|
|
||||||
private _width: Signal<number | undefined> = signal(undefined);
|
private _width: Signal<number | undefined> = signal(undefined);
|
||||||
|
|
||||||
|
private _pollAbortController: AbortController | null = null;
|
||||||
|
|
||||||
private readonly _scrollToEnd = () => {
|
private readonly _scrollToEnd = () => {
|
||||||
if (!this._wheelTriggered) {
|
if (!this._wheelTriggered) {
|
||||||
this._chatMessages.value?.scrollToEnd();
|
this._chatMessages.value?.scrollToEnd();
|
||||||
@@ -345,14 +356,79 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
this._chatSessionId
|
this._chatSessionId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await this._updateChips();
|
await this._initChips();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly _pollContextDocsAndFiles = async () => {
|
||||||
|
if (!this._chatSessionId || !this._chatContextId || !AIProvider.context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._pollAbortController) {
|
||||||
|
// already polling, return
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._pollAbortController = new AbortController();
|
||||||
|
await AIProvider.context.pollContextDocsAndFiles(
|
||||||
|
this.doc.workspace.id,
|
||||||
|
this._chatSessionId,
|
||||||
|
this._chatContextId,
|
||||||
|
this._onPoll,
|
||||||
|
this._pollAbortController.signal
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _onPoll = (
|
||||||
|
result?: BlockSuitePresets.AIDocsAndFilesContext
|
||||||
|
) => {
|
||||||
|
if (!result) {
|
||||||
|
this._abortPoll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { docs = [], files = [] } = result;
|
||||||
|
const hashMap = new Map<string, CopilotContextDoc | CopilotContextFile>();
|
||||||
|
let allDone = true;
|
||||||
|
docs.forEach(doc => {
|
||||||
|
hashMap.set(doc.id, doc);
|
||||||
|
if (doc.status === 'processing') {
|
||||||
|
allDone = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
files.forEach(file => {
|
||||||
|
hashMap.set(file.id, file);
|
||||||
|
if (file.status === 'processing') {
|
||||||
|
allDone = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const nextChips = this.chatContextValue.chips.map(chip => {
|
||||||
|
const id = isDocChip(chip) ? chip.docId : chip.fileId;
|
||||||
|
const item = id && hashMap.get(id);
|
||||||
|
if (item && item.status) {
|
||||||
|
return {
|
||||||
|
...chip,
|
||||||
|
state: item.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return chip;
|
||||||
|
});
|
||||||
|
this.updateContext({
|
||||||
|
chips: nextChips,
|
||||||
|
});
|
||||||
|
if (allDone) {
|
||||||
|
this._abortPoll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _abortPoll = () => {
|
||||||
|
this._pollAbortController?.abort();
|
||||||
|
this._pollAbortController = null;
|
||||||
|
};
|
||||||
|
|
||||||
protected override updated(_changedProperties: PropertyValues) {
|
protected override updated(_changedProperties: PropertyValues) {
|
||||||
if (_changedProperties.has('doc')) {
|
if (_changedProperties.has('doc')) {
|
||||||
|
this._abortPoll();
|
||||||
this._chatSessionId = null;
|
this._chatSessionId = null;
|
||||||
this._chatContextId = null;
|
this._chatContextId = null;
|
||||||
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
|
||||||
@@ -494,6 +570,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
.getContextId=${this._getContextId}
|
.getContextId=${this._getContextId}
|
||||||
.updateContext=${this.updateContext}
|
.updateContext=${this.updateContext}
|
||||||
|
.pollContextDocsAndFiles=${this._pollContextDocsAndFiles}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.docSearchMenuConfig=${this.docSearchMenuConfig}
|
.docSearchMenuConfig=${this.docSearchMenuConfig}
|
||||||
></chat-panel-chips>
|
></chat-panel-chips>
|
||||||
|
|||||||
@@ -451,6 +451,38 @@ Could you make a new website based on these notes and send back just the html fi
|
|||||||
) => {
|
) => {
|
||||||
return client.getContextDocsAndFiles(workspaceId, sessionId, contextId);
|
return client.getContextDocsAndFiles(workspaceId, sessionId, contextId);
|
||||||
},
|
},
|
||||||
|
pollContextDocsAndFiles: async (
|
||||||
|
workspaceId: string,
|
||||||
|
sessionId: string,
|
||||||
|
contextId: string,
|
||||||
|
onPoll: (
|
||||||
|
result: BlockSuitePresets.AIDocsAndFilesContext | undefined
|
||||||
|
) => void,
|
||||||
|
abortSignal: AbortSignal
|
||||||
|
) => {
|
||||||
|
const poll = async () => {
|
||||||
|
const result = await client.getContextDocsAndFiles(
|
||||||
|
workspaceId,
|
||||||
|
sessionId,
|
||||||
|
contextId
|
||||||
|
);
|
||||||
|
onPoll(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
let attempts = 0;
|
||||||
|
const MIN_INTERVAL = 1000;
|
||||||
|
const MAX_INTERVAL = 30 * 1000;
|
||||||
|
|
||||||
|
while (!abortSignal.aborted) {
|
||||||
|
await poll();
|
||||||
|
const interval = Math.min(
|
||||||
|
MIN_INTERVAL * Math.pow(1.5, attempts),
|
||||||
|
MAX_INTERVAL
|
||||||
|
);
|
||||||
|
attempts++;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, interval));
|
||||||
|
}
|
||||||
|
},
|
||||||
matchContext: async (
|
matchContext: async (
|
||||||
contextId: string,
|
contextId: string,
|
||||||
content: string,
|
content: string,
|
||||||
|
|||||||
@@ -1026,7 +1026,7 @@ test.describe('chat with doc', () => {
|
|||||||
expect(await chipTitle.textContent()).toBe('Untitled');
|
expect(await chipTitle.textContent()).toBe('Untitled');
|
||||||
let chip = await page.getByTestId('chat-panel-chip');
|
let chip = await page.getByTestId('chat-panel-chip');
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
||||||
expect(await chip.getAttribute('data-state')).toBe('success');
|
expect(await chip.getAttribute('data-state')).toBe('finished');
|
||||||
|
|
||||||
const editorTitle = await page.locator('doc-title .inline-editor').nth(0);
|
const editorTitle = await page.locator('doc-title .inline-editor').nth(0);
|
||||||
await editorTitle.pressSequentially('AFFiNE AI', {
|
await editorTitle.pressSequentially('AFFiNE AI', {
|
||||||
@@ -1048,7 +1048,7 @@ test.describe('chat with doc', () => {
|
|||||||
|
|
||||||
expect(await chipTitle.textContent()).toBe('AFFiNE AI');
|
expect(await chipTitle.textContent()).toBe('AFFiNE AI');
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
||||||
expect(await chip.getAttribute('data-state')).toBe('success');
|
expect(await chip.getAttribute('data-state')).toBe('finished');
|
||||||
|
|
||||||
await typeChatSequentially(page, 'What is AFiAI?');
|
await typeChatSequentially(page, 'What is AFiAI?');
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
@@ -1071,6 +1071,6 @@ test.describe('chat with doc', () => {
|
|||||||
expect(await chipTitle.textContent()).toBe('AFFiNE AI');
|
expect(await chipTitle.textContent()).toBe('AFFiNE AI');
|
||||||
const chip2 = await page.getByTestId('chat-panel-chip');
|
const chip2 = await page.getByTestId('chat-panel-chip');
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
// oxlint-disable-next-line unicorn/prefer-dom-node-dataset
|
||||||
expect(await chip2.getAttribute('data-state')).toBe('success');
|
expect(await chip2.getAttribute('data-state')).toBe('finished');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user