mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-03 06:12:14 +08:00
feat: add ultra mode
This commit is contained in:
@@ -177,7 +177,8 @@ export default function ChatPage() {
|
||||
threadContext: {
|
||||
...settings.context,
|
||||
thinking_enabled: settings.context.mode !== "flash",
|
||||
is_plan_mode: settings.context.mode === "pro",
|
||||
is_plan_mode: settings.context.mode === "pro" || settings.context.mode === "ultra",
|
||||
subagent_enabled: settings.context.mode === "ultra",
|
||||
},
|
||||
afterSubmit() {
|
||||
router.push(pathOfThread(threadId!));
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PaperclipIcon,
|
||||
PlusIcon,
|
||||
SparklesIcon,
|
||||
RocketIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
@@ -80,9 +81,9 @@ export function InputBox({
|
||||
disabled?: boolean;
|
||||
context: Omit<
|
||||
AgentThreadContext,
|
||||
"thread_id" | "is_plan_mode" | "thinking_enabled"
|
||||
"thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled"
|
||||
> & {
|
||||
mode: "flash" | "thinking" | "pro" | undefined;
|
||||
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
|
||||
};
|
||||
extraHeader?: React.ReactNode;
|
||||
isNewThread?: boolean;
|
||||
@@ -90,9 +91,9 @@ export function InputBox({
|
||||
onContextChange?: (
|
||||
context: Omit<
|
||||
AgentThreadContext,
|
||||
"thread_id" | "is_plan_mode" | "thinking_enabled"
|
||||
"thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled"
|
||||
> & {
|
||||
mode: "flash" | "thinking" | "pro" | undefined;
|
||||
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
|
||||
},
|
||||
) => void;
|
||||
onSubmit?: (message: PromptInputMessage) => void;
|
||||
@@ -131,7 +132,7 @@ export function InputBox({
|
||||
[onContextChange, context],
|
||||
);
|
||||
const handleModeSelect = useCallback(
|
||||
(mode: "flash" | "thinking" | "pro") => {
|
||||
(mode: "flash" | "thinking" | "pro" | "ultra") => {
|
||||
onContextChange?.({
|
||||
...context,
|
||||
mode,
|
||||
@@ -205,11 +206,15 @@ export function InputBox({
|
||||
{context.mode === "pro" && (
|
||||
<GraduationCapIcon className="size-3" />
|
||||
)}
|
||||
{context.mode === "ultra" && (
|
||||
<RocketIcon className="size-3" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs font-normal">
|
||||
{(context.mode === "flash" && t.inputBox.flashMode) ||
|
||||
(context.mode === "thinking" && t.inputBox.reasoningMode) ||
|
||||
(context.mode === "pro" && t.inputBox.proMode)}
|
||||
(context.mode === "pro" && t.inputBox.proMode) ||
|
||||
(context.mode === "ultra" && t.inputBox.ultraMode)}
|
||||
</div>
|
||||
</PromptInputActionMenuTrigger>
|
||||
<PromptInputActionMenuContent className="w-80">
|
||||
@@ -306,6 +311,34 @@ export function InputBox({
|
||||
<div className="ml-auto size-4" />
|
||||
)}
|
||||
</PromptInputActionMenuItem>
|
||||
<PromptInputActionMenuItem
|
||||
className={cn(
|
||||
context.mode === "ultra"
|
||||
? "text-accent-foreground"
|
||||
: "text-muted-foreground/65",
|
||||
)}
|
||||
onSelect={() => handleModeSelect("ultra")}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-1 font-bold">
|
||||
<RocketIcon
|
||||
className={cn(
|
||||
"mr-2 size-4",
|
||||
context.mode === "ultra" && "text-accent-foreground",
|
||||
)}
|
||||
/>
|
||||
{t.inputBox.ultraMode}
|
||||
</div>
|
||||
<div className="pl-7 text-xs">
|
||||
{t.inputBox.ultraModeDescription}
|
||||
</div>
|
||||
</div>
|
||||
{context.mode === "ultra" ? (
|
||||
<CheckIcon className="ml-auto size-4" />
|
||||
) : (
|
||||
<div className="ml-auto size-4" />
|
||||
)}
|
||||
</PromptInputActionMenuItem>
|
||||
</PromptInputActionMenu>
|
||||
</DropdownMenuGroup>
|
||||
</PromptInputActionMenuContent>
|
||||
|
||||
117
frontend/src/components/workspace/subagent-card.tsx
Normal file
117
frontend/src/components/workspace/subagent-card.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import { CheckCircleIcon, Loader2Icon, SquareTerminalIcon, WrenchIcon, XCircleIcon } from "lucide-react";
|
||||
|
||||
import { MessageResponse } from "@/components/ai-elements/message";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import type { SubagentState } from "@/core/threads/types";
|
||||
|
||||
interface SubagentCardProps {
|
||||
subagentType: string;
|
||||
state?: SubagentState;
|
||||
isLoading?: boolean;
|
||||
prompt?: string;
|
||||
}
|
||||
|
||||
export function SubagentCard({ subagentType, state, isLoading, prompt }: SubagentCardProps) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const getSubagentIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "bash":
|
||||
return SquareTerminalIcon;
|
||||
case "general-purpose":
|
||||
return WrenchIcon;
|
||||
default:
|
||||
return WrenchIcon;
|
||||
}
|
||||
};
|
||||
|
||||
const getSubagentLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case "bash":
|
||||
return t.subagents.bash;
|
||||
case "general-purpose":
|
||||
return t.subagents.generalPurpose;
|
||||
default:
|
||||
return t.subagents.unknown;
|
||||
}
|
||||
};
|
||||
|
||||
const IconComponent = getSubagentIcon(subagentType);
|
||||
const label = getSubagentLabel(subagentType);
|
||||
|
||||
// Determine status based on state, not isLoading
|
||||
const status = state?.status || "running";
|
||||
const isRunning = status === "running";
|
||||
const isCompleted = status === "completed";
|
||||
const isFailed = status === "failed";
|
||||
|
||||
const getStatusIcon = () => {
|
||||
if (isCompleted) {
|
||||
return <CheckCircleIcon className="size-4 text-green-600" />;
|
||||
}
|
||||
if (isFailed) {
|
||||
return <XCircleIcon className="size-4 text-red-600" />;
|
||||
}
|
||||
if (isRunning) {
|
||||
return <Loader2Icon className="size-4 animate-spin text-blue-600" />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const borderColorClass = isCompleted
|
||||
? "border-green-200 bg-green-50/30"
|
||||
: isFailed
|
||||
? "border-red-200 bg-red-50/30"
|
||||
: "border-blue-200 bg-blue-50/30";
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"rounded-lg border-l-2 p-4 transition-colors space-y-3",
|
||||
borderColorClass
|
||||
)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="mt-0.5 flex-shrink-0">
|
||||
<IconComponent className="size-4" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">{label}</span>
|
||||
{getStatusIcon()}
|
||||
</div>
|
||||
{prompt && (
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{prompt}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status message for running state */}
|
||||
{isRunning && !state?.result && (
|
||||
<div className="text-sm text-muted-foreground ml-6">
|
||||
{t.subagents.running}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result */}
|
||||
{state?.result && (
|
||||
<div className="ml-6 text-sm">
|
||||
<MessageResponse>{state.result}</MessageResponse>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{state?.status === "failed" && state.error && (
|
||||
<div className="ml-6 rounded-md bg-red-50 p-3 text-sm text-red-700 border border-red-200">
|
||||
<div className="font-medium">{t.subagents.failed}</div>
|
||||
<div className="mt-1 text-xs">{state.error}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -79,6 +79,9 @@ export const enUS: Translations = {
|
||||
proMode: "Pro",
|
||||
proModeDescription:
|
||||
"Reasoning, planning and executing, get more accurate results, may take more time",
|
||||
ultraMode: "Ultra",
|
||||
ultraModeDescription:
|
||||
"Pro mode with subagents enabled, maximum capability for complex multi-step tasks",
|
||||
searchModels: "Search models...",
|
||||
surpriseMe: "Surprise",
|
||||
surpriseMePrompt: "Surprise me",
|
||||
|
||||
@@ -62,6 +62,8 @@ export interface Translations {
|
||||
reasoningModeDescription: string;
|
||||
proMode: string;
|
||||
proModeDescription: string;
|
||||
ultraMode: string;
|
||||
ultraModeDescription: string;
|
||||
searchModels: string;
|
||||
surpriseMe: string;
|
||||
surpriseMePrompt: string;
|
||||
|
||||
@@ -77,6 +77,8 @@ export const zhCN: Translations = {
|
||||
reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡",
|
||||
proMode: "专业",
|
||||
proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间",
|
||||
ultraMode: "超级",
|
||||
ultraModeDescription: "专业模式加子代理,适用于复杂的多步骤任务,功能最强大",
|
||||
searchModels: "搜索模型...",
|
||||
surpriseMe: "小惊喜",
|
||||
surpriseMePrompt: "给我一个小惊喜吧",
|
||||
|
||||
@@ -21,9 +21,9 @@ export interface LocalSettings {
|
||||
};
|
||||
context: Omit<
|
||||
AgentThreadContext,
|
||||
"thread_id" | "is_plan_mode" | "thinking_enabled"
|
||||
"thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled"
|
||||
> & {
|
||||
mode: "flash" | "thinking" | "pro" | undefined;
|
||||
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
|
||||
};
|
||||
layout: {
|
||||
sidebar_collapsed: boolean;
|
||||
|
||||
13
frontend/src/core/subagents/context.ts
Normal file
13
frontend/src/core/subagents/context.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import type { SubagentState } from "../threads/types";
|
||||
|
||||
export const SubagentContext = createContext<Map<string, SubagentState>>(new Map());
|
||||
|
||||
export function useSubagentContext() {
|
||||
const context = useContext(SubagentContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useSubagentContext must be used within a SubagentContext.Provider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
69
frontend/src/core/subagents/hooks.ts
Normal file
69
frontend/src/core/subagents/hooks.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import type { SubagentProgressEvent, SubagentState } from "../threads/types";
|
||||
|
||||
export function useSubagentStates() {
|
||||
const [subagents, setSubagents] = useState<Map<string, SubagentState>>(new Map());
|
||||
const subagentsRef = useRef<Map<string, SubagentState>>(new Map());
|
||||
|
||||
// 保持 ref 与 state 同步
|
||||
useEffect(() => {
|
||||
subagentsRef.current = subagents;
|
||||
}, [subagents]);
|
||||
|
||||
const handleSubagentProgress = useCallback((event: SubagentProgressEvent) => {
|
||||
console.log('[SubagentProgress] Received event:', event);
|
||||
|
||||
const { task_id, trace_id, subagent_type, event_type, result, error } = event;
|
||||
|
||||
setSubagents(prev => {
|
||||
const newSubagents = new Map(prev);
|
||||
const existingState = newSubagents.get(task_id) || {
|
||||
task_id,
|
||||
trace_id,
|
||||
subagent_type,
|
||||
status: "running" as const,
|
||||
};
|
||||
|
||||
let newState = { ...existingState };
|
||||
|
||||
switch (event_type) {
|
||||
case "started":
|
||||
newState = {
|
||||
...newState,
|
||||
status: "running",
|
||||
};
|
||||
break;
|
||||
|
||||
case "completed":
|
||||
newState = {
|
||||
...newState,
|
||||
status: "completed",
|
||||
result,
|
||||
};
|
||||
break;
|
||||
|
||||
case "failed":
|
||||
newState = {
|
||||
...newState,
|
||||
status: "failed",
|
||||
error,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
newSubagents.set(task_id, newState);
|
||||
return newSubagents;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const clearSubagents = useCallback(() => {
|
||||
setSubagents(new Map());
|
||||
}, []);
|
||||
|
||||
return {
|
||||
subagents,
|
||||
handleSubagentProgress,
|
||||
clearSubagents,
|
||||
};
|
||||
}
|
||||
2
frontend/src/core/subagents/index.ts
Normal file
2
frontend/src/core/subagents/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useSubagentStates } from "./hooks";
|
||||
export { SubagentContext, useSubagentContext } from "./context";
|
||||
@@ -17,4 +17,5 @@ export interface AgentThreadContext extends Record<string, unknown> {
|
||||
model_name: string | undefined;
|
||||
thinking_enabled: boolean;
|
||||
is_plan_mode: boolean;
|
||||
subagent_enabled: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user