refactor(core): hide emebedding status tip if completed (#12720)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added real-time embedding status tracking and progress messages to the AI chat composer, with automatic updates every 10 seconds. - **Refactor** - Simplified the embedding status tooltip to display a static message, removing dynamic status updates and hover-based refresh. - **Tests** - Enhanced embedding status tooltip test by creating sample documents and extending visibility timeout to 50 seconds. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -2,6 +2,7 @@ import type {
|
|||||||
ChatHistoryOrder,
|
ChatHistoryOrder,
|
||||||
ContextMatchedDocChunk,
|
ContextMatchedDocChunk,
|
||||||
ContextMatchedFileChunk,
|
ContextMatchedFileChunk,
|
||||||
|
ContextWorkspaceEmbeddingStatus,
|
||||||
CopilotContextCategory,
|
CopilotContextCategory,
|
||||||
CopilotContextDoc,
|
CopilotContextDoc,
|
||||||
CopilotContextFile,
|
CopilotContextFile,
|
||||||
@@ -329,6 +330,11 @@ declare global {
|
|||||||
onPoll: (result: AIDocsAndFilesContext | undefined) => void,
|
onPoll: (result: AIDocsAndFilesContext | undefined) => void,
|
||||||
abortSignal: AbortSignal
|
abortSignal: AbortSignal
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
pollEmbeddingStatus: (
|
||||||
|
workspaceId: string,
|
||||||
|
onPoll: (result: ContextWorkspaceEmbeddingStatus) => void,
|
||||||
|
abortSignal: AbortSignal
|
||||||
|
) => Promise<void>;
|
||||||
matchContext: (
|
matchContext: (
|
||||||
content: string,
|
content: string,
|
||||||
contextId?: string,
|
contextId?: string,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import './ai-chat-composer-tip';
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
ContextEmbedStatus,
|
ContextEmbedStatus,
|
||||||
|
ContextWorkspaceEmbeddingStatus,
|
||||||
CopilotContextDoc,
|
CopilotContextDoc,
|
||||||
CopilotContextFile,
|
CopilotContextFile,
|
||||||
CopilotDocType,
|
CopilotDocType,
|
||||||
@@ -34,6 +35,8 @@ import type {
|
|||||||
} from '../ai-chat-input';
|
} from '../ai-chat-input';
|
||||||
import { MAX_IMAGE_COUNT } from '../ai-chat-input/const';
|
import { MAX_IMAGE_COUNT } from '../ai-chat-input/const';
|
||||||
|
|
||||||
|
export const EMBEDDING_STATUS_CHECK_INTERVAL = 10000;
|
||||||
|
|
||||||
export class AIChatComposer extends SignalWatcher(
|
export class AIChatComposer extends SignalWatcher(
|
||||||
WithDisposable(ShadowlessElement)
|
WithDisposable(ShadowlessElement)
|
||||||
) {
|
) {
|
||||||
@@ -108,6 +111,12 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
@state()
|
@state()
|
||||||
accessor chips: ChatChip[] = [];
|
accessor chips: ChatChip[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor embeddingProgressText = 'Loading embedding status...';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor embeddingCompleted = false;
|
||||||
|
|
||||||
private _isInitialized = false;
|
private _isInitialized = false;
|
||||||
|
|
||||||
private _isLoading = false;
|
private _isLoading = false;
|
||||||
@@ -116,6 +125,8 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
|
|
||||||
private _pollAbortController: AbortController | null = null;
|
private _pollAbortController: AbortController | null = null;
|
||||||
|
|
||||||
|
private _pollEmbeddingStatusAbortController: AbortController | null = null;
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`
|
return html`
|
||||||
<chat-panel-chips
|
<chat-panel-chips
|
||||||
@@ -151,8 +162,12 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
<ai-chat-composer-tip
|
<ai-chat-composer-tip
|
||||||
.tips=${[
|
.tips=${[
|
||||||
html`<span>AI outputs can be misleading or wrong</span>`,
|
html`<span>AI outputs can be misleading or wrong</span>`,
|
||||||
html`<ai-chat-embedding-status-tooltip .host=${this.host} />`,
|
this.embeddingCompleted
|
||||||
]}
|
? null
|
||||||
|
: html`<ai-chat-embedding-status-tooltip
|
||||||
|
.progressText=${this.embeddingProgressText}
|
||||||
|
/>`,
|
||||||
|
].filter(Boolean)}
|
||||||
.loop=${false}
|
.loop=${false}
|
||||||
></ai-chat-composer-tip>
|
></ai-chat-composer-tip>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,10 +189,20 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
if (isVisible && !this._isInitialized) {
|
if (isVisible && !this._isInitialized) {
|
||||||
this._initComposer().catch(console.error);
|
this._initComposer().catch(console.error);
|
||||||
}
|
}
|
||||||
|
if (!isVisible) {
|
||||||
|
this._abortPoll();
|
||||||
|
this._abortPollEmbeddingStatus();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._abortPoll();
|
||||||
|
this._abortPollEmbeddingStatus();
|
||||||
|
}
|
||||||
|
|
||||||
protected override willUpdate(_changedProperties: PropertyValues) {
|
protected override willUpdate(_changedProperties: PropertyValues) {
|
||||||
if (_changedProperties.has('doc')) {
|
if (_changedProperties.has('doc')) {
|
||||||
this._resetComposer();
|
this._resetComposer();
|
||||||
@@ -316,6 +341,40 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly _pollEmbeddingStatus = async () => {
|
||||||
|
if (this._pollEmbeddingStatusAbortController) {
|
||||||
|
this._pollEmbeddingStatusAbortController.abort();
|
||||||
|
}
|
||||||
|
this._pollEmbeddingStatusAbortController = new AbortController();
|
||||||
|
const signal = this._pollEmbeddingStatusAbortController.signal;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AIProvider.context?.pollEmbeddingStatus(
|
||||||
|
this.host.std.workspace.id,
|
||||||
|
(status: ContextWorkspaceEmbeddingStatus) => {
|
||||||
|
if (!status) {
|
||||||
|
this.embeddingProgressText = 'Loading embedding status...';
|
||||||
|
this.embeddingCompleted = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const completed = status.embedded === status.total;
|
||||||
|
this.embeddingCompleted = completed;
|
||||||
|
if (completed) {
|
||||||
|
this.embeddingProgressText =
|
||||||
|
'Embedding finished. You are getting the best results!';
|
||||||
|
} else {
|
||||||
|
this.embeddingProgressText =
|
||||||
|
'File not embedded yet. Results will improve after embedding.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
signal
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
this.embeddingProgressText = 'Failed to load embedding status...';
|
||||||
|
this.embeddingCompleted = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private readonly _onPoll = (
|
private readonly _onPoll = (
|
||||||
result?: BlockSuitePresets.AIDocsAndFilesContext
|
result?: BlockSuitePresets.AIDocsAndFilesContext
|
||||||
) => {
|
) => {
|
||||||
@@ -378,6 +437,11 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
this._pollAbortController = null;
|
this._pollAbortController = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly _abortPollEmbeddingStatus = () => {
|
||||||
|
this._pollEmbeddingStatusAbortController?.abort();
|
||||||
|
this._pollEmbeddingStatusAbortController = null;
|
||||||
|
};
|
||||||
|
|
||||||
private readonly _initComposer = async () => {
|
private readonly _initComposer = async () => {
|
||||||
if (!this.isVisible.value) return;
|
if (!this.isVisible.value) return;
|
||||||
if (this._isLoading) return;
|
if (this._isLoading) return;
|
||||||
@@ -394,12 +458,14 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
if (needPoll) {
|
if (needPoll) {
|
||||||
await this._pollContextDocsAndFiles();
|
await this._pollContextDocsAndFiles();
|
||||||
}
|
}
|
||||||
|
await this._pollEmbeddingStatus();
|
||||||
this._isLoading = false;
|
this._isLoading = false;
|
||||||
this._isInitialized = true;
|
this._isInitialized = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _resetComposer = () => {
|
private readonly _resetComposer = () => {
|
||||||
this._abortPoll();
|
this._abortPoll();
|
||||||
|
this._abortPollEmbeddingStatus();
|
||||||
this.chips = [];
|
this.chips = [];
|
||||||
this._contextId = undefined;
|
this._contextId = undefined;
|
||||||
this._isLoading = false;
|
this._isLoading = false;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { SignalWatcher } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher } from '@blocksuite/affine/global/lit';
|
||||||
import { unsafeCSSVar } from '@blocksuite/affine/shared/theme';
|
import { unsafeCSSVar } from '@blocksuite/affine/shared/theme';
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
|
||||||
import { css, html, LitElement } from 'lit';
|
import { css, html, LitElement } from 'lit';
|
||||||
import { property, state } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
import { debounce, noop } from 'lodash-es';
|
|
||||||
|
|
||||||
import { AIProvider } from '../../provider/ai-provider';
|
|
||||||
|
|
||||||
export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) {
|
export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
@@ -38,47 +34,8 @@ export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host!: EditorHost;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor progressText = 'Loading embedding status...';
|
accessor progressText = 'Loading embedding status...';
|
||||||
|
|
||||||
override connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this._updateEmbeddingStatus().catch(noop);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _updateEmbeddingStatus() {
|
|
||||||
try {
|
|
||||||
const status = await AIProvider.embedding?.getEmbeddingStatus(
|
|
||||||
this.host.std.workspace.id
|
|
||||||
);
|
|
||||||
if (!status) {
|
|
||||||
this.progressText = 'Loading embedding status...';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const completed = status.embedded === status.total;
|
|
||||||
if (completed) {
|
|
||||||
this.progressText =
|
|
||||||
'Embedding finished. You are getting the best results!';
|
|
||||||
} else {
|
|
||||||
this.progressText =
|
|
||||||
'File not embedded yet. Results will improve after embedding.';
|
|
||||||
}
|
|
||||||
this.requestUpdate();
|
|
||||||
} catch {
|
|
||||||
this.progressText = 'Failed to load embedding status...';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly _handleCheckStatusMouseEnter = debounce(
|
|
||||||
() => {
|
|
||||||
this._updateEmbeddingStatus().catch(noop);
|
|
||||||
},
|
|
||||||
1000,
|
|
||||||
{ leading: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
@@ -91,7 +48,6 @@ export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) {
|
|||||||
<div
|
<div
|
||||||
class="check-status"
|
class="check-status"
|
||||||
data-testid="ai-chat-embedding-status-tooltip-check"
|
data-testid="ai-chat-embedding-status-tooltip-check"
|
||||||
@mouseenter=${this._handleCheckStatusMouseEnter}
|
|
||||||
>
|
>
|
||||||
Check status
|
Check status
|
||||||
<affine-tooltip tip-position="top-start"
|
<affine-tooltip tip-position="top-start"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { GlobalDialogService } from '@affine/core/modules/dialogs';
|
|||||||
import {
|
import {
|
||||||
type ChatHistoryOrder,
|
type ChatHistoryOrder,
|
||||||
ContextCategories,
|
ContextCategories,
|
||||||
|
type ContextWorkspaceEmbeddingStatus,
|
||||||
type getCopilotHistoriesQuery,
|
type getCopilotHistoriesQuery,
|
||||||
type RequestOptions,
|
type RequestOptions,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
@@ -698,6 +699,23 @@ Could you make a new website based on these notes and send back just the html fi
|
|||||||
await new Promise(resolve => setTimeout(resolve, interval));
|
await new Promise(resolve => setTimeout(resolve, interval));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
pollEmbeddingStatus: async (
|
||||||
|
workspaceId: string,
|
||||||
|
onPoll: (result: ContextWorkspaceEmbeddingStatus) => void,
|
||||||
|
abortSignal: AbortSignal
|
||||||
|
) => {
|
||||||
|
const poll = async () => {
|
||||||
|
const result = await client.getEmbeddingStatus(workspaceId);
|
||||||
|
onPoll(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const INTERVAL = 10 * 1000;
|
||||||
|
|
||||||
|
while (!abortSignal.aborted) {
|
||||||
|
await poll();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, INTERVAL));
|
||||||
|
}
|
||||||
|
},
|
||||||
matchContext: async (
|
matchContext: async (
|
||||||
content: string,
|
content: string,
|
||||||
contextId?: string,
|
contextId?: string,
|
||||||
@@ -792,12 +810,6 @@ Could you make a new website based on these notes and send back just the html fi
|
|||||||
return client.forkSession(options);
|
return client.forkSession(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
AIProvider.provide('embedding', {
|
|
||||||
getEmbeddingStatus: (workspaceId: string) => {
|
|
||||||
return client.getEmbeddingStatus(workspaceId);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const disposeRequestLoginHandler = AIProvider.slots.requestLogin.subscribe(
|
const disposeRequestLoginHandler = AIProvider.slots.requestLogin.subscribe(
|
||||||
() => {
|
() => {
|
||||||
globalDialogService.open('sign-in', {});
|
globalDialogService.open('sign-in', {});
|
||||||
|
|||||||
@@ -19,11 +19,18 @@ test.describe('AIBasic/Chat', () => {
|
|||||||
|
|
||||||
test('should display embedding status tooltip', async ({
|
test('should display embedding status tooltip', async ({
|
||||||
loggedInPage: page,
|
loggedInPage: page,
|
||||||
|
utils,
|
||||||
}) => {
|
}) => {
|
||||||
|
await utils.editor.createDoc(page, 'Doc 1', 'doc1');
|
||||||
|
await utils.editor.createDoc(page, 'Doc 2', 'doc2');
|
||||||
|
await utils.editor.createDoc(page, 'Doc 3', 'doc3');
|
||||||
|
await utils.editor.createDoc(page, 'Doc 4', 'doc4');
|
||||||
|
await utils.editor.createDoc(page, 'Doc 5', 'doc5');
|
||||||
|
|
||||||
const check = await page.getByTestId(
|
const check = await page.getByTestId(
|
||||||
'ai-chat-embedding-status-tooltip-check'
|
'ai-chat-embedding-status-tooltip-check'
|
||||||
);
|
);
|
||||||
await expect(check).toBeVisible();
|
await expect(check).toBeVisible({ timeout: 50 * 1000 });
|
||||||
|
|
||||||
await check.hover();
|
await check.hover();
|
||||||
const tooltip = await page.getByTestId('ai-chat-embedding-status-tooltip');
|
const tooltip = await page.getByTestId('ai-chat-embedding-status-tooltip');
|
||||||
|
|||||||
Reference in New Issue
Block a user