mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-21 05:14:45 +08:00
feat: support SSE write_file(0
This commit is contained in:
@@ -32,7 +32,7 @@ import { FileViewer } from "./file-viewer";
|
||||
|
||||
export function ArtifactFileDetail({
|
||||
className,
|
||||
filepath,
|
||||
filepath: filepathFromProps,
|
||||
threadId,
|
||||
}: {
|
||||
className?: string;
|
||||
@@ -40,42 +40,65 @@ export function ArtifactFileDetail({
|
||||
threadId: string;
|
||||
}) {
|
||||
const { artifacts, setOpen, select } = useArtifacts();
|
||||
const { isCodeFile } = useMemo(() => checkCodeFile(filepath), [filepath]);
|
||||
const isWriteFile = useMemo(() => {
|
||||
return filepathFromProps.startsWith("write-file:");
|
||||
}, [filepathFromProps]);
|
||||
const filepath = useMemo(() => {
|
||||
if (isWriteFile) {
|
||||
const url = new URL(filepathFromProps);
|
||||
return url.pathname;
|
||||
}
|
||||
return filepathFromProps;
|
||||
}, [filepathFromProps, isWriteFile]);
|
||||
const { isCodeFile } = useMemo(() => {
|
||||
if (isWriteFile) {
|
||||
let language = checkCodeFile(filepath).language;
|
||||
language ??= "markdown";
|
||||
return { isCodeFile: true, language };
|
||||
}
|
||||
return checkCodeFile(filepath);
|
||||
}, [filepath, isWriteFile]);
|
||||
const { content } = useArtifactContent({
|
||||
threadId,
|
||||
filepath,
|
||||
enabled: isCodeFile,
|
||||
filepath: filepathFromProps,
|
||||
enabled: isCodeFile && !isWriteFile,
|
||||
});
|
||||
return (
|
||||
<Artifact className={cn("rounded-none", className)}>
|
||||
<ArtifactHeader className="px-2">
|
||||
<div>
|
||||
<ArtifactTitle>
|
||||
<Select value={filepath} onValueChange={select}>
|
||||
<SelectTrigger className="border-none bg-transparent! shadow-none select-none focus:outline-0 active:outline-0">
|
||||
<SelectValue placeholder="Select a file" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="select-none">
|
||||
<SelectGroup>
|
||||
{(artifacts ?? []).map((filepath) => (
|
||||
<SelectItem key={filepath} value={filepath}>
|
||||
{getFileName(filepath)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{isWriteFile ? (
|
||||
<div className="px-2">{getFileName(filepath)}</div>
|
||||
) : (
|
||||
<Select value={filepath} onValueChange={select}>
|
||||
<SelectTrigger className="border-none bg-transparent! shadow-none select-none focus:outline-0 active:outline-0">
|
||||
<SelectValue placeholder="Select a file" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="select-none">
|
||||
<SelectGroup>
|
||||
{(artifacts ?? []).map((filepath) => (
|
||||
<SelectItem key={filepath} value={filepath}>
|
||||
{getFileName(filepath)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</ArtifactTitle>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArtifactActions>
|
||||
<a href={urlOfArtifact({ filepath, threadId })} target="_blank">
|
||||
<ArtifactAction
|
||||
icon={SquareArrowOutUpRightIcon}
|
||||
label="Open in new window"
|
||||
tooltip="Open in new window"
|
||||
/>
|
||||
</a>
|
||||
{!isWriteFile && (
|
||||
<a href={urlOfArtifact({ filepath, threadId })} target="_blank">
|
||||
<ArtifactAction
|
||||
icon={SquareArrowOutUpRightIcon}
|
||||
label="Open in new window"
|
||||
tooltip="Open in new window"
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
{isCodeFile && (
|
||||
<ArtifactAction
|
||||
icon={CopyIcon}
|
||||
@@ -93,17 +116,19 @@ export function ArtifactFileDetail({
|
||||
tooltip="Copy content to clipboard"
|
||||
/>
|
||||
)}
|
||||
<a
|
||||
href={urlOfArtifact({ filepath, threadId, download: true })}
|
||||
target="_blank"
|
||||
>
|
||||
<ArtifactAction
|
||||
icon={DownloadIcon}
|
||||
label="Download"
|
||||
onClick={() => console.log("Download")}
|
||||
tooltip="Download file"
|
||||
/>
|
||||
</a>
|
||||
{!isWriteFile && (
|
||||
<a
|
||||
href={urlOfArtifact({ filepath, threadId, download: true })}
|
||||
target="_blank"
|
||||
>
|
||||
<ArtifactAction
|
||||
icon={DownloadIcon}
|
||||
label="Download"
|
||||
onClick={() => console.log("Download")}
|
||||
tooltip="Download file"
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
<ArtifactAction
|
||||
icon={XIcon}
|
||||
label="Close"
|
||||
@@ -117,7 +142,7 @@ export function ArtifactFileDetail({
|
||||
<FileViewer
|
||||
className="size-full"
|
||||
threadId={threadId}
|
||||
filepath={filepath}
|
||||
filepath={filepathFromProps}
|
||||
/>
|
||||
</ArtifactContent>
|
||||
</Artifact>
|
||||
|
||||
@@ -16,14 +16,21 @@ export function FileViewer({
|
||||
filepath: string;
|
||||
threadId: string;
|
||||
}) {
|
||||
const { isCodeFile, language } = useMemo(
|
||||
() => checkCodeFile(filepath),
|
||||
[filepath],
|
||||
);
|
||||
if (isCodeFile && language !== "html") {
|
||||
const isWriteFile = useMemo(() => {
|
||||
return filepath.startsWith("write-file:");
|
||||
}, [filepath]);
|
||||
const { isCodeFile, language } = useMemo(() => {
|
||||
if (isWriteFile) {
|
||||
const url = new URL(filepath);
|
||||
const path = url.pathname;
|
||||
return checkCodeFile(path);
|
||||
}
|
||||
return checkCodeFile(filepath);
|
||||
}, [filepath, isWriteFile]);
|
||||
if (isWriteFile || (isCodeFile && language !== "html")) {
|
||||
return (
|
||||
<CodeFileView
|
||||
language={language}
|
||||
language={language ?? "markdown"}
|
||||
filepath={filepath}
|
||||
threadId={threadId}
|
||||
/>
|
||||
@@ -55,7 +62,7 @@ function CodeFileView({
|
||||
if (code) {
|
||||
return (
|
||||
<CodeBlock
|
||||
className="rounded-none border-none"
|
||||
className="size-full rounded-none border-none"
|
||||
language={language}
|
||||
code={code}
|
||||
/>
|
||||
|
||||
21
frontend/src/components/workspace/messages/context.ts
Normal file
21
frontend/src/components/workspace/messages/context.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { UseStream } from "@langchain/langgraph-sdk/react";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import type { AgentThreadState } from "@/core/threads";
|
||||
|
||||
export interface ThreadContextType {
|
||||
threadId: string;
|
||||
thread: UseStream<AgentThreadState>;
|
||||
}
|
||||
|
||||
export const ThreadContext = createContext<ThreadContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function useThread() {
|
||||
const context = useContext(ThreadContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useThread must be used within a ThreadContext");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||
import { extractTitleFromMarkdown } from "@/core/utils/markdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { useArtifacts } from "../artifacts";
|
||||
import { FlipDisplay } from "../flip-display";
|
||||
|
||||
export function MessageGroup({
|
||||
@@ -108,15 +109,18 @@ export function MessageGroup({
|
||||
|
||||
function ToolCall({
|
||||
id,
|
||||
messageId,
|
||||
name,
|
||||
args,
|
||||
result,
|
||||
}: {
|
||||
id?: string;
|
||||
messageId?: string;
|
||||
name: string;
|
||||
args: Record<string, unknown>;
|
||||
result?: string | Record<string, unknown>;
|
||||
}) {
|
||||
const { select, setOpen } = useArtifacts();
|
||||
if (name === "web_search") {
|
||||
let label: React.ReactNode = "Search for related information";
|
||||
if (typeof args.query === "string") {
|
||||
@@ -198,7 +202,20 @@ function ToolCall({
|
||||
}
|
||||
const path: string | undefined = (args as { path: string })?.path;
|
||||
return (
|
||||
<ChainOfThoughtStep key={id} label={description} icon={NotebookPenIcon}>
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
className="cursor-pointer"
|
||||
label={description}
|
||||
icon={NotebookPenIcon}
|
||||
onClick={() => {
|
||||
select(
|
||||
new URL(
|
||||
`write-file:${path}?message_id=${messageId}&tool_call_id=${id}`,
|
||||
).toString(),
|
||||
);
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{path && (
|
||||
<ChainOfThoughtSearchResult>{path}</ChainOfThoughtSearchResult>
|
||||
)}
|
||||
@@ -258,6 +275,7 @@ function ToolCall({
|
||||
|
||||
interface GenericCoTStep<T extends string = string> {
|
||||
id?: string;
|
||||
messageId?: string;
|
||||
type: T;
|
||||
}
|
||||
|
||||
@@ -281,6 +299,7 @@ function convertToSteps(messages: Message[]): CoTStep[] {
|
||||
if (reasoning) {
|
||||
const step: CoTReasoningStep = {
|
||||
id: message.id,
|
||||
messageId: message.id,
|
||||
type: "reasoning",
|
||||
reasoning: extractReasoningContentFromMessage(message),
|
||||
};
|
||||
@@ -289,6 +308,7 @@ function convertToSteps(messages: Message[]): CoTStep[] {
|
||||
for (const tool_call of message.tool_calls ?? []) {
|
||||
const step: CoTToolCallStep = {
|
||||
id: tool_call.id,
|
||||
messageId: message.id,
|
||||
type: "toolCall",
|
||||
name: tool_call.name,
|
||||
args: tool_call.args,
|
||||
|
||||
Reference in New Issue
Block a user