(props: {
data: any;
onOpenNode: (id: any) => void;
onOpenSession: (id: any, opts?: any) => void;
})
| 581 | // --- detail panels --------------------------------------------------------- |
| 582 | |
| 583 | export function MessageDetail(props: { |
| 584 | data: any; |
| 585 | onOpenNode: (id: any) => void; |
| 586 | onOpenSession: (id: any, opts?: any) => void; |
| 587 | }): React.ReactElement { |
| 588 | const d = props.data || {}; |
| 589 | const message = d.message; |
| 590 | const session = d.session; |
| 591 | if (!message) return <EmptyState className="hermes-lcm-empty">Message not found</EmptyState>; |
| 592 | const sessionNodes = (session && session.summary_nodes) || []; |
| 593 | // Prefer the backend's exact message→summary linkage (summary_node_ids, |
| 594 | // additive field) and fall back to same-session summaries when absent. |
| 595 | const linkedIds = parseJsonArray(message.summary_node_ids).map(String); |
| 596 | const linkedSet: Record<string, boolean> = {}; |
| 597 | linkedIds.forEach(function (id) { linkedSet[id] = true; }); |
| 598 | const linkedNodes = linkedIds.length |
| 599 | ? sessionNodes.filter(function (node: any) { return linkedSet[String(node.node_id)]; }) |
| 600 | : []; |
| 601 | const hasExactLinks = linkedIds.length > 0; |
| 602 | const relatedNodes = hasExactLinks ? linkedNodes : sessionNodes; |
| 603 | const unresolvedLinkIds = hasExactLinks |
| 604 | ? linkedIds.filter(function (id) { |
| 605 | return !relatedNodes.some(function (node: any) { return String(node.node_id) === id; }); |
| 606 | }) |
| 607 | : []; |
| 608 | return ( |
| 609 | <div className="hermes-lcm-detail"> |
| 610 | <div className="hermes-lcm-detail-meta"> |
| 611 | <span className="hermes-lcm-tag">{message.role || "message"}</span> |
| 612 | {message.source ? <span className="hermes-lcm-tag hermes-lcm-tag-src">{message.source}</span> : null} |
| 613 | <span className="hermes-lcm-tag">{`#${message.store_id}`}</span> |
| 614 | <TimeText className="hermes-lcm-dim" epoch={message.timestamp} /> |
| 615 | {message.token_estimate ? <span className="hermes-lcm-dim">{`${fmtInt(message.token_estimate)} tok`}</span> : null} |
| 616 | <button |
| 617 | type="button" |
| 618 | className="hermes-lcm-btn" |
| 619 | onClick={function () { props.onOpenSession(message.session_id, { activeMessageId: message.store_id }); }} |
| 620 | >{sessionLabel(message.session_id)}</button> |
| 621 | </div> |
| 622 | <div className="hermes-lcm-msg-actions"> |
| 623 | <CopyButton text={message.content} label="Copy message" title="Copy full message content" /> |
| 624 | </div> |
| 625 | <div className="hermes-lcm-detail-grid"> |
| 626 | <div className="hermes-lcm-detail-cell"> |
| 627 | <div className="hermes-lcm-detail-k">Session</div> |
| 628 | <div className="hermes-lcm-detail-v">{short(message.session_id, 48)}</div> |
| 629 | </div> |
| 630 | <div className="hermes-lcm-detail-cell"> |
| 631 | <div className="hermes-lcm-detail-k">Absolute time</div> |
| 632 | <div className="hermes-lcm-detail-v">{fmtAbsoluteTime(message.timestamp) || "—"}</div> |
| 633 | </div> |
| 634 | <div className="hermes-lcm-detail-cell"> |
| 635 | <div className="hermes-lcm-detail-k">Token estimate</div> |
| 636 | <div className="hermes-lcm-detail-v">{fmtInt(message.token_estimate || 0)}</div> |
| 637 | </div> |
| 638 | <div className="hermes-lcm-detail-cell"> |
| 639 | <div className="hermes-lcm-detail-k">{hasExactLinks ? "Linked summaries" : "Related summaries"}</div> |
| 640 | <div className="hermes-lcm-detail-v"> |
nothing calls this directly
no test coverage detected