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:
yoyoyohamapi
2025-06-10 01:54:12 +00:00
parent b5ef361f87
commit 3c29f62224
5 changed files with 101 additions and 54 deletions

View File

@@ -2,6 +2,7 @@ import type {
ChatHistoryOrder,
ContextMatchedDocChunk,
ContextMatchedFileChunk,
ContextWorkspaceEmbeddingStatus,
CopilotContextCategory,
CopilotContextDoc,
CopilotContextFile,
@@ -329,6 +330,11 @@ declare global {
onPoll: (result: AIDocsAndFilesContext | undefined) => void,
abortSignal: AbortSignal
) => Promise<void>;
pollEmbeddingStatus: (
workspaceId: string,
onPoll: (result: ContextWorkspaceEmbeddingStatus) => void,
abortSignal: AbortSignal
) => Promise<void>;
matchContext: (
content: string,
contextId?: string,

View File

@@ -2,6 +2,7 @@ import './ai-chat-composer-tip';
import type {
ContextEmbedStatus,
ContextWorkspaceEmbeddingStatus,
CopilotContextDoc,
CopilotContextFile,
CopilotDocType,
@@ -34,6 +35,8 @@ import type {
} from '../ai-chat-input';
import { MAX_IMAGE_COUNT } from '../ai-chat-input/const';
export const EMBEDDING_STATUS_CHECK_INTERVAL = 10000;
export class AIChatComposer extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
@@ -108,6 +111,12 @@ export class AIChatComposer extends SignalWatcher(
@state()
accessor chips: ChatChip[] = [];
@state()
accessor embeddingProgressText = 'Loading embedding status...';
@state()
accessor embeddingCompleted = false;
private _isInitialized = false;
private _isLoading = false;
@@ -116,6 +125,8 @@ export class AIChatComposer extends SignalWatcher(
private _pollAbortController: AbortController | null = null;
private _pollEmbeddingStatusAbortController: AbortController | null = null;
override render() {
return html`
<chat-panel-chips
@@ -151,8 +162,12 @@ export class AIChatComposer extends SignalWatcher(
<ai-chat-composer-tip
.tips=${[
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}
></ai-chat-composer-tip>
</div>
@@ -174,10 +189,20 @@ export class AIChatComposer extends SignalWatcher(
if (isVisible && !this._isInitialized) {
this._initComposer().catch(console.error);
}
if (!isVisible) {
this._abortPoll();
this._abortPollEmbeddingStatus();
}
})
);
}
override disconnectedCallback() {
super.disconnectedCallback();
this._abortPoll();
this._abortPollEmbeddingStatus();
}
protected override willUpdate(_changedProperties: PropertyValues) {
if (_changedProperties.has('doc')) {
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 = (
result?: BlockSuitePresets.AIDocsAndFilesContext
) => {
@@ -378,6 +437,11 @@ export class AIChatComposer extends SignalWatcher(
this._pollAbortController = null;
};
private readonly _abortPollEmbeddingStatus = () => {
this._pollEmbeddingStatusAbortController?.abort();
this._pollEmbeddingStatusAbortController = null;
};
private readonly _initComposer = async () => {
if (!this.isVisible.value) return;
if (this._isLoading) return;
@@ -394,12 +458,14 @@ export class AIChatComposer extends SignalWatcher(
if (needPoll) {
await this._pollContextDocsAndFiles();
}
await this._pollEmbeddingStatus();
this._isLoading = false;
this._isInitialized = true;
};
private readonly _resetComposer = () => {
this._abortPoll();
this._abortPollEmbeddingStatus();
this.chips = [];
this._contextId = undefined;
this._isLoading = false;

View File

@@ -1,11 +1,7 @@
import { SignalWatcher } from '@blocksuite/affine/global/lit';
import { unsafeCSSVar } from '@blocksuite/affine/shared/theme';
import type { EditorHost } from '@blocksuite/affine/std';
import { css, html, LitElement } from 'lit';
import { property, state } from 'lit/decorators.js';
import { debounce, noop } from 'lodash-es';
import { AIProvider } from '../../provider/ai-provider';
import { property } from 'lit/decorators.js';
export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) {
static override styles = css`
@@ -38,47 +34,8 @@ export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) {
`;
@property({ attribute: false })
accessor host!: EditorHost;
@state()
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() {
return html`
<div
@@ -91,7 +48,6 @@ export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) {
<div
class="check-status"
data-testid="ai-chat-embedding-status-tooltip-check"
@mouseenter=${this._handleCheckStatusMouseEnter}
>
Check status
<affine-tooltip tip-position="top-start"

View File

@@ -4,6 +4,7 @@ import type { GlobalDialogService } from '@affine/core/modules/dialogs';
import {
type ChatHistoryOrder,
ContextCategories,
type ContextWorkspaceEmbeddingStatus,
type getCopilotHistoriesQuery,
type RequestOptions,
} 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));
}
},
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 (
content: 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);
});
AIProvider.provide('embedding', {
getEmbeddingStatus: (workspaceId: string) => {
return client.getEmbeddingStatus(workspaceId);
},
});
const disposeRequestLoginHandler = AIProvider.slots.requestLogin.subscribe(
() => {
globalDialogService.open('sign-in', {});