mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-22 00:37:05 +08: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 { GfxModel } from '@blocksuite/affine/std/gfx';
|
||||||
import type { BlockModel } from '@blocksuite/affine/store';
|
import type { BlockModel } from '@blocksuite/affine/store';
|
||||||
|
|
||||||
|
import type { AIEmbeddingStatus } from '../provider';
|
||||||
import type { PromptKey } from '../provider/prompt';
|
import type { PromptKey } from '../provider/prompt';
|
||||||
|
|
||||||
export const translateLangs = [
|
export const translateLangs = [
|
||||||
@@ -422,5 +423,9 @@ declare global {
|
|||||||
query: string;
|
query: string;
|
||||||
}): Promise<string[]>;
|
}): Promise<string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AIEmbeddingService {
|
||||||
|
getEmbeddingStatus(workspaceId: string): Promise<AIEmbeddingStatus>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,14 +39,19 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
static override styles = css`
|
static override styles = css`
|
||||||
.chat-panel-footer {
|
.chat-panel-footer {
|
||||||
margin: 8px 0px;
|
margin: 8px 0px;
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-items: center;
|
|
||||||
color: var(--affine-text-secondary-color);
|
color: var(--affine-text-secondary-color);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-misleading-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -148,8 +153,8 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
.addImages=${this.addImages}
|
.addImages=${this.addImages}
|
||||||
></ai-chat-input>
|
></ai-chat-input>
|
||||||
<div class="chat-panel-footer">
|
<div class="chat-panel-footer">
|
||||||
${InformationIcon()}
|
<div class="ai-misleading-info">${InformationIcon()} AI outputs can be misleading or wrong</div>
|
||||||
<div>AI outputs can be misleading or wrong</div>
|
<ai-chat-embedding-status-tooltip .host=${this.host} />
|
||||||
</div>
|
</div>
|
||||||
</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 { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
|
||||||
import { AIChatComposer } from './components/ai-chat-composer';
|
import { AIChatComposer } from './components/ai-chat-composer';
|
||||||
import { AIChatInput } from './components/ai-chat-input';
|
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 { AIChatModels } from './components/ai-chat-models/ai-chat-models';
|
||||||
import { AIHistoryClear } from './components/ai-history-clear';
|
import { AIHistoryClear } from './components/ai-history-clear';
|
||||||
import { effects as componentAiItemEffects } from './components/ai-item';
|
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-messages', ChatPanelMessages);
|
||||||
customElements.define('chat-panel', ChatPanel);
|
customElements.define('chat-panel', ChatPanel);
|
||||||
customElements.define('ai-chat-input', AIChatInput);
|
customElements.define('ai-chat-input', AIChatInput);
|
||||||
|
customElements.define(
|
||||||
|
'ai-chat-embedding-status-tooltip',
|
||||||
|
AIChatEmbeddingStatusTooltip
|
||||||
|
);
|
||||||
customElements.define('ai-chat-composer', AIChatComposer);
|
customElements.define('ai-chat-composer', AIChatComposer);
|
||||||
customElements.define('chat-panel-chips', ChatPanelChips);
|
customElements.define('chat-panel-chips', ChatPanelChips);
|
||||||
customElements.define('ai-history-clear', AIHistoryClear);
|
customElements.define('ai-history-clear', AIHistoryClear);
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ export interface AISendParams {
|
|||||||
context?: Partial<ChatContextValue | null>;
|
context?: Partial<ChatContextValue | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AIEmbeddingStatus {
|
||||||
|
embedded: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type ActionEventType =
|
export type ActionEventType =
|
||||||
| 'started'
|
| 'started'
|
||||||
| 'finished'
|
| 'finished'
|
||||||
@@ -96,6 +101,10 @@ export class AIProvider {
|
|||||||
return AIProvider.instance.forkChat;
|
return AIProvider.instance.forkChat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get embedding() {
|
||||||
|
return AIProvider.instance.embedding;
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly instance = new AIProvider();
|
private static readonly instance = new AIProvider();
|
||||||
|
|
||||||
static LAST_ACTION_SESSIONID = '';
|
static LAST_ACTION_SESSIONID = '';
|
||||||
@@ -152,6 +161,8 @@ export class AIProvider {
|
|||||||
private userInfoFn: () => AIUserInfo | Promise<AIUserInfo> | null = () =>
|
private userInfoFn: () => AIUserInfo | Promise<AIUserInfo> | null = () =>
|
||||||
null;
|
null;
|
||||||
|
|
||||||
|
private embedding: BlockSuitePresets.AIEmbeddingService | null = null;
|
||||||
|
|
||||||
private provideAction<T extends keyof BlockSuitePresets.AIActions>(
|
private provideAction<T extends keyof BlockSuitePresets.AIActions>(
|
||||||
id: T,
|
id: T,
|
||||||
action: (
|
action: (
|
||||||
@@ -307,6 +318,11 @@ export class AIProvider {
|
|||||||
|
|
||||||
static provide(id: 'onboarding', fn: (value: boolean) => void): void;
|
static provide(id: 'onboarding', fn: (value: boolean) => void): void;
|
||||||
|
|
||||||
|
static provide(
|
||||||
|
id: 'embedding',
|
||||||
|
service: BlockSuitePresets.AIEmbeddingService
|
||||||
|
): void;
|
||||||
|
|
||||||
// actions:
|
// actions:
|
||||||
static provide<T extends keyof BlockSuitePresets.AIActions>(
|
static provide<T extends keyof BlockSuitePresets.AIActions>(
|
||||||
id: T,
|
id: T,
|
||||||
@@ -338,6 +354,9 @@ export class AIProvider {
|
|||||||
AIProvider.instance.forkChat = action as (
|
AIProvider.instance.forkChat = action as (
|
||||||
options: BlockSuitePresets.AIForkChatSessionOptions
|
options: BlockSuitePresets.AIForkChatSessionOptions
|
||||||
) => string | Promise<string>;
|
) => string | Promise<string>;
|
||||||
|
} else if (id === 'embedding') {
|
||||||
|
AIProvider.instance.embedding =
|
||||||
|
action as BlockSuitePresets.AIEmbeddingService;
|
||||||
} else {
|
} else {
|
||||||
AIProvider.instance.provideAction(id as any, action as any);
|
AIProvider.instance.provideAction(id as any, action as any);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
getCopilotHistoryIdsQuery,
|
getCopilotHistoryIdsQuery,
|
||||||
getCopilotSessionQuery,
|
getCopilotSessionQuery,
|
||||||
getCopilotSessionsQuery,
|
getCopilotSessionsQuery,
|
||||||
|
getWorkspaceEmbeddingStatusQuery,
|
||||||
type GraphQLQuery,
|
type GraphQLQuery,
|
||||||
listContextObjectQuery,
|
listContextObjectQuery,
|
||||||
listContextQuery,
|
listContextQuery,
|
||||||
@@ -460,4 +461,11 @@ export class CopilotClient {
|
|||||||
});
|
});
|
||||||
return queryString.toString();
|
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);
|
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', {});
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ test.describe('AIBasic/Chat', () => {
|
|||||||
await expect(page.getByTestId('ai-onboarding')).toBeVisible();
|
await expect(page.getByTestId('ai-onboarding')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should display embedding status tooltip', async ({
|
||||||
|
loggedInPage: page,
|
||||||
|
}) => {
|
||||||
|
const check = await page.getByTestId(
|
||||||
|
'ai-chat-embedding-status-tooltip-check'
|
||||||
|
);
|
||||||
|
await expect(check).toBeVisible();
|
||||||
|
|
||||||
|
await check.hover();
|
||||||
|
const tooltip = await page.getByTestId('ai-chat-embedding-status-tooltip');
|
||||||
|
await expect(tooltip).toBeVisible();
|
||||||
|
await expect(tooltip).toHaveText(/Results will improve after embedding/i);
|
||||||
|
});
|
||||||
|
|
||||||
test(`should send message and receive AI response:
|
test(`should send message and receive AI response:
|
||||||
- send message
|
- send message
|
||||||
- AI is loading
|
- AI is loading
|
||||||
|
|||||||
Reference in New Issue
Block a user