mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(core): embedding status tooltip (#12382)
### TL;DR feat: display embedding tip for ai chat  > CLOSE BS-3051 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced an embedding status tooltip in the AI chat interface, providing real-time feedback on embedding progress for your workspace. - Added support for embedding status tracking within the AI provider and client services. - **Style** - Updated the AI chat footer layout for improved clarity and usability. - **Tests** - Added an end-to-end test to ensure the embedding status tooltip displays correctly. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -13,6 +13,7 @@ import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import type { GfxModel } from '@blocksuite/affine/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/affine/store';
|
||||
|
||||
import type { AIEmbeddingStatus } from '../provider';
|
||||
import type { PromptKey } from '../provider/prompt';
|
||||
|
||||
export const translateLangs = [
|
||||
@@ -422,5 +423,9 @@ declare global {
|
||||
query: string;
|
||||
}): Promise<string[]>;
|
||||
}
|
||||
|
||||
interface AIEmbeddingService {
|
||||
getEmbeddingStatus(workspaceId: string): Promise<AIEmbeddingStatus>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,14 +39,19 @@ export class AIChatComposer extends SignalWatcher(
|
||||
static override styles = css`
|
||||
.chat-panel-footer {
|
||||
margin: 8px 0px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
color: var(--affine-text-secondary-color);
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ai-misleading-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -148,8 +153,8 @@ export class AIChatComposer extends SignalWatcher(
|
||||
.addImages=${this.addImages}
|
||||
></ai-chat-input>
|
||||
<div class="chat-panel-footer">
|
||||
${InformationIcon()}
|
||||
<div>AI outputs can be misleading or wrong</div>
|
||||
<div class="ai-misleading-info">${InformationIcon()} AI outputs can be misleading or wrong</div>
|
||||
<ai-chat-embedding-status-tooltip .host=${this.host} />
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { SignalWatcher } from '@blocksuite/affine/global/lit';
|
||||
import { unsafeCSSVar } from '@blocksuite/affine/shared/theme';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { InformationIcon } from '@blocksuite/icons/lit';
|
||||
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';
|
||||
|
||||
export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) {
|
||||
static override styles = css`
|
||||
.embedding-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
user-select: none;
|
||||
}
|
||||
.check-status {
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.check-status:hover {
|
||||
background-color: ${unsafeCSSVar('--affine-hover-color')};
|
||||
}
|
||||
`;
|
||||
|
||||
@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
|
||||
class="embedding-status"
|
||||
data-testid="ai-chat-embedding-status-tooltip"
|
||||
>
|
||||
${InformationIcon()} Better results after embedding finished.
|
||||
<span
|
||||
class="check-status"
|
||||
data-testid="ai-chat-embedding-status-tooltip-check"
|
||||
@mouseenter=${this._handleCheckStatusMouseEnter}
|
||||
>
|
||||
Check status
|
||||
</span>
|
||||
<affine-tooltip>${this.progressText}</affine-tooltip>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ai-chat-embedding-status-tooltip': AIChatEmbeddingStatusTooltip;
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import { ChatPanelFileChip } from './components/ai-chat-chips/file-chip';
|
||||
import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
|
||||
import { AIChatComposer } from './components/ai-chat-composer';
|
||||
import { AIChatInput } from './components/ai-chat-input';
|
||||
import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embedding-status-tooltip';
|
||||
import { AIChatModels } from './components/ai-chat-models/ai-chat-models';
|
||||
import { AIHistoryClear } from './components/ai-history-clear';
|
||||
import { effects as componentAiItemEffects } from './components/ai-item';
|
||||
@@ -98,6 +99,10 @@ export function registerAIEffects() {
|
||||
customElements.define('chat-panel-messages', ChatPanelMessages);
|
||||
customElements.define('chat-panel', ChatPanel);
|
||||
customElements.define('ai-chat-input', AIChatInput);
|
||||
customElements.define(
|
||||
'ai-chat-embedding-status-tooltip',
|
||||
AIChatEmbeddingStatusTooltip
|
||||
);
|
||||
customElements.define('ai-chat-composer', AIChatComposer);
|
||||
customElements.define('chat-panel-chips', ChatPanelChips);
|
||||
customElements.define('ai-history-clear', AIHistoryClear);
|
||||
|
||||
@@ -29,6 +29,11 @@ export interface AISendParams {
|
||||
context?: Partial<ChatContextValue | null>;
|
||||
}
|
||||
|
||||
export interface AIEmbeddingStatus {
|
||||
embedded: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export type ActionEventType =
|
||||
| 'started'
|
||||
| 'finished'
|
||||
@@ -96,6 +101,10 @@ export class AIProvider {
|
||||
return AIProvider.instance.forkChat;
|
||||
}
|
||||
|
||||
static get embedding() {
|
||||
return AIProvider.instance.embedding;
|
||||
}
|
||||
|
||||
private static readonly instance = new AIProvider();
|
||||
|
||||
static LAST_ACTION_SESSIONID = '';
|
||||
@@ -152,6 +161,8 @@ export class AIProvider {
|
||||
private userInfoFn: () => AIUserInfo | Promise<AIUserInfo> | null = () =>
|
||||
null;
|
||||
|
||||
private embedding: BlockSuitePresets.AIEmbeddingService | null = null;
|
||||
|
||||
private provideAction<T extends keyof BlockSuitePresets.AIActions>(
|
||||
id: T,
|
||||
action: (
|
||||
@@ -307,6 +318,11 @@ export class AIProvider {
|
||||
|
||||
static provide(id: 'onboarding', fn: (value: boolean) => void): void;
|
||||
|
||||
static provide(
|
||||
id: 'embedding',
|
||||
service: BlockSuitePresets.AIEmbeddingService
|
||||
): void;
|
||||
|
||||
// actions:
|
||||
static provide<T extends keyof BlockSuitePresets.AIActions>(
|
||||
id: T,
|
||||
@@ -338,6 +354,9 @@ export class AIProvider {
|
||||
AIProvider.instance.forkChat = action as (
|
||||
options: BlockSuitePresets.AIForkChatSessionOptions
|
||||
) => string | Promise<string>;
|
||||
} else if (id === 'embedding') {
|
||||
AIProvider.instance.embedding =
|
||||
action as BlockSuitePresets.AIEmbeddingService;
|
||||
} else {
|
||||
AIProvider.instance.provideAction(id as any, action as any);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getCopilotHistoryIdsQuery,
|
||||
getCopilotSessionQuery,
|
||||
getCopilotSessionsQuery,
|
||||
getWorkspaceEmbeddingStatusQuery,
|
||||
type GraphQLQuery,
|
||||
listContextObjectQuery,
|
||||
listContextQuery,
|
||||
@@ -460,4 +461,11 @@ export class CopilotClient {
|
||||
});
|
||||
return queryString.toString();
|
||||
}
|
||||
|
||||
getEmbeddingStatus(workspaceId: string) {
|
||||
return this.gql({
|
||||
query: getWorkspaceEmbeddingStatusQuery,
|
||||
variables: { workspaceId },
|
||||
}).then(res => res.queryWorkspaceEmbeddingStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,6 +796,12 @@ 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