mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-22 00:37:05 +08:00
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,
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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', {});
|
||||
|
||||
Reference in New Issue
Block a user