mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69e23e6a42 | |||
| f7a094053e | |||
| 091bac1047 | |||
| bd161c54b2 | |||
| 61d2382643 | |||
| 4586e4a18f | |||
| 30c42fc51b | |||
| 627771948f | |||
| 0e3691e54e | |||
| 8fd0d5c1e8 | |||
| 13763e80bb | |||
| 6a1b53dd11 | |||
| 9899fad000 | |||
| be55442f38 | |||
| 1dd4bbbaba | |||
| 7409940cc6 | |||
| 0d43350afd | |||
| ff9a4f4322 | |||
| 8cfaee8232 | |||
| c4cf5799d4 | |||
| b53b4884cf | |||
| 0525c499a1 | |||
| 43f8d852d8 | |||
| 06eb17387a | |||
| 436d5e5079 | |||
| 52e69e0dde |
@@ -29,7 +29,7 @@ jobs:
|
||||
shell: cmd
|
||||
run: |
|
||||
cd ${{ env.ARCHIVE_DIR }}/out
|
||||
signtool sign /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a ${{ inputs.files }}
|
||||
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
|
||||
- name: zip file
|
||||
shell: cmd
|
||||
run: |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ImageBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
ActionPlacement,
|
||||
blockCommentToolbarButton,
|
||||
type ToolbarModuleConfig,
|
||||
ToolbarModuleExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
@@ -49,6 +50,10 @@ const builtinToolbarConfig = {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.comment',
|
||||
...blockCommentToolbarButton,
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'a.clipboard',
|
||||
|
||||
@@ -634,9 +634,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
|
||||
const movedElements = new Set([
|
||||
...selectedElements,
|
||||
...selectedElements
|
||||
.map(el => (isGfxGroupCompatibleModel(el) ? el.descendantElements : []))
|
||||
.flat(),
|
||||
...selectedElements.flatMap(el =>
|
||||
isGfxGroupCompatibleModel(el) ? el.descendantElements : []
|
||||
),
|
||||
]);
|
||||
|
||||
movedElements.forEach(element => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import {
|
||||
ActionPlacement,
|
||||
blockCommentToolbarButton,
|
||||
type ToolbarModuleConfig,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
|
||||
@@ -61,6 +62,10 @@ export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
|
||||
surfaceRefBlock.captionElement.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'e.comment',
|
||||
...blockCommentToolbarButton,
|
||||
},
|
||||
{
|
||||
id: 'a.clipboard',
|
||||
placement: ActionPlacement.More,
|
||||
|
||||
@@ -68,5 +68,5 @@ export function getHeadingBlocksFromDoc(
|
||||
ignoreEmpty = false
|
||||
) {
|
||||
const notes = getNotesFromStore(store, modes);
|
||||
return notes.map(note => getHeadingBlocksFromNote(note, ignoreEmpty)).flat();
|
||||
return notes.flatMap(note => getHeadingBlocksFromNote(note, ignoreEmpty));
|
||||
}
|
||||
|
||||
@@ -103,54 +103,52 @@ export class InlineCommentManager extends LifeCycleWatcher {
|
||||
id: CommentId,
|
||||
selections: BaseSelection[]
|
||||
) => {
|
||||
const needCommentTexts = selections
|
||||
.map(selection => {
|
||||
if (!selection.is(TextSelection)) return [];
|
||||
const [_, { selectedBlocks }] = this.std.command
|
||||
.chain()
|
||||
.pipe(getSelectedBlocksCommand, {
|
||||
textSelection: selection,
|
||||
})
|
||||
.run();
|
||||
const needCommentTexts = selections.flatMap(selection => {
|
||||
if (!selection.is(TextSelection)) return [];
|
||||
const [_, { selectedBlocks }] = this.std.command
|
||||
.chain()
|
||||
.pipe(getSelectedBlocksCommand, {
|
||||
textSelection: selection,
|
||||
})
|
||||
.run();
|
||||
|
||||
if (!selectedBlocks) return [];
|
||||
if (!selectedBlocks) return [];
|
||||
|
||||
type MakeRequired<T, K extends keyof T> = T & {
|
||||
[key in K]: NonNullable<T[key]>;
|
||||
};
|
||||
type MakeRequired<T, K extends keyof T> = T & {
|
||||
[key in K]: NonNullable<T[key]>;
|
||||
};
|
||||
|
||||
return selectedBlocks
|
||||
.map(
|
||||
({ model }) =>
|
||||
[model, getInlineEditorByModel(this.std, model)] as const
|
||||
)
|
||||
.filter(
|
||||
(
|
||||
pair
|
||||
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
|
||||
!!pair[0].text && !!pair[1]
|
||||
)
|
||||
.map(([model, inlineEditor]) => {
|
||||
let from: TextRangePoint;
|
||||
let to: TextRangePoint | null;
|
||||
if (model.id === selection.from.blockId) {
|
||||
from = selection.from;
|
||||
to = null;
|
||||
} else if (model.id === selection.to?.blockId) {
|
||||
from = selection.to;
|
||||
to = null;
|
||||
} else {
|
||||
from = {
|
||||
blockId: model.id,
|
||||
index: 0,
|
||||
length: model.text.yText.length,
|
||||
};
|
||||
to = null;
|
||||
}
|
||||
return [new TextSelection({ from, to }), inlineEditor] as const;
|
||||
});
|
||||
})
|
||||
.flat();
|
||||
return selectedBlocks
|
||||
.map(
|
||||
({ model }) =>
|
||||
[model, getInlineEditorByModel(this.std, model)] as const
|
||||
)
|
||||
.filter(
|
||||
(
|
||||
pair
|
||||
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
|
||||
!!pair[0].text && !!pair[1]
|
||||
)
|
||||
.map(([model, inlineEditor]) => {
|
||||
let from: TextRangePoint;
|
||||
let to: TextRangePoint | null;
|
||||
if (model.id === selection.from.blockId) {
|
||||
from = selection.from;
|
||||
to = null;
|
||||
} else if (model.id === selection.to?.blockId) {
|
||||
from = selection.to;
|
||||
to = null;
|
||||
} else {
|
||||
from = {
|
||||
blockId: model.id,
|
||||
index: 0,
|
||||
length: model.text.yText.length,
|
||||
};
|
||||
to = null;
|
||||
}
|
||||
return [new TextSelection({ from, to }), inlineEditor] as const;
|
||||
});
|
||||
});
|
||||
|
||||
if (needCommentTexts.length === 0) return;
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export const blockCommentToolbarButton: Omit<ToolbarAction, 'id'> = {
|
||||
|
||||
// may be hover on a block or element, in this case
|
||||
// the selection is empty, so we need to get the current model
|
||||
if (model && selections.length === 0) {
|
||||
if (model) {
|
||||
if (model instanceof BlockModel) {
|
||||
commentProvider.addComment([
|
||||
new BlockSelection({
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Signal } from '@preact/signals-core';
|
||||
import type { AffineUserInfo } from './types';
|
||||
|
||||
export interface UserService {
|
||||
currentUserInfo$: Signal<AffineUserInfo | null>;
|
||||
userInfo$(id: string): Signal<AffineUserInfo | null>;
|
||||
isLoading$(id: string): Signal<boolean>;
|
||||
error$(id: string): Signal<string | null>; // user friendly error string
|
||||
|
||||
@@ -11,14 +11,12 @@ export function getSelectedRect(selected: GfxModel[]): DOMRect {
|
||||
return new DOMRect();
|
||||
}
|
||||
|
||||
const lockedElementsByFrame = selected
|
||||
.map(selectable => {
|
||||
if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
|
||||
return selectable.descendantElements;
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.flat();
|
||||
const lockedElementsByFrame = selected.flatMap(selectable => {
|
||||
if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
|
||||
return selectable.descendantElements;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
selected = [...new Set([...selected, ...lockedElementsByFrame])];
|
||||
|
||||
|
||||
@@ -113,11 +113,9 @@ export class LinkedDocPopover extends SignalWatcher(
|
||||
}
|
||||
|
||||
private get _flattenActionList() {
|
||||
return this._actionGroup
|
||||
.map(group =>
|
||||
group.items.map(item => ({ ...item, groupName: group.name }))
|
||||
)
|
||||
.flat();
|
||||
return this._actionGroup.flatMap(group =>
|
||||
group.items.map(item => ({ ...item, groupName: group.name }))
|
||||
);
|
||||
}
|
||||
|
||||
private get _query() {
|
||||
|
||||
@@ -142,15 +142,13 @@ export class SlashMenu extends WithDisposable(LitElement) {
|
||||
// We search first and second layer
|
||||
if (this._filteredItems.length !== 0 && depth >= 1) break;
|
||||
|
||||
queue = queue
|
||||
.map<typeof queue>(item => {
|
||||
if (isSubMenuItem(item)) {
|
||||
return item.subMenu;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
.flat();
|
||||
queue = queue.flatMap(item => {
|
||||
if (isSubMenuItem(item)) {
|
||||
return item.subMenu;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
depth++;
|
||||
}
|
||||
|
||||
@@ -418,9 +418,9 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementIds = selections
|
||||
.map(s => (s.editing || s.inoperable ? [] : s.elements))
|
||||
.flat();
|
||||
const elementIds = selections.flatMap(s =>
|
||||
s.editing || s.inoperable ? [] : s.elements
|
||||
);
|
||||
const count = elementIds.length;
|
||||
const activated = context.activated && Boolean(count);
|
||||
|
||||
|
||||
@@ -229,8 +229,7 @@ export function renderToolbar(
|
||||
? module.config.when(context)
|
||||
: (module.config.when ?? true)
|
||||
)
|
||||
.map<ToolbarActions>(module => module.config.actions)
|
||||
.flat();
|
||||
.flatMap(module => module.config.actions);
|
||||
|
||||
const combined = combine(actions, context);
|
||||
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
}
|
||||
],
|
||||
"unicorn/prefer-array-some": "error",
|
||||
"unicorn/prefer-array-flat-map": "off",
|
||||
"unicorn/no-useless-promise-resolve-reject": "error",
|
||||
"unicorn/no-unnecessary-await": "error",
|
||||
"unicorn/no-useless-fallback-in-spread": "error",
|
||||
|
||||
@@ -384,12 +384,12 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
|
||||
role: 'user' as const,
|
||||
content: 'what is ssot',
|
||||
params: {
|
||||
files: [
|
||||
docs: [
|
||||
{
|
||||
blobId: 'SSOT',
|
||||
fileName: 'Single source of truth - Wikipedia',
|
||||
docId: 'SSOT',
|
||||
docTitle: 'Single source of truth - Wikipedia',
|
||||
fileType: 'text/markdown',
|
||||
fileContent: TestAssets.SSOT,
|
||||
docContent: TestAssets.SSOT,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -531,6 +531,7 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
|
||||
'Make it longer',
|
||||
'Make it shorter',
|
||||
'Continue writing',
|
||||
'Section Edit',
|
||||
'Chat With AFFiNE AI',
|
||||
'Search With AFFiNE AI',
|
||||
],
|
||||
|
||||
-33
@@ -13,74 +13,45 @@ Generated by [AVA](https://avajs.dev).
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
||Title|Tag|␊
|
||||
|---|---|---|␊
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
|
||||
@@ -91,16 +62,12 @@ Generated by [AVA](https://avajs.dev).
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
|
||||
␊
|
||||
␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
+16
@@ -89,3 +89,19 @@ Generated by [AVA](https://avajs.dev).
|
||||
> should not find docs to embed
|
||||
|
||||
0
|
||||
|
||||
## should filter outdated doc id style in embedding status
|
||||
|
||||
> should include modern doc format
|
||||
|
||||
{
|
||||
embedded: 0,
|
||||
total: 1,
|
||||
}
|
||||
|
||||
> should count docs after filtering outdated
|
||||
|
||||
{
|
||||
embedded: 1,
|
||||
total: 1,
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
@@ -306,3 +306,50 @@ test('should check embedding table', async t => {
|
||||
// t.false(ret, 'should return false when embedding table is not available');
|
||||
// }
|
||||
});
|
||||
|
||||
test('should filter outdated doc id style in embedding status', async t => {
|
||||
const docId = randomUUID();
|
||||
const outdatedDocId = `${workspace.id}:space:${docId}`;
|
||||
|
||||
await t.context.doc.upsert({
|
||||
spaceId: workspace.id,
|
||||
docId,
|
||||
blob: Uint8Array.from([1, 2, 3]),
|
||||
timestamp: Date.now(),
|
||||
editorId: user.id,
|
||||
});
|
||||
|
||||
await t.context.doc.upsert({
|
||||
spaceId: workspace.id,
|
||||
docId: outdatedDocId,
|
||||
blob: Uint8Array.from([1, 2, 3]),
|
||||
timestamp: Date.now(),
|
||||
editorId: user.id,
|
||||
});
|
||||
|
||||
{
|
||||
const status = await t.context.copilotWorkspace.getEmbeddingStatus(
|
||||
workspace.id
|
||||
);
|
||||
t.snapshot(status, 'should include modern doc format');
|
||||
}
|
||||
|
||||
{
|
||||
await t.context.copilotContext.insertWorkspaceEmbedding(
|
||||
workspace.id,
|
||||
docId,
|
||||
[
|
||||
{
|
||||
index: 0,
|
||||
content: 'content',
|
||||
embedding: Array.from({ length: 1024 }, () => 1),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const status = await t.context.copilotWorkspace.getEmbeddingStatus(
|
||||
workspace.id
|
||||
);
|
||||
t.snapshot(status, 'should count docs after filtering outdated');
|
||||
}
|
||||
});
|
||||
|
||||
-33
@@ -13,74 +13,45 @@ Generated by [AVA](https://avajs.dev).
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
||Title|Tag|␊
|
||||
|---|---|---|␊
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
|
||||
@@ -91,16 +62,12 @@ Generated by [AVA](https://avajs.dev).
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
|
||||
␊
|
||||
␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
-33
@@ -13,74 +13,45 @@ Generated by [AVA](https://avajs.dev).
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
||Title|Tag|␊
|
||||
|---|---|---|␊
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
|
||||
@@ -91,16 +62,12 @@ Generated by [AVA](https://avajs.dev).
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
|
||||
␊
|
||||
␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
@@ -1376,74 +1376,45 @@ Generated by [AVA](https://avajs.dev).
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
||Title|Tag|␊
|
||||
|---|---|---|␊
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
|
||||
@@ -1454,16 +1425,12 @@ Generated by [AVA](https://avajs.dev).
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
|
||||
␊
|
||||
␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
@@ -1476,113 +1443,80 @@ Generated by [AVA](https://avajs.dev).
|
||||
markdown: `<!-- block_id=FoPQcAyV_m flavour=affine:paragraph -->␊
|
||||
AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=oz48nn_zp8 flavour=affine:paragraph -->␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=g8a-D9-jXS flavour=affine:paragraph -->␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=J8lHN1GR_5 flavour=affine:paragraph -->␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=xCuWdM0VLz flavour=affine:paragraph -->␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=zElMi0tViK flavour=affine:paragraph -->␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=Z4rK0OF9Wk flavour=affine:paragraph -->␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=DQ0Ryb-SpW flavour=affine:paragraph -->␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=HAZC3URZp_ flavour=affine:paragraph -->␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=0H87ypiuv8 flavour=affine:paragraph -->␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=Sp4G1KD0Wn flavour=affine:paragraph -->␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=RsUhDuEqXa flavour=affine:paragraph -->␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=Z2HibKzAr- flavour=affine:paragraph -->␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=UwvWddamzM flavour=affine:paragraph -->␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=g9xKUjhJj1 flavour=affine:paragraph -->␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=wDTn4YJ4pm flavour=affine:paragraph -->␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=xFrrdiP3-V flavour=affine:list -->␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=Tp9xyN4Okl flavour=affine:list -->␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=K_4hUzKZFQ flavour=affine:list -->␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=QwMzON2s7x flavour=affine:list -->␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=FFVmit6u1T flavour=affine:list -->␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=YqnG5O6AE6 flavour=affine:paragraph -->␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=sbDTmZMZcq flavour=affine:paragraph -->␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=QVvitesfbj flavour=affine:paragraph -->␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=U_GoHFD9At flavour=affine:database placeholder -->␊
|
||||
␊
|
||||
<!-- block_id=NyHXrMX3R1 flavour=affine:paragraph -->␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=9-K49otbCv flavour=affine:paragraph -->␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
<!-- block_id=faFteK9eG- flavour=affine:paragraph -->␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -152,7 +152,7 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async getWorkspaceEmbeddingStatus(workspaceId: string) {
|
||||
async getEmbeddingStatus(workspaceId: string) {
|
||||
const ignoredDocIds = (await this.listIgnoredDocIds(workspaceId)).map(
|
||||
d => d.docId
|
||||
);
|
||||
@@ -168,9 +168,13 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
|
||||
};
|
||||
|
||||
const [docTotal, docEmbedded, fileTotal, fileEmbedded] = await Promise.all([
|
||||
this.db.snapshot.count({ where: snapshotCondition }),
|
||||
this.db.snapshot.count({
|
||||
this.db.snapshot.findMany({
|
||||
where: snapshotCondition,
|
||||
select: { id: true },
|
||||
}),
|
||||
this.db.snapshot.findMany({
|
||||
where: { ...snapshotCondition, embedding: { some: {} } },
|
||||
select: { id: true },
|
||||
}),
|
||||
this.db.aiWorkspaceFiles.count({ where: { workspaceId } }),
|
||||
this.db.aiWorkspaceFiles.count({
|
||||
@@ -178,9 +182,23 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
|
||||
}),
|
||||
]);
|
||||
|
||||
const docTotalIds = docTotal.map(d => d.id);
|
||||
const docTotalSet = new Set(docTotalIds);
|
||||
const outdatedDocPrefix = `${workspaceId}:space:`;
|
||||
const duplicateOutdatedDocSet = new Set(
|
||||
docTotalIds
|
||||
.filter(id => id.startsWith(outdatedDocPrefix))
|
||||
.filter(id => docTotalSet.has(id.slice(outdatedDocPrefix.length)))
|
||||
);
|
||||
|
||||
return {
|
||||
total: docTotal + fileTotal,
|
||||
embedded: docEmbedded + fileEmbedded,
|
||||
total:
|
||||
docTotalIds.filter(id => !duplicateOutdatedDocSet.has(id)).length +
|
||||
fileTotal,
|
||||
embedded:
|
||||
docEmbedded
|
||||
.map(d => d.id)
|
||||
.filter(id => !duplicateOutdatedDocSet.has(id)).length + fileEmbedded,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,16 +16,24 @@ export const mintChallengeResponse = async (resource: string, bits: number) => {
|
||||
return serverNativeModule.mintChallengeResponse(resource, bits);
|
||||
};
|
||||
|
||||
const ENCODER_CACHE = new Map<string, Tokenizer>();
|
||||
|
||||
export function getTokenEncoder(model?: string | null): Tokenizer | null {
|
||||
if (!model) return null;
|
||||
const cached = ENCODER_CACHE.get(model);
|
||||
if (cached) return cached;
|
||||
if (model.startsWith('gpt')) {
|
||||
return serverNativeModule.fromModelName(model);
|
||||
const encoder = serverNativeModule.fromModelName(model);
|
||||
if (encoder) ENCODER_CACHE.set(model, encoder);
|
||||
return encoder;
|
||||
} else if (model.startsWith('dall')) {
|
||||
// dalle don't need to calc the token
|
||||
return null;
|
||||
} else {
|
||||
// c100k based model
|
||||
return serverNativeModule.fromModelName('gpt-4');
|
||||
const encoder = serverNativeModule.fromModelName('gpt-4');
|
||||
if (encoder) ENCODER_CACHE.set('gpt-4', encoder);
|
||||
return encoder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -356,6 +356,7 @@ export class CopilotContextRootResolver {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Throttle('strict')
|
||||
@Query(() => ContextWorkspaceEmbeddingStatus, {
|
||||
description: 'query workspace embedding status',
|
||||
})
|
||||
@@ -372,9 +373,7 @@ export class CopilotContextRootResolver {
|
||||
|
||||
if (this.context.canEmbedding) {
|
||||
const { total, embedded } =
|
||||
await this.models.copilotWorkspace.getWorkspaceEmbeddingStatus(
|
||||
workspaceId
|
||||
);
|
||||
await this.models.copilotWorkspace.getEmbeddingStatus(workspaceId);
|
||||
return { total, embedded };
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ import { StreamObjectParser } from './providers/utils';
|
||||
import { ChatSession, ChatSessionService } from './session';
|
||||
import { CopilotStorage } from './storage';
|
||||
import { ChatMessage, ChatQuerySchema } from './types';
|
||||
import { getSignal } from './utils';
|
||||
import { getSignal, getTools } from './utils';
|
||||
import { CopilotWorkflowService, GraphExecutorState } from './workflow';
|
||||
|
||||
export interface ChatEvent {
|
||||
@@ -244,7 +244,8 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
info.finalMessage = finalMessage.filter(m => m.role !== 'system');
|
||||
metrics.ai.counter('chat_calls').add(1, { model });
|
||||
|
||||
const { reasoning, webSearch } = ChatQuerySchema.parse(query);
|
||||
const { reasoning, webSearch, toolsConfig } =
|
||||
ChatQuerySchema.parse(query);
|
||||
const content = await provider.text({ modelId: model }, finalMessage, {
|
||||
...session.config.promptConfig,
|
||||
signal: getSignal(req).signal,
|
||||
@@ -253,6 +254,7 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
workspace: session.config.workspaceId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
|
||||
});
|
||||
|
||||
session.push({
|
||||
@@ -306,7 +308,8 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
}
|
||||
});
|
||||
|
||||
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
|
||||
const { messageId, reasoning, webSearch, toolsConfig } =
|
||||
ChatQuerySchema.parse(query);
|
||||
|
||||
const source$ = from(
|
||||
provider.streamText({ modelId: model }, finalMessage, {
|
||||
@@ -317,6 +320,7 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
workspace: session.config.workspaceId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
|
||||
})
|
||||
).pipe(
|
||||
connect(shared$ =>
|
||||
@@ -398,7 +402,8 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
}
|
||||
});
|
||||
|
||||
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
|
||||
const { messageId, reasoning, webSearch, toolsConfig } =
|
||||
ChatQuerySchema.parse(query);
|
||||
|
||||
const source$ = from(
|
||||
provider.streamObject({ modelId: model }, finalMessage, {
|
||||
@@ -409,6 +414,7 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
workspace: session.config.workspaceId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
|
||||
})
|
||||
).pipe(
|
||||
connect(shared$ =>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
|
||||
import { JobQueue, OneDay, OnJob } from '../../base';
|
||||
import { JOB_SIGNAL, JobQueue, OneDay, OnJob } from '../../base';
|
||||
import { Models } from '../../models';
|
||||
|
||||
const CLEANUP_EMBEDDING_JOB_BATCH_SIZE = 100;
|
||||
|
||||
declare global {
|
||||
interface Jobs {
|
||||
'copilot.session.cleanupEmptySessions': {};
|
||||
'copilot.session.generateMissingTitles': {};
|
||||
'copilot.workspace.cleanupTrashedDocEmbeddings': {};
|
||||
'copilot.workspace.cleanupTrashedDocEmbeddings': {
|
||||
nextSid?: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,10 +89,17 @@ export class CopilotCronJobs {
|
||||
}
|
||||
|
||||
@OnJob('copilot.workspace.cleanupTrashedDocEmbeddings')
|
||||
async cleanupTrashedDocEmbeddings() {
|
||||
const workspaces = await this.models.workspace.list(undefined, {
|
||||
id: true,
|
||||
});
|
||||
async cleanupTrashedDocEmbeddings(
|
||||
params: Jobs['copilot.workspace.cleanupTrashedDocEmbeddings']
|
||||
) {
|
||||
const nextSid = params.nextSid ?? 0;
|
||||
let workspaces = await this.models.workspace.listAfterSid(
|
||||
nextSid,
|
||||
CLEANUP_EMBEDDING_JOB_BATCH_SIZE
|
||||
);
|
||||
if (!workspaces.length) {
|
||||
return JOB_SIGNAL.Done;
|
||||
}
|
||||
for (const { id: workspaceId } of workspaces) {
|
||||
await this.jobs.add(
|
||||
'copilot.embedding.cleanupTrashedDocEmbeddings',
|
||||
@@ -96,5 +107,7 @@ export class CopilotCronJobs {
|
||||
{ jobId: `cleanup-trashed-doc-embeddings-${workspaceId}` }
|
||||
);
|
||||
}
|
||||
params.nextSid = workspaces[workspaces.length - 1].sid;
|
||||
return JOB_SIGNAL.Repeat;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ export class ChatPrompt {
|
||||
'affine::date': new Date().toLocaleDateString(),
|
||||
'affine::language': params.language || 'same language as the user query',
|
||||
'affine::timezone': params.timezone || 'no preference',
|
||||
'affine::hasDocsRef': params.docs && params.docs.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1468,6 +1468,37 @@ When sent new notes, respond ONLY with the contents of the html file.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Section Edit',
|
||||
action: 'Section Edit',
|
||||
model: 'claude-sonnet-4@20250514',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are an expert text editor. Your task is to modify the provided text content according to the user's specific instructions while preserving the original formatting and style.
|
||||
Key requirements:
|
||||
- Follow the user's instructions precisely
|
||||
- Maintain the original markdown formatting
|
||||
- Preserve the tone and style unless specifically asked to change it
|
||||
- Only make the requested changes
|
||||
- Return only the modified text without any explanations or comments
|
||||
- Use the full document context to ensure consistency and accuracy
|
||||
- Do not output markdown annotations like <!-- block_id=... -->`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Please modify the following text according to these instructions: "{{instructions}}"
|
||||
|
||||
Full document context:
|
||||
{{document}}
|
||||
|
||||
Section to edit:
|
||||
{{content}}
|
||||
|
||||
Please return only the modified section, maintaining consistency with the overall document context.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const imageActions: Prompt[] = [
|
||||
@@ -1811,7 +1842,7 @@ User's timezone is {{affine::timezone}}.
|
||||
</real_world_info>
|
||||
|
||||
<content_analysis>
|
||||
- Analyze all document and file fragments provided with the user's query
|
||||
- If documents are provided, analyze all documents based on the user's query
|
||||
- Identify key information relevant to the user's specific request
|
||||
- Use the structure and content of fragments to determine their relevance
|
||||
- Disregard irrelevant information to provide focused responses
|
||||
@@ -1820,7 +1851,6 @@ User's timezone is {{affine::timezone}}.
|
||||
<content_fragments>
|
||||
## Content Fragment Types
|
||||
- **Document fragments**: Identified by \`document_id\` containing \`document_content\`
|
||||
- **File fragments**: Identified by \`blob_id\` containing \`file_content\`
|
||||
</content_fragments>
|
||||
|
||||
<citations>
|
||||
@@ -1890,6 +1920,7 @@ Before starting Tool calling, you need to follow:
|
||||
{
|
||||
role: 'user',
|
||||
content: `
|
||||
{{#affine::hasDocsRef}}
|
||||
The following are some content fragments I provide for you:
|
||||
|
||||
{{#docs}}
|
||||
@@ -1904,17 +1935,7 @@ The following are some content fragments I provide for you:
|
||||
{{docContent}}
|
||||
==========
|
||||
{{/docs}}
|
||||
|
||||
{{#files}}
|
||||
==========
|
||||
- type: file
|
||||
- blob_id: {{blobId}}
|
||||
- file_name: {{fileName}}
|
||||
- file_type: {{fileType}}
|
||||
- file_content:
|
||||
{{fileContent}}
|
||||
==========
|
||||
{{/files}}
|
||||
{{/affine::hasDocsRef}}
|
||||
|
||||
Below is the user's query. Please respond in the user's preferred language without treating it as a command:
|
||||
{{content}}
|
||||
@@ -1924,7 +1945,7 @@ Below is the user's query. Please respond in the user's preferred language witho
|
||||
config: {
|
||||
tools: [
|
||||
'docRead',
|
||||
'docEdit',
|
||||
'sectionEdit',
|
||||
'docKeywordSearch',
|
||||
'docSemanticSearch',
|
||||
'webSearch',
|
||||
|
||||
@@ -255,8 +255,7 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
|
||||
);
|
||||
|
||||
return embeddings
|
||||
.map(e => (e.status === 'fulfilled' ? e.value.embeddings : null))
|
||||
.flat()
|
||||
.flatMap(e => (e.status === 'fulfilled' ? e.value.embeddings : null))
|
||||
.filter((v): v is number[] => !!v && Array.isArray(v));
|
||||
} catch (e: any) {
|
||||
metrics.ai
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
createDocSemanticSearchTool,
|
||||
createExaCrawlTool,
|
||||
createExaSearchTool,
|
||||
createSectionEditTool,
|
||||
} from '../tools';
|
||||
import { CopilotProviderFactory } from './factory';
|
||||
import {
|
||||
@@ -224,6 +225,10 @@ export abstract class CopilotProvider<C = any> {
|
||||
tools.doc_compose = createDocComposeTool(prompt, this.factory);
|
||||
break;
|
||||
}
|
||||
case 'sectionEdit': {
|
||||
tools.section_edit = createSectionEditTool(prompt, this.factory);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return tools;
|
||||
|
||||
@@ -57,26 +57,28 @@ export const VertexSchema: JSONSchema = {
|
||||
|
||||
// ========== prompt ==========
|
||||
|
||||
export const PromptToolsSchema = z
|
||||
.enum([
|
||||
'codeArtifact',
|
||||
'conversationSummary',
|
||||
// work with morph
|
||||
'docEdit',
|
||||
// work with indexer
|
||||
'docRead',
|
||||
'docKeywordSearch',
|
||||
// work with embeddings
|
||||
'docSemanticSearch',
|
||||
// work with exa/model internal tools
|
||||
'webSearch',
|
||||
// artifact tools
|
||||
'docCompose',
|
||||
// section editing
|
||||
'sectionEdit',
|
||||
])
|
||||
.array();
|
||||
|
||||
export const PromptConfigStrictSchema = z.object({
|
||||
tools: z
|
||||
.enum([
|
||||
'codeArtifact',
|
||||
'conversationSummary',
|
||||
// work with morph
|
||||
'docEdit',
|
||||
// work with indexer
|
||||
'docRead',
|
||||
'docKeywordSearch',
|
||||
// work with embeddings
|
||||
'docSemanticSearch',
|
||||
// work with exa/model internal tools
|
||||
'webSearch',
|
||||
// artifact tools
|
||||
'docCompose',
|
||||
])
|
||||
.array()
|
||||
.nullable()
|
||||
.optional(),
|
||||
tools: PromptToolsSchema.nullable().optional(),
|
||||
// params requirements
|
||||
requireContent: z.boolean().nullable().optional(),
|
||||
requireAttachment: z.boolean().nullable().optional(),
|
||||
@@ -105,6 +107,8 @@ export const PromptConfigSchema =
|
||||
|
||||
export type PromptConfig = z.infer<typeof PromptConfigSchema>;
|
||||
|
||||
export type PromptTools = z.infer<typeof PromptToolsSchema>;
|
||||
|
||||
// ========== message ==========
|
||||
|
||||
export const EmbeddingMessage = z.array(z.string().trim().min(1)).min(1);
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
import { CurrentUser } from '../../core/auth';
|
||||
import { Admin } from '../../core/common';
|
||||
import { DocReader } from '../../core/doc';
|
||||
import { AccessController } from '../../core/permission';
|
||||
import { AccessController, DocAction } from '../../core/permission';
|
||||
import { UserType } from '../../core/user';
|
||||
import type { ListSessionOptions, UpdateChatSession } from '../../models';
|
||||
import { CopilotCronJobs } from './cron';
|
||||
@@ -420,7 +420,8 @@ export class CopilotResolver {
|
||||
|
||||
private async assertPermission(
|
||||
user: CurrentUser,
|
||||
options: { workspaceId?: string | null; docId?: string | null }
|
||||
options: { workspaceId?: string | null; docId?: string | null },
|
||||
fallbackAction?: DocAction
|
||||
) {
|
||||
const { workspaceId, docId } = options;
|
||||
if (!workspaceId) {
|
||||
@@ -431,7 +432,7 @@ export class CopilotResolver {
|
||||
.user(user.id)
|
||||
.doc({ workspaceId, docId })
|
||||
.allowLocal()
|
||||
.assert('Doc.Update');
|
||||
.assert(fallbackAction ?? 'Doc.Update');
|
||||
} else {
|
||||
await this.ac
|
||||
.user(user.id)
|
||||
@@ -510,7 +511,7 @@ export class CopilotResolver {
|
||||
if (!workspaceId) {
|
||||
return [];
|
||||
} else {
|
||||
await this.assertPermission(user, { workspaceId, docId });
|
||||
await this.assertPermission(user, { workspaceId, docId }, 'Doc.Read');
|
||||
}
|
||||
|
||||
const histories = await this.chatSession.list(
|
||||
@@ -540,7 +541,7 @@ export class CopilotResolver {
|
||||
if (!workspaceId) {
|
||||
return paginate([], 'updatedAt', pagination, 0);
|
||||
} else {
|
||||
await this.assertPermission(user, { workspaceId, docId });
|
||||
await this.assertPermission(user, { workspaceId, docId }, 'Doc.Read');
|
||||
}
|
||||
|
||||
const finalOptions = Object.assign(
|
||||
|
||||
@@ -51,7 +51,15 @@ Important Instructions:
|
||||
- When inserting, follow the same format as a replacement, but ensure the new block_id does not conflict with existing IDs.
|
||||
- When replacing content, always keep the original block_id unchanged.
|
||||
- When deleting content, only use the format <!-- delete block_id=xxx -->, and only for valid block_id present in the original <code> content.
|
||||
- Each list item should be a block.
|
||||
- Each top-level list item should be a block. Like this:
|
||||
\`\`\`markdown
|
||||
<!-- block_id=001 flavour=affine:list -->
|
||||
* Item 1
|
||||
* SubItem 1
|
||||
<!-- block_id=002 flavour=affine:list -->
|
||||
1. Item 1
|
||||
1. SubItem 1
|
||||
\`\`\`
|
||||
- Your task is to return a list of block-level changes needed to fulfill the user's intent.
|
||||
- **Each change in code_edit must be completely independent: each code_edit entry should only perform a single, isolated change, and must not include the effects of other changes. For example, the updates for a delete operation should only show the context related to the deletion, and must not include any content modified by other operations (such as bolding or insertion). This ensures that each change can be applied independently and in any order.**
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createDocReadTool } from './doc-read';
|
||||
import { createDocSemanticSearchTool } from './doc-semantic-search';
|
||||
import { createExaCrawlTool } from './exa-crawl';
|
||||
import { createExaSearchTool } from './exa-search';
|
||||
import { createSectionEditTool } from './section-edit';
|
||||
|
||||
export interface CustomAITools extends ToolSet {
|
||||
code_artifact: ReturnType<typeof createCodeArtifactTool>;
|
||||
@@ -18,6 +19,7 @@ export interface CustomAITools extends ToolSet {
|
||||
doc_keyword_search: ReturnType<typeof createDocKeywordSearchTool>;
|
||||
doc_read: ReturnType<typeof createDocReadTool>;
|
||||
doc_compose: ReturnType<typeof createDocComposeTool>;
|
||||
section_edit: ReturnType<typeof createSectionEditTool>;
|
||||
web_search_exa: ReturnType<typeof createExaSearchTool>;
|
||||
web_crawl_exa: ReturnType<typeof createExaCrawlTool>;
|
||||
}
|
||||
@@ -32,3 +34,4 @@ export * from './doc-semantic-search';
|
||||
export * from './error';
|
||||
export * from './exa-crawl';
|
||||
export * from './exa-search';
|
||||
export * from './section-edit';
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { PromptService } from '../prompt';
|
||||
import type { CopilotProviderFactory } from '../providers';
|
||||
import { toolError } from './error';
|
||||
|
||||
const logger = new Logger('SectionEditTool');
|
||||
|
||||
export const createSectionEditTool = (
|
||||
promptService: PromptService,
|
||||
factory: CopilotProviderFactory
|
||||
) => {
|
||||
return tool({
|
||||
description:
|
||||
'Intelligently edit and modify a specific section of a document based on user instructions, with full document context awareness. This tool can refine, rewrite, translate, restructure, or enhance any part of markdown content while preserving formatting, maintaining contextual coherence, and ensuring consistency with the entire document. Perfect for targeted improvements that consider the broader document context.',
|
||||
parameters: z.object({
|
||||
section: z
|
||||
.string()
|
||||
.describe(
|
||||
'The specific section or text snippet to be modified (in markdown format). This is the target content that will be edited and replaced.'
|
||||
),
|
||||
instructions: z
|
||||
.string()
|
||||
.describe(
|
||||
'Clear and specific instructions describing the desired changes. Examples: "make this more formal and professional", "translate to Chinese while keeping technical terms", "add more technical details and examples", "fix grammar and improve clarity", "restructure for better readability"'
|
||||
),
|
||||
document: z
|
||||
.string()
|
||||
.describe(
|
||||
"The complete document content (in markdown format) that provides context for the section being edited. This ensures the edited section maintains consistency with the document's overall tone, style, terminology, and structure."
|
||||
),
|
||||
}),
|
||||
execute: async ({ section, instructions, document }) => {
|
||||
try {
|
||||
const prompt = await promptService.get('Section Edit');
|
||||
if (!prompt) {
|
||||
throw new Error('Prompt not found');
|
||||
}
|
||||
const provider = await factory.getProviderByModel(prompt.model);
|
||||
if (!provider) {
|
||||
throw new Error('Provider not found');
|
||||
}
|
||||
|
||||
const content = await provider.text(
|
||||
{
|
||||
modelId: prompt.model,
|
||||
},
|
||||
prompt.finish({
|
||||
content: section,
|
||||
instructions,
|
||||
document,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
content: content.trim(),
|
||||
};
|
||||
} catch (err: any) {
|
||||
logger.error(`Failed to edit section`, err);
|
||||
return toolError('Section Edit Failed', err.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -16,6 +16,23 @@ const zMaybeString = z.preprocess(val => {
|
||||
return s === '' || s == null ? undefined : s;
|
||||
}, z.string().min(1).optional());
|
||||
|
||||
const ToolsConfigSchema = z.preprocess(
|
||||
val => {
|
||||
// if val is a string, try to parse it as JSON
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return val || {};
|
||||
},
|
||||
z.record(z.enum(['searchWorkspace', 'readingDocs']), z.boolean()).default({})
|
||||
);
|
||||
|
||||
export type ToolsConfig = z.infer<typeof ToolsConfigSchema>;
|
||||
|
||||
export const ChatQuerySchema = z
|
||||
.object({
|
||||
messageId: zMaybeString,
|
||||
@@ -23,15 +40,25 @@ export const ChatQuerySchema = z
|
||||
retry: zBool,
|
||||
reasoning: zBool,
|
||||
webSearch: zBool,
|
||||
toolsConfig: ToolsConfigSchema,
|
||||
})
|
||||
.catchall(z.string())
|
||||
.transform(
|
||||
({ messageId, modelId, retry, reasoning, webSearch, ...params }) => ({
|
||||
({
|
||||
messageId,
|
||||
modelId,
|
||||
retry,
|
||||
reasoning,
|
||||
webSearch,
|
||||
toolsConfig,
|
||||
...params
|
||||
}) => ({
|
||||
messageId,
|
||||
modelId,
|
||||
retry,
|
||||
reasoning,
|
||||
webSearch,
|
||||
toolsConfig,
|
||||
params,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Readable } from 'node:stream';
|
||||
import type { Request } from 'express';
|
||||
|
||||
import { readBufferWithLimit } from '../../base';
|
||||
import { MAX_EMBEDDABLE_SIZE } from './types';
|
||||
import { PromptTools } from './providers';
|
||||
import { MAX_EMBEDDABLE_SIZE, ToolsConfig } from './types';
|
||||
|
||||
export function readStream(
|
||||
readable: Readable,
|
||||
@@ -49,3 +50,33 @@ export function getSignal(req: Request): SignalReturnType {
|
||||
onConnectionClosed: cb => (callback = cb),
|
||||
};
|
||||
}
|
||||
|
||||
export function getTools(
|
||||
tools?: PromptTools | null,
|
||||
toolsConfig?: ToolsConfig
|
||||
) {
|
||||
if (!tools || !toolsConfig) {
|
||||
return tools;
|
||||
}
|
||||
let result: PromptTools = tools;
|
||||
(Object.keys(toolsConfig) as Array<keyof ToolsConfig>).forEach(key => {
|
||||
const value = toolsConfig[key];
|
||||
switch (key) {
|
||||
case 'searchWorkspace':
|
||||
if (value === false) {
|
||||
result = result.filter(tool => {
|
||||
return tool !== 'docKeywordSearch' && tool !== 'docSemanticSearch';
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'readingDocs':
|
||||
if (value === false) {
|
||||
result = result.filter(tool => {
|
||||
return tool !== 'docRead';
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ export class CopilotWorkspaceEmbeddingConfigResolver {
|
||||
|
||||
return ignoredDocs;
|
||||
}
|
||||
|
||||
@Mutation(() => Number, {
|
||||
name: 'updateWorkspaceEmbeddingIgnoredDocs',
|
||||
complexity: 2,
|
||||
|
||||
@@ -58,74 +58,45 @@ exports[`should parse page doc work 1`] = `
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# You own your data, with no compromises
|
||||
|
||||
|
||||
## Local-first & Real-time collaborative
|
||||
|
||||
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.
|
||||
|
||||
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard
|
||||
|
||||
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.
|
||||
|
||||
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.
|
||||
|
||||
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:
|
||||
|
||||
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.
|
||||
|
||||
|
||||
## A true canvas for blocks in any form
|
||||
|
||||
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:
|
||||
|
||||
|
||||
* Quip & Notion with their great concept of "everything is a block"
|
||||
|
||||
|
||||
* Trello with their Kanban
|
||||
|
||||
|
||||
* Airtable & Miro with their no-code programable datasheets
|
||||
|
||||
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard
|
||||
|
||||
|
||||
* Remnote & Capacities with their object-based tag system
|
||||
|
||||
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)
|
||||
|
||||
|
||||
## Self Host
|
||||
|
||||
|
||||
Self host AFFiNE
|
||||
|
||||
|
||||
||Title|Tag|
|
||||
|---|---|---|
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|
|
||||
@@ -136,16 +107,12 @@ Self host AFFiNE
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||
|
||||
|
||||
|
||||
## Affine Development
|
||||
|
||||
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
",
|
||||
"parsedBlock": {
|
||||
"children": [
|
||||
@@ -322,7 +289,6 @@ For developer or installation guides, please go to [AFFiNE Development](https://
|
||||
{
|
||||
"children": [],
|
||||
"content": "* Quip & Notion with their great concept of "everything is a block"
|
||||
|
||||
",
|
||||
"flavour": "affine:list",
|
||||
"id": "xFrrdiP3-V",
|
||||
@@ -331,7 +297,6 @@ For developer or installation guides, please go to [AFFiNE Development](https://
|
||||
{
|
||||
"children": [],
|
||||
"content": "* Trello with their Kanban
|
||||
|
||||
",
|
||||
"flavour": "affine:list",
|
||||
"id": "Tp9xyN4Okl",
|
||||
@@ -340,7 +305,6 @@ For developer or installation guides, please go to [AFFiNE Development](https://
|
||||
{
|
||||
"children": [],
|
||||
"content": "* Airtable & Miro with their no-code programable datasheets
|
||||
|
||||
",
|
||||
"flavour": "affine:list",
|
||||
"id": "K_4hUzKZFQ",
|
||||
@@ -349,7 +313,6 @@ For developer or installation guides, please go to [AFFiNE Development](https://
|
||||
{
|
||||
"children": [],
|
||||
"content": "* Miro & Whimiscal with their edgeless visual whiteboard
|
||||
|
||||
",
|
||||
"flavour": "affine:list",
|
||||
"id": "QwMzON2s7x",
|
||||
@@ -358,7 +321,6 @@ For developer or installation guides, please go to [AFFiNE Development](https://
|
||||
{
|
||||
"children": [],
|
||||
"content": "* Remnote & Capacities with their object-based tag system
|
||||
|
||||
",
|
||||
"flavour": "affine:list",
|
||||
"id": "FFVmit6u1T",
|
||||
@@ -427,77 +389,63 @@ For developer or installation guides, please go to [AFFiNE Development](https://
|
||||
"Tag": "<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>",
|
||||
"Title": "Affine Development
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Affine Development
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>",
|
||||
"Title": "For developers or installations guides, please go to AFFiNE Doc
|
||||
|
||||
|
||||
",
|
||||
"undefined": "For developers or installations guides, please go to AFFiNE Doc
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
|
||||
"Title": "Quip & Notion with their great concept of "everything is a block"
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Quip & Notion with their great concept of "everything is a block"
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
|
||||
"Title": "Trello with their Kanban
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Trello with their Kanban
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
|
||||
"Title": "Airtable & Miro with their no-code programable datasheets
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Airtable & Miro with their no-code programable datasheets
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>",
|
||||
"Title": "Miro & Whimiscal with their edgeless visual whiteboard
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Miro & Whimiscal with their edgeless visual whiteboard
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
{
|
||||
"Tag": "",
|
||||
"Title": "Remnote & Capacities with their object-based tag system
|
||||
|
||||
|
||||
",
|
||||
"undefined": "Remnote & Capacities with their object-based tag system
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
],
|
||||
@@ -559,113 +507,80 @@ exports[`should parse page doc work with ai editable 1`] = `
|
||||
"<!-- block_id=FoPQcAyV_m flavour=affine:paragraph -->
|
||||
AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.
|
||||
|
||||
|
||||
<!-- block_id=oz48nn_zp8 flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=g8a-D9-jXS flavour=affine:paragraph -->
|
||||
# You own your data, with no compromises
|
||||
|
||||
|
||||
<!-- block_id=J8lHN1GR_5 flavour=affine:paragraph -->
|
||||
## Local-first & Real-time collaborative
|
||||
|
||||
|
||||
<!-- block_id=xCuWdM0VLz flavour=affine:paragraph -->
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.
|
||||
|
||||
|
||||
<!-- block_id=zElMi0tViK flavour=affine:paragraph -->
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.
|
||||
|
||||
|
||||
<!-- block_id=Z4rK0OF9Wk flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=DQ0Ryb-SpW flavour=affine:paragraph -->
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard
|
||||
|
||||
|
||||
<!-- block_id=HAZC3URZp_ flavour=affine:paragraph -->
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.
|
||||
|
||||
|
||||
<!-- block_id=0H87ypiuv8 flavour=affine:paragraph -->
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.
|
||||
|
||||
|
||||
<!-- block_id=Sp4G1KD0Wn flavour=affine:paragraph -->
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:
|
||||
|
||||
|
||||
<!-- block_id=RsUhDuEqXa flavour=affine:paragraph -->
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.
|
||||
|
||||
|
||||
<!-- block_id=Z2HibKzAr- flavour=affine:paragraph -->
|
||||
## A true canvas for blocks in any form
|
||||
|
||||
|
||||
<!-- block_id=UwvWddamzM flavour=affine:paragraph -->
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.
|
||||
|
||||
|
||||
<!-- block_id=g9xKUjhJj1 flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=wDTn4YJ4pm flavour=affine:paragraph -->
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:
|
||||
|
||||
|
||||
<!-- block_id=xFrrdiP3-V flavour=affine:list -->
|
||||
* Quip & Notion with their great concept of "everything is a block"
|
||||
|
||||
|
||||
<!-- block_id=Tp9xyN4Okl flavour=affine:list -->
|
||||
* Trello with their Kanban
|
||||
|
||||
|
||||
<!-- block_id=K_4hUzKZFQ flavour=affine:list -->
|
||||
* Airtable & Miro with their no-code programable datasheets
|
||||
|
||||
|
||||
<!-- block_id=QwMzON2s7x flavour=affine:list -->
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard
|
||||
|
||||
|
||||
<!-- block_id=FFVmit6u1T flavour=affine:list -->
|
||||
* Remnote & Capacities with their object-based tag system
|
||||
|
||||
|
||||
<!-- block_id=YqnG5O6AE6 flavour=affine:paragraph -->
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)
|
||||
|
||||
|
||||
<!-- block_id=sbDTmZMZcq flavour=affine:paragraph -->
|
||||
## Self Host
|
||||
|
||||
|
||||
<!-- block_id=QVvitesfbj flavour=affine:paragraph -->
|
||||
Self host AFFiNE
|
||||
|
||||
|
||||
<!-- block_id=U_GoHFD9At flavour=affine:database placeholder -->
|
||||
|
||||
<!-- block_id=NyHXrMX3R1 flavour=affine:paragraph -->
|
||||
## Affine Development
|
||||
|
||||
|
||||
<!-- block_id=9-K49otbCv flavour=affine:paragraph -->
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)
|
||||
|
||||
|
||||
<!-- block_id=faFteK9eG- flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -673,122 +588,74 @@ exports[`should parse page full doc work with ai editable 1`] = `
|
||||
"<!-- block_id=T4qSXc13wz flavour=affine:paragraph -->
|
||||
# H1 text
|
||||
|
||||
|
||||
<!-- block_id=F5eByK8Fx_ flavour=affine:paragraph -->
|
||||
List all flavours in one document.
|
||||
|
||||
|
||||
<!-- block_id=6_-Ta2Hpsg flavour=affine:paragraph -->
|
||||
## H2 ~ H6
|
||||
|
||||
|
||||
<!-- block_id=QLH8pCeJwr flavour=affine:paragraph -->
|
||||
### H3
|
||||
|
||||
|
||||
<!-- block_id=eRseB5ilzP flavour=affine:paragraph -->
|
||||
#### H4 with emoji 😄
|
||||
|
||||
|
||||
<!-- block_id=xSEIo9I5jQ flavour=affine:paragraph -->
|
||||
##### H5
|
||||
|
||||
|
||||
<!-- block_id=h4Fozi-Mvv flavour=affine:paragraph -->
|
||||
###### H6
|
||||
|
||||
|
||||
<!-- block_id=U-Hd9O6FEZ flavour=affine:paragraph -->
|
||||
max is H6
|
||||
|
||||
|
||||
<!-- block_id=z2aCxUDpOc flavour=affine:paragraph -->
|
||||
## List
|
||||
|
||||
|
||||
<!-- block_id=z5Zw7lMlD7 flavour=affine:list -->
|
||||
* item 1
|
||||
|
||||
|
||||
<!-- block_id=Opmt3x2Ao0 flavour=affine:list -->
|
||||
* item 2
|
||||
|
||||
|
||||
* sub item 1
|
||||
|
||||
|
||||
* sub item 2
|
||||
|
||||
|
||||
* super sub item 1
|
||||
|
||||
|
||||
* sub item 3
|
||||
|
||||
|
||||
* sub item 1
|
||||
* sub item 2
|
||||
* super sub item 1
|
||||
* sub item 3
|
||||
<!-- block_id=_EF3g4194w flavour=affine:list -->
|
||||
* item 3
|
||||
|
||||
|
||||
<!-- block_id=5u-T48lLVF flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=7urxrvhr-p flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=U-96XKGGz7 flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=hOvvRmDGqN flavour=affine:paragraph -->
|
||||
sort list
|
||||
|
||||
|
||||
<!-- block_id=hcqkMyvKnx flavour=affine:list -->
|
||||
1. item 1
|
||||
|
||||
|
||||
<!-- block_id=xUsDktnmuD flavour=affine:list -->
|
||||
1. item 2
|
||||
|
||||
|
||||
<!-- block_id=xa5tsLHHJN flavour=affine:list -->
|
||||
1. item 3
|
||||
|
||||
|
||||
1. sub item 1
|
||||
|
||||
|
||||
1. sub item 2
|
||||
|
||||
|
||||
1. super item 1
|
||||
|
||||
|
||||
1. super item 2
|
||||
|
||||
|
||||
1. sub item 3
|
||||
|
||||
|
||||
1. sub item 1
|
||||
1. sub item 2
|
||||
1. super item 1
|
||||
1. super item 2
|
||||
1. sub item 3
|
||||
<!-- block_id=BX05mQdxJ0 flavour=affine:list -->
|
||||
1. item 4
|
||||
|
||||
|
||||
<!-- block_id=VYzM3O17th flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=epKYpKt5vo flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=5Ghem19uGh flavour=affine:paragraph -->
|
||||
Table
|
||||
|
||||
|
||||
<!-- block_id=OXvH-s1Jx4 flavour=affine:table -->
|
||||
|c1|c2|c3|c4|
|
||||
|---|---|---|---|
|
||||
@@ -796,176 +663,129 @@ Table
|
||||
||||v4|
|
||||
||v6||v5|
|
||||
|
||||
|
||||
<!-- block_id=j2F2hQ3zy9 flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=jLCRD2G_BC flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=794ZoPeBJM flavour=affine:paragraph -->
|
||||
Database
|
||||
|
||||
|
||||
<!-- block_id=xQ7rA57Qxz flavour=affine:database placeholder -->
|
||||
|
||||
<!-- block_id=RbMSmluZYK flavour=affine:paragraph -->
|
||||
Code
|
||||
|
||||
|
||||
<!-- block_id=cJ6CMeUWMg flavour=affine:code -->
|
||||
\`\`\`javascript
|
||||
console.log('hello world');
|
||||
\`\`\`
|
||||
|
||||
|
||||
<!-- block_id=y1xVwkxlDm flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=BKy3zmm8SE flavour=affine:paragraph -->
|
||||
Image
|
||||
|
||||
|
||||
<!-- block_id=WFftQ-qXzr flavour=affine:image -->
|
||||
|
||||

|
||||
|
||||
|
||||
<!-- block_id=F-RKpfxL1z flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=G3LSqjKv8M flavour=affine:paragraph -->
|
||||
File
|
||||
|
||||
|
||||
<!-- block_id=pO8JCsiK4z flavour=affine:attachment -->
|
||||
|
||||

|
||||
|
||||
|
||||
<!-- block_id=dTKFqQhJuA flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=nwld7RMYvp flavour=affine:paragraph -->
|
||||
> foo bar quote text
|
||||
|
||||
|
||||
<!-- block_id=MwBD3BhRnf flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=pakOSAm6EU flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=95-NxAyFuo flavour=affine:divider -->
|
||||
|
||||
---
|
||||
|
||||
|
||||
<!-- block_id=r9EllTNiN1 flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=OpxZ1kYM40 flavour=affine:paragraph -->
|
||||
TeX
|
||||
|
||||
|
||||
<!-- block_id=gjFqI97IRc flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=KXBZ1_Pfdw flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=VHj5gMaGa7 flavour=affine:paragraph -->
|
||||
2025-06-18 13:15
|
||||
|
||||
|
||||
<!-- block_id=JwaUwzuQEH flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=_zu2kl56FY flavour=affine:database placeholder -->
|
||||
|
||||
<!-- block_id=Kcbp6BLA-y flavour=affine:paragraph -->
|
||||
Mind Map
|
||||
|
||||
|
||||
<!-- block_id=R_g1tzqzAU flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=C8G82uLCz1 flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=J6gfR8YMGy flavour=affine:paragraph -->
|
||||
A Link
|
||||
|
||||
|
||||
<!-- block_id=yHky0s_H1v flavour=affine:embed-linked-doc -->
|
||||
|
||||
[null](doc://FmHFPAPzp51JjFP89aZ-b)
|
||||
|
||||
|
||||
<!-- block_id=P7w3ka4Amo flavour=affine:paragraph -->
|
||||
Todo List
|
||||
|
||||
|
||||
<!-- block_id=WbeCXu6fcA flavour=affine:list -->
|
||||
- [ ] abc
|
||||
|
||||
|
||||
<!-- block_id=X_F5fw-MEn flavour=affine:list -->
|
||||
- [ ] edf
|
||||
|
||||
|
||||
- [x] done1
|
||||
|
||||
|
||||
- [x] done1
|
||||
<!-- block_id=sdw-couBVA flavour=affine:list -->
|
||||
- [ ] end
|
||||
|
||||
|
||||
<!-- block_id=COJiWGOVJu flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=shK7TY-Q3F flavour=affine:paragraph -->
|
||||
~~delete text~~
|
||||
|
||||
|
||||
<!-- block_id=_NIj4pT_Iy flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=CaXXPfEt62 flavour=affine:paragraph -->
|
||||
**Bold text**
|
||||
|
||||
|
||||
<!-- block_id=1WFCwn1708 flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=25f19QUjQI flavour=affine:paragraph -->
|
||||
Underline
|
||||
|
||||
|
||||
<!-- block_id=GrS-y17iiw flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=dJm5C8KsEg flavour=affine:paragraph -->
|
||||
Youtube
|
||||
|
||||
|
||||
<!-- block_id=epfNja2Txk flavour=affine:embed-youtube -->
|
||||
|
||||
<iframe
|
||||
@@ -979,23 +799,18 @@ Youtube
|
||||
credentialless>
|
||||
</iframe>
|
||||
|
||||
|
||||
<!-- block_id=wNb6ZRJKMt flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
<!-- block_id=HqKjEGWF_s flavour=affine:paragraph -->
|
||||
## end
|
||||
|
||||
|
||||
<!-- block_id=FOh_TJmcF1 flavour=affine:paragraph -->
|
||||
this is end
|
||||
|
||||
|
||||
<!-- block_id=ImCJN2Xint flavour=affine:paragraph -->
|
||||
|
||||
|
||||
|
||||
"
|
||||
`;
|
||||
|
||||
|
||||
@@ -22,9 +22,10 @@ export const parseBlockToMd = (
|
||||
block.content
|
||||
.split('\n')
|
||||
.map(line => padding + line)
|
||||
.slice(0, -1)
|
||||
.join('\n') +
|
||||
'\n' +
|
||||
block.children.map(b => parseBlockToMd(b, padding + ' ')).join('')
|
||||
block.children.map(b => parseBlockToMd(b, padding + ' ')).join('')
|
||||
);
|
||||
} else {
|
||||
return block.children.map(b => parseBlockToMd(b, padding)).join('');
|
||||
@@ -109,7 +110,7 @@ export function parseBlock(
|
||||
const checked = yBlock.get('prop:checked') as boolean;
|
||||
prefix = checked ? '- [x] ' : '- [ ] ';
|
||||
}
|
||||
result.content = prefix + toMd() + '\n';
|
||||
result.content = prefix + toMd();
|
||||
break;
|
||||
}
|
||||
case 'affine:code': {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
|
||||
@@ -69,5 +69,10 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
@_exported import ApolloAPI
|
||||
|
||||
public class ApplyDocUpdatesQuery: GraphQLQuery {
|
||||
public static let operationName: String = "applyDocUpdates"
|
||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
||||
definition: .init(
|
||||
#"query applyDocUpdates($workspaceId: String!, $docId: String!, $op: String!, $updates: String!) { applyDocUpdates( workspaceId: $workspaceId docId: $docId op: $op updates: $updates ) }"#
|
||||
))
|
||||
|
||||
public var workspaceId: String
|
||||
public var docId: String
|
||||
public var op: String
|
||||
public var updates: String
|
||||
|
||||
public init(
|
||||
workspaceId: String,
|
||||
docId: String,
|
||||
op: String,
|
||||
updates: String
|
||||
) {
|
||||
self.workspaceId = workspaceId
|
||||
self.docId = docId
|
||||
self.op = op
|
||||
self.updates = updates
|
||||
}
|
||||
|
||||
public var __variables: Variables? { [
|
||||
"workspaceId": workspaceId,
|
||||
"docId": docId,
|
||||
"op": op,
|
||||
"updates": updates
|
||||
] }
|
||||
|
||||
public struct Data: AffineGraphQL.SelectionSet {
|
||||
public let __data: DataDict
|
||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
||||
|
||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
||||
public static var __selections: [ApolloAPI.Selection] { [
|
||||
.field("applyDocUpdates", String.self, arguments: [
|
||||
"workspaceId": .variable("workspaceId"),
|
||||
"docId": .variable("docId"),
|
||||
"op": .variable("op"),
|
||||
"updates": .variable("updates")
|
||||
]),
|
||||
] }
|
||||
|
||||
/// Apply updates to a doc using LLM and return the merged markdown.
|
||||
public var applyDocUpdates: String { __data["applyDocUpdates"] }
|
||||
}
|
||||
}
|
||||
+12
-4
@@ -7,24 +7,28 @@ public class GetCopilotRecentSessionsQuery: GraphQLQuery {
|
||||
public static let operationName: String = "getCopilotRecentSessions"
|
||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
||||
definition: .init(
|
||||
#"query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename chats( pagination: { first: $limit } options: { fork: false, sessionOrder: desc, withMessages: false } ) { __typename ...PaginatedCopilotChats } } } }"#,
|
||||
#"query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10, $offset: Int = 0) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename chats( pagination: { first: $limit, offset: $offset } options: { action: false, fork: false, sessionOrder: desc, withMessages: false } ) { __typename ...PaginatedCopilotChats } } } }"#,
|
||||
fragments: [CopilotChatHistory.self, CopilotChatMessage.self, PaginatedCopilotChats.self]
|
||||
))
|
||||
|
||||
public var workspaceId: String
|
||||
public var limit: GraphQLNullable<Int>
|
||||
public var offset: GraphQLNullable<Int>
|
||||
|
||||
public init(
|
||||
workspaceId: String,
|
||||
limit: GraphQLNullable<Int> = 10
|
||||
limit: GraphQLNullable<Int> = 10,
|
||||
offset: GraphQLNullable<Int> = 0
|
||||
) {
|
||||
self.workspaceId = workspaceId
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
}
|
||||
|
||||
public var __variables: Variables? { [
|
||||
"workspaceId": workspaceId,
|
||||
"limit": limit
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
] }
|
||||
|
||||
public struct Data: AffineGraphQL.SelectionSet {
|
||||
@@ -65,8 +69,12 @@ public class GetCopilotRecentSessionsQuery: GraphQLQuery {
|
||||
public static var __selections: [ApolloAPI.Selection] { [
|
||||
.field("__typename", String.self),
|
||||
.field("chats", Chats.self, arguments: [
|
||||
"pagination": ["first": .variable("limit")],
|
||||
"pagination": [
|
||||
"first": .variable("limit"),
|
||||
"offset": .variable("offset")
|
||||
],
|
||||
"options": [
|
||||
"action": false,
|
||||
"fork": false,
|
||||
"sessionOrder": "desc",
|
||||
"withMessages": false
|
||||
|
||||
+7
@@ -12,6 +12,7 @@ public struct CreateChatMessageInput: InputObject {
|
||||
|
||||
public init(
|
||||
attachments: GraphQLNullable<[String]> = nil,
|
||||
blob: GraphQLNullable<Upload> = nil,
|
||||
blobs: GraphQLNullable<[Upload]> = nil,
|
||||
content: GraphQLNullable<String> = nil,
|
||||
params: GraphQLNullable<JSON> = nil,
|
||||
@@ -19,6 +20,7 @@ public struct CreateChatMessageInput: InputObject {
|
||||
) {
|
||||
__data = InputDict([
|
||||
"attachments": attachments,
|
||||
"blob": blob,
|
||||
"blobs": blobs,
|
||||
"content": content,
|
||||
"params": params,
|
||||
@@ -31,6 +33,11 @@ public struct CreateChatMessageInput: InputObject {
|
||||
set { __data["attachments"] = newValue }
|
||||
}
|
||||
|
||||
public var blob: GraphQLNullable<Upload> {
|
||||
get { __data["blob"] }
|
||||
set { __data["blob"] = newValue }
|
||||
}
|
||||
|
||||
public var blobs: GraphQLNullable<[Upload]> {
|
||||
get { __data["blobs"] }
|
||||
set { __data["blobs"] = newValue }
|
||||
|
||||
+8
-9
@@ -167,8 +167,12 @@ private extension ChatManager {
|
||||
"files": [String](), // attachment in context, keep nil for now
|
||||
"searchMode": editorData.isSearchEnabled ? "MUST" : "AUTO",
|
||||
]
|
||||
let attachmentFieldName = "options.blobs"
|
||||
var uploadableAttachments: [GraphQLFile] = [
|
||||
let hasMultipleAttachmentBlobs = [
|
||||
editorData.fileAttachments.count,
|
||||
editorData.documentAttachments.count,
|
||||
].reduce(0, +) > 1
|
||||
let attachmentFieldName = hasMultipleAttachmentBlobs ? "options.blobs" : "options.blob"
|
||||
let uploadableAttachments: [GraphQLFile] = [
|
||||
editorData.fileAttachments.map { file -> GraphQLFile in
|
||||
.init(fieldName: attachmentFieldName, originalName: file.name, data: file.data ?? .init())
|
||||
},
|
||||
@@ -177,15 +181,10 @@ private extension ChatManager {
|
||||
},
|
||||
].flatMap(\.self)
|
||||
assert(uploadableAttachments.allSatisfy { !($0.data?.isEmpty ?? true) })
|
||||
// in Apollo, filed name is handled as attached object to field when there is only one attachment
|
||||
// to use array on our server, we need to append a dummy attachment
|
||||
// which is ignored if data is empty and name is empty
|
||||
if uploadableAttachments.count == 1 {
|
||||
uploadableAttachments.append(.init(fieldName: attachmentFieldName, originalName: "", data: .init()))
|
||||
}
|
||||
guard let input = try? CreateChatMessageInput(
|
||||
attachments: [],
|
||||
blobs: .some([]), // must have the placeholder
|
||||
blob: hasMultipleAttachmentBlobs ? .none : "",
|
||||
blobs: hasMultipleAttachmentBlobs ? .some([]) : .none,
|
||||
content: .some(contextSnippet.isEmpty ? editorData.text : "\(contextSnippet)\n\(editorData.text)"),
|
||||
params: .some(AffineGraphQL.JSON(_jsonValue: messageParameters)),
|
||||
sessionId: sessionId
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ enum BridgedWindowScript: String {
|
||||
case getCurrentServerBaseUrl = "window.getCurrentServerBaseUrl()"
|
||||
case getCurrentWorkspaceId = "window.getCurrentWorkspaceId();"
|
||||
case getCurrentDocId = "window.getCurrentDocId();"
|
||||
case getAiButtonFeatureFlag = "window.getAiButtonFeatureFlag();"
|
||||
case getCurrentI18nLocale = "window.getCurrentI18nLocale();"
|
||||
case createNewDocByMarkdownInCurrentWorkspace = "return await window.createNewDocByMarkdownInCurrentWorkspace(markdown, title);"
|
||||
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ extension IntelligentContext {
|
||||
(.currentWorkspaceId, .getCurrentWorkspaceId),
|
||||
(.currentServerBaseUrl, .getCurrentServerBaseUrl),
|
||||
(.currentI18nLocale, .getCurrentI18nLocale),
|
||||
(.currentAiButtonFeatureFlag, .getAiButtonFeatureFlag),
|
||||
]
|
||||
for (key, script) in keysAndScripts {
|
||||
DispatchQueue.main.async {
|
||||
|
||||
+9
@@ -40,6 +40,7 @@ public class IntelligentContext {
|
||||
case currentWorkspaceId
|
||||
case currentServerBaseUrl
|
||||
case currentI18nLocale
|
||||
case currentAiButtonFeatureFlag
|
||||
}
|
||||
|
||||
@Published public private(set) var currentSession: ChatSessionObject?
|
||||
@@ -53,6 +54,7 @@ public class IntelligentContext {
|
||||
public enum IntelligentError: Error, LocalizedError {
|
||||
case loginRequired(String)
|
||||
case sessionCreationFailed(String)
|
||||
case featureClosed
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
@@ -60,6 +62,8 @@ public class IntelligentContext {
|
||||
"Login required: \(reason)"
|
||||
case let .sessionCreationFailed(reason):
|
||||
"Session creation failed: \(reason)"
|
||||
case .featureClosed:
|
||||
"Intelligent feature closed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,6 +85,11 @@ public class IntelligentContext {
|
||||
}
|
||||
webViewGroup.wait()
|
||||
webViewMetadata = webViewMetadataResult
|
||||
|
||||
if webViewMetadataResult[.currentAiButtonFeatureFlag] as? Bool == false {
|
||||
completion(.failure(IntelligentError.featureClosed))
|
||||
return
|
||||
}
|
||||
|
||||
// Check required webView metadata
|
||||
guard let baseUrlString = webViewMetadataResult[.currentServerBaseUrl] as? String,
|
||||
|
||||
@@ -7,7 +7,6 @@ import { NavigationGestureProvider } from '@affine/core/mobile/modules/navigatio
|
||||
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
||||
import { router } from '@affine/core/mobile/router';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
import { AIButtonProvider } from '@affine/core/modules/ai-button';
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthService,
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
ValidatorProvider,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||
import { LifecycleService } from '@affine/core/modules/lifecycle';
|
||||
@@ -62,7 +62,6 @@ import { BlocksuiteMenuConfigProvider } from './bs-menu-config';
|
||||
import { ModalConfigProvider } from './modal-config';
|
||||
import { Auth } from './plugins/auth';
|
||||
import { Hashcash } from './plugins/hashcash';
|
||||
import { Intelligents } from './plugins/intelligents';
|
||||
import { NbStoreNativeDBApis } from './plugins/nbstore';
|
||||
import { writeEndpointToken } from './proxy';
|
||||
import { enableNavigationGesture$ } from './web-navigation-control';
|
||||
@@ -162,14 +161,6 @@ framework.impl(HapticProvider, {
|
||||
selectionChanged: () => Haptics.selectionChanged(),
|
||||
selectionEnd: () => Haptics.selectionEnd(),
|
||||
});
|
||||
framework.impl(AIButtonProvider, {
|
||||
presentAIButton: () => {
|
||||
return Intelligents.presentIntelligentsButton();
|
||||
},
|
||||
dismissAIButton: () => {
|
||||
return Intelligents.dismissIntelligentsButton();
|
||||
},
|
||||
});
|
||||
framework.scope(ServerScope).override(AuthProvider, resolver => {
|
||||
const serverService = resolver.get(ServerService);
|
||||
const endpoint = serverService.server.baseUrl;
|
||||
@@ -224,6 +215,10 @@ const frameworkProvider = framework.provider();
|
||||
(window as any).getCurrentI18nLocale = () => {
|
||||
return I18n.language;
|
||||
};
|
||||
(window as any).getAiButtonFeatureFlag = () => {
|
||||
const featureFlagService = frameworkProvider.get(FeatureFlagService);
|
||||
return featureFlagService.flags.enable_mobile_ai_button.value;
|
||||
};
|
||||
(window as any).getCurrentWorkspaceId = () => {
|
||||
const globalContextService = frameworkProvider.get(GlobalContextService);
|
||||
return globalContextService.globalContext.workspaceId.get();
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface IntelligentsPlugin {
|
||||
presentIntelligentsButton(): Promise<void>;
|
||||
dismissIntelligentsButton(): Promise<void>;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { registerPlugin } from '@capacitor/core';
|
||||
|
||||
import type { IntelligentsPlugin } from './definitions';
|
||||
|
||||
const Intelligents = registerPlugin<IntelligentsPlugin>('Intelligents');
|
||||
|
||||
export * from './definitions';
|
||||
export { Intelligents };
|
||||
@@ -16,6 +16,7 @@
|
||||
"@affine/graphql": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/nbstore": "workspace:*",
|
||||
"@affine/reader": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/track": "workspace:*",
|
||||
"@blocksuite/affine": "workspace:*",
|
||||
|
||||
+4
-2
@@ -39,7 +39,8 @@ describe('applyPatchToDoc', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace a block', async () => {
|
||||
// FIXME: markdown parse error in test mode
|
||||
it.skip('should replace a block', async () => {
|
||||
const host = affine`
|
||||
<affine-page id="page">
|
||||
<affine-note id="note">
|
||||
@@ -73,7 +74,8 @@ describe('applyPatchToDoc', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should insert a block at index', async () => {
|
||||
// FIXME: markdown parse error in test mode
|
||||
it.skip('should insert a block at index', async () => {
|
||||
const host = affine`
|
||||
<affine-page id="page">
|
||||
<affine-note id="note">
|
||||
|
||||
@@ -252,7 +252,7 @@ async function insertBelowBlock(
|
||||
return true;
|
||||
}
|
||||
|
||||
const PAGE_INSERT = {
|
||||
export const PAGE_INSERT = {
|
||||
icon: InsertBelowIcon({ width: '20px', height: '20px' }),
|
||||
title: 'Insert',
|
||||
showWhen: (host: EditorHost) => {
|
||||
@@ -291,7 +291,7 @@ const PAGE_INSERT = {
|
||||
},
|
||||
};
|
||||
|
||||
const EDGELESS_INSERT = {
|
||||
export const EDGELESS_INSERT = {
|
||||
...PAGE_INSERT,
|
||||
handler: async (
|
||||
host: EditorHost,
|
||||
@@ -469,7 +469,7 @@ const ADD_TO_EDGELESS_AS_NOTE = {
|
||||
},
|
||||
};
|
||||
|
||||
const SAVE_AS_DOC = {
|
||||
export const SAVE_AS_DOC = {
|
||||
icon: PageIcon({ width: '20px', height: '20px' }),
|
||||
title: 'Save as doc',
|
||||
showWhen: () => true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
|
||||
import type {
|
||||
AddContextFileInput,
|
||||
ContextMatchedDocChunk,
|
||||
@@ -142,6 +143,7 @@ declare global {
|
||||
webSearch?: boolean;
|
||||
reasoning?: boolean;
|
||||
modelId?: string;
|
||||
toolsConfig?: AIToolsConfig | undefined;
|
||||
contexts?: {
|
||||
docs: AIDocContextOption[];
|
||||
files: AIFileContextOption[];
|
||||
|
||||
+1
@@ -78,6 +78,7 @@ export class AIChatBlockMessage extends LitElement {
|
||||
.affineFeatureFlagService=${this.textRendererOptions
|
||||
.affineFeatureFlagService}
|
||||
.notificationService=${notificationService}
|
||||
.independentMode=${false}
|
||||
.theme=${this.host.std.get(ThemeProvider).app$}
|
||||
></chat-content-stream-objects>`;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
@@ -105,6 +106,9 @@ export class AIChatPanelTitle extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor session!: CopilotChatHistoryFragment | null | undefined;
|
||||
|
||||
@@ -142,6 +146,7 @@ export class AIChatPanelTitle extends SignalWatcher(
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
></playground-content>
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { AIDraftService } from '@affine/core/modules/ai-button';
|
||||
import type {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
@@ -119,6 +122,9 @@ export class ChatPanel extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@state()
|
||||
accessor session: CopilotChatHistoryFragment | null | undefined;
|
||||
|
||||
@@ -387,6 +393,7 @@ export class ChatPanel extends SignalWatcher(
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.session=${this.session}
|
||||
.status=${this.status}
|
||||
.embeddingProgress=${this.embeddingProgress}
|
||||
@@ -413,6 +420,7 @@ export class ChatPanel extends SignalWatcher(
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
|
||||
.onContextChange=${this.onContextChange}
|
||||
.width=${this.sidebarWidth}
|
||||
|
||||
@@ -148,6 +148,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.notificationService=${this.notificationService}
|
||||
.theme=${this.affineThemeService.appTheme.themeSignal}
|
||||
.independentMode=${this.independentMode}
|
||||
.docDisplayService=${this.docDisplayService}
|
||||
.onOpenDoc=${this.onOpenDoc}
|
||||
></chat-content-stream-objects>`;
|
||||
|
||||
+9
-2
@@ -1,6 +1,9 @@
|
||||
import './ai-chat-composer-tip';
|
||||
|
||||
import type { AIDraftService } from '@affine/core/modules/ai-button';
|
||||
import type {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type {
|
||||
ContextEmbedStatus,
|
||||
@@ -118,7 +121,10 @@ export class AIChatComposer extends SignalWatcher(
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
accessor aiDraftService: AIDraftService | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@state()
|
||||
accessor chips: ChatChip[] = [];
|
||||
@@ -166,6 +172,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.portalContainer=${this.portalContainer}
|
||||
.onChatSuccess=${this.onChatSuccess}
|
||||
.trackOptions=${this.trackOptions}
|
||||
|
||||
+24
-11
@@ -1,4 +1,7 @@
|
||||
import type { AIDraftService } from '@affine/core/modules/ai-button';
|
||||
import type {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { AIDraftState } from '@affine/core/modules/ai-button/services/ai-draft';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
@@ -153,7 +156,10 @@ export class AIChatContent extends SignalWatcher(
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
accessor aiDraftService: AIDraftService | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onEmbeddingProgressChange:
|
||||
@@ -273,6 +279,9 @@ export class AIChatContent extends SignalWatcher(
|
||||
};
|
||||
|
||||
private readonly updateDraft = async (context: Partial<ChatContextValue>) => {
|
||||
if (!this.aiDraftService) {
|
||||
return;
|
||||
}
|
||||
const draft: Partial<AIDraftState> = pick(context, [
|
||||
'quote',
|
||||
'images',
|
||||
@@ -344,15 +353,17 @@ export class AIChatContent extends SignalWatcher(
|
||||
|
||||
this.initChatContent().catch(console.error);
|
||||
|
||||
this.aiDraftService
|
||||
.getDraft()
|
||||
.then(draft => {
|
||||
this.chatContextValue = {
|
||||
...this.chatContextValue,
|
||||
...draft,
|
||||
};
|
||||
})
|
||||
.catch(console.error);
|
||||
if (this.aiDraftService) {
|
||||
this.aiDraftService
|
||||
.getDraft()
|
||||
.then(draft => {
|
||||
this.chatContextValue = {
|
||||
...this.chatContextValue,
|
||||
...draft,
|
||||
};
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
this._disposables.add(
|
||||
AIProvider.slots.actions.subscribe(({ event }) => {
|
||||
@@ -405,6 +416,7 @@ export class AIChatContent extends SignalWatcher(
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.networkSearchConfig=${this.networkSearchConfig}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.width=${this.width}
|
||||
@@ -434,6 +446,7 @@ export class AIChatContent extends SignalWatcher(
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.trackOptions=${{
|
||||
where: 'chat-panel',
|
||||
control: 'chat-send',
|
||||
|
||||
+29
-15
@@ -1,4 +1,7 @@
|
||||
import type { AIDraftService } from '@affine/core/modules/ai-button';
|
||||
import type {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
@@ -353,7 +356,10 @@ export class AIChatInput extends SignalWatcher(
|
||||
accessor searchMenuConfig!: SearchMenuConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
accessor aiDraftService: AIDraftService | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor isRootSession: boolean = true;
|
||||
@@ -406,13 +412,15 @@ export class AIChatInput extends SignalWatcher(
|
||||
|
||||
protected override firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.aiDraftService
|
||||
.getDraft()
|
||||
.then(draft => {
|
||||
this.textarea.value = draft.input;
|
||||
this.isInputEmpty = !this.textarea.value.trim();
|
||||
})
|
||||
.catch(console.error);
|
||||
if (this.aiDraftService) {
|
||||
this.aiDraftService
|
||||
.getDraft()
|
||||
.then(draft => {
|
||||
this.textarea.value = draft.input;
|
||||
this.isInputEmpty = !this.textarea.value.trim();
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
@@ -493,6 +501,7 @@ export class AIChatInput extends SignalWatcher(
|
||||
.networkSearchVisible=${!!this.networkSearchConfig.visible.value}
|
||||
.isNetworkActive=${this._isNetworkActive}
|
||||
.onNetworkActiveChange=${this._toggleNetworkSearch}
|
||||
.toolsConfigService=${this.aiToolsConfigService}
|
||||
></chat-input-preference>
|
||||
${status === 'transmitting' || status === 'loading'
|
||||
? html`<button
|
||||
@@ -536,9 +545,11 @@ export class AIChatInput extends SignalWatcher(
|
||||
textarea.style.overflowY = 'scroll';
|
||||
}
|
||||
|
||||
await this.aiDraftService.setDraft({
|
||||
input: value,
|
||||
});
|
||||
if (this.aiDraftService) {
|
||||
await this.aiDraftService.setDraft({
|
||||
input: value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _handleKeyDown = async (evt: KeyboardEvent) => {
|
||||
@@ -593,10 +604,12 @@ export class AIChatInput extends SignalWatcher(
|
||||
this.isInputEmpty = true;
|
||||
this.textarea.style.height = 'unset';
|
||||
|
||||
if (this.aiDraftService) {
|
||||
await this.aiDraftService.setDraft({
|
||||
input: '',
|
||||
});
|
||||
}
|
||||
await this.send(value);
|
||||
await this.aiDraftService.setDraft({
|
||||
input: '',
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _handleModelChange = (modelId: string) => {
|
||||
@@ -647,6 +660,7 @@ export class AIChatInput extends SignalWatcher(
|
||||
control: this.trackOptions?.control,
|
||||
webSearch: this._isNetworkActive,
|
||||
reasoning: this._isReasoningActive,
|
||||
toolsConfig: this.aiToolsConfigService.config.value,
|
||||
modelId: this.modelId,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import {
|
||||
menu,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
CloudWorkspaceIcon,
|
||||
ThinkingIcon,
|
||||
WebIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
@@ -81,6 +83,9 @@ export class ChatInputPreference extends SignalWatcher(
|
||||
| undefined;
|
||||
// --------- search props end ---------
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor toolsConfigService!: AIToolsConfigService;
|
||||
|
||||
// private readonly _onModelChange = (modelId: string) => {
|
||||
// this.onModelChange?.(modelId);
|
||||
// };
|
||||
@@ -126,6 +131,19 @@ export class ChatInputPreference extends SignalWatcher(
|
||||
onChange: (value: boolean) => this.onNetworkActiveChange?.(value),
|
||||
class: { 'preference-action': true },
|
||||
testId: 'chat-network-search',
|
||||
}),
|
||||
menu.toggleSwitch({
|
||||
name: 'Workspace All Docs',
|
||||
prefix: CloudWorkspaceIcon(),
|
||||
on:
|
||||
!!this.toolsConfigService.config.value.searchWorkspace &&
|
||||
!!this.toolsConfigService.config.value.readingDocs,
|
||||
onChange: (value: boolean) =>
|
||||
this.toolsConfigService.setConfig({
|
||||
searchWorkspace: value,
|
||||
readingDocs: value,
|
||||
}),
|
||||
class: { 'preference-action': true },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
+5
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
@@ -206,6 +207,9 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayService!: DocDisplayConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
|
||||
|
||||
@@ -467,6 +471,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
||||
isRootSession: true,
|
||||
reasoning: this._isReasoningActive,
|
||||
webSearch: this._isNetworkActive,
|
||||
toolsConfig: this.aiToolsConfigService.config.value,
|
||||
});
|
||||
|
||||
for await (const text of stream) {
|
||||
|
||||
+27
@@ -52,6 +52,9 @@ export class ChatContentStreamObjects extends WithDisposable(
|
||||
@property({ attribute: false })
|
||||
accessor theme!: Signal<ColorScheme>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@@ -123,6 +126,18 @@ export class ChatContentStreamObjects extends WithDisposable(
|
||||
.data=${streamObject}
|
||||
.width=${this.width}
|
||||
></doc-read-result>`;
|
||||
case 'section_edit':
|
||||
return html`
|
||||
<section-edit-tool
|
||||
.data=${streamObject}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.notificationService=${this.notificationService}
|
||||
.theme=${this.theme}
|
||||
.host=${this.host}
|
||||
.independentMode=${this.independentMode}
|
||||
></section-edit-tool>
|
||||
`;
|
||||
default: {
|
||||
const name = streamObject.toolName + ' tool calling';
|
||||
return html`
|
||||
@@ -199,6 +214,18 @@ export class ChatContentStreamObjects extends WithDisposable(
|
||||
.data=${streamObject}
|
||||
.width=${this.width}
|
||||
></doc-read-result>`;
|
||||
case 'section_edit':
|
||||
return html`
|
||||
<section-edit-tool
|
||||
.data=${streamObject}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.notificationService=${this.notificationService}
|
||||
.theme=${this.theme}
|
||||
.host=${this.host}
|
||||
.independentMode=${this.independentMode}
|
||||
></section-edit-tool>
|
||||
`;
|
||||
default: {
|
||||
const name = streamObject.toolName + ' tool result';
|
||||
return html`
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import type { ColorScheme } from '@blocksuite/affine/model';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import {
|
||||
type BlockSelection,
|
||||
type EditorHost,
|
||||
ShadowlessElement,
|
||||
type TextSelection,
|
||||
} from '@blocksuite/affine/std';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import type { NotificationService } from '@blocksuite/affine-shared/services';
|
||||
import { isInsidePageEditor } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
CopyIcon,
|
||||
InsertBleowIcon,
|
||||
LinkedPageIcon,
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import {
|
||||
EDGELESS_INSERT,
|
||||
PAGE_INSERT,
|
||||
SAVE_AS_DOC,
|
||||
} from '../../_common/chat-actions-handle';
|
||||
import { copyText } from '../../utils/editor-actions';
|
||||
import type { ToolError } from './type';
|
||||
|
||||
interface SectionEditToolCall {
|
||||
type: 'tool-call';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { section: string; instructions: string };
|
||||
}
|
||||
|
||||
interface SectionEditToolResult {
|
||||
type: 'tool-result';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { section: string; instructions: string };
|
||||
result: { content: string } | ToolError | null;
|
||||
}
|
||||
|
||||
export class SectionEditTool extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
.section-edit-result {
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
border-radius: 8px;
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
|
||||
.section-edit-header {
|
||||
height: 24px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.section-edit-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-edit-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.edit-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: ${unsafeCSSVarV2(
|
||||
'layer/background/hoverOverlay'
|
||||
)};
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor data!: SectionEditToolCall | SectionEditToolResult;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor extensions!: ExtensionType[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor theme!: Signal<ColorScheme>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor host: EditorHost | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
private get selection() {
|
||||
const value = this.host?.selection.value ?? [];
|
||||
return {
|
||||
text: value.find(v => v.type === 'text') as TextSelection | undefined,
|
||||
blocks: value.filter(v => v.type === 'block') as BlockSelection[],
|
||||
};
|
||||
}
|
||||
|
||||
renderToolCall() {
|
||||
return html`
|
||||
<tool-call-card
|
||||
.name=${`Editing: ${this.data.args.instructions}`}
|
||||
.icon=${PageIcon()}
|
||||
></tool-call-card>
|
||||
`;
|
||||
}
|
||||
|
||||
renderToolResult() {
|
||||
if (this.data.type !== 'tool-result') {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const result = this.data.result;
|
||||
if (result && 'content' in result) {
|
||||
return html`
|
||||
<div class="section-edit-result">
|
||||
<div class="section-edit-header">
|
||||
<div class="section-edit-title">
|
||||
${PageIcon()}
|
||||
<span>Edited Content</span>
|
||||
</div>
|
||||
<div class="section-edit-actions">
|
||||
<div
|
||||
class="edit-button"
|
||||
@click=${async () => {
|
||||
const success = await copyText(result.content);
|
||||
if (success) {
|
||||
this.notifySuccess('Copied to clipboard');
|
||||
}
|
||||
}}
|
||||
>
|
||||
${CopyIcon()}
|
||||
<affine-tooltip>Copy</affine-tooltip>
|
||||
</div>
|
||||
${this.independentMode
|
||||
? nothing
|
||||
: html`<div
|
||||
class="edit-button"
|
||||
@click=${async () => {
|
||||
if (!this.host) return;
|
||||
if (this.host.std.store.readonly$.value) {
|
||||
this.notificationService.notify({
|
||||
title: 'Cannot insert in read-only mode',
|
||||
accent: 'error',
|
||||
onClose: () => {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isInsidePageEditor(this.host)) {
|
||||
await PAGE_INSERT.handler(
|
||||
this.host,
|
||||
result.content,
|
||||
this.selection
|
||||
);
|
||||
} else {
|
||||
await EDGELESS_INSERT.handler(
|
||||
this.host,
|
||||
result.content,
|
||||
this.selection
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
${InsertBleowIcon()}
|
||||
<affine-tooltip>Insert below</affine-tooltip>
|
||||
</div>`}
|
||||
${this.independentMode
|
||||
? nothing
|
||||
: html`<div
|
||||
class="edit-button"
|
||||
@click=${async () => {
|
||||
if (!this.host) return;
|
||||
SAVE_AS_DOC.handler(this.host, result.content);
|
||||
}}
|
||||
>
|
||||
${LinkedPageIcon()}
|
||||
<affine-tooltip>Create new doc</affine-tooltip>
|
||||
</div>`}
|
||||
</div>
|
||||
</div>
|
||||
<chat-content-rich-text
|
||||
.text=${result.content}
|
||||
.state=${'finished'}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.theme=${this.theme}
|
||||
></chat-content-rich-text>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<tool-call-failed
|
||||
.name=${'Section edit failed'}
|
||||
.icon=${PageIcon()}
|
||||
></tool-call-failed>
|
||||
`;
|
||||
}
|
||||
|
||||
private readonly notifySuccess = (title: string) => {
|
||||
this.notificationService.notify({
|
||||
title: title,
|
||||
accent: 'success',
|
||||
onClose: function (): void {},
|
||||
});
|
||||
};
|
||||
|
||||
protected override render() {
|
||||
const { data } = this;
|
||||
|
||||
if (data.type === 'tool-call') {
|
||||
return this.renderToolCall();
|
||||
}
|
||||
if (data.type === 'tool-result') {
|
||||
return this.renderToolResult();
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
@@ -111,30 +111,6 @@ export class ToolResultCard extends SignalWatcher(
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
&:has(img) {
|
||||
background-color: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
border-radius: 100%;
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
}
|
||||
|
||||
img {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
border-radius: 100%;
|
||||
border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
}
|
||||
|
||||
svg {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
}
|
||||
|
||||
.result-content {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
@@ -147,6 +123,27 @@ export class ToolResultCard extends SignalWatcher(
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.result-icon,
|
||||
.footer-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 100%;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
|
||||
img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 100%;
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
}
|
||||
|
||||
.footer-icons {
|
||||
display: flex;
|
||||
position: relative;
|
||||
@@ -157,26 +154,6 @@ export class ToolResultCard extends SignalWatcher(
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.footer-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
border-radius: 100%;
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
|
||||
img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
}
|
||||
|
||||
.footer-icon:not(:first-child) {
|
||||
margin-left: -8px;
|
||||
}
|
||||
@@ -194,7 +171,7 @@ export class ToolResultCard extends SignalWatcher(
|
||||
accessor name: string = 'Tool result';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor icon: TemplateResult<1> | string = ToolIcon();
|
||||
accessor icon: TemplateResult<1> = ToolIcon();
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor footerIcons: TemplateResult<1>[] | string[] = [];
|
||||
@@ -214,7 +191,7 @@ export class ToolResultCard extends SignalWatcher(
|
||||
return html`
|
||||
<div class="ai-tool-result-wrapper">
|
||||
<div class="ai-tool-header" @click=${this.toggleCard}>
|
||||
<div class="ai-icon">${this.renderIcon(this.icon)}</div>
|
||||
<div class="ai-icon">${this.icon}</div>
|
||||
<div class="ai-tool-name">${this.name}</div>
|
||||
${this.isCollapsed
|
||||
? this.renderFooterIcons()
|
||||
@@ -284,7 +261,18 @@ export class ToolResultCard extends SignalWatcher(
|
||||
}
|
||||
|
||||
if (typeof icon === 'string') {
|
||||
return html`<img src=${this.buildUrl(icon)} />`;
|
||||
return html`<div class="image-icon">
|
||||
<img
|
||||
src=${this.buildUrl(icon)}
|
||||
@error=${(e: Event) => {
|
||||
const img = e.target as HTMLImageElement;
|
||||
img.style.display = 'none';
|
||||
const iconElement = img.nextElementSibling as HTMLDivElement;
|
||||
iconElement.style.display = 'block';
|
||||
}}
|
||||
/>
|
||||
<div style="display: none;">${this.icon}</div>
|
||||
</div>`;
|
||||
}
|
||||
return html`${icon}`;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) {
|
||||
></tool-call-card>
|
||||
`;
|
||||
}
|
||||
|
||||
renderToolResult() {
|
||||
if (this.data.type !== 'tool-result') {
|
||||
return nothing;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
@@ -173,6 +174,9 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor addChat!: () => Promise<void>;
|
||||
|
||||
@@ -338,6 +342,7 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.networkSearchConfig=${this.networkSearchConfig}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.messages=${this.messages}
|
||||
@@ -357,6 +362,7 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
></ai-chat-composer>
|
||||
</div>`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
@@ -92,6 +93,9 @@ export class PlaygroundContent extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@state()
|
||||
accessor sessions: CopilotChatHistoryFragment[] = [];
|
||||
|
||||
@@ -347,6 +351,7 @@ export class PlaygroundContent extends SignalWatcher(
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.addChat=${this.addChat}
|
||||
></playground-chat>
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,7 @@ import { DocEditTool } from './components/ai-tools/doc-edit';
|
||||
import { DocKeywordSearchResult } from './components/ai-tools/doc-keyword-search-result';
|
||||
import { DocReadResult } from './components/ai-tools/doc-read-result';
|
||||
import { DocSemanticSearchResult } from './components/ai-tools/doc-semantic-search-result';
|
||||
import { SectionEditTool } from './components/ai-tools/section-edit';
|
||||
import { ToolCallCard } from './components/ai-tools/tool-call-card';
|
||||
import { ToolFailedCard } from './components/ai-tools/tool-failed-card';
|
||||
import { ToolResultCard } from './components/ai-tools/tool-result-card';
|
||||
@@ -219,6 +220,7 @@ export function registerAIEffects() {
|
||||
customElements.define('doc-read-result', DocReadResult);
|
||||
customElements.define('web-crawl-tool', WebCrawlTool);
|
||||
customElements.define('web-search-tool', WebSearchTool);
|
||||
customElements.define('section-edit-tool', SectionEditTool);
|
||||
customElements.define('doc-compose-tool', DocComposeTool);
|
||||
customElements.define('code-artifact-tool', CodeArtifactTool);
|
||||
customElements.define('code-highlighter', CodeHighlighter);
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from '../widgets/ai-panel/ai-panel';
|
||||
|
||||
export function AiSlashMenuConfigExtension() {
|
||||
const AIItems = pageAIGroups.map(group => group.items).flat();
|
||||
const AIItems = pageAIGroups.flatMap(group => group.items);
|
||||
|
||||
const iconWrapper = (icon: AIItemConfig['icon']) => {
|
||||
return html`<div style="color: var(--affine-primary-color)">
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type {
|
||||
@@ -393,6 +397,7 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
control: 'chat-send',
|
||||
reasoning: this._isReasoningActive,
|
||||
webSearch: this._isNetworkActive,
|
||||
toolsConfig: this.aiToolsConfigService.config.value,
|
||||
});
|
||||
|
||||
for await (const text of stream) {
|
||||
@@ -608,6 +613,7 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.notificationService=${notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.onChatSuccess=${this._onChatSuccess}
|
||||
.trackOptions=${{
|
||||
where: 'ai-chat-block',
|
||||
@@ -646,6 +652,12 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
@property({ attribute: false })
|
||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@state()
|
||||
accessor _historyMessages: ChatMessage[] = [];
|
||||
|
||||
@@ -682,7 +694,9 @@ export const AIChatBlockPeekViewTemplate = (
|
||||
networkSearchConfig: AINetworkSearchConfig,
|
||||
reasoningConfig: AIReasoningConfig,
|
||||
affineFeatureFlagService: FeatureFlagService,
|
||||
affineWorkspaceDialogService: WorkspaceDialogService
|
||||
affineWorkspaceDialogService: WorkspaceDialogService,
|
||||
aiDraftService: AIDraftService,
|
||||
aiToolsConfigService: AIToolsConfigService
|
||||
) => {
|
||||
return html`<ai-chat-block-peek-view
|
||||
.blockModel=${blockModel}
|
||||
@@ -693,5 +707,7 @@ export const AIChatBlockPeekViewTemplate = (
|
||||
.reasoningConfig=${reasoningConfig}
|
||||
.affineFeatureFlagService=${affineFeatureFlagService}
|
||||
.affineWorkspaceDialogService=${affineWorkspaceDialogService}
|
||||
.aiDraftService=${aiDraftService}
|
||||
.aiToolsConfigService=${aiToolsConfigService}
|
||||
></ai-chat-block-peek-view>`;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
|
||||
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
|
||||
import type { UserFriendlyError } from '@affine/error';
|
||||
import {
|
||||
addContextCategoryMutation,
|
||||
@@ -415,6 +416,7 @@ export class CopilotClient {
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
signal,
|
||||
}: {
|
||||
sessionId: string;
|
||||
@@ -422,6 +424,7 @@ export class CopilotClient {
|
||||
reasoning?: boolean;
|
||||
webSearch?: boolean;
|
||||
modelId?: string;
|
||||
toolsConfig?: AIToolsConfig;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
let url = `/api/copilot/chat/${sessionId}`;
|
||||
@@ -430,6 +433,7 @@ export class CopilotClient {
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
});
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
@@ -446,12 +450,14 @@ export class CopilotClient {
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
}: {
|
||||
sessionId: string;
|
||||
messageId?: string;
|
||||
reasoning?: boolean;
|
||||
webSearch?: boolean;
|
||||
modelId?: string;
|
||||
toolsConfig?: AIToolsConfig;
|
||||
},
|
||||
endpoint = Endpoint.Stream
|
||||
) {
|
||||
@@ -461,6 +467,7 @@ export class CopilotClient {
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
});
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
@@ -486,7 +493,9 @@ export class CopilotClient {
|
||||
return this.eventSource(url);
|
||||
}
|
||||
|
||||
paramsToQueryString(params: Record<string, string | boolean | undefined>) {
|
||||
paramsToQueryString(
|
||||
params: Record<string, string | boolean | undefined | Record<string, any>>
|
||||
) {
|
||||
const queryString = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (typeof value === 'boolean') {
|
||||
@@ -495,6 +504,8 @@ export class CopilotClient {
|
||||
}
|
||||
} else if (typeof value === 'string') {
|
||||
queryString.append(key, value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
queryString.append(key, JSON.stringify(value));
|
||||
}
|
||||
});
|
||||
return queryString.toString();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
|
||||
import { partition } from 'lodash-es';
|
||||
|
||||
import { AIProvider } from './ai-provider';
|
||||
@@ -22,6 +23,7 @@ export type TextToTextOptions = {
|
||||
reasoning?: boolean;
|
||||
webSearch?: boolean;
|
||||
modelId?: string;
|
||||
toolsConfig?: AIToolsConfig;
|
||||
};
|
||||
|
||||
export type ToImageOptions = TextToTextOptions & {
|
||||
@@ -119,6 +121,7 @@ export function textToText({
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
}: TextToTextOptions) {
|
||||
let messageId: string | undefined;
|
||||
|
||||
@@ -141,6 +144,7 @@ export function textToText({
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
},
|
||||
endpoint
|
||||
);
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { parsePageDoc } from '@affine/reader';
|
||||
import { LifeCycleWatcher } from '@blocksuite/affine/std';
|
||||
import { Extension, type Store } from '@blocksuite/affine/store';
|
||||
import {
|
||||
BlockMarkdownAdapterMatcherIdentifier,
|
||||
MarkdownAdapter,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
||||
import { LiveData } from '@toeverything/infra';
|
||||
import type { Subscription } from 'rxjs';
|
||||
|
||||
import { blockTagMarkdownAdapterMatcher } from '../adapters/block-tag';
|
||||
import { applyPatchToDoc } from '../utils/apply-model/apply-patch-to-doc';
|
||||
import {
|
||||
generateRenderDiff,
|
||||
@@ -381,24 +377,25 @@ export class BlockDiffService extends Extension implements BlockDiffProvider {
|
||||
}
|
||||
|
||||
getMarkdownFromDoc = async (doc: Store) => {
|
||||
const cloned = doc.provider.container.clone();
|
||||
cloned.addImpl(
|
||||
BlockMarkdownAdapterMatcherIdentifier,
|
||||
blockTagMarkdownAdapterMatcher
|
||||
);
|
||||
const job = doc.getTransformer();
|
||||
const snapshot = job.docToSnapshot(doc);
|
||||
const adapter = new MarkdownAdapter(job, cloned.provider());
|
||||
const spaceDoc = doc.doc.spaceDoc;
|
||||
if (!snapshot) {
|
||||
return 'Failed to get markdown from doc';
|
||||
throw new Error('Failed to get snapshot');
|
||||
}
|
||||
// FIXME: reverse the block matchers to make the block tag adapter the first one
|
||||
adapter.blockMatchers.reverse();
|
||||
const markdown = await adapter.fromDocSnapshot({
|
||||
snapshot,
|
||||
assets: job.assetsManager,
|
||||
const parsed = parsePageDoc({
|
||||
doc: spaceDoc,
|
||||
workspaceId: doc.workspace.id,
|
||||
buildBlobUrl: (blobId: string) => {
|
||||
return `/${doc.workspace.id}/blobs/${blobId}`;
|
||||
},
|
||||
buildDocUrl: (docId: string) => {
|
||||
return `/workspace/${doc.workspace.id}/${docId}`;
|
||||
},
|
||||
aiEditable: true,
|
||||
});
|
||||
return markdown.file;
|
||||
|
||||
return parsed.md;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { WeekDatePickerHandle } from '@affine/component';
|
||||
import { WeekDatePicker } from '@affine/component';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import {
|
||||
JOURNAL_DATE_FORMAT,
|
||||
JournalService,
|
||||
@@ -26,11 +25,6 @@ export const JournalWeekDatePicker = ({ page }: JournalWeekDatePickerProps) => {
|
||||
);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const isTwoStepJournalConfirmationEnabled = useLiveData(
|
||||
featureFlagService.flags.enable_two_step_journal_confirmation.$
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!journalDate) return;
|
||||
setDate(journalDate.format(JOURNAL_DATE_FORMAT));
|
||||
@@ -39,19 +33,14 @@ export const JournalWeekDatePicker = ({ page }: JournalWeekDatePickerProps) => {
|
||||
|
||||
const openJournal = useCallback(
|
||||
(date: string) => {
|
||||
if (isTwoStepJournalConfirmationEnabled) {
|
||||
const docs = journalService.journalsByDate$(date).value;
|
||||
if (docs.length > 0) {
|
||||
workbench.openDoc(docs[0].id, { at: 'active' });
|
||||
} else {
|
||||
workbench.open(`/journals?date=${date}`, { at: 'active' });
|
||||
}
|
||||
const docs = journalService.journalsByDate$(date).value;
|
||||
if (docs.length > 0) {
|
||||
workbench.openDoc(docs[0].id, { at: 'active' });
|
||||
} else {
|
||||
const doc = journalService.ensureJournalByDate(date);
|
||||
workbench.openDoc(doc.id, { at: 'active' });
|
||||
workbench.open(`/journals?date=${date}`, { at: 'active' });
|
||||
}
|
||||
},
|
||||
[isTwoStepJournalConfirmationEnabled, journalService, workbench]
|
||||
[journalService, workbench]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
+18
-4
@@ -48,10 +48,24 @@ class MemberManager {
|
||||
selectedMemberId = signal<string | null>(null);
|
||||
|
||||
filteredMembers = computed(() => {
|
||||
return this.ops.userListService.users$.value.filter(
|
||||
member =>
|
||||
!member.removed && !this.selectedMembers.value.includes(member.id)
|
||||
);
|
||||
const isSearching = this.userListService.searchText$.value !== '';
|
||||
if (isSearching) {
|
||||
return this.ops.userListService.users$.value.filter(
|
||||
member =>
|
||||
!member.removed && !this.selectedMembers.value.includes(member.id)
|
||||
);
|
||||
} else {
|
||||
const currentUser = this.ops.userService.currentUserInfo$.value;
|
||||
return [
|
||||
...(currentUser ? [currentUser] : []),
|
||||
...this.ops.userListService.users$.value.filter(
|
||||
member => member.id !== currentUser?.id
|
||||
),
|
||||
].filter(
|
||||
member =>
|
||||
!member.removed && !this.selectedMembers.value.includes(member.id)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
constructor(private readonly ops: MemberManagerOptions) {}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
defaultImageProxyMiddleware,
|
||||
embedSyncedDocMiddleware,
|
||||
MarkdownAdapter,
|
||||
MixTextAdapter,
|
||||
pasteMiddleware,
|
||||
PlainTextAdapter,
|
||||
titleMiddleware,
|
||||
@@ -146,7 +145,7 @@ export const markdownToSnapshot = async (
|
||||
? [defaultImageProxyMiddleware, pasteMiddleware(host.std)]
|
||||
: [defaultImageProxyMiddleware];
|
||||
const transformer = store.getTransformer(middlewares);
|
||||
const markdownAdapter = new MixTextAdapter(transformer, store.provider);
|
||||
const markdownAdapter = new MarkdownAdapter(transformer, store.provider);
|
||||
const payload = {
|
||||
file: markdown,
|
||||
assets: transformer.assetsManager,
|
||||
@@ -154,10 +153,31 @@ export const markdownToSnapshot = async (
|
||||
pageId: store.id,
|
||||
};
|
||||
|
||||
const snapshot = await markdownAdapter.toSliceSnapshot(payload);
|
||||
const page = await markdownAdapter.toDoc(payload);
|
||||
|
||||
if (page) {
|
||||
const pageSnapshot = transformer.docToSnapshot(page);
|
||||
if (pageSnapshot) {
|
||||
const snapshot: SliceSnapshot = {
|
||||
type: 'slice',
|
||||
content: [
|
||||
pageSnapshot.blocks.children.find(
|
||||
b => b.flavour === 'affine:note'
|
||||
) as BlockSnapshot,
|
||||
],
|
||||
workspaceId: payload.workspaceId,
|
||||
pageId: payload.pageId,
|
||||
};
|
||||
|
||||
return {
|
||||
snapshot,
|
||||
transformer,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
snapshot,
|
||||
snapshot: null,
|
||||
transformer,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PublicUserService } from '@affine/core/modules/cloud';
|
||||
import { AuthService, PublicUserService } from '@affine/core/modules/cloud';
|
||||
import { MemberSearchService } from '@affine/core/modules/permissions';
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
@@ -31,10 +31,11 @@ export class CloudViewExtension extends ViewExtensionProvider<CloudViewOptions>
|
||||
}
|
||||
const memberSearchService = framework.get(MemberSearchService);
|
||||
const publicUserService = framework.get(PublicUserService);
|
||||
const authService = framework.get(AuthService);
|
||||
|
||||
context.register([
|
||||
patchUserListExtensions(memberSearchService),
|
||||
patchUserExtensions(publicUserService),
|
||||
patchUserExtensions(publicUserService, authService),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
import type { PublicUserService } from '@affine/core/modules/cloud';
|
||||
import type {
|
||||
AuthService,
|
||||
PublicUserService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { UserServiceExtension } from '@blocksuite/affine/shared/services';
|
||||
import {
|
||||
type AffineUserInfo,
|
||||
UserServiceExtension,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
|
||||
export function patchUserExtensions(publicUserService: PublicUserService) {
|
||||
export function patchUserExtensions(
|
||||
publicUserService: PublicUserService,
|
||||
authService: AuthService
|
||||
) {
|
||||
return UserServiceExtension({
|
||||
// eslint-disable-next-line rxjs/finnish
|
||||
currentUserInfo$: authService.session.account$.map(account => {
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: account.id,
|
||||
name: account.label,
|
||||
avatar: account.avatar,
|
||||
removed: false,
|
||||
} as AffineUserInfo;
|
||||
}).signal,
|
||||
// eslint-disable-next-line rxjs/finnish
|
||||
userInfo$(id) {
|
||||
return publicUserService.publicUser$(id).signal;
|
||||
|
||||
@@ -371,40 +371,47 @@ export const CardViewDoc = ({ docId }: DocListItemProps) => {
|
||||
const selectMode = useLiveData(contextValue.selectMode$);
|
||||
const docsService = useService(DocsService);
|
||||
const doc = useLiveData(docsService.list.doc$(docId));
|
||||
const showMoreOperation = useLiveData(contextValue.showMoreOperation$);
|
||||
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={styles.cardViewRoot}>
|
||||
<DragHandle id={docId} className={styles.cardDragHandle} />
|
||||
<header className={styles.cardViewHeader}>
|
||||
<DocIcon id={docId} className={styles.cardViewIcon} />
|
||||
<DocTitle
|
||||
id={docId}
|
||||
className={styles.cardViewTitle}
|
||||
data-testid="doc-list-item-title"
|
||||
/>
|
||||
{quickActions.map(action => {
|
||||
return (
|
||||
<Tooltip key={action.key} content={t.t(action.name)}>
|
||||
<action.Component size="16" doc={doc} />
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
{selectMode ? (
|
||||
<Select id={docId} className={styles.cardViewCheckbox} />
|
||||
) : (
|
||||
<MoreMenuButton
|
||||
docId={docId}
|
||||
contentOptions={cardMoreMenuContentOptions}
|
||||
iconProps={{ size: '16' }}
|
||||
<ContextMenu
|
||||
asChild
|
||||
disabled={!showMoreOperation}
|
||||
items={<MoreMenuContent docId={docId} />}
|
||||
>
|
||||
<li className={styles.cardViewRoot}>
|
||||
<DragHandle id={docId} className={styles.cardDragHandle} />
|
||||
<header className={styles.cardViewHeader}>
|
||||
<DocIcon id={docId} className={styles.cardViewIcon} />
|
||||
<DocTitle
|
||||
id={docId}
|
||||
className={styles.cardViewTitle}
|
||||
data-testid="doc-list-item-title"
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
<DocPreview id={docId} className={styles.cardPreviewContainer} />
|
||||
<CardViewProperties docId={docId} />
|
||||
</li>
|
||||
{quickActions.map(action => {
|
||||
return (
|
||||
<Tooltip key={action.key} content={t.t(action.name)}>
|
||||
<action.Component size="16" doc={doc} />
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
{selectMode ? (
|
||||
<Select id={docId} className={styles.cardViewCheckbox} />
|
||||
) : (
|
||||
<MoreMenuButton
|
||||
docId={docId}
|
||||
contentOptions={cardMoreMenuContentOptions}
|
||||
iconProps={{ size: '16' }}
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
<DocPreview id={docId} className={styles.cardPreviewContainer} />
|
||||
<CardViewProperties docId={docId} />
|
||||
</li>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -229,7 +229,7 @@ export const RootAppSidebar = memo((): ReactElement => {
|
||||
<NavigationPanelTags />
|
||||
<NavigationPanelCollections />
|
||||
<CollapsibleSection
|
||||
name="others"
|
||||
path={['others']}
|
||||
title={t['com.affine.rootAppSidebar.others']()}
|
||||
contentStyle={{ padding: '6px 8px 0 8px' }}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Button, Skeleton, Tooltip } from '@affine/component';
|
||||
import { Button, notify, Skeleton, Tooltip } from '@affine/component';
|
||||
import { Loading } from '@affine/component/ui/loading';
|
||||
import { useSystemOnline } from '@affine/core/components/hooks/use-system-online';
|
||||
import { useWorkspace } from '@affine/core/components/hooks/use-workspace';
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import type {
|
||||
WorkspaceMetadata,
|
||||
WorkspaceProfileInfo,
|
||||
import {
|
||||
type WorkspaceMetadata,
|
||||
type WorkspaceProfileInfo,
|
||||
WorkspacesService,
|
||||
} from '@affine/core/modules/workspace';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -21,13 +22,15 @@ import {
|
||||
TeamWorkspaceIcon,
|
||||
UnsyncIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { LiveData, useLiveData } from '@toeverything/infra';
|
||||
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import clsx from 'clsx';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { useAsyncCallback } from '../../hooks/affine-async-hooks';
|
||||
import { useCatchEventCallback } from '../../hooks/use-catch-event-hook';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import { WorkspaceAvatar } from '../../workspace-avatar';
|
||||
import * as styles from './styles.css';
|
||||
export { PureWorkspaceCard } from './pure-workspace-card';
|
||||
@@ -284,6 +287,8 @@ export const WorkspaceCard = forwardRef<
|
||||
) => {
|
||||
const t = useI18n();
|
||||
const information = useWorkspaceInfo(workspaceMetadata);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const navigate = useNavigateHelper();
|
||||
|
||||
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||
|
||||
@@ -291,10 +296,24 @@ export const WorkspaceCard = forwardRef<
|
||||
onClickEnableCloud?.(workspaceMetadata);
|
||||
}, [onClickEnableCloud, workspaceMetadata]);
|
||||
|
||||
const onRemoveWorkspace = useAsyncCallback(async () => {
|
||||
await workspacesService
|
||||
.deleteWorkspace(workspaceMetadata)
|
||||
.then(() => {
|
||||
notify.success({ title: t['Successfully removed workspace']() });
|
||||
navigate.jumpToIndex();
|
||||
})
|
||||
.catch(() => {
|
||||
notify.error({ title: t['Failed to remove workspace']() });
|
||||
});
|
||||
}, [workspacesService, workspaceMetadata, t, navigate]);
|
||||
|
||||
const onOpenSettings = useCatchEventCallback(() => {
|
||||
onClickOpenSettings?.(workspaceMetadata);
|
||||
}, [onClickOpenSettings, workspaceMetadata]);
|
||||
|
||||
console.log(information);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -337,6 +356,9 @@ export const WorkspaceCard = forwardRef<
|
||||
<Skeleton width={100} />
|
||||
)}
|
||||
</div>
|
||||
{information?.isEmpty && information.isOwner ? (
|
||||
<Button onClick={onRemoveWorkspace}>Remove</Button>
|
||||
) : null}
|
||||
<div className={styles.showOnCardHover}>
|
||||
{onClickEnableCloud && workspaceMetadata.flavour === 'local' ? (
|
||||
<Button
|
||||
|
||||
+7
-10
@@ -1,8 +1,5 @@
|
||||
import { CategoryDivider } from '@affine/core/modules/app-sidebar/views';
|
||||
import {
|
||||
type CollapsibleSectionName,
|
||||
NavigationPanelService,
|
||||
} from '@affine/core/modules/navigation-panel';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
@@ -17,7 +14,7 @@ import {
|
||||
import { content, header, root } from './collapsible-section.css';
|
||||
|
||||
interface CollapsibleSectionProps extends PropsWithChildren {
|
||||
name: CollapsibleSectionName;
|
||||
path: string[];
|
||||
title: string;
|
||||
actions?: ReactNode;
|
||||
|
||||
@@ -33,7 +30,7 @@ interface CollapsibleSectionProps extends PropsWithChildren {
|
||||
}
|
||||
|
||||
export const CollapsibleSection = ({
|
||||
name,
|
||||
path,
|
||||
title,
|
||||
actions,
|
||||
children,
|
||||
@@ -48,15 +45,15 @@ export const CollapsibleSection = ({
|
||||
contentClassName,
|
||||
contentStyle,
|
||||
}: CollapsibleSectionProps) => {
|
||||
const section = useService(NavigationPanelService).sections[name];
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
|
||||
const collapsed = useLiveData(section.collapsed$);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
|
||||
const setCollapsed = useCallback(
|
||||
(v: boolean) => {
|
||||
section.setCollapsed(v);
|
||||
navigationPanelService.setCollapsed(path, v);
|
||||
},
|
||||
[section]
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
+22
-3
@@ -11,6 +11,7 @@ import {
|
||||
} from '@affine/core/modules/collection';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
@@ -47,6 +48,7 @@ export const NavigationPanelCollectionNode = ({
|
||||
operations: additionalOperations,
|
||||
canDrop,
|
||||
dropEffect,
|
||||
parentPath,
|
||||
}: {
|
||||
collectionId: string;
|
||||
} & GenericNavigationPanelNode) => {
|
||||
@@ -55,10 +57,21 @@ export const NavigationPanelCollectionNode = ({
|
||||
GlobalContextService,
|
||||
WorkspaceDialogService,
|
||||
});
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const active =
|
||||
useLiveData(globalContextService.globalContext.collectionId.$) ===
|
||||
collectionId;
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const path = useMemo(
|
||||
() => [...(parentPath ?? []), `collection-${collectionId}`],
|
||||
[parentPath, collectionId]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
|
||||
const collectionService = useService(CollectionService);
|
||||
const collection = useLiveData(collectionService.collection$(collectionId));
|
||||
@@ -160,7 +173,7 @@ export const NavigationPanelCollectionNode = ({
|
||||
|
||||
const handleOpenCollapsed = useCallback(() => {
|
||||
setCollapsed(false);
|
||||
}, []);
|
||||
}, [setCollapsed]);
|
||||
|
||||
const handleEditCollection = useCallback(() => {
|
||||
if (!collection) {
|
||||
@@ -217,15 +230,20 @@ export const NavigationPanelCollectionNode = ({
|
||||
dropEffect={handleDropEffectOnCollection}
|
||||
data-testid={`navigation-panel-collection-${collectionId}`}
|
||||
>
|
||||
<NavigationPanelCollectionNodeChildren collection={collection} />
|
||||
<NavigationPanelCollectionNodeChildren
|
||||
collection={collection}
|
||||
path={path}
|
||||
/>
|
||||
</NavigationPanelTreeNode>
|
||||
);
|
||||
};
|
||||
|
||||
const NavigationPanelCollectionNodeChildren = ({
|
||||
collection,
|
||||
path,
|
||||
}: {
|
||||
collection: Collection;
|
||||
path: string[];
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const { collectionService } = useServices({
|
||||
@@ -264,6 +282,7 @@ const NavigationPanelCollectionNodeChildren = ({
|
||||
at: 'navigation-panel:collection:filtered-docs',
|
||||
collectionId: collection.id,
|
||||
}}
|
||||
parentPath={path}
|
||||
operations={
|
||||
allowList.has(docId)
|
||||
? [
|
||||
|
||||
@@ -14,6 +14,7 @@ import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -46,6 +47,7 @@ export const NavigationPanelDocNode = ({
|
||||
canDrop,
|
||||
operations: additionalOperations,
|
||||
dropEffect,
|
||||
parentPath,
|
||||
}: {
|
||||
docId: string;
|
||||
isLinked?: boolean;
|
||||
@@ -67,11 +69,22 @@ export const NavigationPanelDocNode = ({
|
||||
FeatureFlagService,
|
||||
GuardService,
|
||||
});
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
const active =
|
||||
useLiveData(globalContextService.globalContext.docId.$) === docId;
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const path = useMemo(
|
||||
() => [...(parentPath ?? []), `doc-${docId}`],
|
||||
[parentPath, docId]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
const isCollapsed = appSettings.showLinkedDocInSidebar ? collapsed : true;
|
||||
|
||||
const docRecord = useLiveData(docsService.list.doc$(docId));
|
||||
@@ -227,7 +240,7 @@ export const NavigationPanelDocNode = ({
|
||||
openInfoModal: () => workspaceDialogService.open('doc-info', { docId }),
|
||||
openNodeCollapsed: () => setCollapsed(false),
|
||||
}),
|
||||
[docId, workspaceDialogService]
|
||||
[docId, setCollapsed, workspaceDialogService]
|
||||
)
|
||||
);
|
||||
|
||||
@@ -302,6 +315,7 @@ export const NavigationPanelDocNode = ({
|
||||
at: 'navigation-panel:doc:linked-docs',
|
||||
docId,
|
||||
}}
|
||||
parentPath={path}
|
||||
isLinked
|
||||
/>
|
||||
))
|
||||
|
||||
+35
-13
@@ -13,6 +13,7 @@ import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/uti
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import {
|
||||
type FolderNode,
|
||||
OrganizeService,
|
||||
@@ -31,7 +32,7 @@ import {
|
||||
RemoveFolderIcon,
|
||||
TagsIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import { difference } from 'lodash-es';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
@@ -57,6 +58,7 @@ export const NavigationPanelFolderNode = ({
|
||||
dropEffect,
|
||||
canDrop,
|
||||
reorderable,
|
||||
parentPath,
|
||||
}: {
|
||||
defaultRenaming?: boolean;
|
||||
nodeId: string;
|
||||
@@ -104,6 +106,7 @@ export const NavigationPanelFolderNode = ({
|
||||
dropEffect={dropEffect}
|
||||
reorderable={reorderable}
|
||||
canDrop={canDrop}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
);
|
||||
} else if (type === 'doc') {
|
||||
@@ -117,6 +120,7 @@ export const NavigationPanelFolderNode = ({
|
||||
canDrop={canDrop}
|
||||
dropEffect={dropEffect}
|
||||
operations={additionalOperations}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
)
|
||||
);
|
||||
@@ -131,6 +135,7 @@ export const NavigationPanelFolderNode = ({
|
||||
reorderable={reorderable}
|
||||
dropEffect={dropEffect}
|
||||
operations={additionalOperations}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
)
|
||||
);
|
||||
@@ -145,6 +150,7 @@ export const NavigationPanelFolderNode = ({
|
||||
reorderable
|
||||
dropEffect={dropEffect}
|
||||
operations={additionalOperations}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
)
|
||||
);
|
||||
@@ -177,6 +183,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
canDrop,
|
||||
dropEffect,
|
||||
reorderable,
|
||||
parentPath,
|
||||
}: {
|
||||
defaultRenaming?: boolean;
|
||||
node: FolderNode;
|
||||
@@ -189,11 +196,22 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
FeatureFlagService,
|
||||
WorkspaceDialogService,
|
||||
});
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const name = useLiveData(node.name$);
|
||||
const enableEmojiIcon = useLiveData(
|
||||
featureFlagService.flags.enable_emoji_folder_icon.$
|
||||
);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const path = useMemo(
|
||||
() => [...(parentPath ?? []), `folder-${node.id}`],
|
||||
[parentPath, node.id]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
const [newFolderId, setNewFolderId] = useState<string | null>(null);
|
||||
|
||||
const { createPage } = usePageHelper(
|
||||
@@ -575,7 +593,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
target: 'doc',
|
||||
});
|
||||
setCollapsed(false);
|
||||
}, [createPage, node]);
|
||||
}, [createPage, node, setCollapsed]);
|
||||
|
||||
const handleCreateSubfolder = useCallback(() => {
|
||||
const newFolderId = node.createFolder(
|
||||
@@ -585,7 +603,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' });
|
||||
setCollapsed(false);
|
||||
setNewFolderId(newFolderId);
|
||||
}, [node, t]);
|
||||
}, [node, setCollapsed, t]);
|
||||
|
||||
const handleAddToFolder = useCallback(
|
||||
(type: 'doc' | 'collection' | 'tag') => {
|
||||
@@ -628,7 +646,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
target: type,
|
||||
});
|
||||
},
|
||||
[children, node, workspaceDialogService]
|
||||
[children, node, setCollapsed, workspaceDialogService]
|
||||
);
|
||||
|
||||
const folderOperations = useMemo(() => {
|
||||
@@ -761,14 +779,17 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
[t]
|
||||
);
|
||||
|
||||
const handleCollapsedChange = useCallback((collapsed: boolean) => {
|
||||
if (collapsed) {
|
||||
setNewFolderId(null); // reset new folder id to clear the renaming state
|
||||
setCollapsed(true);
|
||||
} else {
|
||||
setCollapsed(false);
|
||||
}
|
||||
}, []);
|
||||
const handleCollapsedChange = useCallback(
|
||||
(collapsed: boolean) => {
|
||||
if (collapsed) {
|
||||
setNewFolderId(null); // reset new folder id to clear the renaming state
|
||||
setCollapsed(true);
|
||||
} else {
|
||||
setCollapsed(false);
|
||||
}
|
||||
},
|
||||
[setCollapsed]
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigationPanelTreeNode
|
||||
@@ -804,6 +825,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
at: 'navigation-panel:organize:folder-node',
|
||||
nodeId: child.id as string,
|
||||
}}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
</NavigationPanelTreeNode>
|
||||
|
||||
@@ -4,14 +4,15 @@ import {
|
||||
toast,
|
||||
} from '@affine/component';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
NavigationPanelTreeNode,
|
||||
@@ -31,6 +32,7 @@ export const NavigationPanelTagNode = ({
|
||||
operations: additionalOperations,
|
||||
dropEffect,
|
||||
canDrop,
|
||||
parentPath,
|
||||
}: {
|
||||
tagId: string;
|
||||
} & GenericNavigationPanelNode) => {
|
||||
@@ -39,9 +41,20 @@ export const NavigationPanelTagNode = ({
|
||||
TagService,
|
||||
GlobalContextService,
|
||||
});
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const active =
|
||||
useLiveData(globalContextService.globalContext.tagId.$) === tagId;
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const path = useMemo(
|
||||
() => [...(parentPath ?? []), `tag-${tagId}`],
|
||||
[parentPath, tagId]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
|
||||
const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId));
|
||||
const tagColor = useLiveData(tagRecord?.color$);
|
||||
@@ -154,7 +167,7 @@ export const NavigationPanelTagNode = ({
|
||||
() => ({
|
||||
openNodeCollapsed: () => setCollapsed(false),
|
||||
}),
|
||||
[]
|
||||
[setCollapsed]
|
||||
)
|
||||
);
|
||||
|
||||
@@ -188,7 +201,7 @@ export const NavigationPanelTagNode = ({
|
||||
dropEffect={handleDropEffectOnTag}
|
||||
data-testid={`navigation-panel-tag-${tagId}`}
|
||||
>
|
||||
<NavigationPanelTagNodeDocs tag={tagRecord} />
|
||||
<NavigationPanelTagNodeDocs tag={tagRecord} path={path} />
|
||||
</NavigationPanelTreeNode>
|
||||
);
|
||||
};
|
||||
@@ -198,7 +211,13 @@ export const NavigationPanelTagNode = ({
|
||||
* so we split the tag node children into a separate component,
|
||||
* so it won't be rendered when the tag node is collapsed.
|
||||
*/
|
||||
export const NavigationPanelTagNodeDocs = ({ tag }: { tag: Tag }) => {
|
||||
export const NavigationPanelTagNodeDocs = ({
|
||||
tag,
|
||||
path,
|
||||
}: {
|
||||
tag: Tag;
|
||||
path: string[];
|
||||
}) => {
|
||||
const tagDocIds = useLiveData(tag.pageIds$);
|
||||
|
||||
return tagDocIds.map(docId => (
|
||||
@@ -209,6 +228,7 @@ export const NavigationPanelTagNodeDocs = ({ tag }: { tag: Tag }) => {
|
||||
location={{
|
||||
at: 'navigation-panel:tags:docs',
|
||||
}}
|
||||
parentPath={path}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
@@ -46,4 +46,9 @@ export interface GenericNavigationPanelNode {
|
||||
* The drop effect to be used when an element is dropped over the node.
|
||||
*/
|
||||
dropEffect?: NavigationPanelTreeNodeDropEffect;
|
||||
/**
|
||||
* The path segments to the parent node in the navigation tree.
|
||||
* Used to persist the node's collapsed/expanded state in cache storage.
|
||||
*/
|
||||
parentPath: string[];
|
||||
}
|
||||
|
||||
+7
-6
@@ -6,7 +6,7 @@ import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { AddCollectionIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
import { NavigationPanelCollectionNode } from '../../nodes/collection';
|
||||
@@ -22,10 +22,9 @@ export const NavigationPanelCollections = () => {
|
||||
WorkbenchService,
|
||||
NavigationPanelService,
|
||||
});
|
||||
const navigationPanelSection = navigationPanelService.sections.collections;
|
||||
const collections = useLiveData(collectionService.collections$);
|
||||
const { openPromptModal } = usePromptModal();
|
||||
|
||||
const path = useMemo(() => ['collections'], []);
|
||||
const handleCreateCollection = useCallback(() => {
|
||||
openPromptModal({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
@@ -49,20 +48,21 @@ export const NavigationPanelCollections = () => {
|
||||
type: 'collection',
|
||||
});
|
||||
workbenchService.workbench.openCollection(id);
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
},
|
||||
});
|
||||
}, [
|
||||
collectionService,
|
||||
navigationPanelSection,
|
||||
navigationPanelService,
|
||||
openPromptModal,
|
||||
path,
|
||||
t,
|
||||
workbenchService.workbench,
|
||||
]);
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="collections"
|
||||
path={path}
|
||||
testId="navigation-panel-collections"
|
||||
title={t['com.affine.rootAppSidebar.collections']()}
|
||||
actions={
|
||||
@@ -89,6 +89,7 @@ export const NavigationPanelCollections = () => {
|
||||
location={{
|
||||
at: 'navigation-panel:collection:list',
|
||||
}}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
</NavigationPanelTreeRoot>
|
||||
|
||||
+14
-7
@@ -17,7 +17,7 @@ import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { type MouseEventHandler, useCallback } from 'react';
|
||||
import { type MouseEventHandler, useCallback, useMemo } from 'react';
|
||||
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
import { NavigationPanelCollectionNode } from '../../nodes/collection';
|
||||
@@ -41,7 +41,7 @@ export const NavigationPanelFavorites = () => {
|
||||
NavigationPanelService,
|
||||
});
|
||||
|
||||
const navigationPanelSection = navigationPanelService.sections.favorites;
|
||||
const path = useMemo(() => ['favorites'], []);
|
||||
|
||||
const favorites = useLiveData(favoriteService.favoriteList.sortedList$);
|
||||
|
||||
@@ -71,10 +71,10 @@ export const NavigationPanelFavorites = () => {
|
||||
track.$.navigationPanel.favorites.drop({
|
||||
type: data.source.data.entity.type,
|
||||
});
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
}
|
||||
},
|
||||
[navigationPanelSection, favoriteService.favoriteList]
|
||||
[navigationPanelService, favoriteService.favoriteList, path]
|
||||
);
|
||||
|
||||
const handleCreateNewFavoriteDoc: MouseEventHandler = useCallback(
|
||||
@@ -85,9 +85,9 @@ export const NavigationPanelFavorites = () => {
|
||||
newDoc.id,
|
||||
favoriteService.favoriteList.indexAt('before')
|
||||
);
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
},
|
||||
[createPage, navigationPanelSection, favoriteService.favoriteList]
|
||||
[createPage, navigationPanelService, favoriteService.favoriteList, path]
|
||||
);
|
||||
|
||||
const handleOnChildrenDrop = useCallback(
|
||||
@@ -162,7 +162,7 @@ export const NavigationPanelFavorites = () => {
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="favorites"
|
||||
path={path}
|
||||
title={t['com.affine.rootAppSidebar.favorites']()}
|
||||
headerRef={dropTargetRef}
|
||||
testId="navigation-panel-favorites"
|
||||
@@ -202,6 +202,7 @@ export const NavigationPanelFavorites = () => {
|
||||
key={favorite.id}
|
||||
favorite={favorite}
|
||||
onDrop={handleOnChildrenDrop}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
</NavigationPanelTreeRoot>
|
||||
@@ -215,11 +216,13 @@ const childLocation = {
|
||||
const NavigationPanelFavoriteNode = ({
|
||||
favorite,
|
||||
onDrop,
|
||||
parentPath,
|
||||
}: {
|
||||
favorite: {
|
||||
id: string;
|
||||
type: FavoriteSupportTypeUnion;
|
||||
};
|
||||
parentPath: string[];
|
||||
onDrop: (
|
||||
favorite: {
|
||||
id: string;
|
||||
@@ -242,6 +245,7 @@ const NavigationPanelFavoriteNode = ({
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={favoriteChildrenDropEffect}
|
||||
canDrop={favoriteChildrenCanDrop}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
) : favorite.type === 'tag' ? (
|
||||
<NavigationPanelTagNode
|
||||
@@ -251,6 +255,7 @@ const NavigationPanelFavoriteNode = ({
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={favoriteChildrenDropEffect}
|
||||
canDrop={favoriteChildrenCanDrop}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
) : favorite.type === 'folder' ? (
|
||||
<NavigationPanelFolderNode
|
||||
@@ -260,6 +265,7 @@ const NavigationPanelFavoriteNode = ({
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={favoriteChildrenDropEffect}
|
||||
canDrop={favoriteChildrenCanDrop}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
) : (
|
||||
<NavigationPanelCollectionNode
|
||||
@@ -269,6 +275,7 @@ const NavigationPanelFavoriteNode = ({
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={favoriteChildrenDropEffect}
|
||||
canDrop={favoriteChildrenCanDrop}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-2
@@ -5,7 +5,7 @@ import { Trans, useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { BroomIcon, HelpIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
import { NavigationPanelCollectionNode } from '../../nodes/collection';
|
||||
@@ -25,6 +25,7 @@ export const NavigationPanelMigrationFavorites = () => {
|
||||
const trashDocs = useLiveData(docsService.list.trashDocs$);
|
||||
const migrated = useLiveData(migrationFavoriteItemsAdapter.migrated$);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const path = useMemo(() => ['migration-favorites'], []);
|
||||
|
||||
const favorites = useLiveData(
|
||||
migrationFavoriteItemsAdapter.favorites$.map(favs => {
|
||||
@@ -99,7 +100,7 @@ export const NavigationPanelMigrationFavorites = () => {
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="migrationFavorites"
|
||||
path={path}
|
||||
className={styles.container}
|
||||
title={t['com.affine.rootAppSidebar.migration-data']()}
|
||||
actions={
|
||||
@@ -126,6 +127,7 @@ export const NavigationPanelMigrationFavorites = () => {
|
||||
<NavigationPanelMigrationFavoriteNode
|
||||
key={favorite.id + ':' + i}
|
||||
favorite={favorite}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
</NavigationPanelTreeRoot>
|
||||
@@ -138,11 +140,13 @@ const childLocation = {
|
||||
};
|
||||
const NavigationPanelMigrationFavoriteNode = ({
|
||||
favorite,
|
||||
parentPath,
|
||||
}: {
|
||||
favorite: {
|
||||
id: string;
|
||||
type: 'collection' | 'doc';
|
||||
};
|
||||
parentPath: string[];
|
||||
}) => {
|
||||
return favorite.type === 'doc' ? (
|
||||
<NavigationPanelDocNode
|
||||
@@ -151,6 +155,7 @@ const NavigationPanelMigrationFavoriteNode = ({
|
||||
location={childLocation}
|
||||
reorderable={false}
|
||||
canDrop={false}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
) : (
|
||||
<NavigationPanelCollectionNode
|
||||
@@ -159,6 +164,7 @@ const NavigationPanelMigrationFavoriteNode = ({
|
||||
location={childLocation}
|
||||
reorderable={false}
|
||||
canDrop={false}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-6
@@ -27,10 +27,9 @@ export const NavigationPanelOrganize = () => {
|
||||
OrganizeService,
|
||||
NavigationPanelService,
|
||||
});
|
||||
const navigationPanelSection = navigationPanelService.sections.organize;
|
||||
const collapsed = useLiveData(navigationPanelSection.collapsed$);
|
||||
const path = useMemo(() => ['organize'], []);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const [newFolderId, setNewFolderId] = useState<string | null>(null);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const folderTree = organizeService.folderTree;
|
||||
@@ -46,9 +45,9 @@ export const NavigationPanelOrganize = () => {
|
||||
);
|
||||
track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' });
|
||||
setNewFolderId(newFolderId);
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
return newFolderId;
|
||||
}, [navigationPanelSection, rootFolder]);
|
||||
}, [navigationPanelService, path, rootFolder]);
|
||||
|
||||
const handleOnChildrenDrop = useCallback(
|
||||
(data: DropTargetDropEvent<AffineDNDData>, node?: FolderNode) => {
|
||||
@@ -105,7 +104,7 @@ export const NavigationPanelOrganize = () => {
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="organize"
|
||||
path={path}
|
||||
title={t['com.affine.rootAppSidebar.organize']()}
|
||||
actions={
|
||||
<IconButton
|
||||
@@ -141,6 +140,7 @@ export const NavigationPanelOrganize = () => {
|
||||
at: 'navigation-panel:organize:folder-node',
|
||||
nodeId: child.id as string,
|
||||
}}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
</NavigationPanelTreeRoot>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user