| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- import ReactMarkdown from "react-markdown";
- import { useState } from "react";
- import type { Goal } from "../../types/goal";
- import type { Edge, Message } from "../../types/message";
- import styles from "./DetailPanel.module.css";
- import { ImagePreviewModal } from "../ImagePreview/ImagePreviewModal";
- import { extractImagesFromMessage } from "../../utils/imageExtraction";
- interface DetailPanelProps {
- node: Goal | Message | null;
- edge: Edge | null;
- messages?: Message[];
- onClose: () => void;
- }
- export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelProps) => {
- const [previewImage, setPreviewImage] = useState<string | null>(null);
- const renderImages = (msg: Message) => {
- const images = extractImagesFromMessage(msg);
- if (images.length === 0) return null;
- return (
- <div className="grid grid-cols-3 gap-2 mt-2">
- {images.map((img, idx) => (
- <img
- key={idx}
- src={img.url}
- alt={img.alt || "Extracted"}
- className="w-full h-20 object-cover rounded border border-gray-200 cursor-pointer hover:opacity-80 transition-opacity bg-gray-50"
- onClick={() => setPreviewImage(img.url)}
- />
- ))}
- </div>
- );
- };
- const title = node ? "节点详情" : edge ? "连线详情" : "详情";
- const renderMessageContent = (content: Message["content"]) => {
- if (!content) return "";
- if (typeof content === "string") return <ReactMarkdown>{content}</ReactMarkdown>;
- // 如果有 text,优先显示 text
- if (content.text) return <ReactMarkdown>{content.text}</ReactMarkdown>;
- // 如果有 tool_calls,展示 tool_calls 信息
- if (content.tool_calls && content.tool_calls.length > 0) {
- return (
- <div className={styles.toolCalls}>
- {content.tool_calls.map((call) => (
- <div
- key={call.id}
- className={styles.toolCall}
- >
- <div className={styles.toolName}>工具调用: {call.name}</div>
- <pre className={styles.toolArgs}>{JSON.stringify(call.arguments, null, 2)}</pre>
- </div>
- ))}
- </div>
- );
- }
- return <ReactMarkdown>{JSON.stringify(content)}</ReactMarkdown>;
- };
- const isGoal = (node: Goal | Message): node is Goal => {
- return "status" in node;
- };
- 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;
- const renderKnowledge = (knowledge: Goal["knowledge"]) => {
- if (!knowledge || knowledge.length === 0) return null;
- return (
- <div className={styles.knowledgeList}>
- {knowledge.map((item) => (
- <div
- key={item.id}
- className={styles.knowledgeItem}
- >
- <div className={styles.knowledgeHeader}>
- <span className={styles.knowledgeId}>{item.id}</span>
- <div className={styles.knowledgeMetrics}>
- {item.score !== undefined && <span className={styles.metricScore}>⭐ {item.score}</span>}
- {item.quality_score !== undefined && (
- <span className={styles.metricQuality}>✨ {item.quality_score.toFixed(1)}</span>
- )}
- {item.metrics?.helpful !== undefined && (
- <span className={styles.metricHelpful}>👍 {item.metrics.helpful}</span>
- )}
- {item.metrics?.harmful !== undefined && (
- <span className={styles.metricHarmful}>👎 {item.metrics.harmful}</span>
- )}
- </div>
- </div>
- {item.tags?.type && item.tags.type.length > 0 && (
- <div className={styles.knowledgeTags}>
- {item.tags.type.map((tag) => (
- <span
- key={tag}
- className={styles.tag}
- >
- {tag}
- </span>
- ))}
- </div>
- )}
- <div className={styles.knowledgeScenario}>
- <strong>场景:</strong> {item.scenario}
- </div>
- <div className={styles.knowledgeContent}>
- <ReactMarkdown>{item.content}</ReactMarkdown>
- </div>
- </div>
- ))}
- </div>
- );
- };
- return (
- <aside className={styles.panel}>
- <div className={styles.header}>
- <div className={styles.title}>{title}</div>
- <button
- className={styles.close}
- onClick={onClose}
- aria-label="关闭"
- >
- ×
- </button>
- </div>
- <div className={styles.content}>
- {node && (
- <>
- <div className={styles.sectionTitle}>节点</div>
- <div className={styles.section}>
- <div className={styles.label}>ID</div>
- <div className={styles.value}>{isMessageNode(node) ? node.message_id || node.id : node.id}</div>
- </div>
- <div className={styles.section}>
- <div className={styles.label}>图片</div>
- {isMessageNode(node) && renderImages(node)}
- </div>
- {isGoal(node) ? (
- <>
- <div className={styles.section}>
- <div className={styles.label}>目标描述</div>
- <div className={styles.value}>{node.description}</div>
- </div>
- {node.reason && (
- <div className={styles.section}>
- <div className={styles.label}>创建理由</div>
- <div className={styles.value}>{node.reason}</div>
- </div>
- )}
- {node.summary && (
- <div className={styles.section}>
- <div className={styles.label}>总结</div>
- <div className={styles.value}>{node.summary}</div>
- </div>
- )}
- <div className={styles.section}>
- <div className={styles.label}>状态</div>
- <div className={styles.value}>{node.status}</div>
- </div>
- {node.knowledge && node.knowledge.length > 0 && (
- <div className={styles.section}>
- <div className={styles.sectionTitle}>相关知识</div>
- {renderKnowledge(node.knowledge)}
- </div>
- )}
- </>
- ) : (
- <>
- {node.description && (
- <div className={styles.section}>
- <div className={styles.label}>描述</div>
- <div className={styles.value}>{node.description}</div>
- </div>
- )}
- {node.role && (
- <div className={styles.section}>
- <div className={styles.label}>角色</div>
- <div className={styles.value}>{node.role}</div>
- </div>
- )}
- <div className={styles.section}>
- <div className={styles.label}>内容</div>
- {/* <div className={styles.value}>
- {node.content && renderMessageContent(node.content)}
- {renderImages(node)}
- </div> */}
- </div>
- {node.goal_id && (
- <div className={styles.section}>
- <div className={styles.label}>所属目标</div>
- <div className={styles.value}>{node.goal_id}</div>
- </div>
- )}
- {node.tokens !== undefined && node.tokens !== null && (
- <div className={styles.section}>
- <div className={styles.label}>Token数</div>
- <div className={styles.value}>{node.tokens}</div>
- </div>
- )}
- </>
- )}
- </>
- )}
- {messages && messages.length > 0 && (
- <div className={styles.messages}>
- <div className={styles.sectionTitle}>边</div>
- {messages.map((msg, idx) => (
- <div
- key={msg.id || idx}
- className={styles.messageItem}
- >
- <div className={styles.section}>
- <div className={styles.label}>描述</div>
- <div className={styles.value}>{msg.description || "-"}</div>
- </div>
- <div className={styles.section}>
- <div className={styles.label}>内容</div>
- <div className={styles.value}>
- {msg.content && renderMessageContent(msg.content)}
- {renderImages(msg)}
- </div>
- </div>
- </div>
- ))}
- </div>
- )}
- </div>
- <ImagePreviewModal
- visible={!!previewImage}
- onClose={() => setPreviewImage(null)}
- src={previewImage || ""}
- />
- </aside>
- );
- };
|