2025-11-23 10:55:04 +08:00
|
|
|
import { redirect } from "next/navigation";
|
|
|
|
|
import type { ReactNode } from "react";
|
|
|
|
|
import { Sidebar } from "@/components/sidebar/sidebar";
|
|
|
|
|
import { Breadcrumb } from "@/components/breadcrumb";
|
|
|
|
|
import { BottomToolbar } from "@/components/bottom-toolbar";
|
|
|
|
|
import { MobileSidebarTrigger } from "@/components/mobile-sidebar-trigger";
|
|
|
|
|
import type { DocumentRecord } from "@/lib/documents";
|
|
|
|
|
import type { SidebarInitialData } from "@/components/sidebar/types";
|
|
|
|
|
import { SearchPalette } from "@/components/search/search-palette";
|
2026-01-17 10:12:53 +08:00
|
|
|
import { isConvexEnabled } from "@/lib/convex/enabled";
|
2026-01-18 19:01:31 +08:00
|
|
|
import { getAuthedConvexClient } from "@/lib/convex/route";
|
2026-01-17 10:12:53 +08:00
|
|
|
import { api } from "@/lib/convex/api";
|
|
|
|
|
import type { MediaAsset } from "@/types/media";
|
|
|
|
|
|
|
|
|
|
function extractMindmapImageAssetIdsFromData(input: unknown): string[] {
|
|
|
|
|
const root = (() => {
|
|
|
|
|
if (!input || typeof input !== "object") return input;
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
const record = input as any;
|
|
|
|
|
return record && typeof record === "object" && "root" in record ? record.root : input;
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
const ids: string[] = [];
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
|
|
|
|
const push = (value: unknown) => {
|
|
|
|
|
if (typeof value !== "string") return;
|
|
|
|
|
if (!value.startsWith("asset:")) return;
|
|
|
|
|
const id = value.slice("asset:".length).trim();
|
|
|
|
|
if (!id) return;
|
|
|
|
|
if (seen.has(id)) return;
|
|
|
|
|
seen.add(id);
|
|
|
|
|
ids.push(id);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const get = (obj: unknown, key: string): unknown => {
|
|
|
|
|
if (!obj || typeof obj !== "object") return undefined;
|
|
|
|
|
return (obj as Record<string, unknown>)[key];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const walk = (node: unknown) => {
|
|
|
|
|
if (!node || typeof node !== "object") return;
|
|
|
|
|
const data = get(node, "data");
|
|
|
|
|
const image = get(node, "image");
|
|
|
|
|
push(get(data, "image"));
|
|
|
|
|
push(image);
|
|
|
|
|
push(get(image, "url"));
|
|
|
|
|
push(get(get(data, "image"), "url"));
|
|
|
|
|
const children = get(node, "children");
|
|
|
|
|
if (Array.isArray(children)) children.forEach(walk);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
walk(root);
|
|
|
|
|
return ids;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeId(): string {
|
|
|
|
|
return typeof crypto.randomUUID === "function"
|
|
|
|
|
? crypto.randomUUID()
|
|
|
|
|
: `${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
|
|
|
}
|
2025-11-23 10:55:04 +08:00
|
|
|
|
|
|
|
|
export default async function AppLayout({ children }: { children: ReactNode }) {
|
2026-01-17 10:12:53 +08:00
|
|
|
if (isConvexEnabled()) {
|
2026-01-18 19:01:31 +08:00
|
|
|
const { auth, client } = await getAuthedConvexClient();
|
2026-01-17 10:12:53 +08:00
|
|
|
|
|
|
|
|
const ensured = await client.mutation(api.workspaces.ensureDefaultWorkspace, {
|
|
|
|
|
fallbackName: auth.name ?? auth.email ?? "我的空间",
|
|
|
|
|
workspaceIdIfCreate: makeId(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const workspaces = ensured.workspaces;
|
|
|
|
|
const activeWorkspaceId = ensured.activeWorkspaceId;
|
|
|
|
|
|
|
|
|
|
let documents: DocumentRecord[] = [];
|
|
|
|
|
let sidebarInitialData: SidebarInitialData | null = null;
|
|
|
|
|
|
|
|
|
|
if (activeWorkspaceId) {
|
|
|
|
|
const [docRows, trashedDocs] = await Promise.all([
|
|
|
|
|
client.query(api.documents.listByWorkspace, {
|
|
|
|
|
workspaceId: activeWorkspaceId,
|
|
|
|
|
}),
|
|
|
|
|
client.query(api.documents.listTrashedByWorkspace, {
|
|
|
|
|
workspaceId: activeWorkspaceId,
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
documents = docRows as unknown as DocumentRecord[];
|
|
|
|
|
|
|
|
|
|
const mindmapRows = await client.query(api.mindmaps.listByWorkspace, {
|
|
|
|
|
workspaceId: activeWorkspaceId,
|
|
|
|
|
includeDeleted: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const activeMindmaps = (mindmapRows ?? []).filter((r) => !r.deleted_at);
|
|
|
|
|
const trashedMindmaps = (mindmapRows ?? []).filter((r) => !!r.deleted_at);
|
|
|
|
|
|
|
|
|
|
const mindmapDocs = Array.from(new Set(activeMindmaps.map((r) => r.document_id)));
|
|
|
|
|
|
|
|
|
|
const mindmapAssetChildren: Record<string, string[]> = {};
|
|
|
|
|
activeMindmaps.forEach((r) => {
|
|
|
|
|
const ids = extractMindmapImageAssetIdsFromData(r.data);
|
|
|
|
|
if (ids.length > 0) mindmapAssetChildren[r.mindmap_id] = ids;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const mindmapAssets: MediaAsset[] = activeMindmaps.map((r) => {
|
|
|
|
|
const isLegacy = r.mindmap_id.startsWith("legacy-");
|
|
|
|
|
return {
|
|
|
|
|
id: r.mindmap_id,
|
|
|
|
|
workspace_id: r.workspace_id ?? activeWorkspaceId,
|
|
|
|
|
document_id: r.document_id,
|
|
|
|
|
asset_type: "mindmap",
|
|
|
|
|
file_url: null,
|
|
|
|
|
thumbnail_url: null,
|
|
|
|
|
bucket: null,
|
|
|
|
|
storage_path: null,
|
|
|
|
|
file_name: isLegacy ? "mindmap.json" : `mindmap-${r.mindmap_id}.json`,
|
|
|
|
|
file_size: null,
|
|
|
|
|
mime_type: "application/json",
|
|
|
|
|
ocr_payload: undefined,
|
|
|
|
|
ocr_strategy: null,
|
|
|
|
|
ocr_text: null,
|
|
|
|
|
ocr_status: null,
|
|
|
|
|
signed_url: null,
|
|
|
|
|
created_at: r.created_at ?? "",
|
|
|
|
|
updated_at: r.updated_at ?? "",
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const trashedMindmapAssets: MediaAsset[] = trashedMindmaps.map((r) => {
|
|
|
|
|
const isLegacy = r.mindmap_id.startsWith("legacy-");
|
|
|
|
|
return {
|
|
|
|
|
id: r.mindmap_id,
|
|
|
|
|
workspace_id: r.workspace_id ?? activeWorkspaceId,
|
|
|
|
|
document_id: r.document_id,
|
|
|
|
|
asset_type: "mindmap",
|
|
|
|
|
file_url: null,
|
|
|
|
|
thumbnail_url: null,
|
|
|
|
|
bucket: null,
|
|
|
|
|
storage_path: null,
|
|
|
|
|
file_name: isLegacy ? "mindmap.json" : `mindmap-${r.mindmap_id}.json`,
|
|
|
|
|
file_size: null,
|
|
|
|
|
mime_type: "application/json",
|
|
|
|
|
ocr_payload: undefined,
|
|
|
|
|
ocr_strategy: null,
|
|
|
|
|
ocr_text: null,
|
|
|
|
|
ocr_status: null,
|
|
|
|
|
deleted_at: r.deleted_at ?? null,
|
|
|
|
|
deleted_by: r.deleted_by ?? null,
|
|
|
|
|
purged_at: null,
|
|
|
|
|
signed_url: null,
|
|
|
|
|
created_at: r.created_at ?? "",
|
|
|
|
|
updated_at: r.updated_at ?? "",
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sidebarInitialData = {
|
|
|
|
|
activeWorkspaceId,
|
|
|
|
|
workspaces,
|
|
|
|
|
documents: docRows as unknown as DocumentRecord[],
|
|
|
|
|
trashedDocuments: trashedDocs as unknown as SidebarInitialData["trashedDocuments"],
|
|
|
|
|
trashedMediaAssets: [],
|
|
|
|
|
trashedMindmapAssets,
|
|
|
|
|
mediaAssets: [],
|
|
|
|
|
mindmapDocs,
|
|
|
|
|
mindmapAssets,
|
|
|
|
|
mindmapAssetChildren,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-18 19:01:31 +08:00
|
|
|
<div className="flex h-screen w-full overflow-hidden bg-wolai-bg text-wolai-text-primary">
|
2026-01-17 10:12:53 +08:00
|
|
|
{sidebarInitialData && <Sidebar initialData={sidebarInitialData} />}
|
2026-01-18 19:01:31 +08:00
|
|
|
<div className="flex min-w-0 flex-1 flex-col h-full bg-white relative overflow-hidden">
|
|
|
|
|
<header className="h-[44px] w-full flex items-center px-4 text-wolai-text-secondary text-sm bg-white/80 backdrop-blur-sm sticky top-0 z-50">
|
2026-01-17 10:12:53 +08:00
|
|
|
<MobileSidebarTrigger />
|
|
|
|
|
<Breadcrumb documents={documents} />
|
|
|
|
|
</header>
|
|
|
|
|
<main className="flex-1 overflow-hidden bg-white">{children}</main>
|
|
|
|
|
<BottomToolbar />
|
|
|
|
|
<SearchPalette workspaceId={sidebarInitialData?.activeWorkspaceId ?? null} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 19:01:31 +08:00
|
|
|
redirect("/login");
|
2025-11-23 10:55:04 +08:00
|
|
|
}
|