feat(chat): 完善运行状态与权限交互
This commit is contained in:
@@ -27,6 +27,8 @@ import BoltRounded from "@mui/icons-material/BoltRounded";
|
||||
import AutoAwesomeRounded from "@mui/icons-material/AutoAwesomeRounded";
|
||||
import VerifiedUserRounded from "@mui/icons-material/VerifiedUserRounded";
|
||||
import AdminPanelSettingsRounded from "@mui/icons-material/AdminPanelSettingsRounded";
|
||||
import WarningAmberRounded from "@mui/icons-material/WarningAmberRounded";
|
||||
import type { AgentRuntimeState } from "@/lib/agentRuntime";
|
||||
import type { AgentModelOption } from "@/lib/chatModels";
|
||||
import type { AgentApprovalMode, AgentModel } from "@/lib/chatStream";
|
||||
|
||||
@@ -40,6 +42,7 @@ export type AgentComposerHandle = {
|
||||
|
||||
type AgentComposerProps = {
|
||||
isHydrating?: boolean;
|
||||
runtimeState?: AgentRuntimeState;
|
||||
isStreaming: boolean;
|
||||
isListening: boolean;
|
||||
isSttSupported: boolean;
|
||||
@@ -55,6 +58,35 @@ type AgentComposerProps = {
|
||||
onApprovalModeChange: (mode: AgentApprovalMode) => void;
|
||||
};
|
||||
|
||||
const approvalModeOptions: ReadonlyArray<{
|
||||
value: AgentApprovalMode;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: React.ElementType;
|
||||
}> = [
|
||||
{
|
||||
value: "request",
|
||||
label: "请求批准",
|
||||
description: "白名单外的工具权限逐次确认",
|
||||
icon: VerifiedUserRounded,
|
||||
},
|
||||
{
|
||||
value: "auto",
|
||||
label: "自动批准",
|
||||
description: "低风险自动批准,其余仍需确认",
|
||||
icon: AdminPanelSettingsRounded,
|
||||
},
|
||||
{
|
||||
value: "always",
|
||||
label: "始终允许",
|
||||
description: "除明确禁止项外自动放行",
|
||||
icon: WarningAmberRounded,
|
||||
},
|
||||
];
|
||||
|
||||
const getApprovalModeOption = (value: AgentApprovalMode) =>
|
||||
approvalModeOptions.find((option) => option.value === value) ?? approvalModeOptions[0];
|
||||
|
||||
const renderModelIcon = (
|
||||
icon: AgentModelOption["icon"] | undefined,
|
||||
props?: React.ComponentProps<typeof BoltRounded>,
|
||||
@@ -67,6 +99,7 @@ const renderModelIcon = (
|
||||
|
||||
export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposerProps>(function AgentComposer({
|
||||
isHydrating = false,
|
||||
runtimeState = "ready",
|
||||
isStreaming,
|
||||
isListening,
|
||||
isSttSupported,
|
||||
@@ -85,8 +118,19 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
|
||||
const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
|
||||
const [input, setInput] = React.useState("");
|
||||
const [isPresetOpen, setIsPresetOpen] = React.useState(false);
|
||||
const canSend = input.trim().length > 0 && !isStreaming && !isHydrating;
|
||||
const isRuntimeReady = runtimeState === "ready";
|
||||
const canSend = input.trim().length > 0 && !isStreaming && !isHydrating && isRuntimeReady;
|
||||
const placeholder = isHydrating
|
||||
? "正在加载对话记录..."
|
||||
: runtimeState === "checking"
|
||||
? "正在连接 Agent 服务..."
|
||||
: runtimeState === "models_unavailable"
|
||||
? "模型尚未加载,暂时无法发送消息"
|
||||
: runtimeState === "unavailable"
|
||||
? "Agent 服务未就绪,暂时无法发送消息"
|
||||
: "描述你的分析目标,或点击上方指令库...";
|
||||
const selectedModelOption = modelOptions.find((model) => model.id === selectedModel);
|
||||
const selectedApprovalModeOption = getApprovalModeOption(approvalMode);
|
||||
|
||||
React.useImperativeHandle(
|
||||
ref,
|
||||
@@ -102,10 +146,10 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
|
||||
|
||||
const handleSend = React.useCallback(() => {
|
||||
const prompt = input.trim();
|
||||
if (!prompt || isStreaming || isHydrating) return;
|
||||
if (!prompt || isStreaming || isHydrating || !isRuntimeReady) return;
|
||||
setInput("");
|
||||
onSend(prompt);
|
||||
}, [input, isHydrating, isStreaming, onSend]);
|
||||
}, [input, isHydrating, isRuntimeReady, isStreaming, onSend]);
|
||||
|
||||
return (
|
||||
<Box sx={{ px: 2, pb: 2, pt: 1, zIndex: 10 }}>
|
||||
@@ -154,6 +198,7 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
|
||||
label={prompt.replace(/[。.]$/, "")}
|
||||
size="medium"
|
||||
clickable
|
||||
disabled={!isRuntimeReady || isHydrating || isStreaming}
|
||||
onClick={() => {
|
||||
setInput(prompt);
|
||||
setIsPresetOpen(false);
|
||||
@@ -209,12 +254,12 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
placeholder={isHydrating ? "正在加载对话记录..." : "描述你的分析目标,或点击上方指令库..."}
|
||||
placeholder={placeholder}
|
||||
fullWidth
|
||||
multiline
|
||||
maxRows={5}
|
||||
variant="standard"
|
||||
disabled={isHydrating}
|
||||
disabled={isHydrating || !isRuntimeReady}
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
sx: { px: 1, py: 0.5, fontSize: "1rem", lineHeight: 1.6, fontWeight: 500, color: "text.primary" },
|
||||
@@ -223,26 +268,33 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
|
||||
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mt: 2 }}>
|
||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||
<FormControl size="small" sx={{ minWidth: 96 }}>
|
||||
<FormControl size="small" sx={{ minWidth: 128 }}>
|
||||
<Select
|
||||
value={approvalMode}
|
||||
onChange={(event) =>
|
||||
onApprovalModeChange(event.target.value as AgentApprovalMode)
|
||||
}
|
||||
disabled={isHydrating || isStreaming}
|
||||
disabled={isHydrating || isStreaming || !isRuntimeReady}
|
||||
aria-label="权限批准模式"
|
||||
renderValue={(val) => (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.45 }}>
|
||||
{val === "always" ? (
|
||||
<AdminPanelSettingsRounded sx={{ fontSize: 18, color: "inherit" }} />
|
||||
) : (
|
||||
<VerifiedUserRounded sx={{ fontSize: 18, color: "inherit" }} />
|
||||
)}
|
||||
<Typography sx={{ fontSize: "0.75rem", fontWeight: 600, color: "inherit" }}>
|
||||
{val === "always" ? "始终允许" : "请求批准"}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
renderValue={() => {
|
||||
const SelectedApprovalIcon = selectedApprovalModeOption.icon;
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.45 }}>
|
||||
<SelectedApprovalIcon
|
||||
sx={{
|
||||
fontSize: 18,
|
||||
color:
|
||||
selectedApprovalModeOption.value === "always"
|
||||
? "warning.main"
|
||||
: "inherit",
|
||||
}}
|
||||
/>
|
||||
<Typography sx={{ fontSize: "0.75rem", fontWeight: 600, color: "inherit" }}>
|
||||
{selectedApprovalModeOption.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
MenuProps={{
|
||||
anchorOrigin: { vertical: "top", horizontal: "left" },
|
||||
transformOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
@@ -250,7 +302,7 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
|
||||
PaperProps: {
|
||||
sx: {
|
||||
mb: 1.5,
|
||||
width: 210,
|
||||
width: 248,
|
||||
borderRadius: 4,
|
||||
bgcolor: alpha("#fff", 0.9),
|
||||
backdropFilter: "blur(24px)",
|
||||
@@ -269,6 +321,7 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
|
||||
"&:hover": { bgcolor: alpha("#00acc1", 0.12) },
|
||||
"& .title": { color: "#00838f" },
|
||||
"& .icon": { color: "#00acc1" },
|
||||
"& .always-icon": { color: "warning.main" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -298,20 +351,47 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="request">
|
||||
<VerifiedUserRounded className="icon" sx={{ mr: 1.5, mt: 0.15, fontSize: 18, color: "text.secondary" }} />
|
||||
<Box>
|
||||
<Typography className="title" sx={{ fontSize: "0.85rem", fontWeight: 700, color: "text.primary", mb: 0.2 }}>请求批准</Typography>
|
||||
<Typography sx={{ fontSize: "0.7rem", fontWeight: 500, color: "text.secondary", lineHeight: 1.3 }}>工具权限逐次确认</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value="always">
|
||||
<AdminPanelSettingsRounded className="icon" sx={{ mr: 1.5, mt: 0.15, fontSize: 18, color: "text.secondary" }} />
|
||||
<Box>
|
||||
<Typography className="title" sx={{ fontSize: "0.85rem", fontWeight: 700, color: "text.primary", mb: 0.2 }}>始终允许</Typography>
|
||||
<Typography sx={{ fontSize: "0.7rem", fontWeight: 500, color: "text.secondary", lineHeight: 1.3 }}>自动允许本轮权限请求</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
{approvalModeOptions.map((option) => {
|
||||
const ApprovalIcon = option.icon;
|
||||
const isAlways = option.value === "always";
|
||||
return (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
<ApprovalIcon
|
||||
className={isAlways ? "icon always-icon" : "icon"}
|
||||
sx={{
|
||||
mr: 1.5,
|
||||
mt: 0.15,
|
||||
fontSize: 18,
|
||||
color: isAlways ? "warning.main" : "text.secondary",
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Typography
|
||||
className="title"
|
||||
sx={{
|
||||
mb: 0.2,
|
||||
color: "text.primary",
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
fontSize: "0.7rem",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.3,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{option.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
@@ -452,7 +532,7 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
|
||||
) : (
|
||||
<IconButton
|
||||
onClick={onStartListening}
|
||||
disabled={isStreaming || isHydrating}
|
||||
disabled={isStreaming || isHydrating || !isRuntimeReady}
|
||||
aria-label="语音输入"
|
||||
size="small"
|
||||
sx={{ color: "text.secondary", width: 36, height: 36, bgcolor: alpha("#fff", 0.6) }}
|
||||
|
||||
Reference in New Issue
Block a user