mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-21 21:24:46 +08:00
173 lines
4.9 KiB
TypeScript
173 lines
4.9 KiB
TypeScript
import { FilesIcon, XIcon } from "lucide-react";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import type { GroupImperativeHandle } from "react-resizable-panels";
|
|
|
|
import { ConversationEmptyState } from "@/components/ai-elements/conversation";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
ResizableHandle,
|
|
ResizablePanel,
|
|
ResizablePanelGroup,
|
|
} from "@/components/ui/resizable";
|
|
import { env } from "@/env";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
import {
|
|
ArtifactFileDetail,
|
|
ArtifactFileList,
|
|
useArtifacts,
|
|
} from "../artifacts";
|
|
import { useThread } from "../messages/context";
|
|
|
|
const CLOSE_MODE = { chat: 100, artifacts: 0 };
|
|
const OPEN_MODE = { chat: 60, artifacts: 40 };
|
|
|
|
const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|
children,
|
|
threadId,
|
|
}) => {
|
|
const { thread } = useThread();
|
|
const threadIdRef = useRef(threadId);
|
|
const layoutRef = useRef<GroupImperativeHandle>(null);
|
|
|
|
const {
|
|
artifacts,
|
|
open: artifactsOpen,
|
|
setOpen: setArtifactsOpen,
|
|
setArtifacts,
|
|
select: selectArtifact,
|
|
deselect,
|
|
selectedArtifact,
|
|
} = useArtifacts();
|
|
|
|
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
|
useEffect(() => {
|
|
if (threadIdRef.current !== threadId) {
|
|
threadIdRef.current = threadId;
|
|
deselect();
|
|
}
|
|
|
|
// Update artifacts from the current thread
|
|
setArtifacts(thread.values.artifacts);
|
|
|
|
// Deselect if the currently selected artifact no longer exists
|
|
if (
|
|
selectedArtifact &&
|
|
!thread.values.artifacts?.includes(selectedArtifact)
|
|
) {
|
|
deselect();
|
|
}
|
|
|
|
if (
|
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
|
autoSelectFirstArtifact
|
|
) {
|
|
if (thread?.values?.artifacts?.length > 0) {
|
|
setAutoSelectFirstArtifact(false);
|
|
selectArtifact(thread.values.artifacts[0]!);
|
|
}
|
|
}
|
|
}, [
|
|
threadId,
|
|
autoSelectFirstArtifact,
|
|
deselect,
|
|
selectArtifact,
|
|
selectedArtifact,
|
|
setArtifacts,
|
|
thread.values.artifacts,
|
|
]);
|
|
|
|
const artifactPanelOpen = useMemo(() => {
|
|
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true") {
|
|
return artifactsOpen && artifacts?.length > 0;
|
|
}
|
|
return artifactsOpen;
|
|
}, [artifactsOpen, artifacts]);
|
|
|
|
useEffect(() => {
|
|
if (layoutRef.current) {
|
|
if (artifactPanelOpen) {
|
|
layoutRef.current.setLayout(OPEN_MODE);
|
|
} else {
|
|
layoutRef.current.setLayout(CLOSE_MODE);
|
|
}
|
|
}
|
|
}, [artifactPanelOpen]);
|
|
|
|
return (
|
|
<ResizablePanelGroup
|
|
orientation="horizontal"
|
|
defaultLayout={{ chat: 100, artifacts: 0 }}
|
|
groupRef={layoutRef}
|
|
>
|
|
<ResizablePanel className="relative" defaultSize={100} id="chat">
|
|
{children}
|
|
</ResizablePanel>
|
|
<ResizableHandle
|
|
className={cn(
|
|
"opacity-33 hover:opacity-100",
|
|
!artifactPanelOpen && "pointer-events-none opacity-0",
|
|
)}
|
|
/>
|
|
<ResizablePanel
|
|
className={cn(
|
|
"transition-all duration-300 ease-in-out",
|
|
!artifactsOpen && "opacity-0",
|
|
)}
|
|
id="artifacts"
|
|
>
|
|
<div
|
|
className={cn(
|
|
"h-full p-4 transition-transform duration-300 ease-in-out",
|
|
artifactPanelOpen ? "translate-x-0" : "translate-x-full",
|
|
)}
|
|
>
|
|
{selectedArtifact ? (
|
|
<ArtifactFileDetail
|
|
className="size-full"
|
|
filepath={selectedArtifact}
|
|
threadId={threadId}
|
|
/>
|
|
) : (
|
|
<div className="relative flex size-full justify-center">
|
|
<div className="absolute top-1 right-1 z-30">
|
|
<Button
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setArtifactsOpen(false);
|
|
}}
|
|
>
|
|
<XIcon />
|
|
</Button>
|
|
</div>
|
|
{thread.values.artifacts?.length === 0 ? (
|
|
<ConversationEmptyState
|
|
icon={<FilesIcon />}
|
|
title="No artifact selected"
|
|
description="Select an artifact to view its details"
|
|
/>
|
|
) : (
|
|
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4 pt-8">
|
|
<header className="shrink-0">
|
|
<h2 className="text-lg font-medium">Artifacts</h2>
|
|
</header>
|
|
<main className="min-h-0 grow">
|
|
<ArtifactFileList
|
|
className="max-w-(--container-width-sm) p-4 pt-12"
|
|
files={thread.values.artifacts ?? []}
|
|
threadId={threadId}
|
|
/>
|
|
</main>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
);
|
|
};
|
|
|
|
export { ChatBox };
|