DetailPanel.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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, idx) => {
  44. const anyCall = call as unknown as Record<string, unknown>;
  45. const fn = anyCall.function as Record<string, unknown> | undefined;
  46. const name =
  47. (fn && (fn.name as string)) ||
  48. (anyCall.name as string) ||
  49. ((content as unknown as Record<string, unknown>).tool_name as string) ||
  50. `tool_${idx}`;
  51. let args: unknown =
  52. (fn && fn.arguments) || anyCall.arguments || (content as unknown as Record<string, unknown>).arguments;
  53. if (typeof args === "string") {
  54. try {
  55. args = JSON.parse(args);
  56. } catch {
  57. // keep as string if JSON.parse fails
  58. }
  59. }
  60. const key = (anyCall.id as string) || `${name}-${idx}`;
  61. return (
  62. <div
  63. key={key}
  64. className={styles.toolCall}
  65. >
  66. <div className={styles.toolName}>工具调用: {name}</div>
  67. <pre className={styles.toolArgs}>{JSON.stringify(args, null, 2)}</pre>
  68. </div>
  69. );
  70. })}
  71. </div>
  72. );
  73. }
  74. return <ReactMarkdown>{JSON.stringify(content)}</ReactMarkdown>;
  75. };
  76. const isGoal = (node: Goal | Message): node is Goal => {
  77. return "status" in node;
  78. };
  79. const isMessageNode = (node: Goal | Message): node is Message =>
  80. "message_id" in node || "role" in node || "content" in node || "goal_id" in node || "tokens" in node;
  81. const renderKnowledge = (knowledge: Goal["knowledge"]) => {
  82. if (!knowledge || knowledge.length === 0) return null;
  83. return (
  84. <div className={styles.knowledgeList}>
  85. {knowledge.map((item) => (
  86. <div
  87. key={item.id}
  88. className={styles.knowledgeItem}
  89. >
  90. <div className={styles.knowledgeHeader}>
  91. <span className={styles.knowledgeId}>{item.id}</span>
  92. <div className={styles.knowledgeMetrics}>
  93. {item.score !== undefined && <span className={styles.metricScore}>⭐ {item.score}</span>}
  94. {item.quality_score !== undefined && (
  95. <span className={styles.metricQuality}>✨ {item.quality_score.toFixed(1)}</span>
  96. )}
  97. {item.metrics?.helpful !== undefined && (
  98. <span className={styles.metricHelpful}>👍 {item.metrics.helpful}</span>
  99. )}
  100. {item.metrics?.harmful !== undefined && (
  101. <span className={styles.metricHarmful}>👎 {item.metrics.harmful}</span>
  102. )}
  103. </div>
  104. </div>
  105. {item.tags?.type && item.tags.type.length > 0 && (
  106. <div className={styles.knowledgeTags}>
  107. {item.tags.type.map((tag) => (
  108. <span
  109. key={tag}
  110. className={styles.tag}
  111. >
  112. {tag}
  113. </span>
  114. ))}
  115. </div>
  116. )}
  117. <div className={styles.knowledgeScenario}>
  118. <strong>场景:</strong> {item.scenario}
  119. </div>
  120. <div className={styles.knowledgeContent}>
  121. <ReactMarkdown>{item.content}</ReactMarkdown>
  122. </div>
  123. </div>
  124. ))}
  125. </div>
  126. );
  127. };
  128. return (
  129. <aside className={styles.panel}>
  130. <div className={styles.header}>
  131. <div className={styles.title}>{title}</div>
  132. <button
  133. className={styles.close}
  134. onClick={onClose}
  135. aria-label="关闭"
  136. >
  137. ×
  138. </button>
  139. </div>
  140. <div className={styles.content}>
  141. {node && (
  142. <>
  143. <div className={styles.sectionTitle}>节点</div>
  144. <div className={styles.section}>
  145. <div className={styles.label}>ID</div>
  146. <div className={styles.value}>{isMessageNode(node) ? node.message_id || node.id : node.id}</div>
  147. </div>
  148. {isMessageNode(node) && extractImagesFromMessage(node).length > 0 && (
  149. <div className={styles.section}>
  150. <div className={styles.label}>图片</div>
  151. {renderImages(node)}
  152. </div>
  153. )}
  154. {isGoal(node) ? (
  155. <>
  156. <div className={styles.section}>
  157. <div className={styles.label}>目标描述</div>
  158. <div className={styles.value}>{node.description}</div>
  159. </div>
  160. {node.reason && (
  161. <div className={styles.section}>
  162. <div className={styles.label}>创建理由</div>
  163. <div className={styles.value}>{node.reason}</div>
  164. </div>
  165. )}
  166. {node.summary && (
  167. <div className={styles.section}>
  168. <div className={styles.label}>总结</div>
  169. <div className={styles.value}>{node.summary}</div>
  170. </div>
  171. )}
  172. <div className={styles.section}>
  173. <div className={styles.label}>状态</div>
  174. <div className={styles.value}>{node.status}</div>
  175. </div>
  176. {node.knowledge && node.knowledge.length > 0 && (
  177. <div className={styles.section}>
  178. <div className={styles.sectionTitle}>相关知识</div>
  179. {renderKnowledge(node.knowledge)}
  180. </div>
  181. )}
  182. </>
  183. ) : (
  184. <>
  185. {node.description && (
  186. <div className={styles.section}>
  187. <div className={styles.label}>描述</div>
  188. <div className={styles.value}>{node.description}</div>
  189. </div>
  190. )}
  191. {node.role && (
  192. <div className={styles.section}>
  193. <div className={styles.label}>角色</div>
  194. <div className={styles.value}>{node.role}</div>
  195. </div>
  196. )}
  197. <div className={styles.section}>
  198. <div className={styles.label}>内容</div>
  199. <div className={styles.value}>{node.content && renderMessageContent(node.content)}</div>
  200. </div>
  201. {node.goal_id && (
  202. <div className={styles.section}>
  203. <div className={styles.label}>所属目标</div>
  204. <div className={styles.value}>{node.goal_id}</div>
  205. </div>
  206. )}
  207. {node.tokens !== undefined && node.tokens !== null && (
  208. <div className={styles.section}>
  209. <div className={styles.label}>Token数</div>
  210. <div className={styles.value}>{node.tokens}</div>
  211. </div>
  212. )}
  213. </>
  214. )}
  215. </>
  216. )}
  217. {messages && messages.length > 0 && (
  218. <div className={styles.messages}>
  219. <div className={styles.sectionTitle}>边</div>
  220. {messages.map((msg, idx) => (
  221. <div
  222. key={msg.id || idx}
  223. className={styles.messageItem}
  224. >
  225. <div className={styles.section}>
  226. <div className={styles.label}>描述</div>
  227. <div className={styles.value}>{msg.description || "-"}</div>
  228. </div>
  229. <div className={styles.section}>
  230. <div className={styles.label}>内容</div>
  231. <div className={styles.value}>{msg.content && renderMessageContent(msg.content)}</div>
  232. </div>
  233. </div>
  234. ))}
  235. </div>
  236. )}
  237. </div>
  238. <ImagePreviewModal
  239. visible={!!previewImage}
  240. onClose={() => setPreviewImage(null)}
  241. src={previewImage || ""}
  242. />
  243. </aside>
  244. );
  245. };