mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 16:44:56 +00:00
Compare commits
19 Commits
v0.23.2
...
refactor/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69e23e6a42 | ||
|
|
f7a094053e | ||
|
|
091bac1047 | ||
|
|
bd161c54b2 | ||
|
|
61d2382643 | ||
|
|
4586e4a18f | ||
|
|
30c42fc51b | ||
|
|
627771948f | ||
|
|
0e3691e54e | ||
|
|
8fd0d5c1e8 | ||
|
|
13763e80bb | ||
|
|
6a1b53dd11 | ||
|
|
9899fad000 | ||
|
|
be55442f38 | ||
|
|
1dd4bbbaba | ||
|
|
7409940cc6 | ||
|
|
0d43350afd | ||
|
|
ff9a4f4322 | ||
|
|
8cfaee8232 |
2
.github/workflows/windows-signer.yml
vendored
2
.github/workflows/windows-signer.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
shell: cmd
|
||||
run: |
|
||||
cd ${{ env.ARCHIVE_DIR }}/out
|
||||
signtool sign /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a ${{ inputs.files }}
|
||||
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
|
||||
- name: zip file
|
||||
shell: cmd
|
||||
run: |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ImageBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
ActionPlacement,
|
||||
blockCommentToolbarButton,
|
||||
type ToolbarModuleConfig,
|
||||
ToolbarModuleExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
@@ -49,6 +50,10 @@ const builtinToolbarConfig = {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.comment',
|
||||
...blockCommentToolbarButton,
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'a.clipboard',
|
||||
|
||||
@@ -634,9 +634,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
|
||||
const movedElements = new Set([
|
||||
...selectedElements,
|
||||
...selectedElements
|
||||
.map(el => (isGfxGroupCompatibleModel(el) ? el.descendantElements : []))
|
||||
.flat(),
|
||||
...selectedElements.flatMap(el =>
|
||||
isGfxGroupCompatibleModel(el) ? el.descendantElements : []
|
||||
),
|
||||
]);
|
||||
|
||||
movedElements.forEach(element => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import {
|
||||
ActionPlacement,
|
||||
blockCommentToolbarButton,
|
||||
type ToolbarModuleConfig,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
|
||||
@@ -61,6 +62,10 @@ export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
|
||||
surfaceRefBlock.captionElement.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'e.comment',
|
||||
...blockCommentToolbarButton,
|
||||
},
|
||||
{
|
||||
id: 'a.clipboard',
|
||||
placement: ActionPlacement.More,
|
||||
|
||||
@@ -68,5 +68,5 @@ export function getHeadingBlocksFromDoc(
|
||||
ignoreEmpty = false
|
||||
) {
|
||||
const notes = getNotesFromStore(store, modes);
|
||||
return notes.map(note => getHeadingBlocksFromNote(note, ignoreEmpty)).flat();
|
||||
return notes.flatMap(note => getHeadingBlocksFromNote(note, ignoreEmpty));
|
||||
}
|
||||
|
||||
@@ -103,54 +103,52 @@ export class InlineCommentManager extends LifeCycleWatcher {
|
||||
id: CommentId,
|
||||
selections: BaseSelection[]
|
||||
) => {
|
||||
const needCommentTexts = selections
|
||||
.map(selection => {
|
||||
if (!selection.is(TextSelection)) return [];
|
||||
const [_, { selectedBlocks }] = this.std.command
|
||||
.chain()
|
||||
.pipe(getSelectedBlocksCommand, {
|
||||
textSelection: selection,
|
||||
})
|
||||
.run();
|
||||
const needCommentTexts = selections.flatMap(selection => {
|
||||
if (!selection.is(TextSelection)) return [];
|
||||
const [_, { selectedBlocks }] = this.std.command
|
||||
.chain()
|
||||
.pipe(getSelectedBlocksCommand, {
|
||||
textSelection: selection,
|
||||
})
|
||||
.run();
|
||||
|
||||
if (!selectedBlocks) return [];
|
||||
if (!selectedBlocks) return [];
|
||||
|
||||
type MakeRequired<T, K extends keyof T> = T & {
|
||||
[key in K]: NonNullable<T[key]>;
|
||||
};
|
||||
type MakeRequired<T, K extends keyof T> = T & {
|
||||
[key in K]: NonNullable<T[key]>;
|
||||
};
|
||||
|
||||
return selectedBlocks
|
||||
.map(
|
||||
({ model }) =>
|
||||
[model, getInlineEditorByModel(this.std, model)] as const
|
||||
)
|
||||
.filter(
|
||||
(
|
||||
pair
|
||||
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
|
||||
!!pair[0].text && !!pair[1]
|
||||
)
|
||||
.map(([model, inlineEditor]) => {
|
||||
let from: TextRangePoint;
|
||||
let to: TextRangePoint | null;
|
||||
if (model.id === selection.from.blockId) {
|
||||
from = selection.from;
|
||||
to = null;
|
||||
} else if (model.id === selection.to?.blockId) {
|
||||
from = selection.to;
|
||||
to = null;
|
||||
} else {
|
||||
from = {
|
||||
blockId: model.id,
|
||||
index: 0,
|
||||
length: model.text.yText.length,
|
||||
};
|
||||
to = null;
|
||||
}
|
||||
return [new TextSelection({ from, to }), inlineEditor] as const;
|
||||
});
|
||||
})
|
||||
.flat();
|
||||
return selectedBlocks
|
||||
.map(
|
||||
({ model }) =>
|
||||
[model, getInlineEditorByModel(this.std, model)] as const
|
||||
)
|
||||
.filter(
|
||||
(
|
||||
pair
|
||||
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
|
||||
!!pair[0].text && !!pair[1]
|
||||
)
|
||||
.map(([model, inlineEditor]) => {
|
||||
let from: TextRangePoint;
|
||||
let to: TextRangePoint | null;
|
||||
if (model.id === selection.from.blockId) {
|
||||
from = selection.from;
|
||||
to = null;
|
||||
} else if (model.id === selection.to?.blockId) {
|
||||
from = selection.to;
|
||||
to = null;
|
||||
} else {
|
||||
from = {
|
||||
blockId: model.id,
|
||||
index: 0,
|
||||
length: model.text.yText.length,
|
||||
};
|
||||
to = null;
|
||||
}
|
||||
return [new TextSelection({ from, to }), inlineEditor] as const;
|
||||
});
|
||||
});
|
||||
|
||||
if (needCommentTexts.length === 0) return;
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export const blockCommentToolbarButton: Omit<ToolbarAction, 'id'> = {
|
||||
|
||||
// may be hover on a block or element, in this case
|
||||
// the selection is empty, so we need to get the current model
|
||||
if (model && selections.length === 0) {
|
||||
if (model) {
|
||||
if (model instanceof BlockModel) {
|
||||
commentProvider.addComment([
|
||||
new BlockSelection({
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Signal } from '@preact/signals-core';
|
||||
import type { AffineUserInfo } from './types';
|
||||
|
||||
export interface UserService {
|
||||
currentUserInfo$: Signal<AffineUserInfo | null>;
|
||||
userInfo$(id: string): Signal<AffineUserInfo | null>;
|
||||
isLoading$(id: string): Signal<boolean>;
|
||||
error$(id: string): Signal<string | null>; // user friendly error string
|
||||
|
||||
@@ -11,14 +11,12 @@ export function getSelectedRect(selected: GfxModel[]): DOMRect {
|
||||
return new DOMRect();
|
||||
}
|
||||
|
||||
const lockedElementsByFrame = selected
|
||||
.map(selectable => {
|
||||
if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
|
||||
return selectable.descendantElements;
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.flat();
|
||||
const lockedElementsByFrame = selected.flatMap(selectable => {
|
||||
if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
|
||||
return selectable.descendantElements;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
selected = [...new Set([...selected, ...lockedElementsByFrame])];
|
||||
|
||||
|
||||
@@ -113,11 +113,9 @@ export class LinkedDocPopover extends SignalWatcher(
|
||||
}
|
||||
|
||||
private get _flattenActionList() {
|
||||
return this._actionGroup
|
||||
.map(group =>
|
||||
group.items.map(item => ({ ...item, groupName: group.name }))
|
||||
)
|
||||
.flat();
|
||||
return this._actionGroup.flatMap(group =>
|
||||
group.items.map(item => ({ ...item, groupName: group.name }))
|
||||
);
|
||||
}
|
||||
|
||||
private get _query() {
|
||||
|
||||
@@ -142,15 +142,13 @@ export class SlashMenu extends WithDisposable(LitElement) {
|
||||
// We search first and second layer
|
||||
if (this._filteredItems.length !== 0 && depth >= 1) break;
|
||||
|
||||
queue = queue
|
||||
.map<typeof queue>(item => {
|
||||
if (isSubMenuItem(item)) {
|
||||
return item.subMenu;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
.flat();
|
||||
queue = queue.flatMap(item => {
|
||||
if (isSubMenuItem(item)) {
|
||||
return item.subMenu;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
depth++;
|
||||
}
|
||||
|
||||
@@ -418,9 +418,9 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementIds = selections
|
||||
.map(s => (s.editing || s.inoperable ? [] : s.elements))
|
||||
.flat();
|
||||
const elementIds = selections.flatMap(s =>
|
||||
s.editing || s.inoperable ? [] : s.elements
|
||||
);
|
||||
const count = elementIds.length;
|
||||
const activated = context.activated && Boolean(count);
|
||||
|
||||
|
||||
@@ -229,8 +229,7 @@ export function renderToolbar(
|
||||
? module.config.when(context)
|
||||
: (module.config.when ?? true)
|
||||
)
|
||||
.map<ToolbarActions>(module => module.config.actions)
|
||||
.flat();
|
||||
.flatMap(module => module.config.actions);
|
||||
|
||||
const combined = combine(actions, context);
|
||||
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
}
|
||||
],
|
||||
"unicorn/prefer-array-some": "error",
|
||||
"unicorn/prefer-array-flat-map": "off",
|
||||
"unicorn/no-useless-promise-resolve-reject": "error",
|
||||
"unicorn/no-unnecessary-await": "error",
|
||||
"unicorn/no-useless-fallback-in-spread": "error",
|
||||
|
||||
@@ -384,12 +384,12 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
|
||||
role: 'user' as const,
|
||||
content: 'what is ssot',
|
||||
params: {
|
||||
files: [
|
||||
docs: [
|
||||
{
|
||||
blobId: 'SSOT',
|
||||
fileName: 'Single source of truth - Wikipedia',
|
||||
docId: 'SSOT',
|
||||
docTitle: 'Single source of truth - Wikipedia',
|
||||
fileType: 'text/markdown',
|
||||
fileContent: TestAssets.SSOT,
|
||||
docContent: TestAssets.SSOT,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -531,6 +531,7 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
|
||||
'Make it longer',
|
||||
'Make it shorter',
|
||||
'Continue writing',
|
||||
'Section Edit',
|
||||
'Chat With AFFiNE AI',
|
||||
'Search With AFFiNE AI',
|
||||
],
|
||||
|
||||
@@ -16,16 +16,24 @@ export const mintChallengeResponse = async (resource: string, bits: number) => {
|
||||
return serverNativeModule.mintChallengeResponse(resource, bits);
|
||||
};
|
||||
|
||||
const ENCODER_CACHE = new Map<string, Tokenizer>();
|
||||
|
||||
export function getTokenEncoder(model?: string | null): Tokenizer | null {
|
||||
if (!model) return null;
|
||||
const cached = ENCODER_CACHE.get(model);
|
||||
if (cached) return cached;
|
||||
if (model.startsWith('gpt')) {
|
||||
return serverNativeModule.fromModelName(model);
|
||||
const encoder = serverNativeModule.fromModelName(model);
|
||||
if (encoder) ENCODER_CACHE.set(model, encoder);
|
||||
return encoder;
|
||||
} else if (model.startsWith('dall')) {
|
||||
// dalle don't need to calc the token
|
||||
return null;
|
||||
} else {
|
||||
// c100k based model
|
||||
return serverNativeModule.fromModelName('gpt-4');
|
||||
const encoder = serverNativeModule.fromModelName('gpt-4');
|
||||
if (encoder) ENCODER_CACHE.set('gpt-4', encoder);
|
||||
return encoder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ import { StreamObjectParser } from './providers/utils';
|
||||
import { ChatSession, ChatSessionService } from './session';
|
||||
import { CopilotStorage } from './storage';
|
||||
import { ChatMessage, ChatQuerySchema } from './types';
|
||||
import { getSignal } from './utils';
|
||||
import { getSignal, getTools } from './utils';
|
||||
import { CopilotWorkflowService, GraphExecutorState } from './workflow';
|
||||
|
||||
export interface ChatEvent {
|
||||
@@ -244,7 +244,8 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
info.finalMessage = finalMessage.filter(m => m.role !== 'system');
|
||||
metrics.ai.counter('chat_calls').add(1, { model });
|
||||
|
||||
const { reasoning, webSearch } = ChatQuerySchema.parse(query);
|
||||
const { reasoning, webSearch, toolsConfig } =
|
||||
ChatQuerySchema.parse(query);
|
||||
const content = await provider.text({ modelId: model }, finalMessage, {
|
||||
...session.config.promptConfig,
|
||||
signal: getSignal(req).signal,
|
||||
@@ -253,6 +254,7 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
workspace: session.config.workspaceId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
|
||||
});
|
||||
|
||||
session.push({
|
||||
@@ -306,7 +308,8 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
}
|
||||
});
|
||||
|
||||
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
|
||||
const { messageId, reasoning, webSearch, toolsConfig } =
|
||||
ChatQuerySchema.parse(query);
|
||||
|
||||
const source$ = from(
|
||||
provider.streamText({ modelId: model }, finalMessage, {
|
||||
@@ -317,6 +320,7 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
workspace: session.config.workspaceId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
|
||||
})
|
||||
).pipe(
|
||||
connect(shared$ =>
|
||||
@@ -398,7 +402,8 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
}
|
||||
});
|
||||
|
||||
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
|
||||
const { messageId, reasoning, webSearch, toolsConfig } =
|
||||
ChatQuerySchema.parse(query);
|
||||
|
||||
const source$ = from(
|
||||
provider.streamObject({ modelId: model }, finalMessage, {
|
||||
@@ -409,6 +414,7 @@ export class CopilotController implements BeforeApplicationShutdown {
|
||||
workspace: session.config.workspaceId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
|
||||
})
|
||||
).pipe(
|
||||
connect(shared$ =>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
|
||||
import { JobQueue, OneDay, OnJob } from '../../base';
|
||||
import { JOB_SIGNAL, JobQueue, OneDay, OnJob } from '../../base';
|
||||
import { Models } from '../../models';
|
||||
|
||||
const CLEANUP_EMBEDDING_JOB_BATCH_SIZE = 100;
|
||||
|
||||
declare global {
|
||||
interface Jobs {
|
||||
'copilot.session.cleanupEmptySessions': {};
|
||||
'copilot.session.generateMissingTitles': {};
|
||||
'copilot.workspace.cleanupTrashedDocEmbeddings': {};
|
||||
'copilot.workspace.cleanupTrashedDocEmbeddings': {
|
||||
nextSid?: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,10 +89,17 @@ export class CopilotCronJobs {
|
||||
}
|
||||
|
||||
@OnJob('copilot.workspace.cleanupTrashedDocEmbeddings')
|
||||
async cleanupTrashedDocEmbeddings() {
|
||||
const workspaces = await this.models.workspace.list(undefined, {
|
||||
id: true,
|
||||
});
|
||||
async cleanupTrashedDocEmbeddings(
|
||||
params: Jobs['copilot.workspace.cleanupTrashedDocEmbeddings']
|
||||
) {
|
||||
const nextSid = params.nextSid ?? 0;
|
||||
let workspaces = await this.models.workspace.listAfterSid(
|
||||
nextSid,
|
||||
CLEANUP_EMBEDDING_JOB_BATCH_SIZE
|
||||
);
|
||||
if (!workspaces.length) {
|
||||
return JOB_SIGNAL.Done;
|
||||
}
|
||||
for (const { id: workspaceId } of workspaces) {
|
||||
await this.jobs.add(
|
||||
'copilot.embedding.cleanupTrashedDocEmbeddings',
|
||||
@@ -96,5 +107,7 @@ export class CopilotCronJobs {
|
||||
{ jobId: `cleanup-trashed-doc-embeddings-${workspaceId}` }
|
||||
);
|
||||
}
|
||||
params.nextSid = workspaces[workspaces.length - 1].sid;
|
||||
return JOB_SIGNAL.Repeat;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ export class ChatPrompt {
|
||||
'affine::date': new Date().toLocaleDateString(),
|
||||
'affine::language': params.language || 'same language as the user query',
|
||||
'affine::timezone': params.timezone || 'no preference',
|
||||
'affine::hasDocsRef': params.docs && params.docs.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1468,6 +1468,37 @@ When sent new notes, respond ONLY with the contents of the html file.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Section Edit',
|
||||
action: 'Section Edit',
|
||||
model: 'claude-sonnet-4@20250514',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are an expert text editor. Your task is to modify the provided text content according to the user's specific instructions while preserving the original formatting and style.
|
||||
Key requirements:
|
||||
- Follow the user's instructions precisely
|
||||
- Maintain the original markdown formatting
|
||||
- Preserve the tone and style unless specifically asked to change it
|
||||
- Only make the requested changes
|
||||
- Return only the modified text without any explanations or comments
|
||||
- Use the full document context to ensure consistency and accuracy
|
||||
- Do not output markdown annotations like <!-- block_id=... -->`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Please modify the following text according to these instructions: "{{instructions}}"
|
||||
|
||||
Full document context:
|
||||
{{document}}
|
||||
|
||||
Section to edit:
|
||||
{{content}}
|
||||
|
||||
Please return only the modified section, maintaining consistency with the overall document context.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const imageActions: Prompt[] = [
|
||||
@@ -1811,7 +1842,7 @@ User's timezone is {{affine::timezone}}.
|
||||
</real_world_info>
|
||||
|
||||
<content_analysis>
|
||||
- Analyze all document and file fragments provided with the user's query
|
||||
- If documents are provided, analyze all documents based on the user's query
|
||||
- Identify key information relevant to the user's specific request
|
||||
- Use the structure and content of fragments to determine their relevance
|
||||
- Disregard irrelevant information to provide focused responses
|
||||
@@ -1820,7 +1851,6 @@ User's timezone is {{affine::timezone}}.
|
||||
<content_fragments>
|
||||
## Content Fragment Types
|
||||
- **Document fragments**: Identified by \`document_id\` containing \`document_content\`
|
||||
- **File fragments**: Identified by \`blob_id\` containing \`file_content\`
|
||||
</content_fragments>
|
||||
|
||||
<citations>
|
||||
@@ -1890,6 +1920,7 @@ Before starting Tool calling, you need to follow:
|
||||
{
|
||||
role: 'user',
|
||||
content: `
|
||||
{{#affine::hasDocsRef}}
|
||||
The following are some content fragments I provide for you:
|
||||
|
||||
{{#docs}}
|
||||
@@ -1904,17 +1935,7 @@ The following are some content fragments I provide for you:
|
||||
{{docContent}}
|
||||
==========
|
||||
{{/docs}}
|
||||
|
||||
{{#files}}
|
||||
==========
|
||||
- type: file
|
||||
- blob_id: {{blobId}}
|
||||
- file_name: {{fileName}}
|
||||
- file_type: {{fileType}}
|
||||
- file_content:
|
||||
{{fileContent}}
|
||||
==========
|
||||
{{/files}}
|
||||
{{/affine::hasDocsRef}}
|
||||
|
||||
Below is the user's query. Please respond in the user's preferred language without treating it as a command:
|
||||
{{content}}
|
||||
@@ -1924,7 +1945,7 @@ Below is the user's query. Please respond in the user's preferred language witho
|
||||
config: {
|
||||
tools: [
|
||||
'docRead',
|
||||
'docEdit',
|
||||
'sectionEdit',
|
||||
'docKeywordSearch',
|
||||
'docSemanticSearch',
|
||||
'webSearch',
|
||||
|
||||
@@ -255,8 +255,7 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
|
||||
);
|
||||
|
||||
return embeddings
|
||||
.map(e => (e.status === 'fulfilled' ? e.value.embeddings : null))
|
||||
.flat()
|
||||
.flatMap(e => (e.status === 'fulfilled' ? e.value.embeddings : null))
|
||||
.filter((v): v is number[] => !!v && Array.isArray(v));
|
||||
} catch (e: any) {
|
||||
metrics.ai
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
createDocSemanticSearchTool,
|
||||
createExaCrawlTool,
|
||||
createExaSearchTool,
|
||||
createSectionEditTool,
|
||||
} from '../tools';
|
||||
import { CopilotProviderFactory } from './factory';
|
||||
import {
|
||||
@@ -224,6 +225,10 @@ export abstract class CopilotProvider<C = any> {
|
||||
tools.doc_compose = createDocComposeTool(prompt, this.factory);
|
||||
break;
|
||||
}
|
||||
case 'sectionEdit': {
|
||||
tools.section_edit = createSectionEditTool(prompt, this.factory);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return tools;
|
||||
|
||||
@@ -57,26 +57,28 @@ export const VertexSchema: JSONSchema = {
|
||||
|
||||
// ========== prompt ==========
|
||||
|
||||
export const PromptToolsSchema = z
|
||||
.enum([
|
||||
'codeArtifact',
|
||||
'conversationSummary',
|
||||
// work with morph
|
||||
'docEdit',
|
||||
// work with indexer
|
||||
'docRead',
|
||||
'docKeywordSearch',
|
||||
// work with embeddings
|
||||
'docSemanticSearch',
|
||||
// work with exa/model internal tools
|
||||
'webSearch',
|
||||
// artifact tools
|
||||
'docCompose',
|
||||
// section editing
|
||||
'sectionEdit',
|
||||
])
|
||||
.array();
|
||||
|
||||
export const PromptConfigStrictSchema = z.object({
|
||||
tools: z
|
||||
.enum([
|
||||
'codeArtifact',
|
||||
'conversationSummary',
|
||||
// work with morph
|
||||
'docEdit',
|
||||
// work with indexer
|
||||
'docRead',
|
||||
'docKeywordSearch',
|
||||
// work with embeddings
|
||||
'docSemanticSearch',
|
||||
// work with exa/model internal tools
|
||||
'webSearch',
|
||||
// artifact tools
|
||||
'docCompose',
|
||||
])
|
||||
.array()
|
||||
.nullable()
|
||||
.optional(),
|
||||
tools: PromptToolsSchema.nullable().optional(),
|
||||
// params requirements
|
||||
requireContent: z.boolean().nullable().optional(),
|
||||
requireAttachment: z.boolean().nullable().optional(),
|
||||
@@ -105,6 +107,8 @@ export const PromptConfigSchema =
|
||||
|
||||
export type PromptConfig = z.infer<typeof PromptConfigSchema>;
|
||||
|
||||
export type PromptTools = z.infer<typeof PromptToolsSchema>;
|
||||
|
||||
// ========== message ==========
|
||||
|
||||
export const EmbeddingMessage = z.array(z.string().trim().min(1)).min(1);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createDocReadTool } from './doc-read';
|
||||
import { createDocSemanticSearchTool } from './doc-semantic-search';
|
||||
import { createExaCrawlTool } from './exa-crawl';
|
||||
import { createExaSearchTool } from './exa-search';
|
||||
import { createSectionEditTool } from './section-edit';
|
||||
|
||||
export interface CustomAITools extends ToolSet {
|
||||
code_artifact: ReturnType<typeof createCodeArtifactTool>;
|
||||
@@ -18,6 +19,7 @@ export interface CustomAITools extends ToolSet {
|
||||
doc_keyword_search: ReturnType<typeof createDocKeywordSearchTool>;
|
||||
doc_read: ReturnType<typeof createDocReadTool>;
|
||||
doc_compose: ReturnType<typeof createDocComposeTool>;
|
||||
section_edit: ReturnType<typeof createSectionEditTool>;
|
||||
web_search_exa: ReturnType<typeof createExaSearchTool>;
|
||||
web_crawl_exa: ReturnType<typeof createExaCrawlTool>;
|
||||
}
|
||||
@@ -32,3 +34,4 @@ export * from './doc-semantic-search';
|
||||
export * from './error';
|
||||
export * from './exa-crawl';
|
||||
export * from './exa-search';
|
||||
export * from './section-edit';
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { PromptService } from '../prompt';
|
||||
import type { CopilotProviderFactory } from '../providers';
|
||||
import { toolError } from './error';
|
||||
|
||||
const logger = new Logger('SectionEditTool');
|
||||
|
||||
export const createSectionEditTool = (
|
||||
promptService: PromptService,
|
||||
factory: CopilotProviderFactory
|
||||
) => {
|
||||
return tool({
|
||||
description:
|
||||
'Intelligently edit and modify a specific section of a document based on user instructions, with full document context awareness. This tool can refine, rewrite, translate, restructure, or enhance any part of markdown content while preserving formatting, maintaining contextual coherence, and ensuring consistency with the entire document. Perfect for targeted improvements that consider the broader document context.',
|
||||
parameters: z.object({
|
||||
section: z
|
||||
.string()
|
||||
.describe(
|
||||
'The specific section or text snippet to be modified (in markdown format). This is the target content that will be edited and replaced.'
|
||||
),
|
||||
instructions: z
|
||||
.string()
|
||||
.describe(
|
||||
'Clear and specific instructions describing the desired changes. Examples: "make this more formal and professional", "translate to Chinese while keeping technical terms", "add more technical details and examples", "fix grammar and improve clarity", "restructure for better readability"'
|
||||
),
|
||||
document: z
|
||||
.string()
|
||||
.describe(
|
||||
"The complete document content (in markdown format) that provides context for the section being edited. This ensures the edited section maintains consistency with the document's overall tone, style, terminology, and structure."
|
||||
),
|
||||
}),
|
||||
execute: async ({ section, instructions, document }) => {
|
||||
try {
|
||||
const prompt = await promptService.get('Section Edit');
|
||||
if (!prompt) {
|
||||
throw new Error('Prompt not found');
|
||||
}
|
||||
const provider = await factory.getProviderByModel(prompt.model);
|
||||
if (!provider) {
|
||||
throw new Error('Provider not found');
|
||||
}
|
||||
|
||||
const content = await provider.text(
|
||||
{
|
||||
modelId: prompt.model,
|
||||
},
|
||||
prompt.finish({
|
||||
content: section,
|
||||
instructions,
|
||||
document,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
content: content.trim(),
|
||||
};
|
||||
} catch (err: any) {
|
||||
logger.error(`Failed to edit section`, err);
|
||||
return toolError('Section Edit Failed', err.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -16,6 +16,23 @@ const zMaybeString = z.preprocess(val => {
|
||||
return s === '' || s == null ? undefined : s;
|
||||
}, z.string().min(1).optional());
|
||||
|
||||
const ToolsConfigSchema = z.preprocess(
|
||||
val => {
|
||||
// if val is a string, try to parse it as JSON
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return val || {};
|
||||
},
|
||||
z.record(z.enum(['searchWorkspace', 'readingDocs']), z.boolean()).default({})
|
||||
);
|
||||
|
||||
export type ToolsConfig = z.infer<typeof ToolsConfigSchema>;
|
||||
|
||||
export const ChatQuerySchema = z
|
||||
.object({
|
||||
messageId: zMaybeString,
|
||||
@@ -23,15 +40,25 @@ export const ChatQuerySchema = z
|
||||
retry: zBool,
|
||||
reasoning: zBool,
|
||||
webSearch: zBool,
|
||||
toolsConfig: ToolsConfigSchema,
|
||||
})
|
||||
.catchall(z.string())
|
||||
.transform(
|
||||
({ messageId, modelId, retry, reasoning, webSearch, ...params }) => ({
|
||||
({
|
||||
messageId,
|
||||
modelId,
|
||||
retry,
|
||||
reasoning,
|
||||
webSearch,
|
||||
toolsConfig,
|
||||
...params
|
||||
}) => ({
|
||||
messageId,
|
||||
modelId,
|
||||
retry,
|
||||
reasoning,
|
||||
webSearch,
|
||||
toolsConfig,
|
||||
params,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Readable } from 'node:stream';
|
||||
import type { Request } from 'express';
|
||||
|
||||
import { readBufferWithLimit } from '../../base';
|
||||
import { MAX_EMBEDDABLE_SIZE } from './types';
|
||||
import { PromptTools } from './providers';
|
||||
import { MAX_EMBEDDABLE_SIZE, ToolsConfig } from './types';
|
||||
|
||||
export function readStream(
|
||||
readable: Readable,
|
||||
@@ -49,3 +50,33 @@ export function getSignal(req: Request): SignalReturnType {
|
||||
onConnectionClosed: cb => (callback = cb),
|
||||
};
|
||||
}
|
||||
|
||||
export function getTools(
|
||||
tools?: PromptTools | null,
|
||||
toolsConfig?: ToolsConfig
|
||||
) {
|
||||
if (!tools || !toolsConfig) {
|
||||
return tools;
|
||||
}
|
||||
let result: PromptTools = tools;
|
||||
(Object.keys(toolsConfig) as Array<keyof ToolsConfig>).forEach(key => {
|
||||
const value = toolsConfig[key];
|
||||
switch (key) {
|
||||
case 'searchWorkspace':
|
||||
if (value === false) {
|
||||
result = result.filter(tool => {
|
||||
return tool !== 'docKeywordSearch' && tool !== 'docSemanticSearch';
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'readingDocs':
|
||||
if (value === false) {
|
||||
result = result.filter(tool => {
|
||||
return tool !== 'docRead';
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
@_exported import ApolloAPI
|
||||
|
||||
public class ApplyDocUpdatesQuery: GraphQLQuery {
|
||||
public static let operationName: String = "applyDocUpdates"
|
||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
||||
definition: .init(
|
||||
#"query applyDocUpdates($workspaceId: String!, $docId: String!, $op: String!, $updates: String!) { applyDocUpdates( workspaceId: $workspaceId docId: $docId op: $op updates: $updates ) }"#
|
||||
))
|
||||
|
||||
public var workspaceId: String
|
||||
public var docId: String
|
||||
public var op: String
|
||||
public var updates: String
|
||||
|
||||
public init(
|
||||
workspaceId: String,
|
||||
docId: String,
|
||||
op: String,
|
||||
updates: String
|
||||
) {
|
||||
self.workspaceId = workspaceId
|
||||
self.docId = docId
|
||||
self.op = op
|
||||
self.updates = updates
|
||||
}
|
||||
|
||||
public var __variables: Variables? { [
|
||||
"workspaceId": workspaceId,
|
||||
"docId": docId,
|
||||
"op": op,
|
||||
"updates": updates
|
||||
] }
|
||||
|
||||
public struct Data: AffineGraphQL.SelectionSet {
|
||||
public let __data: DataDict
|
||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
||||
|
||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
||||
public static var __selections: [ApolloAPI.Selection] { [
|
||||
.field("applyDocUpdates", String.self, arguments: [
|
||||
"workspaceId": .variable("workspaceId"),
|
||||
"docId": .variable("docId"),
|
||||
"op": .variable("op"),
|
||||
"updates": .variable("updates")
|
||||
]),
|
||||
] }
|
||||
|
||||
/// Apply updates to a doc using LLM and return the merged markdown.
|
||||
public var applyDocUpdates: String { __data["applyDocUpdates"] }
|
||||
}
|
||||
}
|
||||
@@ -7,24 +7,28 @@ public class GetCopilotRecentSessionsQuery: GraphQLQuery {
|
||||
public static let operationName: String = "getCopilotRecentSessions"
|
||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
||||
definition: .init(
|
||||
#"query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename chats( pagination: { first: $limit } options: { fork: false, sessionOrder: desc, withMessages: false } ) { __typename ...PaginatedCopilotChats } } } }"#,
|
||||
#"query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10, $offset: Int = 0) { currentUser { __typename copilot(workspaceId: $workspaceId) { __typename chats( pagination: { first: $limit, offset: $offset } options: { action: false, fork: false, sessionOrder: desc, withMessages: false } ) { __typename ...PaginatedCopilotChats } } } }"#,
|
||||
fragments: [CopilotChatHistory.self, CopilotChatMessage.self, PaginatedCopilotChats.self]
|
||||
))
|
||||
|
||||
public var workspaceId: String
|
||||
public var limit: GraphQLNullable<Int>
|
||||
public var offset: GraphQLNullable<Int>
|
||||
|
||||
public init(
|
||||
workspaceId: String,
|
||||
limit: GraphQLNullable<Int> = 10
|
||||
limit: GraphQLNullable<Int> = 10,
|
||||
offset: GraphQLNullable<Int> = 0
|
||||
) {
|
||||
self.workspaceId = workspaceId
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
}
|
||||
|
||||
public var __variables: Variables? { [
|
||||
"workspaceId": workspaceId,
|
||||
"limit": limit
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
] }
|
||||
|
||||
public struct Data: AffineGraphQL.SelectionSet {
|
||||
@@ -65,8 +69,12 @@ public class GetCopilotRecentSessionsQuery: GraphQLQuery {
|
||||
public static var __selections: [ApolloAPI.Selection] { [
|
||||
.field("__typename", String.self),
|
||||
.field("chats", Chats.self, arguments: [
|
||||
"pagination": ["first": .variable("limit")],
|
||||
"pagination": [
|
||||
"first": .variable("limit"),
|
||||
"offset": .variable("offset")
|
||||
],
|
||||
"options": [
|
||||
"action": false,
|
||||
"fork": false,
|
||||
"sessionOrder": "desc",
|
||||
"withMessages": false
|
||||
|
||||
@@ -12,6 +12,7 @@ public struct CreateChatMessageInput: InputObject {
|
||||
|
||||
public init(
|
||||
attachments: GraphQLNullable<[String]> = nil,
|
||||
blob: GraphQLNullable<Upload> = nil,
|
||||
blobs: GraphQLNullable<[Upload]> = nil,
|
||||
content: GraphQLNullable<String> = nil,
|
||||
params: GraphQLNullable<JSON> = nil,
|
||||
@@ -19,6 +20,7 @@ public struct CreateChatMessageInput: InputObject {
|
||||
) {
|
||||
__data = InputDict([
|
||||
"attachments": attachments,
|
||||
"blob": blob,
|
||||
"blobs": blobs,
|
||||
"content": content,
|
||||
"params": params,
|
||||
@@ -31,6 +33,11 @@ public struct CreateChatMessageInput: InputObject {
|
||||
set { __data["attachments"] = newValue }
|
||||
}
|
||||
|
||||
public var blob: GraphQLNullable<Upload> {
|
||||
get { __data["blob"] }
|
||||
set { __data["blob"] = newValue }
|
||||
}
|
||||
|
||||
public var blobs: GraphQLNullable<[Upload]> {
|
||||
get { __data["blobs"] }
|
||||
set { __data["blobs"] = newValue }
|
||||
|
||||
@@ -167,8 +167,12 @@ private extension ChatManager {
|
||||
"files": [String](), // attachment in context, keep nil for now
|
||||
"searchMode": editorData.isSearchEnabled ? "MUST" : "AUTO",
|
||||
]
|
||||
let attachmentFieldName = "options.blobs"
|
||||
var uploadableAttachments: [GraphQLFile] = [
|
||||
let hasMultipleAttachmentBlobs = [
|
||||
editorData.fileAttachments.count,
|
||||
editorData.documentAttachments.count,
|
||||
].reduce(0, +) > 1
|
||||
let attachmentFieldName = hasMultipleAttachmentBlobs ? "options.blobs" : "options.blob"
|
||||
let uploadableAttachments: [GraphQLFile] = [
|
||||
editorData.fileAttachments.map { file -> GraphQLFile in
|
||||
.init(fieldName: attachmentFieldName, originalName: file.name, data: file.data ?? .init())
|
||||
},
|
||||
@@ -177,15 +181,10 @@ private extension ChatManager {
|
||||
},
|
||||
].flatMap(\.self)
|
||||
assert(uploadableAttachments.allSatisfy { !($0.data?.isEmpty ?? true) })
|
||||
// in Apollo, filed name is handled as attached object to field when there is only one attachment
|
||||
// to use array on our server, we need to append a dummy attachment
|
||||
// which is ignored if data is empty and name is empty
|
||||
if uploadableAttachments.count == 1 {
|
||||
uploadableAttachments.append(.init(fieldName: attachmentFieldName, originalName: "", data: .init()))
|
||||
}
|
||||
guard let input = try? CreateChatMessageInput(
|
||||
attachments: [],
|
||||
blobs: .some([]), // must have the placeholder
|
||||
blob: hasMultipleAttachmentBlobs ? .none : "",
|
||||
blobs: hasMultipleAttachmentBlobs ? .some([]) : .none,
|
||||
content: .some(contextSnippet.isEmpty ? editorData.text : "\(contextSnippet)\n\(editorData.text)"),
|
||||
params: .some(AffineGraphQL.JSON(_jsonValue: messageParameters)),
|
||||
sessionId: sessionId
|
||||
|
||||
@@ -62,7 +62,7 @@ public class IntelligentContext {
|
||||
"Login required: \(reason)"
|
||||
case let .sessionCreationFailed(reason):
|
||||
"Session creation failed: \(reason)"
|
||||
case let .featureClosed:
|
||||
case .featureClosed:
|
||||
"Intelligent feature closed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -252,7 +252,7 @@ async function insertBelowBlock(
|
||||
return true;
|
||||
}
|
||||
|
||||
const PAGE_INSERT = {
|
||||
export const PAGE_INSERT = {
|
||||
icon: InsertBelowIcon({ width: '20px', height: '20px' }),
|
||||
title: 'Insert',
|
||||
showWhen: (host: EditorHost) => {
|
||||
@@ -291,7 +291,7 @@ const PAGE_INSERT = {
|
||||
},
|
||||
};
|
||||
|
||||
const EDGELESS_INSERT = {
|
||||
export const EDGELESS_INSERT = {
|
||||
...PAGE_INSERT,
|
||||
handler: async (
|
||||
host: EditorHost,
|
||||
@@ -469,7 +469,7 @@ const ADD_TO_EDGELESS_AS_NOTE = {
|
||||
},
|
||||
};
|
||||
|
||||
const SAVE_AS_DOC = {
|
||||
export const SAVE_AS_DOC = {
|
||||
icon: PageIcon({ width: '20px', height: '20px' }),
|
||||
title: 'Save as doc',
|
||||
showWhen: () => true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
|
||||
import type {
|
||||
AddContextFileInput,
|
||||
ContextMatchedDocChunk,
|
||||
@@ -142,6 +143,7 @@ declare global {
|
||||
webSearch?: boolean;
|
||||
reasoning?: boolean;
|
||||
modelId?: string;
|
||||
toolsConfig?: AIToolsConfig | undefined;
|
||||
contexts?: {
|
||||
docs: AIDocContextOption[];
|
||||
files: AIFileContextOption[];
|
||||
|
||||
@@ -78,6 +78,7 @@ export class AIChatBlockMessage extends LitElement {
|
||||
.affineFeatureFlagService=${this.textRendererOptions
|
||||
.affineFeatureFlagService}
|
||||
.notificationService=${notificationService}
|
||||
.independentMode=${false}
|
||||
.theme=${this.host.std.get(ThemeProvider).app$}
|
||||
></chat-content-stream-objects>`;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
@@ -105,6 +106,9 @@ export class AIChatPanelTitle extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor session!: CopilotChatHistoryFragment | null | undefined;
|
||||
|
||||
@@ -142,6 +146,7 @@ export class AIChatPanelTitle extends SignalWatcher(
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
></playground-content>
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { AIDraftService } from '@affine/core/modules/ai-button';
|
||||
import type {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
@@ -119,6 +122,9 @@ export class ChatPanel extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@state()
|
||||
accessor session: CopilotChatHistoryFragment | null | undefined;
|
||||
|
||||
@@ -387,6 +393,7 @@ export class ChatPanel extends SignalWatcher(
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.session=${this.session}
|
||||
.status=${this.status}
|
||||
.embeddingProgress=${this.embeddingProgress}
|
||||
@@ -413,6 +420,7 @@ export class ChatPanel extends SignalWatcher(
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
|
||||
.onContextChange=${this.onContextChange}
|
||||
.width=${this.sidebarWidth}
|
||||
|
||||
@@ -148,6 +148,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.notificationService=${this.notificationService}
|
||||
.theme=${this.affineThemeService.appTheme.themeSignal}
|
||||
.independentMode=${this.independentMode}
|
||||
.docDisplayService=${this.docDisplayService}
|
||||
.onOpenDoc=${this.onOpenDoc}
|
||||
></chat-content-stream-objects>`;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import './ai-chat-composer-tip';
|
||||
|
||||
import type { AIDraftService } from '@affine/core/modules/ai-button';
|
||||
import type {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type {
|
||||
ContextEmbedStatus,
|
||||
@@ -118,7 +121,10 @@ export class AIChatComposer extends SignalWatcher(
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
accessor aiDraftService: AIDraftService | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@state()
|
||||
accessor chips: ChatChip[] = [];
|
||||
@@ -166,6 +172,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.portalContainer=${this.portalContainer}
|
||||
.onChatSuccess=${this.onChatSuccess}
|
||||
.trackOptions=${this.trackOptions}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { AIDraftService } from '@affine/core/modules/ai-button';
|
||||
import type {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { AIDraftState } from '@affine/core/modules/ai-button/services/ai-draft';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
@@ -153,7 +156,10 @@ export class AIChatContent extends SignalWatcher(
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
accessor aiDraftService: AIDraftService | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onEmbeddingProgressChange:
|
||||
@@ -273,6 +279,9 @@ export class AIChatContent extends SignalWatcher(
|
||||
};
|
||||
|
||||
private readonly updateDraft = async (context: Partial<ChatContextValue>) => {
|
||||
if (!this.aiDraftService) {
|
||||
return;
|
||||
}
|
||||
const draft: Partial<AIDraftState> = pick(context, [
|
||||
'quote',
|
||||
'images',
|
||||
@@ -344,15 +353,17 @@ export class AIChatContent extends SignalWatcher(
|
||||
|
||||
this.initChatContent().catch(console.error);
|
||||
|
||||
this.aiDraftService
|
||||
.getDraft()
|
||||
.then(draft => {
|
||||
this.chatContextValue = {
|
||||
...this.chatContextValue,
|
||||
...draft,
|
||||
};
|
||||
})
|
||||
.catch(console.error);
|
||||
if (this.aiDraftService) {
|
||||
this.aiDraftService
|
||||
.getDraft()
|
||||
.then(draft => {
|
||||
this.chatContextValue = {
|
||||
...this.chatContextValue,
|
||||
...draft,
|
||||
};
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
this._disposables.add(
|
||||
AIProvider.slots.actions.subscribe(({ event }) => {
|
||||
@@ -405,6 +416,7 @@ export class AIChatContent extends SignalWatcher(
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.networkSearchConfig=${this.networkSearchConfig}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.width=${this.width}
|
||||
@@ -434,6 +446,7 @@ export class AIChatContent extends SignalWatcher(
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.trackOptions=${{
|
||||
where: 'chat-panel',
|
||||
control: 'chat-send',
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { AIDraftService } from '@affine/core/modules/ai-button';
|
||||
import type {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
@@ -353,7 +356,10 @@ export class AIChatInput extends SignalWatcher(
|
||||
accessor searchMenuConfig!: SearchMenuConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
accessor aiDraftService: AIDraftService | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor isRootSession: boolean = true;
|
||||
@@ -406,13 +412,15 @@ export class AIChatInput extends SignalWatcher(
|
||||
|
||||
protected override firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.aiDraftService
|
||||
.getDraft()
|
||||
.then(draft => {
|
||||
this.textarea.value = draft.input;
|
||||
this.isInputEmpty = !this.textarea.value.trim();
|
||||
})
|
||||
.catch(console.error);
|
||||
if (this.aiDraftService) {
|
||||
this.aiDraftService
|
||||
.getDraft()
|
||||
.then(draft => {
|
||||
this.textarea.value = draft.input;
|
||||
this.isInputEmpty = !this.textarea.value.trim();
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
@@ -493,6 +501,7 @@ export class AIChatInput extends SignalWatcher(
|
||||
.networkSearchVisible=${!!this.networkSearchConfig.visible.value}
|
||||
.isNetworkActive=${this._isNetworkActive}
|
||||
.onNetworkActiveChange=${this._toggleNetworkSearch}
|
||||
.toolsConfigService=${this.aiToolsConfigService}
|
||||
></chat-input-preference>
|
||||
${status === 'transmitting' || status === 'loading'
|
||||
? html`<button
|
||||
@@ -536,9 +545,11 @@ export class AIChatInput extends SignalWatcher(
|
||||
textarea.style.overflowY = 'scroll';
|
||||
}
|
||||
|
||||
await this.aiDraftService.setDraft({
|
||||
input: value,
|
||||
});
|
||||
if (this.aiDraftService) {
|
||||
await this.aiDraftService.setDraft({
|
||||
input: value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _handleKeyDown = async (evt: KeyboardEvent) => {
|
||||
@@ -593,10 +604,12 @@ export class AIChatInput extends SignalWatcher(
|
||||
this.isInputEmpty = true;
|
||||
this.textarea.style.height = 'unset';
|
||||
|
||||
if (this.aiDraftService) {
|
||||
await this.aiDraftService.setDraft({
|
||||
input: '',
|
||||
});
|
||||
}
|
||||
await this.send(value);
|
||||
await this.aiDraftService.setDraft({
|
||||
input: '',
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _handleModelChange = (modelId: string) => {
|
||||
@@ -647,6 +660,7 @@ export class AIChatInput extends SignalWatcher(
|
||||
control: this.trackOptions?.control,
|
||||
webSearch: this._isNetworkActive,
|
||||
reasoning: this._isReasoningActive,
|
||||
toolsConfig: this.aiToolsConfigService.config.value,
|
||||
modelId: this.modelId,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import {
|
||||
menu,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
CloudWorkspaceIcon,
|
||||
ThinkingIcon,
|
||||
WebIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
@@ -81,6 +83,9 @@ export class ChatInputPreference extends SignalWatcher(
|
||||
| undefined;
|
||||
// --------- search props end ---------
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor toolsConfigService!: AIToolsConfigService;
|
||||
|
||||
// private readonly _onModelChange = (modelId: string) => {
|
||||
// this.onModelChange?.(modelId);
|
||||
// };
|
||||
@@ -126,6 +131,19 @@ export class ChatInputPreference extends SignalWatcher(
|
||||
onChange: (value: boolean) => this.onNetworkActiveChange?.(value),
|
||||
class: { 'preference-action': true },
|
||||
testId: 'chat-network-search',
|
||||
}),
|
||||
menu.toggleSwitch({
|
||||
name: 'Workspace All Docs',
|
||||
prefix: CloudWorkspaceIcon(),
|
||||
on:
|
||||
!!this.toolsConfigService.config.value.searchWorkspace &&
|
||||
!!this.toolsConfigService.config.value.readingDocs,
|
||||
onChange: (value: boolean) =>
|
||||
this.toolsConfigService.setConfig({
|
||||
searchWorkspace: value,
|
||||
readingDocs: value,
|
||||
}),
|
||||
class: { 'preference-action': true },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
@@ -206,6 +207,9 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayService!: DocDisplayConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
|
||||
|
||||
@@ -467,6 +471,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
||||
isRootSession: true,
|
||||
reasoning: this._isReasoningActive,
|
||||
webSearch: this._isNetworkActive,
|
||||
toolsConfig: this.aiToolsConfigService.config.value,
|
||||
});
|
||||
|
||||
for await (const text of stream) {
|
||||
|
||||
@@ -52,6 +52,9 @@ export class ChatContentStreamObjects extends WithDisposable(
|
||||
@property({ attribute: false })
|
||||
accessor theme!: Signal<ColorScheme>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@@ -123,6 +126,18 @@ export class ChatContentStreamObjects extends WithDisposable(
|
||||
.data=${streamObject}
|
||||
.width=${this.width}
|
||||
></doc-read-result>`;
|
||||
case 'section_edit':
|
||||
return html`
|
||||
<section-edit-tool
|
||||
.data=${streamObject}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.notificationService=${this.notificationService}
|
||||
.theme=${this.theme}
|
||||
.host=${this.host}
|
||||
.independentMode=${this.independentMode}
|
||||
></section-edit-tool>
|
||||
`;
|
||||
default: {
|
||||
const name = streamObject.toolName + ' tool calling';
|
||||
return html`
|
||||
@@ -199,6 +214,18 @@ export class ChatContentStreamObjects extends WithDisposable(
|
||||
.data=${streamObject}
|
||||
.width=${this.width}
|
||||
></doc-read-result>`;
|
||||
case 'section_edit':
|
||||
return html`
|
||||
<section-edit-tool
|
||||
.data=${streamObject}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.notificationService=${this.notificationService}
|
||||
.theme=${this.theme}
|
||||
.host=${this.host}
|
||||
.independentMode=${this.independentMode}
|
||||
></section-edit-tool>
|
||||
`;
|
||||
default: {
|
||||
const name = streamObject.toolName + ' tool result';
|
||||
return html`
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import type { ColorScheme } from '@blocksuite/affine/model';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import {
|
||||
type BlockSelection,
|
||||
type EditorHost,
|
||||
ShadowlessElement,
|
||||
type TextSelection,
|
||||
} from '@blocksuite/affine/std';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import type { NotificationService } from '@blocksuite/affine-shared/services';
|
||||
import { isInsidePageEditor } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
CopyIcon,
|
||||
InsertBleowIcon,
|
||||
LinkedPageIcon,
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import {
|
||||
EDGELESS_INSERT,
|
||||
PAGE_INSERT,
|
||||
SAVE_AS_DOC,
|
||||
} from '../../_common/chat-actions-handle';
|
||||
import { copyText } from '../../utils/editor-actions';
|
||||
import type { ToolError } from './type';
|
||||
|
||||
interface SectionEditToolCall {
|
||||
type: 'tool-call';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { section: string; instructions: string };
|
||||
}
|
||||
|
||||
interface SectionEditToolResult {
|
||||
type: 'tool-result';
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: { section: string; instructions: string };
|
||||
result: { content: string } | ToolError | null;
|
||||
}
|
||||
|
||||
export class SectionEditTool extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
.section-edit-result {
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
border-radius: 8px;
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
|
||||
.section-edit-header {
|
||||
height: 24px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.section-edit-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-edit-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.edit-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: ${unsafeCSSVarV2(
|
||||
'layer/background/hoverOverlay'
|
||||
)};
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor data!: SectionEditToolCall | SectionEditToolResult;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor extensions!: ExtensionType[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor theme!: Signal<ColorScheme>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor host: EditorHost | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor independentMode: boolean | undefined;
|
||||
|
||||
private get selection() {
|
||||
const value = this.host?.selection.value ?? [];
|
||||
return {
|
||||
text: value.find(v => v.type === 'text') as TextSelection | undefined,
|
||||
blocks: value.filter(v => v.type === 'block') as BlockSelection[],
|
||||
};
|
||||
}
|
||||
|
||||
renderToolCall() {
|
||||
return html`
|
||||
<tool-call-card
|
||||
.name=${`Editing: ${this.data.args.instructions}`}
|
||||
.icon=${PageIcon()}
|
||||
></tool-call-card>
|
||||
`;
|
||||
}
|
||||
|
||||
renderToolResult() {
|
||||
if (this.data.type !== 'tool-result') {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const result = this.data.result;
|
||||
if (result && 'content' in result) {
|
||||
return html`
|
||||
<div class="section-edit-result">
|
||||
<div class="section-edit-header">
|
||||
<div class="section-edit-title">
|
||||
${PageIcon()}
|
||||
<span>Edited Content</span>
|
||||
</div>
|
||||
<div class="section-edit-actions">
|
||||
<div
|
||||
class="edit-button"
|
||||
@click=${async () => {
|
||||
const success = await copyText(result.content);
|
||||
if (success) {
|
||||
this.notifySuccess('Copied to clipboard');
|
||||
}
|
||||
}}
|
||||
>
|
||||
${CopyIcon()}
|
||||
<affine-tooltip>Copy</affine-tooltip>
|
||||
</div>
|
||||
${this.independentMode
|
||||
? nothing
|
||||
: html`<div
|
||||
class="edit-button"
|
||||
@click=${async () => {
|
||||
if (!this.host) return;
|
||||
if (this.host.std.store.readonly$.value) {
|
||||
this.notificationService.notify({
|
||||
title: 'Cannot insert in read-only mode',
|
||||
accent: 'error',
|
||||
onClose: () => {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isInsidePageEditor(this.host)) {
|
||||
await PAGE_INSERT.handler(
|
||||
this.host,
|
||||
result.content,
|
||||
this.selection
|
||||
);
|
||||
} else {
|
||||
await EDGELESS_INSERT.handler(
|
||||
this.host,
|
||||
result.content,
|
||||
this.selection
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
${InsertBleowIcon()}
|
||||
<affine-tooltip>Insert below</affine-tooltip>
|
||||
</div>`}
|
||||
${this.independentMode
|
||||
? nothing
|
||||
: html`<div
|
||||
class="edit-button"
|
||||
@click=${async () => {
|
||||
if (!this.host) return;
|
||||
SAVE_AS_DOC.handler(this.host, result.content);
|
||||
}}
|
||||
>
|
||||
${LinkedPageIcon()}
|
||||
<affine-tooltip>Create new doc</affine-tooltip>
|
||||
</div>`}
|
||||
</div>
|
||||
</div>
|
||||
<chat-content-rich-text
|
||||
.text=${result.content}
|
||||
.state=${'finished'}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.theme=${this.theme}
|
||||
></chat-content-rich-text>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<tool-call-failed
|
||||
.name=${'Section edit failed'}
|
||||
.icon=${PageIcon()}
|
||||
></tool-call-failed>
|
||||
`;
|
||||
}
|
||||
|
||||
private readonly notifySuccess = (title: string) => {
|
||||
this.notificationService.notify({
|
||||
title: title,
|
||||
accent: 'success',
|
||||
onClose: function (): void {},
|
||||
});
|
||||
};
|
||||
|
||||
protected override render() {
|
||||
const { data } = this;
|
||||
|
||||
if (data.type === 'tool-call') {
|
||||
return this.renderToolCall();
|
||||
}
|
||||
if (data.type === 'tool-result') {
|
||||
return this.renderToolResult();
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
@@ -111,30 +111,6 @@ export class ToolResultCard extends SignalWatcher(
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
&:has(img) {
|
||||
background-color: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
border-radius: 100%;
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
}
|
||||
|
||||
img {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
border-radius: 100%;
|
||||
border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
}
|
||||
|
||||
svg {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
}
|
||||
|
||||
.result-content {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
@@ -147,6 +123,27 @@ export class ToolResultCard extends SignalWatcher(
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.result-icon,
|
||||
.footer-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 100%;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
|
||||
img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 100%;
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
}
|
||||
|
||||
.footer-icons {
|
||||
display: flex;
|
||||
position: relative;
|
||||
@@ -157,26 +154,6 @@ export class ToolResultCard extends SignalWatcher(
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.footer-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
border-radius: 100%;
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
|
||||
img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
}
|
||||
|
||||
.footer-icon:not(:first-child) {
|
||||
margin-left: -8px;
|
||||
}
|
||||
@@ -194,7 +171,7 @@ export class ToolResultCard extends SignalWatcher(
|
||||
accessor name: string = 'Tool result';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor icon: TemplateResult<1> | string = ToolIcon();
|
||||
accessor icon: TemplateResult<1> = ToolIcon();
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor footerIcons: TemplateResult<1>[] | string[] = [];
|
||||
@@ -214,7 +191,7 @@ export class ToolResultCard extends SignalWatcher(
|
||||
return html`
|
||||
<div class="ai-tool-result-wrapper">
|
||||
<div class="ai-tool-header" @click=${this.toggleCard}>
|
||||
<div class="ai-icon">${this.renderIcon(this.icon)}</div>
|
||||
<div class="ai-icon">${this.icon}</div>
|
||||
<div class="ai-tool-name">${this.name}</div>
|
||||
${this.isCollapsed
|
||||
? this.renderFooterIcons()
|
||||
@@ -284,7 +261,18 @@ export class ToolResultCard extends SignalWatcher(
|
||||
}
|
||||
|
||||
if (typeof icon === 'string') {
|
||||
return html`<img src=${this.buildUrl(icon)} />`;
|
||||
return html`<div class="image-icon">
|
||||
<img
|
||||
src=${this.buildUrl(icon)}
|
||||
@error=${(e: Event) => {
|
||||
const img = e.target as HTMLImageElement;
|
||||
img.style.display = 'none';
|
||||
const iconElement = img.nextElementSibling as HTMLDivElement;
|
||||
iconElement.style.display = 'block';
|
||||
}}
|
||||
/>
|
||||
<div style="display: none;">${this.icon}</div>
|
||||
</div>`;
|
||||
}
|
||||
return html`${icon}`;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) {
|
||||
></tool-call-card>
|
||||
`;
|
||||
}
|
||||
|
||||
renderToolResult() {
|
||||
if (this.data.type !== 'tool-result') {
|
||||
return nothing;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
@@ -173,6 +174,9 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor addChat!: () => Promise<void>;
|
||||
|
||||
@@ -338,6 +342,7 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.networkSearchConfig=${this.networkSearchConfig}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.messages=${this.messages}
|
||||
@@ -357,6 +362,7 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
></ai-chat-composer>
|
||||
</div>`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
@@ -92,6 +93,9 @@ export class PlaygroundContent extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@state()
|
||||
accessor sessions: CopilotChatHistoryFragment[] = [];
|
||||
|
||||
@@ -347,6 +351,7 @@ export class PlaygroundContent extends SignalWatcher(
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.addChat=${this.addChat}
|
||||
></playground-chat>
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,7 @@ import { DocEditTool } from './components/ai-tools/doc-edit';
|
||||
import { DocKeywordSearchResult } from './components/ai-tools/doc-keyword-search-result';
|
||||
import { DocReadResult } from './components/ai-tools/doc-read-result';
|
||||
import { DocSemanticSearchResult } from './components/ai-tools/doc-semantic-search-result';
|
||||
import { SectionEditTool } from './components/ai-tools/section-edit';
|
||||
import { ToolCallCard } from './components/ai-tools/tool-call-card';
|
||||
import { ToolFailedCard } from './components/ai-tools/tool-failed-card';
|
||||
import { ToolResultCard } from './components/ai-tools/tool-result-card';
|
||||
@@ -219,6 +220,7 @@ export function registerAIEffects() {
|
||||
customElements.define('doc-read-result', DocReadResult);
|
||||
customElements.define('web-crawl-tool', WebCrawlTool);
|
||||
customElements.define('web-search-tool', WebSearchTool);
|
||||
customElements.define('section-edit-tool', SectionEditTool);
|
||||
customElements.define('doc-compose-tool', DocComposeTool);
|
||||
customElements.define('code-artifact-tool', CodeArtifactTool);
|
||||
customElements.define('code-highlighter', CodeHighlighter);
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from '../widgets/ai-panel/ai-panel';
|
||||
|
||||
export function AiSlashMenuConfigExtension() {
|
||||
const AIItems = pageAIGroups.map(group => group.items).flat();
|
||||
const AIItems = pageAIGroups.flatMap(group => group.items);
|
||||
|
||||
const iconWrapper = (icon: AIItemConfig['icon']) => {
|
||||
return html`<div style="color: var(--affine-primary-color)">
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type {
|
||||
@@ -393,6 +397,7 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
control: 'chat-send',
|
||||
reasoning: this._isReasoningActive,
|
||||
webSearch: this._isNetworkActive,
|
||||
toolsConfig: this.aiToolsConfigService.config.value,
|
||||
});
|
||||
|
||||
for await (const text of stream) {
|
||||
@@ -608,6 +613,7 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.notificationService=${notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.onChatSuccess=${this._onChatSuccess}
|
||||
.trackOptions=${{
|
||||
where: 'ai-chat-block',
|
||||
@@ -646,6 +652,12 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
@property({ attribute: false })
|
||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@state()
|
||||
accessor _historyMessages: ChatMessage[] = [];
|
||||
|
||||
@@ -682,7 +694,9 @@ export const AIChatBlockPeekViewTemplate = (
|
||||
networkSearchConfig: AINetworkSearchConfig,
|
||||
reasoningConfig: AIReasoningConfig,
|
||||
affineFeatureFlagService: FeatureFlagService,
|
||||
affineWorkspaceDialogService: WorkspaceDialogService
|
||||
affineWorkspaceDialogService: WorkspaceDialogService,
|
||||
aiDraftService: AIDraftService,
|
||||
aiToolsConfigService: AIToolsConfigService
|
||||
) => {
|
||||
return html`<ai-chat-block-peek-view
|
||||
.blockModel=${blockModel}
|
||||
@@ -693,5 +707,7 @@ export const AIChatBlockPeekViewTemplate = (
|
||||
.reasoningConfig=${reasoningConfig}
|
||||
.affineFeatureFlagService=${affineFeatureFlagService}
|
||||
.affineWorkspaceDialogService=${affineWorkspaceDialogService}
|
||||
.aiDraftService=${aiDraftService}
|
||||
.aiToolsConfigService=${aiToolsConfigService}
|
||||
></ai-chat-block-peek-view>`;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
|
||||
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
|
||||
import type { UserFriendlyError } from '@affine/error';
|
||||
import {
|
||||
addContextCategoryMutation,
|
||||
@@ -415,6 +416,7 @@ export class CopilotClient {
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
signal,
|
||||
}: {
|
||||
sessionId: string;
|
||||
@@ -422,6 +424,7 @@ export class CopilotClient {
|
||||
reasoning?: boolean;
|
||||
webSearch?: boolean;
|
||||
modelId?: string;
|
||||
toolsConfig?: AIToolsConfig;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
let url = `/api/copilot/chat/${sessionId}`;
|
||||
@@ -430,6 +433,7 @@ export class CopilotClient {
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
});
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
@@ -446,12 +450,14 @@ export class CopilotClient {
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
}: {
|
||||
sessionId: string;
|
||||
messageId?: string;
|
||||
reasoning?: boolean;
|
||||
webSearch?: boolean;
|
||||
modelId?: string;
|
||||
toolsConfig?: AIToolsConfig;
|
||||
},
|
||||
endpoint = Endpoint.Stream
|
||||
) {
|
||||
@@ -461,6 +467,7 @@ export class CopilotClient {
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
});
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
@@ -486,7 +493,9 @@ export class CopilotClient {
|
||||
return this.eventSource(url);
|
||||
}
|
||||
|
||||
paramsToQueryString(params: Record<string, string | boolean | undefined>) {
|
||||
paramsToQueryString(
|
||||
params: Record<string, string | boolean | undefined | Record<string, any>>
|
||||
) {
|
||||
const queryString = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (typeof value === 'boolean') {
|
||||
@@ -495,6 +504,8 @@ export class CopilotClient {
|
||||
}
|
||||
} else if (typeof value === 'string') {
|
||||
queryString.append(key, value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
queryString.append(key, JSON.stringify(value));
|
||||
}
|
||||
});
|
||||
return queryString.toString();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
|
||||
import { partition } from 'lodash-es';
|
||||
|
||||
import { AIProvider } from './ai-provider';
|
||||
@@ -22,6 +23,7 @@ export type TextToTextOptions = {
|
||||
reasoning?: boolean;
|
||||
webSearch?: boolean;
|
||||
modelId?: string;
|
||||
toolsConfig?: AIToolsConfig;
|
||||
};
|
||||
|
||||
export type ToImageOptions = TextToTextOptions & {
|
||||
@@ -119,6 +121,7 @@ export function textToText({
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
}: TextToTextOptions) {
|
||||
let messageId: string | undefined;
|
||||
|
||||
@@ -141,6 +144,7 @@ export function textToText({
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
toolsConfig,
|
||||
},
|
||||
endpoint
|
||||
);
|
||||
|
||||
@@ -48,10 +48,24 @@ class MemberManager {
|
||||
selectedMemberId = signal<string | null>(null);
|
||||
|
||||
filteredMembers = computed(() => {
|
||||
return this.ops.userListService.users$.value.filter(
|
||||
member =>
|
||||
!member.removed && !this.selectedMembers.value.includes(member.id)
|
||||
);
|
||||
const isSearching = this.userListService.searchText$.value !== '';
|
||||
if (isSearching) {
|
||||
return this.ops.userListService.users$.value.filter(
|
||||
member =>
|
||||
!member.removed && !this.selectedMembers.value.includes(member.id)
|
||||
);
|
||||
} else {
|
||||
const currentUser = this.ops.userService.currentUserInfo$.value;
|
||||
return [
|
||||
...(currentUser ? [currentUser] : []),
|
||||
...this.ops.userListService.users$.value.filter(
|
||||
member => member.id !== currentUser?.id
|
||||
),
|
||||
].filter(
|
||||
member =>
|
||||
!member.removed && !this.selectedMembers.value.includes(member.id)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
constructor(private readonly ops: MemberManagerOptions) {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PublicUserService } from '@affine/core/modules/cloud';
|
||||
import { AuthService, PublicUserService } from '@affine/core/modules/cloud';
|
||||
import { MemberSearchService } from '@affine/core/modules/permissions';
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
@@ -31,10 +31,11 @@ export class CloudViewExtension extends ViewExtensionProvider<CloudViewOptions>
|
||||
}
|
||||
const memberSearchService = framework.get(MemberSearchService);
|
||||
const publicUserService = framework.get(PublicUserService);
|
||||
const authService = framework.get(AuthService);
|
||||
|
||||
context.register([
|
||||
patchUserListExtensions(memberSearchService),
|
||||
patchUserExtensions(publicUserService),
|
||||
patchUserExtensions(publicUserService, authService),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
import type { PublicUserService } from '@affine/core/modules/cloud';
|
||||
import type {
|
||||
AuthService,
|
||||
PublicUserService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { UserServiceExtension } from '@blocksuite/affine/shared/services';
|
||||
import {
|
||||
type AffineUserInfo,
|
||||
UserServiceExtension,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
|
||||
export function patchUserExtensions(publicUserService: PublicUserService) {
|
||||
export function patchUserExtensions(
|
||||
publicUserService: PublicUserService,
|
||||
authService: AuthService
|
||||
) {
|
||||
return UserServiceExtension({
|
||||
// eslint-disable-next-line rxjs/finnish
|
||||
currentUserInfo$: authService.session.account$.map(account => {
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: account.id,
|
||||
name: account.label,
|
||||
avatar: account.avatar,
|
||||
removed: false,
|
||||
} as AffineUserInfo;
|
||||
}).signal,
|
||||
// eslint-disable-next-line rxjs/finnish
|
||||
userInfo$(id) {
|
||||
return publicUserService.publicUser$(id).signal;
|
||||
|
||||
@@ -371,40 +371,47 @@ export const CardViewDoc = ({ docId }: DocListItemProps) => {
|
||||
const selectMode = useLiveData(contextValue.selectMode$);
|
||||
const docsService = useService(DocsService);
|
||||
const doc = useLiveData(docsService.list.doc$(docId));
|
||||
const showMoreOperation = useLiveData(contextValue.showMoreOperation$);
|
||||
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={styles.cardViewRoot}>
|
||||
<DragHandle id={docId} className={styles.cardDragHandle} />
|
||||
<header className={styles.cardViewHeader}>
|
||||
<DocIcon id={docId} className={styles.cardViewIcon} />
|
||||
<DocTitle
|
||||
id={docId}
|
||||
className={styles.cardViewTitle}
|
||||
data-testid="doc-list-item-title"
|
||||
/>
|
||||
{quickActions.map(action => {
|
||||
return (
|
||||
<Tooltip key={action.key} content={t.t(action.name)}>
|
||||
<action.Component size="16" doc={doc} />
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
{selectMode ? (
|
||||
<Select id={docId} className={styles.cardViewCheckbox} />
|
||||
) : (
|
||||
<MoreMenuButton
|
||||
docId={docId}
|
||||
contentOptions={cardMoreMenuContentOptions}
|
||||
iconProps={{ size: '16' }}
|
||||
<ContextMenu
|
||||
asChild
|
||||
disabled={!showMoreOperation}
|
||||
items={<MoreMenuContent docId={docId} />}
|
||||
>
|
||||
<li className={styles.cardViewRoot}>
|
||||
<DragHandle id={docId} className={styles.cardDragHandle} />
|
||||
<header className={styles.cardViewHeader}>
|
||||
<DocIcon id={docId} className={styles.cardViewIcon} />
|
||||
<DocTitle
|
||||
id={docId}
|
||||
className={styles.cardViewTitle}
|
||||
data-testid="doc-list-item-title"
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
<DocPreview id={docId} className={styles.cardPreviewContainer} />
|
||||
<CardViewProperties docId={docId} />
|
||||
</li>
|
||||
{quickActions.map(action => {
|
||||
return (
|
||||
<Tooltip key={action.key} content={t.t(action.name)}>
|
||||
<action.Component size="16" doc={doc} />
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
{selectMode ? (
|
||||
<Select id={docId} className={styles.cardViewCheckbox} />
|
||||
) : (
|
||||
<MoreMenuButton
|
||||
docId={docId}
|
||||
contentOptions={cardMoreMenuContentOptions}
|
||||
iconProps={{ size: '16' }}
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
<DocPreview id={docId} className={styles.cardPreviewContainer} />
|
||||
<CardViewProperties docId={docId} />
|
||||
</li>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -229,7 +229,7 @@ export const RootAppSidebar = memo((): ReactElement => {
|
||||
<NavigationPanelTags />
|
||||
<NavigationPanelCollections />
|
||||
<CollapsibleSection
|
||||
name="others"
|
||||
path={['others']}
|
||||
title={t['com.affine.rootAppSidebar.others']()}
|
||||
contentStyle={{ padding: '6px 8px 0 8px' }}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Button, Skeleton, Tooltip } from '@affine/component';
|
||||
import { Button, notify, Skeleton, Tooltip } from '@affine/component';
|
||||
import { Loading } from '@affine/component/ui/loading';
|
||||
import { useSystemOnline } from '@affine/core/components/hooks/use-system-online';
|
||||
import { useWorkspace } from '@affine/core/components/hooks/use-workspace';
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import type {
|
||||
WorkspaceMetadata,
|
||||
WorkspaceProfileInfo,
|
||||
import {
|
||||
type WorkspaceMetadata,
|
||||
type WorkspaceProfileInfo,
|
||||
WorkspacesService,
|
||||
} from '@affine/core/modules/workspace';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -21,13 +22,15 @@ import {
|
||||
TeamWorkspaceIcon,
|
||||
UnsyncIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { LiveData, useLiveData } from '@toeverything/infra';
|
||||
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import clsx from 'clsx';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { useAsyncCallback } from '../../hooks/affine-async-hooks';
|
||||
import { useCatchEventCallback } from '../../hooks/use-catch-event-hook';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import { WorkspaceAvatar } from '../../workspace-avatar';
|
||||
import * as styles from './styles.css';
|
||||
export { PureWorkspaceCard } from './pure-workspace-card';
|
||||
@@ -284,6 +287,8 @@ export const WorkspaceCard = forwardRef<
|
||||
) => {
|
||||
const t = useI18n();
|
||||
const information = useWorkspaceInfo(workspaceMetadata);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const navigate = useNavigateHelper();
|
||||
|
||||
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||
|
||||
@@ -291,10 +296,24 @@ export const WorkspaceCard = forwardRef<
|
||||
onClickEnableCloud?.(workspaceMetadata);
|
||||
}, [onClickEnableCloud, workspaceMetadata]);
|
||||
|
||||
const onRemoveWorkspace = useAsyncCallback(async () => {
|
||||
await workspacesService
|
||||
.deleteWorkspace(workspaceMetadata)
|
||||
.then(() => {
|
||||
notify.success({ title: t['Successfully removed workspace']() });
|
||||
navigate.jumpToIndex();
|
||||
})
|
||||
.catch(() => {
|
||||
notify.error({ title: t['Failed to remove workspace']() });
|
||||
});
|
||||
}, [workspacesService, workspaceMetadata, t, navigate]);
|
||||
|
||||
const onOpenSettings = useCatchEventCallback(() => {
|
||||
onClickOpenSettings?.(workspaceMetadata);
|
||||
}, [onClickOpenSettings, workspaceMetadata]);
|
||||
|
||||
console.log(information);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -337,6 +356,9 @@ export const WorkspaceCard = forwardRef<
|
||||
<Skeleton width={100} />
|
||||
)}
|
||||
</div>
|
||||
{information?.isEmpty && information.isOwner ? (
|
||||
<Button onClick={onRemoveWorkspace}>Remove</Button>
|
||||
) : null}
|
||||
<div className={styles.showOnCardHover}>
|
||||
{onClickEnableCloud && workspaceMetadata.flavour === 'local' ? (
|
||||
<Button
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { CategoryDivider } from '@affine/core/modules/app-sidebar/views';
|
||||
import {
|
||||
type CollapsibleSectionName,
|
||||
NavigationPanelService,
|
||||
} from '@affine/core/modules/navigation-panel';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
@@ -17,7 +14,7 @@ import {
|
||||
import { content, header, root } from './collapsible-section.css';
|
||||
|
||||
interface CollapsibleSectionProps extends PropsWithChildren {
|
||||
name: CollapsibleSectionName;
|
||||
path: string[];
|
||||
title: string;
|
||||
actions?: ReactNode;
|
||||
|
||||
@@ -33,7 +30,7 @@ interface CollapsibleSectionProps extends PropsWithChildren {
|
||||
}
|
||||
|
||||
export const CollapsibleSection = ({
|
||||
name,
|
||||
path,
|
||||
title,
|
||||
actions,
|
||||
children,
|
||||
@@ -48,15 +45,15 @@ export const CollapsibleSection = ({
|
||||
contentClassName,
|
||||
contentStyle,
|
||||
}: CollapsibleSectionProps) => {
|
||||
const section = useService(NavigationPanelService).sections[name];
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
|
||||
const collapsed = useLiveData(section.collapsed$);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
|
||||
const setCollapsed = useCallback(
|
||||
(v: boolean) => {
|
||||
section.setCollapsed(v);
|
||||
navigationPanelService.setCollapsed(path, v);
|
||||
},
|
||||
[section]
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@affine/core/modules/collection';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
@@ -47,6 +48,7 @@ export const NavigationPanelCollectionNode = ({
|
||||
operations: additionalOperations,
|
||||
canDrop,
|
||||
dropEffect,
|
||||
parentPath,
|
||||
}: {
|
||||
collectionId: string;
|
||||
} & GenericNavigationPanelNode) => {
|
||||
@@ -55,10 +57,21 @@ export const NavigationPanelCollectionNode = ({
|
||||
GlobalContextService,
|
||||
WorkspaceDialogService,
|
||||
});
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const active =
|
||||
useLiveData(globalContextService.globalContext.collectionId.$) ===
|
||||
collectionId;
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const path = useMemo(
|
||||
() => [...(parentPath ?? []), `collection-${collectionId}`],
|
||||
[parentPath, collectionId]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
|
||||
const collectionService = useService(CollectionService);
|
||||
const collection = useLiveData(collectionService.collection$(collectionId));
|
||||
@@ -160,7 +173,7 @@ export const NavigationPanelCollectionNode = ({
|
||||
|
||||
const handleOpenCollapsed = useCallback(() => {
|
||||
setCollapsed(false);
|
||||
}, []);
|
||||
}, [setCollapsed]);
|
||||
|
||||
const handleEditCollection = useCallback(() => {
|
||||
if (!collection) {
|
||||
@@ -217,15 +230,20 @@ export const NavigationPanelCollectionNode = ({
|
||||
dropEffect={handleDropEffectOnCollection}
|
||||
data-testid={`navigation-panel-collection-${collectionId}`}
|
||||
>
|
||||
<NavigationPanelCollectionNodeChildren collection={collection} />
|
||||
<NavigationPanelCollectionNodeChildren
|
||||
collection={collection}
|
||||
path={path}
|
||||
/>
|
||||
</NavigationPanelTreeNode>
|
||||
);
|
||||
};
|
||||
|
||||
const NavigationPanelCollectionNodeChildren = ({
|
||||
collection,
|
||||
path,
|
||||
}: {
|
||||
collection: Collection;
|
||||
path: string[];
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const { collectionService } = useServices({
|
||||
@@ -264,6 +282,7 @@ const NavigationPanelCollectionNodeChildren = ({
|
||||
at: 'navigation-panel:collection:filtered-docs',
|
||||
collectionId: collection.id,
|
||||
}}
|
||||
parentPath={path}
|
||||
operations={
|
||||
allowList.has(docId)
|
||||
? [
|
||||
|
||||
@@ -14,6 +14,7 @@ import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -46,6 +47,7 @@ export const NavigationPanelDocNode = ({
|
||||
canDrop,
|
||||
operations: additionalOperations,
|
||||
dropEffect,
|
||||
parentPath,
|
||||
}: {
|
||||
docId: string;
|
||||
isLinked?: boolean;
|
||||
@@ -67,11 +69,22 @@ export const NavigationPanelDocNode = ({
|
||||
FeatureFlagService,
|
||||
GuardService,
|
||||
});
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
const active =
|
||||
useLiveData(globalContextService.globalContext.docId.$) === docId;
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const path = useMemo(
|
||||
() => [...(parentPath ?? []), `doc-${docId}`],
|
||||
[parentPath, docId]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
const isCollapsed = appSettings.showLinkedDocInSidebar ? collapsed : true;
|
||||
|
||||
const docRecord = useLiveData(docsService.list.doc$(docId));
|
||||
@@ -227,7 +240,7 @@ export const NavigationPanelDocNode = ({
|
||||
openInfoModal: () => workspaceDialogService.open('doc-info', { docId }),
|
||||
openNodeCollapsed: () => setCollapsed(false),
|
||||
}),
|
||||
[docId, workspaceDialogService]
|
||||
[docId, setCollapsed, workspaceDialogService]
|
||||
)
|
||||
);
|
||||
|
||||
@@ -302,6 +315,7 @@ export const NavigationPanelDocNode = ({
|
||||
at: 'navigation-panel:doc:linked-docs',
|
||||
docId,
|
||||
}}
|
||||
parentPath={path}
|
||||
isLinked
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -13,6 +13,7 @@ import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/uti
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import {
|
||||
type FolderNode,
|
||||
OrganizeService,
|
||||
@@ -31,7 +32,7 @@ import {
|
||||
RemoveFolderIcon,
|
||||
TagsIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import { difference } from 'lodash-es';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
@@ -57,6 +58,7 @@ export const NavigationPanelFolderNode = ({
|
||||
dropEffect,
|
||||
canDrop,
|
||||
reorderable,
|
||||
parentPath,
|
||||
}: {
|
||||
defaultRenaming?: boolean;
|
||||
nodeId: string;
|
||||
@@ -104,6 +106,7 @@ export const NavigationPanelFolderNode = ({
|
||||
dropEffect={dropEffect}
|
||||
reorderable={reorderable}
|
||||
canDrop={canDrop}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
);
|
||||
} else if (type === 'doc') {
|
||||
@@ -117,6 +120,7 @@ export const NavigationPanelFolderNode = ({
|
||||
canDrop={canDrop}
|
||||
dropEffect={dropEffect}
|
||||
operations={additionalOperations}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
)
|
||||
);
|
||||
@@ -131,6 +135,7 @@ export const NavigationPanelFolderNode = ({
|
||||
reorderable={reorderable}
|
||||
dropEffect={dropEffect}
|
||||
operations={additionalOperations}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
)
|
||||
);
|
||||
@@ -145,6 +150,7 @@ export const NavigationPanelFolderNode = ({
|
||||
reorderable
|
||||
dropEffect={dropEffect}
|
||||
operations={additionalOperations}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
)
|
||||
);
|
||||
@@ -177,6 +183,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
canDrop,
|
||||
dropEffect,
|
||||
reorderable,
|
||||
parentPath,
|
||||
}: {
|
||||
defaultRenaming?: boolean;
|
||||
node: FolderNode;
|
||||
@@ -189,11 +196,22 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
FeatureFlagService,
|
||||
WorkspaceDialogService,
|
||||
});
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const name = useLiveData(node.name$);
|
||||
const enableEmojiIcon = useLiveData(
|
||||
featureFlagService.flags.enable_emoji_folder_icon.$
|
||||
);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const path = useMemo(
|
||||
() => [...(parentPath ?? []), `folder-${node.id}`],
|
||||
[parentPath, node.id]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
const [newFolderId, setNewFolderId] = useState<string | null>(null);
|
||||
|
||||
const { createPage } = usePageHelper(
|
||||
@@ -575,7 +593,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
target: 'doc',
|
||||
});
|
||||
setCollapsed(false);
|
||||
}, [createPage, node]);
|
||||
}, [createPage, node, setCollapsed]);
|
||||
|
||||
const handleCreateSubfolder = useCallback(() => {
|
||||
const newFolderId = node.createFolder(
|
||||
@@ -585,7 +603,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' });
|
||||
setCollapsed(false);
|
||||
setNewFolderId(newFolderId);
|
||||
}, [node, t]);
|
||||
}, [node, setCollapsed, t]);
|
||||
|
||||
const handleAddToFolder = useCallback(
|
||||
(type: 'doc' | 'collection' | 'tag') => {
|
||||
@@ -628,7 +646,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
target: type,
|
||||
});
|
||||
},
|
||||
[children, node, workspaceDialogService]
|
||||
[children, node, setCollapsed, workspaceDialogService]
|
||||
);
|
||||
|
||||
const folderOperations = useMemo(() => {
|
||||
@@ -761,14 +779,17 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
[t]
|
||||
);
|
||||
|
||||
const handleCollapsedChange = useCallback((collapsed: boolean) => {
|
||||
if (collapsed) {
|
||||
setNewFolderId(null); // reset new folder id to clear the renaming state
|
||||
setCollapsed(true);
|
||||
} else {
|
||||
setCollapsed(false);
|
||||
}
|
||||
}, []);
|
||||
const handleCollapsedChange = useCallback(
|
||||
(collapsed: boolean) => {
|
||||
if (collapsed) {
|
||||
setNewFolderId(null); // reset new folder id to clear the renaming state
|
||||
setCollapsed(true);
|
||||
} else {
|
||||
setCollapsed(false);
|
||||
}
|
||||
},
|
||||
[setCollapsed]
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigationPanelTreeNode
|
||||
@@ -804,6 +825,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
at: 'navigation-panel:organize:folder-node',
|
||||
nodeId: child.id as string,
|
||||
}}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
</NavigationPanelTreeNode>
|
||||
|
||||
@@ -4,14 +4,15 @@ import {
|
||||
toast,
|
||||
} from '@affine/component';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
NavigationPanelTreeNode,
|
||||
@@ -31,6 +32,7 @@ export const NavigationPanelTagNode = ({
|
||||
operations: additionalOperations,
|
||||
dropEffect,
|
||||
canDrop,
|
||||
parentPath,
|
||||
}: {
|
||||
tagId: string;
|
||||
} & GenericNavigationPanelNode) => {
|
||||
@@ -39,9 +41,20 @@ export const NavigationPanelTagNode = ({
|
||||
TagService,
|
||||
GlobalContextService,
|
||||
});
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const active =
|
||||
useLiveData(globalContextService.globalContext.tagId.$) === tagId;
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const path = useMemo(
|
||||
() => [...(parentPath ?? []), `tag-${tagId}`],
|
||||
[parentPath, tagId]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
|
||||
const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId));
|
||||
const tagColor = useLiveData(tagRecord?.color$);
|
||||
@@ -154,7 +167,7 @@ export const NavigationPanelTagNode = ({
|
||||
() => ({
|
||||
openNodeCollapsed: () => setCollapsed(false),
|
||||
}),
|
||||
[]
|
||||
[setCollapsed]
|
||||
)
|
||||
);
|
||||
|
||||
@@ -188,7 +201,7 @@ export const NavigationPanelTagNode = ({
|
||||
dropEffect={handleDropEffectOnTag}
|
||||
data-testid={`navigation-panel-tag-${tagId}`}
|
||||
>
|
||||
<NavigationPanelTagNodeDocs tag={tagRecord} />
|
||||
<NavigationPanelTagNodeDocs tag={tagRecord} path={path} />
|
||||
</NavigationPanelTreeNode>
|
||||
);
|
||||
};
|
||||
@@ -198,7 +211,13 @@ export const NavigationPanelTagNode = ({
|
||||
* so we split the tag node children into a separate component,
|
||||
* so it won't be rendered when the tag node is collapsed.
|
||||
*/
|
||||
export const NavigationPanelTagNodeDocs = ({ tag }: { tag: Tag }) => {
|
||||
export const NavigationPanelTagNodeDocs = ({
|
||||
tag,
|
||||
path,
|
||||
}: {
|
||||
tag: Tag;
|
||||
path: string[];
|
||||
}) => {
|
||||
const tagDocIds = useLiveData(tag.pageIds$);
|
||||
|
||||
return tagDocIds.map(docId => (
|
||||
@@ -209,6 +228,7 @@ export const NavigationPanelTagNodeDocs = ({ tag }: { tag: Tag }) => {
|
||||
location={{
|
||||
at: 'navigation-panel:tags:docs',
|
||||
}}
|
||||
parentPath={path}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
@@ -46,4 +46,9 @@ export interface GenericNavigationPanelNode {
|
||||
* The drop effect to be used when an element is dropped over the node.
|
||||
*/
|
||||
dropEffect?: NavigationPanelTreeNodeDropEffect;
|
||||
/**
|
||||
* The path segments to the parent node in the navigation tree.
|
||||
* Used to persist the node's collapsed/expanded state in cache storage.
|
||||
*/
|
||||
parentPath: string[];
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { AddCollectionIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
import { NavigationPanelCollectionNode } from '../../nodes/collection';
|
||||
@@ -22,10 +22,9 @@ export const NavigationPanelCollections = () => {
|
||||
WorkbenchService,
|
||||
NavigationPanelService,
|
||||
});
|
||||
const navigationPanelSection = navigationPanelService.sections.collections;
|
||||
const collections = useLiveData(collectionService.collections$);
|
||||
const { openPromptModal } = usePromptModal();
|
||||
|
||||
const path = useMemo(() => ['collections'], []);
|
||||
const handleCreateCollection = useCallback(() => {
|
||||
openPromptModal({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
@@ -49,20 +48,21 @@ export const NavigationPanelCollections = () => {
|
||||
type: 'collection',
|
||||
});
|
||||
workbenchService.workbench.openCollection(id);
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
},
|
||||
});
|
||||
}, [
|
||||
collectionService,
|
||||
navigationPanelSection,
|
||||
navigationPanelService,
|
||||
openPromptModal,
|
||||
path,
|
||||
t,
|
||||
workbenchService.workbench,
|
||||
]);
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="collections"
|
||||
path={path}
|
||||
testId="navigation-panel-collections"
|
||||
title={t['com.affine.rootAppSidebar.collections']()}
|
||||
actions={
|
||||
@@ -89,6 +89,7 @@ export const NavigationPanelCollections = () => {
|
||||
location={{
|
||||
at: 'navigation-panel:collection:list',
|
||||
}}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
</NavigationPanelTreeRoot>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { type MouseEventHandler, useCallback } from 'react';
|
||||
import { type MouseEventHandler, useCallback, useMemo } from 'react';
|
||||
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
import { NavigationPanelCollectionNode } from '../../nodes/collection';
|
||||
@@ -41,7 +41,7 @@ export const NavigationPanelFavorites = () => {
|
||||
NavigationPanelService,
|
||||
});
|
||||
|
||||
const navigationPanelSection = navigationPanelService.sections.favorites;
|
||||
const path = useMemo(() => ['favorites'], []);
|
||||
|
||||
const favorites = useLiveData(favoriteService.favoriteList.sortedList$);
|
||||
|
||||
@@ -71,10 +71,10 @@ export const NavigationPanelFavorites = () => {
|
||||
track.$.navigationPanel.favorites.drop({
|
||||
type: data.source.data.entity.type,
|
||||
});
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
}
|
||||
},
|
||||
[navigationPanelSection, favoriteService.favoriteList]
|
||||
[navigationPanelService, favoriteService.favoriteList, path]
|
||||
);
|
||||
|
||||
const handleCreateNewFavoriteDoc: MouseEventHandler = useCallback(
|
||||
@@ -85,9 +85,9 @@ export const NavigationPanelFavorites = () => {
|
||||
newDoc.id,
|
||||
favoriteService.favoriteList.indexAt('before')
|
||||
);
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
},
|
||||
[createPage, navigationPanelSection, favoriteService.favoriteList]
|
||||
[createPage, navigationPanelService, favoriteService.favoriteList, path]
|
||||
);
|
||||
|
||||
const handleOnChildrenDrop = useCallback(
|
||||
@@ -162,7 +162,7 @@ export const NavigationPanelFavorites = () => {
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="favorites"
|
||||
path={path}
|
||||
title={t['com.affine.rootAppSidebar.favorites']()}
|
||||
headerRef={dropTargetRef}
|
||||
testId="navigation-panel-favorites"
|
||||
@@ -202,6 +202,7 @@ export const NavigationPanelFavorites = () => {
|
||||
key={favorite.id}
|
||||
favorite={favorite}
|
||||
onDrop={handleOnChildrenDrop}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
</NavigationPanelTreeRoot>
|
||||
@@ -215,11 +216,13 @@ const childLocation = {
|
||||
const NavigationPanelFavoriteNode = ({
|
||||
favorite,
|
||||
onDrop,
|
||||
parentPath,
|
||||
}: {
|
||||
favorite: {
|
||||
id: string;
|
||||
type: FavoriteSupportTypeUnion;
|
||||
};
|
||||
parentPath: string[];
|
||||
onDrop: (
|
||||
favorite: {
|
||||
id: string;
|
||||
@@ -242,6 +245,7 @@ const NavigationPanelFavoriteNode = ({
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={favoriteChildrenDropEffect}
|
||||
canDrop={favoriteChildrenCanDrop}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
) : favorite.type === 'tag' ? (
|
||||
<NavigationPanelTagNode
|
||||
@@ -251,6 +255,7 @@ const NavigationPanelFavoriteNode = ({
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={favoriteChildrenDropEffect}
|
||||
canDrop={favoriteChildrenCanDrop}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
) : favorite.type === 'folder' ? (
|
||||
<NavigationPanelFolderNode
|
||||
@@ -260,6 +265,7 @@ const NavigationPanelFavoriteNode = ({
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={favoriteChildrenDropEffect}
|
||||
canDrop={favoriteChildrenCanDrop}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
) : (
|
||||
<NavigationPanelCollectionNode
|
||||
@@ -269,6 +275,7 @@ const NavigationPanelFavoriteNode = ({
|
||||
onDrop={handleOnChildrenDrop}
|
||||
dropEffect={favoriteChildrenDropEffect}
|
||||
canDrop={favoriteChildrenCanDrop}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Trans, useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { BroomIcon, HelpIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
import { NavigationPanelCollectionNode } from '../../nodes/collection';
|
||||
@@ -25,6 +25,7 @@ export const NavigationPanelMigrationFavorites = () => {
|
||||
const trashDocs = useLiveData(docsService.list.trashDocs$);
|
||||
const migrated = useLiveData(migrationFavoriteItemsAdapter.migrated$);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const path = useMemo(() => ['migration-favorites'], []);
|
||||
|
||||
const favorites = useLiveData(
|
||||
migrationFavoriteItemsAdapter.favorites$.map(favs => {
|
||||
@@ -99,7 +100,7 @@ export const NavigationPanelMigrationFavorites = () => {
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="migrationFavorites"
|
||||
path={path}
|
||||
className={styles.container}
|
||||
title={t['com.affine.rootAppSidebar.migration-data']()}
|
||||
actions={
|
||||
@@ -126,6 +127,7 @@ export const NavigationPanelMigrationFavorites = () => {
|
||||
<NavigationPanelMigrationFavoriteNode
|
||||
key={favorite.id + ':' + i}
|
||||
favorite={favorite}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
</NavigationPanelTreeRoot>
|
||||
@@ -138,11 +140,13 @@ const childLocation = {
|
||||
};
|
||||
const NavigationPanelMigrationFavoriteNode = ({
|
||||
favorite,
|
||||
parentPath,
|
||||
}: {
|
||||
favorite: {
|
||||
id: string;
|
||||
type: 'collection' | 'doc';
|
||||
};
|
||||
parentPath: string[];
|
||||
}) => {
|
||||
return favorite.type === 'doc' ? (
|
||||
<NavigationPanelDocNode
|
||||
@@ -151,6 +155,7 @@ const NavigationPanelMigrationFavoriteNode = ({
|
||||
location={childLocation}
|
||||
reorderable={false}
|
||||
canDrop={false}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
) : (
|
||||
<NavigationPanelCollectionNode
|
||||
@@ -159,6 +164,7 @@ const NavigationPanelMigrationFavoriteNode = ({
|
||||
location={childLocation}
|
||||
reorderable={false}
|
||||
canDrop={false}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,10 +27,9 @@ export const NavigationPanelOrganize = () => {
|
||||
OrganizeService,
|
||||
NavigationPanelService,
|
||||
});
|
||||
const navigationPanelSection = navigationPanelService.sections.organize;
|
||||
const collapsed = useLiveData(navigationPanelSection.collapsed$);
|
||||
const path = useMemo(() => ['organize'], []);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const [newFolderId, setNewFolderId] = useState<string | null>(null);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const folderTree = organizeService.folderTree;
|
||||
@@ -46,9 +45,9 @@ export const NavigationPanelOrganize = () => {
|
||||
);
|
||||
track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' });
|
||||
setNewFolderId(newFolderId);
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
return newFolderId;
|
||||
}, [navigationPanelSection, rootFolder]);
|
||||
}, [navigationPanelService, path, rootFolder]);
|
||||
|
||||
const handleOnChildrenDrop = useCallback(
|
||||
(data: DropTargetDropEvent<AffineDNDData>, node?: FolderNode) => {
|
||||
@@ -105,7 +104,7 @@ export const NavigationPanelOrganize = () => {
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="organize"
|
||||
path={path}
|
||||
title={t['com.affine.rootAppSidebar.organize']()}
|
||||
actions={
|
||||
<IconButton
|
||||
@@ -141,6 +140,7 @@ export const NavigationPanelOrganize = () => {
|
||||
at: 'navigation-panel:organize:folder-node',
|
||||
nodeId: child.id as string,
|
||||
}}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
</NavigationPanelTreeRoot>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { AddTagIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
import { NavigationPanelTagNode } from '../../nodes/tag';
|
||||
@@ -19,8 +19,8 @@ export const NavigationPanelTags = () => {
|
||||
TagService,
|
||||
NavigationPanelService,
|
||||
});
|
||||
const navigationPanelSection = navigationPanelService.sections.tags;
|
||||
const collapsed = useLiveData(navigationPanelSection.collapsed$);
|
||||
const path = useMemo(() => ['tags'], []);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const [creating, setCreating] = useState(false);
|
||||
const tags = useLiveData(tagService.tagList.tags$);
|
||||
|
||||
@@ -30,9 +30,9 @@ export const NavigationPanelTags = () => {
|
||||
(name: string) => {
|
||||
tagService.tagList.createTag(name, tagService.randomTagColor());
|
||||
track.$.navigationPanel.organize.createOrganizeItem({ type: 'tag' });
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
},
|
||||
[navigationPanelSection, tagService]
|
||||
[navigationPanelService, path, tagService]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -45,7 +45,7 @@ export const NavigationPanelTags = () => {
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="tags"
|
||||
path={path}
|
||||
testId="navigation-panel-tags"
|
||||
headerClassName={styles.draggedOverHighlight}
|
||||
title={t['com.affine.rootAppSidebar.tags']()}
|
||||
@@ -81,6 +81,7 @@ export const NavigationPanelTags = () => {
|
||||
location={{
|
||||
at: 'navigation-panel:tags:list',
|
||||
}}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
</NavigationPanelTreeRoot>
|
||||
|
||||
@@ -147,9 +147,10 @@ export const Component = ({
|
||||
}, [desktopApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (listIsLoading || list.length > 0) {
|
||||
if (listIsLoading || list.length > 0 || !enableLocalWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
createFirstAppData(workspacesService)
|
||||
.then(createdWorkspace => {
|
||||
if (createdWorkspace) {
|
||||
@@ -177,6 +178,7 @@ export const Component = ({
|
||||
loggedIn,
|
||||
listIsLoading,
|
||||
list,
|
||||
enableLocalWorkspace,
|
||||
]);
|
||||
|
||||
if (navigating || creating) {
|
||||
|
||||
@@ -11,7 +11,10 @@ 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 {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import {
|
||||
EventSourceService,
|
||||
FetchService,
|
||||
@@ -223,6 +226,7 @@ export const Component = () => {
|
||||
confirmModal.openConfirmModal
|
||||
);
|
||||
content.aiDraftService = framework.get(AIDraftService);
|
||||
content.aiToolsConfigService = framework.get(AIToolsConfigService);
|
||||
content.createSession = createSession;
|
||||
content.onOpenDoc = onOpenDoc;
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ 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 {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} 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';
|
||||
@@ -97,6 +100,8 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||
confirmModal.openConfirmModal
|
||||
);
|
||||
chatPanelRef.current.aiDraftService = framework.get(AIDraftService);
|
||||
chatPanelRef.current.aiToolsConfigService =
|
||||
framework.get(AIToolsConfigService);
|
||||
|
||||
containerRef.current?.append(chatPanelRef.current);
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
type CollapsibleSectionName,
|
||||
NavigationPanelService,
|
||||
} from '@affine/core/modules/navigation-panel';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import { ToggleRightIcon } from '@blocksuite/icons/rc';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -22,7 +19,7 @@ import {
|
||||
} from './collapsible-section.css';
|
||||
|
||||
interface CollapsibleSectionProps extends HTMLAttributes<HTMLDivElement> {
|
||||
name: CollapsibleSectionName;
|
||||
path: string[];
|
||||
title: string;
|
||||
actions?: ReactNode;
|
||||
testId?: string;
|
||||
@@ -76,7 +73,7 @@ const CollapsibleSectionTrigger = forwardRef<
|
||||
});
|
||||
|
||||
export const CollapsibleSection = ({
|
||||
name,
|
||||
path,
|
||||
title,
|
||||
actions,
|
||||
testId,
|
||||
@@ -86,12 +83,12 @@ export const CollapsibleSection = ({
|
||||
children,
|
||||
...attrs
|
||||
}: CollapsibleSectionProps) => {
|
||||
const section = useService(NavigationPanelService).sections[name];
|
||||
const collapsed = useLiveData(section.collapsed$);
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
|
||||
const setCollapsed = useCallback(
|
||||
(v: boolean) => section.setCollapsed(v),
|
||||
[section]
|
||||
(v: boolean) => navigationPanelService.setCollapsed(path, v),
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
} from '@affine/core/modules/collection';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { FilterMinusIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
|
||||
@@ -26,9 +27,11 @@ const CollectionIcon = () => <ViewLayersIcon />;
|
||||
export const NavigationPanelCollectionNode = ({
|
||||
collectionId,
|
||||
operations: additionalOperations,
|
||||
parentPath,
|
||||
}: {
|
||||
collectionId: string;
|
||||
operations?: NodeOperation[];
|
||||
parentPath: string[];
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const { globalContextService, collectionService, workspaceDialogService } =
|
||||
@@ -37,17 +40,28 @@ export const NavigationPanelCollectionNode = ({
|
||||
CollectionService,
|
||||
WorkspaceDialogService,
|
||||
});
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const active =
|
||||
useLiveData(globalContextService.globalContext.collectionId.$) ===
|
||||
collectionId;
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const path = useMemo(
|
||||
() => [...parentPath, `collection-${collectionId}`],
|
||||
[parentPath, collectionId]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
|
||||
const collection = useLiveData(collectionService.collection$(collectionId));
|
||||
const name = useLiveData(collection?.name$);
|
||||
|
||||
const handleOpenCollapsed = useCallback(() => {
|
||||
setCollapsed(false);
|
||||
}, []);
|
||||
}, [setCollapsed]);
|
||||
|
||||
const handleEditCollection = useCallback(() => {
|
||||
if (!collection) {
|
||||
@@ -95,6 +109,7 @@ export const NavigationPanelCollectionNode = ({
|
||||
<NavigationPanelCollectionNodeChildren
|
||||
collection={collection}
|
||||
onAddDoc={handleAddDocToCollection}
|
||||
path={path}
|
||||
/>
|
||||
</NavigationPanelTreeNode>
|
||||
);
|
||||
@@ -103,9 +118,11 @@ export const NavigationPanelCollectionNode = ({
|
||||
const NavigationPanelCollectionNodeChildren = ({
|
||||
collection,
|
||||
onAddDoc,
|
||||
path,
|
||||
}: {
|
||||
collection: Collection;
|
||||
onAddDoc?: () => void;
|
||||
path: string[];
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const { shareDocsListService, collectionService } = useServices({
|
||||
@@ -147,6 +164,7 @@ const NavigationPanelCollectionNodeChildren = ({
|
||||
<NavigationPanelDocNode
|
||||
key={docId}
|
||||
docId={docId}
|
||||
parentPath={path}
|
||||
operations={
|
||||
allowList
|
||||
? [
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
LiveData,
|
||||
@@ -29,10 +30,12 @@ export const NavigationPanelDocNode = ({
|
||||
docId,
|
||||
isLinked,
|
||||
operations: additionalOperations,
|
||||
parentPath,
|
||||
}: {
|
||||
docId: string;
|
||||
isLinked?: boolean;
|
||||
operations?: NodeOperation[];
|
||||
parentPath: string[];
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const {
|
||||
@@ -48,9 +51,20 @@ export const NavigationPanelDocNode = ({
|
||||
DocDisplayMetaService,
|
||||
FeatureFlagService,
|
||||
});
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const active =
|
||||
useLiveData(globalContextService.globalContext.docId.$) === docId;
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const path = useMemo(
|
||||
() => [...parentPath, `doc-${docId}`],
|
||||
[parentPath, docId]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
|
||||
const docRecord = useLiveData(docsService.list.doc$(docId));
|
||||
const DocIcon = useLiveData(
|
||||
@@ -103,7 +117,7 @@ export const NavigationPanelDocNode = ({
|
||||
openInfoModal: () => workspaceDialogService.open('doc-info', { docId }),
|
||||
openNodeCollapsed: () => setCollapsed(false),
|
||||
}),
|
||||
[docId, workspaceDialogService]
|
||||
[docId, setCollapsed, workspaceDialogService]
|
||||
);
|
||||
const operations = useNavigationPanelDocNodeOperationsMenu(docId, option);
|
||||
const { handleAddLinkedPage } = useNavigationPanelDocNodeOperations(
|
||||
@@ -150,6 +164,7 @@ export const NavigationPanelDocNode = ({
|
||||
key={`${child.docId}-${index}`}
|
||||
docId={child.docId}
|
||||
isLinked
|
||||
parentPath={path}
|
||||
/>
|
||||
))
|
||||
: null
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import {
|
||||
type FolderNode,
|
||||
OrganizeService,
|
||||
@@ -31,9 +32,9 @@ import {
|
||||
RemoveFolderIcon,
|
||||
TagsIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import { difference } from 'lodash-es';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
|
||||
import { NavigationPanelTreeNode } from '../../tree/node';
|
||||
@@ -46,11 +47,13 @@ import { FavoriteFolderOperation } from './operations';
|
||||
export const NavigationPanelFolderNode = ({
|
||||
nodeId,
|
||||
operations,
|
||||
parentPath,
|
||||
}: {
|
||||
nodeId: string;
|
||||
operations?:
|
||||
| NodeOperation[]
|
||||
| ((type: string, node: FolderNode) => NodeOperation[]);
|
||||
parentPath: string[];
|
||||
}) => {
|
||||
const { organizeService } = useServices({
|
||||
OrganizeService,
|
||||
@@ -78,24 +81,34 @@ export const NavigationPanelFolderNode = ({
|
||||
<NavigationPanelFolderNodeFolder
|
||||
node={node}
|
||||
operations={additionalOperations}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!data) return null;
|
||||
if (type === 'doc') {
|
||||
return (
|
||||
<NavigationPanelDocNode docId={data} operations={additionalOperations} />
|
||||
<NavigationPanelDocNode
|
||||
docId={data}
|
||||
operations={additionalOperations}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
);
|
||||
} else if (type === 'collection') {
|
||||
return (
|
||||
<NavigationPanelCollectionNode
|
||||
collectionId={data}
|
||||
operations={additionalOperations}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
);
|
||||
} else if (type === 'tag') {
|
||||
return (
|
||||
<NavigationPanelTagNode tagId={data} operations={additionalOperations} />
|
||||
<NavigationPanelTagNode
|
||||
tagId={data}
|
||||
operations={additionalOperations}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,9 +132,11 @@ const NavigationPanelFolderIcon: NavigationPanelTreeNodeIcon = ({
|
||||
const NavigationPanelFolderNodeFolder = ({
|
||||
node,
|
||||
operations: additionalOperations,
|
||||
parentPath,
|
||||
}: {
|
||||
node: FolderNode;
|
||||
operations?: NodeOperation[];
|
||||
parentPath: string[];
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const { workspaceService, featureFlagService, workspaceDialogService } =
|
||||
@@ -135,7 +150,18 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
const enableEmojiIcon = useLiveData(
|
||||
featureFlagService.flags.enable_emoji_folder_icon.$
|
||||
);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const path = useMemo(
|
||||
() => [...parentPath, `folder-${node.id}`],
|
||||
[parentPath, node.id]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
|
||||
const { createPage } = usePageHelper(
|
||||
workspaceService.workspace.docCollection
|
||||
@@ -171,7 +197,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
target: 'doc',
|
||||
});
|
||||
setCollapsed(false);
|
||||
}, [createPage, node]);
|
||||
}, [createPage, node, setCollapsed]);
|
||||
|
||||
const handleCreateSubfolder = useCallback(
|
||||
(name: string) => {
|
||||
@@ -179,7 +205,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' });
|
||||
setCollapsed(false);
|
||||
},
|
||||
[node]
|
||||
[node, setCollapsed]
|
||||
);
|
||||
|
||||
const handleAddToFolder = useCallback(
|
||||
@@ -223,7 +249,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
target: type,
|
||||
});
|
||||
},
|
||||
[children, node, workspaceDialogService]
|
||||
[children, node, setCollapsed, workspaceDialogService]
|
||||
);
|
||||
|
||||
const createSubTipRenderer = useCallback(
|
||||
@@ -388,13 +414,16 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
[t]
|
||||
);
|
||||
|
||||
const handleCollapsedChange = useCallback((collapsed: boolean) => {
|
||||
if (collapsed) {
|
||||
setCollapsed(true);
|
||||
} else {
|
||||
setCollapsed(false);
|
||||
}
|
||||
}, []);
|
||||
const handleCollapsedChange = useCallback(
|
||||
(collapsed: boolean) => {
|
||||
if (collapsed) {
|
||||
setCollapsed(true);
|
||||
} else {
|
||||
setCollapsed(false);
|
||||
}
|
||||
},
|
||||
[setCollapsed]
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigationPanelTreeNode
|
||||
@@ -413,6 +442,7 @@ const NavigationPanelFolderNodeFolder = ({
|
||||
key={child.id}
|
||||
nodeId={child.id as string}
|
||||
operations={childrenOperations}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
<AddItemPlaceholder
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { NodeOperation } from '@affine/core/desktop/components/navigation-panel';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
|
||||
import { NavigationPanelTreeNode } from '../../tree/node';
|
||||
@@ -19,18 +20,31 @@ import * as styles from './styles.css';
|
||||
export const NavigationPanelTagNode = ({
|
||||
tagId,
|
||||
operations: additionalOperations,
|
||||
parentPath,
|
||||
}: {
|
||||
tagId: string;
|
||||
operations?: NodeOperation[];
|
||||
parentPath: string[];
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const { tagService, globalContextService } = useServices({
|
||||
TagService,
|
||||
GlobalContextService,
|
||||
});
|
||||
const navigationPanelService = useService(NavigationPanelService);
|
||||
const active =
|
||||
useLiveData(globalContextService.globalContext.tagId.$) === tagId;
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const path = useMemo(
|
||||
() => [...parentPath, `tag-${tagId}`],
|
||||
[parentPath, tagId]
|
||||
);
|
||||
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
|
||||
const setCollapsed = useCallback(
|
||||
(value: boolean) => {
|
||||
navigationPanelService.setCollapsed(path, value);
|
||||
},
|
||||
[navigationPanelService, path]
|
||||
);
|
||||
|
||||
const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId));
|
||||
const tagColor = useLiveData(tagRecord?.color$);
|
||||
@@ -57,7 +71,7 @@ export const NavigationPanelTagNode = ({
|
||||
() => ({
|
||||
openNodeCollapsed: () => setCollapsed(false),
|
||||
}),
|
||||
[]
|
||||
[setCollapsed]
|
||||
);
|
||||
const operations = useNavigationPanelTagNodeOperationsMenu(tagId, option);
|
||||
const { handleNewDoc } = useNavigationPanelTagNodeOperations(tagId, option);
|
||||
@@ -86,7 +100,11 @@ export const NavigationPanelTagNode = ({
|
||||
aria-label={tagName}
|
||||
data-role="navigation-panel-tag"
|
||||
>
|
||||
<NavigationPanelTagNodeDocs tag={tagRecord} onNewDoc={handleNewDoc} />
|
||||
<NavigationPanelTagNodeDocs
|
||||
tag={tagRecord}
|
||||
onNewDoc={handleNewDoc}
|
||||
path={path}
|
||||
/>
|
||||
</NavigationPanelTreeNode>
|
||||
);
|
||||
};
|
||||
@@ -99,9 +117,11 @@ export const NavigationPanelTagNode = ({
|
||||
export const NavigationPanelTagNodeDocs = ({
|
||||
tag,
|
||||
onNewDoc,
|
||||
path,
|
||||
}: {
|
||||
tag: Tag;
|
||||
onNewDoc?: () => void;
|
||||
path: string[];
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const tagDocIds = useLiveData(tag.pageIds$);
|
||||
@@ -109,7 +129,7 @@ export const NavigationPanelTagNodeDocs = ({
|
||||
return (
|
||||
<>
|
||||
{tagDocIds.map(docId => (
|
||||
<NavigationPanelDocNode key={docId} docId={docId} />
|
||||
<NavigationPanelDocNode key={docId} docId={docId} parentPath={path} />
|
||||
))}
|
||||
<AddItemPlaceholder label={t['New Page']()} onClick={onNewDoc} />
|
||||
</>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { AddCollectionIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
@@ -22,7 +22,7 @@ export const NavigationPanelCollections = () => {
|
||||
WorkbenchService,
|
||||
NavigationPanelService,
|
||||
});
|
||||
const navigationPanelSection = navigationPanelService.sections.collections;
|
||||
const path = useMemo(() => ['collections'], []);
|
||||
const collectionMetas = useLiveData(collectionService.collectionMetas$);
|
||||
const { openPromptModal } = usePromptModal();
|
||||
|
||||
@@ -49,12 +49,13 @@ export const NavigationPanelCollections = () => {
|
||||
type: 'collection',
|
||||
});
|
||||
workbenchService.workbench.openCollection(id);
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
},
|
||||
});
|
||||
}, [
|
||||
collectionService,
|
||||
navigationPanelSection,
|
||||
navigationPanelService,
|
||||
path,
|
||||
openPromptModal,
|
||||
t,
|
||||
workbenchService.workbench,
|
||||
@@ -62,7 +63,7 @@ export const NavigationPanelCollections = () => {
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="collections"
|
||||
path={path}
|
||||
testId="navigation-panel-collections"
|
||||
title={t['com.affine.rootAppSidebar.collections']()}
|
||||
>
|
||||
@@ -71,6 +72,7 @@ export const NavigationPanelCollections = () => {
|
||||
<NavigationPanelCollectionNode
|
||||
key={collection.id}
|
||||
collectionId={collection.id}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
<AddItemPlaceholder
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
@@ -24,7 +24,7 @@ export const NavigationPanelFavorites = () => {
|
||||
});
|
||||
|
||||
const t = useI18n();
|
||||
const navigationPanelSection = navigationPanelService.sections.favorites;
|
||||
const path = useMemo(() => ['favorites'], []);
|
||||
const favorites = useLiveData(favoriteService.favoriteList.sortedList$);
|
||||
const isLoading = useLiveData(favoriteService.favoriteList.isLoading$);
|
||||
const { createPage } = usePageHelper(
|
||||
@@ -38,19 +38,23 @@ export const NavigationPanelFavorites = () => {
|
||||
newDoc.id,
|
||||
favoriteService.favoriteList.indexAt('before')
|
||||
);
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
}, [createPage, navigationPanelSection, favoriteService.favoriteList]);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
}, [createPage, favoriteService.favoriteList, navigationPanelService, path]);
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="favorites"
|
||||
path={path}
|
||||
title={t['com.affine.rootAppSidebar.favorites']()}
|
||||
testId="navigation-panel-favorites"
|
||||
headerTestId="navigation-panel-favorite-category-divider"
|
||||
>
|
||||
<NavigationPanelTreeRoot placeholder={isLoading ? 'Loading' : null}>
|
||||
{favorites.map(favorite => (
|
||||
<FavoriteNode key={favorite.id} favorite={favorite} />
|
||||
<FavoriteNode
|
||||
key={favorite.id}
|
||||
favorite={favorite}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
<AddItemPlaceholder
|
||||
data-testid="navigation-panel-bar-add-favorite-button"
|
||||
@@ -66,19 +70,24 @@ export const NavigationPanelFavorites = () => {
|
||||
|
||||
export const FavoriteNode = ({
|
||||
favorite,
|
||||
parentPath,
|
||||
}: {
|
||||
favorite: {
|
||||
id: string;
|
||||
type: FavoriteSupportTypeUnion;
|
||||
};
|
||||
parentPath: string[];
|
||||
}) => {
|
||||
return favorite.type === 'doc' ? (
|
||||
<NavigationPanelDocNode docId={favorite.id} />
|
||||
<NavigationPanelDocNode docId={favorite.id} parentPath={parentPath} />
|
||||
) : favorite.type === 'tag' ? (
|
||||
<NavigationPanelTagNode tagId={favorite.id} />
|
||||
<NavigationPanelTagNode tagId={favorite.id} parentPath={parentPath} />
|
||||
) : favorite.type === 'folder' ? (
|
||||
<NavigationPanelFolderNode nodeId={favorite.id} />
|
||||
<NavigationPanelFolderNode nodeId={favorite.id} parentPath={parentPath} />
|
||||
) : (
|
||||
<NavigationPanelCollectionNode collectionId={favorite.id} />
|
||||
<NavigationPanelCollectionNode
|
||||
collectionId={favorite.id}
|
||||
parentPath={parentPath}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { AddOrganizeIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
@@ -18,7 +18,7 @@ export const NavigationPanelOrganize = () => {
|
||||
OrganizeService,
|
||||
NavigationPanelService,
|
||||
});
|
||||
const navigationPanelSection = navigationPanelService.sections.organize;
|
||||
const path = useMemo(() => ['organize'], []);
|
||||
const [openNewFolderDialog, setOpenNewFolderDialog] = useState(false);
|
||||
|
||||
const t = useI18n();
|
||||
@@ -36,15 +36,15 @@ export const NavigationPanelOrganize = () => {
|
||||
rootFolder.indexAt('before')
|
||||
);
|
||||
track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' });
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
return newFolderId;
|
||||
},
|
||||
[navigationPanelSection, rootFolder]
|
||||
[navigationPanelService, path, rootFolder]
|
||||
);
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="organize"
|
||||
path={path}
|
||||
title={t['com.affine.rootAppSidebar.organize']()}
|
||||
>
|
||||
{/* TODO(@CatsJuice): Organize loading UI */}
|
||||
@@ -53,6 +53,7 @@ export const NavigationPanelOrganize = () => {
|
||||
<NavigationPanelFolderNode
|
||||
key={child.id}
|
||||
nodeId={child.id as string}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
<AddItemPlaceholder
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { AddTagIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
@@ -25,7 +25,7 @@ export const NavigationPanelTags = () => {
|
||||
TagService,
|
||||
NavigationPanelService,
|
||||
});
|
||||
const navigationPanelSection = navigationPanelService.sections.tags;
|
||||
const path = useMemo(() => ['tags'], []);
|
||||
const tags = useLiveData(tagService.tagList.tags$);
|
||||
const [showNewTagDialog, setShowNewTagDialog] = useState(false);
|
||||
|
||||
@@ -36,19 +36,23 @@ export const NavigationPanelTags = () => {
|
||||
setShowNewTagDialog(false);
|
||||
tagService.tagList.createTag(name, color);
|
||||
track.$.navigationPanel.organize.createOrganizeItem({ type: 'tag' });
|
||||
navigationPanelSection.setCollapsed(false);
|
||||
navigationPanelService.setCollapsed(path, false);
|
||||
},
|
||||
[navigationPanelSection, tagService]
|
||||
[navigationPanelService, path, tagService]
|
||||
);
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="tags"
|
||||
path={path}
|
||||
title={t['com.affine.rootAppSidebar.tags']()}
|
||||
>
|
||||
<NavigationPanelTreeRoot>
|
||||
{tags.map(tag => (
|
||||
<NavigationPanelTagNode key={tag.id} tagId={tag.id} />
|
||||
<NavigationPanelTagNode
|
||||
key={tag.id}
|
||||
tagId={tag.id}
|
||||
parentPath={path}
|
||||
/>
|
||||
))}
|
||||
<AddItemPlaceholder
|
||||
icon={<AddTagIcon />}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const RecentDocs = ({ max = 5 }: { max?: number }) => {
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="recent"
|
||||
path={['recent']}
|
||||
title="Recent"
|
||||
headerClassName={styles.header}
|
||||
className={styles.recentSection}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
export { AIButtonProvider } from './provider/ai-button';
|
||||
export { AIButtonService } from './services/ai-button';
|
||||
export { AIDraftService } from './services/ai-draft';
|
||||
export {
|
||||
type AIToolsConfig,
|
||||
AIToolsConfigService,
|
||||
} from './services/tools-config';
|
||||
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
@@ -13,6 +17,7 @@ import { AIDraftService } from './services/ai-draft';
|
||||
import { AINetworkSearchService } from './services/network-search';
|
||||
import { AIPlaygroundService } from './services/playground';
|
||||
import { AIReasoningService } from './services/reasoning';
|
||||
import { AIToolsConfigService } from './services/tools-config';
|
||||
|
||||
export const configureAIButtonModule = (framework: Framework) => {
|
||||
framework.service(AIButtonService, container => {
|
||||
@@ -40,3 +45,7 @@ export function configureAIDraftModule(framework: Framework) {
|
||||
.scope(WorkspaceScope)
|
||||
.service(AIDraftService, [GlobalStateService, CacheStorage]);
|
||||
}
|
||||
|
||||
export function configureAIToolsConfigModule(framework: Framework) {
|
||||
framework.service(AIToolsConfigService, [GlobalStateService]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
createSignalFromObservable,
|
||||
type Signal,
|
||||
} from '@blocksuite/affine/shared/utils';
|
||||
import { LiveData, Service } from '@toeverything/infra';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
import type { GlobalStateService } from '../../storage';
|
||||
|
||||
const AI_TOOLS_CONFIG_KEY = 'AIToolsConfig';
|
||||
|
||||
export interface AIToolsConfig {
|
||||
searchWorkspace?: boolean;
|
||||
readingDocs?: boolean;
|
||||
}
|
||||
|
||||
export class AIToolsConfigService extends Service {
|
||||
constructor(private readonly globalStateService: GlobalStateService) {
|
||||
super();
|
||||
|
||||
const { signal, cleanup: enabledCleanup } =
|
||||
createSignalFromObservable<AIToolsConfig>(this.config$, {
|
||||
searchWorkspace: true,
|
||||
readingDocs: true,
|
||||
});
|
||||
this.config = signal;
|
||||
this.disposables.push(enabledCleanup);
|
||||
}
|
||||
|
||||
config: Signal<AIToolsConfig>;
|
||||
|
||||
private readonly config$ = LiveData.from(
|
||||
this.globalStateService.globalState.watch<AIToolsConfig>(
|
||||
AI_TOOLS_CONFIG_KEY
|
||||
),
|
||||
undefined
|
||||
).pipe(
|
||||
map(config => ({
|
||||
searchWorkspace: config?.searchWorkspace ?? true,
|
||||
readingDocs: config?.readingDocs ?? true,
|
||||
}))
|
||||
);
|
||||
|
||||
setConfig = (data: Partial<AIToolsConfig>) => {
|
||||
this.globalStateService.globalState.set(AI_TOOLS_CONFIG_KEY, {
|
||||
...this.config.value,
|
||||
...data,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const navWrapperStyle = style({
|
||||
'&[data-has-border=true]': {
|
||||
borderRight: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
},
|
||||
'&[data-is-floating="true"]': {
|
||||
'&[data-is-floating="true"], &[data-is-electron="false"]': {
|
||||
backgroundColor: cssVarV2('layer/background/primary'),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ export class Collection extends Entity<{ id: string }> {
|
||||
},
|
||||
],
|
||||
})
|
||||
.pipe(map(result => result.groups.map(group => group.items).flat()));
|
||||
.pipe(map(result => result.groups.flatMap(group => group.items)));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
configureAINetworkSearchModule,
|
||||
configureAIPlaygroundModule,
|
||||
configureAIReasoningModule,
|
||||
configureAIToolsConfigModule,
|
||||
} from './ai-button';
|
||||
import { configureAppSidebarModule } from './app-sidebar';
|
||||
import { configAtMenuConfigModule } from './at-menu-config';
|
||||
@@ -112,6 +113,7 @@ export function configureCommonModules(framework: Framework) {
|
||||
configureAIPlaygroundModule(framework);
|
||||
configureAIButtonModule(framework);
|
||||
configureAIDraftModule(framework);
|
||||
configureAIToolsConfigModule(framework);
|
||||
configureTemplateDocModule(framework);
|
||||
configureBlobManagementModule(framework);
|
||||
configureMediaModule(framework);
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Entity, LiveData } from '@toeverything/infra';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
import type { GlobalCache } from '../../storage';
|
||||
import type { CollapsibleSectionName } from '../types';
|
||||
|
||||
const DEFAULT_COLLAPSABLE_STATE: Record<CollapsibleSectionName, boolean> = {
|
||||
recent: true,
|
||||
favorites: false,
|
||||
organize: false,
|
||||
collections: true,
|
||||
tags: true,
|
||||
favoritesOld: true,
|
||||
migrationFavorites: true,
|
||||
others: false,
|
||||
};
|
||||
|
||||
export class NavigationPanelSection extends Entity<{
|
||||
name: CollapsibleSectionName;
|
||||
}> {
|
||||
name: CollapsibleSectionName = this.props.name;
|
||||
key = `explorer.section.${this.name}`;
|
||||
defaultValue = DEFAULT_COLLAPSABLE_STATE[this.name];
|
||||
|
||||
constructor(private readonly globalCache: GlobalCache) {
|
||||
super();
|
||||
}
|
||||
|
||||
collapsed$ = LiveData.from(
|
||||
this.globalCache
|
||||
.watch<boolean>(this.key)
|
||||
.pipe(map(v => v ?? this.defaultValue)),
|
||||
this.defaultValue
|
||||
);
|
||||
|
||||
setCollapsed(collapsed: boolean) {
|
||||
this.globalCache.set(this.key, collapsed);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
import { type Framework } from '@toeverything/infra';
|
||||
|
||||
import { GlobalCache } from '../storage';
|
||||
import { WorkspaceScope } from '../workspace';
|
||||
import { NavigationPanelSection } from './entities/navigation-panel-section';
|
||||
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||
import { NavigationPanelService } from './services/navigation-panel';
|
||||
export { NavigationPanelService } from './services/navigation-panel';
|
||||
export type { CollapsibleSectionName } from './types';
|
||||
|
||||
export function configureNavigationPanelModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(NavigationPanelService)
|
||||
.entity(NavigationPanelSection, [GlobalCache]);
|
||||
.service(NavigationPanelService, [GlobalCache, WorkspaceService]);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
import { Service } from '@toeverything/infra';
|
||||
import { LiveData, Service } from '@toeverything/infra';
|
||||
|
||||
import { NavigationPanelSection } from '../entities/navigation-panel-section';
|
||||
import type { CollapsibleSectionName } from '../types';
|
||||
import type { GlobalCache } from '../../storage/providers/global';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
|
||||
const allSectionName: Array<CollapsibleSectionName> = [
|
||||
'recent', // mobile only
|
||||
'favorites',
|
||||
'organize',
|
||||
'collections',
|
||||
'tags',
|
||||
'favoritesOld',
|
||||
'migrationFavorites',
|
||||
'others',
|
||||
];
|
||||
const DEFAULT_COLLAPSABLE_STATE: Record<string, boolean> = {
|
||||
recent: true,
|
||||
favorites: false,
|
||||
organize: false,
|
||||
collections: true,
|
||||
tags: true,
|
||||
favoritesOld: true,
|
||||
migrationFavorites: true,
|
||||
others: false,
|
||||
};
|
||||
|
||||
export class NavigationPanelService extends Service {
|
||||
readonly sections = allSectionName.reduce(
|
||||
(prev, name) =>
|
||||
Object.assign(prev, {
|
||||
[name]: this.framework.createEntity(NavigationPanelSection, { name }),
|
||||
}),
|
||||
{} as Record<CollapsibleSectionName, NavigationPanelSection>
|
||||
);
|
||||
constructor(
|
||||
private readonly globalCache: GlobalCache,
|
||||
private readonly workspaceService: WorkspaceService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private readonly collapsedCache = new Map<string, LiveData<boolean>>();
|
||||
|
||||
collapsed$(path: string[]) {
|
||||
const pathKey = path.join(':');
|
||||
const key = `navigation:${this.workspaceService.workspace.id}:${pathKey}`;
|
||||
const cached$ = this.collapsedCache.get(key);
|
||||
if (!cached$) {
|
||||
const liveData$ = LiveData.from(
|
||||
this.globalCache.watch<boolean>(key),
|
||||
undefined
|
||||
).map(v => v ?? DEFAULT_COLLAPSABLE_STATE[pathKey] ?? true);
|
||||
this.collapsedCache.set(key, liveData$);
|
||||
return liveData$;
|
||||
}
|
||||
return cached$;
|
||||
}
|
||||
|
||||
setCollapsed(path: string[], collapsed: boolean) {
|
||||
const pathKey = path.join(':');
|
||||
const key = `navigation:${this.workspaceService.workspace.id}:${pathKey}`;
|
||||
this.globalCache.set(key, collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export type CollapsibleSectionName =
|
||||
| 'recent'
|
||||
| 'collections'
|
||||
| 'favorites'
|
||||
| 'tags'
|
||||
| 'organize'
|
||||
| 'favoritesOld'
|
||||
| 'migrationFavorites'
|
||||
| 'others';
|
||||
@@ -2,6 +2,10 @@ import { toReactNode } from '@affine/component';
|
||||
import { AIChatBlockPeekViewTemplate } from '@affine/core/blocksuite/ai';
|
||||
import type { AIChatBlockModel } from '@affine/core/blocksuite/ai/blocks/ai-chat-block/model/ai-chat-model';
|
||||
import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
|
||||
import {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
@@ -27,6 +31,8 @@ export const AIChatBlockPeekView = ({
|
||||
const framework = useFramework();
|
||||
const affineFeatureFlagService = framework.get(FeatureFlagService);
|
||||
const affineWorkspaceDialogService = framework.get(WorkspaceDialogService);
|
||||
const aiDraftService = framework.get(AIDraftService);
|
||||
const aiToolsConfigService = framework.get(AIToolsConfigService);
|
||||
|
||||
return useMemo(() => {
|
||||
const template = AIChatBlockPeekViewTemplate(
|
||||
@@ -37,7 +43,9 @@ export const AIChatBlockPeekView = ({
|
||||
networkSearchConfig,
|
||||
reasoningConfig,
|
||||
affineFeatureFlagService,
|
||||
affineWorkspaceDialogService
|
||||
affineWorkspaceDialogService,
|
||||
aiDraftService,
|
||||
aiToolsConfigService
|
||||
);
|
||||
return toReactNode(template);
|
||||
}, [
|
||||
@@ -49,5 +57,7 @@ export const AIChatBlockPeekView = ({
|
||||
reasoningConfig,
|
||||
affineFeatureFlagService,
|
||||
affineWorkspaceDialogService,
|
||||
aiDraftService,
|
||||
aiToolsConfigService,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -335,6 +335,10 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||
const localData = (await docStorage.getDoc(id))?.bin;
|
||||
const cloudData = (await cloudStorage.getDoc(id))?.bin;
|
||||
|
||||
const isEmpty = isEmptyUpdate(localData) && isEmptyUpdate(cloudData);
|
||||
|
||||
console.log('isEmpty', isEmpty, localData, cloudData);
|
||||
|
||||
docStorage.connection.disconnect();
|
||||
|
||||
const info = await this.getWorkspaceInfo(id, signal);
|
||||
@@ -344,6 +348,7 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||
isOwner: info.workspace.role === Permission.Owner,
|
||||
isAdmin: info.workspace.role === Permission.Admin,
|
||||
isTeam: info.workspace.team,
|
||||
isEmpty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -360,8 +365,10 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||
isOwner: info.workspace.role === Permission.Owner,
|
||||
isAdmin: info.workspace.role === Permission.Admin,
|
||||
isTeam: info.workspace.team,
|
||||
isEmpty,
|
||||
};
|
||||
}
|
||||
|
||||
async getWorkspaceBlob(id: string, blob: string): Promise<Blob | null> {
|
||||
const storage = new this.BlobStorageType({
|
||||
id: id,
|
||||
@@ -659,3 +666,13 @@ export class CloudWorkspaceFlavoursProvider
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function isEmptyUpdate(binary: Uint8Array | undefined) {
|
||||
if (!binary) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
binary.byteLength === 0 ||
|
||||
(binary.byteLength === 2 && binary[0] === 0 && binary[1] === 0)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface WorkspaceProfileInfo {
|
||||
isOwner?: boolean;
|
||||
isAdmin?: boolean;
|
||||
isTeam?: boolean;
|
||||
isEmpty?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +62,7 @@ export class WorkspaceProfile extends Entity<{ metadata: WorkspaceMetadata }> {
|
||||
}
|
||||
|
||||
private setProfile(info: WorkspaceProfileInfo) {
|
||||
console.log('setProfile', info, isEqual(this.profile$.value, info));
|
||||
if (isEqual(this.profile$.value, info)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,13 +19,7 @@ export class WorkspaceProfileCacheStore extends Store {
|
||||
}
|
||||
|
||||
const info = data as WorkspaceProfileInfo;
|
||||
return {
|
||||
avatar: info.avatar,
|
||||
name: info.name,
|
||||
isOwner: info.isOwner,
|
||||
isAdmin: info.isAdmin,
|
||||
isTeam: info.isTeam,
|
||||
};
|
||||
return info;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@ import stickerContent${id} from './stickers/${category}/Content/${sticker}';`,
|
||||
}
|
||||
|
||||
const importStatements = Object.values(data)
|
||||
.map(v => Object.values(v).map(v => v.importStatement))
|
||||
.flat()
|
||||
.flatMap(v => Object.values(v).map(v => v.importStatement))
|
||||
.join('\n');
|
||||
|
||||
const templates = `const templates = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user