Spaces:
Paused
Paused
| """ | |
| Span Link Layout | |
| Generates the UI for creating and managing relationships/links between spans. | |
| This schema type works in conjunction with a span annotation schema to allow | |
| users to annotate relationships like "PERSON works_for ORGANIZATION". | |
| """ | |
| import logging | |
| from .identifier_utils import ( | |
| safe_generate_layout, | |
| generate_element_identifier, | |
| escape_html_content, | |
| generate_tooltip_html | |
| ) | |
| from .span import get_span_color, SPAN_COLOR_PALETTE | |
| logger = logging.getLogger(__name__) | |
| # Default colors for link types | |
| LINK_COLOR_PALETTE = [ | |
| "#dc2626", # Red | |
| "#22c55e", # Green | |
| "#a855f7", # Purple | |
| "#f59e0b", # Amber | |
| "#3b82f6", # Blue | |
| "#ec4899", # Pink | |
| "#06b6d4", # Cyan | |
| "#f97316", # Orange | |
| "#8b5cf6", # Violet | |
| "#10b981", # Emerald | |
| ] | |
| def _generate_span_link_layout_internal(annotation_scheme, horizontal=False): | |
| """ | |
| Internal function to generate span link layout after validation. | |
| Args: | |
| annotation_scheme: Configuration dictionary containing: | |
| - name: Schema name | |
| - description: Description shown to user | |
| - span_schema: Name of the span schema to link | |
| - link_types: List of link type definitions with: | |
| - name: Link type name (e.g., "WORKS_FOR") | |
| - directed: Whether the link is directed (default: false) | |
| - allowed_source_labels: Optional list of allowed source span labels | |
| - allowed_target_labels: Optional list of allowed target span labels | |
| - max_spans: Maximum spans in a link (default: 2, higher for n-ary) | |
| - color: Optional color for this link type | |
| horizontal: Whether to display horizontally (not used for links) | |
| Returns: | |
| tuple: (HTML string, key bindings list) | |
| """ | |
| scheme_name = annotation_scheme["name"] | |
| description = annotation_scheme.get("description", "Create relationships between spans") | |
| span_schema = annotation_scheme.get("span_schema", "") | |
| link_types = annotation_scheme.get("link_types", []) | |
| visual_display = annotation_scheme.get("visual_display", {}) | |
| # Build link types HTML | |
| link_types_html = "" | |
| key_bindings = [] | |
| for i, link_type in enumerate(link_types): | |
| link_name = link_type.get("name", f"Link_{i}") | |
| directed = link_type.get("directed", False) | |
| max_spans = link_type.get("max_spans", 2) | |
| color = link_type.get("color", LINK_COLOR_PALETTE[i % len(LINK_COLOR_PALETTE)]) | |
| # Direction indicator | |
| direction_icon = "→" if directed else "↔" | |
| direction_class = "directed" if directed else "undirected" | |
| # Tooltip with constraints | |
| tooltip_parts = [] | |
| if link_type.get("allowed_source_labels"): | |
| tooltip_parts.append(f"Source: {', '.join(link_type['allowed_source_labels'])}") | |
| if link_type.get("allowed_target_labels"): | |
| tooltip_parts.append(f"Target: {', '.join(link_type['allowed_target_labels'])}") | |
| if max_spans > 2: | |
| tooltip_parts.append(f"N-ary: up to {max_spans} spans") | |
| tooltip_attr = "" | |
| if tooltip_parts: | |
| tooltip_text = "; ".join(tooltip_parts) | |
| tooltip_attr = f'data-toggle="tooltip" data-placement="top" title="{escape_html_content(tooltip_text)}"' | |
| link_types_html += f""" | |
| <div class="span-link-type" data-link-type="{escape_html_content(link_name)}" | |
| data-directed="{str(directed).lower()}" | |
| data-max-spans="{max_spans}" | |
| data-color="{escape_html_content(color)}" | |
| data-source-labels="{escape_html_content(','.join(link_type.get('allowed_source_labels', [])))}" | |
| data-target-labels="{escape_html_content(','.join(link_type.get('allowed_target_labels', [])))}" | |
| {tooltip_attr}> | |
| <input type="radio" name="{escape_html_content(scheme_name)}_link_type" | |
| id="{escape_html_content(scheme_name)}_link_{escape_html_content(link_name)}" | |
| value="{escape_html_content(link_name)}" | |
| class="span-link-type-radio"> | |
| <label for="{escape_html_content(scheme_name)}_link_{escape_html_content(link_name)}" | |
| class="span-link-type-label {direction_class}" | |
| style="--link-color: {color}"> | |
| <span class="link-color-indicator" style="background-color: {color}"></span> | |
| <span class="link-type-name">{escape_html_content(link_name)}</span> | |
| <span class="link-direction-icon">{direction_icon}</span> | |
| </label> | |
| </div> | |
| """ | |
| # Visual display settings | |
| show_arcs = visual_display.get("enabled", True) | |
| arc_position = visual_display.get("arc_position", "above") | |
| show_labels = visual_display.get("show_labels", True) | |
| # Multi-line arc mode: "single_line" (horizontal scroll) or "bracket" (wrapped text with bracket arcs) | |
| multi_line_mode = visual_display.get("multi_line_mode", "bracket") | |
| schematic = f""" | |
| <div id="{escape_html_content(scheme_name)}" class="span-link-container annotation-form" | |
| data-annotation-type="span_link" | |
| data-annotation-id="{escape_html_content(str(annotation_scheme.get('annotation_id', scheme_name)))}" | |
| data-span-schema="{escape_html_content(span_schema)}" | |
| data-show-arcs="{str(show_arcs).lower()}" | |
| data-arc-position="{escape_html_content(arc_position)}" | |
| data-show-labels="{str(show_labels).lower()}" | |
| data-multi-line-mode="{escape_html_content(multi_line_mode)}"> | |
| <div class="span-link-header"> | |
| <h4 class="span-link-title">{escape_html_content(description)}</h4> | |
| </div> | |
| <!-- Link Type Selector --> | |
| <div class="span-link-type-selector"> | |
| <label class="span-link-section-label">Select Link Type:</label> | |
| <div class="span-link-types"> | |
| {link_types_html} | |
| </div> | |
| </div> | |
| <!-- Selected Spans Display --> | |
| <div class="span-link-selection"> | |
| <label class="span-link-section-label">Selected Spans:</label> | |
| <div class="span-link-selected-spans" id="{escape_html_content(scheme_name)}_selected_spans"> | |
| <p class="no-selection-message">Click on highlighted spans to select them for linking</p> | |
| </div> | |
| </div> | |
| <!-- Action Buttons --> | |
| <div class="span-link-actions"> | |
| <button type="button" class="span-link-create-btn" id="{escape_html_content(scheme_name)}_create_link" | |
| disabled> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" | |
| stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path> | |
| <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path> | |
| </svg> | |
| Create Link | |
| </button> | |
| <button type="button" class="span-link-clear-btn" id="{escape_html_content(scheme_name)}_clear_selection" | |
| title="Exit link mode to create new span annotations (Esc)"> | |
| Exit Link Mode | |
| </button> | |
| </div> | |
| <!-- Existing Links Display --> | |
| <div class="span-link-existing"> | |
| <label class="span-link-section-label">Existing Links:</label> | |
| <div class="span-link-list" id="{escape_html_content(scheme_name)}_link_list"> | |
| <p class="no-links-message">No links created yet</p> | |
| </div> | |
| </div> | |
| <!-- Visual Display Toggle --> | |
| <div class="span-link-visual-toggle"> | |
| <label> | |
| <input type="checkbox" id="{escape_html_content(scheme_name)}_show_arcs" | |
| {'checked' if show_arcs else ''}> | |
| Show link arcs above text | |
| </label> | |
| </div> | |
| <!-- Hidden input to store link data for form submission --> | |
| <input type="hidden" name="span_link:::{escape_html_content(scheme_name)}" | |
| id="{escape_html_content(scheme_name)}_link_data" value="[]"> | |
| </div> | |
| """ | |
| return schematic, key_bindings | |
| def generate_span_link_layout(annotation_scheme, horizontal=False): | |
| """ | |
| Generate span link layout HTML for the given annotation scheme. | |
| Args: | |
| annotation_scheme (dict): The annotation scheme configuration | |
| horizontal (bool): Whether to display horizontally | |
| Returns: | |
| tuple: (HTML string, key bindings list) | |
| """ | |
| return safe_generate_layout(annotation_scheme, _generate_span_link_layout_internal, horizontal) | |
| def render_link_arcs(links, span_positions): | |
| """ | |
| Generate SVG arcs for visualizing links between spans. | |
| Args: | |
| links: List of SpanLink objects | |
| span_positions: Dictionary mapping span_id -> {x, y, width, height} positions | |
| Returns: | |
| str: SVG markup for the link arcs | |
| """ | |
| if not links or not span_positions: | |
| return "" | |
| svg_paths = [] | |
| for link in links: | |
| span_ids = link.get_span_ids() | |
| if len(span_ids) < 2: | |
| continue | |
| # Get color for this link type | |
| link_color = link.get_properties().get("color", "#dc2626") | |
| # For binary links, draw a simple arc | |
| if len(span_ids) == 2: | |
| span1_id, span2_id = span_ids | |
| if span1_id not in span_positions or span2_id not in span_positions: | |
| continue | |
| pos1 = span_positions[span1_id] | |
| pos2 = span_positions[span2_id] | |
| # Calculate arc endpoints (center of each span) | |
| x1 = pos1["x"] + pos1["width"] / 2 | |
| y1 = pos1["y"] | |
| x2 = pos2["x"] + pos2["width"] / 2 | |
| y2 = pos2["y"] | |
| # Calculate control point for the arc | |
| mid_x = (x1 + x2) / 2 | |
| arc_height = min(abs(x2 - x1) / 3, 50) # Arc height proportional to distance | |
| # Create SVG path | |
| if link.is_directed(): | |
| # Directed link with arrow | |
| svg_paths.append(f""" | |
| <path d="M {x1} {y1} Q {mid_x} {y1 - arc_height} {x2} {y2}" | |
| fill="none" stroke="{link_color}" stroke-width="2" | |
| marker-end="url(#arrowhead)" | |
| class="span-link-arc" data-link-id="{link.get_id()}" /> | |
| """) | |
| else: | |
| # Undirected link | |
| svg_paths.append(f""" | |
| <path d="M {x1} {y1} Q {mid_x} {y1 - arc_height} {x2} {y2}" | |
| fill="none" stroke="{link_color}" stroke-width="2" | |
| class="span-link-arc" data-link-id="{link.get_id()}" /> | |
| """) | |
| # For n-ary links, connect all spans to a central point | |
| else: | |
| # Calculate center point | |
| valid_positions = [span_positions[sid] for sid in span_ids if sid in span_positions] | |
| if len(valid_positions) < 2: | |
| continue | |
| center_x = sum(p["x"] + p["width"] / 2 for p in valid_positions) / len(valid_positions) | |
| center_y = min(p["y"] for p in valid_positions) - 30 # Above the spans | |
| # Draw lines from each span to center | |
| for span_id in span_ids: | |
| if span_id not in span_positions: | |
| continue | |
| pos = span_positions[span_id] | |
| x = pos["x"] + pos["width"] / 2 | |
| y = pos["y"] | |
| svg_paths.append(f""" | |
| <line x1="{x}" y1="{y}" x2="{center_x}" y2="{center_y}" | |
| stroke="{link_color}" stroke-width="2" | |
| class="span-link-arc" data-link-id="{link.get_id()}" /> | |
| """) | |
| # Draw central node | |
| svg_paths.append(f""" | |
| <circle cx="{center_x}" cy="{center_y}" r="6" | |
| fill="{link_color}" | |
| class="span-link-node" data-link-id="{link.get_id()}" /> | |
| """) | |
| # Wrap in SVG with defs for arrow markers | |
| svg = f""" | |
| <svg class="span-link-arcs-layer" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible;"> | |
| <defs> | |
| <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto"> | |
| <polygon points="0 0, 10 3.5, 0 7" fill="currentColor" /> | |
| </marker> | |
| </defs> | |
| {''.join(svg_paths)} | |
| </svg> | |
| """ | |
| return svg | |