添加工具调用解析和聊天工具操作处理
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Paper,
|
||||
Stack,
|
||||
Typography,
|
||||
alpha,
|
||||
useTheme,
|
||||
} 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 {
|
||||
useChatToolStore,
|
||||
type ChatToolAction,
|
||||
} from "@/store/chatToolStore";
|
||||
import type { ToolCall } from "./chatMessageSections";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Interactive card rendered inside a chat bubble for tool actions */
|
||||
/* (locate nodes/pipes, open history/SCADA panels). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
type ToolMeta = {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
actionLabel: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const TOOL_META: Record<string, ToolMeta> = {
|
||||
locate_nodes: {
|
||||
label: "定位节点",
|
||||
icon: <LocationOnRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "定位到地图",
|
||||
color: "#5470c6",
|
||||
},
|
||||
locate_pipes: {
|
||||
label: "定位管道",
|
||||
icon: <LocationOnRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "定位到地图",
|
||||
color: "#91cc75",
|
||||
},
|
||||
view_history: {
|
||||
label: "查看计算结果",
|
||||
icon: <TimelineRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "查看曲线",
|
||||
color: "#fac858",
|
||||
},
|
||||
view_scada: {
|
||||
label: "查看监测数据",
|
||||
icon: <SensorsRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "查看数据",
|
||||
color: "#ee6666",
|
||||
},
|
||||
show_chart: {
|
||||
label: "显示图表",
|
||||
icon: <TimelineRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "显示",
|
||||
color: "#73c0de",
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
|
||||
function getToolDescription(toolCall: ToolCall): string {
|
||||
const { params } = toolCall;
|
||||
const resolveScadaFeatureInfos = (): [string, string][] => {
|
||||
const rawFeatureInfos = params.feature_infos;
|
||||
if (Array.isArray(rawFeatureInfos)) {
|
||||
const normalizedFeatureInfos = rawFeatureInfos
|
||||
.map((item) => (Array.isArray(item) ? item : null))
|
||||
.filter((item): item is [unknown, unknown] => Boolean(item))
|
||||
.map(
|
||||
(item) =>
|
||||
[String(item[0] ?? ""), String(item[1] ?? "scada")] as [
|
||||
string,
|
||||
string,
|
||||
],
|
||||
)
|
||||
.filter(([id]) => id.trim().length > 0);
|
||||
if (normalizedFeatureInfos.length > 0) {
|
||||
return normalizedFeatureInfos;
|
||||
}
|
||||
}
|
||||
|
||||
const rawDeviceIds =
|
||||
params.device_ids ??
|
||||
params.deviceId ??
|
||||
params.device_id ??
|
||||
params.id ??
|
||||
params.ids;
|
||||
const deviceIds = Array.isArray(rawDeviceIds)
|
||||
? rawDeviceIds.map((id) => String(id))
|
||||
: typeof rawDeviceIds === "string"
|
||||
? rawDeviceIds
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
return deviceIds.map((id) => [id, "scada"]);
|
||||
};
|
||||
const resolveTimeRange = () => ({
|
||||
startTime:
|
||||
(params.start_time as string | undefined) ??
|
||||
(params.startTime as string | undefined) ??
|
||||
(params.from as string | undefined) ??
|
||||
(params.start as string | undefined),
|
||||
endTime:
|
||||
(params.end_time as string | undefined) ??
|
||||
(params.endTime as string | undefined) ??
|
||||
(params.to as string | undefined) ??
|
||||
(params.end as string | undefined),
|
||||
});
|
||||
switch (toolCall.tool) {
|
||||
case "locate_nodes":
|
||||
case "locate_pipes": {
|
||||
const ids = (params.ids as string[] | undefined) ?? [];
|
||||
return ids.length > 3
|
||||
? `${ids.slice(0, 3).join(", ")} 等 ${ids.length} 个`
|
||||
: ids.join(", ");
|
||||
}
|
||||
case "view_history":
|
||||
case "view_scada": {
|
||||
const infos =
|
||||
toolCall.tool === "view_scada"
|
||||
? resolveScadaFeatureInfos()
|
||||
: ((params.feature_infos as [string, string][] | undefined) ?? []);
|
||||
const names = infos.map(([id]) => id);
|
||||
const base =
|
||||
names.length > 3
|
||||
? `${names.slice(0, 3).join(", ")} 等 ${names.length} 个`
|
||||
: names.join(", ");
|
||||
const { startTime, endTime } = resolveTimeRange();
|
||||
if (!startTime && !endTime) {
|
||||
return base;
|
||||
}
|
||||
const rangeLabel = `时间段: ${startTime ?? "--"} ~ ${endTime ?? "--"}`;
|
||||
return base ? `${base} · ${rangeLabel}` : rangeLabel;
|
||||
}
|
||||
case "show_chart": {
|
||||
return (params.title as string | undefined) ?? "数据图表";
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function buildAction(toolCall: ToolCall): ChatToolAction | null {
|
||||
const { params } = toolCall;
|
||||
const resolveScadaFeatureInfos = (): [string, string][] => {
|
||||
const rawFeatureInfos = params.feature_infos;
|
||||
if (Array.isArray(rawFeatureInfos)) {
|
||||
const normalizedFeatureInfos = rawFeatureInfos
|
||||
.map((item) => (Array.isArray(item) ? item : null))
|
||||
.filter((item): item is [unknown, unknown] => Boolean(item))
|
||||
.map(
|
||||
(item) =>
|
||||
[String(item[0] ?? ""), String(item[1] ?? "scada")] as [
|
||||
string,
|
||||
string,
|
||||
],
|
||||
)
|
||||
.filter(([id]) => id.trim().length > 0);
|
||||
if (normalizedFeatureInfos.length > 0) {
|
||||
return normalizedFeatureInfos;
|
||||
}
|
||||
}
|
||||
|
||||
const rawDeviceIds =
|
||||
params.device_ids ??
|
||||
params.deviceId ??
|
||||
params.device_id ??
|
||||
params.id ??
|
||||
params.ids;
|
||||
const deviceIds = Array.isArray(rawDeviceIds)
|
||||
? rawDeviceIds.map((id) => String(id))
|
||||
: typeof rawDeviceIds === "string"
|
||||
? rawDeviceIds
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
return deviceIds.map((id) => [id, "scada"]);
|
||||
};
|
||||
const resolveTimeRange = () => ({
|
||||
startTime:
|
||||
(params.start_time as string | undefined) ??
|
||||
(params.startTime as string | undefined) ??
|
||||
(params.from as string | undefined) ??
|
||||
(params.start as string | undefined),
|
||||
endTime:
|
||||
(params.end_time as string | undefined) ??
|
||||
(params.endTime as string | undefined) ??
|
||||
(params.to as string | undefined) ??
|
||||
(params.end as string | undefined),
|
||||
});
|
||||
switch (toolCall.tool) {
|
||||
case "locate_nodes":
|
||||
return {
|
||||
type: "locate_nodes",
|
||||
ids: (params.ids as string[] | undefined) ?? [],
|
||||
};
|
||||
case "locate_pipes":
|
||||
return {
|
||||
type: "locate_pipes",
|
||||
ids: (params.ids as string[] | undefined) ?? [],
|
||||
};
|
||||
case "view_history": {
|
||||
const historyRange = resolveTimeRange();
|
||||
return {
|
||||
type: "view_history",
|
||||
featureInfos:
|
||||
(params.feature_infos as [string, string][] | undefined) ?? [],
|
||||
dataType:
|
||||
(params.data_type as "realtime" | "scheme" | "none" | undefined) ??
|
||||
"realtime",
|
||||
startTime: historyRange.startTime,
|
||||
endTime: historyRange.endTime,
|
||||
};
|
||||
}
|
||||
case "view_scada": {
|
||||
const scadaRange = resolveTimeRange();
|
||||
return {
|
||||
type: "view_scada",
|
||||
featureInfos: resolveScadaFeatureInfos(),
|
||||
startTime: scadaRange.startTime,
|
||||
endTime: scadaRange.endTime,
|
||||
};
|
||||
}
|
||||
case "show_chart":
|
||||
return {
|
||||
type: "show_chart",
|
||||
title: params.title as string | undefined,
|
||||
chartType:
|
||||
(params.chart_type as "line" | "bar" | "pie" | undefined) ?? "line",
|
||||
xData: (params.x_data as string[] | undefined) ?? [],
|
||||
series:
|
||||
(params.series as
|
||||
| Array<{ name: string; data: number[]; type?: "line" | "bar" }>
|
||||
| undefined) ?? [],
|
||||
xAxisName: params.x_axis_name as string | undefined,
|
||||
yAxisName: params.y_axis_name as string | undefined,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- component ---------- */
|
||||
|
||||
export interface ChatToolCallBlockProps {
|
||||
toolCall: ToolCall;
|
||||
}
|
||||
|
||||
export const ChatToolCallBlock: React.FC<ChatToolCallBlockProps> = ({
|
||||
toolCall,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useChatToolStore((s) => s.dispatch);
|
||||
const [executed, setExecuted] = useState(false);
|
||||
|
||||
const meta: ToolMeta = TOOL_META[toolCall.tool] ?? {
|
||||
label: toolCall.tool,
|
||||
icon: null,
|
||||
actionLabel: "执行",
|
||||
color: theme.palette.primary.main,
|
||||
};
|
||||
|
||||
const description = getToolDescription(toolCall);
|
||||
|
||||
const handleExecute = useCallback(() => {
|
||||
const action = buildAction(toolCall);
|
||||
if (action) {
|
||||
dispatch(action);
|
||||
setExecuted(true);
|
||||
}
|
||||
}, [toolCall, dispatch]);
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
mt: 1.5,
|
||||
mb: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${alpha(meta.color, 0.25)}`,
|
||||
bgcolor: alpha(meta.color, 0.04),
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||
{/* Icon */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 2,
|
||||
bgcolor: alpha(meta.color, 0.12),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: meta.color,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{meta.icon}
|
||||
</Box>
|
||||
|
||||
{/* Description */}
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
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>
|
||||
)}
|
||||
</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>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user