diff --git a/apps/web/src/components/AssetsPane.tsx b/apps/web/src/components/AssetsPane.tsx
new file mode 100644
index 00000000..280eb048
--- /dev/null
+++ b/apps/web/src/components/AssetsPane.tsx
@@ -0,0 +1,115 @@
+import { useQuery } from "@tanstack/react-query";
+import { Archive, HardDrive, ImageIcon, File as FileIcon, ExternalLink, ChevronLeft } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { api } from "@/lib/api";
+import { formatDateTime } from "@/lib/utils";
+
+export const formatBytes = (bytes: number) => {
+ if (!Number.isFinite(bytes) || bytes <= 0) {
+ return "0 B";
+ }
+
+ const units = ["B", "KB", "MB", "GB", "TB"];
+ const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
+ const value = bytes / 1024 ** exponent;
+
+ return `${exponent === 0 ? value.toFixed(0) : value.toFixed(value >= 10 ? 1 : 2)} ${units[exponent]}`;
+};
+
+interface AssetsPaneProps {
+ onClose: () => void;
+}
+
+export const AssetsPane = ({ onClose }: AssetsPaneProps) => {
+ const resourcesQuery = useQuery({
+ queryKey: ["resources"],
+ queryFn: () => api.listResources(),
+ });
+ const resources = resourcesQuery.data?.resources ?? [];
+ const summary = resourcesQuery.data?.summary ?? {
+ totalCount: 0,
+ totalBytes: 0,
+ imageCount: 0,
+ attachmentCount: 0,
+ };
+
+ return (
+
+
+
+
+
+
+
+ 附件管理
+
+
+
+
+ {formatBytes(summary.totalBytes)}
+
+ •
+ {summary.totalCount} 文件
+ •
+ {summary.imageCount} 图片
+
+
+
+
+
+
+
+ {resourcesQuery.isLoading ? (
+
正在加载附件列表...
+ ) : resources.length === 0 ? (
+
+
+
暂无附件
+
插入到笔记中的图片或文件会显示在这里
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
diff --git a/apps/web/src/components/SettingsPane.tsx b/apps/web/src/components/SettingsPane.tsx
new file mode 100644
index 00000000..114c7088
--- /dev/null
+++ b/apps/web/src/components/SettingsPane.tsx
@@ -0,0 +1,251 @@
+import { useState, type FormEvent } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { KeyRound, ShieldCheck, Trash2, ChevronLeft, Plus } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { api } from "@/lib/api";
+import { cn, formatDateTime } from "@/lib/utils";
+import { AppConfirmDialog } from "./dialogs/ConfirmDialogs";
+import type { ApiToken } from "@edgeever/shared";
+
+const DEFAULT_TOKEN_SCOPES = ["read:notebooks", "read:memos", "read:tags"];
+
+interface SettingsPaneProps {
+ onClose: () => void;
+}
+
+export const SettingsPane = ({ onClose }: SettingsPaneProps) => {
+ const queryClient = useQueryClient();
+ const [name, setName] = useState("MCP Agent");
+ const [selectedScopes, setSelectedScopes] = useState>(() => new Set(DEFAULT_TOKEN_SCOPES));
+ const [createdToken, setCreatedToken] = useState<{ token: string; apiToken: ApiToken } | null>(null);
+ const [tokenRevokeConfirmation, setTokenRevokeConfirmation] = useState(null);
+
+ const tokensQuery = useQuery({
+ queryKey: ["api-tokens"],
+ queryFn: () => api.listApiTokens(),
+ });
+
+ const availableScopes = tokensQuery.data?.availableScopes ?? [
+ "read:notebooks",
+ "write:notebooks",
+ "read:memos",
+ "write:memos",
+ "read:resources",
+ "write:resources",
+ "read:tags",
+ "write:tags",
+ ];
+
+ const createMutation = useMutation({
+ mutationFn: api.createApiToken,
+ onSuccess: async (data) => {
+ setCreatedToken(data);
+ setName("");
+ setSelectedScopes(new Set(DEFAULT_TOKEN_SCOPES));
+ await queryClient.invalidateQueries({ queryKey: ["api-tokens"] });
+ },
+ });
+
+ const revokeMutation = useMutation({
+ mutationFn: api.revokeApiToken,
+ onSuccess: async () => {
+ await queryClient.invalidateQueries({ queryKey: ["api-tokens"] });
+ },
+ });
+
+ const tokens = tokensQuery.data?.apiTokens ?? [];
+
+ const toggleScope = (scope: string) => {
+ setSelectedScopes((current) => {
+ const next = new Set(current);
+ if (next.has(scope)) {
+ next.delete(scope);
+ } else {
+ next.add(scope);
+ }
+ return next;
+ });
+ };
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+ const scopes = Array.from(selectedScopes);
+
+ if (!name.trim() || scopes.length === 0) {
+ return;
+ }
+
+ createMutation.mutate({ name: name.trim(), scopes });
+ };
+
+ return (
+
+
+
+
+
+
+
+ API & MCP 设置
+
+
+ 管理用于本地 MCP 客户端或第三方 API 访问的身份凭证
+
+
+
+
+
+
+
+ {createdToken && (
+
+
+
+ API Token 已成功生成
+
+
+
+
+
+
+ ⚠️ 安全警告:为了您的账户安全,明文 Token 仅在此处展示一次,关闭后将无法再次找回!
+
+
+ )}
+
+
+
+
+
活跃的 Token 列表
+
+ {tokensQuery.isLoading ? (
+
正在加载 Token 列表...
+ ) : tokens.length === 0 ? (
+
+ 暂无活跃的 API Token
+
+ ) : (
+ tokens.map((token) => (
+
+
+ {token.name}
+
+ {token.scopes.join(", ") || "no scopes"}
+
+
+ {token.lastUsedAt ? `上次调用时间:${formatDateTime(token.lastUsedAt)}` : "从未被调用"}
+
+
+
+
+ ))
+ )}
+
+
+
+
+
+ {tokenRevokeConfirmation && (
+
setTokenRevokeConfirmation(null)}
+ onConfirm={() => {
+ revokeMutation.mutate(tokenRevokeConfirmation.id, {
+ onSuccess: () => setTokenRevokeConfirmation(null),
+ });
+ }}
+ />
+ )}
+
+ );
+};
diff --git a/apps/web/src/components/WorkspaceApp.tsx b/apps/web/src/components/WorkspaceApp.tsx
index 608b8e2a..6fe11966 100644
--- a/apps/web/src/components/WorkspaceApp.tsx
+++ b/apps/web/src/components/WorkspaceApp.tsx
@@ -16,9 +16,9 @@ import { Button } from "@/components/ui/button";
import { NotebookPane } from "./NotebookPane";
import { MemoListPane } from "./MemoListPane";
import { EditorPane } from "./EditorPane";
-import { AssetsDialog } from "./dialogs/AssetsDialog";
+import { AssetsPane } from "./AssetsPane";
import { TagsDialog } from "./dialogs/TagsDialog";
-import { SettingsDialog } from "./dialogs/SettingsDialog";
+import { SettingsPane } from "./SettingsPane";
import { TemplatesDialog } from "./dialogs/TemplatesDialog";
import { AppConfirmDialog, MemoDeleteConfirmDialog, NotebookNameDialog } from "./dialogs/ConfirmDialogs";
import { api } from "@/lib/api";
@@ -453,9 +453,8 @@ export const WorkspaceApp = ({
const [appNoticeDialog, setAppNoticeDialog] = useState(null);
const [multiSelectKeyDown, setMultiSelectKeyDown] = useState(false);
const [imageCompressionEnabled, setImageCompressionEnabled] = useState(readImageCompressionPreference);
- const [assetsOpen, setAssetsOpen] = useState(false);
+ const [rightView, setRightView] = useState<"editor" | "settings" | "assets">("editor");
const [tagsOpen, setTagsOpen] = useState(false);
- const [settingsOpen, setSettingsOpen] = useState(false);
const [templatesOpen, setTemplatesOpen] = useState(false);
const [mobileNotebookPickerOpen, setMobileNotebookPickerOpen] = useState(false);
const [mobileBottomNavActive, setMobileBottomNavActive] = useState("home");
@@ -515,9 +514,8 @@ export const WorkspaceApp = ({
mobileNotebookPickerOpen ||
mobileSearchActive ||
templatesOpen ||
- settingsOpen ||
+ rightView !== "editor" ||
tagsOpen ||
- assetsOpen ||
memoSelectionModeActive ||
activePane === "editor" ||
activePane === "notebooks"
@@ -685,6 +683,7 @@ export const WorkspaceApp = ({
setMemoView("notebook");
await queryClient.invalidateQueries({ queryKey: ["memos"] });
queryClient.setQueryData(["memo", data.memo.id], { memo: data.memo });
+ setRightView("editor");
setSelectedMemoId(data.memo.id);
setActivePane("editor");
},
@@ -696,6 +695,7 @@ export const WorkspaceApp = ({
clearMemoSelection();
await queryClient.invalidateQueries({ queryKey: ["memos"] });
queryClient.setQueryData(["memo", data.memo.id], { memo: data.memo });
+ setRightView("editor");
setSelectedMemoId(data.memo.id);
setActivePane("editor");
},
@@ -792,6 +792,7 @@ export const WorkspaceApp = ({
await queryClient.invalidateQueries({ queryKey: ["memos"] });
queryClient.setQueryData(["memo", data.memo.id], { memo: data.memo });
setSelectedNotebookId(data.memo.notebookId);
+ setRightView("editor");
setSelectedMemoId(data.memo.id);
setActivePane("editor");
},
@@ -1083,7 +1084,8 @@ export const WorkspaceApp = ({
const handleOpenAssets = () => {
clearHiddenMobileSearch();
- setAssetsOpen(true);
+ setRightView("assets");
+ setActivePane("editor");
};
const handleOpenTags = () => {
@@ -1099,12 +1101,12 @@ export const WorkspaceApp = ({
const handleOpenSettings = () => {
clearHiddenMobileSearch();
- setMobileBottomNavActive("settings");
- setSettingsOpen(true);
+ setRightView("settings");
+ setActivePane("editor");
};
const handleCloseAssets = () => {
- setAssetsOpen(false);
+ setRightView("editor");
setMobileBottomNavActive("home");
};
@@ -1114,7 +1116,7 @@ export const WorkspaceApp = ({
};
const handleCloseSettings = () => {
- setSettingsOpen(false);
+ setRightView("editor");
setMobileBottomNavActive("home");
};
@@ -1160,7 +1162,7 @@ export const WorkspaceApp = ({
return true;
}
- if (settingsOpen) {
+ if (rightView === "settings") {
handleCloseSettings();
return true;
}
@@ -1170,7 +1172,7 @@ export const WorkspaceApp = ({
return true;
}
- if (assetsOpen) {
+ if (rightView === "assets") {
handleCloseAssets();
return true;
}
@@ -1189,7 +1191,7 @@ export const WorkspaceApp = ({
}, [
activePane,
appNoticeDialog,
- assetsOpen,
+ rightView,
clearMemoSelection,
createNotebookMutation.isPending,
deleteMemoMutation.isPending,
@@ -1205,7 +1207,6 @@ export const WorkspaceApp = ({
mobileSearchActive,
notebookDeleteConfirmation,
notebookNameDialog,
- settingsOpen,
tagsOpen,
templatesOpen,
updateNotebookMutation.isPending,
@@ -1249,12 +1250,11 @@ export const WorkspaceApp = ({
const transientLayerOpen = Boolean(
appNoticeDialog ||
- assetsOpen ||
+ rightView !== "editor" ||
memoDeleteConfirmation ||
mobileNotebookPickerOpen ||
notebookDeleteConfirmation ||
notebookNameDialog ||
- settingsOpen ||
tagsOpen ||
templatesOpen
);
@@ -1290,7 +1290,7 @@ export const WorkspaceApp = ({
window.addEventListener("keydown", handleWorkspaceShortcut);
return () => window.removeEventListener("keydown", handleWorkspaceShortcut);
}, [
- assetsOpen,
+ rightView,
appNoticeDialog,
canCreateMemo,
clearMemoSelection,
@@ -1303,7 +1303,6 @@ export const WorkspaceApp = ({
mobileNotebookPickerOpen,
notebookDeleteConfirmation,
notebookNameDialog,
- settingsOpen,
tagsOpen,
templatesOpen,
]);
@@ -1381,7 +1380,12 @@ export const WorkspaceApp = ({
- {assetsOpen &&
}
{tagsOpen &&
setTagsOpen(false)} />}
- {settingsOpen && }
{templatesOpen && (
{
- if (!Number.isFinite(bytes) || bytes <= 0) {
- return "0 B";
- }
-
- const units = ["B", "KB", "MB", "GB", "TB"];
- const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
- const value = bytes / 1024 ** exponent;
-
- return `${exponent === 0 ? value.toFixed(0) : value.toFixed(value >= 10 ? 1 : 2)} ${units[exponent]}`;
-};
-
-export const AssetsDialog = ({ onClose }: { onClose: () => void }) => {
- const resourcesQuery = useQuery({
- queryKey: ["resources"],
- queryFn: () => api.listResources(),
- });
- const resources = resourcesQuery.data?.resources ?? [];
- const summary = resourcesQuery.data?.summary ?? {
- totalCount: 0,
- totalBytes: 0,
- imageCount: 0,
- attachmentCount: 0,
- };
-
- return (
-
- );
-};
diff --git a/apps/web/src/components/dialogs/SettingsDialog.tsx b/apps/web/src/components/dialogs/SettingsDialog.tsx
deleted file mode 100644
index d0de895e..00000000
--- a/apps/web/src/components/dialogs/SettingsDialog.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import { useState, type FormEvent } from "react";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-import { KeyRound, ShieldCheck, X } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
-import { api } from "@/lib/api";
-import { cn, formatDateTime } from "@/lib/utils";
-import { AppConfirmDialog } from "./ConfirmDialogs";
-import type { ApiToken } from "@edgeever/shared";
-
-const DEFAULT_TOKEN_SCOPES = ["read:notebooks", "read:memos", "read:tags"];
-
-export const SettingsDialog = ({ onClose }: { onClose: () => void }) => {
- const queryClient = useQueryClient();
- const [name, setName] = useState("MCP Agent");
- const [selectedScopes, setSelectedScopes] = useState>(() => new Set(DEFAULT_TOKEN_SCOPES));
- const [createdToken, setCreatedToken] = useState<{ token: string; apiToken: ApiToken } | null>(null);
- const [tokenRevokeConfirmation, setTokenRevokeConfirmation] = useState(null);
-
- const tokensQuery = useQuery({
- queryKey: ["api-tokens"],
- queryFn: () => api.listApiTokens(),
- });
-
- const availableScopes = tokensQuery.data?.availableScopes ?? [
- "read:notebooks",
- "write:notebooks",
- "read:memos",
- "write:memos",
- "read:resources",
- "write:resources",
- "read:tags",
- "write:tags",
- ];
-
- const createMutation = useMutation({
- mutationFn: api.createApiToken,
- onSuccess: async (data) => {
- setCreatedToken(data);
- setName("");
- setSelectedScopes(new Set(DEFAULT_TOKEN_SCOPES));
- await queryClient.invalidateQueries({ queryKey: ["api-tokens"] });
- },
- });
-
- const revokeMutation = useMutation({
- mutationFn: api.revokeApiToken,
- onSuccess: async () => {
- await queryClient.invalidateQueries({ queryKey: ["api-tokens"] });
- },
- });
-
- const tokens = tokensQuery.data?.apiTokens ?? [];
-
- const toggleScope = (scope: string) => {
- setSelectedScopes((current) => {
- const next = new Set(current);
- if (next.has(scope)) {
- next.delete(scope);
- } else {
- next.add(scope);
- }
- return next;
- });
- };
-
- const handleSubmit = (event: FormEvent) => {
- event.preventDefault();
- const scopes = Array.from(selectedScopes);
-
- if (!name.trim() || scopes.length === 0) {
- return;
- }
-
- createMutation.mutate({ name: name.trim(), scopes });
- };
-
- return (
-
- );
-};