feat: 智能体控制台前端(console)
- React+antd+zustand,注册表驱动路由/菜单/权限 - Tauri 桌面壳、后端托管 SPA、跨标签页消息队列
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
.toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1.5px solid var(--border-color, rgba(0, 0, 0, 0.12));
|
||||
background: transparent;
|
||||
color: var(--text-secondary, rgba(0, 0, 0, 0.55));
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
letter-spacing: 0.01em;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: var(--primary-color, #1677ff);
|
||||
color: var(--primary-color, #1677ff);
|
||||
background: var(--primary-bg, rgba(22, 119, 255, 0.06));
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary-color, #1677ff);
|
||||
color: var(--primary-color, #1677ff);
|
||||
background: var(--primary-bg, rgba(22, 119, 255, 0.08));
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// ---- Experimental modal ------------------------------------------------
|
||||
|
||||
.modalTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flaskIcon {
|
||||
color: var(--warning-color, #faad14);
|
||||
}
|
||||
|
||||
.modalBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
|
||||
.modalDesc {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary, rgba(0, 0, 0, 0.88));
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.modalNote {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary, rgba(0, 0, 0, 0.45));
|
||||
background: var(--fill-tertiary, rgba(0, 0, 0, 0.04));
|
||||
border-left: 3px solid var(--warning-color, #faad14);
|
||||
padding: 8px 10px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
// ---- Dark mode -----------------------------------------------------------
|
||||
// Match the GitHub / Resources <Button type="text"> rendered by antd's
|
||||
// dark algorithm — same text color and a subdued border.
|
||||
:global(.dark-mode) {
|
||||
.toggle {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: #4da3ff;
|
||||
color: #4da3ff;
|
||||
background: rgba(22, 119, 255, 0.12);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #4da3ff;
|
||||
color: #4da3ff;
|
||||
background: rgba(22, 119, 255, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.modalDesc {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.modalNote {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { Modal, Tooltip } from "antd";
|
||||
import { Code, FlaskConical, MessageSquare } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCodingMode, useProjectDir } from "../../stores/codingModeStore";
|
||||
import { useAgentStore } from "../../stores/agentStore";
|
||||
import { getApiUrl } from "../../api/config";
|
||||
import { buildAuthHeaders } from "../../api/authHeaders";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import ProjectSelectModal from "../ProjectSelectModal";
|
||||
import {
|
||||
buildSessionPath,
|
||||
getSessionIdFromPath,
|
||||
} from "../../utils/sessionRoute";
|
||||
import { useSidebarModeStore } from "../../stores/sidebarModeStore";
|
||||
import styles from "./index.module.less";
|
||||
|
||||
const CONFIRMED_KEY = "qwenpaw-coding-mode-confirmed";
|
||||
|
||||
export default function CodingModeToggle() {
|
||||
const { t } = useTranslation();
|
||||
const { codingMode, initialized, setCodingMode } = useCodingMode();
|
||||
const { selectedAgent } = useAgentStore();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { projectDir } = useProjectDir();
|
||||
const { setMode: setSidebarMode } = useSidebarModeStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [showProjectSelect, setShowProjectSelect] = useState(false);
|
||||
/** Remember sidebar mode before switching to coding, so we can restore on exit */
|
||||
const previousSidebarModeRef = useRef<"simple" | "full" | null>(null);
|
||||
|
||||
const activate = useCallback(async () => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await fetch(getApiUrl("/coding-mode"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...buildAuthHeaders(),
|
||||
"X-Agent-Id": selectedAgent,
|
||||
},
|
||||
body: JSON.stringify({ enabled: true }),
|
||||
});
|
||||
setCodingMode(true);
|
||||
setSidebarMode("simple");
|
||||
const currentSessionId = getSessionIdFromPath(location.pathname);
|
||||
navigate(buildSessionPath("coding", currentSessionId));
|
||||
} catch {
|
||||
// Silently ignore
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [
|
||||
loading,
|
||||
selectedAgent,
|
||||
setCodingMode,
|
||||
setSidebarMode,
|
||||
navigate,
|
||||
location.pathname,
|
||||
]);
|
||||
|
||||
const deactivate = useCallback(async () => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await fetch(getApiUrl("/coding-mode"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...buildAuthHeaders(),
|
||||
"X-Agent-Id": selectedAgent,
|
||||
},
|
||||
body: JSON.stringify({ enabled: false }),
|
||||
});
|
||||
setCodingMode(false);
|
||||
// Restore sidebar mode to what it was before entering coding mode
|
||||
if (previousSidebarModeRef.current) {
|
||||
setSidebarMode(previousSidebarModeRef.current);
|
||||
previousSidebarModeRef.current = null;
|
||||
}
|
||||
const currentSessionId = getSessionIdFromPath(location.pathname);
|
||||
navigate(buildSessionPath("chat", currentSessionId));
|
||||
} catch {
|
||||
// Silently ignore
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [
|
||||
loading,
|
||||
selectedAgent,
|
||||
setCodingMode,
|
||||
setSidebarMode,
|
||||
navigate,
|
||||
location.pathname,
|
||||
]);
|
||||
|
||||
const toggle = useCallback(async () => {
|
||||
if (codingMode) {
|
||||
// Exiting doesn't need confirmation
|
||||
await deactivate();
|
||||
return;
|
||||
}
|
||||
// Remember current sidebar mode before switching to coding
|
||||
previousSidebarModeRef.current = useSidebarModeStore.getState().mode;
|
||||
// First-time activation: show experimental warning
|
||||
const confirmed = localStorage.getItem(CONFIRMED_KEY);
|
||||
if (!confirmed) {
|
||||
setShowConfirm(true);
|
||||
} else if (projectDir === undefined) {
|
||||
// Never selected a project yet → show project picker
|
||||
setShowProjectSelect(true);
|
||||
} else {
|
||||
// null = workspace default, string = specific path → go directly
|
||||
await activate();
|
||||
}
|
||||
}, [codingMode, activate, deactivate, projectDir]);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
localStorage.setItem(CONFIRMED_KEY, "1");
|
||||
setShowConfirm(false);
|
||||
// After confirming experimental warning, show project selection
|
||||
setShowProjectSelect(true);
|
||||
}, []);
|
||||
|
||||
const handleProjectConfirm = useCallback(async () => {
|
||||
setShowProjectSelect(false);
|
||||
await activate();
|
||||
}, [activate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
title={
|
||||
codingMode
|
||||
? t("codingMode.exitTooltip")
|
||||
: t("codingMode.enterTooltip")
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.toggle} ${codingMode ? styles.active : ""}`}
|
||||
onClick={() => void toggle()}
|
||||
disabled={loading || !initialized}
|
||||
aria-label={
|
||||
codingMode
|
||||
? t("codingMode.exitTooltip")
|
||||
: t("codingMode.enterTooltip")
|
||||
}
|
||||
>
|
||||
<span className={styles.icon}>
|
||||
{codingMode ? <MessageSquare size={14} /> : <Code size={14} />}
|
||||
</span>
|
||||
<span className={styles.label}>
|
||||
{codingMode ? t("codingMode.btnChat") : t("codingMode.btnCode")}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{/* Step 1: Experimental warning */}
|
||||
<Modal
|
||||
open={showConfirm}
|
||||
title={
|
||||
<span className={styles.modalTitle}>
|
||||
<FlaskConical size={16} className={styles.flaskIcon} />
|
||||
{t("codingMode.experimental")}
|
||||
</span>
|
||||
}
|
||||
okText={t("codingMode.confirmBtn")}
|
||||
cancelText={t("common.cancel")}
|
||||
onOk={handleConfirm}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
confirmLoading={loading}
|
||||
width={440}
|
||||
>
|
||||
<div className={styles.modalBody}>
|
||||
<p className={styles.modalDesc}>{t("codingMode.experimentalDesc")}</p>
|
||||
<p className={styles.modalNote}>{t("codingMode.experimentalNote")}</p>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Step 2: Project selection */}
|
||||
<ProjectSelectModal
|
||||
open={showProjectSelect}
|
||||
onClose={() => {
|
||||
// User dismissed → enter with default workspace
|
||||
setShowProjectSelect(false);
|
||||
void activate();
|
||||
}}
|
||||
onConfirm={() => void handleProjectConfirm()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user