Compare commits

..

1 Commits

Author SHA1 Message Date
DarkSky
28b939e6d8 chore(server): down scale service (#13367)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Reduced the number of deployment replicas for web, graphql, sync,
renderer, and doc components across all build types (stable, beta,
canary).

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-30 17:31:40 +08:00
102 changed files with 536 additions and 1510 deletions

View File

@@ -29,25 +29,25 @@ const isInternal = buildType === 'internal';
const replicaConfig = {
stable: {
web: 3,
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 3,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 3,
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 3,
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 3,
web: 2,
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 2,
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 2,
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 2,
},
beta: {
web: 2,
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 2,
sync: Number(process.env.BETA_SYNC_REPLICA) || 2,
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 2,
doc: Number(process.env.BETA_DOC_REPLICA) || 2,
web: 1,
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1,
sync: Number(process.env.BETA_SYNC_REPLICA) || 1,
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 1,
doc: Number(process.env.BETA_DOC_REPLICA) || 1,
},
canary: {
web: 2,
graphql: 2,
sync: 2,
renderer: 2,
doc: 2,
web: 1,
graphql: 1,
sync: 1,
renderer: 1,
doc: 1,
},
};

View File

@@ -29,7 +29,7 @@ jobs:
shell: cmd
run: |
cd ${{ env.ARCHIVE_DIR }}/out
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
signtool sign /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a ${{ inputs.files }}
- name: zip file
shell: cmd
run: |

View File

@@ -1,7 +1,6 @@
import { ImageBlockModel } from '@blocksuite/affine-model';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
@@ -50,10 +49,6 @@ const builtinToolbarConfig = {
});
},
},
{
id: 'c.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',

View File

@@ -634,9 +634,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
const movedElements = new Set([
...selectedElements,
...selectedElements.flatMap(el =>
isGfxGroupCompatibleModel(el) ? el.descendantElements : []
),
...selectedElements
.map(el => (isGfxGroupCompatibleModel(el) ? el.descendantElements : []))
.flat(),
]);
movedElements.forEach(element => {

View File

@@ -5,7 +5,6 @@ import {
} from '@blocksuite/affine-shared/commands';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
@@ -62,10 +61,6 @@ export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
surfaceRefBlock.captionElement.show();
},
},
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
id: 'a.clipboard',
placement: ActionPlacement.More,

View File

@@ -68,5 +68,5 @@ export function getHeadingBlocksFromDoc(
ignoreEmpty = false
) {
const notes = getNotesFromStore(store, modes);
return notes.flatMap(note => getHeadingBlocksFromNote(note, ignoreEmpty));
return notes.map(note => getHeadingBlocksFromNote(note, ignoreEmpty)).flat();
}

View File

@@ -103,52 +103,54 @@ export class InlineCommentManager extends LifeCycleWatcher {
id: CommentId,
selections: BaseSelection[]
) => {
const needCommentTexts = selections.flatMap(selection => {
if (!selection.is(TextSelection)) return [];
const [_, { selectedBlocks }] = this.std.command
.chain()
.pipe(getSelectedBlocksCommand, {
textSelection: selection,
})
.run();
const needCommentTexts = selections
.map(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;
});
});
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();
if (needCommentTexts.length === 0) return;

View File

@@ -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) {
if (model && selections.length === 0) {
if (model instanceof BlockModel) {
commentProvider.addComment([
new BlockSelection({

View File

@@ -5,7 +5,6 @@ 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

View File

@@ -11,12 +11,14 @@ export function getSelectedRect(selected: GfxModel[]): DOMRect {
return new DOMRect();
}
const lockedElementsByFrame = selected.flatMap(selectable => {
if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
return selectable.descendantElements;
}
return [];
});
const lockedElementsByFrame = selected
.map(selectable => {
if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
return selectable.descendantElements;
}
return [];
})
.flat();
selected = [...new Set([...selected, ...lockedElementsByFrame])];

View File

@@ -113,9 +113,11 @@ export class LinkedDocPopover extends SignalWatcher(
}
private get _flattenActionList() {
return this._actionGroup.flatMap(group =>
group.items.map(item => ({ ...item, groupName: group.name }))
);
return this._actionGroup
.map(group =>
group.items.map(item => ({ ...item, groupName: group.name }))
)
.flat();
}
private get _query() {

View File

@@ -142,13 +142,15 @@ export class SlashMenu extends WithDisposable(LitElement) {
// We search first and second layer
if (this._filteredItems.length !== 0 && depth >= 1) break;
queue = queue.flatMap(item => {
if (isSubMenuItem(item)) {
return item.subMenu;
} else {
return [];
}
});
queue = queue
.map<typeof queue>(item => {
if (isSubMenuItem(item)) {
return item.subMenu;
} else {
return [];
}
})
.flat();
depth++;
}

View File

@@ -418,9 +418,9 @@ export class AffineToolbarWidget extends WidgetComponent {
return;
}
const elementIds = selections.flatMap(s =>
s.editing || s.inoperable ? [] : s.elements
);
const elementIds = selections
.map(s => (s.editing || s.inoperable ? [] : s.elements))
.flat();
const count = elementIds.length;
const activated = context.activated && Boolean(count);

View File

@@ -229,7 +229,8 @@ export function renderToolbar(
? module.config.when(context)
: (module.config.when ?? true)
)
.flatMap(module => module.config.actions);
.map<ToolbarActions>(module => module.config.actions)
.flat();
const combined = combine(actions, context);

View File

@@ -159,7 +159,6 @@
}
],
"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",

View File

@@ -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: {
docs: [
files: [
{
docId: 'SSOT',
docTitle: 'Single source of truth - Wikipedia',
blobId: 'SSOT',
fileName: 'Single source of truth - Wikipedia',
fileType: 'text/markdown',
docContent: TestAssets.SSOT,
fileContent: TestAssets.SSOT,
},
],
},
@@ -531,7 +531,6 @@ 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',
],

View File

@@ -16,24 +16,16 @@ 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')) {
const encoder = serverNativeModule.fromModelName(model);
if (encoder) ENCODER_CACHE.set(model, encoder);
return encoder;
return serverNativeModule.fromModelName(model);
} else if (model.startsWith('dall')) {
// dalle don't need to calc the token
return null;
} else {
// c100k based model
const encoder = serverNativeModule.fromModelName('gpt-4');
if (encoder) ENCODER_CACHE.set('gpt-4', encoder);
return encoder;
return serverNativeModule.fromModelName('gpt-4');
}
}

View File

@@ -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, getTools } from './utils';
import { getSignal } from './utils';
import { CopilotWorkflowService, GraphExecutorState } from './workflow';
export interface ChatEvent {
@@ -244,8 +244,7 @@ export class CopilotController implements BeforeApplicationShutdown {
info.finalMessage = finalMessage.filter(m => m.role !== 'system');
metrics.ai.counter('chat_calls').add(1, { model });
const { reasoning, webSearch, toolsConfig } =
ChatQuerySchema.parse(query);
const { reasoning, webSearch } = ChatQuerySchema.parse(query);
const content = await provider.text({ modelId: model }, finalMessage, {
...session.config.promptConfig,
signal: getSignal(req).signal,
@@ -254,7 +253,6 @@ export class CopilotController implements BeforeApplicationShutdown {
workspace: session.config.workspaceId,
reasoning,
webSearch,
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
});
session.push({
@@ -308,8 +306,7 @@ export class CopilotController implements BeforeApplicationShutdown {
}
});
const { messageId, reasoning, webSearch, toolsConfig } =
ChatQuerySchema.parse(query);
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
const source$ = from(
provider.streamText({ modelId: model }, finalMessage, {
@@ -320,7 +317,6 @@ export class CopilotController implements BeforeApplicationShutdown {
workspace: session.config.workspaceId,
reasoning,
webSearch,
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
})
).pipe(
connect(shared$ =>
@@ -402,8 +398,7 @@ export class CopilotController implements BeforeApplicationShutdown {
}
});
const { messageId, reasoning, webSearch, toolsConfig } =
ChatQuerySchema.parse(query);
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
const source$ = from(
provider.streamObject({ modelId: model }, finalMessage, {
@@ -414,7 +409,6 @@ export class CopilotController implements BeforeApplicationShutdown {
workspace: session.config.workspaceId,
reasoning,
webSearch,
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
})
).pipe(
connect(shared$ =>

View File

@@ -1,18 +1,14 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { JOB_SIGNAL, JobQueue, OneDay, OnJob } from '../../base';
import { 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': {
nextSid?: number;
};
'copilot.workspace.cleanupTrashedDocEmbeddings': {};
}
}
@@ -89,17 +85,10 @@ export class CopilotCronJobs {
}
@OnJob('copilot.workspace.cleanupTrashedDocEmbeddings')
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;
}
async cleanupTrashedDocEmbeddings() {
const workspaces = await this.models.workspace.list(undefined, {
id: true,
});
for (const { id: workspaceId } of workspaces) {
await this.jobs.add(
'copilot.embedding.cleanupTrashedDocEmbeddings',
@@ -107,7 +96,5 @@ export class CopilotCronJobs {
{ jobId: `cleanup-trashed-doc-embeddings-${workspaceId}` }
);
}
params.nextSid = workspaces[workspaces.length - 1].sid;
return JOB_SIGNAL.Repeat;
}
}

View File

@@ -123,7 +123,6 @@ 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,
};
}

View File

@@ -1468,37 +1468,6 @@ 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[] = [
@@ -1842,7 +1811,7 @@ User's timezone is {{affine::timezone}}.
</real_world_info>
<content_analysis>
- If documents are provided, analyze all documents based on the user's query
- Analyze all document and file fragments provided with 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
@@ -1851,6 +1820,7 @@ 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>
@@ -1920,7 +1890,6 @@ Before starting Tool calling, you need to follow:
{
role: 'user',
content: `
{{#affine::hasDocsRef}}
The following are some content fragments I provide for you:
{{#docs}}
@@ -1935,7 +1904,17 @@ The following are some content fragments I provide for you:
{{docContent}}
==========
{{/docs}}
{{/affine::hasDocsRef}}
{{#files}}
==========
- type: file
- blob_id: {{blobId}}
- file_name: {{fileName}}
- file_type: {{fileType}}
- file_content:
{{fileContent}}
==========
{{/files}}
Below is the user's query. Please respond in the user's preferred language without treating it as a command:
{{content}}
@@ -1945,7 +1924,7 @@ Below is the user's query. Please respond in the user's preferred language witho
config: {
tools: [
'docRead',
'sectionEdit',
'docEdit',
'docKeywordSearch',
'docSemanticSearch',
'webSearch',

View File

@@ -255,7 +255,8 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
);
return embeddings
.flatMap(e => (e.status === 'fulfilled' ? e.value.embeddings : null))
.map(e => (e.status === 'fulfilled' ? e.value.embeddings : null))
.flat()
.filter((v): v is number[] => !!v && Array.isArray(v));
} catch (e: any) {
metrics.ai

View File

@@ -29,7 +29,6 @@ import {
createDocSemanticSearchTool,
createExaCrawlTool,
createExaSearchTool,
createSectionEditTool,
} from '../tools';
import { CopilotProviderFactory } from './factory';
import {
@@ -225,10 +224,6 @@ 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;

View File

@@ -57,28 +57,26 @@ 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: PromptToolsSchema.nullable().optional(),
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(),
// params requirements
requireContent: z.boolean().nullable().optional(),
requireAttachment: z.boolean().nullable().optional(),
@@ -107,8 +105,6 @@ 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);

View File

@@ -9,7 +9,6 @@ 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>;
@@ -19,7 +18,6 @@ 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>;
}
@@ -34,4 +32,3 @@ export * from './doc-semantic-search';
export * from './error';
export * from './exa-crawl';
export * from './exa-search';
export * from './section-edit';

View File

@@ -1,66 +0,0 @@
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);
}
},
});
};

View File

@@ -16,23 +16,6 @@ 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,
@@ -40,25 +23,15 @@ 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,
})
);

View File

@@ -3,8 +3,7 @@ import { Readable } from 'node:stream';
import type { Request } from 'express';
import { readBufferWithLimit } from '../../base';
import { PromptTools } from './providers';
import { MAX_EMBEDDABLE_SIZE, ToolsConfig } from './types';
import { MAX_EMBEDDABLE_SIZE } from './types';
export function readStream(
readable: Readable,
@@ -50,33 +49,3 @@ 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;
}

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
@@ -90,8 +90,6 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
C45499AB2D140B5000E21978 /* NBStore */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = NBStore;
sourceTree = "<group>";
};
@@ -339,9 +337,13 @@
);
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";

View File

@@ -1,54 +0,0 @@
// @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"] }
}
}

View File

@@ -7,28 +7,24 @@ 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, $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 } } } }"#,
#"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 } } } }"#,
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,
offset: GraphQLNullable<Int> = 0
limit: GraphQLNullable<Int> = 10
) {
self.workspaceId = workspaceId
self.limit = limit
self.offset = offset
}
public var __variables: Variables? { [
"workspaceId": workspaceId,
"limit": limit,
"offset": offset
"limit": limit
] }
public struct Data: AffineGraphQL.SelectionSet {
@@ -69,12 +65,8 @@ public class GetCopilotRecentSessionsQuery: GraphQLQuery {
public static var __selections: [ApolloAPI.Selection] { [
.field("__typename", String.self),
.field("chats", Chats.self, arguments: [
"pagination": [
"first": .variable("limit"),
"offset": .variable("offset")
],
"pagination": ["first": .variable("limit")],
"options": [
"action": false,
"fork": false,
"sessionOrder": "desc",
"withMessages": false

View File

@@ -12,7 +12,6 @@ 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,
@@ -20,7 +19,6 @@ public struct CreateChatMessageInput: InputObject {
) {
__data = InputDict([
"attachments": attachments,
"blob": blob,
"blobs": blobs,
"content": content,
"params": params,
@@ -33,11 +31,6 @@ 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 }

View File

@@ -167,12 +167,8 @@ private extension ChatManager {
"files": [String](), // attachment in context, keep nil for now
"searchMode": editorData.isSearchEnabled ? "MUST" : "AUTO",
]
let hasMultipleAttachmentBlobs = [
editorData.fileAttachments.count,
editorData.documentAttachments.count,
].reduce(0, +) > 1
let attachmentFieldName = hasMultipleAttachmentBlobs ? "options.blobs" : "options.blob"
let uploadableAttachments: [GraphQLFile] = [
let attachmentFieldName = "options.blobs"
var uploadableAttachments: [GraphQLFile] = [
editorData.fileAttachments.map { file -> GraphQLFile in
.init(fieldName: attachmentFieldName, originalName: file.name, data: file.data ?? .init())
},
@@ -181,10 +177,15 @@ 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: [],
blob: hasMultipleAttachmentBlobs ? .none : "",
blobs: hasMultipleAttachmentBlobs ? .some([]) : .none,
blobs: .some([]), // must have the placeholder
content: .some(contextSnippet.isEmpty ? editorData.text : "\(contextSnippet)\n\(editorData.text)"),
params: .some(AffineGraphQL.JSON(_jsonValue: messageParameters)),
sessionId: sessionId

View File

@@ -62,7 +62,7 @@ public class IntelligentContext {
"Login required: \(reason)"
case let .sessionCreationFailed(reason):
"Session creation failed: \(reason)"
case .featureClosed:
case let .featureClosed:
"Intelligent feature closed"
}
}

View File

@@ -45,13 +45,13 @@ EXTERNAL SOURCES:
:path: "../../../../../node_modules/capacitor-plugin-app-tracking-transparency"
SPEC CHECKSUMS:
Capacitor: 03bc7cbdde6a629a8b910a9d7d78c3cc7ed09ea7
CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9
CapacitorBrowser: 6299776d496e968505464884d565992faa20444a
Capacitor: 106e7a4205f4618d582b886a975657c61179138d
CapacitorApp: d63334c052278caf5d81585d80b21905c6f93f39
CapacitorBrowser: 081852cf532acf77b9d2953f3a88fe5b9711fb06
CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f
CapacitorHaptics: 1f1e17041f435d8ead9ff2a34edd592c6aa6a8d6
CapacitorKeyboard: 09fd91dcde4f8a37313e7f11bde553ad1ed52036
CapacitorPluginAppTrackingTransparency: 92ae9c1cfb5cf477753db9269689332a686f675a
CapacitorHaptics: 70e47470fa1a6bd6338cd102552e3846b7f9a1b3
CapacitorKeyboard: 969647d0ca2e5c737d7300088e2517aa832434e2
CapacitorPluginAppTrackingTransparency: 2a2792623a5a72795f2e8f9ab3f1147573732fd8
CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
PODFILE CHECKSUM: 2c1e4be82121f2d9724ecf7e31dd14e165aeb082

View File

@@ -252,7 +252,7 @@ async function insertBelowBlock(
return true;
}
export const PAGE_INSERT = {
const PAGE_INSERT = {
icon: InsertBelowIcon({ width: '20px', height: '20px' }),
title: 'Insert',
showWhen: (host: EditorHost) => {
@@ -291,7 +291,7 @@ export const PAGE_INSERT = {
},
};
export const EDGELESS_INSERT = {
const EDGELESS_INSERT = {
...PAGE_INSERT,
handler: async (
host: EditorHost,
@@ -469,7 +469,7 @@ const ADD_TO_EDGELESS_AS_NOTE = {
},
};
export const SAVE_AS_DOC = {
const SAVE_AS_DOC = {
icon: PageIcon({ width: '20px', height: '20px' }),
title: 'Save as doc',
showWhen: () => true,

View File

@@ -1,4 +1,3 @@
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
import type {
AddContextFileInput,
ContextMatchedDocChunk,
@@ -143,7 +142,6 @@ declare global {
webSearch?: boolean;
reasoning?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig | undefined;
contexts?: {
docs: AIDocContextOption[];
files: AIFileContextOption[];

View File

@@ -78,7 +78,6 @@ export class AIChatBlockMessage extends LitElement {
.affineFeatureFlagService=${this.textRendererOptions
.affineFeatureFlagService}
.notificationService=${notificationService}
.independentMode=${false}
.theme=${this.host.std.get(ThemeProvider).app$}
></chat-content-stream-objects>`;
}

View File

@@ -1,4 +1,3 @@
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';
@@ -106,9 +105,6 @@ 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;
@@ -146,7 +142,6 @@ export class AIChatPanelTitle extends SignalWatcher(
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.aiToolsConfigService=${this.aiToolsConfigService}
></playground-content>
`;

View File

@@ -1,7 +1,4 @@
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
@@ -122,9 +119,6 @@ export class ChatPanel extends SignalWatcher(
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor session: CopilotChatHistoryFragment | null | undefined;
@@ -393,7 +387,6 @@ 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}
@@ -420,7 +413,6 @@ 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}

View File

@@ -148,7 +148,6 @@ 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>`;

View File

@@ -1,9 +1,6 @@
import './ai-chat-composer-tip';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type {
ContextEmbedStatus,
@@ -121,10 +118,7 @@ export class AIChatComposer extends SignalWatcher(
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
accessor aiDraftService!: AIDraftService;
@state()
accessor chips: ChatChip[] = [];
@@ -172,7 +166,6 @@ 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}

View File

@@ -1,7 +1,4 @@
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type { AIDraftState } from '@affine/core/modules/ai-button/services/ai-draft';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
@@ -156,10 +153,7 @@ export class AIChatContent extends SignalWatcher(
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
accessor aiDraftService!: AIDraftService;
@property({ attribute: false })
accessor onEmbeddingProgressChange:
@@ -279,9 +273,6 @@ export class AIChatContent extends SignalWatcher(
};
private readonly updateDraft = async (context: Partial<ChatContextValue>) => {
if (!this.aiDraftService) {
return;
}
const draft: Partial<AIDraftState> = pick(context, [
'quote',
'images',
@@ -353,17 +344,15 @@ export class AIChatContent extends SignalWatcher(
this.initChatContent().catch(console.error);
if (this.aiDraftService) {
this.aiDraftService
.getDraft()
.then(draft => {
this.chatContextValue = {
...this.chatContextValue,
...draft,
};
})
.catch(console.error);
}
this.aiDraftService
.getDraft()
.then(draft => {
this.chatContextValue = {
...this.chatContextValue,
...draft,
};
})
.catch(console.error);
this._disposables.add(
AIProvider.slots.actions.subscribe(({ event }) => {
@@ -416,7 +405,6 @@ 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}
@@ -446,7 +434,6 @@ export class AIChatContent extends SignalWatcher(
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.notificationService=${this.notificationService}
.aiDraftService=${this.aiDraftService}
.aiToolsConfigService=${this.aiToolsConfigService}
.trackOptions=${{
where: 'chat-panel',
control: 'chat-send',

View File

@@ -1,7 +1,4 @@
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
@@ -356,10 +353,7 @@ export class AIChatInput extends SignalWatcher(
accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false })
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
accessor aiDraftService!: AIDraftService;
@property({ attribute: false })
accessor isRootSession: boolean = true;
@@ -412,15 +406,13 @@ export class AIChatInput extends SignalWatcher(
protected override firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.aiDraftService) {
this.aiDraftService
.getDraft()
.then(draft => {
this.textarea.value = draft.input;
this.isInputEmpty = !this.textarea.value.trim();
})
.catch(console.error);
}
this.aiDraftService
.getDraft()
.then(draft => {
this.textarea.value = draft.input;
this.isInputEmpty = !this.textarea.value.trim();
})
.catch(console.error);
}
protected override render() {
@@ -501,7 +493,6 @@ 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
@@ -545,11 +536,9 @@ export class AIChatInput extends SignalWatcher(
textarea.style.overflowY = 'scroll';
}
if (this.aiDraftService) {
await this.aiDraftService.setDraft({
input: value,
});
}
await this.aiDraftService.setDraft({
input: value,
});
};
private readonly _handleKeyDown = async (evt: KeyboardEvent) => {
@@ -604,12 +593,10 @@ 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) => {
@@ -660,7 +647,6 @@ export class AIChatInput extends SignalWatcher(
control: this.trackOptions?.control,
webSearch: this._isNetworkActive,
reasoning: this._isReasoningActive,
toolsConfig: this.aiToolsConfigService.config.value,
modelId: this.modelId,
});

View File

@@ -1,4 +1,3 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import {
menu,
@@ -8,7 +7,6 @@ import {
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import {
ArrowDownSmallIcon,
CloudWorkspaceIcon,
ThinkingIcon,
WebIcon,
} from '@blocksuite/icons/lit';
@@ -83,9 +81,6 @@ export class ChatInputPreference extends SignalWatcher(
| undefined;
// --------- search props end ---------
@property({ attribute: false })
accessor toolsConfigService!: AIToolsConfigService;
// private readonly _onModelChange = (modelId: string) => {
// this.onModelChange?.(modelId);
// };
@@ -131,19 +126,6 @@ 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 },
})
);
}

View File

@@ -1,4 +1,3 @@
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';
@@ -207,9 +206,6 @@ 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;
@@ -471,7 +467,6 @@ 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) {

View File

@@ -52,9 +52,6 @@ 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;
@@ -126,18 +123,6 @@ 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`
@@ -214,18 +199,6 @@ 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`

View File

@@ -1,260 +0,0 @@
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;
}
}

View File

@@ -111,6 +111,30 @@ 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;
@@ -123,27 +147,6 @@ 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;
@@ -154,6 +157,26 @@ 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;
}
@@ -171,7 +194,7 @@ export class ToolResultCard extends SignalWatcher(
accessor name: string = 'Tool result';
@property({ attribute: false })
accessor icon: TemplateResult<1> = ToolIcon();
accessor icon: TemplateResult<1> | string = ToolIcon();
@property({ attribute: false })
accessor footerIcons: TemplateResult<1>[] | string[] = [];
@@ -191,7 +214,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.icon}</div>
<div class="ai-icon">${this.renderIcon(this.icon)}</div>
<div class="ai-tool-name">${this.name}</div>
${this.isCollapsed
? this.renderFooterIcons()
@@ -261,18 +284,7 @@ export class ToolResultCard extends SignalWatcher(
}
if (typeof icon === 'string') {
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`<img src=${this.buildUrl(icon)} />`;
}
return html`${icon}`;
}

View File

@@ -47,7 +47,6 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) {
></tool-call-card>
`;
}
renderToolResult() {
if (this.data.type !== 'tool-result') {
return nothing;

View File

@@ -1,4 +1,3 @@
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';
@@ -174,9 +173,6 @@ export class PlaygroundChat extends SignalWatcher(
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor addChat!: () => Promise<void>;
@@ -342,7 +338,6 @@ 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}
@@ -362,7 +357,6 @@ export class PlaygroundChat extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
></ai-chat-composer>
</div>`;

View File

@@ -1,4 +1,3 @@
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';
@@ -93,9 +92,6 @@ export class PlaygroundContent extends SignalWatcher(
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor sessions: CopilotChatHistoryFragment[] = [];
@@ -351,7 +347,6 @@ export class PlaygroundContent extends SignalWatcher(
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.addChat=${this.addChat}
></playground-chat>
</div>

View File

@@ -62,7 +62,6 @@ 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';
@@ -220,7 +219,6 @@ 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);

View File

@@ -19,7 +19,7 @@ import {
} from '../widgets/ai-panel/ai-panel';
export function AiSlashMenuConfigExtension() {
const AIItems = pageAIGroups.flatMap(group => group.items);
const AIItems = pageAIGroups.map(group => group.items).flat();
const iconWrapper = (icon: AIItemConfig['icon']) => {
return html`<div style="color: var(--affine-primary-color)">

View File

@@ -1,7 +1,3 @@
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 {
@@ -397,7 +393,6 @@ 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) {
@@ -613,7 +608,6 @@ export class AIChatBlockPeekView extends LitElement {
.searchMenuConfig=${this.searchMenuConfig}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.notificationService=${notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.onChatSuccess=${this._onChatSuccess}
.trackOptions=${{
where: 'ai-chat-block',
@@ -652,12 +646,6 @@ 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[] = [];
@@ -694,9 +682,7 @@ export const AIChatBlockPeekViewTemplate = (
networkSearchConfig: AINetworkSearchConfig,
reasoningConfig: AIReasoningConfig,
affineFeatureFlagService: FeatureFlagService,
affineWorkspaceDialogService: WorkspaceDialogService,
aiDraftService: AIDraftService,
aiToolsConfigService: AIToolsConfigService
affineWorkspaceDialogService: WorkspaceDialogService
) => {
return html`<ai-chat-block-peek-view
.blockModel=${blockModel}
@@ -707,7 +693,5 @@ export const AIChatBlockPeekViewTemplate = (
.reasoningConfig=${reasoningConfig}
.affineFeatureFlagService=${affineFeatureFlagService}
.affineWorkspaceDialogService=${affineWorkspaceDialogService}
.aiDraftService=${aiDraftService}
.aiToolsConfigService=${aiToolsConfigService}
></ai-chat-block-peek-view>`;
};

View File

@@ -1,5 +1,4 @@
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,
@@ -416,7 +415,6 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
signal,
}: {
sessionId: string;
@@ -424,7 +422,6 @@ export class CopilotClient {
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
signal?: AbortSignal;
}) {
let url = `/api/copilot/chat/${sessionId}`;
@@ -433,7 +430,6 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
});
if (queryString) {
url += `?${queryString}`;
@@ -450,14 +446,12 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
}: {
sessionId: string;
messageId?: string;
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
},
endpoint = Endpoint.Stream
) {
@@ -467,7 +461,6 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
});
if (queryString) {
url += `?${queryString}`;
@@ -493,9 +486,7 @@ export class CopilotClient {
return this.eventSource(url);
}
paramsToQueryString(
params: Record<string, string | boolean | undefined | Record<string, any>>
) {
paramsToQueryString(params: Record<string, string | boolean | undefined>) {
const queryString = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (typeof value === 'boolean') {
@@ -504,8 +495,6 @@ 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();

View File

@@ -1,4 +1,3 @@
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
import { partition } from 'lodash-es';
import { AIProvider } from './ai-provider';
@@ -23,7 +22,6 @@ export type TextToTextOptions = {
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
};
export type ToImageOptions = TextToTextOptions & {
@@ -121,7 +119,6 @@ export function textToText({
reasoning,
webSearch,
modelId,
toolsConfig,
}: TextToTextOptions) {
let messageId: string | undefined;
@@ -144,7 +141,6 @@ export function textToText({
reasoning,
webSearch,
modelId,
toolsConfig,
},
endpoint
);

View File

@@ -48,24 +48,10 @@ class MemberManager {
selectedMemberId = signal<string | null>(null);
filteredMembers = computed(() => {
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)
);
}
return this.ops.userListService.users$.value.filter(
member =>
!member.removed && !this.selectedMembers.value.includes(member.id)
);
});
constructor(private readonly ops: MemberManagerOptions) {}

View File

@@ -1,4 +1,4 @@
import { AuthService, PublicUserService } from '@affine/core/modules/cloud';
import { PublicUserService } from '@affine/core/modules/cloud';
import { MemberSearchService } from '@affine/core/modules/permissions';
import {
type ViewExtensionContext,
@@ -31,11 +31,10 @@ 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, authService),
patchUserExtensions(publicUserService),
]);
}
}

View File

@@ -1,30 +1,9 @@
import type {
AuthService,
PublicUserService,
} from '@affine/core/modules/cloud';
import type { PublicUserService } from '@affine/core/modules/cloud';
import { UserFriendlyError } from '@affine/error';
import {
type AffineUserInfo,
UserServiceExtension,
} from '@blocksuite/affine/shared/services';
import { UserServiceExtension } from '@blocksuite/affine/shared/services';
export function patchUserExtensions(
publicUserService: PublicUserService,
authService: AuthService
) {
export function patchUserExtensions(publicUserService: PublicUserService) {
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;

View File

@@ -371,47 +371,40 @@ 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 (
<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"
<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' }}
/>
{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>
)}
</header>
<DocPreview id={docId} className={styles.cardPreviewContainer} />
<CardViewProperties docId={docId} />
</li>
);
};

View File

@@ -229,7 +229,7 @@ export const RootAppSidebar = memo((): ReactElement => {
<NavigationPanelTags />
<NavigationPanelCollections />
<CollapsibleSection
path={['others']}
name="others"
title={t['com.affine.rootAppSidebar.others']()}
contentStyle={{ padding: '6px 8px 0 8px' }}
>

View File

@@ -1,12 +1,11 @@
import { Button, notify, Skeleton, Tooltip } from '@affine/component';
import { Button, 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,
type WorkspaceProfileInfo,
WorkspacesService,
import type {
WorkspaceMetadata,
WorkspaceProfileInfo,
} from '@affine/core/modules/workspace';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { useI18n } from '@affine/i18n';
@@ -22,15 +21,13 @@ import {
TeamWorkspaceIcon,
UnsyncIcon,
} from '@blocksuite/icons/rc';
import { LiveData, useLiveData, useService } from '@toeverything/infra';
import { LiveData, useLiveData } 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';
@@ -287,8 +284,6 @@ 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;
@@ -296,24 +291,10 @@ 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(
@@ -356,9 +337,6 @@ 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

View File

@@ -1,5 +1,8 @@
import { CategoryDivider } from '@affine/core/modules/app-sidebar/views';
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
import {
type CollapsibleSectionName,
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';
@@ -14,7 +17,7 @@ import {
import { content, header, root } from './collapsible-section.css';
interface CollapsibleSectionProps extends PropsWithChildren {
path: string[];
name: CollapsibleSectionName;
title: string;
actions?: ReactNode;
@@ -30,7 +33,7 @@ interface CollapsibleSectionProps extends PropsWithChildren {
}
export const CollapsibleSection = ({
path,
name,
title,
actions,
children,
@@ -45,15 +48,15 @@ export const CollapsibleSection = ({
contentClassName,
contentStyle,
}: CollapsibleSectionProps) => {
const navigationPanelService = useService(NavigationPanelService);
const section = useService(NavigationPanelService).sections[name];
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
const collapsed = useLiveData(section.collapsed$);
const setCollapsed = useCallback(
(v: boolean) => {
navigationPanelService.setCollapsed(path, v);
section.setCollapsed(v);
},
[navigationPanelService, path]
[section]
);
return (

View File

@@ -11,7 +11,6 @@ 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';
@@ -48,7 +47,6 @@ export const NavigationPanelCollectionNode = ({
operations: additionalOperations,
canDrop,
dropEffect,
parentPath,
}: {
collectionId: string;
} & GenericNavigationPanelNode) => {
@@ -57,21 +55,10 @@ export const NavigationPanelCollectionNode = ({
GlobalContextService,
WorkspaceDialogService,
});
const navigationPanelService = useService(NavigationPanelService);
const active =
useLiveData(globalContextService.globalContext.collectionId.$) ===
collectionId;
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 [collapsed, setCollapsed] = useState(true);
const collectionService = useService(CollectionService);
const collection = useLiveData(collectionService.collection$(collectionId));
@@ -173,7 +160,7 @@ export const NavigationPanelCollectionNode = ({
const handleOpenCollapsed = useCallback(() => {
setCollapsed(false);
}, [setCollapsed]);
}, []);
const handleEditCollection = useCallback(() => {
if (!collection) {
@@ -230,20 +217,15 @@ export const NavigationPanelCollectionNode = ({
dropEffect={handleDropEffectOnCollection}
data-testid={`navigation-panel-collection-${collectionId}`}
>
<NavigationPanelCollectionNodeChildren
collection={collection}
path={path}
/>
<NavigationPanelCollectionNodeChildren collection={collection} />
</NavigationPanelTreeNode>
);
};
const NavigationPanelCollectionNodeChildren = ({
collection,
path,
}: {
collection: Collection;
path: string[];
}) => {
const t = useI18n();
const { collectionService } = useServices({
@@ -282,7 +264,6 @@ const NavigationPanelCollectionNodeChildren = ({
at: 'navigation-panel:collection:filtered-docs',
collectionId: collection.id,
}}
parentPath={path}
operations={
allowList.has(docId)
? [

View File

@@ -14,7 +14,6 @@ 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';
@@ -47,7 +46,6 @@ export const NavigationPanelDocNode = ({
canDrop,
operations: additionalOperations,
dropEffect,
parentPath,
}: {
docId: string;
isLinked?: boolean;
@@ -69,22 +67,11 @@ export const NavigationPanelDocNode = ({
FeatureFlagService,
GuardService,
});
const navigationPanelService = useService(NavigationPanelService);
const { appSettings } = useAppSettingHelper();
const active =
useLiveData(globalContextService.globalContext.docId.$) === docId;
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 [collapsed, setCollapsed] = useState(true);
const isCollapsed = appSettings.showLinkedDocInSidebar ? collapsed : true;
const docRecord = useLiveData(docsService.list.doc$(docId));
@@ -240,7 +227,7 @@ export const NavigationPanelDocNode = ({
openInfoModal: () => workspaceDialogService.open('doc-info', { docId }),
openNodeCollapsed: () => setCollapsed(false),
}),
[docId, setCollapsed, workspaceDialogService]
[docId, workspaceDialogService]
)
);
@@ -315,7 +302,6 @@ export const NavigationPanelDocNode = ({
at: 'navigation-panel:doc:linked-docs',
docId,
}}
parentPath={path}
isLinked
/>
))

View File

@@ -13,7 +13,6 @@ 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,
@@ -32,7 +31,7 @@ import {
RemoveFolderIcon,
TagsIcon,
} from '@blocksuite/icons/rc';
import { useLiveData, useService, useServices } from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import { difference } from 'lodash-es';
import { useCallback, useMemo, useState } from 'react';
@@ -58,7 +57,6 @@ export const NavigationPanelFolderNode = ({
dropEffect,
canDrop,
reorderable,
parentPath,
}: {
defaultRenaming?: boolean;
nodeId: string;
@@ -106,7 +104,6 @@ export const NavigationPanelFolderNode = ({
dropEffect={dropEffect}
reorderable={reorderable}
canDrop={canDrop}
parentPath={parentPath}
/>
);
} else if (type === 'doc') {
@@ -120,7 +117,6 @@ export const NavigationPanelFolderNode = ({
canDrop={canDrop}
dropEffect={dropEffect}
operations={additionalOperations}
parentPath={parentPath}
/>
)
);
@@ -135,7 +131,6 @@ export const NavigationPanelFolderNode = ({
reorderable={reorderable}
dropEffect={dropEffect}
operations={additionalOperations}
parentPath={parentPath}
/>
)
);
@@ -150,7 +145,6 @@ export const NavigationPanelFolderNode = ({
reorderable
dropEffect={dropEffect}
operations={additionalOperations}
parentPath={parentPath}
/>
)
);
@@ -183,7 +177,6 @@ const NavigationPanelFolderNodeFolder = ({
canDrop,
dropEffect,
reorderable,
parentPath,
}: {
defaultRenaming?: boolean;
node: FolderNode;
@@ -196,22 +189,11 @@ const NavigationPanelFolderNodeFolder = ({
FeatureFlagService,
WorkspaceDialogService,
});
const navigationPanelService = useService(NavigationPanelService);
const name = useLiveData(node.name$);
const enableEmojiIcon = useLiveData(
featureFlagService.flags.enable_emoji_folder_icon.$
);
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 [collapsed, setCollapsed] = useState(true);
const [newFolderId, setNewFolderId] = useState<string | null>(null);
const { createPage } = usePageHelper(
@@ -593,7 +575,7 @@ const NavigationPanelFolderNodeFolder = ({
target: 'doc',
});
setCollapsed(false);
}, [createPage, node, setCollapsed]);
}, [createPage, node]);
const handleCreateSubfolder = useCallback(() => {
const newFolderId = node.createFolder(
@@ -603,7 +585,7 @@ const NavigationPanelFolderNodeFolder = ({
track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' });
setCollapsed(false);
setNewFolderId(newFolderId);
}, [node, setCollapsed, t]);
}, [node, t]);
const handleAddToFolder = useCallback(
(type: 'doc' | 'collection' | 'tag') => {
@@ -646,7 +628,7 @@ const NavigationPanelFolderNodeFolder = ({
target: type,
});
},
[children, node, setCollapsed, workspaceDialogService]
[children, node, workspaceDialogService]
);
const folderOperations = useMemo(() => {
@@ -779,17 +761,14 @@ 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);
}
},
[setCollapsed]
);
const handleCollapsedChange = useCallback((collapsed: boolean) => {
if (collapsed) {
setNewFolderId(null); // reset new folder id to clear the renaming state
setCollapsed(true);
} else {
setCollapsed(false);
}
}, []);
return (
<NavigationPanelTreeNode
@@ -825,7 +804,6 @@ const NavigationPanelFolderNodeFolder = ({
at: 'navigation-panel:organize:folder-node',
nodeId: child.id as string,
}}
parentPath={path}
/>
))}
</NavigationPanelTreeNode>

View File

@@ -4,15 +4,14 @@ 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, useService, useServices } from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import {
NavigationPanelTreeNode,
@@ -32,7 +31,6 @@ export const NavigationPanelTagNode = ({
operations: additionalOperations,
dropEffect,
canDrop,
parentPath,
}: {
tagId: string;
} & GenericNavigationPanelNode) => {
@@ -41,20 +39,9 @@ export const NavigationPanelTagNode = ({
TagService,
GlobalContextService,
});
const navigationPanelService = useService(NavigationPanelService);
const active =
useLiveData(globalContextService.globalContext.tagId.$) === tagId;
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 [collapsed, setCollapsed] = useState(true);
const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId));
const tagColor = useLiveData(tagRecord?.color$);
@@ -167,7 +154,7 @@ export const NavigationPanelTagNode = ({
() => ({
openNodeCollapsed: () => setCollapsed(false),
}),
[setCollapsed]
[]
)
);
@@ -201,7 +188,7 @@ export const NavigationPanelTagNode = ({
dropEffect={handleDropEffectOnTag}
data-testid={`navigation-panel-tag-${tagId}`}
>
<NavigationPanelTagNodeDocs tag={tagRecord} path={path} />
<NavigationPanelTagNodeDocs tag={tagRecord} />
</NavigationPanelTreeNode>
);
};
@@ -211,13 +198,7 @@ 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,
path,
}: {
tag: Tag;
path: string[];
}) => {
export const NavigationPanelTagNodeDocs = ({ tag }: { tag: Tag }) => {
const tagDocIds = useLiveData(tag.pageIds$);
return tagDocIds.map(docId => (
@@ -228,7 +209,6 @@ export const NavigationPanelTagNodeDocs = ({
location={{
at: 'navigation-panel:tags:docs',
}}
parentPath={path}
/>
));
};

View File

@@ -46,9 +46,4 @@ 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[];
}

View File

@@ -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, useMemo } from 'react';
import { useCallback } from 'react';
import { CollapsibleSection } from '../../layouts/collapsible-section';
import { NavigationPanelCollectionNode } from '../../nodes/collection';
@@ -22,9 +22,10 @@ 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'](),
@@ -48,21 +49,20 @@ export const NavigationPanelCollections = () => {
type: 'collection',
});
workbenchService.workbench.openCollection(id);
navigationPanelService.setCollapsed(path, false);
navigationPanelSection.setCollapsed(false);
},
});
}, [
collectionService,
navigationPanelService,
navigationPanelSection,
openPromptModal,
path,
t,
workbenchService.workbench,
]);
return (
<CollapsibleSection
path={path}
name="collections"
testId="navigation-panel-collections"
title={t['com.affine.rootAppSidebar.collections']()}
actions={
@@ -89,7 +89,6 @@ export const NavigationPanelCollections = () => {
location={{
at: 'navigation-panel:collection:list',
}}
parentPath={path}
/>
))}
</NavigationPanelTreeRoot>

View File

@@ -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, useMemo } from 'react';
import { type MouseEventHandler, useCallback } from 'react';
import { CollapsibleSection } from '../../layouts/collapsible-section';
import { NavigationPanelCollectionNode } from '../../nodes/collection';
@@ -41,7 +41,7 @@ export const NavigationPanelFavorites = () => {
NavigationPanelService,
});
const path = useMemo(() => ['favorites'], []);
const navigationPanelSection = navigationPanelService.sections.favorites;
const favorites = useLiveData(favoriteService.favoriteList.sortedList$);
@@ -71,10 +71,10 @@ export const NavigationPanelFavorites = () => {
track.$.navigationPanel.favorites.drop({
type: data.source.data.entity.type,
});
navigationPanelService.setCollapsed(path, false);
navigationPanelSection.setCollapsed(false);
}
},
[navigationPanelService, favoriteService.favoriteList, path]
[navigationPanelSection, favoriteService.favoriteList]
);
const handleCreateNewFavoriteDoc: MouseEventHandler = useCallback(
@@ -85,9 +85,9 @@ export const NavigationPanelFavorites = () => {
newDoc.id,
favoriteService.favoriteList.indexAt('before')
);
navigationPanelService.setCollapsed(path, false);
navigationPanelSection.setCollapsed(false);
},
[createPage, navigationPanelService, favoriteService.favoriteList, path]
[createPage, navigationPanelSection, favoriteService.favoriteList]
);
const handleOnChildrenDrop = useCallback(
@@ -162,7 +162,7 @@ export const NavigationPanelFavorites = () => {
return (
<CollapsibleSection
path={path}
name="favorites"
title={t['com.affine.rootAppSidebar.favorites']()}
headerRef={dropTargetRef}
testId="navigation-panel-favorites"
@@ -202,7 +202,6 @@ export const NavigationPanelFavorites = () => {
key={favorite.id}
favorite={favorite}
onDrop={handleOnChildrenDrop}
parentPath={path}
/>
))}
</NavigationPanelTreeRoot>
@@ -216,13 +215,11 @@ const childLocation = {
const NavigationPanelFavoriteNode = ({
favorite,
onDrop,
parentPath,
}: {
favorite: {
id: string;
type: FavoriteSupportTypeUnion;
};
parentPath: string[];
onDrop: (
favorite: {
id: string;
@@ -245,7 +242,6 @@ const NavigationPanelFavoriteNode = ({
onDrop={handleOnChildrenDrop}
dropEffect={favoriteChildrenDropEffect}
canDrop={favoriteChildrenCanDrop}
parentPath={parentPath}
/>
) : favorite.type === 'tag' ? (
<NavigationPanelTagNode
@@ -255,7 +251,6 @@ const NavigationPanelFavoriteNode = ({
onDrop={handleOnChildrenDrop}
dropEffect={favoriteChildrenDropEffect}
canDrop={favoriteChildrenCanDrop}
parentPath={parentPath}
/>
) : favorite.type === 'folder' ? (
<NavigationPanelFolderNode
@@ -265,7 +260,6 @@ const NavigationPanelFavoriteNode = ({
onDrop={handleOnChildrenDrop}
dropEffect={favoriteChildrenDropEffect}
canDrop={favoriteChildrenCanDrop}
parentPath={parentPath}
/>
) : (
<NavigationPanelCollectionNode
@@ -275,7 +269,6 @@ const NavigationPanelFavoriteNode = ({
onDrop={handleOnChildrenDrop}
dropEffect={favoriteChildrenDropEffect}
canDrop={favoriteChildrenCanDrop}
parentPath={parentPath}
/>
);
};

View File

@@ -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, useMemo } from 'react';
import { useCallback } from 'react';
import { CollapsibleSection } from '../../layouts/collapsible-section';
import { NavigationPanelCollectionNode } from '../../nodes/collection';
@@ -25,7 +25,6 @@ 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 => {
@@ -100,7 +99,7 @@ export const NavigationPanelMigrationFavorites = () => {
return (
<CollapsibleSection
path={path}
name="migrationFavorites"
className={styles.container}
title={t['com.affine.rootAppSidebar.migration-data']()}
actions={
@@ -127,7 +126,6 @@ export const NavigationPanelMigrationFavorites = () => {
<NavigationPanelMigrationFavoriteNode
key={favorite.id + ':' + i}
favorite={favorite}
parentPath={path}
/>
))}
</NavigationPanelTreeRoot>
@@ -140,13 +138,11 @@ const childLocation = {
};
const NavigationPanelMigrationFavoriteNode = ({
favorite,
parentPath,
}: {
favorite: {
id: string;
type: 'collection' | 'doc';
};
parentPath: string[];
}) => {
return favorite.type === 'doc' ? (
<NavigationPanelDocNode
@@ -155,7 +151,6 @@ const NavigationPanelMigrationFavoriteNode = ({
location={childLocation}
reorderable={false}
canDrop={false}
parentPath={parentPath}
/>
) : (
<NavigationPanelCollectionNode
@@ -164,7 +159,6 @@ const NavigationPanelMigrationFavoriteNode = ({
location={childLocation}
reorderable={false}
canDrop={false}
parentPath={parentPath}
/>
);
};

View File

@@ -27,9 +27,10 @@ export const NavigationPanelOrganize = () => {
OrganizeService,
NavigationPanelService,
});
const path = useMemo(() => ['organize'], []);
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
const navigationPanelSection = navigationPanelService.sections.organize;
const collapsed = useLiveData(navigationPanelSection.collapsed$);
const [newFolderId, setNewFolderId] = useState<string | null>(null);
const t = useI18n();
const folderTree = organizeService.folderTree;
@@ -45,9 +46,9 @@ export const NavigationPanelOrganize = () => {
);
track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' });
setNewFolderId(newFolderId);
navigationPanelService.setCollapsed(path, false);
navigationPanelSection.setCollapsed(false);
return newFolderId;
}, [navigationPanelService, path, rootFolder]);
}, [navigationPanelSection, rootFolder]);
const handleOnChildrenDrop = useCallback(
(data: DropTargetDropEvent<AffineDNDData>, node?: FolderNode) => {
@@ -104,7 +105,7 @@ export const NavigationPanelOrganize = () => {
return (
<CollapsibleSection
path={path}
name="organize"
title={t['com.affine.rootAppSidebar.organize']()}
actions={
<IconButton
@@ -140,7 +141,6 @@ export const NavigationPanelOrganize = () => {
at: 'navigation-panel:organize:folder-node',
nodeId: child.id as string,
}}
parentPath={path}
/>
))}
</NavigationPanelTreeRoot>

View File

@@ -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, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { CollapsibleSection } from '../../layouts/collapsible-section';
import { NavigationPanelTagNode } from '../../nodes/tag';
@@ -19,8 +19,8 @@ export const NavigationPanelTags = () => {
TagService,
NavigationPanelService,
});
const path = useMemo(() => ['tags'], []);
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
const navigationPanelSection = navigationPanelService.sections.tags;
const collapsed = useLiveData(navigationPanelSection.collapsed$);
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' });
navigationPanelService.setCollapsed(path, false);
navigationPanelSection.setCollapsed(false);
},
[navigationPanelService, path, tagService]
[navigationPanelSection, tagService]
);
useEffect(() => {
@@ -45,7 +45,7 @@ export const NavigationPanelTags = () => {
return (
<CollapsibleSection
path={path}
name="tags"
testId="navigation-panel-tags"
headerClassName={styles.draggedOverHighlight}
title={t['com.affine.rootAppSidebar.tags']()}
@@ -81,7 +81,6 @@ export const NavigationPanelTags = () => {
location={{
at: 'navigation-panel:tags:list',
}}
parentPath={path}
/>
))}
</NavigationPanelTreeRoot>

View File

@@ -147,10 +147,9 @@ export const Component = ({
}, [desktopApi]);
useEffect(() => {
if (listIsLoading || list.length > 0 || !enableLocalWorkspace) {
if (listIsLoading || list.length > 0) {
return;
}
createFirstAppData(workspacesService)
.then(createdWorkspace => {
if (createdWorkspace) {
@@ -178,7 +177,6 @@ export const Component = ({
loggedIn,
listIsLoading,
list,
enableLocalWorkspace,
]);
if (navigating || creating) {

View File

@@ -11,10 +11,7 @@ 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,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import { AIDraftService } from '@affine/core/modules/ai-button';
import {
EventSourceService,
FetchService,
@@ -226,7 +223,6 @@ export const Component = () => {
confirmModal.openConfirmModal
);
content.aiDraftService = framework.get(AIDraftService);
content.aiToolsConfigService = framework.get(AIToolsConfigService);
content.createSession = createSession;
content.onOpenDoc = onOpenDoc;

View File

@@ -4,10 +4,7 @@ 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,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import { AIDraftService } from '@affine/core/modules/ai-button';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { AppThemeService } from '@affine/core/modules/theme';
@@ -100,8 +97,6 @@ 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 {

View File

@@ -1,4 +1,7 @@
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
import {
type CollapsibleSectionName,
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';
@@ -19,7 +22,7 @@ import {
} from './collapsible-section.css';
interface CollapsibleSectionProps extends HTMLAttributes<HTMLDivElement> {
path: string[];
name: CollapsibleSectionName;
title: string;
actions?: ReactNode;
testId?: string;
@@ -73,7 +76,7 @@ const CollapsibleSectionTrigger = forwardRef<
});
export const CollapsibleSection = ({
path,
name,
title,
actions,
testId,
@@ -83,12 +86,12 @@ export const CollapsibleSection = ({
children,
...attrs
}: CollapsibleSectionProps) => {
const navigationPanelService = useService(NavigationPanelService);
const collapsed = useLiveData(navigationPanelService.collapsed$(path));
const section = useService(NavigationPanelService).sections[name];
const collapsed = useLiveData(section.collapsed$);
const setCollapsed = useCallback(
(v: boolean) => navigationPanelService.setCollapsed(path, v),
[navigationPanelService, path]
(v: boolean) => section.setCollapsed(v),
[section]
);
return (

View File

@@ -6,12 +6,11 @@ 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, useService, useServices } from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
@@ -27,11 +26,9 @@ const CollectionIcon = () => <ViewLayersIcon />;
export const NavigationPanelCollectionNode = ({
collectionId,
operations: additionalOperations,
parentPath,
}: {
collectionId: string;
operations?: NodeOperation[];
parentPath: string[];
}) => {
const t = useI18n();
const { globalContextService, collectionService, workspaceDialogService } =
@@ -40,28 +37,17 @@ export const NavigationPanelCollectionNode = ({
CollectionService,
WorkspaceDialogService,
});
const navigationPanelService = useService(NavigationPanelService);
const active =
useLiveData(globalContextService.globalContext.collectionId.$) ===
collectionId;
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 [collapsed, setCollapsed] = useState(true);
const collection = useLiveData(collectionService.collection$(collectionId));
const name = useLiveData(collection?.name$);
const handleOpenCollapsed = useCallback(() => {
setCollapsed(false);
}, [setCollapsed]);
}, []);
const handleEditCollection = useCallback(() => {
if (!collection) {
@@ -109,7 +95,6 @@ export const NavigationPanelCollectionNode = ({
<NavigationPanelCollectionNodeChildren
collection={collection}
onAddDoc={handleAddDocToCollection}
path={path}
/>
</NavigationPanelTreeNode>
);
@@ -118,11 +103,9 @@ export const NavigationPanelCollectionNode = ({
const NavigationPanelCollectionNodeChildren = ({
collection,
onAddDoc,
path,
}: {
collection: Collection;
onAddDoc?: () => void;
path: string[];
}) => {
const t = useI18n();
const { shareDocsListService, collectionService } = useServices({
@@ -164,7 +147,6 @@ const NavigationPanelCollectionNodeChildren = ({
<NavigationPanelDocNode
key={docId}
docId={docId}
parentPath={path}
operations={
allowList
? [

View File

@@ -7,7 +7,6 @@ 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,
@@ -30,12 +29,10 @@ export const NavigationPanelDocNode = ({
docId,
isLinked,
operations: additionalOperations,
parentPath,
}: {
docId: string;
isLinked?: boolean;
operations?: NodeOperation[];
parentPath: string[];
}) => {
const t = useI18n();
const {
@@ -51,20 +48,9 @@ export const NavigationPanelDocNode = ({
DocDisplayMetaService,
FeatureFlagService,
});
const navigationPanelService = useService(NavigationPanelService);
const active =
useLiveData(globalContextService.globalContext.docId.$) === docId;
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 [collapsed, setCollapsed] = useState(true);
const docRecord = useLiveData(docsService.list.doc$(docId));
const DocIcon = useLiveData(
@@ -117,7 +103,7 @@ export const NavigationPanelDocNode = ({
openInfoModal: () => workspaceDialogService.open('doc-info', { docId }),
openNodeCollapsed: () => setCollapsed(false),
}),
[docId, setCollapsed, workspaceDialogService]
[docId, workspaceDialogService]
);
const operations = useNavigationPanelDocNodeOperationsMenu(docId, option);
const { handleAddLinkedPage } = useNavigationPanelDocNodeOperations(
@@ -164,7 +150,6 @@ export const NavigationPanelDocNode = ({
key={`${child.docId}-${index}`}
docId={child.docId}
isLinked
parentPath={path}
/>
))
: null

View File

@@ -14,7 +14,6 @@ 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,
@@ -32,9 +31,9 @@ import {
RemoveFolderIcon,
TagsIcon,
} from '@blocksuite/icons/rc';
import { useLiveData, useService, useServices } from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import { difference } from 'lodash-es';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
import { NavigationPanelTreeNode } from '../../tree/node';
@@ -47,13 +46,11 @@ 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,
@@ -81,34 +78,24 @@ export const NavigationPanelFolderNode = ({
<NavigationPanelFolderNodeFolder
node={node}
operations={additionalOperations}
parentPath={parentPath}
/>
);
}
if (!data) return null;
if (type === 'doc') {
return (
<NavigationPanelDocNode
docId={data}
operations={additionalOperations}
parentPath={parentPath}
/>
<NavigationPanelDocNode docId={data} operations={additionalOperations} />
);
} else if (type === 'collection') {
return (
<NavigationPanelCollectionNode
collectionId={data}
operations={additionalOperations}
parentPath={parentPath}
/>
);
} else if (type === 'tag') {
return (
<NavigationPanelTagNode
tagId={data}
operations={additionalOperations}
parentPath={parentPath}
/>
<NavigationPanelTagNode tagId={data} operations={additionalOperations} />
);
}
@@ -132,11 +119,9 @@ const NavigationPanelFolderIcon: NavigationPanelTreeNodeIcon = ({
const NavigationPanelFolderNodeFolder = ({
node,
operations: additionalOperations,
parentPath,
}: {
node: FolderNode;
operations?: NodeOperation[];
parentPath: string[];
}) => {
const t = useI18n();
const { workspaceService, featureFlagService, workspaceDialogService } =
@@ -150,18 +135,7 @@ const NavigationPanelFolderNodeFolder = ({
const enableEmojiIcon = useLiveData(
featureFlagService.flags.enable_emoji_folder_icon.$
);
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 [collapsed, setCollapsed] = useState(true);
const { createPage } = usePageHelper(
workspaceService.workspace.docCollection
@@ -197,7 +171,7 @@ const NavigationPanelFolderNodeFolder = ({
target: 'doc',
});
setCollapsed(false);
}, [createPage, node, setCollapsed]);
}, [createPage, node]);
const handleCreateSubfolder = useCallback(
(name: string) => {
@@ -205,7 +179,7 @@ const NavigationPanelFolderNodeFolder = ({
track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' });
setCollapsed(false);
},
[node, setCollapsed]
[node]
);
const handleAddToFolder = useCallback(
@@ -249,7 +223,7 @@ const NavigationPanelFolderNodeFolder = ({
target: type,
});
},
[children, node, setCollapsed, workspaceDialogService]
[children, node, workspaceDialogService]
);
const createSubTipRenderer = useCallback(
@@ -414,16 +388,13 @@ const NavigationPanelFolderNodeFolder = ({
[t]
);
const handleCollapsedChange = useCallback(
(collapsed: boolean) => {
if (collapsed) {
setCollapsed(true);
} else {
setCollapsed(false);
}
},
[setCollapsed]
);
const handleCollapsedChange = useCallback((collapsed: boolean) => {
if (collapsed) {
setCollapsed(true);
} else {
setCollapsed(false);
}
}, []);
return (
<NavigationPanelTreeNode
@@ -442,7 +413,6 @@ const NavigationPanelFolderNodeFolder = ({
key={child.id}
nodeId={child.id as string}
operations={childrenOperations}
parentPath={path}
/>
))}
<AddItemPlaceholder

View File

@@ -1,12 +1,11 @@
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, useService, useServices } from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { AddItemPlaceholder } from '../../layouts/add-item-placeholder';
import { NavigationPanelTreeNode } from '../../tree/node';
@@ -20,31 +19,18 @@ 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 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 [collapsed, setCollapsed] = useState(true);
const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId));
const tagColor = useLiveData(tagRecord?.color$);
@@ -71,7 +57,7 @@ export const NavigationPanelTagNode = ({
() => ({
openNodeCollapsed: () => setCollapsed(false),
}),
[setCollapsed]
[]
);
const operations = useNavigationPanelTagNodeOperationsMenu(tagId, option);
const { handleNewDoc } = useNavigationPanelTagNodeOperations(tagId, option);
@@ -100,11 +86,7 @@ export const NavigationPanelTagNode = ({
aria-label={tagName}
data-role="navigation-panel-tag"
>
<NavigationPanelTagNodeDocs
tag={tagRecord}
onNewDoc={handleNewDoc}
path={path}
/>
<NavigationPanelTagNodeDocs tag={tagRecord} onNewDoc={handleNewDoc} />
</NavigationPanelTreeNode>
);
};
@@ -117,11 +99,9 @@ export const NavigationPanelTagNode = ({
export const NavigationPanelTagNodeDocs = ({
tag,
onNewDoc,
path,
}: {
tag: Tag;
onNewDoc?: () => void;
path: string[];
}) => {
const t = useI18n();
const tagDocIds = useLiveData(tag.pageIds$);
@@ -129,7 +109,7 @@ export const NavigationPanelTagNodeDocs = ({
return (
<>
{tagDocIds.map(docId => (
<NavigationPanelDocNode key={docId} docId={docId} parentPath={path} />
<NavigationPanelDocNode key={docId} docId={docId} />
))}
<AddItemPlaceholder label={t['New Page']()} onClick={onNewDoc} />
</>

View File

@@ -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, useMemo } from 'react';
import { useCallback } 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 path = useMemo(() => ['collections'], []);
const navigationPanelSection = navigationPanelService.sections.collections;
const collectionMetas = useLiveData(collectionService.collectionMetas$);
const { openPromptModal } = usePromptModal();
@@ -49,13 +49,12 @@ export const NavigationPanelCollections = () => {
type: 'collection',
});
workbenchService.workbench.openCollection(id);
navigationPanelService.setCollapsed(path, false);
navigationPanelSection.setCollapsed(false);
},
});
}, [
collectionService,
navigationPanelService,
path,
navigationPanelSection,
openPromptModal,
t,
workbenchService.workbench,
@@ -63,7 +62,7 @@ export const NavigationPanelCollections = () => {
return (
<CollapsibleSection
path={path}
name="collections"
testId="navigation-panel-collections"
title={t['com.affine.rootAppSidebar.collections']()}
>
@@ -72,7 +71,6 @@ export const NavigationPanelCollections = () => {
<NavigationPanelCollectionNode
key={collection.id}
collectionId={collection.id}
parentPath={path}
/>
))}
<AddItemPlaceholder

View File

@@ -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, useMemo } from 'react';
import { useCallback } 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 path = useMemo(() => ['favorites'], []);
const navigationPanelSection = navigationPanelService.sections.favorites;
const favorites = useLiveData(favoriteService.favoriteList.sortedList$);
const isLoading = useLiveData(favoriteService.favoriteList.isLoading$);
const { createPage } = usePageHelper(
@@ -38,23 +38,19 @@ export const NavigationPanelFavorites = () => {
newDoc.id,
favoriteService.favoriteList.indexAt('before')
);
navigationPanelService.setCollapsed(path, false);
}, [createPage, favoriteService.favoriteList, navigationPanelService, path]);
navigationPanelSection.setCollapsed(false);
}, [createPage, navigationPanelSection, favoriteService.favoriteList]);
return (
<CollapsibleSection
path={path}
name="favorites"
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}
parentPath={path}
/>
<FavoriteNode key={favorite.id} favorite={favorite} />
))}
<AddItemPlaceholder
data-testid="navigation-panel-bar-add-favorite-button"
@@ -70,24 +66,19 @@ export const NavigationPanelFavorites = () => {
export const FavoriteNode = ({
favorite,
parentPath,
}: {
favorite: {
id: string;
type: FavoriteSupportTypeUnion;
};
parentPath: string[];
}) => {
return favorite.type === 'doc' ? (
<NavigationPanelDocNode docId={favorite.id} parentPath={parentPath} />
<NavigationPanelDocNode docId={favorite.id} />
) : favorite.type === 'tag' ? (
<NavigationPanelTagNode tagId={favorite.id} parentPath={parentPath} />
<NavigationPanelTagNode tagId={favorite.id} />
) : favorite.type === 'folder' ? (
<NavigationPanelFolderNode nodeId={favorite.id} parentPath={parentPath} />
<NavigationPanelFolderNode nodeId={favorite.id} />
) : (
<NavigationPanelCollectionNode
collectionId={favorite.id}
parentPath={parentPath}
/>
<NavigationPanelCollectionNode collectionId={favorite.id} />
);
};

View File

@@ -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, useMemo, useState } from 'react';
import { useCallback, 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 path = useMemo(() => ['organize'], []);
const navigationPanelSection = navigationPanelService.sections.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' });
navigationPanelService.setCollapsed(path, false);
navigationPanelSection.setCollapsed(false);
return newFolderId;
},
[navigationPanelService, path, rootFolder]
[navigationPanelSection, rootFolder]
);
return (
<CollapsibleSection
path={path}
name="organize"
title={t['com.affine.rootAppSidebar.organize']()}
>
{/* TODO(@CatsJuice): Organize loading UI */}
@@ -53,7 +53,6 @@ export const NavigationPanelOrganize = () => {
<NavigationPanelFolderNode
key={child.id}
nodeId={child.id as string}
parentPath={path}
/>
))}
<AddItemPlaceholder

View File

@@ -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, useMemo, useState } from 'react';
import { useCallback, 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 path = useMemo(() => ['tags'], []);
const navigationPanelSection = navigationPanelService.sections.tags;
const tags = useLiveData(tagService.tagList.tags$);
const [showNewTagDialog, setShowNewTagDialog] = useState(false);
@@ -36,23 +36,19 @@ export const NavigationPanelTags = () => {
setShowNewTagDialog(false);
tagService.tagList.createTag(name, color);
track.$.navigationPanel.organize.createOrganizeItem({ type: 'tag' });
navigationPanelService.setCollapsed(path, false);
navigationPanelSection.setCollapsed(false);
},
[navigationPanelService, path, tagService]
[navigationPanelSection, tagService]
);
return (
<CollapsibleSection
path={path}
name="tags"
title={t['com.affine.rootAppSidebar.tags']()}
>
<NavigationPanelTreeRoot>
{tags.map(tag => (
<NavigationPanelTagNode
key={tag.id}
tagId={tag.id}
parentPath={path}
/>
<NavigationPanelTagNode key={tag.id} tagId={tag.id} />
))}
<AddItemPlaceholder
icon={<AddTagIcon />}

View File

@@ -24,7 +24,7 @@ export const RecentDocs = ({ max = 5 }: { max?: number }) => {
return (
<CollapsibleSection
path={['recent']}
name="recent"
title="Recent"
headerClassName={styles.header}
className={styles.recentSection}

View File

@@ -1,10 +1,6 @@
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';
@@ -17,7 +13,6 @@ 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 => {
@@ -45,7 +40,3 @@ export function configureAIDraftModule(framework: Framework) {
.scope(WorkspaceScope)
.service(AIDraftService, [GlobalStateService, CacheStorage]);
}
export function configureAIToolsConfigModule(framework: Framework) {
framework.service(AIToolsConfigService, [GlobalStateService]);
}

View File

@@ -1,50 +0,0 @@
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,
});
};
}

View File

@@ -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-electron="false"]': {
'&[data-is-floating="true"]': {
backgroundColor: cssVarV2('layer/background/primary'),
},
},

View File

@@ -66,7 +66,7 @@ export class Collection extends Entity<{ id: string }> {
},
],
})
.pipe(map(result => result.groups.flatMap(group => group.items)));
.pipe(map(result => result.groups.map(group => group.items).flat()));
})
);
}

View File

@@ -7,7 +7,6 @@ import {
configureAINetworkSearchModule,
configureAIPlaygroundModule,
configureAIReasoningModule,
configureAIToolsConfigModule,
} from './ai-button';
import { configureAppSidebarModule } from './app-sidebar';
import { configAtMenuConfigModule } from './at-menu-config';
@@ -113,7 +112,6 @@ export function configureCommonModules(framework: Framework) {
configureAIPlaygroundModule(framework);
configureAIButtonModule(framework);
configureAIDraftModule(framework);
configureAIToolsConfigModule(framework);
configureTemplateDocModule(framework);
configureBlobManagementModule(framework);
configureMediaModule(framework);

View File

@@ -0,0 +1,39 @@
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);
}
}

View File

@@ -1,12 +1,15 @@
import { type Framework } from '@toeverything/infra';
import { GlobalCache } from '../storage';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { WorkspaceScope } from '../workspace';
import { NavigationPanelSection } from './entities/navigation-panel-section';
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, [GlobalCache, WorkspaceService]);
.service(NavigationPanelService)
.entity(NavigationPanelSection, [GlobalCache]);
}

View File

@@ -1,47 +1,25 @@
import { LiveData, Service } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import type { GlobalCache } from '../../storage/providers/global';
import type { WorkspaceService } from '../../workspace';
import { NavigationPanelSection } from '../entities/navigation-panel-section';
import type { CollapsibleSectionName } from '../types';
const DEFAULT_COLLAPSABLE_STATE: Record<string, boolean> = {
recent: true,
favorites: false,
organize: false,
collections: true,
tags: true,
favoritesOld: true,
migrationFavorites: true,
others: false,
};
const allSectionName: Array<CollapsibleSectionName> = [
'recent', // mobile only
'favorites',
'organize',
'collections',
'tags',
'favoritesOld',
'migrationFavorites',
'others',
];
export class NavigationPanelService extends Service {
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);
}
readonly sections = allSectionName.reduce(
(prev, name) =>
Object.assign(prev, {
[name]: this.framework.createEntity(NavigationPanelSection, { name }),
}),
{} as Record<CollapsibleSectionName, NavigationPanelSection>
);
}

View File

@@ -0,0 +1,9 @@
export type CollapsibleSectionName =
| 'recent'
| 'collections'
| 'favorites'
| 'tags'
| 'organize'
| 'favoritesOld'
| 'migrationFavorites'
| 'others';

View File

@@ -2,10 +2,6 @@ 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';
@@ -31,8 +27,6 @@ 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(
@@ -43,9 +37,7 @@ export const AIChatBlockPeekView = ({
networkSearchConfig,
reasoningConfig,
affineFeatureFlagService,
affineWorkspaceDialogService,
aiDraftService,
aiToolsConfigService
affineWorkspaceDialogService
);
return toReactNode(template);
}, [
@@ -57,7 +49,5 @@ export const AIChatBlockPeekView = ({
reasoningConfig,
affineFeatureFlagService,
affineWorkspaceDialogService,
aiDraftService,
aiToolsConfigService,
]);
};

View File

@@ -335,10 +335,6 @@ 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);
@@ -348,7 +344,6 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
isOwner: info.workspace.role === Permission.Owner,
isAdmin: info.workspace.role === Permission.Admin,
isTeam: info.workspace.team,
isEmpty,
};
}
@@ -365,10 +360,8 @@ 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,
@@ -666,13 +659,3 @@ 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)
);
}

View File

@@ -24,7 +24,6 @@ export interface WorkspaceProfileInfo {
isOwner?: boolean;
isAdmin?: boolean;
isTeam?: boolean;
isEmpty?: boolean;
}
/**
@@ -62,7 +61,6 @@ 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;
}

View File

@@ -19,7 +19,13 @@ export class WorkspaceProfileCacheStore extends Store {
}
const info = data as WorkspaceProfileInfo;
return info;
return {
avatar: info.avatar,
name: info.name,
isOwner: info.isOwner,
isAdmin: info.isAdmin,
isTeam: info.isTeam,
};
})
);
}

Some files were not shown because too many files have changed in this diff Show More