mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-19 12:24:46 +08:00
feat: add Todos
This commit is contained in:
@@ -28,7 +28,7 @@ export default function ChatLayout({
|
|||||||
color={
|
color={
|
||||||
resolvedTheme === "dark" ? "#60A5FA" : "oklch(0 0.0098 87.47)"
|
resolvedTheme === "dark" ? "#60A5FA" : "oklch(0 0.0098 87.47)"
|
||||||
}
|
}
|
||||||
maxOpacity={resolvedTheme === "dark" ? 0.04 : 0.03}
|
maxOpacity={resolvedTheme === "dark" ? 0.04 : 0.025}
|
||||||
flickerChance={0.1}
|
flickerChance={0.1}
|
||||||
/>
|
/>
|
||||||
<FlickeringGrid
|
<FlickeringGrid
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { InputBox } from "@/components/workspace/input-box";
|
|||||||
import { MessageList } from "@/components/workspace/messages";
|
import { MessageList } from "@/components/workspace/messages";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||||
|
import { TodoList } from "@/components/workspace/todo-list";
|
||||||
import { Tooltip } from "@/components/workspace/tooltip";
|
import { Tooltip } from "@/components/workspace/tooltip";
|
||||||
import { Welcome } from "@/components/workspace/welcome";
|
import { Welcome } from "@/components/workspace/welcome";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
@@ -75,6 +76,8 @@ export default function ChatPage() {
|
|||||||
setArtifacts(thread.values.artifacts);
|
setArtifacts(thread.values.artifacts);
|
||||||
}, [setArtifacts, thread.values.artifacts]);
|
}, [setArtifacts, thread.values.artifacts]);
|
||||||
|
|
||||||
|
const [todoListCollapsed, setTodoListCollapsed] = useState(true);
|
||||||
|
|
||||||
const handleSubmit = useSubmitThread({
|
const handleSubmit = useSubmitThread({
|
||||||
isNewThread,
|
isNewThread,
|
||||||
threadId,
|
threadId,
|
||||||
@@ -137,6 +140,7 @@ export default function ChatPage() {
|
|||||||
className="size-full"
|
className="size-full"
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
thread={thread}
|
thread={thread}
|
||||||
|
paddingBottom={todoListCollapsed ? 160 : 280}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
||||||
@@ -149,19 +153,34 @@ export default function ChatPage() {
|
|||||||
: "max-w-(--container-width-md)",
|
: "max-w-(--container-width-md)",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
{isNewThread && (
|
||||||
className={cn(
|
<div
|
||||||
"absolute right-0 bottom-[136px] left-0 flex",
|
className={cn(
|
||||||
isNewThread ? "" : "pointer-events-none opacity-0",
|
"absolute right-0 bottom-[136px] left-0 flex",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Welcome />
|
<Welcome />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<InputBox
|
<InputBox
|
||||||
className={cn("bg-background/5 w-full")}
|
className={cn("bg-background/5 w-full -translate-y-4")}
|
||||||
autoFocus={isNewThread}
|
autoFocus={isNewThread}
|
||||||
status={thread.isLoading ? "streaming" : "ready"}
|
status={thread.isLoading ? "streaming" : "ready"}
|
||||||
context={settings.context}
|
context={settings.context}
|
||||||
|
extraHeader={
|
||||||
|
thread.values.todos?.length ? (
|
||||||
|
<div className="mx-4">
|
||||||
|
<TodoList
|
||||||
|
className="bg-background/5"
|
||||||
|
todos={thread.values.todos ?? []}
|
||||||
|
collapsed={todoListCollapsed}
|
||||||
|
onToggle={() =>
|
||||||
|
setTodoListCollapsed(!todoListCollapsed)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
onContextChange={(context) =>
|
onContextChange={(context) =>
|
||||||
setSettings("context", context)
|
setSettings("context", context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -789,7 +789,7 @@ export const PromptInput = ({
|
|||||||
ref={formRef}
|
ref={formRef}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<InputGroup className="overflow-hidden">{children}</InputGroup>
|
<InputGroup>{children}</InputGroup>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ export interface ArtifactsContextType {
|
|||||||
setArtifacts: (artifacts: string[]) => void;
|
setArtifacts: (artifacts: string[]) => void;
|
||||||
|
|
||||||
selectedArtifact: string | null;
|
selectedArtifact: string | null;
|
||||||
|
autoSelect: boolean;
|
||||||
|
select: (artifact: string, autoSelect?: boolean) => void;
|
||||||
|
deselect: () => void;
|
||||||
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
autoOpen: boolean;
|
autoOpen: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
deselect: () => void;
|
|
||||||
|
|
||||||
select: (artifact: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArtifactsContext = createContext<ArtifactsContextType | undefined>(
|
const ArtifactsContext = createContext<ArtifactsContextType | undefined>(
|
||||||
@@ -27,17 +27,22 @@ interface ArtifactsProviderProps {
|
|||||||
export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||||
const [artifacts, setArtifacts] = useState<string[]>([]);
|
const [artifacts, setArtifacts] = useState<string[]>([]);
|
||||||
const [selectedArtifact, setSelectedArtifact] = useState<string | null>(null);
|
const [selectedArtifact, setSelectedArtifact] = useState<string | null>(null);
|
||||||
|
const [autoSelect, setAutoSelect] = useState(true);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [autoOpen, setAutoOpen] = useState(true);
|
const [autoOpen, setAutoOpen] = useState(true);
|
||||||
const { setOpen: setSidebarOpen } = useSidebar();
|
const { setOpen: setSidebarOpen } = useSidebar();
|
||||||
|
|
||||||
const select = (artifact: string) => {
|
const select = (artifact: string, autoSelect = false) => {
|
||||||
setSelectedArtifact(artifact);
|
setSelectedArtifact(artifact);
|
||||||
setSidebarOpen(false);
|
setSidebarOpen(false);
|
||||||
|
if (!autoSelect) {
|
||||||
|
setAutoSelect(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deselect = () => {
|
const deselect = () => {
|
||||||
setSelectedArtifact(null);
|
setSelectedArtifact(null);
|
||||||
|
setAutoSelect(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const value: ArtifactsContextType = {
|
const value: ArtifactsContextType = {
|
||||||
@@ -46,9 +51,11 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
|||||||
|
|
||||||
open,
|
open,
|
||||||
autoOpen,
|
autoOpen,
|
||||||
|
autoSelect,
|
||||||
setOpen: (isOpen: boolean) => {
|
setOpen: (isOpen: boolean) => {
|
||||||
if (!isOpen && autoOpen) {
|
if (!isOpen && autoOpen) {
|
||||||
setAutoOpen(false);
|
setAutoOpen(false);
|
||||||
|
setAutoSelect(false);
|
||||||
}
|
}
|
||||||
setOpen(isOpen);
|
setOpen(isOpen);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ChatStatus } from "ai";
|
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 { useCallback, useMemo, useState, type ComponentProps } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +35,7 @@ export function InputBox({
|
|||||||
autoFocus,
|
autoFocus,
|
||||||
status = "ready",
|
status = "ready",
|
||||||
context,
|
context,
|
||||||
|
extraHeader,
|
||||||
onContextChange,
|
onContextChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onStop,
|
onStop,
|
||||||
@@ -43,6 +44,7 @@ export function InputBox({
|
|||||||
assistantId?: string | null;
|
assistantId?: string | null;
|
||||||
status?: ChatStatus;
|
status?: ChatStatus;
|
||||||
context: Omit<AgentThreadContext, "thread_id">;
|
context: Omit<AgentThreadContext, "thread_id">;
|
||||||
|
extraHeader?: React.ReactNode;
|
||||||
onContextChange?: (context: Omit<AgentThreadContext, "thread_id">) => void;
|
onContextChange?: (context: Omit<AgentThreadContext, "thread_id">) => void;
|
||||||
onSubmit?: (message: PromptInputMessage) => void;
|
onSubmit?: (message: PromptInputMessage) => void;
|
||||||
onStop?: () => void;
|
onStop?: () => void;
|
||||||
@@ -72,6 +74,12 @@ export function InputBox({
|
|||||||
thinking_enabled: !context.thinking_enabled,
|
thinking_enabled: !context.thinking_enabled,
|
||||||
});
|
});
|
||||||
}, [onContextChange, context]);
|
}, [onContextChange, context]);
|
||||||
|
const handlePlanModeToggle = useCallback(() => {
|
||||||
|
onContextChange?.({
|
||||||
|
...context,
|
||||||
|
is_plan_mode: !context.is_plan_mode,
|
||||||
|
});
|
||||||
|
}, [onContextChange, context]);
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (message: PromptInputMessage) => {
|
async (message: PromptInputMessage) => {
|
||||||
if (status === "streaming") {
|
if (status === "streaming") {
|
||||||
@@ -89,7 +97,6 @@ export function InputBox({
|
|||||||
<PromptInput
|
<PromptInput
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background/85 rounded-2xl backdrop-blur-sm transition-all duration-300 ease-out *:data-[slot='input-group']:rounded-2xl",
|
"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,
|
className,
|
||||||
)}
|
)}
|
||||||
globalDrop
|
globalDrop
|
||||||
@@ -97,6 +104,11 @@ export function InputBox({
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
{...props}
|
{...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>
|
<PromptInputBody>
|
||||||
<PromptInputTextarea
|
<PromptInputTextarea
|
||||||
className={cn("size-full")}
|
className={cn("size-full")}
|
||||||
@@ -105,7 +117,7 @@ export function InputBox({
|
|||||||
/>
|
/>
|
||||||
</PromptInputBody>
|
</PromptInputBody>
|
||||||
<PromptInputFooter className="flex">
|
<PromptInputFooter className="flex">
|
||||||
<div>
|
<div className="flex items-center">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
context.thinking_enabled ? (
|
context.thinking_enabled ? (
|
||||||
@@ -131,7 +143,7 @@ export function InputBox({
|
|||||||
{context.thinking_enabled ? (
|
{context.thinking_enabled ? (
|
||||||
<LightbulbIcon className="text-primary size-4" />
|
<LightbulbIcon className="text-primary size-4" />
|
||||||
) : (
|
) : (
|
||||||
<LightbulbOffIcon className="size-4" />
|
<LightbulbIcon className="size-4" />
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -147,6 +159,47 @@ export function InputBox({
|
|||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ModelSelector
|
<ModelSelector
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import type { Message } from "@langchain/langgraph-sdk";
|
|||||||
import {
|
import {
|
||||||
BookOpenTextIcon,
|
BookOpenTextIcon,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
FileTextIcon,
|
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
LightbulbIcon,
|
LightbulbIcon,
|
||||||
|
ListTodoIcon,
|
||||||
MessageCircleQuestionMarkIcon,
|
MessageCircleQuestionMarkIcon,
|
||||||
NotebookPenIcon,
|
NotebookPenIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
@@ -115,12 +115,17 @@ export function MessageGroup({
|
|||||||
}
|
}
|
||||||
></ChainOfThoughtStep>
|
></ChainOfThoughtStep>
|
||||||
) : (
|
) : (
|
||||||
<ToolCall key={step.id} {...step} />
|
<ToolCall key={step.id} {...step} isLoading={isLoading} />
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
{lastToolCallStep && (
|
{lastToolCallStep && (
|
||||||
<FlipDisplay uniqueKey={lastToolCallStep.id ?? ""}>
|
<FlipDisplay uniqueKey={lastToolCallStep.id ?? ""}>
|
||||||
<ToolCall key={lastToolCallStep.id} {...lastToolCallStep} />
|
<ToolCall
|
||||||
|
key={lastToolCallStep.id}
|
||||||
|
{...lastToolCallStep}
|
||||||
|
isLast={true}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
</FlipDisplay>
|
</FlipDisplay>
|
||||||
)}
|
)}
|
||||||
</ChainOfThoughtContent>
|
</ChainOfThoughtContent>
|
||||||
@@ -173,15 +178,20 @@ function ToolCall({
|
|||||||
name,
|
name,
|
||||||
args,
|
args,
|
||||||
result,
|
result,
|
||||||
|
isLast = false,
|
||||||
|
isLoading = false,
|
||||||
}: {
|
}: {
|
||||||
id?: string;
|
id?: string;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
name: string;
|
name: string;
|
||||||
args: Record<string, unknown>;
|
args: Record<string, unknown>;
|
||||||
result?: string | Record<string, unknown>;
|
result?: string | Record<string, unknown>;
|
||||||
|
isLast?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { setOpen, autoOpen, selectedArtifact, select } = useArtifacts();
|
const { setOpen, autoOpen, autoSelect, selectedArtifact, select } =
|
||||||
|
useArtifacts();
|
||||||
if (name === "web_search") {
|
if (name === "web_search") {
|
||||||
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
|
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
|
||||||
if (typeof args.query === "string") {
|
if (typeof args.query === "string") {
|
||||||
@@ -265,7 +275,7 @@ function ToolCall({
|
|||||||
description = t.toolCalls.writeFile;
|
description = t.toolCalls.writeFile;
|
||||||
}
|
}
|
||||||
const path: string | undefined = (args as { path: string })?.path;
|
const path: string | undefined = (args as { path: string })?.path;
|
||||||
if (autoOpen && path) {
|
if (isLoading && isLast && autoOpen && autoSelect && path) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const url = new URL(
|
const url = new URL(
|
||||||
`write-file:${path}?message_id=${messageId}&tool_call_id=${id}`,
|
`write-file:${path}?message_id=${messageId}&tool_call_id=${id}`,
|
||||||
@@ -273,7 +283,7 @@ function ToolCall({
|
|||||||
if (selectedArtifact === url) {
|
if (selectedArtifact === url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
select(url);
|
select(url, true);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
@@ -320,27 +330,7 @@ function ToolCall({
|
|||||||
)}
|
)}
|
||||||
</ChainOfThoughtStep>
|
</ChainOfThoughtStep>
|
||||||
);
|
);
|
||||||
} else if (name === "present_files") {
|
} else if (name === "ask_clarification") {
|
||||||
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") {
|
|
||||||
return (
|
return (
|
||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
key={id}
|
key={id}
|
||||||
@@ -348,6 +338,14 @@ function ToolCall({
|
|||||||
icon={MessageCircleQuestionMarkIcon}
|
icon={MessageCircleQuestionMarkIcon}
|
||||||
></ChainOfThoughtStep>
|
></ChainOfThoughtStep>
|
||||||
);
|
);
|
||||||
|
} else if (name === "write_todos") {
|
||||||
|
return (
|
||||||
|
<ChainOfThoughtStep
|
||||||
|
key={id}
|
||||||
|
label={t.toolCalls.writeTodos}
|
||||||
|
icon={ListTodoIcon}
|
||||||
|
></ChainOfThoughtStep>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const description: string | undefined = (args as { description: string })
|
const description: string | undefined = (args as { description: string })
|
||||||
?.description;
|
?.description;
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ export function MessageList({
|
|||||||
className,
|
className,
|
||||||
threadId,
|
threadId,
|
||||||
thread,
|
thread,
|
||||||
|
paddingBottom = 160,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
thread: UseStream<AgentThreadState>;
|
thread: UseStream<AgentThreadState>;
|
||||||
|
paddingBottom?: number;
|
||||||
}) {
|
}) {
|
||||||
if (thread.isThreadLoading) {
|
if (thread.isThreadLoading) {
|
||||||
return <MessageListSkeleton />;
|
return <MessageListSkeleton />;
|
||||||
@@ -70,7 +72,7 @@ export function MessageList({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{thread.isLoading && <StreamingIndicator className="my-4" />}
|
{thread.isLoading && <StreamingIndicator className="my-4" />}
|
||||||
<div className="h-40" />
|
<div style={{ height: `${paddingBottom}px` }} />
|
||||||
</ConversationContent>
|
</ConversationContent>
|
||||||
</Conversation>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -41,6 +41,11 @@ export const enUS: Translations = {
|
|||||||
thinkingDisabled: "Thinking is disabled",
|
thinkingDisabled: "Thinking is disabled",
|
||||||
clickToDisableThinking: "Click to disable thinking",
|
clickToDisableThinking: "Click to disable thinking",
|
||||||
clickToEnableThinking: "Click to enable thinking",
|
clickToEnableThinking: "Click to enable thinking",
|
||||||
|
planMode: "Plan mode",
|
||||||
|
planModeEnabled: "Plan mode is enabled",
|
||||||
|
planModeDisabled: "Plan mode is disabled",
|
||||||
|
clickToDisablePlanMode: "Click to disable plan mode",
|
||||||
|
clickToEnablePlanMode: "Click to enable plan mode",
|
||||||
searchModels: "Search models...",
|
searchModels: "Search models...",
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -87,6 +92,7 @@ export const enUS: Translations = {
|
|||||||
listFolder: "List folder",
|
listFolder: "List folder",
|
||||||
readFile: "Read file",
|
readFile: "Read file",
|
||||||
writeFile: "Write file",
|
writeFile: "Write file",
|
||||||
|
writeTodos: "Update to-do list",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export interface Translations {
|
|||||||
thinkingDisabled: string;
|
thinkingDisabled: string;
|
||||||
clickToDisableThinking: string;
|
clickToDisableThinking: string;
|
||||||
clickToEnableThinking: string;
|
clickToEnableThinking: string;
|
||||||
|
planMode: string;
|
||||||
|
planModeEnabled: string;
|
||||||
|
planModeDisabled: string;
|
||||||
|
clickToDisablePlanMode: string;
|
||||||
|
clickToEnablePlanMode: string;
|
||||||
searchModels: string;
|
searchModels: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,6 +89,7 @@ export interface Translations {
|
|||||||
listFolder: string;
|
listFolder: string;
|
||||||
readFile: string;
|
readFile: string;
|
||||||
writeFile: string;
|
writeFile: string;
|
||||||
|
writeTodos: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ export const zhCN: Translations = {
|
|||||||
thinkingDisabled: "思考功能已禁用",
|
thinkingDisabled: "思考功能已禁用",
|
||||||
clickToDisableThinking: "点击禁用思考功能",
|
clickToDisableThinking: "点击禁用思考功能",
|
||||||
clickToEnableThinking: "点击启用思考功能",
|
clickToEnableThinking: "点击启用思考功能",
|
||||||
|
planMode: "To-do 模式",
|
||||||
|
planModeEnabled: "To-do 模式已启用",
|
||||||
|
planModeDisabled: "To-do 模式已禁用",
|
||||||
|
clickToDisablePlanMode: "点击禁用 To-do 模式",
|
||||||
|
clickToEnablePlanMode: "点击启用 To-do 模式",
|
||||||
searchModels: "搜索模型...",
|
searchModels: "搜索模型...",
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -87,6 +92,7 @@ export const zhCN: Translations = {
|
|||||||
listFolder: "列出文件夹",
|
listFolder: "列出文件夹",
|
||||||
readFile: "读取文件",
|
readFile: "读取文件",
|
||||||
writeFile: "写入文件",
|
writeFile: "写入文件",
|
||||||
|
writeTodos: "更新 To-do 列表",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
|
|||||||
@@ -85,29 +85,6 @@ export function groupMessages<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (!isLoading) {
|
|
||||||
// const lastGroup: MessageGroup | undefined = groups[groups.length - 1];
|
|
||||||
// if (
|
|
||||||
// lastGroup?.type === "assistant:processing" &&
|
|
||||||
// lastGroup.messages.length > 0
|
|
||||||
// ) {
|
|
||||||
// const lastMessage = lastGroup.messages[lastGroup.messages.length - 1]!;
|
|
||||||
// const reasoningContent = extractReasoningContentFromMessage(lastMessage);
|
|
||||||
// const content = extractContentFromMessage(lastMessage);
|
|
||||||
// if (reasoningContent && !content) {
|
|
||||||
// lastGroup.messages.pop();
|
|
||||||
// if (lastGroup.messages.length === 0) {
|
|
||||||
// groups.pop();
|
|
||||||
// }
|
|
||||||
// groups.push({
|
|
||||||
// id: lastMessage.id,
|
|
||||||
// type: "assistant",
|
|
||||||
// messages: [lastMessage],
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const resultsOfGroups: T[] = [];
|
const resultsOfGroups: T[] = [];
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
const resultOfGroup = mapper(group);
|
const resultOfGroup = mapper(group);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
|
|||||||
context: {
|
context: {
|
||||||
model_name: "deepseek-v3.2",
|
model_name: "deepseek-v3.2",
|
||||||
thinking_enabled: true,
|
thinking_enabled: true,
|
||||||
|
is_plan_mode: true,
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
sidebar_collapsed: false,
|
sidebar_collapsed: false,
|
||||||
@@ -27,10 +28,18 @@ export function getLocalSettings(): LocalSettings {
|
|||||||
try {
|
try {
|
||||||
if (json) {
|
if (json) {
|
||||||
const settings = JSON.parse(json);
|
const settings = JSON.parse(json);
|
||||||
return {
|
const mergedSettings = {
|
||||||
...DEFAULT_LOCAL_SETTINGS,
|
...DEFAULT_LOCAL_SETTINGS,
|
||||||
...settings,
|
context: {
|
||||||
|
...DEFAULT_LOCAL_SETTINGS.context,
|
||||||
|
...settings.context,
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
...DEFAULT_LOCAL_SETTINGS.layout,
|
||||||
|
...settings.layout,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
return mergedSettings;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
return DEFAULT_LOCAL_SETTINGS;
|
return DEFAULT_LOCAL_SETTINGS;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { type BaseMessage } from "@langchain/core/messages";
|
import { type BaseMessage } from "@langchain/core/messages";
|
||||||
import type { Thread } from "@langchain/langgraph-sdk";
|
import type { Thread } from "@langchain/langgraph-sdk";
|
||||||
|
|
||||||
|
import type { Todo } from "../todos";
|
||||||
|
|
||||||
export interface AgentThreadState extends Record<string, unknown> {
|
export interface AgentThreadState extends Record<string, unknown> {
|
||||||
title: string;
|
title: string;
|
||||||
messages: BaseMessage[];
|
messages: BaseMessage[];
|
||||||
artifacts: string[];
|
artifacts: string[];
|
||||||
|
todos?: Todo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentThread extends Thread<AgentThreadState> {}
|
export interface AgentThread extends Thread<AgentThreadState> {}
|
||||||
@@ -13,4 +16,5 @@ export interface AgentThreadContext extends Record<string, unknown> {
|
|||||||
thread_id: string;
|
thread_id: string;
|
||||||
model_name: string;
|
model_name: string;
|
||||||
thinking_enabled: boolean;
|
thinking_enabled: boolean;
|
||||||
|
is_plan_mode: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
1
frontend/src/core/todos/index.ts
Normal file
1
frontend/src/core/todos/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./types";
|
||||||
4
frontend/src/core/todos/types.ts
Normal file
4
frontend/src/core/todos/types.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Todo {
|
||||||
|
content?: string;
|
||||||
|
status?: "pending" | "in_progress" | "completed";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user