mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-04 19:15:33 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e186ac71c3 |
@@ -75,7 +75,6 @@ jobs:
|
||||
with:
|
||||
secret: ${{ secrets.GITHUB_TOKEN }}
|
||||
approvers: forehalo,fengmk2
|
||||
minimum-approvals: 1
|
||||
fail-on-denial: true
|
||||
issue-title: Please confirm to release docker image
|
||||
issue-body: |
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"@blocksuite/affine-block-paragraph": "workspace:*",
|
||||
"@blocksuite/affine-block-surface": "workspace:*",
|
||||
"@blocksuite/affine-block-surface-ref": "workspace:*",
|
||||
"@blocksuite/affine-block-table": "workspace:*",
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-fragment-doc-title": "workspace:*",
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
} from '@blocksuite/affine-block-paragraph';
|
||||
import { DefaultTool, getSurfaceBlock } from '@blocksuite/affine-block-surface';
|
||||
import { insertSurfaceRefBlockCommand } from '@blocksuite/affine-block-surface-ref';
|
||||
import { insertTableBlockCommand } from '@blocksuite/affine-block-table';
|
||||
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import { insertInlineLatex } from '@blocksuite/affine-inline-latex';
|
||||
@@ -41,20 +40,14 @@ import {
|
||||
deleteSelectedModelsCommand,
|
||||
draftSelectedModelsCommand,
|
||||
duplicateSelectedModelsCommand,
|
||||
focusBlockEnd,
|
||||
getBlockSelectionsCommand,
|
||||
getSelectedModelsCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { AffineTextStyleAttributes } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
createDefaultDoc,
|
||||
isInsideBlockByFlavour,
|
||||
openSingleFileWith,
|
||||
type Signal,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
@@ -94,7 +87,6 @@ import {
|
||||
RedoIcon,
|
||||
RightTabIcon,
|
||||
StrikeThroughIcon,
|
||||
TableIcon,
|
||||
TeXIcon,
|
||||
TextIcon,
|
||||
TodayIcon,
|
||||
@@ -266,62 +258,6 @@ const textToolActionItems: KeyboardToolbarActionItem[] = [
|
||||
.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Table',
|
||||
icon: TableIcon(),
|
||||
showWhen: ({ std, rootComponent: { model } }) =>
|
||||
std.store.schema.flavourSchemaMap.has('affine:table') &&
|
||||
!isInsideBlockByFlavour(std.store, model, 'affine:edgeless-text'),
|
||||
action: ({ std }) => {
|
||||
std.command
|
||||
.chain()
|
||||
.pipe(getSelectedModelsCommand)
|
||||
.pipe(insertTableBlockCommand, {
|
||||
place: 'after',
|
||||
removeEmptyLine: true,
|
||||
})
|
||||
.pipe(({ insertedTableBlockId }) => {
|
||||
if (insertedTableBlockId) {
|
||||
const telemetry = std.getOptional(TelemetryProvider);
|
||||
telemetry?.track('BlockCreated', {
|
||||
blockType: 'affine:table',
|
||||
});
|
||||
}
|
||||
})
|
||||
.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Callout',
|
||||
icon: FontIcon(),
|
||||
showWhen: ({ std, rootComponent: { model } }) => {
|
||||
return (
|
||||
std.get(FeatureFlagService).getFlag('enable_callout') &&
|
||||
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text')
|
||||
);
|
||||
},
|
||||
action: ({ rootComponent: { model }, std }) => {
|
||||
const { store } = model;
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
const index = parent.children.indexOf(model);
|
||||
if (index === -1) return;
|
||||
const calloutId = store.addBlock('affine:callout', {}, parent, index + 1);
|
||||
if (!calloutId) return;
|
||||
const paragraphId = store.addBlock('affine:paragraph', {}, calloutId);
|
||||
if (!paragraphId) return;
|
||||
std.host.updateComplete
|
||||
.then(() => {
|
||||
const paragraph = std.view.getBlock(paragraphId);
|
||||
if (!paragraph) return;
|
||||
std.command.exec(focusBlockEnd, {
|
||||
focusBlock: paragraph,
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const listToolActionItems: KeyboardToolbarActionItem[] = [
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
{ "path": "../../blocks/paragraph" },
|
||||
{ "path": "../../blocks/surface" },
|
||||
{ "path": "../../blocks/surface-ref" },
|
||||
{ "path": "../../blocks/table" },
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../ext-loader" },
|
||||
{ "path": "../../fragments/doc-title" },
|
||||
|
||||
@@ -343,18 +343,7 @@ export class LinkedDocPopover extends SignalWatcher(
|
||||
override willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
const updatePosition = throttle(() => {
|
||||
this._position = getPopperPosition(
|
||||
{
|
||||
getBoundingClientRect: () => {
|
||||
return {
|
||||
...this.getBoundingClientRect(),
|
||||
// Workaround: the width of the popover is zero when it is not rendered
|
||||
width: 280,
|
||||
};
|
||||
},
|
||||
},
|
||||
this.context.startNativeRange
|
||||
);
|
||||
this._position = getPopperPosition(this, this.context.startNativeRange);
|
||||
}, 10);
|
||||
|
||||
this.disposables.addFromEvent(window, 'resize', updatePosition);
|
||||
|
||||
@@ -461,29 +461,6 @@ test('should create message correctly', async t => {
|
||||
sessionId,
|
||||
undefined,
|
||||
undefined,
|
||||
new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' })
|
||||
);
|
||||
t.truthy(messageId, 'should be able to create message with blob');
|
||||
}
|
||||
|
||||
// with attachments
|
||||
{
|
||||
const { id } = await createWorkspace(app);
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
id,
|
||||
randomUUID(),
|
||||
textPromptName
|
||||
);
|
||||
const smallestPng =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
|
||||
const pngData = await fetch(smallestPng).then(res => res.arrayBuffer());
|
||||
const messageId = await createCopilotMessage(
|
||||
app,
|
||||
sessionId,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
[new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' })]
|
||||
);
|
||||
t.truthy(messageId, 'should be able to create message with blobs');
|
||||
|
||||
+33
@@ -13,45 +13,74 @@ 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>|␊
|
||||
@@ -62,12 +91,16 @@ 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,19 +89,3 @@ 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,50 +306,3 @@ 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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -554,73 +554,52 @@ export async function createCopilotMessage(
|
||||
sessionId: string,
|
||||
content?: string,
|
||||
attachments?: string[],
|
||||
blob?: File,
|
||||
blobs?: File[],
|
||||
params?: Record<string, string>
|
||||
): Promise<string> {
|
||||
const gql = {
|
||||
query: `
|
||||
let resp = app
|
||||
.POST('/graphql')
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.field(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation createCopilotMessage($options: CreateChatMessageInput!) {
|
||||
createCopilotMessage(options: $options)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
options: {
|
||||
sessionId,
|
||||
content,
|
||||
attachments,
|
||||
blob: null,
|
||||
blobs: [],
|
||||
params,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let resp = app
|
||||
.POST('/graphql')
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' });
|
||||
if (blob || blobs) {
|
||||
resp = resp.field('operations', JSON.stringify(gql));
|
||||
|
||||
if (blob) {
|
||||
resp = resp.field(
|
||||
'map',
|
||||
JSON.stringify({ '0': ['variables.options.blob'] })
|
||||
);
|
||||
resp = resp.attach('0', Buffer.from(await blob.arrayBuffer()), {
|
||||
filename: blob.name || 'file',
|
||||
contentType: blob.type || 'application/octet-stream',
|
||||
});
|
||||
} else if (blobs && blobs.length) {
|
||||
resp = resp.field(
|
||||
'map',
|
||||
JSON.stringify(
|
||||
Array.from<any>({ length: blobs?.length ?? 0 }).reduce(
|
||||
(acc, _, idx) => {
|
||||
acc[idx.toString()] = [`variables.options.blobs.${idx}`];
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
)
|
||||
variables: {
|
||||
options: { sessionId, content, attachments, blobs: [], params },
|
||||
},
|
||||
})
|
||||
)
|
||||
.field(
|
||||
'map',
|
||||
JSON.stringify(
|
||||
Array.from<any>({ length: blobs?.length ?? 0 }).reduce(
|
||||
(acc, _, idx) => {
|
||||
acc[idx.toString()] = [`variables.options.blobs.${idx}`];
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
)
|
||||
)
|
||||
);
|
||||
if (blobs && blobs.length) {
|
||||
for (const [idx, file] of blobs.entries()) {
|
||||
resp = resp.attach(
|
||||
idx.toString(),
|
||||
Buffer.from(await file.arrayBuffer()),
|
||||
{
|
||||
filename: file.name || `file${idx}`,
|
||||
contentType: file.type || 'application/octet-stream',
|
||||
}
|
||||
);
|
||||
for (const [idx, file] of blobs.entries()) {
|
||||
resp = resp.attach(
|
||||
idx.toString(),
|
||||
Buffer.from(await file.arrayBuffer()),
|
||||
{
|
||||
filename: file.name || `file${idx}`,
|
||||
contentType: file.type || 'application/octet-stream',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resp = resp.send(gql);
|
||||
}
|
||||
|
||||
const res = await resp.expect(200);
|
||||
console.log('createCopilotMessage', res.body);
|
||||
|
||||
return res.body.data.createCopilotMessage;
|
||||
}
|
||||
|
||||
|
||||
+33
@@ -13,45 +13,74 @@ 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>|␊
|
||||
@@ -62,12 +91,16 @@ 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,45 +13,74 @@ 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>|␊
|
||||
@@ -62,12 +91,16 @@ 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,45 +1376,74 @@ 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>|␊
|
||||
@@ -1425,12 +1454,16 @@ 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.',
|
||||
}
|
||||
@@ -1443,80 +1476,113 @@ 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 getEmbeddingStatus(workspaceId: string) {
|
||||
async getWorkspaceEmbeddingStatus(workspaceId: string) {
|
||||
const ignoredDocIds = (await this.listIgnoredDocIds(workspaceId)).map(
|
||||
d => d.docId
|
||||
);
|
||||
@@ -168,13 +168,9 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
|
||||
};
|
||||
|
||||
const [docTotal, docEmbedded, fileTotal, fileEmbedded] = await Promise.all([
|
||||
this.db.snapshot.findMany({
|
||||
where: snapshotCondition,
|
||||
select: { id: true },
|
||||
}),
|
||||
this.db.snapshot.findMany({
|
||||
this.db.snapshot.count({ where: snapshotCondition }),
|
||||
this.db.snapshot.count({
|
||||
where: { ...snapshotCondition, embedding: { some: {} } },
|
||||
select: { id: true },
|
||||
}),
|
||||
this.db.aiWorkspaceFiles.count({ where: { workspaceId } }),
|
||||
this.db.aiWorkspaceFiles.count({
|
||||
@@ -182,23 +178,9 @@ 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:
|
||||
docTotalIds.filter(id => !duplicateOutdatedDocSet.has(id)).length +
|
||||
fileTotal,
|
||||
embedded:
|
||||
docEmbedded
|
||||
.map(d => d.id)
|
||||
.filter(id => !duplicateOutdatedDocSet.has(id)).length + fileEmbedded,
|
||||
total: docTotal + fileTotal,
|
||||
embedded: docEmbedded + fileEmbedded,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -356,7 +356,6 @@ export class CopilotContextRootResolver {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Throttle('strict')
|
||||
@Query(() => ContextWorkspaceEmbeddingStatus, {
|
||||
description: 'query workspace embedding status',
|
||||
})
|
||||
@@ -373,7 +372,9 @@ export class CopilotContextRootResolver {
|
||||
|
||||
if (this.context.canEmbedding) {
|
||||
const { total, embedded } =
|
||||
await this.models.copilotWorkspace.getEmbeddingStatus(workspaceId);
|
||||
await this.models.copilotWorkspace.getWorkspaceEmbeddingStatus(
|
||||
workspaceId
|
||||
);
|
||||
return { total, embedded };
|
||||
}
|
||||
|
||||
|
||||
@@ -303,7 +303,7 @@ const textActions: Prompt[] = [
|
||||
{
|
||||
name: 'Transcript audio',
|
||||
action: 'Transcript audio',
|
||||
model: 'gemini-2.5-flash',
|
||||
model: 'gemini-2.5-pro',
|
||||
optionalModels: ['gemini-2.5-flash', 'gemini-2.5-pro'],
|
||||
messages: [
|
||||
{
|
||||
@@ -334,7 +334,6 @@ Convert a multi-speaker audio recording into a structured JSON format by transcr
|
||||
config: {
|
||||
requireContent: false,
|
||||
requireAttachment: true,
|
||||
maxRetries: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -129,16 +129,7 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
|
||||
system,
|
||||
messages: msgs,
|
||||
schema,
|
||||
providerOptions: {
|
||||
google: {
|
||||
thinkingConfig: {
|
||||
thinkingBudget: -1,
|
||||
includeThoughts: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
abortSignal: options.signal,
|
||||
maxRetries: options.maxRetries || 3,
|
||||
experimental_repairText: async ({ text, error }) => {
|
||||
if (error instanceof JSONParseError) {
|
||||
// strange fixed response, temporarily replace it
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
import { CurrentUser } from '../../core/auth';
|
||||
import { Admin } from '../../core/common';
|
||||
import { DocReader } from '../../core/doc';
|
||||
import { AccessController, DocAction } from '../../core/permission';
|
||||
import { AccessController } from '../../core/permission';
|
||||
import { UserType } from '../../core/user';
|
||||
import type { ListSessionOptions, UpdateChatSession } from '../../models';
|
||||
import { CopilotCronJobs } from './cron';
|
||||
@@ -143,9 +143,6 @@ class CreateChatMessageInput implements Omit<SubmittedMessage, 'content'> {
|
||||
@Field(() => [String], { nullable: true, deprecationReason: 'use blobs' })
|
||||
attachments!: string[] | undefined;
|
||||
|
||||
@Field(() => GraphQLUpload, { nullable: true })
|
||||
blob!: Promise<FileUpload> | undefined;
|
||||
|
||||
@Field(() => [GraphQLUpload], { nullable: true })
|
||||
blobs!: Promise<FileUpload>[] | undefined;
|
||||
|
||||
@@ -420,8 +417,7 @@ export class CopilotResolver {
|
||||
|
||||
private async assertPermission(
|
||||
user: CurrentUser,
|
||||
options: { workspaceId?: string | null; docId?: string | null },
|
||||
fallbackAction?: DocAction
|
||||
options: { workspaceId?: string | null; docId?: string | null }
|
||||
) {
|
||||
const { workspaceId, docId } = options;
|
||||
if (!workspaceId) {
|
||||
@@ -432,7 +428,7 @@ export class CopilotResolver {
|
||||
.user(user.id)
|
||||
.doc({ workspaceId, docId })
|
||||
.allowLocal()
|
||||
.assert(fallbackAction ?? 'Doc.Update');
|
||||
.assert('Doc.Update');
|
||||
} else {
|
||||
await this.ac
|
||||
.user(user.id)
|
||||
@@ -511,7 +507,7 @@ export class CopilotResolver {
|
||||
if (!workspaceId) {
|
||||
return [];
|
||||
} else {
|
||||
await this.assertPermission(user, { workspaceId, docId }, 'Doc.Read');
|
||||
await this.assertPermission(user, { workspaceId, docId });
|
||||
}
|
||||
|
||||
const histories = await this.chatSession.list(
|
||||
@@ -541,7 +537,7 @@ export class CopilotResolver {
|
||||
if (!workspaceId) {
|
||||
return paginate([], 'updatedAt', pagination, 0);
|
||||
} else {
|
||||
await this.assertPermission(user, { workspaceId, docId }, 'Doc.Read');
|
||||
await this.assertPermission(user, { workspaceId, docId });
|
||||
}
|
||||
|
||||
const finalOptions = Object.assign(
|
||||
@@ -707,13 +703,10 @@ export class CopilotResolver {
|
||||
}
|
||||
|
||||
const attachments: PromptMessage['attachments'] = options.attachments || [];
|
||||
if (options.blob || options.blobs) {
|
||||
if (options.blobs) {
|
||||
const { workspaceId } = session.config;
|
||||
|
||||
const blobs = await Promise.all(
|
||||
options.blob ? [options.blob] : options.blobs || []
|
||||
);
|
||||
delete options.blob;
|
||||
const blobs = await Promise.all(options.blobs);
|
||||
delete options.blobs;
|
||||
|
||||
for (const blob of blobs) {
|
||||
|
||||
@@ -150,33 +150,24 @@ You should specify the following arguments before the others: [doc_id], [origin_
|
||||
'A short, first-person description of the intended edit, clearly summarizing what I will change. For example: "I will translate the steps into English and delete the paragraph explaining the delay." This helps the downstream system understand the purpose of the changes.'
|
||||
),
|
||||
|
||||
code_edit: z.preprocess(
|
||||
val => {
|
||||
// BACKGROUND: LLM sometimes returns a JSON string instead of an array.
|
||||
if (typeof val === 'string') {
|
||||
return JSON.parse(val);
|
||||
}
|
||||
return val;
|
||||
},
|
||||
z
|
||||
.array(
|
||||
z.object({
|
||||
op: z
|
||||
.string()
|
||||
.describe(
|
||||
'A short description of the change, such as "Bold intro name"'
|
||||
),
|
||||
updates: z
|
||||
.string()
|
||||
.describe(
|
||||
'Markdown block fragments that represent the change, including the block_id and type'
|
||||
),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'An array of independent semantic changes to apply to the document.'
|
||||
)
|
||||
),
|
||||
code_edit: z
|
||||
.array(
|
||||
z.object({
|
||||
op: z
|
||||
.string()
|
||||
.describe(
|
||||
'A short description of the change, such as "Bold intro name"'
|
||||
),
|
||||
updates: z
|
||||
.string()
|
||||
.describe(
|
||||
'Markdown block fragments that represent the change, including the block_id and type'
|
||||
),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'An array of independent semantic changes to apply to the document.'
|
||||
),
|
||||
}),
|
||||
execute: async ({ doc_id, origin_content, code_edit }) => {
|
||||
try {
|
||||
|
||||
@@ -15,6 +15,7 @@ import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
|
||||
import {
|
||||
CopilotTranscriptionAudioNotProvided,
|
||||
CopilotTranscriptionJobNotFound,
|
||||
type FileUpload,
|
||||
} from '../../../base';
|
||||
import { CurrentUser } from '../../../core/auth';
|
||||
@@ -73,7 +74,7 @@ const FinishedStatus: Set<AiJobStatus> = new Set([
|
||||
export class CopilotTranscriptionResolver {
|
||||
constructor(
|
||||
private readonly ac: AccessController,
|
||||
private readonly transcript: CopilotTranscriptionService
|
||||
private readonly service: CopilotTranscriptionService
|
||||
) {}
|
||||
|
||||
private handleJobResult(
|
||||
@@ -121,7 +122,7 @@ export class CopilotTranscriptionResolver {
|
||||
throw new CopilotTranscriptionAudioNotProvided();
|
||||
}
|
||||
|
||||
const jobResult = await this.transcript.submitJob(
|
||||
const jobResult = await this.service.submitTranscriptionJob(
|
||||
user.id,
|
||||
workspaceId,
|
||||
blobId,
|
||||
@@ -143,11 +144,19 @@ export class CopilotTranscriptionResolver {
|
||||
.allowLocal()
|
||||
.assert('Workspace.Copilot');
|
||||
|
||||
const jobResult = await this.transcript.retryJob(
|
||||
const job = await this.service.queryTranscriptionJob(
|
||||
user.id,
|
||||
workspaceId,
|
||||
jobId
|
||||
);
|
||||
if (!job || !job.infos) {
|
||||
throw new CopilotTranscriptionJobNotFound();
|
||||
}
|
||||
|
||||
const jobResult = await this.service.executeTranscriptionJob(
|
||||
job.id,
|
||||
job.infos
|
||||
);
|
||||
|
||||
return this.handleJobResult(jobResult);
|
||||
}
|
||||
@@ -157,7 +166,7 @@ export class CopilotTranscriptionResolver {
|
||||
@CurrentUser() user: CurrentUser,
|
||||
@Args('jobId') jobId: string
|
||||
): Promise<TranscriptionResultType | null> {
|
||||
const job = await this.transcript.claimJob(user.id, jobId);
|
||||
const job = await this.service.claimTranscriptionJob(user.id, jobId);
|
||||
return this.handleJobResult(job);
|
||||
}
|
||||
|
||||
@@ -181,7 +190,7 @@ export class CopilotTranscriptionResolver {
|
||||
.allowLocal()
|
||||
.assert('Workspace.Copilot');
|
||||
|
||||
const job = await this.transcript.queryJob(
|
||||
const job = await this.service.queryTranscriptionJob(
|
||||
user.id,
|
||||
copilot.workspaceId,
|
||||
jobId,
|
||||
|
||||
@@ -49,17 +49,7 @@ export class CopilotTranscriptionService {
|
||||
private readonly providerFactory: CopilotProviderFactory
|
||||
) {}
|
||||
|
||||
private async getModel(userId: string) {
|
||||
const prompt = await this.prompt.get('Transcript audio');
|
||||
const hasAccess = await this.models.userFeature.has(
|
||||
userId,
|
||||
'unlimited_copilot'
|
||||
);
|
||||
// choose the pro model if user has copilot plan
|
||||
return prompt?.optionalModels[hasAccess ? 1 : 0];
|
||||
}
|
||||
|
||||
async submitJob(
|
||||
async submitTranscriptionJob(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
blobId: string,
|
||||
@@ -88,26 +78,12 @@ export class CopilotTranscriptionService {
|
||||
infos.push({ url, mimeType: blob.mimetype });
|
||||
}
|
||||
|
||||
const model = await this.getModel(userId);
|
||||
return await this.executeJob(jobId, infos, model);
|
||||
return await this.executeTranscriptionJob(jobId, infos);
|
||||
}
|
||||
|
||||
async retryJob(userId: string, workspaceId: string, jobId: string) {
|
||||
const job = await this.queryJob(userId, workspaceId, jobId);
|
||||
if (!job || !job.infos) {
|
||||
throw new CopilotTranscriptionJobNotFound();
|
||||
}
|
||||
|
||||
const model = await this.getModel(userId);
|
||||
const jobResult = await this.executeJob(job.id, job.infos, model);
|
||||
|
||||
return jobResult;
|
||||
}
|
||||
|
||||
async executeJob(
|
||||
async executeTranscriptionJob(
|
||||
jobId: string,
|
||||
infos: AudioBlobInfos,
|
||||
modelId?: string
|
||||
infos: AudioBlobInfos
|
||||
): Promise<TranscriptionJob> {
|
||||
const status = AiJobStatus.running;
|
||||
const success = await this.models.copilotJob.update(jobId, {
|
||||
@@ -122,13 +98,12 @@ export class CopilotTranscriptionService {
|
||||
await this.job.add('copilot.transcript.submit', {
|
||||
jobId,
|
||||
infos,
|
||||
modelId,
|
||||
});
|
||||
|
||||
return { id: jobId, status };
|
||||
}
|
||||
|
||||
async claimJob(
|
||||
async claimTranscriptionJob(
|
||||
userId: string,
|
||||
jobId: string
|
||||
): Promise<TranscriptionJob | null> {
|
||||
@@ -143,7 +118,7 @@ export class CopilotTranscriptionService {
|
||||
return null;
|
||||
}
|
||||
|
||||
async queryJob(
|
||||
async queryTranscriptionJob(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
jobId?: string,
|
||||
@@ -206,20 +181,14 @@ export class CopilotTranscriptionService {
|
||||
promptName: string,
|
||||
message: Partial<PromptMessage>,
|
||||
schema?: ZodType<any>,
|
||||
prefer?: CopilotProviderType,
|
||||
modelId?: string
|
||||
prefer?: CopilotProviderType
|
||||
): Promise<string> {
|
||||
const prompt = await this.prompt.get(promptName);
|
||||
if (!prompt) {
|
||||
throw new CopilotPromptNotFound({ name: promptName });
|
||||
}
|
||||
|
||||
const cond = {
|
||||
modelId:
|
||||
modelId && prompt.optionalModels.includes(modelId)
|
||||
? modelId
|
||||
: prompt.model,
|
||||
};
|
||||
const cond = { modelId: prompt.model };
|
||||
const msg = { role: 'user' as const, content: '', ...message };
|
||||
const config = Object.assign({}, prompt.config);
|
||||
if (schema) {
|
||||
@@ -262,19 +231,13 @@ export class CopilotTranscriptionService {
|
||||
return `${hoursStr}:${minutesStr}:${secondsStr}`;
|
||||
}
|
||||
|
||||
private async callTranscript(
|
||||
url: string,
|
||||
mimeType: string,
|
||||
offset: number,
|
||||
modelId?: string
|
||||
) {
|
||||
private async callTranscript(url: string, mimeType: string, offset: number) {
|
||||
// NOTE: Vertex provider not support transcription yet, we always use Gemini here
|
||||
const result = await this.chatWithPrompt(
|
||||
'Transcript audio',
|
||||
{ attachments: [url], params: { mimetype: mimeType } },
|
||||
TranscriptionResponseSchema,
|
||||
CopilotProviderType.Gemini,
|
||||
modelId
|
||||
CopilotProviderType.Gemini
|
||||
);
|
||||
|
||||
const transcription = TranscriptionResponseSchema.parse(
|
||||
@@ -293,7 +256,6 @@ export class CopilotTranscriptionService {
|
||||
async transcriptAudio({
|
||||
jobId,
|
||||
infos,
|
||||
modelId,
|
||||
// @deprecated
|
||||
url,
|
||||
mimeType,
|
||||
@@ -302,7 +264,7 @@ export class CopilotTranscriptionService {
|
||||
const blobInfos = this.mergeInfos(infos, url, mimeType);
|
||||
const transcriptions = await Promise.all(
|
||||
Array.from(blobInfos.entries()).map(([idx, { url, mimeType }]) =>
|
||||
this.callTranscript(url, mimeType, idx * 10 * 60, modelId)
|
||||
this.callTranscript(url, mimeType, idx * 10 * 60)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ declare global {
|
||||
'copilot.transcript.submit': {
|
||||
jobId: string;
|
||||
infos?: AudioBlobInfos;
|
||||
modelId?: string;
|
||||
/// @deprecated use `infos` instead
|
||||
url?: string;
|
||||
/// @deprecated use `infos` instead
|
||||
|
||||
@@ -103,7 +103,6 @@ export class CopilotWorkspaceEmbeddingConfigResolver {
|
||||
|
||||
return ignoredDocs;
|
||||
}
|
||||
|
||||
@Mutation(() => Number, {
|
||||
name: 'updateWorkspaceEmbeddingIgnoredDocs',
|
||||
complexity: 2,
|
||||
|
||||
@@ -457,7 +457,6 @@ type CopilotWorkspaceIgnoredDocTypeEdge {
|
||||
|
||||
input CreateChatMessageInput {
|
||||
attachments: [String!]
|
||||
blob: Upload
|
||||
blobs: [Upload!]
|
||||
content: String
|
||||
params: JSON
|
||||
|
||||
@@ -569,7 +569,6 @@ export interface CopilotWorkspaceIgnoredDocTypeEdge {
|
||||
|
||||
export interface CreateChatMessageInput {
|
||||
attachments?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
blob?: InputMaybe<Scalars['Upload']['input']>;
|
||||
blobs?: InputMaybe<Array<Scalars['Upload']['input']>>;
|
||||
content?: InputMaybe<Scalars['String']['input']>;
|
||||
params?: InputMaybe<Scalars['JSON']['input']>;
|
||||
|
||||
@@ -58,45 +58,74 @@ 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>|
|
||||
@@ -107,12 +136,16 @@ 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": [
|
||||
@@ -289,6 +322,7 @@ 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",
|
||||
@@ -297,6 +331,7 @@ For developer or installation guides, please go to [AFFiNE Development](https://
|
||||
{
|
||||
"children": [],
|
||||
"content": "* Trello with their Kanban
|
||||
|
||||
",
|
||||
"flavour": "affine:list",
|
||||
"id": "Tp9xyN4Okl",
|
||||
@@ -305,6 +340,7 @@ 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",
|
||||
@@ -313,6 +349,7 @@ 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",
|
||||
@@ -321,6 +358,7 @@ 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",
|
||||
@@ -389,63 +427,77 @@ 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
|
||||
|
||||
|
||||
",
|
||||
},
|
||||
],
|
||||
@@ -507,80 +559,113 @@ 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 -->
|
||||
|
||||
|
||||
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -588,74 +673,122 @@ 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|
|
||||
|---|---|---|---|
|
||||
@@ -663,129 +796,176 @@ 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
|
||||
@@ -799,18 +979,23 @@ 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,10 +22,9 @@ 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('');
|
||||
@@ -110,7 +109,7 @@ export function parseBlock(
|
||||
const checked = yBlock.get('prop:checked') as boolean;
|
||||
prefix = checked ? '- [x] ' : '- [ ] ';
|
||||
}
|
||||
result.content = prefix + toMd();
|
||||
result.content = prefix + toMd() + '\n';
|
||||
break;
|
||||
}
|
||||
case 'affine:code': {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Suspense } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import { setupEffects } from './effects';
|
||||
import { DesktopLanguageSync } from './language-sync';
|
||||
import { DesktopThemeSync } from './theme-sync';
|
||||
|
||||
const { frameworkProvider } = setupEffects();
|
||||
@@ -47,7 +46,6 @@ export function App() {
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<DesktopThemeSync />
|
||||
<DesktopLanguageSync />
|
||||
<RouterProvider
|
||||
fallbackElement={<AppContainer fallback />}
|
||||
router={router}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api';
|
||||
import { I18nService } from '@affine/core/modules/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const DesktopLanguageSync = () => {
|
||||
const i18nService = useService(I18nService);
|
||||
const currentLanguage = useLiveData(i18nService.i18n.currentLanguageKey$);
|
||||
const handler = useService(DesktopApiService).api.handler;
|
||||
|
||||
useEffect(() => {
|
||||
handler.i18n.changeLanguage(currentLanguage ?? 'en').catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [currentLanguage, handler]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -33,7 +33,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine-tools/utils": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/native": "workspace:*",
|
||||
"@affine/nbstore": "workspace:*",
|
||||
"@electron-forge/cli": "^7.6.0",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { I18n } from '@affine/i18n';
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import { AFFINE_API_CHANNEL_NAME } from '../shared/type';
|
||||
@@ -22,12 +21,6 @@ export const debugHandlers = {
|
||||
},
|
||||
};
|
||||
|
||||
export const i18nHandlers = {
|
||||
changeLanguage: async (_: Electron.IpcMainInvokeEvent, language: string) => {
|
||||
return I18n.changeLanguage(language);
|
||||
},
|
||||
};
|
||||
|
||||
// Note: all of these handlers will be the single-source-of-truth for the apis exposed to the renderer process
|
||||
export const allHandlers = {
|
||||
debug: debugHandlers,
|
||||
@@ -40,7 +33,6 @@ export const allHandlers = {
|
||||
worker: workerHandlers,
|
||||
recording: recordingHandlers,
|
||||
popup: popupHandlers,
|
||||
i18n: i18nHandlers,
|
||||
};
|
||||
|
||||
export const registerHandlers = () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { I18n } from '@affine/i18n';
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
@@ -823,53 +822,42 @@ export class WebContentViewsManager {
|
||||
},
|
||||
});
|
||||
|
||||
view.webContents.on('context-menu', (_event, params) => {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
id: 'cut',
|
||||
label: I18n['com.affine.context-menu.cut'](),
|
||||
role: 'cut',
|
||||
enabled: params.editFlags.canCut,
|
||||
},
|
||||
{
|
||||
id: 'copy',
|
||||
label: I18n['com.affine.context-menu.copy'](),
|
||||
role: 'copy',
|
||||
enabled: params.editFlags.canCopy,
|
||||
},
|
||||
{
|
||||
id: 'paste',
|
||||
label: I18n['com.affine.context-menu.paste'](),
|
||||
role: 'paste',
|
||||
enabled: params.editFlags.canPaste,
|
||||
},
|
||||
]);
|
||||
if (spellCheckSettings.enabled) {
|
||||
view.webContents.on('context-menu', (_event, params) => {
|
||||
const shouldShow =
|
||||
params.misspelledWord && params.dictionarySuggestions.length > 0;
|
||||
|
||||
// Add each spelling suggestion
|
||||
for (const suggestion of params.dictionarySuggestions) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: suggestion,
|
||||
click: () => view.webContents.replaceMisspelling(suggestion),
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!shouldShow) {
|
||||
return;
|
||||
}
|
||||
const menu = new Menu();
|
||||
|
||||
// Allow users to add the misspelled word to the dictionary
|
||||
if (params.misspelledWord) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: 'Add to dictionary', // TODO: i18n
|
||||
click: () =>
|
||||
view.webContents.session.addWordToSpellCheckerDictionary(
|
||||
params.misspelledWord
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
// Add each spelling suggestion
|
||||
for (const suggestion of params.dictionarySuggestions) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: suggestion,
|
||||
click: () => view.webContents.replaceMisspelling(suggestion),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
menu.popup();
|
||||
});
|
||||
// Allow users to add the misspelled word to the dictionary
|
||||
if (params.misspelledWord) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: 'Add to dictionary', // TODO: i18n
|
||||
click: () =>
|
||||
view.webContents.session.addWordToSpellCheckerDictionary(
|
||||
params.misspelledWord
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
menu.popup();
|
||||
});
|
||||
}
|
||||
|
||||
this.webViewsMap$.next(this.tabViewsMap.set(viewId, view));
|
||||
let unsub = () => {};
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../../../../tools/utils" },
|
||||
{ "path": "../../i18n" },
|
||||
{ "path": "../../native" },
|
||||
{ "path": "../../../common/nbstore" },
|
||||
{ "path": "../../../common/infra" }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -90,6 +90,8 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
C45499AB2D140B5000E21978 /* NBStore */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = NBStore;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -337,13 +339,9 @@
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AFFiNE/Pods-AFFiNE-frameworks.sh\"\n";
|
||||
|
||||
+13
-4
@@ -27,6 +27,15 @@
|
||||
"version" : "1.1.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "litext",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Lakr233/Litext",
|
||||
"state" : {
|
||||
"revision" : "c37f3ab5826659854311e20d6c3942d4905b00b6",
|
||||
"version" : "0.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "lrucache",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -41,8 +50,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Lakr233/MarkdownView",
|
||||
"state" : {
|
||||
"revision" : "446dba45be81c67d0717d19277367dcbe5b2fb12",
|
||||
"version" : "3.1.9"
|
||||
"revision" : "29a9da19d6dc21af4e629c423961b0f453ffe192",
|
||||
"version" : "2.3.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -59,8 +68,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Lakr233/Splash",
|
||||
"state" : {
|
||||
"revision" : "de9cde249fdb7a173a6e6b950ab18b11f6c2a557",
|
||||
"version" : "0.18.0"
|
||||
"revision" : "4d997712fe07f75695aacdf287aeb3b1f2c6ab88",
|
||||
"version" : "0.17.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,5 +13,23 @@ extension AFFiNEViewController: IntelligentsButtonDelegate {
|
||||
// if it shows up then we are ready to go
|
||||
let controller = IntelligentsController()
|
||||
self.present(controller, animated: true)
|
||||
// IntelligentContext.shared.webView = webView
|
||||
// button.beginProgress()
|
||||
// IntelligentContext.shared.preparePresent { result in
|
||||
// DispatchQueue.main.async {
|
||||
// button.stopProgress()
|
||||
// switch result {
|
||||
// case .success:
|
||||
// case let .failure(failure):
|
||||
// let alert = UIAlertController(
|
||||
// title: "Error",
|
||||
// message: failure.localizedDescription,
|
||||
// preferredStyle: .alert
|
||||
// )
|
||||
// alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
// self.present(alert, animated: true)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,12 @@ class AFFiNEViewController: CAPBridgeViewController {
|
||||
switch result {
|
||||
case .failure: break
|
||||
case .success:
|
||||
#if DEBUG
|
||||
// only show the button in debug mode before we get done
|
||||
self.presentIntelligentsButton()
|
||||
#else
|
||||
break
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,5 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ 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) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename chats( pagination: { first: $limit } options: { fork: false, sessionOrder: desc, withMessages: true } ) { __typename ...PaginatedCopilotChats } } } }"#,
|
||||
fragments: [CopilotChatHistory.self, CopilotChatMessage.self, PaginatedCopilotChats.self]
|
||||
))
|
||||
|
||||
@@ -69,7 +69,7 @@ public class GetCopilotRecentSessionsQuery: GraphQLQuery {
|
||||
"options": [
|
||||
"fork": false,
|
||||
"sessionOrder": "desc",
|
||||
"withMessages": false
|
||||
"withMessages": true
|
||||
]
|
||||
]),
|
||||
] }
|
||||
|
||||
+1
-5
@@ -7,7 +7,7 @@ public class GetWorkspacePageByIdQuery: GraphQLQuery {
|
||||
public static let operationName: String = "getWorkspacePageById"
|
||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
||||
definition: .init(
|
||||
#"query getWorkspacePageById($workspaceId: String!, $pageId: String!) { workspace(id: $workspaceId) { __typename doc(docId: $pageId) { __typename id mode defaultRole public title summary } } }"#
|
||||
#"query getWorkspacePageById($workspaceId: String!, $pageId: String!) { workspace(id: $workspaceId) { __typename doc(docId: $pageId) { __typename id mode defaultRole public } } }"#
|
||||
))
|
||||
|
||||
public var workspaceId: String
|
||||
@@ -68,16 +68,12 @@ public class GetWorkspacePageByIdQuery: GraphQLQuery {
|
||||
.field("mode", GraphQLEnum<AffineGraphQL.PublicDocMode>.self),
|
||||
.field("defaultRole", GraphQLEnum<AffineGraphQL.DocRole>.self),
|
||||
.field("public", Bool.self),
|
||||
.field("title", String?.self),
|
||||
.field("summary", String?.self),
|
||||
] }
|
||||
|
||||
public var id: String { __data["id"] }
|
||||
public var mode: GraphQLEnum<AffineGraphQL.PublicDocMode> { __data["mode"] }
|
||||
public var defaultRole: GraphQLEnum<AffineGraphQL.DocRole> { __data["defaultRole"] }
|
||||
public var `public`: Bool { __data["public"] }
|
||||
public var title: String? { __data["title"] }
|
||||
public var summary: String? { __data["summary"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ let package = Package(
|
||||
.package(url: "https://github.com/SwifterSwift/SwifterSwift.git", from: "6.0.0"),
|
||||
.package(url: "https://github.com/Recouse/EventSource", from: "0.1.4"),
|
||||
.package(url: "https://github.com/Lakr233/ListViewKit", from: "1.1.6"),
|
||||
.package(url: "https://github.com/Lakr233/MarkdownView", from: "3.1.9"),
|
||||
.package(url: "https://github.com/Lakr233/MarkdownView", exact: "2.3.8"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "Intelligents", dependencies: [
|
||||
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
//
|
||||
// ChatManager+CURD.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 7/14/25.
|
||||
//
|
||||
|
||||
import AffineGraphQL
|
||||
import Apollo
|
||||
import ApolloAPI
|
||||
import EventSource
|
||||
import Foundation
|
||||
import MarkdownParser
|
||||
import MarkdownView
|
||||
|
||||
extension ChatManager {
|
||||
func clearCurrentSession() {
|
||||
guard let session = IntelligentContext.shared.currentSession else {
|
||||
print("[-] no current session to clear")
|
||||
return
|
||||
}
|
||||
|
||||
let mutation = CleanupCopilotSessionMutation(input: .init(
|
||||
docId: session.docId ?? "",
|
||||
sessionIds: [session.id],
|
||||
workspaceId: session.workspaceId
|
||||
))
|
||||
|
||||
QLService.shared.client.perform(mutation: mutation) { result in
|
||||
print("[+] cleanup session result: \(result)")
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
-151
@@ -24,173 +24,53 @@ private extension InputBoxData {
|
||||
}
|
||||
}
|
||||
|
||||
public extension ChatManager {
|
||||
func startUserRequest(editorData: InputBoxData, sessionId: String) {
|
||||
extension ChatManager {
|
||||
public func startUserRequest(
|
||||
content: String,
|
||||
inputBoxData: InputBoxData,
|
||||
sessionId: String
|
||||
) {
|
||||
append(sessionId: sessionId, UserMessageCellViewModel(
|
||||
id: .init(),
|
||||
content: editorData.text,
|
||||
content: inputBoxData.text,
|
||||
timestamp: .init()
|
||||
))
|
||||
append(sessionId: sessionId, UserHintCellViewModel(
|
||||
id: .init(),
|
||||
timestamp: .init(),
|
||||
imageAttachments: editorData.imageAttachments,
|
||||
fileAttachments: editorData.fileAttachments,
|
||||
docAttachments: editorData.documentAttachments
|
||||
imageAttachments: inputBoxData.imageAttachments,
|
||||
fileAttachments: inputBoxData.fileAttachments,
|
||||
docAttachments: inputBoxData.documentAttachments
|
||||
))
|
||||
|
||||
let viewModelId = append(sessionId: sessionId, AssistantMessageCellViewModel(
|
||||
id: .init(),
|
||||
content: "...",
|
||||
timestamp: .init()
|
||||
))
|
||||
scrollToBottomPublisher.send(sessionId)
|
||||
|
||||
guard let workspaceId = IntelligentContext.shared.currentWorkspaceId,
|
||||
!workspaceId.isEmpty
|
||||
else {
|
||||
report(sessionId, ChatError.unknownError)
|
||||
assertionFailure("Invalid workspace ID")
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global().async {
|
||||
self.prepareContext(
|
||||
workspaceId: workspaceId,
|
||||
sessionId: sessionId,
|
||||
editorData: editorData,
|
||||
viewModelId: viewModelId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ChatManager {
|
||||
func prepareContext(
|
||||
workspaceId: String,
|
||||
sessionId: String,
|
||||
editorData: InputBoxData,
|
||||
viewModelId: UUID
|
||||
) {
|
||||
assert(!Thread.isMainThread)
|
||||
let createContext = CreateCopilotContextMutation(
|
||||
workspaceId: workspaceId,
|
||||
sessionId: sessionId
|
||||
)
|
||||
QLService.shared.client.perform(mutation: createContext) { result in
|
||||
DispatchQueue.main.async {
|
||||
switch result {
|
||||
case let .success(graphQLResult):
|
||||
guard let contextId = graphQLResult.data?.createCopilotContext else {
|
||||
self.report(sessionId, ChatError.invalidResponse)
|
||||
return
|
||||
}
|
||||
print("[+] copilot context created: \(contextId)")
|
||||
|
||||
DispatchQueue.global().async {
|
||||
let docAttachGroup = DispatchGroup()
|
||||
for docAttach in editorData.documentAttachments {
|
||||
let addDoc = AddContextDocMutation(
|
||||
options: .init(
|
||||
contextId: contextId,
|
||||
docId: docAttach.documentID
|
||||
)
|
||||
)
|
||||
docAttachGroup.enter()
|
||||
QLService.shared.client.perform(mutation: addDoc) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
print("[+] doc \(docAttach.documentID) added to context")
|
||||
case let .failure(error):
|
||||
print("[-] addContextDoc failed: \(error)")
|
||||
}
|
||||
docAttachGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
docAttachGroup.notify(queue: .global()) {
|
||||
var contextSnippet = ""
|
||||
if !editorData.documentAttachments.isEmpty {
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
let matchQuery = MatchContextQuery(
|
||||
contextId: .some(contextId),
|
||||
workspaceId: .some(workspaceId),
|
||||
content: editorData.text,
|
||||
limit: .none,
|
||||
scopedThreshold: .none,
|
||||
threshold: .none
|
||||
)
|
||||
QLService.shared.client.fetch(query: matchQuery) { result in
|
||||
switch result {
|
||||
case let .success(queryResult):
|
||||
let matches = queryResult.data?.currentUser?.copilot.contexts ?? []
|
||||
let matchDocs = matches.compactMap(\.matchWorkspaceDocs).flatMap(\.self)
|
||||
for context in matchDocs {
|
||||
contextSnippet += "<file docId=\"\(context.docId)\" chunk=\"\(context.chunk)\">\(context.content)</file>\n"
|
||||
}
|
||||
case let .failure(error):
|
||||
print("[-] matchContext failed: \(error)")
|
||||
// self.report(sessionId, error)
|
||||
}
|
||||
sem.signal()
|
||||
}
|
||||
sem.wait()
|
||||
}
|
||||
print("[+] context snippet prepared: \(contextSnippet)")
|
||||
self.startCopilotResponse(
|
||||
editorData: editorData,
|
||||
contextSnippet: contextSnippet,
|
||||
sessionId: sessionId,
|
||||
viewModelId: viewModelId
|
||||
)
|
||||
}
|
||||
}
|
||||
case let .failure(error):
|
||||
self.report(sessionId, error)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startCopilotResponse(
|
||||
editorData: InputBoxData,
|
||||
contextSnippet: String,
|
||||
sessionId: String,
|
||||
viewModelId: UUID
|
||||
) {
|
||||
assert(!Thread.isMainThread)
|
||||
|
||||
let messageParameters: [String: AnyHashable] = [
|
||||
// packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx
|
||||
"docs": editorData.documentAttachments.map(\.documentID), // affine doc
|
||||
"docs": inputBoxData.documentAttachments.map(\.documentID), // affine doc
|
||||
"files": [String](), // attachment in context, keep nil for now
|
||||
"searchMode": editorData.isSearchEnabled ? "MUST" : "AUTO",
|
||||
"searchMode": inputBoxData.isSearchEnabled ? "MUST" : "AUTO",
|
||||
]
|
||||
let attachmentFieldName = "options.blobs"
|
||||
var uploadableAttachments: [GraphQLFile] = [
|
||||
editorData.fileAttachments.map { file -> GraphQLFile in
|
||||
.init(fieldName: attachmentFieldName, originalName: file.name, data: file.data ?? .init())
|
||||
let uploadableAttachments: [GraphQLFile] = [
|
||||
inputBoxData.fileAttachments.map { file -> GraphQLFile in
|
||||
.init(
|
||||
fieldName: file.name,
|
||||
originalName: file.name,
|
||||
data: file.data ?? .init()
|
||||
)
|
||||
},
|
||||
editorData.imageAttachments.map { image -> GraphQLFile in
|
||||
.init(fieldName: attachmentFieldName, originalName: "image.jpg", data: image.imageData)
|
||||
inputBoxData.imageAttachments.map { image -> GraphQLFile in
|
||||
.init(
|
||||
fieldName: image.hashValue.description,
|
||||
originalName: "image.jpg",
|
||||
data: image.imageData
|
||||
)
|
||||
},
|
||||
].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
|
||||
content: .some(contextSnippet.isEmpty ? editorData.text : "\(contextSnippet)\n\(editorData.text)"),
|
||||
content: .some(content),
|
||||
params: .some(AffineGraphQL.JSON(_jsonValue: messageParameters)),
|
||||
sessionId: sessionId
|
||||
) else {
|
||||
report(sessionId, ChatError.unknownError)
|
||||
assertionFailure() // very unlikely to happen
|
||||
return
|
||||
}
|
||||
@@ -203,6 +83,11 @@ private extension ChatManager {
|
||||
self.report(sessionId, ChatError.invalidResponse)
|
||||
return
|
||||
}
|
||||
let viewModelId = self.append(sessionId: sessionId, AssistantMessageCellViewModel(
|
||||
id: .init(),
|
||||
content: .init(),
|
||||
timestamp: .init()
|
||||
))
|
||||
self.startStreamingResponse(
|
||||
sessionId: sessionId,
|
||||
messageId: messageIdentifier,
|
||||
@@ -214,10 +99,8 @@ private extension ChatManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ChatManager {
|
||||
func startStreamingResponse(sessionId: String, messageId: String, applyingTo vmId: UUID) {
|
||||
private func startStreamingResponse(sessionId: String, messageId: String, applyingTo vmId: UUID) {
|
||||
let base = IntelligentContext.shared.webViewMetadata[.currentServerBaseUrl] as? String
|
||||
guard let base, let url = URL(string: base) else {
|
||||
report(sessionId, ChatError.invalidServerConfiguration)
|
||||
@@ -281,11 +164,24 @@ private extension ChatManager {
|
||||
vmId: UUID
|
||||
) {
|
||||
let result = MarkdownParser().parse(document)
|
||||
let content = MarkdownTextView.PreprocessContent(parserResult: result, theme: .default)
|
||||
var renderedContexts: [String: RenderedItem] = [:]
|
||||
for (key, value) in result.mathContext {
|
||||
let image = MathRenderer.renderToImage(
|
||||
latex: value,
|
||||
fontSize: MarkdownTheme.default.fonts.body.pointSize,
|
||||
textColor: MarkdownTheme.default.colors.body
|
||||
)?.withRenderingMode(.alwaysTemplate)
|
||||
let renderedContext = RenderedItem(
|
||||
image: image,
|
||||
text: value
|
||||
)
|
||||
renderedContexts["math://\(key)"] = renderedContext
|
||||
}
|
||||
|
||||
with(sessionId: sessionId, vmId: vmId) { (viewModel: inout AssistantMessageCellViewModel) in
|
||||
viewModel.content = document
|
||||
viewModel.preprocessedContent = content
|
||||
viewModel.documentBlocks = result.document
|
||||
viewModel.documentRenderedContent = renderedContexts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-6
@@ -33,12 +33,6 @@ public class ChatManager: ObservableObject, @unchecked Sendable {
|
||||
closable.removeAll()
|
||||
}
|
||||
|
||||
public func clearAll() {
|
||||
assert(Thread.isMainThread)
|
||||
closeAll()
|
||||
viewModels.removeAll()
|
||||
}
|
||||
|
||||
public func with(sessionId: String, _ action: (inout OrderedDictionary<MessageID, any ChatCellViewModel>) -> Void) {
|
||||
if Thread.isMainThread {
|
||||
if var sessionViewModels = viewModels[sessionId] {
|
||||
@@ -65,6 +59,8 @@ public class ChatManager: ObservableObject, @unchecked Sendable {
|
||||
return
|
||||
}
|
||||
sessionViewModels[vmId] = vm
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -17,7 +17,6 @@ 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,7 +17,6 @@ extension IntelligentContext {
|
||||
(.currentWorkspaceId, .getCurrentWorkspaceId),
|
||||
(.currentServerBaseUrl, .getCurrentServerBaseUrl),
|
||||
(.currentI18nLocale, .getCurrentI18nLocale),
|
||||
(.currentAiButtonFeatureFlag, .getAiButtonFeatureFlag),
|
||||
]
|
||||
for (key, script) in keysAndScripts {
|
||||
DispatchQueue.main.async {
|
||||
|
||||
-9
@@ -40,7 +40,6 @@ public class IntelligentContext {
|
||||
case currentWorkspaceId
|
||||
case currentServerBaseUrl
|
||||
case currentI18nLocale
|
||||
case currentAiButtonFeatureFlag
|
||||
}
|
||||
|
||||
@Published public private(set) var currentSession: ChatSessionObject?
|
||||
@@ -54,7 +53,6 @@ public class IntelligentContext {
|
||||
public enum IntelligentError: Error, LocalizedError {
|
||||
case loginRequired(String)
|
||||
case sessionCreationFailed(String)
|
||||
case featureClosed
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
@@ -62,8 +60,6 @@ public class IntelligentContext {
|
||||
"Login required: \(reason)"
|
||||
case let .sessionCreationFailed(reason):
|
||||
"Session creation failed: \(reason)"
|
||||
case let .featureClosed:
|
||||
"Intelligent feature closed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,11 +81,6 @@ 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,
|
||||
|
||||
+5
-1
@@ -85,7 +85,11 @@ extension MainViewController: InputBoxDelegate {
|
||||
}
|
||||
|
||||
ChatManager.shared.closeAll()
|
||||
ChatManager.shared.startUserRequest(editorData: inputData, sessionId: currentSession.id)
|
||||
ChatManager.shared.startUserRequest(
|
||||
content: inputData.text,
|
||||
inputBoxData: inputData,
|
||||
sessionId: currentSession.id
|
||||
)
|
||||
}
|
||||
|
||||
private func showAlert(title: String, message: String) {
|
||||
|
||||
+13
-2
@@ -21,6 +21,11 @@ class AssistantMessageCell: ChatBaseCell {
|
||||
contentView.addSubview(markdownView)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
markdownView.prepareForReuse()
|
||||
}
|
||||
|
||||
override func configure(with viewModel: any ChatCellViewModel) {
|
||||
super.configure(with: viewModel)
|
||||
|
||||
@@ -28,7 +33,10 @@ class AssistantMessageCell: ChatBaseCell {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
markdownView.setMarkdown(vm.preprocessedContent)
|
||||
markdownView.setMarkdown(
|
||||
vm.documentBlocks,
|
||||
renderedContent: vm.documentRenderedContent
|
||||
)
|
||||
}
|
||||
|
||||
override func layoutContentView(bounds: CGRect) {
|
||||
@@ -45,7 +53,10 @@ class AssistantMessageCell: ChatBaseCell {
|
||||
markdownViewForSizeCalculation.frame = .init(
|
||||
x: 0, y: 0, width: width, height: .greatestFiniteMagnitude
|
||||
)
|
||||
markdownViewForSizeCalculation.setMarkdownManually(vm.preprocessedContent)
|
||||
markdownViewForSizeCalculation.setMarkdown(
|
||||
vm.documentBlocks,
|
||||
renderedContent: vm.documentRenderedContent
|
||||
)
|
||||
let boundingSize = markdownViewForSizeCalculation.boundingSize(for: width)
|
||||
return ceil(boundingSize.height)
|
||||
}
|
||||
|
||||
+18
-6
@@ -38,7 +38,8 @@ struct AssistantMessageCellViewModel: ChatCellViewModel {
|
||||
var citations: [CitationViewModel]?
|
||||
var actions: [MessageActionViewModel]?
|
||||
|
||||
var preprocessedContent: MarkdownTextView.PreprocessContent
|
||||
var documentBlocks: [MarkdownBlockNode]
|
||||
var documentRenderedContent: RenderContext
|
||||
|
||||
init(
|
||||
id: UUID,
|
||||
@@ -52,7 +53,7 @@ struct AssistantMessageCellViewModel: ChatCellViewModel {
|
||||
actions: [MessageActionViewModel]? = nil
|
||||
) {
|
||||
// time expensive rendering should not happen here
|
||||
assert(!Thread.isMainThread || content.count < 10) // allow placeholder content
|
||||
assert(!Thread.isMainThread || content.isEmpty)
|
||||
|
||||
self.id = id
|
||||
self.content = content
|
||||
@@ -66,10 +67,21 @@ struct AssistantMessageCellViewModel: ChatCellViewModel {
|
||||
|
||||
let parser = MarkdownParser()
|
||||
let parserResult = parser.parse(content)
|
||||
preprocessedContent = MarkdownTextView.PreprocessContent(
|
||||
parserResult: parserResult,
|
||||
theme: .default,
|
||||
)
|
||||
documentBlocks = parserResult.document
|
||||
var renderedContexts: [String: RenderedItem] = [:]
|
||||
for (key, value) in parserResult.mathContext {
|
||||
let image = MathRenderer.renderToImage(
|
||||
latex: value,
|
||||
fontSize: MarkdownTheme.default.fonts.body.pointSize,
|
||||
textColor: MarkdownTheme.default.colors.body
|
||||
)?.withRenderingMode(.alwaysTemplate)
|
||||
let renderedContext = RenderedItem(
|
||||
image: image,
|
||||
text: value
|
||||
)
|
||||
renderedContexts["math://\(key)"] = renderedContext
|
||||
}
|
||||
documentRenderedContent = renderedContexts
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ private let unselectedColor: UIColor = .affineIconPrimary
|
||||
private let selectedColor: UIColor = .affineIconActivated
|
||||
|
||||
private let configurableOptions: [ConfigurableOptions] = [
|
||||
// .networking,
|
||||
// .reasoning,
|
||||
.networking,
|
||||
.reasoning,
|
||||
]
|
||||
enum ConfigurableOptions {
|
||||
case tool
|
||||
|
||||
-16
@@ -22,20 +22,11 @@ class MainHeaderView: UIView {
|
||||
$0.textAlignment = .center
|
||||
}
|
||||
|
||||
private lazy var modelMenu = UIDeferredMenuElement.uncached { completion in
|
||||
completion([])
|
||||
}
|
||||
|
||||
private lazy var dropdownButton = UIButton(type: .system).then {
|
||||
$0.imageView?.contentMode = .scaleAspectFit
|
||||
$0.setImage(UIImage.affineArrowDown, for: .normal)
|
||||
$0.tintColor = UIColor.affineIconPrimary
|
||||
$0.addTarget(self, action: #selector(dropdownButtonTapped), for: .touchUpInside)
|
||||
$0.showsMenuAsPrimaryAction = true
|
||||
$0.menu = UIMenu(options: [.displayInline], children: [
|
||||
modelMenu,
|
||||
])
|
||||
$0.isHidden = true
|
||||
}
|
||||
|
||||
private lazy var centerStackView = UIStackView().then {
|
||||
@@ -54,13 +45,6 @@ class MainHeaderView: UIView {
|
||||
$0.layer.cornerRadius = 8
|
||||
$0.addTarget(self, action: #selector(menuButtonTapped), for: .touchUpInside)
|
||||
$0.setContentHuggingPriority(.required, for: .horizontal)
|
||||
$0.showsMenuAsPrimaryAction = true
|
||||
$0.menu = .init(options: [.displayInline], children: [
|
||||
UIAction(title: "Clear History", image: .affineBroom, handler: { _ in
|
||||
ChatManager.shared.clearCurrentSession()
|
||||
ChatManager.shared.clearAll()
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
private lazy var leftSpacerView = UIView()
|
||||
|
||||
@@ -45,13 +45,13 @@ EXTERNAL SOURCES:
|
||||
:path: "../../../../../node_modules/capacitor-plugin-app-tracking-transparency"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Capacitor: 106e7a4205f4618d582b886a975657c61179138d
|
||||
CapacitorApp: d63334c052278caf5d81585d80b21905c6f93f39
|
||||
CapacitorBrowser: 081852cf532acf77b9d2953f3a88fe5b9711fb06
|
||||
Capacitor: 03bc7cbdde6a629a8b910a9d7d78c3cc7ed09ea7
|
||||
CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9
|
||||
CapacitorBrowser: 6299776d496e968505464884d565992faa20444a
|
||||
CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f
|
||||
CapacitorHaptics: 70e47470fa1a6bd6338cd102552e3846b7f9a1b3
|
||||
CapacitorKeyboard: 969647d0ca2e5c737d7300088e2517aa832434e2
|
||||
CapacitorPluginAppTrackingTransparency: 2a2792623a5a72795f2e8f9ab3f1147573732fd8
|
||||
CapacitorHaptics: 1f1e17041f435d8ead9ff2a34edd592c6aa6a8d6
|
||||
CapacitorKeyboard: 09fd91dcde4f8a37313e7f11bde553ad1ed52036
|
||||
CapacitorPluginAppTrackingTransparency: 92ae9c1cfb5cf477753db9269689332a686f675a
|
||||
CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
|
||||
|
||||
PODFILE CHECKSUM: 2c1e4be82121f2d9724ecf7e31dd14e165aeb082
|
||||
|
||||
@@ -7,6 +7,7 @@ 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,
|
||||
@@ -17,7 +18,6 @@ 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,6 +62,7 @@ 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';
|
||||
@@ -161,6 +162,14 @@ 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;
|
||||
@@ -215,10 +224,6 @@ 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();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IntelligentsPlugin {
|
||||
presentIntelligentsButton(): Promise<void>;
|
||||
dismissIntelligentsButton(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { registerPlugin } from '@capacitor/core';
|
||||
|
||||
import type { IntelligentsPlugin } from './definitions';
|
||||
|
||||
const Intelligents = registerPlugin<IntelligentsPlugin>('Intelligents');
|
||||
|
||||
export * from './definitions';
|
||||
export { Intelligents };
|
||||
@@ -34,7 +34,6 @@
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.2",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.3",
|
||||
"@radix-ui/react-popover": "^1.1.3",
|
||||
|
||||
@@ -310,10 +310,7 @@ math {
|
||||
|
||||
/* AI Block Diff */
|
||||
.ai-block-diff-deleted {
|
||||
background-color: var(
|
||||
--affine-v2-aI-applyDeleteHighlight,
|
||||
#ffeaea
|
||||
) !important;
|
||||
background-color: var(--aI-applyDeleteHighlight, #ffeaea) !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 8px 0px !important;
|
||||
margin-bottom: 10px !important;
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import * as RadixContextMenu from '@radix-ui/react-context-menu';
|
||||
import clsx from 'clsx';
|
||||
import type { RefAttributes } from 'react';
|
||||
|
||||
import * as styles from '../styles.css';
|
||||
import { DesktopMenuContext } from './context';
|
||||
import * as desktopStyles from './styles.css';
|
||||
|
||||
export type ContextMenuProps = RadixContextMenu.ContextMenuProps &
|
||||
RadixContextMenu.ContextMenuTriggerProps &
|
||||
RefAttributes<HTMLSpanElement> & {
|
||||
items: React.ReactNode;
|
||||
contentProps?: RadixContextMenu.ContextMenuContentProps;
|
||||
};
|
||||
|
||||
const ContextMenuContextValue = {
|
||||
type: 'context-menu',
|
||||
} as const;
|
||||
|
||||
export const ContextMenu = ({
|
||||
children,
|
||||
onOpenChange,
|
||||
dir,
|
||||
modal,
|
||||
items,
|
||||
contentProps,
|
||||
...props
|
||||
}: ContextMenuProps) => {
|
||||
return (
|
||||
<DesktopMenuContext.Provider value={ContextMenuContextValue}>
|
||||
<RadixContextMenu.Root
|
||||
onOpenChange={onOpenChange}
|
||||
dir={dir}
|
||||
modal={modal}
|
||||
>
|
||||
<RadixContextMenu.Trigger {...props}>
|
||||
{children}
|
||||
</RadixContextMenu.Trigger>
|
||||
<RadixContextMenu.Portal>
|
||||
<RadixContextMenu.Content
|
||||
className={clsx(
|
||||
styles.menuContent,
|
||||
desktopStyles.contentAnimation,
|
||||
contentProps?.className
|
||||
)}
|
||||
style={{
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
...contentProps?.style,
|
||||
}}
|
||||
{...contentProps}
|
||||
>
|
||||
{items}
|
||||
</RadixContextMenu.Content>
|
||||
</RadixContextMenu.Portal>
|
||||
</RadixContextMenu.Root>
|
||||
</DesktopMenuContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
interface DesktopMenuContextValue {
|
||||
type: 'dropdown-menu' | 'context-menu';
|
||||
}
|
||||
|
||||
export const DesktopMenuContext = createContext<DesktopMenuContextValue>({
|
||||
type: 'dropdown-menu',
|
||||
});
|
||||
@@ -1,30 +1,13 @@
|
||||
import * as ContextMenu from '@radix-ui/react-context-menu';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import type { MenuItemProps } from '../menu.types';
|
||||
import { useMenuItem } from '../use-menu-item';
|
||||
import { DesktopMenuContext } from './context';
|
||||
|
||||
export const DesktopMenuItem = (props: MenuItemProps) => {
|
||||
const { type } = useContext(DesktopMenuContext);
|
||||
const { className, children, otherProps } = useMenuItem(props);
|
||||
|
||||
if (type === 'dropdown-menu') {
|
||||
return (
|
||||
<DropdownMenu.Item className={className} {...otherProps}>
|
||||
{children}
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'context-menu') {
|
||||
return (
|
||||
<ContextMenu.Item className={className} {...otherProps}>
|
||||
{children}
|
||||
</ContextMenu.Item>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return (
|
||||
<DropdownMenu.Item className={className} {...otherProps}>
|
||||
{children}
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,13 +4,8 @@ import React, { useCallback, useImperativeHandle, useState } from 'react';
|
||||
|
||||
import type { MenuProps } from '../menu.types';
|
||||
import * as styles from '../styles.css';
|
||||
import { DesktopMenuContext } from './context';
|
||||
import * as desktopStyles from './styles.css';
|
||||
|
||||
const MenuContextValue = {
|
||||
type: 'dropdown-menu',
|
||||
} as const;
|
||||
|
||||
export const DesktopMenu = ({
|
||||
children,
|
||||
items,
|
||||
@@ -58,39 +53,37 @@ export const DesktopMenu = ({
|
||||
|
||||
const ContentWrapper = noPortal ? React.Fragment : DropdownMenu.Portal;
|
||||
return (
|
||||
<DesktopMenuContext.Provider value={MenuContextValue}>
|
||||
<DropdownMenu.Root
|
||||
modal={modal ?? false}
|
||||
open={finalOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
{...rootOptions}
|
||||
<DropdownMenu.Root
|
||||
modal={modal ?? false}
|
||||
open={finalOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
{...rootOptions}
|
||||
>
|
||||
<DropdownMenu.Trigger
|
||||
asChild
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Trigger
|
||||
asChild
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DropdownMenu.Trigger>
|
||||
{children}
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<ContentWrapper {...portalOptions}>
|
||||
<DropdownMenu.Content
|
||||
className={clsx(
|
||||
styles.menuContent,
|
||||
desktopStyles.contentAnimation,
|
||||
className
|
||||
)}
|
||||
sideOffset={4}
|
||||
align="start"
|
||||
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
||||
{...otherContentOptions}
|
||||
>
|
||||
{items}
|
||||
</DropdownMenu.Content>
|
||||
</ContentWrapper>
|
||||
</DropdownMenu.Root>
|
||||
</DesktopMenuContext.Provider>
|
||||
<ContentWrapper {...portalOptions}>
|
||||
<DropdownMenu.Content
|
||||
className={clsx(
|
||||
styles.menuContent,
|
||||
desktopStyles.contentAnimation,
|
||||
className
|
||||
)}
|
||||
sideOffset={4}
|
||||
align="start"
|
||||
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
||||
{...otherContentOptions}
|
||||
>
|
||||
{items}
|
||||
</DropdownMenu.Content>
|
||||
</ContentWrapper>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
import * as ContextMenu from '@radix-ui/react-context-menu';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import clsx from 'clsx';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { MenuSubProps } from '../menu.types';
|
||||
import * as styles from '../styles.css';
|
||||
import { useMenuItem } from '../use-menu-item';
|
||||
import { DesktopMenuContext } from './context';
|
||||
|
||||
export const DesktopMenuSub = ({
|
||||
children: propsChildren,
|
||||
@@ -21,37 +19,12 @@ export const DesktopMenuSub = ({
|
||||
...otherSubContentOptions
|
||||
} = {},
|
||||
}: MenuSubProps) => {
|
||||
const { type } = useContext(DesktopMenuContext);
|
||||
const { className, children, otherProps } = useMenuItem({
|
||||
children: propsChildren,
|
||||
suffixIcon: <ArrowRightSmallIcon />,
|
||||
...triggerOptions,
|
||||
});
|
||||
|
||||
const contentClassName = useMemo(
|
||||
() => clsx(styles.menuContent, subContentClassName),
|
||||
[subContentClassName]
|
||||
);
|
||||
|
||||
if (type === 'context-menu') {
|
||||
return (
|
||||
<ContextMenu.Sub defaultOpen={defaultOpen} {...otherSubOptions}>
|
||||
<ContextMenu.SubTrigger className={className} {...otherProps}>
|
||||
{children}
|
||||
</ContextMenu.SubTrigger>
|
||||
<ContextMenu.Portal {...portalOptions}>
|
||||
<ContextMenu.SubContent
|
||||
className={contentClassName}
|
||||
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
||||
{...otherSubContentOptions}
|
||||
>
|
||||
{items}
|
||||
</ContextMenu.SubContent>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu.Sub>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu.Sub defaultOpen={defaultOpen} {...otherSubOptions}>
|
||||
<DropdownMenu.SubTrigger className={className} {...otherProps}>
|
||||
@@ -59,7 +32,10 @@ export const DesktopMenuSub = ({
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.Portal {...portalOptions}>
|
||||
<DropdownMenu.SubContent
|
||||
className={contentClassName}
|
||||
className={useMemo(
|
||||
() => clsx(styles.menuContent, subContentClassName),
|
||||
[subContentClassName]
|
||||
)}
|
||||
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
||||
{...otherSubContentOptions}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './menu.types';
|
||||
import { ContextMenu } from './desktop/context-menu';
|
||||
import { DesktopMenuItem } from './desktop/item';
|
||||
import { DesktopMenu } from './desktop/root';
|
||||
import { DesktopMenuSeparator } from './desktop/separator';
|
||||
@@ -20,7 +19,6 @@ const MenuSub = BUILD_CONFIG.isMobileEdition ? MobileMenuSub : DesktopMenuSub;
|
||||
const Menu = BUILD_CONFIG.isMobileEdition ? MobileMenu : DesktopMenu;
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
DesktopMenu,
|
||||
DesktopMenuItem,
|
||||
DesktopMenuSeparator,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createVar, fallbackVar, style } from '@vanilla-extract/css';
|
||||
import { createVar, style } from '@vanilla-extract/css';
|
||||
|
||||
export const topOffsetVar = createVar();
|
||||
export const bottomOffsetVar = createVar();
|
||||
@@ -9,13 +9,14 @@ export const safeArea = style({
|
||||
paddingTop: `calc(${topOffsetVar} + 12px)`,
|
||||
},
|
||||
'&[data-bottom]': {
|
||||
paddingBottom: `calc(${fallbackVar(bottomOffsetVar, '0px')} + 0px)`,
|
||||
paddingBottom: `calc(${bottomOffsetVar} + 0px)`,
|
||||
},
|
||||
'&[data-standalone][data-top]': {
|
||||
paddingTop: `calc(env(safe-area-inset-top, 12px) + ${topOffsetVar})`,
|
||||
},
|
||||
'&[data-standalone][data-bottom]': {
|
||||
paddingBottom: `calc(env(safe-area-inset-bottom, 0px) + ${fallbackVar(bottomOffsetVar, '0px')})`,
|
||||
// paddingBottom: 'env(safe-area-inset-bottom, 12px)',
|
||||
paddingBottom: `calc(env(safe-area-inset-bottom, 0px) + ${bottomOffsetVar})`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"@affine/graphql": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/nbstore": "workspace:*",
|
||||
"@affine/reader": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/track": "workspace:*",
|
||||
"@blocksuite/affine": "workspace:*",
|
||||
@@ -35,7 +34,6 @@
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@radix-ui/react-collapsible": "^1.1.2",
|
||||
"@radix-ui/react-context-menu": "^2.1.15",
|
||||
"@radix-ui/react-dialog": "^1.1.3",
|
||||
"@radix-ui/react-popover": "^1.1.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
@@ -84,7 +82,6 @@
|
||||
"react-transition-state": "^2.2.0",
|
||||
"react-virtuoso": "^4.12.3",
|
||||
"rxjs": "^7.8.1",
|
||||
"semver": "^7.7.2",
|
||||
"ses": "^1.10.0",
|
||||
"shiki": "^3.7.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
@@ -102,7 +99,6 @@
|
||||
"@types/bytes": "^3.1.5",
|
||||
"@types/image-blob-reduce": "^4.1.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/semver": "^7",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
+2
-4
@@ -39,8 +39,7 @@ describe('applyPatchToDoc', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// FIXME: markdown parse error in test mode
|
||||
it.skip('should replace a block', async () => {
|
||||
it('should replace a block', async () => {
|
||||
const host = affine`
|
||||
<affine-page id="page">
|
||||
<affine-note id="note">
|
||||
@@ -74,8 +73,7 @@ describe('applyPatchToDoc', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// FIXME: markdown parse error in test mode
|
||||
it.skip('should insert a block at index', async () => {
|
||||
it('should insert a block at index', async () => {
|
||||
const host = affine`
|
||||
<affine-page id="page">
|
||||
<affine-note id="note">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { AIDraftService } 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';
|
||||
@@ -116,9 +115,6 @@ export class ChatPanel extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
|
||||
@state()
|
||||
accessor session: CopilotChatHistoryFragment | null | undefined;
|
||||
|
||||
@@ -412,7 +408,6 @@ export class ChatPanel extends SignalWatcher(
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
|
||||
.onContextChange=${this.onContextChange}
|
||||
.width=${this.sidebarWidth}
|
||||
|
||||
-5
@@ -1,6 +1,5 @@
|
||||
import './ai-chat-composer-tip';
|
||||
|
||||
import type { AIDraftService } from '@affine/core/modules/ai-button';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type {
|
||||
ContextEmbedStatus,
|
||||
@@ -117,9 +116,6 @@ export class AIChatComposer extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
|
||||
@state()
|
||||
accessor chips: ChatChip[] = [];
|
||||
|
||||
@@ -165,7 +161,6 @@ export class AIChatComposer extends SignalWatcher(
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
.portalContainer=${this.portalContainer}
|
||||
.onChatSuccess=${this.onChatSuccess}
|
||||
.trackOptions=${this.trackOptions}
|
||||
|
||||
-31
@@ -1,5 +1,3 @@
|
||||
import type { AIDraftService } 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';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
@@ -17,7 +15,6 @@ import { property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const';
|
||||
import { type AIChatParams, AIProvider } from '../../provider/ai-provider';
|
||||
@@ -152,9 +149,6 @@ export class AIChatContent extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onEmbeddingProgressChange:
|
||||
| ((count: Record<ContextEmbedStatus, number>) => void)
|
||||
@@ -269,19 +263,6 @@ export class AIChatContent extends SignalWatcher(
|
||||
private readonly updateContext = (context: Partial<ChatContextValue>) => {
|
||||
this.chatContextValue = { ...this.chatContextValue, ...context };
|
||||
this.onContextChange?.(context);
|
||||
this.updateDraft(context).catch(console.error);
|
||||
};
|
||||
|
||||
private readonly updateDraft = async (context: Partial<ChatContextValue>) => {
|
||||
const draft: Partial<AIDraftState> = pick(context, [
|
||||
'quote',
|
||||
'images',
|
||||
'markdown',
|
||||
]);
|
||||
if (!Object.keys(draft).length) {
|
||||
return;
|
||||
}
|
||||
await this.aiDraftService.setDraft(draft);
|
||||
};
|
||||
|
||||
private readonly initChatContent = async () => {
|
||||
@@ -341,19 +322,8 @@ export class AIChatContent extends SignalWatcher(
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.initChatContent().catch(console.error);
|
||||
|
||||
this.aiDraftService
|
||||
.getDraft()
|
||||
.then(draft => {
|
||||
this.chatContextValue = {
|
||||
...this.chatContextValue,
|
||||
...draft,
|
||||
};
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
this._disposables.add(
|
||||
AIProvider.slots.actions.subscribe(({ event }) => {
|
||||
const { status } = this.chatContextValue;
|
||||
@@ -433,7 +403,6 @@ export class AIChatContent extends SignalWatcher(
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
.trackOptions=${{
|
||||
where: 'chat-panel',
|
||||
control: 'chat-send',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { AIDraftService } 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';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import { ArrowUpBigIcon, CloseIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, nothing, type PropertyValues } from 'lit';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
@@ -352,9 +351,6 @@ export class AIChatInput extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor searchMenuConfig!: SearchMenuConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor isRootSession: boolean = true;
|
||||
|
||||
@@ -383,7 +379,6 @@ export class AIChatInput extends SignalWatcher(
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this._disposables.add(
|
||||
AIProvider.slots.requestSendWithChat.subscribe(
|
||||
(params: AISendParams | null) => {
|
||||
@@ -404,17 +399,6 @@ 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);
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
const { images, status } = this.chatContextValue;
|
||||
const hasImages = images.length > 0;
|
||||
@@ -522,11 +506,9 @@ export class AIChatInput extends SignalWatcher(
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _handleInput = async () => {
|
||||
private readonly _handleInput = () => {
|
||||
const { textarea } = this;
|
||||
const value = textarea.value.trim();
|
||||
this.isInputEmpty = !value;
|
||||
|
||||
this.isInputEmpty = !textarea.value.trim();
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = textarea.scrollHeight + 'px';
|
||||
let imagesHeight = this.imagePreviewGrid?.scrollHeight ?? 0;
|
||||
@@ -535,10 +517,6 @@ export class AIChatInput extends SignalWatcher(
|
||||
textarea.style.height = '148px';
|
||||
textarea.style.overflowY = 'scroll';
|
||||
}
|
||||
|
||||
await this.aiDraftService.setDraft({
|
||||
input: value,
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _handleKeyDown = async (evt: KeyboardEvent) => {
|
||||
@@ -594,9 +572,6 @@ export class AIChatInput extends SignalWatcher(
|
||||
this.textarea.style.height = 'unset';
|
||||
|
||||
await this.send(value);
|
||||
await this.aiDraftService.setDraft({
|
||||
input: '',
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _handleModelChange = (modelId: string) => {
|
||||
|
||||
+2
-8
@@ -1,4 +1,4 @@
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import type { CopilotSessionType } from '@affine/graphql';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
@@ -73,10 +73,6 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
||||
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||
}
|
||||
|
||||
.ai-session-item[aria-selected='true'] .ai-session-title {
|
||||
color: ${unsafeCSSVarV2('text/emphasis')};
|
||||
}
|
||||
|
||||
.ai-session-doc:hover {
|
||||
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||
}
|
||||
@@ -123,7 +119,7 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor session!: CopilotChatHistoryFragment | null | undefined;
|
||||
accessor session!: CopilotSessionType | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor workspaceId!: string;
|
||||
@@ -260,8 +256,6 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
||||
e.stopPropagation();
|
||||
this.onSessionClick(session.sessionId);
|
||||
}}
|
||||
aria-selected=${this.session?.sessionId === session.sessionId}
|
||||
data-session-id=${session.sessionId}
|
||||
>
|
||||
<div class="ai-session-title">
|
||||
${session.title || 'New chat'}
|
||||
|
||||
@@ -2,7 +2,7 @@ import track from '@affine/track';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import { LoadingIcon } from '@blocksuite/affine-components/icons';
|
||||
import { AIStarIconWithAnimation } from '@blocksuite/affine-components/icons';
|
||||
import type { NotificationService } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
CloseIcon,
|
||||
@@ -83,7 +83,7 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: ${unsafeCSSVar('--affine-overlay-panel-shadow')};
|
||||
background: ${unsafeCSSVarV2('layer/background/overlayPanel')};
|
||||
box-shadow: ${unsafeCSSVar('shadow1')};
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
@@ -96,6 +96,7 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
|
||||
.doc-edit-tool-result-card-header-title {
|
||||
display: flex;
|
||||
@@ -117,17 +118,9 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) {
|
||||
padding-right: 8px;
|
||||
color: ${unsafeCSSVarV2('text/secondary')};
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: ${unsafeCSSVar('hoverColor')};
|
||||
span {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,27 +128,23 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) {
|
||||
.doc-edit-tool-result-card-content {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
border-top: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
}
|
||||
|
||||
.doc-edit-tool-result-card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
button {
|
||||
.doc-edit-tool-result-reject,
|
||||
.doc-edit-tool-result-accept {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: ${unsafeCSSVar('hoverColor')};
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,55 +421,55 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) {
|
||||
${docId}
|
||||
</div>
|
||||
<div class="doc-edit-tool-result-card-header-operations">
|
||||
<button @click=${() => this._toggleCollapse()}>
|
||||
${this.isCollapsed ? ExpandFullIcon() : ExpandCloseIcon()}
|
||||
<affine-tooltip>
|
||||
${this.isCollapsed ? 'Expand' : 'Collapse'}
|
||||
</affine-tooltip>
|
||||
</button>
|
||||
<button @click=${() => this._handleCopy(changedContent)}>
|
||||
<span @click=${() => this._toggleCollapse()}
|
||||
>${this.isCollapsed
|
||||
? ExpandFullIcon()
|
||||
: ExpandCloseIcon()}</span
|
||||
>
|
||||
<span @click=${() => this._handleCopy(changedContent)}>
|
||||
${CopyIcon()}
|
||||
<affine-tooltip>Copy</affine-tooltip>
|
||||
</button>
|
||||
</span>
|
||||
<button
|
||||
@click=${() => this._handleApply(op, updates)}
|
||||
?disabled=${this.isBusyForOp(op)}
|
||||
>
|
||||
${this.applyingMap[op]
|
||||
? html`${LoadingIcon()} Applying`
|
||||
: 'Apply'}
|
||||
? AIStarIconWithAnimation
|
||||
: html`Apply`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-edit-tool-result-card-content">
|
||||
${this.renderBlockDiffs(diffs)}
|
||||
<div class="doc-edit-tool-result-card-footer">
|
||||
<button
|
||||
class="doc-edit-tool-result-reject"
|
||||
@click=${() => this._handleReject(op)}
|
||||
>
|
||||
${CloseIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/secondary')}`,
|
||||
})}
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
class="doc-edit-tool-result-accept"
|
||||
@click=${() => this._handleAccept(op, updates)}
|
||||
?disabled=${this.isBusyForOp(op)}
|
||||
style="${this.isBusyForOp(op)
|
||||
? 'pointer-events: none; opacity: 0.6;'
|
||||
: ''}"
|
||||
>
|
||||
${this.acceptingMap[op]
|
||||
? html`${LoadingIcon()}`
|
||||
: DoneIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/activated')}`,
|
||||
})}
|
||||
${this.acceptingMap[op] ? 'Accepting...' : 'Accept'}
|
||||
</button>
|
||||
<div class="doc-edit-tool-result-card-content-title">
|
||||
${this.renderBlockDiffs(diffs)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-edit-tool-result-card-footer">
|
||||
<div
|
||||
class="doc-edit-tool-result-reject"
|
||||
@click=${() => this._handleReject(op)}
|
||||
>
|
||||
${CloseIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/secondary')}`,
|
||||
})}
|
||||
Reject
|
||||
</div>
|
||||
<button
|
||||
class="doc-edit-tool-result-accept"
|
||||
@click=${() => this._handleAccept(op, updates)}
|
||||
?disabled=${this.isBusyForOp(op)}
|
||||
style="${this.isBusyForOp(op)
|
||||
? 'pointer-events: none; opacity: 0.6;'
|
||||
: ''}"
|
||||
>
|
||||
${this.acceptingMap[op]
|
||||
? AIStarIconWithAnimation
|
||||
: DoneIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/activated')}`,
|
||||
})}
|
||||
${this.acceptingMap[op] ? 'Accepting...' : 'Accept'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
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,
|
||||
@@ -377,25 +381,24 @@ 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 spaceDoc = doc.doc.spaceDoc;
|
||||
const adapter = new MarkdownAdapter(job, cloned.provider());
|
||||
if (!snapshot) {
|
||||
throw new Error('Failed to get snapshot');
|
||||
return 'Failed to get markdown from doc';
|
||||
}
|
||||
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,
|
||||
// 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,
|
||||
});
|
||||
|
||||
return parsed.md;
|
||||
return markdown.file;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { WidgetComponent, WidgetViewExtension } from '@blocksuite/affine/std';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { css, html, nothing, type TemplateResult } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { BlockDiffProvider } from '../../services/block-diff';
|
||||
@@ -92,13 +91,11 @@ export class AffineBlockDiffWidgetForBlock extends WidgetComponent {
|
||||
}
|
||||
|
||||
private _renderInsert(from: string, blocks: Block[]) {
|
||||
return html`${repeat(
|
||||
blocks,
|
||||
block => block.id,
|
||||
(block, offset) => {
|
||||
return blocks
|
||||
.map((block, offset) => {
|
||||
const diffId = `insert-${block.id}-${offset}`;
|
||||
return this.diffService.isRejected('insert', `${from}:${offset}`)
|
||||
? nothing
|
||||
? null
|
||||
: html`<div class="ai-block-diff insert" data-diff-id=${diffId}>
|
||||
<chat-content-rich-text
|
||||
.text=${block.content}
|
||||
@@ -123,8 +120,8 @@ export class AffineBlockDiffWidgetForBlock extends WidgetComponent {
|
||||
})}
|
||||
></ai-block-diff-options>
|
||||
</div>`;
|
||||
}
|
||||
)}`;
|
||||
})
|
||||
.filter(Boolean) as TemplateResult[];
|
||||
}
|
||||
|
||||
private _renderUpdate(blockId: string, content: string) {
|
||||
@@ -192,12 +189,11 @@ export class AffineBlockDiffWidgetForBlock extends WidgetComponent {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const diffMap = service.getDiff();
|
||||
const { deletes, inserts, updates } = diffMap;
|
||||
const { deletes, inserts, updates } = service.getDiff();
|
||||
|
||||
let deleteDiff: TemplateResult | symbol = nothing;
|
||||
let updateDiff: TemplateResult | symbol = nothing;
|
||||
let insertDiff: TemplateResult | symbol = nothing;
|
||||
let insertDiff: TemplateResult[] | symbol = nothing;
|
||||
|
||||
if (deletes.includes(attached)) {
|
||||
deleteDiff = this._renderDelete(attached);
|
||||
|
||||
@@ -9,40 +9,20 @@ import * as styles from './styles.css';
|
||||
export const BlocksuiteEditorJournalDocTitle = ({ page }: { page: Store }) => {
|
||||
const journalService = useService(JournalService);
|
||||
const journalDateStr = useLiveData(journalService.journalDate$(page.id));
|
||||
|
||||
return <BlocksuiteEditorJournalDocTitleUI date={journalDateStr} />;
|
||||
};
|
||||
|
||||
export const BlocksuiteEditorJournalDocTitleUI = ({
|
||||
date: dateStr,
|
||||
overrideClassName,
|
||||
}: {
|
||||
date?: string;
|
||||
/**
|
||||
* The `doc-title-container` class style is defined in editor,
|
||||
* which means if we use this component outside editor, the style will not work,
|
||||
* so we provide a className to override
|
||||
*/
|
||||
overrideClassName?: string;
|
||||
}) => {
|
||||
const localizedJournalDate = i18nTime(dateStr, {
|
||||
const journalDate = journalDateStr ? dayjs(journalDateStr) : null;
|
||||
const isTodayJournal = useLiveData(journalService.journalToday$(page.id));
|
||||
const localizedJournalDate = i18nTime(journalDateStr, {
|
||||
absolute: { accuracy: 'day' },
|
||||
});
|
||||
const t = useI18n();
|
||||
|
||||
// TODO(catsjuice): i18n
|
||||
const today = dayjs();
|
||||
const date = dayjs(dateStr);
|
||||
const day = dayjs(date).format('dddd') ?? null;
|
||||
const isToday = date.isSame(today, 'day');
|
||||
const day = journalDate?.format('dddd') ?? null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={overrideClassName ?? 'doc-title-container'}
|
||||
data-testid="journal-title"
|
||||
>
|
||||
<div className="doc-title-container" data-testid="journal-title">
|
||||
<span data-testid="date">{localizedJournalDate}</span>
|
||||
{isToday ? (
|
||||
{isTodayJournal ? (
|
||||
<span className={styles.titleTodayTag} data-testid="date-today-label">
|
||||
{t['com.affine.today']()}
|
||||
</span>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import type { WeekDatePickerHandle } from '@affine/component';
|
||||
import { WeekDatePicker } from '@affine/component';
|
||||
import {
|
||||
JOURNAL_DATE_FORMAT,
|
||||
JournalService,
|
||||
} from '@affine/core/modules/journal';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export interface JournalWeekDatePickerProps {
|
||||
page: Store;
|
||||
@@ -20,29 +17,17 @@ export const JournalWeekDatePicker = ({ page }: JournalWeekDatePickerProps) => {
|
||||
const journalService = useService(JournalService);
|
||||
const journalDateStr = useLiveData(journalService.journalDate$(page.id));
|
||||
const journalDate = journalDateStr ? dayjs(journalDateStr) : null;
|
||||
const { openJournal } = useJournalRouteHelper();
|
||||
const [date, setDate] = useState(
|
||||
(journalDate ?? dayjs()).format(JOURNAL_DATE_FORMAT)
|
||||
(journalDate ?? dayjs()).format('YYYY-MM-DD')
|
||||
);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
useEffect(() => {
|
||||
if (!journalDate) return;
|
||||
setDate(journalDate.format(JOURNAL_DATE_FORMAT));
|
||||
setDate(journalDate.format('YYYY-MM-DD'));
|
||||
handleRef.current?.setCursor?.(journalDate);
|
||||
}, [journalDate]);
|
||||
|
||||
const openJournal = useCallback(
|
||||
(date: string) => {
|
||||
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' });
|
||||
}
|
||||
},
|
||||
[journalService, workbench]
|
||||
);
|
||||
|
||||
return (
|
||||
<WeekDatePicker
|
||||
data-testid="journal-week-picker"
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
defaultImageProxyMiddleware,
|
||||
embedSyncedDocMiddleware,
|
||||
MarkdownAdapter,
|
||||
MixTextAdapter,
|
||||
pasteMiddleware,
|
||||
PlainTextAdapter,
|
||||
titleMiddleware,
|
||||
@@ -145,7 +146,7 @@ export const markdownToSnapshot = async (
|
||||
? [defaultImageProxyMiddleware, pasteMiddleware(host.std)]
|
||||
: [defaultImageProxyMiddleware];
|
||||
const transformer = store.getTransformer(middlewares);
|
||||
const markdownAdapter = new MarkdownAdapter(transformer, store.provider);
|
||||
const markdownAdapter = new MixTextAdapter(transformer, store.provider);
|
||||
const payload = {
|
||||
file: markdown,
|
||||
assets: transformer.assetsManager,
|
||||
@@ -153,31 +154,10 @@ export const markdownToSnapshot = async (
|
||||
pageId: store.id,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
const snapshot = await markdownAdapter.toSliceSnapshot(payload);
|
||||
|
||||
return {
|
||||
snapshot: null,
|
||||
snapshot,
|
||||
transformer,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -514,7 +514,7 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
|
||||
return;
|
||||
}, [doc, onChange, snapshotHelper]);
|
||||
|
||||
// Add keydown handler to commit on Enter key
|
||||
// Add keydown handler to commit on CMD/CTRL + Enter key
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (readonly) return;
|
||||
@@ -523,8 +523,8 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
|
||||
const activeElement = document.activeElement;
|
||||
if (!editorRef.current?.contains(activeElement)) return;
|
||||
|
||||
// If Enter is pressed without CMD/CTRL key, commit the comment
|
||||
if (e.key === 'Enter' && !(e.metaKey || e.ctrlKey)) {
|
||||
// If Enter is pressed with CMD/CTRL key, commit the comment
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCommit();
|
||||
|
||||
@@ -157,7 +157,6 @@ const ActionMenu = ({
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
className={styles.actionButton}
|
||||
variant="solid"
|
||||
icon={<MoreHorizontalIcon />}
|
||||
disabled={disabled}
|
||||
@@ -492,7 +491,6 @@ const CommentItem = ({
|
||||
>
|
||||
{canResolveComment && (
|
||||
<IconButton
|
||||
className={styles.actionButton}
|
||||
variant="solid"
|
||||
onClick={handleResolve}
|
||||
icon={<DoneIcon />}
|
||||
|
||||
@@ -163,10 +163,6 @@ export const commentActions = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const actionButton = style({
|
||||
backgroundColor: cssVarV2('button/buttonOverHover'),
|
||||
});
|
||||
|
||||
export const readonlyCommentContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Checkbox,
|
||||
ContextMenu,
|
||||
DragHandle as DragHandleIcon,
|
||||
Tooltip,
|
||||
useDraggable,
|
||||
@@ -30,7 +29,7 @@ import { PagePreview } from '../../page-list/page-content-preview';
|
||||
import { DocExplorerContext } from '../context';
|
||||
import { quickActions } from '../quick-actions.constants';
|
||||
import * as styles from './doc-list-item.css';
|
||||
import { MoreMenuButton, MoreMenuContent } from './more-menu';
|
||||
import { MoreMenuButton } from './more-menu';
|
||||
import { CardViewProperties, ListViewProperties } from './properties';
|
||||
|
||||
export type DocListItemView = 'list' | 'grid' | 'masonry';
|
||||
@@ -315,46 +314,38 @@ export const ListViewDoc = ({ docId }: DocListItemProps) => {
|
||||
const t = useI18n();
|
||||
const docsService = useService(DocsService);
|
||||
const doc = useLiveData(docsService.list.doc$(docId));
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const showMoreOperation = useLiveData(contextValue.showMoreOperation$);
|
||||
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
asChild
|
||||
disabled={!showMoreOperation}
|
||||
items={<MoreMenuContent docId={docId} />}
|
||||
>
|
||||
<li className={styles.listViewRoot}>
|
||||
<DragHandle id={docId} className={styles.listDragHandle} />
|
||||
<Select id={docId} className={styles.listSelect} />
|
||||
<DocIcon id={docId} className={styles.listIcon} />
|
||||
<div className={styles.listBrief}>
|
||||
<DocTitle
|
||||
id={docId}
|
||||
className={styles.listTitle}
|
||||
data-testid="doc-list-item-title"
|
||||
/>
|
||||
<DocPreview id={docId} className={styles.listPreview} />
|
||||
</div>
|
||||
<div className={styles.listSpace} />
|
||||
<ListViewProperties docId={docId} />
|
||||
{quickActions.map(action => {
|
||||
return (
|
||||
<Tooltip key={action.key} content={t.t(action.name)}>
|
||||
<action.Component doc={doc} />
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
<MoreMenuButton
|
||||
docId={docId}
|
||||
contentOptions={listMoreMenuContentOptions}
|
||||
<li className={styles.listViewRoot}>
|
||||
<DragHandle id={docId} className={styles.listDragHandle} />
|
||||
<Select id={docId} className={styles.listSelect} />
|
||||
<DocIcon id={docId} className={styles.listIcon} />
|
||||
<div className={styles.listBrief}>
|
||||
<DocTitle
|
||||
id={docId}
|
||||
className={styles.listTitle}
|
||||
data-testid="doc-list-item-title"
|
||||
/>
|
||||
</li>
|
||||
</ContextMenu>
|
||||
<DocPreview id={docId} className={styles.listPreview} />
|
||||
</div>
|
||||
<div className={styles.listSpace} />
|
||||
<ListViewProperties docId={docId} />
|
||||
{quickActions.map(action => {
|
||||
return (
|
||||
<Tooltip key={action.key} content={t.t(action.name)}>
|
||||
<action.Component doc={doc} />
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
<MoreMenuButton
|
||||
docId={docId}
|
||||
contentOptions={listMoreMenuContentOptions}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
import type { Server } from '@affine/core/modules/cloud';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import semver from 'semver';
|
||||
|
||||
const rules = [
|
||||
{
|
||||
min: '0.23.0',
|
||||
tip: (receivedVersion: string, requiredVersion: string) => (
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
color: cssVarV2('status/error'),
|
||||
fontSize: 14,
|
||||
lineHeight: '22px',
|
||||
}}
|
||||
>
|
||||
Your server version{' '}
|
||||
<b style={{ fontWeight: 600 }}>{receivedVersion}</b> is not compatible
|
||||
with current client. Please upgrade your server to{' '}
|
||||
<b style={{ fontWeight: 600 }}>{requiredVersion}</b> or higher to use
|
||||
this client.
|
||||
</p>
|
||||
<div style={{ marginTop: '12px', color: cssVarV2.text.primary }}>
|
||||
<span style={{ fontWeight: 500 }}>Instructions:</span>
|
||||
<br />
|
||||
<a
|
||||
style={{
|
||||
whiteSpace: 'break-spaces',
|
||||
wordBreak: 'break-all',
|
||||
fontSize: 12,
|
||||
lineHeight: '16px',
|
||||
}}
|
||||
>
|
||||
https://docs.affine.pro/self-host-affine/install/upgrade
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Return the error tip if the server version is not meet the requirement
|
||||
*/
|
||||
export const useSelfhostLoginVersionGuard = (server: Server) => {
|
||||
const serverVersion =
|
||||
useLiveData(server.config$.selector(c => c.version)) ?? '0.0.0';
|
||||
|
||||
for (const rule of rules) {
|
||||
if (semver.lt(serverVersion, rule.min)) {
|
||||
return rule.tip(serverVersion, rule.min);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -10,9 +10,8 @@ import {
|
||||
SidebarScrollableContainer,
|
||||
} from '@affine/core/modules/app-sidebar/views';
|
||||
import { ExternalMenuLinkItem } from '@affine/core/modules/app-sidebar/views/menu-item/external-menu-link-item';
|
||||
import { AuthService, ServerService } from '@affine/core/modules/cloud';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk';
|
||||
import type { Workspace } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -89,11 +88,6 @@ const AllDocsButton = () => {
|
||||
};
|
||||
|
||||
const AIChatButton = () => {
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const serverService = useService(ServerService);
|
||||
const serverFeatures = useLiveData(serverService.server.features$);
|
||||
const enableAI = useLiveData(featureFlagService.flags.enable_ai.$);
|
||||
|
||||
const { workbenchService } = useServices({
|
||||
WorkbenchService,
|
||||
});
|
||||
@@ -102,10 +96,6 @@ const AIChatButton = () => {
|
||||
workbench.location$.selector(location => location.pathname === '/chat')
|
||||
);
|
||||
|
||||
if (!enableAI || !serverFeatures?.copilot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuLinkItem icon={<AiOutlineIcon />} active={aiChatActive} to={'/chat'}>
|
||||
<span data-testid="ai-chat">Intelligence</span>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const AppSidebarJournalButton = () => {
|
||||
return (
|
||||
<MenuLinkItem
|
||||
data-testid="slider-bar-journals-button"
|
||||
active={isJournal || location.pathname.startsWith('/journals')}
|
||||
active={isJournal}
|
||||
to={'/journals'}
|
||||
icon={<Icon />}
|
||||
>
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useSelfhostLoginVersionGuard } from '../hooks/affine/use-selfhost-login-version-guard';
|
||||
import type { SignInState } from '.';
|
||||
import { Back } from './back';
|
||||
import * as style from './style.css';
|
||||
@@ -55,7 +54,6 @@ export const SignInStep = ({
|
||||
const serverName = useLiveData(
|
||||
serverService.server.config$.selector(c => c.serverName)
|
||||
);
|
||||
const versionError = useSelfhostLoginVersionGuard(serverService.server);
|
||||
const isSelfhosted = useLiveData(
|
||||
serverService.server.config$.selector(
|
||||
c => c.type === ServerDeploymentType.Selfhosted
|
||||
@@ -127,20 +125,6 @@ export const SignInStep = ({
|
||||
}));
|
||||
}, [changeState]);
|
||||
|
||||
if (versionError && isSelfhosted) {
|
||||
return (
|
||||
<AuthContainer>
|
||||
<AuthHeader
|
||||
title={t['com.affine.auth.sign.in']()}
|
||||
subTitle={serverName}
|
||||
/>
|
||||
<AuthContent>
|
||||
<div>{versionError}</div>
|
||||
</AuthContent>
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContainer>
|
||||
<AuthHeader
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
ContextMenu,
|
||||
DropIndicator,
|
||||
type DropTargetDropEvent,
|
||||
type DropTargetOptions,
|
||||
@@ -467,54 +466,47 @@ export const NavigationPanelTreeNode = ({
|
||||
ref={rootRef}
|
||||
{...otherProps}
|
||||
>
|
||||
<ContextMenu
|
||||
asChild
|
||||
items={menuOperations.map(({ view, index }) => (
|
||||
<Fragment key={index}>{view}</Fragment>
|
||||
))}
|
||||
<div
|
||||
className={clsx(styles.contentContainer, styles.draggedOverEffect)}
|
||||
data-open={!collapsed}
|
||||
data-self-dragged-over={isSelfDraggedOver}
|
||||
ref={dropTargetRef}
|
||||
>
|
||||
<div
|
||||
className={clsx(styles.contentContainer, styles.draggedOverEffect)}
|
||||
data-open={!collapsed}
|
||||
data-self-dragged-over={isSelfDraggedOver}
|
||||
ref={dropTargetRef}
|
||||
>
|
||||
{to ? (
|
||||
<LinkComponent
|
||||
to={to}
|
||||
className={styles.linkItemRoot}
|
||||
ref={dragRef}
|
||||
draggable={false}
|
||||
>
|
||||
{content}
|
||||
</LinkComponent>
|
||||
) : (
|
||||
<div ref={dragRef}>{content}</div>
|
||||
{to ? (
|
||||
<LinkComponent
|
||||
to={to}
|
||||
className={styles.linkItemRoot}
|
||||
ref={dragRef}
|
||||
draggable={false}
|
||||
>
|
||||
{content}
|
||||
</LinkComponent>
|
||||
) : (
|
||||
<div ref={dragRef}>{content}</div>
|
||||
)}
|
||||
<CustomDragPreview>
|
||||
<div className={styles.draggingContainer}>{content}</div>
|
||||
</CustomDragPreview>
|
||||
{treeInstruction &&
|
||||
// Do not show drop indicator for self dragged over
|
||||
!(treeInstruction.type !== 'reparent' && isSelfDraggedOver) &&
|
||||
treeInstruction.type !== 'instruction-blocked' && (
|
||||
<DropIndicator instruction={treeInstruction} />
|
||||
)}
|
||||
<CustomDragPreview>
|
||||
<div className={styles.draggingContainer}>{content}</div>
|
||||
</CustomDragPreview>
|
||||
{treeInstruction &&
|
||||
// Do not show drop indicator for self dragged over
|
||||
!(treeInstruction.type !== 'reparent' && isSelfDraggedOver) &&
|
||||
treeInstruction.type !== 'instruction-blocked' && (
|
||||
<DropIndicator instruction={treeInstruction} />
|
||||
)}
|
||||
{draggedOver &&
|
||||
dropEffect &&
|
||||
draggedOverPosition &&
|
||||
!isSelfDraggedOver &&
|
||||
draggedOverDraggable && (
|
||||
<DropEffect
|
||||
dropEffect={dropEffect({
|
||||
source: draggedOverDraggable,
|
||||
treeInstruction: treeInstruction,
|
||||
})}
|
||||
position={draggedOverPosition}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ContextMenu>
|
||||
{draggedOver &&
|
||||
dropEffect &&
|
||||
draggedOverPosition &&
|
||||
!isSelfDraggedOver &&
|
||||
draggedOverDraggable && (
|
||||
<DropEffect
|
||||
dropEffect={dropEffect({
|
||||
source: draggedOverDraggable,
|
||||
treeInstruction: treeInstruction,
|
||||
})}
|
||||
position={draggedOverPosition}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Collapsible.Content style={{ display: dragging ? 'none' : undefined }}>
|
||||
{/* For lastInGroup check, the placeholder must be placed above all children in the dom */}
|
||||
<div className={styles.collapseContentPlaceholder}>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Scrollable } from '@affine/component';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { UserPlanButton } from '@affine/core/components/affine/auth/user-plan-button';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
@@ -226,18 +225,13 @@ export const SettingSidebar = ({
|
||||
</Suspense>
|
||||
) : null}
|
||||
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Viewport>
|
||||
{groups.map(group => (
|
||||
<SettingSidebarGroup
|
||||
key={group.key}
|
||||
title={group.title}
|
||||
items={group.items}
|
||||
/>
|
||||
))}
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Viewport>
|
||||
</Scrollable.Root>
|
||||
{groups.map(group => (
|
||||
<SettingSidebarGroup
|
||||
key={group.key}
|
||||
title={group.title}
|
||||
items={group.items}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ export const settingSlideBar = style({
|
||||
width: '25%',
|
||||
maxWidth: '242px',
|
||||
background: cssVar('backgroundSecondaryColor'),
|
||||
padding: '20px 0px 0px 12px',
|
||||
padding: '20px 12px',
|
||||
height: '100%',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
@@ -123,7 +123,6 @@ export const sidebarGroup = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
paddingRight: '12px',
|
||||
});
|
||||
|
||||
export const accountButton = style({
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// this route page acts as a redirector to today's journal
|
||||
export const Component = () => {
|
||||
const { openToday } = useJournalRouteHelper();
|
||||
useEffect(() => {
|
||||
openToday({ replaceHistory: true });
|
||||
}, [openToday]);
|
||||
return null;
|
||||
};
|
||||
@@ -11,7 +11,6 @@ import { getViewManager } from '@affine/core/blocksuite/manager/view';
|
||||
import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service';
|
||||
import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
|
||||
import { useAISpecs } from '@affine/core/components/hooks/affine/use-ai-specs';
|
||||
import { AIDraftService } from '@affine/core/modules/ai-button';
|
||||
import {
|
||||
EventSourceService,
|
||||
FetchService,
|
||||
@@ -222,7 +221,6 @@ export const Component = () => {
|
||||
confirmModal.closeConfirmModal,
|
||||
confirmModal.openConfirmModal
|
||||
);
|
||||
content.aiDraftService = framework.get(AIDraftService);
|
||||
content.createSession = createSession;
|
||||
content.onOpenDoc = onOpenDoc;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-
|
||||
import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service';
|
||||
import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
|
||||
import { useAISpecs } from '@affine/core/components/hooks/affine/use-ai-specs';
|
||||
import { AIDraftService } from '@affine/core/modules/ai-button';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { AppThemeService } from '@affine/core/modules/theme';
|
||||
@@ -96,7 +95,6 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||
confirmModal.closeConfirmModal,
|
||||
confirmModal.openConfirmModal
|
||||
);
|
||||
chatPanelRef.current.aiDraftService = framework.get(AIDraftService);
|
||||
|
||||
containerRef.current?.append(chatPanelRef.current);
|
||||
} else {
|
||||
|
||||
+3
-17
@@ -9,6 +9,7 @@ import {
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { Guard } from '@affine/core/components/guard';
|
||||
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { MoveToTrash } from '@affine/core/components/page-list';
|
||||
import {
|
||||
type DocRecord,
|
||||
@@ -17,10 +18,7 @@ import {
|
||||
} from '@affine/core/modules/doc';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import {
|
||||
WorkbenchLink,
|
||||
WorkbenchService,
|
||||
} from '@affine/core/modules/workbench';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { CalendarXmarkIcon, EditIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
@@ -105,25 +103,13 @@ const mobile = environment.isMobile;
|
||||
export const EditorJournalPanel = () => {
|
||||
const t = useI18n();
|
||||
const doc = useServiceOptional(DocService)?.doc;
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const journalService = useService(JournalService);
|
||||
const journalDateStr = useLiveData(
|
||||
doc ? journalService.journalDate$(doc.id) : null
|
||||
);
|
||||
const journalDate = journalDateStr ? dayjs(journalDateStr) : null;
|
||||
const isJournal = !!journalDate;
|
||||
|
||||
const openJournal = useCallback(
|
||||
(date: string) => {
|
||||
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' });
|
||||
}
|
||||
},
|
||||
[journalService, workbench]
|
||||
);
|
||||
const { openJournal } = useJournalRouteHelper();
|
||||
|
||||
const onDateSelect = useCallback(
|
||||
(date: string) => {
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const header = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
padding: '0 36px',
|
||||
});
|
||||
|
||||
export const todayButton = style({
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
});
|
||||
|
||||
export const body = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderTop: `0.5px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||
selectors: {
|
||||
'&[data-mobile]': {
|
||||
borderTop: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
maxWidth: 944,
|
||||
padding: '0px 50px',
|
||||
margin: '0 auto',
|
||||
selectors: {
|
||||
'[data-mobile] &': {
|
||||
padding: '0 24px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const docTitleContainer = style({
|
||||
color: cssVarV2.text.primary,
|
||||
fontSize: 40,
|
||||
lineHeight: '50px',
|
||||
fontWeight: 700,
|
||||
padding: '38px 0',
|
||||
});
|
||||
|
||||
export const placeholder = style({
|
||||
height: 200,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: `1px dashed ${cssVarV2.layer.insideBorder.border}`,
|
||||
borderRadius: 8,
|
||||
});
|
||||
|
||||
export const placeholderIcon = style({
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 36,
|
||||
backgroundColor: cssVarV2.button.emptyIconBackground,
|
||||
color: cssVarV2.icon.primary,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 20,
|
||||
marginBottom: 4,
|
||||
});
|
||||
|
||||
export const placeholderText = style({
|
||||
fontSize: 14,
|
||||
lineHeight: '22px',
|
||||
marginBottom: 16,
|
||||
color: cssVarV2.text.tertiary,
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user