-
- {t.ideaGen.criticIterations}
-
- {statValueLabel(
- run.critic_iteration_count,
- run.max_critic_iterations,
- )}
-
-
-
-
+
-
-
{t.ideaGen.currentNode}
-
- {getIdeaGenNodeLabel(t.ideaGen, run.current_node) || "—"}
-
-
-
-
{t.ideaGen.researchUnits}
-
{run.unit_count}
-
-
-
{t.ideaGen.papers}
-
{run.papers_count}
-
-
-
- {t.ideaGen.compressedPapers}
-
-
{run.compressed_papers_count}
-
-
-
{t.ideaGen.searchResults}
-
{run.search_results_count}
+
+
+
+
+
+
);
-}
+});
+IdeaGenProgressCard.displayName = "IdeaGenProgressCard";
diff --git a/frontend/src/app/workspace/idea-gen/components/idea-gen-status-badge.tsx b/frontend/src/app/workspace/idea-gen/components/idea-gen-status-badge.tsx
index a379aeca..e799a50c 100644
--- a/frontend/src/app/workspace/idea-gen/components/idea-gen-status-badge.tsx
+++ b/frontend/src/app/workspace/idea-gen/components/idea-gen-status-badge.tsx
@@ -1,5 +1,8 @@
"use client";
+import { motion } from "motion/react";
+import { memo } from "react";
+
import { Badge } from "@/components/ui/badge";
import { useI18n } from "@/core/i18n/hooks";
import type { IdeaGenRunStatus } from "@/core/idea-gen";
@@ -7,28 +10,59 @@ import { cn } from "@/lib/utils";
const statusClassName: Record
= {
pending: "border-slate-300 bg-slate-100 text-slate-700",
- running: "border-blue-200 bg-blue-100 text-blue-700",
- completed: "border-emerald-200 bg-emerald-100 text-emerald-700",
- failed: "border-red-200 bg-red-100 text-red-700",
- clarification_required: "border-amber-200 bg-amber-100 text-amber-700",
+ running: "border-blue-200 bg-blue-100 text-blue-700 shadow-sm",
+ completed: "border-emerald-200 bg-emerald-100 text-emerald-700 shadow-sm",
+ failed: "border-red-200 bg-red-100 text-red-700 shadow-sm",
+ clarification_required: "border-amber-200 bg-amber-100 text-amber-700 shadow-sm",
deleted: "border-zinc-300 bg-zinc-100 text-zinc-700",
};
-export function IdeaGenStatusBadge({
- status,
- className,
-}: {
- status: IdeaGenRunStatus;
- className?: string;
-}) {
- const { t } = useI18n();
+const statusAnimation: Record = {
+ pending: {},
+ running: {
+ scale: [1, 1.05, 1],
+ transition: { repeat: Infinity, duration: 2, ease: "easeInOut" },
+ },
+ completed: {
+ scale: [0.95, 1],
+ transition: { type: "spring", stiffness: 200, damping: 10 },
+ },
+ failed: {
+ x: [-2, 2, -2, 2, 0],
+ transition: { duration: 0.4 },
+ },
+ clarification_required: {
+ scale: [1, 1.05, 1],
+ transition: { repeat: Infinity, duration: 1.5, ease: "easeInOut" },
+ },
+ deleted: {},
+};
+
+export const IdeaGenStatusBadge = memo(
+ ({ status, className }: { status: IdeaGenRunStatus; className?: string }) => {
+ const { t } = useI18n();
- return (
-
- {t.ideaGen.status[status]}
-
- );
-}
+ return (
+
+
+ {status === "running" && (
+
+ )}
+ {t.ideaGen.status[status]}
+
+
+ );
+ }
+);
+IdeaGenStatusBadge.displayName = "IdeaGenStatusBadge";
diff --git a/frontend/src/app/workspace/idea-gen/components/idea-gen-timeline.tsx b/frontend/src/app/workspace/idea-gen/components/idea-gen-timeline.tsx
index ba734df4..9aca4770 100644
--- a/frontend/src/app/workspace/idea-gen/components/idea-gen-timeline.tsx
+++ b/frontend/src/app/workspace/idea-gen/components/idea-gen-timeline.tsx
@@ -1,6 +1,8 @@
"use client";
import { CheckIcon, CircleDotIcon, LoaderCircleIcon, RotateCcwIcon } from "lucide-react";
+import { motion } from "motion/react";
+import { memo } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useI18n } from "@/core/i18n/hooks";
@@ -44,54 +46,126 @@ function iconOfState(state: TimelineState) {
function classNameOfState(state: TimelineState) {
if (state === "done") return "border-emerald-200 bg-emerald-100 text-emerald-700";
- if (state === "active") return "border-blue-200 bg-blue-100 text-blue-700";
+ if (state === "active") return "border-blue-200 bg-blue-100 text-blue-700 shadow-sm";
if (state === "warning") return "border-amber-200 bg-amber-100 text-amber-700";
if (state === "failed") return "border-red-200 bg-red-100 text-red-700";
return "border-zinc-200 bg-zinc-100 text-zinc-500";
}
-export function IdeaGenTimeline({ run }: { run: IdeaGenRun }) {
+const TimelineNode = memo(
+ ({
+ nodeName,
+ state,
+ label,
+ isLast,
+ index,
+ iterationCount,
+ criticIterationCount,
+ t,
+ }: {
+ nodeName: IdeaGenNodeName;
+ state: TimelineState;
+ label: string;
+ isLast: boolean;
+ index: number;
+ iterationCount?: number;
+ criticIterationCount?: number;
+ t: any;
+ }) => (
+
+
+
+ {iconOfState(state)}
+
+ {!isLast && (
+
+ )}
+
+
+ {label}
+ {nodeName === "supervisor" && iterationCount && iterationCount > 1 && (
+
+
+ {t.ideaGen.iterationProgress}: {iterationCount}
+
+ )}
+ {nodeName === "critic" && criticIterationCount && criticIterationCount > 0 && (
+
+
+ {t.ideaGen.criticIterations}: {criticIterationCount}
+
+ )}
+
+
+ )
+);
+TimelineNode.displayName = "TimelineNode";
+
+export const IdeaGenTimeline = memo(({ run }: { run: IdeaGenRun }) => {
const { t } = useI18n();
return (
-
+
- {t.ideaGen.timelineTitle}
+ {t.ideaGen.timelineTitle}
{IDEA_GEN_NODE_NAMES.map((nodeName, index) => {
const state = stateOfTimelineNode(run, nodeName);
return (
-
-
-
- {iconOfState(state)}
-
- {index < IDEA_GEN_NODE_NAMES.length - 1 && (
-
- )}
-
-
-
- {getIdeaGenNodeLabel(t.ideaGen, nodeName)}
-
- {nodeName === "supervisor" && run.iteration_count > 1 && (
-
-
- {t.ideaGen.iterationProgress}: {run.iteration_count}
-
- )}
- {nodeName === "critic" && run.critic_iteration_count > 0 && (
-
-
- {t.ideaGen.criticIterations}: {run.critic_iteration_count}
-
- )}
-
-
+
);
})}
);
-}
+});
+IdeaGenTimeline.displayName = "IdeaGenTimeline";
diff --git a/frontend/src/core/idea-gen/api.ts b/frontend/src/core/idea-gen/api.ts
index 53eef912..219ca2ca 100644
--- a/frontend/src/core/idea-gen/api.ts
+++ b/frontend/src/core/idea-gen/api.ts
@@ -2,6 +2,7 @@ import { getBackendBaseURL } from "@/core/config";
import type {
ClarificationResponse,
+ IdeaGenPresetDefinition,
IdeaGenRun,
IdeaGenRunCreate,
} from "./types";
@@ -51,6 +52,10 @@ export async function fetchIdeaGenRuns(): Promise {
return requestIdeaGen("/api/idea-gen/runs");
}
+export async function fetchIdeaGenPresets(): Promise {
+ return requestIdeaGen("/api/idea-gen/runs/presets");
+}
+
export async function fetchIdeaGenRun(runId: string): Promise {
return requestIdeaGen(getIdeaGenRunAPIPath(runId));
}
diff --git a/frontend/src/core/idea-gen/hooks.ts b/frontend/src/core/idea-gen/hooks.ts
index 781c198f..ec91f8db 100644
--- a/frontend/src/core/idea-gen/hooks.ts
+++ b/frontend/src/core/idea-gen/hooks.ts
@@ -10,6 +10,7 @@ import {
createIdeaGenRun,
deleteIdeaGenRun,
fetchIdeaGenArtifactText,
+ fetchIdeaGenPresets,
fetchIdeaGenRun,
fetchIdeaGenRuns,
} from "./api";
@@ -27,6 +28,7 @@ import {
export const ideaGenQueryKeys = {
all: ["idea-gen-runs"] as const,
+ presets: ["idea-gen-presets"] as const,
detail: (runId: string) => ["idea-gen-run", runId] as const,
};
@@ -40,6 +42,14 @@ export function useIdeaGenRuns() {
});
}
+export function useIdeaGenPresets() {
+ return useQuery({
+ queryKey: ideaGenQueryKeys.presets,
+ queryFn: fetchIdeaGenPresets,
+ staleTime: 60_000,
+ });
+}
+
export function useIdeaGenRun(runId: string | null | undefined) {
return useQuery({
queryKey: runId ? ideaGenQueryKeys.detail(runId) : ["idea-gen-run", "idle"],
diff --git a/frontend/src/core/idea-gen/types.ts b/frontend/src/core/idea-gen/types.ts
index 4bbc1fb2..f5780779 100644
--- a/frontend/src/core/idea-gen/types.ts
+++ b/frontend/src/core/idea-gen/types.ts
@@ -52,6 +52,7 @@ export const IDEA_GEN_DEFAULT_LANGUAGE = "zh";
export const IDEA_GEN_DEFAULT_MAX_ITERATIONS = 8;
export const IDEA_GEN_DEFAULT_MAX_CRITIC_ITERATIONS = 2;
+export type IdeaGenPresetMode = "fast" | "balanced" | "quality" | "custom";
export type IdeaGenRunStatus = (typeof IDEA_GEN_RUN_STATUSES)[number];
export type IdeaGenTerminalStatus = (typeof IDEA_GEN_TERMINAL_STATUSES)[number];
export type IdeaGenNodeName = (typeof IDEA_GEN_NODE_NAMES)[number];
@@ -65,6 +66,7 @@ export type IdeaGenStreamStatus =
export interface IdeaGenRunCreate {
target: string;
+ preset_mode: IdeaGenPresetMode;
academic_domain: string;
target_venues: string[];
language: string;
@@ -82,13 +84,14 @@ export interface IdeaGenRunCreate {
enable_pdf_parsing?: boolean;
enable_summarization?: boolean;
enable_paper_cache?: boolean;
- // Node model config
- model_clarify?: string;
- model_brief?: string;
- model_query_generation?: string;
- model_supervisor?: string;
- model_compress?: string;
- model_report?: string;
+}
+
+export interface IdeaGenPresetDefinition {
+ mode: IdeaGenPresetMode;
+ label: string;
+ description: string;
+ default_max_iterations: number;
+ max_critic_iterations: number;
}
export interface ClarificationResponse {
diff --git a/frontend/src/core/idea-gen/utils.ts b/frontend/src/core/idea-gen/utils.ts
index a9f6d9f4..f4baf25e 100644
--- a/frontend/src/core/idea-gen/utils.ts
+++ b/frontend/src/core/idea-gen/utils.ts
@@ -18,111 +18,24 @@ import {
IDEA_GEN_TERMINAL_STATUSES,
} from "./types";
-export type IdeaGenPreset = "light" | "medium" | "complex" | "custom";
-
export function createDefaultIdeaGenRunInput(): IdeaGenRunCreate {
return {
target: "",
+ preset_mode: "balanced",
academic_domain: IDEA_GEN_DEFAULT_ACADEMIC_DOMAIN,
target_venues: [...IDEA_GEN_DEFAULT_TARGET_VENUES],
language: IDEA_GEN_DEFAULT_LANGUAGE,
allow_clarification: true,
max_iterations: IDEA_GEN_DEFAULT_MAX_ITERATIONS,
- // Advanced workflow config (optional)
- max_concurrent_research_units: 8,
- search_results_per_topic: 20,
- max_critic_iterations: 2,
- critic_quality_threshold: 7.5,
- // Advanced paper config (optional)
- enable_scoring: false,
- score_threshold: 7.0,
- top_k_papers: 3,
- enable_pdf_parsing: true,
- enable_summarization: false,
- enable_paper_cache: true,
- // Node model config (optional)
- model_clarify: "gpt-4o-mini",
- model_brief: "gpt-4o-mini",
- model_query_generation: "gpt-4o-mini",
- model_supervisor: "gpt-4o-mini",
- model_compress: "gpt-4o-mini",
- model_report: "gpt-4o-mini",
};
}
-export function getPresetConfig(preset: IdeaGenPreset): Partial {
- switch (preset) {
- case "light":
- return {
- max_iterations: 3,
- max_concurrent_research_units: 4,
- search_results_per_topic: 10,
- max_critic_iterations: 1,
- critic_quality_threshold: 7.0,
- enable_scoring: false,
- score_threshold: 6.5,
- top_k_papers: 2,
- enable_pdf_parsing: false,
- enable_summarization: false,
- enable_paper_cache: true,
- model_clarify: "gpt-4o-mini",
- model_brief: "gpt-4o-mini",
- model_query_generation: "gpt-4",
- model_supervisor: "gpt-4o",
- model_compress: "gpt-4o",
- model_report: "gpt-4o",
- };
- case "medium":
- return {
- max_iterations: 6,
- max_concurrent_research_units: 8,
- search_results_per_topic: 20,
- max_critic_iterations: 2,
- critic_quality_threshold: 7.5,
- enable_scoring: true,
- score_threshold: 7.0,
- top_k_papers: 3,
- enable_pdf_parsing: true,
- enable_summarization: false,
- enable_paper_cache: true,
- model_clarify: "gpt-4o",
- model_brief: "gpt-4o",
- model_query_generation: "gpt-4o",
- model_supervisor: "gpt-5.4",
- model_compress: "gpt-4o",
- model_report: "gpt-5.4",
- };
- case "complex":
- return {
- max_iterations: 10,
- max_concurrent_research_units: 12,
- search_results_per_topic: 30,
- max_critic_iterations: 3,
- critic_quality_threshold: 8.0,
- enable_scoring: true,
- score_threshold: 7.5,
- top_k_papers: 5,
- enable_pdf_parsing: true,
- enable_summarization: true,
- enable_paper_cache: true,
- model_clarify: "gpt-4o",
- model_brief: "gpt-5.4",
- model_query_generation: "gpt-5.4",
- model_supervisor: "gpt-5.4",
- model_compress: "gpt-4o",
- model_report: "gpt-5.4",
- };
- case "custom":
- default:
- return {};
- }
-}
-
export function normalizeIdeaGenRunInput(
input: Partial,
): IdeaGenRunCreate {
return {
target: input.target?.trim() ?? "",
+ preset_mode: input.preset_mode ?? "balanced",
academic_domain:
input.academic_domain?.trim() ?? IDEA_GEN_DEFAULT_ACADEMIC_DOMAIN,
target_venues: (input.target_venues ?? IDEA_GEN_DEFAULT_TARGET_VENUES)
@@ -142,13 +55,6 @@ export function normalizeIdeaGenRunInput(
...(input.enable_pdf_parsing !== undefined && { enable_pdf_parsing: input.enable_pdf_parsing }),
...(input.enable_summarization !== undefined && { enable_summarization: input.enable_summarization }),
...(input.enable_paper_cache !== undefined && { enable_paper_cache: input.enable_paper_cache }),
- // Node model config (optional)
- ...(input.model_clarify && { model_clarify: input.model_clarify.trim() }),
- ...(input.model_brief && { model_brief: input.model_brief.trim() }),
- ...(input.model_query_generation && { model_query_generation: input.model_query_generation.trim() }),
- ...(input.model_supervisor && { model_supervisor: input.model_supervisor.trim() }),
- ...(input.model_compress && { model_compress: input.model_compress.trim() }),
- ...(input.model_report && { model_report: input.model_report.trim() }),
};
}
diff --git a/scripts/start-services.sh b/scripts/start-services.sh
index 0156e608..38ab1262 100755
--- a/scripts/start-services.sh
+++ b/scripts/start-services.sh
@@ -152,7 +152,7 @@ start_bg \
-u NEXT_PUBLIC_BACKEND_BASE_URL \
BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET_VALUE" \
DEV_BACKEND_PROXY_TARGET="$DEV_BACKEND_PROXY_TARGET" \
- bash -c "cd '$PROJECT_ROOT/frontend' && exec pnpm exec next dev --turbo --hostname 0.0.0.0 --port ${FRONTEND_PORT}"
+ bash -c "cd '$PROJECT_ROOT/frontend' && exec pnpm dev --hostname 0.0.0.0 --port ${FRONTEND_PORT}"
wait_for_http "Frontend" "http://127.0.0.1:${FRONTEND_PORT}/workspace/review-gen"
if [[ "$START_NGINX" == "1" || "$START_NGINX" == "true" ]]; then