重构 Agent 聊天,支持分支管理与消息克隆
This commit is contained in:
@@ -10,11 +10,15 @@ import {
|
||||
Typography,
|
||||
alpha,
|
||||
useTheme,
|
||||
Collapse,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
import LocationOnRounded from "@mui/icons-material/LocationOnRounded";
|
||||
import TimelineRounded from "@mui/icons-material/TimelineRounded";
|
||||
import SensorsRounded from "@mui/icons-material/SensorsRounded";
|
||||
import CheckCircleRounded from "@mui/icons-material/CheckCircleRounded";
|
||||
import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded";
|
||||
import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded";
|
||||
|
||||
import {
|
||||
useChatToolStore,
|
||||
@@ -45,6 +49,26 @@ const LOCATE_TOOL_TO_LAYER: Record<string, string> = {
|
||||
};
|
||||
|
||||
const LOCATE_LINE_TOOLS = new Set<string>(["locate_pipes"]);
|
||||
const LOCATE_ID_PARAM_KEYS = [
|
||||
"ids",
|
||||
"id",
|
||||
"feature_ids",
|
||||
"feature_id",
|
||||
"node_ids",
|
||||
"node_id",
|
||||
"junction_ids",
|
||||
"junction_id",
|
||||
"pipe_ids",
|
||||
"pipe_id",
|
||||
"valve_ids",
|
||||
"valve_id",
|
||||
"reservoir_ids",
|
||||
"reservoir_id",
|
||||
"pump_ids",
|
||||
"pump_id",
|
||||
"tank_ids",
|
||||
"tank_id",
|
||||
] as const;
|
||||
|
||||
const TOOL_META: Record<string, ToolMeta> = {
|
||||
locate_features: {
|
||||
@@ -111,21 +135,32 @@ const TOOL_META: Record<string, ToolMeta> = {
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
|
||||
function getToolDescription(toolCall: ToolCall): string {
|
||||
const { params } = toolCall;
|
||||
const normalizeIds = (): string[] => {
|
||||
const rawIds = params.ids;
|
||||
if (Array.isArray(rawIds)) {
|
||||
return rawIds.map((id) => String(id)).filter((id) => id.trim().length > 0);
|
||||
function normalizeLocateIds(params: Record<string, unknown>): string[] {
|
||||
for (const key of LOCATE_ID_PARAM_KEYS) {
|
||||
const rawValue = params[key];
|
||||
if (Array.isArray(rawValue)) {
|
||||
const normalized = rawValue
|
||||
.map((id) => String(id).trim())
|
||||
.filter(Boolean);
|
||||
if (normalized.length > 0) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
if (typeof rawIds === "string") {
|
||||
return rawIds
|
||||
if (typeof rawValue === "string" || typeof rawValue === "number") {
|
||||
const normalized = String(rawValue)
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean);
|
||||
if (normalized.length > 0) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getToolDescription(toolCall: ToolCall): string {
|
||||
const { params } = toolCall;
|
||||
const resolveScadaFeatureInfos = (): [string, string][] => {
|
||||
const rawFeatureInfos = params.feature_infos;
|
||||
if (Array.isArray(rawFeatureInfos)) {
|
||||
@@ -189,7 +224,7 @@ function getToolDescription(toolCall: ToolCall): string {
|
||||
case "locate_reservoirs":
|
||||
case "locate_pumps":
|
||||
case "locate_tanks": {
|
||||
const ids = normalizeIds();
|
||||
const ids = normalizeLocateIds(params);
|
||||
const idsText =
|
||||
ids.length > 3
|
||||
? `${ids.slice(0, 3).join(", ")} 等 ${ids.length} 个`
|
||||
@@ -233,19 +268,6 @@ function getToolDescription(toolCall: ToolCall): string {
|
||||
|
||||
function buildAction(toolCall: ToolCall): ChatToolAction | null {
|
||||
const { params } = toolCall;
|
||||
const normalizeIds = (): string[] => {
|
||||
const rawIds = params.ids;
|
||||
if (Array.isArray(rawIds)) {
|
||||
return rawIds.map((id) => String(id)).filter((id) => id.trim().length > 0);
|
||||
}
|
||||
if (typeof rawIds === "string") {
|
||||
return rawIds
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
const resolveScadaFeatureInfos = (): [string, string][] => {
|
||||
const rawFeatureInfos = params.feature_infos;
|
||||
if (Array.isArray(rawFeatureInfos)) {
|
||||
@@ -302,13 +324,13 @@ function buildAction(toolCall: ToolCall): ChatToolAction | null {
|
||||
? featureTypeRaw.trim().toLowerCase()
|
||||
: "";
|
||||
const config = locateFeatureTypeToConfig(featureType);
|
||||
if (!config) return null;
|
||||
return {
|
||||
type: "locate_features",
|
||||
ids: normalizeIds(),
|
||||
layer: config.layer,
|
||||
geometryKind: config.geometryKind,
|
||||
};
|
||||
if (!config) return null;
|
||||
return {
|
||||
type: "locate_features",
|
||||
ids: normalizeLocateIds(params),
|
||||
layer: config.layer,
|
||||
geometryKind: config.geometryKind,
|
||||
};
|
||||
}
|
||||
case "locate_junctions":
|
||||
case "locate_pipes":
|
||||
@@ -320,7 +342,7 @@ function buildAction(toolCall: ToolCall): ChatToolAction | null {
|
||||
if (!layer) return null;
|
||||
return {
|
||||
type: "locate_features",
|
||||
ids: normalizeIds(),
|
||||
ids: normalizeLocateIds(params),
|
||||
layer,
|
||||
geometryKind: LOCATE_LINE_TOOLS.has(toolCall.tool) ? "line" : "point",
|
||||
};
|
||||
@@ -378,12 +400,13 @@ export const ChatToolCallBlock: React.FC<ChatToolCallBlockProps> = ({
|
||||
const theme = useTheme();
|
||||
const dispatch = useChatToolStore((s) => s.dispatch);
|
||||
const [executed, setExecuted] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const meta: ToolMeta = TOOL_META[toolCall.tool] ?? {
|
||||
label: toolCall.tool,
|
||||
icon: null,
|
||||
icon: <TimelineRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "执行",
|
||||
color: theme.palette.primary.main,
|
||||
color: "#00acc1",
|
||||
};
|
||||
|
||||
const description = getToolDescription(toolCall);
|
||||
@@ -400,97 +423,143 @@ export const ChatToolCallBlock: React.FC<ChatToolCallBlockProps> = ({
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
mt: 1.5,
|
||||
mt: 1,
|
||||
mb: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${alpha(meta.color, 0.25)}`,
|
||||
bgcolor: alpha(meta.color, 0.04),
|
||||
overflow: "hidden",
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${alpha(meta.color, 0.3)}`,
|
||||
bgcolor: alpha(meta.color, 0.05),
|
||||
backdropFilter: "blur(12px)",
|
||||
transition: "all 0.3s ease",
|
||||
"&:hover": {
|
||||
bgcolor: alpha(meta.color, 0.08),
|
||||
border: `1px solid ${alpha(meta.color, 0.4)}`,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||
<Box
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
{/* Icon */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 2,
|
||||
bgcolor: alpha(meta.color, 0.12),
|
||||
borderRadius: "50%",
|
||||
bgcolor: alpha(meta.color, 0.15),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: meta.color,
|
||||
flexShrink: 0,
|
||||
boxShadow: `0 2px 8px ${alpha(meta.color, 0.2)}`,
|
||||
}}
|
||||
>
|
||||
{meta.icon}
|
||||
</Box>
|
||||
|
||||
{/* Description */}
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
{/* Title */}
|
||||
<Box sx={{ flex: 1, minWidth: 0, display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontWeight: 700,
|
||||
color: "text.primary",
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
{meta.label}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
fontSize: "0.75rem",
|
||||
display: "block",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
{!expanded && description && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
fontSize: "0.75rem",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: 180,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
• {description}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Action */}
|
||||
{executed ? (
|
||||
<Chip
|
||||
icon={<CheckCircleRounded sx={{ fontSize: 16 }} />}
|
||||
label="已执行"
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: alpha("#4caf50", 0.1),
|
||||
color: "#4caf50",
|
||||
fontWeight: 600,
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleExecute}
|
||||
sx={{
|
||||
borderColor: alpha(meta.color, 0.4),
|
||||
color: meta.color,
|
||||
fontWeight: 600,
|
||||
fontSize: "0.75rem",
|
||||
borderRadius: 2,
|
||||
textTransform: "none",
|
||||
whiteSpace: "nowrap",
|
||||
"&:hover": {
|
||||
borderColor: meta.color,
|
||||
bgcolor: alpha(meta.color, 0.08),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{meta.actionLabel}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
<IconButton size="small" sx={{ color: "text.secondary", width: 28, height: 28, pointerEvents: "none" }}>
|
||||
{expanded ? <KeyboardArrowUpRounded fontSize="small" /> : <KeyboardArrowDownRounded fontSize="small" />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ px: 1.5, pb: 1.5, pt: 0 }}>
|
||||
<Stack direction="column" spacing={1.5}>
|
||||
{description && (
|
||||
<Box sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 3,
|
||||
bgcolor: alpha("#000", 0.03),
|
||||
border: `1px solid ${alpha("#000", 0.05)}`,
|
||||
}}>
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={700} sx={{ mb: 0.5, display: 'block' }}>
|
||||
执行参数
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.primary" sx={{ wordBreak: 'break-word', fontFamily: 'monospace', fontSize: '0.8rem' }}>
|
||||
{description}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
{executed ? (
|
||||
<Chip
|
||||
icon={<CheckCircleRounded sx={{ fontSize: 16 }} />}
|
||||
label="已执行"
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: alpha("#00e676", 0.15),
|
||||
color: "#00c853",
|
||||
fontWeight: 700,
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
disableElevation
|
||||
onClick={(e) => { e.stopPropagation(); handleExecute(); }}
|
||||
sx={{
|
||||
bgcolor: meta.color,
|
||||
color: "#fff",
|
||||
fontWeight: 700,
|
||||
fontSize: "0.8rem",
|
||||
borderRadius: 2.5,
|
||||
px: 2,
|
||||
textTransform: "none",
|
||||
boxShadow: `0 4px 12px ${alpha(meta.color, 0.3)}`,
|
||||
"&:hover": {
|
||||
bgcolor: meta.color,
|
||||
filter: "brightness(0.9)",
|
||||
boxShadow: `0 6px 16px ${alpha(meta.color, 0.4)}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{meta.actionLabel}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user