|
@@ -15,13 +15,7 @@ interface DetailPanelProps {
|
|
|
traceId?: string;
|
|
traceId?: string;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export const DetailPanel = ({
|
|
|
|
|
- node,
|
|
|
|
|
- edge,
|
|
|
|
|
- messages = [],
|
|
|
|
|
- onClose,
|
|
|
|
|
- traceId,
|
|
|
|
|
-}: DetailPanelProps) => {
|
|
|
|
|
|
|
+export const DetailPanel = ({ node, edge, messages = [], onClose, traceId }: DetailPanelProps) => {
|
|
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
|
const [expandedReasoning, setExpandedReasoning] = useState<Set<string>>(new Set());
|
|
const [expandedReasoning, setExpandedReasoning] = useState<Set<string>>(new Set());
|
|
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
@@ -30,8 +24,19 @@ export const DetailPanel = ({
|
|
|
console.log("DetailPanel - edge:", edge);
|
|
console.log("DetailPanel - edge:", edge);
|
|
|
console.log("DetailPanel - messages:", messages);
|
|
console.log("DetailPanel - messages:", messages);
|
|
|
|
|
|
|
|
|
|
+ // 一些图床 (mmbiz.qpic.cn / qlogo.cn 等) 对部分浏览器组合 / 扩展会拒绝直链;
|
|
|
|
|
+ // 直接 <img> 失败时,改走 images.weserv.nl 公共代理 (会以无 referer 重新拉取并转码)。
|
|
|
|
|
+ const PROXY_PREFIX = "https://images.weserv.nl/?url=";
|
|
|
|
|
+ const buildProxyUrl = (url: string) => {
|
|
|
|
|
+ if (!url || url.startsWith("data:")) return url;
|
|
|
|
|
+ // weserv 期望传入不带 protocol 的主机+路径
|
|
|
|
|
+ const stripped = url.replace(/^https?:\/\//, "");
|
|
|
|
|
+ return PROXY_PREFIX + encodeURIComponent(stripped);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const renderImages = (msg: Message) => {
|
|
const renderImages = (msg: Message) => {
|
|
|
const images = extractImagesFromMessage(msg);
|
|
const images = extractImagesFromMessage(msg);
|
|
|
|
|
+ console.log("%c [ images ]-29", "font-size:13px; background:pink; color:#bf2c9f;", images);
|
|
|
if (images.length === 0) return null;
|
|
if (images.length === 0) return null;
|
|
|
return (
|
|
return (
|
|
|
<div className="grid grid-cols-3 gap-2 mt-2">
|
|
<div className="grid grid-cols-3 gap-2 mt-2">
|
|
@@ -41,33 +46,17 @@ export const DetailPanel = ({
|
|
|
src={img.url}
|
|
src={img.url}
|
|
|
alt={img.alt || "Extracted"}
|
|
alt={img.alt || "Extracted"}
|
|
|
referrerPolicy="no-referrer"
|
|
referrerPolicy="no-referrer"
|
|
|
|
|
+ loading="lazy"
|
|
|
className="w-full h-20 object-cover rounded border border-gray-200 cursor-pointer hover:opacity-80 transition-opacity bg-gray-50"
|
|
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)}
|
|
onClick={() => setPreviewImage(img.url)}
|
|
|
- />
|
|
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const renderResultImages = (imageUrls: string[]) => {
|
|
|
|
|
- if (!imageUrls || imageUrls.length === 0) return null;
|
|
|
|
|
- const normalized = imageUrls
|
|
|
|
|
- .map((raw) => String(raw))
|
|
|
|
|
- .map((raw) => raw.replace(/^[\s`'"]+|[\s`'"]+$/g, ""))
|
|
|
|
|
- .filter((url) => url.startsWith("http") || url.startsWith("data:"));
|
|
|
|
|
- if (normalized.length === 0) return null;
|
|
|
|
|
- return (
|
|
|
|
|
- <div className="grid grid-cols-4 gap-2 mt-2">
|
|
|
|
|
- {normalized.map((url, idx) => (
|
|
|
|
|
- <img
|
|
|
|
|
- key={`${url}-${idx}`}
|
|
|
|
|
- src={url}
|
|
|
|
|
- alt={`Image ${idx + 1}`}
|
|
|
|
|
- referrerPolicy="no-referrer"
|
|
|
|
|
- className="w-full h-16 object-cover rounded border border-gray-200 cursor-pointer hover:opacity-80 transition-opacity bg-gray-50"
|
|
|
|
|
- onClick={() => setPreviewImage(url)}
|
|
|
|
|
onError={(e) => {
|
|
onError={(e) => {
|
|
|
- (e.target as HTMLImageElement).style.display = "none";
|
|
|
|
|
|
|
+ const t = e.currentTarget;
|
|
|
|
|
+ if (t.dataset.proxied === "1") return; // 防止无限重试
|
|
|
|
|
+ const proxied = buildProxyUrl(img.url);
|
|
|
|
|
+ if (proxied && proxied !== t.src) {
|
|
|
|
|
+ t.dataset.proxied = "1";
|
|
|
|
|
+ t.src = proxied;
|
|
|
|
|
+ }
|
|
|
}}
|
|
}}
|
|
|
/>
|
|
/>
|
|
|
))}
|
|
))}
|
|
@@ -75,21 +64,11 @@ export const DetailPanel = ({
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const normalizeImageUrl = (raw: unknown) =>
|
|
|
|
|
- String(raw).replace(/^[\s`'"]+|[\s`'"]+$/g, "");
|
|
|
|
|
-
|
|
|
|
|
- const getValidImageUrls = (values: unknown[]): string[] => {
|
|
|
|
|
- return values
|
|
|
|
|
- .map((v) => normalizeImageUrl(v))
|
|
|
|
|
- .filter((url) => url.startsWith("http") || url.startsWith("data:"));
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
const title = node ? "节点详情" : edge ? "连线详情" : "详情";
|
|
const title = node ? "节点详情" : edge ? "连线详情" : "详情";
|
|
|
|
|
|
|
|
const renderMessageContent = (content: Message["content"], msgKey?: string) => {
|
|
const renderMessageContent = (content: Message["content"], msgKey?: string) => {
|
|
|
if (!content) return "";
|
|
if (!content) return "";
|
|
|
- if (typeof content === "string")
|
|
|
|
|
- return <ReactMarkdown>{content}</ReactMarkdown>;
|
|
|
|
|
|
|
+ if (typeof content === "string") return <ReactMarkdown>{content}</ReactMarkdown>;
|
|
|
|
|
|
|
|
const hasText = !!content.text;
|
|
const hasText = !!content.text;
|
|
|
const hasReasoning = !!content.reasoning_content;
|
|
const hasReasoning = !!content.reasoning_content;
|
|
@@ -116,9 +95,7 @@ export const DetailPanel = ({
|
|
|
>
|
|
>
|
|
|
<span className={styles.reasoningIcon}>{isExpanded ? "▼" : "▶"}</span>
|
|
<span className={styles.reasoningIcon}>{isExpanded ? "▼" : "▶"}</span>
|
|
|
<span>思考过程</span>
|
|
<span>思考过程</span>
|
|
|
- <span className={styles.reasoningLen}>
|
|
|
|
|
- ({content.reasoning_content!.length} 字)
|
|
|
|
|
- </span>
|
|
|
|
|
|
|
+ <span className={styles.reasoningLen}>({content.reasoning_content!.length} 字)</span>
|
|
|
</div>
|
|
</div>
|
|
|
{isExpanded && (
|
|
{isExpanded && (
|
|
|
<div className={styles.reasoningContent}>
|
|
<div className={styles.reasoningContent}>
|
|
@@ -132,14 +109,11 @@ export const DetailPanel = ({
|
|
|
<div className={styles.toolCalls}>
|
|
<div className={styles.toolCalls}>
|
|
|
{content.tool_calls!.map((call, idx) => {
|
|
{content.tool_calls!.map((call, idx) => {
|
|
|
const anyCall = call as unknown as Record<string, unknown>;
|
|
const anyCall = call as unknown as Record<string, unknown>;
|
|
|
- const fn = anyCall.function as
|
|
|
|
|
- | Record<string, unknown>
|
|
|
|
|
- | undefined;
|
|
|
|
|
|
|
+ const fn = anyCall.function as Record<string, unknown> | undefined;
|
|
|
const name =
|
|
const name =
|
|
|
(fn && (fn.name as string)) ||
|
|
(fn && (fn.name as string)) ||
|
|
|
(anyCall.name as string) ||
|
|
(anyCall.name as string) ||
|
|
|
- ((content as unknown as Record<string, unknown>)
|
|
|
|
|
- .tool_name as string) ||
|
|
|
|
|
|
|
+ ((content as unknown as Record<string, unknown>).tool_name as string) ||
|
|
|
`tool_${idx}`;
|
|
`tool_${idx}`;
|
|
|
let args: unknown =
|
|
let args: unknown =
|
|
|
(fn && fn.arguments) ||
|
|
(fn && fn.arguments) ||
|
|
@@ -154,11 +128,12 @@ export const DetailPanel = ({
|
|
|
}
|
|
}
|
|
|
const key = (anyCall.id as string) || `${name}-${idx}`;
|
|
const key = (anyCall.id as string) || `${name}-${idx}`;
|
|
|
return (
|
|
return (
|
|
|
- <div key={key} className={styles.toolCall}>
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ key={key}
|
|
|
|
|
+ className={styles.toolCall}
|
|
|
|
|
+ >
|
|
|
<div className={styles.toolName}>工具调用: {name}</div>
|
|
<div className={styles.toolName}>工具调用: {name}</div>
|
|
|
- <pre className={styles.toolArgs}>
|
|
|
|
|
- {JSON.stringify(args, null, 2)}
|
|
|
|
|
- </pre>
|
|
|
|
|
|
|
+ <pre className={styles.toolArgs}>{JSON.stringify(args, null, 2)}</pre>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
})}
|
|
})}
|
|
@@ -202,9 +177,7 @@ export const DetailPanel = ({
|
|
|
for (const msg of messages) {
|
|
for (const msg of messages) {
|
|
|
const role = msg.role || "unknown";
|
|
const role = msg.role || "unknown";
|
|
|
const seq = msg.sequence ?? "";
|
|
const seq = msg.sequence ?? "";
|
|
|
- const content = typeof msg.content === "string"
|
|
|
|
|
- ? msg.content
|
|
|
|
|
- : JSON.stringify(msg.content);
|
|
|
|
|
|
|
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
|
lines.push(`[${role}#${seq}] ${content}`);
|
|
lines.push(`[${role}#${seq}] ${content}`);
|
|
|
}
|
|
}
|
|
|
return lines.join("\n");
|
|
return lines.join("\n");
|
|
@@ -212,9 +185,7 @@ export const DetailPanel = ({
|
|
|
const msg = node as Message;
|
|
const msg = node as Message;
|
|
|
const role = msg.role || "unknown";
|
|
const role = msg.role || "unknown";
|
|
|
const seq = msg.sequence ?? "";
|
|
const seq = msg.sequence ?? "";
|
|
|
- const content = typeof msg.content === "string"
|
|
|
|
|
- ? msg.content
|
|
|
|
|
- : JSON.stringify(msg.content);
|
|
|
|
|
|
|
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
|
return `[${role}#${seq}] ${content}`;
|
|
return `[${role}#${seq}] ${content}`;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
@@ -240,34 +211,32 @@ export const DetailPanel = ({
|
|
|
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>
|
|
|
))}
|
|
))}
|
|
@@ -289,7 +258,11 @@ export const DetailPanel = ({
|
|
|
<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>
|
|
@@ -299,17 +272,14 @@ export const DetailPanel = ({
|
|
|
<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 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>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ {isMessageNode(node) && extractImagesFromMessage(node).length > 0 && (
|
|
|
|
|
+ <div className={styles.section}>
|
|
|
|
|
+ <div className={styles.label}>图片</div>
|
|
|
|
|
+ {renderImages(node)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
|
|
|
{isGoal(node) ? (
|
|
{isGoal(node) ? (
|
|
|
<>
|
|
<>
|
|
@@ -357,7 +327,8 @@ export const DetailPanel = ({
|
|
|
<div className={styles.section}>
|
|
<div className={styles.section}>
|
|
|
<div className={styles.label}>内容</div>
|
|
<div className={styles.label}>内容</div>
|
|
|
<div className={styles.value}>
|
|
<div className={styles.value}>
|
|
|
- {node.content && renderMessageContent(node.content, isMessageNode(node) ? node.message_id || node.id : node.id)}
|
|
|
|
|
|
|
+ {node.content &&
|
|
|
|
|
+ renderMessageContent(node.content, isMessageNode(node) ? node.message_id || node.id : node.id)}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
{node.goal_id && (
|
|
{node.goal_id && (
|
|
@@ -380,7 +351,10 @@ export const DetailPanel = ({
|
|
|
<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>
|