From e5050c6c1e5cd7fbd1218ac7421909cbe438a129 Mon Sep 17 00:00:00 2001 From: Henry Li Date: Sat, 17 Jan 2026 00:02:03 +0800 Subject: [PATCH] feat: integrated with artifacts --- .../app/workspace/chats/[thread_id]/page.tsx | 107 ++++++++++++------ .../artifacts/artifact-file-detail.tsx | 14 +++ .../artifact-file-list.tsx} | 27 ++++- .../workspace/artifacts/context.tsx | 58 ++++++++++ .../components/workspace/artifacts/index.ts | 3 + .../workspace/message-list/message-list.tsx | 4 +- 6 files changed, 174 insertions(+), 39 deletions(-) create mode 100644 frontend/src/components/workspace/artifacts/artifact-file-detail.tsx rename frontend/src/components/workspace/{message-list/present-file-list.tsx => artifacts/artifact-file-list.tsx} (55%) create mode 100644 frontend/src/components/workspace/artifacts/context.tsx create mode 100644 frontend/src/components/workspace/artifacts/index.ts diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 66ef937..02755a9 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -1,10 +1,20 @@ "use client"; +import type { UseStream } from "@langchain/langgraph-sdk/react"; import { useParams, useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { BreadcrumbItem } from "@/components/ui/breadcrumb"; -import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { ArtifactFileDetail } from "@/components/workspace/artifacts"; +import { + ArtifactsProvider, + useArtifacts, +} from "@/components/workspace/artifacts/context"; import { InputBox } from "@/components/workspace/input-box"; import { MessageList } from "@/components/workspace/message-list/message-list"; import { @@ -13,20 +23,18 @@ import { WorkspaceHeader, } from "@/components/workspace/workspace-container"; import { useLocalSettings } from "@/core/settings"; -import { type AgentThread } from "@/core/threads"; +import { type AgentThread, type AgentThreadState } from "@/core/threads"; import { useSubmitThread, useThreadStream } from "@/core/threads/hooks"; import { pathOfThread, titleOfThread } from "@/core/threads/utils"; import { uuid } from "@/core/utils/uuid"; export default function ChatPage() { - const router = useRouter(); const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>(); const isNewThread = useMemo( () => threadIdFromPath === "new", [threadIdFromPath], ); const [threadId, setThreadId] = useState(null); - const [settings, setSettings] = useLocalSettings(); useEffect(() => { if (threadIdFromPath !== "new") { @@ -39,6 +47,40 @@ export default function ChatPage() { isNewThread, threadId, }); + return ( + + + + {isNewThread + ? "New" + : titleOfThread(thread as unknown as AgentThread)} + + + + + + + + + ); +} + +function ThreadDetail({ + threadId, + thread, + isNewThread, +}: { + threadId?: string | null; + thread: UseStream; + isNewThread: boolean; +}) { + const router = useRouter(); + const [settings, setSettings] = useLocalSettings(); + const { open, selectedArtifact } = useArtifacts(); const handleSubmit = useSubmitThread({ isNewThread, threadId, @@ -52,36 +94,33 @@ export default function ChatPage() { await thread.stop(); }, [thread]); return ( - - - - {isNewThread - ? "New" - : titleOfThread(thread as unknown as AgentThread)} - - - - - -
- -
-
- setSettings("context", context)} - onSubmit={handleSubmit} - onStop={handleStop} - /> -
+ + +
+ +
+
+ setSettings("context", context)} + onSubmit={handleSubmit} + onStop={handleStop} + /> +
+
+ {open && ( + <> + + + {selectedArtifact && ( + + )} - {/* - */} -
-
-
+ + )} + ); } diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx new file mode 100644 index 0000000..6ae28ff --- /dev/null +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -0,0 +1,14 @@ +import { FileIcon } from "lucide-react"; + +export function ArtifactFileDetail({ filepath }: { filepath: string }) { + return ( +
+
+
+ +
+
{filepath}
+
+
+ ); +} diff --git a/frontend/src/components/workspace/message-list/present-file-list.tsx b/frontend/src/components/workspace/artifacts/artifact-file-list.tsx similarity index 55% rename from frontend/src/components/workspace/message-list/present-file-list.tsx rename to frontend/src/components/workspace/artifacts/artifact-file-list.tsx index 314c4df..44309cb 100644 --- a/frontend/src/components/workspace/message-list/present-file-list.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-list.tsx @@ -1,4 +1,5 @@ import { DownloadIcon } from "lucide-react"; +import { useCallback } from "react"; import { Button } from "@/components/ui/button"; import { @@ -9,12 +10,32 @@ import { CardTitle, } from "@/components/ui/card"; import { getFileExtension, getFileName } from "@/core/utils/files"; +import { cn } from "@/lib/utils"; -export function PresentFileList({ files }: { files: string[] }) { +import { useArtifacts } from "./context"; + +export function ArtifactFileList({ + className, + files, +}: { + className?: string; + files: string[]; +}) { + const { openArtifact } = useArtifacts(); + const handleClick = useCallback( + (filepath: string) => { + openArtifact(filepath); + }, + [openArtifact], + ); return ( -
    +
      {files.map((file) => ( - + handleClick(file)} + > {getFileName(file)} {getFileExtension(file)} file diff --git a/frontend/src/components/workspace/artifacts/context.tsx b/frontend/src/components/workspace/artifacts/context.tsx new file mode 100644 index 0000000..a585870 --- /dev/null +++ b/frontend/src/components/workspace/artifacts/context.tsx @@ -0,0 +1,58 @@ +import { createContext, useContext, useState, type ReactNode } from "react"; + +export interface ArtifactsContextType { + artifacts: string[]; + selectedArtifact: string | null; + + open: boolean; + setOpen: (open: boolean) => void; + + addArtifacts: (artifacts: string[]) => void; + openArtifact: (artifact: string) => void; +} + +const ArtifactsContext = createContext( + undefined, +); + +interface ArtifactsProviderProps { + children: ReactNode; +} + +export function ArtifactsProvider({ children }: ArtifactsProviderProps) { + const [artifacts, setArtifacts] = useState([]); + const [selectedArtifact, setSelectedArtifact] = useState(null); + const [open, setOpen] = useState(false); + + const addArtifacts = (newArtifacts: string[]) => { + setArtifacts((prev) => [...prev, ...newArtifacts]); + }; + + const openArtifact = (artifact: string) => { + setSelectedArtifact(artifact); + setOpen(true); + }; + + const value: ArtifactsContextType = { + artifacts, + selectedArtifact, + open, + setOpen, + addArtifacts, + openArtifact, + }; + + return ( + + {children} + + ); +} + +export function useArtifacts() { + const context = useContext(ArtifactsContext); + if (context === undefined) { + throw new Error("useArtifacts must be used within an ArtifactsProvider"); + } + return context; +} diff --git a/frontend/src/components/workspace/artifacts/index.ts b/frontend/src/components/workspace/artifacts/index.ts new file mode 100644 index 0000000..bedf505 --- /dev/null +++ b/frontend/src/components/workspace/artifacts/index.ts @@ -0,0 +1,3 @@ +export * from "./artifact-file-detail"; +export * from "./artifact-file-list"; +export * from "./context"; diff --git a/frontend/src/components/workspace/message-list/message-list.tsx b/frontend/src/components/workspace/message-list/message-list.tsx index 862b4d2..07d5655 100644 --- a/frontend/src/components/workspace/message-list/message-list.tsx +++ b/frontend/src/components/workspace/message-list/message-list.tsx @@ -13,11 +13,11 @@ import { import type { AgentThreadState } from "@/core/threads"; import { cn } from "@/lib/utils"; +import { ArtifactFileList } from "../artifacts/artifact-file-list"; import { StreamingIndicator } from "../streaming-indicator"; import { MessageGroup } from "./message-group"; import { MessageListItem } from "./message-list-item"; -import { PresentFileList } from "./present-file-list"; import { MessageListSkeleton } from "./skeleton"; export function MessageList({ @@ -57,7 +57,7 @@ export function MessageList({ } } return ( - + ); } return (