codebook / potato /server_utils /displays /conversation_tree_display.py
davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
5.07 kB
"""
Conversation Tree Display
Renders branching conversation trees as nested collapsible nodes.
Each node represents a message/turn in a dialogue with possible
multiple branches (e.g., different model responses).
Input data format:
{"id": "root", "speaker": "User", "text": "Question?",
"children": [
{"id": "r1", "speaker": "Bot A", "text": "Answer 1", "children": []},
{"id": "r2", "speaker": "Bot B", "text": "Answer 2", "children": []}
]}
"""
import json
from html import escape
from typing import Dict, Any, List
from .base import BaseDisplay
class ConversationTreeDisplay(BaseDisplay):
name = "conversation_tree"
required_fields = ["key"]
optional_fields = {
"collapsed_depth": 2,
"node_style": "card",
"show_node_ids": False,
"max_depth": None,
}
description = "Conversation tree display with collapsible branching nodes"
supports_span_target = False
def render(self, field_config: Dict[str, Any], data: Any) -> str:
if not data:
return '<div class="conv-tree-empty">No conversation tree data</div>'
collapsed_depth = field_config.get("collapsed_depth", 2)
node_style = field_config.get("node_style", "card")
show_ids = field_config.get("show_node_ids", False)
max_depth = field_config.get("max_depth")
if isinstance(data, str):
try:
data = json.loads(data)
except (json.JSONDecodeError, TypeError):
return f'<div class="conv-tree-error">Invalid tree data</div>'
config_json = escape(json.dumps({
"collapsedDepth": collapsed_depth,
"nodeStyle": node_style,
"showIds": show_ids,
"maxDepth": max_depth,
}))
tree_html = self._render_node(data, 0, collapsed_depth, node_style, show_ids, max_depth)
return (
f'<div class="conv-tree" data-tree-config="{config_json}">'
f' <div class="conv-tree-controls">'
f' <button type="button" class="conv-tree-btn conv-tree-expand-all" title="Expand all">Expand All</button>'
f' <button type="button" class="conv-tree-btn conv-tree-collapse-all" title="Collapse all">Collapse All</button>'
f' </div>'
f' <div class="conv-tree-root">{tree_html}</div>'
f'</div>'
)
def _render_node(self, node: dict, depth: int, collapsed_depth: int,
node_style: str, show_ids: bool, max_depth) -> str:
if not node or not isinstance(node, dict):
return ""
if max_depth is not None and depth > max_depth:
return '<div class="conv-tree-truncated">[depth limit reached]</div>'
node_id = escape(str(node.get("id", f"node_{depth}")))
speaker = escape(str(node.get("speaker", "")))
text = escape(str(node.get("text", "")))
children = node.get("children", [])
is_collapsed = depth >= collapsed_depth and len(children) > 0
# Speaker color class based on name hash
speaker_class = f"conv-tree-speaker-{abs(hash(speaker)) % 6}"
parts = []
parts.append(
f'<div class="conv-tree-node {node_style}" '
f'data-node-id="{node_id}" data-depth="{depth}">'
)
# Node header
parts.append(f'<div class="conv-tree-node-header {speaker_class}">')
if children:
arrow = "▶" if is_collapsed else "▼"
parts.append(
f'<span class="conv-tree-toggle" data-collapsed="{str(is_collapsed).lower()}">{arrow}</span>'
)
parts.append(f'<span class="conv-tree-speaker">{speaker}</span>')
if show_ids:
parts.append(f'<span class="conv-tree-node-id">({node_id})</span>')
if children:
parts.append(
f'<span class="conv-tree-branch-count">{len(children)} '
f'{"branch" if len(children) == 1 else "branches"}</span>'
)
parts.append('</div>')
# Node text
parts.append(f'<div class="conv-tree-node-text">{text}</div>')
# Children
if children:
display = "none" if is_collapsed else "block"
parts.append(f'<div class="conv-tree-children" style="display:{display}">')
for child in children:
parts.append(self._render_node(
child, depth + 1, collapsed_depth, node_style, show_ids, max_depth
))
parts.append('</div>')
parts.append('</div>')
return "\n".join(parts)
def get_css_classes(self, field_config: Dict[str, Any]) -> List[str]:
classes = super().get_css_classes(field_config)
classes.append("conv-tree-container")
return classes
def get_data_attributes(self, field_config: Dict[str, Any], data: Any) -> Dict[str, str]:
attrs = super().get_data_attributes(field_config, data)
attrs["display-type"] = "conversation_tree"
return attrs