221 lines
6.6 KiB
TypeScript
221 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import { useParams } from "next/navigation";
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
type MouseEvent as ReactMouseEvent,
|
|
} from "react";
|
|
import MindmapCanvas from "@/components/MindmapCanvas";
|
|
import ChatPanel from "@/components/ChatPanel";
|
|
import { getMindmap } from "@/lib/api";
|
|
import type { Mindmap } from "@/types/mindmap";
|
|
|
|
type TriggerRequest = {
|
|
id: number;
|
|
content: string;
|
|
};
|
|
|
|
const MIN_CHAT_PANEL_WIDTH = 320;
|
|
const DEFAULT_CHAT_PANEL_WIDTH = 420;
|
|
const MAX_CHAT_PANEL_WIDTH_RATIO = 0.55;
|
|
|
|
export default function MindmapDetailPage() {
|
|
const params = useParams<{ id: string }>();
|
|
const uniqueId = params?.id ?? "";
|
|
const containerRef = useRef<HTMLElement | null>(null);
|
|
const [mindmap, setMindmap] = useState<Mindmap | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
const [showChat, setShowChat] = useState(false);
|
|
const [isResizingChatPanel, setIsResizingChatPanel] = useState(false);
|
|
const [chatPanelWidth, setChatPanelWidth] = useState(DEFAULT_CHAT_PANEL_WIDTH);
|
|
const [triggerRequest, setTriggerRequest] = useState<TriggerRequest | undefined>();
|
|
|
|
const clampChatPanelWidth = useCallback((nextWidth: number) => {
|
|
const containerWidth =
|
|
containerRef.current?.clientWidth ??
|
|
(typeof window === "undefined" ? DEFAULT_CHAT_PANEL_WIDTH : window.innerWidth);
|
|
const maxWidth = Math.max(
|
|
MIN_CHAT_PANEL_WIDTH,
|
|
Math.floor(containerWidth * MAX_CHAT_PANEL_WIDTH_RATIO),
|
|
);
|
|
|
|
return Math.min(Math.max(nextWidth, MIN_CHAT_PANEL_WIDTH), maxWidth);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
async function loadPageData() {
|
|
if (!uniqueId) {
|
|
setLoading(false);
|
|
setError("缺少脑图 ID");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError("");
|
|
const mindmapData = await getMindmap(uniqueId);
|
|
setMindmap(mindmapData);
|
|
} catch (pageError) {
|
|
const message =
|
|
pageError instanceof Error ? pageError.message : "加载脑图失败";
|
|
setError(message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
void loadPageData();
|
|
}, [uniqueId]);
|
|
|
|
const handleNodeChat = useCallback(
|
|
(nodeLabel: string) => {
|
|
setShowChat(true);
|
|
setChatPanelWidth((currentWidth) => clampChatPanelWidth(currentWidth));
|
|
setTriggerRequest({
|
|
id: Date.now() + Math.floor(Math.random() * 1000),
|
|
content: `帮我解释一下 ${nodeLabel}`,
|
|
});
|
|
},
|
|
[clampChatPanelWidth],
|
|
);
|
|
|
|
const handleCloseChat = useCallback(() => {
|
|
setIsResizingChatPanel(false);
|
|
setShowChat(false);
|
|
setTriggerRequest(undefined);
|
|
}, []);
|
|
|
|
const handleResizeStart = useCallback(
|
|
(event: ReactMouseEvent<HTMLDivElement>) => {
|
|
event.preventDefault();
|
|
setChatPanelWidth((currentWidth) => clampChatPanelWidth(currentWidth));
|
|
setIsResizingChatPanel(true);
|
|
},
|
|
[clampChatPanelWidth],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!showChat) {
|
|
setIsResizingChatPanel(false);
|
|
return;
|
|
}
|
|
|
|
const handleWindowResize = () => {
|
|
setChatPanelWidth((currentWidth) => clampChatPanelWidth(currentWidth));
|
|
};
|
|
|
|
handleWindowResize();
|
|
window.addEventListener("resize", handleWindowResize);
|
|
return () => window.removeEventListener("resize", handleWindowResize);
|
|
}, [clampChatPanelWidth, showChat]);
|
|
|
|
useEffect(() => {
|
|
if (!showChat || !isResizingChatPanel) {
|
|
return;
|
|
}
|
|
|
|
const handleMouseMove = (event: MouseEvent) => {
|
|
if (!containerRef.current) {
|
|
return;
|
|
}
|
|
|
|
const bounds = containerRef.current.getBoundingClientRect();
|
|
const nextWidth = bounds.right - event.clientX;
|
|
setChatPanelWidth(clampChatPanelWidth(nextWidth));
|
|
};
|
|
|
|
const stopResizing = () => {
|
|
setIsResizingChatPanel(false);
|
|
};
|
|
|
|
const { style } = document.body;
|
|
const previousCursor = style.cursor;
|
|
const previousUserSelect = style.userSelect;
|
|
|
|
style.cursor = "col-resize";
|
|
style.userSelect = "none";
|
|
|
|
window.addEventListener("mousemove", handleMouseMove);
|
|
window.addEventListener("mouseup", stopResizing);
|
|
window.addEventListener("blur", stopResizing);
|
|
|
|
return () => {
|
|
style.cursor = previousCursor;
|
|
style.userSelect = previousUserSelect;
|
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
window.removeEventListener("mouseup", stopResizing);
|
|
window.removeEventListener("blur", stopResizing);
|
|
};
|
|
}, [clampChatPanelWidth, isResizingChatPanel, showChat]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<main className="flex h-screen w-screen items-center justify-center bg-white">
|
|
<div className="rounded-3xl bg-white px-8 py-6 shadow-panel">
|
|
正在加载思维导图...
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
if (error || !mindmap) {
|
|
return (
|
|
<main className="flex h-screen w-screen flex-col items-center justify-center bg-white px-6 text-center">
|
|
<div className="rounded-3xl bg-white px-8 py-8 shadow-panel">
|
|
<p className="text-lg font-semibold text-rose-600">
|
|
{error || "未找到对应脑图"}
|
|
</p>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<main ref={containerRef} className="flex h-screen w-screen overflow-hidden bg-white">
|
|
<section className="min-w-0 flex-1">
|
|
<MindmapCanvas tree={mindmap.tree} onNodeChat={handleNodeChat} />
|
|
</section>
|
|
{showChat && (
|
|
<>
|
|
<div
|
|
role="separator"
|
|
aria-label="调整聊天区域宽度"
|
|
aria-orientation="vertical"
|
|
onMouseDown={handleResizeStart}
|
|
className="group relative w-3 shrink-0 cursor-col-resize bg-transparent"
|
|
>
|
|
<div
|
|
className={`absolute inset-y-0 left-1/2 w-px -translate-x-1/2 transition-colors ${
|
|
isResizingChatPanel
|
|
? "bg-blue-500"
|
|
: "bg-slate-200 group-hover:bg-blue-300"
|
|
}`}
|
|
/>
|
|
<div
|
|
className={`absolute left-1/2 top-1/2 h-16 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full transition-colors ${
|
|
isResizingChatPanel
|
|
? "bg-blue-500"
|
|
: "bg-slate-300 group-hover:bg-blue-400"
|
|
}`}
|
|
/>
|
|
</div>
|
|
<aside
|
|
style={{ width: `${chatPanelWidth}px` }}
|
|
className="min-w-0 shrink-0 overflow-hidden bg-slate-50 shadow-[-18px_0_36px_rgba(15,23,42,0.06)]"
|
|
>
|
|
<ChatPanel
|
|
sessionId={mindmap.session_id}
|
|
triggerRequest={triggerRequest}
|
|
onClose={handleCloseChat}
|
|
/>
|
|
</aside>
|
|
</>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|