feat(core): support chat panel chips and suggest current doc for embedding (#9747)

Support issue [BS-2347](https://linear.app/affine-design/issue/BS-2347).

## What Changed?
- Add chat panel components
  - `chat-panel-chips`
  - `chat-panel-doc-chip`
  - `chat-panel-file-chip`
  - `chat-panel-chip`
- Add `chips` and `docs` field in `ChatContextValue`
- Add `extractMarkdownFromDoc` function to extract markdown content of a doc
- Add e2e test

Click a candidate card to add it into AI chat context:
<div class='graphite__hidden'>
          <div>🎥 Video uploaded on Graphite:</div>
            <a href="https://app.graphite.dev/media/video/sJGviKxfE3Ap685cl5bj/4e6b11ef-f993-4e6a-9f40-b2826af1990c.mov">
              <img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/sJGviKxfE3Ap685cl5bj/4e6b11ef-f993-4e6a-9f40-b2826af1990c.mov">
            </a>
          </div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/4e6b11ef-f993-4e6a-9f40-b2826af1990c.mov">录屏2025-01-17 01.02.04.mov</video>
This commit is contained in:
akumatus
2025-01-18 08:35:19 +00:00
parent 59611fa002
commit d048ac6c91
14 changed files with 656 additions and 35 deletions

View File

@@ -0,0 +1,17 @@
import type { Store } from '@blocksuite/store';
import type { Signal } from '@preact/signals-core';
export interface AINetworkSearchConfig {
visible: Signal<boolean | undefined>;
enabled: Signal<boolean | undefined>;
setEnabled: (state: boolean) => void;
}
export interface DocDisplayConfig {
getIcon: (docId: string) => any;
getTitle: (docId: string) => {
signal: Signal<string>;
cleanup: () => void;
};
getDoc: (docId: string) => Store | null;
}

View File

@@ -24,13 +24,28 @@ export type ChatStatus =
| 'idle'
| 'transmitting';
export interface DocContext {
docId: string;
plaintext?: string;
markdown?: string;
images?: File[];
}
export type ChatContextValue = {
// history messages of the chat
items: ChatItem[];
status: ChatStatus;
error: AIError | null;
// plain-text of the selected content
quote: string;
// markdown of the selected content
markdown: string;
// images of the selected content or user uploaded
images: File[];
// chips of workspace doc or user uploaded file
chips: ChatChip[];
// content of selected workspace doc
docs: DocContext[];
abortController: AbortController | null;
chatSessionId: string | null;
};
@@ -40,3 +55,34 @@ export type ChatBlockMessage = ChatMessage & {
userName?: string;
avatarUrl?: string;
};
export type ChipState =
| 'candidate'
| 'uploading'
| 'embedding'
| 'success'
| 'failed';
export interface BaseChip {
/**
* candidate: the chip is a candidate for the chat
* uploading: the chip is uploading
* embedding: the chip is embedding
* success: the chip is successfully embedded
* failed: the chip is failed to embed
*/
state: ChipState;
tooltip?: string;
}
export interface DocChip extends BaseChip {
docId: string;
}
export interface FileChip extends BaseChip {
fileName: string;
fileId: string;
fileType: string;
}
export type ChatChip = DocChip | FileChip;

View File

@@ -0,0 +1,59 @@
import {
type EditorHost,
ShadowlessElement,
} from '@blocksuite/affine/block-std';
import { WithDisposable } from '@blocksuite/affine/global/utils';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import type { DocDisplayConfig } from './chat-config';
import type { ChatContextValue } from './chat-context';
import { getChipKey, isDocChip, isFileChip } from './components/utils';
export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
static override styles = css`
.chip-list {
display: flex;
flex-wrap: wrap;
}
`;
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor chatContextValue!: ChatContextValue;
@property({ attribute: false })
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
@property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig;
override render() {
return html`<div class="chip-list">
${repeat(
this.chatContextValue.chips,
chip => getChipKey(chip),
chip => {
if (isDocChip(chip)) {
return html`<chat-panel-doc-chip
.chip=${chip}
.docDisplayConfig=${this.docDisplayConfig}
.host=${this.host}
.chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext}
></chat-panel-doc-chip>`;
}
if (isFileChip(chip)) {
return html`<chat-panel-file-chip
.chip=${chip}
></chat-panel-file-chip>`;
}
return null;
}
)}
</div>`;
}
}

View File

