feat(core): open doc in semantic and keyword result (#13217)

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

## Summary by CodeRabbit

* **New Features**
* Added clickable document titles in AI chat search results, allowing
users to open documents directly from chat interactions.
* Enhanced interactivity in AI chat by making relevant search result
titles visually indicate clickability (pointer cursor).

* **Style**
* Updated styles to visually highlight clickable search result titles in
AI chat results.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Cats Juice
2025-07-15 16:06:17 +08:00
committed by GitHub
parent 613597e642
commit cd91bea5c1
9 changed files with 61 additions and 5 deletions

View File

@@ -411,6 +411,7 @@ export class ChatPanel extends SignalWatcher(
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
.onContextChange=${this.onContextChange}
.width=${this.sidebarWidth}
.onOpenDoc=${this.openDoc}
></ai-chat-content>`
)}
</div>`;

View File

@@ -86,6 +86,9 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor docDisplayService!: DocDisplayConfig;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
get state() {
const { isLast, status } = this;
return isLast
@@ -146,6 +149,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
.notificationService=${this.notificationService}
.theme=${this.affineThemeService.appTheme.themeSignal}
.docDisplayService=${this.docDisplayService}
.onOpenDoc=${this.onOpenDoc}
></chat-content-stream-objects>`;
}

View File

@@ -157,6 +157,9 @@ export class AIChatContent extends SignalWatcher(
@property({ attribute: false })
accessor onContextChange!: (context: Partial<ChatContextValue>) => void;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
@property({ attribute: false })
accessor width: Signal<number | undefined> | undefined;
@@ -378,6 +381,7 @@ export class AIChatContent extends SignalWatcher(
.independentMode=${this.independentMode}
.messages=${this.messages}
.docDisplayService=${this.docDisplayConfig}
.onOpenDoc=${this.onOpenDoc}
></ai-chat-messages>
<ai-chat-composer
style=${styleMap({

View File

@@ -206,6 +206,9 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor docDisplayService!: DocDisplayConfig;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
@query('.chat-panel-messages-container')
accessor messagesContainer: HTMLDivElement | null = null;
@@ -333,6 +336,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
.width=${this.width}
.independentMode=${this.independentMode}
.docDisplayService=${this.docDisplayService}
.onOpenDoc=${this.onOpenDoc}
></chat-message-assistant>`;
} else if (isChatAction(item) && this.host) {
return html`<chat-message-action

View File

@@ -58,6 +58,9 @@ export class ChatContentStreamObjects extends WithDisposable(
@property({ attribute: false })
accessor docDisplayService!: DocDisplayConfig;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
private renderToolCall(streamObject: StreamObject) {
if (streamObject.type !== 'tool-call') {
return nothing;
@@ -183,11 +186,13 @@ export class ChatContentStreamObjects extends WithDisposable(
.data=${streamObject}
.width=${this.width}
.docDisplayService=${this.docDisplayService}
.onOpenDoc=${this.onOpenDoc}
></doc-semantic-search-result>`;
case 'doc_keyword_search':
return html`<doc-keyword-search-result
.data=${streamObject}
.width=${this.width}
.onOpenDoc=${this.onOpenDoc}
></doc-keyword-search-result>`;
case 'doc_read':
return html`<doc-read-result

View File

@@ -2,7 +2,7 @@ import { WithDisposable } from '@blocksuite/global/lit';
import { PageIcon, SearchIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
import type { Signal } from '@preact/signals-core';
import { html, nothing } from 'lit';
import { css, html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import type { ToolResult } from './tool-result-card';
@@ -26,12 +26,21 @@ interface DocKeywordSearchToolResult {
}
export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
static override styles = css`
.doc-keyword-search-result-title {
cursor: pointer;
}
`;
@property({ attribute: false })
accessor data!: DocKeywordSearchToolCall | DocKeywordSearchToolResult;
@property({ attribute: false })
accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
renderToolCall() {
return html`<tool-call-card
.name=${`Searching workspace documents for "${this.data.args.query}"`}
@@ -47,7 +56,12 @@ export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
let results: ToolResult[] = [];
try {
results = this.data.result.map(item => ({
title: item.title,
title: html`<span
class="doc-keyword-search-result-title"
@click=${() => this.onOpenDoc(item.docId)}
>
${item.title}
</span>`,
icon: PageIcon(),
}));
} catch (err) {

View File

@@ -2,7 +2,7 @@ import { WithDisposable } from '@blocksuite/global/lit';
import { AiEmbeddingIcon, PageIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
import type { Signal } from '@preact/signals-core';
import { html, nothing } from 'lit';
import { css, html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import type { DocDisplayConfig } from '../ai-chat-chips';
@@ -54,6 +54,12 @@ function parseResultContent(content: string) {
}
export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
static override styles = css`
.doc-semantic-search-result-title {
cursor: pointer;
}
`;
@property({ attribute: false })
accessor data!: DocSemanticSearchToolCall | DocSemanticSearchToolResult;
@@ -63,6 +69,9 @@ export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor docDisplayService!: DocDisplayConfig;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
renderToolCall() {
return html`<tool-call-card
.name=${`Finding semantically related pages for "${this.data.args.query}"`}
@@ -82,7 +91,12 @@ export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
.results=${this.data.result
.map(result => ({
...parseResultContent(result.content),
title: this.docDisplayService.getTitle(result.docId),
title: html`<span
class="doc-semantic-search-result-title"
@click=${() => this.onOpenDoc(result.docId)}
>
${this.docDisplayService.getTitle(result.docId)}
</span>`,
}))
.filter(Boolean)}
></tool-result-card>`;

View File

@@ -8,7 +8,7 @@ import { css, html, nothing, type TemplateResult } from 'lit';
import { property, state } from 'lit/decorators.js';
export interface ToolResult {
title: string;
title: string | TemplateResult<1>;
icon?: string | TemplateResult<1>;
content?: string;
}

View File

@@ -94,6 +94,7 @@ export const Component = () => {
const chatToolContainerRef = useRef<HTMLDivElement>(null);
const widthSignalRef = useRef<Signal<number>>(signal(0));
const client = useCopilotClient();
const workbench = useService(WorkbenchService).workbench;
const workspaceId = useService(WorkspaceService).workspace.id;
@@ -173,6 +174,13 @@ export const Component = () => {
setStatus(context.status ?? 'idle');
}, []);
const onOpenDoc = useCallback(
(docId: string) => {
workbench.openDoc(docId, { at: 'active' });
},
[workbench]
);
const confirmModal = useConfirmModal();
const specs = useAISpecs();
const mockStd = useMockStd();
@@ -208,6 +216,7 @@ export const Component = () => {
confirmModal.openConfirmModal
);
content.createSession = createSession;
content.onOpenDoc = onOpenDoc;
if (!chatContent) {
// initial values that won't change
@@ -232,6 +241,7 @@ export const Component = () => {
confirmModal,
onContextChange,
specs,
onOpenDoc,
]);
// init or update header ai-chat-toolbar