feat(core): center peek doc in chat semantic/keyword search result (#13380)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added the ability to preview documents directly from AI chat search
results using a new document peek view.
* Search result items in AI chat are now clickable, allowing for quick
document previews without leaving the chat interface.

* **Style**
* Updated clickable item styles in search results for improved visual
feedback and consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Cats Juice
2025-08-01 09:57:28 +08:00
committed by GitHub
parent 2990a96ec9
commit cd29028311
10 changed files with 86 additions and 2 deletions
@@ -4,6 +4,7 @@ import type {
} from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { PeekViewService } from '@affine/core/modules/peek-view';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { WorkbenchService } from '@affine/core/modules/workbench';
import type {
@@ -125,6 +126,9 @@ export class ChatPanel extends SignalWatcher(
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor peekViewService!: PeekViewService;
@state()
accessor session: CopilotChatHistoryFragment | null | undefined;
@@ -421,6 +425,7 @@ export class ChatPanel extends SignalWatcher(
.notificationService=${this.notificationService}
.aiDraftService=${this.aiDraftService}
.aiToolsConfigService=${this.aiToolsConfigService}
.peekViewService=${this.peekViewService}
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
.onContextChange=${this.onContextChange}
.width=${this.sidebarWidth}
@@ -1,4 +1,5 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { PeekViewService } from '@affine/core/modules/peek-view';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit';
@@ -86,6 +87,9 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor docDisplayService!: DocDisplayConfig;
@property({ attribute: false })
accessor peekViewService!: PeekViewService;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
@@ -150,6 +154,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
.theme=${this.affineThemeService.appTheme.themeSignal}
.independentMode=${this.independentMode}
.docDisplayService=${this.docDisplayService}
.peekViewService=${this.peekViewService}
.onOpenDoc=${this.onOpenDoc}
></chat-content-stream-objects>`;
}
@@ -5,6 +5,7 @@ import type {
import type { AIDraftState } from '@affine/core/modules/ai-button/services/ai-draft';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { PeekViewService } from '@affine/core/modules/peek-view';
import type { AppThemeService } from '@affine/core/modules/theme';
import type {
ContextEmbedStatus,
@@ -178,6 +179,9 @@ export class AIChatContent extends SignalWatcher(
@property({ attribute: false })
accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false })
accessor peekViewService!: PeekViewService;
@state()
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
@@ -427,6 +431,7 @@ export class AIChatContent extends SignalWatcher(
.independentMode=${this.independentMode}
.messages=${this.messages}
.docDisplayService=${this.docDisplayConfig}
.peekViewService=${this.peekViewService}
.onOpenDoc=${this.onOpenDoc}
></ai-chat-messages>
<ai-chat-composer
@@ -1,4 +1,5 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { PeekViewService } from '@affine/core/modules/peek-view';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit';
@@ -210,6 +211,9 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor peekViewService!: PeekViewService;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
@@ -340,6 +344,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
.width=${this.width}
.independentMode=${this.independentMode}
.docDisplayService=${this.docDisplayService}
.peekViewService=${this.peekViewService}
.onOpenDoc=${this.onOpenDoc}
></chat-message-assistant>`;
} else if (isChatAction(item) && this.host) {
@@ -1,4 +1,5 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { PeekViewService } from '@affine/core/modules/peek-view';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import type { ColorScheme } from '@blocksuite/affine/model';
import {
@@ -61,6 +62,9 @@ export class ChatContentStreamObjects extends WithDisposable(
@property({ attribute: false })
accessor docDisplayService!: DocDisplayConfig;
@property({ attribute: false })
accessor peekViewService!: PeekViewService;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
@@ -115,6 +119,7 @@ export class ChatContentStreamObjects extends WithDisposable(
return html`<doc-semantic-search-result
.data=${streamObject}
.width=${this.width}
.peekViewService=${this.peekViewService}
></doc-semantic-search-result>`;
case 'doc_keyword_search':
return html`<doc-keyword-search-result
@@ -201,12 +206,14 @@ export class ChatContentStreamObjects extends WithDisposable(
.data=${streamObject}
.width=${this.width}
.docDisplayService=${this.docDisplayService}
.peekViewService=${this.peekViewService}
.onOpenDoc=${this.onOpenDoc}
></doc-semantic-search-result>`;
case 'doc_keyword_search':
return html`<doc-keyword-search-result
.data=${streamObject}
.width=${this.width}
.peekViewService=${this.peekViewService}
.onOpenDoc=${this.onOpenDoc}
></doc-keyword-search-result>`;
case 'doc_read':
@@ -1,3 +1,4 @@
import type { PeekViewService } from '@affine/core/modules/peek-view';
import { WithDisposable } from '@blocksuite/global/lit';
import { PageIcon, SearchIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
@@ -41,6 +42,9 @@ export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
@property({ attribute: false })
accessor peekViewService!: PeekViewService;
renderToolCall() {
return html`<tool-call-card
.name=${`Searching workspace documents for "${this.data.args.query}"`}
@@ -63,6 +67,14 @@ export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
${item.title}
</span>`,
icon: PageIcon(),
onClick: () => {
this.peekViewService.peekView
.open({
type: 'doc',
docRef: { docId: item.docId },
})
.catch(console.error);
},
}));
} catch (err) {
console.error('Failed to parse result', err);
@@ -1,3 +1,4 @@
import type { PeekViewService } from '@affine/core/modules/peek-view';
import { WithDisposable } from '@blocksuite/global/lit';
import { AiEmbeddingIcon, PageIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
@@ -72,6 +73,9 @@ export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
@property({ attribute: false })
accessor peekViewService!: PeekViewService;
renderToolCall() {
return html`<tool-call-card
.name=${`Finding semantically related pages for "${this.data.args.query}"`}
@@ -97,6 +101,16 @@ export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
>
${this.docDisplayService.getTitle(result.docId)}
</span>`,
onClick: () => {
this.peekViewService.peekView
.open({
type: 'doc',
docRef: {
docId: result.docId,
},
})
.catch(console.error);
},
}))
.filter(Boolean)}
></tool-result-card>`;
@@ -13,6 +13,7 @@ export interface ToolResult {
icon?: string | TemplateResult<1>;
content?: string;
href?: string;
onClick?: () => void;
}
export class ToolResultCard extends SignalWatcher(
@@ -95,7 +96,8 @@ export class ToolResultCard extends SignalWatcher(
cursor: default;
}
.result-item[href] {
.result-item[href],
.result-item[data-clickable] {
cursor: pointer;
}
@@ -154,7 +156,9 @@ export class ToolResultCard extends SignalWatcher(
}
.result-item[href]:hover .result-title,
.result-item[href]:hover .result-content {
.result-item[href]:hover .result-content,
.result-item[data-clickable]:hover .result-title,
.result-item[data-clickable]:hover .result-content {
color: ${unsafeCSSVarV2('text/primary')};
}
@@ -184,6 +188,27 @@ export class ToolResultCard extends SignalWatcher(
color: ${unsafeCSSVarV2('text/primary')};
}
.result-icon,
.footer-icon {
width: 18px;
height: 18px;
border-radius: 100%;
background-color: ${unsafeCSSVarV2('layer/background/primary')};
img {
width: 18px;
height: 18px;
border-radius: 100%;
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
}
svg {
width: 18px;
height: 18px;
color: ${unsafeCSSVarV2('icon/primary')};
}
}
.footer-icons {
display: flex;
position: relative;
@@ -244,11 +269,13 @@ export class ToolResultCard extends SignalWatcher(
result => html`
<a
class="result-item"
data-clickable=${!!result.onClick}
href=${ifDefined(result.href)}
target=${ifDefined(result.href ? '_blank' : undefined)}
rel=${ifDefined(
result.href ? 'noopener noreferrer' : undefined
)}
@click=${result.onClick}
>
<div class="result-header">
<div class="result-title">${result.title}</div>
@@ -22,6 +22,7 @@ import {
} from '@affine/core/modules/cloud';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { PeekViewService } from '@affine/core/modules/peek-view';
import { AppThemeService } from '@affine/core/modules/theme';
import {
ViewBody,
@@ -220,6 +221,7 @@ export const Component = () => {
content.affineWorkspaceDialogService = framework.get(
WorkspaceDialogService
);
content.peekViewService = framework.get(PeekViewService);
content.affineThemeService = framework.get(AppThemeService);
content.notificationService = new NotificationServiceImpl(
confirmModal.closeConfirmModal,
@@ -10,6 +10,7 @@ import {
} from '@affine/core/modules/ai-button';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { PeekViewService } from '@affine/core/modules/peek-view';
import { AppThemeService } from '@affine/core/modules/theme';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
@@ -95,6 +96,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
chatPanelRef.current.affineWorkbenchService =
framework.get(WorkbenchService);
chatPanelRef.current.affineThemeService = framework.get(AppThemeService);
chatPanelRef.current.peekViewService = framework.get(PeekViewService);
chatPanelRef.current.notificationService = new NotificationServiceImpl(
confirmModal.closeConfirmModal,
confirmModal.openConfirmModal