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:
@@ -411,6 +411,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
|
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
|
||||||
.onContextChange=${this.onContextChange}
|
.onContextChange=${this.onContextChange}
|
||||||
.width=${this.sidebarWidth}
|
.width=${this.sidebarWidth}
|
||||||
|
.onOpenDoc=${this.openDoc}
|
||||||
></ai-chat-content>`
|
></ai-chat-content>`
|
||||||
)}
|
)}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor docDisplayService!: DocDisplayConfig;
|
accessor docDisplayService!: DocDisplayConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
|
||||||
|
|
||||||
get state() {
|
get state() {
|
||||||
const { isLast, status } = this;
|
const { isLast, status } = this;
|
||||||
return isLast
|
return isLast
|
||||||
@@ -146,6 +149,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
|||||||
.notificationService=${this.notificationService}
|
.notificationService=${this.notificationService}
|
||||||
.theme=${this.affineThemeService.appTheme.themeSignal}
|
.theme=${this.affineThemeService.appTheme.themeSignal}
|
||||||
.docDisplayService=${this.docDisplayService}
|
.docDisplayService=${this.docDisplayService}
|
||||||
|
.onOpenDoc=${this.onOpenDoc}
|
||||||
></chat-content-stream-objects>`;
|
></chat-content-stream-objects>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ export class AIChatContent extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onContextChange!: (context: Partial<ChatContextValue>) => void;
|
accessor onContextChange!: (context: Partial<ChatContextValue>) => void;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor width: Signal<number | undefined> | undefined;
|
accessor width: Signal<number | undefined> | undefined;
|
||||||
|
|
||||||
@@ -378,6 +381,7 @@ export class AIChatContent extends SignalWatcher(
|
|||||||
.independentMode=${this.independentMode}
|
.independentMode=${this.independentMode}
|
||||||
.messages=${this.messages}
|
.messages=${this.messages}
|
||||||
.docDisplayService=${this.docDisplayConfig}
|
.docDisplayService=${this.docDisplayConfig}
|
||||||
|
.onOpenDoc=${this.onOpenDoc}
|
||||||
></ai-chat-messages>
|
></ai-chat-messages>
|
||||||
<ai-chat-composer
|
<ai-chat-composer
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
|
|||||||
@@ -206,6 +206,9 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor docDisplayService!: DocDisplayConfig;
|
accessor docDisplayService!: DocDisplayConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
|
||||||
|
|
||||||
@query('.chat-panel-messages-container')
|
@query('.chat-panel-messages-container')
|
||||||
accessor messagesContainer: HTMLDivElement | null = null;
|
accessor messagesContainer: HTMLDivElement | null = null;
|
||||||
|
|
||||||
@@ -333,6 +336,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
.width=${this.width}
|
.width=${this.width}
|
||||||
.independentMode=${this.independentMode}
|
.independentMode=${this.independentMode}
|
||||||
.docDisplayService=${this.docDisplayService}
|
.docDisplayService=${this.docDisplayService}
|
||||||
|
.onOpenDoc=${this.onOpenDoc}
|
||||||
></chat-message-assistant>`;
|
></chat-message-assistant>`;
|
||||||
} else if (isChatAction(item) && this.host) {
|
} else if (isChatAction(item) && this.host) {
|
||||||
return html`<chat-message-action
|
return html`<chat-message-action
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ export class ChatContentStreamObjects extends WithDisposable(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor docDisplayService!: DocDisplayConfig;
|
accessor docDisplayService!: DocDisplayConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
|
||||||
|
|
||||||
private renderToolCall(streamObject: StreamObject) {
|
private renderToolCall(streamObject: StreamObject) {
|
||||||
if (streamObject.type !== 'tool-call') {
|
if (streamObject.type !== 'tool-call') {
|
||||||
return nothing;
|
return nothing;
|
||||||
@@ -183,11 +186,13 @@ export class ChatContentStreamObjects extends WithDisposable(
|
|||||||
.data=${streamObject}
|
.data=${streamObject}
|
||||||
.width=${this.width}
|
.width=${this.width}
|
||||||
.docDisplayService=${this.docDisplayService}
|
.docDisplayService=${this.docDisplayService}
|
||||||
|
.onOpenDoc=${this.onOpenDoc}
|
||||||
></doc-semantic-search-result>`;
|
></doc-semantic-search-result>`;
|
||||||
case 'doc_keyword_search':
|
case 'doc_keyword_search':
|
||||||
return html`<doc-keyword-search-result
|
return html`<doc-keyword-search-result
|
||||||
.data=${streamObject}
|
.data=${streamObject}
|
||||||
.width=${this.width}
|
.width=${this.width}
|
||||||
|
.onOpenDoc=${this.onOpenDoc}
|
||||||
></doc-keyword-search-result>`;
|
></doc-keyword-search-result>`;
|
||||||
case 'doc_read':
|
case 'doc_read':
|
||||||
return html`<doc-read-result
|
return html`<doc-read-result
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { WithDisposable } from '@blocksuite/global/lit';
|
|||||||
import { PageIcon, SearchIcon } from '@blocksuite/icons/lit';
|
import { PageIcon, SearchIcon } from '@blocksuite/icons/lit';
|
||||||
import { ShadowlessElement } from '@blocksuite/std';
|
import { ShadowlessElement } from '@blocksuite/std';
|
||||||
import type { Signal } from '@preact/signals-core';
|
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 { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
import type { ToolResult } from './tool-result-card';
|
import type { ToolResult } from './tool-result-card';
|
||||||
@@ -26,12 +26,21 @@ interface DocKeywordSearchToolResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
|
export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
|
||||||
|
static override styles = css`
|
||||||
|
.doc-keyword-search-result-title {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor data!: DocKeywordSearchToolCall | DocKeywordSearchToolResult;
|
accessor data!: DocKeywordSearchToolCall | DocKeywordSearchToolResult;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor width: Signal<number | undefined> | undefined;
|
accessor width: Signal<number | undefined> | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
|
||||||
|
|
||||||
renderToolCall() {
|
renderToolCall() {
|
||||||
return html`<tool-call-card
|
return html`<tool-call-card
|
||||||
.name=${`Searching workspace documents for "${this.data.args.query}"`}
|
.name=${`Searching workspace documents for "${this.data.args.query}"`}
|
||||||
@@ -47,7 +56,12 @@ export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
|
|||||||
let results: ToolResult[] = [];
|
let results: ToolResult[] = [];
|
||||||
try {
|
try {
|
||||||
results = this.data.result.map(item => ({
|
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(),
|
icon: PageIcon(),
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { WithDisposable } from '@blocksuite/global/lit';
|
|||||||
import { AiEmbeddingIcon, PageIcon } from '@blocksuite/icons/lit';
|
import { AiEmbeddingIcon, PageIcon } from '@blocksuite/icons/lit';
|
||||||
import { ShadowlessElement } from '@blocksuite/std';
|
import { ShadowlessElement } from '@blocksuite/std';
|
||||||
import type { Signal } from '@preact/signals-core';
|
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 { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
import type { DocDisplayConfig } from '../ai-chat-chips';
|
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||||
@@ -54,6 +54,12 @@ function parseResultContent(content: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
|
export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
|
||||||
|
static override styles = css`
|
||||||
|
.doc-semantic-search-result-title {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor data!: DocSemanticSearchToolCall | DocSemanticSearchToolResult;
|
accessor data!: DocSemanticSearchToolCall | DocSemanticSearchToolResult;
|
||||||
|
|
||||||
@@ -63,6 +69,9 @@ export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor docDisplayService!: DocDisplayConfig;
|
accessor docDisplayService!: DocDisplayConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
|
||||||
|
|
||||||
renderToolCall() {
|
renderToolCall() {
|
||||||
return html`<tool-call-card
|
return html`<tool-call-card
|
||||||
.name=${`Finding semantically related pages for "${this.data.args.query}"`}
|
.name=${`Finding semantically related pages for "${this.data.args.query}"`}
|
||||||
@@ -82,7 +91,12 @@ export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
|
|||||||
.results=${this.data.result
|
.results=${this.data.result
|
||||||
.map(result => ({
|
.map(result => ({
|
||||||
...parseResultContent(result.content),
|
...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)}
|
.filter(Boolean)}
|
||||||
></tool-result-card>`;
|
></tool-result-card>`;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { css, html, nothing, type TemplateResult } from 'lit';
|
|||||||
import { property, state } from 'lit/decorators.js';
|
import { property, state } from 'lit/decorators.js';
|
||||||
|
|
||||||
export interface ToolResult {
|
export interface ToolResult {
|
||||||
title: string;
|
title: string | TemplateResult<1>;
|
||||||
icon?: string | TemplateResult<1>;
|
icon?: string | TemplateResult<1>;
|
||||||
content?: string;
|
content?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const Component = () => {
|
|||||||
const chatToolContainerRef = useRef<HTMLDivElement>(null);
|
const chatToolContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const widthSignalRef = useRef<Signal<number>>(signal(0));
|
const widthSignalRef = useRef<Signal<number>>(signal(0));
|
||||||
const client = useCopilotClient();
|
const client = useCopilotClient();
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
|
||||||
const workspaceId = useService(WorkspaceService).workspace.id;
|
const workspaceId = useService(WorkspaceService).workspace.id;
|
||||||
|
|
||||||
@@ -173,6 +174,13 @@ export const Component = () => {
|
|||||||
setStatus(context.status ?? 'idle');
|
setStatus(context.status ?? 'idle');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onOpenDoc = useCallback(
|
||||||
|
(docId: string) => {
|
||||||
|
workbench.openDoc(docId, { at: 'active' });
|
||||||
|
},
|
||||||
|
[workbench]
|
||||||
|
);
|
||||||
|
|
||||||
const confirmModal = useConfirmModal();
|
const confirmModal = useConfirmModal();
|
||||||
const specs = useAISpecs();
|
const specs = useAISpecs();
|
||||||
const mockStd = useMockStd();
|
const mockStd = useMockStd();
|
||||||
@@ -208,6 +216,7 @@ export const Component = () => {
|
|||||||
confirmModal.openConfirmModal
|
confirmModal.openConfirmModal
|
||||||
);
|
);
|
||||||
content.createSession = createSession;
|
content.createSession = createSession;
|
||||||
|
content.onOpenDoc = onOpenDoc;
|
||||||
|
|
||||||
if (!chatContent) {
|
if (!chatContent) {
|
||||||
// initial values that won't change
|
// initial values that won't change
|
||||||
@@ -232,6 +241,7 @@ export const Component = () => {
|
|||||||
confirmModal,
|
confirmModal,
|
||||||
onContextChange,
|
onContextChange,
|
||||||
specs,
|
specs,
|
||||||
|
onOpenDoc,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// init or update header ai-chat-toolbar
|
// init or update header ai-chat-toolbar
|
||||||
|
|||||||
Reference in New Issue
Block a user