mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-10 01:04:46 +08:00
feat: add Todos
This commit is contained in:
@@ -789,7 +789,7 @@ export const PromptInput = ({
|
||||
ref={formRef}
|
||||
{...props}
|
||||
>
|
||||
<InputGroup className="overflow-hidden">{children}</InputGroup>
|
||||
<InputGroup>{children}</InputGroup>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -7,13 +7,13 @@ export interface ArtifactsContextType {
|
||||
setArtifacts: (artifacts: string[]) => void;
|
||||
|
||||
selectedArtifact: string | null;
|
||||
autoSelect: boolean;
|
||||
select: (artifact: string, autoSelect?: boolean) => void;
|
||||
deselect: () => void;
|
||||
|
||||
open: boolean;
|
||||
autoOpen: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
deselect: () => void;
|
||||
|
||||
select: (artifact: string) => void;
|
||||
}
|
||||
|
||||
const ArtifactsContext = createContext<ArtifactsContextType | undefined>(
|
||||
@@ -27,17 +27,22 @@ interface ArtifactsProviderProps {
|
||||
export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||
const [artifacts, setArtifacts] = useState<string[]>([]);
|
||||
const [selectedArtifact, setSelectedArtifact] = useState<string | null>(null);
|
||||
const [autoSelect, setAutoSelect] = useState(true);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [autoOpen, setAutoOpen] = useState(true);
|
||||
const { setOpen: setSidebarOpen } = useSidebar();
|
||||
|
||||
const select = (artifact: string) => {
|
||||
const select = (artifact: string, autoSelect = false) => {
|
||||
setSelectedArtifact(artifact);
|
||||
setSidebarOpen(false);
|
||||
if (!autoSelect) {
|
||||
setAutoSelect(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deselect = () => {
|
||||
setSelectedArtifact(null);
|
||||
setAutoSelect(true);
|
||||
};
|
||||
|
||||
const value: ArtifactsContextType = {
|
||||
@@ -46,9 +51,11 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||
|
||||
open,
|
||||
autoOpen,
|
||||
autoSelect,
|
||||
setOpen: (isOpen: boolean) => {
|
||||
if (!isOpen && autoOpen) {
|
||||
setAutoOpen(false);
|
||||
setAutoSelect(false);
|
||||
}
|
||||
setOpen(isOpen);
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ChatStatus } from "ai";
|
||||
import { CheckIcon, LightbulbIcon, LightbulbOffIcon } from "lucide-react";
|
||||
import { CheckIcon, LightbulbIcon, ListTodoIcon } from "lucide-react";
|
||||
import { useCallback, useMemo, useState, type ComponentProps } from "react";
|
||||
|
||||
import {
|
||||
@@ -35,6 +35,7 @@ export function InputBox({
|
||||
autoFocus,
|
||||
status = "ready",
|
||||
context,
|
||||
extraHeader,
|
||||
onContextChange,
|
||||
onSubmit,
|
||||
onStop,
|
||||
@@ -43,6 +44,7 @@ export function InputBox({
|
||||
assistantId?: string | null;
|
||||
status?: ChatStatus;
|
||||
context: Omit<AgentThreadContext, "thread_id">;
|
||||
extraHeader?: React.ReactNode;
|
||||
onContextChange?: (context: Omit<AgentThreadContext, "thread_id">) => void;
|
||||
onSubmit?: (message: PromptInputMessage) => void;
|
||||
onStop?: () => void;
|
||||
@@ -72,6 +74,12 @@ export function InputBox({
|
||||
thinking_enabled: !context.thinking_enabled,
|
||||
});
|
||||
}, [onContextChange, context]);
|
||||
const handlePlanModeToggle = useCallback(() => {
|
||||
onContextChange?.({
|
||||
...context,
|
||||
is_plan_mode: !context.is_plan_mode,
|
||||
});
|
||||
}, [onContextChange, context]);
|
||||
const handleSubmit = useCallback(
|
||||
async (message: PromptInputMessage) => {
|
||||
if (status === "streaming") {
|
||||
@@ -89,7 +97,6 @@ export function InputBox({
|
||||
<PromptInput
|
||||
className={cn(
|
||||
"bg-background/85 rounded-2xl backdrop-blur-sm transition-all duration-300 ease-out *:data-[slot='input-group']:rounded-2xl",
|
||||
"-translate-y-4 overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
globalDrop
|
||||
@@ -97,6 +104,11 @@ export function InputBox({
|
||||
onSubmit={handleSubmit}
|
||||
{...props}
|
||||
>
|
||||
{extraHeader && (
|
||||
<div className="absolute top-0 right-0 left-0 z-100">
|
||||
<div className="absolute right-0 bottom-0 left-0">{extraHeader}</div>
|
||||
</div>
|
||||
)}
|
||||
<PromptInputBody>
|
||||
<PromptInputTextarea
|
||||
className={cn("size-full")}
|
||||
@@ -105,7 +117,7 @@ export function InputBox({
|
||||
/>
|
||||
</PromptInputBody>
|
||||
<PromptInputFooter className="flex">
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<Tooltip
|
||||
content={
|
||||
context.thinking_enabled ? (
|
||||
@@ -131,7 +143,7 @@ export function InputBox({
|
||||
{context.thinking_enabled ? (
|
||||
<LightbulbIcon className="text-primary size-4" />
|
||||
) : (
|
||||
<LightbulbOffIcon className="size-4" />
|
||||
<LightbulbIcon className="size-4" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
@@ -147,6 +159,47 @@ export function InputBox({
|
||||
</PromptInputButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
context.is_plan_mode ? (
|
||||
<div className="tex-sm flex flex-col gap-1">
|
||||
<div>{t.inputBox.planMode}</div>
|
||||
<div className="opacity-50">
|
||||
{t.inputBox.clickToDisablePlanMode}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="tex-sm flex flex-col gap-1">
|
||||
<div>{t.inputBox.planMode}</div>
|
||||
<div className="opacity-50">
|
||||
{t.inputBox.clickToEnablePlanMode}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
{selectedModel?.supports_thinking && (
|
||||
<PromptInputButton onClick={handlePlanModeToggle}>
|
||||
<>
|
||||
{context.is_plan_mode ? (
|
||||
<ListTodoIcon className="text-primary size-4" />
|
||||
) : (
|
||||
<ListTodoIcon className="size-4" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-normal",
|
||||
context.is_plan_mode
|
||||
? "text-primary"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{t.inputBox.planMode}
|
||||
</span>
|
||||
</>
|
||||
</PromptInputButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ModelSelector
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { Message } from "@langchain/langgraph-sdk";
|
||||
import {
|
||||
BookOpenTextIcon,
|
||||
ChevronUp,
|
||||
FileTextIcon,
|
||||
FolderOpenIcon,
|
||||
GlobeIcon,
|
||||
LightbulbIcon,
|
||||
ListTodoIcon,
|
||||
MessageCircleQuestionMarkIcon,
|
||||
NotebookPenIcon,
|
||||
SearchIcon,
|
||||
@@ -115,12 +115,17 @@ export function MessageGroup({
|
||||
}
|
||||
></ChainOfThoughtStep>
|
||||
) : (
|
||||
<ToolCall key={step.id} {...step} />
|
||||
<ToolCall key={step.id} {...step} isLoading={isLoading} />
|
||||
),
|
||||
)}
|
||||
{lastToolCallStep && (
|
||||
<FlipDisplay uniqueKey={lastToolCallStep.id ?? ""}>
|
||||
<ToolCall key={lastToolCallStep.id} {...lastToolCallStep} />
|
||||
<ToolCall
|
||||
key={lastToolCallStep.id}
|
||||
{...lastToolCallStep}
|
||||
isLast={true}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</FlipDisplay>
|
||||
)}
|
||||
</ChainOfThoughtContent>
|
||||
@@ -173,15 +178,20 @@ function ToolCall({
|
||||
name,
|
||||
args,
|
||||
result,
|
||||
isLast = false,
|
||||
isLoading = false,
|
||||
}: {
|
||||
id?: string;
|
||||
messageId?: string;
|
||||
name: string;
|
||||
args: Record<string, unknown>;
|
||||
result?: string | Record<string, unknown>;
|
||||
isLast?: boolean;
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { setOpen, autoOpen, selectedArtifact, select } = useArtifacts();
|
||||
const { setOpen, autoOpen, autoSelect, selectedArtifact, select } =
|
||||
useArtifacts();
|
||||
if (name === "web_search") {
|
||||
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
|
||||
if (typeof args.query === "string") {
|
||||
@@ -265,7 +275,7 @@ function ToolCall({
|
||||
description = t.toolCalls.writeFile;
|
||||
}
|
||||
const path: string | undefined = (args as { path: string })?.path;
|
||||
if (autoOpen && path) {
|
||||
if (isLoading && isLast && autoOpen && autoSelect && path) {
|
||||
setTimeout(() => {
|
||||
const url = new URL(
|
||||
`write-file:${path}?message_id=${messageId}&tool_call_id=${id}`,
|
||||
@@ -273,7 +283,7 @@ function ToolCall({
|
||||
if (selectedArtifact === url) {
|
||||
return;
|
||||
}
|
||||
select(url);
|
||||
select(url, true);
|
||||
setOpen(true);
|
||||
}, 100);
|
||||
}
|
||||
@@ -320,27 +330,7 @@ function ToolCall({
|
||||
)}
|
||||
</ChainOfThoughtStep>
|
||||
);
|
||||
} else if (name === "present_files") {
|
||||
return (
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
label={t.toolCalls.presentFiles}
|
||||
icon={FileTextIcon}
|
||||
>
|
||||
<ChainOfThoughtSearchResult>
|
||||
{Array.isArray((args as { filepaths: string[] }).filepaths) &&
|
||||
(args as { filepaths: string[] }).filepaths.map(
|
||||
(filepath: string) => (
|
||||
<ChainOfThoughtSearchResult key={filepath}>
|
||||
{filepath}
|
||||
</ChainOfThoughtSearchResult>
|
||||
),
|
||||
)}
|
||||
</ChainOfThoughtSearchResult>
|
||||
</ChainOfThoughtStep>
|
||||
);
|
||||
}
|
||||
if (name === "ask_clarification") {
|
||||
} else if (name === "ask_clarification") {
|
||||
return (
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
@@ -348,6 +338,14 @@ function ToolCall({
|
||||
icon={MessageCircleQuestionMarkIcon}
|
||||
></ChainOfThoughtStep>
|
||||
);
|
||||
} else if (name === "write_todos") {
|
||||
return (
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
label={t.toolCalls.writeTodos}
|
||||
icon={ListTodoIcon}
|
||||
></ChainOfThoughtStep>
|
||||
);
|
||||
} else {
|
||||
const description: string | undefined = (args as { description: string })
|
||||
?.description;
|
||||
|
||||
@@ -23,10 +23,12 @@ export function MessageList({
|
||||
className,
|
||||
threadId,
|
||||
thread,
|
||||
paddingBottom = 160,
|
||||
}: {
|
||||
className?: string;
|
||||
threadId: string;
|
||||
thread: UseStream<AgentThreadState>;
|
||||
paddingBottom?: number;
|
||||
}) {
|
||||
if (thread.isThreadLoading) {
|
||||
return <MessageListSkeleton />;
|
||||
@@ -70,7 +72,7 @@ export function MessageList({
|
||||
);
|
||||
})}
|
||||
{thread.isLoading && <StreamingIndicator className="my-4" />}
|
||||
<div className="h-40" />
|
||||
<div style={{ height: `${paddingBottom}px` }} />
|
||||
</ConversationContent>
|
||||
</Conversation>
|
||||
);
|
||||
|
||||
68
frontend/src/components/workspace/todo-list.tsx
Normal file
68
frontend/src/components/workspace/todo-list.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ChevronUpIcon } from "lucide-react";
|
||||
|
||||
import type { Todo } from "@/core/todos";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import {
|
||||
QueueItem,
|
||||
QueueItemContent,
|
||||
QueueItemIndicator,
|
||||
QueueList,
|
||||
} from "../ai-elements/queue";
|
||||
|
||||
export function TodoList({
|
||||
className,
|
||||
todos,
|
||||
collapsed = false,
|
||||
onToggle,
|
||||
}: {
|
||||
className?: string;
|
||||
todos: Todo[];
|
||||
collapsed?: boolean;
|
||||
onToggle?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-fit w-full flex-col overflow-hidden rounded-t-xl border bg-white backdrop-blur-sm",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<header
|
||||
className="bg-accent flex min-h-8 shrink-0 cursor-pointer items-center justify-between px-4 text-sm"
|
||||
onClick={() => {
|
||||
onToggle?.();
|
||||
}}
|
||||
>
|
||||
<div className="text-muted-foreground">To-dos</div>
|
||||
<div>
|
||||
<ChevronUpIcon
|
||||
className={cn(
|
||||
"text-muted-foreground size-4 transition-transform duration-300 ease-out",
|
||||
collapsed ? "" : "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<main
|
||||
className={cn(
|
||||
"bg-accent flex grow px-2 transition-all duration-300 ease-out",
|
||||
collapsed ? "h-0" : "h-28",
|
||||
)}
|
||||
>
|
||||
<QueueList className="bg-background mt-0 w-full rounded-t-xl">
|
||||
{todos.map((todo, i) => (
|
||||
<QueueItem key={i + (todo.content ?? "")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<QueueItemIndicator completed={todo.status === "completed"} />
|
||||
<QueueItemContent completed={todo.status === "completed"}>
|
||||
{todo.content}
|
||||
</QueueItemContent>
|
||||
</div>
|
||||
</QueueItem>
|
||||
))}
|
||||
</QueueList>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user