openai-gabriel 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gabriel/__init__.py +61 -0
- gabriel/_version.py +1 -0
- gabriel/api.py +2284 -0
- gabriel/cli/__main__.py +60 -0
- gabriel/core/__init__.py +7 -0
- gabriel/core/llm_client.py +34 -0
- gabriel/core/pipeline.py +18 -0
- gabriel/core/prompt_template.py +152 -0
- gabriel/prompts/__init__.py +1 -0
- gabriel/prompts/bucket_prompt.jinja2 +113 -0
- gabriel/prompts/classification_prompt.jinja2 +50 -0
- gabriel/prompts/codify_prompt.jinja2 +95 -0
- gabriel/prompts/comparison_prompt.jinja2 +60 -0
- gabriel/prompts/deduplicate_prompt.jinja2 +41 -0
- gabriel/prompts/deidentification_prompt.jinja2 +112 -0
- gabriel/prompts/extraction_prompt.jinja2 +61 -0
- gabriel/prompts/filter_prompt.jinja2 +31 -0
- gabriel/prompts/ideation_prompt.jinja2 +80 -0
- gabriel/prompts/merge_prompt.jinja2 +47 -0
- gabriel/prompts/paraphrase_prompt.jinja2 +17 -0
- gabriel/prompts/rankings_prompt.jinja2 +49 -0
- gabriel/prompts/ratings_prompt.jinja2 +50 -0
- gabriel/prompts/regional_analysis_prompt.jinja2 +40 -0
- gabriel/prompts/seed.jinja2 +43 -0
- gabriel/prompts/snippets.jinja2 +117 -0
- gabriel/tasks/__init__.py +63 -0
- gabriel/tasks/_attribute_utils.py +69 -0
- gabriel/tasks/bucket.py +432 -0
- gabriel/tasks/classify.py +562 -0
- gabriel/tasks/codify.py +1033 -0
- gabriel/tasks/compare.py +235 -0
- gabriel/tasks/debias.py +1460 -0
- gabriel/tasks/deduplicate.py +341 -0
- gabriel/tasks/deidentify.py +316 -0
- gabriel/tasks/discover.py +524 -0
- gabriel/tasks/extract.py +455 -0
- gabriel/tasks/filter.py +169 -0
- gabriel/tasks/ideate.py +782 -0
- gabriel/tasks/merge.py +464 -0
- gabriel/tasks/paraphrase.py +531 -0
- gabriel/tasks/rank.py +2041 -0
- gabriel/tasks/rate.py +347 -0
- gabriel/tasks/seed.py +465 -0
- gabriel/tasks/whatever.py +344 -0
- gabriel/utils/__init__.py +64 -0
- gabriel/utils/audio_utils.py +42 -0
- gabriel/utils/file_utils.py +464 -0
- gabriel/utils/image_utils.py +22 -0
- gabriel/utils/jinja.py +31 -0
- gabriel/utils/logging.py +86 -0
- gabriel/utils/mapmaker.py +304 -0
- gabriel/utils/media_utils.py +78 -0
- gabriel/utils/modality_utils.py +148 -0
- gabriel/utils/openai_utils.py +5470 -0
- gabriel/utils/parsing.py +282 -0
- gabriel/utils/passage_viewer.py +2557 -0
- gabriel/utils/pdf_utils.py +20 -0
- gabriel/utils/plot_utils.py +2881 -0
- gabriel/utils/prompt_utils.py +42 -0
- gabriel/utils/word_matching.py +158 -0
- openai_gabriel-1.0.1.dist-info/METADATA +443 -0
- openai_gabriel-1.0.1.dist-info/RECORD +67 -0
- openai_gabriel-1.0.1.dist-info/WHEEL +5 -0
- openai_gabriel-1.0.1.dist-info/entry_points.txt +2 -0
- openai_gabriel-1.0.1.dist-info/licenses/LICENSE +201 -0
- openai_gabriel-1.0.1.dist-info/licenses/NOTICE +13 -0
- openai_gabriel-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2557 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import colorsys
|
|
3
|
+
import math
|
|
4
|
+
import html
|
|
5
|
+
import json
|
|
6
|
+
import random
|
|
7
|
+
import re
|
|
8
|
+
import uuid
|
|
9
|
+
from string import Template
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import (
|
|
12
|
+
Any,
|
|
13
|
+
Dict,
|
|
14
|
+
Iterable,
|
|
15
|
+
List,
|
|
16
|
+
Mapping,
|
|
17
|
+
Optional,
|
|
18
|
+
Sequence,
|
|
19
|
+
Tuple,
|
|
20
|
+
Union,
|
|
21
|
+
Literal,
|
|
22
|
+
Set,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
import pandas as pd
|
|
26
|
+
try:
|
|
27
|
+
import matplotlib.pyplot as plt
|
|
28
|
+
except Exception: # pragma: no cover - optional dependency
|
|
29
|
+
plt = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _generate_distinct_colors(n: int) -> List[str]:
|
|
33
|
+
"""Generate ``n`` visually distinct hex colors.
|
|
34
|
+
|
|
35
|
+
This helper is shared by both the rich ``tkinter`` viewer and the simpler
|
|
36
|
+
HTML based viewer used in headless environments such as Google Colab.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
base_colors: List[str] = []
|
|
40
|
+
if plt is not None:
|
|
41
|
+
if n <= 20:
|
|
42
|
+
cmap = plt.get_cmap("tab20")
|
|
43
|
+
for i in range(n):
|
|
44
|
+
rgb = cmap(i)[:3]
|
|
45
|
+
base_colors.append(
|
|
46
|
+
"#{:02x}{:02x}{:02x}".format(
|
|
47
|
+
int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255)
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
return base_colors
|
|
51
|
+
else:
|
|
52
|
+
cmap = plt.get_cmap("tab20")
|
|
53
|
+
for i in range(20):
|
|
54
|
+
rgb = cmap(i)[:3]
|
|
55
|
+
base_colors.append(
|
|
56
|
+
"#{:02x}{:02x}{:02x}".format(
|
|
57
|
+
int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255)
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for i in range(len(base_colors), n):
|
|
62
|
+
hue = (i * 1.0 / n) % 1.0
|
|
63
|
+
r, g, b = colorsys.hsv_to_rgb(hue, 0.7, 1.0)
|
|
64
|
+
base_colors.append(
|
|
65
|
+
"#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255))
|
|
66
|
+
)
|
|
67
|
+
return base_colors[:n]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _parse_hex_color(value: str) -> Optional[Tuple[int, int, int]]:
|
|
71
|
+
"""Return an RGB tuple from a hex color string."""
|
|
72
|
+
|
|
73
|
+
if not value:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
text = value.strip()
|
|
77
|
+
if not text:
|
|
78
|
+
return None
|
|
79
|
+
if text.startswith("#"):
|
|
80
|
+
text = text[1:]
|
|
81
|
+
if len(text) == 3:
|
|
82
|
+
text = "".join(ch * 2 for ch in text)
|
|
83
|
+
if len(text) != 6:
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
r = int(text[0:2], 16)
|
|
87
|
+
g = int(text[2:4], 16)
|
|
88
|
+
b = int(text[4:6], 16)
|
|
89
|
+
except ValueError:
|
|
90
|
+
return None
|
|
91
|
+
return r, g, b
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _rgba_string_from_hex(value: str, alpha: float) -> Optional[str]:
|
|
95
|
+
"""Convert a hex color to an ``rgba()`` string with the given alpha."""
|
|
96
|
+
|
|
97
|
+
rgb = _parse_hex_color(value)
|
|
98
|
+
if rgb is None:
|
|
99
|
+
return None
|
|
100
|
+
try:
|
|
101
|
+
alpha_float = float(alpha)
|
|
102
|
+
except (TypeError, ValueError): # pragma: no cover - defensive
|
|
103
|
+
alpha_float = 1.0
|
|
104
|
+
alpha_float = max(0.0, min(1.0, alpha_float))
|
|
105
|
+
r, g, b = rgb
|
|
106
|
+
return f"rgba({r}, {g}, {b}, {alpha_float:.3f})"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _build_chip_style_tokens(color: str, intensity: float = 1.0) -> Optional[str]:
|
|
110
|
+
"""Produce inline CSS custom properties for legend chips.
|
|
111
|
+
|
|
112
|
+
``intensity`` allows callers to dim or brighten the glow that surrounds a
|
|
113
|
+
chip. The value is normalized to the ``[0.0, 1.0]`` range so that
|
|
114
|
+
``intensity=1`` matches the original styling, while lower values softly
|
|
115
|
+
fade the background/border/glow.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
if not color:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
normalized = float(intensity)
|
|
123
|
+
except (TypeError, ValueError):
|
|
124
|
+
normalized = 1.0
|
|
125
|
+
if not math.isfinite(normalized):
|
|
126
|
+
normalized = 1.0
|
|
127
|
+
normalized = max(0.0, min(1.0, normalized))
|
|
128
|
+
|
|
129
|
+
tokens: List[str] = []
|
|
130
|
+
bg = _rgba_string_from_hex(color, 0.05 + 0.17 * normalized)
|
|
131
|
+
border = _rgba_string_from_hex(color, 0.08 + 0.47 * normalized)
|
|
132
|
+
glow = _rgba_string_from_hex(color, 0.04 + 0.31 * normalized)
|
|
133
|
+
if bg:
|
|
134
|
+
tokens.append(f"--gabriel-chip-bg:{bg}")
|
|
135
|
+
if border:
|
|
136
|
+
tokens.append(f"--gabriel-chip-border:{border}")
|
|
137
|
+
if glow:
|
|
138
|
+
tokens.append(f"--gabriel-chip-glow:{glow}")
|
|
139
|
+
if not tokens:
|
|
140
|
+
return None
|
|
141
|
+
return ";".join(tokens)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _compute_numeric_intensity(
|
|
145
|
+
value: Optional[float],
|
|
146
|
+
bounds: Optional[Tuple[Optional[float], Optional[float]]],
|
|
147
|
+
) -> float:
|
|
148
|
+
"""Return a 0-1 score indicating where ``value`` sits within ``bounds``."""
|
|
149
|
+
|
|
150
|
+
if value is None or not bounds:
|
|
151
|
+
return 0.0
|
|
152
|
+
|
|
153
|
+
lower, upper = bounds
|
|
154
|
+
if lower is None and upper is None:
|
|
155
|
+
return 0.0
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
numeric_value = float(value)
|
|
159
|
+
except (TypeError, ValueError):
|
|
160
|
+
return 0.0
|
|
161
|
+
|
|
162
|
+
lower_bound = float(lower) if lower is not None else numeric_value
|
|
163
|
+
upper_bound = float(upper) if upper is not None else numeric_value
|
|
164
|
+
if not math.isfinite(lower_bound):
|
|
165
|
+
lower_bound = numeric_value
|
|
166
|
+
if not math.isfinite(upper_bound):
|
|
167
|
+
upper_bound = numeric_value
|
|
168
|
+
|
|
169
|
+
# If every datapoint shares the exact same numeric value the rating should
|
|
170
|
+
# not appear highlighted. Treat effectively zero-width ranges as having no
|
|
171
|
+
# intensity so the chip renders in its neutral state.
|
|
172
|
+
if math.isclose(lower_bound, upper_bound, rel_tol=1e-9, abs_tol=1e-12):
|
|
173
|
+
return 0.0
|
|
174
|
+
|
|
175
|
+
span = upper_bound - lower_bound
|
|
176
|
+
if span == 0:
|
|
177
|
+
return 0.0
|
|
178
|
+
|
|
179
|
+
normalized = (numeric_value - lower_bound) / span
|
|
180
|
+
return max(0.0, min(1.0, normalized))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True)
|
|
184
|
+
class _AttributeRequest:
|
|
185
|
+
column: str
|
|
186
|
+
label: str
|
|
187
|
+
dynamic: bool = False
|
|
188
|
+
description: Optional[str] = None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(frozen=True)
|
|
192
|
+
class _AttributeSpec:
|
|
193
|
+
column: str
|
|
194
|
+
label: str
|
|
195
|
+
kind: Literal["snippet", "boolean", "numeric", "text"]
|
|
196
|
+
dynamic: bool = False
|
|
197
|
+
description: Optional[str] = None
|
|
198
|
+
|
|
199
|
+
def _coerce_category_spec(
|
|
200
|
+
categories: Optional[Union[Sequence[Any], Any]]
|
|
201
|
+
) -> Optional[Union[List[str], str]]:
|
|
202
|
+
"""Normalize the ``categories`` argument into a predictable form."""
|
|
203
|
+
|
|
204
|
+
if categories is None:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
if isinstance(categories, str):
|
|
208
|
+
return categories if categories == "coded_passages" else [categories]
|
|
209
|
+
|
|
210
|
+
if isinstance(categories, Iterable):
|
|
211
|
+
normalized: List[str] = []
|
|
212
|
+
for item in categories:
|
|
213
|
+
if item is None:
|
|
214
|
+
continue
|
|
215
|
+
normalized.append(str(item))
|
|
216
|
+
return normalized
|
|
217
|
+
|
|
218
|
+
return [str(categories)]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _normalize_attribute_requests(
|
|
222
|
+
attributes: Optional[Union[Mapping[str, Any], Sequence[Any], Any]]
|
|
223
|
+
) -> List[_AttributeRequest]:
|
|
224
|
+
"""Coerce attribute selections into a structured list."""
|
|
225
|
+
|
|
226
|
+
if attributes is None:
|
|
227
|
+
return []
|
|
228
|
+
|
|
229
|
+
label_overrides: Dict[str, str] = {}
|
|
230
|
+
descriptions: Dict[str, str] = {}
|
|
231
|
+
if isinstance(attributes, Mapping):
|
|
232
|
+
iterable: Iterable[Any] = attributes.keys()
|
|
233
|
+
for key, value in attributes.items():
|
|
234
|
+
if value is None:
|
|
235
|
+
continue
|
|
236
|
+
column_key = str(key)
|
|
237
|
+
if isinstance(value, Mapping):
|
|
238
|
+
label_hint = str(value.get("label", "")).strip()
|
|
239
|
+
desc_hint = str(value.get("description", "")).strip()
|
|
240
|
+
if label_hint:
|
|
241
|
+
label_overrides[column_key] = label_hint
|
|
242
|
+
if desc_hint:
|
|
243
|
+
descriptions[column_key] = desc_hint
|
|
244
|
+
continue
|
|
245
|
+
text = str(value).strip()
|
|
246
|
+
if not text:
|
|
247
|
+
continue
|
|
248
|
+
descriptions[column_key] = text
|
|
249
|
+
elif isinstance(attributes, (str, bytes)):
|
|
250
|
+
iterable = [attributes]
|
|
251
|
+
elif isinstance(attributes, Iterable):
|
|
252
|
+
iterable = attributes
|
|
253
|
+
else:
|
|
254
|
+
iterable = [attributes]
|
|
255
|
+
|
|
256
|
+
requests: List[_AttributeRequest] = []
|
|
257
|
+
seen: Set[Tuple[str, bool]] = set()
|
|
258
|
+
for entry in iterable:
|
|
259
|
+
dynamic = False
|
|
260
|
+
label_hint: Optional[str] = None
|
|
261
|
+
description_hint: Optional[str] = None
|
|
262
|
+
if isinstance(entry, tuple) and entry:
|
|
263
|
+
column = str(entry[0])
|
|
264
|
+
label_hint = str(entry[1]) if len(entry) > 1 else None
|
|
265
|
+
elif isinstance(entry, list) and entry:
|
|
266
|
+
column = str(entry[0])
|
|
267
|
+
label_hint = str(entry[1]) if len(entry) > 1 else None
|
|
268
|
+
elif isinstance(entry, tuple) and not entry:
|
|
269
|
+
continue
|
|
270
|
+
elif isinstance(entry, Mapping):
|
|
271
|
+
for key, value in entry.items():
|
|
272
|
+
column = str(key)
|
|
273
|
+
override = str(value).strip() if value is not None else ""
|
|
274
|
+
dynamic = column == "coded_passages"
|
|
275
|
+
pretty = override or column.replace("_", " ").title()
|
|
276
|
+
identity = (column, dynamic)
|
|
277
|
+
if identity in seen:
|
|
278
|
+
continue
|
|
279
|
+
seen.add(identity)
|
|
280
|
+
requests.append(
|
|
281
|
+
_AttributeRequest(
|
|
282
|
+
column=column,
|
|
283
|
+
label=pretty,
|
|
284
|
+
dynamic=dynamic,
|
|
285
|
+
description=descriptions.get(column),
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
continue
|
|
289
|
+
else:
|
|
290
|
+
column = str(entry)
|
|
291
|
+
|
|
292
|
+
dynamic = column == "coded_passages"
|
|
293
|
+
label_source = label_overrides.get(column) or label_hint
|
|
294
|
+
description_hint = descriptions.get(column)
|
|
295
|
+
if label_source:
|
|
296
|
+
pretty = label_source
|
|
297
|
+
else:
|
|
298
|
+
pretty = column.replace("_", " ").title()
|
|
299
|
+
identity = (column, dynamic)
|
|
300
|
+
if identity in seen:
|
|
301
|
+
continue
|
|
302
|
+
seen.add(identity)
|
|
303
|
+
requests.append(
|
|
304
|
+
_AttributeRequest(
|
|
305
|
+
column=column,
|
|
306
|
+
label=pretty,
|
|
307
|
+
dynamic=dynamic,
|
|
308
|
+
description=description_hint,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return requests
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _collect_mapping_keys(series: pd.Series, limit: int = 64) -> List[str]:
|
|
316
|
+
keys: List[str] = []
|
|
317
|
+
seen: Set[str] = set()
|
|
318
|
+
for value in series:
|
|
319
|
+
parsed = _parse_structured_cell(value)
|
|
320
|
+
if not isinstance(parsed, Mapping):
|
|
321
|
+
continue
|
|
322
|
+
for key in parsed.keys():
|
|
323
|
+
if key is None:
|
|
324
|
+
continue
|
|
325
|
+
text = str(key).strip()
|
|
326
|
+
if not text or text in seen:
|
|
327
|
+
continue
|
|
328
|
+
seen.add(text)
|
|
329
|
+
keys.append(text)
|
|
330
|
+
if len(keys) >= limit:
|
|
331
|
+
return keys
|
|
332
|
+
return keys
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _extract_mapping_value(value: Any, key: str) -> Any:
|
|
336
|
+
parsed = _parse_structured_cell(value)
|
|
337
|
+
if not isinstance(parsed, Mapping):
|
|
338
|
+
return None
|
|
339
|
+
raw = parsed.get(key)
|
|
340
|
+
if isinstance(raw, Mapping):
|
|
341
|
+
for candidate in ("value", "rating", "score", "answer", "label", "text"):
|
|
342
|
+
if candidate in raw and raw[candidate] is not None:
|
|
343
|
+
return raw[candidate]
|
|
344
|
+
return raw.get("value")
|
|
345
|
+
return raw
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _expand_mapping_attribute_requests(
|
|
349
|
+
df: pd.DataFrame,
|
|
350
|
+
requests: List[_AttributeRequest],
|
|
351
|
+
) -> Tuple[pd.DataFrame, List[_AttributeRequest]]:
|
|
352
|
+
if df.empty or not requests:
|
|
353
|
+
return df, requests
|
|
354
|
+
|
|
355
|
+
expanded: List[_AttributeRequest] = []
|
|
356
|
+
for request in requests:
|
|
357
|
+
if request.dynamic or request.column not in df.columns:
|
|
358
|
+
expanded.append(request)
|
|
359
|
+
continue
|
|
360
|
+
series = df[request.column]
|
|
361
|
+
mapping_keys = _collect_mapping_keys(series)
|
|
362
|
+
if not mapping_keys:
|
|
363
|
+
expanded.append(request)
|
|
364
|
+
continue
|
|
365
|
+
for key in mapping_keys:
|
|
366
|
+
derived_column = f"{request.column}::{key}"
|
|
367
|
+
if derived_column not in df.columns:
|
|
368
|
+
df[derived_column] = series.apply(
|
|
369
|
+
lambda value, key=key: _extract_mapping_value(value, key)
|
|
370
|
+
)
|
|
371
|
+
expanded.append(
|
|
372
|
+
_AttributeRequest(
|
|
373
|
+
column=derived_column,
|
|
374
|
+
label=str(key),
|
|
375
|
+
dynamic=False,
|
|
376
|
+
description=request.description,
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
return df, expanded
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _coerce_bool_value(value: Any) -> Optional[bool]:
|
|
383
|
+
if isinstance(value, bool):
|
|
384
|
+
return value
|
|
385
|
+
if isinstance(value, str):
|
|
386
|
+
lowered = value.strip().lower()
|
|
387
|
+
if lowered in {"true", "yes", "y"}:
|
|
388
|
+
return True
|
|
389
|
+
if lowered in {"false", "no", "n"}:
|
|
390
|
+
return False
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _coerce_numeric_value(value: Any) -> Optional[float]:
|
|
395
|
+
if _is_na(value):
|
|
396
|
+
return None
|
|
397
|
+
if isinstance(value, bool):
|
|
398
|
+
return None
|
|
399
|
+
if isinstance(value, (int, float)):
|
|
400
|
+
result = float(value)
|
|
401
|
+
if math.isnan(result):
|
|
402
|
+
return None
|
|
403
|
+
return result
|
|
404
|
+
if isinstance(value, str):
|
|
405
|
+
stripped = value.strip()
|
|
406
|
+
if not stripped:
|
|
407
|
+
return None
|
|
408
|
+
try:
|
|
409
|
+
result = float(stripped)
|
|
410
|
+
if math.isnan(result):
|
|
411
|
+
return None
|
|
412
|
+
return result
|
|
413
|
+
except ValueError:
|
|
414
|
+
return None
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _looks_like_snippet_column(values: Sequence[Any]) -> bool:
|
|
419
|
+
hits = 0
|
|
420
|
+
inspected = 0
|
|
421
|
+
structured = 0
|
|
422
|
+
for value in values:
|
|
423
|
+
if _is_na(value):
|
|
424
|
+
continue
|
|
425
|
+
inspected += 1
|
|
426
|
+
parsed = _parse_structured_cell(value)
|
|
427
|
+
|
|
428
|
+
candidate = parsed
|
|
429
|
+
if isinstance(parsed, str):
|
|
430
|
+
candidate = _parse_structured_cell(parsed)
|
|
431
|
+
|
|
432
|
+
if isinstance(candidate, dict):
|
|
433
|
+
structured += 1
|
|
434
|
+
if candidate:
|
|
435
|
+
hits += 1
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
if isinstance(candidate, (list, tuple, set)):
|
|
439
|
+
structured += 1
|
|
440
|
+
if any(str(item).strip() for item in candidate if not _is_na(item)):
|
|
441
|
+
hits += 1
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
if inspected == 0:
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
threshold = max(1, inspected // 2)
|
|
448
|
+
if hits >= threshold:
|
|
449
|
+
return True
|
|
450
|
+
|
|
451
|
+
return structured >= threshold
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _infer_attribute_kind(series: Optional[pd.Series]) -> Literal["snippet", "boolean", "numeric", "text"]:
|
|
455
|
+
if series is None:
|
|
456
|
+
return "text"
|
|
457
|
+
|
|
458
|
+
sample: List[Any] = []
|
|
459
|
+
for value in series:
|
|
460
|
+
if _is_na(value):
|
|
461
|
+
continue
|
|
462
|
+
sample.append(value)
|
|
463
|
+
if len(sample) >= 25:
|
|
464
|
+
break
|
|
465
|
+
|
|
466
|
+
if not sample:
|
|
467
|
+
return "text"
|
|
468
|
+
|
|
469
|
+
if _looks_like_snippet_column(sample):
|
|
470
|
+
return "snippet"
|
|
471
|
+
|
|
472
|
+
bool_hits = 0
|
|
473
|
+
numeric_hits = 0
|
|
474
|
+
for value in sample:
|
|
475
|
+
if _coerce_bool_value(value) is not None:
|
|
476
|
+
bool_hits += 1
|
|
477
|
+
continue
|
|
478
|
+
if _coerce_numeric_value(value) is not None:
|
|
479
|
+
numeric_hits += 1
|
|
480
|
+
|
|
481
|
+
threshold = max(1, int(len(sample) * 0.6))
|
|
482
|
+
if bool_hits >= threshold:
|
|
483
|
+
return "boolean"
|
|
484
|
+
if numeric_hits >= threshold:
|
|
485
|
+
return "numeric"
|
|
486
|
+
return "text"
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _format_numeric_chip(value: float) -> str:
|
|
490
|
+
magnitude = abs(value)
|
|
491
|
+
if magnitude >= 100:
|
|
492
|
+
text = f"{value:.0f}"
|
|
493
|
+
elif magnitude >= 10:
|
|
494
|
+
text = f"{value:.1f}"
|
|
495
|
+
else:
|
|
496
|
+
text = f"{value:.2f}"
|
|
497
|
+
trimmed = text
|
|
498
|
+
if "." in trimmed:
|
|
499
|
+
trimmed = trimmed.rstrip("0").rstrip(".")
|
|
500
|
+
if len(trimmed) > 4:
|
|
501
|
+
trimmed = trimmed[:4]
|
|
502
|
+
return trimmed
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _passage_matches_filters(
|
|
506
|
+
passage: Mapping[str, Any],
|
|
507
|
+
*,
|
|
508
|
+
required_snippets: Optional[Set[str]] = None,
|
|
509
|
+
required_bools: Optional[Set[str]] = None,
|
|
510
|
+
numeric_filters: Optional[Mapping[str, Tuple[float, float]]] = None,
|
|
511
|
+
) -> bool:
|
|
512
|
+
"""Return ``True`` if a passage matches the provided filter selections."""
|
|
513
|
+
|
|
514
|
+
snippet_map = passage.get("snippets") or {}
|
|
515
|
+
for category in required_snippets or ():
|
|
516
|
+
if not snippet_map.get(category):
|
|
517
|
+
return False
|
|
518
|
+
|
|
519
|
+
bool_map = passage.get("bools") or {}
|
|
520
|
+
for column in required_bools or ():
|
|
521
|
+
if bool_map.get(column) is not True:
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
numeric_map = passage.get("numeric") or {}
|
|
525
|
+
for column, bounds in (numeric_filters or {}).items():
|
|
526
|
+
value = numeric_map.get(column)
|
|
527
|
+
if value is None:
|
|
528
|
+
return False
|
|
529
|
+
lower, upper = bounds
|
|
530
|
+
if value < lower - 1e-9 or value > upper + 1e-9:
|
|
531
|
+
return False
|
|
532
|
+
|
|
533
|
+
return True
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class PassageViewer:
|
|
537
|
+
"""Legacy desktop viewer placeholder."""
|
|
538
|
+
|
|
539
|
+
def __init__(self, *_, **__):
|
|
540
|
+
raise RuntimeError(
|
|
541
|
+
"The tkinter-based PassageViewer has been retired. "
|
|
542
|
+
"Use gabriel.view(...) inside a notebook environment."
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def show(self):
|
|
546
|
+
raise RuntimeError(
|
|
547
|
+
"gabriel.view now renders exclusively via the notebook interface; "
|
|
548
|
+
"desktop mode is no longer available."
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
_COLAB_STYLE = """
|
|
552
|
+
<style>
|
|
553
|
+
.gabriel-codify-viewer {
|
|
554
|
+
font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
555
|
+
color: #f5f7fa;
|
|
556
|
+
background: transparent;
|
|
557
|
+
}
|
|
558
|
+
.gabriel-codify-viewer .gabriel-status {
|
|
559
|
+
font-size: 14px;
|
|
560
|
+
letter-spacing: 0.02em;
|
|
561
|
+
text-transform: uppercase;
|
|
562
|
+
color: rgba(255, 255, 255, 0.7);
|
|
563
|
+
margin-bottom: 8px;
|
|
564
|
+
}
|
|
565
|
+
.gabriel-codify-viewer .gabriel-controls {
|
|
566
|
+
display: flex;
|
|
567
|
+
gap: 10px;
|
|
568
|
+
align-items: center;
|
|
569
|
+
flex-wrap: wrap;
|
|
570
|
+
margin-bottom: 12px;
|
|
571
|
+
}
|
|
572
|
+
.gabriel-codify-viewer .gabriel-nav-group {
|
|
573
|
+
display: inline-flex;
|
|
574
|
+
gap: 8px;
|
|
575
|
+
align-items: center;
|
|
576
|
+
flex-wrap: wrap;
|
|
577
|
+
}
|
|
578
|
+
.gabriel-codify-viewer .gabriel-nav-button {
|
|
579
|
+
border-radius: 999px;
|
|
580
|
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
581
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.03));
|
|
582
|
+
color: rgba(255, 255, 255, 0.95);
|
|
583
|
+
padding: 6px 16px;
|
|
584
|
+
font-size: 12px;
|
|
585
|
+
font-weight: 600;
|
|
586
|
+
letter-spacing: 0.06em;
|
|
587
|
+
text-transform: uppercase;
|
|
588
|
+
cursor: pointer;
|
|
589
|
+
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease, border-color 0.18s ease;
|
|
590
|
+
}
|
|
591
|
+
.gabriel-codify-viewer .gabriel-nav-button:hover:not(:disabled) {
|
|
592
|
+
transform: translateY(-1px);
|
|
593
|
+
background: linear-gradient(135deg, rgba(0, 188, 212, 0.55), rgba(0, 188, 212, 0.2));
|
|
594
|
+
border-color: rgba(0, 188, 212, 0.65);
|
|
595
|
+
box-shadow: 0 10px 22px rgba(0, 188, 212, 0.25);
|
|
596
|
+
}
|
|
597
|
+
.gabriel-codify-viewer .gabriel-nav-button:disabled {
|
|
598
|
+
opacity: 0.35;
|
|
599
|
+
cursor: not-allowed;
|
|
600
|
+
box-shadow: none;
|
|
601
|
+
}
|
|
602
|
+
.gabriel-codify-viewer .gabriel-slider-shell {
|
|
603
|
+
display: inline-flex;
|
|
604
|
+
align-items: center;
|
|
605
|
+
gap: 12px;
|
|
606
|
+
padding: 6px 16px;
|
|
607
|
+
border-radius: 999px;
|
|
608
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
609
|
+
background: rgba(255, 255, 255, 0.03);
|
|
610
|
+
}
|
|
611
|
+
.gabriel-codify-viewer .gabriel-slider {
|
|
612
|
+
-webkit-appearance: none;
|
|
613
|
+
appearance: none;
|
|
614
|
+
width: 160px;
|
|
615
|
+
height: 4px;
|
|
616
|
+
background: transparent;
|
|
617
|
+
cursor: pointer;
|
|
618
|
+
}
|
|
619
|
+
.gabriel-codify-viewer .gabriel-slider:focus-visible {
|
|
620
|
+
outline: none;
|
|
621
|
+
}
|
|
622
|
+
.gabriel-codify-viewer .gabriel-slider::-webkit-slider-runnable-track {
|
|
623
|
+
height: 4px;
|
|
624
|
+
border-radius: 999px;
|
|
625
|
+
background: rgba(255, 255, 255, 0.25);
|
|
626
|
+
}
|
|
627
|
+
.gabriel-codify-viewer .gabriel-slider::-moz-range-track {
|
|
628
|
+
height: 4px;
|
|
629
|
+
border-radius: 999px;
|
|
630
|
+
background: rgba(255, 255, 255, 0.25);
|
|
631
|
+
}
|
|
632
|
+
.gabriel-codify-viewer .gabriel-slider::-webkit-slider-thumb {
|
|
633
|
+
-webkit-appearance: none;
|
|
634
|
+
appearance: none;
|
|
635
|
+
width: 18px;
|
|
636
|
+
height: 18px;
|
|
637
|
+
border-radius: 50%;
|
|
638
|
+
background: #00bcd4;
|
|
639
|
+
border: 2px solid #0b1016;
|
|
640
|
+
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.45);
|
|
641
|
+
margin-top: -7px;
|
|
642
|
+
}
|
|
643
|
+
.gabriel-codify-viewer .gabriel-slider::-moz-range-thumb {
|
|
644
|
+
width: 18px;
|
|
645
|
+
height: 18px;
|
|
646
|
+
border-radius: 50%;
|
|
647
|
+
background: #00bcd4;
|
|
648
|
+
border: 2px solid #0b1016;
|
|
649
|
+
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.45);
|
|
650
|
+
}
|
|
651
|
+
.gabriel-codify-viewer .gabriel-slider:disabled {
|
|
652
|
+
opacity: 0.35;
|
|
653
|
+
cursor: not-allowed;
|
|
654
|
+
}
|
|
655
|
+
.gabriel-codify-viewer .gabriel-slider-count {
|
|
656
|
+
font-size: 11px;
|
|
657
|
+
letter-spacing: 0.08em;
|
|
658
|
+
text-transform: uppercase;
|
|
659
|
+
color: rgba(255, 255, 255, 0.78);
|
|
660
|
+
font-weight: 600;
|
|
661
|
+
}
|
|
662
|
+
.gabriel-codify-viewer .gabriel-passage-panel {
|
|
663
|
+
background: #13161a;
|
|
664
|
+
border: 1px solid #2b323c;
|
|
665
|
+
border-radius: 14px;
|
|
666
|
+
padding: 18px 20px;
|
|
667
|
+
box-shadow: 0 16px 40px rgba(9, 11, 16, 0.45);
|
|
668
|
+
}
|
|
669
|
+
.gabriel-codify-viewer .gabriel-passage-scroll {
|
|
670
|
+
max-height: 560px;
|
|
671
|
+
overflow-y: auto;
|
|
672
|
+
padding-right: 12px;
|
|
673
|
+
}
|
|
674
|
+
.gabriel-codify-viewer .gabriel-legend {
|
|
675
|
+
position: sticky;
|
|
676
|
+
top: 0;
|
|
677
|
+
z-index: 2;
|
|
678
|
+
background: #13161a;
|
|
679
|
+
padding-bottom: 12px;
|
|
680
|
+
margin-bottom: 16px;
|
|
681
|
+
border-bottom: 1px solid #2b323c;
|
|
682
|
+
}
|
|
683
|
+
.gabriel-codify-viewer .gabriel-legend-grid {
|
|
684
|
+
display: flex;
|
|
685
|
+
flex-wrap: wrap;
|
|
686
|
+
gap: 12px;
|
|
687
|
+
}
|
|
688
|
+
.gabriel-codify-viewer .gabriel-legend-item {
|
|
689
|
+
display: inline-flex;
|
|
690
|
+
align-items: center;
|
|
691
|
+
gap: 10px;
|
|
692
|
+
padding: 6px 12px;
|
|
693
|
+
border-radius: 10px;
|
|
694
|
+
background: rgba(255, 255, 255, 0.05);
|
|
695
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
696
|
+
font-size: 13px;
|
|
697
|
+
color: rgba(255, 255, 255, 0.88);
|
|
698
|
+
cursor: pointer;
|
|
699
|
+
transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
|
|
700
|
+
text-decoration: none;
|
|
701
|
+
font: inherit;
|
|
702
|
+
line-height: 1.2;
|
|
703
|
+
}
|
|
704
|
+
.gabriel-codify-viewer .gabriel-legend-item--boolean,
|
|
705
|
+
.gabriel-codify-viewer .gabriel-legend-item--numeric {
|
|
706
|
+
min-height: 36px;
|
|
707
|
+
--gabriel-chip-bg: rgba(255, 255, 255, 0.05);
|
|
708
|
+
--gabriel-chip-border: rgba(255, 255, 255, 0.12);
|
|
709
|
+
--gabriel-chip-glow: rgba(0, 0, 0, 0.12);
|
|
710
|
+
}
|
|
711
|
+
.gabriel-codify-viewer .gabriel-legend-item:hover {
|
|
712
|
+
background: rgba(255, 255, 255, 0.12);
|
|
713
|
+
border-color: rgba(255, 255, 255, 0.18);
|
|
714
|
+
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.25);
|
|
715
|
+
}
|
|
716
|
+
.gabriel-codify-viewer .gabriel-legend-item:focus-visible {
|
|
717
|
+
outline: none;
|
|
718
|
+
box-shadow: 0 0 0 2px rgba(0, 188, 212, 0.65);
|
|
719
|
+
}
|
|
720
|
+
.gabriel-codify-viewer .gabriel-legend-item span {
|
|
721
|
+
pointer-events: none;
|
|
722
|
+
}
|
|
723
|
+
.gabriel-codify-viewer .gabriel-legend-item--snippet {
|
|
724
|
+
--gabriel-chip-bg: rgba(255, 255, 255, 0.12);
|
|
725
|
+
--gabriel-chip-border: rgba(255, 255, 255, 0.3);
|
|
726
|
+
--gabriel-chip-glow: rgba(0, 0, 0, 0.25);
|
|
727
|
+
padding: 6px 14px;
|
|
728
|
+
}
|
|
729
|
+
.gabriel-codify-viewer .gabriel-legend-item--snippet[data-count]:not([data-count="0"]) {
|
|
730
|
+
background: var(--gabriel-chip-bg);
|
|
731
|
+
border-color: var(--gabriel-chip-border);
|
|
732
|
+
box-shadow: 0 12px 26px var(--gabriel-chip-glow);
|
|
733
|
+
}
|
|
734
|
+
.gabriel-codify-viewer .gabriel-legend-item--snippet[data-count]:not([data-count="0"]):hover {
|
|
735
|
+
background: var(--gabriel-chip-bg);
|
|
736
|
+
border-color: var(--gabriel-chip-border);
|
|
737
|
+
box-shadow: 0 16px 32px var(--gabriel-chip-glow);
|
|
738
|
+
}
|
|
739
|
+
.gabriel-codify-viewer .gabriel-legend-item--snippet[data-count="0"] {
|
|
740
|
+
opacity: 0.55;
|
|
741
|
+
}
|
|
742
|
+
.gabriel-codify-viewer .gabriel-legend-item--snippet .gabriel-legend-count {
|
|
743
|
+
background: rgba(0, 0, 0, 0.4);
|
|
744
|
+
}
|
|
745
|
+
.gabriel-codify-viewer .gabriel-legend-color {
|
|
746
|
+
width: 16px;
|
|
747
|
+
height: 16px;
|
|
748
|
+
border-radius: 4px;
|
|
749
|
+
border: 1px solid rgba(0, 0, 0, 0.18);
|
|
750
|
+
}
|
|
751
|
+
.gabriel-codify-viewer .gabriel-legend-label {
|
|
752
|
+
font-weight: 600;
|
|
753
|
+
color: inherit;
|
|
754
|
+
}
|
|
755
|
+
.gabriel-codify-viewer .gabriel-legend-count {
|
|
756
|
+
font-size: 11px;
|
|
757
|
+
padding: 2px 6px;
|
|
758
|
+
border-radius: 999px;
|
|
759
|
+
background: rgba(255, 255, 255, 0.12);
|
|
760
|
+
color: rgba(255, 255, 255, 0.78);
|
|
761
|
+
}
|
|
762
|
+
.gabriel-codify-viewer .gabriel-legend-value {
|
|
763
|
+
font-size: 11px;
|
|
764
|
+
padding: 2px 8px;
|
|
765
|
+
border-radius: 999px;
|
|
766
|
+
background: rgba(255, 255, 255, 0.14);
|
|
767
|
+
color: rgba(255, 255, 255, 0.85);
|
|
768
|
+
display: inline-flex;
|
|
769
|
+
justify-content: center;
|
|
770
|
+
align-items: center;
|
|
771
|
+
min-width: calc(3ch + 10px);
|
|
772
|
+
text-align: center;
|
|
773
|
+
line-height: 1.3;
|
|
774
|
+
font-variant-numeric: tabular-nums;
|
|
775
|
+
font-feature-settings: 'tnum' 1, 'liga' 0;
|
|
776
|
+
}
|
|
777
|
+
.gabriel-codify-viewer .gabriel-legend-item--boolean {
|
|
778
|
+
--gabriel-chip-bg: rgba(0, 188, 212, 0.18);
|
|
779
|
+
--gabriel-chip-border: rgba(0, 188, 212, 0.55);
|
|
780
|
+
--gabriel-chip-glow: rgba(0, 188, 212, 0.25);
|
|
781
|
+
background: rgba(255, 255, 255, 0.04);
|
|
782
|
+
border-color: rgba(255, 255, 255, 0.08);
|
|
783
|
+
opacity: 0.85;
|
|
784
|
+
}
|
|
785
|
+
.gabriel-codify-viewer .gabriel-legend-item--boolean .gabriel-legend-value {
|
|
786
|
+
text-transform: uppercase;
|
|
787
|
+
letter-spacing: 0.06em;
|
|
788
|
+
}
|
|
789
|
+
.gabriel-codify-viewer .gabriel-legend-item--boolean .gabriel-legend-swatch {
|
|
790
|
+
width: 10px;
|
|
791
|
+
height: 10px;
|
|
792
|
+
border-radius: 999px;
|
|
793
|
+
background: var(--gabriel-chip-border, rgba(0, 188, 212, 0.55));
|
|
794
|
+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
|
|
795
|
+
opacity: 0.75;
|
|
796
|
+
transition: box-shadow 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
|
|
797
|
+
flex-shrink: 0;
|
|
798
|
+
}
|
|
799
|
+
.gabriel-codify-viewer .gabriel-legend-item--boolean.is-true {
|
|
800
|
+
background: var(--gabriel-chip-bg);
|
|
801
|
+
border-color: var(--gabriel-chip-border);
|
|
802
|
+
box-shadow: 0 12px 28px var(--gabriel-chip-glow);
|
|
803
|
+
color: #e6fcff;
|
|
804
|
+
opacity: 1;
|
|
805
|
+
}
|
|
806
|
+
.gabriel-codify-viewer .gabriel-legend-item--boolean.is-true:hover {
|
|
807
|
+
background: var(--gabriel-chip-bg);
|
|
808
|
+
border-color: var(--gabriel-chip-border);
|
|
809
|
+
box-shadow: 0 16px 34px var(--gabriel-chip-glow);
|
|
810
|
+
}
|
|
811
|
+
.gabriel-codify-viewer .gabriel-legend-item--boolean.is-true .gabriel-legend-value {
|
|
812
|
+
background: var(--gabriel-chip-bg);
|
|
813
|
+
color: #e6fcff;
|
|
814
|
+
}
|
|
815
|
+
.gabriel-codify-viewer .gabriel-legend-item--boolean.is-true .gabriel-legend-swatch {
|
|
816
|
+
opacity: 1;
|
|
817
|
+
box-shadow: 0 0 12px var(--gabriel-chip-glow, rgba(0, 188, 212, 0.3));
|
|
818
|
+
transform: scale(1.1);
|
|
819
|
+
}
|
|
820
|
+
.gabriel-codify-viewer .gabriel-legend-item--boolean.is-false {
|
|
821
|
+
opacity: 0.6;
|
|
822
|
+
}
|
|
823
|
+
.gabriel-codify-viewer .gabriel-legend-item--boolean.is-false .gabriel-legend-value {
|
|
824
|
+
background: rgba(255, 255, 255, 0.1);
|
|
825
|
+
color: rgba(255, 255, 255, 0.65);
|
|
826
|
+
}
|
|
827
|
+
.gabriel-codify-viewer .gabriel-legend-item.is-filtered,
|
|
828
|
+
.gabriel-codify-viewer .gabriel-legend-item.is-sorted {
|
|
829
|
+
border-color: var(--gabriel-chip-border, rgba(0, 188, 212, 0.6));
|
|
830
|
+
box-shadow: 0 6px 18px var(--gabriel-chip-glow, rgba(0, 188, 212, 0.25));
|
|
831
|
+
}
|
|
832
|
+
.gabriel-codify-viewer .gabriel-legend-item--numeric {
|
|
833
|
+
background: var(--gabriel-chip-bg, rgba(255, 255, 255, 0.05));
|
|
834
|
+
border-color: var(--gabriel-chip-border, rgba(255, 255, 255, 0.12));
|
|
835
|
+
box-shadow: 0 8px 18px var(--gabriel-chip-glow, rgba(0, 0, 0, 0.12));
|
|
836
|
+
}
|
|
837
|
+
.gabriel-codify-viewer .gabriel-legend-item--numeric .gabriel-sort-indicator {
|
|
838
|
+
display: inline-flex;
|
|
839
|
+
width: 16px;
|
|
840
|
+
height: 16px;
|
|
841
|
+
align-items: center;
|
|
842
|
+
justify-content: center;
|
|
843
|
+
font-size: 11px;
|
|
844
|
+
color: rgba(255, 255, 255, 0.7);
|
|
845
|
+
}
|
|
846
|
+
.gabriel-codify-viewer .gabriel-legend-item--numeric .gabriel-sort-indicator::before {
|
|
847
|
+
content: '↕';
|
|
848
|
+
}
|
|
849
|
+
.gabriel-codify-viewer .gabriel-legend-item--numeric.is-sorted-asc .gabriel-sort-indicator::before {
|
|
850
|
+
content: '↑';
|
|
851
|
+
}
|
|
852
|
+
.gabriel-codify-viewer .gabriel-legend-item--numeric.is-sorted-desc .gabriel-sort-indicator::before {
|
|
853
|
+
content: '↓';
|
|
854
|
+
}
|
|
855
|
+
.gabriel-codify-viewer .gabriel-reset-button {
|
|
856
|
+
border-radius: 999px;
|
|
857
|
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
858
|
+
background: rgba(255, 255, 255, 0.05);
|
|
859
|
+
color: rgba(255, 255, 255, 0.8);
|
|
860
|
+
padding: 5px 16px;
|
|
861
|
+
font-size: 11px;
|
|
862
|
+
letter-spacing: 0.08em;
|
|
863
|
+
text-transform: uppercase;
|
|
864
|
+
cursor: pointer;
|
|
865
|
+
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
|
866
|
+
}
|
|
867
|
+
.gabriel-codify-viewer .gabriel-reset-button:hover:not(:disabled) {
|
|
868
|
+
background: rgba(0, 188, 212, 0.15);
|
|
869
|
+
border-color: rgba(0, 188, 212, 0.6);
|
|
870
|
+
color: #e6fcff;
|
|
871
|
+
}
|
|
872
|
+
.gabriel-codify-viewer .gabriel-reset-button:disabled {
|
|
873
|
+
opacity: 0.4;
|
|
874
|
+
cursor: not-allowed;
|
|
875
|
+
}
|
|
876
|
+
.gabriel-codify-viewer .gabriel-header {
|
|
877
|
+
margin-bottom: 18px;
|
|
878
|
+
padding: 14px 16px;
|
|
879
|
+
border-radius: 16px;
|
|
880
|
+
background: linear-gradient(140deg, rgba(255, 255, 255, 0.05), rgba(9, 12, 18, 0.2));
|
|
881
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
882
|
+
box-shadow: 0 18px 36px rgba(5, 8, 13, 0.55);
|
|
883
|
+
display: flex;
|
|
884
|
+
flex-direction: column;
|
|
885
|
+
gap: 12px;
|
|
886
|
+
}
|
|
887
|
+
.gabriel-codify-viewer .gabriel-header-grid {
|
|
888
|
+
display: grid;
|
|
889
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
890
|
+
gap: 10px;
|
|
891
|
+
width: 100%;
|
|
892
|
+
}
|
|
893
|
+
.gabriel-codify-viewer .gabriel-header-row {
|
|
894
|
+
display: flex;
|
|
895
|
+
align-items: center;
|
|
896
|
+
gap: 10px;
|
|
897
|
+
padding: 8px 14px;
|
|
898
|
+
border-radius: 999px;
|
|
899
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
900
|
+
background: rgba(255, 255, 255, 0.03);
|
|
901
|
+
min-height: 0;
|
|
902
|
+
flex-wrap: wrap;
|
|
903
|
+
}
|
|
904
|
+
.gabriel-codify-viewer .gabriel-header-label {
|
|
905
|
+
font-weight: 600;
|
|
906
|
+
text-transform: uppercase;
|
|
907
|
+
font-size: 10px;
|
|
908
|
+
letter-spacing: 0.12em;
|
|
909
|
+
color: rgba(255, 255, 255, 0.68);
|
|
910
|
+
white-space: nowrap;
|
|
911
|
+
line-height: 1.1;
|
|
912
|
+
flex-shrink: 0;
|
|
913
|
+
}
|
|
914
|
+
.gabriel-codify-viewer .gabriel-header-value {
|
|
915
|
+
font-size: 13px;
|
|
916
|
+
color: rgba(248, 250, 252, 0.96);
|
|
917
|
+
font-weight: 600;
|
|
918
|
+
letter-spacing: 0.01em;
|
|
919
|
+
line-height: 1.35;
|
|
920
|
+
word-break: break-word;
|
|
921
|
+
text-align: left;
|
|
922
|
+
max-width: 100%;
|
|
923
|
+
flex: 1;
|
|
924
|
+
min-width: 0;
|
|
925
|
+
}
|
|
926
|
+
.gabriel-codify-viewer .gabriel-header-label,
|
|
927
|
+
.gabriel-codify-viewer .gabriel-header-value {
|
|
928
|
+
display: block;
|
|
929
|
+
}
|
|
930
|
+
@supports (backdrop-filter: blur(6px)) {
|
|
931
|
+
.gabriel-codify-viewer .gabriel-header {
|
|
932
|
+
backdrop-filter: blur(12px);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
@media (max-width: 640px) {
|
|
936
|
+
.gabriel-codify-viewer .gabriel-header-grid {
|
|
937
|
+
grid-template-columns: 1fr;
|
|
938
|
+
}
|
|
939
|
+
.gabriel-codify-viewer .gabriel-header-label {
|
|
940
|
+
white-space: normal;
|
|
941
|
+
font-size: 10px;
|
|
942
|
+
letter-spacing: 0.08em;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
.gabriel-codify-viewer .gabriel-active-cats {
|
|
946
|
+
margin-top: 6px;
|
|
947
|
+
display: flex;
|
|
948
|
+
flex-wrap: wrap;
|
|
949
|
+
gap: 6px 12px;
|
|
950
|
+
align-items: center;
|
|
951
|
+
}
|
|
952
|
+
.gabriel-codify-viewer .gabriel-active-label {
|
|
953
|
+
text-transform: uppercase;
|
|
954
|
+
font-size: 10px;
|
|
955
|
+
letter-spacing: 0.1em;
|
|
956
|
+
color: rgba(255, 255, 255, 0.62);
|
|
957
|
+
}
|
|
958
|
+
.gabriel-codify-viewer .gabriel-active-pill-stack {
|
|
959
|
+
display: flex;
|
|
960
|
+
flex-wrap: wrap;
|
|
961
|
+
gap: 6px;
|
|
962
|
+
}
|
|
963
|
+
.gabriel-codify-viewer .gabriel-active-pill {
|
|
964
|
+
font-size: 11px;
|
|
965
|
+
padding: 4px 10px;
|
|
966
|
+
border-radius: 999px;
|
|
967
|
+
background: rgba(255, 255, 255, 0.08);
|
|
968
|
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
969
|
+
color: rgba(255, 255, 255, 0.85);
|
|
970
|
+
}
|
|
971
|
+
.gabriel-codify-viewer .gabriel-note {
|
|
972
|
+
font-size: 13px;
|
|
973
|
+
color: rgba(255, 255, 255, 0.72);
|
|
974
|
+
margin-bottom: 10px;
|
|
975
|
+
}
|
|
976
|
+
.gabriel-codify-viewer.gabriel-theme-dark {
|
|
977
|
+
color-scheme: dark;
|
|
978
|
+
}
|
|
979
|
+
.gabriel-codify-viewer .gabriel-text {
|
|
980
|
+
font-size: 15px;
|
|
981
|
+
line-height: 1.7;
|
|
982
|
+
color: rgba(245, 247, 250, 0.96);
|
|
983
|
+
}
|
|
984
|
+
.gabriel-codify-viewer .gabriel-text p {
|
|
985
|
+
margin: 0 0 1em 0;
|
|
986
|
+
}
|
|
987
|
+
.gabriel-codify-viewer .gabriel-snippet {
|
|
988
|
+
position: relative;
|
|
989
|
+
border-radius: 6px;
|
|
990
|
+
padding: 1px 5px;
|
|
991
|
+
font-weight: 600;
|
|
992
|
+
color: #0d1014;
|
|
993
|
+
cursor: pointer;
|
|
994
|
+
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
|
995
|
+
}
|
|
996
|
+
.gabriel-codify-viewer .gabriel-snippet::after {
|
|
997
|
+
content: attr(data-label);
|
|
998
|
+
position: absolute;
|
|
999
|
+
left: 0;
|
|
1000
|
+
bottom: 100%;
|
|
1001
|
+
transform: translateY(-6px);
|
|
1002
|
+
background: rgba(8, 11, 17, 0.92);
|
|
1003
|
+
color: #f8fafc;
|
|
1004
|
+
padding: 3px 8px;
|
|
1005
|
+
border-radius: 6px;
|
|
1006
|
+
font-size: 11px;
|
|
1007
|
+
white-space: nowrap;
|
|
1008
|
+
opacity: 0;
|
|
1009
|
+
pointer-events: none;
|
|
1010
|
+
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
1011
|
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
|
|
1012
|
+
z-index: 5;
|
|
1013
|
+
}
|
|
1014
|
+
.gabriel-codify-viewer .gabriel-snippet:hover::after {
|
|
1015
|
+
opacity: 1;
|
|
1016
|
+
transform: translateY(-10px);
|
|
1017
|
+
}
|
|
1018
|
+
.gabriel-codify-viewer .gabriel-snippet-active {
|
|
1019
|
+
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 0 18px rgba(255, 255, 255, 0.35);
|
|
1020
|
+
}
|
|
1021
|
+
.gabriel-codify-viewer .gabriel-empty {
|
|
1022
|
+
font-style: italic;
|
|
1023
|
+
color: rgba(255, 255, 255, 0.65);
|
|
1024
|
+
}
|
|
1025
|
+
@media (prefers-color-scheme: light) {
|
|
1026
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) {
|
|
1027
|
+
color: #1f2933;
|
|
1028
|
+
}
|
|
1029
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-passage-panel {
|
|
1030
|
+
background: #f7f9fb;
|
|
1031
|
+
border-color: #d0d7e2;
|
|
1032
|
+
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
|
|
1033
|
+
}
|
|
1034
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend {
|
|
1035
|
+
background: #f7f9fb;
|
|
1036
|
+
border-color: #d0d7e2;
|
|
1037
|
+
}
|
|
1038
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-header {
|
|
1039
|
+
background: rgba(15, 23, 42, 0.06);
|
|
1040
|
+
border-color: rgba(15, 23, 42, 0.12);
|
|
1041
|
+
}
|
|
1042
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-header-row {
|
|
1043
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1044
|
+
border-color: rgba(15, 23, 42, 0.12);
|
|
1045
|
+
}
|
|
1046
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-header-label {
|
|
1047
|
+
color: rgba(15, 23, 42, 0.65);
|
|
1048
|
+
}
|
|
1049
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-header-value {
|
|
1050
|
+
color: rgba(15, 23, 42, 0.92);
|
|
1051
|
+
}
|
|
1052
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-text {
|
|
1053
|
+
color: #1f2933;
|
|
1054
|
+
}
|
|
1055
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item {
|
|
1056
|
+
background: rgba(15, 23, 42, 0.06);
|
|
1057
|
+
border-color: rgba(15, 23, 42, 0.12);
|
|
1058
|
+
color: rgba(15, 23, 42, 0.82);
|
|
1059
|
+
}
|
|
1060
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item:hover {
|
|
1061
|
+
background: rgba(15, 23, 42, 0.1);
|
|
1062
|
+
border-color: rgba(15, 23, 42, 0.18);
|
|
1063
|
+
}
|
|
1064
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item:focus-visible {
|
|
1065
|
+
box-shadow: 0 0 0 2px rgba(0, 188, 212, 0.4);
|
|
1066
|
+
}
|
|
1067
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item--snippet .gabriel-legend-count {
|
|
1068
|
+
background: rgba(15, 23, 42, 0.15);
|
|
1069
|
+
}
|
|
1070
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item--snippet[data-count]:not([data-count="0"]) {
|
|
1071
|
+
box-shadow: 0 12px 24px var(--gabriel-chip-glow, rgba(15, 23, 42, 0.18));
|
|
1072
|
+
}
|
|
1073
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item--boolean {
|
|
1074
|
+
background: rgba(15, 23, 42, 0.04);
|
|
1075
|
+
border-color: rgba(15, 23, 42, 0.08);
|
|
1076
|
+
color: rgba(15, 23, 42, 0.82);
|
|
1077
|
+
}
|
|
1078
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item--boolean .gabriel-legend-swatch {
|
|
1079
|
+
box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.2);
|
|
1080
|
+
}
|
|
1081
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item--boolean.is-true {
|
|
1082
|
+
background: var(--gabriel-chip-bg, rgba(37, 99, 235, 0.18));
|
|
1083
|
+
border-color: var(--gabriel-chip-border, rgba(37, 99, 235, 0.4));
|
|
1084
|
+
box-shadow: 0 12px 24px var(--gabriel-chip-glow, rgba(37, 99, 235, 0.2));
|
|
1085
|
+
color: rgba(15, 23, 42, 0.92);
|
|
1086
|
+
}
|
|
1087
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item--boolean.is-true .gabriel-legend-value {
|
|
1088
|
+
background: var(--gabriel-chip-bg, rgba(37, 99, 235, 0.35));
|
|
1089
|
+
color: rgba(15, 23, 42, 0.92);
|
|
1090
|
+
}
|
|
1091
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item--boolean.is-true .gabriel-legend-swatch {
|
|
1092
|
+
box-shadow: 0 0 12px var(--gabriel-chip-glow, rgba(37, 99, 235, 0.3));
|
|
1093
|
+
}
|
|
1094
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item--boolean.is-false .gabriel-legend-value {
|
|
1095
|
+
background: rgba(15, 23, 42, 0.08);
|
|
1096
|
+
color: rgba(15, 23, 42, 0.6);
|
|
1097
|
+
}
|
|
1098
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-active-label {
|
|
1099
|
+
color: rgba(15, 23, 42, 0.55);
|
|
1100
|
+
}
|
|
1101
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-active-pill {
|
|
1102
|
+
background: rgba(15, 23, 42, 0.06);
|
|
1103
|
+
border-color: rgba(15, 23, 42, 0.15);
|
|
1104
|
+
color: rgba(15, 23, 42, 0.8);
|
|
1105
|
+
}
|
|
1106
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-item--numeric {
|
|
1107
|
+
background: var(--gabriel-chip-bg, rgba(15, 23, 42, 0.06));
|
|
1108
|
+
border-color: var(--gabriel-chip-border, rgba(15, 23, 42, 0.12));
|
|
1109
|
+
box-shadow: 0 10px 20px var(--gabriel-chip-glow, rgba(15, 23, 42, 0.12));
|
|
1110
|
+
}
|
|
1111
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-legend-count {
|
|
1112
|
+
background: rgba(15, 23, 42, 0.1);
|
|
1113
|
+
color: rgba(15, 23, 42, 0.75);
|
|
1114
|
+
}
|
|
1115
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-nav-button {
|
|
1116
|
+
background: linear-gradient(135deg, #ffffff, #e0e7ff);
|
|
1117
|
+
border-color: rgba(15, 23, 42, 0.12);
|
|
1118
|
+
color: #1f2933;
|
|
1119
|
+
}
|
|
1120
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-slider-shell {
|
|
1121
|
+
background: rgba(255, 255, 255, 0.9);
|
|
1122
|
+
border-color: rgba(15, 23, 42, 0.12);
|
|
1123
|
+
}
|
|
1124
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-slider::-webkit-slider-runnable-track,
|
|
1125
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-slider::-moz-range-track {
|
|
1126
|
+
background: rgba(15, 23, 42, 0.2);
|
|
1127
|
+
}
|
|
1128
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-slider::-webkit-slider-thumb,
|
|
1129
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-slider::-moz-range-thumb {
|
|
1130
|
+
background: #2563eb;
|
|
1131
|
+
border-color: #ffffff;
|
|
1132
|
+
}
|
|
1133
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-numeric-values {
|
|
1134
|
+
color: rgba(15, 23, 42, 0.7);
|
|
1135
|
+
}
|
|
1136
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-numeric-value-chip {
|
|
1137
|
+
background: rgba(15, 23, 42, 0.08);
|
|
1138
|
+
color: rgba(15, 23, 42, 0.85);
|
|
1139
|
+
}
|
|
1140
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-numeric-slider::before {
|
|
1141
|
+
background: rgba(15, 23, 42, 0.15);
|
|
1142
|
+
}
|
|
1143
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-numeric-slider::after {
|
|
1144
|
+
background: linear-gradient(135deg, rgba(37, 99, 235, 0.85), rgba(14, 165, 233, 0.85));
|
|
1145
|
+
}
|
|
1146
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-range-input::-webkit-slider-thumb,
|
|
1147
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-range-input::-moz-range-thumb {
|
|
1148
|
+
background: #2563eb;
|
|
1149
|
+
border-color: rgba(255, 255, 255, 0.95);
|
|
1150
|
+
}
|
|
1151
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-note {
|
|
1152
|
+
color: rgba(15, 23, 42, 0.6);
|
|
1153
|
+
}
|
|
1154
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-empty {
|
|
1155
|
+
color: rgba(15, 23, 42, 0.55);
|
|
1156
|
+
}
|
|
1157
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-snippet::after {
|
|
1158
|
+
background: rgba(15, 23, 42, 0.92);
|
|
1159
|
+
color: #f8fafc;
|
|
1160
|
+
}
|
|
1161
|
+
.gabriel-codify-viewer:not(.gabriel-theme-dark) .gabriel-snippet-active {
|
|
1162
|
+
box-shadow: 0 0 0 2px rgba(15, 23, 42, 0.28), 0 0 18px rgba(15, 23, 42, 0.3);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
.gabriel-codify-viewer.gabriel-theme-light {
|
|
1166
|
+
color: #1f2933;
|
|
1167
|
+
color-scheme: light;
|
|
1168
|
+
}
|
|
1169
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-passage-panel {
|
|
1170
|
+
background: #f7f9fb;
|
|
1171
|
+
border-color: #d0d7e2;
|
|
1172
|
+
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
|
|
1173
|
+
}
|
|
1174
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-legend {
|
|
1175
|
+
background: #f7f9fb;
|
|
1176
|
+
border-color: #d0d7e2;
|
|
1177
|
+
}
|
|
1178
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-header {
|
|
1179
|
+
background: rgba(15, 23, 42, 0.06);
|
|
1180
|
+
border-color: rgba(15, 23, 42, 0.12);
|
|
1181
|
+
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
|
|
1182
|
+
}
|
|
1183
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-header-row {
|
|
1184
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1185
|
+
border-color: rgba(15, 23, 42, 0.12);
|
|
1186
|
+
}
|
|
1187
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-header-label {
|
|
1188
|
+
color: rgba(15, 23, 42, 0.65);
|
|
1189
|
+
}
|
|
1190
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-header-value {
|
|
1191
|
+
color: rgba(15, 23, 42, 0.92);
|
|
1192
|
+
}
|
|
1193
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-text {
|
|
1194
|
+
color: #1f2933;
|
|
1195
|
+
}
|
|
1196
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-legend-item {
|
|
1197
|
+
background: rgba(15, 23, 42, 0.06);
|
|
1198
|
+
border-color: rgba(15, 23, 42, 0.12);
|
|
1199
|
+
color: rgba(15, 23, 42, 0.82);
|
|
1200
|
+
}
|
|
1201
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-legend-item:hover {
|
|
1202
|
+
background: rgba(15, 23, 42, 0.1);
|
|
1203
|
+
border-color: rgba(15, 23, 42, 0.18);
|
|
1204
|
+
}
|
|
1205
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-legend-item:focus-visible {
|
|
1206
|
+
box-shadow: 0 0 0 2px rgba(0, 188, 212, 0.4);
|
|
1207
|
+
}
|
|
1208
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-legend-count {
|
|
1209
|
+
background: rgba(15, 23, 42, 0.1);
|
|
1210
|
+
color: rgba(15, 23, 42, 0.75);
|
|
1211
|
+
}
|
|
1212
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-legend-item--boolean {
|
|
1213
|
+
background: rgba(15, 23, 42, 0.04);
|
|
1214
|
+
border-color: rgba(15, 23, 42, 0.08);
|
|
1215
|
+
color: rgba(15, 23, 42, 0.82);
|
|
1216
|
+
}
|
|
1217
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-legend-item--boolean .gabriel-legend-swatch {
|
|
1218
|
+
box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.2);
|
|
1219
|
+
}
|
|
1220
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-legend-item--boolean.is-true {
|
|
1221
|
+
background: var(--gabriel-chip-bg, rgba(37, 99, 235, 0.18));
|
|
1222
|
+
border-color: var(--gabriel-chip-border, rgba(37, 99, 235, 0.4));
|
|
1223
|
+
box-shadow: 0 12px 24px var(--gabriel-chip-glow, rgba(37, 99, 235, 0.2));
|
|
1224
|
+
color: rgba(15, 23, 42, 0.92);
|
|
1225
|
+
}
|
|
1226
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-legend-item--boolean.is-true .gabriel-legend-value {
|
|
1227
|
+
background: var(--gabriel-chip-bg, rgba(37, 99, 235, 0.35));
|
|
1228
|
+
color: rgba(15, 23, 42, 0.92);
|
|
1229
|
+
}
|
|
1230
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-legend-item--boolean.is-true .gabriel-legend-swatch {
|
|
1231
|
+
box-shadow: 0 0 12px var(--gabriel-chip-glow, rgba(37, 99, 235, 0.3));
|
|
1232
|
+
}
|
|
1233
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-legend-item--boolean.is-false .gabriel-legend-value {
|
|
1234
|
+
background: rgba(15, 23, 42, 0.08);
|
|
1235
|
+
color: rgba(15, 23, 42, 0.6);
|
|
1236
|
+
}
|
|
1237
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-nav-button {
|
|
1238
|
+
background: linear-gradient(135deg, #ffffff, #e0e7ff);
|
|
1239
|
+
border-color: rgba(15, 23, 42, 0.12);
|
|
1240
|
+
color: #1f2933;
|
|
1241
|
+
}
|
|
1242
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-slider-shell {
|
|
1243
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1244
|
+
border-color: rgba(15, 23, 42, 0.12);
|
|
1245
|
+
}
|
|
1246
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-slider::-webkit-slider-runnable-track,
|
|
1247
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-slider::-moz-range-track {
|
|
1248
|
+
background: rgba(15, 23, 42, 0.2);
|
|
1249
|
+
}
|
|
1250
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-slider::-webkit-slider-thumb,
|
|
1251
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-slider::-moz-range-thumb {
|
|
1252
|
+
background: #2563eb;
|
|
1253
|
+
border-color: #ffffff;
|
|
1254
|
+
}
|
|
1255
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-numeric-values {
|
|
1256
|
+
color: rgba(15, 23, 42, 0.7);
|
|
1257
|
+
}
|
|
1258
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-numeric-value-chip {
|
|
1259
|
+
background: rgba(15, 23, 42, 0.08);
|
|
1260
|
+
color: rgba(15, 23, 42, 0.85);
|
|
1261
|
+
}
|
|
1262
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-numeric-slider::before {
|
|
1263
|
+
background: rgba(15, 23, 42, 0.15);
|
|
1264
|
+
}
|
|
1265
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-numeric-slider::after {
|
|
1266
|
+
background: linear-gradient(135deg, rgba(37, 99, 235, 0.85), rgba(14, 165, 233, 0.85));
|
|
1267
|
+
}
|
|
1268
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-active-label {
|
|
1269
|
+
color: rgba(15, 23, 42, 0.55);
|
|
1270
|
+
}
|
|
1271
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-active-pill {
|
|
1272
|
+
background: rgba(15, 23, 42, 0.06);
|
|
1273
|
+
border-color: rgba(15, 23, 42, 0.15);
|
|
1274
|
+
color: rgba(15, 23, 42, 0.8);
|
|
1275
|
+
}
|
|
1276
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-range-input::-webkit-slider-thumb,
|
|
1277
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-range-input::-moz-range-thumb {
|
|
1278
|
+
background: #2563eb;
|
|
1279
|
+
border-color: rgba(255, 255, 255, 0.95);
|
|
1280
|
+
}
|
|
1281
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-note {
|
|
1282
|
+
color: rgba(15, 23, 42, 0.6);
|
|
1283
|
+
}
|
|
1284
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-empty {
|
|
1285
|
+
color: rgba(15, 23, 42, 0.55);
|
|
1286
|
+
}
|
|
1287
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-snippet::after {
|
|
1288
|
+
background: rgba(15, 23, 42, 0.92);
|
|
1289
|
+
color: #f8fafc;
|
|
1290
|
+
}
|
|
1291
|
+
.gabriel-codify-viewer.gabriel-theme-light .gabriel-snippet-active {
|
|
1292
|
+
box-shadow: 0 0 0 2px rgba(15, 23, 42, 0.28), 0 0 18px rgba(15, 23, 42, 0.3);
|
|
1293
|
+
}
|
|
1294
|
+
</style>
|
|
1295
|
+
<script>
|
|
1296
|
+
(function () {
|
|
1297
|
+
if (window.__gabrielPassageViewerEnhancer) {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
window.__gabrielPassageViewerEnhancer = true;
|
|
1301
|
+
|
|
1302
|
+
const stateMap = new WeakMap();
|
|
1303
|
+
|
|
1304
|
+
function ensureState(container, token) {
|
|
1305
|
+
let record = stateMap.get(container);
|
|
1306
|
+
if (!record || record.token !== token) {
|
|
1307
|
+
record = { token: token, indices: {} };
|
|
1308
|
+
stateMap.set(container, record);
|
|
1309
|
+
}
|
|
1310
|
+
return record;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function escapeSelector(value) {
|
|
1314
|
+
if (window.CSS && typeof window.CSS.escape === 'function') {
|
|
1315
|
+
return window.CSS.escape(value);
|
|
1316
|
+
}
|
|
1317
|
+
return String(value).replace(/[^a-zA-Z0-9_-]/g, '\\$&');
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function bindLegendItem(item) {
|
|
1321
|
+
if (!(item instanceof Element) || item.dataset.gabrielBound === '1') {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const legend = item.closest('.gabriel-legend');
|
|
1325
|
+
const container = item.closest('.gabriel-codify-viewer');
|
|
1326
|
+
if (!legend || !container) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const category = item.getAttribute('data-category');
|
|
1330
|
+
if (!category) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const token = legend.getAttribute('data-legend-token') || '';
|
|
1334
|
+
const state = ensureState(container, token);
|
|
1335
|
+
item.dataset.gabrielBound = '1';
|
|
1336
|
+
item.addEventListener('click', function (event) {
|
|
1337
|
+
event.preventDefault();
|
|
1338
|
+
const selector = '.gabriel-snippet[data-category="' + escapeSelector(category) + '"]';
|
|
1339
|
+
const snippets = container.querySelectorAll(selector);
|
|
1340
|
+
if (!snippets.length) {
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const nextIndex = state.indices[category] || 0;
|
|
1344
|
+
const target = snippets[nextIndex % snippets.length];
|
|
1345
|
+
state.indices[category] = (nextIndex + 1) % snippets.length;
|
|
1346
|
+
container.querySelectorAll('.gabriel-snippet.gabriel-snippet-active').forEach(function (el) {
|
|
1347
|
+
if (el !== target) {
|
|
1348
|
+
el.classList.remove('gabriel-snippet-active');
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
target.classList.add('gabriel-snippet-active');
|
|
1352
|
+
if (typeof target.scrollIntoView === 'function') {
|
|
1353
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1354
|
+
}
|
|
1355
|
+
window.setTimeout(function () {
|
|
1356
|
+
target.classList.remove('gabriel-snippet-active');
|
|
1357
|
+
}, 1600);
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function scan(root) {
|
|
1362
|
+
if (!(root instanceof Element)) {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
root.querySelectorAll('.gabriel-legend-item').forEach(bindLegendItem);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const observer = new MutationObserver(function (mutations) {
|
|
1369
|
+
mutations.forEach(function (mutation) {
|
|
1370
|
+
mutation.addedNodes.forEach(function (node) {
|
|
1371
|
+
if (!(node instanceof Element)) {
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
if (node.classList.contains('gabriel-legend-item')) {
|
|
1375
|
+
bindLegendItem(node);
|
|
1376
|
+
} else {
|
|
1377
|
+
scan(node);
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
if (document && document.body) {
|
|
1384
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
1385
|
+
scan(document.body);
|
|
1386
|
+
}
|
|
1387
|
+
})();
|
|
1388
|
+
</script>
|
|
1389
|
+
"""
|
|
1390
|
+
|
|
1391
|
+
def _build_style_overrides(
|
|
1392
|
+
font_scale: float = 1.0,
|
|
1393
|
+
font_family: Optional[str] = None,
|
|
1394
|
+
color_mode: str = "auto",
|
|
1395
|
+
) -> str:
|
|
1396
|
+
"""Return additional CSS overrides based on user preferences."""
|
|
1397
|
+
|
|
1398
|
+
fragments: List[str] = []
|
|
1399
|
+
|
|
1400
|
+
try:
|
|
1401
|
+
scale = float(font_scale)
|
|
1402
|
+
except (TypeError, ValueError):
|
|
1403
|
+
scale = 1.0
|
|
1404
|
+
if not math.isfinite(scale):
|
|
1405
|
+
scale = 1.0
|
|
1406
|
+
scale = max(0.6, min(2.5, scale))
|
|
1407
|
+
|
|
1408
|
+
if not math.isclose(scale, 1.0, rel_tol=1e-3):
|
|
1409
|
+
for selector, base_size in _FONT_SIZE_OVERRIDES.items():
|
|
1410
|
+
scaled = max(8.0, min(48.0, base_size * scale))
|
|
1411
|
+
fragments.append(f"{selector} {{ font-size: {scaled:.2f}px; }}")
|
|
1412
|
+
|
|
1413
|
+
if font_family:
|
|
1414
|
+
preferred = str(font_family).strip()
|
|
1415
|
+
if preferred:
|
|
1416
|
+
sanitized = preferred.replace("'", "\\'")
|
|
1417
|
+
fragments.append(
|
|
1418
|
+
".gabriel-codify-viewer { font-family: '"
|
|
1419
|
+
+ sanitized
|
|
1420
|
+
+ "', 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif; }"
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
if color_mode == "light":
|
|
1424
|
+
fragments.append(".gabriel-codify-viewer { color-scheme: light; }")
|
|
1425
|
+
elif color_mode == "dark":
|
|
1426
|
+
fragments.append(".gabriel-codify-viewer { color-scheme: dark; }")
|
|
1427
|
+
|
|
1428
|
+
if not fragments:
|
|
1429
|
+
return ""
|
|
1430
|
+
|
|
1431
|
+
return "<style>" + "".join(fragments) + "</style>"
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
def _normalize_header_columns(
|
|
1435
|
+
header_columns: Optional[Union[Sequence[Any], Any]]
|
|
1436
|
+
) -> List[Tuple[str, str]]:
|
|
1437
|
+
if header_columns is None:
|
|
1438
|
+
return []
|
|
1439
|
+
|
|
1440
|
+
if isinstance(header_columns, Mapping):
|
|
1441
|
+
header_sequence: Iterable[Any] = header_columns.items()
|
|
1442
|
+
elif isinstance(header_columns, (str, bytes)):
|
|
1443
|
+
header_sequence = [header_columns]
|
|
1444
|
+
elif isinstance(header_columns, Iterable):
|
|
1445
|
+
header_sequence = header_columns
|
|
1446
|
+
else:
|
|
1447
|
+
header_sequence = [header_columns]
|
|
1448
|
+
|
|
1449
|
+
normalized: List[Tuple[str, str]] = []
|
|
1450
|
+
for entry in header_sequence:
|
|
1451
|
+
if isinstance(entry, tuple) and entry:
|
|
1452
|
+
column = str(entry[0])
|
|
1453
|
+
label = str(entry[1]) if len(entry) > 1 else column
|
|
1454
|
+
elif isinstance(entry, list) and entry:
|
|
1455
|
+
column = str(entry[0])
|
|
1456
|
+
label = str(entry[1]) if len(entry) > 1 else column
|
|
1457
|
+
else:
|
|
1458
|
+
column = str(entry)
|
|
1459
|
+
label = column
|
|
1460
|
+
pretty_label = label.replace("_", " ").title()
|
|
1461
|
+
normalized.append((column, pretty_label))
|
|
1462
|
+
return normalized
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
def _is_na(value: Any) -> bool:
|
|
1466
|
+
if value is None:
|
|
1467
|
+
return True
|
|
1468
|
+
try:
|
|
1469
|
+
result = pd.isna(value)
|
|
1470
|
+
except Exception:
|
|
1471
|
+
return False
|
|
1472
|
+
if isinstance(result, bool):
|
|
1473
|
+
return result
|
|
1474
|
+
return False
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
def _parse_structured_cell(value: Any) -> Any:
|
|
1478
|
+
"""Attempt to parse serialized list/dict values from CSV/JSON sources."""
|
|
1479
|
+
|
|
1480
|
+
if not isinstance(value, str):
|
|
1481
|
+
return value
|
|
1482
|
+
|
|
1483
|
+
stripped = value.strip()
|
|
1484
|
+
if not stripped:
|
|
1485
|
+
return []
|
|
1486
|
+
|
|
1487
|
+
lowered = stripped.lower()
|
|
1488
|
+
if lowered in {"nan", "none", "null", "n/a"}:
|
|
1489
|
+
return None
|
|
1490
|
+
|
|
1491
|
+
for loader in (json.loads, ast.literal_eval):
|
|
1492
|
+
try:
|
|
1493
|
+
return loader(stripped)
|
|
1494
|
+
except Exception:
|
|
1495
|
+
continue
|
|
1496
|
+
|
|
1497
|
+
return value
|
|
1498
|
+
|
|
1499
|
+
|
|
1500
|
+
def _coerce_snippet_list(value: Any) -> List[str]:
|
|
1501
|
+
parsed = _parse_structured_cell(value)
|
|
1502
|
+
|
|
1503
|
+
if _is_na(parsed) or parsed is None:
|
|
1504
|
+
return []
|
|
1505
|
+
|
|
1506
|
+
if isinstance(parsed, str):
|
|
1507
|
+
return [parsed] if parsed.strip() else []
|
|
1508
|
+
|
|
1509
|
+
if isinstance(parsed, (list, tuple, set)):
|
|
1510
|
+
snippets: List[str] = []
|
|
1511
|
+
for item in parsed:
|
|
1512
|
+
if _is_na(item) or item is None:
|
|
1513
|
+
continue
|
|
1514
|
+
text = str(item)
|
|
1515
|
+
if text.strip():
|
|
1516
|
+
snippets.append(text)
|
|
1517
|
+
return snippets
|
|
1518
|
+
|
|
1519
|
+
text = str(parsed)
|
|
1520
|
+
return [text] if text.strip() else []
|
|
1521
|
+
|
|
1522
|
+
|
|
1523
|
+
def _coerce_coded_passage_map(value: Any) -> Dict[str, List[str]]:
|
|
1524
|
+
parsed = _parse_structured_cell(value)
|
|
1525
|
+
|
|
1526
|
+
if _is_na(parsed) or parsed is None:
|
|
1527
|
+
return {}
|
|
1528
|
+
|
|
1529
|
+
if isinstance(parsed, dict):
|
|
1530
|
+
normalized: Dict[str, List[str]] = {}
|
|
1531
|
+
for key, snippets in parsed.items():
|
|
1532
|
+
if _is_na(key) or key is None:
|
|
1533
|
+
continue
|
|
1534
|
+
cat = str(key)
|
|
1535
|
+
normalized[cat] = _coerce_snippet_list(snippets)
|
|
1536
|
+
return normalized
|
|
1537
|
+
|
|
1538
|
+
if isinstance(parsed, (list, tuple)):
|
|
1539
|
+
aggregated: Dict[str, List[str]] = {}
|
|
1540
|
+
for entry in parsed:
|
|
1541
|
+
if isinstance(entry, dict):
|
|
1542
|
+
for key, snippets in entry.items():
|
|
1543
|
+
cat = str(key)
|
|
1544
|
+
aggregated.setdefault(cat, []).extend(_coerce_snippet_list(snippets))
|
|
1545
|
+
return aggregated
|
|
1546
|
+
|
|
1547
|
+
return {}
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
def _normalize_structured_dataframe(
|
|
1551
|
+
df: pd.DataFrame,
|
|
1552
|
+
categories: Optional[Union[List[str], str]],
|
|
1553
|
+
) -> pd.DataFrame:
|
|
1554
|
+
if "coded_passages" in df.columns:
|
|
1555
|
+
df["coded_passages"] = df["coded_passages"].apply(_coerce_coded_passage_map)
|
|
1556
|
+
|
|
1557
|
+
category_columns: Iterable[str]
|
|
1558
|
+
if categories is None or categories == "coded_passages":
|
|
1559
|
+
category_columns = []
|
|
1560
|
+
else:
|
|
1561
|
+
category_columns = categories
|
|
1562
|
+
|
|
1563
|
+
for column in category_columns:
|
|
1564
|
+
if column in df.columns:
|
|
1565
|
+
df[column] = df[column].apply(_coerce_snippet_list)
|
|
1566
|
+
|
|
1567
|
+
return df
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
def _extract_categories_from_coded_passages(df: pd.DataFrame) -> List[str]:
|
|
1571
|
+
if "coded_passages" not in df.columns:
|
|
1572
|
+
return []
|
|
1573
|
+
|
|
1574
|
+
all_categories = set()
|
|
1575
|
+
for entry in df["coded_passages"]:
|
|
1576
|
+
if isinstance(entry, dict):
|
|
1577
|
+
for key in entry.keys():
|
|
1578
|
+
all_categories.add(str(key))
|
|
1579
|
+
|
|
1580
|
+
return sorted(all_categories)
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def _format_header_value(value: Any) -> str:
|
|
1584
|
+
if _is_na(value):
|
|
1585
|
+
return ""
|
|
1586
|
+
|
|
1587
|
+
if isinstance(value, str):
|
|
1588
|
+
stripped = value.strip()
|
|
1589
|
+
if not stripped:
|
|
1590
|
+
return ""
|
|
1591
|
+
parsed = _parse_structured_cell(value)
|
|
1592
|
+
if isinstance(parsed, dict) and not parsed:
|
|
1593
|
+
return ""
|
|
1594
|
+
if isinstance(parsed, (list, tuple, set)) and not parsed:
|
|
1595
|
+
return ""
|
|
1596
|
+
return stripped
|
|
1597
|
+
if isinstance(value, (list, tuple, set)):
|
|
1598
|
+
parts = [str(item).strip() for item in value if str(item).strip()]
|
|
1599
|
+
return ", ".join(parts)
|
|
1600
|
+
return str(value)
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
def _build_highlighted_text(
|
|
1604
|
+
text: str,
|
|
1605
|
+
snippet_map: Dict[str, List[str]],
|
|
1606
|
+
category_colors: Dict[str, str],
|
|
1607
|
+
category_labels: Dict[str, str],
|
|
1608
|
+
) -> str:
|
|
1609
|
+
if not text:
|
|
1610
|
+
return "<div class='gabriel-empty'>No text available.</div>"
|
|
1611
|
+
|
|
1612
|
+
spans: List[Tuple[int, int, str]] = []
|
|
1613
|
+
for category, snippets in snippet_map.items():
|
|
1614
|
+
if not snippets or category not in category_colors:
|
|
1615
|
+
continue
|
|
1616
|
+
for snippet in snippets:
|
|
1617
|
+
if not snippet:
|
|
1618
|
+
continue
|
|
1619
|
+
start = 0
|
|
1620
|
+
while True:
|
|
1621
|
+
index = text.find(snippet, start)
|
|
1622
|
+
if index == -1:
|
|
1623
|
+
break
|
|
1624
|
+
spans.append((index, index + len(snippet), category))
|
|
1625
|
+
start = index + len(snippet)
|
|
1626
|
+
|
|
1627
|
+
if not spans:
|
|
1628
|
+
return html.escape(text).replace("\n", "<br/>")
|
|
1629
|
+
|
|
1630
|
+
spans.sort(key=lambda x: (x[0], -(x[1] - x[0])))
|
|
1631
|
+
merged: List[Tuple[int, int, str]] = []
|
|
1632
|
+
current_end = -1
|
|
1633
|
+
for start, end, category in spans:
|
|
1634
|
+
if start < current_end:
|
|
1635
|
+
continue
|
|
1636
|
+
merged.append((start, end, category))
|
|
1637
|
+
current_end = end
|
|
1638
|
+
|
|
1639
|
+
pieces: List[str] = []
|
|
1640
|
+
cursor = 0
|
|
1641
|
+
snippet_indices: Dict[str, int] = {}
|
|
1642
|
+
for start, end, category in merged:
|
|
1643
|
+
pieces.append(html.escape(text[cursor:start]).replace("\n", "<br/>"))
|
|
1644
|
+
snippet_html = html.escape(text[start:end]).replace("\n", "<br/>")
|
|
1645
|
+
category_key = str(category)
|
|
1646
|
+
label_source = category_labels.get(category_key, category_key)
|
|
1647
|
+
label = html.escape(label_source.replace("_", " ").title())
|
|
1648
|
+
color = category_colors.get(category_key, "#ffd54f")
|
|
1649
|
+
safe_color = html.escape(color, quote=True)
|
|
1650
|
+
safe_category = html.escape(category_key, quote=True)
|
|
1651
|
+
index = snippet_indices.get(category_key, 0)
|
|
1652
|
+
snippet_indices[category_key] = index + 1
|
|
1653
|
+
slug = re.sub(r"[^0-9a-zA-Z_-]+", "-", category_key).strip("-")
|
|
1654
|
+
if not slug:
|
|
1655
|
+
slug = "category"
|
|
1656
|
+
element_id = f"gabriel-snippet-{slug}-{index}"
|
|
1657
|
+
pieces.append(
|
|
1658
|
+
"<span class='gabriel-snippet' "
|
|
1659
|
+
f"data-category='{safe_category}' data-index='{index}' "
|
|
1660
|
+
f"data-label='{label}' id='{element_id}' "
|
|
1661
|
+
f"style='background-color:{safe_color}' title='{label}'>"
|
|
1662
|
+
f"{snippet_html}</span>"
|
|
1663
|
+
)
|
|
1664
|
+
cursor = end
|
|
1665
|
+
pieces.append(html.escape(text[cursor:]).replace("\n", "<br/>"))
|
|
1666
|
+
return "".join(pieces)
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
def _build_header_html(
|
|
1670
|
+
header_rows: List[Tuple[str, str]],
|
|
1671
|
+
active_categories: List[str],
|
|
1672
|
+
) -> str:
|
|
1673
|
+
if not header_rows and not active_categories:
|
|
1674
|
+
return ""
|
|
1675
|
+
|
|
1676
|
+
card_rows: List[str] = []
|
|
1677
|
+
for label, value in header_rows:
|
|
1678
|
+
safe_label = html.escape(label)
|
|
1679
|
+
safe_value = html.escape(value).replace("\n", "<br/>")
|
|
1680
|
+
card_rows.append(
|
|
1681
|
+
f"<div class='gabriel-header-row'>"
|
|
1682
|
+
f"<span class='gabriel-header-label'>{safe_label}</span>"
|
|
1683
|
+
f"<span class='gabriel-header-value'>{safe_value}</span>"
|
|
1684
|
+
f"</div>"
|
|
1685
|
+
)
|
|
1686
|
+
|
|
1687
|
+
sections: List[str] = []
|
|
1688
|
+
if card_rows:
|
|
1689
|
+
sections.append(
|
|
1690
|
+
"<div class='gabriel-header-grid'>" + "".join(card_rows) + "</div>"
|
|
1691
|
+
)
|
|
1692
|
+
|
|
1693
|
+
if active_categories:
|
|
1694
|
+
pills = "".join(
|
|
1695
|
+
"<span class='gabriel-active-pill'>"
|
|
1696
|
+
+ html.escape(cat.replace("_", " ").title())
|
|
1697
|
+
+ "</span>"
|
|
1698
|
+
for cat in active_categories
|
|
1699
|
+
)
|
|
1700
|
+
sections.append(
|
|
1701
|
+
"<div class='gabriel-active-cats'>"
|
|
1702
|
+
"<span class='gabriel-active-label'>Categories</span>"
|
|
1703
|
+
f"<div class='gabriel-active-pill-stack'>{pills}</div>"
|
|
1704
|
+
"</div>"
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
return "<div class='gabriel-header'>" + "".join(sections) + "</div>"
|
|
1708
|
+
|
|
1709
|
+
|
|
1710
|
+
def _build_note_html(notes: Optional[Sequence[Any]]) -> str:
|
|
1711
|
+
"""Render helper notes such as truncation or text-only warnings."""
|
|
1712
|
+
|
|
1713
|
+
if not notes:
|
|
1714
|
+
return ""
|
|
1715
|
+
|
|
1716
|
+
fragments: List[str] = []
|
|
1717
|
+
for message in notes:
|
|
1718
|
+
if message is None:
|
|
1719
|
+
continue
|
|
1720
|
+
text = str(message).strip()
|
|
1721
|
+
if not text:
|
|
1722
|
+
continue
|
|
1723
|
+
fragments.append(
|
|
1724
|
+
f"<div class='gabriel-note'>{html.escape(text)}</div>"
|
|
1725
|
+
)
|
|
1726
|
+
|
|
1727
|
+
return "".join(fragments)
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def _build_legend_html(
|
|
1731
|
+
category_colors: Dict[str, str],
|
|
1732
|
+
category_counts: Dict[str, int],
|
|
1733
|
+
category_labels: Dict[str, str],
|
|
1734
|
+
legend_token: Optional[str] = None,
|
|
1735
|
+
*,
|
|
1736
|
+
boolean_specs: Optional[Sequence[_AttributeSpec]] = None,
|
|
1737
|
+
boolean_values: Optional[Mapping[str, Optional[bool]]] = None,
|
|
1738
|
+
numeric_specs: Optional[Sequence[_AttributeSpec]] = None,
|
|
1739
|
+
numeric_values: Optional[Mapping[str, Optional[float]]] = None,
|
|
1740
|
+
boolean_colors: Optional[Mapping[str, str]] = None,
|
|
1741
|
+
numeric_colors: Optional[Mapping[str, str]] = None,
|
|
1742
|
+
numeric_ranges: Optional[
|
|
1743
|
+
Mapping[str, Tuple[Optional[float], Optional[float]]]
|
|
1744
|
+
] = None,
|
|
1745
|
+
) -> str:
|
|
1746
|
+
if not category_colors and not boolean_specs and not numeric_specs:
|
|
1747
|
+
return ""
|
|
1748
|
+
|
|
1749
|
+
boolean_values = boolean_values or {}
|
|
1750
|
+
numeric_values = numeric_values or {}
|
|
1751
|
+
boolean_colors = boolean_colors or {}
|
|
1752
|
+
numeric_colors = numeric_colors or {}
|
|
1753
|
+
numeric_ranges = numeric_ranges or {}
|
|
1754
|
+
|
|
1755
|
+
def _format_bool(value: Optional[bool]) -> str:
|
|
1756
|
+
if value is True:
|
|
1757
|
+
return "Yes"
|
|
1758
|
+
if value is False:
|
|
1759
|
+
return "No"
|
|
1760
|
+
return "—"
|
|
1761
|
+
|
|
1762
|
+
def _format_numeric(value: Optional[float]) -> str:
|
|
1763
|
+
if value is None:
|
|
1764
|
+
return "—"
|
|
1765
|
+
return _format_numeric_chip(value)
|
|
1766
|
+
|
|
1767
|
+
items = []
|
|
1768
|
+
for category, color in category_colors.items():
|
|
1769
|
+
pretty = category_labels.get(category, category).replace("_", " ").title()
|
|
1770
|
+
label = html.escape(pretty)
|
|
1771
|
+
raw_count = category_counts.get(category, 0)
|
|
1772
|
+
try:
|
|
1773
|
+
count_value = int(raw_count)
|
|
1774
|
+
except (TypeError, ValueError):
|
|
1775
|
+
count_value = 0
|
|
1776
|
+
aria_label = html.escape(f"{pretty} ({count_value})", quote=True)
|
|
1777
|
+
count = html.escape(str(count_value))
|
|
1778
|
+
safe_color = html.escape(color, quote=True)
|
|
1779
|
+
safe_category = html.escape(category, quote=True)
|
|
1780
|
+
chip_style = _build_chip_style_tokens(color)
|
|
1781
|
+
style_attr = (
|
|
1782
|
+
f" style=\"{html.escape(chip_style, quote=True)}\""
|
|
1783
|
+
if chip_style
|
|
1784
|
+
else ""
|
|
1785
|
+
)
|
|
1786
|
+
items.append(
|
|
1787
|
+
"<button type='button' class='gabriel-legend-item gabriel-legend-item--snippet' "
|
|
1788
|
+
f"data-category='{safe_category}' data-count='{count}' aria-label='{aria_label}'{style_attr}>"
|
|
1789
|
+
f"<span class='gabriel-legend-color' style='background:{safe_color}'></span>"
|
|
1790
|
+
f"<span class='gabriel-legend-label'>{label}</span>"
|
|
1791
|
+
f"<span class='gabriel-legend-count'>{count}</span>"
|
|
1792
|
+
"</button>"
|
|
1793
|
+
)
|
|
1794
|
+
|
|
1795
|
+
for spec in boolean_specs or ():
|
|
1796
|
+
column = spec.column
|
|
1797
|
+
label_text = spec.label or column.replace("_", " ").title()
|
|
1798
|
+
label = html.escape(label_text)
|
|
1799
|
+
value = boolean_values.get(column)
|
|
1800
|
+
display = html.escape(_format_bool(value))
|
|
1801
|
+
aria_label = html.escape(f"{label_text}: {display}", quote=True)
|
|
1802
|
+
safe_column = html.escape(column, quote=True)
|
|
1803
|
+
state_class = " is-true" if value is True else (" is-false" if value is False else "")
|
|
1804
|
+
chip_style = _build_chip_style_tokens(boolean_colors.get(column))
|
|
1805
|
+
style_attr = (
|
|
1806
|
+
f" style=\"{html.escape(chip_style, quote=True)}\""
|
|
1807
|
+
if chip_style
|
|
1808
|
+
else ""
|
|
1809
|
+
)
|
|
1810
|
+
items.append(
|
|
1811
|
+
"<button type='button' class='gabriel-legend-item gabriel-legend-item--boolean"
|
|
1812
|
+
f"{state_class}' data-boolean='{safe_column}' aria-pressed='false' aria-label='{aria_label}'{style_attr}>"
|
|
1813
|
+
"<span class='gabriel-legend-swatch' aria-hidden='true'></span>"
|
|
1814
|
+
f"<span class='gabriel-legend-label'>{label}</span>"
|
|
1815
|
+
f"<span class='gabriel-legend-value'>{display}</span>"
|
|
1816
|
+
"</button>"
|
|
1817
|
+
)
|
|
1818
|
+
|
|
1819
|
+
for spec in numeric_specs or ():
|
|
1820
|
+
column = spec.column
|
|
1821
|
+
label_text = spec.label or column.replace("_", " ").title()
|
|
1822
|
+
label = html.escape(label_text)
|
|
1823
|
+
value = numeric_values.get(column)
|
|
1824
|
+
display = html.escape(_format_numeric(value))
|
|
1825
|
+
aria_label = html.escape(f"{label_text}: {display}", quote=True)
|
|
1826
|
+
safe_column = html.escape(column, quote=True)
|
|
1827
|
+
bounds = numeric_ranges.get(column)
|
|
1828
|
+
intensity = _compute_numeric_intensity(value, bounds)
|
|
1829
|
+
chip_style = _build_chip_style_tokens(
|
|
1830
|
+
numeric_colors.get(column), intensity=intensity
|
|
1831
|
+
)
|
|
1832
|
+
style_attr = (
|
|
1833
|
+
f" style=\"{html.escape(chip_style, quote=True)}\""
|
|
1834
|
+
if chip_style
|
|
1835
|
+
else ""
|
|
1836
|
+
)
|
|
1837
|
+
items.append(
|
|
1838
|
+
"<button type='button' class='gabriel-legend-item gabriel-legend-item--numeric' "
|
|
1839
|
+
f"data-numeric='{safe_column}' aria-pressed='false' aria-label='{aria_label}'{style_attr}>"
|
|
1840
|
+
f"<span class='gabriel-legend-label'>{label}</span>"
|
|
1841
|
+
f"<span class='gabriel-legend-value'>{display}</span>"
|
|
1842
|
+
"<span class='gabriel-sort-indicator' aria-hidden='true'></span>"
|
|
1843
|
+
"</button>"
|
|
1844
|
+
)
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
token_attr = (
|
|
1848
|
+
f" data-legend-token='{html.escape(legend_token, quote=True)}'"
|
|
1849
|
+
if legend_token
|
|
1850
|
+
else ""
|
|
1851
|
+
)
|
|
1852
|
+
return (
|
|
1853
|
+
f"<div class='gabriel-legend'{token_attr}><div class='gabriel-legend-grid'>"
|
|
1854
|
+
+ "".join(items)
|
|
1855
|
+
+ "</div></div>"
|
|
1856
|
+
)
|
|
1857
|
+
|
|
1858
|
+
|
|
1859
|
+
def _render_passage_viewer(
|
|
1860
|
+
df: pd.DataFrame,
|
|
1861
|
+
column_name: str,
|
|
1862
|
+
attributes: Optional[Union[Mapping[str, Any], Sequence[Any], Any]] = None,
|
|
1863
|
+
header_columns: Optional[Union[Sequence[Any], Any]] = None,
|
|
1864
|
+
*,
|
|
1865
|
+
max_passages: Optional[int] = None,
|
|
1866
|
+
font_scale: float = 1.0,
|
|
1867
|
+
font_family: Optional[str] = None,
|
|
1868
|
+
color_mode: str = "auto",
|
|
1869
|
+
) -> None:
|
|
1870
|
+
"""Display passages inside a Jupyter notebook."""
|
|
1871
|
+
|
|
1872
|
+
from IPython.display import HTML, display # pragma: no cover - optional
|
|
1873
|
+
|
|
1874
|
+
df = df.copy()
|
|
1875
|
+
attribute_requests = _normalize_attribute_requests(attributes)
|
|
1876
|
+
if attributes is None and "coded_passages" in df.columns:
|
|
1877
|
+
if not any(req.column == "coded_passages" for req in attribute_requests):
|
|
1878
|
+
attribute_requests.append(
|
|
1879
|
+
_AttributeRequest("coded_passages", "Coded Passages", dynamic=True)
|
|
1880
|
+
)
|
|
1881
|
+
if "overall_rank" in df.columns and not any(
|
|
1882
|
+
req.column == "overall_rank" for req in attribute_requests
|
|
1883
|
+
):
|
|
1884
|
+
attribute_requests.insert(
|
|
1885
|
+
0, _AttributeRequest("overall_rank", "Overall Rank")
|
|
1886
|
+
)
|
|
1887
|
+
df, attribute_requests = _expand_mapping_attribute_requests(df, attribute_requests)
|
|
1888
|
+
|
|
1889
|
+
attribute_specs: List[_AttributeSpec] = []
|
|
1890
|
+
boolean_specs: List[_AttributeSpec] = []
|
|
1891
|
+
numeric_specs: List[_AttributeSpec] = []
|
|
1892
|
+
snippet_columns: List[str] = []
|
|
1893
|
+
for request in attribute_requests:
|
|
1894
|
+
if request.dynamic:
|
|
1895
|
+
attribute_specs.append(
|
|
1896
|
+
_AttributeSpec(
|
|
1897
|
+
request.column,
|
|
1898
|
+
request.label,
|
|
1899
|
+
"snippet",
|
|
1900
|
+
dynamic=True,
|
|
1901
|
+
description=request.description,
|
|
1902
|
+
)
|
|
1903
|
+
)
|
|
1904
|
+
continue
|
|
1905
|
+
series = df[request.column] if request.column in df.columns else None
|
|
1906
|
+
kind = _infer_attribute_kind(series)
|
|
1907
|
+
spec = _AttributeSpec(
|
|
1908
|
+
request.column,
|
|
1909
|
+
request.label,
|
|
1910
|
+
kind,
|
|
1911
|
+
dynamic=False,
|
|
1912
|
+
description=request.description,
|
|
1913
|
+
)
|
|
1914
|
+
attribute_specs.append(spec)
|
|
1915
|
+
if kind == "snippet":
|
|
1916
|
+
snippet_columns.append(request.column)
|
|
1917
|
+
elif kind == "boolean":
|
|
1918
|
+
boolean_specs.append(spec)
|
|
1919
|
+
elif kind == "numeric":
|
|
1920
|
+
numeric_specs.append(spec)
|
|
1921
|
+
|
|
1922
|
+
df = _normalize_structured_dataframe(df, snippet_columns)
|
|
1923
|
+
normalized_headers = _normalize_header_columns(header_columns)
|
|
1924
|
+
|
|
1925
|
+
numeric_bounds: Dict[str, List[Optional[float]]] = {
|
|
1926
|
+
spec.column: [None, None] for spec in numeric_specs
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
has_dynamic = any(spec.dynamic for spec in attribute_specs)
|
|
1930
|
+
text_only_mode = not attribute_specs
|
|
1931
|
+
category_names: List[str] = []
|
|
1932
|
+
category_labels: Dict[str, str] = {}
|
|
1933
|
+
if has_dynamic:
|
|
1934
|
+
for cat in _extract_categories_from_coded_passages(df):
|
|
1935
|
+
if cat not in category_labels:
|
|
1936
|
+
category_names.append(cat)
|
|
1937
|
+
category_labels[cat] = cat
|
|
1938
|
+
for spec in attribute_specs:
|
|
1939
|
+
if spec.kind == "snippet" and not spec.dynamic:
|
|
1940
|
+
if spec.column not in category_labels:
|
|
1941
|
+
category_names.append(spec.column)
|
|
1942
|
+
category_labels[spec.column] = spec.label
|
|
1943
|
+
|
|
1944
|
+
total_color_targets = len(category_names) + len(boolean_specs) + len(numeric_specs)
|
|
1945
|
+
palette = _generate_distinct_colors(total_color_targets)
|
|
1946
|
+
palette_iter = iter(palette)
|
|
1947
|
+
category_colors = {
|
|
1948
|
+
cat: next(palette_iter, "#ffd54f")
|
|
1949
|
+
for cat in category_names
|
|
1950
|
+
}
|
|
1951
|
+
boolean_colors: Dict[str, str] = {
|
|
1952
|
+
spec.column: next(palette_iter, "#80cbc4")
|
|
1953
|
+
for spec in boolean_specs
|
|
1954
|
+
}
|
|
1955
|
+
numeric_colors: Dict[str, str] = {
|
|
1956
|
+
spec.column: next(palette_iter, "#ffe082")
|
|
1957
|
+
for spec in numeric_specs
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
passages: List[Dict[str, Any]] = []
|
|
1961
|
+
for _, row in df.iterrows():
|
|
1962
|
+
raw_text = row.get(column_name)
|
|
1963
|
+
text = "" if _is_na(raw_text) else str(raw_text)
|
|
1964
|
+
snippet_map: Dict[str, List[str]] = {cat: [] for cat in category_names}
|
|
1965
|
+
bool_values: Dict[str, Optional[bool]] = {}
|
|
1966
|
+
numeric_values: Dict[str, Optional[float]] = {}
|
|
1967
|
+
if has_dynamic:
|
|
1968
|
+
raw_map = row.get("coded_passages")
|
|
1969
|
+
if isinstance(raw_map, dict):
|
|
1970
|
+
for cat, snippets in raw_map.items():
|
|
1971
|
+
cat_key = str(cat)
|
|
1972
|
+
if cat_key in snippet_map:
|
|
1973
|
+
snippet_map[cat_key] = _coerce_snippet_list(snippets)
|
|
1974
|
+
|
|
1975
|
+
for spec in attribute_specs:
|
|
1976
|
+
if spec.dynamic or spec.kind != "snippet":
|
|
1977
|
+
continue
|
|
1978
|
+
cat_key = spec.column
|
|
1979
|
+
if cat_key in snippet_map:
|
|
1980
|
+
snippet_map[cat_key] = _coerce_snippet_list(row.get(cat_key, []))
|
|
1981
|
+
|
|
1982
|
+
header_rows: List[Tuple[str, str]] = []
|
|
1983
|
+
for column, label in normalized_headers:
|
|
1984
|
+
value = row.get(column)
|
|
1985
|
+
formatted = _format_header_value(value)
|
|
1986
|
+
if formatted:
|
|
1987
|
+
header_rows.append((label, formatted))
|
|
1988
|
+
|
|
1989
|
+
text_attributes: List[Tuple[str, str]] = []
|
|
1990
|
+
for spec in attribute_specs:
|
|
1991
|
+
if spec.dynamic or spec.kind == "snippet":
|
|
1992
|
+
continue
|
|
1993
|
+
value = row.get(spec.column)
|
|
1994
|
+
if spec.kind == "boolean":
|
|
1995
|
+
bool_value = _coerce_bool_value(value)
|
|
1996
|
+
bool_values[spec.column] = bool_value
|
|
1997
|
+
elif spec.kind == "numeric":
|
|
1998
|
+
numeric_value = _coerce_numeric_value(value)
|
|
1999
|
+
numeric_values[spec.column] = numeric_value
|
|
2000
|
+
if numeric_value is not None:
|
|
2001
|
+
bounds = numeric_bounds.setdefault(spec.column, [None, None])
|
|
2002
|
+
lower, upper = bounds
|
|
2003
|
+
if lower is None or numeric_value < lower:
|
|
2004
|
+
bounds[0] = numeric_value
|
|
2005
|
+
if upper is None or numeric_value > upper:
|
|
2006
|
+
bounds[1] = numeric_value
|
|
2007
|
+
else:
|
|
2008
|
+
formatted = _format_header_value(value)
|
|
2009
|
+
if formatted:
|
|
2010
|
+
label = spec.label or spec.column.replace("_", " ").title()
|
|
2011
|
+
text_attributes.append((label, formatted))
|
|
2012
|
+
|
|
2013
|
+
if text_attributes:
|
|
2014
|
+
header_rows.extend(text_attributes)
|
|
2015
|
+
|
|
2016
|
+
active_categories = [cat for cat, snippets in snippet_map.items() if snippets]
|
|
2017
|
+
passage_counts = {
|
|
2018
|
+
cat: len(snippet_map.get(cat, []))
|
|
2019
|
+
for cat in category_names
|
|
2020
|
+
}
|
|
2021
|
+
passages.append(
|
|
2022
|
+
{
|
|
2023
|
+
"text": text,
|
|
2024
|
+
"snippets": snippet_map,
|
|
2025
|
+
"header": header_rows,
|
|
2026
|
+
"active": active_categories,
|
|
2027
|
+
"counts": passage_counts,
|
|
2028
|
+
"bools": bool_values,
|
|
2029
|
+
"numeric": numeric_values,
|
|
2030
|
+
}
|
|
2031
|
+
)
|
|
2032
|
+
|
|
2033
|
+
numeric_ranges: Dict[str, Tuple[Optional[float], Optional[float]]] = {
|
|
2034
|
+
column: (bounds[0], bounds[1])
|
|
2035
|
+
for column, bounds in numeric_bounds.items()
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
color_choice = str(color_mode or "auto").lower()
|
|
2039
|
+
if color_choice not in {"auto", "dark", "light"}:
|
|
2040
|
+
color_choice = "auto"
|
|
2041
|
+
theme_class = (
|
|
2042
|
+
" gabriel-theme-light"
|
|
2043
|
+
if color_choice == "light"
|
|
2044
|
+
else (" gabriel-theme-dark" if color_choice == "dark" else "")
|
|
2045
|
+
)
|
|
2046
|
+
|
|
2047
|
+
style_html = _COLAB_STYLE + _build_style_overrides(
|
|
2048
|
+
font_scale=font_scale,
|
|
2049
|
+
font_family=font_family,
|
|
2050
|
+
color_mode=color_choice,
|
|
2051
|
+
)
|
|
2052
|
+
|
|
2053
|
+
original_total = len(passages)
|
|
2054
|
+
limit = max_passages
|
|
2055
|
+
trunc_note: Optional[str] = None
|
|
2056
|
+
if limit is not None and limit >= 0 and original_total > limit:
|
|
2057
|
+
trunc_note = f"Showing first {limit} of {original_total} passages."
|
|
2058
|
+
passages = passages[:limit]
|
|
2059
|
+
|
|
2060
|
+
total = len(passages)
|
|
2061
|
+
note_messages: List[str] = []
|
|
2062
|
+
if text_only_mode:
|
|
2063
|
+
note_messages.append(
|
|
2064
|
+
"Displaying raw text because no attribute columns were provided."
|
|
2065
|
+
)
|
|
2066
|
+
if trunc_note:
|
|
2067
|
+
note_messages.append(trunc_note)
|
|
2068
|
+
note_html = _build_note_html(note_messages)
|
|
2069
|
+
root_class = f"gabriel-codify-viewer{theme_class}"
|
|
2070
|
+
|
|
2071
|
+
viewer_id = f"gabriel-viewer-{uuid.uuid4().hex}"
|
|
2072
|
+
if total == 0:
|
|
2073
|
+
empty_html = (
|
|
2074
|
+
f"<div class='{root_class}'><div class='gabriel-empty'>No passages to display.</div>"
|
|
2075
|
+
f"{note_html}</div>"
|
|
2076
|
+
)
|
|
2077
|
+
display(HTML(style_html + empty_html))
|
|
2078
|
+
return
|
|
2079
|
+
|
|
2080
|
+
render_entries: List[Dict[str, Any]] = []
|
|
2081
|
+
for idx, payload in enumerate(passages):
|
|
2082
|
+
body_html = _build_highlighted_text(
|
|
2083
|
+
payload["text"], payload["snippets"], category_colors, category_labels
|
|
2084
|
+
)
|
|
2085
|
+
header_html = _build_header_html(
|
|
2086
|
+
payload["header"], payload["active"]
|
|
2087
|
+
)
|
|
2088
|
+
legend_token = f"interactive-{idx}-{random.random()}"
|
|
2089
|
+
legend_html = _build_legend_html(
|
|
2090
|
+
category_colors,
|
|
2091
|
+
payload["counts"],
|
|
2092
|
+
category_labels,
|
|
2093
|
+
legend_token,
|
|
2094
|
+
boolean_specs=boolean_specs,
|
|
2095
|
+
boolean_values=payload.get("bools"),
|
|
2096
|
+
numeric_specs=numeric_specs,
|
|
2097
|
+
numeric_values=payload.get("numeric"),
|
|
2098
|
+
boolean_colors=boolean_colors,
|
|
2099
|
+
numeric_colors=numeric_colors,
|
|
2100
|
+
numeric_ranges=numeric_ranges,
|
|
2101
|
+
)
|
|
2102
|
+
snippet_flags = {
|
|
2103
|
+
cat: bool(payload["snippets"].get(cat)) for cat in category_names
|
|
2104
|
+
}
|
|
2105
|
+
bool_map: Dict[str, Optional[bool]] = {}
|
|
2106
|
+
for column, value in (payload.get("bools") or {}).items():
|
|
2107
|
+
if value is True:
|
|
2108
|
+
bool_map[column] = True
|
|
2109
|
+
elif value is False:
|
|
2110
|
+
bool_map[column] = False
|
|
2111
|
+
else:
|
|
2112
|
+
bool_map[column] = None
|
|
2113
|
+
numeric_map: Dict[str, Optional[float]] = {}
|
|
2114
|
+
for column, value in (payload.get("numeric") or {}).items():
|
|
2115
|
+
if value is None:
|
|
2116
|
+
numeric_map[column] = None
|
|
2117
|
+
else:
|
|
2118
|
+
try:
|
|
2119
|
+
numeric_map[column] = float(value)
|
|
2120
|
+
except (TypeError, ValueError):
|
|
2121
|
+
numeric_map[column] = None
|
|
2122
|
+
render_entries.append(
|
|
2123
|
+
{
|
|
2124
|
+
"html": f"{legend_html}{header_html}<div class='gabriel-text'>{body_html}</div>",
|
|
2125
|
+
"snippets": snippet_flags,
|
|
2126
|
+
"bools": bool_map,
|
|
2127
|
+
"numeric": numeric_map,
|
|
2128
|
+
}
|
|
2129
|
+
)
|
|
2130
|
+
|
|
2131
|
+
numeric_label_map = {
|
|
2132
|
+
spec.column: spec.label or spec.column.replace("_", " ").title()
|
|
2133
|
+
for spec in numeric_specs
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
data_payload = {
|
|
2137
|
+
"passages": render_entries,
|
|
2138
|
+
"numericLabels": numeric_label_map,
|
|
2139
|
+
}
|
|
2140
|
+
data_json = json.dumps(data_payload).replace("</", r"<\/")
|
|
2141
|
+
note_block = (
|
|
2142
|
+
f"<div class='gabriel-note-stack' data-role='note'>{note_html}</div>"
|
|
2143
|
+
if note_html
|
|
2144
|
+
else ""
|
|
2145
|
+
)
|
|
2146
|
+
slider_max = max(1, total)
|
|
2147
|
+
slider_count = f"1 / {total}"
|
|
2148
|
+
viewer_template = Template(
|
|
2149
|
+
"""
|
|
2150
|
+
<div id="$viewer_id" class="$root_class">
|
|
2151
|
+
<div class="gabriel-status" data-role="status"></div>
|
|
2152
|
+
<div class="gabriel-controls">
|
|
2153
|
+
<div class="gabriel-nav-group">
|
|
2154
|
+
<button type="button" class="gabriel-nav-button" data-action="prev">◀ Previous</button>
|
|
2155
|
+
<button type="button" class="gabriel-nav-button" data-action="random">Random</button>
|
|
2156
|
+
<button type="button" class="gabriel-nav-button" data-action="next">Next ▶</button>
|
|
2157
|
+
</div>
|
|
2158
|
+
<div class="gabriel-slider-shell">
|
|
2159
|
+
<input type="range" min="1" max="$slider_max" value="1" class="gabriel-slider" data-role="slider" />
|
|
2160
|
+
<div class="gabriel-slider-count" data-role="slider-count">$slider_count</div>
|
|
2161
|
+
</div>
|
|
2162
|
+
<button type="button" class="gabriel-reset-button" data-action="reset" disabled>Reset</button>
|
|
2163
|
+
</div>
|
|
2164
|
+
<div class="gabriel-passage-panel">
|
|
2165
|
+
<div class="gabriel-passage-scroll" data-role="passage"></div>
|
|
2166
|
+
</div>
|
|
2167
|
+
$note_block
|
|
2168
|
+
</div>
|
|
2169
|
+
<script>
|
|
2170
|
+
(function() {
|
|
2171
|
+
const container = document.getElementById("$viewer_id");
|
|
2172
|
+
if (!container) {
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
const data = $data_json;
|
|
2176
|
+
const statusEl = container.querySelector('[data-role="status"]');
|
|
2177
|
+
const passageEl = container.querySelector('[data-role="passage"]');
|
|
2178
|
+
const sliderEl = container.querySelector('[data-role="slider"]');
|
|
2179
|
+
const sliderCountEl = container.querySelector('[data-role="slider-count"]');
|
|
2180
|
+
const prevBtn = container.querySelector('[data-action="prev"]');
|
|
2181
|
+
const nextBtn = container.querySelector('[data-action="next"]');
|
|
2182
|
+
const randomBtn = container.querySelector('[data-action="random"]');
|
|
2183
|
+
const resetBtn = container.querySelector('[data-action="reset"]');
|
|
2184
|
+
const total = data.passages.length;
|
|
2185
|
+
const numericLabels = data.numericLabels || {};
|
|
2186
|
+
if (!total) {
|
|
2187
|
+
if (passageEl) {
|
|
2188
|
+
passageEl.innerHTML = "<div class='gabriel-empty'>No passages to display.</div>";
|
|
2189
|
+
}
|
|
2190
|
+
if (statusEl) {
|
|
2191
|
+
statusEl.textContent = "No passages available.";
|
|
2192
|
+
}
|
|
2193
|
+
if (sliderEl) sliderEl.disabled = true;
|
|
2194
|
+
if (prevBtn) prevBtn.disabled = true;
|
|
2195
|
+
if (nextBtn) nextBtn.disabled = true;
|
|
2196
|
+
if (randomBtn) randomBtn.disabled = true;
|
|
2197
|
+
if (resetBtn) resetBtn.disabled = true;
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
const state = {
|
|
2201
|
+
active: data.passages.map((_, idx) => idx),
|
|
2202
|
+
index: 0,
|
|
2203
|
+
bools: new Set(),
|
|
2204
|
+
sort: null,
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
function matchesFilters(entry) {
|
|
2208
|
+
if (!entry) {
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
if (state.bools.size) {
|
|
2212
|
+
for (const column of state.bools) {
|
|
2213
|
+
if (!entry.bools || entry.bools[column] !== true) {
|
|
2214
|
+
return false;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
return true;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
function compareForSort(aIdx, bIdx) {
|
|
2222
|
+
if (!state.sort || !state.sort.column) {
|
|
2223
|
+
return 0;
|
|
2224
|
+
}
|
|
2225
|
+
const column = state.sort.column;
|
|
2226
|
+
const entryA = data.passages[aIdx];
|
|
2227
|
+
const entryB = data.passages[bIdx];
|
|
2228
|
+
const valA = entryA && entryA.numeric ? entryA.numeric[column] : null;
|
|
2229
|
+
const valB = entryB && entryB.numeric ? entryB.numeric[column] : null;
|
|
2230
|
+
const aValid = typeof valA === 'number' && isFinite(valA);
|
|
2231
|
+
const bValid = typeof valB === 'number' && isFinite(valB);
|
|
2232
|
+
if (!aValid && !bValid) {
|
|
2233
|
+
return 0;
|
|
2234
|
+
}
|
|
2235
|
+
if (!aValid) {
|
|
2236
|
+
return 1;
|
|
2237
|
+
}
|
|
2238
|
+
if (!bValid) {
|
|
2239
|
+
return -1;
|
|
2240
|
+
}
|
|
2241
|
+
if (valA === valB) {
|
|
2242
|
+
return 0;
|
|
2243
|
+
}
|
|
2244
|
+
const base = valA < valB ? -1 : 1;
|
|
2245
|
+
return state.sort.direction === 'asc' ? base : -base;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
function renderPassage() {
|
|
2249
|
+
if (!passageEl) {
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
if (!state.active.length) {
|
|
2253
|
+
passageEl.innerHTML = "<div class='gabriel-empty'>No passages match the current selection.</div>";
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
const idx = state.active[state.index];
|
|
2257
|
+
const payload = data.passages[idx];
|
|
2258
|
+
passageEl.innerHTML = payload && payload.html ? payload.html : '';
|
|
2259
|
+
bindLegendInteractions();
|
|
2260
|
+
syncLegendStates();
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
function bindLegendInteractions() {
|
|
2264
|
+
if (!passageEl) {
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
passageEl.querySelectorAll('.gabriel-legend-item').forEach(item => {
|
|
2268
|
+
if (!(item instanceof Element) || item.dataset.interactionBound === '1') {
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
if (item.hasAttribute('data-boolean')) {
|
|
2272
|
+
item.addEventListener('click', event => {
|
|
2273
|
+
event.preventDefault();
|
|
2274
|
+
toggleBooleanFilter(item.getAttribute('data-boolean'));
|
|
2275
|
+
});
|
|
2276
|
+
} else if (item.hasAttribute('data-numeric')) {
|
|
2277
|
+
item.addEventListener('click', event => {
|
|
2278
|
+
event.preventDefault();
|
|
2279
|
+
cycleNumericSort(item.getAttribute('data-numeric'));
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
item.dataset.interactionBound = '1';
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
function syncLegendStates() {
|
|
2287
|
+
if (!passageEl) {
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
passageEl.querySelectorAll('[data-boolean]').forEach(chip => {
|
|
2291
|
+
const column = chip.getAttribute('data-boolean');
|
|
2292
|
+
const active = column && state.bools.has(column);
|
|
2293
|
+
chip.classList.toggle('is-filtered', Boolean(active));
|
|
2294
|
+
chip.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
2295
|
+
});
|
|
2296
|
+
passageEl.querySelectorAll('[data-numeric]').forEach(chip => {
|
|
2297
|
+
const column = chip.getAttribute('data-numeric');
|
|
2298
|
+
const isActive = Boolean(state.sort && state.sort.column === column);
|
|
2299
|
+
chip.classList.toggle('is-sorted', isActive);
|
|
2300
|
+
chip.classList.toggle('is-sorted-asc', Boolean(isActive && state.sort && state.sort.direction === 'asc'));
|
|
2301
|
+
chip.classList.toggle('is-sorted-desc', Boolean(isActive && state.sort && state.sort.direction === 'desc'));
|
|
2302
|
+
chip.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
function updateStatus() {
|
|
2307
|
+
if (!statusEl) {
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
if (!state.active.length) {
|
|
2311
|
+
statusEl.textContent = 'No passages match the current selection.';
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
let message = 'Passage <strong>' + (state.index + 1) + '</strong> of ' + state.active.length;
|
|
2315
|
+
if (state.active.length !== total) {
|
|
2316
|
+
message += ' • ' + state.active.length + ' / ' + total + ' match';
|
|
2317
|
+
}
|
|
2318
|
+
if (state.sort && state.sort.column) {
|
|
2319
|
+
const label = numericLabels[state.sort.column] || state.sort.column;
|
|
2320
|
+
const arrow = state.sort.direction === 'asc' ? '↑' : '↓';
|
|
2321
|
+
message += ' • ' + arrow + ' ' + label;
|
|
2322
|
+
}
|
|
2323
|
+
statusEl.innerHTML = message;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
function updateSlider(forceValue = true) {
|
|
2327
|
+
if (!sliderEl || !sliderCountEl) {
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
const max = Math.max(1, state.active.length || 1);
|
|
2331
|
+
sliderEl.max = String(max);
|
|
2332
|
+
if (forceValue !== false) {
|
|
2333
|
+
sliderEl.value = state.active.length ? String(state.index + 1) : '1';
|
|
2334
|
+
}
|
|
2335
|
+
sliderEl.disabled = state.active.length <= 1;
|
|
2336
|
+
sliderCountEl.textContent = state.active.length ? (state.index + 1) + ' / ' + state.active.length : '0 / 0';
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
function updateNavDisabled() {
|
|
2340
|
+
const disabled = state.active.length === 0;
|
|
2341
|
+
if (prevBtn) prevBtn.disabled = disabled;
|
|
2342
|
+
if (nextBtn) nextBtn.disabled = disabled;
|
|
2343
|
+
if (randomBtn) randomBtn.disabled = disabled;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
function updateResetButton() {
|
|
2347
|
+
if (!resetBtn) {
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
const hasActive = state.bools.size > 0 || Boolean(state.sort);
|
|
2351
|
+
resetBtn.disabled = !hasActive;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function moveIndex(delta) {
|
|
2355
|
+
if (!state.active.length) {
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
const length = state.active.length;
|
|
2359
|
+
state.index = (state.index + delta + length) % length;
|
|
2360
|
+
renderPassage();
|
|
2361
|
+
updateStatus();
|
|
2362
|
+
updateSlider();
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
function toggleBooleanFilter(column) {
|
|
2366
|
+
if (!column) {
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
if (state.bools.has(column)) {
|
|
2370
|
+
state.bools.delete(column);
|
|
2371
|
+
} else {
|
|
2372
|
+
state.bools.add(column);
|
|
2373
|
+
}
|
|
2374
|
+
applyFilters(true);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function cycleNumericSort(column) {
|
|
2378
|
+
if (!column) {
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
const current = state.sort && state.sort.column === column ? state.sort : null;
|
|
2382
|
+
if (!current) {
|
|
2383
|
+
state.sort = { column, direction: 'desc' };
|
|
2384
|
+
} else if (current.direction === 'desc') {
|
|
2385
|
+
state.sort = { column, direction: 'asc' };
|
|
2386
|
+
} else {
|
|
2387
|
+
state.sort = null;
|
|
2388
|
+
}
|
|
2389
|
+
applyFilters(false);
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
function resetSelections() {
|
|
2393
|
+
state.bools.clear();
|
|
2394
|
+
state.sort = null;
|
|
2395
|
+
applyFilters(true);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function applyFilters(resetIndex = true) {
|
|
2399
|
+
const matches = [];
|
|
2400
|
+
data.passages.forEach((entry, idx) => {
|
|
2401
|
+
if (matchesFilters(entry)) {
|
|
2402
|
+
matches.push(idx);
|
|
2403
|
+
}
|
|
2404
|
+
});
|
|
2405
|
+
if (state.sort && state.sort.column) {
|
|
2406
|
+
matches.sort(compareForSort);
|
|
2407
|
+
}
|
|
2408
|
+
state.active = matches;
|
|
2409
|
+
if (!matches.length) {
|
|
2410
|
+
state.index = 0;
|
|
2411
|
+
} else if (resetIndex || state.index >= matches.length) {
|
|
2412
|
+
state.index = 0;
|
|
2413
|
+
}
|
|
2414
|
+
renderPassage();
|
|
2415
|
+
updateStatus();
|
|
2416
|
+
updateSlider();
|
|
2417
|
+
updateNavDisabled();
|
|
2418
|
+
updateResetButton();
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
if (prevBtn) {
|
|
2422
|
+
prevBtn.addEventListener('click', () => moveIndex(-1));
|
|
2423
|
+
}
|
|
2424
|
+
if (nextBtn) {
|
|
2425
|
+
nextBtn.addEventListener('click', () => moveIndex(1));
|
|
2426
|
+
}
|
|
2427
|
+
if (randomBtn) {
|
|
2428
|
+
randomBtn.addEventListener('click', () => {
|
|
2429
|
+
if (!state.active.length) {
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
const idx = Math.floor(Math.random() * state.active.length);
|
|
2433
|
+
state.index = idx;
|
|
2434
|
+
renderPassage();
|
|
2435
|
+
updateStatus();
|
|
2436
|
+
updateSlider();
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
if (resetBtn) {
|
|
2440
|
+
resetBtn.addEventListener('click', () => resetSelections());
|
|
2441
|
+
}
|
|
2442
|
+
if (sliderEl) {
|
|
2443
|
+
sliderEl.addEventListener('input', event => {
|
|
2444
|
+
if (!state.active.length) {
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
const value = parseInt(event.target.value, 10);
|
|
2448
|
+
if (Number.isNaN(value)) {
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
state.index = Math.min(state.active.length - 1, Math.max(0, value - 1));
|
|
2452
|
+
renderPassage();
|
|
2453
|
+
updateStatus();
|
|
2454
|
+
updateSlider(false);
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
applyFilters(true);
|
|
2459
|
+
})();
|
|
2460
|
+
</script>
|
|
2461
|
+
"""
|
|
2462
|
+
)
|
|
2463
|
+
viewer_html = viewer_template.substitute(
|
|
2464
|
+
viewer_id=viewer_id,
|
|
2465
|
+
root_class=root_class,
|
|
2466
|
+
slider_max=slider_max,
|
|
2467
|
+
slider_count=slider_count,
|
|
2468
|
+
note_block=note_block,
|
|
2469
|
+
data_json=data_json,
|
|
2470
|
+
)
|
|
2471
|
+
display(HTML(style_html + viewer_html))
|
|
2472
|
+
|
|
2473
|
+
|
|
2474
|
+
def view(
|
|
2475
|
+
df: pd.DataFrame,
|
|
2476
|
+
column_name: str,
|
|
2477
|
+
attributes: Optional[Union[Mapping[str, Any], Sequence[Any], Any]] = None,
|
|
2478
|
+
*,
|
|
2479
|
+
header_columns: Optional[Union[Sequence[Any], Any]] = None,
|
|
2480
|
+
max_passages: Optional[int] = None,
|
|
2481
|
+
font_scale: float = 1.0,
|
|
2482
|
+
font_family: Optional[str] = None,
|
|
2483
|
+
color_mode: str = "auto",
|
|
2484
|
+
):
|
|
2485
|
+
"""View passages and their associated attributes.
|
|
2486
|
+
|
|
2487
|
+
Parameters
|
|
2488
|
+
----------
|
|
2489
|
+
df:
|
|
2490
|
+
DataFrame containing the passages.
|
|
2491
|
+
column_name:
|
|
2492
|
+
Column name in ``df`` holding the raw text.
|
|
2493
|
+
attributes:
|
|
2494
|
+
Attribute columns to render. Accepts sequences of column names, tuples
|
|
2495
|
+
of ``(column, label)``, mappings, or the special string
|
|
2496
|
+
``"coded_passages"`` for Codify outputs.
|
|
2497
|
+
header_columns:
|
|
2498
|
+
Optional sequence of column names (or ``(column, label)`` tuples)
|
|
2499
|
+
displayed above each passage. Values are rendered in the provided
|
|
2500
|
+
order to expose metadata such as speaker names or timestamps.
|
|
2501
|
+
max_passages:
|
|
2502
|
+
Optional cap on the number of passages rendered in the notebook.
|
|
2503
|
+
When ``None`` (default) all passages are available inside the viewer.
|
|
2504
|
+
font_scale:
|
|
2505
|
+
Multiplier applied to key font sizes inside the viewer.
|
|
2506
|
+
font_family:
|
|
2507
|
+
Optional custom font family prepended to the default stack.
|
|
2508
|
+
color_mode:
|
|
2509
|
+
``"auto"`` (default), ``"dark"``, or ``"light"`` to force a theme.
|
|
2510
|
+
"""
|
|
2511
|
+
|
|
2512
|
+
_render_passage_viewer(
|
|
2513
|
+
df,
|
|
2514
|
+
column_name,
|
|
2515
|
+
attributes=attributes,
|
|
2516
|
+
header_columns=header_columns,
|
|
2517
|
+
max_passages=max_passages,
|
|
2518
|
+
font_scale=font_scale,
|
|
2519
|
+
font_family=font_family,
|
|
2520
|
+
color_mode=color_mode,
|
|
2521
|
+
)
|
|
2522
|
+
return None
|
|
2523
|
+
|
|
2524
|
+
|
|
2525
|
+
if __name__ == "__main__":
|
|
2526
|
+
# Example usage
|
|
2527
|
+
import pandas as pd
|
|
2528
|
+
|
|
2529
|
+
# Sample data
|
|
2530
|
+
sample_data = {
|
|
2531
|
+
'id': [1, 2, 3],
|
|
2532
|
+
'text': [
|
|
2533
|
+
"This is a great example of positive text. I really appreciate your help with this matter.",
|
|
2534
|
+
"I can't believe how terrible this service is. This is absolutely unacceptable behavior.",
|
|
2535
|
+
"Could you please explain how this works? I'm genuinely curious about the process."
|
|
2536
|
+
],
|
|
2537
|
+
'positive_sentiment': [
|
|
2538
|
+
["This is a great example of positive text", "I really appreciate your help"],
|
|
2539
|
+
[],
|
|
2540
|
+
["I'm genuinely curious about the process"]
|
|
2541
|
+
],
|
|
2542
|
+
'negative_sentiment': [
|
|
2543
|
+
[],
|
|
2544
|
+
["I can't believe how terrible this service is", "This is absolutely unacceptable behavior"],
|
|
2545
|
+
[]
|
|
2546
|
+
],
|
|
2547
|
+
'questions': [
|
|
2548
|
+
[],
|
|
2549
|
+
[],
|
|
2550
|
+
["Could you please explain how this works?"]
|
|
2551
|
+
]
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
df = pd.DataFrame(sample_data)
|
|
2555
|
+
categories = ['positive_sentiment', 'negative_sentiment', 'questions']
|
|
2556
|
+
|
|
2557
|
+
view(df, 'text', attributes=categories)
|