mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-01 17:50:50 +08:00
feat(core): send message with extracted info
This commit is contained in:
@@ -36,7 +36,16 @@ export interface CollectionChip extends BaseChip {
|
|||||||
collectionId: string;
|
collectionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChatChip = DocChip | FileChip | TagChip | CollectionChip;
|
export interface SelectedContextChip extends FileChip {
|
||||||
|
isSelectedContext: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChatChip =
|
||||||
|
| DocChip
|
||||||
|
| FileChip
|
||||||
|
| TagChip
|
||||||
|
| CollectionChip
|
||||||
|
| SelectedContextChip;
|
||||||
|
|
||||||
export interface DocDisplayConfig {
|
export interface DocDisplayConfig {
|
||||||
getIcon: (docId: string) => any;
|
getIcon: (docId: string) => any;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
CollectionChip,
|
CollectionChip,
|
||||||
DocChip,
|
DocChip,
|
||||||
FileChip,
|
FileChip,
|
||||||
|
SelectedContextChip,
|
||||||
TagChip,
|
TagChip,
|
||||||
} from './type';
|
} from './type';
|
||||||
|
|
||||||
@@ -62,6 +63,12 @@ export function isCollectionChip(chip: ChatChip): chip is CollectionChip {
|
|||||||
return 'collectionId' in chip;
|
return 'collectionId' in chip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSelectedContextChip(
|
||||||
|
chip: ChatChip
|
||||||
|
): chip is SelectedContextChip {
|
||||||
|
return 'isSelectedContext' in chip && chip.isSelectedContext;
|
||||||
|
}
|
||||||
|
|
||||||
export function getChipKey(chip: ChatChip) {
|
export function getChipKey(chip: ChatChip) {
|
||||||
if (isDocChip(chip)) {
|
if (isDocChip(chip)) {
|
||||||
return chip.docId;
|
return chip.docId;
|
||||||
|
|||||||
+42
-2
@@ -32,6 +32,7 @@ import {
|
|||||||
isCollectionChip,
|
isCollectionChip,
|
||||||
isDocChip,
|
isDocChip,
|
||||||
isFileChip,
|
isFileChip,
|
||||||
|
isSelectedContextChip,
|
||||||
isTagChip,
|
isTagChip,
|
||||||
omitChip,
|
omitChip,
|
||||||
} from '../ai-chat-chips';
|
} from '../ai-chat-chips';
|
||||||
@@ -157,6 +158,8 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
.session=${this.session}
|
.session=${this.session}
|
||||||
.chips=${this.chips}
|
.chips=${this.chips}
|
||||||
.addChip=${this.addChip}
|
.addChip=${this.addChip}
|
||||||
|
.removeSelectedContextChips=${this.removeSelectedContextChips}
|
||||||
|
.waitForSelectedContextChipsFinished=${this.waitForSelectedContextChipsFinished}
|
||||||
.addImages=${this.addImages}
|
.addImages=${this.addImages}
|
||||||
.createSession=${this.createSession}
|
.createSession=${this.createSession}
|
||||||
.chatContextValue=${this.chatContextValue}
|
.chatContextValue=${this.chatContextValue}
|
||||||
@@ -329,12 +332,17 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly addChip = async (chip: ChatChip) => {
|
private readonly addChip = async (
|
||||||
|
chip: ChatChip,
|
||||||
|
silent: boolean = false
|
||||||
|
) => {
|
||||||
this.isChipsCollapsed = false;
|
this.isChipsCollapsed = false;
|
||||||
// if already exists
|
// if already exists
|
||||||
const index = findChipIndex(this.chips, chip);
|
const index = findChipIndex(this.chips, chip);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.notificationService.toast('chip already exists');
|
if (!silent) {
|
||||||
|
this.notificationService.toast('chip already exists');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.updateChips([...this.chips, chip]);
|
this.updateChips([...this.chips, chip]);
|
||||||
@@ -348,6 +356,15 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
await this.removeFromContext(chip);
|
await this.removeFromContext(chip);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly removeSelectedContextChips = async () => {
|
||||||
|
const selectedContextChips = this.chips.filter(c =>
|
||||||
|
isSelectedContextChip(c)
|
||||||
|
);
|
||||||
|
for (const chip of selectedContextChips) {
|
||||||
|
await this.removeChip(chip);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private readonly addToContext = async (chip: ChatChip) => {
|
private readonly addToContext = async (chip: ChatChip) => {
|
||||||
if (isDocChip(chip)) {
|
if (isDocChip(chip)) {
|
||||||
return await this.addDocToContext(chip);
|
return await this.addDocToContext(chip);
|
||||||
@@ -639,4 +656,27 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
await this.pollEmbeddingStatus();
|
await this.pollEmbeddingStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly waitForSelectedContextChipsFinished = async (
|
||||||
|
timeout = 10000,
|
||||||
|
interval = 500
|
||||||
|
): Promise<void> => {
|
||||||
|
const start = Date.now();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const check = () => {
|
||||||
|
const selectedChips = this.chips.filter(c => isSelectedContextChip(c));
|
||||||
|
const allFinished = selectedChips.every(c => c.state === 'finished');
|
||||||
|
if (allFinished) {
|
||||||
|
resolve();
|
||||||
|
} else if (Date.now() - start >= timeout) {
|
||||||
|
reject(
|
||||||
|
new Error('Timeout waiting for selected context chips to finish')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setTimeout(check, interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
|
|||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
markdown: '',
|
markdown: '',
|
||||||
|
attachments: [],
|
||||||
|
snapshot: null,
|
||||||
|
markdownFile: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AIChatContent extends SignalWatcher(
|
export class AIChatContent extends SignalWatcher(
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { DocSnapshot } from '@blocksuite/affine/store';
|
|
||||||
|
|
||||||
import type { AIError } from '../../provider';
|
import type { AIError } from '../../provider';
|
||||||
import type { ChatStatus, HistoryMessage } from '../ai-chat-messages';
|
import type { ChatStatus, HistoryMessage } from '../ai-chat-messages';
|
||||||
|
|
||||||
@@ -15,7 +13,10 @@ export type ChatContextValue = {
|
|||||||
// 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 of the selected content
|
||||||
snapshot: DocSnapshot;
|
snapshot: File | null;
|
||||||
|
// attachments of the selected content
|
||||||
attachments: File[];
|
attachments: File[];
|
||||||
|
// markdown file of the selected content
|
||||||
|
markdownFile: File | null;
|
||||||
abortController: AbortController | null;
|
abortController: AbortController | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -338,7 +338,13 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
accessor addImages!: (images: File[]) => void;
|
accessor addImages!: (images: File[]) => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor addChip!: (chip: ChatChip) => Promise<void>;
|
accessor addChip!: (chip: ChatChip, silent?: boolean) => Promise<void>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor removeSelectedContextChips!: () => Promise<void>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor waitForSelectedContextChipsFinished!: () => Promise<void>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor networkSearchConfig!: AINetworkSearchConfig;
|
accessor networkSearchConfig!: AINetworkSearchConfig;
|
||||||
@@ -603,9 +609,47 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
this.modelId = modelId;
|
this.modelId = modelId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly addSelectedContextChips = async () => {
|
||||||
|
const { snapshot, markdownFile, attachments } = this.chatContextValue;
|
||||||
|
await this.removeSelectedContextChips();
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
await this.addChip(
|
||||||
|
{
|
||||||
|
file: attachment,
|
||||||
|
state: 'processing',
|
||||||
|
isSelectedContext: true,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (snapshot) {
|
||||||
|
await this.addChip(
|
||||||
|
{
|
||||||
|
file: snapshot,
|
||||||
|
state: 'processing',
|
||||||
|
isSelectedContext: true,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (markdownFile) {
|
||||||
|
await this.addChip(
|
||||||
|
{
|
||||||
|
file: markdownFile,
|
||||||
|
state: 'processing',
|
||||||
|
isSelectedContext: true,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.waitForSelectedContextChipsFinished();
|
||||||
|
};
|
||||||
|
|
||||||
send = async (text: string) => {
|
send = async (text: string) => {
|
||||||
try {
|
try {
|
||||||
const { status, markdown, images } = this.chatContextValue;
|
const { status, markdown, images } = this.chatContextValue;
|
||||||
|
await this.addSelectedContextChips();
|
||||||
|
|
||||||
if (status === 'loading' || status === 'transmitting') return;
|
if (status === 'loading' || status === 'transmitting') return;
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
if (!AIProvider.actions.chat) return;
|
if (!AIProvider.actions.chat) return;
|
||||||
@@ -620,13 +664,13 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
abortController,
|
abortController,
|
||||||
});
|
});
|
||||||
|
|
||||||
const attachments = await Promise.all(
|
const imageAttachments = await Promise.all(
|
||||||
images?.map(image => readBlobAsURL(image))
|
images?.map(image => readBlobAsURL(image))
|
||||||
);
|
);
|
||||||
const userInput = (markdown ? `${markdown}\n` : '') + text;
|
const userInput = (markdown ? `${markdown}\n` : '') + text;
|
||||||
|
|
||||||
// optimistic update messages
|
// optimistic update messages
|
||||||
await this._preUpdateMessages(userInput, attachments);
|
await this._preUpdateMessages(userInput, imageAttachments);
|
||||||
|
|
||||||
const sessionId = (await this.createSession())?.sessionId;
|
const sessionId = (await this.createSession())?.sessionId;
|
||||||
let contexts = await this._getMatchedContexts();
|
let contexts = await this._getMatchedContexts();
|
||||||
|
|||||||
@@ -26,5 +26,8 @@ export type AIChatInputContext = {
|
|||||||
quote?: string;
|
quote?: string;
|
||||||
markdown?: string;
|
markdown?: string;
|
||||||
images: File[];
|
images: File[];
|
||||||
|
attachments: File[];
|
||||||
|
snapshot: File | null;
|
||||||
|
markdownFile: File | null;
|
||||||
abortController: AbortController | null;
|
abortController: AbortController | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,6 +1,5 @@
|
|||||||
import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace';
|
import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace';
|
||||||
import {
|
import {
|
||||||
AttachmentBlockModel,
|
|
||||||
DatabaseBlockModel,
|
DatabaseBlockModel,
|
||||||
ImageBlockModel,
|
ImageBlockModel,
|
||||||
NoteBlockModel,
|
NoteBlockModel,
|
||||||
@@ -32,6 +31,7 @@ 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 {
|
||||||
getSelectedAttachmentsAsBlobs,
|
getSelectedAttachmentsAsBlobs,
|
||||||
getSelectedImagesAsBlobs,
|
getSelectedImagesAsBlobs,
|
||||||
@@ -63,8 +63,6 @@ async function extractEdgelessSelected(
|
|||||||
const attachments: ChatContextValue['attachments'] = [];
|
const attachments: ChatContextValue['attachments'] = [];
|
||||||
|
|
||||||
if (selectedElements.length) {
|
if (selectedElements.length) {
|
||||||
console.log('selectedElements', selectedElements);
|
|
||||||
|
|
||||||
const transformer = host.store.getTransformer();
|
const transformer = host.store.getTransformer();
|
||||||
const markdownAdapter = new MarkdownAdapter(
|
const markdownAdapter = new MarkdownAdapter(
|
||||||
transformer,
|
transformer,
|
||||||
@@ -76,6 +74,8 @@ async function extractEdgelessSelected(
|
|||||||
});
|
});
|
||||||
collection.meta.initialize();
|
collection.meta.initialize();
|
||||||
|
|
||||||
|
let needSnapshot = false;
|
||||||
|
let needMarkdown = false;
|
||||||
try {
|
try {
|
||||||
const fragmentDoc = collection.createDoc();
|
const fragmentDoc = collection.createDoc();
|
||||||
const fragment = fragmentDoc.getStore();
|
const fragment = fragmentDoc.getStore();
|
||||||
@@ -85,19 +85,13 @@ async function extractEdgelessSelected(
|
|||||||
const surfaceId = fragment.addBlock('affine:surface', {}, rootId);
|
const surfaceId = fragment.addBlock('affine:surface', {}, rootId);
|
||||||
const noteId = fragment.addBlock('affine:note', {}, rootId);
|
const noteId = fragment.addBlock('affine:note', {}, rootId);
|
||||||
for (const element of selectedElements) {
|
for (const element of selectedElements) {
|
||||||
if (element instanceof GfxBlockElementModel) {
|
|
||||||
const props = getBlockProps(element);
|
|
||||||
fragment.addBlock(element.flavour, props, surfaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element instanceof NoteBlockModel) {
|
if (element instanceof NoteBlockModel) {
|
||||||
|
needMarkdown = true;
|
||||||
for (const child of element.children) {
|
for (const child of element.children) {
|
||||||
const props = getBlockProps(child);
|
const props = getBlockProps(child);
|
||||||
fragment.addBlock(child.flavour, props, noteId);
|
fragment.addBlock(child.flavour, props, noteId);
|
||||||
}
|
}
|
||||||
}
|
} else if (isAttachment(element)) {
|
||||||
|
|
||||||
if (element instanceof AttachmentBlockModel) {
|
|
||||||
const { name, sourceId } = element.props;
|
const { name, sourceId } = element.props;
|
||||||
if (name && sourceId) {
|
if (name && sourceId) {
|
||||||
const blob = await host.store.blobSync.get(sourceId);
|
const blob = await host.store.blobSync.get(sourceId);
|
||||||
@@ -105,11 +99,19 @@ async function extractEdgelessSelected(
|
|||||||
attachments.push(new File([blob], name));
|
attachments.push(new File([blob], name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (element instanceof GfxBlockElementModel) {
|
||||||
|
const props = getBlockProps(element);
|
||||||
|
needSnapshot = true;
|
||||||
|
fragment.addBlock(element.flavour, props, surfaceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot = transformer.docToSnapshot(fragment) ?? null;
|
if (needSnapshot) {
|
||||||
markdown = (await markdownAdapter.fromDoc(fragment))?.file ?? '';
|
snapshot = transformer.docToSnapshot(fragment) ?? null;
|
||||||
|
}
|
||||||
|
if (needMarkdown) {
|
||||||
|
markdown = (await markdownAdapter.fromDoc(fragment))?.file ?? '';
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
collection.dispose();
|
collection.dispose();
|
||||||
}
|
}
|
||||||
@@ -125,8 +127,10 @@ async function extractEdgelessSelected(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
images: [new File([blob], 'selected.png')],
|
images: [new File([blob], 'selected.png')],
|
||||||
snapshot: snapshot ?? undefined,
|
snapshot: snapshot
|
||||||
markdown: markdown.length ? markdown : undefined,
|
? new File([JSON.stringify(snapshot)], 'selected.json')
|
||||||
|
: null,
|
||||||
|
markdownFile: markdown.length ? new File([markdown], 'selected.md') : null,
|
||||||
attachments,
|
attachments,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import {
|
|||||||
getSurfaceBlock,
|
getSurfaceBlock,
|
||||||
type SurfaceBlockComponent,
|
type SurfaceBlockComponent,
|
||||||
} from '@blocksuite/affine/blocks/surface';
|
} from '@blocksuite/affine/blocks/surface';
|
||||||
import {
|
import { DatabaseBlockModel, ImageBlockModel } from '@blocksuite/affine/model';
|
||||||
AttachmentBlockModel,
|
|
||||||
DatabaseBlockModel,
|
|
||||||
ImageBlockModel,
|
|
||||||
} from '@blocksuite/affine/model';
|
|
||||||
import {
|
import {
|
||||||
getBlockSelectionsCommand,
|
getBlockSelectionsCommand,
|
||||||
getImageSelectionsCommand,
|
getImageSelectionsCommand,
|
||||||
@@ -33,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) {
|
||||||
@@ -245,7 +242,7 @@ export const getSelectedAttachmentsAsBlobs = async (host: EditorHost) => {
|
|||||||
const attachments: { sourceId: string; name: string }[] = [];
|
const attachments: { sourceId: string; name: string }[] = [];
|
||||||
|
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
if (block.model instanceof AttachmentBlockModel) {
|
if (isAttachment(block.model)) {
|
||||||
const { sourceId, name } = block.model.props;
|
const { sourceId, name } = block.model.props;
|
||||||
if (sourceId && name) {
|
if (sourceId && name) {
|
||||||
attachments.push({ sourceId, name });
|
attachments.push({ sourceId, name });
|
||||||
|
|||||||
Reference in New Issue
Block a user