import { useEffect, useMemo, useRef, useState, type ChangeEvent, type MouseEvent } from 'react'; import { useLocation } from 'react-router-dom'; import { useChat } from '../context/ChatProvider'; import { cancelMindSpaceAgentJob, createMindSpaceAgentJob, createMindSpacePage, createMindSpacePageFromAsset, deleteMindSpaceAsset, deleteMindSpacePage, deleteMindSpaceScheduleReminders, bindMindSpacePageLiveEdit, getMindSpaceAgentJob, getMindSpace, getMindSpacePage, getMindSpacePageDeletePreview, ignoreMindSpaceScheduleReminder, isMindSpacePageNotFoundError, listMindSpaceAgentJobs, listMindSpaceAssets, listMindSpaceCleanupItems, listMindSpacePages, listMindSpaceAchievementPages, retryMindSpaceAgentJob, runMindSpaceAgentJob, runMindSpaceCleanup, purchaseSpaceQuota, uploadMindSpaceAsset, ApiError, } from '../api/client'; import { MINDSPACE_IMAGE_UPLOAD_MAX_BYTES } from '../utils/imageUpload'; import type { MindSpace, MindSpaceAsset, MindSpaceAgentJob, MindSpaceCategory, MindSpaceCleanupItem, MindSpacePage, MindSpacePageDeletePreview, MindSpaceQuota, MindSpaceScheduleReminder, MindSpaceSaveCategory, PortalUser, } from '../types'; import { buildMindSpaceChatContext } from '../utils/mindspaceChatContext'; import { resolveH5ApiBase } from '../utils/h5ApiBase'; import { MindSpaceSpaceChat } from './MindSpaceSpaceChat'; import { assetIcon, assetSourceLabel, buildAbsoluteAssetDownloadUrl, buildAssetImageUrl, buildAssetPreviewFrameConfig, isImageAsset, isPreviewableAsset, isWorkAsset, } from '../utils/mindspaceCards'; import { PREVIEW_ASSETS, PREVIEW_PAGES, PREVIEW_SPACE } from '../dev/mindspacePreviewData'; import { MindSpaceFeedCard } from './MindSpaceFeedCard'; import { MindSpaceAchievementsPanel } from './MindSpaceAchievementsPanel'; import { MindSpacePageDetail } from './MindSpacePageDetail'; import { MindSpaceModal } from './MindSpaceModal'; import { MindSpaceDeletePlazaOption } from './MindSpaceDeletePlazaOption'; import { ShareSheet } from './ShareSheet'; import { WorkCover } from './WorkCover'; import { buildAssetSharePayload, buildPageSharePayload, type SharePayload } from '../utils/shareChannels'; import { resolveMindSpaceHomeUrl, resolveMindSpacePageOwnerPreviewUrl, resolveMindSpacePagePublicViewUrl, resolvePublicPageUrl, resolvePlazaHomeUrl } from '../utils/publicUrl'; import { PagePreviewFrame } from './PagePreviewFrame'; import { BalanceRing } from './BalanceRing'; import { NotificationCenter } from './NotificationCenter'; const CATEGORY_DESCRIPTIONS: Record = { oa: '上传文档、表格和资料,让 AI 生成报告与页面', private: '已停用', public: '管理可公开使用的页面、图片和静态资源', draft: '继续编辑尚未发布或等待安全检查的页面', archive: '保存不再活跃但仍需要保留的内容', }; const CATEGORY_ACTIONS: Partial> = { oa: '进入工作区', public: '查看发布', draft: '继续创作', }; const AGENT_JOBS_PAGE_SIZE = 10; const RECENT_PAGES_PAGE_SIZE = 6; const ALL_PAGES_PAGE_SIZE = 15; const ACHIEVEMENTS_PAGE_SIZE = 20; function sortPagesByCreatedAt(pages: MindSpacePage[]) { return [...pages].sort((left, right) => { const createdDiff = (right.createdAt ?? 0) - (left.createdAt ?? 0); if (createdDiff !== 0) return createdDiff; return (right.updatedAt ?? 0) - (left.updatedAt ?? 0); }); } const IMAGE_PAGE_SIZE = 10; const DEFAULT_MAX_UPLOAD_FILE_BYTES = 30 * 1024 * 1024; const UPLOAD_FILE_EXTENSIONS = [ '.doc', '.docx', '.xls', '.xlsx', '.pdf', '.ppt', '.pptx', '.png', '.jpg', '.jpeg', '.webp', '.md', '.csv', '.txt', '.html', '.htm', ] as const; const UPLOAD_ACCEPT = UPLOAD_FILE_EXTENSIONS.join(','); const UPLOAD_FILE_TYPE_LABEL = 'Word、Excel、PDF、PPT、图片、Markdown、CSV、TXT 和 HTML'; const JOB_STATUS_LABELS: Record = { queued: '排队中', running: '执行中', completed: '已完成', failed: '失败', cancelled: '已取消', timed_out: '超时', }; const PREVIEW_SCHEDULE_REMINDERS = [ { id: 'schedule-preview-rem-1', time: '明天 09:00', title: '带项目材料去会议', meta: '提前 30 分钟 · 服务号推送', notifyLabel: '待通知', canIgnore: true, }, { id: 'schedule-preview-rem-2', time: '今天 14:00', title: '产品评审会', meta: '服务号推送', notifyLabel: '已通知', canIgnore: false, }, ] as const; function scheduleReminderNotifyLabel(status: MindSpaceScheduleReminder['status']) { return status === 'sent' ? '已通知' : '待通知'; } function formatReminderEventWindow(reminder: MindSpaceScheduleReminder) { const tz = reminder.itemTimezone || 'Asia/Shanghai'; const start = reminder.itemStartAt ?? null; const end = reminder.itemEndAt ?? null; if (!start) return null; const startLabel = formatScheduleWhen(start, tz); if (!end || end === start) return `事项 ${startLabel}`; const endClock = formatScheduleClock(end, tz); return `事项 ${startLabel} ~ ${endClock}`; } function formatScheduleClock(value: number | null | undefined, timezone?: string | null) { if (typeof value !== 'number' || Number.isNaN(value)) return '—'; try { return new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: timezone || undefined, }).format(new Date(value)); } catch { return new Date(value).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false, }); } } function formatDigestSchedule(digest: { hour: number; minute: number; timezone?: string | null; nextRunAt?: number | null; }) { const time = `${String(digest.hour).padStart(2, '0')}:${String(digest.minute).padStart(2, '0')}`; const nextRun = formatScheduleClock(digest.nextRunAt, digest.timezone); return nextRun === '—' ? time : `${time} · 下次 ${nextRun}`; } function scheduleLocalDateParts(epochMs: number, timezone?: string | null) { const parts = new Intl.DateTimeFormat('en-US', { timeZone: timezone || 'Asia/Shanghai', year: 'numeric', month: 'numeric', day: 'numeric', }).formatToParts(new Date(epochMs)); const pick = (type: string) => Number(parts.find((part) => part.type === type)?.value ?? 0); return { year: pick('year'), month: pick('month'), day: pick('day') }; } function isSameScheduleDay(aMs: number, bMs: number, timezone?: string | null) { const a = scheduleLocalDateParts(aMs, timezone); const b = scheduleLocalDateParts(bMs, timezone); return a.year === b.year && a.month === b.month && a.day === b.day; } function formatScheduleDayLabel(epochMs: number, timezone?: string | null) { const tz = timezone || 'Asia/Shanghai'; const now = Date.now(); if (isSameScheduleDay(epochMs, now, tz)) return '今天'; if (isSameScheduleDay(epochMs, now + 24 * 60 * 60 * 1000, tz)) return '明天'; try { return new Intl.DateTimeFormat('zh-CN', { timeZone: tz, month: 'numeric', day: 'numeric', weekday: 'short', }).format(new Date(epochMs)); } catch { return formatScheduleDayLabel(epochMs, 'Asia/Shanghai'); } } function formatScheduleWhen(epochMs: number | null | undefined, timezone?: string | null, allDay = false) { if (typeof epochMs !== 'number' || Number.isNaN(epochMs)) return '—'; const day = formatScheduleDayLabel(epochMs, timezone); if (allDay) return day; const clock = formatScheduleClock(epochMs, timezone); return clock === '—' ? day : `${day} ${clock}`; } const PREVIEW_PAGE_READERS: Record = { 'page-1': { kicker: '市场趋势', body: ['消费者对 AI 辅助决策的接受度持续提升,企业侧开始从试点转向流程内嵌。', '下一阶段重点是把洞察沉淀为可复用页面,而不是停留在一次性聊天结论。'], }, 'page-2': { kicker: '旅行攻略', body: ['吉隆坡和槟城适合组合成 5 天轻行程,城市交通、夜市和海岛体验都比较均衡。', '预算建议优先保证住宿位置,减少跨城交通带来的时间损耗。'], }, 'page-3': { kicker: '客户成功', body: ['本月高价值客户续费意向稳定,但两个账号存在使用频次下降信号。', '建议在下周安排专项回访,并用案例页同步给销售团队。'], }, 'page-4': { kicker: '经营摘要', body: ['收入增长主要来自老客户扩容,新线索转化仍需提高。', '成本侧波动集中在交付人力,建议把高频交付问题整理为自动化模板。'], }, 'page-5': { kicker: '私人清单', body: ['出行前 48 小时确认证件、充电器、常用药和当地交通 App。', '个人资料区默认不公开,适合保存只给自己看的备忘。'], }, 'page-6': { kicker: '选型笔记', body: ['模型选择需要同时看质量、延迟、成本和可观测性。', '对高频生产任务,优先选择稳定性更强、可回放的流程。'], }, 'page-7': { kicker: '发布指南', body: ['公开发布前先做风险检查,再确认标题、摘要和封面是否准确。', '适合进入 Plaza 的内容应该有明确主题和可快速理解的首屏。'], }, 'page-8': { kicker: '活动复盘', body: ['旧活动的关键价值在于保留假设、执行过程和结论,而不是只保存结果截图。', '归档内容保持可搜索即可,不需要占据首页主要空间。'], }, }; type Html2CanvasRenderer = ( element: HTMLElement, options?: Record, ) => Promise; type Html2CanvasWindow = Window & { html2canvas?: Html2CanvasRenderer }; const HTML2CANVAS_CDN = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js'; const LONG_IMAGE_VIEWPORT_WIDTH = 1280; const LONG_IMAGE_VIEWPORT_HEIGHT = 720; const LONG_IMAGE_RENDER_SCALE = 2; const LONG_IMAGE_MAX_PIXELS = 64_000_000; const SPACE_PURCHASE_PRESETS_MB = [5, 10, 20]; let html2CanvasLoader: Promise | null = null; function formatYuan(cents: number) { return `¥${(cents / 100).toFixed(2)}`; } async function loadHtml2CanvasRenderer(): Promise { const existing = (window as Html2CanvasWindow).html2canvas; if (existing) return existing; if (html2CanvasLoader) return html2CanvasLoader; html2CanvasLoader = new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = HTML2CANVAS_CDN; script.async = true; script.onload = () => { const loaded = (window as Html2CanvasWindow).html2canvas; if (!loaded) { reject(new Error('无法加载页面截图能力')); return; } resolve(loaded); }; script.onerror = () => { reject(new Error('无法加载页面截图能力')); }; document.body.appendChild(script); }).catch((error) => { html2CanvasLoader = null; throw error; }); return html2CanvasLoader; } function blobToDataUrl(blob: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result ?? '')); reader.onerror = () => reject(new Error('无法读取图片数据')); reader.readAsDataURL(blob); }); } function downloadBlobAsFile(dataUrl: string, filename: string) { const link = document.createElement('a'); link.href = dataUrl; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); } function buildExportFilename(pageId: string, title: string) { const safeTitle = String(title ?? '') .trim() .replace(/[\\/*?:"<>|]/g, '-') .replace(/\s+/g, '-') .slice(0, 24); const suffix = safeTitle ? `-${safeTitle}` : ''; const timestamp = new Date().toISOString().replace(/[T:]/g, '-').replace(/\..+/, ''); return `mindspace-page-${pageId}${suffix}-${timestamp}.png`; } function fileExtension(filename: string) { const dotIndex = filename.lastIndexOf('.'); return dotIndex >= 0 ? filename.slice(dotIndex).toLowerCase() : ''; } function isUploadImageFile(file: File) { const extension = fileExtension(file.name); return ( file.type.startsWith('image/') || extension === '.png' || extension === '.jpg' || extension === '.jpeg' || extension === '.webp' ); } function validateSelectedUploadFile(file: File, maxBytes: number) { if (file.size <= 0) return '文件不能为空,请重新选择。'; const extension = fileExtension(file.name); if (!UPLOAD_FILE_EXTENSIONS.includes(extension as (typeof UPLOAD_FILE_EXTENSIONS)[number])) { return `暂不支持 ${extension || '无扩展名'} 文件,请选择支持的资料格式。`; } const effectiveMaxBytes = isUploadImageFile(file) ? MINDSPACE_IMAGE_UPLOAD_MAX_BYTES : maxBytes; if (file.size > effectiveMaxBytes) { return `文件不能超过 ${formatBytes(effectiveMaxBytes)},当前为 ${formatBytes(file.size)}。`; } return null; } function ensureBlobFromCanvas(canvas: HTMLCanvasElement): Promise { return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (!blob) { reject(new Error('图片生成失败')); return; } resolve(blob); }, 'image/png'); }); } async function exportIframeToPng(iframe: HTMLIFrameElement): Promise { const renderer = await loadHtml2CanvasRenderer(); const doc = iframe.contentDocument; if (!doc || !doc.body) { throw new Error('预览内容无法访问'); } const width = Math.max( doc.documentElement?.scrollWidth ?? 0, doc.body.scrollWidth, LONG_IMAGE_VIEWPORT_WIDTH, ); const height = Math.max( doc.documentElement?.scrollHeight ?? 0, doc.body.scrollHeight, LONG_IMAGE_VIEWPORT_HEIGHT, ); const scale = Math.min( LONG_IMAGE_RENDER_SCALE, Math.max(1, Math.sqrt(LONG_IMAGE_MAX_PIXELS / Math.max(width * height, 1))), ); const canvas = await renderer(doc.body, { useCORS: true, scale, backgroundColor: '#ffffff', width, height, windowWidth: width, windowHeight: height, x: 0, y: 0, scrollX: 0, scrollY: 0, }); const blob = await ensureBlobFromCanvas(canvas); return await blobToDataUrl(blob); } async function waitForIframeAssets(iframe: HTMLIFrameElement): Promise { const doc = iframe.contentDocument; if (!doc) return; await doc.fonts?.ready.catch(() => undefined); await Promise.all( Array.from(doc.images) .filter((img) => !img.complete) .map( (img) => new Promise((resolve) => { img.addEventListener('load', () => resolve(), { once: true }); img.addEventListener('error', () => resolve(), { once: true }); }), ), ); await new Promise((resolve) => window.setTimeout(resolve, 450)); } async function exportPagePreviewToPng(page: MindSpacePage): Promise { const iframe = document.createElement('iframe'); iframe.src = `/api/mindspace/v1/pages/${encodeURIComponent(page.id)}/preview?v=${page.versionNo ?? page.updatedAt}`; iframe.setAttribute('aria-hidden', 'true'); iframe.tabIndex = -1; iframe.style.position = 'fixed'; iframe.style.left = '-99999px'; iframe.style.top = '0'; iframe.style.width = `${LONG_IMAGE_VIEWPORT_WIDTH}px`; iframe.style.height = `${LONG_IMAGE_VIEWPORT_HEIGHT}px`; iframe.style.border = '0'; iframe.style.opacity = '0'; iframe.style.pointerEvents = 'none'; document.body.appendChild(iframe); try { await new Promise((resolve, reject) => { const timer = window.setTimeout(() => reject(new Error('页面预览加载超时')), 12000); iframe.onload = () => { window.clearTimeout(timer); resolve(); }; iframe.onerror = () => { window.clearTimeout(timer); reject(new Error('页面预览加载失败')); }; }); await waitForIframeAssets(iframe); return await exportIframeToPng(iframe); } finally { iframe.remove(); } } function formatBytes(bytes: number) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(1)} MB`; } type AssetFilter = 'all' | 'images' | 'files' | 'pages'; function previewBlocked() { return new Error('预览模式仅供查看,不能修改内容'); } function canGenerateWithAgent(asset: MindSpaceAsset) { const textLikeMimeTypes = new Set([ 'text/plain', 'text/markdown', 'text/csv', 'text/html', 'application/json', ]); return ( asset.status === 'ready' && asset.scanStatus !== 'blocked' && textLikeMimeTypes.has(asset.mimeType) ); } type MindSpaceRouteSync = { pushHome: () => void; pushAchievements: () => void; pushCategory: (code: MindSpaceSaveCategory | MindSpaceCategory['code']) => void; pushPage: (pageId: string) => void; }; function MindSpaceBalanceRing() { const { balanceCents, totalCreditCents, tokensUsed, subscription, openRecharge, openSubscribe } = useChat(); if (typeof balanceCents !== 'number') return null; return ( ); } function MindSpaceNotificationCenter() { const { openRecharge } = useChat(); return openRecharge(false)} />; } export function MindSpaceView({ user, previewMode = false, initialPageId, initialCategoryCode, initialAchievementsOpen = false, onBack, onLogout, onOpenFeedback, routeSync, }: { user: PortalUser; previewMode?: boolean; initialPageId?: string | null; initialCategoryCode?: MindSpaceSaveCategory | null; initialAchievementsOpen?: boolean; onBack: () => void; onLogout: () => void; onOpenFeedback?: () => void; routeSync?: MindSpaceRouteSync; }) { const [space, setSpace] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [selectedCategory, setSelectedCategory] = useState(null); const [assets, setAssets] = useState([]); const [pages, setPages] = useState([]); const [selectedPageId, setSelectedPageId] = useState(initialPageId ?? null); const [selectedPageAutoPlaza, setSelectedPageAutoPlaza] = useState(false); const [assetsLoading, setAssetsLoading] = useState(false); const [uploadOpen, setUploadOpen] = useState(false); const [uploading, setUploading] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [uploadError, setUploadError] = useState(null); const [pendingDeleteId, setPendingDeleteId] = useState(null); const [selectedAssetIds, setSelectedAssetIds] = useState([]); const [bulkDeletingAssets, setBulkDeletingAssets] = useState(false); const [selectedDraftPageIds, setSelectedDraftPageIds] = useState([]); const [bulkDeletingDraftPages, setBulkDeletingDraftPages] = useState(false); const [newPageOpen, setNewPageOpen] = useState(false); const [newPageTitle, setNewPageTitle] = useState(''); const [newPageContent, setNewPageContent] = useState(''); const [newPageTemplate, setNewPageTemplate] = useState('editorial'); const [creatingPage, setCreatingPage] = useState(false); const [assetFilter, setAssetFilter] = useState('all'); const [imagePage, setImagePage] = useState(0); const [previewAssetId, setPreviewAssetId] = useState(null); const [openAssetMenuId, setOpenAssetMenuId] = useState(null); const [editingAssetId, setEditingAssetId] = useState(null); const [cleanupOpen, setCleanupOpen] = useState(false); const [cleanupItems, setCleanupItems] = useState([]); const [cleanupLoading, setCleanupLoading] = useState(false); const [cleanupRunning, setCleanupRunning] = useState(false); const [selectedCleanupIds, setSelectedCleanupIds] = useState([]); const [agentAsset, setAgentAsset] = useState(null); const [agentInstruction, setAgentInstruction] = useState(''); const [agentJob, setAgentJob] = useState(null); const [agentSubmitting, setAgentSubmitting] = useState(false); const [agentJobsPanelOpen, setAgentJobsPanelOpen] = useState(false); const [agentJobsList, setAgentJobsList] = useState([]); const [agentJobsTotal, setAgentJobsTotal] = useState(0); const [agentJobsPage, setAgentJobsPage] = useState(0); const [agentJobsLoading, setAgentJobsLoading] = useState(false); const [allPagesOpen, setAllPagesOpen] = useState(false); const [allPagesItems, setAllPagesItems] = useState([]); const [allPagesTotal, setAllPagesTotal] = useState(0); const [allPagesPageIndex, setAllPagesPageIndex] = useState(0); const [allPagesLoading, setAllPagesLoading] = useState(false); const [achievementsOpen, setAchievementsOpen] = useState(initialAchievementsOpen); const [achievementsItems, setAchievementsItems] = useState([]); const [achievementsTotal, setAchievementsTotal] = useState(0); const [achievementsPageIndex, setAchievementsPageIndex] = useState(0); const [achievementsLoading, setAchievementsLoading] = useState(false); const [selectedAchievementPageIds, setSelectedAchievementPageIds] = useState([]); const [bulkDeletingAchievements, setBulkDeletingAchievements] = useState(false); const [selectedAllPageIds, setSelectedAllPageIds] = useState([]); const [bulkDeletingAllPages, setBulkDeletingAllPages] = useState(false); const [selectedScheduleReminderIds, setSelectedScheduleReminderIds] = useState([]); const [scheduleReminderActionLoading, setScheduleReminderActionLoading] = useState(false); const [recentPagesTotal, setRecentPagesTotal] = useState(0); const [exportingPageId, setExportingPageId] = useState(null); const [sharePayload, setSharePayload] = useState(null); const [previewPageTarget, setPreviewPageTarget] = useState(null); const [pageDeleteTarget, setPageDeleteTarget] = useState(null); const [pageDeletePreview, setPageDeletePreview] = useState( null, ); const [pageDeleteLoading, setPageDeleteLoading] = useState(false); const [pageDeleteRemoveFromPlaza, setPageDeleteRemoveFromPlaza] = useState(false); const [deletingPageId, setDeletingPageId] = useState(null); const [bulkPageDeleteOpen, setBulkPageDeleteOpen] = useState(false); const [bulkPageDeleteSource, setBulkPageDeleteSource] = useState<'all' | 'draft' | 'achievements'>('all'); const [bulkPageDeletePreviews, setBulkPageDeletePreviews] = useState< MindSpacePageDeletePreview[] | null >(null); const [bulkPageDeleteLoading, setBulkPageDeleteLoading] = useState(false); const [bulkPageDeleteRemoveFromPlaza, setBulkPageDeleteRemoveFromPlaza] = useState(false); const [assetDeleteDialog, setAssetDeleteDialog] = useState<{ message: string; references: Array<{ id: string; title: string; publishedOnline?: boolean }>; } | null>(null); const [pageLiveContext, setPageLiveContext] = useState<{ page: MindSpacePage; title: string; summary: string; content: string; } | null>(null); const [pageRefreshTrigger, setPageRefreshTrigger] = useState(0); const [pageFullscreenPreviewOpen, setPageFullscreenPreviewOpen] = useState(false); const [spacePurchaseOpen, setSpacePurchaseOpen] = useState(false); const [spacePurchaseMb, setSpacePurchaseMb] = useState('5'); const [spacePurchasePending, setSpacePurchasePending] = useState(false); const [spacePurchaseMessage, setSpacePurchaseMessage] = useState(null); const { chatState, messages, session, balanceCents, completeRecharge, openRecharge } = useChat(); const prevChatStateRef = useRef(chatState); const h5ApiBase = useMemo(() => resolveH5ApiBase(), []); const location = useLocation(); const fromPlaza = useMemo(() => { const params = new URLSearchParams(location.search); return params.get('utm_source') === 'plaza'; }, [location.search]); const plazaReturnUrl = useMemo(() => { const params = new URLSearchParams(location.search); return params.get('return_to') || resolvePlazaHomeUrl(); }, [location.search]); const maxUploadFileBytes = space?.quota.maxFileBytes ?? DEFAULT_MAX_UPLOAD_FILE_BYTES; useEffect(() => { if (!selectedPageId) { setPageLiveContext(null); setPageFullscreenPreviewOpen(false); } }, [selectedPageId]); useEffect(() => { if (!allPagesOpen && !achievementsOpen && !selectedPageId) return; window.requestAnimationFrame(() => { window.scrollTo({ top: 0, left: 0 }); }); }, [allPagesOpen, achievementsOpen, selectedPageId]); useEffect(() => { if (!selectedPageId || !session?.id || previewMode || pageFullscreenPreviewOpen) return; void bindMindSpacePageLiveEdit(selectedPageId, session.id).catch(() => {}); }, [selectedPageId, session?.id, previewMode, pageFullscreenPreviewOpen]); useEffect(() => { if (!openAssetMenuId) return; const handlePointerDown = (event: PointerEvent) => { const target = event.target; if (!(target instanceof Element)) return; if (target.closest('.mindspace-item-actions')) return; setOpenAssetMenuId(null); }; document.addEventListener('pointerdown', handlePointerDown); return () => document.removeEventListener('pointerdown', handlePointerDown); }, [openAssetMenuId]); const selectedAssetsForChat = useMemo( () => selectedAssetIds .map((id) => assets.find((asset) => asset.id === id)) .filter((asset): asset is MindSpaceAsset => Boolean(asset)), [selectedAssetIds, assets], ); const mindspaceChatContext = useMemo(() => { if (!space) return null; return buildMindSpaceChatContext({ space, ownerUsername: user.username, selectedCategory, selectedPageId, pages, pageLive: pageLiveContext, assets, selectedAssets: selectedAssetsForChat, route: `${location.pathname}${location.search}`, agentSessionId: session?.id ?? null, h5ApiBase, }); }, [ space, user.username, selectedCategory, selectedPageId, pages, pageLiveContext, assets, selectedAssetsForChat, location.pathname, location.search, session?.id, h5ApiBase, ]); const truncateLabel = (text: string, max = 48) => { const value = String(text ?? '').replace(/\s+/g, ' ').trim(); if (value.length <= max) return value; return `${value.slice(0, max - 1)}…`; }; const resolvePagePublicViewUrl = async (page: MindSpacePage): Promise => { const fromList = resolveMindSpacePagePublicViewUrl(page); if (fromList) return fromList; try { const pageDetail = await getMindSpacePage(page.id); const publicUrl = pageDetail.publication?.publicUrl ?? pageDetail.publicationUrl; return publicUrl ? resolvePublicPageUrl(publicUrl) : null; } catch { return null; } }; const openPageViewInNewTab = async (page: MindSpacePage) => { if (previewMode) { setError('预览模式下暂不支持打开页面'); return; } const publicUrl = await resolvePagePublicViewUrl(page); const viewUrl = publicUrl ?? resolveMindSpacePageOwnerPreviewUrl(page); window.open(viewUrl, '_blank', 'noopener,noreferrer'); }; const openPageShare = async (page: MindSpacePage) => { if (previewMode) { setError('预览模式下暂不支持分享'); return; } const publicUrl = await resolvePagePublicViewUrl(page); if (!publicUrl) { setError('该页面尚未对外发布,暂无法分享链接;你可以先打开预览,或在编辑页完成发布后再分享'); return; } setSharePayload(buildPageSharePayload(page, publicUrl)); }; const showPage = (pageId: string, autoPlaza = false) => { setSelectedPageId(pageId); setSelectedPageAutoPlaza(autoPlaza); routeSync?.pushPage(pageId); }; const openPagePreview = (page: MindSpacePage) => { setPreviewPageTarget(page); }; const openPageDeleteDialog = async (page: MindSpacePage) => { if (previewMode) { setError('预览模式下无法删除页面'); return; } setPageDeleteTarget(page); setPageDeletePreview(null); setPageDeleteRemoveFromPlaza(false); setPageDeleteLoading(true); setError(null); try { setPageDeletePreview(await getMindSpacePageDeletePreview(page.id)); } catch (err) { setPageDeleteTarget(null); setError(err instanceof Error ? err.message : '无法加载删除预览'); } finally { setPageDeleteLoading(false); } }; const closePageDeleteDialog = () => { if (deletingPageId) return; setPageDeleteTarget(null); setPageDeletePreview(null); setPageDeleteRemoveFromPlaza(false); }; const confirmDeletePage = async () => { if (!pageDeleteTarget) return; setDeletingPageId(pageDeleteTarget.id); setError(null); try { const result = await deleteMindSpacePage(pageDeleteTarget.id, { removeFromPlaza: pageDeleteRemoveFromPlaza, }); if (selectedPageId === pageDeleteTarget.id) { closePage(); } setPageDeleteTarget(null); setPageDeletePreview(null); setPageDeleteRemoveFromPlaza(false); setSelectedAchievementPageIds((ids) => ids.filter((id) => id !== pageDeleteTarget.id)); setAchievementsItems((items) => items.filter((page) => page.id !== pageDeleteTarget.id)); setAchievementsTotal((total) => Math.max(0, total - 1)); await refreshMindSpaceSnapshot({ quota: result.quota, freedBytes: result.freedBytes }); if (achievementsOpen) { await loadAchievementsPage(achievementsPageIndex); } } catch (err) { if (isMindSpacePageNotFoundError(err)) { if (selectedPageId === pageDeleteTarget.id) { closePage(); } setPageDeleteTarget(null); setPageDeletePreview(null); setPageDeleteRemoveFromPlaza(false); setSelectedAchievementPageIds((ids) => ids.filter((id) => id !== pageDeleteTarget.id)); setAchievementsItems((items) => items.filter((page) => page.id !== pageDeleteTarget.id)); setAchievementsTotal((total) => Math.max(0, total - 1)); await refreshMindSpaceSnapshot(); if (achievementsOpen) { await loadAchievementsPage(achievementsPageIndex); } return; } setError(err instanceof Error ? err.message : '删除页面失败'); } finally { setDeletingPageId(null); } }; const buildPageCardActions = (page: MindSpacePage) => [ { label: '预览', onClick: (event: MouseEvent) => { event.stopPropagation(); openPagePreview(page); }, }, { label: '编辑', onClick: (event: MouseEvent) => { event.stopPropagation(); showPage(page.id); }, }, { label: '公开链接', onClick: (event: MouseEvent) => { event.stopPropagation(); void openPagePublicShare(page); }, }, { label: exportingPageId === page.id ? '保存中…' : '保存长图', onClick: (event: MouseEvent) => { event.stopPropagation(); void exportPageLongImage(page); }, }, { label: '删除', onClick: (event: MouseEvent) => { event.stopPropagation(); void openPageDeleteDialog(page); }, }, ]; const closePage = () => { setSelectedPageId(null); if (selectedCategory) { routeSync?.pushCategory(selectedCategory.code); } else { routeSync?.pushHome(); } }; const openPagePublicShare = async (page: MindSpacePage) => { await openPageViewInNewTab(page); }; const exportPageLongImage = async (page: MindSpacePage) => { if (previewMode) { setError('预览模式下暂不支持保存长图'); return; } setExportingPageId(page.id); setError(null); try { const dataUrl = await exportPagePreviewToPng(page); downloadBlobAsFile(dataUrl, buildExportFilename(page.id, page.title || 'page')); } catch (err) { setError(err instanceof Error ? err.message : '保存长图失败'); } finally { setExportingPageId((current) => (current === page.id ? null : current)); } }; const closeCategory = () => { setSelectedCategory(null); setUploadOpen(false); setPendingDeleteId(null); setSelectedAssetIds([]); setImagePage(0); void refreshSpaceQuietly(); routeSync?.pushHome(); }; const loadCleanupItems = async () => { if (previewMode) { setCleanupItems([]); return; } setCleanupLoading(true); try { const result = await listMindSpaceCleanupItems(); setCleanupItems(result.items); setSelectedCleanupIds(result.items.map((item) => item.id)); } catch (err) { setError(err instanceof Error ? err.message : '临时文件扫描失败'); } finally { setCleanupLoading(false); } }; const runCleanup = async () => { if (!selectedCleanupIds.length) return; if (previewMode) { setError(previewBlocked().message); return; } setCleanupRunning(true); setError(null); try { const result = await runMindSpaceCleanup(selectedCleanupIds); if (result.quota) { setSpace((prev) => (prev ? { ...prev, quota: result.quota! } : prev)); } else { await refreshSpaceQuietly(); } setCleanupOpen(false); setCleanupItems([]); setSelectedCleanupIds([]); } catch (err) { setError(err instanceof Error ? err.message : '清理失败'); } finally { setCleanupRunning(false); } }; const loadAllPagesPage = async (pageIndex: number) => { if (previewMode) { setAllPagesItems(PREVIEW_PAGES); setAllPagesTotal(PREVIEW_PAGES.length); setAllPagesPageIndex(0); return; } setAllPagesLoading(true); setError(null); try { const { items, page } = await listMindSpacePages({ limit: ALL_PAGES_PAGE_SIZE, offset: pageIndex * ALL_PAGES_PAGE_SIZE, }); setAllPagesItems(items); setAllPagesPageIndex(pageIndex); setAllPagesTotal(page.total ?? items.length); } catch (err) { setError(err instanceof Error ? err.message : '全部页面加载失败'); } finally { setAllPagesLoading(false); } }; const openAllPagesPanel = () => { setSelectedCategory(null); setSelectedPageId(null); setNewPageOpen(false); setAgentJobsPanelOpen(false); setAchievementsOpen(false); setAllPagesOpen(true); setSelectedAllPageIds([]); routeSync?.pushHome(); void loadAllPagesPage(0); }; const loadAchievementsPage = async (pageIndex: number) => { if (previewMode) { setAchievementsItems(PREVIEW_PAGES); setAchievementsTotal(PREVIEW_PAGES.length); setAchievementsPageIndex(0); setSelectedAchievementPageIds([]); return; } setAchievementsLoading(true); setError(null); try { const { items, page } = await listMindSpaceAchievementPages({ limit: ACHIEVEMENTS_PAGE_SIZE, offset: pageIndex * ACHIEVEMENTS_PAGE_SIZE, includeStats: false, }); setAchievementsItems(items); setAchievementsPageIndex(pageIndex); setAchievementsTotal(page.total ?? items.length); setSelectedAchievementPageIds([]); } catch (err) { setError(err instanceof Error ? err.message : '成果列表加载失败'); } finally { setAchievementsLoading(false); } }; const openAchievementsPanel = () => { setSelectedCategory(null); setSelectedPageId(null); setNewPageOpen(false); setAgentJobsPanelOpen(false); setAllPagesOpen(false); setAchievementsOpen(true); setSelectedAchievementPageIds([]); routeSync?.pushAchievements(); void loadAchievementsPage(0); }; const closeAchievementsPanel = () => { setAchievementsOpen(false); setSelectedAchievementPageIds([]); routeSync?.pushHome(); }; const refreshAgentJobsSummary = async () => { if (previewMode) return; const { page } = await listMindSpaceAgentJobs({ limit: 1, offset: 0 }); setAgentJobsTotal(page.total ?? 0); }; const loadAgentJobsPage = async (pageIndex: number) => { if (previewMode) { setAgentJobsList([]); setAgentJobsTotal(0); return; } setAgentJobsLoading(true); setError(null); try { const { items, page } = await listMindSpaceAgentJobs({ limit: AGENT_JOBS_PAGE_SIZE, offset: pageIndex * AGENT_JOBS_PAGE_SIZE, }); setAgentJobsList(items); setAgentJobsPage(pageIndex); setAgentJobsTotal(page.total ?? 0); } catch (err) { setError(err instanceof Error ? err.message : '任务记录加载失败'); } finally { setAgentJobsLoading(false); } }; const openAgentJobsPanel = () => { setSelectedCategory(null); setSelectedPageId(null); setNewPageOpen(false); setAllPagesOpen(false); setAchievementsOpen(false); setAgentJobsPanelOpen(true); routeSync?.pushHome(); void loadAgentJobsPage(0); }; const closeAgentJobsPanel = () => { setAgentJobsPanelOpen(false); }; const refreshAgentJobsPanel = async () => { await refreshAgentJobsSummary(); if (agentJobsPanelOpen) { await loadAgentJobsPage(agentJobsPage); } }; const load = async () => { setLoading(true); setError(null); try { if (previewMode) { setSpace(PREVIEW_SPACE); setPages(PREVIEW_PAGES); return; } const [nextSpace, recentPagesResult] = await Promise.all([ getMindSpace(), listMindSpacePages({ limit: RECENT_PAGES_PAGE_SIZE, offset: 0 }), ]); setSpace(nextSpace); setPages(recentPagesResult.items); setRecentPagesTotal(recentPagesResult.page.total ?? recentPagesResult.items.length); // The agent-job count is supplementary. A slow job store must not keep the // already-loaded MindSpace home screen in its global loading state. void refreshAgentJobsSummary().catch(() => { setAgentJobsTotal(0); }); } catch (err) { setError(err instanceof Error ? err.message : '空间加载失败'); } finally { setLoading(false); } }; const refreshSpaceQuietly = async () => { if (previewMode) return null; try { const nextSpace = await getMindSpace(); setSpace(nextSpace); return nextSpace; } catch { return null; } }; const refreshPagesSilently = async () => { if (previewMode) return; try { const recentPagesResult = await listMindSpacePages({ limit: RECENT_PAGES_PAGE_SIZE, offset: 0, }); setPages(recentPagesResult.items); setRecentPagesTotal(recentPagesResult.page.total ?? recentPagesResult.items.length); if (allPagesOpen) { await loadAllPagesPage(allPagesPageIndex); } if (achievementsOpen) { await loadAchievementsPage(achievementsPageIndex); } } catch { // ignore background list refresh errors } }; const refreshMindSpaceSnapshot = async (options?: { quota?: MindSpaceQuota; freedBytes?: number; }) => { if (options?.quota) { setSpace((prev) => (prev ? { ...prev, quota: options.quota! } : prev)); } else if (typeof options?.freedBytes === 'number' && options.freedBytes > 0) { setSpace((prev) => { if (!prev) return prev; const usedBytes = Math.max(0, prev.quota.usedBytes - options.freedBytes!); return { ...prev, quota: { ...prev.quota, usedBytes, availableBytes: Math.max( 0, prev.quota.quotaBytes - usedBytes - prev.quota.reservedBytes, ), }, }; }); } else { await refreshSpaceQuietly(); } await refreshPagesSilently(); }; useEffect(() => { const previous = prevChatStateRef.current; prevChatStateRef.current = chatState; const wasBusy = previous === 'streaming' || previous === 'waiting'; if (!wasBusy || chatState !== 'idle' || !selectedPageId || previewMode || pageFullscreenPreviewOpen) return; void refreshPagesSilently(); }, [chatState, pageFullscreenPreviewOpen, previewMode, selectedPageId]); useEffect(() => { void load(); }, []); useEffect(() => { if (previewMode) return; const refreshSchedule = () => { if (!selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen && !achievementsOpen) { void refreshSpaceQuietly(); } }; window.addEventListener('focus', refreshSchedule); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') refreshSchedule(); }); return () => window.removeEventListener('focus', refreshSchedule); }, [ previewMode, selectedCategory, selectedPageId, newPageOpen, agentJobsPanelOpen, allPagesOpen, achievementsOpen, ]); useEffect(() => { if (initialPageId) setSelectedPageId(initialPageId); }, [initialPageId]); useEffect(() => { if (!space || !initialCategoryCode || initialCategoryCode === 'draft' || initialAchievementsOpen) return; const category = space.categories.find((item) => item.code === initialCategoryCode); if (category) void openCategory(category); }, [space, initialCategoryCode, initialAchievementsOpen]); useEffect(() => { if (!initialAchievementsOpen || previewMode) return; setSelectedCategory(null); setSelectedPageId(null); setNewPageOpen(false); setAgentJobsPanelOpen(false); setAllPagesOpen(false); setAchievementsOpen(true); void loadAchievementsPage(0); }, [initialAchievementsOpen, previewMode]); useEffect(() => { if (previewMode || !agentJob || !['queued', 'running'].includes(agentJob.status)) return; const timer = window.setInterval(() => { void getMindSpaceAgentJob(agentJob.id) .then(async (nextJob) => { setAgentJob(nextJob); await refreshAgentJobsPanel(); if (nextJob.status === 'completed' && nextJob.resultPageId) { await load(); setAgentAsset(null); setAgentInstruction(''); setSelectedPageId(nextJob.resultPageId); routeSync?.pushPage(nextJob.resultPageId); } }) .catch((err) => { setError(err instanceof Error ? err.message : '任务状态刷新失败'); }); }, 1200); return () => window.clearInterval(timer); }, [agentJob, previewMode]); const openCategory = async (category: MindSpaceCategory) => { setAgentJobsPanelOpen(false); setAllPagesOpen(false); setAchievementsOpen(false); setSelectedCategory(category); setSelectedPageId(null); setNewPageOpen(false); setUploadOpen(false); setPendingDeleteId(null); setImagePage(0); setAssetsLoading(true); setError(null); try { if (previewMode) { setAssets(PREVIEW_ASSETS[category.code] ?? []); if (category.code === 'draft') { setPages(PREVIEW_PAGES); } return; } setAssets(await listMindSpaceAssets(category.code)); if (['draft', 'oa', 'public'].includes(category.code)) { const { items } = await listMindSpacePages({ categoryCode: category.code, limit: 100 }); setPages(items); if (category.code === 'draft') { setSelectedDraftPageIds([]); } } } catch (err) { setError(err instanceof Error ? err.message : '资产加载失败'); } finally { setAssetsLoading(false); } routeSync?.pushCategory(category.code); }; const openUpload = (category?: MindSpaceCategory) => { const target = category ?? selectedCategory ?? space?.categories.find((item) => item.code === 'oa') ?? null; if (target) setSelectedCategory(target); setSelectedFile(null); setUploadError(null); setUploadOpen(true); }; const closeUpload = () => { if (uploading) return; setUploadOpen(false); setSelectedFile(null); setUploadError(null); }; const uploadDialogTitle = (category: MindSpaceCategory) => category.code === 'oa' ? '上传资料' : `上传到 ${category.name}`; const submitUpload = async () => { if (!selectedCategory || !selectedFile) return; const validationError = validateSelectedUploadFile(selectedFile, maxUploadFileBytes); if (validationError) { setUploadError(validationError); return; } if (previewMode) { setError(previewBlocked().message); return; } setUploading(true); setError(null); setUploadError(null); try { await uploadMindSpaceAsset(selectedCategory.id, selectedFile, { maxImageBytes: MINDSPACE_IMAGE_UPLOAD_MAX_BYTES, }); setUploadOpen(false); setSelectedFile(null); await load(); await openCategory(selectedCategory); } catch (err) { setError(err instanceof Error ? err.message : '上传失败'); } finally { setUploading(false); } }; const submitSpacePurchase = async (requestedMb?: number) => { const sizeMb = Math.floor(Number(requestedMb ?? spacePurchaseMb)); if (!Number.isFinite(sizeMb) || sizeMb <= 0) { setSpacePurchaseMessage('请输入大于 0 的空间大小(MB)'); return; } setSpacePurchasePending(true); setSpacePurchaseMessage(null); setError(null); try { const result = await purchaseSpaceQuota(sizeMb); completeRecharge(result.balanceCents); await refreshMindSpaceSnapshot({ quota: result.quota }); setSpacePurchaseMb(String(sizeMb)); setSpacePurchaseMessage( `已扩容 ${sizeMb} MB,支付 ¥${(result.costCents / 100).toFixed(2)}。`, ); } catch (err) { if (err instanceof ApiError && err.code === 'INSUFFICIENT_BALANCE') { setSpacePurchaseMessage('余额不足,请先到余额里充值,再回来购买空间。'); } else { setSpacePurchaseMessage(err instanceof Error ? err.message : '购买空间失败'); } } finally { setSpacePurchasePending(false); } }; const handleUploadFileChange = (event: ChangeEvent) => { const file = event.target.files?.[0] ?? null; if (!file) { setSelectedFile(null); setUploadError(null); return; } const validationError = validateSelectedUploadFile(file, maxUploadFileBytes); if (validationError) { setSelectedFile(null); setUploadError(validationError); event.currentTarget.value = ''; return; } setSelectedFile(file); setUploadError(null); }; const removeAsset = async (assetId: string) => { if (!selectedCategory) return; if (previewMode) { setError(previewBlocked().message); return; } setError(null); setAssetDeleteDialog(null); try { await deleteMindSpaceAsset(assetId); setPendingDeleteId(null); setSelectedAssetIds((ids) => ids.filter((id) => id !== assetId)); await load(); await openCategory(selectedCategory); } catch (err) { if (err instanceof ApiError && err.code === 'asset_in_use') { const details = err.details as | { hint?: string; references?: Array<{ id: string; title: string; publishedOnline?: boolean; }>; } | undefined; const references = details?.references ?? []; setAssetDeleteDialog({ message: details?.hint ?? (references.length ? `无法删除:页面「${truncateLabel(references[0]?.title ?? '未命名')}」正在使用该资产。` : '资产正在被页面使用,不能删除'), references, }); return; } setAssetDeleteDialog({ message: err instanceof Error ? err.message : '删除失败', references: [], }); } }; const occupiedBytes = space ? space.quota.usedBytes + space.quota.reservedBytes : 0; const usedPercent = space ? Math.min(100, (occupiedBytes / space.quota.quotaBytes) * 100) : 0; const currentBalanceCents = typeof balanceCents === 'number' ? balanceCents : user.balanceCents; const purchaseMbValue = Math.max(0, Math.floor(Number(spacePurchaseMb) || 0)); const purchaseCostCents = purchaseMbValue * 200; const canPurchaseSpace = purchaseMbValue > 0 && currentBalanceCents >= purchaseCostCents; const openAssetEditor = async (asset: MindSpaceAsset) => { if (previewMode) { setError(previewBlocked().message); return; } const linkedPage = pages.find( (page) => page.sourceAssetId === asset.id && page.status !== 'deleted', ); if (linkedPage) { setPreviewAssetId(null); showPage(linkedPage.id); return; } setEditingAssetId(asset.id); setError(null); try { const page = await createMindSpacePageFromAsset({ assetId: asset.id, title: asset.displayName, }); setPreviewAssetId(null); showPage(page.id); void load(); } catch (err) { setError(err instanceof Error ? err.message : '打开编辑失败'); } finally { setEditingAssetId(null); } }; const openAgentGenerator = (asset: MindSpaceAsset) => { setAgentAsset(asset); setAgentJob(null); setAgentInstruction(`请根据《${asset.displayName}》生成一个中文页面草稿,提炼核心结论、重点信息和可执行建议。`); }; const closeAgentGenerator = () => { if (agentSubmitting) return; setAgentAsset(null); setAgentInstruction(''); setAgentJob(null); }; const submitAgentGenerator = async () => { if (!agentAsset || !agentInstruction.trim()) return; if (previewMode) { setError(previewBlocked().message); return; } setAgentSubmitting(true); setError(null); try { const created = await createMindSpaceAgentJob({ jobType: 'generate_page', instruction: agentInstruction.trim(), allowedAssetIds: [agentAsset.id], outputType: 'page_draft', }); setAgentJob(created); await refreshAgentJobsSummary(); await runMindSpaceAgentJob(created.id); const running = await getMindSpaceAgentJob(created.id); setAgentJob(running); if (agentJobsPanelOpen) await loadAgentJobsPage(0); } catch (err) { setError(err instanceof Error ? err.message : '启动 AI 生成失败'); } finally { setAgentSubmitting(false); } }; const cancelAgentJob = async (jobId: string) => { try { const cancelled = await cancelMindSpaceAgentJob(jobId); setAgentJob((current) => (current?.id === jobId ? cancelled : current)); await refreshAgentJobsPanel(); } catch (err) { setError(err instanceof Error ? err.message : '取消任务失败'); } }; const retryAgentJob = async (jobId: string) => { try { const retried = await retryMindSpaceAgentJob(jobId); setAgentJob((current) => (current?.id === jobId ? retried : current)); await refreshAgentJobsPanel(); await runMindSpaceAgentJob(jobId); const running = await getMindSpaceAgentJob(jobId); setAgentJob((current) => (current?.id === jobId ? running : current)); await refreshAgentJobsPanel(); } catch (err) { setError(err instanceof Error ? err.message : '重试任务失败'); } }; const imageAssets = useMemo(() => assets.filter(isImageAsset), [assets]); const fileAssets = useMemo( () => assets.filter((asset) => !isWorkAsset(asset) && !isImageAsset(asset)), [assets], ); const workAssets = useMemo(() => assets.filter(isWorkAsset), [assets]); const visibleImageAssets = assetFilter === 'all' || assetFilter === 'images' ? imageAssets : []; const imagePaginationEnabled = selectedCategory?.code !== 'public' && visibleImageAssets.length > IMAGE_PAGE_SIZE; const imagePageCount = Math.max( 1, Math.ceil(visibleImageAssets.length / IMAGE_PAGE_SIZE), ); const safeImagePage = Math.min(imagePage, imagePageCount - 1); const pagedImageAssets = imagePaginationEnabled ? visibleImageAssets.slice( safeImagePage * IMAGE_PAGE_SIZE, safeImagePage * IMAGE_PAGE_SIZE + IMAGE_PAGE_SIZE, ) : visibleImageAssets; const sortedPages = useMemo(() => sortPagesByCreatedAt(pages), [pages]); const recentPagesPreview = sortedPages.slice(0, RECENT_PAGES_PAGE_SIZE); const imageAssetsByDate = useMemo(() => { const groups = new Map(); for (const asset of visibleImageAssets) { const match = asset.filename.match(/images\/(\d{4}-\d{2}-\d{2})\//); const dateKey = match?.[1] ?? '其他'; const bucket = groups.get(dateKey) ?? []; bucket.push(asset); groups.set(dateKey, bucket); } return [...groups.entries()].sort((left, right) => right[0].localeCompare(left[0])); }, [visibleImageAssets]); const visibleCardAssets = assetFilter === 'all' ? [...fileAssets, ...workAssets] : assetFilter === 'files' ? fileAssets : assetFilter === 'pages' ? workAssets : []; const visibleSelectableAssets = [...pagedImageAssets, ...visibleCardAssets]; const selectedAssetSet = useMemo(() => new Set(selectedAssetIds), [selectedAssetIds]); const allVisibleAssetsSelected = visibleSelectableAssets.length > 0 && visibleSelectableAssets.every((asset) => selectedAssetSet.has(asset.id)); useEffect(() => { const availableIds = new Set(assets.map((asset) => asset.id)); setSelectedAssetIds((ids) => ids.filter((id) => availableIds.has(id))); }, [assets]); const toggleAssetSelected = (assetId: string) => { setSelectedAssetIds((ids) => ids.includes(assetId) ? ids.filter((id) => id !== assetId) : [...ids, assetId], ); }; const toggleVisibleAssetsSelected = () => { const visibleIds = visibleSelectableAssets.map((asset) => asset.id); if (allVisibleAssetsSelected) { setSelectedAssetIds((ids) => ids.filter((id) => !visibleIds.includes(id))); return; } setSelectedAssetIds((ids) => Array.from(new Set([...ids, ...visibleIds]))); }; const removeSelectedAssets = async () => { if (!selectedCategory || selectedAssetIds.length === 0) return; if (previewMode) { setError(previewBlocked().message); return; } setBulkDeletingAssets(true); setError(null); setAssetDeleteDialog(null); const deletingIds = [...selectedAssetIds]; const failedIds: string[] = []; const failedNames: string[] = []; try { for (const assetId of deletingIds) { try { await deleteMindSpaceAsset(assetId); } catch (err) { const assetName = assets.find((asset) => asset.id === assetId)?.displayName ?? assetId; failedIds.push(assetId); failedNames.push(assetName); } } setSelectedAssetIds((ids) => ids.filter((id) => failedIds.includes(id))); await load(); await openCategory(selectedCategory); if (failedNames.length > 0) { setAssetDeleteDialog({ message: `有 ${failedNames.length} 个资产未删除,可能正在被页面使用。`, references: failedNames.slice(0, 6).map((title, index) => ({ id: `${index}`, title, })), }); } } finally { setBulkDeletingAssets(false); } }; const selectedDraftPageSet = useMemo(() => new Set(selectedDraftPageIds), [selectedDraftPageIds]); const allDraftPagesSelected = pages.length > 0 && pages.every((p) => selectedDraftPageSet.has(p.id)); const toggleDraftPageSelected = (pageId: string) => { setSelectedDraftPageIds((ids) => ids.includes(pageId) ? ids.filter((id) => id !== pageId) : [...ids, pageId], ); }; const toggleAllDraftPagesSelected = () => { if (allDraftPagesSelected) { setSelectedDraftPageIds([]); } else { setSelectedDraftPageIds(pages.map((p) => p.id)); } }; const bulkDeletePlazaPosts = useMemo(() => { if (!bulkPageDeletePreviews) return []; const seen = new Set(); const posts = []; for (const preview of bulkPageDeletePreviews) { for (const post of preview.plazaPosts ?? []) { if (seen.has(post.id)) continue; seen.add(post.id); posts.push(post); } } return posts; }, [bulkPageDeletePreviews]); const closeBulkPageDeleteDialog = () => { if (bulkDeletingAllPages || bulkDeletingDraftPages || bulkDeletingAchievements) return; setBulkPageDeleteOpen(false); setBulkPageDeletePreviews(null); setBulkPageDeleteRemoveFromPlaza(false); }; const startBulkPageDelete = async (pageIds: string[], source: 'all' | 'draft' | 'achievements') => { if (pageIds.length === 0) return; if (previewMode) { setError('预览模式下无法删除页面'); return; } setBulkPageDeleteSource(source); setBulkPageDeletePreviews(null); setBulkPageDeleteRemoveFromPlaza(false); setBulkPageDeleteLoading(true); setBulkPageDeleteOpen(true); setError(null); try { const previews = await Promise.all( pageIds.map((pageId) => getMindSpacePageDeletePreview(pageId).catch((err) => { console.error('[MindSpace] delete preview failed:', pageId, err); return null; }), ), ); const valid = previews.filter(Boolean) as MindSpacePageDeletePreview[]; if (valid.length === 0) { setBulkPageDeleteOpen(false); setError('无法加载删除预览'); return; } setBulkPageDeletePreviews(valid); } catch (err) { setBulkPageDeleteOpen(false); setError(err instanceof Error ? err.message : '无法加载删除预览'); } finally { setBulkPageDeleteLoading(false); } }; const confirmBulkPageDelete = async () => { if (!bulkPageDeletePreviews || bulkPageDeletePreviews.length === 0) return; const deletingIds = bulkPageDeletePreviews.map((preview) => preview.page.id); const setBulkDeleting = bulkPageDeleteSource === 'all' ? setBulkDeletingAllPages : bulkPageDeleteSource === 'draft' ? setBulkDeletingDraftPages : setBulkDeletingAchievements; setBulkDeleting(true); setError(null); const failedIds: string[] = []; const failedNames: string[] = []; try { for (const preview of bulkPageDeletePreviews) { try { await deleteMindSpacePage(preview.page.id, { removeFromPlaza: bulkPageDeleteRemoveFromPlaza, }); if (selectedPageId === preview.page.id) closePage(); } catch (err) { if (isMindSpacePageNotFoundError(err)) { if (selectedPageId === preview.page.id) closePage(); continue; } failedIds.push(preview.page.id); failedNames.push(preview.page.title); console.error('[MindSpace] delete page failed:', preview.page.id, err); } } const deletedIds = deletingIds.filter((id) => !failedIds.includes(id)); if (bulkPageDeleteSource === 'all') { setSelectedAllPageIds((ids) => ids.filter((id) => failedIds.includes(id))); } else if (bulkPageDeleteSource === 'draft') { setSelectedDraftPageIds((ids) => ids.filter((id) => failedIds.includes(id))); } else { setSelectedAchievementPageIds((ids) => ids.filter((id) => failedIds.includes(id))); } setBulkPageDeleteOpen(false); setBulkPageDeletePreviews(null); setBulkPageDeleteRemoveFromPlaza(false); await refreshMindSpaceSnapshot(); await load(); if (bulkPageDeleteSource === 'all' && deletedIds.length > 0) { const nextPageIndex = allPagesPageIndex > 0 && allPagesItems.length <= deletedIds.length ? Math.max(0, allPagesPageIndex - 1) : allPagesPageIndex; await loadAllPagesPage(nextPageIndex); } if (bulkPageDeleteSource === 'achievements' && deletedIds.length > 0) { setAchievementsItems((items) => items.filter((page) => !deletedIds.includes(page.id))); setAchievementsTotal((total) => Math.max(0, total - deletedIds.length)); setSelectedAchievementPageIds((ids) => ids.filter((id) => !deletedIds.includes(id))); await loadAchievementsPage(achievementsPageIndex); } if (failedNames.length > 0) { setError(`有 ${failedNames.length} 个页面删除失败:${failedNames.slice(0, 3).join('、')}`); } } finally { setBulkDeleting(false); } }; const removeDraftPages = () => { void startBulkPageDelete([...selectedDraftPageIds], 'draft'); }; const selectedAllPageSet = useMemo(() => new Set(selectedAllPageIds), [selectedAllPageIds]); const allCurrentAllPagesSelected = allPagesItems.length > 0 && allPagesItems.every((page) => selectedAllPageSet.has(page.id)); const toggleAllPagesOnCurrentPageSelected = () => { if (allCurrentAllPagesSelected) { const currentIds = new Set(allPagesItems.map((page) => page.id)); setSelectedAllPageIds((ids) => ids.filter((id) => !currentIds.has(id))); } else { const merged = new Set([...selectedAllPageIds, ...allPagesItems.map((page) => page.id)]); setSelectedAllPageIds([...merged]); } }; const removeSelectedAllPages = () => { void startBulkPageDelete([...selectedAllPageIds], 'all'); }; const selectedAchievementPageSet = useMemo( () => new Set(selectedAchievementPageIds), [selectedAchievementPageIds], ); const toggleAchievementPageSelected = (pageId: string, checked: boolean) => { setSelectedAchievementPageIds((ids) => checked ? (ids.includes(pageId) ? ids : [...ids, pageId]) : ids.filter((id) => id !== pageId), ); }; const toggleAchievementDateGroupSelected = (pageIds: string[], checked: boolean) => { setSelectedAchievementPageIds((ids) => { if (!checked) { const remove = new Set(pageIds); return ids.filter((id) => !remove.has(id)); } return [...new Set([...ids, ...pageIds])]; }); }; const toggleAllAchievementPagesSelected = () => { const allSelected = achievementsItems.length > 0 && achievementsItems.every((page) => selectedAchievementPageSet.has(page.id)); if (allSelected) { const visibleIds = new Set(achievementsItems.map((page) => page.id)); setSelectedAchievementPageIds((ids) => ids.filter((id) => !visibleIds.has(id))); return; } setSelectedAchievementPageIds((ids) => [ ...new Set([...ids, ...achievementsItems.map((page) => page.id)]), ]); }; const removeSelectedAchievementPages = () => { void startBulkPageDelete([...selectedAchievementPageIds], 'achievements'); }; const allPagesPageCount = Math.max(1, Math.ceil(allPagesTotal / ALL_PAGES_PAGE_SIZE)); const previewAsset = useMemo( () => assets.find((asset) => asset.id === previewAssetId) ?? null, [assets, previewAssetId], ); const previewAssetFrame = previewAsset ? buildAssetPreviewFrameConfig(previewAsset) : null; const openAssetViewInNewTab = (asset: MindSpaceAsset) => { if (previewMode) { setError('预览模式下暂不支持打开页面'); return; } const viewUrl = asset.publicUrl ? resolvePublicPageUrl(asset.publicUrl) : buildAbsoluteAssetDownloadUrl(asset); window.open(viewUrl, '_blank', 'noopener,noreferrer'); }; const renderAssetActions = (asset: MindSpaceAsset) => { const shareReady = asset.status === 'ready' && asset.scanStatus !== 'blocked'; const menuOpen = openAssetMenuId === asset.id; return (
{isWorkAsset(asset) ? ( ) : isPreviewableAsset(asset) ? ( ) : ( 暂不可预览 )} {shareReady ? ( <> {selectedCategory?.code === 'public' && isWorkAsset(asset) && ( )} ) : ( 暂不可分享 )}
{menuOpen && (
{isPreviewableAsset(asset) && !isWorkAsset(asset) && ( )} {asset.sourcePageId && ( )} {asset.publicUrl && ( setOpenAssetMenuId(null)} > 打开公网图 )} {pendingDeleteId === asset.id ? ( <> ) : ( )}
)}
); }; const createPage = async () => { if (!newPageTitle.trim() || !newPageContent.trim()) return; if (previewMode) { setError(previewBlocked().message); return; } setCreatingPage(true); setError(null); try { const page = await createMindSpacePage({ title: newPageTitle.trim(), content: newPageContent.trim(), templateId: newPageTemplate, }); setNewPageOpen(false); setNewPageTitle(''); setNewPageContent(''); await load(); showPage(page.id); } catch (err) { setError(err instanceof Error ? err.message : '页面创建失败'); } finally { setCreatingPage(false); } }; const agentJobsPageCount = Math.max(1, Math.ceil(agentJobsTotal / AGENT_JOBS_PAGE_SIZE)); const renderAgentJobCard = (job: MindSpaceAgentJob) => (
{job.assets[0]?.displayName ?? '未命名资料'}

{job.instruction}

{JOB_STATUS_LABELS[job.status]}
阶段:{job.progress.stage} {new Date(job.queuedAt).toLocaleString()}
{job.errorMessage &&
{job.errorMessage}
}
{job.resultPageId && ( )} {['queued', 'running'].includes(job.status) && ( )} {job.retryable && ['failed', 'cancelled', 'timed_out'].includes(job.status) && ( )}
); const showHomeModules = space && !selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen && !achievementsOpen; const rawUpcomingReminders = previewMode ? [] : space?.schedule?.upcomingReminders ?? []; const scheduleReminders = previewMode ? PREVIEW_SCHEDULE_REMINDERS.map((item) => ({ ...item })) : rawUpcomingReminders.map((reminder) => { const metaParts = [ formatReminderEventWindow(reminder), reminder.offsetMinutes != null && reminder.offsetMinutes > 0 ? `提前 ${reminder.offsetMinutes} 分钟` : null, reminder.channel === 'wechat' ? '服务号推送' : '站内提醒', ].filter(Boolean); return { id: reminder.id, time: formatScheduleWhen(reminder.remindAt, reminder.itemTimezone), title: reminder.itemTitle || '待办提醒', meta: metaParts.join(' · '), notifyLabel: scheduleReminderNotifyLabel(reminder.status), canIgnore: reminder.status === 'pending' || reminder.status === 'locked', }; }); const scheduleDigests = previewMode ? [] : space?.schedule?.digestSubscriptions ?? []; const pendingReminderCount = scheduleReminders.filter((item) => item.notifyLabel === '待通知').length; const hasScheduleContent = scheduleReminders.length > 0 || scheduleDigests.length > 0; const toggleScheduleReminderSelection = (reminderId: string) => { setSelectedScheduleReminderIds((prev) => prev.includes(reminderId) ? prev.filter((id) => id !== reminderId) : [...prev, reminderId], ); }; const toggleAllScheduleReminders = () => { if (selectedScheduleReminderIds.length === scheduleReminders.length) { setSelectedScheduleReminderIds([]); } else { setSelectedScheduleReminderIds(scheduleReminders.map((item) => item.id)); } }; const ignoreScheduleReminder = async (reminderId: string) => { if (previewMode) return; setScheduleReminderActionLoading(true); setError(null); try { await ignoreMindSpaceScheduleReminder(reminderId); setSelectedScheduleReminderIds((prev) => prev.filter((id) => id !== reminderId)); await refreshSpaceQuietly(); } catch (err) { setError(err instanceof Error ? err.message : '忽略提醒失败'); } finally { setScheduleReminderActionLoading(false); } }; const deleteSelectedScheduleReminders = async () => { if (previewMode || selectedScheduleReminderIds.length === 0) return; setScheduleReminderActionLoading(true); setError(null); try { await deleteMindSpaceScheduleReminders(selectedScheduleReminderIds); setSelectedScheduleReminderIds([]); await refreshSpaceQuietly(); } catch (err) { setError(err instanceof Error ? err.message : '删除提醒失败'); } finally { setScheduleReminderActionLoading(false); } }; const selectedPreviewPage = selectedPageId ? pages.find((page) => page.id === selectedPageId) ?? null : null; const selectedPreviewReader = selectedPreviewPage ? PREVIEW_PAGE_READERS[selectedPreviewPage.id] ?? { kicker: selectedPreviewPage.templateId, body: [ selectedPreviewPage.summary || '这是该页面的站内预览内容。', '正式接入真实数据后,这里会直接呈现用户生成页面的完整内容。', ], } : null; return (
{previewMode && (
UI 预览模式 · 示例数据,无需登录 退出预览
)}
{fromPlaza && ( 返回广场 )}
{!fromPlaza && ( 发现广场 )} {previewMode ? ( {}} /> ) : ( )} {previewMode ? ( {}} /> ) : ( )} {!previewMode && onOpenFeedback && ( )} {user.displayName}
{!allPagesOpen && !achievementsOpen && (

MINDSPACE

{space?.name ?? '我的空间'}

让每一次 AI 对话、分析和创作,都成为可以继续使用的作品。

逛 Plaza
{space && ( )} {cleanupOpen && (
{cleanupLoading &&

正在扫描可清理项…

} {!cleanupLoading && cleanupItems.length === 0 && (

{space && occupiedBytes >= space.quota.quotaBytes * 0.85 ? `空间已用 ${formatBytes(occupiedBytes)} / ${formatBytes(space.quota.quotaBytes)},暂无可自动清理的临时文件或历史版本。请到下方分类中手动删除不需要的页面或资产。` : '当前没有可清理的临时文件或历史页面版本。'}

)} {!cleanupLoading && cleanupItems.length > 0 && ( <>

可释放约{' '} {formatBytes( cleanupItems .filter((item) => selectedCleanupIds.includes(item.id)) .reduce((sum, item) => sum + item.sizeBytes, 0), )}

    {cleanupItems.map((item) => (
  • ))}
)}
)}
)} {showHomeModules && (

TODAY

待办行事历

未来 7 天 {scheduleReminders.length} 条提醒 · {pendingReminderCount} 待通知 {hasScheduleContent ? '仅展示提醒通知' : '还没有安排'}
{scheduleReminders.length > 0 && (
)} {scheduleReminders.length > 0 && (
{scheduleReminders.map((item) => (
{item.notifyLabel}
{item.title} {item.meta}
{item.canIgnore && !previewMode && ( )}
))}
)} {scheduleDigests.length > 0 && (

订阅

{scheduleDigests.map((digest) => (
订阅
每天待办摘要 {formatDigestSchedule(digest)}
))}
)} {!hasScheduleContent && (

未来 7 天还没有提醒,可以在聊天里说「明天早上 6 点跑步,5 点 55 提醒我」。

)}
)} {loading &&
正在加载个人空间…
} {error && !assetDeleteDialog && (
{error}
)} {space && ( <> {selectedPageId ? ( previewMode ? (
站内浏览
{selectedPreviewPage && selectedPreviewReader ? (

{selectedPreviewReader.kicker}

{selectedPreviewPage.title}

{selectedPreviewPage.summary}

{selectedPreviewReader.body.map((paragraph) => (

{paragraph}

))}
) : (
页面不存在或已被删除
)}
) : ( ) ) : newPageOpen ? (

CREATE PAGE

创建页面草稿