diff --git a/.gitignore b/.gitignore index 1c229eaa3..ac67a5919 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ services/local-service/internal/rpc/workspace/ apps/.temp/* !apps/.temp/.gitkeep +.sisyphus/ \ No newline at end of file diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 9add3fcc4..07507aa72 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -26,9 +26,11 @@ "ahooks": "^3.9.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "lucide-react": "^0.468.0", "motion": "^11.15.0", "react": "^18.3.1", + "react-day-picker": "^10.0.0", "react-dom": "^18.3.1", "react-fast-compare": "^3.2.2", "react-router-dom": "^7.14.0", diff --git a/apps/desktop/src-tauri/scripts/runWithSidecar.mjs b/apps/desktop/src-tauri/scripts/runWithSidecar.mjs index 661e70449..30b84ab84 100644 --- a/apps/desktop/src-tauri/scripts/runWithSidecar.mjs +++ b/apps/desktop/src-tauri/scripts/runWithSidecar.mjs @@ -1,11 +1,29 @@ /* global process, console */ -import { spawn } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { buildLocalServiceSidecar } from "./ensureLocalServiceSidecar.mjs"; const currentDirectory = dirname(fileURLToPath(import.meta.url)); +function escapePowerShellLiteral(value) { + return value.replace(/'/g, "''"); +} + +function buildStopStaleBundledSidecarsCommand(targetRoot) { + const escapedTargetRoot = escapePowerShellLiteral(targetRoot); + return [ + `$targetRoot = ([System.IO.Path]::GetFullPath('${escapedTargetRoot}')).TrimEnd('\\') + '\\'`, + "Get-CimInstance Win32_Process | Where-Object {", + " $executablePath = $_.ExecutablePath", + " if (-not $executablePath) { return $false }", + " $fullPath = [System.IO.Path]::GetFullPath($executablePath)", + " $fileName = [System.IO.Path]::GetFileName($fullPath)", + " $fullPath.StartsWith($targetRoot, [System.StringComparison]::OrdinalIgnoreCase) -and $fileName.StartsWith('cialloclaw-service', [System.StringComparison]::OrdinalIgnoreCase)", + "} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }", + ].join("; "); +} + function resolveCommand(name) { return process.platform === "win32" && name === "corepack" ? "corepack.cmd" : name; } @@ -36,6 +54,42 @@ function runFrontendCommand(commandName) { }); } +function stopStaleBundledSidecars() { + if (process.platform !== "win32") { + return; + } + + const staleSidecarTargetRoot = resolve(currentDirectory, "..", "target"); + + // Tauri copies the bundled sidecar into `src-tauri/target/*` before booting + // the app. On Windows, a stale child keeps that copied executable locked and + // the next build panics with `PermissionDenied` while refreshing the bundle. + // Only terminate copied sidecars for the current workspace target directory. + const result = spawnSync( + "powershell.exe", + [ + "-NoProfile", + "-NonInteractive", + "-Command", + buildStopStaleBundledSidecarsCommand(staleSidecarTargetRoot), + ], + { + stdio: "pipe", + encoding: "utf8", + }, + ); + + if (result.error) { + console.warn("Failed to stop stale bundled sidecars before launching Tauri."); + return; + } + + if (result.status !== 0) { + const details = result.stderr?.trim() || result.stdout?.trim(); + console.warn(`Failed to stop stale bundled sidecars before launching Tauri.${details ? ` ${details}` : ""}`); + } +} + const commandName = process.argv[2]; if (commandName !== "dev" && commandName !== "build") { @@ -44,6 +98,7 @@ if (commandName !== "dev" && commandName !== "build") { } try { + stopStaleBundledSidecars(); const sidecarPath = buildLocalServiceSidecar(); console.log(`Prepared local-service sidecar: ${sidecarPath}`); } catch (error) { diff --git a/apps/desktop/src-tauri/src/selection/windows.rs b/apps/desktop/src-tauri/src/selection/windows.rs index a32b669ac..69cb56113 100644 --- a/apps/desktop/src-tauri/src/selection/windows.rs +++ b/apps/desktop/src-tauri/src/selection/windows.rs @@ -47,6 +47,7 @@ static SHELL_BALL_SELECTION_MONITOR_STATE: Lazy> = #[derive(Default)] struct SelectionMonitorState { + invalidated_fingerprint: Option, last_fingerprint: Option, probe_pending: bool, } @@ -132,6 +133,13 @@ pub fn install_selection_listener(app: &AppHandle) -> Result<(), String> { /// a shell-ball selection snapshot. pub fn read_selection_snapshot( app: &AppHandle, +) -> Result, String> { + let snapshot = read_selection_snapshot_raw(app)?; + Ok(suppress_invalidated_selection_snapshot(snapshot)) +} + +fn read_selection_snapshot_raw( + app: &AppHandle, ) -> Result, String> { let _com_guard = ComGuard::initialize()?; let foreground_window = unsafe { GetForegroundWindow() }; @@ -174,6 +182,22 @@ pub fn read_selection_snapshot( ))) } +fn suppress_invalidated_selection_snapshot( + snapshot: Option, +) -> Option { + let fingerprint = selection_snapshot_fingerprint(snapshot.as_ref()); + let invalidated_fingerprint = SHELL_BALL_SELECTION_MONITOR_STATE + .lock() + .ok() + .and_then(|state| state.invalidated_fingerprint.clone()); + + if fingerprint.is_some() && fingerprint == invalidated_fingerprint { + return None; + } + + snapshot +} + unsafe extern "system" fn shell_ball_selection_mouse_hook( n_code: i32, w_param: WPARAM, @@ -183,6 +207,7 @@ unsafe extern "system" fn shell_ball_selection_mouse_hook( // left click will re-probe the selection and clear shell-ball alert state // if the user no longer has a live selection. if n_code >= 0 && w_param.0 as u32 == WM_LBUTTONUP { + clear_invalidated_selection_fingerprint(); schedule_selection_probe(SHELL_BALL_SELECTION_MOUSE_DELAY_MS); } @@ -196,6 +221,12 @@ unsafe extern "system" fn shell_ball_selection_keyboard_hook( ) -> LRESULT { if n_code >= 0 && (w_param.0 as u32 == WM_KEYDOWN || w_param.0 as u32 == WM_SYSKEYDOWN) { let keyboard_info = *(l_param.0 as *const KBDLLHOOKSTRUCT); + if should_invalidate_selection_from_key_event(keyboard_info.vkCode) { + invalidate_current_selection(); + } else if should_clear_selection_invalidation_from_key_event(keyboard_info.vkCode) { + clear_invalidated_selection_fingerprint(); + } + if should_probe_selection_from_key_event(keyboard_info.vkCode) { schedule_selection_probe(SHELL_BALL_SELECTION_KEYBOARD_DELAY_MS); } @@ -216,6 +247,10 @@ fn should_probe_selection_from_key_event(vk_code: u32) -> bool { return true; } + if ctrl_down && vk_code == b'X' as u32 { + return true; + } + if !shift_down { return false; } @@ -233,6 +268,76 @@ fn should_probe_selection_from_key_event(vk_code: u32) -> bool { ) } +fn should_invalidate_selection_from_key_event(vk_code: u32) -> bool { + let ctrl_down = unsafe { (GetAsyncKeyState(VK_CONTROL.0 as i32) as u16 & 0x8000) != 0 }; + ctrl_down && vk_code == b'X' as u32 +} + +fn should_clear_selection_invalidation_from_key_event(vk_code: u32) -> bool { + let ctrl_down = unsafe { (GetAsyncKeyState(VK_CONTROL.0 as i32) as u16 & 0x8000) != 0 }; + let shift_down = unsafe { (GetAsyncKeyState(VK_SHIFT.0 as i32) as u16 & 0x8000) != 0 }; + + if ctrl_down && vk_code == b'A' as u32 { + return true; + } + + if !shift_down { + return false; + } + + matches!( + vk_code, + code if code == VK_LEFT.0 as u32 + || code == VK_RIGHT.0 as u32 + || code == VK_UP.0 as u32 + || code == VK_DOWN.0 as u32 + || code == VK_HOME.0 as u32 + || code == VK_END.0 as u32 + || code == VK_PRIOR.0 as u32 + || code == VK_NEXT.0 as u32 + ) +} + +fn invalidate_current_selection() { + thread::spawn(move || { + let Some(app) = SHELL_BALL_SELECTION_APP_HANDLE + .lock() + .ok() + .and_then(|guard| guard.as_ref().cloned()) + else { + return; + }; + + let snapshot = read_selection_snapshot_raw(&app).ok().flatten(); + let fingerprint = selection_snapshot_fingerprint(snapshot.as_ref()); + + let should_emit = { + let mut state = match SHELL_BALL_SELECTION_MONITOR_STATE.lock() { + Ok(guard) => guard, + Err(_) => return, + }; + + state.invalidated_fingerprint = fingerprint; + if state.last_fingerprint.is_none() { + false + } else { + state.last_fingerprint = None; + true + } + }; + + if should_emit { + emit_selection_snapshot(&app, None); + } + }); +} + +fn clear_invalidated_selection_fingerprint() { + if let Ok(mut state) = SHELL_BALL_SELECTION_MONITOR_STATE.lock() { + state.invalidated_fingerprint = None; + } +} + fn schedule_selection_probe(delay_ms: u64) { { let mut state = match SHELL_BALL_SELECTION_MONITOR_STATE.lock() { @@ -281,16 +386,20 @@ fn schedule_selection_probe(delay_ms: u64) { return; } - let _ = app.emit_to( - "shell-ball", - SHELL_BALL_SELECTION_SNAPSHOT_EVENT, - serde_json::json!({ - "snapshot": snapshot, - }), - ); + emit_selection_snapshot(&app, snapshot); }); } +fn emit_selection_snapshot(app: &AppHandle, snapshot: Option) { + let _ = app.emit_to( + "shell-ball", + SHELL_BALL_SELECTION_SNAPSHOT_EVENT, + serde_json::json!({ + "snapshot": snapshot, + }), + ); +} + fn reset_probe_pending() { if let Ok(mut state) = SHELL_BALL_SELECTION_MONITOR_STATE.lock() { state.probe_pending = false; diff --git a/apps/desktop/src/app/dashboard/DashboardRoot.tsx b/apps/desktop/src/app/dashboard/DashboardRoot.tsx index 961a455b8..cbe1a1a3e 100644 --- a/apps/desktop/src/app/dashboard/DashboardRoot.tsx +++ b/apps/desktop/src/app/dashboard/DashboardRoot.tsx @@ -1,5 +1,4 @@ -import { useEffect, useRef, useState } from "react"; -import { AnimatePresence, motion } from "motion/react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { HashRouter, Link, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"; @@ -11,6 +10,7 @@ import { import { MemoryPage } from "@/features/dashboard/memory/MemoryPage"; import { NotesPage } from "@/features/dashboard/notes/NotesPage"; import { SafetyPage } from "@/features/dashboard/safety/SafetyPage"; +import { DashboardModuleFloatingNav } from "@/features/dashboard/shared/DashboardModuleFloatingNav"; import { dashboardTaskDetailNavigationEvent, navigateToDashboardTaskDetail, @@ -21,6 +21,7 @@ import { useDashboardEscapeCoordinator, useDashboardEscapeHandler, } from "@/features/dashboard/shared/dashboardEscapeCoordinator"; +import { dashboardModules } from "@/features/dashboard/shared/dashboardRoutes"; import { resolveDashboardModuleRoutePath, resolveDashboardRoutePath } from "@/features/dashboard/shared/dashboardRouteTargets"; import { dashboardTaskDeliveryNavigationEvent, @@ -318,31 +319,29 @@ function DashboardRoutes() { /> ); + const activeSharedModule = useMemo( + () => dashboardModules.find((module) => location.pathname === module.path || location.pathname.startsWith(`${module.path}/`)) ?? null, + [location.pathname], + ); + return (
- - - - - } path={`${resolveDashboardModuleRoutePath("tasks")}/*`} /> - } path={`${resolveDashboardModuleRoutePath("notes")}/*`} /> - } path={`${resolveDashboardModuleRoutePath("memory")}/*`} /> - } path={`${resolveDashboardModuleRoutePath("safety")}/*`} /> - } path="*" /> - - - + {activeSharedModule ? ( + + ) : null} +
+ + + } path={`${resolveDashboardModuleRoutePath("tasks")}/*`} /> + } path={`${resolveDashboardModuleRoutePath("notes")}/*`} /> + } path={`${resolveDashboardModuleRoutePath("memory")}/*`} /> + } path={`${resolveDashboardModuleRoutePath("safety")}/*`} /> + } path="*" /> + +
setVoiceOpen(false)} diff --git a/apps/desktop/src/app/dashboard/dashboard.css b/apps/desktop/src/app/dashboard/dashboard.css index 70a0ac3cf..348520feb 100644 --- a/apps/desktop/src/app/dashboard/dashboard.css +++ b/apps/desktop/src/app/dashboard/dashboard.css @@ -77,6 +77,10 @@ min-height: 100vh; } +.dashboard-route-layer--with-shared-topbar { + min-height: 100vh; +} + .dashboard-home { position: relative; min-height: 100vh; @@ -454,25 +458,42 @@ justify-content: space-between; } +.dashboard-page__topbar--shared { + -webkit-app-region: no-drag; + app-region: no-drag; + position: fixed; + top: clamp(18px, 3vw, 28px); + left: clamp(18px, 3vw, 28px); + right: clamp(18px, 3vw, 28px); + z-index: 90; + background: transparent; + pointer-events: none; +} + +.dashboard-page__topbar--shared > * { + pointer-events: auto; +} + .dashboard-page__home-link, .dashboard-page__module-link { display: inline-flex; gap: 0.5rem; align-items: center; padding: 0.72rem 1rem; - border: 1px solid rgba(92, 116, 80, 0.14); + border: 1px solid color-mix(in srgb, var(--dashboard-nav-accent, #5d6f52) 16%, rgba(92, 116, 80, 0.14)); border-radius: 999px; - color: #4b6640; + color: color-mix(in srgb, var(--dashboard-nav-accent, #5d6f52) 74%, #2d3b28 26%); text-decoration: none; - background: rgba(255, 251, 244, 0.72); + background: rgba(255, 251, 244, 0.42); + backdrop-filter: blur(10px); transition: border-color 180ms ease, background-color 180ms ease, transform 180ms ease; } .dashboard-page__home-link:hover, .dashboard-page__module-link:hover, .dashboard-page__module-link.is-active { - border-color: rgba(92, 116, 80, 0.28); - background: rgba(249, 247, 239, 0.92); + border-color: color-mix(in srgb, var(--dashboard-nav-accent, #5d6f52) 34%, rgba(92, 116, 80, 0.28)); + background: color-mix(in srgb, rgba(249, 247, 239, 0.9) 82%, var(--dashboard-nav-accent, #5d6f52) 18%); transform: translateY(-1px); } @@ -490,6 +511,11 @@ gap: 0.75rem; } +.dashboard-page__topbar-spacer { + height: clamp(56px, 7.2vh, 72px); + width: 100%; +} + .dashboard-page__module-nav--floating { position: fixed; top: clamp(18px, 3vw, 28px); diff --git a/apps/desktop/src/assets/background_memory.png b/apps/desktop/src/assets/background_memory.png new file mode 100644 index 000000000..bed572c46 Binary files /dev/null and b/apps/desktop/src/assets/background_memory.png differ diff --git a/apps/desktop/src/assets/lily-of-the-valley/branch.png b/apps/desktop/src/assets/lily-of-the-valley/branch.png deleted file mode 100644 index 11e41a6e1..000000000 Binary files a/apps/desktop/src/assets/lily-of-the-valley/branch.png and /dev/null differ diff --git a/apps/desktop/src/assets/lily-of-the-valley/flower1.png b/apps/desktop/src/assets/lily-of-the-valley/flower1.png deleted file mode 100644 index 87d598184..000000000 Binary files a/apps/desktop/src/assets/lily-of-the-valley/flower1.png and /dev/null differ diff --git a/apps/desktop/src/assets/lily-of-the-valley/flower2.png b/apps/desktop/src/assets/lily-of-the-valley/flower2.png deleted file mode 100644 index 78b1aa311..000000000 Binary files a/apps/desktop/src/assets/lily-of-the-valley/flower2.png and /dev/null differ diff --git a/apps/desktop/src/assets/lily-of-the-valley/flower3.png b/apps/desktop/src/assets/lily-of-the-valley/flower3.png deleted file mode 100644 index 094a7629d..000000000 Binary files a/apps/desktop/src/assets/lily-of-the-valley/flower3.png and /dev/null differ diff --git a/apps/desktop/src/assets/lily-of-the-valley/flower4.png b/apps/desktop/src/assets/lily-of-the-valley/flower4.png deleted file mode 100644 index 2cb1d547d..000000000 Binary files a/apps/desktop/src/assets/lily-of-the-valley/flower4.png and /dev/null differ diff --git a/apps/desktop/src/assets/lily-of-the-valley/leaf1.png b/apps/desktop/src/assets/lily-of-the-valley/leaf1.png deleted file mode 100644 index 6cd9520a0..000000000 Binary files a/apps/desktop/src/assets/lily-of-the-valley/leaf1.png and /dev/null differ diff --git a/apps/desktop/src/assets/lily-of-the-valley/leaf2.png b/apps/desktop/src/assets/lily-of-the-valley/leaf2.png deleted file mode 100644 index e2f9abc91..000000000 Binary files a/apps/desktop/src/assets/lily-of-the-valley/leaf2.png and /dev/null differ diff --git a/apps/desktop/src/assets/lily-of-the-valley/leaf3.png b/apps/desktop/src/assets/lily-of-the-valley/leaf3.png deleted file mode 100644 index 90651809e..000000000 Binary files a/apps/desktop/src/assets/lily-of-the-valley/leaf3.png and /dev/null differ diff --git a/apps/desktop/src/components/ui/button.tsx b/apps/desktop/src/components/ui/button.tsx index 09df75367..8202bbff0 100644 --- a/apps/desktop/src/components/ui/button.tsx +++ b/apps/desktop/src/components/ui/button.tsx @@ -1,4 +1,5 @@ import { Button as ButtonPrimitive } from "@base-ui/react/button" +import { forwardRef } from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" @@ -40,19 +41,24 @@ const buttonVariants = cva( } ) -function Button({ - className, - variant = "default", - size = "default", - ...props -}: ButtonPrimitive.Props & VariantProps) { - return ( - - ) -} +const Button = forwardRef>( + ({ + className, + variant = "default", + size = "default", + ...props + }, ref) => { + return ( + + ) + }, +) + +Button.displayName = "Button" export { Button, buttonVariants } diff --git a/apps/desktop/src/components/ui/calendar.tsx b/apps/desktop/src/components/ui/calendar.tsx new file mode 100644 index 000000000..547d7b304 --- /dev/null +++ b/apps/desktop/src/components/ui/calendar.tsx @@ -0,0 +1,54 @@ +import * as React from "react" +import { format } from "date-fns" +import { zhCN } from "date-fns/locale" +import { DayPicker } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: React.ComponentProps) { + return ( + format(month, "yyyy年M月", { locale: zhCN }), + formatWeekdayName: (date) => format(date, "EEEEE", { locale: zhCN }), + }} + {...props} + /> + ) +} + +export { Calendar } diff --git a/apps/desktop/src/components/ui/popover.tsx b/apps/desktop/src/components/ui/popover.tsx new file mode 100644 index 000000000..1428fddf3 --- /dev/null +++ b/apps/desktop/src/components/ui/popover.tsx @@ -0,0 +1,88 @@ +import * as React from "react" +import { Popover as PopoverPrimitive } from "@base-ui/react/popover" + +import { cn } from "@/lib/utils" + +function Popover({ ...props }: PopoverPrimitive.Root.Props) { + return +} + +function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) { + return +} + +function PopoverContent({ + className, + align = "center", + alignOffset = 0, + side = "bottom", + sideOffset = 4, + ...props +}: PopoverPrimitive.Popup.Props & + Pick< + PopoverPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" + >) { + return ( + + + + + + ) +} + +function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) { + return ( + + ) +} + +function PopoverDescription({ + className, + ...props +}: PopoverPrimitive.Description.Props) { + return ( + + ) +} + +export { + Popover, + PopoverContent, + PopoverDescription, + PopoverHeader, + PopoverTitle, + PopoverTrigger, +} diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index 13e5efcaf..c76683701 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -2151,16 +2151,6 @@ test("dashboard home randomizes summons while preferring a different module when assert.match(dashboardHomeSource, /onClose=\{closeActiveOverlay\}/); }); -test("mirror page stays RPC-only instead of exposing a page-level mock toggle", () => { - const mirrorAppSource = readFileSync(resolve(desktopRoot, "src/features/dashboard/memory/MirrorApp.tsx"), "utf8"); - - assert.match(mirrorAppSource, /const dataMode: MirrorOverviewSource = "rpc";/); - assert.doesNotMatch(mirrorAppSource, /DashboardMockToggle/); - assert.doesNotMatch(mirrorAppSource, /loadDashboardDataMode\("memory"\)/); - assert.doesNotMatch(mirrorAppSource, /saveDashboardDataMode\("memory"\)/); - assert.doesNotMatch(mirrorAppSource, /setDataMode\(/); -}); - test("safety page stays RPC-only instead of exposing a page-level mock toggle", () => { const securityAppSource = readFileSync(resolve(desktopRoot, "src/features/dashboard/safety/SecurityApp.tsx"), "utf8"); diff --git a/apps/desktop/src/features/dashboard/home/components/DashboardCenterOrb.tsx b/apps/desktop/src/features/dashboard/home/components/DashboardCenterOrb.tsx index de8203379..ed5fa90fa 100644 --- a/apps/desktop/src/features/dashboard/home/components/DashboardCenterOrb.tsx +++ b/apps/desktop/src/features/dashboard/home/components/DashboardCenterOrb.tsx @@ -21,6 +21,8 @@ type DragState = { startY: number; }; +type OrbPointerEvent = ReactPointerEvent; + function clamp(value: number, minimum: number, maximum: number) { return Math.min(maximum, Math.max(minimum, value)); } @@ -117,7 +119,7 @@ export function DashboardCenterOrb({ activeColor, onDragOffset, onLongPress, vis }, LONG_PRESS_DURATION); }, [cancelLongPress, onLongPress]); - function handlePointerDown(event: ReactPointerEvent) { + function handlePointerDown(event: OrbPointerEvent) { event.preventDefault(); window.cancelAnimationFrame(returnFrameRef.current); event.currentTarget.setPointerCapture(event.pointerId); @@ -128,7 +130,7 @@ export function DashboardCenterOrb({ activeColor, onDragOffset, onLongPress, vis beginLongPress(); } - function handlePointerMove(event: ReactPointerEvent) { + function handlePointerMove(event: OrbPointerEvent) { if (!dragState || dragState.pointerId !== event.pointerId) { return; } @@ -152,7 +154,7 @@ export function DashboardCenterOrb({ activeColor, onDragOffset, onLongPress, vis setOffset(next); } - function handlePointerEnd(event: ReactPointerEvent) { + function handlePointerEnd(event: OrbPointerEvent) { if (!dragState || dragState.pointerId !== event.pointerId) { return; } @@ -200,14 +202,13 @@ export function DashboardCenterOrb({ activeColor, onDragOffset, onLongPress, vis ) : null} - +
diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.config.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.config.ts index 23c59dd34..c63157eb6 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.config.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.config.ts @@ -12,7 +12,7 @@ export const dashboardEntranceOrbs: DashboardEntranceOrbConfig[] = [ orbitSpeed: 2.2, orbitOffset: 264, route: resolveDashboardModuleRoutePath("tasks"), - size: 66, + size: 90, }, { key: "entrance-notes", @@ -24,7 +24,7 @@ export const dashboardEntranceOrbs: DashboardEntranceOrbConfig[] = [ orbitSpeed: 1.76, orbitOffset: 28, route: resolveDashboardModuleRoutePath("notes"), - size: 58, + size: 90, }, { key: "entrance-memory", @@ -36,7 +36,7 @@ export const dashboardEntranceOrbs: DashboardEntranceOrbConfig[] = [ orbitSpeed: 1.5, orbitOffset: 132, route: resolveDashboardModuleRoutePath("memory"), - size: 56, + size: 90, }, { key: "entrance-safety", @@ -48,7 +48,7 @@ export const dashboardEntranceOrbs: DashboardEntranceOrbConfig[] = [ orbitSpeed: 1.22, orbitOffset: 324, route: resolveDashboardModuleRoutePath("safety"), - size: 58, + size: 90, }, ]; diff --git a/apps/desktop/src/features/dashboard/memory/MemoryPage.tsx b/apps/desktop/src/features/dashboard/memory/MemoryPage.tsx index 3f4fff772..47bf02d77 100644 --- a/apps/desktop/src/features/dashboard/memory/MemoryPage.tsx +++ b/apps/desktop/src/features/dashboard/memory/MemoryPage.tsx @@ -1,13 +1,8 @@ -import { DashboardBackHomeLink } from "@/features/dashboard/shared/DashboardBackHomeLink"; -import { DashboardModuleFloatingNav } from "@/features/dashboard/shared/DashboardModuleFloatingNav"; -import { MirrorApp } from "./MirrorApp"; +import { MockMirrorMemoryOverview } from "./MockMirrorMemoryPage"; +/** + * Mounts the current mock-only mirror memory route for the dashboard. + */ export function MemoryPage() { - return ( - <> - - - - - ); + return ; } diff --git a/apps/desktop/src/features/dashboard/memory/MirrorApp.tsx b/apps/desktop/src/features/dashboard/memory/MirrorApp.tsx deleted file mode 100644 index 74d25bd80..000000000 --- a/apps/desktop/src/features/dashboard/memory/MirrorApp.tsx +++ /dev/null @@ -1,1246 +0,0 @@ -import { - useCallback, - useEffect, - useLayoutEffect, - useRef, - useState, - type KeyboardEvent, - type PointerEvent, -} from "react"; -import type { MirrorOverviewUpdatedNotification } from "@cialloclaw/protocol"; -import { X } from "lucide-react"; -import { useLocation, useNavigate } from "react-router-dom"; -import { PanelSurface, StatusBadge } from "@cialloclaw/ui"; -import { subscribeMirrorOverviewUpdated } from "@/rpc/subscriptions"; -import { useDashboardEscapeHandler } from "@/features/dashboard/shared/dashboardEscapeCoordinator"; -import { - formatDashboardSettingsMutationFeedback, - updateDashboardSettings, - type DashboardSettingsPatch, -} from "@/features/dashboard/shared/dashboardSettingsMutation"; -import { - applyMirrorSettingsSnapshot, - loadMirrorOverviewData, - type MirrorOverviewData, - type MirrorOverviewSource, -} from "./mirrorService"; -import { MirrorDetailContent, type MirrorHistoryDetailView } from "./MirrorDetailContent"; -import { loadMirrorFloatingPositions, saveMirrorFloatingPositions } from "./mirrorLayoutStorage"; -import { MirrorDecorativeBirds } from "./MirrorDecorativeBirds"; -import { - DEFAULT_MIRROR_DIRECTION_STACK, - FLOATING_MIRROR_DIRECTION_KEYS, - MIRROR_ORBITAL_TARGETS, - getMirrorDirectionMeta, - type FloatingMirrorDirectionKey, - type MirrorCardAccent, - type MirrorDirectionKey, -} from "./mirrorDirections"; -import { buildMirrorProfileView } from "./mirrorViewModel"; -import "./mirror.css"; - -type ModulePosition = { x: number; y: number }; -type ModulePositions = Record; -type ModuleSize = { width: number; height: number }; -type ModuleSizes = Record; -type BoardBounds = { minX: number; minY: number; maxX: number; maxY: number }; -type BoardGrid = { columns: number; rows: number }; -type OccupiedModule = { position: ModulePosition; size: ModuleSize }; -type LayoutMode = "default" | "compact"; -type BoardLayout = { - canvasWidth: number; - canvasHeight: number; - mode: LayoutMode; - bounds: BoardBounds; - memoryBounds: BoardBounds; - regularSize: ModuleSize; - moduleSizes: ModuleSizes; - grid: BoardGrid; - candidates: ModulePosition[]; -}; -type CardSummary = { - badge: string; - tone: string; - mainLine: string; - detailLine: string; - accent: MirrorCardAccent; - emphasis?: "memory"; -}; -type DetailBadge = { - label: string; - tone: string; -}; -type DragState = { - key: FloatingMirrorDirectionKey; - pointerId: number; - startX: number; - startY: number; - originX: number; - originY: number; - moved: boolean; -}; -type MirrorRouteState = { - activeDetailKey?: MirrorDirectionKey; - focusMemoryId?: string; - historyDetailView?: MirrorHistoryDetailView; -}; - -const INITIAL_MODULE_STACK: MirrorDirectionKey[] = DEFAULT_MIRROR_DIRECTION_STACK; -const DRAG_THRESHOLD = 8; -const BOARD_PADDING = 12; -const CARD_CLEARANCE = 10; -const CARD_STEP = 16; -const COMPACT_MEMORY_GAP = 14; -/* Keep floating mirror cards large enough to show two readable headline lines. */ -const MIN_COMPACT_CARD_WIDTH = 132; -const MIN_COMPACT_CARD_HEIGHT = 132; -const MIN_COMPACT_MEMORY_HEIGHT = 132; -const DEFAULT_CARD_SIZE: ModuleSize = { width: 376, height: 252 }; -const DEFAULT_MEMORY_CARD_SIZE: ModuleSize = { width: 480, height: 320 }; -const PINNED_MEMORY_CARD_OFFSET = { x: 20, y: 104 }; -const DEFAULT_MODULE_SIZES: ModuleSizes = { - dailyStage: DEFAULT_CARD_SIZE, - profile: DEFAULT_CARD_SIZE, - memory: DEFAULT_MEMORY_CARD_SIZE, - history: DEFAULT_CARD_SIZE, -}; -const DEFAULT_MODULE_POSITIONS: ModulePositions = { - dailyStage: { x: 0, y: 0 }, - profile: { x: 0, y: 0 }, - memory: { x: 0, y: 0 }, - history: { x: 0, y: 0 }, -}; - -function isMirrorDirectionKey(value: string): value is MirrorDirectionKey { - return INITIAL_MODULE_STACK.includes(value as MirrorDirectionKey); -} -function readMirrorRouteState(value: unknown) { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return null; - } - - const state = value as MirrorRouteState; - const focusMemoryId = typeof state.focusMemoryId === "string" && state.focusMemoryId.trim().length > 0 ? state.focusMemoryId : null; - const activeDetailKey = - typeof state.activeDetailKey === "string" && isMirrorDirectionKey(state.activeDetailKey) - ? state.activeDetailKey - : focusMemoryId - ? "memory" - : null; - - if (!activeDetailKey) { - return null; - } - - return { - activeDetailKey, - focusMemoryId, - historyDetailView: - activeDetailKey === "history" && - (state.historyDetailView === "summary" || state.historyDetailView === "conversation") - ? state.historyDetailView - : null, - }; -} - -function formatShortMirrorDate(value: string) { - return new Date(value).toLocaleDateString("zh-CN", { - month: "short", - day: "numeric", - }); -} - -function formatMirrorDateTime(value: string) { - return new Date(value).toLocaleString("zh-CN", { - month: "numeric", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); -} - -function clampValue(value: number, min: number, max: number) { - return Math.min(Math.max(value, min), max); -} - -function clampPosition(value: ModulePosition, bounds: BoardBounds) { - return { - x: clampValue(value.x, bounds.minX, bounds.maxX), - y: clampValue(value.y, bounds.minY, bounds.maxY), - }; -} - -function buildAxisPositions(min: number, max: number) { - if (max <= min) { - return [Math.round(min)]; - } - - const values: number[] = []; - - for (let value = min; value <= max; value += CARD_STEP) { - values.push(Math.round(value)); - } - - if (values[values.length - 1] !== Math.round(max)) { - values.push(Math.round(max)); - } - - return Array.from(new Set(values)); -} - -function clampDimension(value: number, min: number, max: number) { - if (max <= 1) { - return 1; - } - - const effectiveMin = Math.min(min, max); - return Math.min(Math.max(value, effectiveMin), max); -} - -function getBoardGrid(canvasWidth: number, canvasHeight: number): BoardGrid { - let bestGrid: BoardGrid = { columns: FLOATING_MIRROR_DIRECTION_KEYS.length, rows: 1 }; - let bestScore = Number.NEGATIVE_INFINITY; - - for (let columns = 1; columns <= FLOATING_MIRROR_DIRECTION_KEYS.length; columns += 1) { - const rows = Math.ceil(FLOATING_MIRROR_DIRECTION_KEYS.length / columns); - const width = (canvasWidth - BOARD_PADDING * 2 - CARD_CLEARANCE * (columns - 1)) / columns; - const height = (canvasHeight - BOARD_PADDING * 2 - CARD_CLEARANCE * (rows - 1)) / rows; - const score = Math.min(width, height); - - if (score > bestScore) { - bestGrid = { columns, rows }; - bestScore = score; - } - } - - return bestGrid; -} - -function getBoardCardSize(canvasWidth: number, canvasHeight: number, grid: BoardGrid) { - const width = Math.floor((canvasWidth - BOARD_PADDING * 2 - CARD_CLEARANCE * (grid.columns - 1)) / grid.columns); - const height = Math.floor((canvasHeight - BOARD_PADDING * 2 - CARD_CLEARANCE * (grid.rows - 1)) / grid.rows); - - return { - width: clampValue(width, 1, DEFAULT_CARD_SIZE.width), - height: clampValue(height, 1, DEFAULT_CARD_SIZE.height), - } satisfies ModuleSize; -} - -function getMemoryCardSize(canvasWidth: number, canvasHeight: number) { - const maxWidth = Math.max(1, canvasWidth - BOARD_PADDING * 2); - const maxHeight = Math.max(1, canvasHeight - BOARD_PADDING * 2); - - return { - width: clampDimension(Math.floor(canvasWidth * 0.42), 360, Math.min(640, maxWidth)), - height: clampDimension(Math.floor(canvasHeight * 0.38), 272, Math.min(420, maxHeight)), - } satisfies ModuleSize; -} - -function getModuleSizes(regularSize: ModuleSize, memorySize: ModuleSize): ModuleSizes { - return { - dailyStage: regularSize, - profile: regularSize, - memory: memorySize, - history: regularSize, - } satisfies ModuleSizes; -} - -function getBoardBounds(canvasWidth: number, canvasHeight: number, size: ModuleSize) { - const horizontalInset = Math.min(BOARD_PADDING, Math.max(0, (canvasWidth - size.width) * 0.5)); - const verticalInset = Math.min(BOARD_PADDING, Math.max(0, (canvasHeight - size.height) * 0.5)); - - return { - minX: horizontalInset, - minY: verticalInset, - maxX: Math.max(horizontalInset, canvasWidth - size.width - horizontalInset), - maxY: Math.max(verticalInset, canvasHeight - size.height - verticalInset), - } satisfies BoardBounds; -} - -function buildBoardCandidates(bounds: BoardBounds) { - const positions: ModulePosition[] = []; - const xs = buildAxisPositions(bounds.minX, bounds.maxX); - const ys = buildAxisPositions(bounds.minY, bounds.maxY); - - for (const y of ys) { - for (const x of xs) { - positions.push({ x, y }); - } - } - - return positions; -} - -function overlapsOccupied(position: ModulePosition, size: ModuleSize, occupied: OccupiedModule[]) { - return occupied.some((item) => { - const separatedHorizontally = - position.x + size.width + CARD_CLEARANCE <= item.position.x || - item.position.x + item.size.width + CARD_CLEARANCE <= position.x; - const separatedVertically = - position.y + size.height + CARD_CLEARANCE <= item.position.y || - item.position.y + item.size.height + CARD_CLEARANCE <= position.y; - - return !(separatedHorizontally || separatedVertically); - }); -} - -function resolveSettledPosition(target: ModulePosition, size: ModuleSize, occupied: OccupiedModule[], layout: BoardLayout) { - const clampedTarget = clampPosition(target, layout.bounds); - - if (!overlapsOccupied(clampedTarget, size, occupied)) { - return clampedTarget; - } - - let bestCandidate = clampedTarget; - let bestDistance = Number.POSITIVE_INFINITY; - - for (const candidate of layout.candidates) { - if (overlapsOccupied(candidate, size, occupied)) { - continue; - } - - const distance = Math.hypot(candidate.x - clampedTarget.x, candidate.y - clampedTarget.y); - - if (distance < bestDistance) { - bestCandidate = candidate; - bestDistance = distance; - } - } - - return bestDistance === Number.POSITIVE_INFINITY ? null : bestCandidate; -} - -function getGridModuleTargets(bounds: BoardBounds, grid: BoardGrid, size: ModuleSize): ModulePositions { - const availableWidth = Math.max(size.width, bounds.maxX - bounds.minX + size.width); - const availableHeight = Math.max(size.height, bounds.maxY - bounds.minY + size.height); - const gridHeight = grid.rows * size.height + Math.max(0, grid.rows - 1) * CARD_CLEARANCE; - const gridStartY = bounds.minY + Math.max(0, (availableHeight - gridHeight) / 2); - const positions = { ...DEFAULT_MODULE_POSITIONS }; - - FLOATING_MIRROR_DIRECTION_KEYS.forEach((key, index) => { - const row = Math.floor(index / grid.columns); - const indexInRow = index % grid.columns; - const remainingCards = FLOATING_MIRROR_DIRECTION_KEYS.length - row * grid.columns; - const cardsInRow = Math.min(grid.columns, remainingCards); - const rowWidth = cardsInRow * size.width + Math.max(0, cardsInRow - 1) * CARD_CLEARANCE; - const gridStartX = bounds.minX + Math.max(0, (availableWidth - rowWidth) / 2); - - positions[key] = { - x: gridStartX + indexInRow * (size.width + CARD_CLEARANCE), - y: gridStartY + row * (size.height + CARD_CLEARANCE), - }; - }); - - return positions; -} - -function getOrbitalModuleTargets(bounds: BoardBounds) { - const positions = { ...DEFAULT_MODULE_POSITIONS }; - const travelX = Math.max(0, bounds.maxX - bounds.minX); - const travelY = Math.max(0, bounds.maxY - bounds.minY); - - FLOATING_MIRROR_DIRECTION_KEYS.forEach((key) => { - const target = MIRROR_ORBITAL_TARGETS[key]; - positions[key] = { - x: Math.round(bounds.minX + travelX * target.x), - y: Math.round(bounds.minY + travelY * target.y), - }; - }); - - return positions; -} - -function getDefaultModuleTargets(bounds: BoardBounds, grid: BoardGrid, size: ModuleSize): ModulePositions { - const availableWidth = bounds.maxX - bounds.minX + size.width; - const availableHeight = bounds.maxY - bounds.minY + size.height; - const canUseOrbitalLayout = availableWidth >= size.width * 3.15 && availableHeight >= size.height * 2.6; - - if (!canUseOrbitalLayout) { - return getGridModuleTargets(bounds, grid, size); - } - - return getOrbitalModuleTargets(bounds); -} - -function getPinnedMemoryTarget(bounds: BoardBounds) { - return clampPosition( - { - x: bounds.minX + PINNED_MEMORY_CARD_OFFSET.x, - y: bounds.minY + PINNED_MEMORY_CARD_OFFSET.y, - }, - bounds, - ); -} - -function getCompactLayout(canvasWidth: number, canvasHeight: number): BoardLayout { - const canvasInnerWidth = Math.max(1, canvasWidth - BOARD_PADDING * 2); - const canvasInnerHeight = Math.max(1, canvasHeight - BOARD_PADDING * 2); - let bestLayout: BoardLayout | null = null; - let bestScore = Number.NEGATIVE_INFINITY; - - for (let columns = 1; columns <= FLOATING_MIRROR_DIRECTION_KEYS.length; columns += 1) { - const rows = Math.ceil(FLOATING_MIRROR_DIRECTION_KEYS.length / columns); - const regularWidth = Math.floor((canvasInnerWidth - CARD_CLEARANCE * (columns - 1)) / columns); - - if (regularWidth < MIN_COMPACT_CARD_WIDTH) { - continue; - } - - const maxMemoryHeight = - canvasInnerHeight - - COMPACT_MEMORY_GAP - - rows * MIN_COMPACT_CARD_HEIGHT - - CARD_CLEARANCE * Math.max(0, rows - 1); - - if (maxMemoryHeight < MIN_COMPACT_MEMORY_HEIGHT) { - continue; - } - - const memoryHeight = clampValue(Math.floor(canvasHeight * 0.28), MIN_COMPACT_MEMORY_HEIGHT, Math.min(248, maxMemoryHeight)); - const availableRegularHeight = - canvasInnerHeight - memoryHeight - COMPACT_MEMORY_GAP - CARD_CLEARANCE * Math.max(0, rows - 1); - const regularHeight = clampValue( - Math.floor(Math.min(availableRegularHeight / rows, regularWidth * 0.72)), - MIN_COMPACT_CARD_HEIGHT, - 224, - ); - const score = regularWidth * regularHeight; - - if (score <= bestScore) { - continue; - } - - const regularSize = { - width: regularWidth, - height: regularHeight, - } satisfies ModuleSize; - const memorySize = { - width: canvasInnerWidth, - height: memoryHeight, - } satisfies ModuleSize; - const bounds = getBoardBounds(canvasWidth, canvasHeight, regularSize); - const memoryBounds = getBoardBounds(canvasWidth, canvasHeight, memorySize); - - bestLayout = { - canvasWidth, - canvasHeight, - mode: "compact", - bounds, - memoryBounds, - regularSize, - moduleSizes: getModuleSizes(regularSize, memorySize), - grid: { columns, rows }, - candidates: buildBoardCandidates(bounds), - }; - bestScore = score; - } - - if (bestLayout) { - return bestLayout; - } - - const fallbackGrid = { columns: FLOATING_MIRROR_DIRECTION_KEYS.length, rows: 1 } satisfies BoardGrid; - const regularSize = { - width: Math.max(1, Math.floor((canvasInnerWidth - CARD_CLEARANCE * (fallbackGrid.columns - 1)) / fallbackGrid.columns)), - height: clampValue(Math.floor(canvasInnerHeight * 0.32), 1, 180), - } satisfies ModuleSize; - const memoryHeight = Math.max(1, canvasInnerHeight - regularSize.height - COMPACT_MEMORY_GAP); - const memorySize = { - width: canvasInnerWidth, - height: memoryHeight, - } satisfies ModuleSize; - - return { - canvasWidth, - canvasHeight, - mode: "compact", - bounds: getBoardBounds(canvasWidth, canvasHeight, regularSize), - memoryBounds: getBoardBounds(canvasWidth, canvasHeight, memorySize), - regularSize, - moduleSizes: getModuleSizes(regularSize, memorySize), - grid: fallbackGrid, - candidates: buildBoardCandidates(getBoardBounds(canvasWidth, canvasHeight, regularSize)), - }; -} - -function getCompactModuleTargets(layout: BoardLayout): ModulePositions { - const positions = { ...DEFAULT_MODULE_POSITIONS }; - const memoryWidth = layout.moduleSizes.memory.width; - const memoryHeight = layout.moduleSizes.memory.height; - const regularSize = layout.regularSize; - const horizontalGap = CARD_CLEARANCE; - const verticalGap = CARD_CLEARANCE; - const memoryPosition = clampPosition( - { - x: Math.round((layout.memoryBounds.minX + layout.memoryBounds.maxX) / 2), - y: layout.memoryBounds.minY, - }, - layout.memoryBounds, - ); - const rows = layout.grid.rows; - const contentHeight = - rows * regularSize.height + Math.max(0, rows - 1) * verticalGap; - const startY = clampValue( - memoryPosition.y + memoryHeight + COMPACT_MEMORY_GAP, - layout.bounds.minY, - Math.max(layout.bounds.minY, layout.bounds.maxY - contentHeight + regularSize.height), - ); - - positions.memory = memoryPosition; - - FLOATING_MIRROR_DIRECTION_KEYS.forEach((key, index) => { - const row = Math.floor(index / layout.grid.columns); - const remainingCards = FLOATING_MIRROR_DIRECTION_KEYS.length - row * layout.grid.columns; - const cardsInRow = Math.min(layout.grid.columns, remainingCards); - const rowWidth = cardsInRow * regularSize.width + Math.max(0, cardsInRow - 1) * horizontalGap; - const rowStartX = layout.bounds.minX + Math.max(0, (memoryWidth - rowWidth) / 2); - - positions[key] = clampPosition( - { - x: rowStartX + (index % layout.grid.columns) * (regularSize.width + horizontalGap), - y: startY + row * (regularSize.height + verticalGap), - }, - layout.bounds, - ); - }); - - return positions; -} - -function normalizeModulePositions(targets: ModulePositions, layout: BoardLayout) { - if (layout.mode === "compact") { - return getCompactModuleTargets(layout); - } - - const nextPositions = { ...DEFAULT_MODULE_POSITIONS }; - const pinnedMemoryPosition = getPinnedMemoryTarget(layout.memoryBounds); - const occupied: OccupiedModule[] = [{ position: pinnedMemoryPosition, size: layout.moduleSizes.memory }]; - - nextPositions.memory = pinnedMemoryPosition; - - for (const key of FLOATING_MIRROR_DIRECTION_KEYS) { - const settledPosition = resolveSettledPosition(targets[key], layout.moduleSizes[key], occupied, layout); - - if (!settledPosition) { - return getCompactModuleTargets(getCompactLayout(layout.canvasWidth, layout.canvasHeight)); - } - - nextPositions[key] = settledPosition; - occupied.push({ position: settledPosition, size: layout.moduleSizes[key] }); - } - - return nextPositions; -} - -function getDirectionTitle(key: MirrorDirectionKey) { - return getMirrorDirectionMeta(key).title; -} - -function getDirectionEyebrow(key: MirrorDirectionKey) { - return getMirrorDirectionMeta(key).eyebrow; -} - -function getDirectionPlateCode(key: MirrorDirectionKey) { - const codes: Record = { - dailyStage: "SL-02", - profile: "SL-03", - memory: "MS-01", - history: "SL-04", - }; - - return codes[key]; -} - -function pickFloatingModulePositions(positions: ModulePositions): Record { - return { - dailyStage: positions.dailyStage, - profile: positions.profile, - history: positions.history, - }; -} - -export function MirrorApp() { - const location = useLocation(); - const navigate = useNavigate(); - const storedFloatingPositionsRef = useRef(loadMirrorFloatingPositions()); - const hasStoredFloatingPositionsRef = useRef(storedFloatingPositionsRef.current !== null); - const [mirrorData, setMirrorData] = useState(null); - const dataMode: MirrorOverviewSource = "rpc"; - const [loadError, setLoadError] = useState(null); - const [modulePositions, setModulePositions] = useState(() => ({ - ...DEFAULT_MODULE_POSITIONS, - ...(storedFloatingPositionsRef.current ?? {}), - })); - const [moduleStack, setModuleStack] = useState(INITIAL_MODULE_STACK); - const [moduleSizes, setModuleSizes] = useState(DEFAULT_MODULE_SIZES); - const [layoutMode, setLayoutMode] = useState("default"); - const [draggingKey, setDraggingKey] = useState(null); - const [activeDetailKey, setActiveDetailKey] = useState(null); - const [focusedMemoryId, setFocusedMemoryId] = useState(null); - const [historyDetailView, setHistoryDetailView] = useState("conversation"); - const [boardReady, setBoardReady] = useState(false); - const [lastMirrorUpdate, setLastMirrorUpdate] = useState(null); - const canvasRef = useRef(null); - const dragStateRef = useRef(null); - const modulePositionsRef = useRef(DEFAULT_MODULE_POSITIONS); - const hasPlacedModulesRef = useRef(false); - const isMountedRef = useRef(true); - const dataModeRef = useRef(dataMode); - const fetchInFlightRef = useRef(false); - const pendingRefreshRef = useRef(false); - const refreshSequenceRef = useRef(0); - const lastSavedFloatingPositionsRef = useRef(null); - - dataModeRef.current = dataMode; - - const openDetail = useCallback((key: MirrorDirectionKey, options?: { focusMemoryId?: string | null; historyDetailView?: MirrorHistoryDetailView | null }) => { - setActiveDetailKey(key); - setFocusedMemoryId(key === "memory" ? options?.focusMemoryId ?? null : null); - if (key === "history" && options?.historyDetailView) { - setHistoryDetailView(options.historyDetailView); - } - }, []); - - const closeDetail = useCallback(() => { - setActiveDetailKey(null); - setFocusedMemoryId(null); - }, []); - - useDashboardEscapeHandler({ - enabled: activeDetailKey !== null, - handleEscape: closeDetail, - priority: 220, - }); - - useEffect(() => { - return () => { - isMountedRef.current = false; - }; - }, []); - - useEffect(() => { - const routeState = readMirrorRouteState(location.state); - - if (!routeState) { - return; - } - - openDetail(routeState.activeDetailKey, { - focusMemoryId: routeState.focusMemoryId, - historyDetailView: routeState.historyDetailView, - }); - navigate(location.pathname, { replace: true, state: null }); - }, [location.pathname, location.state, navigate, openDetail]); - - useEffect(() => { - if (!mirrorData || mirrorData.conversations.length > 0 || historyDetailView === "summary") { - return; - } - - setHistoryDetailView("summary"); - }, [historyDetailView, mirrorData]); - - const refreshMirrorData = useCallback(() => { - if (fetchInFlightRef.current) { - pendingRefreshRef.current = true; - return; - } - - fetchInFlightRef.current = true; - let nextSequence = ++refreshSequenceRef.current; - - void (async () => { - try { - do { - pendingRefreshRef.current = false; - const nextData = await loadMirrorOverviewData("rpc"); - - if (!isMountedRef.current || refreshSequenceRef.current !== nextSequence) { - return; - } - - setLoadError(null); - setMirrorData(nextData); - - if (pendingRefreshRef.current) { - nextSequence = ++refreshSequenceRef.current; - } - } while (pendingRefreshRef.current); - } catch (error) { - if (!isMountedRef.current || refreshSequenceRef.current !== nextSequence) { - return; - } - - setLoadError(error instanceof Error ? error.message : "镜子数据请求失败"); - } finally { - fetchInFlightRef.current = false; - - if (pendingRefreshRef.current && isMountedRef.current && dataModeRef.current === "rpc") { - refreshMirrorData(); - } - } - })(); - }, []); - - useEffect(() => { - setMirrorData(null); - - const unsubscribe = subscribeMirrorOverviewUpdated((notification) => { - setLastMirrorUpdate(notification); - refreshMirrorData(); - }); - - refreshMirrorData(); - - return () => { - unsubscribe(); - }; - }, [dataMode, refreshMirrorData]); - - const bringModuleToFront = useCallback((key: MirrorDirectionKey) => { - setModuleStack((currentStack) => [...currentStack.filter((item) => item !== key), key]); - }, []); - - const persistFloatingModulePositions = useCallback((positions: ModulePositions) => { - const floatingPositions = pickFloatingModulePositions(positions); - const serializedFloatingPositions = JSON.stringify(floatingPositions); - - if (lastSavedFloatingPositionsRef.current === serializedFloatingPositions) { - return; - } - - saveMirrorFloatingPositions(floatingPositions); - lastSavedFloatingPositionsRef.current = serializedFloatingPositions; - }, []); - - const getBoardLayout = useCallback(() => { - const canvas = canvasRef.current; - - if (!canvas) { - return null; - } - - if (canvas.clientWidth <= 760 || canvas.clientHeight <= 560) { - return getCompactLayout(canvas.clientWidth, canvas.clientHeight); - } - - const grid = getBoardGrid(canvas.clientWidth, canvas.clientHeight); - const regularSize = getBoardCardSize(canvas.clientWidth, canvas.clientHeight, grid); - const memorySize = getMemoryCardSize(canvas.clientWidth, canvas.clientHeight); - const bounds = getBoardBounds(canvas.clientWidth, canvas.clientHeight, regularSize); - const memoryBounds = getBoardBounds(canvas.clientWidth, canvas.clientHeight, memorySize); - - return { - canvasWidth: canvas.clientWidth, - canvasHeight: canvas.clientHeight, - mode: "default", - bounds, - memoryBounds, - regularSize, - moduleSizes: getModuleSizes(regularSize, memorySize), - grid, - candidates: buildBoardCandidates(bounds), - } satisfies BoardLayout; - }, []); - - const getSettledModulePosition = useCallback( - (key: MirrorDirectionKey, target: ModulePosition, positions: ModulePositions) => { - const layout = getBoardLayout(); - - if (!layout) { - return target; - } - - if (layout.mode === "compact") { - return positions[key]; - } - - const occupied = INITIAL_MODULE_STACK.filter((item) => item !== key).map((item) => ({ - position: positions[item], - size: layout.moduleSizes[item], - })); - return resolveSettledPosition(target, layout.moduleSizes[key], occupied, layout) ?? positions[key]; - }, - [getBoardLayout], - ); - - useLayoutEffect(() => { - if (!mirrorData) { - return; - } - - const syncBoardLayout = () => { - const layout = getBoardLayout(); - - if (!layout) { - return; - } - - setLayoutMode(layout.mode); - setModuleSizes(layout.moduleSizes); - setModulePositions((currentPositions) => { - const targets = hasPlacedModulesRef.current - ? currentPositions - : { - ...getDefaultModuleTargets(layout.bounds, layout.grid, layout.regularSize), - ...(hasStoredFloatingPositionsRef.current ? pickFloatingModulePositions(currentPositions) : {}), - }; - return normalizeModulePositions(targets, layout); - }); - hasPlacedModulesRef.current = true; - setBoardReady(true); - }; - - syncBoardLayout(); - window.addEventListener("resize", syncBoardLayout); - - return () => { - window.removeEventListener("resize", syncBoardLayout); - }; - }, [getBoardLayout, mirrorData]); - - useEffect(() => { - modulePositionsRef.current = modulePositions; - }, [modulePositions]); - - useEffect(() => { - if (!boardReady || draggingKey) { - return; - } - - persistFloatingModulePositions(modulePositions); - }, [boardReady, draggingKey, modulePositions, persistFloatingModulePositions]); - - const handleSettingsUpdate = useCallback( - async (subject: string, patch: DashboardSettingsPatch) => { - const result = await updateDashboardSettings(patch, dataMode); - - if (isMountedRef.current) { - setLoadError(null); - setMirrorData((current) => (current ? applyMirrorSettingsSnapshot(current, result.snapshot) : current)); - } - - return formatDashboardSettingsMutationFeedback(result, subject); - }, - [dataMode], - ); - - if (!mirrorData) { - return ( -
-
-

{loadError ? `镜子页同步失败:${loadError}` : "正在点亮检片台…"}

-
-
- ); - } - - const profileView = buildMirrorProfileView(mirrorData.profileItems); - - const { overview } = mirrorData; - const dataSourceDetails = [ - mirrorData.source === "rpc" - ? "当前展示来自本地服务。" - : "当前展示的是本地缓存的概览,用于在服务不可用时保持页面可读性。", - ]; - - if (mirrorData.rpcContext.serverTime) { - dataSourceDetails.push(`服务端时间 ${formatMirrorDateTime(mirrorData.rpcContext.serverTime)}`); - } - - if (lastMirrorUpdate) { - dataSourceDetails.push( - lastMirrorUpdate.source - ? `最近通知 revision #${lastMirrorUpdate.revision} · ${lastMirrorUpdate.source}` - : `最近通知 revision #${lastMirrorUpdate.revision}`, - ); - } - - if (mirrorData.rpcContext.warnings.length) { - dataSourceDetails.push(`warnings:${mirrorData.rpcContext.warnings.join(";")}`); - } - - if (loadError && dataMode === "rpc") { - dataSourceDetails.push(`error:${loadError}`); - } - - const dataSourceBadge = - mirrorData.source === "rpc" - ? { label: "LIVE", tone: "green" as const, copy: dataSourceDetails.join(" · ") } - : { label: "本地", tone: "processing" as const, copy: dataSourceDetails.join(" · ") }; - const latestMemoryReference = overview.memory_references[0] ?? null; - const latestConversation = mirrorData.conversations[0] ?? null; - - const releaseDrag = () => { - dragStateRef.current = null; - setDraggingKey(null); - }; - - const handleModulePointerDown = (key: FloatingMirrorDirectionKey) => (event: PointerEvent) => { - if (event.button !== 0) { - return; - } - - bringModuleToFront(key); - setDraggingKey(key); - dragStateRef.current = { - key, - pointerId: event.pointerId, - startX: event.clientX, - startY: event.clientY, - originX: modulePositions[key].x, - originY: modulePositions[key].y, - moved: false, - }; - event.currentTarget.setPointerCapture(event.pointerId); - }; - - const handleModulePointerMove = (key: FloatingMirrorDirectionKey) => (event: PointerEvent) => { - const dragState = dragStateRef.current; - - if (!dragState || dragState.key !== key || dragState.pointerId !== event.pointerId) { - return; - } - - const deltaX = event.clientX - dragState.startX; - const deltaY = event.clientY - dragState.startY; - - if (!dragState.moved) { - if (Math.hypot(deltaX, deltaY) < DRAG_THRESHOLD) { - return; - } - - dragStateRef.current = { - ...dragState, - moved: true, - }; - } - - setModulePositions((currentPositions) => ({ - ...currentPositions, - [key]: getSettledModulePosition( - key, - { - x: dragState.originX + deltaX, - y: dragState.originY + deltaY, - }, - currentPositions, - ), - })); - }; - - const handleModulePointerUp = (key: FloatingMirrorDirectionKey) => (event: PointerEvent) => { - const dragState = dragStateRef.current; - - if (!dragState || dragState.key !== key || dragState.pointerId !== event.pointerId) { - return; - } - - const travelled = Math.hypot(event.clientX - dragState.startX, event.clientY - dragState.startY); - - if (event.currentTarget.hasPointerCapture(event.pointerId)) { - event.currentTarget.releasePointerCapture(event.pointerId); - } - - releaseDrag(); - - if (dragState.moved) { - persistFloatingModulePositions(modulePositionsRef.current); - } - - if (!dragState.moved && travelled < DRAG_THRESHOLD) { - openDetail(key); - } - }; - - const handleModulePointerCancel = (_key: FloatingMirrorDirectionKey) => (event: PointerEvent) => { - if (event.currentTarget.hasPointerCapture(event.pointerId)) { - event.currentTarget.releasePointerCapture(event.pointerId); - } - - releaseDrag(); - }; - - const handleModuleKeyDown = (key: MirrorDirectionKey) => (event: KeyboardEvent) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - bringModuleToFront(key); - openDetail(key); - } - }; - - const getDetailBadge = (key: MirrorDirectionKey): DetailBadge => { - if (key === "dailyStage") { - return { - label: formatShortMirrorDate(mirrorData.dailyDigest.date), - tone: "processing", - }; - } - - if (key === "profile") { - return { - label: profileView.total_items > 0 ? `${profileView.total_items} 项资料` : "暂无资料", - tone: "green", - }; - } - - if (key === "history") { - return { - label: mirrorData.conversationSummary.total_records ? `${mirrorData.conversationSummary.total_records} 条对话` : "暂无对话", - tone: "processing", - }; - } - - return { - label: overview.memory_references.length ? `${overview.memory_references.length} 条引用` : "暂无引用", - tone: "processing", - }; - }; - - const renderDetailOverlay = () => { - if (!activeDetailKey) { - return null; - } - - const detailBadge = getDetailBadge(activeDetailKey); - const detailTitleAccessory = - activeDetailKey === "history" ? ( -
- - -
- ) : null; - - return ( -
-
event.stopPropagation()} - > - -
-
- {detailBadge.label} -
- -
-
- -
-
-
-
- ); - }; - - const getCardSummary = (key: MirrorDirectionKey): CardSummary => { - if (key === "dailyStage") { - return { - badge: formatShortMirrorDate(mirrorData.dailyDigest.date), - tone: "processing", - detailLine: mirrorData.dailyDigest.lede, - accent: getMirrorDirectionMeta(key).accent, - mainLine: mirrorData.dailyDigest.headline, - }; - } - - if (key === "profile") { - const primaryProfileItem = profileView.backend_items[0] ?? profileView.local_stat_items[0] ?? null; - return { - badge: profileView.backend_items.length > 0 ? `${profileView.backend_items.length} 个画像项` : "暂无画像项", - tone: "green", - detailLine: profileView.local_stat_items.length > 0 ? `${profileView.local_stat_items.length} 条最近本地统计。` : "仅显示你的画像信息。", - accent: getMirrorDirectionMeta(key).accent, - mainLine: primaryProfileItem?.value ?? "暂无画像资料", - }; - } - - if (key === "history") { - const historyHeadline = overview.history_summary[0] ?? latestConversation?.user_text ?? "暂无历史概要"; - const historyDetail = - overview.history_summary[1] ?? - latestConversation?.agent_text ?? - latestConversation?.user_text ?? - (mirrorData.conversationSummary.total_records > 0 - ? `本地仍保留 ${mirrorData.conversationSummary.total_records} 条最近对话。` - : "轻触查看历史概要与本地最近 100 条对话记录。"); - - return { - badge: - overview.history_summary.length > 0 - ? `${overview.history_summary.length} 条概要` - : mirrorData.conversationSummary.total_records - ? `${mirrorData.conversationSummary.total_records} 条本地对话` - : "暂无历史概要", - tone: "processing", - detailLine: historyDetail, - accent: getMirrorDirectionMeta(key).accent, - mainLine: historyHeadline, - }; - } - - const memorySummary = latestMemoryReference?.summary || latestMemoryReference?.reason; - - return { - badge: `${overview.memory_references.length} 条引用`, - tone: "processing", - detailLine: - memorySummary ?? - (mirrorData.conversationSummary.total_records > 0 - ? `暂无新引用;本地仍保留 ${mirrorData.conversationSummary.total_records} 条最近对话统计。` - : "等待新的记忆引用记录。"), - accent: getMirrorDirectionMeta(key).accent, - mainLine: latestMemoryReference?.memory_id ?? "暂无近期被调用记忆", - emphasis: "memory", - }; - }; - - const renderDraggableModule = (key: MirrorDirectionKey) => { - const isDragging = draggingKey === key; - const isExpanded = activeDetailKey === key; - const isPinnedMemoryCard = key === "memory"; - const moduleSize = moduleSizes[key]; - const summary = getCardSummary(key); - const directionMeta = getMirrorDirectionMeta(key); - const inspectionCode = getDirectionPlateCode(key); - const summaryClassName = [ - "mirror-page__card-line", - summary.emphasis ? `mirror-page__card-line--${summary.emphasis}` : null, - ] - .filter(Boolean) - .join(" "); - - const pointerHandlers = isPinnedMemoryCard || layoutMode === "compact" - ? { - onClick: () => { - bringModuleToFront(key); - openDetail(key); - }, - } - : { - onPointerDown: handleModulePointerDown(key), - onPointerMove: handleModulePointerMove(key), - onPointerUp: handleModulePointerUp(key), - onPointerCancel: handleModulePointerCancel(key), - }; - - return ( -
- -
- ); - }; - - return ( -
-
-
- {dataSourceBadge.label} -

{dataSourceBadge.copy}

-
- - {moduleStack.map(renderDraggableModule)} - {activeDetailKey ?
{renderDetailOverlay()}
: null} -
-
- ); -} diff --git a/apps/desktop/src/features/dashboard/memory/MirrorDecorativeBirds.tsx b/apps/desktop/src/features/dashboard/memory/MirrorDecorativeBirds.tsx deleted file mode 100644 index c3046437e..000000000 --- a/apps/desktop/src/features/dashboard/memory/MirrorDecorativeBirds.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { ShellBallMascot } from "@/features/shell-ball/components/ShellBallMascot"; -import { getShellBallMotionConfig } from "@/features/shell-ball/shellBall.motion"; -import type { ShellBallMotionConfig, ShellBallVisualState } from "@/features/shell-ball/shellBall.types"; -import "@/features/shell-ball/shellBall.css"; - -type DecorativeBird = { - key: string; - role: "lookout" | "dozer" | "tinkerer"; - cornerClassName: string; - visualState: ShellBallVisualState; - motionConfig: ShellBallMotionConfig; -}; - -const noop = () => {}; -const noopPressEnd = () => false; - -function createDecorativeMotionConfig( - visualState: ShellBallVisualState, - overrides: Partial, -): ShellBallMotionConfig { - return { - ...getShellBallMotionConfig(visualState), - ringMode: "hidden", - showAuthMarker: false, - ...overrides, - }; -} - -const DECORATIVE_BIRDS: DecorativeBird[] = [ - { - key: "top-right-lookout", - role: "lookout", - cornerClassName: "mirror-page__decor-bird--top-right", - visualState: "hover_input", - motionConfig: createDecorativeMotionConfig("hover_input", { - accentTone: "sky", - eyeMode: "curious", - wingMode: "lift", - bodyScale: 0.88, - bodyTiltDeg: -12, - floatOffsetY: -14, - floatDurationMs: 3200, - breatheScale: 1.022, - breatheDurationMs: 2800, - wingLiftDeg: 15, - wingSpreadPx: 7, - wingDurationMs: 1700, - tailSwingDeg: 9, - tailDurationMs: 2200, - crestLiftPx: 4, - blinkDelayMs: 120, - }), - }, - { - key: "bottom-right-dozer", - role: "dozer", - cornerClassName: "mirror-page__decor-bird--bottom-right", - visualState: "idle", - motionConfig: createDecorativeMotionConfig("idle", { - accentTone: "amber", - eyeMode: "careful", - wingMode: "tucked", - bodyScale: 1.02, - bodyTiltDeg: 10, - floatOffsetY: -5, - floatDurationMs: 7200, - breatheScale: 1.016, - breatheDurationMs: 5600, - wingLiftDeg: 2, - wingSpreadPx: 1, - wingDurationMs: 3600, - tailSwingDeg: 3, - tailDurationMs: 5200, - crestLiftPx: 0, - blinkDelayMs: 920, - }), - }, - { - key: "bottom-left-tinkerer", - role: "tinkerer", - cornerClassName: "mirror-page__decor-bird--bottom-left", - visualState: "processing", - motionConfig: createDecorativeMotionConfig("processing", { - accentTone: "teal", - eyeMode: "focus", - wingMode: "flutter", - bodyScale: 0.88, - bodyTiltDeg: 13, - floatOffsetY: -7, - floatDurationMs: 1900, - breatheScale: 1.028, - breatheDurationMs: 1450, - wingLiftDeg: 24, - wingSpreadPx: 10, - wingDurationMs: 560, - tailSwingDeg: 22, - tailDurationMs: 980, - crestLiftPx: 7, - blinkDelayMs: 40, - }), - }, -]; - -export function MirrorDecorativeBirds() { - return ( - - ); -} diff --git a/apps/desktop/src/features/dashboard/memory/MockMirrorMemoryPage.tsx b/apps/desktop/src/features/dashboard/memory/MockMirrorMemoryPage.tsx new file mode 100644 index 000000000..44903fb04 --- /dev/null +++ b/apps/desktop/src/features/dashboard/memory/MockMirrorMemoryPage.tsx @@ -0,0 +1,497 @@ +import { useMemo, useState } from "react"; +import { format } from "date-fns"; +import { zhCN } from "date-fns/locale"; +import { ArrowRight, Calendar as CalendarIcon, Copy, X } from "lucide-react"; +import memoryBackground from "@/assets/background_memory.png"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { useDashboardEscapeHandler } from "@/features/dashboard/shared/dashboardEscapeCoordinator"; +import { + findMockRecentMemory, + findMockSessionSummary, + findMockSessionSummaryByDate, + mockDailySummary, + mockHeatmap, + mockPhaseSummary, + mockProfileAxes, + mockProfileMetrics, + mockRecentMemories, + type MockRecentMemory, + type MockSessionSummary, +} from "./mockMirrorMemoryData"; +import "./mockMirrorMemory.css"; + +type MainTab = "profile" | "period"; +type HistoryDate = string; +type MemoryModalState = + | { kind: "recent"; memory: MockRecentMemory } + | { kind: "session"; session: MockSessionSummary } + | null; + +const defaultHistoryDate = new Date(2026, 4, 14); +const historyDataDate = new Date(2026, 4, 14); + +function formatHistoryDate(date: Date) { + return format(date, "yyyy.MM.dd"); +} + +/** + * Renders the new mock-only mirror memory workspace used by the dashboard. + */ +export function MockMirrorMemoryOverview() { + const [mainTab, setMainTab] = useState("profile"); + const [selectedHistoryDate, setSelectedHistoryDate] = useState(defaultHistoryDate); + const [historyCalendarMonth, setHistoryCalendarMonth] = useState(defaultHistoryDate); + const [activeModal, setActiveModal] = useState(null); + const recentMemory = mockRecentMemories[0] ?? null; + const selectedHistoryDateKey = useMemo(() => (selectedHistoryDate ? formatHistoryDate(selectedHistoryDate) : formatHistoryDate(defaultHistoryDate)), [selectedHistoryDate]); + const selectedSession = useMemo(() => findMockSessionSummaryByDate(selectedHistoryDateKey), [selectedHistoryDateKey]); + + useDashboardEscapeHandler({ + enabled: activeModal !== null, + handleEscape: () => setActiveModal(null), + priority: 220, + }); + + function openRecent(memoryId: string) { + const memory = findMockRecentMemory(memoryId); + if (memory) { + setActiveModal({ kind: "recent", memory }); + } + } + + function openSession(sessionId: string) { + const session = findMockSessionSummary(sessionId); + if (session) { + setActiveModal({ kind: "session", session }); + } + } + + function handleHistoryDateSelect(date: Date | undefined) { + if (!date) { + return; + } + + setSelectedHistoryDate(date); + setHistoryCalendarMonth(date); + } + + return ( + +
+ + +
+
+ setMainTab("profile")} /> + setMainTab("period")} /> +
+ + {mainTab === "profile" ? ( +
+
+
+ +
+ +
+
+
+
+
PROFILE ID
+

初次协作样本

+
+ 观察中 +
+
+ +
+ {mockProfileMetrics.map((metric) => ( +
+ {metric.label} + {metric.value} +
+ ))} +
+
+
+
+ ) : ( +
+
+
日报与阶段总结
+

日报与阶段总结

+
+ +
+
+
{mockDailySummary.title}
+
    + {mockDailySummary.lines.map((line) => ( +
  • {line}
  • + ))} +
+
+
+
{mockPhaseSummary.title}
+
    + {mockPhaseSummary.lines.map((line) => ( +
  • {line}
  • + ))} +
+
+
+ +
+
活跃时段热力图
+ +
+
+ )} +
+
+ + {activeModal ? setActiveModal(null)} /> : null} +
+ ); +} + +function MemoryModalOverlay({ modal, onClose }: { modal: Exclude; onClose: () => void }) { + return ( +
+ +
+ {modal.kind === "recent" ? : } +
+
+
+ ); +} + +function RecentMemoryModalBody({ memory }: { memory: MockRecentMemory }) { + const [feedback, setFeedback] = useState(null); + + const copyPayload = useMemo(() => { + return [ + memory.title, + `来源:${memory.source}`, + `引用时间:${memory.lastReferencedAt}`, + `来自任务:${memory.sourceTask}`, + `引用次数:${memory.referenceCount}`, + `记忆摘要:${memory.summary}`, + `适用范围:${memory.scope}`, + `关键要点:${memory.highlights.join(" / ")}`, + `原始记录:${memory.rawRecord}`, + `关联偏好:${memory.relatedPreference}`, + `相关历史:${memory.historyLinks.join(" / ")}`, + ].join("\n"); + }, [memory]); + + async function handleCopy() { + try { + await navigator.clipboard.writeText(copyPayload); + setFeedback("已复制到剪贴板"); + } catch { + setFeedback("当前环境无法直接复制,但内容仍然保留在这页"); + } + } + + return ( + <> +
+
+
记忆详情
+

{memory.title}

+

{memory.summary}

+
+
+ 引用次数 + {memory.referenceCount} +
+
+ +
+ + + + +
+ +
+ +
+ {memory.highlights.map((highlight) => ( + {highlight} + ))} +
+
+ +

{memory.rawRecord}

+
+ +

{memory.relatedPreference}

+
+ +
    + {memory.historyLinks.map((item) => ( +
  • {item}
  • + ))} +
+
+
+ +
+ + + +
+ + {feedback ?
{feedback}
: null} + + ); +} + +function SessionModalBody({ session }: { session: MockSessionSummary }) { + return ( + <> +
+
+
会话摘要详情
+

{session.title}

+

{session.summary}

+
+
+ 消息数 + {session.messageCount} +
+
+ +
+ + + + +
+ +
+ +

{session.summary}

+
+ +
    + {session.detailLines.map((line) => ( +
  • {line}
  • + ))} +
+
+
+ + ); +} + +function MemoryScene({ children }: { children: React.ReactNode }) { + return
{children}
; +} + +function DetailBlock({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ); +} + +function DetailSection({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+
{title}
+
{children}
+
+ ); +} + +function TabButton({ active, label, onClick }: { active: boolean; label: string; onClick: () => void }) { + return ( + + ); +} + +function RadarChart() { + const center = 86; + const radius = 62; + const levels = [0.2, 0.4, 0.6, 0.8]; + const angleStep = (Math.PI * 2) / mockProfileAxes.length; + const points = mockProfileAxes.map((axis, index) => { + const angle = -Math.PI / 2 + index * angleStep; + const x = center + Math.cos(angle) * radius * axis.value; + const y = center + Math.sin(angle) * radius * axis.value; + return `${x},${y}`; + }).join(" "); + + return ( + + ); +} + +function HeatmapGrid() { + const columns = ["一", "二", "三", "四", "五", "六", "日"]; + + return ( +
+
+ {columns.map((column) => {column})} +
+
+ {mockHeatmap.flatMap((row, rowIndex) => row.map((value, columnIndex) => ( + + )))} +
+
+ ); +} diff --git a/apps/desktop/src/features/dashboard/memory/mirror.css b/apps/desktop/src/features/dashboard/memory/mirror.css deleted file mode 100644 index e4d3432ef..000000000 --- a/apps/desktop/src/features/dashboard/memory/mirror.css +++ /dev/null @@ -1,1681 +0,0 @@ -.mirror-page { - --mirror-font-display: var(--cc-font-display); - --mirror-font-body: var(--cc-font-ui); - --mirror-space-1: 0.55rem; - --mirror-space-2: 0.8rem; - --mirror-space-3: 1.05rem; - --mirror-space-4: 1.45rem; - --mirror-space-5: 2rem; - --mirror-space-6: 2.7rem; - --mirror-radius-sm: 1rem; - --mirror-radius-md: 1.55rem; - --mirror-radius-lg: 2rem; - --mirror-desk: var(--cc-cream-3); - --mirror-desk-soft: var(--cc-paper-soft); - --mirror-platen: var(--cc-paper); - --mirror-platen-glow: rgba(255, 244, 224, 0.88); - --mirror-paper: var(--cc-paper); - --mirror-paper-strong: var(--cc-paper-strong); - --mirror-film: rgba(251, 255, 253, 0.54); - --mirror-film-strong: rgba(255, 255, 255, 0.68); - --mirror-line: var(--cc-line); - --mirror-line-strong: var(--cc-line-strong); - --mirror-ink: var(--cc-ink); - --mirror-copy: var(--cc-ink-soft); - --mirror-muted: var(--cc-ink-muted); - --mirror-accent: var(--cc-module-memory-strong); - --mirror-focus: rgba(124, 98, 63, 0.58); - background: - var(--cc-page-bg); - color: var(--mirror-ink); - color-scheme: light; - font-family: var(--mirror-font-body); - height: 100vh; - max-height: 100vh; - overflow: hidden; - padding: 0; - position: relative; -} - -.app-shell.mirror-page { - padding: 0; -} - -.mirror-page::before, -.mirror-page::after, -.mirror-page__canvas::before, -.mirror-page__canvas::after { - content: ""; - inset: 0; - pointer-events: none; - position: absolute; -} - -.mirror-page::before { - background: - linear-gradient(135deg, rgba(255, 255, 255, 0.42), transparent 36%), - linear-gradient(180deg, rgba(116, 86, 56, 0.08), transparent 20%, transparent 80%, rgba(116, 86, 56, 0.12)); -} - -.mirror-page::after { - background-image: - radial-gradient(rgba(120, 96, 71, 0.08) 0.8px, transparent 0.8px), - linear-gradient(rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0)); - background-position: 0 0, 0 0; - background-size: 26px 26px, 100% 100%; - mask-image: radial-gradient(ellipse 92% 84% at 50% 50%, black 0%, transparent 100%); - opacity: 0.28; -} - -.mirror-page__canvas, -.mirror-page__detail-panel { - height: 100%; - min-height: 0; - position: relative; -} - -.mirror-page__canvas--full { - height: 100%; -} - -.mirror-page__canvas { - overflow: hidden; -} - -.mirror-page__source-status { - align-items: center; - display: inline-flex; - gap: 0.7rem; - left: 1rem; - position: absolute; - top: 1rem; - z-index: 4; -} - -.mirror-page__source-copy { - color: var(--mirror-muted); - font-size: 0.78rem; - margin: 0; -} - -.mirror-page__canvas::before { - background: - radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.22), transparent 20%), - linear-gradient(90deg, transparent 8%, rgba(255, 255, 255, 0.14) 22%, transparent 42%, transparent 58%, rgba(255, 255, 255, 0.16) 76%, transparent 94%); - opacity: 0.66; -} - -.mirror-page__canvas::after { - background: - linear-gradient(rgba(116, 86, 56, 0.045) 1px, transparent 1px), - linear-gradient(90deg, rgba(116, 86, 56, 0.045) 1px, transparent 1px); - background-size: 132px 132px; - mask-image: radial-gradient(circle at 50% 50%, black 0%, rgba(0, 0, 0, 0.18) 64%, transparent 100%); - opacity: 0.5; -} - -.mirror-page__canvas--loading { - display: grid; - place-items: center; -} - -.mirror-page__loading-copy, -.mirror-page__card-kicker, -.mirror-page__micro-label, -.mirror-page__memory-index, -.mirror-page__history-label, -.mirror-page__module-hint, -.mirror-page__surface-code { - font-family: var(--font-mono); - font-size: 0.7rem; - font-weight: 600; - letter-spacing: 0.24em; - margin: 0; - text-transform: uppercase; -} - -.mirror-page__loading-copy { - color: var(--mirror-muted); -} - -.mirror-page__scene { - inset: 0; - pointer-events: none; - position: absolute; - z-index: 1; -} - -.mirror-page__desk-glow, -.mirror-page__desk-shadow-band, -.mirror-page__inspection-field, -.mirror-page__inspection-field-core, -.mirror-page__inspection-grid, -.mirror-page__inspection-register, -.mirror-page__inspection-corner, -.mirror-page__inspection-pin { - position: absolute; -} - -.mirror-page__desk-glow { - border-radius: 999px; - filter: blur(18px); -} - -.mirror-page__desk-glow--north { - background: radial-gradient(circle, rgba(255, 255, 255, 0.7) 0%, rgba(255, 239, 211, 0.2) 42%, transparent 76%); - height: min(18rem, 32vw); - left: 50%; - top: 12%; - transform: translateX(-50%); - width: min(34rem, 54vw); -} - -.mirror-page__desk-glow--east { - background: radial-gradient(circle, rgba(249, 217, 187, 0.28) 0%, rgba(240, 208, 167, 0.08) 46%, transparent 74%); - height: min(22rem, 38vw); - right: 9%; - top: 28%; - width: min(18rem, 28vw); -} - -.mirror-page__desk-shadow-band { - background: linear-gradient(180deg, rgba(117, 82, 49, 0), rgba(117, 82, 49, 0.14)); - bottom: 0; - height: 24%; - left: 0; - right: 0; -} - -.mirror-page__inspection-field { - animation: mirror-field-breathe 7.8s ease-in-out infinite; - background: - linear-gradient(180deg, rgba(255, 252, 247, 0.98), rgba(248, 241, 231, 0.92)), - var(--mirror-platen); - border: 1px solid rgba(151, 122, 90, 0.26); - border-radius: 2.4rem; - box-shadow: - 0 0 0 1px rgba(255, 255, 255, 0.84) inset, - 0 32px 68px -52px rgba(115, 80, 48, 0.48), - 0 0 88px rgba(255, 239, 210, 0.42); - height: min(33rem, 46vw); - left: 50%; - top: 51%; - transform: translate(-50%, -50%); - width: min(54rem, 72vw); -} - -.mirror-page__inspection-field::before, -.mirror-page__inspection-field::after, -.mirror-page__card-surface::before, -.mirror-page__card-surface::after, -.mirror-page__detail-panel > section::before, -.mirror-page__detail-panel > section::after { - content: ""; - inset: 0; - pointer-events: none; - position: absolute; -} - -.mirror-page__inspection-field::before { - background: - linear-gradient(90deg, transparent 8%, rgba(255, 255, 255, 0.44) 26%, transparent 44%, transparent 60%, rgba(255, 255, 255, 0.34) 78%, transparent 92%), - radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.38), transparent 44%); - border-radius: inherit; -} - -.mirror-page__inspection-field::after { - border-radius: inherit; - box-shadow: 0 0 0 1px rgba(131, 103, 72, 0.14) inset; -} - -.mirror-page__inspection-field-core { - background: radial-gradient(circle, var(--mirror-platen-glow) 0%, rgba(255, 245, 221, 0.58) 42%, rgba(255, 238, 210, 0.1) 70%, transparent 100%); - border-radius: 50%; - height: 74%; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - width: 62%; -} - -.mirror-page__inspection-grid { - background-image: - linear-gradient(rgba(150, 122, 94, 0.1) 1px, transparent 1px), - linear-gradient(90deg, rgba(150, 122, 94, 0.1) 1px, transparent 1px); - background-size: 40px 40px; - inset: 1.1rem; - mask-image: radial-gradient(circle at 50% 50%, black 0%, rgba(0, 0, 0, 0.3) 68%, transparent 100%); - opacity: 0.78; -} - -.mirror-page__inspection-register { - opacity: 0.9; -} - -.mirror-page__inspection-register--horizontal { - border-top: 1px dashed rgba(122, 92, 61, 0.3); - left: 12%; - right: 12%; - top: 50%; -} - -.mirror-page__inspection-register--vertical { - border-left: 1px dashed rgba(122, 92, 61, 0.3); - bottom: 14%; - left: 50%; - top: 14%; -} - -.mirror-page__inspection-corner { - border-color: rgba(123, 95, 66, 0.34); - height: 1.45rem; - width: 1.45rem; -} - -.mirror-page__inspection-corner--top-left { - border-left: 1px solid currentColor; - border-top: 1px solid currentColor; - color: rgba(123, 95, 66, 0.34); - left: 1rem; - top: 1rem; -} - -.mirror-page__inspection-corner--top-right { - border-right: 1px solid currentColor; - border-top: 1px solid currentColor; - color: rgba(123, 95, 66, 0.34); - right: 1rem; - top: 1rem; -} - -.mirror-page__inspection-corner--bottom-left { - border-bottom: 1px solid currentColor; - border-left: 1px solid currentColor; - color: rgba(123, 95, 66, 0.34); - bottom: 1rem; - left: 1rem; -} - -.mirror-page__inspection-corner--bottom-right { - border-bottom: 1px solid currentColor; - border-right: 1px solid currentColor; - color: rgba(123, 95, 66, 0.34); - bottom: 1rem; - right: 1rem; -} - -.mirror-page__inspection-pin { - background: rgba(135, 104, 71, 0.16); - border-radius: 999px; - box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.7) inset; -} - -.mirror-page__inspection-pin--top { - height: 0.3rem; - left: 50%; - top: 1.2rem; - transform: translateX(-50%); - width: 4.6rem; -} - -.mirror-page__inspection-pin--right { - height: 4.6rem; - right: 1.2rem; - top: 50%; - transform: translateY(-50%); - width: 0.3rem; -} - -.mirror-page__decor-birds { - inset: 0; - pointer-events: none; - position: absolute; - z-index: 2; -} - -.mirror-page__decor-bird { - --mirror-bird-aura: radial-gradient(circle, rgba(255, 255, 255, 0.56), transparent 74%); - --mirror-bird-aura-blur: 16px; - --mirror-bird-aura-inset: 18% 14% 26%; - --mirror-bird-aura-opacity: 0.3; - --mirror-bird-aura-rotate: 0deg; - --mirror-bird-orbital-back-opacity: 0.74; - --mirror-bird-orbital-back-scale: 1; - --mirror-bird-orbital-back-shift-x: -50%; - --mirror-bird-orbital-back-shift-y: -50%; - --mirror-bird-orbital-front-opacity: 0.72; - --mirror-bird-orbital-front-scale: 1; - --mirror-bird-orbital-front-shift-x: -50%; - --mirror-bird-orbital-front-shift-y: -50%; - --mirror-bird-perch-bottom: 1rem; - --mirror-bird-perch-opacity: 1; - --mirror-bird-perch-rotate: 0deg; - --mirror-bird-perch-shift-x: -50%; - --mirror-bird-perch-width: 9.8rem; - --mirror-bird-saturate: 0.92; - --mirror-bird-shadow-alpha: 0.18; - --mirror-bird-shadow-bottom: 2.1rem; - --mirror-bird-shadow-height: 2.9rem; - --mirror-bird-shadow-opacity: 1; - --mirror-bird-shadow-scale: 1; - --mirror-bird-shadow-shift-x: -50%; - --mirror-bird-shadow-width: 10rem; - --mirror-bird-shell-opacity: 0.9; - --mirror-bird-shell-rotate: 0deg; - --mirror-bird-shell-scale-x: 0.88; - --mirror-bird-shell-scale-y: 0.88; - --mirror-bird-shell-shift-x: 0rem; - --mirror-bird-shell-shift-y: 0rem; - pointer-events: none; - position: absolute; -} - -.mirror-page__decor-bird-shell { - filter: saturate(var(--mirror-bird-saturate)) drop-shadow(0 24px 36px rgba(111, 84, 54, var(--mirror-bird-shadow-alpha))); - isolation: isolate; - opacity: var(--mirror-bird-shell-opacity); - position: relative; - transform-origin: center; - transform: - translate3d(var(--mirror-bird-shell-shift-x), var(--mirror-bird-shell-shift-y), 0) - rotate(var(--mirror-bird-shell-rotate)) - scaleX(var(--mirror-bird-shell-scale-x)) - scaleY(var(--mirror-bird-shell-scale-y)); -} - -.mirror-page__decor-bird, -.mirror-page__decor-bird * { - pointer-events: none; -} - -.mirror-page__decor-bird-shell::before { - background: var(--mirror-bird-aura); - border-radius: 999px; - content: ""; - filter: blur(var(--mirror-bird-aura-blur)); - inset: var(--mirror-bird-aura-inset); - opacity: var(--mirror-bird-aura-opacity); - pointer-events: none; - position: absolute; - transform: rotate(var(--mirror-bird-aura-rotate)); - z-index: 0; -} - -.mirror-page__decor-bird-shell > .shell-ball-mascot { - position: relative; - z-index: 1; -} - -.mirror-page__decor-bird .shell-ball-mascot { - --shell-ball-size: 11.5rem; - opacity: 0.95; - pointer-events: none; -} - -.mirror-page__decor-bird .shell-ball-mascot__hotspot { - display: none; -} - -.mirror-page__decor-bird .shell-ball-mascot__shadow { - bottom: var(--mirror-bird-shadow-bottom); - height: var(--mirror-bird-shadow-height); - opacity: var(--mirror-bird-shadow-opacity); - transform: translateX(var(--mirror-bird-shadow-shift-x)) scaleX(var(--mirror-bird-shadow-scale)); - width: var(--mirror-bird-shadow-width); -} - -.mirror-page__decor-bird .shell-ball-mascot__orbital--back { - opacity: var(--mirror-bird-orbital-back-opacity); - transform: translate(var(--mirror-bird-orbital-back-shift-x), var(--mirror-bird-orbital-back-shift-y)) scale(var(--mirror-bird-orbital-back-scale)); -} - -.mirror-page__decor-bird .shell-ball-mascot__orbital--front { - opacity: var(--mirror-bird-orbital-front-opacity); - transform: translate(var(--mirror-bird-orbital-front-shift-x), var(--mirror-bird-orbital-front-shift-y)) scale(var(--mirror-bird-orbital-front-scale)); -} - -.mirror-page__decor-bird .shell-ball-mascot__perch { - bottom: var(--mirror-bird-perch-bottom); - opacity: var(--mirror-bird-perch-opacity); - transform: translateX(var(--mirror-bird-perch-shift-x)) rotate(var(--mirror-bird-perch-rotate)); - width: var(--mirror-bird-perch-width); -} - -.mirror-page__decor-bird--top-right { - right: 4.5%; - top: 4.5%; -} - -.mirror-page__decor-bird[data-role="lookout"] { - --mirror-bird-aura: radial-gradient(circle at 34% 48%, rgba(180, 211, 235, 0.56), rgba(255, 255, 255, 0.14) 48%, transparent 76%); - --mirror-bird-aura-inset: 18% 18% 26% 4%; - --mirror-bird-aura-opacity: 0.48; - --mirror-bird-aura-rotate: -8deg; - --mirror-bird-orbital-back-opacity: 0.82; - --mirror-bird-orbital-back-scale: 0.92; - --mirror-bird-orbital-back-shift-x: -46%; - --mirror-bird-orbital-front-opacity: 0.58; - --mirror-bird-orbital-front-scale: 0.88; - --mirror-bird-orbital-front-shift-x: -45%; - --mirror-bird-orbital-front-shift-y: -52%; - --mirror-bird-perch-bottom: 1.18rem; - --mirror-bird-perch-rotate: -6deg; - --mirror-bird-perch-shift-x: -58%; - --mirror-bird-perch-width: 8rem; - --mirror-bird-shadow-bottom: 2.2rem; - --mirror-bird-shadow-scale: 0.94; - --mirror-bird-shadow-shift-x: -56%; - --mirror-bird-shadow-width: 8.8rem; - --mirror-bird-shell-opacity: 0.86; - --mirror-bird-shell-rotate: -11deg; - --mirror-bird-shell-scale-x: 0.8; - --mirror-bird-shell-scale-y: 0.84; - --mirror-bird-shell-shift-x: -0.55rem; - --mirror-bird-shell-shift-y: -0.2rem; -} - -.mirror-page__decor-bird--bottom-right { - bottom: 3.8%; - right: 5.4%; -} - -.mirror-page__decor-bird[data-role="dozer"] { - --mirror-bird-aura: radial-gradient(circle at 56% 58%, rgba(255, 234, 203, 0.58), rgba(255, 248, 236, 0.18) 52%, transparent 80%); - --mirror-bird-aura-blur: 20px; - --mirror-bird-aura-inset: 24% 10% 18% 14%; - --mirror-bird-aura-opacity: 0.46; - --mirror-bird-orbital-back-opacity: 0.56; - --mirror-bird-orbital-back-scale: 1.05; - --mirror-bird-orbital-front-opacity: 0.42; - --mirror-bird-orbital-front-scale: 0.82; - --mirror-bird-orbital-front-shift-y: -46%; - --mirror-bird-perch-bottom: 0.7rem; - --mirror-bird-perch-opacity: 0.94; - --mirror-bird-perch-rotate: 7deg; - --mirror-bird-perch-shift-x: -48%; - --mirror-bird-perch-width: 11rem; - --mirror-bird-saturate: 0.86; - --mirror-bird-shadow-alpha: 0.14; - --mirror-bird-shadow-bottom: 1.55rem; - --mirror-bird-shadow-height: 3.3rem; - --mirror-bird-shadow-opacity: 0.88; - --mirror-bird-shadow-shift-x: -48%; - --mirror-bird-shadow-width: 11rem; - --mirror-bird-shell-opacity: 0.92; - --mirror-bird-shell-rotate: 11deg; - --mirror-bird-shell-scale-x: 0.95; - --mirror-bird-shell-scale-y: 0.9; - --mirror-bird-shell-shift-y: 0.3rem; -} - -.mirror-page__decor-bird--bottom-left { - bottom: 4.6%; - left: 3.8%; -} - -.mirror-page__decor-bird[data-role="tinkerer"] { - --mirror-bird-aura: linear-gradient(122deg, rgba(94, 164, 157, 0.38), rgba(255, 255, 255, 0) 52%), radial-gradient(circle at 66% 58%, rgba(245, 227, 177, 0.22), rgba(175, 223, 217, 0.12) 42%, transparent 76%); - --mirror-bird-aura-blur: 18px; - --mirror-bird-aura-inset: 18% 10% 18% 12%; - --mirror-bird-aura-opacity: 0.58; - --mirror-bird-aura-rotate: -14deg; - --mirror-bird-orbital-back-opacity: 0.8; - --mirror-bird-orbital-back-scale: 1.02; - --mirror-bird-orbital-back-shift-x: -48%; - --mirror-bird-orbital-back-shift-y: -46%; - --mirror-bird-orbital-front-opacity: 0.54; - --mirror-bird-orbital-front-scale: 0.9; - --mirror-bird-orbital-front-shift-x: -46%; - --mirror-bird-orbital-front-shift-y: -45%; - --mirror-bird-perch-bottom: 0.84rem; - --mirror-bird-perch-rotate: 16deg; - --mirror-bird-perch-shift-x: -34%; - --mirror-bird-perch-width: 9.4rem; - --mirror-bird-saturate: 1.04; - --mirror-bird-shadow-bottom: 1.7rem; - --mirror-bird-shadow-height: 3.1rem; - --mirror-bird-shadow-scale: 0.88; - --mirror-bird-shadow-shift-x: -36%; - --mirror-bird-shadow-width: 9.8rem; - --mirror-bird-shell-opacity: 0.9; - --mirror-bird-shell-rotate: 18deg; - --mirror-bird-shell-scale-x: 0.84; - --mirror-bird-shell-scale-y: 0.88; - --mirror-bird-shell-shift-x: 0.95rem; - --mirror-bird-shell-shift-y: 0.22rem; -} - -.mirror-page__decor-bird[data-role="lookout"] .shell-ball-mascot__perch { - box-shadow: 0 14px 24px -24px rgba(96, 129, 170, 0.34); -} - -.mirror-page__decor-bird[data-role="dozer"] .shell-ball-mascot__perch { - background: linear-gradient(180deg, rgba(255, 251, 246, 0.94), rgba(236, 221, 201, 0.94)); - box-shadow: 0 16px 28px -24px rgba(139, 107, 63, 0.34); -} - -.mirror-page__decor-bird[data-role="tinkerer"] .shell-ball-mascot__perch { - background: - linear-gradient(90deg, rgba(255, 249, 242, 0.92), rgba(227, 241, 239, 0.95) 42%, rgba(240, 227, 196, 0.94) 76%, rgba(255, 249, 242, 0.9)); - box-shadow: 0 14px 24px -20px rgba(82, 142, 137, 0.28); -} - -.mirror-page__decor-bird[data-role="tinkerer"] .mirror-page__decor-bird-shell::after { - background: - linear-gradient(104deg, rgba(242, 228, 191, 0), rgba(242, 228, 191, 0.76) 36%, rgba(121, 183, 174, 0.7) 64%, rgba(121, 183, 174, 0)), - radial-gradient(circle at 52% 52%, rgba(255, 247, 222, 0.74), rgba(255, 247, 222, 0)); - border-radius: 999px; - bottom: 1.5rem; - content: ""; - height: 1.6rem; - left: 2.8rem; - opacity: 0.62; - position: absolute; - transform: rotate(16deg); - width: 5.6rem; - z-index: 0; -} - -.mirror-page__decor-bird[data-role="tinkerer"] .shell-ball-mascot__shadow { - filter: blur(13px); -} - -.mirror-page__decor-bird[data-role="tinkerer"] .shell-ball-mascot__tail-shell { - left: calc(50% + 2.45rem); -} - -.mirror-page__decor-bird[data-role="tinkerer"] .shell-ball-mascot__wing-shell--left { - left: -0.75rem; -} - -.mirror-page__decor-bird[data-role="tinkerer"] .shell-ball-mascot__wing-shell--right { - right: -1.25rem; - top: 4rem; -} - -.mirror-page__decor-bird[data-role="tinkerer"] .shell-ball-mascot__body { - border-radius: 46% 50% 44% 50% / 40% 42% 56% 58%; -} - -.mirror-page__decor-bird[data-role="tinkerer"] .shell-ball-mascot__face { - top: 2.78rem; -} - -.mirror-page__decor-bird[data-role="tinkerer"] .shell-ball-mascot__crest { - left: 48%; -} - -.mirror-page :where(span.inline-flex.rounded-full.px-3.py-1.text-xs.font-medium) { - background: rgba(255, 251, 244, 0.82); - border: 1px solid rgba(151, 122, 90, 0.18); - color: rgba(74, 58, 42, 0.76); - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.mirror-page__draggable { - --mirror-card-accent: rgba(160, 132, 100, 0.18); - --mirror-card-glow: rgba(172, 141, 108, 0.18); - --mirror-card-rotate: 0deg; - --mirror-card-lift: 0px; - cursor: grab; - opacity: 0; - position: absolute; - touch-action: none; - transition: opacity 180ms ease; - user-select: none; - will-change: transform; - z-index: 3; -} - -.mirror-page__draggable--memory { - --mirror-card-rotate: -1.2deg; - z-index: 5; -} - -.mirror-page__draggable--dailyStage { - --mirror-card-rotate: -4.2deg; -} - -.mirror-page__draggable--profile { - --mirror-card-rotate: 4.6deg; -} - -.mirror-page__draggable--history { - --mirror-card-rotate: 7.1deg; -} - -.mirror-page__draggable--pinned { - cursor: pointer; - touch-action: auto; -} - -.mirror-page__draggable.is-ready { - opacity: 1; -} - -.mirror-page__draggable.is-dragging { - cursor: grabbing; - z-index: 8; -} - -.mirror-page__draggable[data-accent="warm"] { - --mirror-card-accent: rgba(188, 145, 85, 0.24); - --mirror-card-glow: rgba(191, 147, 86, 0.24); -} - -.mirror-page__draggable[data-accent="sky"] { - --mirror-card-accent: rgba(128, 155, 183, 0.24); - --mirror-card-glow: rgba(150, 181, 211, 0.28); -} - -.mirror-page__draggable[data-accent="sage"] { - --mirror-card-accent: rgba(114, 144, 116, 0.24); - --mirror-card-glow: rgba(128, 156, 128, 0.24); -} - -.mirror-page__draggable[data-accent="rose"] { - --mirror-card-accent: rgba(180, 136, 126, 0.24); - --mirror-card-glow: rgba(201, 155, 145, 0.24); -} - -.mirror-page__card-surface, -.mirror-page__detail-panel > section { - backdrop-filter: blur(18px); - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; - overflow: hidden; - position: relative; -} - -.mirror-page__card-surface { - border: 1px solid rgba(143, 111, 77, 0.2); - box-shadow: - 0 28px 58px -44px rgba(97, 69, 42, 0.5), - 0 10px 22px -18px rgba(97, 69, 42, 0.24); - padding: 1rem 1.08rem 1rem; - transform: translateY(var(--mirror-card-lift)) rotate(var(--mirror-card-rotate)); - transform-origin: 52% 14%; - transition: - transform 180ms ease, - box-shadow 180ms ease, - border-color 180ms ease, - background 180ms ease; -} - -.mirror-page__card-surface--master { - background: - linear-gradient(180deg, rgba(255, 253, 249, 0.98), rgba(250, 244, 235, 0.94)), - var(--mirror-paper); - border-radius: 1.6rem 1.6rem 1.95rem 1.6rem; - box-shadow: - 0 36px 74px -56px rgba(75, 53, 31, 0.56), - 0 18px 28px -20px rgba(75, 53, 31, 0.28), - 0 0 0 1px rgba(255, 255, 255, 0.74) inset; -} - -.mirror-page__card-surface--slide { - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.52), rgba(252, 247, 238, 0.32)), - var(--mirror-film); - border-radius: 1.35rem; - box-shadow: - 0 24px 56px -44px rgba(84, 59, 35, 0.42), - 0 12px 24px -18px rgba(84, 59, 35, 0.22), - 0 0 0 1px rgba(255, 255, 255, 0.44) inset; -} - -.mirror-page__card-surface::before { - background: - linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.58), transparent), - linear-gradient(180deg, rgba(133, 100, 69, 0.08), transparent 42%); - border-radius: inherit; - opacity: 0.82; -} - -.mirror-page__card-surface::after { - border-radius: inherit; - box-shadow: 0 0 0 1px var(--mirror-card-accent) inset; -} - -.mirror-page__card-surface--master::after { - background: - linear-gradient(rgba(133, 100, 69, 0.08) 1px, transparent 1px), - linear-gradient(90deg, rgba(133, 100, 69, 0.05) 1px, transparent 1px); - background-position: 0 1.2rem, 1rem 0; - background-size: 100% 1.55rem, 2.8rem 100%; - mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.48), transparent 56%); -} - -.mirror-page__card-surface--slide::after { - background: - linear-gradient(135deg, rgba(255, 255, 255, 0.36), transparent 44%), - linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0)); -} - -.mirror-page__master-clip, -.mirror-page__slide-tab { - left: 50%; - position: absolute; - top: 0; - transform: translate(-50%, -46%); - z-index: 3; -} - -.mirror-page__master-clip { - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(214, 200, 183, 0.98)), - rgba(224, 209, 191, 0.96); - border: 1px solid rgba(132, 103, 72, 0.24); - border-radius: 0.9rem; - box-shadow: - 0 8px 16px -10px rgba(88, 65, 43, 0.34), - 0 0 0 1px rgba(255, 255, 255, 0.5) inset; - height: 1.1rem; - width: 5.3rem; -} - -.mirror-page__slide-tab { - background: linear-gradient(180deg, rgba(255, 251, 238, 0.74), rgba(242, 225, 196, 0.68)); - border: 1px solid rgba(151, 120, 88, 0.18); - border-radius: 999px; - box-shadow: 0 8px 14px -10px rgba(85, 59, 33, 0.28); - height: 0.62rem; - width: 4.2rem; -} - -.mirror-page__surface-registers { - inset: 0; - pointer-events: none; - position: absolute; - z-index: 2; -} - -.mirror-page__surface-register { - border-color: rgba(132, 102, 72, 0.34); - height: 0.95rem; - position: absolute; - width: 0.95rem; -} - -.mirror-page__surface-register--top-left { - border-left: 1px solid currentColor; - border-top: 1px solid currentColor; - color: rgba(132, 102, 72, 0.34); - left: 0.8rem; - top: 0.8rem; -} - -.mirror-page__surface-register--top-right { - border-right: 1px solid currentColor; - border-top: 1px solid currentColor; - color: rgba(132, 102, 72, 0.34); - right: 0.8rem; - top: 0.8rem; -} - -.mirror-page__surface-register--bottom-left { - border-bottom: 1px solid currentColor; - border-left: 1px solid currentColor; - bottom: 0.8rem; - color: rgba(132, 102, 72, 0.34); - left: 0.8rem; -} - -.mirror-page__surface-register--bottom-right { - border-bottom: 1px solid currentColor; - border-right: 1px solid currentColor; - bottom: 0.8rem; - color: rgba(132, 102, 72, 0.34); - right: 0.8rem; -} - -.mirror-page__draggable:hover > .mirror-page__card-surface, -.mirror-page__draggable.is-active > .mirror-page__card-surface, -.mirror-page__draggable.is-dragging > .mirror-page__card-surface { - --mirror-card-lift: -3px; - border-color: rgba(143, 111, 77, 0.34); - box-shadow: - 0 0 0 1px rgba(255, 255, 255, 0.68) inset, - 0 30px 72px -48px var(--mirror-card-glow), - 0 16px 28px -18px rgba(97, 69, 42, 0.28); -} - -.mirror-page__draggable.is-dragging > .mirror-page__card-surface { - --mirror-card-lift: -6px; - box-shadow: - 0 0 0 1px rgba(255, 255, 255, 0.74) inset, - 0 36px 76px -46px var(--mirror-card-glow), - 0 22px 34px -18px rgba(97, 69, 42, 0.3); -} - -.mirror-page__draggable--pinned .mirror-page__card-surface { - padding: 1.5rem 1.55rem 1.35rem; -} - -.mirror-page__draggable--pinned .mirror-page__card-shell { - gap: 0.95rem; -} - -.mirror-page__draggable--pinned .mirror-page__card-title { - font-size: 1.1rem; -} - -.mirror-page__draggable--pinned .mirror-page__card-line { - font-size: clamp(1.28rem, 1.82vw, 1.62rem); - line-height: 1.24; - -webkit-line-clamp: 3; -} - -.mirror-page__draggable--pinned .mirror-page__card-line--memory { - font-size: clamp(1rem, 1.3vw, 1.18rem); -} - -.mirror-page__draggable--pinned .mirror-page__card-detail { - -webkit-line-clamp: 6; - font-size: 0.92rem; -} - -.mirror-page__card-shell { - display: grid; - gap: 0.72rem; - grid-template-rows: auto auto minmax(0, 1fr) auto; - height: 100%; - position: relative; - z-index: 1; -} - -.mirror-page__card-shell > *, -.mirror-page__detail-panel > section > * { - min-width: 0; -} - -.mirror-page__card-top, -.mirror-page__memory-header, -.mirror-page__memory-title-row, -.mirror-page__profile-heading, -.mirror-page__detail-topbar { - align-items: center; - display: flex; - gap: var(--mirror-space-2); -} - -.mirror-page__card-top, -.mirror-page__memory-header, -.mirror-page__detail-topbar { - justify-content: space-between; -} - -.mirror-page__card-heading, -.mirror-page__history-copy, -.mirror-page__memory-meta, -.mirror-page__detail-note-shell, -.mirror-page__card-top-meta, -.mirror-page__detail-meta { - display: grid; - gap: 0.22rem; -} - -.mirror-page__card-top-meta { - justify-items: end; -} - -.mirror-page__card-shell :where(span.inline-flex.rounded-full.px-3.py-1.text-xs.font-medium) { - font-size: 0.64rem; - letter-spacing: 0.14em; - line-height: 1; - padding: 0.38rem 0.68rem; - width: max-content; -} - -.mirror-page__card-kicker, -.mirror-page__surface-code, -.mirror-page__micro-label, -.mirror-page__memory-index, -.mirror-page__history-label, -.mirror-page__module-hint { - color: var(--mirror-muted); -} - -.mirror-page__card-title, -.mirror-page__memory-title, -.mirror-page__card-line, -.mirror-page__date-value, -.mirror-page__hours-value, -.mirror-page__summary-value, -.mirror-page__detail-panel > section > div:first-child > h2, -.mirror-page__stage-headline { - color: var(--mirror-ink); - font-family: var(--mirror-font-display); - letter-spacing: -0.04em; - margin: 0; -} - -.mirror-page__card-title, -.mirror-page__memory-title { - font-size: 0.98rem; - font-weight: 600; - line-height: 1.34; -} - -.mirror-page__card-line { - display: -webkit-box; - font-size: clamp(0.98rem, 1.34vw, 1.22rem); - font-weight: 700; - line-height: 1.28; - overflow: hidden; - overflow-wrap: anywhere; - padding-bottom: 0.12em; - text-wrap: pretty; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; -} - -.mirror-page__card-line--memory { - font-size: clamp(0.92rem, 1.08vw, 1.02rem); - word-break: break-word; -} - -.mirror-page__card-detail, -.mirror-page__history-text, -.mirror-page__profile-copy, -.mirror-page__memory-reason, -.mirror-page__memory-summary, -.mirror-page__note, -.mirror-page__summary-copy, -.mirror-page__empty-state { - color: var(--mirror-copy); - line-height: 1.7; - margin: 0; -} - -.mirror-page__card-detail { - display: -webkit-box; - font-size: 0.8rem; - overflow: hidden; - overflow-wrap: anywhere; - padding-bottom: 0.06rem; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; -} - -.mirror-page__module-hint { - border-top: 1px solid rgba(132, 102, 72, 0.16); - margin-top: auto; - padding-top: 0.78rem; -} - -.mirror-page__profile-icon, -.mirror-page__memory-icon, -.mirror-page__close-icon, -.mirror-page__date-icon { - color: var(--mirror-accent); - flex: none; - height: 1.05rem; - width: 1.05rem; -} - -.mirror-page__detail-layer { - align-items: center; - background: rgba(76, 54, 34, 0.12); - backdrop-filter: blur(12px); - display: flex; - inset: 0; - justify-content: center; - padding: var(--mirror-space-4); - position: absolute; - z-index: 20; -} - -.mirror-page__detail-panel { - height: min(42rem, 100%); - max-width: 56rem; - width: min(56rem, 100%); -} - -.mirror-page__detail-panel > section { - background: - linear-gradient(180deg, rgba(255, 253, 249, 0.98), rgba(248, 241, 232, 0.94)), - var(--mirror-paper-strong); - border: 1px solid rgba(143, 111, 77, 0.2); - border-radius: 1.85rem; - box-shadow: - 0 30px 80px -48px rgba(76, 54, 34, 0.42), - 0 0 0 1px rgba(255, 255, 255, 0.7) inset; - padding: clamp(1.25rem, 2vw, 1.7rem); -} - -.mirror-page__detail-panel > section::before { - background: - linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent), - radial-gradient(circle at 12% 0%, rgba(255, 255, 255, 0.7), transparent 22%); - border-radius: inherit; -} - -.mirror-page__detail-panel > section::after { - border-radius: inherit; - box-shadow: 0 0 0 1px rgba(151, 122, 90, 0.14) inset; -} - -.mirror-page__detail-panel > section > .cc-panel-surface__header { - margin-bottom: 0.75rem; - padding-bottom: 0.7rem; - position: relative; -} - -.mirror-page__detail-panel > section > .cc-panel-surface__header::after { - background: linear-gradient(90deg, rgba(143, 111, 77, 0.18), rgba(143, 111, 77, 0)); - bottom: 0; - content: ""; - height: 1px; - left: 0; - position: absolute; - right: 0; -} - -.mirror-page__detail-panel > section > .cc-panel-surface__header .cc-panel-surface__eyebrow { - color: rgba(92, 72, 53, 0.5); - margin-bottom: 0.18rem; -} - -.mirror-page__detail-panel > section > .cc-panel-surface__header .cc-panel-surface__title-row { - align-items: center; - display: flex; - flex-wrap: wrap; - gap: var(--mirror-space-2); - justify-content: space-between; -} - -.mirror-page__detail-panel > section > .cc-panel-surface__header .cc-panel-surface__title-accessory { - display: flex; - justify-content: flex-end; - max-width: 100%; -} - -.mirror-page__detail-panel > section > .cc-panel-surface__header .cc-panel-surface__title { - font-size: clamp(1.38rem, 1.7vw, 1.72rem); -} - -.mirror-page__close-button { - align-items: center; - background: rgba(255, 251, 244, 0.82); - border: 1px solid rgba(143, 111, 77, 0.2); - border-radius: 999px; - color: var(--mirror-accent); - cursor: pointer; - display: inline-flex; - height: 2rem; - justify-content: center; - transition: background 160ms ease, transform 160ms ease, box-shadow 160ms ease; - width: 2rem; -} - -.mirror-page__close-button:hover { - background: rgba(255, 255, 255, 0.94); - transform: translateY(-1px); -} - -.mirror-page__history-list, -.mirror-page__daily-stack, -.mirror-page__profile-list, -.mirror-page__detail-body, -.mirror-page__memory-list { - display: grid; - gap: var(--mirror-space-3); -} - -.mirror-page__detail-tabs, -.mirror-page__detail-tab-panel, -.mirror-page__conversation-filter-shell, -.mirror-page__conversation-filter-bar, -.mirror-page__conversation-days, -.mirror-page__conversation-records, -.mirror-page__profile-grid, -.mirror-page__profile-archive-list, -.mirror-page__risk-list, -.mirror-page__stage-grid, -.mirror-page__stage-task-list { - display: grid; - gap: var(--mirror-space-3); -} - -.mirror-page__detail-topbar { - margin-bottom: 0.1rem; -} - -.mirror-page__detail-meta { - gap: 0; -} - -.mirror-page__detail-tab-list { - align-self: start; - background: rgba(255, 251, 244, 0.76); - border: 1px solid rgba(143, 111, 77, 0.12); - border-radius: 999px; - display: inline-flex; - flex-wrap: wrap; - padding: 0.26rem; - width: max-content; -} - -.mirror-page__detail-tab-list--title { - margin-left: auto; - max-width: 100%; -} - -.mirror-page__detail-tab-trigger { - background: transparent; - border: 0; - border-radius: 999px; - color: var(--mirror-muted); - cursor: pointer; - font: inherit; - min-height: 2rem; - padding-inline: 0.95rem; - white-space: nowrap; -} - -.mirror-page__detail-tab-trigger[data-active] { - background: rgba(255, 255, 255, 0.9); - color: var(--mirror-ink); -} - -.mirror-page__detail-tab-trigger:focus-visible { - outline: 2px solid var(--mirror-focus); - outline-offset: 3px; -} - -.mirror-page__detail-tab-panel { - min-height: 0; -} - -.mirror-page__history-summary-grid, -.mirror-page__summary-grid, -.mirror-page__stage-grid, -.mirror-page__profile-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.mirror-page__continuity-card, -.mirror-page__stage-card, -.mirror-page__risk-card, -.mirror-page__profile-item, -.mirror-page__conversation-record { - background: rgba(255, 255, 255, 0.42); - border: 1px solid rgba(143, 111, 77, 0.12); - border-radius: 1.2rem; - box-shadow: 0 18px 34px -30px rgba(97, 69, 42, 0.18); - padding: 1rem; -} - -.mirror-page__stage-card-top, -.mirror-page__conversation-meta, -.mirror-page__conversation-day-header, -.mirror-page__profile-actions, -.mirror-page__profile-badges, -.mirror-page__profile-local-note { - align-items: center; - display: flex; - gap: var(--mirror-space-2); -} - -.mirror-page__stage-card-top, -.mirror-page__conversation-meta, -.mirror-page__conversation-day-header { - justify-content: space-between; -} - -.mirror-page__profile-local-note { - background: rgba(255, 250, 242, 0.76); - border: 1px dashed rgba(143, 111, 77, 0.28); - border-radius: var(--mirror-radius-sm); - margin-bottom: 0.25rem; - padding: 0.9rem 1rem; -} - -.mirror-page__settings-feedback { - background: rgba(243, 250, 245, 0.86); - border-style: solid; - border-color: rgba(110, 148, 118, 0.24); -} - -.mirror-page__settings-controls { - align-items: center; - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - margin-top: 0.85rem; -} - -.mirror-page__settings-segmented { - flex-wrap: wrap; - max-width: 100%; -} - -.mirror-page__conversation-day { - display: grid; - gap: var(--mirror-space-2); -} - -.mirror-page__conversation-records, -.mirror-page__profile-archive-list { - gap: var(--mirror-space-2); -} - -.mirror-page__conversation-scroll, -.mirror-page__profile-archive-scroll { - max-height: 26rem; -} - -.mirror-page__conversation-meta-copy { - display: grid; - gap: 0.18rem; -} - -.mirror-page__conversation-filter-bar, -.mirror-page__conversation-actions, -.mirror-page__stage-task-actions { - align-items: center; - display: flex; - flex-wrap: wrap; - gap: 0.45rem; -} - -.mirror-page__conversation-date-shell, -.mirror-page__conversation-date-control, -.mirror-page__conversation-date-meta { - display: grid; - gap: 0.4rem; -} - -.mirror-page__conversation-filter-shell { - gap: 0.7rem; -} - -.mirror-page__conversation-date-shell { - align-items: end; - grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); -} - -.mirror-page__conversation-date-control { - gap: 0.28rem; - max-width: none; -} - -.mirror-page__conversation-date-meta { - align-items: center; - display: flex; - flex-wrap: wrap; - min-width: 0; -} - -.mirror-page__conversation-date-meta .mirror-page__summary-copy { - flex: 1 1 18rem; -} - -.mirror-page__conversation-date-input { - background: rgba(255, 250, 242, 0.84); - border: 1px solid rgba(143, 111, 77, 0.14); - border-radius: 0.95rem; - color: var(--mirror-ink); - font: inherit; - min-height: 2.2rem; - padding: 0.42rem 0.76rem; -} - -.mirror-page__conversation-filter { - background: rgba(255, 250, 242, 0.84); - border: 1px solid rgba(143, 111, 77, 0.14); - border-radius: 999px; - color: var(--mirror-copy); - cursor: pointer; - font: inherit; - line-height: 1.2; - padding: 0.3rem 0.72rem; - transition: background-color 160ms ease, border-color 160ms ease, transform 160ms ease; -} - -.mirror-page__conversation-filter.is-active, -.mirror-page__conversation-filter:hover { - background: rgba(255, 255, 255, 0.92); - border-color: rgba(143, 111, 77, 0.24); - color: var(--mirror-ink); - transform: translateY(-1px); -} - -.mirror-page__task-link { - background: rgba(255, 250, 242, 0.84); - border: 1px solid rgba(143, 111, 77, 0.14); - border-radius: 999px; - color: var(--mirror-copy); - cursor: pointer; - font: inherit; - line-height: 1.2; - padding: 0.3rem 0.72rem; - width: max-content; -} - -.mirror-page__task-link:hover { - background: rgba(255, 255, 255, 0.92); - border-color: rgba(143, 111, 77, 0.24); - color: var(--mirror-ink); -} - -.mirror-page__conversation-filter:focus-visible, -.mirror-page__task-link:focus-visible, -.mirror-page__conversation-date-input:focus-visible { - outline: 2px solid var(--mirror-focus); - outline-offset: 4px; -} - -.mirror-page__conversation-bubble { - border-radius: 1rem; - display: grid; - gap: 0.42rem; - margin-top: 0.85rem; - padding: 0.9rem 1rem; -} - -.mirror-page__conversation-bubble--user { - background: rgba(255, 251, 244, 0.88); - border: 1px solid rgba(143, 111, 77, 0.1); -} - -.mirror-page__conversation-bubble--agent { - background: rgba(244, 250, 246, 0.84); - border: 1px solid rgba(110, 148, 118, 0.14); -} - -.mirror-page__conversation-bubble--failed { - background: rgba(255, 244, 240, 0.82); - border: 1px solid rgba(181, 116, 102, 0.16); -} - -.mirror-page__stage-task-list { - gap: 0.7rem; - margin-top: 0.3rem; -} - -.mirror-page__stage-task { - align-items: center; - background: rgba(255, 253, 249, 0.74); - border: 1px solid rgba(143, 111, 77, 0.08); - border-radius: 1rem; - display: flex; - gap: var(--mirror-space-2); - justify-content: space-between; - padding: 0.78rem 0.85rem; -} - -.mirror-page__profile-item { - display: grid; - gap: var(--mirror-space-2); -} - -.mirror-page__profile-item--archived { - background: rgba(252, 247, 240, 0.78); -} - -.mirror-page__profile-actions { - flex-wrap: wrap; -} - -.mirror-page__action-button { - border-color: rgba(143, 111, 77, 0.14) !important; - color: var(--mirror-copy) !important; -} - -.mirror-page__profile-edit-shell { - display: grid; - gap: 0.7rem; -} - -.mirror-page__profile-edit-input { - background: rgba(255, 255, 255, 0.84); - border: 1px solid rgba(143, 111, 77, 0.16); - border-radius: 1rem; - color: var(--mirror-ink); - font: inherit; - line-height: 1.6; - min-height: 5.5rem; - padding: 0.85rem 0.95rem; - resize: vertical; -} - -.mirror-page__profile-edit-input:focus-visible { - outline: 2px solid var(--mirror-focus); - outline-offset: 4px; -} - -.mirror-page__history-list, -.mirror-page__detail-body { - min-height: 0; - overflow: auto; - padding-right: 0.25rem; -} - -.mirror-page__history-item, -.mirror-page__summary-card, -.mirror-page__date-card, -.mirror-page__profile-card, -.mirror-page__memory-card, -.mirror-page__detail-note-shell { - border-top: 1px solid var(--mirror-line); - padding-top: 1rem; -} - -.mirror-page__history-item { - align-items: start; - display: grid; - gap: var(--mirror-space-3); - grid-template-columns: auto minmax(0, 1fr); -} - -.mirror-page__history-index { - color: var(--mirror-accent); - display: block; - font-size: 0.72rem; - padding-top: 0.2rem; -} - -.mirror-page__profile-list--expanded, -.mirror-page__daily-stack--expanded, -.mirror-page__memory-list--expanded { - gap: var(--mirror-space-4); -} - -.mirror-page__date-card { - align-items: center; - border-bottom: 1px solid var(--mirror-line); - display: flex; - gap: 1rem; - padding: 0 0 1rem; -} - -.mirror-page__date-value, -.mirror-page__hours-value { - font-size: 1.38rem; - margin-top: 0.3rem; -} - -.mirror-page__summary-grid { - display: grid; - gap: var(--mirror-space-3); - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.mirror-page__summary-card--accent, -.mirror-page__profile-card--accent { - border-top-color: rgba(189, 146, 89, 0.3); -} - -.mirror-page__summary-value { - font-size: 1.9rem; - line-height: 1; - margin-top: 0.55rem; -} - -.mirror-page__note, -.mirror-page__detail-note-shell { - border-left: 1px solid var(--mirror-line-strong); - padding-left: 1rem; -} - -.mirror-page__detail-note-shell .mirror-page__note { - border-left: 0; - padding-left: 0; -} - -.mirror-page__stage-headline { - font-size: clamp(1.06rem, 1.35vw, 1.2rem); - font-weight: 700; - line-height: 1.35; -} - -.mirror-page__memory-card { - gap: 0.9rem; -} - -.mirror-page__memory-card.is-active { - border-top-color: rgba(110, 148, 118, 0.34); -} - -.mirror-page__memory-summary { - background: rgba(255, 255, 255, 0.46); - border: 1px solid rgba(143, 111, 77, 0.12); - border-radius: var(--mirror-radius-sm); - padding: 0.95rem 1rem; -} - -.mirror-page__memory-card.is-active .mirror-page__memory-summary { - background: rgba(244, 250, 246, 0.92); - border-color: rgba(110, 148, 118, 0.24); -} - -.mirror-page__close-button:focus-visible, -.mirror-page__draggable:focus-visible { - box-shadow: 0 0 0 4px rgba(255, 247, 234, 0.9); - outline: 2px solid var(--mirror-focus); - outline-offset: 5px; -} - -@keyframes mirror-field-breathe { - 0%, - 100% { - box-shadow: - 0 0 0 1px rgba(255, 255, 255, 0.84) inset, - 0 32px 68px -52px rgba(115, 80, 48, 0.48), - 0 0 88px rgba(255, 239, 210, 0.36); - } - - 50% { - box-shadow: - 0 0 0 1px rgba(255, 255, 255, 0.9) inset, - 0 34px 72px -50px rgba(115, 80, 48, 0.5), - 0 0 106px rgba(255, 242, 216, 0.48); - } -} - -@media (max-width: 1100px) { - .mirror-page__inspection-field { - height: min(31rem, 52vw); - width: min(50rem, 78vw); - } - - .mirror-page__decor-bird .shell-ball-mascot { - --shell-ball-size: 10rem; - } -} - -@media (max-width: 860px) { - .mirror-page__inspection-field { - border-radius: 1.9rem; - height: min(27rem, 58vw); - width: min(44rem, 86vw); - } - - .mirror-page__decor-bird--top-right { - right: 2.2%; - top: 2.8%; - } - - .mirror-page__decor-bird--bottom-right { - bottom: 1.8%; - right: 2.2%; - } - - .mirror-page__decor-bird--bottom-left { - bottom: 2.6%; - left: 1.6%; - } - - .mirror-page__decor-bird .shell-ball-mascot { - --shell-ball-size: 8.8rem; - } -} - -@media (max-width: 760px) { - .mirror-page__source-status { - left: 0.85rem; - right: 0.85rem; - top: 0.85rem; - } - - .mirror-page__source-copy { - display: none; - } - - .mirror-page__detail-layer { - padding: 1rem; - } - - .mirror-page__summary-grid { - grid-template-columns: 1fr; - } - - .mirror-page__conversation-date-shell { - grid-template-columns: 1fr; - } - - .mirror-page__history-summary-grid, - .mirror-page__stage-grid, - .mirror-page__profile-grid { - grid-template-columns: 1fr; - } - - .mirror-page__draggable--pinned .mirror-page__card-surface { - padding: 1.2rem 1.2rem 1.15rem; - } - - .mirror-page__draggable--pinned .mirror-page__card-line { - font-size: clamp(1.12rem, 3.8vw, 1.36rem); - } - - .mirror-page__draggable--pinned .mirror-page__card-detail { - -webkit-line-clamp: 5; - font-size: 0.84rem; - } - - .mirror-page__decor-bird { - display: none; - } -} - -@media (prefers-reduced-motion: reduce) { - .mirror-page__inspection-field { - animation: none; - } - - .mirror-page__decor-bird-shell { - filter: saturate(0.92) drop-shadow(0 24px 36px rgba(111, 84, 54, 0.12)); - } - - .mirror-page__card-surface, - .mirror-page__draggable, - .mirror-page__close-button, - .mirror-page__decor-bird .shell-ball-mascot * { - transition: none; - } -} diff --git a/apps/desktop/src/features/dashboard/memory/mockMirrorMemory.css b/apps/desktop/src/features/dashboard/memory/mockMirrorMemory.css new file mode 100644 index 000000000..cf25cd23c --- /dev/null +++ b/apps/desktop/src/features/dashboard/memory/mockMirrorMemory.css @@ -0,0 +1,786 @@ +.mirror-memory-page { + --memory-header-offset: clamp(88px, 11vh, 118px); + --memory-bottom-offset: clamp(24px, 4vh, 36px); + --memory-right-offset: clamp(28px, 3.6vw, 52px); + --memory-left-offset: clamp(320px, 26vw, 470px); + --memory-session-max-height: calc(100vh - var(--memory-header-offset) - var(--memory-bottom-offset)); + --memory-stack-card-height: 400px; + --memory-left-column-gap: clamp(16px, 1.8vw, 22px); + --memory-spotlight-height: calc(var(--memory-stack-card-height) * 2 + var(--memory-left-column-gap)); + min-height: 100vh; + height: 100vh; + overflow: hidden; + position: relative; + background-color: #fbf3e4; + background-position: center center; + background-repeat: no-repeat; + background-size: cover; +} + +.mirror-memory-page::before { + content: ""; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 23% 42%, rgba(255, 255, 255, 0.54), transparent 26%), + linear-gradient(180deg, rgba(255, 251, 246, 0.2), rgba(248, 232, 205, 0.2)); + pointer-events: none; +} + +.mirror-memory-layout { + box-sizing: border-box; + position: relative; + z-index: 1; + display: grid; + grid-template-columns: minmax(360px, 460px) minmax(420px, 560px); + gap: clamp(18px, 2vw, 28px); + align-items: start; + justify-content: end; + align-content: start; + max-height: var(--memory-session-max-height); + min-height: 0; + padding: var(--memory-header-offset) var(--memory-right-offset) var(--memory-bottom-offset) var(--memory-left-offset); +} + +.mirror-memory-left-column, +.mirror-memory-card--spotlight { + min-height: 0; + max-height: calc(var(--memory-session-max-height) - 2px); +} + +.mirror-memory-left-column { + display: flex; + flex-direction: column; + gap: var(--memory-left-column-gap); + height: var(--memory-spotlight-height); +} + +.mirror-memory-card, +.mirror-memory-detail-card { + display: flex; + flex-direction: column; + min-height: fit-content; + max-height: 100%; + border-radius: 26px; + border: 1px solid rgba(255, 255, 255, 0.66); + background: rgba(255, 255, 255, 0.72); + box-shadow: 0 24px 60px -40px rgba(141, 109, 60, 0.34); + backdrop-filter: blur(22px); + -webkit-backdrop-filter: blur(22px); +} + +.mirror-memory-card { + padding: 22px; +} + +.mirror-memory-card--recent, +.mirror-memory-card--history { + height: var(--memory-stack-card-height); + min-height: var(--memory-stack-card-height); + max-height: var(--memory-stack-card-height); +} + +.mirror-memory-card--spotlight { + width: min(560px, 100%); + height: var(--memory-spotlight-height); + min-height: var(--memory-spotlight-height); + max-height: var(--memory-spotlight-height); + padding: 20px; + overflow: hidden; +} + +.mirror-memory-card-header { + display: flex; + flex-direction: column; + gap: 0.45rem; + margin-bottom: 14px; + flex: none; +} + +.mirror-memory-card-header--split { + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.mirror-memory-card-header h2, +.mirror-memory-detail-header h1 { + margin: 0; + color: #594328; + font-family: var(--cc-font-display); + font-size: clamp(1.08rem, 1.3vw, 1.42rem); + font-weight: 600; + letter-spacing: 0.01em; +} + +.mirror-memory-card-header p, +.mirror-memory-detail-header p, +.mirror-memory-detail-section p, +.mirror-memory-empty-state, +.mirror-memory-section-copy { + margin: 0; + color: rgba(90, 70, 42, 0.76); + font-size: 0.92rem; + line-height: 1.62; +} + +.mirror-memory-eyebrow { + color: rgba(153, 118, 64, 0.92); + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.mirror-memory-pill { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 32px; + padding: 0 12px; + border-radius: 999px; + background: rgba(240, 204, 131, 0.26); + color: #8a6736; + font-size: 0.8rem; + font-weight: 600; + flex: none; +} + +.mirror-memory-scroll-shell { + min-height: 0; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.mirror-memory-scroll-shell::-webkit-scrollbar { + width: 0; + height: 0; +} + +.mirror-memory-scroll-shell--history { + flex: 1; +} + +.mirror-memory-list-link, +.mirror-memory-session-card { + width: 100%; + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.52); + background: linear-gradient(145deg, rgba(255, 255, 255, 0.54), rgba(255, 248, 239, 0.78)); + box-sizing: border-box; +} + +.mirror-memory-list-link { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: center; + padding: 14px 16px; + appearance: none; + cursor: pointer; + text-align: left; +} + +.mirror-memory-list-copy { + display: flex; + flex-direction: column; + gap: 4px; +} + +.mirror-memory-list-title, +.mirror-memory-session-title { + color: #533d23; + font-size: 1rem; + font-weight: 600; +} + +.mirror-memory-list-meta, +.mirror-memory-session-meta, +.mirror-memory-date-label { + color: rgba(112, 86, 53, 0.68); + font-size: 0.82rem; +} + +.mirror-memory-list-side { + display: flex; + align-items: center; + gap: 10px; + color: #8f7246; + font-size: 0.82rem; +} + +.mirror-memory-list-side--stacked { + flex-direction: column; + align-items: flex-end; +} + +.mirror-memory-list-arrow, +.mirror-memory-action-icon, +.mirror-memory-note-icon { + width: 16px; + height: 16px; +} + +.mirror-memory-date-label { + margin-bottom: 6px; +} + +.mirror-memory-date-picker-shell { + position: relative; + z-index: 2; + margin-bottom: 12px; +} +.mirror-memory-session-card { + display: flex; + flex-direction: column; + gap: 12px; + padding: 14px 16px; +} + +.mirror-memory-session-top { + display: flex; + gap: 16px; + justify-content: space-between; + align-items: flex-start; +} + +.mirror-memory-inline-button { + flex: none; + min-height: 38px; + padding: 0 14px; + border: 1px solid rgba(214, 183, 132, 0.48); + border-radius: 999px; + background: rgba(255, 251, 244, 0.84); + color: #684e2d; + cursor: pointer; +} + +.mirror-memory-date-picker-button { + width: 100%; + min-height: 42px; + border-radius: 14px; + border-color: rgba(214, 183, 132, 0.48); + background: rgba(255, 251, 244, 0.84); + color: #684e2d; +} + +.mirror-memory-date-picker-popover { + width: 260px; + max-width: 280px; + margin-bottom: 12px; + padding: 12px; + overflow: hidden; + border-radius: 22px; + border: 1px solid rgba(255, 255, 255, 0.7); + background: rgba(255, 250, 242, 0.8); + box-shadow: 0 20px 50px rgba(120, 84, 38, 0.12); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); +} + +.mirror-memory-calendar { + width: 100%; +} + +.mirror-memory-calendar .rdp-root { + width: 100%; +} + +.mirror-memory-calendar .rdp-months, +.mirror-memory-calendar .rdp-month, +.mirror-memory-calendar .rdp-month_grid { + width: 100%; +} + +.mirror-memory-calendar .rdp-month_caption { + margin-bottom: 8px; +} + +.mirror-memory-calendar .rdp-nav { + gap: 6px; +} + +.mirror-memory-calendar .rdp-weekdays { + margin-bottom: 4px; +} + +.mirror-memory-calendar .rdp-weekday { + font-size: 0.72rem; +} + +.mirror-memory-calendar .rdp-day_button { + transition: + background-color 140ms ease, + border-color 140ms ease, + color 140ms ease, + transform 140ms ease; +} + +.mirror-memory-calendar .mirror-memory-calendar-day--has-data .rdp-day_button::after, +.mirror-memory-calendar .mirror-memory-calendar-day--has-data button::after { + content: ""; + position: absolute; + left: 50%; + bottom: 4px; + width: 5px; + height: 5px; + border-radius: 999px; + transform: translateX(-50%); + background: rgba(210, 160, 70, 0.95); +} + +.mirror-memory-empty-state { + border-radius: 18px; + border: 1px dashed rgba(214, 183, 132, 0.52); + background: rgba(255, 249, 239, 0.56); + padding: 18px; +} + +.mirror-memory-main-tabs { + display: inline-flex; + align-self: flex-start; + gap: 8px; + padding: 6px; + margin-bottom: 14px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.6); + background: rgba(255, 250, 242, 0.8); + flex: none; +} + +.mirror-memory-tab-button { + border: 0; + border-radius: 999px; + background: transparent; + color: rgba(93, 74, 46, 0.66); + cursor: pointer; + font-size: 0.92rem; + font-weight: 500; + padding: 10px 18px; +} + +.mirror-memory-tab-button.is-active { + background: linear-gradient(145deg, rgba(240, 204, 131, 0.92), rgba(224, 185, 106, 0.88)); + color: #553e1d; + box-shadow: 0 14px 30px -24px rgba(192, 148, 80, 0.84); +} + +.mirror-memory-profile-panel, +.mirror-memory-period-panel { + display: flex; + flex: 1; + height: 100%; + min-height: 0; + max-height: 100%; + flex-direction: column; + gap: 12px; +} + +.mirror-memory-profile-top { + display: flex; + flex-direction: column; + gap: 12px; + min-height: 0; +} + +.mirror-memory-radar-wrap, +.mirror-memory-identity-card, +.mirror-memory-summary-card, +.mirror-memory-heatmap-card, +.mirror-memory-note-band, +.mirror-memory-detail-block, +.mirror-memory-detail-section { + border-radius: 22px; + border: 1px solid rgba(255, 255, 255, 0.54); + background: linear-gradient(155deg, rgba(255, 255, 255, 0.56), rgba(255, 248, 238, 0.82)); +} + +.mirror-memory-radar-wrap { + display: flex; + align-items: center; + justify-content: center; + padding: 20px 26px 18px; + min-height: 210px; + overflow: visible; +} + +.mirror-memory-radar { + width: 100%; + max-width: 210px; + height: auto; + overflow: visible; +} + +.mirror-memory-radar-grid, +.mirror-memory-radar-axis { + fill: rgba(213, 194, 232, 0.08); + stroke: rgba(172, 147, 210, 0.24); + stroke-width: 1; +} + +.mirror-memory-radar-shape--weak { + fill: rgba(238, 199, 118, 0.16); + stroke: rgba(205, 165, 88, 0.58); + stroke-width: 1.5; +} + +.mirror-memory-radar-point { + fill: rgba(195, 152, 80, 0.82); +} + +.mirror-memory-radar-label { + fill: rgba(109, 83, 48, 0.72); + font-size: 9px; +} + +.mirror-memory-profile-side { + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; +} + +.mirror-memory-identity-card { + padding: 16px 18px; +} + +.mirror-memory-identity-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} + +.mirror-memory-identity-card h2 { + margin: 0; + color: #563e22; + font-family: var(--cc-font-display); + font-size: 1.28rem; +} + +.mirror-memory-identity-card p, +.mirror-memory-section-copy { + margin: 0; + color: rgba(97, 75, 46, 0.68); + font-size: 0.9rem; +} + +.mirror-memory-metric-grid, +.mirror-memory-detail-grid { + display: grid; + gap: 12px; +} + +.mirror-memory-metric-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.mirror-memory-metric-item, +.mirror-memory-detail-block { + display: flex; + flex-direction: column; + gap: 6px; + min-height: 84px; + padding: 12px 14px; + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.46); + background: rgb(236 225 216 / 50%); +} + +.mirror-memory-metric-item strong, +.mirror-memory-detail-block strong, +.mirror-memory-detail-counter strong { + color: #604728; + font-size: 1rem; + font-weight: 600; +} + +.mirror-memory-note-band { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 18px; +} + +.mirror-memory-note-band p { + margin: 0; + color: #684f2c; + font-size: 0.92rem; +} + +.mirror-memory-summary-card { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + padding: 16px 18px; +} + +.mirror-memory-summary-block { + min-height: 0; +} + +.mirror-memory-summary-title { + color: #5d4325; + font-size: 0.94rem; + font-weight: 600; + margin-bottom: 8px; +} + +.mirror-memory-copy-list { + display: flex; + flex-direction: column; + gap: 10px; + margin: 0; + padding: 0; + list-style: none; +} + +.mirror-memory-copy-list li { + color: rgba(91, 70, 42, 0.82); + font-size: 0.92rem; + line-height: 1.62; +} + +.mirror-memory-copy-list--clean li::before { + content: none; +} + +.mirror-memory-heatmap-card { + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; +} + +.mirror-memory-heatmap-grid { + display: flex; + flex-direction: column; + gap: 10px; + padding: 16px 18px; + max-width: 500px; +} + +.mirror-memory-heatmap-label-row, +.mirror-memory-heatmap-cells { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 6px; +} + +.mirror-memory-heatmap-label-row span { + color: rgba(112, 86, 53, 0.62); + font-size: 0.78rem; + text-align: center; +} + +.mirror-memory-heatmap-cell { + width: 100%; + aspect-ratio: 1; + max-width: 35px; + justify-self: center; + border-radius: 7px; + border: 1px solid rgba(208, 208, 208, 0.55); +} + +.mirror-memory-modal-layer { + position: absolute; + inset: 0; + z-index: 6; +} + +.mirror-memory-modal-backdrop { + position: absolute; + inset: 0; + border: 0; + background: rgba(138, 111, 72, 0.16); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +.mirror-memory-modal-shell { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: var(--memory-header-offset) clamp(24px, 3vw, 42px) var(--memory-bottom-offset); + pointer-events: none; +} + +.mirror-memory-detail-card { + box-sizing: border-box; + width: min(980px, calc(100vw - 140px)); + max-height: min(calc(100vh - var(--memory-header-offset) - 28px), 820px); + padding: 22px; + overflow: hidden; + pointer-events: auto; +} + +.mirror-memory-modal-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; + flex: none; +} + +.mirror-memory-modal-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: 1px solid rgba(255, 255, 255, 0.54); + border-radius: 999px; + background: rgba(255, 251, 244, 0.82); + color: #71522d; + cursor: pointer; +} + +.mirror-memory-modal-close-icon { + width: 18px; + height: 18px; +} + +.mirror-memory-detail-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 16px; + margin-bottom: 18px; + flex: none; +} + +.mirror-memory-detail-counter { + display: flex; + min-width: 120px; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 16px; + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.5); + background: rgba(255, 249, 239, 0.84); +} + +.mirror-memory-detail-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-bottom: 16px; + flex: none; +} + +.mirror-memory-detail-body { + display: flex; + flex-direction: column; + gap: 12px; + min-height: 0; + margin-bottom: 18px; +} + +.mirror-memory-detail-section { + padding: 16px 18px; +} + +.mirror-memory-highlight-row { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.mirror-memory-highlight-chip { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 34px; + padding: 0 14px; + border-radius: 999px; + background: rgba(240, 204, 131, 0.22); + color: #7d5a2a; + font-size: 0.85rem; + font-weight: 500; +} + +.mirror-memory-action-bar { + display: flex; + flex-wrap: wrap; + gap: 12px; + flex: none; + margin-top: auto; +} + +.mirror-memory-action-button { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 44px; + padding: 0 18px; + border: 1px solid rgba(214, 183, 132, 0.48); + border-radius: 999px; + background: rgba(255, 251, 244, 0.82); + color: #684e2d; + cursor: pointer; + font-size: 0.9rem; +} + +.mirror-memory-action-button--primary { + background: linear-gradient(145deg, rgba(240, 204, 131, 0.94), rgba(223, 184, 106, 0.88)); +} + +.mirror-memory-feedback { + margin-top: 14px; + color: rgba(107, 79, 40, 0.84); + font-size: 0.84rem; +} + +@media (max-width: 1380px) { + .mirror-memory-layout { + grid-template-columns: minmax(320px, 404px) minmax(400px, 520px); + } +} + +@media (max-width: 1120px) { + .mirror-memory-page { + --memory-header-offset: 92px; + --memory-bottom-offset: 24px; + --memory-left-offset: 20px; + --memory-right-offset: 20px; + } + + .mirror-memory-layout { + grid-template-columns: minmax(0, 1fr); + justify-content: stretch; + padding: var(--memory-header-offset) 20px var(--memory-bottom-offset); + } + + .mirror-memory-card--recent, + .mirror-memory-card--history, + .mirror-memory-card--spotlight { + width: 100%; + max-height: none; + } + + .mirror-memory-card--recent, + .mirror-memory-card--history { + height: 400px; + min-height: 400px; + max-height: 400px; + } + + .mirror-memory-summary-card, + .mirror-memory-detail-grid { + grid-template-columns: minmax(0, 1fr); + } + + .mirror-memory-modal-shell { + justify-content: center; + padding: var(--memory-header-offset) 18px var(--memory-bottom-offset); + } + + .mirror-memory-detail-card { + width: min(100%, 920px); + max-width: min(100%, 920px); + } +} diff --git a/apps/desktop/src/features/dashboard/memory/mockMirrorMemoryData.ts b/apps/desktop/src/features/dashboard/memory/mockMirrorMemoryData.ts new file mode 100644 index 000000000..04096b7b5 --- /dev/null +++ b/apps/desktop/src/features/dashboard/memory/mockMirrorMemoryData.ts @@ -0,0 +1,125 @@ +export type MockRecentMemory = { + id: string; + title: string; + lastReferencedAt: string; + sourceTask: string; + referenceCount: number; + source: string; + summary: string; + scope: string; + highlights: string[]; + rawRecord: string; + relatedPreference: string; + historyLinks: string[]; +}; + +export type MockSessionSummary = { + id: string; + date: string; + title: string; + timeRange: string; + messageCount: number; + durationLabel: string; + sourceFile: string; + summary: string; + detailLines: string[]; +}; + +export type MockProfileMetric = { + label: string; + value: string; +}; + +export const mockRecentMemories: MockRecentMemory[] = [ + { + id: "memory-security-001", + title: "补齐安全详情中的授权说明与恢复点解释", + lastReferencedAt: "周四 13:24", + sourceTask: "task_security_mock_001", + referenceCount: 1, + source: "2026.05.14 会话摘要", + summary: "这条记忆来自今天唯一一次协作,核心是把安全详情页中的授权说明、恢复点含义和异常处理文案说清楚。", + scope: "适用于补充安全说明、授权范围解释和恢复点展示文案时。", + highlights: ["授权范围说明", "恢复点作用解释", "异常情况处理提示"], + rawRecord: "用户要求在安全详情中补齐授权说明,并明确解释恢复点的作用,避免只给结论不说明边界。", + relatedPreference: "当前样本显示,用户更关注说明是否清楚、恢复路径是否明确。", + historyLinks: ["2026.05.14 会话摘要", "conversation_20260514_1300.json"], + }, +]; + +export const mockSessionSummaries: MockSessionSummary[] = [ + { + id: "session-20260514-1300", + date: "2026.05.14", + title: "安全说明补充与恢复点解释", + timeRange: "周四 13:00–13:30", + messageCount: 12, + durationLabel: "30 分钟", + sourceFile: "conversation_20260514_1300.json", + summary: "本次会话主要围绕安全详情页的授权说明、恢复点解释和展示文案进行补充,重点是让用户更清楚地理解授权范围、恢复点作用以及异常情况下的处理方式。", + detailLines: [ + "讨论重点集中在授权说明是否清楚。", + "恢复点需要解释用途,而不只是显示字段名。", + "异常情况文案要强调如何回退和如何理解当前状态。", + ], + }, +]; + +export const mockHistoryDates = ["2026.05.14", "2026.05.13", "2026.05.12"] as const; + +export const mockProfileMetrics: MockProfileMetric[] = [ + { label: "活跃时间", value: "周四 13:00–13:30" }, + { label: "累计使用", value: "30 分钟" }, + { label: "活跃天数", value: "1 天" }, + { label: "使用状态", value: "初次观察中" }, +]; + +export const mockProfileAxes = [ + { label: "表达偏好", value: 0.28 }, + { label: "结构敏感", value: 0.24 }, + { label: "安全关注", value: 0.32 }, + { label: "节奏稳定", value: 0.16 }, + { label: "样本量", value: 0.08 }, +]; + +export const mockDailySummary = { + title: "日报", + lines: [ + "日期:2026.05.14", + "完成任务:1 项", + "使用时长:30 分钟", + "活跃时间:13:00–13:30", + ], +}; + +export const mockPhaseSummary = { + title: "阶段总结", + lines: [ + "统计周期:本周", + "活跃天数:1/7 天", + "累计使用:0.5h", + "会话数量:1 个", + ], +}; + +export const mockHeatmap = [ + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0.9, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], +]; + +export function findMockRecentMemory(memoryId: string) { + return mockRecentMemories.find((memory) => memory.id === memoryId) ?? null; +} + +export function findMockSessionSummary(sessionId: string) { + return mockSessionSummaries.find((session) => session.id === sessionId) ?? null; +} + +export function findMockSessionSummaryByDate(date: string) { + return mockSessionSummaries.find((session) => session.date === date) ?? null; +} diff --git a/apps/desktop/src/features/dashboard/notes/NotePage.tsx b/apps/desktop/src/features/dashboard/notes/NotePage.tsx index 5033591af..003341fc6 100644 --- a/apps/desktop/src/features/dashboard/notes/NotePage.tsx +++ b/apps/desktop/src/features/dashboard/notes/NotePage.tsx @@ -5,17 +5,15 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useUnmount } from "ahooks"; import type { CSSProperties, PointerEvent as ReactPointerEvent, UIEvent } from "react"; -import { Link, NavLink, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; -import { AlertTriangle, ArrowLeft, ArrowUpRight, FilePlus2, PanelLeftClose, PanelLeftOpen, RefreshCcw, ScanSearch, X } from "lucide-react"; +import { AlertTriangle, Archive, ArrowUpRight, FilePlus2, Info, PanelLeftClose, PanelLeftOpen, RefreshCcw, ScanSearch, X } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import type { NotepadAction, Task, TodoItem } from "@cialloclaw/protocol"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { useDashboardEscapeHandler } from "@/features/dashboard/shared/dashboardEscapeCoordinator"; import { navigateToDashboardTaskDetail } from "@/features/dashboard/shared/dashboardTaskDetailNavigation"; -import { resolveDashboardRoutePath } from "@/features/dashboard/shared/dashboardRouteTargets"; -import { dashboardModules } from "@/features/dashboard/shared/dashboardRoutes"; import { openTaskDeliveryForTask, performTaskOpenExecution, @@ -2814,20 +2812,7 @@ export function NotePage() {
<>
-
- - - 返回首页 - - - -
+