DetailPanel.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import ReactMarkdown from "react-markdown";
  2. import { useState } from "react";
  3. import type { Goal } from "../../types/goal";
  4. import type { Edge, Message } from "../../types/message";
  5. import styles from "./DetailPanel.module.css";
  6. import { ImagePreviewModal } from "../ImagePreview/ImagePreviewModal";
  7. import { extractImagesFromMessage } from "../../utils/imageExtraction";
  8. interface DetailPanelProps {
  9. node: Goal | Message | null;
  10. edge: Edge | null;
  11. messages?: Message[];
  12. onClose: () => void;
  13. }
  14. export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelProps) => {
  15. const [previewImage, setPreviewImage] = useState<string | null>(null);
  16. const renderImages = (msg: Message) => {
  17. const images = extractImagesFromMessage(msg);
  18. if (images.length === 0) return null;
  19. return (
  20. <div className="grid grid-cols-3 gap-2 mt-2">
  21. {images.map((img, idx) => (
  22. <img
  23. key={idx}
  24. src={img.url}
  25. alt={img.alt || "Extracted"}
  26. className="w-full h-20 object-cover rounded border border-gray-200 cursor-pointer hover:opacity-80 transition-opacity bg-gray-50"
  27. onClick={() => setPreviewImage(img.url)}
  28. />
  29. ))}
  30. </div>
  31. );
  32. };
  33. const title = node ? "节点详情" : edge ? "连线详情" : "详情";
  34. const renderMessageContent = (content: Message["content"]) => {
  35. if (!content) return "";
  36. if (typeof content === "string") return <ReactMarkdown>{content}</ReactMarkdown>;
  37. // 如果有 text,优先显示 text
  38. if (content.text) return <ReactMarkdown>{content.text}</ReactMarkdown>;
  39. // 如果有 tool_calls,展示 tool_calls 信息
  40. if (content.tool_calls && content.tool_calls.length > 0) {
  41. return (
  42. <div className={styles.toolCalls}>
  43. {content.tool_calls.map((call) => (
  44. <div
  45. key={call.id}
  46. className={styles.toolCall}
  47. >
  48. <div className={styles.toolName}>工具调用: {call.name}</div>
  49. <pre className={styles.toolArgs}>{JSON.stringify(call.arguments, null, 2)}</pre>
  50. </div>
  51. ))}
  52. </div>
  53. );
  54. }
  55. return <ReactMarkdown>{JSON.stringify(content)}</ReactMarkdown>;
  56. };
  57. const isGoal = (node: Goal | Message): node is Goal => {
  58. return "status" in node;
  59. };
  60. const isMessageNode = (node: Goal | Message): node is Message =>
  61. "message_id" in node || "role" in node || "content" in node || "goal_id" in node || "tokens" in node;
  62. const renderKnowledge = (knowledge: Goal["knowledge"]) => {
  63. if (!knowledge || knowledge.length === 0) return null;
  64. return (
  65. <div className={styles.knowledgeList}>
  66. {knowledge.map((item) => (
  67. <div
  68. key={item.id}
  69. className={styles.knowledgeItem}
  70. >
  71. <div className={styles.knowledgeHeader}>
  72. <span className={styles.knowledgeId}>{item.id}</span>
  73. <div className={styles.knowledgeMetrics}>
  74. {item.score !== undefined && <span className={styles.metricScore}>⭐ {item.score}</span>}
  75. {item.quality_score !== undefined && (
  76. <span className={styles.metricQuality}>✨ {item.quality_score.toFixed(1)}</span>
  77. )}
  78. {item.metrics?.helpful !== undefined && (
  79. <span className={styles.metricHelpful}>👍 {item.metrics.helpful}</span>
  80. )}
  81. {item.metrics?.harmful !== undefined && (
  82. <span className={styles.metricHarmful}>👎 {item.metrics.harmful}</span>
  83. )}
  84. </div>
  85. </div>
  86. {item.tags?.type && item.tags.type.length > 0 && (
  87. <div className={styles.knowledgeTags}>
  88. {item.tags.type.map((tag) => (
  89. <span
  90. key={tag}
  91. className={styles.tag}
  92. >
  93. {tag}
  94. </span>
  95. ))}
  96. </div>
  97. )}
  98. <div className={styles.knowledgeScenario}>
  99. <strong>场景:</strong> {item.scenario}
  100. </div>
  101. <div className={styles.knowledgeContent}>
  102. <ReactMarkdown>{item.content}</ReactMarkdown>
  103. </div>
  104. </div>
  105. ))}
  106. </div>
  107. );
  108. };
  109. return (
  110. <aside className={styles.panel}>
  111. <div className={styles.header}>
  112. <div className={styles.title}>{title}</div>
  113. <button
  114. className={styles.close}
  115. onClick={onClose}
  116. aria-label="关闭"
  117. >
  118. ×
  119. </button>
  120. </div>
  121. <div className={styles.content}>
  122. {node && (
  123. <>
  124. <div className={styles.sectionTitle}>节点</div>
  125. <div className={styles.section}>
  126. <div className={styles.label}>ID</div>
  127. <div className={styles.value}>{isMessageNode(node) ? node.message_id || node.id : node.id}</div>
  128. </div>
  129. <div className={styles.section}>
  130. <div className={styles.label}>图片</div>
  131. {isMessageNode(node) && renderImages(node)}
  132. </div>
  133. {isGoal(node) ? (
  134. <>
  135. <div className={styles.section}>
  136. <div className={styles.label}>目标描述</div>
  137. <div className={styles.value}>{node.description}</div>
  138. </div>
  139. {node.reason && (
  140. <div className={styles.section}>
  141. <div className={styles.label}>创建理由</div>
  142. <div className={styles.value}>{node.reason}</div>
  143. </div>
  144. )}
  145. {node.summary && (
  146. <div className={styles.section}>
  147. <div className={styles.label}>总结</div>
  148. <div className={styles.value}>{node.summary}</div>
  149. </div>
  150. )}
  151. <div className={styles.section}>
  152. <div className={styles.label}>状态</div>
  153. <div className={styles.value}>{node.status}</div>
  154. </div>
  155. {node.knowledge && node.knowledge.length > 0 && (
  156. <div className={styles.section}>
  157. <div className={styles.sectionTitle}>相关知识</div>
  158. {renderKnowledge(node.knowledge)}
  159. </div>
  160. )}
  161. </>
  162. ) : (
  163. <>
  164. {node.description && (
  165. <div className={styles.section}>
  166. <div className={styles.label}>描述</div>
  167. <div className={styles.value}>{node.description}</div>
  168. </div>
  169. )}
  170. {node.role && (
  171. <div className={styles.section}>
  172. <div className={styles.label}>角色</div>
  173. <div className={styles.value}>{node.role}</div>
  174. </div>
  175. )}
  176. <div className={styles.section}>
  177. <div className={styles.label}>内容</div>
  178. {/* <div className={styles.value}>
  179. {node.content && renderMessageContent(node.content)}
  180. {renderImages(node)}
  181. </div> */}
  182. </div>
  183. {node.goal_id && (
  184. <div className={styles.section}>
  185. <div className={styles.label}>所属目标</div>
  186. <div className={styles.value}>{node.goal_id}</div>
  187. </div>
  188. )}
  189. {node.tokens !== undefined && node.tokens !== null && (
  190. <div className={styles.section}>
  191. <div className={styles.label}>Token数</div>
  192. <div className={styles.value}>{node.tokens}</div>
  193. </div>
  194. )}
  195. </>
  196. )}
  197. </>
  198. )}
  199. {messages && messages.length > 0 && (
  200. <div className={styles.messages}>
  201. <div className={styles.sectionTitle}>边</div>
  202. {messages.map((msg, idx) => (
  203. <div
  204. key={msg.id || idx}
  205. className={styles.messageItem}
  206. >
  207. <div className={styles.section}>
  208. <div className={styles.label}>描述</div>
  209. <div className={styles.value}>{msg.description || "-"}</div>
  210. </div>
  211. <div className={styles.section}>
  212. <div className={styles.label}>内容</div>
  213. <div className={styles.value}>
  214. {msg.content && renderMessageContent(msg.content)}
  215. {renderImages(msg)}
  216. </div>
  217. </div>
  218. </div>
  219. ))}
  220. </div>
  221. )}
  222. </div>
  223. <ImagePreviewModal
  224. visible={!!previewImage}
  225. onClose={() => setPreviewImage(null)}
  226. src={previewImage || ""}
  227. />
  228. </aside>
  229. );
  230. };