mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
feat(core): extract md & snapshot & attachments from selected (#13312)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Enhanced extraction of selected content in the editor to include document snapshots, markdown summaries, and attachments for both edgeless and page modes. * Attachments related to selected content are now available in chat and input contexts, providing additional metadata. * Added utility to identify and retrieve selected attachments in editor content. * **Bug Fixes** * Improved consistency in attachment retrieval when extracting selected content. * **Chores** * Updated dependencies and workspace references to include new block suite components. <!-- end of auto-generated comment: release notes by coderabbit.ai --> > CLOSE AF-2770
This commit is contained in:
@@ -4,6 +4,6 @@ export * from './clipboard/command';
|
|||||||
export * from './edgeless-root-block.js';
|
export * from './edgeless-root-block.js';
|
||||||
export { EdgelessRootService } from './edgeless-root-service.js';
|
export { EdgelessRootService } from './edgeless-root-service.js';
|
||||||
export * from './utils/clipboard-utils.js';
|
export * from './utils/clipboard-utils.js';
|
||||||
export { sortEdgelessElements } from './utils/clone-utils.js';
|
export { getElementProps, sortEdgelessElements } from './utils/clone-utils.js';
|
||||||
export { isCanvasElement } from './utils/query.js';
|
export { isCanvasElement } from './utils/query.js';
|
||||||
export { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
export { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@affine/templates": "workspace:*",
|
"@affine/templates": "workspace:*",
|
||||||
"@affine/track": "workspace:*",
|
"@affine/track": "workspace:*",
|
||||||
"@blocksuite/affine": "workspace:*",
|
"@blocksuite/affine": "workspace:*",
|
||||||
|
"@blocksuite/affine-block-root": "workspace:*",
|
||||||
"@blocksuite/affine-components": "workspace:*",
|
"@blocksuite/affine-components": "workspace:*",
|
||||||
"@blocksuite/affine-shared": "workspace:*",
|
"@blocksuite/affine-shared": "workspace:*",
|
||||||
"@blocksuite/global": "workspace:*",
|
"@blocksuite/global": "workspace:*",
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
|
|||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
markdown: '',
|
markdown: '',
|
||||||
|
snapshot: null,
|
||||||
|
attachments: [],
|
||||||
|
combinedElementsMarkdown: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AIChatContent extends SignalWatcher(
|
export class AIChatContent extends SignalWatcher(
|
||||||
|
|||||||
@@ -12,5 +12,11 @@ export type ChatContextValue = {
|
|||||||
markdown: string;
|
markdown: string;
|
||||||
// images of the selected content or user uploaded
|
// images of the selected content or user uploaded
|
||||||
images: File[];
|
images: File[];
|
||||||
|
// snapshot of the selected content
|
||||||
|
snapshot: string | null;
|
||||||
|
// attachments of the selected content
|
||||||
|
attachments: { sourceId: string; name: string }[];
|
||||||
|
// combined markdown of the selected elements
|
||||||
|
combinedElementsMarkdown: string | null;
|
||||||
abortController: AbortController | null;
|
abortController: AbortController | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Signal } from '@preact/signals-core';
|
import type { Signal } from '@preact/signals-core';
|
||||||
|
|
||||||
import type { AIError } from '../../provider';
|
import type { AIError } from '../../provider';
|
||||||
|
import type { ChatContextValue } from '../ai-chat-content';
|
||||||
import type { ChatStatus, HistoryMessage } from '../ai-chat-messages';
|
import type { ChatStatus, HistoryMessage } from '../ai-chat-messages';
|
||||||
|
|
||||||
export interface AINetworkSearchConfig {
|
export interface AINetworkSearchConfig {
|
||||||
@@ -27,4 +28,7 @@ export type AIChatInputContext = {
|
|||||||
markdown?: string;
|
markdown?: string;
|
||||||
images: File[];
|
images: File[];
|
||||||
abortController: AbortController | null;
|
abortController: AbortController | null;
|
||||||
};
|
} & Pick<
|
||||||
|
ChatContextValue,
|
||||||
|
'snapshot' | 'combinedElementsMarkdown' | 'attachments'
|
||||||
|
>;
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
|
|||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
markdown: '',
|
markdown: '',
|
||||||
|
snapshot: null,
|
||||||
|
attachments: [],
|
||||||
|
combinedElementsMarkdown: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PlaygroundChat extends SignalWatcher(
|
export class PlaygroundChat extends SignalWatcher(
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { AttachmentBlockModel } from '@blocksuite/affine/model';
|
||||||
|
import type { BlockModel } from '@blocksuite/affine/store';
|
||||||
|
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
|
export function isAttachment(
|
||||||
|
model: GfxModel | BlockModel
|
||||||
|
): model is AttachmentBlockModel {
|
||||||
|
return model instanceof AttachmentBlockModel;
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace';
|
||||||
|
import { getSurfaceBlock } from '@blocksuite/affine/blocks/surface';
|
||||||
import {
|
import {
|
||||||
DatabaseBlockModel,
|
DatabaseBlockModel,
|
||||||
ImageBlockModel,
|
ImageBlockModel,
|
||||||
type NoteBlockModel,
|
NoteBlockModel,
|
||||||
NoteDisplayMode,
|
NoteDisplayMode,
|
||||||
} from '@blocksuite/affine/model';
|
} from '@blocksuite/affine/model';
|
||||||
import {
|
import {
|
||||||
@@ -15,16 +17,25 @@ import {
|
|||||||
} from '@blocksuite/affine/shared/commands';
|
} from '@blocksuite/affine/shared/commands';
|
||||||
import { DocModeProvider } from '@blocksuite/affine/shared/services';
|
import { DocModeProvider } from '@blocksuite/affine/shared/services';
|
||||||
import {
|
import {
|
||||||
|
getBlockProps,
|
||||||
isInsideEdgelessEditor,
|
isInsideEdgelessEditor,
|
||||||
matchModels,
|
matchModels,
|
||||||
} from '@blocksuite/affine/shared/utils';
|
} from '@blocksuite/affine/shared/utils';
|
||||||
import { BlockStdScope, type EditorHost } from '@blocksuite/affine/std';
|
import { BlockStdScope, type EditorHost } from '@blocksuite/affine/std';
|
||||||
import type { BlockModel, Store } from '@blocksuite/affine/store';
|
import {
|
||||||
|
GfxControllerIdentifier,
|
||||||
|
GfxPrimitiveElementModel,
|
||||||
|
} from '@blocksuite/affine/std/gfx';
|
||||||
|
import type { BlockModel, DocSnapshot, Store } from '@blocksuite/affine/store';
|
||||||
import { Slice, toDraftModel } from '@blocksuite/affine/store';
|
import { Slice, toDraftModel } from '@blocksuite/affine/store';
|
||||||
|
import { getElementProps } from '@blocksuite/affine-block-root';
|
||||||
|
import { Doc as YDoc } from 'yjs';
|
||||||
|
|
||||||
import { getStoreManager } from '../../manager/store';
|
import { getStoreManager } from '../../manager/store';
|
||||||
import type { ChatContextValue } from '../components/ai-chat-content';
|
import type { ChatContextValue } from '../components/ai-chat-content';
|
||||||
|
import { isAttachment } from './attachment';
|
||||||
import {
|
import {
|
||||||
|
getSelectedAttachments,
|
||||||
getSelectedImagesAsBlobs,
|
getSelectedImagesAsBlobs,
|
||||||
getSelectedTextContent,
|
getSelectedTextContent,
|
||||||
selectedToCanvas,
|
selectedToCanvas,
|
||||||
@@ -46,6 +57,69 @@ async function extractEdgelessSelected(
|
|||||||
host: EditorHost
|
host: EditorHost
|
||||||
): Promise<Partial<ChatContextValue> | null> {
|
): Promise<Partial<ChatContextValue> | null> {
|
||||||
if (!isInsideEdgelessEditor(host)) return null;
|
if (!isInsideEdgelessEditor(host)) return null;
|
||||||
|
const gfx = host.std.get(GfxControllerIdentifier);
|
||||||
|
const selectedElements = gfx.selection.selectedElements;
|
||||||
|
|
||||||
|
let snapshot: DocSnapshot | null = null;
|
||||||
|
let markdown = '';
|
||||||
|
const attachments: ChatContextValue['attachments'] = [];
|
||||||
|
|
||||||
|
if (selectedElements.length) {
|
||||||
|
const transformer = host.store.getTransformer();
|
||||||
|
const markdownAdapter = new MarkdownAdapter(
|
||||||
|
transformer,
|
||||||
|
host.store.provider
|
||||||
|
);
|
||||||
|
const collection = new WorkspaceImpl({
|
||||||
|
id: 'AI_EXTRACT',
|
||||||
|
rootDoc: new YDoc({ guid: 'AI_EXTRACT' }),
|
||||||
|
});
|
||||||
|
collection.meta.initialize();
|
||||||
|
|
||||||
|
let needSnapshot = false;
|
||||||
|
let needMarkdown = false;
|
||||||
|
try {
|
||||||
|
const fragmentDoc = collection.createDoc();
|
||||||
|
const fragment = fragmentDoc.getStore();
|
||||||
|
fragmentDoc.load();
|
||||||
|
|
||||||
|
const rootId = fragment.addBlock('affine:page');
|
||||||
|
fragment.addBlock('affine:surface', {}, rootId);
|
||||||
|
const noteId = fragment.addBlock('affine:note', {}, rootId);
|
||||||
|
const surface = getSurfaceBlock(fragment);
|
||||||
|
if (!surface) {
|
||||||
|
throw new Error('Failed to get surface block');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const element of selectedElements) {
|
||||||
|
if (element instanceof NoteBlockModel) {
|
||||||
|
needMarkdown = true;
|
||||||
|
for (const child of element.children) {
|
||||||
|
const props = getBlockProps(child);
|
||||||
|
fragment.addBlock(child.flavour, props, noteId);
|
||||||
|
}
|
||||||
|
} else if (isAttachment(element)) {
|
||||||
|
const { name, sourceId } = element.props;
|
||||||
|
if (name && sourceId) {
|
||||||
|
attachments.push({ name, sourceId });
|
||||||
|
}
|
||||||
|
} else if (element instanceof GfxPrimitiveElementModel) {
|
||||||
|
needSnapshot = true;
|
||||||
|
const props = getElementProps(element, new Map());
|
||||||
|
surface.addElement(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needSnapshot) {
|
||||||
|
snapshot = transformer.docToSnapshot(fragment) ?? null;
|
||||||
|
}
|
||||||
|
if (needMarkdown) {
|
||||||
|
markdown = (await markdownAdapter.fromDoc(fragment))?.file ?? '';
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
collection.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const canvas = await selectedToCanvas(host);
|
const canvas = await selectedToCanvas(host);
|
||||||
if (!canvas) return null;
|
if (!canvas) return null;
|
||||||
@@ -57,6 +131,9 @@ async function extractEdgelessSelected(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
images: [new File([blob], 'selected.png')],
|
images: [new File([blob], 'selected.png')],
|
||||||
|
snapshot: snapshot ? JSON.stringify(snapshot) : null,
|
||||||
|
combinedElementsMarkdown: markdown.length ? markdown : null,
|
||||||
|
attachments,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +142,7 @@ async function extractPageSelected(
|
|||||||
): Promise<Partial<ChatContextValue> | null> {
|
): Promise<Partial<ChatContextValue> | null> {
|
||||||
const text = await getSelectedTextContent(host, 'plain-text');
|
const text = await getSelectedTextContent(host, 'plain-text');
|
||||||
const images = await getSelectedImagesAsBlobs(host);
|
const images = await getSelectedImagesAsBlobs(host);
|
||||||
|
const attachments = await getSelectedAttachments(host);
|
||||||
const hasText = text.length > 0;
|
const hasText = text.length > 0;
|
||||||
const hasImages = images.length > 0;
|
const hasImages = images.length > 0;
|
||||||
|
|
||||||
@@ -73,6 +151,7 @@ async function extractPageSelected(
|
|||||||
return {
|
return {
|
||||||
quote: text,
|
quote: text,
|
||||||
markdown: markdown,
|
markdown: markdown,
|
||||||
|
attachments,
|
||||||
};
|
};
|
||||||
} else if (!hasText && hasImages && images.length === 1) {
|
} else if (!hasText && hasImages && images.length === 1) {
|
||||||
host.command
|
host.command
|
||||||
@@ -83,6 +162,7 @@ async function extractPageSelected(
|
|||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
return {
|
return {
|
||||||
|
attachments,
|
||||||
images,
|
images,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -92,6 +172,7 @@ async function extractPageSelected(
|
|||||||
quote: text,
|
quote: text,
|
||||||
markdown,
|
markdown,
|
||||||
images,
|
images,
|
||||||
|
attachments,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
|
|
||||||
import { getContentFromSlice } from '../../utils';
|
import { getContentFromSlice } from '../../utils';
|
||||||
import type { CopilotTool } from '../tool/copilot-tool';
|
import type { CopilotTool } from '../tool/copilot-tool';
|
||||||
|
import { isAttachment } from './attachment';
|
||||||
import { getEdgelessCopilotWidget } from './get-edgeless-copilot-widget';
|
import { getEdgelessCopilotWidget } from './get-edgeless-copilot-widget';
|
||||||
|
|
||||||
export async function selectedToCanvas(host: EditorHost) {
|
export async function selectedToCanvas(host: EditorHost) {
|
||||||
@@ -232,6 +233,26 @@ export const getSelectedImagesAsBlobs = async (host: EditorHost) => {
|
|||||||
return blobs.filter((blob): blob is File => !!blob);
|
return blobs.filter((blob): blob is File => !!blob);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSelectedAttachments = async (host: EditorHost) => {
|
||||||
|
const [_, data] = host.command.exec(getSelectedBlocksCommand, {
|
||||||
|
types: ['block'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const blocks = data.selectedBlocks ?? [];
|
||||||
|
const attachments: { sourceId: string; name: string }[] = [];
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (isAttachment(block.model)) {
|
||||||
|
const { sourceId, name } = block.model.props;
|
||||||
|
if (sourceId && name) {
|
||||||
|
attachments.push({ sourceId, name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachments;
|
||||||
|
};
|
||||||
|
|
||||||
export const getSelectedNoteAnchor = (host: EditorHost, id: string) => {
|
export const getSelectedNoteAnchor = (host: EditorHost, id: string) => {
|
||||||
return host.querySelector(`affine-edgeless-note[data-block-id="${id}"]`);
|
return host.querySelector(`affine-edgeless-note[data-block-id="${id}"]`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
{ "path": "../../common/reader" },
|
{ "path": "../../common/reader" },
|
||||||
{ "path": "../track" },
|
{ "path": "../track" },
|
||||||
{ "path": "../../../blocksuite/affine/all" },
|
{ "path": "../../../blocksuite/affine/all" },
|
||||||
|
{ "path": "../../../blocksuite/affine/blocks/root" },
|
||||||
{ "path": "../../../blocksuite/affine/components" },
|
{ "path": "../../../blocksuite/affine/components" },
|
||||||
{ "path": "../../../blocksuite/affine/shared" },
|
{ "path": "../../../blocksuite/affine/shared" },
|
||||||
{ "path": "../../../blocksuite/framework/global" },
|
{ "path": "../../../blocksuite/framework/global" },
|
||||||
|
|||||||
@@ -1351,6 +1351,7 @@ export const PackageList = [
|
|||||||
'packages/frontend/templates',
|
'packages/frontend/templates',
|
||||||
'packages/frontend/track',
|
'packages/frontend/track',
|
||||||
'blocksuite/affine/all',
|
'blocksuite/affine/all',
|
||||||
|
'blocksuite/affine/blocks/root',
|
||||||
'blocksuite/affine/components',
|
'blocksuite/affine/components',
|
||||||
'blocksuite/affine/shared',
|
'blocksuite/affine/shared',
|
||||||
'blocksuite/framework/global',
|
'blocksuite/framework/global',
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ __metadata:
|
|||||||
"@affine/templates": "workspace:*"
|
"@affine/templates": "workspace:*"
|
||||||
"@affine/track": "workspace:*"
|
"@affine/track": "workspace:*"
|
||||||
"@blocksuite/affine": "workspace:*"
|
"@blocksuite/affine": "workspace:*"
|
||||||
|
"@blocksuite/affine-block-root": "workspace:*"
|
||||||
"@blocksuite/affine-components": "workspace:*"
|
"@blocksuite/affine-components": "workspace:*"
|
||||||
"@blocksuite/affine-ext-loader": "workspace:*"
|
"@blocksuite/affine-ext-loader": "workspace:*"
|
||||||
"@blocksuite/affine-shared": "workspace:*"
|
"@blocksuite/affine-shared": "workspace:*"
|
||||||
|
|||||||
Reference in New Issue
Block a user