添加工具调用解析和聊天工具操作处理
This commit is contained in:
@@ -41,7 +41,18 @@ import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded";
|
||||
|
||||
// Logic
|
||||
import { streamCopilotChat } from "@/lib/chatStream";
|
||||
import { parseAssistantMessageSections } from "./chatMessageSections";
|
||||
import type { StreamEvent } from "@/lib/chatStream";
|
||||
import {
|
||||
parseAssistantMessageSections,
|
||||
parseContentWithToolCalls,
|
||||
type ContentSegment,
|
||||
} from "./chatMessageSections";
|
||||
import { ChatInlineChart } from "./ChatInlineChart";
|
||||
import { ChatToolCallBlock } from "./ChatToolCallBlock";
|
||||
import {
|
||||
useChatToolStore,
|
||||
type ChatToolAction,
|
||||
} from "@/store/chatToolStore";
|
||||
|
||||
// WebKit Speech Recognition compatibility
|
||||
interface SpeechRecognitionEvent extends Event {
|
||||
@@ -213,10 +224,11 @@ type ChatMessageItemProps = {
|
||||
onResume: () => void;
|
||||
onStopSpeech: () => void;
|
||||
isTtsSupported: boolean;
|
||||
sseChartParams?: Array<{ tool: string; params: Record<string, unknown> }>;
|
||||
};
|
||||
|
||||
const ChatMessageItem = React.memo(
|
||||
({ message, theme, messageSpeechState, onSpeak, onPause, onResume, onStopSpeech, isTtsSupported }: ChatMessageItemProps) => {
|
||||
({ message, theme, messageSpeechState, onSpeak, onPause, onResume, onStopSpeech, isTtsSupported, sseChartParams }: ChatMessageItemProps) => {
|
||||
const isUser = message.role === "user";
|
||||
const isErrorMessage = Boolean(message.isError);
|
||||
const parsedAssistantSections =
|
||||
@@ -225,6 +237,12 @@ const ChatMessageItem = React.memo(
|
||||
: null;
|
||||
const answerContent = parsedAssistantSections?.answer ?? message.content;
|
||||
|
||||
// Parse tool_call blocks from the answer for inline rendering
|
||||
const contentSegments: ContentSegment[] =
|
||||
!isUser && !isErrorMessage
|
||||
? parseContentWithToolCalls(answerContent).segments
|
||||
: [{ type: "text", content: answerContent }];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8, x: isUser ? 50 : -50 }}
|
||||
@@ -338,9 +356,89 @@ const ChatMessageItem = React.memo(
|
||||
: "#475569",
|
||||
}}
|
||||
>
|
||||
<div className={markdownStyles.markdown}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{answerContent || "..."}</ReactMarkdown>
|
||||
</div>
|
||||
{contentSegments.map((segment, segIdx) => {
|
||||
if (segment.type === "text") {
|
||||
const text = segment.content.trim();
|
||||
if (!text && contentSegments.length > 1) return null;
|
||||
return (
|
||||
<div key={segIdx} className={markdownStyles.markdown}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{text || "..."}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (segment.type === "tool_call") {
|
||||
if (segment.toolCall.tool === "chart") {
|
||||
return (
|
||||
<ChatInlineChart
|
||||
key={segment.toolCall.id}
|
||||
{...(segment.toolCall.params as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (segment.toolCall.tool === "show_chart") {
|
||||
const p = segment.toolCall.params;
|
||||
return (
|
||||
<ChatInlineChart
|
||||
key={segment.toolCall.id}
|
||||
title={(p.title as string) ?? undefined}
|
||||
chart_type={(p.chart_type as "line" | "bar" | "pie") ?? "line"}
|
||||
x_data={(p.x_data as string[]) ?? []}
|
||||
series={(p.series as import("./ChatInlineChart").ChatChartSeries[]) ?? []}
|
||||
x_axis_name={(p.x_axis_name as string) ?? undefined}
|
||||
y_axis_name={(p.y_axis_name as string) ?? undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ChatToolCallBlock
|
||||
key={segment.toolCall.id}
|
||||
toolCall={segment.toolCall}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (segment.type === "tool_call_pending") {
|
||||
return (
|
||||
<motion.div
|
||||
key="tool-pending"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: [0.4, 1, 0.4] }}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
style={{
|
||||
marginTop: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<AutoAwesome
|
||||
sx={{ fontSize: 14, color: "primary.main" }}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
正在准备工具调用...
|
||||
</Typography>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
{/* SSE-sourced inline charts (from show_chart tool_call events) */}
|
||||
{sseChartParams?.map((chart, idx) => (
|
||||
<ChatInlineChart
|
||||
key={`sse-chart-${idx}`}
|
||||
title={(chart.params.title as string) ?? undefined}
|
||||
chart_type={(chart.params.chart_type as "line" | "bar" | "pie") ?? "line"}
|
||||
x_data={(chart.params.x_data as string[]) ?? []}
|
||||
series={(chart.params.series as import("./ChatInlineChart").ChatChartSeries[]) ?? []}
|
||||
x_axis_name={(chart.params.x_axis_name as string) ?? undefined}
|
||||
y_axis_name={(chart.params.y_axis_name as string) ?? undefined}
|
||||
/>
|
||||
))}
|
||||
</Paper>
|
||||
{!isUser && !isErrorMessage && isTtsSupported && (
|
||||
<Stack direction="row" spacing={0.5} sx={{ mt: 0.5, ml: 0.5 }}>
|
||||
@@ -546,6 +644,13 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
);
|
||||
const [headerMenuAnchorEl, setHeaderMenuAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const [isPresetPanelOpen, setIsPresetPanelOpen] = useState(false);
|
||||
|
||||
// SSE tool_call → inline chart data (keyed by assistantMessageId)
|
||||
const [sseCharts, setSseCharts] = useState<
|
||||
Record<string, Array<{ tool: string; params: Record<string, unknown> }>>
|
||||
>({});
|
||||
|
||||
const dispatchToolAction = useChatToolStore((s) => s.dispatch);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
@@ -619,6 +724,101 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
// Track SSE tool_call hashes to deduplicate against text-parsed tool_calls
|
||||
const sseToolHashes = new Set<string>();
|
||||
|
||||
const handleSseToolCall = (event: StreamEvent & { type: "tool_call" }) => {
|
||||
const { tool, params } = event;
|
||||
const hash = `${tool}:${JSON.stringify(params)}`;
|
||||
sseToolHashes.add(hash);
|
||||
const startTime =
|
||||
(params.start_time as string | undefined) ??
|
||||
(params.startTime as string | undefined) ??
|
||||
(params.from as string | undefined) ??
|
||||
(params.start as string | undefined);
|
||||
const endTime =
|
||||
(params.end_time as string | undefined) ??
|
||||
(params.endTime as string | undefined) ??
|
||||
(params.to as string | undefined) ??
|
||||
(params.end as string | undefined);
|
||||
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"]);
|
||||
};
|
||||
|
||||
// show_chart → store as inline chart for rendering
|
||||
if (tool === "show_chart") {
|
||||
setSseCharts((prev) => ({
|
||||
...prev,
|
||||
[assistantId]: [
|
||||
...(prev[assistantId] ?? []),
|
||||
{ tool, params },
|
||||
],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Other frontend tools → dispatch to chatToolStore immediately
|
||||
const actionMap: Record<string, () => ChatToolAction | null> = {
|
||||
locate_nodes: () => ({
|
||||
type: "locate_nodes" as const,
|
||||
ids: (params.ids as string[]) ?? [],
|
||||
}),
|
||||
locate_pipes: () => ({
|
||||
type: "locate_pipes" as const,
|
||||
ids: (params.ids as string[]) ?? [],
|
||||
}),
|
||||
view_history: () => ({
|
||||
type: "view_history" as const,
|
||||
featureInfos: (params.feature_infos as [string, string][]) ?? [],
|
||||
dataType: (params.data_type as "realtime" | "scheme" | "none") ?? "realtime",
|
||||
startTime,
|
||||
endTime,
|
||||
}),
|
||||
view_scada: () => ({
|
||||
type: "view_scada" as const,
|
||||
featureInfos: resolveScadaFeatureInfos(),
|
||||
startTime,
|
||||
endTime,
|
||||
}),
|
||||
};
|
||||
const buildAction = actionMap[tool];
|
||||
if (buildAction) {
|
||||
const action = buildAction();
|
||||
if (action) dispatchToolAction(action);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await streamCopilotChat({
|
||||
message: prompt,
|
||||
@@ -651,6 +851,8 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
)
|
||||
);
|
||||
setIsStreaming(false);
|
||||
} else if (event.type === "tool_call") {
|
||||
handleSseToolCall(event);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -674,7 +876,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
setIsStreaming(false);
|
||||
}
|
||||
},
|
||||
[conversationId, isStreaming, stopListening],
|
||||
[conversationId, isStreaming, stopListening, dispatchToolAction],
|
||||
);
|
||||
|
||||
const handleSend = async () => {
|
||||
@@ -764,9 +966,10 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
onResume={handleResumeSpeech}
|
||||
onStopSpeech={handleStopSpeech}
|
||||
isTtsSupported={isTtsSupported}
|
||||
sseChartParams={sseCharts[message.id]}
|
||||
/>
|
||||
)),
|
||||
[messages, theme, speechState, speakingMessageId, handleSpeak, handlePauseSpeech, handleResumeSpeech, handleStopSpeech, isTtsSupported],
|
||||
[messages, theme, speechState, speakingMessageId, handleSpeak, handlePauseSpeech, handleResumeSpeech, handleStopSpeech, isTtsSupported, sseCharts],
|
||||
);
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user