mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-06 01:23:46 +00:00
Compare commits
35 Commits
v0.22.2-be
...
v0.22.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e98f035f97 | ||
|
|
1d4bc81e90 | ||
|
|
deeea3428e | ||
|
|
8b0dd3c067 | ||
|
|
8ca17864f1 | ||
|
|
d2664480f7 | ||
|
|
b986a39da3 | ||
|
|
097a63362c | ||
|
|
7284320355 | ||
|
|
b4401a8abf | ||
|
|
0351fbcb86 | ||
|
|
6eed9c686b | ||
|
|
8d2214424c | ||
|
|
d12954f8c3 | ||
|
|
83733cd828 | ||
|
|
ed56f076ed | ||
|
|
2d17c265ca | ||
|
|
2a9f7e1835 | ||
|
|
a71904e641 | ||
|
|
814364489f | ||
|
|
24448659a4 | ||
|
|
c846c57a12 | ||
|
|
e82c9d2ddc | ||
|
|
3c29f62224 | ||
|
|
b5ef361f87 | ||
|
|
4fa85416ae | ||
|
|
f69a98eb8c | ||
|
|
115496aa8e | ||
|
|
7aafbf12a5 | ||
|
|
0f9b7d4a0d | ||
|
|
2817b5aec4 | ||
|
|
72e66aca11 | ||
|
|
7d1f2adb7f | ||
|
|
512a908fd4 | ||
|
|
71be1d424a |
@@ -95,11 +95,13 @@ spec:
|
||||
path: /info
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /info
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
|
||||
1
.github/helm/affine/charts/doc/values.yaml
vendored
1
.github/helm/affine/charts/doc/values.yaml
vendored
@@ -36,6 +36,7 @@ resources:
|
||||
|
||||
probe:
|
||||
initialDelaySeconds: 20
|
||||
timeoutSeconds: 5
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
|
||||
1
.github/workflows/build-test.yml
vendored
1
.github/workflows/build-test.yml
vendored
@@ -827,7 +827,6 @@ jobs:
|
||||
- optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -22,7 +22,10 @@ import {
|
||||
FileSizeLimitProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { formatSize } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
formatSize,
|
||||
openSingleFileWith,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
AttachmentIcon,
|
||||
ResetIcon,
|
||||
@@ -31,7 +34,7 @@ import {
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { nanoid, Slice } from '@blocksuite/store';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { batch, computed, signal } from '@preact/signals-core';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
@@ -42,7 +45,7 @@ import { filter } from 'rxjs/operators';
|
||||
|
||||
import { AttachmentEmbedProvider } from './embed';
|
||||
import { styles } from './styles';
|
||||
import { downloadAttachmentBlob, refreshData } from './utils';
|
||||
import { downloadAttachmentBlob, getFileType, refreshData } from './utils';
|
||||
|
||||
type AttachmentResolvedStateInfo = ResolvedStateInfo & {
|
||||
kind?: TemplateResult;
|
||||
@@ -129,12 +132,50 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
// Refreshes the embed component.
|
||||
reload = () => {
|
||||
if (this.model.props.embed) {
|
||||
this._refreshKey$.value = nanoid();
|
||||
return;
|
||||
}
|
||||
batch(() => {
|
||||
if (this.model.props.embed$.value) {
|
||||
this._refreshKey$.value = nanoid();
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshData();
|
||||
this.refreshData();
|
||||
});
|
||||
};
|
||||
|
||||
// Replaces the current attachment.
|
||||
replace = async () => {
|
||||
const state = this.resourceController.state$.peek();
|
||||
if (state.uploading) return;
|
||||
|
||||
const file = await openSingleFileWith();
|
||||
if (!file) return;
|
||||
|
||||
const sourceId = await this.std.store.blobSync.set(file);
|
||||
const type = await getFileType(file);
|
||||
const { name, size } = file;
|
||||
|
||||
let embed = this.model.props.embed$.value ?? false;
|
||||
|
||||
this.std.store.captureSync();
|
||||
this.std.store.transact(() => {
|
||||
this.std.store.updateBlock(this.blockId, {
|
||||
name,
|
||||
size,
|
||||
type,
|
||||
sourceId,
|
||||
embed: false,
|
||||
});
|
||||
|
||||
const provider = this.std.get(AttachmentEmbedProvider);
|
||||
embed &&= provider.embedded(this.model);
|
||||
|
||||
if (embed) {
|
||||
provider.convertTo(this.model);
|
||||
}
|
||||
|
||||
// Reloads
|
||||
this.reload();
|
||||
});
|
||||
};
|
||||
|
||||
private _selectBlock() {
|
||||
@@ -403,7 +444,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
protected renderEmbedView = () => {
|
||||
const { model, blobUrl } = this;
|
||||
if (!model.props.embed || !blobUrl) return null;
|
||||
if (!model.props.embed$.value || !blobUrl) return null;
|
||||
|
||||
const { std, _maxFileSize } = this;
|
||||
const provider = std.get(AttachmentEmbedProvider);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
DownloadIcon,
|
||||
DuplicateIcon,
|
||||
EditIcon,
|
||||
ReplaceIcon,
|
||||
ResetIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockFlavourIdentifier } from '@blocksuite/std';
|
||||
@@ -139,27 +140,42 @@ export const attachmentViewDropdownMenu = {
|
||||
});
|
||||
};
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-view-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions.value}
|
||||
.context=${ctx}
|
||||
.viewType$=${viewType$}
|
||||
></affine-view-dropdown-menu>`
|
||||
)}`;
|
||||
return html`<affine-view-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions.value}
|
||||
.context=${ctx}
|
||||
.viewType$=${viewType$}
|
||||
></affine-view-dropdown-menu>`;
|
||||
},
|
||||
} as const satisfies ToolbarActionGroup<ToolbarAction>;
|
||||
|
||||
const replaceAction = {
|
||||
id: 'c.replace',
|
||||
tooltip: 'Replace attachment',
|
||||
icon: ReplaceIcon(),
|
||||
disabled(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
if (!block) return true;
|
||||
|
||||
const { downloading = false, uploading = false } =
|
||||
block.resourceController.state$.value;
|
||||
return downloading || uploading;
|
||||
},
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
block?.replace().catch(console.error);
|
||||
},
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const downloadAction = {
|
||||
id: 'c.download',
|
||||
id: 'd.download',
|
||||
tooltip: 'Download',
|
||||
icon: DownloadIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
block?.download();
|
||||
},
|
||||
when: ctx => {
|
||||
when(ctx) {
|
||||
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
|
||||
if (!model) return false;
|
||||
// Current citation attachment block does not support download
|
||||
@@ -168,7 +184,7 @@ const downloadAction = {
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const captionAction = {
|
||||
id: 'd.caption',
|
||||
id: 'e.caption',
|
||||
tooltip: 'Caption',
|
||||
icon: CaptionIcon(),
|
||||
run(ctx) {
|
||||
@@ -221,6 +237,7 @@ const builtinToolbarConfig = {
|
||||
},
|
||||
},
|
||||
attachmentViewDropdownMenu,
|
||||
replaceAction,
|
||||
downloadAction,
|
||||
captionAction,
|
||||
{
|
||||
@@ -354,13 +371,17 @@ const builtinSurfaceToolbarConfig = {
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
...replaceAction,
|
||||
id: 'd.replace',
|
||||
},
|
||||
{
|
||||
...downloadAction,
|
||||
id: 'd.download',
|
||||
id: 'e.download',
|
||||
},
|
||||
{
|
||||
...captionAction,
|
||||
id: 'e.caption',
|
||||
id: 'f.caption',
|
||||
},
|
||||
],
|
||||
when: ctx => ctx.getSurfaceModelsByType(AttachmentBlockModel).length === 1,
|
||||
|
||||
61
blocksuite/affine/blocks/code/src/markdown.ts
Normal file
61
blocksuite/affine/blocks/code/src/markdown.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
type CodeBlockModel,
|
||||
CodeBlockSchema,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const CodeBlockMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'code-block',
|
||||
pattern: /^```([a-zA-Z0-9]*)\s$/,
|
||||
action: ({ inlineEditor, inlineRange, prefixText, pattern }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const language = match[1];
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type === 'quote'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
const codeId = store.addBlock<CodeBlockModel>(
|
||||
CodeBlockSchema.model.flavour,
|
||||
{ language },
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
if (model.text && model.text.length > prefixText.length) {
|
||||
const text = model.text.clone();
|
||||
store.addBlock('affine:paragraph', { text }, parent, index + 1);
|
||||
text.delete(0, prefixText.length);
|
||||
}
|
||||
store.deleteBlock(model, { bringChildrenTo: parent });
|
||||
|
||||
focusTextModel(std, codeId);
|
||||
},
|
||||
});
|
||||
@@ -33,6 +33,10 @@ export const codeBlockStyles = css`
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.affine-code-block-container.disable-line-numbers v-line {
|
||||
grid-template-columns: unset;
|
||||
}
|
||||
|
||||
.affine-code-block-container div:has(> v-line) {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { CodeKeymapExtension } from './code-keymap.js';
|
||||
import { AFFINE_CODE_TOOLBAR_WIDGET } from './code-toolbar/index.js';
|
||||
import { codeSlashMenuConfig } from './configs/slash-menu.js';
|
||||
import { effects } from './effects.js';
|
||||
import { CodeBlockMarkdownExtension } from './markdown.js';
|
||||
|
||||
const codeToolbarWidget = WidgetViewExtension(
|
||||
'affine:code',
|
||||
@@ -44,6 +45,7 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
|
||||
BlockViewExtension('affine:code', literal`affine-code`),
|
||||
SlashMenuConfigExtension('affine:code', codeSlashMenuConfig),
|
||||
CodeKeymapExtension,
|
||||
CodeBlockMarkdownExtension,
|
||||
...getCodeClipboardExtensions(),
|
||||
]);
|
||||
context.register([
|
||||
|
||||
@@ -331,7 +331,6 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
|
||||
this.inlineEditor$.value?.selectAll();
|
||||
}
|
||||
};
|
||||
this.addEventListener('keydown', selectAll);
|
||||
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
|
||||
@@ -209,10 +209,19 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
||||
}
|
||||
};
|
||||
|
||||
this.addEventListener('keydown', selectAll);
|
||||
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
||||
}
|
||||
|
||||
private readonly _handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Escape') {
|
||||
if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
override firstUpdated(props: Map<string, unknown>) {
|
||||
super.firstUpdated(props);
|
||||
this.richText.value?.updateComplete
|
||||
@@ -233,6 +242,12 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
||||
'paste',
|
||||
this._onPaste
|
||||
);
|
||||
const inlineEditor = this.inlineEditor;
|
||||
if (inlineEditor) {
|
||||
this.disposables.add(
|
||||
inlineEditor.slots.keydown.subscribe(this._handleKeyDown)
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-rich-text": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
|
||||
63
blocksuite/affine/blocks/divider/src/markdown.ts
Normal file
63
blocksuite/affine/blocks/divider/src/markdown.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
type DividerBlockModel,
|
||||
DividerBlockSchema,
|
||||
ParagraphBlockModel,
|
||||
ParagraphBlockSchema,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const DividerMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'divider',
|
||||
pattern: /^(-{3,}|\*{3,}|_{3,})\s$/,
|
||||
action: ({ inlineEditor, inlineRange }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type !== 'quote'
|
||||
) {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
store.addBlock<DividerBlockModel>(
|
||||
DividerBlockSchema.model.flavour,
|
||||
{
|
||||
children: model.children,
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
const nextBlock = parent.children.at(index + 1);
|
||||
let id = nextBlock?.id;
|
||||
if (!id) {
|
||||
id = store.addBlock<ParagraphBlockModel>(
|
||||
ParagraphBlockSchema.model.flavour,
|
||||
{},
|
||||
parent
|
||||
);
|
||||
}
|
||||
focusTextModel(std, id);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { BlockViewExtension } from '@blocksuite/std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { effects } from './effects';
|
||||
import { DividerMarkdownExtension } from './markdown';
|
||||
|
||||
export class DividerViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-divider-block';
|
||||
@@ -19,6 +20,7 @@ export class DividerViewExtension extends ViewExtensionProvider {
|
||||
super.setup(context);
|
||||
context.register([
|
||||
BlockViewExtension('affine:divider', literal`affine-divider`),
|
||||
DividerMarkdownExtension,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../ext-loader" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../rich-text" },
|
||||
{ "path": "../../shared" },
|
||||
{ "path": "../../../framework/global" },
|
||||
{ "path": "../../../framework/std" },
|
||||
|
||||
@@ -107,10 +107,10 @@ export class EmbedHtmlFullscreenToolbar extends LitElement {
|
||||
if (this._copied) return;
|
||||
|
||||
this.embedHtml.std.clipboard
|
||||
.writeToClipboard(items => {
|
||||
items['text/plain'] = this.embedHtml.model.props.html ?? '';
|
||||
return items;
|
||||
})
|
||||
.writeToClipboard(items => ({
|
||||
...items,
|
||||
'text/plain': this.embedHtml.model.props.html ?? '',
|
||||
}))
|
||||
.then(() => {
|
||||
this._copied = true;
|
||||
setTimeout(() => (this._copied = false), 1500);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
NativeClipboardProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
convertToPng,
|
||||
formatSize,
|
||||
getBlockProps,
|
||||
isInsidePageEditor,
|
||||
@@ -111,28 +112,6 @@ export async function resetImageSize(
|
||||
block.store.updateBlock(model, props);
|
||||
}
|
||||
|
||||
function convertToPng(blob: Blob): Promise<Blob | null> {
|
||||
return new Promise(resolve => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', _ => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = img.width;
|
||||
c.height = img.height;
|
||||
const ctx = c.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
c.toBlob(resolve, 'image/png');
|
||||
};
|
||||
img.onerror = () => resolve(null);
|
||||
img.src = reader.result as string;
|
||||
});
|
||||
reader.addEventListener('error', () => resolve(null));
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
export async function copyImageBlob(
|
||||
block: ImageBlockComponent | ImageEdgelessBlockComponent
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { textKeymap } from '@blocksuite/affine-inline-preset';
|
||||
import { ListBlockSchema } from '@blocksuite/affine-model';
|
||||
import { markdownInput } from '@blocksuite/affine-rich-text';
|
||||
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
|
||||
import { IS_MAC } from '@blocksuite/global/env';
|
||||
import { KeymapExtension, TextSelection } from '@blocksuite/std';
|
||||
@@ -125,20 +124,6 @@ export const ListKeymapExtension = KeymapExtension(
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
Space: ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Shift-Space': ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
91
blocksuite/affine/blocks/list/src/markdown.ts
Normal file
91
blocksuite/affine/blocks/list/src/markdown.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
type ListBlockModel,
|
||||
ListBlockSchema,
|
||||
type ListType,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels, toNumberedList } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const ListMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'list',
|
||||
// group 2: number
|
||||
// group 3: bullet
|
||||
// group 4: bullet
|
||||
// group 5: todo
|
||||
// group 6: todo checked
|
||||
pattern: /^((\d+\.)|(-)|(\*)|(\[ ?\])|(\[x\]))\s$/,
|
||||
action: ({ inlineEditor, pattern, inlineRange, prefixText }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
let type: ListType;
|
||||
|
||||
if (match[2]) {
|
||||
type = 'numbered';
|
||||
} else if (match[3] || match[4]) {
|
||||
type = 'bulleted';
|
||||
} else if (match[5] || match[6]) {
|
||||
type = 'todo';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const checked = match[6] !== undefined;
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
if (!matchModels(model, [ParagraphBlockModel])) return;
|
||||
|
||||
if (type !== 'numbered') {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
const id = store.addBlock<ListBlockModel>(
|
||||
ListBlockSchema.model.flavour,
|
||||
{
|
||||
type: type,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
...(type === 'todo' ? { checked } : {}),
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
store.deleteBlock(model, { deleteChildren: false });
|
||||
focusTextModel(std, id);
|
||||
} else {
|
||||
let order = parseInt(match[2]);
|
||||
if (!Number.isInteger(order)) order = 1;
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
|
||||
const id = toNumberedList(std, model, order);
|
||||
if (!id) return;
|
||||
|
||||
focusTextModel(std, id);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { effects } from './effects.js';
|
||||
import { ListKeymapExtension, ListTextKeymapExtension } from './list-keymap.js';
|
||||
import { ListMarkdownExtension } from './markdown.js';
|
||||
|
||||
export class ListViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-list-block';
|
||||
@@ -23,6 +24,7 @@ export class ListViewExtension extends ViewExtensionProvider {
|
||||
BlockViewExtension('affine:list', literal`affine-list`),
|
||||
ListKeymapExtension,
|
||||
ListTextKeymapExtension,
|
||||
ListMarkdownExtension,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
74
blocksuite/affine/blocks/paragraph/src/markdown.ts
Normal file
74
blocksuite/affine/blocks/paragraph/src/markdown.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
ListBlockModel,
|
||||
ParagraphBlockModel,
|
||||
ParagraphBlockSchema,
|
||||
type ParagraphType,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const ParagraphMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'heading',
|
||||
pattern: /^((#{1,6})|(>))\s$/,
|
||||
action: ({ inlineEditor, pattern, inlineRange, prefixText }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const type = (
|
||||
match[2] ? `h${match[2].length}` : 'quote'
|
||||
) as ParagraphType;
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
if (
|
||||
!matchModels(model, [ParagraphBlockModel]) &&
|
||||
matchModels(model, [ListBlockModel])
|
||||
) {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
store.deleteBlock(model, { deleteChildren: false });
|
||||
const id = store.addBlock<ParagraphBlockModel>(
|
||||
ParagraphBlockSchema.model.flavour,
|
||||
{
|
||||
type: type,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
focusTextModel(std, id);
|
||||
} else if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type !== type
|
||||
) {
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
store.updateBlock(model, { type });
|
||||
focusTextModel(std, model.id);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import {
|
||||
focusTextModel,
|
||||
getInlineEditorByModel,
|
||||
markdownInput,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
@@ -148,10 +147,6 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
||||
|
||||
raw.preventDefault();
|
||||
|
||||
if (markdownInput(std, model.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (model.props.type.startsWith('h') && model.props.collapsed) {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return true;
|
||||
@@ -199,20 +194,6 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
||||
event.preventDefault();
|
||||
return true;
|
||||
},
|
||||
Space: ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Shift-Space': ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
Tab: ctx => {
|
||||
const [success] = std.command
|
||||
.chain()
|
||||
|
||||
@@ -20,6 +20,7 @@ import { EMBED_BLOCK_MODEL_LIST } from '@blocksuite/affine-shared/consts';
|
||||
import type { ExtendedModel } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
focusTitle,
|
||||
getDocTitleInlineEditor,
|
||||
getPrevContentBlock,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
@@ -45,10 +46,6 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) {
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return false;
|
||||
|
||||
if (matchModels(parent, [EdgelessTextBlockModel])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const prevBlock = getPrevContentBlock(editorHost, model);
|
||||
if (!prevBlock) {
|
||||
return handleNoPreviousSibling(editorHost, model);
|
||||
@@ -123,36 +120,63 @@ function handleNoPreviousSibling(editorHost: EditorHost, model: ExtendedModel) {
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return false;
|
||||
|
||||
if (matchModels(parent, [NoteBlockModel]) && parent.isPageBlock()) {
|
||||
const focusFirstBlockStart = () => {
|
||||
const firstBlock = parent.firstChild();
|
||||
if (firstBlock) {
|
||||
focusTextModel(editorHost.std, firstBlock.id, 0);
|
||||
}
|
||||
};
|
||||
|
||||
if (matchModels(parent, [NoteBlockModel])) {
|
||||
const hasTitleEditor = getDocTitleInlineEditor(editorHost);
|
||||
const rootModel = model.store.root as RootBlockModel;
|
||||
const title = rootModel.props.title;
|
||||
|
||||
const shouldHandleTitle = parent.isPageBlock() && hasTitleEditor;
|
||||
|
||||
doc.captureSync();
|
||||
let textLength = 0;
|
||||
if (text) {
|
||||
textLength = text.length;
|
||||
title.join(text);
|
||||
|
||||
if (shouldHandleTitle) {
|
||||
let textLength = 0;
|
||||
if (text) {
|
||||
textLength = text.length;
|
||||
title.join(text);
|
||||
}
|
||||
if (model.children.length > 0 || doc.getNext(model)) {
|
||||
doc.deleteBlock(model, {
|
||||
bringChildrenTo: parent,
|
||||
});
|
||||
}
|
||||
// no other blocks, preserve a empty line
|
||||
else {
|
||||
text?.clear();
|
||||
}
|
||||
focusTitle(editorHost, title.length - textLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Preserve at least one block to be able to focus on container click
|
||||
if (doc.getNext(model) || model.children.length > 0) {
|
||||
if (
|
||||
text?.length === 0 &&
|
||||
(model.children.length > 0 || doc.getNext(model))
|
||||
) {
|
||||
doc.deleteBlock(model, {
|
||||
bringChildrenTo: parent,
|
||||
});
|
||||
} else {
|
||||
text?.clear();
|
||||
focusFirstBlockStart();
|
||||
return true;
|
||||
}
|
||||
focusTitle(editorHost, title.length - textLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
matchModels(parent, [EdgelessTextBlockModel]) ||
|
||||
model.children.length > 0
|
||||
matchModels(parent, [EdgelessTextBlockModel]) &&
|
||||
text?.length === 0 &&
|
||||
(model.children.length > 0 || doc.getNext(model))
|
||||
) {
|
||||
doc.deleteBlock(model, {
|
||||
bringChildrenTo: parent,
|
||||
});
|
||||
focusFirstBlockStart();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,13 @@ import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { effects } from './effects';
|
||||
import { ParagraphMarkdownExtension } from './markdown.js';
|
||||
import { ParagraphBlockConfigExtension } from './paragraph-block-config.js';
|
||||
import {
|
||||
ParagraphKeymapExtension,
|
||||
@@ -22,11 +26,6 @@ const placeholders = {
|
||||
quote: '',
|
||||
};
|
||||
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { effects } from './effects';
|
||||
|
||||
const optionsSchema = z.object({
|
||||
getPlaceholder: z.optional(
|
||||
z.function().args(z.instanceof(ParagraphBlockModel)).returns(z.string())
|
||||
@@ -61,6 +60,7 @@ export class ParagraphViewExtension extends ViewExtensionProvider<
|
||||
ParagraphBlockConfigExtension({
|
||||
getPlaceholder,
|
||||
}),
|
||||
ParagraphMarkdownExtension,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
convertToPng,
|
||||
isInsidePageEditor,
|
||||
isTopLevelBlock,
|
||||
isUrlInClipboard,
|
||||
@@ -67,7 +68,7 @@ import * as Y from 'yjs';
|
||||
|
||||
import { PageClipboard } from '../../clipboard/index.js';
|
||||
import { getSortedCloneElements } from '../utils/clone-utils.js';
|
||||
import { isCanvasElementWithText } from '../utils/query.js';
|
||||
import { isCanvasElementWithText, isImageBlock } from '../utils/query.js';
|
||||
import { createElementsFromClipboardDataCommand } from './command.js';
|
||||
import {
|
||||
isPureFileInClipboard,
|
||||
@@ -126,6 +127,49 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only when an image is selected, it can be pasted normally to page mode.
|
||||
if (elements.length === 1 && isImageBlock(elements[0])) {
|
||||
const element = elements[0];
|
||||
const sourceId = element.props.sourceId$.peek();
|
||||
if (!sourceId) return;
|
||||
|
||||
await this.std.clipboard.writeToClipboard(async items => {
|
||||
const job = this.std.store.getTransformer();
|
||||
await job.assetsManager.readFromBlob(sourceId);
|
||||
|
||||
let blob = job.assetsManager.getAssets().get(sourceId) ?? null;
|
||||
if (!blob) {
|
||||
return items;
|
||||
}
|
||||
|
||||
let type = blob.type;
|
||||
let supported = false;
|
||||
|
||||
try {
|
||||
supported = ClipboardItem?.supports(type) ?? false;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// TODO(@fundon): when converting jpeg to png, image may become larger and exceed the limit.
|
||||
if (!supported) {
|
||||
type = 'image/png';
|
||||
blob = await convertToPng(blob);
|
||||
}
|
||||
|
||||
if (blob) {
|
||||
return {
|
||||
...items,
|
||||
[`${type}`]: blob,
|
||||
};
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.std.clipboard.writeToClipboard(async _items => {
|
||||
const data = await prepareClipboardData(elements, this.std);
|
||||
return {
|
||||
@@ -562,6 +606,10 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
}
|
||||
|
||||
private async _pasteTextContentAsNote(content: BlockSnapshot[] | string) {
|
||||
if (content === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y } = this.toolManager.lastMousePos$.peek();
|
||||
|
||||
const noteProps = {
|
||||
|
||||
@@ -69,37 +69,39 @@ export async function prepareClipboardData(
|
||||
|
||||
export function isPureFileInClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
return (
|
||||
(types.length === 1 && types[0] === 'Files') ||
|
||||
(types.length === 2 &&
|
||||
(types.includes('text/plain') || types.includes('text/html')) &&
|
||||
types.includes('Files'))
|
||||
);
|
||||
const allowedTypes = new Set([
|
||||
'Files',
|
||||
'text/plain',
|
||||
'text/html',
|
||||
'application/x-moz-file',
|
||||
]);
|
||||
|
||||
return types.includes('Files') && types.every(type => allowedTypes.has(type));
|
||||
}
|
||||
|
||||
export function tryGetSvgFromClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(
|
||||
clipboardData.getData('text/plain'),
|
||||
'image/svg+xml'
|
||||
);
|
||||
const svg = svgDoc.documentElement;
|
||||
|
||||
if (types.length === 1 && types[0] !== 'text/plain') {
|
||||
if (svg.tagName !== 'svg' || !svg.hasAttribute('xmlns')) {
|
||||
return null;
|
||||
}
|
||||
const svgContent = DOMPurify.sanitize(svgDoc.documentElement, {
|
||||
USE_PROFILES: { svg: true },
|
||||
});
|
||||
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||
const file = new File([blob], 'pasted-image.svg', {
|
||||
type: 'image/svg+xml',
|
||||
});
|
||||
return file;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(
|
||||
clipboardData.getData('text/plain'),
|
||||
'image/svg+xml'
|
||||
);
|
||||
const svg = svgDoc.documentElement;
|
||||
|
||||
if (svg.tagName !== 'svg' || !svg.hasAttribute('xmlns')) {
|
||||
return null;
|
||||
}
|
||||
const svgContent = DOMPurify.sanitize(svgDoc.documentElement, {
|
||||
USE_PROFILES: { svg: true },
|
||||
});
|
||||
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||
const file = new File([blob], 'pasted-image.svg', { type: 'image/svg+xml' });
|
||||
return file;
|
||||
}
|
||||
|
||||
export function edgelessElementsBoundFromRawData(
|
||||
|
||||
@@ -647,6 +647,16 @@ export class TableCell extends SignalWatcher(
|
||||
return this.richText$.value?.inlineEditor;
|
||||
}
|
||||
|
||||
private readonly _handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.readonly) {
|
||||
@@ -659,10 +669,7 @@ export class TableCell extends SignalWatcher(
|
||||
this.inlineEditor?.selectAll();
|
||||
}
|
||||
};
|
||||
this.addEventListener('keydown', selectAll);
|
||||
this.disposables.add(() => {
|
||||
this.removeEventListener('keydown', selectAll);
|
||||
});
|
||||
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
||||
this.disposables.addFromEvent(this, 'click', (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
requestAnimationFrame(() => {
|
||||
@@ -679,6 +686,13 @@ export class TableCell extends SignalWatcher(
|
||||
}
|
||||
this.richText$.value?.updateComplete
|
||||
.then(() => {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
if (inlineEditor) {
|
||||
this.disposables.add(
|
||||
inlineEditor.slots.keydown.subscribe(this._handleKeyDown)
|
||||
);
|
||||
}
|
||||
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const richText = this.richText$.value;
|
||||
|
||||
@@ -195,8 +195,7 @@ export class EmbedCardEditModal extends SignalWatcher(
|
||||
|
||||
const description = this.description$.value.trim();
|
||||
|
||||
const props: AliasInfo = { title };
|
||||
if (description) props.description = description;
|
||||
const props: AliasInfo = { title, description };
|
||||
|
||||
this.onSave?.(std, blockComponent, props);
|
||||
|
||||
|
||||
@@ -112,7 +112,10 @@ export class GroupTrait {
|
||||
return;
|
||||
}
|
||||
const { staticMap, groupInfo } = staticInfo;
|
||||
const groupMap: Record<string, Group> = { ...staticMap };
|
||||
const groupMap: Record<string, Group> = {};
|
||||
Object.entries(staticMap).forEach(([key, group]) => {
|
||||
groupMap[key] = new Group(key, group.value, groupInfo, this);
|
||||
});
|
||||
this.view.rows$.value.forEach(row => {
|
||||
const value = this.view.cellGetOrCreate(row.rowId, groupInfo.property.id)
|
||||
.jsonValue$.value;
|
||||
@@ -182,6 +185,7 @@ export class GroupTrait {
|
||||
) {}
|
||||
|
||||
addToGroup(rowId: string, key: string) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
const groupInfo = this.groupInfo$.value;
|
||||
if (!groupMap || !groupInfo) {
|
||||
@@ -254,6 +258,7 @@ export class GroupTrait {
|
||||
toGroupKey: string,
|
||||
position: InsertToPosition
|
||||
) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
if (!groupMap) {
|
||||
return;
|
||||
@@ -290,6 +295,7 @@ export class GroupTrait {
|
||||
}
|
||||
|
||||
moveGroupTo(groupKey: string, position: InsertToPosition) {
|
||||
this.view.lockRows(false);
|
||||
const groups = this.groupsDataList$.value;
|
||||
if (!groups) {
|
||||
return;
|
||||
@@ -305,6 +311,7 @@ export class GroupTrait {
|
||||
}
|
||||
|
||||
removeFromGroup(rowId: string, key: string) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
if (!groupMap) {
|
||||
return;
|
||||
@@ -323,6 +330,7 @@ export class GroupTrait {
|
||||
}
|
||||
|
||||
updateValue(rows: string[], value: unknown) {
|
||||
this.view.lockRows(false);
|
||||
const propertyId = this.property$.value?.id;
|
||||
if (!propertyId) {
|
||||
return;
|
||||
|
||||
@@ -128,6 +128,7 @@ export abstract class SingleViewBase<
|
||||
);
|
||||
|
||||
rowsDelete(rows: string[]): void {
|
||||
this.lockRows(false);
|
||||
this.dataSource.rowDelete(rows);
|
||||
}
|
||||
|
||||
@@ -258,6 +259,7 @@ export abstract class SingleViewBase<
|
||||
abstract propertyGetOrCreate(propertyId: string): Property;
|
||||
|
||||
rowAdd(insertPosition: InsertToPosition | number): string {
|
||||
this.lockRows(false);
|
||||
return this.dataSource.rowAdd(insertPosition);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,12 @@ export class MobileKanbanGroup extends SignalWatcher(
|
||||
|
||||
private readonly clickAddCard = () => {
|
||||
this.view.addCard('end', this.group.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickAddCardInStart = () => {
|
||||
this.view.addCard('start', this.group.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickGroupOptions = (e: MouseEvent) => {
|
||||
@@ -79,12 +81,14 @@ export class MobileKanbanGroup extends SignalWatcher(
|
||||
this.group.rows.forEach(row => {
|
||||
this.group.manager.removeFromGroup(row.rowId, this.group.key);
|
||||
});
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(this.group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -66,7 +66,9 @@ export class MobileKanbanViewUILogic extends DataViewUILogicBase<
|
||||
|
||||
addRow = (position: InsertToPosition) => {
|
||||
if (this.readonly) return;
|
||||
return this.view.rowAdd(position);
|
||||
const id = this.view.rowAdd(position);
|
||||
this.ui$.value?.requestUpdate();
|
||||
return id;
|
||||
};
|
||||
|
||||
focusFirstCell = () => {};
|
||||
|
||||
@@ -83,6 +83,7 @@ export const popCardMenu = (
|
||||
{ before: true, id: cardId },
|
||||
groupKey
|
||||
);
|
||||
kanbanViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
@@ -97,6 +98,7 @@ export const popCardMenu = (
|
||||
{ before: false, id: cardId },
|
||||
groupKey
|
||||
);
|
||||
kanbanViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -111,6 +113,7 @@ export const popCardMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
kanbanViewLogic.view.rowsDelete([cardId]);
|
||||
kanbanViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -128,6 +128,7 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
if (selection.selectionType === 'card') {
|
||||
this.view.rowsDelete(selection.cards.map(v => v.cardId));
|
||||
this.selection = undefined;
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ export class KanbanGroup extends SignalWatcher(
|
||||
isEditing: true,
|
||||
};
|
||||
});
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickAddCardInStart = () => {
|
||||
@@ -127,6 +128,7 @@ export class KanbanGroup extends SignalWatcher(
|
||||
isEditing: true,
|
||||
};
|
||||
});
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickGroupOptions = (e: MouseEvent) => {
|
||||
@@ -139,12 +141,14 @@ export class KanbanGroup extends SignalWatcher(
|
||||
this.group.rows.forEach(row => {
|
||||
this.group.manager.removeFromGroup(row.rowId, this.group.key);
|
||||
});
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(this.group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -73,6 +73,7 @@ export class KanbanViewUILogic extends DataViewUILogicBase<
|
||||
rowId,
|
||||
});
|
||||
}
|
||||
this.ui$.value?.requestUpdate();
|
||||
return rowId;
|
||||
};
|
||||
|
||||
|
||||
@@ -51,10 +51,12 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
|
||||
private readonly clickAddRow = () => {
|
||||
this.view.rowAdd('end', this.group?.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickAddRowInStart = () => {
|
||||
this.view.rowAdd('start', this.group?.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickGroupOptions = (e: MouseEvent) => {
|
||||
@@ -77,6 +79,7 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -38,6 +38,7 @@ export const popMobileRowMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
view.rowsDelete([rowId]);
|
||||
tableViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -44,6 +44,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
if (deleteRows.length) {
|
||||
this.logic.view.rowsDelete(deleteRows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
}
|
||||
this.clipboard
|
||||
@@ -79,6 +80,14 @@ export class TableClipboardController implements ReactiveController {
|
||||
const event = _context.get('clipboardState').raw;
|
||||
event.stopPropagation();
|
||||
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (
|
||||
active &&
|
||||
(active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData) return;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export class TableHotkeysController implements ReactiveController {
|
||||
const rows = TableViewRowSelection.rowsIds(selection);
|
||||
this.selectionController.selection = undefined;
|
||||
this.logic.view.rowsDelete(rows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
return;
|
||||
}
|
||||
const {
|
||||
|
||||
@@ -376,6 +376,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
deleteRow(rowId: string) {
|
||||
this.view.rowsDelete([rowId]);
|
||||
this.focusToCell('up');
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
|
||||
focusFirstCell() {
|
||||
|
||||
@@ -45,6 +45,7 @@ export class TableGroupFooter extends WithDisposable(ShadowlessElement) {
|
||||
private readonly clickAddRow = () => {
|
||||
const group = this.group$.value;
|
||||
const rowId = this.tableViewManager.rowAdd('end', group?.key);
|
||||
this.requestUpdate();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const rowIndex = this.selectionController.getRow(group?.key, rowId)
|
||||
|
||||
@@ -58,6 +58,7 @@ export class TableGroupHeader extends SignalWatcher(
|
||||
return;
|
||||
}
|
||||
this.tableViewManager.rowAdd('start', group.key);
|
||||
this.requestUpdate();
|
||||
const selectionController = this.selectionController;
|
||||
selectionController.selection = undefined;
|
||||
requestAnimationFrame(() => {
|
||||
@@ -95,6 +96,7 @@ export class TableGroupHeader extends SignalWatcher(
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.tableViewManager.rowsDelete(group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -71,6 +71,7 @@ export const popRowMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
selectionController.view.rowsDelete(rows);
|
||||
selectionController.logic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -43,6 +43,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
if (deleteRows.length) {
|
||||
this.logic.view.rowsDelete(deleteRows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
}
|
||||
this.clipboard
|
||||
@@ -78,6 +79,14 @@ export class TableClipboardController implements ReactiveController {
|
||||
const event = _context.get('clipboardState').raw;
|
||||
event.stopPropagation();
|
||||
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (
|
||||
active &&
|
||||
(active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData) return;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export class TableHotkeysController implements ReactiveController {
|
||||
const rows = TableViewRowSelection.rowsIds(selection);
|
||||
this.selectionController.selection = undefined;
|
||||
this.logic.view.rowsDelete(rows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
return;
|
||||
}
|
||||
const {
|
||||
|
||||
@@ -351,6 +351,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
deleteRow(rowId: string) {
|
||||
this.view.rowsDelete([rowId]);
|
||||
this.focusToCell('up');
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
|
||||
focusFirstCell() {
|
||||
|
||||
@@ -83,6 +83,7 @@ export class TableGroup extends SignalWatcher(
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
this.requestUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -102,6 +103,7 @@ export class TableGroup extends SignalWatcher(
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
this.requestUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -125,6 +127,7 @@ export class TableGroup extends SignalWatcher(
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -71,6 +71,7 @@ export const popRowMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
selectionController.view.rowsDelete(rows);
|
||||
selectionController.logic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -10,7 +10,7 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'latex',
|
||||
|
||||
pattern:
|
||||
/(?:\$\$)(?<content>[^$]+)(?:\$\$)$|(?<blockPrefix>\$\$\$\$)|(?<inlinePrefix>\$\$)$/g,
|
||||
/(?:\$\$)(?<content>[^$]+)(?:\$\$)\s$|(?<blockPrefix>\$\$\$\$)\s$|(?<inlinePrefix>\$\$)\s$/g,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = pattern.exec(prefixText);
|
||||
if (!match || !match.groups) return;
|
||||
@@ -33,22 +33,10 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
const ifEdgelessText = blockComponent.closest('affine-edgeless-text');
|
||||
|
||||
if (blockPrefix === '$$$$') {
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 4,
|
||||
index: inlineRange.index - 5,
|
||||
length: 5,
|
||||
});
|
||||
|
||||
@@ -88,34 +76,22 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
}
|
||||
|
||||
if (inlinePrefix === '$$') {
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index - 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.formatText(
|
||||
{
|
||||
index: inlineRange.index - 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 1,
|
||||
},
|
||||
{
|
||||
@@ -129,7 +105,7 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
await inlineEditor.waitForUpdate();
|
||||
|
||||
const textPoint = inlineEditor.getTextPoint(
|
||||
inlineRange.index - 2 + 1
|
||||
inlineRange.index - 3 + 1
|
||||
);
|
||||
if (!textPoint) return;
|
||||
|
||||
@@ -159,21 +135,9 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
|
||||
if (!content || content.length === 0) return;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
const startIndex = inlineRange.index - 2 - content.length - 2;
|
||||
const startIndex = inlineRange.index - 1 - 2 - content.length - 2;
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 2 + content.length + 2 + 1,
|
||||
|
||||
@@ -3,27 +3,18 @@ import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const LinkExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'link',
|
||||
pattern: /.*\[(.+?)\]\((.+?)\)$/,
|
||||
pattern: /.*\[(.+?)\]\((.+?)\)\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const linkText = match[1];
|
||||
const linkUrl = match[2];
|
||||
const annotatedText = match[0].slice(-linkText.length - linkUrl.length - 4);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-(linkText.length + linkUrl.length + 4 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export const StrikeInlineSpecExtension =
|
||||
|
||||
export const CodeInlineSpecExtension =
|
||||
InlineSpecExtension<AffineTextAttributes>({
|
||||
name: 'code',
|
||||
name: 'inline-code',
|
||||
schema: z.literal(true).optional().nullable().catch(undefined),
|
||||
match: delta => {
|
||||
return !!delta.attributes?.code;
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { ExtensionType } from '@blocksuite/store';
|
||||
export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
{
|
||||
name: 'bolditalic',
|
||||
pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}$|.*\*{3}([^\s*])\*{3}$/,
|
||||
pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}\s$|.*\*{3}([^\s*])\*{3}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
@@ -25,20 +25,11 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 3 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-(targetText.length + 3 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -54,18 +45,13 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 3,
|
||||
length: 3,
|
||||
index: inlineRange.index - 4,
|
||||
length: 4,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 3,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 6,
|
||||
length: 0,
|
||||
@@ -76,26 +62,14 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
|
||||
export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'bold',
|
||||
pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}$|.*\*{2}([^\s*])\*{2}$/,
|
||||
pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}\s$|.*\*{2}([^\s*])\*{2}\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 2 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const annotatedText = match[0].slice(-(targetText.length + 2 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -110,18 +84,13 @@ export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 2,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 4,
|
||||
length: 0,
|
||||
@@ -131,26 +100,14 @@ export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
|
||||
export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'italic',
|
||||
pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}$|.*\*{1}([^\s*])\*{1}$/,
|
||||
pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}\s$|.*\*{1}([^\s*])\*{1}\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -165,18 +122,13 @@ export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 1,
|
||||
length: 1,
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 1,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 0,
|
||||
@@ -187,7 +139,7 @@ export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
export const StrikethroughExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'strikethrough',
|
||||
pattern: /.*~{2}([^\s][^~]*[^\s])~{2}$|.*~{2}([^\s~])~{2}$/,
|
||||
pattern: /.*~{2}([^\s][^~]*[^\s])~{2}\s$|.*~{2}([^\s~])~{2}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
@@ -199,20 +151,11 @@ export const StrikethroughExtension =
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 2 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-targetText.length - (2 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -227,12 +170,8 @@ export const StrikethroughExtension =
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
@@ -249,7 +188,7 @@ export const StrikethroughExtension =
|
||||
export const UnderthroughExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'underthrough',
|
||||
pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}$|.*~{1}([^\s~])~{1}$/,
|
||||
pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}\s$|.*~{1}([^\s~])~{1}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
@@ -261,20 +200,11 @@ export const UnderthroughExtension =
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-(targetText.length + 1 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -289,12 +219,8 @@ export const UnderthroughExtension =
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 1,
|
||||
length: 1,
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
@@ -310,26 +236,14 @@ export const UnderthroughExtension =
|
||||
|
||||
export const CodeExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'code',
|
||||
pattern: /.*`([^\s][^`]*[^\s])`$|.*`([^\s`])`$/,
|
||||
pattern: /.*`([^\s][^`]*[^\s])`\s$|.*`([^\s`])`\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -344,12 +258,8 @@ export const CodeExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 1,
|
||||
length: 1,
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
|
||||
@@ -10,6 +10,5 @@ export {
|
||||
onModelTextUpdated,
|
||||
selectTextModel,
|
||||
} from './dom';
|
||||
export { markdownInput } from './markdown';
|
||||
export { RichText } from './rich-text';
|
||||
export * from './utils';
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import {
|
||||
DividerBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
import { beforeConvert } from './utils.js';
|
||||
|
||||
export function toDivider(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
prefix: string
|
||||
) {
|
||||
const { store: doc } = std;
|
||||
if (
|
||||
matchModels(model, [DividerBlockModel]) ||
|
||||
(matchModels(model, [ParagraphBlockModel]) && model.props.type === 'quote')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
const index = parent.children.indexOf(model);
|
||||
beforeConvert(std, model, prefix.length);
|
||||
const blockProps = {
|
||||
children: model.children,
|
||||
};
|
||||
doc.addBlock('affine:divider', blockProps, parent, index);
|
||||
|
||||
const nextBlock = parent.children[index + 1];
|
||||
let id = nextBlock?.id;
|
||||
if (!id) {
|
||||
id = doc.addBlock('affine:paragraph', {}, parent);
|
||||
}
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { markdownInput } from './markdown-input.js';
|
||||
@@ -1,54 +0,0 @@
|
||||
import {
|
||||
type ListProps,
|
||||
type ListType,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels, toNumberedList } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
import { beforeConvert } from './utils.js';
|
||||
|
||||
export function toList(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
listType: ListType,
|
||||
prefix: string,
|
||||
otherProperties?: Partial<ListProps>
|
||||
) {
|
||||
if (!matchModels(model, [ParagraphBlockModel])) {
|
||||
return;
|
||||
}
|
||||
const { store: doc } = std;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
beforeConvert(std, model, prefix.length);
|
||||
|
||||
if (listType !== 'numbered') {
|
||||
const index = parent.children.indexOf(model);
|
||||
const blockProps = {
|
||||
type: listType,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
...otherProperties,
|
||||
};
|
||||
doc.deleteBlock(model, {
|
||||
deleteChildren: false,
|
||||
});
|
||||
|
||||
const id = doc.addBlock('affine:list', blockProps, parent, index);
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
let order = parseInt(prefix.slice(0, -1));
|
||||
if (!Number.isInteger(order)) order = 1;
|
||||
|
||||
const id = toNumberedList(std, model, order);
|
||||
if (!id) return;
|
||||
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import {
|
||||
CalloutBlockModel,
|
||||
CodeBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
isHorizontalRuleMarkdown,
|
||||
isMarkdownPrefix,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { type BlockStdScope, TextSelection } from '@blocksuite/std';
|
||||
|
||||
import { getInlineEditorByModel } from '../dom.js';
|
||||
import { toDivider } from './divider.js';
|
||||
import { toList } from './list.js';
|
||||
import { toParagraph } from './paragraph.js';
|
||||
import { toCode } from './to-code.js';
|
||||
import { getPrefixText } from './utils.js';
|
||||
|
||||
export function markdownInput(
|
||||
std: BlockStdScope,
|
||||
id?: string
|
||||
): string | undefined {
|
||||
if (!id) {
|
||||
const selection = std.selection;
|
||||
const text = selection.find(TextSelection);
|
||||
id = text?.from.blockId;
|
||||
}
|
||||
if (!id) return;
|
||||
const model = std.store.getBlock(id)?.model;
|
||||
if (!model) return;
|
||||
const inline = getInlineEditorByModel(std, model);
|
||||
if (!inline) return;
|
||||
const range = inline.getInlineRange();
|
||||
if (!range) return;
|
||||
|
||||
const prefixText = getPrefixText(inline);
|
||||
if (!isMarkdownPrefix(prefixText)) return;
|
||||
|
||||
const isParagraph = matchModels(model, [ParagraphBlockModel]);
|
||||
const isHeading = isParagraph && model.props.type.startsWith('h');
|
||||
const isParagraphQuoteBlock = isParagraph && model.props.type === 'quote';
|
||||
const isCodeBlock = matchModels(model, [CodeBlockModel]);
|
||||
if (
|
||||
isHeading ||
|
||||
isParagraphQuoteBlock ||
|
||||
isCodeBlock ||
|
||||
matchModels(model.parent, [CalloutBlockModel])
|
||||
)
|
||||
return;
|
||||
|
||||
const lineInfo = inline.getLine(range.index);
|
||||
if (!lineInfo) return;
|
||||
|
||||
const { lineIndex, rangeIndexRelatedToLine } = lineInfo;
|
||||
if (lineIndex !== 0 || rangeIndexRelatedToLine > prefixText.length) return;
|
||||
|
||||
// try to add code block
|
||||
const codeMatch = prefixText.match(/^```([a-zA-Z0-9]*)$/g);
|
||||
if (codeMatch) {
|
||||
return toCode(std, model, prefixText, codeMatch[0].slice(3));
|
||||
}
|
||||
|
||||
if (isHorizontalRuleMarkdown(prefixText.trim())) {
|
||||
return toDivider(std, model, prefixText);
|
||||
}
|
||||
|
||||
switch (prefixText.trim()) {
|
||||
case '[]':
|
||||
case '[ ]':
|
||||
return toList(std, model, 'todo', prefixText, {
|
||||
checked: false,
|
||||
});
|
||||
case '[x]':
|
||||
return toList(std, model, 'todo', prefixText, {
|
||||
checked: true,
|
||||
});
|
||||
case '-':
|
||||
case '*':
|
||||
return toList(std, model, 'bulleted', prefixText);
|
||||
case '#':
|
||||
return toParagraph(std, model, 'h1', prefixText);
|
||||
case '##':
|
||||
return toParagraph(std, model, 'h2', prefixText);
|
||||
case '###':
|
||||
return toParagraph(std, model, 'h3', prefixText);
|
||||
case '####':
|
||||
return toParagraph(std, model, 'h4', prefixText);
|
||||
case '#####':
|
||||
return toParagraph(std, model, 'h5', prefixText);
|
||||
case '######':
|
||||
return toParagraph(std, model, 'h6', prefixText);
|
||||
case '>':
|
||||
return toParagraph(std, model, 'quote', prefixText);
|
||||
default:
|
||||
return toList(std, model, 'numbered', prefixText);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import {
|
||||
ParagraphBlockModel,
|
||||
type ParagraphType,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
import { beforeConvert } from './utils.js';
|
||||
|
||||
export function toParagraph(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
type: ParagraphType,
|
||||
prefix: string
|
||||
) {
|
||||
const { store: doc } = std;
|
||||
if (!matchModels(model, [ParagraphBlockModel])) {
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
beforeConvert(std, model, prefix.length);
|
||||
|
||||
const blockProps = {
|
||||
type: type,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
};
|
||||
doc.deleteBlock(model, { deleteChildren: false });
|
||||
const id = doc.addBlock('affine:paragraph', blockProps, parent, index);
|
||||
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
if (matchModels(model, [ParagraphBlockModel]) && model.props.type !== type) {
|
||||
beforeConvert(std, model, prefix.length);
|
||||
|
||||
doc.updateBlock(model, { type });
|
||||
|
||||
focusTextModel(std, model.id);
|
||||
}
|
||||
|
||||
// If the model is already a paragraph with the same type, do nothing
|
||||
return model.id;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
|
||||
export function toCode(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
prefixText: string,
|
||||
language: string | null
|
||||
) {
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type === 'quote'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = model.store;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.captureSync();
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
const codeId = doc.addBlock('affine:code', { language }, parent, index);
|
||||
|
||||
if (model.text && model.text.length > prefixText.length) {
|
||||
const text = model.text.clone();
|
||||
doc.addBlock('affine:paragraph', { text }, parent, index + 1);
|
||||
text.delete(0, prefixText.length);
|
||||
}
|
||||
doc.deleteBlock(model, { bringChildrenTo: parent });
|
||||
|
||||
focusTextModel(std, codeId);
|
||||
|
||||
return codeId;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { InlineEditor } from '@blocksuite/std/inline';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
|
||||
export function getPrefixText(inlineEditor: InlineEditor) {
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return '';
|
||||
const firstLineEnd = inlineEditor.yTextString.search(/\n/);
|
||||
if (firstLineEnd !== -1 && inlineRange.index > firstLineEnd) {
|
||||
return '';
|
||||
}
|
||||
const textPoint = inlineEditor.getTextPoint(inlineRange.index);
|
||||
if (!textPoint) return '';
|
||||
const [leafStart, offsetStart] = textPoint;
|
||||
return leafStart.textContent
|
||||
? leafStart.textContent.slice(0, offsetStart)
|
||||
: '';
|
||||
}
|
||||
|
||||
export function beforeConvert(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
index: number
|
||||
) {
|
||||
const { text } = model;
|
||||
if (!text) return;
|
||||
// Add a space after the text, then stop capturing
|
||||
// So when the user undo, the prefix will be restored with a `space`
|
||||
// Ex. (| is the cursor position)
|
||||
// *| <- user input
|
||||
// <space> -> bullet list
|
||||
// *<space>| -> undo
|
||||
text.insert(' ', index);
|
||||
focusTextModel(std, model.id, index + 1);
|
||||
std.store.captureSync();
|
||||
text.delete(0, index + 1);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import * as Y from 'yjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { onVBeforeinput, onVCompositionEnd } from './hooks.js';
|
||||
import { getPrefixText } from './utils.js';
|
||||
|
||||
interface RichTextStackItem {
|
||||
meta: Map<'richtext-v-range', InlineRange | null>;
|
||||
@@ -186,38 +187,60 @@ export class RichText extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
const markdownMatches = this.markdownMatches;
|
||||
if (markdownMatches) {
|
||||
inlineEditor.disposables.addFromEvent(
|
||||
this.inlineEventSource ?? this.inlineEditorContainer,
|
||||
'keydown',
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key !== ' ' && e.key !== 'Enter') return;
|
||||
const markdownTransform = (isEnter: boolean = false) => {
|
||||
let inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return false;
|
||||
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange || inlineRange.length > 0) return;
|
||||
let prefixText = getPrefixText(inlineEditor);
|
||||
if (isEnter) prefixText = `${prefixText} `;
|
||||
|
||||
const nearestLineBreakIndex = inlineEditor.yTextString
|
||||
.slice(0, inlineRange.index)
|
||||
.lastIndexOf('\n');
|
||||
const prefixText = inlineEditor.yTextString.slice(
|
||||
nearestLineBreakIndex + 1,
|
||||
inlineRange.index
|
||||
);
|
||||
|
||||
for (const match of markdownMatches) {
|
||||
const { pattern, action } = match;
|
||||
if (prefixText.match(pattern)) {
|
||||
action({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
inlineRange,
|
||||
pattern,
|
||||
undoManager: this.undoManager,
|
||||
for (const match of markdownMatches) {
|
||||
const { pattern, action } = match;
|
||||
if (prefixText.match(pattern)) {
|
||||
if (isEnter) {
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
e.preventDefault();
|
||||
break;
|
||||
inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return false;
|
||||
}
|
||||
|
||||
action({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
inlineRange,
|
||||
pattern,
|
||||
undoManager: this.undoManager,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
inlineEditor.disposables.add(
|
||||
inlineEditor.slots.inputting.subscribe(data => {
|
||||
if (!inlineEditor.isComposing && data === ' ') {
|
||||
markdownTransform();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
inlineEditor.disposables.add(
|
||||
inlineEditor.slots.keydown.subscribe(event => {
|
||||
if (event.key === 'Enter' && markdownTransform(true)) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,3 +52,17 @@ export function clearMarksOnDiscontinuousInput(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getPrefixText(inlineEditor: InlineEditor) {
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange || inlineRange.length > 0) return '';
|
||||
|
||||
const nearestLineBreakIndex = inlineEditor.yTextString
|
||||
.slice(0, inlineRange.index)
|
||||
.lastIndexOf('\n');
|
||||
const prefixText = inlineEditor.yTextString.slice(
|
||||
nearestLineBreakIndex + 1,
|
||||
inlineRange.index
|
||||
);
|
||||
return prefixText;
|
||||
}
|
||||
|
||||
@@ -176,9 +176,7 @@ export async function openFilesWith(
|
||||
resolve(input.files ? Array.from(input.files) : null);
|
||||
});
|
||||
// The `cancel` event fires when the user cancels the dialog.
|
||||
input.addEventListener('cancel', () => {
|
||||
resolve(null);
|
||||
});
|
||||
input.addEventListener('cancel', () => resolve(null));
|
||||
// Show the picker.
|
||||
if ('showPicker' in HTMLInputElement.prototype) {
|
||||
input.showPicker();
|
||||
@@ -188,16 +186,16 @@ export async function openFilesWith(
|
||||
});
|
||||
}
|
||||
|
||||
export function openSingleFileWith(
|
||||
export async function openSingleFileWith(
|
||||
acceptType?: AcceptTypes
|
||||
): Promise<File | null> {
|
||||
return openFilesWith(acceptType, false).then(files => files?.at(0) ?? null);
|
||||
const files = await openFilesWith(acceptType, false);
|
||||
return files?.at(0) ?? null;
|
||||
}
|
||||
|
||||
export async function getImageFilesFromLocal() {
|
||||
const imageFiles = await openFilesWith('Images');
|
||||
if (!imageFiles) return [];
|
||||
return imageFiles;
|
||||
const files = await openFilesWith('Images');
|
||||
return files ?? [];
|
||||
}
|
||||
|
||||
export function downloadBlob(blob: Blob, name: string) {
|
||||
|
||||
@@ -26,3 +26,34 @@ export function readImageSize(file: File | Blob) {
|
||||
img.src = sanitizedURL;
|
||||
});
|
||||
}
|
||||
|
||||
export function convertToPng(blob: Blob): Promise<Blob | null> {
|
||||
return new Promise(resolve => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.addEventListener('load', _ => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = img.width;
|
||||
c.height = img.height;
|
||||
const ctx = c.getContext('2d');
|
||||
if (!ctx) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
c.toBlob(resolve, 'image/png');
|
||||
};
|
||||
|
||||
img.onerror = () => resolve(null);
|
||||
|
||||
img.src = reader.result as string;
|
||||
});
|
||||
|
||||
reader.addEventListener('error', () => resolve(null));
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,35 +73,3 @@ export function substringMatchScore(name: string, query: string) {
|
||||
// normalize
|
||||
return 0.5 * score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the prefix is a markdown prefix.
|
||||
* Ex. 1. 2. 3. - * [] [ ] [x] # ## ### #### ##### ###### --- *** ___ > ```
|
||||
*/
|
||||
export function isMarkdownPrefix(prefix: string) {
|
||||
return (
|
||||
!!prefix.match(
|
||||
/^(\d+\.|-|\*|\[ ?\]|\[x\]|(#){1,6}|>|```([a-zA-Z0-9]*))$/
|
||||
) || isHorizontalRuleMarkdown(prefix)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the prefix is a valid markdown horizontal rule - https://www.markdownguide.org/basic-syntax/#horizontal-rules
|
||||
* @param prefix - The string to check for horizontal rule syntax
|
||||
* @returns boolean - True if the string represents a valid horizontal rule
|
||||
*
|
||||
* Valid horizontal rules:
|
||||
* - Three or more consecutive hyphens (e.g., "---", "----")
|
||||
* - Three or more consecutive asterisks (e.g., "***", "****")
|
||||
* - Three or more consecutive underscores (e.g., "___", "____")
|
||||
*
|
||||
* Invalid examples:
|
||||
* - Mixed characters (e.g., "--*", "-*-")
|
||||
* - Less than three characters (e.g., "--", "**")
|
||||
*/
|
||||
export function isHorizontalRuleMarkdown(prefix: string) {
|
||||
const horizontalRulePattern = /^(-{3,}|\*{3,}|_{3,})$/;
|
||||
|
||||
return !!prefix.match(horizontalRulePattern);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,11 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
}
|
||||
}
|
||||
|
||||
editor-toolbar[data-open][data-inline='true'] {
|
||||
transition-property: opacity, overlay, display, transform;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
editor-toolbar[data-placement='inner'] {
|
||||
background-color: unset;
|
||||
box-shadow: unset;
|
||||
@@ -536,9 +541,7 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
disposables.add(subscription);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -632,6 +635,7 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
|
||||
// Hides toolbar
|
||||
if (Flag.None === value || flags.check(Flag.Hiding, value)) {
|
||||
if ('inline' in toolbar.dataset) delete toolbar.dataset.inline;
|
||||
if (toolbar.dataset.open) delete toolbar.dataset.open;
|
||||
// Closes dropdown menus
|
||||
toolbar
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
shift,
|
||||
size,
|
||||
} from '@floating-ui/dom';
|
||||
import { html, nothing, render } from 'lit';
|
||||
import { html, render } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { join } from 'lit/directives/join.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
@@ -297,11 +297,22 @@ export function renderToolbar(
|
||||
render(
|
||||
join(
|
||||
renderActions(primaryActionGroup, context),
|
||||
innerToolbar ? nothing : renderToolbarSeparator()
|
||||
innerToolbar ? null : renderToolbarSeparator()
|
||||
),
|
||||
toolbar
|
||||
);
|
||||
|
||||
// Avoids shaking
|
||||
if (flavour === 'affine:note' && context.std.range.value) {
|
||||
if (!('inline' in toolbar.dataset)) {
|
||||
toolbar.dataset.inline = '';
|
||||
} else {
|
||||
toolbar.dataset.inline = 'true';
|
||||
}
|
||||
} else {
|
||||
delete toolbar.dataset.inline;
|
||||
}
|
||||
|
||||
if (toolbar.dataset.open) return;
|
||||
toolbar.dataset.open = 'true';
|
||||
}
|
||||
@@ -358,14 +369,22 @@ function renderActionItem(action: ToolbarAction, context: ToolbarContext) {
|
||||
const innerToolbar = context.placement$.value === 'inner';
|
||||
const ids = action.id.split('.');
|
||||
const id = ids[ids.length - 1];
|
||||
const label = action.label ?? action.tooltip ?? id;
|
||||
const actived =
|
||||
typeof action.active === 'function'
|
||||
? action.active(context)
|
||||
: action.active;
|
||||
const disabled =
|
||||
typeof action.disabled === 'function'
|
||||
? action.disabled(context)
|
||||
: action.disabled;
|
||||
|
||||
return html`
|
||||
<editor-icon-button
|
||||
data-testid=${ifDefined(id)}
|
||||
aria-label=${ifDefined(action.label ?? action.tooltip ?? id)}
|
||||
?active=${typeof action.active === 'function'
|
||||
? action.active(context)
|
||||
: action.active}
|
||||
?disabled=${action.disabled}
|
||||
aria-label=${ifDefined(label)}
|
||||
?active=${actived}
|
||||
?disabled=${disabled}
|
||||
.tooltip=${action.tooltip}
|
||||
.iconContainerPadding=${innerToolbar ? 4 : 2}
|
||||
.iconSize=${innerToolbar ? '16px' : undefined}
|
||||
@@ -383,17 +402,24 @@ function renderMenuActionItem(action: ToolbarAction, context: ToolbarContext) {
|
||||
const innerToolbar = context.placement$.value === 'inner';
|
||||
const ids = action.id.split('.');
|
||||
const id = ids[ids.length - 1];
|
||||
const label = action.label ?? action.tooltip ?? id;
|
||||
const actived =
|
||||
typeof action.active === 'function'
|
||||
? action.active(context)
|
||||
: action.active;
|
||||
const disabled =
|
||||
typeof action.disabled === 'function'
|
||||
? action.disabled(context)
|
||||
: action.disabled;
|
||||
const destructive = action.variant === 'destructive' ? 'delete' : undefined;
|
||||
|
||||
return html`
|
||||
<editor-menu-action
|
||||
data-testid=${ifDefined(id)}
|
||||
aria-label=${ifDefined(action.label ?? action.tooltip ?? id)}
|
||||
class="${ifDefined(
|
||||
action.variant === 'destructive' ? 'delete' : undefined
|
||||
)}"
|
||||
?active=${typeof action.active === 'function'
|
||||
? action.active(context)
|
||||
: action.active}
|
||||
?disabled=${action.disabled}
|
||||
aria-label=${ifDefined(label)}
|
||||
class="${ifDefined(destructive)}"
|
||||
?active=${actived}
|
||||
?disabled=${disabled}
|
||||
.tooltip=${ifDefined(action.tooltip)}
|
||||
.iconContainerPadding=${innerToolbar ? 4 : 2}
|
||||
.iconSize=${innerToolbar ? '16px' : undefined}
|
||||
|
||||
@@ -118,10 +118,16 @@ Get the root block of the store.
|
||||
|
||||
### addBlock()
|
||||
|
||||
> **addBlock**(`flavour`, `blockProps`, `parent?`, `parentIndex?`): `string`
|
||||
> **addBlock**\<`T`\>(`flavour`, `blockProps`, `parent?`, `parentIndex?`): `string`
|
||||
|
||||
Creates and adds a new block to the store
|
||||
|
||||
#### Type Parameters
|
||||
|
||||
##### T
|
||||
|
||||
`T` *extends* `BlockModel`\<`object`\> = `BlockModel`\<`object`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### flavour
|
||||
@@ -132,7 +138,7 @@ The block's flavour (type)
|
||||
|
||||
##### blockProps
|
||||
|
||||
`Partial`\<`BlockSysProps` & `Record`\<`string`, `unknown`\> & `Omit`\<`BlockProps`, `"flavour"`\>\> = `{}`
|
||||
`Partial`\<`BlockProps` \| `PropsOfModel`\<`T`\> & `BlockSysProps`\> = `{}`
|
||||
|
||||
Optional properties for the new block
|
||||
|
||||
|
||||
@@ -124,18 +124,23 @@ export class Clipboard extends LifeCycleWatcher {
|
||||
copySlice = async (slice: Slice) => {
|
||||
const adapterKeys = this._adapters.map(adapter => adapter.mimeType);
|
||||
|
||||
await this.writeToClipboard(async _items => {
|
||||
const items = { ..._items };
|
||||
await this.writeToClipboard(async items => {
|
||||
const filtered = (
|
||||
await Promise.all(
|
||||
adapterKeys.map(async type => {
|
||||
const item = await this._getClipboardItem(slice, type);
|
||||
if (typeof item === 'string') {
|
||||
return [type, item];
|
||||
}
|
||||
return null;
|
||||
})
|
||||
)
|
||||
).filter((adapter): adapter is string[] => Boolean(adapter));
|
||||
|
||||
await Promise.all(
|
||||
adapterKeys.map(async type => {
|
||||
const item = await this._getClipboardItem(slice, type);
|
||||
if (typeof item === 'string') {
|
||||
items[type] = item;
|
||||
}
|
||||
})
|
||||
);
|
||||
return items;
|
||||
return {
|
||||
...items,
|
||||
...Object.fromEntries(filtered),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -263,49 +268,56 @@ export class Clipboard extends LifeCycleWatcher {
|
||||
}
|
||||
|
||||
async writeToClipboard(
|
||||
updateItems: (
|
||||
items: Record<string, unknown>
|
||||
) => Promise<Record<string, unknown>> | Record<string, unknown>
|
||||
updateItems: <T extends Record<string, unknown>>(items: T) => Promise<T> | T
|
||||
) {
|
||||
const _items = {
|
||||
const items = await updateItems<
|
||||
Partial<{
|
||||
'text/plain': string;
|
||||
'text/html': string;
|
||||
'image/png': string | Blob;
|
||||
}>
|
||||
>({
|
||||
'text/plain': '',
|
||||
'text/html': '',
|
||||
'image/png': '',
|
||||
};
|
||||
|
||||
const items = await updateItems(_items);
|
||||
|
||||
const text = items['text/plain'] as string;
|
||||
const innerHTML = items['text/html'] as string;
|
||||
const png = items['image/png'] as string | Blob;
|
||||
});
|
||||
const text = items['text/plain'] ?? '';
|
||||
const innerHTML = items['text/html'] ?? '';
|
||||
const image = items['image/png'];
|
||||
|
||||
delete items['text/plain'];
|
||||
delete items['text/html'];
|
||||
delete items['image/png'];
|
||||
|
||||
const snapshot = lz.compressToEncodedURIComponent(JSON.stringify(items));
|
||||
const html = `<div data-blocksuite-snapshot='${snapshot}'>${innerHTML}</div>`;
|
||||
const htmlBlob = new Blob([html], {
|
||||
type: 'text/html',
|
||||
});
|
||||
const clipboardItems: Record<string, Blob> = {
|
||||
'text/html': htmlBlob,
|
||||
};
|
||||
const clipboardItems: Record<string, Blob> = {};
|
||||
|
||||
if (image) {
|
||||
const type = 'image/png';
|
||||
|
||||
delete items[type];
|
||||
|
||||
if (typeof image === 'string') {
|
||||
clipboardItems[type] = new Blob([image], { type });
|
||||
} else if (image instanceof Blob) {
|
||||
clipboardItems[type] = image;
|
||||
}
|
||||
}
|
||||
|
||||
if (text.length > 0) {
|
||||
const textBlob = new Blob([text], {
|
||||
type: 'text/plain',
|
||||
});
|
||||
clipboardItems['text/plain'] = textBlob;
|
||||
const type = 'text/plain';
|
||||
clipboardItems[type] = new Blob([text], { type });
|
||||
}
|
||||
|
||||
if (png instanceof Blob) {
|
||||
clipboardItems['image/png'] = png;
|
||||
} else if (png.length > 0) {
|
||||
const pngBlob = new Blob([png], {
|
||||
type: 'image/png',
|
||||
});
|
||||
clipboardItems['image/png'] = pngBlob;
|
||||
const hasInnerHTML = Boolean(innerHTML.length);
|
||||
const isEmpty = Object.keys(clipboardItems).length === 0;
|
||||
|
||||
// If there are no items, fall back to snapshot.
|
||||
if (hasInnerHTML || isEmpty) {
|
||||
const type = 'text/html';
|
||||
const snapshot = lz.compressToEncodedURIComponent(JSON.stringify(items));
|
||||
const html = `<div data-blocksuite-snapshot='${snapshot}'>${innerHTML}</div>`;
|
||||
clipboardItems[type] = new Blob([html], { type });
|
||||
}
|
||||
|
||||
await navigator.clipboard.write([new ClipboardItem(clipboardItems)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,8 +165,9 @@ export class InlineEditor<
|
||||
inlineRangeSync: new Subject<Range | null>(),
|
||||
/**
|
||||
* Corresponding to the `compositionUpdate` and `beforeInput` events, and triggered only when the `inlineRange` is not null.
|
||||
* The parameter is the `event.data`.
|
||||
*/
|
||||
inputting: new Subject<void>(),
|
||||
inputting: new Subject<string>(),
|
||||
/**
|
||||
* Triggered only when the `inlineRange` is not null.
|
||||
*/
|
||||
|
||||
@@ -119,7 +119,7 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
|
||||
this.editor as never
|
||||
);
|
||||
|
||||
this.editor.slots.inputting.next();
|
||||
this.editor.slots.inputting.next(event.data ?? '');
|
||||
};
|
||||
|
||||
private readonly _onClick = (event: MouseEvent) => {
|
||||
@@ -181,10 +181,10 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
|
||||
});
|
||||
}
|
||||
|
||||
this.editor.slots.inputting.next();
|
||||
this.editor.slots.inputting.next(event.data ?? '');
|
||||
};
|
||||
|
||||
private readonly _onCompositionStart = () => {
|
||||
private readonly _onCompositionStart = (event: CompositionEvent) => {
|
||||
this._isComposing = true;
|
||||
if (!this.editor.rootElement) return;
|
||||
// embeds is not editable and it will break IME
|
||||
@@ -201,9 +201,11 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
|
||||
} else {
|
||||
this._compositionInlineRange = null;
|
||||
}
|
||||
|
||||
this.editor.slots.inputting.next(event.data ?? '');
|
||||
};
|
||||
|
||||
private readonly _onCompositionUpdate = () => {
|
||||
private readonly _onCompositionUpdate = (event: CompositionEvent) => {
|
||||
if (!this.editor.rootElement || !this.editor.rootElement.isConnected) {
|
||||
return;
|
||||
}
|
||||
@@ -216,7 +218,7 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
|
||||
)
|
||||
return;
|
||||
|
||||
this.editor.slots.inputting.next();
|
||||
this.editor.slots.inputting.next(event.data ?? '');
|
||||
};
|
||||
|
||||
private readonly _onKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -359,13 +361,9 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
|
||||
'compositionupdate',
|
||||
this._onCompositionUpdate
|
||||
);
|
||||
this.editor.disposables.addFromEvent(
|
||||
eventSource,
|
||||
'compositionend',
|
||||
(event: CompositionEvent) => {
|
||||
this._onCompositionEnd(event).catch(console.error);
|
||||
}
|
||||
);
|
||||
this.editor.disposables.addFromEvent(eventSource, 'compositionend', e => {
|
||||
this._onCompositionEnd(e).catch(console.error);
|
||||
});
|
||||
this.editor.disposables.addFromEvent(
|
||||
eventSource,
|
||||
'keydown',
|
||||
|
||||
@@ -740,9 +740,9 @@ export class Store {
|
||||
*
|
||||
* @category Block CRUD
|
||||
*/
|
||||
addBlock(
|
||||
addBlock<T extends BlockModel = BlockModel>(
|
||||
flavour: string,
|
||||
blockProps: Partial<BlockProps & Omit<BlockProps, 'flavour'>> = {},
|
||||
blockProps: Partial<(PropsOfModel<T> & BlockSysProps) | BlockProps> = {},
|
||||
parent?: BlockModel | string | null,
|
||||
parentIndex?: number
|
||||
): string {
|
||||
|
||||
@@ -52,6 +52,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
[
|
||||
{
|
||||
id: 'docId1',
|
||||
status: 'processing',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
Binary file not shown.
@@ -11,11 +11,11 @@ import { JobQueue } from '../base';
|
||||
import { ConfigModule } from '../base/config';
|
||||
import { AuthService } from '../core/auth';
|
||||
import { DocReader } from '../core/doc';
|
||||
import { CopilotContextService } from '../plugins/copilot/context';
|
||||
import {
|
||||
CopilotContextDocJob,
|
||||
CopilotContextService,
|
||||
} from '../plugins/copilot/context';
|
||||
import { MockEmbeddingClient } from '../plugins/copilot/context/embedding';
|
||||
CopilotEmbeddingJob,
|
||||
MockEmbeddingClient,
|
||||
} from '../plugins/copilot/embedding';
|
||||
import { prompts, PromptService } from '../plugins/copilot/prompt';
|
||||
import {
|
||||
CopilotProviderFactory,
|
||||
@@ -65,7 +65,7 @@ const test = ava as TestFn<{
|
||||
app: TestingApp;
|
||||
db: PrismaClient;
|
||||
context: CopilotContextService;
|
||||
jobs: CopilotContextDocJob;
|
||||
jobs: CopilotEmbeddingJob;
|
||||
prompt: PromptService;
|
||||
factory: CopilotProviderFactory;
|
||||
storage: CopilotStorage;
|
||||
@@ -115,7 +115,7 @@ test.before(async t => {
|
||||
const context = app.get(CopilotContextService);
|
||||
const prompt = app.get(PromptService);
|
||||
const storage = app.get(CopilotStorage);
|
||||
const jobs = app.get(CopilotContextDocJob);
|
||||
const jobs = app.get(CopilotEmbeddingJob);
|
||||
|
||||
t.context.app = app;
|
||||
t.context.db = db;
|
||||
|
||||
@@ -13,11 +13,11 @@ import { AuthService } from '../core/auth';
|
||||
import { QuotaModule } from '../core/quota';
|
||||
import { ContextCategories, WorkspaceModel } from '../models';
|
||||
import { CopilotModule } from '../plugins/copilot';
|
||||
import { CopilotContextService } from '../plugins/copilot/context';
|
||||
import {
|
||||
CopilotContextDocJob,
|
||||
CopilotContextService,
|
||||
} from '../plugins/copilot/context';
|
||||
import { MockEmbeddingClient } from '../plugins/copilot/context/embedding';
|
||||
CopilotEmbeddingJob,
|
||||
MockEmbeddingClient,
|
||||
} from '../plugins/copilot/embedding';
|
||||
import { prompts, PromptService } from '../plugins/copilot/prompt';
|
||||
import {
|
||||
CopilotProviderFactory,
|
||||
@@ -69,7 +69,7 @@ const test = ava as TestFn<{
|
||||
workspaceEmbedding: CopilotWorkspaceService;
|
||||
factory: CopilotProviderFactory;
|
||||
session: ChatSessionService;
|
||||
jobs: CopilotContextDocJob;
|
||||
jobs: CopilotEmbeddingJob;
|
||||
storage: CopilotStorage;
|
||||
workflow: CopilotWorkflowService;
|
||||
executors: {
|
||||
@@ -127,7 +127,7 @@ test.before(async t => {
|
||||
const storage = module.get(CopilotStorage);
|
||||
|
||||
const context = module.get(CopilotContextService);
|
||||
const jobs = module.get(CopilotContextDocJob);
|
||||
const jobs = module.get(CopilotEmbeddingJob);
|
||||
const transcript = module.get(CopilotTranscriptionService);
|
||||
const workspaceEmbedding = module.get(CopilotWorkspaceService);
|
||||
|
||||
|
||||
@@ -53,3 +53,195 @@ Generated by [AVA](https://avajs.dev).
|
||||
> should return true when embedding table is available
|
||||
|
||||
true
|
||||
|
||||
## should merge doc status correctly
|
||||
|
||||
> basic doc status merge
|
||||
|
||||
[
|
||||
{
|
||||
id: 'doc1',
|
||||
status: 'processing',
|
||||
},
|
||||
{
|
||||
id: 'doc2',
|
||||
status: 'processing',
|
||||
},
|
||||
{
|
||||
id: 'doc3',
|
||||
status: 'failed',
|
||||
},
|
||||
{
|
||||
id: 'doc4',
|
||||
status: 'processing',
|
||||
},
|
||||
]
|
||||
|
||||
> mixed doc status merge
|
||||
|
||||
[
|
||||
{
|
||||
id: 'doc5',
|
||||
status: 'finished',
|
||||
},
|
||||
{
|
||||
id: 'doc5',
|
||||
status: 'finished',
|
||||
},
|
||||
{
|
||||
id: 'doc6',
|
||||
status: 'processing',
|
||||
},
|
||||
{
|
||||
id: 'doc6',
|
||||
status: 'failed',
|
||||
},
|
||||
{
|
||||
id: 'doc7',
|
||||
status: 'processing',
|
||||
},
|
||||
]
|
||||
|
||||
> edge cases results
|
||||
|
||||
[
|
||||
{
|
||||
case: 0,
|
||||
length: 1,
|
||||
statuses: [
|
||||
'processing',
|
||||
],
|
||||
},
|
||||
{
|
||||
case: 1,
|
||||
length: 1,
|
||||
statuses: [
|
||||
'processing',
|
||||
],
|
||||
},
|
||||
{
|
||||
case: 2,
|
||||
length: 100,
|
||||
statuses: [
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
## should handle concurrent mergeDocStatus calls
|
||||
|
||||
> concurrent calls results
|
||||
|
||||
[
|
||||
{
|
||||
call: 1,
|
||||
status: 'finished',
|
||||
},
|
||||
{
|
||||
call: 2,
|
||||
status: 'finished',
|
||||
},
|
||||
{
|
||||
call: 3,
|
||||
status: 'processing',
|
||||
},
|
||||
]
|
||||
|
||||
Binary file not shown.
@@ -2,8 +2,10 @@ import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { AiSession, PrismaClient, User, Workspace } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { Config } from '../../base';
|
||||
import { ContextEmbedStatus } from '../../models/common/copilot';
|
||||
import { CopilotContextModel } from '../../models/copilot-context';
|
||||
import { CopilotSessionModel } from '../../models/copilot-session';
|
||||
import { CopilotWorkspaceConfigModel } from '../../models/copilot-workspace';
|
||||
@@ -236,3 +238,173 @@ test('should check embedding table', async t => {
|
||||
// t.false(ret, 'should return false when embedding table is not available');
|
||||
// }
|
||||
});
|
||||
|
||||
test('should merge doc status correctly', async t => {
|
||||
const createDoc = (id: string, status?: string) => ({
|
||||
id,
|
||||
createdAt: Date.now(),
|
||||
...(status && { status: status as any }),
|
||||
});
|
||||
|
||||
const createDocWithEmbedding = async (docId: string) => {
|
||||
await t.context.db.snapshot.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
id: docId,
|
||||
blob: Buffer.from([1, 1]),
|
||||
state: Buffer.from([1, 1]),
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await t.context.copilotContext.insertWorkspaceEmbedding(
|
||||
workspace.id,
|
||||
docId,
|
||||
[
|
||||
{
|
||||
index: 0,
|
||||
content: 'content',
|
||||
embedding: Array.from({ length: 1024 }, () => 1),
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const emptyResult = await t.context.copilotContext.mergeDocStatus(
|
||||
workspace.id,
|
||||
[]
|
||||
);
|
||||
t.deepEqual(emptyResult, []);
|
||||
|
||||
const basicDocs = [
|
||||
createDoc('doc1'),
|
||||
createDoc('doc2'),
|
||||
createDoc('doc3', 'failed'),
|
||||
createDoc('doc4', 'processing'),
|
||||
];
|
||||
const basicResult = await t.context.copilotContext.mergeDocStatus(
|
||||
workspace.id,
|
||||
basicDocs
|
||||
);
|
||||
t.snapshot(
|
||||
basicResult.map(d => ({ id: d.id, status: d.status })),
|
||||
'basic doc status merge'
|
||||
);
|
||||
|
||||
{
|
||||
await createDocWithEmbedding('doc5');
|
||||
|
||||
const mixedDocs = [
|
||||
createDoc('doc5'),
|
||||
createDoc('doc5', 'processing'),
|
||||
createDoc('doc6'),
|
||||
createDoc('doc6', 'failed'),
|
||||
createDoc('doc7'),
|
||||
];
|
||||
const mixedResult = await t.context.copilotContext.mergeDocStatus(
|
||||
workspace.id,
|
||||
mixedDocs
|
||||
);
|
||||
t.snapshot(
|
||||
mixedResult.map(d => ({ id: d.id, status: d.status })),
|
||||
'mixed doc status merge'
|
||||
);
|
||||
|
||||
const hasEmbeddingStub = Sinon.stub(
|
||||
t.context.copilotContext,
|
||||
'hasWorkspaceEmbedding'
|
||||
).resolves(new Set<string>());
|
||||
|
||||
const stubResult = await t.context.copilotContext.mergeDocStatus(
|
||||
workspace.id,
|
||||
[createDoc('doc5')]
|
||||
);
|
||||
t.is(stubResult[0].status, ContextEmbedStatus.processing);
|
||||
|
||||
hasEmbeddingStub.restore();
|
||||
}
|
||||
|
||||
{
|
||||
const testCases = [
|
||||
{
|
||||
workspaceId: 'invalid-workspace',
|
||||
docs: [{ id: 'doc1', createdAt: Date.now() }],
|
||||
},
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
docs: [{ id: 'doc1', createdAt: Date.now(), status: undefined as any }],
|
||||
},
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
docs: Array.from({ length: 100 }, (_, i) => ({
|
||||
id: `doc-${i}`,
|
||||
createdAt: Date.now() + i,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const results = await Promise.all(
|
||||
testCases.map(testCase =>
|
||||
t.context.copilotContext.mergeDocStatus(
|
||||
testCase.workspaceId,
|
||||
testCase.docs
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
t.snapshot(
|
||||
results.map((result, index) => ({
|
||||
case: index,
|
||||
length: result.length,
|
||||
statuses: result.map(d => d.status),
|
||||
})),
|
||||
'edge cases results'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle concurrent mergeDocStatus calls', async t => {
|
||||
await t.context.db.snapshot.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
id: 'concurrent-doc',
|
||||
blob: Buffer.from([1, 1]),
|
||||
state: Buffer.from([1, 1]),
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await t.context.copilotContext.insertWorkspaceEmbedding(
|
||||
workspace.id,
|
||||
'concurrent-doc',
|
||||
[
|
||||
{
|
||||
index: 0,
|
||||
content: 'content',
|
||||
embedding: Array.from({ length: 1024 }, () => 1),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const concurrentDocs = [
|
||||
[{ id: 'concurrent-doc', createdAt: Date.now() }],
|
||||
[{ id: 'concurrent-doc', createdAt: Date.now() + 1000 }],
|
||||
[{ id: 'non-existent-doc', createdAt: Date.now() }],
|
||||
];
|
||||
|
||||
const results = await Promise.all(
|
||||
concurrentDocs.map(docs =>
|
||||
t.context.copilotContext.mergeDocStatus(workspace.id, docs)
|
||||
)
|
||||
);
|
||||
|
||||
t.snapshot(
|
||||
results.map((result, index) => ({
|
||||
call: index + 1,
|
||||
status: result[0].status,
|
||||
})),
|
||||
'concurrent calls results'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -36,6 +36,10 @@ test.beforeEach(async t => {
|
||||
workspace = await t.context.models.workspace.create(user.id);
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
@@ -176,6 +180,15 @@ test('should get doc content in json format', async t => {
|
||||
summary: 'test summary',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docId}/content?full=false`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect({
|
||||
title: 'test title',
|
||||
summary: 'test summary',
|
||||
})
|
||||
.expect(200);
|
||||
t.pass();
|
||||
});
|
||||
|
||||
@@ -184,7 +197,7 @@ test('should get full doc content in json format', async t => {
|
||||
mock.method(t.context.databaseDocReader, 'getFullDocContent', async () => {
|
||||
return {
|
||||
title: 'test title',
|
||||
summary: 'test summary',
|
||||
summary: 'test summary full',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -194,7 +207,7 @@ test('should get full doc content in json format', async t => {
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect({
|
||||
title: 'test title',
|
||||
summary: 'test summary',
|
||||
summary: 'test summary full',
|
||||
})
|
||||
.expect(200);
|
||||
t.pass();
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
Logger,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
RawBody,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { Args } from '@nestjs/graphql';
|
||||
import type { Response } from 'express';
|
||||
|
||||
import { NotFound, SkipThrottle } from '../../base';
|
||||
@@ -78,11 +78,12 @@ export class DocRpcController {
|
||||
async getDocContent(
|
||||
@Param('workspaceId') workspaceId: string,
|
||||
@Param('docId') docId: string,
|
||||
@Args('full', { nullable: true }) fullContent?: boolean
|
||||
@Query('full') fullContent?: string
|
||||
) {
|
||||
const content = fullContent
|
||||
? await this.docReader.getFullDocContent(workspaceId, docId)
|
||||
: await this.docReader.getDocContent(workspaceId, docId);
|
||||
const content =
|
||||
fullContent === 'true'
|
||||
? await this.docReader.getFullDocContent(workspaceId, docId)
|
||||
: await this.docReader.getDocContent(workspaceId, docId);
|
||||
if (!content) {
|
||||
throw new NotFound('Doc not found');
|
||||
}
|
||||
|
||||
@@ -91,7 +91,9 @@ export class CopilotContextModel extends BaseModel {
|
||||
const status = finishedDoc.has(doc.id)
|
||||
? ContextEmbedStatus.finished
|
||||
: undefined;
|
||||
doc.status = status || doc.status;
|
||||
// NOTE: when the document has not been synchronized to the server or is in the embedding queue
|
||||
// the status will be empty, fallback to processing if no status is provided
|
||||
doc.status = status || doc.status || ContextEmbedStatus.processing;
|
||||
}
|
||||
|
||||
return docs;
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { CopilotContextDocJob } from './job';
|
||||
export { CopilotContextResolver, CopilotContextRootResolver } from './resolver';
|
||||
export { CopilotContextService } from './service';
|
||||
|
||||
@@ -44,12 +44,12 @@ import {
|
||||
FileChunkSimilarity,
|
||||
Models,
|
||||
} from '../../../models';
|
||||
import { CopilotEmbeddingJob } from '../embedding';
|
||||
import { COPILOT_LOCKER, CopilotType } from '../resolver';
|
||||
import { ChatSessionService } from '../session';
|
||||
import { CopilotStorage } from '../storage';
|
||||
import { MAX_EMBEDDABLE_SIZE } from '../types';
|
||||
import { readStream } from '../utils';
|
||||
import { CopilotContextDocJob } from './job';
|
||||
import { CopilotContextService } from './service';
|
||||
|
||||
@InputType()
|
||||
@@ -387,7 +387,7 @@ export class CopilotContextResolver {
|
||||
private readonly models: Models,
|
||||
private readonly mutex: RequestMutex,
|
||||
private readonly context: CopilotContextService,
|
||||
private readonly jobs: CopilotContextDocJob,
|
||||
private readonly jobs: CopilotEmbeddingJob,
|
||||
private readonly storage: CopilotStorage
|
||||
) {}
|
||||
|
||||
|
||||
@@ -14,11 +14,10 @@ import {
|
||||
ContextFile,
|
||||
Models,
|
||||
} from '../../../models';
|
||||
import { type EmbeddingClient, getEmbeddingClient } from '../embedding';
|
||||
import { PromptService } from '../prompt';
|
||||
import { CopilotProviderFactory } from '../providers';
|
||||
import { getEmbeddingClient } from './embedding';
|
||||
import { ContextSession } from './session';
|
||||
import type { EmbeddingClient } from './types';
|
||||
|
||||
const CONTEXT_SESSION_KEY = 'context-session';
|
||||
|
||||
@@ -158,14 +157,15 @@ export class CopilotContextService implements OnApplicationBootstrap {
|
||||
const embedding = await this.embeddingClient.getEmbedding(content, signal);
|
||||
if (!embedding) return [];
|
||||
|
||||
const chunks = await this.models.copilotWorkspace.matchFileEmbedding(
|
||||
const fileChunks = await this.models.copilotWorkspace.matchFileEmbedding(
|
||||
workspaceId,
|
||||
embedding,
|
||||
topK * 2,
|
||||
threshold
|
||||
);
|
||||
if (!fileChunks.length) return [];
|
||||
|
||||
return this.embeddingClient.reRank(content, chunks, topK, signal);
|
||||
return this.embeddingClient.reRank(content, fileChunks, topK, signal);
|
||||
}
|
||||
|
||||
async matchWorkspaceDocs(
|
||||
@@ -179,14 +179,16 @@ export class CopilotContextService implements OnApplicationBootstrap {
|
||||
const embedding = await this.embeddingClient.getEmbedding(content, signal);
|
||||
if (!embedding) return [];
|
||||
|
||||
const workspace = await this.models.copilotContext.matchWorkspaceEmbedding(
|
||||
embedding,
|
||||
workspaceId,
|
||||
topK * 2,
|
||||
threshold
|
||||
);
|
||||
const workspaceChunks =
|
||||
await this.models.copilotContext.matchWorkspaceEmbedding(
|
||||
embedding,
|
||||
workspaceId,
|
||||
topK * 2,
|
||||
threshold
|
||||
);
|
||||
if (!workspaceChunks.length) return [];
|
||||
|
||||
return this.embeddingClient.reRank(content, workspace, topK);
|
||||
return this.embeddingClient.reRank(content, workspaceChunks, topK, signal);
|
||||
}
|
||||
|
||||
@OnEvent('workspace.doc.embed.failed')
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
FileChunkSimilarity,
|
||||
Models,
|
||||
} from '../../../models';
|
||||
import { EmbeddingClient } from './types';
|
||||
import { EmbeddingClient } from '../embedding';
|
||||
|
||||
export class ContextSession implements AsyncDisposable {
|
||||
constructor(
|
||||
private readonly client: EmbeddingClient,
|
||||
private readonly client: EmbeddingClient | undefined,
|
||||
private readonly contextId: string,
|
||||
private readonly config: ContextConfig,
|
||||
private readonly models: Models,
|
||||
@@ -204,6 +204,7 @@ export class ContextSession implements AsyncDisposable {
|
||||
scopedThreshold: number = 0.85,
|
||||
threshold: number = 0.5
|
||||
): Promise<FileChunkSimilarity[]> {
|
||||
if (!this.client) return [];
|
||||
const embedding = await this.client.getEmbedding(content, signal);
|
||||
if (!embedding) return [];
|
||||
|
||||
@@ -256,6 +257,7 @@ export class ContextSession implements AsyncDisposable {
|
||||
scopedThreshold: number = 0.85,
|
||||
threshold: number = 0.5
|
||||
) {
|
||||
if (!this.client) return [];
|
||||
const embedding = await this.client.getEmbedding(content, signal);
|
||||
if (!embedding) return [];
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export class GqlSignal implements AsyncDisposable {
|
||||
readonly abortController = new AbortController();
|
||||
|
||||
get signal() {
|
||||
return this.abortController.signal;
|
||||
}
|
||||
|
||||
async [Symbol.asyncDispose]() {
|
||||
this.abortController.abort();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { chunk } from 'lodash-es';
|
||||
|
||||
import {
|
||||
CopilotPromptNotFound,
|
||||
CopilotProviderNotSupported,
|
||||
} from '../../../base';
|
||||
import type { ChunkSimilarity, Embedding } from '../../../models';
|
||||
import type { PromptService } from '../prompt';
|
||||
import { ChunkSimilarity, Embedding } from '../../../models';
|
||||
import { PromptService } from '../prompt';
|
||||
import {
|
||||
type CopilotProvider,
|
||||
type CopilotProviderFactory,
|
||||
CopilotProviderFactory,
|
||||
type ModelFullConditions,
|
||||
ModelInputType,
|
||||
ModelOutputType,
|
||||
@@ -23,7 +22,7 @@ import {
|
||||
|
||||
const RERANK_PROMPT = 'Rerank results';
|
||||
|
||||
export class ProductionEmbeddingClient extends EmbeddingClient {
|
||||
class ProductionEmbeddingClient extends EmbeddingClient {
|
||||
private readonly logger = new Logger(ProductionEmbeddingClient.name);
|
||||
|
||||
constructor(
|
||||
@@ -63,7 +62,9 @@ export class ProductionEmbeddingClient extends EmbeddingClient {
|
||||
const provider = await this.getProvider({
|
||||
outputType: ModelOutputType.Embedding,
|
||||
});
|
||||
this.logger.verbose(`Using provider ${provider.type} for embedding`, input);
|
||||
this.logger.verbose(
|
||||
`Using provider ${provider.type} for embedding: ${input.join(', ')}`
|
||||
);
|
||||
|
||||
const embeddings = await provider.embedding(
|
||||
{ inputTypes: [ModelInputType.Text] },
|
||||
@@ -78,6 +79,14 @@ export class ProductionEmbeddingClient extends EmbeddingClient {
|
||||
}));
|
||||
}
|
||||
|
||||
private getTargetId<T extends ChunkSimilarity>(embedding: T) {
|
||||
return 'docId' in embedding
|
||||
? embedding.docId
|
||||
: 'fileId' in embedding
|
||||
? embedding.fileId
|
||||
: '';
|
||||
}
|
||||
|
||||
private async getEmbeddingRelevance<
|
||||
Chunk extends ChunkSimilarity = ChunkSimilarity,
|
||||
>(
|
||||
@@ -98,11 +107,11 @@ export class ProductionEmbeddingClient extends EmbeddingClient {
|
||||
{ modelId: prompt.model },
|
||||
prompt.finish({
|
||||
query,
|
||||
results: embeddings.map(e => {
|
||||
const targetId =
|
||||
'docId' in e ? e.docId : 'fileId' in e ? e.fileId : '';
|
||||
return { targetId, chunk: e.chunk, content: e.content };
|
||||
}),
|
||||
results: embeddings.map(e => ({
|
||||
targetId: this.getTargetId(e),
|
||||
chunk: e.chunk,
|
||||
content: e.content,
|
||||
})),
|
||||
schema,
|
||||
}),
|
||||
{ maxRetries: 3, signal }
|
||||
@@ -123,7 +132,19 @@ export class ProductionEmbeddingClient extends EmbeddingClient {
|
||||
topK: number,
|
||||
signal?: AbortSignal
|
||||
): Promise<Chunk[]> {
|
||||
const sortedEmbeddings = embeddings.toSorted(
|
||||
// search in context and workspace may find same chunks, de-duplicate them
|
||||
const { deduped: dedupedEmbeddings } = embeddings.reduce(
|
||||
(acc, e) => {
|
||||
const key = `${this.getTargetId(e)}:${e.chunk}`;
|
||||
if (!acc.seen.has(key)) {
|
||||
acc.seen.add(key);
|
||||
acc.deduped.push(e);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ deduped: [] as Chunk[], seen: new Set<string>() }
|
||||
);
|
||||
const sortedEmbeddings = dedupedEmbeddings.toSorted(
|
||||
(a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity)
|
||||
);
|
||||
|
||||
@@ -137,24 +158,36 @@ export class ProductionEmbeddingClient extends EmbeddingClient {
|
||||
{} as Record<string, Chunk>
|
||||
);
|
||||
|
||||
const ranks = [];
|
||||
for (const c of chunk(sortedEmbeddings, Math.min(topK, 10))) {
|
||||
const rank = await this.getEmbeddingRelevance(query, c, signal);
|
||||
if (c.length !== rank.length) {
|
||||
try {
|
||||
// 4.1 mini's context windows large enough to handle all embeddings
|
||||
const ranks = await this.getEmbeddingRelevance(
|
||||
query,
|
||||
sortedEmbeddings,
|
||||
signal
|
||||
);
|
||||
if (sortedEmbeddings.length !== ranks.length) {
|
||||
// llm return wrong result, fallback to default sorting
|
||||
return super.reRank(query, embeddings, topK, signal);
|
||||
this.logger.warn(
|
||||
`Batch size mismatch: expected ${sortedEmbeddings.length}, got ${ranks.length}`
|
||||
);
|
||||
return await super.reRank(query, dedupedEmbeddings, topK, signal);
|
||||
}
|
||||
ranks.push(rank);
|
||||
|
||||
const highConfidenceChunks = ranks
|
||||
.flat()
|
||||
.toSorted((a, b) => b.scores.score - a.scores.score)
|
||||
.filter(r => r.scores.score > 5)
|
||||
.map(r => chunks[`${r.scores.targetId}:${r.scores.chunk}`])
|
||||
.filter(Boolean);
|
||||
|
||||
this.logger.verbose(
|
||||
`ReRank completed: ${highConfidenceChunks.length} high-confidence results found`
|
||||
);
|
||||
return highConfidenceChunks.slice(0, topK);
|
||||
} catch (error) {
|
||||
this.logger.warn('ReRank failed, falling back to default sorting', error);
|
||||
return await super.reRank(query, dedupedEmbeddings, topK, signal);
|
||||
}
|
||||
|
||||
const highConfidenceChunks = ranks
|
||||
.flat()
|
||||
.toSorted((a, b) => b.scores.score - a.scores.score)
|
||||
.filter(r => r.scores.score > 5)
|
||||
.map(r => chunks[`${r.scores.targetId}:${r.scores.chunk}`])
|
||||
.filter(Boolean);
|
||||
|
||||
return highConfidenceChunks.slice(0, topK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export { getEmbeddingClient, MockEmbeddingClient } from './client';
|
||||
export { CopilotEmbeddingJob } from './job';
|
||||
export type { Chunk, DocFragment } from './types';
|
||||
export { EMBEDDING_DIMENSIONS, EmbeddingClient } from './types';
|
||||
@@ -18,12 +18,12 @@ import { PromptService } from '../prompt';
|
||||
import { CopilotProviderFactory } from '../providers';
|
||||
import { CopilotStorage } from '../storage';
|
||||
import { readStream } from '../utils';
|
||||
import { getEmbeddingClient } from './embedding';
|
||||
import { getEmbeddingClient } from './client';
|
||||
import type { Chunk, DocFragment } from './types';
|
||||
import { EMBEDDING_DIMENSIONS, EmbeddingClient } from './types';
|
||||
|
||||
@Injectable()
|
||||
export class CopilotContextDocJob {
|
||||
export class CopilotEmbeddingJob {
|
||||
private readonly workspaceJobAbortController: Map<string, AbortController> =
|
||||
new Map();
|
||||
|
||||
@@ -40,7 +40,7 @@ export class CopilotContextDocJob {
|
||||
private readonly queue: JobQueue,
|
||||
private readonly storage: CopilotStorage
|
||||
) {
|
||||
this.logger.setContext(CopilotContextDocJob.name);
|
||||
this.logger.setContext(CopilotEmbeddingJob.name);
|
||||
}
|
||||
|
||||
@OnEvent('config.init')
|
||||
@@ -140,10 +140,9 @@ export class CopilotContextDocJob {
|
||||
if (enableDocEmbedding) {
|
||||
const toBeEmbedDocIds =
|
||||
await this.models.copilotWorkspace.findDocsToEmbed(workspaceId);
|
||||
this.logger.debug('Trigger embedding for docs', {
|
||||
workspaceId,
|
||||
toBeEmbedDocs: toBeEmbedDocIds.length,
|
||||
});
|
||||
this.logger.debug(
|
||||
`Trigger embedding for ${toBeEmbedDocIds.length} docs in workspace ${workspaceId}`
|
||||
);
|
||||
for (const docId of toBeEmbedDocIds) {
|
||||
await this.queue.add(
|
||||
'copilot.embedding.docs',
|
||||
@@ -9,12 +9,12 @@ import { PermissionModule } from '../../core/permission';
|
||||
import { QuotaModule } from '../../core/quota';
|
||||
import { WorkspaceModule } from '../../core/workspaces';
|
||||
import {
|
||||
CopilotContextDocJob,
|
||||
CopilotContextResolver,
|
||||
CopilotContextRootResolver,
|
||||
CopilotContextService,
|
||||
} from './context';
|
||||
import { CopilotController } from './controller';
|
||||
import { CopilotEmbeddingJob } from './embedding';
|
||||
import { ChatMessageCache } from './message';
|
||||
import { PromptService } from './prompt';
|
||||
import { CopilotProviderFactory, CopilotProviders } from './providers';
|
||||
@@ -61,7 +61,7 @@ import {
|
||||
// context
|
||||
CopilotContextResolver,
|
||||
CopilotContextService,
|
||||
CopilotContextDocJob,
|
||||
CopilotEmbeddingJob,
|
||||
// transcription
|
||||
CopilotTranscriptionService,
|
||||
CopilotTranscriptionResolver,
|
||||
|
||||
@@ -356,6 +356,7 @@ Consider various factors such as content alignment with the query, source credib
|
||||
- Evaluate the alignment with potential user intent based on the query.
|
||||
3. **Scoring**:
|
||||
- Assign a score from 1 to 10 based on the overall relevance and quality, with 10 being the most relevant.
|
||||
- Each chunk returns a score and should not be mixed together.
|
||||
|
||||
# Output Format
|
||||
|
||||
@@ -478,7 +479,34 @@ You are an assistant helping summarize a document. Use this format, replacing te
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are an editor. Please analyze all content provided by the user and provide a brief summary and more detailed insights in its original language, with the insights listed in the form of an outline.\nYou can refer to this template:\n### Summary\nyour summary content here\n### Insights\n- Insight 1\n- Insight 2\n- Insight 3`,
|
||||
content: `**Role: Expert Content Analyst & Strategist**
|
||||
|
||||
You are a highly skilled content analyst and strategist. Your expertise lies in deconstructing written content to reveal its core message, underlying structure, and deeper implications. Your primary function is to analyze any article, report, or text provided by the user and produce a clear, concise, and insightful analysis in the **{{affine::language}}**.
|
||||
|
||||
**Core Task: Analyze and Explain**
|
||||
|
||||
For the user-provided text, you must perform the following analysis:
|
||||
|
||||
1. **Identify Core Message:** Distill the central thesis or main argument of the article. What is the single most important message the author is trying to convey?
|
||||
2. **Deconstruct Arguments:** Identify the key supporting points, evidence, and reasoning the author uses to build their case.
|
||||
3. **Uncover Deeper Insights:** Go beyond the surface-level summary. Your insights should illuminate the "so what?" of the article. This may include:
|
||||
* The underlying assumptions or biases of the author.
|
||||
* The potential implications or consequences of the ideas presented.
|
||||
* The intended audience and how the article is tailored to them.
|
||||
* Contrasting viewpoints or potential weaknesses in the argument.
|
||||
* The broader context or significance of the topic.
|
||||
|
||||
**Mandatory Output Format:**
|
||||
|
||||
You MUST structure your entire response using the following Markdown template. Do not add any introductory or concluding remarks. Your response must begin directly with "### Summary".
|
||||
|
||||
### Summary
|
||||
A concise paragraph that captures the article's main argument and key conclusions. This should be a neutral, objective overview.
|
||||
|
||||
### Insights
|
||||
- **[Insight 1 title]:** A detailed, bulleted list of 3-5 distinct, profound insights based on your analysis. Each bullet point should explain a specific observation (e.g., an underlying assumption, a key strategy, a potential impact).
|
||||
- **[Insight 2 title]:** [Continue the list]
|
||||
- **[Insight 3 title]:** [Continue the list]`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
@@ -1627,7 +1655,7 @@ const CHAT_PROMPT: Omit<Prompt, 'name'> = {
|
||||
'claude-3-7-sonnet-20250219',
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'gemini-2.5-flash-preview-05-20',
|
||||
'gemini-2.5-pro-preview-05-06',
|
||||
'gemini-2.5-pro-preview-06-05',
|
||||
'claude-opus-4@20250514',
|
||||
'claude-sonnet-4@20250514',
|
||||
'claude-3-7-sonnet@20250219',
|
||||
|
||||
@@ -46,7 +46,7 @@ export class GeminiGenerativeProvider extends GeminiProvider<GeminiGenerativeCon
|
||||
},
|
||||
{
|
||||
name: 'Gemini 2.5 Pro',
|
||||
id: 'gemini-2.5-pro-preview-05-06',
|
||||
id: 'gemini-2.5-pro-preview-06-05',
|
||||
capabilities: [
|
||||
{
|
||||
input: [
|
||||
|
||||
@@ -29,7 +29,7 @@ export class GeminiVertexProvider extends GeminiProvider<GeminiVertexConfig> {
|
||||
},
|
||||
{
|
||||
name: 'Gemini 2.5 Pro',
|
||||
id: 'gemini-2.5-pro-preview-05-06',
|
||||
id: 'gemini-2.5-pro-preview-06-05',
|
||||
capabilities: [
|
||||
{
|
||||
input: [
|
||||
|
||||
@@ -60,7 +60,7 @@ export class OIDCProvider extends OAuthProvider {
|
||||
const validate = async () => {
|
||||
this.#endpoints = null;
|
||||
|
||||
if (this.configured) {
|
||||
if (super.configured) {
|
||||
const config = this.config as OAuthOIDCProviderConfig;
|
||||
try {
|
||||
const res = await fetch(
|
||||
@@ -73,7 +73,6 @@ export class OIDCProvider extends OAuthProvider {
|
||||
|
||||
if (res.ok) {
|
||||
this.#endpoints = OIDCConfigurationSchema.parse(await res.json());
|
||||
super.setup();
|
||||
} else {
|
||||
this.logger.error(`Invalid OIDC issuer ${config.issuer}`);
|
||||
}
|
||||
@@ -81,6 +80,8 @@ export class OIDCProvider extends OAuthProvider {
|
||||
this.logger.error('Failed to validate OIDC configuration', e);
|
||||
}
|
||||
}
|
||||
|
||||
super.setup();
|
||||
};
|
||||
|
||||
validate().catch(() => {
|
||||
|
||||
@@ -250,6 +250,9 @@ export class LiveData<T = unknown>
|
||||
private isPoisoned = false;
|
||||
private poisonedError: PoisonedError | null = null;
|
||||
|
||||
private _signal: Signal<T> | undefined;
|
||||
private _signalSubscription: Subscription | undefined;
|
||||
|
||||
constructor(
|
||||
initialValue: T,
|
||||
upstream:
|
||||
@@ -302,12 +305,10 @@ export class LiveData<T = unknown>
|
||||
this.next(v);
|
||||
}
|
||||
|
||||
private _signal: Signal<T> | undefined;
|
||||
|
||||
get signal(): ReadonlySignal<T> {
|
||||
if (!this._signal) {
|
||||
this._signal = signal(this.value);
|
||||
this.subscribe(v => {
|
||||
this._signalSubscription = this.subscribe(v => {
|
||||
// oxlint-disable-next-line no-non-null-assertion
|
||||
this._signal!.value = v;
|
||||
});
|
||||
@@ -464,6 +465,7 @@ export class LiveData<T = unknown>
|
||||
this.ops$.complete();
|
||||
this.raw$.complete();
|
||||
this.upstreamSubscription?.unsubscribe();
|
||||
this._signalSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,8 +38,6 @@ import type {
|
||||
TappableAppInfo,
|
||||
} from './types';
|
||||
|
||||
const MAX_DURATION_FOR_TRANSCRIPTION = 1.5 * 60 * 60 * 1000; // 1.5 hours
|
||||
|
||||
export const MeetingsSettingsState = {
|
||||
$: globalStateStorage.watch<MeetingSettingsSchema>(MeetingSettingsKey).pipe(
|
||||
map(v => MeetingSettingsSchema.parse(v ?? {})),
|
||||
@@ -547,19 +545,6 @@ export function startRecording(
|
||||
createRecording(state);
|
||||
}
|
||||
|
||||
// set a timeout to stop the recording after MAX_DURATION_FOR_TRANSCRIPTION
|
||||
setTimeout(() => {
|
||||
const state = recordingStateMachine.status$.value;
|
||||
if (
|
||||
state?.status === 'recording' &&
|
||||
state.id === recordingStatus$.value?.id
|
||||
) {
|
||||
stopRecording(state.id).catch(err => {
|
||||
logger.error('failed to stop recording', err);
|
||||
});
|
||||
}
|
||||
}, MAX_DURATION_FOR_TRANSCRIPTION);
|
||||
|
||||
recordingStateMachine.status$.next(state);
|
||||
|
||||
return state;
|
||||
|
||||
@@ -530,7 +530,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 0.21.3;
|
||||
MARKETING_VERSION = 0.22.2;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.affine.pro;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -568,7 +568,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 0.21.3;
|
||||
MARKETING_VERSION = 0.22.2;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.affine.pro;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -8,10 +8,6 @@ export default {
|
||||
stories: ['../src/ui/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
|
||||
|
||||
addons: [
|
||||
getAbsolutePath('@storybook/addon-links'),
|
||||
getAbsolutePath('@storybook/addon-essentials'),
|
||||
getAbsolutePath('@storybook/addon-interactions'),
|
||||
getAbsolutePath('@storybook/addon-mdx-gfm'),
|
||||
'@chromatic-com/storybook',
|
||||
],
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user