Localize the remaining dashboard UI clusters and refresh the generated i18n resources. - Replace hardcoded labels across agent, plugin, mission, workflow node, research, document, activity, and miscellaneous dashboard components with app namespace translations. - Add and synchronize locale catalog entries for English, Spanish, French, Korean, Simplified Chinese, and Traditional Chinese. - Update i18n lint configuration and generated resource types, with a patch changeset for the published CLI package. Files changed: .changeset/fn-6769-i18n-cluster.md | 5 + i18next.config.ts | 47 +-- .../dashboard/app/components/ActiveAgentsPanel.tsx | 2 +- packages/dashboard/app/components/ActivityFeed.tsx | 12 +- .../dashboard/app/components/ActivityLogModal.tsx | 4 +- packages/dashboard/app/components/AddNodeModal.tsx | 4 +- .../dashboard/app/components/AgentDetailView.tsx | 6 +- .../app/components/AgentGenerationModal.tsx | 2 +- .../dashboard/app/components/AgentImportModal.tsx | 4 +- .../app/components/AgentOnboardingModal.tsx | 4 +- .../app/components/AgentPromptsManager.tsx | 2 +- .../dashboard/app/components/AgentRunHistory.tsx | 2 +- .../dashboard/app/components/AgentsOverviewBar.tsx | 2 +- .../dashboard/app/components/BranchGroupCard.tsx | 4 +- .../dashboard/app/components/CliBinaryPanel.tsx | 8 +- .../app/components/CursorCliProviderCard.tsx | 4 +- .../dashboard/app/components/DashboardLoader.tsx | 4 +- .../dashboard/app/components/DevServerView.tsx | 8 +- .../dashboard/app/components/DocumentsView.tsx | 6 +- .../dashboard/app/components/ErrorBoundary.tsx | 9 +- .../dashboard/app/components/ExecutorStatusBar.tsx | 5 +- .../dashboard/app/components/GitHubStarPrompt.tsx | 14 +- .../dashboard/app/components/GitManagerModal.tsx | 4 +- .../dashboard/app/components/GroupTaskModal.tsx | 2 +- packages/dashboard/app/components/Header.tsx | 4 +- .../dashboard/app/components/HermesRuntimeCard.tsx | 2 +- packages/dashboard/app/components/InsightsView.tsx | 2 +- .../dashboard/app/components/IssueMentionPopup.tsx | 5 +- packages/dashboard/app/components/MailboxView.tsx | 6 +- .../components/MilestoneSliceInterviewModal.tsx | 2 +- .../app/components/MissionInterviewModal.tsx | 2 +- .../dashboard/app/components/MissionManager.tsx | 8 +- .../app/components/ModelOnboardingModal.tsx | 2 +- .../dashboard/app/components/NodeDetailModal.tsx | 2 +- .../app/components/PiExtensionsManager.tsx | 10 +- .../dashboard/app/components/ProjectSelector.tsx | 6 +- packages/dashboard/app/components/QuickChatFAB.tsx | 44 +-- packages/dashboard/app/components/ResearchView.tsx | 2 +- packages/dashboard/app/components/RoutineCard.tsx | 6 +- .../dashboard/app/components/RoutineEditor.tsx | 2 +- packages/dashboard/app/components/RoutingTab.tsx | 2 +- packages/dashboard/app/components/ScheduleCard.tsx | 6 +- packages/dashboard/app/components/ScheduleForm.tsx | 2 +- packages/dashboard/app/components/ScriptsModal.tsx | 2 +- .../app/components/StashConflictModal.tsx | 4 +- .../dashboard/app/components/TerminalModal.tsx | 2 +- .../app/components/nodes/WorkflowNodeTypes.tsx | 47 +-- packages/i18n/locales/en/app.json | 274 +++++++-------- packages/i18n/locales/es/app.json | 144 ++++++-- packages/i18n/locales/fr/app.json | 144 ++++++-- packages/i18n/locales/ko/app.json | 144 ++++++-- packages/i18n/locales/zh-CN/app.json | 144 ++++++-- packages/i18n/locales/zh-TW/app.json | 144 ++++++-- packages/i18n/src/resources.d.ts | 375 +++++++++++++++++++-- 54 files changed, 1261 insertions(+), 442 deletions(-) Fusion-Task-Id: FN-6769 Fusion-Task-Lineage: a8733dc9-3b48-47e6-88c4-020f83ab41de
335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
import { useState, useCallback, useEffect, useRef } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { AgentGenerationSpec } from "../api";
|
|
import { useMobileScrollLock } from "../hooks/useMobileScrollLock";
|
|
import {
|
|
startAgentGeneration,
|
|
generateAgentSpec,
|
|
cancelAgentGeneration,
|
|
} from "../api";
|
|
|
|
interface AgentGenerationModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onGenerated: (spec: AgentGenerationSpec) => void;
|
|
projectId?: string;
|
|
}
|
|
|
|
type ViewState =
|
|
| { type: "input" }
|
|
| { type: "loading" }
|
|
| { type: "preview"; spec: AgentGenerationSpec; sessionId: string };
|
|
|
|
const MIN_ROLE_LENGTH = 3;
|
|
const MAX_ROLE_LENGTH = 1000;
|
|
|
|
/**
|
|
* Modal for AI-assisted agent creation.
|
|
*
|
|
* The user enters a role description and the system generates a complete
|
|
* agent specification including title, icon, system prompt, and suggested
|
|
* runtime configuration.
|
|
*
|
|
* Follows the same general modal pattern as PlanningModeModal but simplified
|
|
* (no multi-step Q&A — single input → single generation result).
|
|
*/
|
|
export function AgentGenerationModal({
|
|
isOpen,
|
|
onClose,
|
|
onGenerated,
|
|
projectId,
|
|
}: AgentGenerationModalProps) {
|
|
const { t } = useTranslation("app");
|
|
useMobileScrollLock(isOpen);
|
|
const [roleDescription, setRoleDescription] = useState("");
|
|
const [view, setView] = useState<ViewState>({ type: "input" });
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [systemPromptExpanded, setSystemPromptExpanded] = useState(false);
|
|
const sessionIdRef = useRef<string | null>(null);
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
|
|
// Focus textarea on open
|
|
useEffect(() => {
|
|
if (isOpen && view.type === "input") {
|
|
textareaRef.current?.focus();
|
|
}
|
|
}, [isOpen, view.type]);
|
|
|
|
// Cleanup session on unmount or modal close
|
|
useEffect(() => {
|
|
if (!isOpen && sessionIdRef.current) {
|
|
const sid = sessionIdRef.current;
|
|
sessionIdRef.current = null;
|
|
cancelAgentGeneration(sid, projectId).catch(() => {
|
|
/* ignore cleanup errors */
|
|
});
|
|
}
|
|
}, [isOpen, projectId]);
|
|
|
|
// Handle escape key
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
handleCancel();
|
|
}
|
|
};
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
}, [isOpen]);
|
|
|
|
const handleCancel = useCallback(() => {
|
|
// Cleanup session server-side
|
|
if (sessionIdRef.current) {
|
|
const sid = sessionIdRef.current;
|
|
sessionIdRef.current = null;
|
|
cancelAgentGeneration(sid, projectId).catch(() => {
|
|
/* ignore cleanup errors */
|
|
});
|
|
}
|
|
setRoleDescription("");
|
|
setView({ type: "input" });
|
|
setError(null);
|
|
setSystemPromptExpanded(false);
|
|
onClose();
|
|
}, [onClose, projectId]);
|
|
|
|
const handleGenerate = useCallback(async () => {
|
|
if (!roleDescription.trim() || roleDescription.trim().length < MIN_ROLE_LENGTH) return;
|
|
|
|
setError(null);
|
|
setView({ type: "loading" });
|
|
|
|
try {
|
|
// Phase 1: Start session
|
|
const { sessionId } = await startAgentGeneration(roleDescription.trim(), projectId);
|
|
sessionIdRef.current = sessionId;
|
|
|
|
// Phase 2: Generate spec (single combined loading state)
|
|
const { spec } = await generateAgentSpec(sessionId, projectId);
|
|
|
|
setView({ type: "preview", spec, sessionId });
|
|
} catch (err: unknown) {
|
|
const message =
|
|
err instanceof Error ? err.message : "Failed to generate agent specification";
|
|
// Handle rate limit errors with user-friendly message
|
|
if (message.includes("429") || message.toLowerCase().includes("rate limit")) {
|
|
setError(t("agents.generation.rateLimited", "Too many requests. Please wait a moment and try again."));
|
|
} else {
|
|
setError(message);
|
|
}
|
|
setView({ type: "input" });
|
|
sessionIdRef.current = null;
|
|
}
|
|
}, [roleDescription, projectId]);
|
|
|
|
const handleRegenerate = useCallback(async () => {
|
|
// Cancel existing session and create a new one
|
|
if (sessionIdRef.current) {
|
|
const oldSid = sessionIdRef.current;
|
|
sessionIdRef.current = null;
|
|
try {
|
|
await cancelAgentGeneration(oldSid, projectId);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
// Re-run generation with same role description
|
|
await handleGenerate();
|
|
}, [handleGenerate, projectId]);
|
|
|
|
const handleUseSpec = useCallback(() => {
|
|
if (view.type !== "preview") return;
|
|
// Clear session ref so we don't cancel on close (we're using the spec)
|
|
sessionIdRef.current = null;
|
|
onGenerated(view.spec);
|
|
// Reset and close
|
|
setRoleDescription("");
|
|
setView({ type: "input" });
|
|
setError(null);
|
|
setSystemPromptExpanded(false);
|
|
onClose();
|
|
}, [view, onGenerated, onClose]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const canGenerate =
|
|
roleDescription.trim().length >= MIN_ROLE_LENGTH &&
|
|
roleDescription.trim().length <= MAX_ROLE_LENGTH;
|
|
|
|
return (
|
|
<div
|
|
className="agent-dialog-overlay"
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) handleCancel();
|
|
}}
|
|
>
|
|
<div
|
|
className="agent-dialog"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={t("agents.generateWithAiLabel", "Generate agent with AI")}
|
|
>
|
|
{/* Header */}
|
|
<div className="agent-dialog-header">
|
|
<span className="agent-dialog-header-title">
|
|
<span className="agent-dialog-header-sparkle">✨</span>
|
|
{t("agents.generation.title", "Generate Agent")}
|
|
</span>
|
|
<button
|
|
className="modal-close"
|
|
onClick={handleCancel}
|
|
aria-label={t("actions.close", "Close")}
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="agent-dialog-body">
|
|
{error && <div className="agent-dialog-error-banner">{error}</div>}
|
|
|
|
{view.type === "input" && (
|
|
<div>
|
|
<p className="agent-dialog-info">
|
|
{t("agents.generation.info", "Describe your agent's role and the AI will generate a complete specification including system prompt, suggested configuration, and more.")}
|
|
</p>
|
|
<div className="agent-dialog-field">
|
|
<label htmlFor="agent-role-description">{t("agents.generation.roleLabel", "Role Description")}</label>
|
|
<textarea
|
|
ref={textareaRef}
|
|
id="agent-role-description"
|
|
className="input agent-dialog-textarea"
|
|
rows={4}
|
|
placeholder={t("agents.generation.rolePlaceholder", 'e.g. "Senior frontend code reviewer who specializes in React accessibility"')}
|
|
value={roleDescription}
|
|
onChange={(e) => setRoleDescription(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" && !e.shiftKey && canGenerate) {
|
|
e.preventDefault();
|
|
handleGenerate();
|
|
}
|
|
}}
|
|
maxLength={MAX_ROLE_LENGTH}
|
|
aria-describedby="role-description-hint"
|
|
/>
|
|
<div
|
|
id="role-description-hint"
|
|
className="agent-dialog-hint"
|
|
>
|
|
<span>{t("agents.generation.roleHint", "Describe what your agent should do")}</span>
|
|
<span>
|
|
{roleDescription.length}/{MAX_ROLE_LENGTH}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{view.type === "loading" && (
|
|
<div className="agent-dialog-loading-center">
|
|
<div className="agent-dialog-spinner spin" />
|
|
<p className="agent-dialog-loading-text">
|
|
{t("agents.generation.loading", "Generating agent specification...")}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{view.type === "preview" && (
|
|
<div>
|
|
<div className="agent-dialog-summary agent-dialog-summary--spaced">
|
|
<div className="agent-dialog-summary-row">
|
|
<span className="agent-dialog-summary-row-label agent-dialog-summary-row-label--fixed">
|
|
{t("agents.generation.previewTitle", "Title")}
|
|
</span>
|
|
<span className="agent-dialog-summary-row-value">
|
|
{view.spec.icon} {view.spec.title}
|
|
</span>
|
|
</div>
|
|
<div className="agent-dialog-summary-row">
|
|
<span className="agent-dialog-summary-row-label agent-dialog-summary-row-label--fixed">
|
|
{t("agents.generation.previewRole", "Role")}
|
|
</span>
|
|
<span>{view.spec.role}</span>
|
|
</div>
|
|
<div className="agent-dialog-summary-row">
|
|
<span className="agent-dialog-summary-row-label agent-dialog-summary-row-label--fixed">
|
|
{t("agents.generation.previewDescription", "Description")}
|
|
</span>
|
|
<span className="agent-dialog-summary-row-value agent-dialog-summary-row-value--body">{view.spec.description}</span>
|
|
</div>
|
|
<div className="agent-dialog-summary-row">
|
|
<span className="agent-dialog-summary-row-label agent-dialog-summary-row-label--fixed">
|
|
{t("agents.generation.previewThinking", "Thinking")}
|
|
</span>
|
|
<span className="agent-dialog-summary-row-value agent-dialog-summary-row-value--capitalize">
|
|
{view.spec.thinkingLevel}
|
|
</span>
|
|
</div>
|
|
<div className="agent-dialog-summary-row">
|
|
<span className="agent-dialog-summary-row-label agent-dialog-summary-row-label--fixed">
|
|
{t("agents.generation.previewMaxTurns", "Max Turns")}
|
|
</span>
|
|
<span>{view.spec.maxTurns}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* System prompt preview */}
|
|
<div className="agent-dialog-field">
|
|
<label>
|
|
{t("agents.generation.systemPrompt", "System Prompt")}
|
|
<button
|
|
type="button"
|
|
className="agent-dialog-expand-btn"
|
|
onClick={() => setSystemPromptExpanded(!systemPromptExpanded)}
|
|
>
|
|
{systemPromptExpanded ? t("agents.generation.collapse", "Collapse") : t("agents.generation.expand", "Expand")}
|
|
</button>
|
|
</label>
|
|
<div
|
|
className={`agent-generation-prompt-box${systemPromptExpanded ? "" : " agent-generation-prompt-box--collapsed"}`}
|
|
>
|
|
{view.spec.systemPrompt}
|
|
{!systemPromptExpanded &&
|
|
view.spec.systemPrompt.length > 500 && (
|
|
<div className="agent-generation-prompt-fade" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="agent-dialog-footer">
|
|
<button className="btn" onClick={handleCancel}>
|
|
{t("actions.cancel", "Cancel")}
|
|
</button>
|
|
{view.type === "input" && (
|
|
<button
|
|
className="btn btn-task-create"
|
|
onClick={() => void handleGenerate()}
|
|
disabled={!canGenerate}
|
|
>
|
|
{t("agents.generation.generate", "Generate")}
|
|
</button>
|
|
)}
|
|
{view.type === "preview" && (
|
|
<>
|
|
<button
|
|
className="btn"
|
|
onClick={() => void handleRegenerate()}
|
|
>
|
|
{t("agents.generation.regenerate", "Regenerate")}
|
|
</button>
|
|
<button className="btn btn-task-create" onClick={handleUseSpec}>
|
|
{t("agents.generation.useThis", "Use This")}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|