|
@@ -13,9 +13,18 @@ interface DetailPanelProps {
|
|
|
onClose: () => void;
|
|
onClose: () => void;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelProps) => {
|
|
|
|
|
|
|
+export const DetailPanel = ({
|
|
|
|
|
+ node,
|
|
|
|
|
+ edge,
|
|
|
|
|
+ messages = [],
|
|
|
|
|
+ onClose,
|
|
|
|
|
+}: DetailPanelProps) => {
|
|
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
+ console.log("DetailPanel - node:", node);
|
|
|
|
|
+ console.log("DetailPanel - edge:", edge);
|
|
|
|
|
+ console.log("DetailPanel - messages:", messages);
|
|
|
|
|
+
|
|
|
const renderImages = (msg: Message) => {
|
|
const renderImages = (msg: Message) => {
|
|
|
const images = extractImagesFromMessage(msg);
|
|
const images = extractImagesFromMessage(msg);
|
|
|
if (images.length === 0) return null;
|
|
if (images.length === 0) return null;
|
|
@@ -38,44 +47,66 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
|
|
|
|
|
|
|
|
const renderMessageContent = (content: Message["content"]) => {
|
|
const renderMessageContent = (content: Message["content"]) => {
|
|
|
if (!content) return "";
|
|
if (!content) return "";
|
|
|
- if (typeof content === "string") return <ReactMarkdown>{content}</ReactMarkdown>;
|
|
|
|
|
|
|
+ if (typeof content === "string")
|
|
|
|
|
+ return <ReactMarkdown>{content}</ReactMarkdown>;
|
|
|
|
|
|
|
|
- // 如果有 text,优先显示 text
|
|
|
|
|
- if (content.text) return <ReactMarkdown>{content.text}</ReactMarkdown>;
|
|
|
|
|
|
|
+ const hasText = !!content.text;
|
|
|
|
|
+ const hasToolCalls = content.tool_calls && content.tool_calls.length > 0;
|
|
|
|
|
+ const hasToolResult = !!content.tool_name && !!content.result;
|
|
|
|
|
|
|
|
- // 如果有 tool_calls,展示 tool_calls 信息
|
|
|
|
|
- if (content.tool_calls && content.tool_calls.length > 0) {
|
|
|
|
|
|
|
+ if (hasText || hasToolCalls || hasToolResult) {
|
|
|
return (
|
|
return (
|
|
|
- <div className={styles.toolCalls}>
|
|
|
|
|
- {content.tool_calls.map((call, idx) => {
|
|
|
|
|
- const anyCall = call as unknown as Record<string, unknown>;
|
|
|
|
|
- const fn = anyCall.function as Record<string, unknown> | undefined;
|
|
|
|
|
- const name =
|
|
|
|
|
- (fn && (fn.name as string)) ||
|
|
|
|
|
- (anyCall.name as string) ||
|
|
|
|
|
- ((content as unknown as Record<string, unknown>).tool_name as string) ||
|
|
|
|
|
- `tool_${idx}`;
|
|
|
|
|
- let args: unknown =
|
|
|
|
|
- (fn && fn.arguments) || anyCall.arguments || (content as unknown as Record<string, unknown>).arguments;
|
|
|
|
|
- if (typeof args === "string") {
|
|
|
|
|
- try {
|
|
|
|
|
- args = JSON.parse(args);
|
|
|
|
|
- } catch {
|
|
|
|
|
- // keep as string if JSON.parse fails
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- const key = (anyCall.id as string) || `${name}-${idx}`;
|
|
|
|
|
- return (
|
|
|
|
|
- <div
|
|
|
|
|
- key={key}
|
|
|
|
|
- className={styles.toolCall}
|
|
|
|
|
- >
|
|
|
|
|
- <div className={styles.toolName}>工具调用: {name}</div>
|
|
|
|
|
- <pre className={styles.toolArgs}>{JSON.stringify(args, null, 2)}</pre>
|
|
|
|
|
|
|
+ <>
|
|
|
|
|
+ {hasText && <ReactMarkdown>{content.text!}</ReactMarkdown>}
|
|
|
|
|
+ {hasToolCalls && (
|
|
|
|
|
+ <div className={styles.toolCalls}>
|
|
|
|
|
+ {content.tool_calls!.map((call, idx) => {
|
|
|
|
|
+ const anyCall = call as unknown as Record<string, unknown>;
|
|
|
|
|
+ const fn = anyCall.function as
|
|
|
|
|
+ | Record<string, unknown>
|
|
|
|
|
+ | undefined;
|
|
|
|
|
+ const name =
|
|
|
|
|
+ (fn && (fn.name as string)) ||
|
|
|
|
|
+ (anyCall.name as string) ||
|
|
|
|
|
+ ((content as unknown as Record<string, unknown>)
|
|
|
|
|
+ .tool_name as string) ||
|
|
|
|
|
+ `tool_${idx}`;
|
|
|
|
|
+ let args: unknown =
|
|
|
|
|
+ (fn && fn.arguments) ||
|
|
|
|
|
+ anyCall.arguments ||
|
|
|
|
|
+ (content as unknown as Record<string, unknown>).arguments;
|
|
|
|
|
+ if (typeof args === "string") {
|
|
|
|
|
+ try {
|
|
|
|
|
+ args = JSON.parse(args);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // keep as string if JSON.parse fails
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const key = (anyCall.id as string) || `${name}-${idx}`;
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div key={key} className={styles.toolCall}>
|
|
|
|
|
+ <div className={styles.toolName}>工具调用: {name}</div>
|
|
|
|
|
+ <pre className={styles.toolArgs}>
|
|
|
|
|
+ {JSON.stringify(args, null, 2)}
|
|
|
|
|
+ </pre>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {hasToolResult && (
|
|
|
|
|
+ <div className={styles.toolResult}>
|
|
|
|
|
+ <div className={styles.toolName}>工具: {content.tool_name}</div>
|
|
|
|
|
+ <div className={styles.resultContent}>
|
|
|
|
|
+ {typeof content.result === "string" ? (
|
|
|
|
|
+ <ReactMarkdown>{content.result}</ReactMarkdown>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <pre>{JSON.stringify(content.result, null, 2)}</pre>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
- );
|
|
|
|
|
- })}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -83,11 +114,15 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const isGoal = (node: Goal | Message): node is Goal => {
|
|
const isGoal = (node: Goal | Message): node is Goal => {
|
|
|
- return "status" in node;
|
|
|
|
|
|
|
+ // Message 有 message_id 或 role 字段,Goal 没有
|
|
|
|
|
+ // 优先判断是否为 Message,排除后才是 Goal
|
|
|
|
|
+ return !("message_id" in node) && !("role" in node) && "status" in node;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const isMessageNode = (node: Goal | Message): node is Message =>
|
|
const isMessageNode = (node: Goal | Message): node is Message =>
|
|
|
- "message_id" in node || "role" in node || "content" in node || "goal_id" in node || "tokens" in node;
|
|
|
|
|
|
|
+ "message_id" in node ||
|
|
|
|
|
+ "role" in node ||
|
|
|
|
|
+ "sequence" in node;
|
|
|
|
|
|
|
|
const renderKnowledge = (knowledge: Goal["knowledge"]) => {
|
|
const renderKnowledge = (knowledge: Goal["knowledge"]) => {
|
|
|
if (!knowledge || knowledge.length === 0) return null;
|
|
if (!knowledge || knowledge.length === 0) return null;
|
|
@@ -95,32 +130,34 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
|
|
|
return (
|
|
return (
|
|
|
<div className={styles.knowledgeList}>
|
|
<div className={styles.knowledgeList}>
|
|
|
{knowledge.map((item) => (
|
|
{knowledge.map((item) => (
|
|
|
- <div
|
|
|
|
|
- key={item.id}
|
|
|
|
|
- className={styles.knowledgeItem}
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <div key={item.id} className={styles.knowledgeItem}>
|
|
|
<div className={styles.knowledgeHeader}>
|
|
<div className={styles.knowledgeHeader}>
|
|
|
<span className={styles.knowledgeId}>{item.id}</span>
|
|
<span className={styles.knowledgeId}>{item.id}</span>
|
|
|
<div className={styles.knowledgeMetrics}>
|
|
<div className={styles.knowledgeMetrics}>
|
|
|
- {item.score !== undefined && <span className={styles.metricScore}>⭐ {item.score}</span>}
|
|
|
|
|
|
|
+ {item.score !== undefined && (
|
|
|
|
|
+ <span className={styles.metricScore}>⭐ {item.score}</span>
|
|
|
|
|
+ )}
|
|
|
{item.quality_score !== undefined && (
|
|
{item.quality_score !== undefined && (
|
|
|
- <span className={styles.metricQuality}>✨ {item.quality_score.toFixed(1)}</span>
|
|
|
|
|
|
|
+ <span className={styles.metricQuality}>
|
|
|
|
|
+ ✨ {item.quality_score.toFixed(1)}
|
|
|
|
|
+ </span>
|
|
|
)}
|
|
)}
|
|
|
{item.metrics?.helpful !== undefined && (
|
|
{item.metrics?.helpful !== undefined && (
|
|
|
- <span className={styles.metricHelpful}>👍 {item.metrics.helpful}</span>
|
|
|
|
|
|
|
+ <span className={styles.metricHelpful}>
|
|
|
|
|
+ 👍 {item.metrics.helpful}
|
|
|
|
|
+ </span>
|
|
|
)}
|
|
)}
|
|
|
{item.metrics?.harmful !== undefined && (
|
|
{item.metrics?.harmful !== undefined && (
|
|
|
- <span className={styles.metricHarmful}>👎 {item.metrics.harmful}</span>
|
|
|
|
|
|
|
+ <span className={styles.metricHarmful}>
|
|
|
|
|
+ 👎 {item.metrics.harmful}
|
|
|
|
|
+ </span>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
{item.tags?.type && item.tags.type.length > 0 && (
|
|
{item.tags?.type && item.tags.type.length > 0 && (
|
|
|
<div className={styles.knowledgeTags}>
|
|
<div className={styles.knowledgeTags}>
|
|
|
{item.tags.type.map((tag) => (
|
|
{item.tags.type.map((tag) => (
|
|
|
- <span
|
|
|
|
|
- key={tag}
|
|
|
|
|
- className={styles.tag}
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <span key={tag} className={styles.tag}>
|
|
|
{tag}
|
|
{tag}
|
|
|
</span>
|
|
</span>
|
|
|
))}
|
|
))}
|
|
@@ -142,11 +179,7 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
|
|
|
<aside className={styles.panel}>
|
|
<aside className={styles.panel}>
|
|
|
<div className={styles.header}>
|
|
<div className={styles.header}>
|
|
|
<div className={styles.title}>{title}</div>
|
|
<div className={styles.title}>{title}</div>
|
|
|
- <button
|
|
|
|
|
- className={styles.close}
|
|
|
|
|
- onClick={onClose}
|
|
|
|
|
- aria-label="关闭"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <button className={styles.close} onClick={onClose} aria-label="关闭">
|
|
|
×
|
|
×
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
@@ -156,14 +189,17 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
|
|
|
<div className={styles.sectionTitle}>节点</div>
|
|
<div className={styles.sectionTitle}>节点</div>
|
|
|
<div className={styles.section}>
|
|
<div className={styles.section}>
|
|
|
<div className={styles.label}>ID</div>
|
|
<div className={styles.label}>ID</div>
|
|
|
- <div className={styles.value}>{isMessageNode(node) ? node.message_id || node.id : node.id}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- {isMessageNode(node) && extractImagesFromMessage(node).length > 0 && (
|
|
|
|
|
- <div className={styles.section}>
|
|
|
|
|
- <div className={styles.label}>图片</div>
|
|
|
|
|
- {renderImages(node)}
|
|
|
|
|
|
|
+ <div className={styles.value}>
|
|
|
|
|
+ {isMessageNode(node) ? node.message_id || node.id : node.id}
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {isMessageNode(node) &&
|
|
|
|
|
+ extractImagesFromMessage(node).length > 0 && (
|
|
|
|
|
+ <div className={styles.section}>
|
|
|
|
|
+ <div className={styles.label}>图片</div>
|
|
|
|
|
+ {renderImages(node)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
|
|
|
{isGoal(node) ? (
|
|
{isGoal(node) ? (
|
|
|
<>
|
|
<>
|
|
@@ -210,7 +246,9 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
|
|
|
)}
|
|
)}
|
|
|
<div className={styles.section}>
|
|
<div className={styles.section}>
|
|
|
<div className={styles.label}>内容</div>
|
|
<div className={styles.label}>内容</div>
|
|
|
- <div className={styles.value}>{node.content && renderMessageContent(node.content)}</div>
|
|
|
|
|
|
|
+ <div className={styles.value}>
|
|
|
|
|
+ {node.content && renderMessageContent(node.content)}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
{node.goal_id && (
|
|
{node.goal_id && (
|
|
|
<div className={styles.section}>
|
|
<div className={styles.section}>
|
|
@@ -232,17 +270,16 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
|
|
|
<div className={styles.messages}>
|
|
<div className={styles.messages}>
|
|
|
<div className={styles.sectionTitle}>边</div>
|
|
<div className={styles.sectionTitle}>边</div>
|
|
|
{messages.map((msg, idx) => (
|
|
{messages.map((msg, idx) => (
|
|
|
- <div
|
|
|
|
|
- key={msg.id || idx}
|
|
|
|
|
- className={styles.messageItem}
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <div key={msg.id || idx} className={styles.messageItem}>
|
|
|
<div className={styles.section}>
|
|
<div className={styles.section}>
|
|
|
<div className={styles.label}>描述</div>
|
|
<div className={styles.label}>描述</div>
|
|
|
<div className={styles.value}>{msg.description || "-"}</div>
|
|
<div className={styles.value}>{msg.description || "-"}</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div className={styles.section}>
|
|
<div className={styles.section}>
|
|
|
<div className={styles.label}>内容</div>
|
|
<div className={styles.label}>内容</div>
|
|
|
- <div className={styles.value}>{msg.content && renderMessageContent(msg.content)}</div>
|
|
|
|
|
|
|
+ <div className={styles.value}>
|
|
|
|
|
+ {msg.content && renderMessageContent(msg.content)}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|