|
@@ -1,7 +1,10 @@
|
|
|
import ReactMarkdown from "react-markdown";
|
|
import ReactMarkdown from "react-markdown";
|
|
|
|
|
+import { useState } from "react";
|
|
|
import type { Goal } from "../../types/goal";
|
|
import type { Goal } from "../../types/goal";
|
|
|
import type { Edge, Message } from "../../types/message";
|
|
import type { Edge, Message } from "../../types/message";
|
|
|
import styles from "./DetailPanel.module.css";
|
|
import styles from "./DetailPanel.module.css";
|
|
|
|
|
+import { ImagePreviewModal } from "../ImagePreview/ImagePreviewModal";
|
|
|
|
|
+import { extractImagesFromMessage } from "../../utils/imageExtraction";
|
|
|
|
|
|
|
|
interface DetailPanelProps {
|
|
interface DetailPanelProps {
|
|
|
node: Goal | Message | null;
|
|
node: Goal | Message | null;
|
|
@@ -11,6 +14,26 @@ interface DetailPanelProps {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelProps) => {
|
|
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 title = node ? "节点详情" : edge ? "连线详情" : "详情";
|
|
|
|
|
|
|
|
const renderMessageContent = (content: Message["content"]) => {
|
|
const renderMessageContent = (content: Message["content"]) => {
|
|
@@ -60,9 +83,7 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
|
|
|
<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>
|
|
|
)}
|
|
)}
|
|
@@ -118,6 +139,10 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
|
|
|
<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 className={styles.value}>{isMessageNode(node) ? node.message_id || node.id : node.id}</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div className={styles.section}>
|
|
|
|
|
+ <div className={styles.label}>图片</div>
|
|
|
|
|
+ {isMessageNode(node) && renderImages(node)}
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
{isGoal(node) ? (
|
|
{isGoal(node) ? (
|
|
|
<>
|
|
<>
|
|
@@ -162,12 +187,13 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
|
|
|
<div className={styles.value}>{node.role}</div>
|
|
<div className={styles.value}>{node.role}</div>
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
- {node.content && (
|
|
|
|
|
- <div className={styles.section}>
|
|
|
|
|
- <div className={styles.label}>内容</div>
|
|
|
|
|
- <div className={styles.value}>{renderMessageContent(node.content)}</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 && (
|
|
{node.goal_id && (
|
|
|
<div className={styles.section}>
|
|
<div className={styles.section}>
|
|
|
<div className={styles.label}>所属目标</div>
|
|
<div className={styles.label}>所属目标</div>
|
|
@@ -198,13 +224,21 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
|
|
|
</div>
|
|
</div>
|
|
|
<div className={styles.section}>
|
|
<div className={styles.section}>
|
|
|
<div className={styles.label}>内容</div>
|
|
<div className={styles.label}>内容</div>
|
|
|
- <div className={styles.value}>{renderMessageContent(msg.content)}</div>
|
|
|
|
|
|
|
+ <div className={styles.value}>
|
|
|
|
|
+ {msg.content && renderMessageContent(msg.content)}
|
|
|
|
|
+ {renderImages(msg)}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <ImagePreviewModal
|
|
|
|
|
+ visible={!!previewImage}
|
|
|
|
|
+ onClose={() => setPreviewImage(null)}
|
|
|
|
|
+ src={previewImage || ""}
|
|
|
|
|
+ />
|
|
|
</aside>
|
|
</aside>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|