@@ -8,7 +8,6 @@ import {
} from '@blocksuite/affine/global/utils';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { ImageIcon, PublishIcon } from '@blocksuite/icons/lit';
import type { Signal } from '@preact/signals-core';
import { css, html, LitElement, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
@@ -22,6 +21,7 @@ import {
import { AIProvider } from '../provider';
import { reportResponse } from '../utils/action-reporter';
import { readBlobAsURL } from '../utils/image';
import type { AINetworkSearchConfig } from './chat-config';
import type { ChatContextValue, ChatMessage } from './chat-context';
const MaximumImageCount = 32;
@@ -31,12 +31,6 @@ function getFirstTwoLines(text: string) {
return lines.slice(0, 2);
}
export interface AINetworkSearchConfig {
visible: Signal<boolean | undefined>;
enabled: Signal<boolean | undefined>;
setEnabled: (state: boolean) => void;
}
export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
static override styles = css`
.chat-panel-input {
@@ -510,7 +504,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
};
send = async (text: string) => {
const { status, markdown } = this.chatContextValue;
const { status, markdown, docs } = this.chatContextValue;
if (status === 'loading' || status === 'transmitting') return;
const { images } = this.chatContextValue;
@@ -531,7 +525,8 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
images?.map(image => readBlobAsURL(image))
);
const content = (markdown ? `${markdown}\n` : '') + text;
const refDocs = docs.map(doc => doc.markdown).join('\n');
const content = (markdown ? `${markdown}\n` : '') + `${refDocs}\n` + text;
this.updateContext({
items: [

View File

@@ -0,0 +1,106 @@
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/utils';
import { CloseIcon } from '@blocksuite/icons/lit';
import { css, html, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
import type { ChipState } from '../chat-context';
export class ChatPanelChip extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css`
.chip-card {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
margin: 4px;
border-radius: 4px;
border: 0.5px solid var(--affine-border-color);
background: var(--affine-background-primary-color);
}
.chip-card[data-state='candidate'] {
border-width: 0.5px;
border-style: dashed;
background: var(--affine-background-secondary-color);
}
.chip-card[data-state='failed'] {
color: var(--affine-error-color);
background: var(--affine-background-error-color);
}
.chip-card[data-state='failed'] svg {
color: var(--affine-error-color);
}
.chip-card svg {
width: 16px;
height: 16px;
color: var(--affine-v2-icon-primary);
}
.chip-card-title {
display: inline-block;
margin: 0 4px;
font-size: 12px;
min-width: 16px;
max-width: 124px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.chip-card-close {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 4px;
}
.chip-card-close:hover {
background: var(--affine-hover-color);
}
`;
@property({ attribute: false })
accessor state!: ChipState;
@property({ attribute: false })
accessor name!: string;
@property({ attribute: false })
accessor tooltip!: string;
@property({ attribute: false })
accessor icon!: TemplateResult<1>;
@property({ attribute: false })
accessor closeable: boolean = false;
@property({ attribute: false })
accessor onChipDelete: () => void = () => {};
@property({ attribute: false })
accessor onChipClick: () => void = () => {};
override render() {
return html`
<div
class="chip-card"
data-testid="chat-panel-chip"
data-state=${this.state}
>
${this.icon}
<span class="chip-card-title" @click=${this.onChipClick}>
<affine-tooltip>${this.tooltip}</affine-tooltip>
<span data-testid="chat-panel-chip-title">${this.name}</span>
</span>
${this.closeable
? html`
<div class="chip-card-close" @click=${this.onChipDelete}>
${CloseIcon()}
</div>
`
: ''}
</div>
`;
}
}

View File

@@ -0,0 +1,127 @@
import {
type EditorHost,
ShadowlessElement,
} from '@blocksuite/affine/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/utils';
import { Signal } from '@preact/signals-core';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { extractMarkdownFromDoc } from '../../utils/extract';
import type { DocDisplayConfig } from '../chat-config';
import type { ChatContextValue, DocChip } from '../chat-context';
import { getChipIcon, getChipTooltip, isDocChip } from './utils';
export class ChatPanelDocChip extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
@property({ attribute: false })
accessor chip!: DocChip;
@property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor chatContextValue!: ChatContextValue;
@property({ attribute: false })
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
private name = new Signal<string>('');
private cleanup?: () => any;
override connectedCallback() {
super.connectedCallback();
const { signal, cleanup } = this.docDisplayConfig.getTitle(this.chip.docId);
this.name = signal;
this.cleanup = cleanup;
}
override disconnectedCallback() {
super.disconnectedCallback();
this.cleanup?.();
}
private readonly onChipClick = () => {
if (this.chip.state === 'candidate') {
const doc = this.docDisplayConfig.getDoc(this.chip.docId);
if (!doc) {
return;
}
this.updateChipContext({
state: 'embedding',
});
extractMarkdownFromDoc(doc, this.host.std.provider)
.then(result => {
this.updateChipContext({
state: 'success',
});
this.updateContext({
docs: [...this.chatContextValue.docs, result],
});
})
.catch(e => {
this.updateChipContext({
state: 'failed',
tooltip: e.message,
});
});
}
};
private updateChipContext(options: Partial<DocChip>) {
const index = this.chatContextValue.chips.findIndex(item => {
return isDocChip(item) && item.docId === this.chip.docId;
});
const nextChip: DocChip = {
...this.chip,
...options,
};
this.updateContext({
chips: [
...this.chatContextValue.chips.slice(0, index),
nextChip,
...this.chatContextValue.chips.slice(index + 1),
],
});
}
private readonly onChipDelete = () => {
if (this.chip.state === 'success') {
this.updateContext({
docs: this.chatContextValue.docs.filter(
doc => doc.docId !== this.chip.docId
),
});
}
this.updateContext({
chips: this.chatContextValue.chips.filter(
chip => isDocChip(chip) && chip.docId !== this.chip.docId
),
});
};
override render() {
const { state, docId } = this.chip;
const isLoading = state === 'embedding' || state === 'uploading';
const getIcon = this.docDisplayConfig.getIcon(docId);
const docIcon = typeof getIcon === 'function' ? getIcon() : getIcon;
const icon = getChipIcon(state, docIcon);
const tooltip = getChipTooltip(state, this.name.value, this.chip.tooltip);
return html`<chat-panel-chip
.state=${state}
.name=${this.name.value}
.tooltip=${tooltip}
.icon=${icon}
.closeable=${!isLoading}
.onChipClick=${this.onChipClick}
.onChipDelete=${this.onChipDelete}
></chat-panel-chip>`;
}
}

View File

@@ -0,0 +1,31 @@
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/utils';
import { getAttachmentFileIcons } from '@blocksuite/affine-components/icons';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import type { FileChip } from '../chat-context';
import { getChipIcon, getChipTooltip } from './utils';
export class ChatPanelFileChip extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
@property({ attribute: false })
accessor chip!: FileChip;
override render() {
const { state, fileName, fileType } = this.chip;
const isLoading = state === 'embedding' || state === 'uploading';
const tooltip = getChipTooltip(state, fileName, this.chip.tooltip);
const fileIcon = getAttachmentFileIcons(fileType);
const icon = getChipIcon(state, fileIcon);
return html`<chat-panel-chip
.state=${state}
.name=${fileName}
.tooltip=${tooltip}
.icon=${icon}
.closeable=${!isLoading}
></chat-panel-chip>`;
}
}

View File

@@ -0,0 +1,61 @@
import { WarningIcon } from '@blocksuite/icons/lit';
import { type TemplateResult } from 'lit';
import { LoadingIcon } from '../../../blocks/_common/icon';
import type { ChatChip, ChipState, DocChip, FileChip } from '../chat-context';
export function getChipTooltip(
state: ChipState,
title: string,
tooltip?: string
) {
if (tooltip) {
return tooltip;
}
if (state === 'candidate') {
return 'Click to add doc';
}
if (state === 'embedding') {
return 'Embedding...';
}
if (state === 'uploading') {
return 'Uploading...';
}
if (state === 'failed') {
return 'Failed to embed';
}
return title;
}
export function getChipIcon(
state: ChipState,
icon: TemplateResult<1>
): TemplateResult<1> {
const isLoading = state === 'embedding' || state === 'uploading';
const isFailed = state === 'failed';
if (isFailed) {
return WarningIcon();
}
if (isLoading) {
return LoadingIcon;
}
return icon;
}
export function isDocChip(chip: ChatChip): chip is DocChip {
return 'docId' in chip;
}
export function isFileChip(chip: ChatChip): chip is FileChip {
return 'fileId' in chip;
}
export function getChipKey(chip: ChatChip) {
if (isDocChip(chip)) {
return chip.docId;
}
if (isFileChip(chip)) {
return chip.fileId;
}
return null;
}

View File

@@ -17,8 +17,13 @@ import {
getSelectedImagesAsBlobs,
getSelectedTextContent,
} from '../utils/selection-utils';
import type { ChatAction, ChatContextValue, ChatItem } from './chat-context';
import type { AINetworkSearchConfig } from './chat-panel-input';
import type { AINetworkSearchConfig, DocDisplayConfig } from './chat-config';
import type {
ChatAction,
ChatContextValue,
ChatItem,
DocChip,
} from './chat-context';
import type { ChatPanelMessages } from './chat-panel-messages';
export class ChatPanel extends WithDisposable(ShadowlessElement) {
@@ -125,6 +130,13 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
AIProvider.LAST_ROOT_SESSION_ID = history.sessionId;
}
const { chips } = this.chatContextValue;
const defaultChip: DocChip = {
docId: this.doc.id,
state: 'candidate',
};
const nextChips =
items.length === 0 && chips.length === 0 ? [defaultChip] : chips;
this.chatContextValue = {
...this.chatContextValue,
items: items.sort((a, b) => {
@@ -132,6 +144,7 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
}),
chips: nextChips,
};
this.isLoading = false;
@@ -148,6 +161,9 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig;
@property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig;
@state()
accessor isLoading = false;
@@ -157,6 +173,8 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
images: [],
abortController: null,
items: [],
chips: [],
docs: [],
status: 'idle',
error: null,
markdown: '',
@@ -198,6 +216,9 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
if (_changedProperties.has('doc')) {
requestAnimationFrame(() => {
this.chatContextValue.chatSessionId = null;
// TODO get from CopilotContext
this.chatContextValue.chips = [];
this.chatContextValue.docs = [];
this._resetItems();
});
}
@@ -281,6 +302,12 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
.host=${this.host}
.isLoading=${this.isLoading}
></chat-panel-messages>
<chat-panel-chips
.host=${this.host}
.chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext}
.docDisplayConfig=${this.docDisplayConfig}
></chat-panel-chips>
<chat-panel-input
.chatContextValue=${this.chatContextValue}
.networkSearchConfig=${this.networkSearchConfig}

View File

@@ -1,20 +1,27 @@
import type { EditorHost } from '@blocksuite/affine/block-std';
import {
BlocksUtils,
DocModeProvider,
embedSyncedDocMiddleware,
type ImageBlockModel,
isInsideEdgelessEditor,
MarkdownAdapter,
type NoteBlockModel,
NoteDisplayMode,
titleMiddleware,
} from '@blocksuite/affine/blocks';
import type { BlockModel } from '@blocksuite/affine/store';
import type { BlockModel, Store } from '@blocksuite/affine/store';
import { Slice, toDraftModel, Transformer } from '@blocksuite/affine/store';
import type { ServiceProvider } from '@blocksuite/global/di';
import type { ChatContextValue } from '../chat-panel/chat-context';
import type { ChatContextValue, DocContext } from '../chat-panel/chat-context';
import {
allToCanvas,
getSelectedImagesAsBlobs,
getSelectedTextContent,
getTextContentFromBlockModels,
selectedToCanvas,
traverse,
} from './selection-utils';
export async function extractSelectedContent(
@@ -95,7 +102,7 @@ export async function extractAllContent(
}
}
async function extractEdgelessAll(
export async function extractEdgelessAll(
host: EditorHost
): Promise<Partial<ChatContextValue> | null> {
if (!isInsideEdgelessEditor(host)) return null;
@@ -113,21 +120,10 @@ async function extractEdgelessAll(
};
}
async function extractPageAll(
export async function extractPageAll(
host: EditorHost
): Promise<Partial<ChatContextValue> | null> {
const notes = host.doc
.getBlocksByFlavour('affine:note')
.filter(
note =>
(note.model as NoteBlockModel).displayMode !==
NoteDisplayMode.EdgelessOnly
)
.map(note => note.model as NoteBlockModel);
const blockModels = notes.reduce((acc, note) => {
acc.push(...note.children);
return acc;
}, [] as BlockModel[]);
const blockModels = getNoteBlockModels(host.doc);
const text = await getTextContentFromBlockModels(
host,
blockModels,
@@ -156,3 +152,64 @@ async function extractPageAll(
images,
};
}
export async function extractMarkdownFromDoc(
doc: Store,
provider: ServiceProvider
): Promise<DocContext> {
const transformer = await getTransformer(doc);
const adapter = new MarkdownAdapter(transformer, provider);
const blockModels = getNoteBlockModels(doc);
const textModels = blockModels.filter(
model =>
!BlocksUtils.matchFlavours(model, ['affine:image', 'affine:database'])
);
const drafts = textModels.map(toDraftModel);
drafts.forEach(draft => traverse(draft, drafts));
const slice = Slice.fromModels(doc, drafts);
const snapshot = transformer.sliceToSnapshot(slice);
if (!snapshot) {
throw new Error('Failed to extract markdown, snapshot is undefined');
}
const content = await adapter.fromSliceSnapshot({
snapshot,
assets: transformer.assetsManager,
});
return {
docId: doc.id,
markdown: content.file,
};
}
function getNoteBlockModels(doc: Store) {
const notes = doc
.getBlocksByFlavour('affine:note')
.filter(
note =>
(note.model as NoteBlockModel).displayMode !==
NoteDisplayMode.EdgelessOnly
)
.map(note => note.model as NoteBlockModel);
const blockModels = notes.reduce((acc, note) => {
acc.push(...note.children);
return acc;
}, [] as BlockModel[]);
return blockModels;
}
async function getTransformer(doc: Store) {
return new Transformer({
schema: doc.workspace.schema,
blobCRUD: doc.workspace.blobSync,
docCRUD: {
create: (id: string) => doc.workspace.createDoc({ id }),
get: (id: string) => doc.workspace.getDoc(id),
delete: (id: string) => doc.workspace.removeDoc(id),
},
middlewares: [
titleMiddleware(doc.workspace.meta.docMetas),
embedSyncedDocMiddleware('content'),
],
});
}

View File

@@ -105,7 +105,7 @@ export function getSelectedModels(editorHost: EditorHost) {
return selectedModels;
}
function traverse(model: DraftModel, drafts: DraftModel[]) {
export function traverse(model: DraftModel, drafts: DraftModel[]) {
const isDatabase = model.flavour === 'affine:database';
const children = isDatabase
? model.children

View File

@@ -15,8 +15,12 @@ import { ActionMindmap } from './ai/chat-panel/actions/mindmap';
import { ActionSlides } from './ai/chat-panel/actions/slides';
import { ActionText } from './ai/chat-panel/actions/text';
import { AILoading } from './ai/chat-panel/ai-loading';
import { ChatPanelChips } from './ai/chat-panel/chat-panel-chips';
import { ChatPanelInput } from './ai/chat-panel/chat-panel-input';
import { ChatPanelMessages } from './ai/chat-panel/chat-panel-messages';
import { ChatPanelChip } from './ai/chat-panel/components/chip';
import { ChatPanelDocChip } from './ai/chat-panel/components/doc-chip';
import { ChatPanelFileChip } from './ai/chat-panel/components/file-chip';
import { AIErrorWrapper } from './ai/messages/error';
import { AISlidesRenderer } from './ai/messages/slides-renderer';
import { AIAnswerWrapper } from './ai/messages/wrapper';
@@ -57,6 +61,10 @@ export function registerBlocksuitePresetsCustomComponents() {
customElements.define('chat-panel-input', ChatPanelInput);
customElements.define('chat-panel-messages', ChatPanelMessages);
customElements.define('chat-panel', ChatPanel);
customElements.define('chat-panel-chips', ChatPanelChips);
customElements.define('chat-panel-doc-chip', ChatPanelDocChip);
customElements.define('chat-panel-file-chip', ChatPanelFileChip);
customElements.define('chat-panel-chip', ChatPanelChip);
customElements.define('ai-error-wrapper', AIErrorWrapper);
customElements.define('ai-slides-renderer', AISlidesRenderer);
customElements.define('ai-answer-wrapper', AIAnswerWrapper);

View File

@@ -1,10 +1,13 @@
import { ChatPanel } from '@affine/core/blocksuite/presets/ai';
import { AINetworkSearchService } from '@affine/core/modules/ai-button/services/network-search';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { WorkspaceService } from '@affine/core/modules/workspace';
import {
DocModeProvider,
RefNodeSlotsProvider,
} from '@blocksuite/affine/blocks';
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
import { createSignalFromObservable } from '@blocksuite/affine-shared/utils';
import { useFramework } from '@toeverything/infra';
import { forwardRef, useEffect, useRef } from 'react';
@@ -49,12 +52,26 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
chatPanelRef.current.doc = editor.doc;
containerRef.current?.append(chatPanelRef.current);
const searchService = framework.get(AINetworkSearchService);
const networkSearchConfig = {
const docDisplayMetaService = framework.get(DocDisplayMetaService);
const workspaceService = framework.get(WorkspaceService);
chatPanelRef.current.networkSearchConfig = {
visible: searchService.visible,
enabled: searchService.enabled,
setEnabled: searchService.setEnabled,
};
chatPanelRef.current.networkSearchConfig = networkSearchConfig;
chatPanelRef.current.docDisplayConfig = {
getIcon: (docId: string) => {
return docDisplayMetaService.icon$(docId, { type: 'lit' }).value;
},
getTitle: (docId: string) => {
const title$ = docDisplayMetaService.title$(docId);
return createSignalFromObservable(title$, '');
},
getDoc: (docId: string) => {
const doc = workspaceService.workspace.docCollection.getDoc(docId);
return doc;
},
};
} else {
chatPanelRef.current.host = editor.host;
chatPanelRef.current.doc = editor.doc;