DAG新增tooltip选项;新增step-down模式;释放semantic query功能;支持递归归因(recursive attribution);其它小改进
Browse files- backend/api/client_activity.py +5 -1
- backend/completion_generator.py +1 -1
- backend/visit_stats.py +28 -1
- client/src/analysis.html +16 -4
- client/src/attribution.html +4 -2
- client/src/chat.html +7 -5
- client/src/css/_buttons.scss +6 -2
- client/src/css/_demo-list.scss +1 -1
- client/src/css/_responsive.scss +5 -2
- client/src/css/_semantic-analysis.scss +30 -0
- client/src/css/_tooltip-vars.scss +15 -0
- client/src/css/compare.scss +6 -1
- client/src/css/gen_attribute.scss +88 -44
- client/src/css/start.scss +44 -23
- client/src/demos/gen_attribute/CN->EN翻译.json +1 -1
- client/src/demos/gen_attribute/CoT | 苏州所在省的省会.json +0 -0
- client/src/demos/gen_attribute/Write a sonnet about love.json +1 -1
- client/src/demos/gen_attribute/写一首绝句,主题是春天.json +1 -1
- client/src/demos/gen_attribute/过拟合|李白 将进酒.json +1 -1
- client/src/gen_attribute.html +56 -17
- client/src/partials/settings-menu-analysis.html +0 -6
- client/src/ts/api/GLTR_API.ts +3 -0
- client/src/ts/attribution/genAttributeDagEdgeDisplay.ts +22 -6
- client/src/ts/attribution/genAttributeDagPreprocess.ts +9 -18
- client/src/ts/attribution/genAttributeDagTopkToken.ts +37 -0
- client/src/ts/attribution/genAttributeDagView.ts +844 -244
- client/src/ts/attribution/genAttributeDagViewLinearArcMode.ts +59 -6
- client/src/ts/demos/genAttributeBundledDemoManifest.generated.ts +1 -1
- client/src/ts/gen_attribute.ts +325 -52
- client/src/ts/lang/translations.ts +15 -1
- client/src/ts/storage/genAttributeRunCache.ts +47 -3
- client/src/ts/utils/clientActivityPing.ts +10 -0
- client/src/ts/utils/surprisalMath.ts +50 -3
- client/src/ts/utils/tokenDisplayUtils.ts +3 -3
- client/src/ts/utils/topkChartUtils.ts +2 -2
- client/src/ts/utils/visitStatsDialog.ts +35 -2
- client/src/ts/utils/visualizationUpdater.ts +18 -29
- client/src/ts/vis/ToolTip.ts +137 -97
- client/src/ts/vis/constants.ts +1 -1
backend/api/client_activity.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
from urllib.parse import unquote
|
| 2 |
|
| 3 |
from backend.access_log import _log_request
|
| 4 |
-
from backend.visit_stats import record_activity_report
|
| 5 |
|
| 6 |
|
| 7 |
def _sparse_page_activity_log_cum(cum: int) -> bool:
|
|
@@ -38,6 +38,10 @@ def client_activity_report(activity_body=None):
|
|
| 38 |
client_os = str(raw_os).strip() if raw_os is not None else None
|
| 39 |
|
| 40 |
record_activity_report(page_key, dlt, cum, client_os)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
if _sparse_page_activity_log_cum(cum):
|
| 42 |
_log_request(
|
| 43 |
"📄 页面活跃",
|
|
|
|
| 1 |
from urllib.parse import unquote
|
| 2 |
|
| 3 |
from backend.access_log import _log_request
|
| 4 |
+
from backend.visit_stats import record_activity_report, record_gen_attr_opt_sec
|
| 5 |
|
| 6 |
|
| 7 |
def _sparse_page_activity_log_cum(cum: int) -> bool:
|
|
|
|
| 38 |
client_os = str(raw_os).strip() if raw_os is not None else None
|
| 39 |
|
| 40 |
record_activity_report(page_key, dlt, cum, client_os)
|
| 41 |
+
if page_key == "gen_attribute.html":
|
| 42 |
+
raw_opts = d.get("page_opts")
|
| 43 |
+
if isinstance(raw_opts, dict):
|
| 44 |
+
record_gen_attr_opt_sec(dlt, {k: bool(v) for k, v in raw_opts.items() if isinstance(k, str)})
|
| 45 |
if _sparse_page_activity_log_cum(cum):
|
| 46 |
_log_request(
|
| 47 |
"📄 页面活跃",
|
backend/completion_generator.py
CHANGED
|
@@ -24,7 +24,7 @@ from backend.pred_topk_format import pred_topk_pairs_from_probs_1d
|
|
| 24 |
from backend.runtime_config import DEFAULT_TOPK
|
| 25 |
|
| 26 |
# 续写路径:prompt + 续写合计不得超过该 token 数(与语义分析 runtime 无关)。
|
| 27 |
-
completion_max_token_length =
|
| 28 |
|
| 29 |
# 特殊 token 亦视为分析/展示内容,故不跳过。
|
| 30 |
_COMPLETION_DECODE_SKIP_SPECIAL = False
|
|
|
|
| 24 |
from backend.runtime_config import DEFAULT_TOPK
|
| 25 |
|
| 26 |
# 续写路径:prompt + 续写合计不得超过该 token 数(与语义分析 runtime 无关)。
|
| 27 |
+
completion_max_token_length = 500
|
| 28 |
|
| 29 |
# 特殊 token 亦视为分析/展示内容,故不跳过。
|
| 30 |
_COMPLETION_DECODE_SKIP_SPECIAL = False
|
backend/visit_stats.py
CHANGED
|
@@ -14,6 +14,7 @@ _WIN = {"page_loads": 0, "active_visits": 0}
|
|
| 14 |
_PAGE_SEC = defaultdict(int)
|
| 15 |
_API = defaultdict(int)
|
| 16 |
_OS_REPORTS = defaultdict(int) # 与同页「首轮心跳」(delta_active_sec == total_active_sec) 对齐,仅凭该包附带 client_os 计一次
|
|
|
|
| 17 |
_VALID_CLIENT_OS = frozenset({"ios", "android", "windows", "macos", "linux", "unknown"})
|
| 18 |
|
| 19 |
# client/src/ts/utils/settingsMenuManager.ts handleVisitStatsClick:PAGE_ORDER / API_ORDER / OS_ORDER
|
|
@@ -36,6 +37,10 @@ _STATS_API_ORDER = (
|
|
| 36 |
"prediction_attribute__analysis.html",
|
| 37 |
)
|
| 38 |
_STATS_OS_ORDER = ("ios", "android", "windows", "macos", "linux", "unknown")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
# RLock:_persist_tick 在已持锁时调用 _sample_locked_counters,同线程需可重入。
|
| 41 |
_LOCK = threading.RLock()
|
|
@@ -190,7 +195,7 @@ def _increment_nonempty(h: dict) -> bool:
|
|
| 190 |
"""是否有尚未写入远端的任意增量。"""
|
| 191 |
if h.get("page_loads") or h.get("active_visits"):
|
| 192 |
return True
|
| 193 |
-
if h.get("page_sec") or h.get("api") or h.get("os"):
|
| 194 |
return True
|
| 195 |
return False
|
| 196 |
|
|
@@ -214,6 +219,7 @@ def _apply_persist_success(total_rec: dict, committed_sample: dict) -> None:
|
|
| 214 |
_subtract_defaultdict_int(_PAGE_SEC, committed_sample["session_page_sec"])
|
| 215 |
_subtract_defaultdict_int(_API, committed_sample["session_api"])
|
| 216 |
_subtract_defaultdict_int(_OS_REPORTS, committed_sample["session_os_reports"])
|
|
|
|
| 217 |
|
| 218 |
|
| 219 |
def _load_base():
|
|
@@ -355,21 +361,35 @@ def bump_api(kind: str):
|
|
| 355 |
_API[kind] += 1
|
| 356 |
|
| 357 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
def _sample_locked_counters() -> dict:
|
| 359 |
with _LOCK:
|
| 360 |
bo = _base.get("os")
|
| 361 |
base_os = dict(bo) if isinstance(bo, dict) else {}
|
|
|
|
|
|
|
| 362 |
return {
|
| 363 |
"sw_pl": _WIN["page_loads"],
|
| 364 |
"sw_av": _WIN["active_visits"],
|
| 365 |
"session_page_sec": dict(_PAGE_SEC),
|
| 366 |
"session_api": dict(_API),
|
| 367 |
"session_os_reports": dict(_OS_REPORTS),
|
|
|
|
| 368 |
"bp": int(_base_int(_base, "page_loads")),
|
| 369 |
"bav": _base_int(_base, "active_visits"),
|
| 370 |
"base_page_sec": dict(_base.get("page_sec") or {}),
|
| 371 |
"base_api": dict(_base.get("api") or {}),
|
| 372 |
"base_os": base_os,
|
|
|
|
| 373 |
"saved_at": _base.get("saved_at"),
|
| 374 |
}
|
| 375 |
|
|
@@ -378,6 +398,7 @@ def _merge_from_sample(s: dict) -> tuple[dict, dict, dict]:
|
|
| 378 |
"""(管理员 API 快照, stats_total 的 body 不含 saved_at, stats_delta 的 body)。"""
|
| 379 |
sp, sa, so = s["session_page_sec"], s["session_api"], s["session_os_reports"]
|
| 380 |
bpp, bpa, bpo = s["base_page_sec"], s["base_api"], s["base_os"]
|
|
|
|
| 381 |
|
| 382 |
total_page_sec = {k: bpp.get(k, 0) + sp.get(k, 0) for k in set(bpp) | set(sp)}
|
| 383 |
total_api = {k: bpa.get(k, 0) + sa.get(k, 0) for k in set(bpa) | set(sa)}
|
|
@@ -385,13 +406,16 @@ def _merge_from_sample(s: dict) -> tuple[dict, dict, dict]:
|
|
| 385 |
k: int(bpo.get(k, 0)) + int(so.get(k, 0))
|
| 386 |
for k in set(bpo) | set(so)
|
| 387 |
}
|
|
|
|
| 388 |
|
| 389 |
total_page_sec = _ordered_str_int_map(_STATS_PAGE_ORDER, total_page_sec)
|
| 390 |
total_api = _ordered_str_int_map(_STATS_API_ORDER, total_api)
|
| 391 |
total_os = _ordered_str_int_map(_STATS_OS_ORDER, total_os)
|
|
|
|
| 392 |
ord_pg = _ordered_str_int_map(_STATS_PAGE_ORDER, sp)
|
| 393 |
ord_api = _ordered_str_int_map(_STATS_API_ORDER, sa)
|
| 394 |
ord_os = _ordered_str_int_map(_STATS_OS_ORDER, so)
|
|
|
|
| 395 |
|
| 396 |
tpl, tav = s["bp"] + s["sw_pl"], s["bav"] + s["sw_av"]
|
| 397 |
|
|
@@ -401,6 +425,7 @@ def _merge_from_sample(s: dict) -> tuple[dict, dict, dict]:
|
|
| 401 |
"os": total_os,
|
| 402 |
"page_sec": total_page_sec,
|
| 403 |
"api": total_api,
|
|
|
|
| 404 |
"saved_at": s["saved_at"],
|
| 405 |
}
|
| 406 |
stats_body = {
|
|
@@ -409,6 +434,7 @@ def _merge_from_sample(s: dict) -> tuple[dict, dict, dict]:
|
|
| 409 |
"os": total_os,
|
| 410 |
"page_sec": total_page_sec,
|
| 411 |
"api": total_api,
|
|
|
|
| 412 |
}
|
| 413 |
delta_body = {
|
| 414 |
"page_loads": s["sw_pl"],
|
|
@@ -416,6 +442,7 @@ def _merge_from_sample(s: dict) -> tuple[dict, dict, dict]:
|
|
| 416 |
"os": ord_os,
|
| 417 |
"page_sec": ord_pg,
|
| 418 |
"api": ord_api,
|
|
|
|
| 419 |
}
|
| 420 |
return public, stats_body, delta_body
|
| 421 |
|
|
|
|
| 14 |
_PAGE_SEC = defaultdict(int)
|
| 15 |
_API = defaultdict(int)
|
| 16 |
_OS_REPORTS = defaultdict(int) # 与同页「首轮心跳」(delta_active_sec == total_active_sec) 对齐,仅凭该包附带 client_os 计一次
|
| 17 |
+
_GEN_ATTR_OPT_SEC = defaultdict(int) # gen_attribute.html 各非默认选项处于激活状态的活跃秒
|
| 18 |
_VALID_CLIENT_OS = frozenset({"ios", "android", "windows", "macos", "linux", "unknown"})
|
| 19 |
|
| 20 |
# client/src/ts/utils/settingsMenuManager.ts handleVisitStatsClick:PAGE_ORDER / API_ORDER / OS_ORDER
|
|
|
|
| 37 |
"prediction_attribute__analysis.html",
|
| 38 |
)
|
| 39 |
_STATS_OS_ORDER = ("ios", "android", "windows", "macos", "linux", "unknown")
|
| 40 |
+
_STATS_GEN_ATTR_OPT_ORDER = (
|
| 41 |
+
"layout_linear_arc", "layout_step_down", "layout_spiral",
|
| 42 |
+
"propagated", "downstream", "token_tooltip",
|
| 43 |
+
)
|
| 44 |
|
| 45 |
# RLock:_persist_tick 在已持锁时调用 _sample_locked_counters,同线程需可重入。
|
| 46 |
_LOCK = threading.RLock()
|
|
|
|
| 195 |
"""是否有尚未写入远端的任意增量。"""
|
| 196 |
if h.get("page_loads") or h.get("active_visits"):
|
| 197 |
return True
|
| 198 |
+
if h.get("page_sec") or h.get("api") or h.get("os") or h.get("gen_attr_opt_sec"):
|
| 199 |
return True
|
| 200 |
return False
|
| 201 |
|
|
|
|
| 219 |
_subtract_defaultdict_int(_PAGE_SEC, committed_sample["session_page_sec"])
|
| 220 |
_subtract_defaultdict_int(_API, committed_sample["session_api"])
|
| 221 |
_subtract_defaultdict_int(_OS_REPORTS, committed_sample["session_os_reports"])
|
| 222 |
+
_subtract_defaultdict_int(_GEN_ATTR_OPT_SEC, committed_sample["session_gen_attr_opt_sec"])
|
| 223 |
|
| 224 |
|
| 225 |
def _load_base():
|
|
|
|
| 361 |
_API[kind] += 1
|
| 362 |
|
| 363 |
|
| 364 |
+
def record_gen_attr_opt_sec(delta_sec: int, opts: dict[str, bool]) -> None:
|
| 365 |
+
"""累计 gen_attribute.html 各非默认选项处于激活状态的活跃秒。"""
|
| 366 |
+
if delta_sec <= 0:
|
| 367 |
+
return
|
| 368 |
+
with _LOCK:
|
| 369 |
+
for k, v in opts.items():
|
| 370 |
+
if v:
|
| 371 |
+
_GEN_ATTR_OPT_SEC[k] += delta_sec
|
| 372 |
+
|
| 373 |
+
|
| 374 |
def _sample_locked_counters() -> dict:
|
| 375 |
with _LOCK:
|
| 376 |
bo = _base.get("os")
|
| 377 |
base_os = dict(bo) if isinstance(bo, dict) else {}
|
| 378 |
+
bgo = _base.get("gen_attr_opt_sec")
|
| 379 |
+
base_gen_attr_opt_sec = dict(bgo) if isinstance(bgo, dict) else {}
|
| 380 |
return {
|
| 381 |
"sw_pl": _WIN["page_loads"],
|
| 382 |
"sw_av": _WIN["active_visits"],
|
| 383 |
"session_page_sec": dict(_PAGE_SEC),
|
| 384 |
"session_api": dict(_API),
|
| 385 |
"session_os_reports": dict(_OS_REPORTS),
|
| 386 |
+
"session_gen_attr_opt_sec": dict(_GEN_ATTR_OPT_SEC),
|
| 387 |
"bp": int(_base_int(_base, "page_loads")),
|
| 388 |
"bav": _base_int(_base, "active_visits"),
|
| 389 |
"base_page_sec": dict(_base.get("page_sec") or {}),
|
| 390 |
"base_api": dict(_base.get("api") or {}),
|
| 391 |
"base_os": base_os,
|
| 392 |
+
"base_gen_attr_opt_sec": base_gen_attr_opt_sec,
|
| 393 |
"saved_at": _base.get("saved_at"),
|
| 394 |
}
|
| 395 |
|
|
|
|
| 398 |
"""(管理员 API 快照, stats_total 的 body 不含 saved_at, stats_delta 的 body)。"""
|
| 399 |
sp, sa, so = s["session_page_sec"], s["session_api"], s["session_os_reports"]
|
| 400 |
bpp, bpa, bpo = s["base_page_sec"], s["base_api"], s["base_os"]
|
| 401 |
+
sg, bgo = s["session_gen_attr_opt_sec"], s["base_gen_attr_opt_sec"]
|
| 402 |
|
| 403 |
total_page_sec = {k: bpp.get(k, 0) + sp.get(k, 0) for k in set(bpp) | set(sp)}
|
| 404 |
total_api = {k: bpa.get(k, 0) + sa.get(k, 0) for k in set(bpa) | set(sa)}
|
|
|
|
| 406 |
k: int(bpo.get(k, 0)) + int(so.get(k, 0))
|
| 407 |
for k in set(bpo) | set(so)
|
| 408 |
}
|
| 409 |
+
total_gen_attr_opt_sec = {k: bgo.get(k, 0) + sg.get(k, 0) for k in set(bgo) | set(sg)}
|
| 410 |
|
| 411 |
total_page_sec = _ordered_str_int_map(_STATS_PAGE_ORDER, total_page_sec)
|
| 412 |
total_api = _ordered_str_int_map(_STATS_API_ORDER, total_api)
|
| 413 |
total_os = _ordered_str_int_map(_STATS_OS_ORDER, total_os)
|
| 414 |
+
total_gen_attr_opt_sec = _ordered_str_int_map(_STATS_GEN_ATTR_OPT_ORDER, total_gen_attr_opt_sec)
|
| 415 |
ord_pg = _ordered_str_int_map(_STATS_PAGE_ORDER, sp)
|
| 416 |
ord_api = _ordered_str_int_map(_STATS_API_ORDER, sa)
|
| 417 |
ord_os = _ordered_str_int_map(_STATS_OS_ORDER, so)
|
| 418 |
+
ord_gen_attr_opt_sec = _ordered_str_int_map(_STATS_GEN_ATTR_OPT_ORDER, sg)
|
| 419 |
|
| 420 |
tpl, tav = s["bp"] + s["sw_pl"], s["bav"] + s["sw_av"]
|
| 421 |
|
|
|
|
| 425 |
"os": total_os,
|
| 426 |
"page_sec": total_page_sec,
|
| 427 |
"api": total_api,
|
| 428 |
+
"gen_attr_opt_sec": total_gen_attr_opt_sec,
|
| 429 |
"saved_at": s["saved_at"],
|
| 430 |
}
|
| 431 |
stats_body = {
|
|
|
|
| 434 |
"os": total_os,
|
| 435 |
"page_sec": total_page_sec,
|
| 436 |
"api": total_api,
|
| 437 |
+
"gen_attr_opt_sec": total_gen_attr_opt_sec,
|
| 438 |
}
|
| 439 |
delta_body = {
|
| 440 |
"page_loads": s["sw_pl"],
|
|
|
|
| 442 |
"os": ord_os,
|
| 443 |
"page_sec": ord_pg,
|
| 444 |
"api": ord_api,
|
| 445 |
+
"gen_attr_opt_sec": ord_gen_attr_opt_sec,
|
| 446 |
}
|
| 447 |
return public, stats_body, delta_body
|
| 448 |
|
client/src/analysis.html
CHANGED
|
@@ -72,9 +72,21 @@
|
|
| 72 |
<textarea id="test_text"></textarea>
|
| 73 |
<div class="button-group">
|
| 74 |
<div class="button-left">
|
| 75 |
-
<
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
</div>
|
| 79 |
<div id="text_metrics" class="text-metrics">
|
| 80 |
<div class="text-metrics-primary">
|
|
@@ -101,7 +113,7 @@
|
|
| 101 |
<div class="semantic-analysis-controls">
|
| 102 |
<div class="semantic-search-row">
|
| 103 |
<div class="semantic-search-input-wrapper">
|
| 104 |
-
<input type="text" id="semantic_search_input" class="semantic-search-input" placeholder="Enter query
|
| 105 |
<button type="button" id="semantic_search_clear" class="semantic-search-clear demo-delete-btn" title="Clear" aria-label="Clear" data-i18n="title,aria-label">×</button>
|
| 106 |
<ul id="semantic_search_history_dropdown" class="semantic-search-history-dropdown"></ul>
|
| 107 |
</div>
|
|
|
|
| 72 |
<textarea id="test_text"></textarea>
|
| 73 |
<div class="button-group">
|
| 74 |
<div class="button-left">
|
| 75 |
+
<div class="button-left-stack">
|
| 76 |
+
<div class="button-left-primary">
|
| 77 |
+
<button id="submit_text_btn" class="primary-btn" data-i18n>Analyze</button>
|
| 78 |
+
<div class="loadersmall loader-small-container"></div>
|
| 79 |
+
<span id="analyze_progress" class="analyze-progress"></span>
|
| 80 |
+
</div>
|
| 81 |
+
<div class="semantic-submode-row semantic-analysis-enable-row">
|
| 82 |
+
<span class="semantic-submode-group">
|
| 83 |
+
<label class="semantic-submode-label" for="semantic_analysis_toggle">
|
| 84 |
+
<input type="checkbox" id="semantic_analysis_toggle" />
|
| 85 |
+
<span data-i18n>Semantic Query(Beta)</span>
|
| 86 |
+
</label>
|
| 87 |
+
</span>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
</div>
|
| 91 |
<div id="text_metrics" class="text-metrics">
|
| 92 |
<div class="text-metrics-primary">
|
|
|
|
| 113 |
<div class="semantic-analysis-controls">
|
| 114 |
<div class="semantic-search-row">
|
| 115 |
<div class="semantic-search-input-wrapper">
|
| 116 |
+
<input type="text" id="semantic_search_input" class="semantic-search-input" placeholder="Enter query question or topic" data-i18n="placeholder">
|
| 117 |
<button type="button" id="semantic_search_clear" class="semantic-search-clear demo-delete-btn" title="Clear" aria-label="Clear" data-i18n="title,aria-label">×</button>
|
| 118 |
<ul id="semantic_search_history_dropdown" class="semantic-search-history-dropdown"></ul>
|
| 119 |
</div>
|
client/src/attribution.html
CHANGED
|
@@ -89,8 +89,10 @@
|
|
| 89 |
</div>
|
| 90 |
<div class="button-group">
|
| 91 |
<div class="button-left">
|
| 92 |
-
<
|
| 93 |
-
|
|
|
|
|
|
|
| 94 |
</div>
|
| 95 |
<div id="attribution_result_info" class="text-metrics is-hidden"></div>
|
| 96 |
<div class="button-right">
|
|
|
|
| 89 |
</div>
|
| 90 |
<div class="button-group">
|
| 91 |
<div class="button-left">
|
| 92 |
+
<div class="button-left-primary">
|
| 93 |
+
<button type="button" id="analyze_btn" class="primary-btn inactive" disabled data-i18n>Analyze attribution</button>
|
| 94 |
+
<div class="loadersmall loader-small-container"></div>
|
| 95 |
+
</div>
|
| 96 |
</div>
|
| 97 |
<div id="attribution_result_info" class="text-metrics is-hidden"></div>
|
| 98 |
<div class="button-right">
|
client/src/chat.html
CHANGED
|
@@ -118,12 +118,14 @@
|
|
| 118 |
</div>
|
| 119 |
<div class="button-group">
|
| 120 |
<div class="button-left">
|
| 121 |
-
<
|
| 122 |
-
|
| 123 |
-
<div class="
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
| 125 |
</div>
|
| 126 |
-
<span id="analyze_progress" class="analyze-progress"></span>
|
| 127 |
</div>
|
| 128 |
<div id="text_metrics" class="text-metrics text-metrics-chat">
|
| 129 |
<div id="metric_usage" class="text-metrics-secondary"></div>
|
|
|
|
| 118 |
</div>
|
| 119 |
<div class="button-group">
|
| 120 |
<div class="button-left">
|
| 121 |
+
<div class="button-left-primary">
|
| 122 |
+
<button type="button" id="submit_text_btn" class="primary-btn inactive" disabled data-i18n>Ask</button>
|
| 123 |
+
<div class="generation-status-slot loader-small-container">
|
| 124 |
+
<div class="loadersmall"></div>
|
| 125 |
+
<span id="chat_complete_reason" class="generation-end-reason"></span>
|
| 126 |
+
</div>
|
| 127 |
+
<span id="analyze_progress" class="analyze-progress"></span>
|
| 128 |
</div>
|
|
|
|
| 129 |
</div>
|
| 130 |
<div id="text_metrics" class="text-metrics text-metrics-chat">
|
| 131 |
<div id="metric_usage" class="text-metrics-secondary"></div>
|
client/src/css/_buttons.scss
CHANGED
|
@@ -62,11 +62,15 @@
|
|
| 62 |
@extend %button-inactive;
|
| 63 |
}
|
| 64 |
|
| 65 |
-
&:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
background-color: var(--text-action-btn-hover); // hover时浅灰色背景
|
| 67 |
}
|
| 68 |
|
| 69 |
-
&:active:not(.inactive) {
|
| 70 |
background-color: var(--text-action-btn-hover); // active时更深的灰色背景
|
| 71 |
opacity: 0.8;
|
| 72 |
}
|
|
|
|
| 62 |
@extend %button-inactive;
|
| 63 |
}
|
| 64 |
|
| 65 |
+
&:disabled {
|
| 66 |
+
@extend %button-inactive;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
&:hover:not(:disabled):not(.inactive) {
|
| 70 |
background-color: var(--text-action-btn-hover); // hover时浅灰色背景
|
| 71 |
}
|
| 72 |
|
| 73 |
+
&:active:not(:disabled):not(.inactive) {
|
| 74 |
background-color: var(--text-action-btn-hover); // active时更深的灰色背景
|
| 75 |
opacity: 0.8;
|
| 76 |
}
|
client/src/css/_demo-list.scss
CHANGED
|
@@ -35,7 +35,7 @@
|
|
| 35 |
white-space: nowrap; // 防止文字换行
|
| 36 |
background-color: var(--button-bg);
|
| 37 |
color: var(--text-color);
|
| 38 |
-
transition:
|
| 39 |
|
| 40 |
&:hover {
|
| 41 |
background-color: var(--button-hover-bg);
|
|
|
|
| 35 |
white-space: nowrap; // 防止文字换行
|
| 36 |
background-color: var(--button-bg);
|
| 37 |
color: var(--text-color);
|
| 38 |
+
transition: none;
|
| 39 |
|
| 40 |
&:hover {
|
| 41 |
background-color: var(--button-hover-bg);
|
client/src/css/_responsive.scss
CHANGED
|
@@ -207,7 +207,8 @@
|
|
| 207 |
padding: 8px 20px 20px;
|
| 208 |
overflow-y: auto;
|
| 209 |
overflow-x: hidden; // 防止水平滚动,确保内容不会被遮挡
|
| 210 |
-
|
|
|
|
| 211 |
background-color: var(--bg-color);
|
| 212 |
box-sizing: border-box; // 确保padding包含在宽度计算中
|
| 213 |
transition: background-color 0.3s ease, border-color 0.3s ease;
|
|
@@ -218,9 +219,11 @@
|
|
| 218 |
}
|
| 219 |
}
|
| 220 |
|
|
|
|
|
|
|
| 221 |
.resizer {
|
| 222 |
width: 8px;
|
| 223 |
-
background:
|
| 224 |
cursor: col-resize;
|
| 225 |
position: relative;
|
| 226 |
flex-shrink: 0;
|
|
|
|
| 207 |
padding: 8px 20px 20px;
|
| 208 |
overflow-y: auto;
|
| 209 |
overflow-x: hidden; // 防止水平滚动,确保内容不会被遮挡
|
| 210 |
+
// 竖向分隔只做在 .resizer::before,避免与拖条中线叠成双线
|
| 211 |
+
border-right: none;
|
| 212 |
background-color: var(--bg-color);
|
| 213 |
box-sizing: border-box; // 确保padding包含在宽度计算中
|
| 214 |
transition: background-color 0.3s ease, border-color 0.3s ease;
|
|
|
|
| 219 |
}
|
| 220 |
}
|
| 221 |
|
| 222 |
+
// 占位与命中区保持 8px(与 LayoutController / chatPanelLayout 的 −8 一致);
|
| 223 |
+
// 底色透明,仅用 ::before 画窄线,避免「视觉上收窄」时拖累拖拽热区。
|
| 224 |
.resizer {
|
| 225 |
width: 8px;
|
| 226 |
+
background: transparent;
|
| 227 |
cursor: col-resize;
|
| 228 |
position: relative;
|
| 229 |
flex-shrink: 0;
|
client/src/css/_semantic-analysis.scss
CHANGED
|
@@ -25,6 +25,14 @@
|
|
| 25 |
&.semantic-submode-group-right {
|
| 26 |
margin-left: auto;
|
| 27 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
.semantic-submode-label {
|
|
@@ -53,6 +61,28 @@
|
|
| 53 |
}
|
| 54 |
}
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
// 语义分析区(在 button-group 与 histogram 之间,仅在 semantic_analysis 模式下显示)
|
| 57 |
.semantic-analysis-section {
|
| 58 |
margin-top: 10px;
|
|
|
|
| 25 |
&.semantic-submode-group-right {
|
| 26 |
margin-left: auto;
|
| 27 |
}
|
| 28 |
+
|
| 29 |
+
// 组内「主控」标签/下拉:比默认 submode 更醒目(仅直接子级,避免加粗单位后缀等嵌套 label)
|
| 30 |
+
&.semantic-submode-group--emphasis {
|
| 31 |
+
> .semantic-submode-label,
|
| 32 |
+
> .semantic-submode-select {
|
| 33 |
+
font-weight: 700;
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
}
|
| 37 |
|
| 38 |
.semantic-submode-label {
|
|
|
|
| 61 |
}
|
| 62 |
}
|
| 63 |
|
| 64 |
+
// Analyze 下方 Semantic Query 总开关:与 gen_attribute「Raw prompt mode」同款(未勾选整行灰色,勾选正文色)
|
| 65 |
+
.textarea-wrapper .semantic-submode-row.semantic-analysis-enable-row {
|
| 66 |
+
label.semantic-submode-label {
|
| 67 |
+
display: inline-flex;
|
| 68 |
+
align-items: center;
|
| 69 |
+
gap: 6px;
|
| 70 |
+
cursor: pointer;
|
| 71 |
+
user-select: none;
|
| 72 |
+
color: var(--text-primary, var(--text-color));
|
| 73 |
+
|
| 74 |
+
&:has(> input[type='checkbox']:not(:checked)) {
|
| 75 |
+
color: var(--text-muted);
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
input[type='checkbox'] {
|
| 80 |
+
cursor: pointer;
|
| 81 |
+
margin: 0;
|
| 82 |
+
flex-shrink: 0;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
// 语义分析区(在 button-group 与 histogram 之间,仅在 semantic_analysis 模式下显示)
|
| 87 |
.semantic-analysis-section {
|
| 88 |
margin-top: 10px;
|
client/src/css/_tooltip-vars.scss
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ToolTip 与其它入口共用的 CSS 变量(compare 仅打 compare.css,需与 start 共用此文件,避免重复字面量)
|
| 2 |
+
:root {
|
| 3 |
+
--tooltip-visible-opacity: 1;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
// {@link ToolTipOptions.pointerInteractive} false:整块及子节点均不参与命中(子元素默认 `auto` 会覆盖父 none,必须用 `*`)
|
| 7 |
+
.tooltip.tooltip-no-pointer-hit {
|
| 8 |
+
pointer-events: none !important;
|
| 9 |
+
cursor: default !important;
|
| 10 |
+
|
| 11 |
+
&,
|
| 12 |
+
* {
|
| 13 |
+
pointer-events: none !important;
|
| 14 |
+
}
|
| 15 |
+
}
|
client/src/css/compare.scss
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
@use "lmf-readout" as lmf;
|
| 4 |
@use "demo-list";
|
| 5 |
@use "buttons";
|
|
|
|
| 6 |
|
| 7 |
// 夜间模式:强制使用暗色滚动条
|
| 8 |
:root[data-theme="dark"] {
|
|
@@ -366,6 +367,7 @@
|
|
| 366 |
padding: 5px;
|
| 367 |
background: var(--tooltip-bg);
|
| 368 |
color: var(--text-color);
|
|
|
|
| 369 |
transition-property: opacity, background-color, color;
|
| 370 |
transition-duration: .2s;
|
| 371 |
position: absolute; // 绝对定位
|
|
@@ -374,13 +376,16 @@
|
|
| 374 |
cursor: pointer; // 添加指针样式,提示可点击
|
| 375 |
user-select: none;
|
| 376 |
-webkit-user-select: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
}
|
| 378 |
|
| 379 |
// 全局tooltip容器
|
| 380 |
#global_tooltip {
|
| 381 |
position: fixed; // 使用fixed定位,相对于视口
|
| 382 |
pointer-events: none; // 初始状态不接收事件
|
| 383 |
-
opacity: 0;
|
| 384 |
transition-property: background-color, color;
|
| 385 |
}
|
| 386 |
|
|
|
|
| 3 |
@use "lmf-readout" as lmf;
|
| 4 |
@use "demo-list";
|
| 5 |
@use "buttons";
|
| 6 |
+
@use "tooltip-vars";
|
| 7 |
|
| 8 |
// 夜间模式:强制使用暗色滚动条
|
| 9 |
:root[data-theme="dark"] {
|
|
|
|
| 367 |
padding: 5px;
|
| 368 |
background: var(--tooltip-bg);
|
| 369 |
color: var(--text-color);
|
| 370 |
+
opacity: 0;
|
| 371 |
transition-property: opacity, background-color, color;
|
| 372 |
transition-duration: .2s;
|
| 373 |
position: absolute; // 绝对定位
|
|
|
|
| 376 |
cursor: pointer; // 添加指针样式,提示可点击
|
| 377 |
user-select: none;
|
| 378 |
-webkit-user-select: none;
|
| 379 |
+
|
| 380 |
+
&.tooltip-visible {
|
| 381 |
+
opacity: var(--tooltip-visible-opacity);
|
| 382 |
+
}
|
| 383 |
}
|
| 384 |
|
| 385 |
// 全局tooltip容器
|
| 386 |
#global_tooltip {
|
| 387 |
position: fixed; // 使用fixed定位,相对于视口
|
| 388 |
pointer-events: none; // 初始状态不接收事件
|
|
|
|
| 389 |
transition-property: background-color, color;
|
| 390 |
}
|
| 391 |
|
client/src/css/gen_attribute.scss
CHANGED
|
@@ -47,7 +47,6 @@ $gen-attr-option-row-gap: 12px;
|
|
| 47 |
left: auto;
|
| 48 |
right: 0;
|
| 49 |
width: max-content;
|
| 50 |
-
min-width: max(100%, 240px);
|
| 51 |
max-width: min(100vw - 32px, 40rem);
|
| 52 |
box-sizing: border-box;
|
| 53 |
}
|
|
@@ -62,21 +61,15 @@ $gen-attr-option-row-gap: 12px;
|
|
| 62 |
min-height: 0;
|
| 63 |
width: 100%;
|
| 64 |
box-sizing: border-box;
|
| 65 |
-
padding:
|
| 66 |
background: var(--text-area-bg);
|
| 67 |
color: var(--text-color);
|
| 68 |
overflow: visible;
|
| 69 |
}
|
| 70 |
|
| 71 |
-
@media (max-width: 767px) {
|
| 72 |
-
#results.gen-attr-results-surface.LMF {
|
| 73 |
-
padding: 15px 12px;
|
| 74 |
-
}
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
// DAG:不可见测量层撑开高度,SVG 绝对定位叠在同栈内(坐标与 LMF 正文一致)
|
| 78 |
// 初始 d3.zoom 缩放为 1/本变量,抵消 display-scale 在屏上的整体缩小感(见 genAttributeDagView)
|
| 79 |
-
// 边色:`--dag-
|
| 80 |
#results.gen-attr-results-surface.LMF .gen-attr-dag-stack {
|
| 81 |
// compactness:dag界面紧凑程度,范围 [0.05, 1];1时和普通文本排版一致;由 JS 写入 inline style,0.5 为纯 CSS 降级兜底。
|
| 82 |
--gen-attr-dag-display-scale: var(--gen-attr-dag-compactness, 0.5);
|
|
@@ -96,6 +89,32 @@ $gen-attr-option-row-gap: 12px;
|
|
| 96 |
box-sizing: border-box;
|
| 97 |
}
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
// 勾选「Hide inactive edges」时隐藏未与焦点相邻的灰边(linkG 容器内的边);
|
| 100 |
// 与焦点相邻的高亮边由 genAttributeDagView 运行时搬运到 .gen-attr-dag-links-front 中,不受影响。
|
| 101 |
#results.gen-attr-results-surface.LMF.gen-attr-dag-hide-inactive-edges .gen-attr-dag-svg .gen-attr-dag-links {
|
|
@@ -115,27 +134,54 @@ $gen-attr-option-row-gap: 12px;
|
|
| 115 |
white-space: pre;
|
| 116 |
}
|
| 117 |
|
| 118 |
-
// 节点
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
#results.gen-attr-results-surface.LMF .gen-attr-dag-svg .gen-attr-dag-node > rect {
|
| 122 |
-
stroke: none;
|
| 123 |
-
stroke-width: 0;
|
| 124 |
-
fill: var(--gen-attr-dag-generated-node-fill);
|
| 125 |
}
|
| 126 |
|
| 127 |
-
#results.gen-attr-results-surface.LMF .gen-attr-dag-svg .gen-attr-dag-node--
|
| 128 |
-
fill:
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
|
| 131 |
-
|
| 132 |
-
#results.gen-attr-results-surface.LMF .gen-attr-dag-svg .gen-attr-dag-node
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
}
|
| 136 |
|
| 137 |
-
// 与 genAttributeDagView 中「仅 default(text-flow) 且选中时可拖」一致:可拖时提示 grab
|
| 138 |
-
#results.gen-attr-results-surface.LMF .gen-attr-dag-svg .gen-attr-dag-node--selected
|
|
|
|
| 139 |
cursor: grab;
|
| 140 |
&:active {
|
| 141 |
cursor: grabbing;
|
|
@@ -146,7 +192,11 @@ $gen-attr-option-row-gap: 12px;
|
|
| 146 |
#results.gen-attr-results-surface.LMF
|
| 147 |
.gen-attr-dag-stack.gen-attr-dag-no-node-drag-layout
|
| 148 |
.gen-attr-dag-svg
|
| 149 |
-
.gen-attr-dag-node--selected
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
cursor: default;
|
| 151 |
&:active {
|
| 152 |
cursor: default;
|
|
@@ -394,14 +444,22 @@ body.gen-attribute-page .input-section {
|
|
| 394 |
margin-top: 10px;
|
| 395 |
}
|
| 396 |
|
| 397 |
-
// Start / Model 行与
|
| 398 |
-
.gen-attribute-page
|
| 399 |
-
.input-section
|
| 400 |
-
> .textarea-wrapper.chat-prompt-actions-row
|
| 401 |
-
+ .gen-attr-dag-measure-width-row {
|
| 402 |
margin-top: 30px;
|
| 403 |
}
|
| 404 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
.attribution-exclude-prompt-patterns-header {
|
| 406 |
flex-wrap: wrap;
|
| 407 |
|
|
@@ -441,20 +499,6 @@ body.gen-attribute-page .input-section {
|
|
| 441 |
flex-wrap: nowrap;
|
| 442 |
}
|
| 443 |
|
| 444 |
-
.gen-attr-dag-measure-width-row .gen-attr-dag-layout-mode-group {
|
| 445 |
-
.semantic-submode-label,
|
| 446 |
-
.gen-attr-dag-layout-mode-select {
|
| 447 |
-
font-weight: 700;
|
| 448 |
-
}
|
| 449 |
-
}
|
| 450 |
-
|
| 451 |
-
.gen-attr-dag-measure-width-row .gen-attr-dag-replay-speed-row {
|
| 452 |
-
> .semantic-submode-label,
|
| 453 |
-
.gen-attr-dag-replay-mode-select {
|
| 454 |
-
font-weight: 700;
|
| 455 |
-
}
|
| 456 |
-
}
|
| 457 |
-
|
| 458 |
.gen-attr-dag-replay-speed-row {
|
| 459 |
display: flex;
|
| 460 |
flex-wrap: wrap;
|
|
|
|
| 47 |
left: auto;
|
| 48 |
right: 0;
|
| 49 |
width: max-content;
|
|
|
|
| 50 |
max-width: min(100vw - 32px, 40rem);
|
| 51 |
box-sizing: border-box;
|
| 52 |
}
|
|
|
|
| 61 |
min-height: 0;
|
| 62 |
width: 100%;
|
| 63 |
box-sizing: border-box;
|
| 64 |
+
padding: 6px;
|
| 65 |
background: var(--text-area-bg);
|
| 66 |
color: var(--text-color);
|
| 67 |
overflow: visible;
|
| 68 |
}
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
// DAG:不可见测量层撑开高度,SVG 绝对定位叠在同栈内(坐标与 LMF 正文一致)
|
| 71 |
// 初始 d3.zoom 缩放为 1/本变量,抵消 display-scale 在屏上的整体缩小感(见 genAttributeDagView)
|
| 72 |
+
// 边色与节点追因/选中描边:`--dag-highlight-line-color-in`(accent 80%),见 start.scss。
|
| 73 |
#results.gen-attr-results-surface.LMF .gen-attr-dag-stack {
|
| 74 |
// compactness:dag界面紧凑程度,范围 [0.05, 1];1时和普通文本排版一致;由 JS 写入 inline style,0.5 为纯 CSS 降级兜底。
|
| 75 |
--gen-attr-dag-display-scale: var(--gen-attr-dag-compactness, 0.5);
|
|
|
|
| 89 |
box-sizing: border-box;
|
| 90 |
}
|
| 91 |
|
| 92 |
+
// DAG Top‑K:贴在 #results 内侧右下(absolute);固定尺寸 HUD;Top‑K 区超出则在滚动层内滚动。
|
| 93 |
+
#results.gen-attr-results-surface.LMF > .tooltip.gen-attr-dag-topk-tooltip {
|
| 94 |
+
box-sizing: border-box;
|
| 95 |
+
// 与 #major_tooltip 一致:不参与 opacity 过渡,避免 reposition 后与底层叠影
|
| 96 |
+
transition-property: background-color, color;
|
| 97 |
+
max-width: none;
|
| 98 |
+
width: 14rem;
|
| 99 |
+
height: 300px;
|
| 100 |
+
display: flex;
|
| 101 |
+
flex-direction: column;
|
| 102 |
+
overflow: hidden;
|
| 103 |
+
|
| 104 |
+
.currentToken,
|
| 105 |
+
.myDetail {
|
| 106 |
+
flex-shrink: 0;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.gen-attr-dag-topk-tooltip-predictions-scroll {
|
| 110 |
+
flex: 1 1 auto;
|
| 111 |
+
min-height: 0;
|
| 112 |
+
margin-top: 6px;
|
| 113 |
+
overflow-x: hidden;
|
| 114 |
+
overflow-y: auto;
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
// 勾选「Hide inactive edges」时隐藏未与焦点相邻的灰边(linkG 容器内的边);
|
| 119 |
// 与焦点相邻的高亮边由 genAttributeDagView 运行时搬运到 .gen-attr-dag-links-front 中,不受影响。
|
| 120 |
#results.gen-attr-results-surface.LMF.gen-attr-dag-hide-inactive-edges .gen-attr-dag-svg .gen-attr-dag-links {
|
|
|
|
| 134 |
white-space: pre;
|
| 135 |
}
|
| 136 |
|
| 137 |
+
// 视觉节点不接收指针事件;命中由顶层 .gen-attr-dag-nodes-hit 承担(见 genAttributeDagView)。
|
| 138 |
+
#results.gen-attr-results-surface.LMF .gen-attr-dag-svg .gen-attr-dag-nodes .gen-attr-dag-node {
|
| 139 |
+
pointer-events: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
}
|
| 141 |
|
| 142 |
+
#results.gen-attr-results-surface.LMF .gen-attr-dag-svg .gen-attr-dag-node-hit-target {
|
| 143 |
+
fill: transparent;
|
| 144 |
+
stroke: none;
|
| 145 |
+
pointer-events: all;
|
| 146 |
}
|
| 147 |
|
| 148 |
+
// 节点 fill/stroke 分层:stroke rect 外扩见 syncNodeStrokeRects(屏上描边约 2px)。
|
| 149 |
+
#results.gen-attr-results-surface.LMF .gen-attr-dag-svg .gen-attr-dag-node {
|
| 150 |
+
.gen-attr-dag-node-stroke {
|
| 151 |
+
fill: none;
|
| 152 |
+
stroke: none;
|
| 153 |
+
stroke-width: 0;
|
| 154 |
+
pointer-events: none;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.gen-attr-dag-node-fill {
|
| 158 |
+
stroke: none;
|
| 159 |
+
fill: var(--gen-attr-dag-generated-node-fill);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
&--prompt .gen-attr-dag-node-fill {
|
| 163 |
+
fill: var(--gen-attr-dag-prompt-node-fill);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// 递归追因链上游节点描边(直接模式仅一跳,由蓝/红边表达);opacity 见 genAttributeDagView 中 stay→[0.3,1] 映射。
|
| 167 |
+
&--recursive-chain .gen-attr-dag-node-stroke {
|
| 168 |
+
stroke: var(--dag-highlight-line-color-in);
|
| 169 |
+
stroke-width: calc(2 * var(--gen-attr-dag-display-scale, 1));
|
| 170 |
+
stroke-opacity: var(--gen-attr-dag-node-recursive-share, 1);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// 选中/悬停 = 焦点(追因起点,nodeShare=1);置于 recursive-chain 之后以覆盖链上节点的 stroke-opacity
|
| 174 |
+
&--hover .gen-attr-dag-node-stroke,
|
| 175 |
+
&--selected .gen-attr-dag-node-stroke {
|
| 176 |
+
stroke: var(--dag-highlight-line-color-in);
|
| 177 |
+
stroke-width: calc(2 * var(--gen-attr-dag-display-scale, 1));
|
| 178 |
+
stroke-opacity: 1;
|
| 179 |
+
}
|
| 180 |
}
|
| 181 |
|
| 182 |
+
// 与 genAttributeDagView 中「仅 default(text-flow) 且选中时可拖」一致:可拖时提示 grab(命中层与视觉层共用 class)
|
| 183 |
+
#results.gen-attr-results-surface.LMF .gen-attr-dag-svg .gen-attr-dag-node--selected,
|
| 184 |
+
#results.gen-attr-results-surface.LMF .gen-attr-dag-svg .gen-attr-dag-node-hit.gen-attr-dag-node--selected {
|
| 185 |
cursor: grab;
|
| 186 |
&:active {
|
| 187 |
cursor: grabbing;
|
|
|
|
| 192 |
#results.gen-attr-results-surface.LMF
|
| 193 |
.gen-attr-dag-stack.gen-attr-dag-no-node-drag-layout
|
| 194 |
.gen-attr-dag-svg
|
| 195 |
+
.gen-attr-dag-node--selected,
|
| 196 |
+
#results.gen-attr-results-surface.LMF
|
| 197 |
+
.gen-attr-dag-stack.gen-attr-dag-no-node-drag-layout
|
| 198 |
+
.gen-attr-dag-svg
|
| 199 |
+
.gen-attr-dag-node-hit.gen-attr-dag-node--selected {
|
| 200 |
cursor: default;
|
| 201 |
&:active {
|
| 202 |
cursor: default;
|
|
|
|
| 444 |
margin-top: 10px;
|
| 445 |
}
|
| 446 |
|
| 447 |
+
// Start / Model 行与 Reset UI options 之间略加大;“重置”与下方 DAG layout 一行紧凑
|
| 448 |
+
.gen-attribute-page .input-section > .textarea-wrapper.chat-prompt-actions-row + .gen-attr-reset-ui-options-row {
|
|
|
|
|
|
|
|
|
|
| 449 |
margin-top: 30px;
|
| 450 |
}
|
| 451 |
|
| 452 |
+
.gen-attr-reset-ui-options-row {
|
| 453 |
+
display: flex;
|
| 454 |
+
flex-wrap: wrap;
|
| 455 |
+
align-items: center;
|
| 456 |
+
justify-content: flex-end;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.gen-attr-reset-ui-options-row + .gen-attr-dag-measure-width-row {
|
| 460 |
+
margin-top: 10px;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
.attribution-exclude-prompt-patterns-header {
|
| 464 |
flex-wrap: wrap;
|
| 465 |
|
|
|
|
| 499 |
flex-wrap: nowrap;
|
| 500 |
}
|
| 501 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
.gen-attr-dag-replay-speed-row {
|
| 503 |
display: flex;
|
| 504 |
flex-wrap: wrap;
|
client/src/css/start.scss
CHANGED
|
@@ -12,6 +12,7 @@
|
|
| 12 |
@use "semantic-analysis";
|
| 13 |
@use "attribution-inspector";
|
| 14 |
@use "attribution-sidebar";
|
|
|
|
| 15 |
|
| 16 |
// CSS变量定义 - 日间模式(默认)
|
| 17 |
:root {
|
|
@@ -60,6 +61,11 @@
|
|
| 60 |
--bg-hover: #f5f5f5; // 弹窗内信息块/列表项背景(日间)
|
| 61 |
--bg-hover-light: #f8f8f8; // 弹窗内列表项 hover(日间)
|
| 62 |
--primary-color: #2196F3; // 主题色(链接、当前模型标题等)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
--text-disabled: #999; // 禁用态文本
|
| 64 |
--text-area-bg: #fff; // 文本显示区域背景色
|
| 65 |
--textarea-actions-button-row-min-height: 3.5rem; // textarea 下「按钮 + stats」行最小高度(analysis / chat / attribution / gen_attribute)
|
|
@@ -68,14 +74,14 @@
|
|
| 68 |
--tooltip-text-selected: #933; // tooltip选中文本颜色
|
| 69 |
--tooltip-text-detail: #666666; // tooltip详情文本颜色
|
| 70 |
--tooltip-text-value: #333; // tooltip数值文本颜色
|
| 71 |
-
--token-hover-shadow:
|
| 72 |
-
--token-hover-outline:
|
| 73 |
// 与 rgba(128,128,128,0.252) 叠在 var(--text-area-bg) 上等效(实心,避免多线重叠加深)
|
| 74 |
--dag-normal-line-color: #ddd;
|
| 75 |
-
--dag-highlight-line-color-in:
|
| 76 |
--dag-highlight-line-color-out: rgba(255, 71, 64, 0.5);
|
| 77 |
-
--bin-highlight-outline:
|
| 78 |
-
--bin-highlight-shadow:
|
| 79 |
--avg-line-color: #8c8c8c; // 平均值参考线颜色(日间模式)
|
| 80 |
--text-color-light: #c3c3c3; // 浅色文本颜色(主要用于夜间模式)
|
| 81 |
--token-truncated-color: #888; // 被截断 token 的文本颜色(日间模式)
|
|
@@ -124,7 +130,11 @@ html {
|
|
| 124 |
--hover-bg-color: #3a3a3a; // 菜单项悬浮背景色(夜间模式,更暗)
|
| 125 |
--bg-hover: #2d2d2d; // 弹窗内信息块/列表项背景(夜间)
|
| 126 |
--bg-hover-light: #353535; // 弹窗内列表项 hover(夜间)
|
| 127 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
--text-disabled: #666; // 禁用态文本(夜间)
|
| 129 |
--text-area-bg: #191919; // 夜间模式:文本显示区域背景色与主背景一致
|
| 130 |
--tooltip-text-normal: #ccc; // tooltip普通文本颜色(夜间模式)
|
|
@@ -132,15 +142,13 @@ html {
|
|
| 132 |
--tooltip-text-detail: #888; // tooltip详情文本颜色(夜间模式)
|
| 133 |
--tooltip-text-value: var(--text-color-light); // tooltip数值文本颜色(夜间模式)
|
| 134 |
--token-truncated-color: #888; // 被截断 token 的文本颜色(夜间模式)
|
| 135 |
-
--token-hover-shadow:
|
| 136 |
-
--token-hover-outline:
|
| 137 |
--dag-normal-line-color: #333;
|
| 138 |
-
|
| 139 |
-
// 入边数是有限的,所以透明度高一些,以免过暗
|
| 140 |
-
--dag-highlight-line-color-in: rgba(92, 141, 255, 0.8); // 入边:与 --token-hover-outline #5c8dff 同 hue
|
| 141 |
--dag-highlight-line-color-out: rgba(255, 71, 64, 0.5);
|
| 142 |
-
--bin-highlight-outline:
|
| 143 |
-
--bin-highlight-shadow:
|
| 144 |
--avg-line-color: #b0b0b0; // 平均值参考线颜色(夜间模式)
|
| 145 |
|
| 146 |
// 夜间模式:LMF 区域使用 Light 字重(300)
|
|
@@ -213,12 +221,21 @@ textarea{
|
|
| 213 |
flex-wrap: wrap; // 允许换行
|
| 214 |
|
| 215 |
.button-left {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
display: flex;
|
| 217 |
align-items: center;
|
| 218 |
gap: 10px;
|
| 219 |
-
|
| 220 |
-
position: relative; // 为绝对定位的进度消息提供定位上下文
|
| 221 |
-
|
| 222 |
}
|
| 223 |
|
| 224 |
.button-right {
|
|
@@ -645,7 +662,7 @@ select {
|
|
| 645 |
// drop-shadow(0 0 2px rgba(30, 111, 255, 1))
|
| 646 |
// drop-shadow(0 0 5px rgba(30, 111, 255, 0.85))
|
| 647 |
// drop-shadow(0 0 10px rgba(30, 111, 255, 0.7));
|
| 648 |
-
stroke:
|
| 649 |
stroke-width: 2.5px;
|
| 650 |
}
|
| 651 |
|
|
@@ -656,7 +673,7 @@ select {
|
|
| 656 |
// drop-shadow(0 0 2px rgba(30, 111, 255, 1))
|
| 657 |
// drop-shadow(0 0 5px rgba(30, 111, 255, 0.85))
|
| 658 |
// drop-shadow(0 0 10px rgba(30, 111, 255, 0.7));
|
| 659 |
-
stroke:
|
| 660 |
stroke-width: 2.5px;
|
| 661 |
}
|
| 662 |
}
|
|
@@ -678,7 +695,7 @@ select {
|
|
| 678 |
// drop-shadow(0 0 10px rgba(30, 111, 255, 0.7));
|
| 679 |
// 移动端备用方案:使用 stroke 作为发光效果(iOS Safari filter 支持不佳)
|
| 680 |
// 使用 2.5px 区分于 bin-highlighted 的 1.5px
|
| 681 |
-
stroke:
|
| 682 |
stroke-width: 2.5px;
|
| 683 |
}
|
| 684 |
|
|
@@ -697,7 +714,7 @@ select {
|
|
| 697 |
// drop-shadow(0 0 5px rgba(30, 111, 255, 0.85))
|
| 698 |
// drop-shadow(0 0 10px rgba(30, 111, 255, 0.7));
|
| 699 |
// 覆盖 bin-highlighted 的 stroke,使用更粗的 stroke 表示同时悬浮
|
| 700 |
-
stroke:
|
| 701 |
stroke-width: 2.5px;
|
| 702 |
}
|
| 703 |
}
|
|
@@ -771,6 +788,7 @@ select {
|
|
| 771 |
padding: 5px;
|
| 772 |
background: var(--tooltip-bg);
|
| 773 |
color: var(--text-color);
|
|
|
|
| 774 |
transition-property: opacity, background-color, color;
|
| 775 |
transition-duration: .2s;
|
| 776 |
position: absolute; // 相对于 #results 容器定位,与文本区域使用相同的坐标系统
|
|
@@ -783,6 +801,10 @@ select {
|
|
| 783 |
max-width: min(16rem, calc(100vw - 24px));
|
| 784 |
box-sizing: border-box;
|
| 785 |
overflow-wrap: break-word;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 786 |
}
|
| 787 |
|
| 788 |
// 当前token显示区域,使用等宽字体以便分辨连续的␣(字号继承 .tooltip 9pt,行高与历史一致)
|
|
@@ -981,7 +1003,7 @@ select {
|
|
| 981 |
white-space: nowrap;
|
| 982 |
line-height: 1.2;
|
| 983 |
position: absolute; // 绝对定位,不占用正常布局空间
|
| 984 |
-
left: 100%; // 定位在button-left的右侧
|
| 985 |
margin-left: 30px; // loader宽度(10px) + loader的margin(10px) + 额外间距(10px)
|
| 986 |
z-index: 10; // 确保在上层显示
|
| 987 |
|
|
@@ -1411,7 +1433,7 @@ select {
|
|
| 1411 |
// 加载器小图标容器
|
| 1412 |
.loader-small-container {
|
| 1413 |
position: absolute; // 绝对定位,不占用正常布局空间
|
| 1414 |
-
left: 100%; // 定位在button-left的右侧
|
| 1415 |
margin-left: 10px; // 与左侧元素保持间距
|
| 1416 |
z-index: 10; // 确保在上层显示
|
| 1417 |
}
|
|
@@ -1433,7 +1455,6 @@ select {
|
|
| 1433 |
#major_tooltip {
|
| 1434 |
position: absolute;
|
| 1435 |
pointer-events: none;
|
| 1436 |
-
opacity: 0;
|
| 1437 |
// 不参与 opacity 过渡:否则 hide 时 top/left 已归位而 opacity 仍 >0,会与 fixed 叠成视口角上鬼影
|
| 1438 |
transition-property: background-color, color;
|
| 1439 |
}
|
|
|
|
| 12 |
@use "semantic-analysis";
|
| 13 |
@use "attribution-inspector";
|
| 14 |
@use "attribution-sidebar";
|
| 15 |
+
@use "tooltip-vars";
|
| 16 |
|
| 17 |
// CSS变量定义 - 日间模式(默认)
|
| 18 |
:root {
|
|
|
|
| 61 |
--bg-hover: #f5f5f5; // 弹窗内信息块/列表项背景(日间)
|
| 62 |
--bg-hover-light: #f8f8f8; // 弹窗内列表项 hover(日间)
|
| 63 |
--primary-color: #2196F3; // 主题色(链接、当前模型标题等)
|
| 64 |
+
// 交互高亮蓝(token / bin / DAG 归因份额=1);与 --primary-color 日间 intentionally 区分
|
| 65 |
+
--accent-color: #1e6fff;
|
| 66 |
+
--accent-glow: color-mix(in srgb, var(--accent-color) 60%, transparent);
|
| 67 |
+
--accent-shadow: color-mix(in srgb, var(--accent-color) 65%, transparent);
|
| 68 |
+
--accent-stroke: color-mix(in srgb, var(--accent-color) 90%, transparent);
|
| 69 |
--text-disabled: #999; // 禁用态文本
|
| 70 |
--text-area-bg: #fff; // 文本显示区域背景色
|
| 71 |
--textarea-actions-button-row-min-height: 3.5rem; // textarea 下「按钮 + stats」行最小高度(analysis / chat / attribution / gen_attribute)
|
|
|
|
| 74 |
--tooltip-text-selected: #933; // tooltip选中文本颜色
|
| 75 |
--tooltip-text-detail: #666666; // tooltip详情文本颜色
|
| 76 |
--tooltip-text-value: #333; // tooltip数值文本颜色
|
| 77 |
+
--token-hover-shadow: var(--accent-glow);
|
| 78 |
+
--token-hover-outline: var(--accent-color);
|
| 79 |
// 与 rgba(128,128,128,0.252) 叠在 var(--text-area-bg) 上等效(实心,避免多线重叠加深)
|
| 80 |
--dag-normal-line-color: #ddd;
|
| 81 |
+
--dag-highlight-line-color-in: color-mix(in srgb, var(--accent-color) 80%, transparent);
|
| 82 |
--dag-highlight-line-color-out: rgba(255, 71, 64, 0.5);
|
| 83 |
+
--bin-highlight-outline: var(--accent-color);
|
| 84 |
+
--bin-highlight-shadow: var(--accent-shadow);
|
| 85 |
--avg-line-color: #8c8c8c; // 平均值参考线颜色(日间模式)
|
| 86 |
--text-color-light: #c3c3c3; // 浅色文本颜色(主要用于夜间模式)
|
| 87 |
--token-truncated-color: #888; // 被截断 token 的文本颜色(日间模式)
|
|
|
|
| 130 |
--hover-bg-color: #3a3a3a; // 菜单项悬浮背景色(夜间模式,更暗)
|
| 131 |
--bg-hover: #2d2d2d; // 弹窗内信息块/列表项背景(夜间)
|
| 132 |
--bg-hover-light: #353535; // 弹窗内列表项 hover(夜间)
|
| 133 |
+
--accent-color: #5c8dff;
|
| 134 |
+
--accent-glow: color-mix(in srgb, var(--accent-color) 80%, transparent);
|
| 135 |
+
--accent-shadow: var(--accent-glow);
|
| 136 |
+
--accent-stroke: color-mix(in srgb, var(--accent-color) 90%, transparent);
|
| 137 |
+
--primary-color: var(--accent-color);
|
| 138 |
--text-disabled: #666; // 禁用态文本(夜间)
|
| 139 |
--text-area-bg: #191919; // 夜间模式:文本显示区域背景色与主背景一致
|
| 140 |
--tooltip-text-normal: #ccc; // tooltip普通文本颜色(夜间模式)
|
|
|
|
| 142 |
--tooltip-text-detail: #888; // tooltip详情文本颜色(夜间模式)
|
| 143 |
--tooltip-text-value: var(--text-color-light); // tooltip数值文本颜色(夜间模式)
|
| 144 |
--token-truncated-color: #888; // 被截断 token 的文本颜色(夜间模式)
|
| 145 |
+
--token-hover-shadow: var(--accent-glow);
|
| 146 |
+
--token-hover-outline: var(--accent-color);
|
| 147 |
--dag-normal-line-color: #333;
|
| 148 |
+
--dag-highlight-line-color-in: color-mix(in srgb, var(--accent-color) 80%, transparent);
|
|
|
|
|
|
|
| 149 |
--dag-highlight-line-color-out: rgba(255, 71, 64, 0.5);
|
| 150 |
+
--bin-highlight-outline: var(--accent-color);
|
| 151 |
+
--bin-highlight-shadow: var(--accent-shadow);
|
| 152 |
--avg-line-color: #b0b0b0; // 平均值参考线颜色(夜间模式)
|
| 153 |
|
| 154 |
// 夜间模式:LMF 区域使用 Light 字重(300)
|
|
|
|
| 221 |
flex-wrap: wrap; // 允许换行
|
| 222 |
|
| 223 |
.button-left {
|
| 224 |
+
flex-shrink: 0; // 防止左侧按钮被压缩
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.button-left-stack {
|
| 228 |
+
display: flex;
|
| 229 |
+
flex-direction: column;
|
| 230 |
+
align-items: flex-start;
|
| 231 |
+
gap: 25px; // Analyze 行与下方 Semantic Query 开关间距(略大于常规行距)
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.button-left-primary {
|
| 235 |
display: flex;
|
| 236 |
align-items: center;
|
| 237 |
gap: 10px;
|
| 238 |
+
position: relative; // 为绝对定位的 loader / 进度消息提供定位上下文
|
|
|
|
|
|
|
| 239 |
}
|
| 240 |
|
| 241 |
.button-right {
|
|
|
|
| 662 |
// drop-shadow(0 0 2px rgba(30, 111, 255, 1))
|
| 663 |
// drop-shadow(0 0 5px rgba(30, 111, 255, 0.85))
|
| 664 |
// drop-shadow(0 0 10px rgba(30, 111, 255, 0.7));
|
| 665 |
+
stroke: var(--accent-stroke);
|
| 666 |
stroke-width: 2.5px;
|
| 667 |
}
|
| 668 |
|
|
|
|
| 673 |
// drop-shadow(0 0 2px rgba(30, 111, 255, 1))
|
| 674 |
// drop-shadow(0 0 5px rgba(30, 111, 255, 0.85))
|
| 675 |
// drop-shadow(0 0 10px rgba(30, 111, 255, 0.7));
|
| 676 |
+
stroke: var(--accent-stroke);
|
| 677 |
stroke-width: 2.5px;
|
| 678 |
}
|
| 679 |
}
|
|
|
|
| 695 |
// drop-shadow(0 0 10px rgba(30, 111, 255, 0.7));
|
| 696 |
// 移动端备用方案:使用 stroke 作为发光效果(iOS Safari filter 支持不佳)
|
| 697 |
// 使用 2.5px 区分于 bin-highlighted 的 1.5px
|
| 698 |
+
stroke: var(--accent-stroke);
|
| 699 |
stroke-width: 2.5px;
|
| 700 |
}
|
| 701 |
|
|
|
|
| 714 |
// drop-shadow(0 0 5px rgba(30, 111, 255, 0.85))
|
| 715 |
// drop-shadow(0 0 10px rgba(30, 111, 255, 0.7));
|
| 716 |
// 覆盖 bin-highlighted 的 stroke,使用更粗的 stroke 表示同时悬浮
|
| 717 |
+
stroke: var(--accent-stroke);
|
| 718 |
stroke-width: 2.5px;
|
| 719 |
}
|
| 720 |
}
|
|
|
|
| 788 |
padding: 5px;
|
| 789 |
background: var(--tooltip-bg);
|
| 790 |
color: var(--text-color);
|
| 791 |
+
opacity: 0;
|
| 792 |
transition-property: opacity, background-color, color;
|
| 793 |
transition-duration: .2s;
|
| 794 |
position: absolute; // 相对于 #results 容器定位,与文本区域使用相同的坐标系统
|
|
|
|
| 801 |
max-width: min(16rem, calc(100vw - 24px));
|
| 802 |
box-sizing: border-box;
|
| 803 |
overflow-wrap: break-word;
|
| 804 |
+
|
| 805 |
+
&.tooltip-visible {
|
| 806 |
+
opacity: var(--tooltip-visible-opacity);
|
| 807 |
+
}
|
| 808 |
}
|
| 809 |
|
| 810 |
// 当前token显示区域,使用等宽字体以便分辨连续的␣(字号继承 .tooltip 9pt,行高与历史一致)
|
|
|
|
| 1003 |
white-space: nowrap;
|
| 1004 |
line-height: 1.2;
|
| 1005 |
position: absolute; // 绝对定位,不占用正常布局空间
|
| 1006 |
+
left: 100%; // 定位在 button-left-primary(Analyze 行)的右侧
|
| 1007 |
margin-left: 30px; // loader宽度(10px) + loader的margin(10px) + 额外间距(10px)
|
| 1008 |
z-index: 10; // 确保在上层显示
|
| 1009 |
|
|
|
|
| 1433 |
// 加载器小图标容器
|
| 1434 |
.loader-small-container {
|
| 1435 |
position: absolute; // 绝对定位,不占用正常布局空间
|
| 1436 |
+
left: 100%; // 定位在 button-left-primary(Analyze 行)的右侧
|
| 1437 |
margin-left: 10px; // 与左侧元素保持间距
|
| 1438 |
z-index: 10; // 确保在上层显示
|
| 1439 |
}
|
|
|
|
| 1455 |
#major_tooltip {
|
| 1456 |
position: absolute;
|
| 1457 |
pointer-events: none;
|
|
|
|
| 1458 |
// 不参与 opacity 过渡:否则 hide 时 top/left 已归位而 opacity 仍 >0,会与 fixed 叠成视口角上鬼影
|
| 1459 |
transition-property: background-color, color;
|
| 1460 |
}
|
client/src/demos/gen_attribute/CN->EN翻译.json
CHANGED
|
@@ -13404,7 +13404,7 @@
|
|
| 13404 |
"hideExcludedTokens": true,
|
| 13405 |
"edgeTopPCoverage": 0.1,
|
| 13406 |
"nodeCiVisualScaleEnabled": true,
|
| 13407 |
-
"
|
| 13408 |
"hideInactiveEdges": false,
|
| 13409 |
"replayPacingMode": "step",
|
| 13410 |
"playbackTotalS": 7,
|
|
|
|
| 13404 |
"hideExcludedTokens": true,
|
| 13405 |
"edgeTopPCoverage": 0.1,
|
| 13406 |
"nodeCiVisualScaleEnabled": true,
|
| 13407 |
+
"decayAttributionToHighSurprisalTargetEnabled": true,
|
| 13408 |
"hideInactiveEdges": false,
|
| 13409 |
"replayPacingMode": "step",
|
| 13410 |
"playbackTotalS": 7,
|
client/src/demos/gen_attribute/CoT | 苏州所在省的省会.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
client/src/demos/gen_attribute/Write a sonnet about love.json
CHANGED
|
@@ -117407,7 +117407,7 @@
|
|
| 117407 |
"hideExcludedTokens": false,
|
| 117408 |
"edgeTopPCoverage": 0.7,
|
| 117409 |
"nodeCiVisualScaleEnabled": true,
|
| 117410 |
-
"
|
| 117411 |
"hideInactiveEdges": false,
|
| 117412 |
"replayPacingMode": "step",
|
| 117413 |
"playbackTotalS": 7,
|
|
|
|
| 117407 |
"hideExcludedTokens": false,
|
| 117408 |
"edgeTopPCoverage": 0.7,
|
| 117409 |
"nodeCiVisualScaleEnabled": true,
|
| 117410 |
+
"decayAttributionToHighSurprisalTargetEnabled": true,
|
| 117411 |
"hideInactiveEdges": false,
|
| 117412 |
"replayPacingMode": "step",
|
| 117413 |
"playbackTotalS": 7,
|
client/src/demos/gen_attribute/写一首绝句,主题是春天.json
CHANGED
|
@@ -15973,7 +15973,7 @@
|
|
| 15973 |
"hideExcludedTokens": false,
|
| 15974 |
"edgeTopPCoverage": 0.7,
|
| 15975 |
"nodeCiVisualScaleEnabled": true,
|
| 15976 |
-
"
|
| 15977 |
"hideInactiveEdges": false,
|
| 15978 |
"replayPacingMode": "step",
|
| 15979 |
"playbackTotalS": 7,
|
|
|
|
| 15973 |
"hideExcludedTokens": false,
|
| 15974 |
"edgeTopPCoverage": 0.7,
|
| 15975 |
"nodeCiVisualScaleEnabled": true,
|
| 15976 |
+
"decayAttributionToHighSurprisalTargetEnabled": true,
|
| 15977 |
"hideInactiveEdges": false,
|
| 15978 |
"replayPacingMode": "step",
|
| 15979 |
"playbackTotalS": 7,
|
client/src/demos/gen_attribute/过拟合|李白 将进酒.json
CHANGED
|
@@ -154547,7 +154547,7 @@
|
|
| 154547 |
"hideExcludedTokens": true,
|
| 154548 |
"edgeTopPCoverage": 0.5,
|
| 154549 |
"nodeCiVisualScaleEnabled": true,
|
| 154550 |
-
"
|
| 154551 |
"hideInactiveEdges": false,
|
| 154552 |
"replayPacingMode": "step",
|
| 154553 |
"playbackTotalS": 7,
|
|
|
|
| 154547 |
"hideExcludedTokens": true,
|
| 154548 |
"edgeTopPCoverage": 0.5,
|
| 154549 |
"nodeCiVisualScaleEnabled": true,
|
| 154550 |
+
"decayAttributionToHighSurprisalTargetEnabled": true,
|
| 154551 |
"hideInactiveEdges": false,
|
| 154552 |
"replayPacingMode": "step",
|
| 154553 |
"playbackTotalS": 7,
|
client/src/gen_attribute.html
CHANGED
|
@@ -174,12 +174,14 @@
|
|
| 174 |
</div>
|
| 175 |
<div class="button-group">
|
| 176 |
<div class="button-left">
|
| 177 |
-
<
|
| 178 |
-
|
| 179 |
-
<div class="
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
| 181 |
</div>
|
| 182 |
-
<span id="gen_attr_analyze_progress" class="analyze-progress"></span>
|
| 183 |
</div>
|
| 184 |
<div id="gen_attr_text_metrics" class="text-metrics text-metrics-chat">
|
| 185 |
<div id="gen_attr_metric_usage" class="text-metrics-secondary"></div>
|
|
@@ -189,15 +191,24 @@
|
|
| 189 |
</div>
|
| 190 |
</div>
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 193 |
-
<span class="semantic-submode-group
|
| 194 |
<label class="semantic-submode-label" for="gen_attr_dag_layout_mode">DAG layout mode</label>
|
| 195 |
<select id="gen_attr_dag_layout_mode"
|
| 196 |
class="semantic-submode-select gen-attr-dag-layout-mode-select"
|
| 197 |
-
title="Choose DAG layout mode. 'text-flow' follows text layout geometry; 'linear-arc' uses fixed-order linear nodes with arc links; 'spiral' lays nodes on an Archimedean spiral (for fun)."
|
| 198 |
data-i18n="title">
|
| 199 |
<option value="text-flow">text-flow</option>
|
| 200 |
<option value="linear-arc">linear-arc</option>
|
|
|
|
| 201 |
<option value="spiral">spiral (for fun)</option>
|
| 202 |
</select>
|
| 203 |
</span>
|
|
@@ -220,7 +231,7 @@
|
|
| 220 |
<label class="semantic-submode-label" for="gen_attr_dag_linear_arc_interval" data-i18n>Token distance</label>
|
| 221 |
<input type="number" id="gen_attr_dag_linear_arc_interval" class="gen-attr-dag-measure-width-input"
|
| 222 |
value="0" min="0" max="400" step="1"
|
| 223 |
-
title="Horizontal gap (px) between the outer left/right edges of adjacent token nodes in linear-arc layout only. When idle, the DAG refits; during generation or DAG playback, the value is stored and applied on the next sync."
|
| 224 |
data-i18n="title">
|
| 225 |
<span class="semantic-submode-label">px</span>
|
| 226 |
</span>
|
|
@@ -236,27 +247,35 @@
|
|
| 236 |
</span>
|
| 237 |
<span class="semantic-submode-group">
|
| 238 |
<label class="semantic-submode-label">
|
| 239 |
-
<input type="checkbox" id="
|
| 240 |
-
title="When checked,
|
| 241 |
data-i18n="title">
|
| 242 |
-
|
| 243 |
</label>
|
| 244 |
</span>
|
| 245 |
</div>
|
| 246 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 247 |
<span class="semantic-submode-group">
|
| 248 |
-
<label class="semantic-submode-label" for="gen_attr_dag_edge_top_p_coverage" data-i18n>
|
| 249 |
<input type="number" id="gen_attr_dag_edge_top_p_coverage" class="gen-attr-dag-measure-width-input"
|
| 250 |
value="0.7" min="0.05" max="1" step="0.05"
|
| 251 |
title="Coverage is the cumulative mass share within each generation step's Top-N candidate pool (after sorting candidates into the pool and normalizing mass inside that pool). Higher values keep more incoming edges. The denominator is this pool only, not every token-attribution entry returned for the step."
|
| 252 |
data-i18n="title">
|
| 253 |
</span>
|
| 254 |
-
<span class="semantic-submode-group">
|
| 255 |
<label class="semantic-submode-label">
|
| 256 |
-
<input type="checkbox" id="
|
| 257 |
-
title="
|
| 258 |
data-i18n="title">
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
</label>
|
| 261 |
</span>
|
| 262 |
</div>
|
|
@@ -315,7 +334,27 @@
|
|
| 315 |
</span>
|
| 316 |
</div>
|
| 317 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 318 |
-
<span class="semantic-submode-group
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
<label class="semantic-submode-label" for="gen_attr_dag_replay_mode" data-i18n>DAG replay speed</label>
|
| 320 |
<select id="gen_attr_dag_replay_mode"
|
| 321 |
class="semantic-submode-select gen-attr-dag-replay-mode-select"
|
|
|
|
| 174 |
</div>
|
| 175 |
<div class="button-group">
|
| 176 |
<div class="button-left">
|
| 177 |
+
<div class="button-left-primary">
|
| 178 |
+
<button type="button" id="gen_attr_submit_btn" class="primary-btn inactive" disabled>Start</button>
|
| 179 |
+
<div class="generation-status-slot loader-small-container">
|
| 180 |
+
<div class="loadersmall"></div>
|
| 181 |
+
<span id="gen_attr_complete_reason" class="generation-end-reason"></span>
|
| 182 |
+
</div>
|
| 183 |
+
<span id="gen_attr_analyze_progress" class="analyze-progress"></span>
|
| 184 |
</div>
|
|
|
|
| 185 |
</div>
|
| 186 |
<div id="gen_attr_text_metrics" class="text-metrics text-metrics-chat">
|
| 187 |
<div id="gen_attr_metric_usage" class="text-metrics-secondary"></div>
|
|
|
|
| 191 |
</div>
|
| 192 |
</div>
|
| 193 |
|
| 194 |
+
<div class="gen-attr-reset-ui-options-row">
|
| 195 |
+
<button type="button" id="gen_attr_reset_ui_options_btn" class="text-action-btn"
|
| 196 |
+
title="Restore DAG options, replay speed, exclusions, etc. to defaults and clear saved preferences for those controls."
|
| 197 |
+
data-i18n="text,title">
|
| 198 |
+
Reset UI options
|
| 199 |
+
</button>
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 203 |
+
<span class="semantic-submode-group semantic-submode-group--emphasis">
|
| 204 |
<label class="semantic-submode-label" for="gen_attr_dag_layout_mode">DAG layout mode</label>
|
| 205 |
<select id="gen_attr_dag_layout_mode"
|
| 206 |
class="semantic-submode-select gen-attr-dag-layout-mode-select"
|
| 207 |
+
title="Choose DAG layout mode. 'text-flow' follows text layout geometry; 'linear-arc' uses fixed-order linear nodes with arc links; 'linear-arc-step-down' is like linear-arc but drops each successive token vertically by CI (baseline = unscaled node height); 'spiral' lays nodes on an Archimedean spiral (for fun)."
|
| 208 |
data-i18n="title">
|
| 209 |
<option value="text-flow">text-flow</option>
|
| 210 |
<option value="linear-arc">linear-arc</option>
|
| 211 |
+
<option value="linear-arc-step-down">step-down</option>
|
| 212 |
<option value="spiral">spiral (for fun)</option>
|
| 213 |
</select>
|
| 214 |
</span>
|
|
|
|
| 231 |
<label class="semantic-submode-label" for="gen_attr_dag_linear_arc_interval" data-i18n>Token distance</label>
|
| 232 |
<input type="number" id="gen_attr_dag_linear_arc_interval" class="gen-attr-dag-measure-width-input"
|
| 233 |
value="0" min="0" max="400" step="1"
|
| 234 |
+
title="Horizontal gap (px) between the outer left/right edges of adjacent token nodes in linear-arc / linear-arc-step-down layout only. When idle, the DAG refits; during generation or DAG playback, the value is stored and applied on the next sync."
|
| 235 |
data-i18n="title">
|
| 236 |
<span class="semantic-submode-label">px</span>
|
| 237 |
</span>
|
|
|
|
| 247 |
</span>
|
| 248 |
<span class="semantic-submode-group">
|
| 249 |
<label class="semantic-submode-label">
|
| 250 |
+
<input type="checkbox" id="gen_attr_dag_decay_attribution_high_surprisal" checked
|
| 251 |
+
title="When checked, recursive trace decays propagated attribution at high-surprisal generated tokens (MI discount), so surprising or teacher-forced tokens can act as sources. Unchecked: all generated tokens are transparent conduits; trace continues to prompt only."
|
| 252 |
data-i18n="title">
|
| 253 |
+
Decay attribution to high-surprisal targets
|
| 254 |
</label>
|
| 255 |
</span>
|
| 256 |
</div>
|
| 257 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 258 |
<span class="semantic-submode-group">
|
| 259 |
+
<label class="semantic-submode-label" for="gen_attr_dag_edge_top_p_coverage" data-i18n>Attribution top-p coverage</label>
|
| 260 |
<input type="number" id="gen_attr_dag_edge_top_p_coverage" class="gen-attr-dag-measure-width-input"
|
| 261 |
value="0.7" min="0.05" max="1" step="0.05"
|
| 262 |
title="Coverage is the cumulative mass share within each generation step's Top-N candidate pool (after sorting candidates into the pool and normalizing mass inside that pool). Higher values keep more incoming edges. The denominator is this pool only, not every token-attribution entry returned for the step."
|
| 263 |
data-i18n="title">
|
| 264 |
</span>
|
| 265 |
+
<span class="semantic-submode-group semantic-submode-group--emphasis">
|
| 266 |
<label class="semantic-submode-label">
|
| 267 |
+
<input type="checkbox" id="gen_attr_dag_recursive_attribution"
|
| 268 |
+
title="Trace from the focused token back to information sources (not one hop). Sources: prompt; surprising or teacher-forced generated tokens (MI decay stops the chain). Conduits: high-confidence middle tokens—attribution passes through. Blue edges: propagated share; node ring: attribution stay (strong where explanation lands). Use with Decay attribution to high-surprisal targets. Off = direct mode (immediate predecessors only)."
|
| 269 |
data-i18n="title">
|
| 270 |
+
Propagated attribution mode
|
| 271 |
+
</label>
|
| 272 |
+
</span>
|
| 273 |
+
<span class="semantic-submode-group" id="gen_attr_dag_show_downstream_influence_group">
|
| 274 |
+
<label class="semantic-submode-label">
|
| 275 |
+
<input type="checkbox" id="gen_attr_dag_show_downstream_influence"
|
| 276 |
+
title="When checked, direct attribution focus also shows outgoing edges from the selected or hovered token as downstream influence. Propagated attribution keeps showing upstream attribution chains only."
|
| 277 |
+
data-i18n="title">
|
| 278 |
+
Show downstream influence
|
| 279 |
</label>
|
| 280 |
</span>
|
| 281 |
</div>
|
|
|
|
| 334 |
</span>
|
| 335 |
</div>
|
| 336 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 337 |
+
<span class="semantic-submode-group">
|
| 338 |
+
<label class="semantic-submode-label">
|
| 339 |
+
<input type="checkbox" id="gen_attr_dag_hide_inactive_edges"
|
| 340 |
+
title="When checked, gray DAG edges not adjacent to the hovered or selected node are hidden."
|
| 341 |
+
data-i18n="title">
|
| 342 |
+
Hide inactive edges
|
| 343 |
+
</label>
|
| 344 |
+
</span>
|
| 345 |
+
</div>
|
| 346 |
+
<div class="gen-attr-dag-measure-width-row semantic-submode-row gen-attr-dag-show-topk-on-selected-row">
|
| 347 |
+
<span class="semantic-submode-group">
|
| 348 |
+
<label class="semantic-submode-label">
|
| 349 |
+
<input type="checkbox" id="gen_attr_dag_show_topk_on_selected"
|
| 350 |
+
title="When checked, selecting or hovering a token node shows token information in the results area."
|
| 351 |
+
data-i18n="title">
|
| 352 |
+
<span data-i18n>Show token tooltip</span>
|
| 353 |
+
</label>
|
| 354 |
+
</span>
|
| 355 |
+
</div>
|
| 356 |
+
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 357 |
+
<span class="semantic-submode-group gen-attr-dag-replay-speed-row semantic-submode-group--emphasis">
|
| 358 |
<label class="semantic-submode-label" for="gen_attr_dag_replay_mode" data-i18n>DAG replay speed</label>
|
| 359 |
<select id="gen_attr_dag_replay_mode"
|
| 360 |
class="semantic-submode-select gen-attr-dag-replay-mode-select"
|
client/src/partials/settings-menu-analysis.html
CHANGED
|
@@ -4,12 +4,6 @@
|
|
| 4 |
<input type="checkbox" id="enable_minimap_toggle">
|
| 5 |
</label>
|
| 6 |
</div>
|
| 7 |
-
<div id="semantic_analysis_item" class="settings-menu-item settings-menu-row">
|
| 8 |
-
<span class="settings-menu-label">Semantic analysis (Beta):</span>
|
| 9 |
-
<label class="settings-menu-control settings-menu-checkbox" style="cursor: pointer;">
|
| 10 |
-
<input type="checkbox" id="semantic_analysis_toggle">
|
| 11 |
-
</label>
|
| 12 |
-
</div>
|
| 13 |
<div id="token_render_style_item" class="settings-menu-item settings-menu-row" data-admin-only style="display: none;">
|
| 14 |
<span class="settings-menu-label">Token render style:</span>
|
| 15 |
<div id="token_render_style_dropdown" class="settings-menu-control settings-dropdown-in-menu"></div>
|
|
|
|
| 4 |
<input type="checkbox" id="enable_minimap_toggle">
|
| 5 |
</label>
|
| 6 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
<div id="token_render_style_item" class="settings-menu-item settings-menu-row" data-admin-only style="display: none;">
|
| 8 |
<span class="settings-menu-label">Token render style:</span>
|
| 9 |
<div id="token_render_style_dropdown" class="settings-menu-control settings-dropdown-in-menu"></div>
|
client/src/ts/api/GLTR_API.ts
CHANGED
|
@@ -244,6 +244,7 @@ export class TextAnalysisAPI {
|
|
| 244 |
os: Record<string, number>,
|
| 245 |
page_sec: Record<string, number>,
|
| 246 |
api: Record<string, number>,
|
|
|
|
| 247 |
saved_at: string | null,
|
| 248 |
process_start_at?: string | null,
|
| 249 |
startup_base?: {
|
|
@@ -252,6 +253,7 @@ export class TextAnalysisAPI {
|
|
| 252 |
page_sec?: Record<string, number>,
|
| 253 |
api?: Record<string, number>,
|
| 254 |
os?: Record<string, number>,
|
|
|
|
| 255 |
},
|
| 256 |
reset_base?: {
|
| 257 |
page_loads?: number,
|
|
@@ -259,6 +261,7 @@ export class TextAnalysisAPI {
|
|
| 259 |
page_sec?: Record<string, number>,
|
| 260 |
api?: Record<string, number>,
|
| 261 |
os?: Record<string, number>,
|
|
|
|
| 262 |
},
|
| 263 |
reset_at?: string | null,
|
| 264 |
}> {
|
|
|
|
| 244 |
os: Record<string, number>,
|
| 245 |
page_sec: Record<string, number>,
|
| 246 |
api: Record<string, number>,
|
| 247 |
+
gen_attr_opt_sec?: Record<string, number>,
|
| 248 |
saved_at: string | null,
|
| 249 |
process_start_at?: string | null,
|
| 250 |
startup_base?: {
|
|
|
|
| 253 |
page_sec?: Record<string, number>,
|
| 254 |
api?: Record<string, number>,
|
| 255 |
os?: Record<string, number>,
|
| 256 |
+
gen_attr_opt_sec?: Record<string, number>,
|
| 257 |
},
|
| 258 |
reset_base?: {
|
| 259 |
page_loads?: number,
|
|
|
|
| 261 |
page_sec?: Record<string, number>,
|
| 262 |
api?: Record<string, number>,
|
| 263 |
os?: Record<string, number>,
|
| 264 |
+
gen_attr_opt_sec?: Record<string, number>,
|
| 265 |
},
|
| 266 |
reset_at?: string | null,
|
| 267 |
}> {
|
client/src/ts/attribution/genAttributeDagEdgeDisplay.ts
CHANGED
|
@@ -1,8 +1,24 @@
|
|
| 1 |
/**
|
| 2 |
-
*
|
| 3 |
-
*
|
| 4 |
-
*
|
| 5 |
-
* 与同数值在 `genAttributeDagPreprocess.ts` 池内前缀选取里 `relativeFloor = 常数 × topFrac` 复用:
|
| 6 |
-
* max 归一后首条 `normalizedScore === 1`,故低于该相对份额的条目不可能在 MI≤1 下达到本阈值,属提前筛除。
|
| 7 |
*/
|
| 8 |
-
export const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/**
|
| 2 |
+
* 建边剪枝:候选池内 max 归一后的 `normalizedScore` 低于该值不连边;
|
| 3 |
+
* decay 开启时再要求 `mutualInformationRatio × normalizedScore` 不低于本值。
|
| 4 |
+
* 与 {@link DAG_EDGE_RENDER_OPACITY_FLOOR} 独立;数值可分别调整。
|
|
|
|
|
|
|
| 5 |
*/
|
| 6 |
+
export const DAG_EDGE_MIN_NORMALIZED_SCORE = 0.1;
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* 焦点传播 / 高亮剪枝:传播后的节点份额、边份额、下游强度低于该阈值的边/节点不参与追因高亮。
|
| 10 |
+
*/
|
| 11 |
+
export const DAG_MIN_ATTRIBUTION_SHARE = 0.01;
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* 归一后 `stroke-opacity` 的显示下限(`Math.max(本常数, scaled)`):弱边被抬高到此值,而非滤掉。
|
| 15 |
+
* 与 {@link DAG_EDGE_MIN_NORMALIZED_SCORE}(低于则剪枝)语义相反。
|
| 16 |
+
*/
|
| 17 |
+
export const DAG_EDGE_RENDER_OPACITY_FLOOR = 0.1;
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* 递归链候选节点描边 `stroke-opacity` 下限:`stay / max(stay)` 线性映射到 `[本值, 1]`。
|
| 21 |
+
* 弱 stay 若直接当 opacity,在链上 max(stay) 较大时会接近 0、描边几乎看不见;抬高下限保留相对强弱对比。
|
| 22 |
+
* 与 {@link DAG_EDGE_RENDER_OPACITY_FLOOR}(边)独立。
|
| 23 |
+
*/
|
| 24 |
+
export const DAG_NODE_STROKE_OPACITY_BASE = 0.3;
|
client/src/ts/attribution/genAttributeDagPreprocess.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
| 5 |
import type { NodeAggregatedEntry } from './genAttributeDagIntervalResolve';
|
| 6 |
import type { TokenGenStep } from './tokenGenAttributionRunner';
|
| 7 |
import { getAttentionRawScore } from '../utils/semanticUtils';
|
| 8 |
-
import {
|
| 9 |
|
| 10 |
/** 与 DAG 节点 id 一致:来自 API `token_attribution` 几何(按 offset 去重,独立于 exclude/归一化)。 */
|
| 11 |
export type PromptTokenSpan = {
|
|
@@ -58,38 +58,29 @@ function normalizeTopNPoolForDagSparse<T extends { score: number }>(tokens: T[])
|
|
| 58 |
}
|
| 59 |
|
| 60 |
/**
|
| 61 |
-
* 在候选池已按 `score` 降序、池内归一
|
| 62 |
-
* -
|
| 63 |
* - 累计达到给定阈值(默认 {@link DAG_EDGE_TOP_P_COVERAGE_DEFAULT};候选池内 Top-P,非整步全量 token 的分母)。
|
| 64 |
-
* (池内份额与 `score` 单调一致,无需再排序。)
|
| 65 |
-
*
|
| 66 |
-
* `relativeFloor`:{@link normalizeTopNPoolForDagSparse} 后首条 `normalizedScore === 1`,且对正分条目有
|
| 67 |
-
* `poolMassFrac_i / topFrac === normalizedScore_i`。故 `frac < β×topFrac` ⇔ `normalizedScore < β`;
|
| 68 |
-
* 再乘互信息率(≤1)后不可能达到视图层最小 `stroke-opacity`,等于提前剔除注定画不出的边,与
|
| 69 |
-
* {@link DAG_EDGE_MIN_DISPLAY_OPACITY} 在视图中的含义对齐。
|
| 70 |
*/
|
| 71 |
-
function selectTokenAttributionByCumulativeShare<T extends { poolMassFrac: number }>(
|
| 72 |
normalized: Array<T>,
|
| 73 |
cumulativeShareThreshold: number,
|
| 74 |
): Array<T> {
|
| 75 |
if (normalized.length === 0) return [];
|
| 76 |
-
|
| 77 |
-
const topFrac = normalized[0]?.poolMassFrac ?? 0;
|
| 78 |
-
if (!(topFrac > 0)) return [];
|
| 79 |
-
const relativeFloor = DAG_EDGE_MIN_DISPLAY_OPACITY * topFrac;
|
| 80 |
|
| 81 |
let cum = 0;
|
| 82 |
const picked: Array<T> = [];
|
| 83 |
for (const t of normalized) {
|
| 84 |
-
|
| 85 |
-
if (!(frac > 0)) {
|
| 86 |
break;
|
| 87 |
}
|
| 88 |
-
|
|
|
|
| 89 |
break;
|
| 90 |
}
|
| 91 |
picked.push(t);
|
| 92 |
-
cum +=
|
| 93 |
if (cum >= cumulativeShareThreshold) {
|
| 94 |
break;
|
| 95 |
}
|
|
|
|
| 5 |
import type { NodeAggregatedEntry } from './genAttributeDagIntervalResolve';
|
| 6 |
import type { TokenGenStep } from './tokenGenAttributionRunner';
|
| 7 |
import { getAttentionRawScore } from '../utils/semanticUtils';
|
| 8 |
+
import { DAG_EDGE_MIN_NORMALIZED_SCORE } from './genAttributeDagEdgeDisplay';
|
| 9 |
|
| 10 |
/** 与 DAG 节点 id 一致:来自 API `token_attribution` 几何(按 offset 去重,独立于 exclude/归一化)。 */
|
| 11 |
export type PromptTokenSpan = {
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
/**
|
| 61 |
+
* 在候选池已按 `score` 降序、池内 max 归一(`score` 即 `normalizedScore`)的前提下,按遍历顺序取前缀,直到:
|
| 62 |
+
* - `normalizedScore < {@link DAG_EDGE_MIN_NORMALIZED_SCORE}`,或
|
| 63 |
* - 累计达到给定阈值(默认 {@link DAG_EDGE_TOP_P_COVERAGE_DEFAULT};候选池内 Top-P,非整步全量 token 的分母)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
*/
|
| 65 |
+
function selectTokenAttributionByCumulativeShare<T extends { score: number; poolMassFrac: number }>(
|
| 66 |
normalized: Array<T>,
|
| 67 |
cumulativeShareThreshold: number,
|
| 68 |
): Array<T> {
|
| 69 |
if (normalized.length === 0) return [];
|
| 70 |
+
if (!(normalized[0]!.poolMassFrac > 0)) return [];
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
let cum = 0;
|
| 73 |
const picked: Array<T> = [];
|
| 74 |
for (const t of normalized) {
|
| 75 |
+
if (!(t.poolMassFrac > 0)) {
|
|
|
|
| 76 |
break;
|
| 77 |
}
|
| 78 |
+
const normalizedScore = t.score;
|
| 79 |
+
if (normalizedScore < DAG_EDGE_MIN_NORMALIZED_SCORE) {
|
| 80 |
break;
|
| 81 |
}
|
| 82 |
picked.push(t);
|
| 83 |
+
cum += t.poolMassFrac;
|
| 84 |
if (cum >= cumulativeShareThreshold) {
|
| 85 |
break;
|
| 86 |
}
|
client/src/ts/attribution/genAttributeDagTopkToken.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { FrontendToken } from '../api/GLTR_API';
|
| 2 |
+
import type { TokenGenStep } from './tokenGenAttributionRunner';
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* 将单步生成归因结果转为与 {@link ToolTip} / Top‑K 条形图一致的 {@link FrontendToken}。
|
| 6 |
+
* `debug_info.topk_*` 来自 `/api/prediction-attribute`,与语义分析同源。
|
| 7 |
+
*/
|
| 8 |
+
export function frontendTokenFromGenAttrStep(step: TokenGenStep): FrontendToken | null {
|
| 9 |
+
const raw = step.token;
|
| 10 |
+
if (!raw) return null;
|
| 11 |
+
|
| 12 |
+
const start = step.context.length;
|
| 13 |
+
const end = start + raw.length;
|
| 14 |
+
const prob = step.response.target_prob;
|
| 15 |
+
const real_topk =
|
| 16 |
+
prob != null && Number.isFinite(prob) ? ([0, prob] as [number, number]) : undefined;
|
| 17 |
+
|
| 18 |
+
const dbg = step.response.debug_info;
|
| 19 |
+
let pred_topk: [string, number][] = [];
|
| 20 |
+
if (dbg?.topk_tokens?.length && dbg?.topk_probs?.length) {
|
| 21 |
+
const n = Math.min(dbg.topk_tokens.length, dbg.topk_probs.length);
|
| 22 |
+
for (let i = 0; i < n; i++) {
|
| 23 |
+
const t = dbg.topk_tokens[i]!;
|
| 24 |
+
const p = dbg.topk_probs[i]!;
|
| 25 |
+
if (typeof t === 'string' && typeof p === 'number' && Number.isFinite(p)) {
|
| 26 |
+
pred_topk.push([t, p]);
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
return {
|
| 32 |
+
offset: [start, end],
|
| 33 |
+
raw,
|
| 34 |
+
pred_topk,
|
| 35 |
+
...(real_topk !== undefined ? { real_topk } : {}),
|
| 36 |
+
};
|
| 37 |
+
}
|
client/src/ts/attribution/genAttributeDagView.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import * as d3 from 'd3';
|
| 2 |
import { DirectedGraph } from 'graphology';
|
| 3 |
-
import
|
| 4 |
import { visualizeSpecialChars } from '../utils/tokenDisplayUtils';
|
| 5 |
import {
|
| 6 |
clampDagEdgeTopPCoverage,
|
|
@@ -10,10 +10,17 @@ import {
|
|
| 10 |
phase2RankAndSparsify,
|
| 11 |
type PromptTokenSpan,
|
| 12 |
} from './genAttributeDagPreprocess';
|
| 13 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
import {
|
| 15 |
computeMutualInformationRatio,
|
| 16 |
computeConditionalInformationRatio,
|
|
|
|
|
|
|
| 17 |
FULL_CONFIDENCE_PROBABILITY_BASELINE,
|
| 18 |
} from '../utils/surprisalMath';
|
| 19 |
import { isOffsetSpanFullyExcluded } from './attributionDisplayModel';
|
|
@@ -23,8 +30,12 @@ import {
|
|
| 23 |
type NodeInterval,
|
| 24 |
type PieceEntry,
|
| 25 |
} from './genAttributeDagIntervalResolve';
|
|
|
|
| 26 |
import type { TokenGenStep } from './tokenGenAttributionRunner';
|
| 27 |
import { createGenAttributeDagTextMeasure } from './genAttributeDagTextMeasure';
|
|
|
|
|
|
|
|
|
|
| 28 |
import { formatTopkTooltipProbabilityPercent } from '../utils/topkChartUtils';
|
| 29 |
import {
|
| 30 |
CSS_PSEUDO_FULLSCREEN_CHANGE_EVENT,
|
|
@@ -38,20 +49,22 @@ import {
|
|
| 38 |
LINEAR_ARC_ADJACENT_GAP_MAX,
|
| 39 |
LINEAR_ARC_ADJACENT_GAP_MIN,
|
| 40 |
LINEAR_ARC_BEZIER_HANDLE_INSET_FRACTION,
|
|
|
|
| 41 |
paintLinearArcLayout,
|
| 42 |
} from './genAttributeDagViewLinearArcMode';
|
| 43 |
import { paintTextFlowLayout } from './genAttributeDagViewTextFlowMode';
|
| 44 |
import { paintSpiralLayout } from './genAttributeDagViewSpiralMode';
|
| 45 |
import { tr } from '../lang/i18n-lite';
|
| 46 |
|
| 47 |
-
/** 与 {@link ToolTip} 中 surprisal 数值格式一致(`.3g`) */
|
| 48 |
-
const DAG_TITLE_SURPRISAL_FMT = d3.format('.3g');
|
| 49 |
-
|
| 50 |
/** 再次挂载前执行上一轮 detach(当前为空操作,保留扩展点) */
|
| 51 |
const detachGenAttributeDagPanel = new WeakMap<HTMLElement, () => void>();
|
| 52 |
|
| 53 |
-
/** 节点布局模式:`text-flow` 按文字排版层几何;`linear-arc`
|
| 54 |
-
export type DagLayoutMode = 'text-flow' | 'linear-arc' | 'spiral';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
export const DAG_COMPACTNESS_DEFAULT = 0.5;
|
| 57 |
/** 下限取小正数以满足 {@link readDisplayScaleFromCss}「必须为正」且不出现零宽度边线。 */
|
|
@@ -71,10 +84,16 @@ export function setDagNodeCiVisualScaleEnabled(enabled: boolean): void {
|
|
| 71 |
dagNodeCiVisualScaleEnabled = enabled;
|
| 72 |
}
|
| 73 |
|
| 74 |
-
/**
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
}
|
| 79 |
|
| 80 |
/**
|
|
@@ -83,30 +102,81 @@ export function setDagEdgeWeakenHighSurprisalEnabled(enabled: boolean): void {
|
|
| 83 |
* {@link dagNodeCiVisualScaleEnabled} 为 false 时恒返回 1。
|
| 84 |
*/
|
| 85 |
function dagGeneratedNodeCiVisualScale(targetProb: number | undefined): number {
|
| 86 |
-
|
| 87 |
-
if (targetProb !== undefined && Number.isFinite(targetProb) && targetProb > FULL_CONFIDENCE_PROBABILITY_BASELINE) return 1;
|
| 88 |
-
return 1 + computeConditionalInformationRatio(targetProb);
|
| 89 |
}
|
| 90 |
|
| 91 |
-
/**
|
| 92 |
-
function
|
|
|
|
|
|
|
|
|
|
| 93 |
const ci = Number.isFinite(ciRatio) ? formatTopkTooltipProbabilityPercent(ciRatio) : String(ciRatio);
|
| 94 |
const mi = Number.isFinite(miRatio) ? formatTopkTooltipProbabilityPercent(miRatio) : String(miRatio);
|
| 95 |
-
return
|
| 96 |
}
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
| 101 |
return formatTopkTooltipProbabilityPercent(miRatio);
|
| 102 |
}
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
export {
|
| 105 |
clampLinearArcAdjacentGap,
|
| 106 |
LINEAR_ARC_ADJACENT_GAP_DEFAULT,
|
| 107 |
LINEAR_ARC_ADJACENT_GAP_MAX,
|
| 108 |
LINEAR_ARC_ADJACENT_GAP_MIN,
|
| 109 |
LINEAR_ARC_BEZIER_HANDLE_INSET_FRACTION,
|
|
|
|
| 110 |
};
|
| 111 |
|
| 112 |
/** 图中节点业务字段(与 graphology 节点 attributes 为同一对象) */
|
|
@@ -132,10 +202,17 @@ type DagNodeAttrs = {
|
|
| 132 |
nodeH: number;
|
| 133 |
/** CI 视觉缩放倍数 `1 + CI` ∈ [1, 2];prompt 节点为 `1`。供 CSS 字号变量使用。 */
|
| 134 |
ciVisualScale: number;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
/** {@link visualizeSpecialChars}(DAG:仅「空格后是 [A-Za-z0-9]」保留空格,其余空格为 ·),建点后不变 */
|
| 136 |
displayLabel: string;
|
| 137 |
-
/**
|
| 138 |
-
|
|
|
|
|
|
|
| 139 |
};
|
| 140 |
|
| 141 |
type DagNode = DagNodeAttrs;
|
|
@@ -145,29 +222,164 @@ type DagLink = {
|
|
| 145 |
target: string;
|
| 146 |
/**
|
| 147 |
* 候选池内 max 归一后的归因分,区间约 [0, 1];作为 `stroke-opacity` 的基项(再乘 {@link mutualInformationRatio})。
|
| 148 |
-
* 池内稀疏化与建边前过滤均使用 {@link
|
| 149 |
*/
|
| 150 |
normalizedScore?: number;
|
| 151 |
/** 互信息率:仅作为本步入边的视觉透明度系数,不参与归因筛选。 */
|
| 152 |
mutualInformationRatio?: number;
|
| 153 |
-
/** 本步内:该边池内 L1 份额
|
| 154 |
-
|
| 155 |
/** 与 `console.warn('[genAttributeDagView.align] …')` 正文一致(可多条,换行拼接) */
|
| 156 |
alignmentNote?: string;
|
| 157 |
-
/** 边创建时固定的 `<title>` 全文 */
|
| 158 |
-
titleText: string;
|
| 159 |
};
|
| 160 |
|
| 161 |
-
/**
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
function dagLinkEndpointKey(source: string, target: string): string {
|
| 168 |
return `${source}->${target}`;
|
| 169 |
}
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
/**
|
| 172 |
* 流式增量:任一端节点 span 完全落在排除区间内则删边(不重算 Top‑N,与全量重放可轻微不一致)。
|
| 173 |
* 同步 graphology 与并行 `links`。
|
|
@@ -175,6 +387,7 @@ function dagLinkEndpointKey(source: string, target: string): string {
|
|
| 175 |
function pruneDagLinksTouchingFullyExcludedNodes(
|
| 176 |
graph: DirectedGraph<DagNodeAttrs>,
|
| 177 |
links: DagLink[],
|
|
|
|
| 178 |
intervals: [number, number][],
|
| 179 |
): void {
|
| 180 |
if (intervals.length === 0) return;
|
|
@@ -205,11 +418,36 @@ function pruneDagLinksTouchingFullyExcludedNodes(
|
|
| 205 |
links[write++] = link;
|
| 206 |
}
|
| 207 |
links.length = write;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
}
|
| 209 |
|
| 210 |
const SVG_MIN_W = 320;
|
| 211 |
const SVG_MIN_H = 280;
|
| 212 |
|
|
|
|
|
|
|
|
|
|
| 213 |
/**
|
| 214 |
* `.gen-attr-dag-stack` 布局尺寸(px),供 SVG width/height 与 `fitViewportToContent` 共用。
|
| 215 |
* 用 offsetWidth/offsetHeight(布局流尺寸)而非 getBoundingClientRect,
|
|
@@ -224,7 +462,7 @@ function stackLayoutViewportPx(stackEl: HTMLElement): { w: number; h: number } {
|
|
| 224 |
|
| 225 |
/** text-flow:在「抵消 display-scale」基准上的初始 zoom 倍率(d3 的 k) */
|
| 226 |
const DAG_INITIAL_ZOOM_BOOST_TEXT_FLOW = 2;
|
| 227 |
-
/** linear-arc:同上 */
|
| 228 |
const DAG_INITIAL_ZOOM_BOOST_LINEAR_ARC = 4;
|
| 229 |
/** spiral:同上 */
|
| 230 |
const DAG_INITIAL_ZOOM_BOOST_SPIRAL = 2;
|
|
@@ -234,6 +472,7 @@ function dagInitialZoomBoost(mode: DagLayoutMode): number {
|
|
| 234 |
case 'text-flow':
|
| 235 |
return DAG_INITIAL_ZOOM_BOOST_TEXT_FLOW;
|
| 236 |
case 'linear-arc':
|
|
|
|
| 237 |
return DAG_INITIAL_ZOOM_BOOST_LINEAR_ARC;
|
| 238 |
case 'spiral':
|
| 239 |
return DAG_INITIAL_ZOOM_BOOST_SPIRAL;
|
|
@@ -253,19 +492,18 @@ const CSS_VAR_DAG_LINK_STROKE_WIDTH = '--gen-attr-dag-link-stroke-width';
|
|
| 253 |
|
| 254 |
/** 与 {@link start.scss} `--dag-normal-line-color` 一致(普通边:线 stroke + 箭头 marker stroke) */
|
| 255 |
const CSS_VAR_DAG_NORMAL_LINE_COLOR = '--dag-normal-line-color';
|
| 256 |
-
/** 与 {@link start.scss} `--dag-highlight-line-color-in`
|
| 257 |
const CSS_VAR_DAG_HIGHLIGHT_LINE_IN = '--dag-highlight-line-color-in';
|
| 258 |
/** 与 {@link start.scss} `--dag-highlight-line-color-out` 一致(出边:从焦点出发) */
|
| 259 |
const CSS_VAR_DAG_HIGHLIGHT_LINE_OUT = '--dag-highlight-line-color-out';
|
|
|
|
|
|
|
| 260 |
|
| 261 |
/** 弱化:未排除的 prompt 无出边,或(prompt/生成区)邻域外且存在悬停/选中焦点时 */
|
| 262 |
const DAG_NODE_WEAKEN_OPACITY = 0.5;
|
| 263 |
/** 隐藏:节点 span 完全落在 exclude 规则命中区间内(prompt 与生成区各一套模式) */
|
| 264 |
const DAG_NODE_HIDDEN_OPACITY = 0.1;
|
| 265 |
|
| 266 |
-
/** 暂时关闭节点上的原生 `<title>` 悬浮提示;恢复时改为 `false`(边不受影响) */
|
| 267 |
-
const DISABLE_DAG_NODE_TOOLTIPS = false;
|
| 268 |
-
|
| 269 |
/**
|
| 270 |
* 边端在矩形边界外侧的留白,相对测量层「1em」的比例(无单位);与箭头/描边衔接用。
|
| 271 |
* 测量层与节点几何同源(lmf-readout-text),故随字号/CSS 变化而变。
|
|
@@ -316,6 +554,21 @@ function nodeRx(d: DagNode): number {
|
|
| 316 |
return Math.min(d.nodeW / 2, d.nodeH / 2);
|
| 317 |
}
|
| 318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
export type GenAttributeDagHandle = {
|
| 320 |
/**
|
| 321 |
* 在首帧 `update`(第一步生成 token)之前调用一次:用全量 prompt token spans 建 prompt 层节点。
|
|
@@ -346,12 +599,16 @@ export type GenAttributeDagHandle = {
|
|
| 346 |
reset(preserveUserViewport?: boolean): void;
|
| 347 |
/**
|
| 348 |
* zoom identity 后按内容适配视口;空图走默认缩放;`k` 上限 `k₀`(随当前布局模式的初始 zoom 倍率变化)。
|
| 349 |
-
* - `text-flow`:`rootG.getBBox()`(含边)等比落入内框。
|
| 350 |
-
* - `linear-arc`:仅按 `gen-attr-dag-nodes` 行宽定比,token 行相对内框竖直居中(弧不参与)。
|
| 351 |
* 若 `layoutDirty` 为真则 no-op(仅已执行的 `syncSvgSize` 生效,不改 pan/zoom),但 `force` 为真时仍
|
| 352 |
* fit 并清 dirty(例如刷新按钮的强制适配)。
|
| 353 |
*/
|
| 354 |
fitViewportToContent(force?: boolean): void;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
/** 清除节点选中态(与点击画布空白等价);不改变图数据,生成结束后可调用以去掉末 token 描边 */
|
| 356 |
clearNodeSelection(): void;
|
| 357 |
/** DAG 步进重放:更新 ▶ / ⏸ 按钮文案(由页面在播放开始/结束/暂停时调用) */
|
|
@@ -366,7 +623,7 @@ export type GenAttributeDagHandle = {
|
|
| 366 |
/** 切换 DAG 节点布局模式并立即重排现有节点/边。 */
|
| 367 |
setLayoutMode(mode: DagLayoutMode): void;
|
| 368 |
/**
|
| 369 |
-
* linear-arc 下相邻节点矩形外侧边的水平间隙(px)。
|
| 370 |
* `skipRefit` 为真,仅写入值,下一轮 `syncGraphToSvg`/空闲后再反映(与测量宽度语义一致)。
|
| 371 |
*/
|
| 372 |
setLinearArcAdjacentGapPx(px: number, opts?: { skipRefit?: boolean }): void;
|
|
@@ -383,6 +640,12 @@ export type GenAttributeDagHandle = {
|
|
| 383 |
* - `false`(默认):保留为低透明度({@link DAG_NODE_HIDDEN_OPACITY})占位。
|
| 384 |
*/
|
| 385 |
setHideExcludedTokens(hide: boolean): void;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
/** prompt 层节点是否已注入(即 {@link setPromptTokenSpans} 至少成功添加过一个节点) */
|
| 387 |
hasPromptSpans(): boolean;
|
| 388 |
/** 移除 DAG 栈与刷新按钮(离开页面时调用) */
|
|
@@ -409,55 +672,50 @@ function formatNodeOffsetRange(id: string): string {
|
|
| 409 |
return `[${a}, ${b})`;
|
| 410 |
}
|
| 411 |
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
];
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
lines.push(`\nProb: ${formatTopkTooltipProbabilityPercent(targetProb)}`);
|
| 423 |
-
lines.push(`Information: ${DAG_TITLE_SURPRISAL_FMT(calculateSurprisal(targetProb))} bits`);
|
| 424 |
-
lines.push(
|
| 425 |
-
formatCiMiRatiosLineForTooltip(
|
| 426 |
-
computeConditionalInformationRatio(targetProb),
|
| 427 |
-
computeMutualInformationRatio(targetProb),
|
| 428 |
-
),
|
| 429 |
-
);
|
| 430 |
}
|
| 431 |
-
return lines.join('\n');
|
| 432 |
-
}
|
| 433 |
-
|
| 434 |
-
/** 建边时调用:端点已带 {@link DagNodeAttrs.displayLabel} */
|
| 435 |
-
function buildLinkTitleText(
|
| 436 |
-
d: Pick<DagLink, 'normalizedScore' | 'mutualInformationRatio' | 'scoreShare' | 'alignmentNote'>,
|
| 437 |
-
src: DagNode,
|
| 438 |
-
tgt: DagNode
|
| 439 |
-
): string {
|
| 440 |
-
const s = d.normalizedScore ?? 1;
|
| 441 |
-
const normStr = Number.isFinite(s) ? s.toFixed(3) : String(s);
|
| 442 |
-
const opacity = dagLinkStrokeOpacity(d);
|
| 443 |
-
const opacityStr = Number.isFinite(opacity) ? opacity.toFixed(3) : String(opacity);
|
| 444 |
|
| 445 |
const metrics = [
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
`
|
|
|
|
| 449 |
];
|
| 450 |
-
const share = d.scoreShare;
|
| 451 |
-
if (typeof share === 'number' && Number.isFinite(share) && share > 0) {
|
| 452 |
-
metrics.push(`Fan in share: ${(share * 100).toFixed(1)}%`);
|
| 453 |
-
}
|
| 454 |
-
if (d.alignmentNote) {
|
| 455 |
-
metrics.push(d.alignmentNote);
|
| 456 |
-
}
|
| 457 |
|
| 458 |
return [
|
| 459 |
-
`From:\n${src.displayLabel}\nOffset: ${formatNodeOffsetRange(src.id)}`,
|
| 460 |
-
`To:\n${tgt.displayLabel}\nOffset: ${formatNodeOffsetRange(tgt.id)}`,
|
| 461 |
metrics.join('\n'),
|
| 462 |
].join('\n\n');
|
| 463 |
}
|
|
@@ -480,45 +738,147 @@ function snapSubwordNode(node: DagNode, prev: DagNode | null): void {
|
|
| 480 |
node.cx = prev.cx + (prev.nodeW + node.nodeW) / 2;
|
| 481 |
}
|
| 482 |
|
| 483 |
-
/**
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
}
|
| 494 |
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
}
|
| 506 |
|
| 507 |
-
|
| 508 |
-
* 邻接焦点时边的描边:从焦点出发 → 红;指向焦点 → 蓝(自环视为「出发」)。
|
| 509 |
-
* 非邻接返回 `null`,调用方用默认边色。
|
| 510 |
-
*/
|
| 511 |
-
function dagLinkHighlightStroke(
|
| 512 |
graph: DirectedGraph<DagNodeAttrs>,
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
}
|
| 523 |
|
| 524 |
/**
|
|
@@ -562,12 +922,18 @@ export type InitGenAttributeDagViewOptions = {
|
|
| 562 |
/** DAG 节点布局模式;默认 `text-flow`。 */
|
| 563 |
layoutMode?: DagLayoutMode;
|
| 564 |
/**
|
| 565 |
-
* linear-arc:相邻节点矩形外侧边的水平间隙(px),决定水平方向疏密;
|
| 566 |
* 默认 {@link LINEAR_ARC_ADJACENT_GAP_DEFAULT}。
|
| 567 |
*/
|
| 568 |
linearArcAdjacentGapPx?: number;
|
| 569 |
/** 被 exclude 规则命中的节点是否完全隐藏(true)还是仅降至 {@link DAG_NODE_HIDDEN_OPACITY}(false,默认)。 */
|
| 570 |
hideExcludedTokens?: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
/** 边 Top-P 覆盖阈值(候选池内累计份额);���认 {@link DAG_EDGE_TOP_P_COVERAGE_DEFAULT}。 */
|
| 572 |
edgeTopPCoverage?: number;
|
| 573 |
/** 进入/退出/切换全屏失败时(常见于移动端不支持元素全屏等)。不传则无提示。 */
|
|
@@ -597,6 +963,9 @@ export function initGenAttributeDagView(
|
|
| 597 |
linearArcAdjacentGapPx = clampLinearArcAdjacentGap(iv);
|
| 598 |
}
|
| 599 |
let hideExcludedTokens: boolean = options?.hideExcludedTokens ?? false;
|
|
|
|
|
|
|
|
|
|
| 600 |
let edgeTopPCoverage = clampDagEdgeTopPCoverage(
|
| 601 |
options?.edgeTopPCoverage ?? DAG_EDGE_TOP_P_COVERAGE_DEFAULT,
|
| 602 |
);
|
|
@@ -624,6 +993,8 @@ export function initGenAttributeDagView(
|
|
| 624 |
isBatching: () => false,
|
| 625 |
reset: noop,
|
| 626 |
fitViewportToContent: noop,
|
|
|
|
|
|
|
| 627 |
clearNodeSelection: noop,
|
| 628 |
setDagPlaybackPlaying: noop,
|
| 629 |
setMeasureWidthPx: noop,
|
|
@@ -632,6 +1003,9 @@ export function initGenAttributeDagView(
|
|
| 632 |
setDagCompactness: noop,
|
| 633 |
setEdgeTopPCoverage: noop,
|
| 634 |
setHideExcludedTokens: noop,
|
|
|
|
|
|
|
|
|
|
| 635 |
hasPromptSpans: () => false,
|
| 636 |
detach: noop,
|
| 637 |
};
|
|
@@ -642,13 +1016,33 @@ export function initGenAttributeDagView(
|
|
| 642 |
detachGenAttributeDagPanel.get(rootEl)?.();
|
| 643 |
resultsRoot
|
| 644 |
.selectAll(
|
| 645 |
-
'.gen-attr-dag-stack, svg.gen-attr-dag-svg, button.gen-attr-dag-refresh, button.gen-attr-dag-play, button.gen-attr-dag-fullscreen'
|
| 646 |
)
|
| 647 |
.remove();
|
| 648 |
|
| 649 |
const stack = resultsRoot.append('div').attr('class', 'gen-attr-dag-stack');
|
| 650 |
const stackEl = stack.node() as HTMLElement;
|
| 651 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
/** 非 text-flow 时节点不可拖;用该类覆盖选中态的 grab 光标(linear-arc / spiral 等)。 */
|
| 653 |
function syncStackLayoutDragUi(): void {
|
| 654 |
stackEl.classList.toggle('gen-attr-dag-no-node-drag-layout', layoutMode !== 'text-flow');
|
|
@@ -697,6 +1091,7 @@ export function initGenAttributeDagView(
|
|
| 697 |
function refreshDagScaleDerivedFromCss(): void {
|
| 698 |
displayScale = readDisplayScaleFromCss(stackEl);
|
| 699 |
linkEndInsetPx = linkEndInsetBaseAtUnitScalePx(measureRoot) * displayScale;
|
|
|
|
| 700 |
}
|
| 701 |
|
| 702 |
function setDagCompactness(c: number): void {
|
|
@@ -735,6 +1130,7 @@ export function initGenAttributeDagView(
|
|
| 735 |
// 仅用户交互(滚轮/拖平移/双击)计入「改动布局」;程序触发的 transform
|
| 736 |
// (init 初始缩放、`fitViewportToContent`)`sourceEvent === null`,不置 dirty。
|
| 737 |
if (event.sourceEvent) layoutDirty = true;
|
|
|
|
| 738 |
});
|
| 739 |
|
| 740 |
function applyInitialDagZoom(): void {
|
|
@@ -750,14 +1146,37 @@ export function initGenAttributeDagView(
|
|
| 750 |
const nodeG = rootG.append('g').attr('class', 'gen-attr-dag-nodes');
|
| 751 |
/** 邻接焦点的高亮边:在节点层之后绘制,避免被节点遮挡 */
|
| 752 |
const linkGFront = rootG.append('g').attr('class', 'gen-attr-dag-links-front');
|
|
|
|
|
|
|
| 753 |
|
| 754 |
const graph = new DirectedGraph<DagNodeAttrs>();
|
| 755 |
let nodes: DagNode[] = [];
|
|
|
|
|
|
|
| 756 |
let links: DagLink[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 757 |
let stepProcessed = 0;
|
| 758 |
let selectedId: string | null = null;
|
| 759 |
-
/**
|
| 760 |
let hoveredId: string | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 761 |
/**
|
| 762 |
* 与 {@link pruneDagLinksTouchingFullyExcludedNodes} / 预处理同源:全串上的 exclude 半开区间,
|
| 763 |
* 供节点「隐藏」透明度判定({@link isOffsetSpanFullyExcluded})。在 {@link setPromptTokenSpans} 与每步
|
|
@@ -783,6 +1202,32 @@ export function initGenAttributeDagView(
|
|
| 783 |
.selectAll<SVGGElement, DagLink>('g.gen-attr-dag-link')
|
| 784 |
.data<DagLink>([], dagLinkDataKey);
|
| 785 |
let nodeSel = nodeG.selectAll<SVGGElement, DagNode>('g.gen-attr-dag-node').data<DagNode>([], (d) => d.id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 786 |
|
| 787 |
function syncSvgSize(): void {
|
| 788 |
const { w, h } = stackLayoutViewportPx(stackEl);
|
|
@@ -790,7 +1235,8 @@ export function initGenAttributeDagView(
|
|
| 790 |
}
|
| 791 |
|
| 792 |
function paint(): void {
|
| 793 |
-
|
|
|
|
| 794 |
const layoutNodes = hideExcludedTokens
|
| 795 |
? nodes.filter((n) => !isOffsetSpanFullyExcluded(n.start, n.end, dagExcludeIntervals))
|
| 796 |
: nodes;
|
|
@@ -799,14 +1245,13 @@ export function initGenAttributeDagView(
|
|
| 799 |
nodeSel,
|
| 800 |
nodes: layoutNodes,
|
| 801 |
adjacentGapPx: linearArcAdjacentGapPx,
|
|
|
|
| 802 |
getLinkNodes: (d) => ({
|
| 803 |
src: endpointNode(d.source, graph),
|
| 804 |
tgt: endpointNode(d.target, graph),
|
| 805 |
}),
|
| 806 |
});
|
| 807 |
-
|
| 808 |
-
}
|
| 809 |
-
if (layoutMode === 'spiral') {
|
| 810 |
const layoutNodes = hideExcludedTokens
|
| 811 |
? nodes.filter((n) => !isOffsetSpanFullyExcluded(n.start, n.end, dagExcludeIntervals))
|
| 812 |
: nodes;
|
|
@@ -820,17 +1265,18 @@ export function initGenAttributeDagView(
|
|
| 820 |
tgt: endpointNode(d.target, graph),
|
| 821 |
}),
|
| 822 |
});
|
| 823 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 824 |
}
|
| 825 |
-
|
| 826 |
-
linkSel,
|
| 827 |
-
nodeSel,
|
| 828 |
-
linkEndInsetPx,
|
| 829 |
-
getLinkNodes: (d) => ({
|
| 830 |
-
src: endpointNode(d.source, graph),
|
| 831 |
-
tgt: endpointNode(d.target, graph),
|
| 832 |
-
}),
|
| 833 |
-
});
|
| 834 |
}
|
| 835 |
|
| 836 |
let dragPointerOffset: { x: number; y: number } | null = null;
|
|
@@ -858,32 +1304,56 @@ export function initGenAttributeDagView(
|
|
| 858 |
d.cx = x - offset.x;
|
| 859 |
d.cy = y - offset.y;
|
| 860 |
paint();
|
|
|
|
| 861 |
})
|
| 862 |
.on('end', () => {
|
| 863 |
dragPointerOffset = null;
|
| 864 |
});
|
| 865 |
|
| 866 |
-
/**
|
| 867 |
-
* 节点透明度:先按「一跳邻域」提亮为 1(选中先于悬停,邻域内一致);
|
| 868 |
-
* 否则被 exclude 整段命中的为「隐藏」({@link DAG_NODE_HIDDEN_OPACITY})(prompt/生成区一致);
|
| 869 |
-
* 其余节点:prompt 无出边时弱化;存在焦点时,所有邻域外非隐藏节点弱化(与原先焦点压暗一致)。
|
| 870 |
-
* 边的高亮仍以悬停优先、否则选中为焦点({@link dagLinkIncidentToFocus})。
|
| 871 |
-
*/
|
| 872 |
function refreshNodeLinkHighlight(): void {
|
| 873 |
-
const focusId =
|
| 874 |
-
const
|
| 875 |
-
|
| 876 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 877 |
nodeSel
|
| 878 |
.classed('gen-attr-dag-node--hover', (d) => hoveredId === d.id)
|
| 879 |
.classed('gen-attr-dag-node--selected', (d) => selectedId === d.id)
|
| 880 |
-
.style('display',
|
| 881 |
-
hideExcludedTokens && isOffsetSpanFullyExcluded(d.start, d.end, dagExcludeIntervals)
|
| 882 |
-
? 'none' : null
|
| 883 |
-
)
|
| 884 |
.attr('opacity', (d) => {
|
| 885 |
-
|
| 886 |
-
|
|
|
|
|
|
|
| 887 |
if (isOffsetSpanFullyExcluded(d.start, d.end, dagExcludeIntervals)) {
|
| 888 |
return hideExcludedTokens ? 0 : DAG_NODE_HIDDEN_OPACITY;
|
| 889 |
}
|
|
@@ -891,36 +1361,134 @@ export function initGenAttributeDagView(
|
|
| 891 |
const isPromptLeaf = hasGenTokens && d.step === -1 && graph.outDegree(d.id) === 0;
|
| 892 |
if (focusId || isPromptLeaf) return DAG_NODE_WEAKEN_OPACITY;
|
| 893 |
return 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
});
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 901 |
const g = d3.select(this);
|
| 902 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 903 |
linkMarkersDefs
|
| 904 |
.select<SVGPathElement>(`#${dagLinkMarkerElementId(d.source, d.target)} path`)
|
| 905 |
.attr('stroke', stroke)
|
| 906 |
-
.attr('stroke-opacity',
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
// - 下面仅在父节点不一致时才 `appendChild`,避免白搬动导致末尾顺序被打乱。
|
| 913 |
-
rootG.selectAll<SVGGElement, DagLink>('g.gen-attr-dag-link').each(function(d) {
|
| 914 |
-
const incident = dagLinkIncidentToFocus(graph, focusId, d);
|
| 915 |
const parent = incident ? linkGFront : linkG;
|
| 916 |
const parentNode = parent.node()!;
|
| 917 |
if (this.parentNode !== parentNode) {
|
| 918 |
parentNode.appendChild(this as SVGGElement);
|
| 919 |
}
|
| 920 |
});
|
|
|
|
|
|
|
| 921 |
}
|
| 922 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 923 |
function setSelectedNodeId(id: string | null): void {
|
|
|
|
|
|
|
|
|
|
| 924 |
selectedId = id;
|
| 925 |
refreshNodeLinkHighlight();
|
| 926 |
}
|
|
@@ -931,6 +1499,7 @@ export function initGenAttributeDagView(
|
|
| 931 |
|
| 932 |
/** 将当前 `nodes` / `links` 同步到 SVG:join 新 DOM、`paint` 几何、`refreshNodeLinkHighlight` 样式。 */
|
| 933 |
function syncGraphToSvg(): void {
|
|
|
|
| 934 |
linkGFront.selectAll<SVGGElement, DagLink>('g.gen-attr-dag-link').each(function() {
|
| 935 |
linkG.node()!.appendChild(this as SVGGElement);
|
| 936 |
});
|
|
@@ -966,7 +1535,7 @@ export function initGenAttributeDagView(
|
|
| 966 |
g.each(function(d: DagLink) {
|
| 967 |
const el = d3.select(this);
|
| 968 |
const mkId = dagLinkMarkerElementId(d.source, d.target);
|
| 969 |
-
el.append('title')
|
| 970 |
el.append('path')
|
| 971 |
.attr('class', 'gen-attr-dag-link-visible')
|
| 972 |
.attr('fill', 'none')
|
|
@@ -978,55 +1547,53 @@ export function initGenAttributeDagView(
|
|
| 978 |
return g;
|
| 979 |
});
|
| 980 |
// 不在此处全量重置 marker `stroke-opacity`:紧接着的 {@link refreshNodeLinkHighlight} 会按边
|
| 981 |
-
// 逐条写
|
| 982 |
|
| 983 |
nodeSel = nodeG
|
| 984 |
.selectAll<SVGGElement, DagNode>('g.gen-attr-dag-node')
|
| 985 |
.data(nodes, (d) => d.id)
|
| 986 |
-
.join(
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
}
|
| 1029 |
-
);
|
| 1030 |
|
| 1031 |
paint();
|
| 1032 |
refreshNodeLinkHighlight();
|
|
@@ -1082,11 +1649,6 @@ export function initGenAttributeDagView(
|
|
| 1082 |
nodeH: g.height * displayScale,
|
| 1083 |
ciVisualScale: 1,
|
| 1084 |
displayLabel,
|
| 1085 |
-
nativeTitleText: buildNodeNativeTitleText({
|
| 1086 |
-
displayLabel,
|
| 1087 |
-
id: srcId,
|
| 1088 |
-
step: -1,
|
| 1089 |
-
}),
|
| 1090 |
};
|
| 1091 |
graph.addNode(srcId, srcNode);
|
| 1092 |
nodes.push(srcNode);
|
|
@@ -1103,6 +1665,8 @@ export function initGenAttributeDagView(
|
|
| 1103 |
getEffectiveExcludePromptPatternsText(),
|
| 1104 |
getEffectiveExcludeGeneratedPatternsText(),
|
| 1105 |
);
|
|
|
|
|
|
|
| 1106 |
if (batchDepth === 0) syncGraphToSvg();
|
| 1107 |
}
|
| 1108 |
|
|
@@ -1130,6 +1694,8 @@ export function initGenAttributeDagView(
|
|
| 1130 |
spaceDotExceptBeforeAsciiLetterOrNumber: true,
|
| 1131 |
});
|
| 1132 |
const ciVisualScale = dagGeneratedNodeCiVisualScale(response.target_prob);
|
|
|
|
|
|
|
| 1133 |
const targetNode: DagNode = {
|
| 1134 |
id: targetId,
|
| 1135 |
label: token,
|
|
@@ -1141,16 +1707,15 @@ export function initGenAttributeDagView(
|
|
| 1141 |
nodeW: g.width * displayScale * ciVisualScale,
|
| 1142 |
nodeH: g.height * displayScale * ciVisualScale,
|
| 1143 |
ciVisualScale,
|
|
|
|
| 1144 |
displayLabel,
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
id: targetId,
|
| 1148 |
-
step: stepProcessed,
|
| 1149 |
-
targetProb: response.target_prob,
|
| 1150 |
-
}),
|
| 1151 |
};
|
| 1152 |
graph.addNode(targetId, targetNode);
|
| 1153 |
nodes.push(targetNode);
|
|
|
|
|
|
|
| 1154 |
snapSubwordNode(targetNode, nodes.length >= 2 ? nodes[nodes.length - 2]! : null);
|
| 1155 |
|
| 1156 |
// align → exclude → rank:Top-N / β / cumP 在节点语义上工作(合并型「如下」/ 拆分型等)。
|
|
@@ -1173,15 +1738,14 @@ export function initGenAttributeDagView(
|
|
| 1173 |
const selected = phase2RankAndSparsify(afterExclude, { cumulativeShare: edgeTopPCoverage });
|
| 1174 |
|
| 1175 |
const mutualInformationRatio = computeMutualInformationRatio(response.target_prob);
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
}) >= DAG_EDGE_MIN_DISPLAY_OPACITY
|
| 1183 |
-
);
|
| 1184 |
const massSum = selectedForDisplay.reduce((acc, t) => acc + Math.max(0, t.poolMassFrac), 0);
|
|
|
|
| 1185 |
for (const item of selectedForDisplay) {
|
| 1186 |
const srcId = item.nodeId;
|
| 1187 |
if (!graph.hasNode(srcId)) {
|
|
@@ -1202,19 +1766,19 @@ export function initGenAttributeDagView(
|
|
| 1202 |
const edgeAttrs = {
|
| 1203 |
normalizedScore: item.score,
|
| 1204 |
mutualInformationRatio,
|
| 1205 |
-
|
| 1206 |
...(alignmentNote ? { alignmentNote } : {}),
|
| 1207 |
};
|
| 1208 |
graph.addEdge(srcId, targetId, edgeAttrs);
|
| 1209 |
-
const
|
| 1210 |
-
const tgtAttrs = graph.getNodeAttributes(targetId) as DagNode;
|
| 1211 |
-
links.push({
|
| 1212 |
source: srcId,
|
| 1213 |
target: targetId,
|
| 1214 |
...edgeAttrs,
|
| 1215 |
-
|
| 1216 |
-
|
|
|
|
| 1217 |
}
|
|
|
|
| 1218 |
|
| 1219 |
const excludeIntervals = collectGenAttrDagExcludeIntervals(
|
| 1220 |
intervalCtx,
|
|
@@ -1223,10 +1787,10 @@ export function initGenAttributeDagView(
|
|
| 1223 |
getEffectiveExcludeGeneratedPatternsText(),
|
| 1224 |
);
|
| 1225 |
dagExcludeIntervals = excludeIntervals;
|
| 1226 |
-
pruneDagLinksTouchingFullyExcludedNodes(graph, links, excludeIntervals);
|
| 1227 |
|
| 1228 |
stepProcessed++;
|
| 1229 |
-
// 每步生成后:默认
|
| 1230 |
selectedId = targetId;
|
| 1231 |
if (batchDepth === 0) {
|
| 1232 |
syncGraphToSvg();
|
|
@@ -1242,19 +1806,27 @@ export function initGenAttributeDagView(
|
|
| 1242 |
textMeasure.reset();
|
| 1243 |
graph.clear();
|
| 1244 |
nodes = [];
|
|
|
|
| 1245 |
links = [];
|
|
|
|
|
|
|
| 1246 |
stepProcessed = 0;
|
| 1247 |
selectedId = null;
|
| 1248 |
hoveredId = null;
|
|
|
|
| 1249 |
linkMarkersDefs.selectAll('marker').remove();
|
| 1250 |
linkG.selectAll('*').remove();
|
| 1251 |
linkGFront.selectAll('*').remove();
|
| 1252 |
nodeG.selectAll('*').remove();
|
|
|
|
| 1253 |
dagExcludeIntervals = [];
|
| 1254 |
linkSel = rootG
|
| 1255 |
.selectAll<SVGGElement, DagLink>('g.gen-attr-dag-link')
|
| 1256 |
.data<DagLink>([], dagLinkDataKey);
|
| 1257 |
nodeSel = nodeG.selectAll<SVGGElement, DagNode>('g.gen-attr-dag-node').data<DagNode>([], (d) => d.id);
|
|
|
|
|
|
|
|
|
|
| 1258 |
layoutDirty = preserveUserViewport ? wasLayoutDirty : false;
|
| 1259 |
userDraggedNodes = false;
|
| 1260 |
}
|
|
@@ -1273,7 +1845,7 @@ export function initGenAttributeDagView(
|
|
| 1273 |
const { w, h } = stackLayoutViewportPx(stackEl);
|
| 1274 |
const innerW = Math.max(w - 2 * pad, 1);
|
| 1275 |
const innerH = Math.max(h - 2 * pad, 1);
|
| 1276 |
-
if (layoutMode
|
| 1277 |
/** 仅用 token 行宽度定比;竖直按行中心居中(弧不参与 bbox → 不致上下抖) */
|
| 1278 |
const bn = nodeG.node()!.getBBox();
|
| 1279 |
const bw = Math.max(bn.width, 1e-6);
|
|
@@ -1309,14 +1881,17 @@ export function initGenAttributeDagView(
|
|
| 1309 |
const ty = pad + halfH;
|
| 1310 |
svg.call(zoomBehavior.transform, d3.zoomIdentity.translate(tx, ty).scale(k));
|
| 1311 |
} else if (layoutMode === 'text-flow') {
|
| 1312 |
-
/**
|
|
|
|
|
|
|
|
|
|
| 1313 |
const b = rootG.node()!.getBBox();
|
| 1314 |
const bw = Math.max(b.width, 1e-6);
|
| 1315 |
const bh = Math.max(b.height, 1e-6);
|
| 1316 |
-
const kRaw = Math.min(
|
| 1317 |
const k = Math.min(Number.isFinite(kRaw) && kRaw > 0 ? kRaw : k0, k0);
|
| 1318 |
-
const tx =
|
| 1319 |
-
const ty =
|
| 1320 |
svg.call(zoomBehavior.transform, d3.zoomIdentity.translate(tx, ty).scale(k));
|
| 1321 |
} else {
|
| 1322 |
const _: never = layoutMode;
|
|
@@ -1381,7 +1956,7 @@ export function initGenAttributeDagView(
|
|
| 1381 |
if (linearArcAdjacentGapPx === next) return;
|
| 1382 |
linearArcAdjacentGapPx = next;
|
| 1383 |
if (opts?.skipRefit || batchDepth > 0) return;
|
| 1384 |
-
if (layoutMode
|
| 1385 |
paint();
|
| 1386 |
fitViewportToContent(true);
|
| 1387 |
}
|
|
@@ -1395,6 +1970,25 @@ export function initGenAttributeDagView(
|
|
| 1395 |
fitViewportToContent(true);
|
| 1396 |
}
|
| 1397 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1398 |
const fullscreenBtn = resultsRoot
|
| 1399 |
.append('button')
|
| 1400 |
.attr('type', 'button')
|
|
@@ -1464,9 +2058,10 @@ export function initGenAttributeDagView(
|
|
| 1464 |
ro.disconnect();
|
| 1465 |
document.removeEventListener('fullscreenchange', refreshFullscreenChrome);
|
| 1466 |
document.removeEventListener(CSS_PSEUDO_FULLSCREEN_CHANGE_EVENT, refreshFullscreenChrome);
|
|
|
|
| 1467 |
resultsRoot
|
| 1468 |
.selectAll(
|
| 1469 |
-
'.gen-attr-dag-stack, button.gen-attr-dag-refresh, button.gen-attr-dag-play, button.gen-attr-dag-fullscreen'
|
| 1470 |
)
|
| 1471 |
.remove();
|
| 1472 |
detachGenAttributeDagPanel.delete(rootEl);
|
|
@@ -1482,6 +2077,8 @@ export function initGenAttributeDagView(
|
|
| 1482 |
isBatching,
|
| 1483 |
reset,
|
| 1484 |
fitViewportToContent,
|
|
|
|
|
|
|
| 1485 |
clearNodeSelection,
|
| 1486 |
setDagPlaybackPlaying,
|
| 1487 |
setMeasureWidthPx,
|
|
@@ -1490,6 +2087,9 @@ export function initGenAttributeDagView(
|
|
| 1490 |
setDagCompactness,
|
| 1491 |
setEdgeTopPCoverage,
|
| 1492 |
setHideExcludedTokens,
|
|
|
|
|
|
|
|
|
|
| 1493 |
hasPromptSpans: () => nodes.some((n) => n.step === -1),
|
| 1494 |
detach,
|
| 1495 |
};
|
|
|
|
| 1 |
import * as d3 from 'd3';
|
| 2 |
import { DirectedGraph } from 'graphology';
|
| 3 |
+
import type { D3Sel } from '../utils/Util';
|
| 4 |
import { visualizeSpecialChars } from '../utils/tokenDisplayUtils';
|
| 5 |
import {
|
| 6 |
clampDagEdgeTopPCoverage,
|
|
|
|
| 10 |
phase2RankAndSparsify,
|
| 11 |
type PromptTokenSpan,
|
| 12 |
} from './genAttributeDagPreprocess';
|
| 13 |
+
import {
|
| 14 |
+
DAG_EDGE_MIN_NORMALIZED_SCORE,
|
| 15 |
+
DAG_EDGE_RENDER_OPACITY_FLOOR,
|
| 16 |
+
DAG_MIN_ATTRIBUTION_SHARE,
|
| 17 |
+
DAG_NODE_STROKE_OPACITY_BASE,
|
| 18 |
+
} from './genAttributeDagEdgeDisplay';
|
| 19 |
import {
|
| 20 |
computeMutualInformationRatio,
|
| 21 |
computeConditionalInformationRatio,
|
| 22 |
+
dagCiVisualScaleFromTargetProb,
|
| 23 |
+
dagPropagationMiRatio,
|
| 24 |
FULL_CONFIDENCE_PROBABILITY_BASELINE,
|
| 25 |
} from '../utils/surprisalMath';
|
| 26 |
import { isOffsetSpanFullyExcluded } from './attributionDisplayModel';
|
|
|
|
| 30 |
type NodeInterval,
|
| 31 |
type PieceEntry,
|
| 32 |
} from './genAttributeDagIntervalResolve';
|
| 33 |
+
import type { FrontendToken } from '../api/GLTR_API';
|
| 34 |
import type { TokenGenStep } from './tokenGenAttributionRunner';
|
| 35 |
import { createGenAttributeDagTextMeasure } from './genAttributeDagTextMeasure';
|
| 36 |
+
import { frontendTokenFromGenAttrStep } from './genAttributeDagTopkToken';
|
| 37 |
+
import { SimpleEventHandler } from '../utils/SimpleEventHandler';
|
| 38 |
+
import { ToolTip, type ToolTipUpdateAugment } from '../vis/ToolTip';
|
| 39 |
import { formatTopkTooltipProbabilityPercent } from '../utils/topkChartUtils';
|
| 40 |
import {
|
| 41 |
CSS_PSEUDO_FULLSCREEN_CHANGE_EVENT,
|
|
|
|
| 49 |
LINEAR_ARC_ADJACENT_GAP_MAX,
|
| 50 |
LINEAR_ARC_ADJACENT_GAP_MIN,
|
| 51 |
LINEAR_ARC_BEZIER_HANDLE_INSET_FRACTION,
|
| 52 |
+
LINEAR_ARC_STEP_DOWN_DISTANCE_SCALE,
|
| 53 |
paintLinearArcLayout,
|
| 54 |
} from './genAttributeDagViewLinearArcMode';
|
| 55 |
import { paintTextFlowLayout } from './genAttributeDagViewTextFlowMode';
|
| 56 |
import { paintSpiralLayout } from './genAttributeDagViewSpiralMode';
|
| 57 |
import { tr } from '../lang/i18n-lite';
|
| 58 |
|
|
|
|
|
|
|
|
|
|
| 59 |
/** 再次挂载前执行上一轮 detach(当前为空操作,保留扩展点) */
|
| 60 |
const detachGenAttributeDagPanel = new WeakMap<HTMLElement, () => void>();
|
| 61 |
|
| 62 |
+
/** 节点布局模式:`text-flow` 按文字排版层几何;`linear-arc` / `linear-arc-step-down` 为线性序 + 弧线连边(后者按 CI 逐级下移);`spiral` 螺旋排布。 */
|
| 63 |
+
export type DagLayoutMode = 'text-flow' | 'linear-arc' | 'linear-arc-step-down' | 'spiral';
|
| 64 |
+
|
| 65 |
+
function isLinearArcFamilyLayout(mode: DagLayoutMode): mode is 'linear-arc' | 'linear-arc-step-down' {
|
| 66 |
+
return mode === 'linear-arc' || mode === 'linear-arc-step-down';
|
| 67 |
+
}
|
| 68 |
|
| 69 |
export const DAG_COMPACTNESS_DEFAULT = 0.5;
|
| 70 |
/** 下限取小正数以满足 {@link readDisplayScaleFromCss}「必须为正」且不出现零宽度边线。 */
|
|
|
|
| 84 |
dagNodeCiVisualScaleEnabled = enabled;
|
| 85 |
}
|
| 86 |
|
| 87 |
+
/**
|
| 88 |
+
* 「Decay attribution to high-surprisal targets」——递归归因的配套开关。
|
| 89 |
+
* 开启:沿链向上时,在高惊讶度(低置信 / teacher forcing)的**生成 token** 处用 MI 折扣传播预算,
|
| 90 |
+
* 使它们成为与 prompt 同类的「来源」,链在此变短。
|
| 91 |
+
* 关闭:所有生成 token 视为透明管道,预算不衰减,链只止于 prompt。
|
| 92 |
+
* `false` 时 `mutualInformationRatio` 仍按目标概率存储与展示,传播/边强度计算中 MI 系数恒为 1。
|
| 93 |
+
*/
|
| 94 |
+
let dagDecayAttributionToHighSurprisalTargetEnabled = true;
|
| 95 |
+
export function setDagDecayAttributionToHighSurprisalTargetEnabled(enabled: boolean): void {
|
| 96 |
+
dagDecayAttributionToHighSurprisalTargetEnabled = enabled;
|
| 97 |
}
|
| 98 |
|
| 99 |
/**
|
|
|
|
| 102 |
* {@link dagNodeCiVisualScaleEnabled} 为 false 时恒返回 1。
|
| 103 |
*/
|
| 104 |
function dagGeneratedNodeCiVisualScale(targetProb: number | undefined): number {
|
| 105 |
+
return dagCiVisualScaleFromTargetProb(targetProb, dagNodeCiVisualScaleEnabled);
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
|
| 108 |
+
/** DAG Top‑K tooltip 内 CI/MI 行,数值格式与原节点原生 title 一致({@link formatTopkTooltipProbabilityPercent})。 */
|
| 109 |
+
function dagCiMiTooltipRowForProb(targetProb: number | undefined): { label: string; value: string } | undefined {
|
| 110 |
+
if (targetProb === undefined || !Number.isFinite(targetProb)) return undefined;
|
| 111 |
+
const ciRatio = computeConditionalInformationRatio(targetProb);
|
| 112 |
+
const miRatio = computeMutualInformationRatio(targetProb);
|
| 113 |
const ci = Number.isFinite(ciRatio) ? formatTopkTooltipProbabilityPercent(ciRatio) : String(ciRatio);
|
| 114 |
const mi = Number.isFinite(miRatio) ? formatTopkTooltipProbabilityPercent(miRatio) : String(miRatio);
|
| 115 |
+
return { label: 'CI/MI:', value: `${ci} / ${mi}` };
|
| 116 |
}
|
| 117 |
|
| 118 |
+
const TOOLTIP_NA = 'N/A';
|
| 119 |
+
|
| 120 |
+
/** 边原生 `<title>` 中互信息率 α 的展示。 */
|
| 121 |
+
function formatMutualInformationRatioForTooltip(miRatio: number | undefined): string {
|
| 122 |
+
if (miRatio === undefined || !Number.isFinite(miRatio)) return TOOLTIP_NA;
|
| 123 |
return formatTopkTooltipProbabilityPercent(miRatio);
|
| 124 |
}
|
| 125 |
|
| 126 |
+
function isPositiveFiniteShare(share: number | undefined): share is number {
|
| 127 |
+
return typeof share === 'number' && Number.isFinite(share) && share > 0;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/**
|
| 131 |
+
* 边级 MI 系数(直接归因强度、无焦点灰边)。
|
| 132 |
+
* 递归链上的传播折扣在节点级 {@link nodePropagationMiRatio},二者分工不同。
|
| 133 |
+
*/
|
| 134 |
+
function effectiveMiRatio(miRatio: number | undefined): number | undefined {
|
| 135 |
+
if (!dagDecayAttributionToHighSurprisalTargetEnabled) return 1;
|
| 136 |
+
if (miRatio === undefined || !Number.isFinite(miRatio)) return undefined;
|
| 137 |
+
return miRatio;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
function formatTooltipAttributionScore(normalizedScore: number | undefined): string {
|
| 141 |
+
if (normalizedScore === undefined || !Number.isFinite(normalizedScore)) return TOOLTIP_NA;
|
| 142 |
+
return normalizedScore.toFixed(3);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/** 直接归因份额的展示:L1 份额 × 目标真实 MI(与弱化开关无关,仅供读数)。 */
|
| 146 |
+
function formatTooltipDirectAttributionShare(
|
| 147 |
+
attributionShare: number | undefined,
|
| 148 |
+
miRatio: number | undefined,
|
| 149 |
+
): string {
|
| 150 |
+
if (!isPositiveFiniteShare(attributionShare)) return TOOLTIP_NA;
|
| 151 |
+
if (miRatio === undefined || !Number.isFinite(miRatio)) return TOOLTIP_NA;
|
| 152 |
+
return formatTopkTooltipProbabilityPercent(attributionShare * miRatio);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
function formatTooltipRecursiveAttributionShare(share: number | undefined): string {
|
| 156 |
+
if (share === undefined || !Number.isFinite(share)) return TOOLTIP_NA;
|
| 157 |
+
return formatAttributionSharePercentForTooltip(share);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/** 节点 tooltip 归因份额:低于 {@link DAG_MIN_ATTRIBUTION_SHARE} 时显示 `< x%`(x 为阈值,1 位有效数字)。 */
|
| 161 |
+
function formatAttributionSharePercentForTooltip(share: number): string {
|
| 162 |
+
const thresholdLabel = d3.format('.1g')(DAG_MIN_ATTRIBUTION_SHARE * 100) + '%';
|
| 163 |
+
if (!Number.isFinite(share) || share < DAG_MIN_ATTRIBUTION_SHARE) {
|
| 164 |
+
return `< ${thresholdLabel}`;
|
| 165 |
+
}
|
| 166 |
+
return formatTopkTooltipProbabilityPercent(share);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
function formatTooltipLinkStrength(strength: number): string {
|
| 170 |
+
return Number.isFinite(strength) ? strength.toFixed(3) : TOOLTIP_NA;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
export {
|
| 174 |
clampLinearArcAdjacentGap,
|
| 175 |
LINEAR_ARC_ADJACENT_GAP_DEFAULT,
|
| 176 |
LINEAR_ARC_ADJACENT_GAP_MAX,
|
| 177 |
LINEAR_ARC_ADJACENT_GAP_MIN,
|
| 178 |
LINEAR_ARC_BEZIER_HANDLE_INSET_FRACTION,
|
| 179 |
+
LINEAR_ARC_STEP_DOWN_DISTANCE_SCALE,
|
| 180 |
};
|
| 181 |
|
| 182 |
/** 图中节点业务字段(与 graphology 节点 attributes 为同一对象) */
|
|
|
|
| 202 |
nodeH: number;
|
| 203 |
/** CI 视觉缩放倍数 `1 + CI` ∈ [1, 2];prompt 节点为 `1`。供 CSS 字号变量使用。 */
|
| 204 |
ciVisualScale: number;
|
| 205 |
+
/**
|
| 206 |
+
* 本步 {@link TokenGenStep} 的 `response.target_prob`(仅生成节点)。
|
| 207 |
+
* 下台阶等处用 {@link dagStepDownEffectiveCiRatio}(dagTargetProb)(高置信 p>p₁ 为 0;与「关闭 CI 视觉」无关);
|
| 208 |
+
*/
|
| 209 |
+
dagTargetProb?: number;
|
| 210 |
/** {@link visualizeSpecialChars}(DAG:仅「空格后是 [A-Za-z0-9]」保留空格,其余空格为 ·),建点后不变 */
|
| 211 |
displayLabel: string;
|
| 212 |
+
/** 悬停 / 选中焦点时 Top‑K tooltip;仅生成节点(`step >= 0`) */
|
| 213 |
+
gltrTooltipToken?: FrontendToken;
|
| 214 |
+
/** 跟在 tooltip 内 log perplexity 行之后的 CI/MI;与 {@link dagCiMiTooltipRowForProb} 同源 */
|
| 215 |
+
dagCiMiTooltipRow?: { label: string; value: string };
|
| 216 |
};
|
| 217 |
|
| 218 |
type DagNode = DagNodeAttrs;
|
|
|
|
| 222 |
target: string;
|
| 223 |
/**
|
| 224 |
* 候选池内 max 归一后的归因分,区间约 [0, 1];作为 `stroke-opacity` 的基项(再乘 {@link mutualInformationRatio})。
|
| 225 |
+
* 池内稀疏化与建边前过滤均使用 {@link DAG_EDGE_MIN_NORMALIZED_SCORE}(见 genAttributeDagEdgeDisplay)。
|
| 226 |
*/
|
| 227 |
normalizedScore?: number;
|
| 228 |
/** 互信息率:仅作为本步入边的视觉透明度系数,不参与归因筛选。 */
|
| 229 |
mutualInformationRatio?: number;
|
| 230 |
+
/** 本步内:该边在可见入边池内的 L1 份额(建边阈值过滤后归一),追因传播的基本单位。 */
|
| 231 |
+
attributionShare?: number;
|
| 232 |
/** 与 `console.warn('[genAttributeDagView.align] …')` 正文一致(可多条,换行拼接) */
|
| 233 |
alignmentNote?: string;
|
|
|
|
|
|
|
| 234 |
};
|
| 235 |
|
| 236 |
+
/**
|
| 237 |
+
* 该边的 attribution share:优先使用可见边池内的 L1 份额;无 attributionShare 时回退到 max-normalized score。
|
| 238 |
+
* max-normalized score 作为后备仅用于 attributionShare 尚未计算(如阈值过滤前)的场景。
|
| 239 |
+
*/
|
| 240 |
+
function edgeAttributionShare(d: Pick<DagLink, 'attributionShare' | 'normalizedScore'>): number {
|
| 241 |
+
const share = d.attributionShare;
|
| 242 |
+
if (typeof share === 'number' && Number.isFinite(share) && share > 0) return share;
|
| 243 |
+
const s = d.normalizedScore ?? 1;
|
| 244 |
+
return Number.isFinite(s) ? Math.max(0, s) : 1;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
/**
|
| 248 |
+
* 无焦点时的边渲染强度:attribution share × {@link effectiveMiRatio}。
|
| 249 |
+
* 「Decay attribution to high-surprisal targets」关闭时 MI 系数恒为 1(展示仍见 {@link formatMutualInformationRatioForTooltip})。
|
| 250 |
+
*/
|
| 251 |
+
function directAttributionStrength(
|
| 252 |
+
d: Pick<DagLink, 'attributionShare' | 'normalizedScore' | 'mutualInformationRatio'>,
|
| 253 |
+
): number {
|
| 254 |
+
const mi = effectiveMiRatio(d.mutualInformationRatio) ?? 1;
|
| 255 |
+
return edgeAttributionShare(d) * mi;
|
| 256 |
}
|
| 257 |
|
| 258 |
function dagLinkEndpointKey(source: string, target: string): string {
|
| 259 |
return `${source}->${target}`;
|
| 260 |
}
|
| 261 |
|
| 262 |
+
/** 节点 target 端 MI ratio(与 tooltip「Target MI ratio」同源;与 decay 开关无关)。 */
|
| 263 |
+
function nodeTargetMiRatio(node: DagNode): number {
|
| 264 |
+
return computeMutualInformationRatio(node.dagTargetProb);
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
function maxHighlightEdgeShare(sharesByKey: Map<string, number>): number {
|
| 268 |
+
let max = 0;
|
| 269 |
+
for (const share of sharesByKey.values()) {
|
| 270 |
+
if (share > max) max = share;
|
| 271 |
+
}
|
| 272 |
+
return max;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
/**
|
| 276 |
+
* 池内 max 归一后的 `stroke-opacity`;最强边刻度为 {@link maxOpacity}(默认 1)。
|
| 277 |
+
* 按实际值计算后,最终不低于 {@link DAG_EDGE_RENDER_OPACITY_FLOOR},防止过淡不可见。
|
| 278 |
+
*/
|
| 279 |
+
function normalizeEdgeRenderOpacity(share: number, maxShare: number, maxOpacity = 1): number {
|
| 280 |
+
if (!Number.isFinite(share) || share <= 0) return 0;
|
| 281 |
+
const cap = Number.isFinite(maxOpacity) && maxOpacity > 0 ? maxOpacity : 1;
|
| 282 |
+
const scaled =
|
| 283 |
+
!Number.isFinite(maxShare) || maxShare <= 0
|
| 284 |
+
? Math.min(cap, share)
|
| 285 |
+
: Math.min(cap, (share / maxShare) * cap);
|
| 286 |
+
if (scaled <= 0) return 0;
|
| 287 |
+
return Math.max(DAG_EDGE_RENDER_OPACITY_FLOOR, scaled);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
/**
|
| 291 |
+
* 候选归因节点描边透明度:池内 `stay / max(stay)` 线性映射到 `[{@link DAG_NODE_STROKE_OPACITY_BASE}, 1]`,
|
| 292 |
+
* 避免弱节点描边过淡、在 UI 里看不出来(见 {@link DAG_NODE_STROKE_OPACITY_BASE})。
|
| 293 |
+
*/
|
| 294 |
+
function normalizeNodeStrokeRenderOpacity(share: number, maxShare: number): number {
|
| 295 |
+
if (!Number.isFinite(share) || share <= 0) return 0;
|
| 296 |
+
const scaled =
|
| 297 |
+
!Number.isFinite(maxShare) || maxShare <= 0
|
| 298 |
+
? Math.min(1, share)
|
| 299 |
+
: Math.min(1, share / maxShare);
|
| 300 |
+
if (scaled <= 0) return 0;
|
| 301 |
+
return DAG_NODE_STROKE_OPACITY_BASE + scaled * (1 - DAG_NODE_STROKE_OPACITY_BASE);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
/** 焦点在 target 时单条入边份额(直接模式一跳;灰边与此时蓝边共用)。 */
|
| 305 |
+
function perTargetIncomingEdgeShare(
|
| 306 |
+
link: Pick<DagLink, 'attributionShare' | 'normalizedScore'>,
|
| 307 |
+
targetNode: DagNode,
|
| 308 |
+
): number {
|
| 309 |
+
const upstreamBudget = nodePropagationMiRatio(targetNode);
|
| 310 |
+
return Math.min(1, upstreamBudget * edgeAttributionShare(link));
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
/** 灰边 stroke-opacity:按各 target 入边池归一���与焦点在该 target 时的蓝边一致。 */
|
| 314 |
+
function buildGrayRenderStrengthByEdgeKey(
|
| 315 |
+
graph: DirectedGraph<DagNodeAttrs>,
|
| 316 |
+
incomingLinksByTarget: Map<string, DagLink[]>,
|
| 317 |
+
): Map<string, number> {
|
| 318 |
+
const byKey = new Map<string, number>();
|
| 319 |
+
for (const [targetId, links] of incomingLinksByTarget) {
|
| 320 |
+
if (!graph.hasNode(targetId)) continue;
|
| 321 |
+
const targetNode = graph.getNodeAttributes(targetId) as DagNode;
|
| 322 |
+
// prompt 节点(step < 0)不应出现在 incomingLinksByTarget(仅 update() 中生成节点作为 target 时写入),
|
| 323 |
+
// 此处防御:nodePropagationMiRatio 对 prompt 返回 0,全组 share=0,跳过以节省迭代。
|
| 324 |
+
if (targetNode.step < 0) continue;
|
| 325 |
+
let maxShare = 0;
|
| 326 |
+
const rows: Array<{ key: string; share: number }> = [];
|
| 327 |
+
for (const link of links) {
|
| 328 |
+
if (!graph.hasEdge(link.source, link.target)) continue;
|
| 329 |
+
const srcId = endpointNode(link.source, graph).id;
|
| 330 |
+
const share = perTargetIncomingEdgeShare(link, targetNode);
|
| 331 |
+
if (share > maxShare) maxShare = share;
|
| 332 |
+
rows.push({ key: dagLinkEndpointKey(srcId, targetId), share });
|
| 333 |
+
}
|
| 334 |
+
for (const { key, share } of rows) {
|
| 335 |
+
byKey.set(key, normalizeEdgeRenderOpacity(share, maxShare));
|
| 336 |
+
}
|
| 337 |
+
}
|
| 338 |
+
return byKey;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
/**
|
| 342 |
+
* 递归模式:焦点链上**上游**节点的描边 raw 强度(stay;不含焦点本身)。
|
| 343 |
+
* 直接模式仅一跳,由蓝/红高亮边表达,不画来源描边。
|
| 344 |
+
* 显示判定:stay ≥ {@link DAG_MIN_ATTRIBUTION_SHARE};描边透明度见 {@link buildNodeStrokeRenderStrengthById}。
|
| 345 |
+
*/
|
| 346 |
+
function computeUpstreamNodeStrokeShareById(
|
| 347 |
+
nodeShareById: Map<string, number>,
|
| 348 |
+
graph: DirectedGraph<DagNodeAttrs>,
|
| 349 |
+
focusId: string,
|
| 350 |
+
): Map<string, number> {
|
| 351 |
+
const byNodeId = new Map<string, number>();
|
| 352 |
+
for (const [nodeId, nodeShare] of nodeShareById) {
|
| 353 |
+
if (nodeId === focusId) continue;
|
| 354 |
+
const stay = nodeShare * (1 - nodePropagationMiRatio(graph.getNodeAttributes(nodeId) as DagNode));
|
| 355 |
+
if (stay >= DAG_MIN_ATTRIBUTION_SHARE) byNodeId.set(nodeId, stay);
|
| 356 |
+
}
|
| 357 |
+
return byNodeId;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
/** 池内 max 归一后的 render 强度;{@link maxOpacity} 为链内最强边刻度(蓝入边见 {@link refreshNodeLinkHighlight},默认 1)。 */
|
| 361 |
+
function buildMaxNormalizedRenderStrengthByKey(
|
| 362 |
+
sharesByKey: Map<string, number>,
|
| 363 |
+
maxOpacity = 1,
|
| 364 |
+
): Map<string, number> {
|
| 365 |
+
const maxShare = maxHighlightEdgeShare(sharesByKey);
|
| 366 |
+
const byKey = new Map<string, number>();
|
| 367 |
+
for (const [key, share] of sharesByKey) {
|
| 368 |
+
byKey.set(key, normalizeEdgeRenderOpacity(share, maxShare, maxOpacity));
|
| 369 |
+
}
|
| 370 |
+
return byKey;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
/** 递归链候选节点描边强度:stay 池内 max 归一后映射到 `[{@link DAG_NODE_STROKE_OPACITY_BASE}, 1]`。 */
|
| 374 |
+
function buildNodeStrokeRenderStrengthById(stayByNodeId: Map<string, number>): Map<string, number> {
|
| 375 |
+
const maxShare = maxHighlightEdgeShare(stayByNodeId);
|
| 376 |
+
const byNodeId = new Map<string, number>();
|
| 377 |
+
for (const [nodeId, stay] of stayByNodeId) {
|
| 378 |
+
byNodeId.set(nodeId, normalizeNodeStrokeRenderOpacity(stay, maxShare));
|
| 379 |
+
}
|
| 380 |
+
return byNodeId;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
/**
|
| 384 |
* 流式增量:任一端节点 span 完全落在排除区间内则删边(不重算 Top‑N,与全量重放可轻微不一致)。
|
| 385 |
* 同步 graphology 与并行 `links`。
|
|
|
|
| 387 |
function pruneDagLinksTouchingFullyExcludedNodes(
|
| 388 |
graph: DirectedGraph<DagNodeAttrs>,
|
| 389 |
links: DagLink[],
|
| 390 |
+
incomingLinksByTarget: Map<string, DagLink[]>,
|
| 391 |
intervals: [number, number][],
|
| 392 |
): void {
|
| 393 |
if (intervals.length === 0) return;
|
|
|
|
| 418 |
links[write++] = link;
|
| 419 |
}
|
| 420 |
links.length = write;
|
| 421 |
+
|
| 422 |
+
for (const [targetId, incoming] of incomingLinksByTarget) {
|
| 423 |
+
if (incoming.length === 0) {
|
| 424 |
+
incomingLinksByTarget.delete(targetId);
|
| 425 |
+
continue;
|
| 426 |
+
}
|
| 427 |
+
let keep = 0;
|
| 428 |
+
for (const link of incoming) {
|
| 429 |
+
if (removedLinkKeys.has(dagLinkEndpointKey(link.source, link.target))) {
|
| 430 |
+
continue;
|
| 431 |
+
}
|
| 432 |
+
incoming[keep++] = link;
|
| 433 |
+
}
|
| 434 |
+
if (keep === 0) {
|
| 435 |
+
incomingLinksByTarget.delete(targetId);
|
| 436 |
+
continue;
|
| 437 |
+
}
|
| 438 |
+
incoming.length = keep;
|
| 439 |
+
if (!graph.hasNode(targetId)) {
|
| 440 |
+
incomingLinksByTarget.delete(targetId);
|
| 441 |
+
}
|
| 442 |
+
}
|
| 443 |
}
|
| 444 |
|
| 445 |
const SVG_MIN_W = 320;
|
| 446 |
const SVG_MIN_H = 280;
|
| 447 |
|
| 448 |
+
/** text-flow:`fitViewportToContent` 四边对称边距(px)。 */
|
| 449 |
+
const DAG_TEXT_FLOW_FIT_PAD_PX = 24;
|
| 450 |
+
|
| 451 |
/**
|
| 452 |
* `.gen-attr-dag-stack` 布局尺寸(px),供 SVG width/height 与 `fitViewportToContent` 共用。
|
| 453 |
* 用 offsetWidth/offsetHeight(布局流尺寸)而非 getBoundingClientRect,
|
|
|
|
| 462 |
|
| 463 |
/** text-flow:在「抵消 display-scale」基准上的初始 zoom 倍率(d3 的 k) */
|
| 464 |
const DAG_INITIAL_ZOOM_BOOST_TEXT_FLOW = 2;
|
| 465 |
+
/** linear-arc / linear-arc-step-down:同上 */
|
| 466 |
const DAG_INITIAL_ZOOM_BOOST_LINEAR_ARC = 4;
|
| 467 |
/** spiral:同上 */
|
| 468 |
const DAG_INITIAL_ZOOM_BOOST_SPIRAL = 2;
|
|
|
|
| 472 |
case 'text-flow':
|
| 473 |
return DAG_INITIAL_ZOOM_BOOST_TEXT_FLOW;
|
| 474 |
case 'linear-arc':
|
| 475 |
+
case 'linear-arc-step-down':
|
| 476 |
return DAG_INITIAL_ZOOM_BOOST_LINEAR_ARC;
|
| 477 |
case 'spiral':
|
| 478 |
return DAG_INITIAL_ZOOM_BOOST_SPIRAL;
|
|
|
|
| 492 |
|
| 493 |
/** 与 {@link start.scss} `--dag-normal-line-color` 一致(普通边:线 stroke + 箭头 marker stroke) */
|
| 494 |
const CSS_VAR_DAG_NORMAL_LINE_COLOR = '--dag-normal-line-color';
|
| 495 |
+
/** 与 {@link start.scss} `--dag-highlight-line-color-in`(`--accent-color`)一致(入边:指向焦点) */
|
| 496 |
const CSS_VAR_DAG_HIGHLIGHT_LINE_IN = '--dag-highlight-line-color-in';
|
| 497 |
/** 与 {@link start.scss} `--dag-highlight-line-color-out` 一致(出边:从焦点出发) */
|
| 498 |
const CSS_VAR_DAG_HIGHLIGHT_LINE_OUT = '--dag-highlight-line-color-out';
|
| 499 |
+
/** 与 {@link gen_attribute.scss} `.gen-attr-dag-node--recursive-chain` 中 `stroke-opacity` 一致(由 JS 写入 g 元素) */
|
| 500 |
+
const CSS_VAR_DAG_NODE_RECURSIVE_SHARE = '--gen-attr-dag-node-recursive-share';
|
| 501 |
|
| 502 |
/** 弱化:未排除的 prompt 无出边,或(prompt/生成区)邻域外且存在悬停/选中焦点时 */
|
| 503 |
const DAG_NODE_WEAKEN_OPACITY = 0.5;
|
| 504 |
/** 隐藏:节点 span 完全落在 exclude 规则命中区间内(prompt 与生成区各一套模式) */
|
| 505 |
const DAG_NODE_HIDDEN_OPACITY = 0.1;
|
| 506 |
|
|
|
|
|
|
|
|
|
|
| 507 |
/**
|
| 508 |
* 边端在矩形边界外侧的留白,相对测量层「1em」的比例(无单位);与箭头/描边衔接用。
|
| 509 |
* 测量层与节点几何同源(lmf-readout-text),故随字号/CSS 变化而变。
|
|
|
|
| 554 |
return Math.min(d.nodeW / 2, d.nodeH / 2);
|
| 555 |
}
|
| 556 |
|
| 557 |
+
/** stroke rect 外扩 pad=displayScale,与 scss `stroke-width: calc(2 * display-scale)` 一致,描边不压 fill。 */
|
| 558 |
+
function syncNodeStrokeRects(
|
| 559 |
+
sel: d3.Selection<SVGGElement, DagNode, SVGGElement | null, unknown>,
|
| 560 |
+
displayScale: number,
|
| 561 |
+
): void {
|
| 562 |
+
const p = displayScale;
|
| 563 |
+
sel.select('rect.gen-attr-dag-node-stroke')
|
| 564 |
+
.attr('x', -p)
|
| 565 |
+
.attr('y', -p)
|
| 566 |
+
.attr('width', (d) => d.nodeW + 2 * p)
|
| 567 |
+
.attr('height', (d) => d.nodeH + 2 * p)
|
| 568 |
+
.attr('rx', (d) => nodeRx(d) + p)
|
| 569 |
+
.attr('ry', (d) => nodeRx(d) + p);
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
export type GenAttributeDagHandle = {
|
| 573 |
/**
|
| 574 |
* 在首帧 `update`(第一步生成 token)之前调用一次:用全量 prompt token spans 建 prompt 层节点。
|
|
|
|
| 599 |
reset(preserveUserViewport?: boolean): void;
|
| 600 |
/**
|
| 601 |
* zoom identity 后按内容适配视口;空图走默认缩放;`k` 上限 `k₀`(随当前布局模式的初始 zoom 倍率变化)。
|
| 602 |
+
* - `text-flow`:`rootG.getBBox()`(含边)等比落入内框;四边对称各 {@link DAG_TEXT_FLOW_FIT_PAD_PX}px。
|
| 603 |
+
* - `linear-arc` / `linear-arc-step-down`:仅按 `gen-attr-dag-nodes` 行宽定比,token 行相对内框竖直居中(弧不参与)。
|
| 604 |
* 若 `layoutDirty` 为真则 no-op(仅已执行的 `syncSvgSize` 生效,不改 pan/zoom),但 `force` 为真时仍
|
| 605 |
* fit 并清 dirty(例如刷新按钮的强制适配)。
|
| 606 |
*/
|
| 607 |
fitViewportToContent(force?: boolean): void;
|
| 608 |
+
/** 当前选中节点 id;无选中为 `null`。 */
|
| 609 |
+
getSelectedNodeId(): string | null;
|
| 610 |
+
/** 设置选中节点(`null` 清除);节点须已存在于图中。 */
|
| 611 |
+
setSelectedNodeId(id: string | null): void;
|
| 612 |
/** 清除节点选中态(与点击画布空白等价);不改变图数据,生成结束后可调用以去掉末 token 描边 */
|
| 613 |
clearNodeSelection(): void;
|
| 614 |
/** DAG 步进重放:更新 ▶ / ⏸ 按钮文案(由页面在播放开始/结束/暂停时调用) */
|
|
|
|
| 623 |
/** 切换 DAG 节点布局模式并立即重排现有节点/边。 */
|
| 624 |
setLayoutMode(mode: DagLayoutMode): void;
|
| 625 |
/**
|
| 626 |
+
* linear-arc 家族下相邻节点矩形外侧边的水平间隙(px)。��影响该家族几何;若在生成/播放中途调用且
|
| 627 |
* `skipRefit` 为真,仅写入值,下一轮 `syncGraphToSvg`/空闲后再反映(与测量宽度语义一致)。
|
| 628 |
*/
|
| 629 |
setLinearArcAdjacentGapPx(px: number, opts?: { skipRefit?: boolean }): void;
|
|
|
|
| 640 |
* - `false`(默认):保留为低透明度({@link DAG_NODE_HIDDEN_OPACITY})占位。
|
| 641 |
*/
|
| 642 |
setHideExcludedTokens(hide: boolean): void;
|
| 643 |
+
/** 是否显示 token tooltip(UI: Show token tooltip;`showTokenInfoOnSelected`)。 */
|
| 644 |
+
setShowTokenInfoOnSelected(show: boolean): void;
|
| 645 |
+
/** 是否启用传播归因(UI: Propagated attribution mode;`recursiveAttributionEnabled`)。 */
|
| 646 |
+
setRecursiveAttributionEnabled(enabled: boolean): void;
|
| 647 |
+
/** 是否在直接归因焦点上额外展示从焦点出发的下游影响出边。 */
|
| 648 |
+
setShowDownstreamInfluence(show: boolean): void;
|
| 649 |
/** prompt 层节点是否已注入(即 {@link setPromptTokenSpans} 至少成功添加过一个节点) */
|
| 650 |
hasPromptSpans(): boolean;
|
| 651 |
/** 移除 DAG 栈与刷新按钮(离开页面时调用) */
|
|
|
|
| 672 |
return `[${a}, ${b})`;
|
| 673 |
}
|
| 674 |
|
| 675 |
+
/**
|
| 676 |
+
* 边当前显示状态;在 {@link refreshNodeLinkHighlight} 中与 stroke 一并刷新 `<title>`。
|
| 677 |
+
*
|
| 678 |
+
* {@link recursiveAttributionShare} 为当前焦点下传播归因链上的份额(UI: Propagated;仅入边链;无焦点或不在链上为 undefined)。
|
| 679 |
+
* {@link linkStrength} 为 tooltip 用的原始强度;{@link renderStrength} 为写入 stroke-opacity 的值(直接模式灰边与蓝边同刻度)。
|
| 680 |
+
* 空行以上为建边后不变的直接归因指标。不用「opacity」命名:灰边与蓝/红高亮边在相同强度下 `stroke-opacity` 数值可相同,但肉眼对比度不同,
|
| 681 |
+
* 视觉效果由 stroke 颜色与透明度共同衍生,强度才是可比较的固定量。
|
| 682 |
+
*/
|
| 683 |
+
type DagLinkTitleSnapshot = {
|
| 684 |
+
normalizedScore?: number;
|
| 685 |
+
mutualInformationRatio?: number;
|
| 686 |
+
attributionShare?: number;
|
| 687 |
+
alignmentNote?: string;
|
| 688 |
+
src: DagNode;
|
| 689 |
+
tgt: DagNode;
|
| 690 |
+
/** 递归链入边上的传播份额 edgeShare;不在链上时为 undefined(直接模式或无焦点)。 */
|
| 691 |
+
recursiveAttributionShare?: number;
|
| 692 |
+
linkStrength: number;
|
| 693 |
+
};
|
| 694 |
+
|
| 695 |
+
function buildLinkTitleText(snapshot: DagLinkTitleSnapshot): string {
|
| 696 |
+
// 建边后不变;空行以下随焦点/传播归因变化(Attribution share (Propagated)、Link strength)。
|
| 697 |
+
const staticMetrics = [
|
| 698 |
+
`Attribution score: ${formatTooltipAttributionScore(snapshot.normalizedScore)}`,
|
| 699 |
+
`Target MI ratio: ${formatMutualInformationRatioForTooltip(snapshot.mutualInformationRatio)}`,
|
| 700 |
+
`Attribution share (Adjacent): ${formatTooltipDirectAttributionShare(
|
| 701 |
+
snapshot.attributionShare,
|
| 702 |
+
snapshot.mutualInformationRatio,
|
| 703 |
+
)}`,
|
| 704 |
];
|
| 705 |
+
if (snapshot.alignmentNote) {
|
| 706 |
+
staticMetrics.push(snapshot.alignmentNote);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
|
| 709 |
const metrics = [
|
| 710 |
+
staticMetrics.join('\n'),
|
| 711 |
+
'',
|
| 712 |
+
`Attribution share (Propagated): ${formatTooltipRecursiveAttributionShare(snapshot.recursiveAttributionShare)}`,
|
| 713 |
+
`Link strength: ${formatTooltipLinkStrength(snapshot.linkStrength)}`,
|
| 714 |
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 715 |
|
| 716 |
return [
|
| 717 |
+
`From:\n${snapshot.src.displayLabel}\nOffset: ${formatNodeOffsetRange(snapshot.src.id)}`,
|
| 718 |
+
`To:\n${snapshot.tgt.displayLabel}\nOffset: ${formatNodeOffsetRange(snapshot.tgt.id)}`,
|
| 719 |
metrics.join('\n'),
|
| 720 |
].join('\n\n');
|
| 721 |
}
|
|
|
|
| 738 |
node.cx = prev.cx + (prev.nodeW + node.nodeW) / 2;
|
| 739 |
}
|
| 740 |
|
| 741 |
+
/**
|
| 742 |
+
* 传播归因 vs 直接归因(设计理念)
|
| 743 |
+
*
|
| 744 |
+
* UI 称 Propagated attribution mode;代码标识 `recursiveAttribution*`(递归向上传播份额,二者同义)。
|
| 745 |
+
*
|
| 746 |
+
* - 直接归因:只看一跳前驱,回答“它直接依赖了谁”。
|
| 747 |
+
* - 传播归因:持续向上追溯,直到信息来源,回答“真正原因来自哪里”。
|
| 748 |
+
*
|
| 749 |
+
* 来源通常有两类:prompt,或低置信/高惊讶的生成 token(含 teacher forcing)。
|
| 750 |
+
* 高置信中间 token 更像传导节点,归因会继续穿过它。
|
| 751 |
+
*
|
| 752 |
+
* UI 语义:
|
| 753 |
+
* - 灰边:各 target 入边池内 max 归一(无焦点时的默认边);
|
| 754 |
+
* - 焦点蓝入边:链内 max 归一,最强边刻度为焦点 target 的实际 MI ratio;最终 opacity 不低于 {@link DAG_EDGE_RENDER_OPACITY_FLOOR};
|
| 755 |
+
* - 上游节点描边(仅传播归因):stay 池内 max 归一,映射到 `[{@link DAG_NODE_STROKE_OPACITY_BASE}, 1]`;直接模式一跳由边色表达,不描边。
|
| 756 |
+
* - 传播模式节点提亮与描边一致:仅焦点 + stay 达阈的上游(传导节点仅保留蓝边,不提亮)。
|
| 757 |
+
*/
|
| 758 |
+
type FocusAttributionState = {
|
| 759 |
+
activeNodeIds: Set<string>;
|
| 760 |
+
/** 传播归因链上入边的份额(用于蓝边强度;`recursiveAttributionShare`)。 */
|
| 761 |
+
incomingEdgeShareByKey: Map<string, number>;
|
| 762 |
+
/** 仅直接模式:焦点出发的下游影响边。 */
|
| 763 |
+
downstreamEdgeStrengthByKey: Map<string, number>;
|
| 764 |
+
/** 链上各节点累计份额(用于计算节点停留量)。 */
|
| 765 |
+
nodeShareById: Map<string, number>;
|
| 766 |
+
};
|
| 767 |
+
|
| 768 |
+
/** 节点在递归传播中的传导系数:越低越像来源,越高越像传导节点。 */
|
| 769 |
+
function nodePropagationMiRatio(node: DagNode): number {
|
| 770 |
+
if (node.step < 0) return 0;
|
| 771 |
+
if (!dagDecayAttributionToHighSurprisalTargetEnabled) return 1;
|
| 772 |
+
return dagPropagationMiRatio(node.dagTargetProb);
|
| 773 |
}
|
| 774 |
|
| 775 |
+
type DagLinkHighlightDisplay = {
|
| 776 |
+
stroke: string;
|
| 777 |
+
/** 写入 stroke-opacity(链内 max 归一;蓝入边最强边刻度见 {@link refreshNodeLinkHighlight},红出边/灰边为 1)。 */
|
| 778 |
+
renderStrength: number;
|
| 779 |
+
/** tooltip「Link strength」:原始强度,不做归一。 */
|
| 780 |
+
linkStrength: number;
|
| 781 |
+
recursiveAttributionShare?: number;
|
| 782 |
+
};
|
| 783 |
+
|
| 784 |
+
/** 焦点下边的视觉规则:传播归因看“向上原因链”,直接看“一跳关系 + 可选下游影响”。 */
|
| 785 |
+
function resolveDagLinkHighlightDisplay(
|
| 786 |
+
d: DagLink,
|
| 787 |
+
edgeKey: string,
|
| 788 |
+
focusState: FocusAttributionState | null,
|
| 789 |
+
recursiveAttributionEnabled: boolean,
|
| 790 |
+
grayRenderByKey: Map<string, number>,
|
| 791 |
+
incomingHighlightRenderByKey: Map<string, number>,
|
| 792 |
+
downstreamHighlightRenderByKey: Map<string, number>,
|
| 793 |
+
): DagLinkHighlightDisplay {
|
| 794 |
+
const directStrength = directAttributionStrength(d);
|
| 795 |
+
const grayRender = grayRenderByKey.get(edgeKey) ?? directStrength;
|
| 796 |
+
|
| 797 |
+
if (focusState) {
|
| 798 |
+
const downstreamStrength = focusState.downstreamEdgeStrengthByKey.get(edgeKey);
|
| 799 |
+
if (downstreamStrength != null) {
|
| 800 |
+
return {
|
| 801 |
+
stroke: `var(${CSS_VAR_DAG_HIGHLIGHT_LINE_OUT})`,
|
| 802 |
+
renderStrength: downstreamHighlightRenderByKey.get(edgeKey)!,
|
| 803 |
+
linkStrength: downstreamStrength,
|
| 804 |
+
};
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
const incomingShare = focusState.incomingEdgeShareByKey.get(edgeKey);
|
| 808 |
+
if (incomingShare != null) {
|
| 809 |
+
return {
|
| 810 |
+
stroke: `var(${CSS_VAR_DAG_HIGHLIGHT_LINE_IN})`,
|
| 811 |
+
renderStrength: incomingHighlightRenderByKey.get(edgeKey)!,
|
| 812 |
+
linkStrength: incomingShare,
|
| 813 |
+
recursiveAttributionShare: recursiveAttributionEnabled ? incomingShare : undefined,
|
| 814 |
+
};
|
| 815 |
+
}
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
return {
|
| 819 |
+
stroke: `var(${CSS_VAR_DAG_NORMAL_LINE_COLOR})`,
|
| 820 |
+
renderStrength: grayRender,
|
| 821 |
+
linkStrength: directStrength,
|
| 822 |
+
};
|
| 823 |
}
|
| 824 |
|
| 825 |
+
function computeFocusAttributionState(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
graph: DirectedGraph<DagNodeAttrs>,
|
| 827 |
+
nodesSortedByStepDesc: DagNode[],
|
| 828 |
+
incomingLinksByTarget: Map<string, DagLink[]>,
|
| 829 |
+
focusId: string,
|
| 830 |
+
options: { maxIncomingDepth: number; includeDownstreamInfluence: boolean },
|
| 831 |
+
): FocusAttributionState | null {
|
| 832 |
+
if (!graph.hasNode(focusId)) return null;
|
| 833 |
+
|
| 834 |
+
// 从焦点向上追溯:递归模式追到来源;直接模式仅保留一跳前驱。
|
| 835 |
+
const activeNodeIds = new Set<string>([focusId]);
|
| 836 |
+
const incomingEdgeShareByKey = new Map<string, number>();
|
| 837 |
+
const downstreamEdgeStrengthByKey = new Map<string, number>();
|
| 838 |
+
const nodeShareById = new Map<string, number>([[focusId, 1]]);
|
| 839 |
+
const remainingDepthByNodeId = new Map<string, number>([[focusId, options.maxIncomingDepth]]);
|
| 840 |
+
|
| 841 |
+
for (const node of nodesSortedByStepDesc) {
|
| 842 |
+
// min(1)/max(0) 仅防御,正常路径下 nodeShare ∈ (0, 1]。
|
| 843 |
+
const nodeShare = Math.min(1, Math.max(0, nodeShareById.get(node.id) ?? 0));
|
| 844 |
+
if (nodeShare <= 0) continue;
|
| 845 |
+
const remainingDepth = remainingDepthByNodeId.get(node.id) ?? 0;
|
| 846 |
+
if (remainingDepth <= 0) continue;
|
| 847 |
+
|
| 848 |
+
// 低系数节点更接近来源,高系数节点更接近传导。
|
| 849 |
+
const upstreamBudget = nodeShare * nodePropagationMiRatio(node);
|
| 850 |
+
if (upstreamBudget < DAG_MIN_ATTRIBUTION_SHARE) continue;
|
| 851 |
+
|
| 852 |
+
for (const link of incomingLinksByTarget.get(node.id) ?? []) {
|
| 853 |
+
if (!graph.hasEdge(link.source, link.target)) continue;
|
| 854 |
+
const srcId = endpointNode(link.source, graph).id;
|
| 855 |
+
// min(1) 仅防御:attributionShare L1 归一且 MI ≤ 1 保证乘积不超过 1。
|
| 856 |
+
const edgeShare = Math.min(1, upstreamBudget * edgeAttributionShare(link));
|
| 857 |
+
if (edgeShare < DAG_MIN_ATTRIBUTION_SHARE) continue;
|
| 858 |
+
|
| 859 |
+
incomingEdgeShareByKey.set(dagLinkEndpointKey(srcId, node.id), edgeShare);
|
| 860 |
+
activeNodeIds.add(srcId);
|
| 861 |
+
// min(1) 仅防御:nodeShare 从 1 出发,经 attributionShare 分配与 MI 衰减后各节点累积不超过 1。
|
| 862 |
+
nodeShareById.set(srcId, Math.min(1, (nodeShareById.get(srcId) ?? 0) + edgeShare));
|
| 863 |
+
remainingDepthByNodeId.set(
|
| 864 |
+
srcId,
|
| 865 |
+
Math.max(remainingDepthByNodeId.get(srcId) ?? 0, remainingDepth - 1),
|
| 866 |
+
);
|
| 867 |
+
}
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
// 直接模式可附带展示“焦点影响了谁”。
|
| 871 |
+
if (options.includeDownstreamInfluence) {
|
| 872 |
+
graph.forEachOutEdge(focusId, (_edgeId, edgeAttrs, srcId, tgtId) => {
|
| 873 |
+
const link = edgeAttrs as unknown as Pick<DagLink, 'attributionShare' | 'normalizedScore' | 'mutualInformationRatio'>;
|
| 874 |
+
const strength = directAttributionStrength(link);
|
| 875 |
+
if (strength < DAG_MIN_ATTRIBUTION_SHARE) return;
|
| 876 |
+
downstreamEdgeStrengthByKey.set(dagLinkEndpointKey(srcId, tgtId), strength);
|
| 877 |
+
activeNodeIds.add(tgtId);
|
| 878 |
+
});
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
return { activeNodeIds, incomingEdgeShareByKey, downstreamEdgeStrengthByKey, nodeShareById };
|
| 882 |
}
|
| 883 |
|
| 884 |
/**
|
|
|
|
| 922 |
/** DAG 节点布局模式;默认 `text-flow`。 */
|
| 923 |
layoutMode?: DagLayoutMode;
|
| 924 |
/**
|
| 925 |
+
* linear-arc 家族:相邻节点矩形外侧边的水平间隙(px),决定水平方向疏密;
|
| 926 |
* 默认 {@link LINEAR_ARC_ADJACENT_GAP_DEFAULT}。
|
| 927 |
*/
|
| 928 |
linearArcAdjacentGapPx?: number;
|
| 929 |
/** 被 exclude 规则命中的节点是否完全隐藏(true)还是仅降至 {@link DAG_NODE_HIDDEN_OPACITY}(false,默认)。 */
|
| 930 |
hideExcludedTokens?: boolean;
|
| 931 |
+
/** 是否显示 token tooltip(UI: Show token tooltip;`showTokenInfoOnSelected`)。 */
|
| 932 |
+
showTokenInfoOnSelected?: boolean;
|
| 933 |
+
/** 传播归因(UI: Propagated attribution mode;`recursiveAttributionEnabled`);默认 `false`。 */
|
| 934 |
+
recursiveAttributionEnabled?: boolean;
|
| 935 |
+
/** 直接归因模式下是否展示从焦点出发的下游影响出边;默认 `false`。 */
|
| 936 |
+
showDownstreamInfluence?: boolean;
|
| 937 |
/** 边 Top-P 覆盖阈值(候选池内累计份额);���认 {@link DAG_EDGE_TOP_P_COVERAGE_DEFAULT}。 */
|
| 938 |
edgeTopPCoverage?: number;
|
| 939 |
/** 进入/退出/切换全屏失败时(常见于移动端不支持元素全屏等)。不传则无提示。 */
|
|
|
|
| 963 |
linearArcAdjacentGapPx = clampLinearArcAdjacentGap(iv);
|
| 964 |
}
|
| 965 |
let hideExcludedTokens: boolean = options?.hideExcludedTokens ?? false;
|
| 966 |
+
let showTokenInfoOnSelected: boolean = options?.showTokenInfoOnSelected ?? false;
|
| 967 |
+
let recursiveAttributionEnabled: boolean = options?.recursiveAttributionEnabled ?? false;
|
| 968 |
+
let showDownstreamInfluence: boolean = options?.showDownstreamInfluence ?? false;
|
| 969 |
let edgeTopPCoverage = clampDagEdgeTopPCoverage(
|
| 970 |
options?.edgeTopPCoverage ?? DAG_EDGE_TOP_P_COVERAGE_DEFAULT,
|
| 971 |
);
|
|
|
|
| 993 |
isBatching: () => false,
|
| 994 |
reset: noop,
|
| 995 |
fitViewportToContent: noop,
|
| 996 |
+
getSelectedNodeId: () => null,
|
| 997 |
+
setSelectedNodeId: noop,
|
| 998 |
clearNodeSelection: noop,
|
| 999 |
setDagPlaybackPlaying: noop,
|
| 1000 |
setMeasureWidthPx: noop,
|
|
|
|
| 1003 |
setDagCompactness: noop,
|
| 1004 |
setEdgeTopPCoverage: noop,
|
| 1005 |
setHideExcludedTokens: noop,
|
| 1006 |
+
setShowTokenInfoOnSelected: noop,
|
| 1007 |
+
setRecursiveAttributionEnabled: noop,
|
| 1008 |
+
setShowDownstreamInfluence: noop,
|
| 1009 |
hasPromptSpans: () => false,
|
| 1010 |
detach: noop,
|
| 1011 |
};
|
|
|
|
| 1016 |
detachGenAttributeDagPanel.get(rootEl)?.();
|
| 1017 |
resultsRoot
|
| 1018 |
.selectAll(
|
| 1019 |
+
'.gen-attr-dag-stack, .gen-attr-dag-topk-tooltip, svg.gen-attr-dag-svg, button.gen-attr-dag-refresh, button.gen-attr-dag-play, button.gen-attr-dag-fullscreen'
|
| 1020 |
)
|
| 1021 |
.remove();
|
| 1022 |
|
| 1023 |
const stack = resultsRoot.append('div').attr('class', 'gen-attr-dag-stack');
|
| 1024 |
const stackEl = stack.node() as HTMLElement;
|
| 1025 |
|
| 1026 |
+
const dagTooltipEh = new SimpleEventHandler(stackEl);
|
| 1027 |
+
const dagTooltipRoot = resultsRoot.append('div').attr('class', 'tooltip gen-attr-dag-topk-tooltip');
|
| 1028 |
+
dagTooltipRoot.append('div').attr('class', 'currentToken');
|
| 1029 |
+
dagTooltipRoot.append('div').attr('class', 'myDetail');
|
| 1030 |
+
dagTooltipRoot
|
| 1031 |
+
.append('div')
|
| 1032 |
+
.attr('class', 'gen-attr-dag-topk-tooltip-predictions-scroll')
|
| 1033 |
+
.append('div')
|
| 1034 |
+
.attr('class', 'predictions predictions-table');
|
| 1035 |
+
const dagTopkToolTip = new ToolTip(dagTooltipRoot, dagTooltipEh, {
|
| 1036 |
+
surprisalRowLabel: tr('log perplexity:'),
|
| 1037 |
+
placement: 'parent-bottom-right',
|
| 1038 |
+
pointerInteractive: false,
|
| 1039 |
+
});
|
| 1040 |
+
|
| 1041 |
+
/** DAG Top‑K tooltip:挂载初期为 stub;{@link syncGenAttrDagTopkTooltipImpl} 在 {@link refreshNodeLinkHighlight} 定义之后赋值 */
|
| 1042 |
+
let syncGenAttrDagTopkTooltipImpl: () => void = () => {
|
| 1043 |
+
dagTopkToolTip.hideAndReset();
|
| 1044 |
+
};
|
| 1045 |
+
|
| 1046 |
/** 非 text-flow 时节点不可拖;用该类覆盖选中态的 grab 光标(linear-arc / spiral 等)。 */
|
| 1047 |
function syncStackLayoutDragUi(): void {
|
| 1048 |
stackEl.classList.toggle('gen-attr-dag-no-node-drag-layout', layoutMode !== 'text-flow');
|
|
|
|
| 1091 |
function refreshDagScaleDerivedFromCss(): void {
|
| 1092 |
displayScale = readDisplayScaleFromCss(stackEl);
|
| 1093 |
linkEndInsetPx = linkEndInsetBaseAtUnitScalePx(measureRoot) * displayScale;
|
| 1094 |
+
syncNodeStrokeRects(nodeSel, displayScale);
|
| 1095 |
}
|
| 1096 |
|
| 1097 |
function setDagCompactness(c: number): void {
|
|
|
|
| 1130 |
// 仅用户交互(滚轮/拖平移/双击)计入「改动布局」;程序触发的 transform
|
| 1131 |
// (init 初始缩放、`fitViewportToContent`)`sourceEvent === null`,不置 dirty。
|
| 1132 |
if (event.sourceEvent) layoutDirty = true;
|
| 1133 |
+
syncGenAttrDagTopkTooltipImpl();
|
| 1134 |
});
|
| 1135 |
|
| 1136 |
function applyInitialDagZoom(): void {
|
|
|
|
| 1146 |
const nodeG = rootG.append('g').attr('class', 'gen-attr-dag-nodes');
|
| 1147 |
/** 邻接焦点的高亮边:在节点层之后绘制,避免被节点遮挡 */
|
| 1148 |
const linkGFront = rootG.append('g').attr('class', 'gen-attr-dag-links-front');
|
| 1149 |
+
/** 与视觉节点同几何的透明命中层,置于 linkGFront 之上,避免蓝线挡住 hover/click */
|
| 1150 |
+
const nodeGHit = rootG.append('g').attr('class', 'gen-attr-dag-nodes-hit');
|
| 1151 |
|
| 1152 |
const graph = new DirectedGraph<DagNodeAttrs>();
|
| 1153 |
let nodes: DagNode[] = [];
|
| 1154 |
+
/** `nodes` 按 step 降序(新→旧→prompt)排列的副本,供 {@link computeFocusAttributionState} 使用,避免每次 hover 重新排序。 */
|
| 1155 |
+
let nodesSortedByStepDesc: DagNode[] = [];
|
| 1156 |
let links: DagLink[] = [];
|
| 1157 |
+
/** 按 targetId 索引的入边列表,供 {@link computeFocusAttributionState} 使用,避免每次 hover O(N×E) 全扫描。 */
|
| 1158 |
+
const incomingLinksByTarget = new Map<string, DagLink[]>();
|
| 1159 |
+
/** 灰边渲染强度缓存;图结构变化({@link syncGraphToSvg})或 {@link reset} 时置 null 失效。 */
|
| 1160 |
+
let grayRenderCache: Map<string, number> | null = null;
|
| 1161 |
let stepProcessed = 0;
|
| 1162 |
let selectedId: string | null = null;
|
| 1163 |
+
/** 悬浮节点 id;无选中时参与归因预览焦点,有选中时仅驱动 `--hover` 等样式,不改归因焦点 */
|
| 1164 |
let hoveredId: string | null = null;
|
| 1165 |
+
/** 最近一次 {@link refreshNodeLinkHighlight} 计算出的归因状态(基于 {@link effectiveFocusId});tooltip 用于展示归因份额 */
|
| 1166 |
+
let currentFocusState: FocusAttributionState | null = null;
|
| 1167 |
+
|
| 1168 |
+
/** 归因预览焦点:有选中则固定选中节点,否则随悬浮临时预览 */
|
| 1169 |
+
function effectiveFocusId(): string | null {
|
| 1170 |
+
return selectedId ?? hoveredId;
|
| 1171 |
+
}
|
| 1172 |
+
|
| 1173 |
+
/** tooltip 锚点:悬浮任意节点时展示该节点;无悬浮则展示焦点节点(选中 > 无) */
|
| 1174 |
+
function tooltipFocusId(): string | null {
|
| 1175 |
+
if (hoveredId != null) {
|
| 1176 |
+
return graph.hasNode(hoveredId) ? hoveredId : null;
|
| 1177 |
+
}
|
| 1178 |
+
return effectiveFocusId();
|
| 1179 |
+
}
|
| 1180 |
/**
|
| 1181 |
* 与 {@link pruneDagLinksTouchingFullyExcludedNodes} / 预处理同源:全串上的 exclude 半开区间,
|
| 1182 |
* 供节点「隐藏」透明度判定({@link isOffsetSpanFullyExcluded})。在 {@link setPromptTokenSpans} 与每步
|
|
|
|
| 1202 |
.selectAll<SVGGElement, DagLink>('g.gen-attr-dag-link')
|
| 1203 |
.data<DagLink>([], dagLinkDataKey);
|
| 1204 |
let nodeSel = nodeG.selectAll<SVGGElement, DagNode>('g.gen-attr-dag-node').data<DagNode>([], (d) => d.id);
|
| 1205 |
+
let nodeHitSel = nodeGHit
|
| 1206 |
+
.selectAll<SVGGElement, DagNode>('g.gen-attr-dag-node-hit')
|
| 1207 |
+
.data<DagNode>([], (d) => d.id);
|
| 1208 |
+
|
| 1209 |
+
/** 与 {@link nodeSel} 同序同 transform(paint 各布局模式之后调用) */
|
| 1210 |
+
function syncNodeHitTransforms(): void {
|
| 1211 |
+
const visualNodes = nodeSel.nodes();
|
| 1212 |
+
nodeHitSel.attr('transform', (_d, i) => d3.select(visualNodes[i]).attr('transform'));
|
| 1213 |
+
}
|
| 1214 |
+
|
| 1215 |
+
function bindNodePointerHandlers(
|
| 1216 |
+
sel: d3.Selection<SVGGElement, DagNode, SVGGElement | null, unknown>,
|
| 1217 |
+
): void {
|
| 1218 |
+
sel.on('mouseenter', (_event, d) => {
|
| 1219 |
+
hoveredId = d.id;
|
| 1220 |
+
refreshNodeLinkHighlight();
|
| 1221 |
+
})
|
| 1222 |
+
.on('mouseleave', () => {
|
| 1223 |
+
hoveredId = null;
|
| 1224 |
+
refreshNodeLinkHighlight();
|
| 1225 |
+
})
|
| 1226 |
+
.on('click', (event, d) => {
|
| 1227 |
+
event.stopPropagation();
|
| 1228 |
+
setSelectedNodeId(selectedId === d.id ? null : d.id);
|
| 1229 |
+
});
|
| 1230 |
+
}
|
| 1231 |
|
| 1232 |
function syncSvgSize(): void {
|
| 1233 |
const { w, h } = stackLayoutViewportPx(stackEl);
|
|
|
|
| 1235 |
}
|
| 1236 |
|
| 1237 |
function paint(): void {
|
| 1238 |
+
syncNodeStrokeRects(nodeSel, displayScale);
|
| 1239 |
+
if (layoutMode === 'linear-arc' || layoutMode === 'linear-arc-step-down') {
|
| 1240 |
const layoutNodes = hideExcludedTokens
|
| 1241 |
? nodes.filter((n) => !isOffsetSpanFullyExcluded(n.start, n.end, dagExcludeIntervals))
|
| 1242 |
: nodes;
|
|
|
|
| 1245 |
nodeSel,
|
| 1246 |
nodes: layoutNodes,
|
| 1247 |
adjacentGapPx: linearArcAdjacentGapPx,
|
| 1248 |
+
variant: layoutMode === 'linear-arc-step-down' ? 'step-down' : 'flat',
|
| 1249 |
getLinkNodes: (d) => ({
|
| 1250 |
src: endpointNode(d.source, graph),
|
| 1251 |
tgt: endpointNode(d.target, graph),
|
| 1252 |
}),
|
| 1253 |
});
|
| 1254 |
+
} else if (layoutMode === 'spiral') {
|
|
|
|
|
|
|
| 1255 |
const layoutNodes = hideExcludedTokens
|
| 1256 |
? nodes.filter((n) => !isOffsetSpanFullyExcluded(n.start, n.end, dagExcludeIntervals))
|
| 1257 |
: nodes;
|
|
|
|
| 1265 |
tgt: endpointNode(d.target, graph),
|
| 1266 |
}),
|
| 1267 |
});
|
| 1268 |
+
} else {
|
| 1269 |
+
paintTextFlowLayout({
|
| 1270 |
+
linkSel,
|
| 1271 |
+
nodeSel,
|
| 1272 |
+
linkEndInsetPx,
|
| 1273 |
+
getLinkNodes: (d) => ({
|
| 1274 |
+
src: endpointNode(d.source, graph),
|
| 1275 |
+
tgt: endpointNode(d.target, graph),
|
| 1276 |
+
}),
|
| 1277 |
+
});
|
| 1278 |
}
|
| 1279 |
+
syncNodeHitTransforms();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1280 |
}
|
| 1281 |
|
| 1282 |
let dragPointerOffset: { x: number; y: number } | null = null;
|
|
|
|
| 1304 |
d.cx = x - offset.x;
|
| 1305 |
d.cy = y - offset.y;
|
| 1306 |
paint();
|
| 1307 |
+
syncGenAttrDagTopkTooltipImpl();
|
| 1308 |
})
|
| 1309 |
.on('end', () => {
|
| 1310 |
dragPointerOffset = null;
|
| 1311 |
});
|
| 1312 |
|
| 1313 |
+
/** 焦点高亮:递归强调来源链,直接强调一跳关系。 */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1314 |
function refreshNodeLinkHighlight(): void {
|
| 1315 |
+
const focusId = effectiveFocusId();
|
| 1316 |
+
const focusState = focusId
|
| 1317 |
+
? computeFocusAttributionState(graph, nodesSortedByStepDesc, incomingLinksByTarget, focusId, {
|
| 1318 |
+
maxIncomingDepth: recursiveAttributionEnabled ? Number.POSITIVE_INFINITY : 1,
|
| 1319 |
+
includeDownstreamInfluence: !recursiveAttributionEnabled && showDownstreamInfluence,
|
| 1320 |
+
})
|
| 1321 |
+
: null;
|
| 1322 |
+
currentFocusState = focusState;
|
| 1323 |
+
const focusNodeIds = focusState?.activeNodeIds ?? null;
|
| 1324 |
+
const nodeStrokeShareById =
|
| 1325 |
+
!recursiveAttributionEnabled || focusState == null || focusId == null
|
| 1326 |
+
? null
|
| 1327 |
+
: computeUpstreamNodeStrokeShareById(focusState.nodeShareById, graph, focusId);
|
| 1328 |
+
const nodeStrokeRenderById =
|
| 1329 |
+
nodeStrokeShareById == null ? null : buildNodeStrokeRenderStrengthById(nodeStrokeShareById);
|
| 1330 |
+
const focusTargetMiRatio =
|
| 1331 |
+
focusId != null && graph.hasNode(focusId)
|
| 1332 |
+
? nodeTargetMiRatio(graph.getNodeAttributes(focusId) as DagNode)
|
| 1333 |
+
: 1;
|
| 1334 |
+
const incomingHighlightRenderByKey =
|
| 1335 |
+
focusState == null
|
| 1336 |
+
? new Map<string, number>()
|
| 1337 |
+
: buildMaxNormalizedRenderStrengthByKey(focusState.incomingEdgeShareByKey, focusTargetMiRatio);
|
| 1338 |
+
const downstreamHighlightRenderByKey =
|
| 1339 |
+
focusState == null
|
| 1340 |
+
? new Map<string, number>()
|
| 1341 |
+
: buildMaxNormalizedRenderStrengthByKey(focusState.downstreamEdgeStrengthByKey);
|
| 1342 |
+
grayRenderCache ??= buildGrayRenderStrengthByEdgeKey(graph, incomingLinksByTarget);
|
| 1343 |
+
const grayRenderByKey = grayRenderCache;
|
| 1344 |
+
const nodeDisplay = (d: DagNode): string | null =>
|
| 1345 |
+
hideExcludedTokens && isOffsetSpanFullyExcluded(d.start, d.end, dagExcludeIntervals)
|
| 1346 |
+
? 'none'
|
| 1347 |
+
: null;
|
| 1348 |
nodeSel
|
| 1349 |
.classed('gen-attr-dag-node--hover', (d) => hoveredId === d.id)
|
| 1350 |
.classed('gen-attr-dag-node--selected', (d) => selectedId === d.id)
|
| 1351 |
+
.style('display', nodeDisplay)
|
|
|
|
|
|
|
|
|
|
| 1352 |
.attr('opacity', (d) => {
|
| 1353 |
+
const nodeFullyHighlighted = recursiveAttributionEnabled
|
| 1354 |
+
? d.id === focusId || (nodeStrokeShareById?.has(d.id) ?? false)
|
| 1355 |
+
: (focusNodeIds?.has(d.id) ?? false);
|
| 1356 |
+
if (nodeFullyHighlighted) return 1;
|
| 1357 |
if (isOffsetSpanFullyExcluded(d.start, d.end, dagExcludeIntervals)) {
|
| 1358 |
return hideExcludedTokens ? 0 : DAG_NODE_HIDDEN_OPACITY;
|
| 1359 |
}
|
|
|
|
| 1361 |
const isPromptLeaf = hasGenTokens && d.step === -1 && graph.outDegree(d.id) === 0;
|
| 1362 |
if (focusId || isPromptLeaf) return DAG_NODE_WEAKEN_OPACITY;
|
| 1363 |
return 1;
|
| 1364 |
+
})
|
| 1365 |
+
.classed('gen-attr-dag-node--recursive-chain', (d) => nodeStrokeShareById?.has(d.id) ?? false)
|
| 1366 |
+
.style(CSS_VAR_DAG_NODE_RECURSIVE_SHARE, (d) => {
|
| 1367 |
+
const renderStrength = nodeStrokeRenderById?.get(d.id);
|
| 1368 |
+
return renderStrength != null ? String(renderStrength) : null;
|
| 1369 |
});
|
| 1370 |
+
nodeHitSel
|
| 1371 |
+
.classed('gen-attr-dag-node--hover', (d) => hoveredId === d.id)
|
| 1372 |
+
.classed('gen-attr-dag-node--selected', (d) => selectedId === d.id)
|
| 1373 |
+
.style('display', nodeDisplay);
|
| 1374 |
+
// 每条边:颜色/强度(见 resolveDagLinkHighlightDisplay)、`<title>` 一并刷新(含 linkGFront 高亮边)。
|
| 1375 |
+
rootG.selectAll<SVGGElement, DagLink>('g.gen-attr-dag-link').each(function(d) {
|
| 1376 |
+
const srcId = endpointNode(d.source, graph).id;
|
| 1377 |
+
const tgtId = endpointNode(d.target, graph).id;
|
| 1378 |
+
const edgeKey = dagLinkEndpointKey(srcId, tgtId);
|
| 1379 |
+
const { stroke, renderStrength, linkStrength, recursiveAttributionShare } =
|
| 1380 |
+
resolveDagLinkHighlightDisplay(
|
| 1381 |
+
d,
|
| 1382 |
+
edgeKey,
|
| 1383 |
+
focusState,
|
| 1384 |
+
recursiveAttributionEnabled,
|
| 1385 |
+
grayRenderByKey,
|
| 1386 |
+
incomingHighlightRenderByKey,
|
| 1387 |
+
downstreamHighlightRenderByKey,
|
| 1388 |
+
);
|
| 1389 |
const g = d3.select(this);
|
| 1390 |
+
const srcAttrs = graph.getNodeAttributes(srcId) as DagNode;
|
| 1391 |
+
const tgtAttrs = graph.getNodeAttributes(tgtId) as DagNode;
|
| 1392 |
+
g.select('title').text(
|
| 1393 |
+
buildLinkTitleText({
|
| 1394 |
+
normalizedScore: d.normalizedScore,
|
| 1395 |
+
mutualInformationRatio: d.mutualInformationRatio,
|
| 1396 |
+
attributionShare: d.attributionShare,
|
| 1397 |
+
alignmentNote: d.alignmentNote,
|
| 1398 |
+
src: srcAttrs,
|
| 1399 |
+
tgt: tgtAttrs,
|
| 1400 |
+
recursiveAttributionShare,
|
| 1401 |
+
linkStrength,
|
| 1402 |
+
}),
|
| 1403 |
+
);
|
| 1404 |
+
g.select('path.gen-attr-dag-link-visible').attr('stroke', stroke).attr('stroke-opacity', renderStrength);
|
| 1405 |
linkMarkersDefs
|
| 1406 |
.select<SVGPathElement>(`#${dagLinkMarkerElementId(d.source, d.target)} path`)
|
| 1407 |
.attr('stroke', stroke)
|
| 1408 |
+
.attr('stroke-opacity', renderStrength);
|
| 1409 |
+
|
| 1410 |
+
const incident =
|
| 1411 |
+
focusState != null &&
|
| 1412 |
+
(focusState.incomingEdgeShareByKey.has(edgeKey) ||
|
| 1413 |
+
focusState.downstreamEdgeStrengthByKey.has(edgeKey));
|
|
|
|
|
|
|
|
|
|
| 1414 |
const parent = incident ? linkGFront : linkG;
|
| 1415 |
const parentNode = parent.node()!;
|
| 1416 |
if (this.parentNode !== parentNode) {
|
| 1417 |
parentNode.appendChild(this as SVGGElement);
|
| 1418 |
}
|
| 1419 |
});
|
| 1420 |
+
|
| 1421 |
+
syncGenAttrDagTopkTooltipImpl();
|
| 1422 |
}
|
| 1423 |
|
| 1424 |
+
syncGenAttrDagTopkTooltipImpl = (): void => {
|
| 1425 |
+
if (!showTokenInfoOnSelected) {
|
| 1426 |
+
dagTopkToolTip.hideAndReset();
|
| 1427 |
+
return;
|
| 1428 |
+
}
|
| 1429 |
+
const focusIdNext = tooltipFocusId();
|
| 1430 |
+
if (!focusIdNext || !graph.hasNode(focusIdNext)) {
|
| 1431 |
+
dagTopkToolTip.hideAndReset();
|
| 1432 |
+
return;
|
| 1433 |
+
}
|
| 1434 |
+
const attrs = graph.getNodeAttributes(focusIdNext) as DagNode;
|
| 1435 |
+
// 生成节点必须有 gltrTooltipToken;prompt 节点用 label 构造最简 token
|
| 1436 |
+
const isPromptNode = attrs.step < 0;
|
| 1437 |
+
if (!isPromptNode && !attrs.gltrTooltipToken) {
|
| 1438 |
+
dagTopkToolTip.hideAndReset();
|
| 1439 |
+
return;
|
| 1440 |
+
}
|
| 1441 |
+
const rect = nodeSel
|
| 1442 |
+
.filter((d: DagNode) => d.id === focusIdNext)
|
| 1443 |
+
.select<SVGRectElement>('rect.gen-attr-dag-node-fill')
|
| 1444 |
+
.node();
|
| 1445 |
+
if (!rect) {
|
| 1446 |
+
dagTopkToolTip.hideAndReset();
|
| 1447 |
+
return;
|
| 1448 |
+
}
|
| 1449 |
+
const tokenForTooltip: FrontendToken = attrs.gltrTooltipToken ?? {
|
| 1450 |
+
raw: attrs.label,
|
| 1451 |
+
offset: [attrs.start, attrs.end],
|
| 1452 |
+
pred_topk: [],
|
| 1453 |
+
};
|
| 1454 |
+
|
| 1455 |
+
// 构建 augment:归因份额行(token 下方最前)+ CI/MI 行(surprisal 之后)
|
| 1456 |
+
const rowsBeforeInfo: ToolTipUpdateAugment['rowsBeforeInfo'] = [];
|
| 1457 |
+
if (selectedId && hoveredId && currentFocusState && hoveredId !== selectedId && graph.hasNode(selectedId)) {
|
| 1458 |
+
const selectedStep = (graph.getNodeAttributes(selectedId) as DagNode).step;
|
| 1459 |
+
// 归因范围:选中 token 之前的所有 token(prompt 节点 step=-1,生成节点 step < selectedStep)
|
| 1460 |
+
const inAttributionRange =
|
| 1461 |
+
selectedStep >= 0 &&
|
| 1462 |
+
(attrs.step === -1 || (attrs.step >= 0 && attrs.step < selectedStep));
|
| 1463 |
+
if (inAttributionRange) {
|
| 1464 |
+
const share = currentFocusState.nodeShareById.get(hoveredId) ?? 0;
|
| 1465 |
+
if (recursiveAttributionEnabled) {
|
| 1466 |
+
const stay = share * (1 - nodePropagationMiRatio(attrs));
|
| 1467 |
+
rowsBeforeInfo.push(
|
| 1468 |
+
{ label: tr('Attribution share (Total):'), value: formatAttributionSharePercentForTooltip(share) },
|
| 1469 |
+
{ label: tr('Attribution share (Self):'), value: formatAttributionSharePercentForTooltip(stay) },
|
| 1470 |
+
);
|
| 1471 |
+
} else {
|
| 1472 |
+
rowsBeforeInfo.push({
|
| 1473 |
+
label: tr('Attribution share:'),
|
| 1474 |
+
value: formatAttributionSharePercentForTooltip(share),
|
| 1475 |
+
});
|
| 1476 |
+
}
|
| 1477 |
+
}
|
| 1478 |
+
}
|
| 1479 |
+
const rowsAfterSurprisal: ToolTipUpdateAugment['rowsAfterSurprisal'] =
|
| 1480 |
+
attrs.dagCiMiTooltipRow != null ? [attrs.dagCiMiTooltipRow] : [];
|
| 1481 |
+
const augment: ToolTipUpdateAugment | undefined =
|
| 1482 |
+
rowsBeforeInfo.length > 0 || rowsAfterSurprisal.length > 0
|
| 1483 |
+
? { rowsBeforeInfo, rowsAfterSurprisal }
|
| 1484 |
+
: undefined;
|
| 1485 |
+
dagTopkToolTip.updateData({ tokenData: tokenForTooltip }, rect, augment);
|
| 1486 |
+
};
|
| 1487 |
+
|
| 1488 |
function setSelectedNodeId(id: string | null): void {
|
| 1489 |
+
if (id != null && !graph.hasNode(id)) {
|
| 1490 |
+
throw new Error(`genAttributeDagView: unknown node id ${id}`);
|
| 1491 |
+
}
|
| 1492 |
selectedId = id;
|
| 1493 |
refreshNodeLinkHighlight();
|
| 1494 |
}
|
|
|
|
| 1499 |
|
| 1500 |
/** 将当前 `nodes` / `links` 同步到 SVG:join 新 DOM、`paint` 几何、`refreshNodeLinkHighlight` 样式。 */
|
| 1501 |
function syncGraphToSvg(): void {
|
| 1502 |
+
grayRenderCache = null;
|
| 1503 |
linkGFront.selectAll<SVGGElement, DagLink>('g.gen-attr-dag-link').each(function() {
|
| 1504 |
linkG.node()!.appendChild(this as SVGGElement);
|
| 1505 |
});
|
|
|
|
| 1535 |
g.each(function(d: DagLink) {
|
| 1536 |
const el = d3.select(this);
|
| 1537 |
const mkId = dagLinkMarkerElementId(d.source, d.target);
|
| 1538 |
+
el.append('title');
|
| 1539 |
el.append('path')
|
| 1540 |
.attr('class', 'gen-attr-dag-link-visible')
|
| 1541 |
.attr('fill', 'none')
|
|
|
|
| 1547 |
return g;
|
| 1548 |
});
|
| 1549 |
// 不在此处全量重置 marker `stroke-opacity`:紧接着的 {@link refreshNodeLinkHighlight} 会按边
|
| 1550 |
+
// 逐条写 resolveDagLinkHighlightDisplay(与 `<title>` 中 Link strength 同源),任何前值都会被覆盖,全量重置纯冗余。
|
| 1551 |
|
| 1552 |
nodeSel = nodeG
|
| 1553 |
.selectAll<SVGGElement, DagNode>('g.gen-attr-dag-node')
|
| 1554 |
.data(nodes, (d) => d.id)
|
| 1555 |
+
.join((enter) => {
|
| 1556 |
+
// 节点身份 append-only、几何(nodeW/nodeH)一旦建立不再变化(drag 仅改 x/y,
|
| 1557 |
+
// 由 paint 通过 transform 处理),故与几何相关的属性仅在 enter 写一次即可;
|
| 1558 |
+
// 同理 `--prompt` class 依据 step === -1,step 初始化后不变。
|
| 1559 |
+
const g = enter
|
| 1560 |
+
.append('g')
|
| 1561 |
+
.attr('class', 'gen-attr-dag-node')
|
| 1562 |
+
.style('--gen-attr-dag-node-ci-visual-scale', (d: DagNode) => String(d.ciVisualScale));
|
| 1563 |
+
g.classed('gen-attr-dag-node--prompt', (d: DagNode) => d.step === -1);
|
| 1564 |
+
g.append('rect').attr('class', 'gen-attr-dag-node-stroke');
|
| 1565 |
+
g.append('rect')
|
| 1566 |
+
.attr('class', 'gen-attr-dag-node-fill')
|
| 1567 |
+
.attr('width', (d: DagNode) => d.nodeW)
|
| 1568 |
+
.attr('height', (d: DagNode) => d.nodeH)
|
| 1569 |
+
.attr('rx', (d: DagNode) => nodeRx(d))
|
| 1570 |
+
.attr('ry', (d: DagNode) => nodeRx(d));
|
| 1571 |
+
g.append('text')
|
| 1572 |
+
.attr('class', 'gen-attr-dag-node-text')
|
| 1573 |
+
.attr('xml:space', 'preserve')
|
| 1574 |
+
.attr('pointer-events', 'none')
|
| 1575 |
+
.attr('text-anchor', 'middle')
|
| 1576 |
+
.attr('dominant-baseline', 'central')
|
| 1577 |
+
.attr('x', (d: DagNode) => d.nodeW / 2)
|
| 1578 |
+
.attr('y', (d: DagNode) => d.nodeH / 2)
|
| 1579 |
+
.text((d: DagNode) => d.displayLabel);
|
| 1580 |
+
return g;
|
| 1581 |
+
});
|
| 1582 |
+
|
| 1583 |
+
nodeHitSel = nodeGHit
|
| 1584 |
+
.selectAll<SVGGElement, DagNode>('g.gen-attr-dag-node-hit')
|
| 1585 |
+
.data(nodes, (d) => d.id)
|
| 1586 |
+
.join((enter) => {
|
| 1587 |
+
const g = enter.append('g').attr('class', 'gen-attr-dag-node-hit');
|
| 1588 |
+
g.append('rect')
|
| 1589 |
+
.attr('class', 'gen-attr-dag-node-hit-target')
|
| 1590 |
+
.attr('width', (d: DagNode) => d.nodeW)
|
| 1591 |
+
.attr('height', (d: DagNode) => d.nodeH)
|
| 1592 |
+
.attr('rx', (d: DagNode) => nodeRx(d))
|
| 1593 |
+
.attr('ry', (d: DagNode) => nodeRx(d));
|
| 1594 |
+
bindNodePointerHandlers(g);
|
| 1595 |
+
return g.call(drag);
|
| 1596 |
+
});
|
|
|
|
|
|
|
| 1597 |
|
| 1598 |
paint();
|
| 1599 |
refreshNodeLinkHighlight();
|
|
|
|
| 1649 |
nodeH: g.height * displayScale,
|
| 1650 |
ciVisualScale: 1,
|
| 1651 |
displayLabel,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1652 |
};
|
| 1653 |
graph.addNode(srcId, srcNode);
|
| 1654 |
nodes.push(srcNode);
|
|
|
|
| 1665 |
getEffectiveExcludePromptPatternsText(),
|
| 1666 |
getEffectiveExcludeGeneratedPatternsText(),
|
| 1667 |
);
|
| 1668 |
+
// prompt 节点 step=-1 始终排在末尾;重建一次即可(setPromptTokenSpans 只调一次)。
|
| 1669 |
+
nodesSortedByStepDesc = [...nodes].sort((a, b) => b.step - a.step || b.start - a.start);
|
| 1670 |
if (batchDepth === 0) syncGraphToSvg();
|
| 1671 |
}
|
| 1672 |
|
|
|
|
| 1694 |
spaceDotExceptBeforeAsciiLetterOrNumber: true,
|
| 1695 |
});
|
| 1696 |
const ciVisualScale = dagGeneratedNodeCiVisualScale(response.target_prob);
|
| 1697 |
+
const gltrTooltipToken = frontendTokenFromGenAttrStep(step);
|
| 1698 |
+
const dagCiMiTooltipRow = dagCiMiTooltipRowForProb(response.target_prob);
|
| 1699 |
const targetNode: DagNode = {
|
| 1700 |
id: targetId,
|
| 1701 |
label: token,
|
|
|
|
| 1707 |
nodeW: g.width * displayScale * ciVisualScale,
|
| 1708 |
nodeH: g.height * displayScale * ciVisualScale,
|
| 1709 |
ciVisualScale,
|
| 1710 |
+
dagTargetProb: response.target_prob,
|
| 1711 |
displayLabel,
|
| 1712 |
+
...(gltrTooltipToken != null ? { gltrTooltipToken } : {}),
|
| 1713 |
+
...(dagCiMiTooltipRow != null ? { dagCiMiTooltipRow } : {}),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1714 |
};
|
| 1715 |
graph.addNode(targetId, targetNode);
|
| 1716 |
nodes.push(targetNode);
|
| 1717 |
+
// 新 token 的 step 最大,直接放到排序列表最前面,无需重新全排序。
|
| 1718 |
+
nodesSortedByStepDesc.unshift(targetNode);
|
| 1719 |
snapSubwordNode(targetNode, nodes.length >= 2 ? nodes[nodes.length - 2]! : null);
|
| 1720 |
|
| 1721 |
// align → exclude → rank:Top-N / β / cumP 在节点语义上工作(合并型「如下」/ 拆分型等)。
|
|
|
|
| 1738 |
const selected = phase2RankAndSparsify(afterExclude, { cumulativeShare: edgeTopPCoverage });
|
| 1739 |
|
| 1740 |
const mutualInformationRatio = computeMutualInformationRatio(response.target_prob);
|
| 1741 |
+
const selectedForDisplay = selected.filter((item) => {
|
| 1742 |
+
const normalizedScore = item.score;
|
| 1743 |
+
const edgeVisibility =
|
| 1744 |
+
(dagDecayAttributionToHighSurprisalTargetEnabled ? mutualInformationRatio : 1) * normalizedScore;
|
| 1745 |
+
return edgeVisibility >= DAG_EDGE_MIN_NORMALIZED_SCORE;
|
| 1746 |
+
});
|
|
|
|
|
|
|
| 1747 |
const massSum = selectedForDisplay.reduce((acc, t) => acc + Math.max(0, t.poolMassFrac), 0);
|
| 1748 |
+
const linksForTarget: DagLink[] = [];
|
| 1749 |
for (const item of selectedForDisplay) {
|
| 1750 |
const srcId = item.nodeId;
|
| 1751 |
if (!graph.hasNode(srcId)) {
|
|
|
|
| 1766 |
const edgeAttrs = {
|
| 1767 |
normalizedScore: item.score,
|
| 1768 |
mutualInformationRatio,
|
| 1769 |
+
attributionShare: share,
|
| 1770 |
...(alignmentNote ? { alignmentNote } : {}),
|
| 1771 |
};
|
| 1772 |
graph.addEdge(srcId, targetId, edgeAttrs);
|
| 1773 |
+
const newLink: DagLink = {
|
|
|
|
|
|
|
| 1774 |
source: srcId,
|
| 1775 |
target: targetId,
|
| 1776 |
...edgeAttrs,
|
| 1777 |
+
};
|
| 1778 |
+
links.push(newLink);
|
| 1779 |
+
linksForTarget.push(newLink);
|
| 1780 |
}
|
| 1781 |
+
if (linksForTarget.length > 0) incomingLinksByTarget.set(targetId, linksForTarget);
|
| 1782 |
|
| 1783 |
const excludeIntervals = collectGenAttrDagExcludeIntervals(
|
| 1784 |
intervalCtx,
|
|
|
|
| 1787 |
getEffectiveExcludeGeneratedPatternsText(),
|
| 1788 |
);
|
| 1789 |
dagExcludeIntervals = excludeIntervals;
|
| 1790 |
+
pruneDagLinksTouchingFullyExcludedNodes(graph, links, incomingLinksByTarget, excludeIntervals);
|
| 1791 |
|
| 1792 |
stepProcessed++;
|
| 1793 |
+
// 每步生成后:默认选中本步新生成的 token;无其它选中时悬浮仍可临时预览
|
| 1794 |
selectedId = targetId;
|
| 1795 |
if (batchDepth === 0) {
|
| 1796 |
syncGraphToSvg();
|
|
|
|
| 1806 |
textMeasure.reset();
|
| 1807 |
graph.clear();
|
| 1808 |
nodes = [];
|
| 1809 |
+
nodesSortedByStepDesc = [];
|
| 1810 |
links = [];
|
| 1811 |
+
incomingLinksByTarget.clear();
|
| 1812 |
+
grayRenderCache = null;
|
| 1813 |
stepProcessed = 0;
|
| 1814 |
selectedId = null;
|
| 1815 |
hoveredId = null;
|
| 1816 |
+
dagTopkToolTip.hideAndReset();
|
| 1817 |
linkMarkersDefs.selectAll('marker').remove();
|
| 1818 |
linkG.selectAll('*').remove();
|
| 1819 |
linkGFront.selectAll('*').remove();
|
| 1820 |
nodeG.selectAll('*').remove();
|
| 1821 |
+
nodeGHit.selectAll('*').remove();
|
| 1822 |
dagExcludeIntervals = [];
|
| 1823 |
linkSel = rootG
|
| 1824 |
.selectAll<SVGGElement, DagLink>('g.gen-attr-dag-link')
|
| 1825 |
.data<DagLink>([], dagLinkDataKey);
|
| 1826 |
nodeSel = nodeG.selectAll<SVGGElement, DagNode>('g.gen-attr-dag-node').data<DagNode>([], (d) => d.id);
|
| 1827 |
+
nodeHitSel = nodeGHit
|
| 1828 |
+
.selectAll<SVGGElement, DagNode>('g.gen-attr-dag-node-hit')
|
| 1829 |
+
.data<DagNode>([], (d) => d.id);
|
| 1830 |
layoutDirty = preserveUserViewport ? wasLayoutDirty : false;
|
| 1831 |
userDraggedNodes = false;
|
| 1832 |
}
|
|
|
|
| 1845 |
const { w, h } = stackLayoutViewportPx(stackEl);
|
| 1846 |
const innerW = Math.max(w - 2 * pad, 1);
|
| 1847 |
const innerH = Math.max(h - 2 * pad, 1);
|
| 1848 |
+
if (isLinearArcFamilyLayout(layoutMode)) {
|
| 1849 |
/** 仅用 token 行宽度定比;竖直按行中心居中(弧不参与 bbox → 不致上下抖) */
|
| 1850 |
const bn = nodeG.node()!.getBBox();
|
| 1851 |
const bw = Math.max(bn.width, 1e-6);
|
|
|
|
| 1881 |
const ty = pad + halfH;
|
| 1882 |
svg.call(zoomBehavior.transform, d3.zoomIdentity.translate(tx, ty).scale(k));
|
| 1883 |
} else if (layoutMode === 'text-flow') {
|
| 1884 |
+
/** `rootG` 整包 bbox + 宽高双约束顶对齐 */
|
| 1885 |
+
const padTf = DAG_TEXT_FLOW_FIT_PAD_PX;
|
| 1886 |
+
const innerWTextFlow = Math.max(w - 2 * padTf, 1);
|
| 1887 |
+
const innerHTextFlow = Math.max(h - 2 * padTf, 1);
|
| 1888 |
const b = rootG.node()!.getBBox();
|
| 1889 |
const bw = Math.max(b.width, 1e-6);
|
| 1890 |
const bh = Math.max(b.height, 1e-6);
|
| 1891 |
+
const kRaw = Math.min(innerWTextFlow / bw, innerHTextFlow / bh);
|
| 1892 |
const k = Math.min(Number.isFinite(kRaw) && kRaw > 0 ? kRaw : k0, k0);
|
| 1893 |
+
const tx = padTf - k * b.x;
|
| 1894 |
+
const ty = padTf - k * b.y;
|
| 1895 |
svg.call(zoomBehavior.transform, d3.zoomIdentity.translate(tx, ty).scale(k));
|
| 1896 |
} else {
|
| 1897 |
const _: never = layoutMode;
|
|
|
|
| 1956 |
if (linearArcAdjacentGapPx === next) return;
|
| 1957 |
linearArcAdjacentGapPx = next;
|
| 1958 |
if (opts?.skipRefit || batchDepth > 0) return;
|
| 1959 |
+
if (!isLinearArcFamilyLayout(layoutMode) || nodes.length === 0) return;
|
| 1960 |
paint();
|
| 1961 |
fitViewportToContent(true);
|
| 1962 |
}
|
|
|
|
| 1970 |
fitViewportToContent(true);
|
| 1971 |
}
|
| 1972 |
|
| 1973 |
+
function setShowTokenInfoOnSelected(show: boolean): void {
|
| 1974 |
+
if (showTokenInfoOnSelected === show) return;
|
| 1975 |
+
showTokenInfoOnSelected = show;
|
| 1976 |
+
syncGenAttrDagTopkTooltipImpl();
|
| 1977 |
+
}
|
| 1978 |
+
|
| 1979 |
+
/** 传播归因(UI: Propagated attribution mode;`recursiveAttributionEnabled`):向上追到来源;关闭则为直接归因(一跳)。 */
|
| 1980 |
+
function setRecursiveAttributionEnabled(enabled: boolean): void {
|
| 1981 |
+
if (recursiveAttributionEnabled === enabled) return;
|
| 1982 |
+
recursiveAttributionEnabled = enabled;
|
| 1983 |
+
refreshNodeLinkHighlight();
|
| 1984 |
+
}
|
| 1985 |
+
|
| 1986 |
+
function setShowDownstreamInfluence(show: boolean): void {
|
| 1987 |
+
if (showDownstreamInfluence === show) return;
|
| 1988 |
+
showDownstreamInfluence = show;
|
| 1989 |
+
refreshNodeLinkHighlight();
|
| 1990 |
+
}
|
| 1991 |
+
|
| 1992 |
const fullscreenBtn = resultsRoot
|
| 1993 |
.append('button')
|
| 1994 |
.attr('type', 'button')
|
|
|
|
| 2058 |
ro.disconnect();
|
| 2059 |
document.removeEventListener('fullscreenchange', refreshFullscreenChrome);
|
| 2060 |
document.removeEventListener(CSS_PSEUDO_FULLSCREEN_CHANGE_EVENT, refreshFullscreenChrome);
|
| 2061 |
+
dagTopkToolTip.dispose();
|
| 2062 |
resultsRoot
|
| 2063 |
.selectAll(
|
| 2064 |
+
'.gen-attr-dag-stack, .gen-attr-dag-topk-tooltip, button.gen-attr-dag-refresh, button.gen-attr-dag-play, button.gen-attr-dag-fullscreen'
|
| 2065 |
)
|
| 2066 |
.remove();
|
| 2067 |
detachGenAttributeDagPanel.delete(rootEl);
|
|
|
|
| 2077 |
isBatching,
|
| 2078 |
reset,
|
| 2079 |
fitViewportToContent,
|
| 2080 |
+
getSelectedNodeId: () => selectedId,
|
| 2081 |
+
setSelectedNodeId,
|
| 2082 |
clearNodeSelection,
|
| 2083 |
setDagPlaybackPlaying,
|
| 2084 |
setMeasureWidthPx,
|
|
|
|
| 2087 |
setDagCompactness,
|
| 2088 |
setEdgeTopPCoverage,
|
| 2089 |
setHideExcludedTokens,
|
| 2090 |
+
setShowTokenInfoOnSelected,
|
| 2091 |
+
setRecursiveAttributionEnabled,
|
| 2092 |
+
setShowDownstreamInfluence,
|
| 2093 |
hasPromptSpans: () => nodes.some((n) => n.step === -1),
|
| 2094 |
detach,
|
| 2095 |
};
|
client/src/ts/attribution/genAttributeDagViewLinearArcMode.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import * as d3 from 'd3';
|
|
|
|
| 2 |
|
| 3 |
/** linear-arc:相邻节点矩形水平方向「外侧边与边之间的空隙」(px,SVG 内部坐标) */
|
| 4 |
export const LINEAR_ARC_ADJACENT_GAP_DEFAULT = 0;
|
|
@@ -30,6 +31,40 @@ type LinearArcNodeLike = { nodeW: number; nodeH: number; ciVisualScale: number }
|
|
| 30 |
/** `step === -1` 表示 prompt(与 `genAttributeDagView` 中 `DagNode.step` 约定一致) */
|
| 31 |
type LinearArcSteppedNode = LinearArcNodeLike & { step: number };
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
function computeNodeCenterXs(nodes: LinearArcSteppedNode[], adjacentGapPx: number): number[] {
|
| 34 |
const xs: number[] = [];
|
| 35 |
if (nodes.length === 0) return xs;
|
|
@@ -49,6 +84,8 @@ function computeNodeCenterXs(nodes: LinearArcSteppedNode[], adjacentGapPx: numbe
|
|
| 49 |
*
|
| 50 |
* `nodes` 为参与布局的可见节点子集(可能少于 `nodeSel` 绑定的全量节点);
|
| 51 |
* 不在 `nodes` 中的节点(如被隐藏的 excluded 节点)transform 保持不变——调用方已将它们设为 `display:none`。
|
|
|
|
|
|
|
| 52 |
*/
|
| 53 |
export function paintLinearArcLayout<
|
| 54 |
LinkDatum,
|
|
@@ -59,8 +96,9 @@ export function paintLinearArcLayout<
|
|
| 59 |
nodes: NodeDatum[];
|
| 60 |
adjacentGapPx: number;
|
| 61 |
getLinkNodes: (link: LinkDatum) => { src: NodeDatum; tgt: NodeDatum };
|
|
|
|
| 62 |
}): void {
|
| 63 |
-
const { linkSel, nodeSel, nodes, adjacentGapPx, getLinkNodes } = params;
|
| 64 |
|
| 65 |
const centerXs = computeNodeCenterXs(nodes, adjacentGapPx);
|
| 66 |
|
|
@@ -70,23 +108,37 @@ export function paintLinearArcLayout<
|
|
| 70 |
centerXByNode.set(nodes[i]!, centerXs[i]!);
|
| 71 |
}
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
const arcPathBetweenNodes = (src: NodeDatum, tgt: NodeDatum): string => {
|
| 74 |
const srcCx = centerXByNode.get(src);
|
| 75 |
const tgtCx = centerXByNode.get(tgt);
|
| 76 |
if (srcCx === undefined || tgtCx === undefined) {
|
| 77 |
throw new Error('paintLinearArcLayout: link endpoint not in linear node list');
|
| 78 |
}
|
| 79 |
-
|
| 80 |
-
const
|
| 81 |
const dx = Math.abs(tgtCx - srcCx);
|
| 82 |
const arcH = dx * 0.4;
|
| 83 |
-
const upY =
|
| 84 |
const t = Math.max(0, Math.min(1, LINEAR_ARC_BEZIER_HANDLE_INSET_FRACTION));
|
| 85 |
const inset = t * (dx / 2);
|
| 86 |
const dir = tgtCx >= srcCx ? 1 : -1;
|
| 87 |
const p1x = srcCx + dir * inset;
|
| 88 |
const p2x = tgtCx - dir * inset;
|
| 89 |
-
return `M ${srcCx} ${
|
| 90 |
};
|
| 91 |
|
| 92 |
linkSel.each(function(d) {
|
|
@@ -99,6 +151,7 @@ export function paintLinearArcLayout<
|
|
| 99 |
nodeSel.attr('transform', (d) => {
|
| 100 |
const cx = centerXByNode.get(d);
|
| 101 |
if (cx === undefined) return null; // 不在布局列表中(已 display:none),不更新 transform
|
| 102 |
-
|
|
|
|
| 103 |
});
|
| 104 |
}
|
|
|
|
| 1 |
import * as d3 from 'd3';
|
| 2 |
+
import { dagStepDownEffectiveCiRatio } from '../utils/surprisalMath';
|
| 3 |
|
| 4 |
/** linear-arc:相邻节点矩形水平方向「外侧边与边之间的空隙」(px,SVG 内部坐标) */
|
| 5 |
export const LINEAR_ARC_ADJACENT_GAP_DEFAULT = 0;
|
|
|
|
| 31 |
/** `step === -1` 表示 prompt(与 `genAttributeDagView` 中 `DagNode.step` 约定一致) */
|
| 32 |
type LinearArcSteppedNode = LinearArcNodeLike & { step: number };
|
| 33 |
|
| 34 |
+
/** 下台阶布局:节点可带 `dagTargetProb`;有效 CI 为 {@link dagStepDownEffectiveCiRatio}(高置信 p>p₁ 为 0;与「关掉 CI 视觉」无关)。 */
|
| 35 |
+
export type LinearArcStepDownNode = LinearArcSteppedNode & { dagTargetProb?: number };
|
| 36 |
+
|
| 37 |
+
export type LinearArcPaintVariant = 'flat' | 'step-down';
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* 下台阶:每档竖直落差 = `LINEAR_ARC_STEP_DOWN_DISTANCE_SCALE × linearArcUnscaledNodeHeight × CI`。
|
| 41 |
+
* 仅代码可调;不对该系数做运行时 clamp。
|
| 42 |
+
*/
|
| 43 |
+
export const LINEAR_ARC_STEP_DOWN_DISTANCE_SCALE = 1;
|
| 44 |
+
|
| 45 |
+
/** 未做 CI 视觉放大时的节点高度(SVG 坐标),作下台阶落差的 100% CI 基准 */
|
| 46 |
+
export function linearArcUnscaledNodeHeight(n: Pick<LinearArcNodeLike, 'nodeH' | 'ciVisualScale'>): number {
|
| 47 |
+
return n.nodeH / n.ciVisualScale;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/**
|
| 51 |
+
* 第 i 个节点相对首节点的累积下移:对每个 j≥1,加上
|
| 52 |
+
* `LINEAR_ARC_STEP_DOWN_DISTANCE_SCALE × linearArcUnscaledNodeHeight × CI`,
|
| 53 |
+
* CI 为 {@link dagStepDownEffectiveCiRatio}(dagTargetProb)。
|
| 54 |
+
*/
|
| 55 |
+
export function computeLinearArcStepDownOffsetYs(nodes: LinearArcStepDownNode[]): number[] {
|
| 56 |
+
const offsetY: number[] = [];
|
| 57 |
+
let acc = 0;
|
| 58 |
+
for (let i = 0; i < nodes.length; i++) {
|
| 59 |
+
offsetY.push(acc);
|
| 60 |
+
const next = nodes[i + 1];
|
| 61 |
+
if (!next) continue;
|
| 62 |
+
const ratio = dagStepDownEffectiveCiRatio(next.dagTargetProb);
|
| 63 |
+
acc += LINEAR_ARC_STEP_DOWN_DISTANCE_SCALE * linearArcUnscaledNodeHeight(next) * ratio;
|
| 64 |
+
}
|
| 65 |
+
return offsetY;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
function computeNodeCenterXs(nodes: LinearArcSteppedNode[], adjacentGapPx: number): number[] {
|
| 69 |
const xs: number[] = [];
|
| 70 |
if (nodes.length === 0) return xs;
|
|
|
|
| 84 |
*
|
| 85 |
* `nodes` 为参与布局的可见节点子集(可能少于 `nodeSel` 绑定的全量节点);
|
| 86 |
* 不在 `nodes` 中的节点(如被隐藏的 excluded 节点)transform 保持不变——调用方已将它们设为 `display:none`。
|
| 87 |
+
*
|
| 88 |
+
* `variant === 'step-down'`:竖直落差 × {@link LINEAR_ARC_STEP_DOWN_DISTANCE_SCALE} × {@link linearArcUnscaledNodeHeight} × {@link dagStepDownEffectiveCiRatio}。
|
| 89 |
*/
|
| 90 |
export function paintLinearArcLayout<
|
| 91 |
LinkDatum,
|
|
|
|
| 96 |
nodes: NodeDatum[];
|
| 97 |
adjacentGapPx: number;
|
| 98 |
getLinkNodes: (link: LinkDatum) => { src: NodeDatum; tgt: NodeDatum };
|
| 99 |
+
variant?: LinearArcPaintVariant;
|
| 100 |
}): void {
|
| 101 |
+
const { linkSel, nodeSel, nodes, adjacentGapPx, getLinkNodes, variant = 'flat' } = params;
|
| 102 |
|
| 103 |
const centerXs = computeNodeCenterXs(nodes, adjacentGapPx);
|
| 104 |
|
|
|
|
| 108 |
centerXByNode.set(nodes[i]!, centerXs[i]!);
|
| 109 |
}
|
| 110 |
|
| 111 |
+
const offsetYs =
|
| 112 |
+
variant === 'step-down' ? computeLinearArcStepDownOffsetYs(nodes as LinearArcStepDownNode[]) : null;
|
| 113 |
+
const offsetYByNode = new Map<NodeDatum, number>();
|
| 114 |
+
if (offsetYs) {
|
| 115 |
+
for (let i = 0; i < nodes.length; i++) {
|
| 116 |
+
offsetYByNode.set(nodes[i]!, offsetYs[i]!);
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
const arcTopY = (n: NodeDatum): number => {
|
| 121 |
+
const oy = offsetYByNode.get(n) ?? 0;
|
| 122 |
+
return LINEAR_ARC_BASELINE_Y - n.nodeH / (2 * n.ciVisualScale) + oy;
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
const arcPathBetweenNodes = (src: NodeDatum, tgt: NodeDatum): string => {
|
| 126 |
const srcCx = centerXByNode.get(src);
|
| 127 |
const tgtCx = centerXByNode.get(tgt);
|
| 128 |
if (srcCx === undefined || tgtCx === undefined) {
|
| 129 |
throw new Error('paintLinearArcLayout: link endpoint not in linear node list');
|
| 130 |
}
|
| 131 |
+
const yStart = arcTopY(src);
|
| 132 |
+
const yEnd = arcTopY(tgt);
|
| 133 |
const dx = Math.abs(tgtCx - srcCx);
|
| 134 |
const arcH = dx * 0.4;
|
| 135 |
+
const upY = Math.min(yStart, yEnd) - arcH;
|
| 136 |
const t = Math.max(0, Math.min(1, LINEAR_ARC_BEZIER_HANDLE_INSET_FRACTION));
|
| 137 |
const inset = t * (dx / 2);
|
| 138 |
const dir = tgtCx >= srcCx ? 1 : -1;
|
| 139 |
const p1x = srcCx + dir * inset;
|
| 140 |
const p2x = tgtCx - dir * inset;
|
| 141 |
+
return `M ${srcCx} ${yStart} C ${p1x} ${upY}, ${p2x} ${upY}, ${tgtCx} ${yEnd}`;
|
| 142 |
};
|
| 143 |
|
| 144 |
linkSel.each(function(d) {
|
|
|
|
| 151 |
nodeSel.attr('transform', (d) => {
|
| 152 |
const cx = centerXByNode.get(d);
|
| 153 |
if (cx === undefined) return null; // 不在布局列表中(已 display:none),不更新 transform
|
| 154 |
+
const oy = offsetYByNode.get(d) ?? 0;
|
| 155 |
+
return `translate(${cx - d.nodeW / 2},${LINEAR_ARC_BASELINE_Y - d.nodeH / 2 + oy})`;
|
| 156 |
});
|
| 157 |
}
|
client/src/ts/demos/genAttributeBundledDemoManifest.generated.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
/**
|
| 2 |
* Generated by GenAttributeDemoManifestPlugin — do not edit.
|
| 3 |
*/
|
| 4 |
-
export const GEN_ATTRIBUTE_BUNDLED_DEMO_SLUGS: readonly string[] = ["CN->EN翻译","Write a sonnet about love","写一首绝句,主题是春天","过拟合|李白 将进酒"];
|
|
|
|
| 1 |
/**
|
| 2 |
* Generated by GenAttributeDemoManifestPlugin — do not edit.
|
| 3 |
*/
|
| 4 |
+
export const GEN_ATTRIBUTE_BUNDLED_DEMO_SLUGS: readonly string[] = ["CN->EN翻译","CoT | 苏州所在省的省会","Write a sonnet about love","写一首绝句,主题是春天","过拟合|李白 将进酒"];
|
client/src/ts/gen_attribute.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { initChatPanelLayout } from './chat/chatPanelLayout';
|
|
| 13 |
import { PANEL_SPLIT_STORAGE_KEY_GEN_ATTRIBUTE } from './utils/panelSplitStorage';
|
| 14 |
import { TextInputController } from './controllers/textInputController';
|
| 15 |
import { initializeCommonApp } from './appInitializer';
|
|
|
|
| 16 |
import { showAlertDialog } from './ui/dialog';
|
| 17 |
import URLHandler from './utils/URLHandler';
|
| 18 |
import { createToast } from './ui/toast';
|
|
@@ -26,7 +27,7 @@ import {
|
|
| 26 |
import {
|
| 27 |
initGenAttributeDagView,
|
| 28 |
setDagNodeCiVisualScaleEnabled,
|
| 29 |
-
|
| 30 |
type DagLayoutMode,
|
| 31 |
clampDagCompactness,
|
| 32 |
clampLinearArcAdjacentGap,
|
|
@@ -106,9 +107,17 @@ const GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY = 'info_radar_gen_attr_dag_playb
|
|
| 106 |
const GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_replay_pacing_mode';
|
| 107 |
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_total_s';
|
| 108 |
const GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY = 'info_radar_gen_attr_dag_node_ci_visual_scale';
|
| 109 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
const GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY = 'info_radar_gen_attr_dag_hide_inactive_edges';
|
|
|
|
|
|
|
|
|
|
| 111 |
const GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY = 'info_radar_gen_attr_dag_hide_excluded_tokens';
|
|
|
|
| 112 |
const GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY =
|
| 113 |
'info_radar_gen_attr_dag_linear_arc_adjacent_gap';
|
| 114 |
const GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY = 'info_radar_gen_attr_dag_compactness';
|
|
@@ -136,6 +145,29 @@ const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT = 7;
|
|
| 136 |
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MIN = 1;
|
| 137 |
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MAX = 3600;
|
| 138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
const GENERATE_BTN_LABEL = 'Start';
|
| 140 |
const STOP_BTN_LABEL = 'Stop';
|
| 141 |
|
|
@@ -260,17 +292,24 @@ function readStoredDagReplayPacingMode(): DagReplayPacingMode {
|
|
| 260 |
} catch {
|
| 261 |
// ignore
|
| 262 |
}
|
| 263 |
-
return
|
| 264 |
}
|
| 265 |
|
| 266 |
function readStoredDagLayoutMode(): DagLayoutMode {
|
| 267 |
try {
|
| 268 |
const v = localStorage.getItem(GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY);
|
| 269 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
} catch {
|
| 271 |
// ignore
|
| 272 |
}
|
| 273 |
-
return
|
| 274 |
}
|
| 275 |
|
| 276 |
const apiPrefix = URLHandler.parameters['api'] || '';
|
|
@@ -375,36 +414,48 @@ function applyDagReplaySpeedUi(): void {
|
|
| 375 |
|
| 376 |
function currentDagLayoutMode(): DagLayoutMode {
|
| 377 |
const v = dagLayoutModeSelect?.value;
|
| 378 |
-
if (v === 'linear-arc' || v === 'spiral') return v;
|
| 379 |
return 'text-flow';
|
| 380 |
}
|
| 381 |
|
| 382 |
function applyDagLayoutModeUi(): void {
|
| 383 |
const mode = currentDagLayoutMode();
|
| 384 |
if (dagCompactnessGroup) {
|
| 385 |
-
/** text-flow / spiral 均使用 display-scale 驱动的节点宽高与边回缩;linear-arc 不适用。 */
|
| 386 |
-
dagCompactnessGroup.hidden = mode === 'linear-arc';
|
| 387 |
}
|
| 388 |
if (dagMeasureWidthGroup) {
|
| 389 |
dagMeasureWidthGroup.hidden = mode !== 'text-flow';
|
| 390 |
}
|
| 391 |
if (dagLinearArcIntervalGroup) {
|
| 392 |
-
dagLinearArcIntervalGroup.hidden = mode !== 'linear-arc';
|
| 393 |
}
|
| 394 |
}
|
| 395 |
|
| 396 |
const dagHideExcludedTokensInput = document.getElementById(
|
| 397 |
'gen_attr_dag_hide_excluded_tokens'
|
| 398 |
) as HTMLInputElement | null;
|
|
|
|
|
|
|
|
|
|
| 399 |
const dagNodeCiVisualScaleInput = document.getElementById(
|
| 400 |
'gen_attr_dag_node_ci_visual_scale'
|
| 401 |
) as HTMLInputElement | null;
|
| 402 |
-
const
|
| 403 |
-
'
|
| 404 |
) as HTMLInputElement | null;
|
| 405 |
const dagHideInactiveEdgesInput = document.getElementById(
|
| 406 |
'gen_attr_dag_hide_inactive_edges'
|
| 407 |
) as HTMLInputElement | null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
const genAttrExcludePromptPatternsTa = document.getElementById(
|
| 409 |
'gen_attr_exclude_prompt_patterns'
|
| 410 |
) as HTMLTextAreaElement | null;
|
|
@@ -417,6 +468,9 @@ const genAttrExcludeGeneratedPatternsTa = document.getElementById(
|
|
| 417 |
const genAttrExcludeGeneratedPatternsEnable = document.getElementById(
|
| 418 |
'gen_attr_exclude_generated_patterns_enable'
|
| 419 |
) as HTMLInputElement | null;
|
|
|
|
|
|
|
|
|
|
| 420 |
const completeReasonEl = d3.select('#gen_attr_complete_reason');
|
| 421 |
|
| 422 |
function syncGenAttrExcludePatternTextareasDisabled(): void {
|
|
@@ -495,10 +549,11 @@ const genAttrResultsNode = genAttrResultsEl.node() as HTMLElement | null;
|
|
| 495 |
function readStoredDagNodeCiVisualScale(): boolean {
|
| 496 |
try {
|
| 497 |
const v = localStorage.getItem(GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY);
|
| 498 |
-
|
| 499 |
} catch {
|
| 500 |
-
|
| 501 |
}
|
|
|
|
| 502 |
}
|
| 503 |
const initialDagNodeCiVisualScale = readStoredDagNodeCiVisualScale();
|
| 504 |
if (dagNodeCiVisualScaleInput) dagNodeCiVisualScaleInput.checked = initialDagNodeCiVisualScale;
|
|
@@ -514,25 +569,30 @@ dagNodeCiVisualScaleInput?.addEventListener('change', () => {
|
|
| 514 |
tryResetAndReplayDag();
|
| 515 |
});
|
| 516 |
|
| 517 |
-
function
|
| 518 |
try {
|
| 519 |
-
const v = localStorage.getItem(
|
| 520 |
-
|
|
|
|
|
|
|
| 521 |
} catch {
|
| 522 |
-
|
| 523 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
}
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
dagEdgeWeakenHighSurprisalInput?.addEventListener('change', () => {
|
| 529 |
-
const enabled = dagEdgeWeakenHighSurprisalInput.checked;
|
| 530 |
try {
|
| 531 |
-
localStorage.setItem(
|
| 532 |
} catch {
|
| 533 |
/* ignore */
|
| 534 |
}
|
| 535 |
-
|
| 536 |
tryResetAndReplayDag();
|
| 537 |
});
|
| 538 |
|
|
@@ -542,10 +602,12 @@ function applyDagHideInactiveEdges(hide: boolean): void {
|
|
| 542 |
}
|
| 543 |
function readStoredDagHideInactiveEdges(): boolean {
|
| 544 |
try {
|
| 545 |
-
|
|
|
|
| 546 |
} catch {
|
| 547 |
-
|
| 548 |
}
|
|
|
|
| 549 |
}
|
| 550 |
const initialDagHideInactiveEdges = readStoredDagHideInactiveEdges();
|
| 551 |
if (dagHideInactiveEdgesInput) dagHideInactiveEdgesInput.checked = initialDagHideInactiveEdges;
|
|
@@ -560,15 +622,82 @@ dagHideInactiveEdgesInput?.addEventListener('change', () => {
|
|
| 560 |
applyDagHideInactiveEdges(hide);
|
| 561 |
});
|
| 562 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
function readStoredDagHideExcludedTokens(): boolean {
|
| 564 |
try {
|
| 565 |
-
|
|
|
|
| 566 |
} catch {
|
| 567 |
-
|
| 568 |
}
|
|
|
|
| 569 |
}
|
| 570 |
const initialDagHideExcludedTokens = readStoredDagHideExcludedTokens();
|
| 571 |
if (dagHideExcludedTokensInput) dagHideExcludedTokensInput.checked = initialDagHideExcludedTokens;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
dagHideExcludedTokensInput?.addEventListener('change', () => {
|
| 573 |
const hide = dagHideExcludedTokensInput.checked;
|
| 574 |
try {
|
|
@@ -579,6 +708,28 @@ dagHideExcludedTokensInput?.addEventListener('change', () => {
|
|
| 579 |
dagHandle.setHideExcludedTokens(hide);
|
| 580 |
});
|
| 581 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 582 |
modelVariantSelect?.addEventListener('change', () => {
|
| 583 |
try {
|
| 584 |
localStorage.setItem(GEN_ATTR_MODEL_VARIANT_STORAGE_KEY, currentModelVariant());
|
|
@@ -998,6 +1149,9 @@ const dagHandle = initGenAttributeDagView(d3.select('#results'), {
|
|
| 998 |
dagCompactness: initialDagCompactness,
|
| 999 |
linearArcAdjacentGapPx: initialDagLinearArcGap,
|
| 1000 |
hideExcludedTokens: initialDagHideExcludedTokens,
|
|
|
|
|
|
|
|
|
|
| 1001 |
edgeTopPCoverage: initialDagEdgeTopPCoverage,
|
| 1002 |
onFullscreenError: (message) => showToast(message, 'error'),
|
| 1003 |
getEffectiveExcludePromptPatternsText: genAttrEffectiveExcludePromptPatternsText,
|
|
@@ -1024,16 +1178,25 @@ function isDagBusy(): boolean {
|
|
| 1024 |
return inFlight || dagPlaybackTimer !== null || dagLastTokenDwellTimer !== null;
|
| 1025 |
}
|
| 1026 |
|
| 1027 |
-
/**
|
| 1028 |
-
|
|
|
|
|
|
|
|
|
|
| 1029 |
if (isDagBusy()) return;
|
|
|
|
|
|
|
| 1030 |
const h = runnerHandle;
|
| 1031 |
dagHandle.reset();
|
| 1032 |
if (h && h.tokenCount > 0) {
|
| 1033 |
replayRunnerStepsIntoDag(h, currentRunPromptSpans.length > 0 ? currentRunPromptSpans : undefined);
|
| 1034 |
}
|
| 1035 |
dagHandle.fitViewportToContent();
|
| 1036 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1037 |
}
|
| 1038 |
|
| 1039 |
dagMeasureWidthInput?.addEventListener('change', () => {
|
|
@@ -1127,8 +1290,12 @@ function readGenAttrDemoUiOptionsFromControls(): GenAttrDemoUiOptions {
|
|
| 1127 |
hideExcludedTokens: dagHideExcludedTokensInput?.checked ?? false,
|
| 1128 |
edgeTopPCoverage,
|
| 1129 |
nodeCiVisualScaleEnabled: dagNodeCiVisualScaleInput?.checked ?? true,
|
| 1130 |
-
|
|
|
|
| 1131 |
hideInactiveEdges: dagHideInactiveEdgesInput?.checked ?? false,
|
|
|
|
|
|
|
|
|
|
| 1132 |
replayPacingMode: currentDagReplayPacingMode(),
|
| 1133 |
playbackTotalS,
|
| 1134 |
playbackStepMs,
|
|
@@ -1136,9 +1303,31 @@ function readGenAttrDemoUiOptionsFromControls(): GenAttrDemoUiOptions {
|
|
| 1136 |
excludePromptPatternsText: genAttrExcludePromptPatternsTa?.value ?? '',
|
| 1137 |
excludeGeneratedPatternsEnabled: genAttrExcludeGeneratedPatternsEnable?.checked ?? true,
|
| 1138 |
excludeGeneratedPatternsText: genAttrExcludeGeneratedPatternsTa?.value ?? '',
|
|
|
|
| 1139 |
};
|
| 1140 |
}
|
| 1141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1142 |
/**
|
| 1143 |
* 从 `demoUiOptions` 还原排除控件(仅 DOM);与施加 DAG demo 不回写 GEN_ATTR_LS 的策略一致,`replay` 读当前控件生效。
|
| 1144 |
*/
|
|
@@ -1172,12 +1361,8 @@ function applyGenAttrExcludePatternsFromDemoUiSnap(snap: Partial<GenAttrDemoUiOp
|
|
| 1172 |
syncGenAttrExcludePatternTextareasDisabled();
|
| 1173 |
}
|
| 1174 |
|
| 1175 |
-
/**
|
| 1176 |
-
|
| 1177 |
-
*/
|
| 1178 |
-
function applyGenAttrDemoUiOptionsFromRecord(rec: GenAttrCachedRun): void {
|
| 1179 |
-
const snap = rec.demoUiOptions;
|
| 1180 |
-
if (!snap) return;
|
| 1181 |
const mode = snap.layoutMode;
|
| 1182 |
if (mode) {
|
| 1183 |
if (dagLayoutModeSelect) {
|
|
@@ -1215,14 +1400,36 @@ function applyGenAttrDemoUiOptionsFromRecord(rec: GenAttrCachedRun): void {
|
|
| 1215 |
if (dagNodeCiVisualScaleInput) dagNodeCiVisualScaleInput.checked = snap.nodeCiVisualScaleEnabled;
|
| 1216 |
setDagNodeCiVisualScaleEnabled(snap.nodeCiVisualScaleEnabled);
|
| 1217 |
}
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1221 |
}
|
| 1222 |
if (snap.hideInactiveEdges !== undefined) {
|
| 1223 |
if (dagHideInactiveEdgesInput) dagHideInactiveEdgesInput.checked = snap.hideInactiveEdges;
|
| 1224 |
applyDagHideInactiveEdges(snap.hideInactiveEdges);
|
| 1225 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1226 |
if (snap.replayPacingMode !== undefined) {
|
| 1227 |
if (dagReplayModeSelect) dagReplayModeSelect.value = snap.replayPacingMode;
|
| 1228 |
applyDagReplaySpeedUi();
|
|
@@ -1237,8 +1444,80 @@ function applyGenAttrDemoUiOptionsFromRecord(rec: GenAttrCachedRun): void {
|
|
| 1237 |
}
|
| 1238 |
|
| 1239 |
applyGenAttrExcludePatternsFromDemoUiSnap(snap);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1240 |
}
|
| 1241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1242 |
window.addEventListener('pagehide', (ev) => {
|
| 1243 |
if (ev.persisted) return;
|
| 1244 |
dagHandle.detach();
|
|
@@ -1247,9 +1526,7 @@ window.addEventListener('pagehide', (ev) => {
|
|
| 1247 |
function onExcludePatternsEffectiveChange(): void {
|
| 1248 |
const h = runnerHandle;
|
| 1249 |
if (!h || h.tokenCount === 0) return;
|
| 1250 |
-
|
| 1251 |
-
replayRunnerStepsIntoDag(h, currentRunPromptSpans.length > 0 ? currentRunPromptSpans : undefined);
|
| 1252 |
-
dagHandle.clearNodeSelection();
|
| 1253 |
}
|
| 1254 |
|
| 1255 |
function bindExcludePatternControls(
|
|
@@ -1576,6 +1853,7 @@ async function applyGenAttrCachedRun(
|
|
| 1576 |
stopDagPlayback();
|
| 1577 |
dagHandle.reset();
|
| 1578 |
applyGenAttrDemoUiOptionsFromRecord(rec);
|
|
|
|
| 1579 |
runnerHandle = createHydratedTokenGenHandle(rec.steps);
|
| 1580 |
lastRunInitialContext = rec.initialContext;
|
| 1581 |
lastRunInputSnapshot = getInputSnapshotForRun();
|
|
@@ -1585,7 +1863,7 @@ async function applyGenAttrCachedRun(
|
|
| 1585 |
currentRunPromptSpans = replayPromptSpans;
|
| 1586 |
replayRunnerStepsIntoDag(runnerHandle, replayPromptSpans);
|
| 1587 |
dagHandle.fitViewportToContent();
|
| 1588 |
-
|
| 1589 |
const n = runnerHandle.tokenCount;
|
| 1590 |
setGenAttrUsageMetric(initialPromptTokensFromFirstStep(rec.steps[0]!), n);
|
| 1591 |
if (validateMetricsElements(metricModel) && n > 0) {
|
|
@@ -1993,13 +2271,8 @@ submitBtn.on('click', () => {
|
|
| 1993 |
function refreshDagForThemeChange(): void {
|
| 1994 |
stopDagPlayback();
|
| 1995 |
const h = runnerHandle;
|
| 1996 |
-
if (!h || h.tokenCount === 0)
|
| 1997 |
-
|
| 1998 |
-
}
|
| 1999 |
-
dagHandle.reset();
|
| 2000 |
-
replayRunnerStepsIntoDag(h, currentRunPromptSpans.length > 0 ? currentRunPromptSpans : undefined);
|
| 2001 |
-
dagHandle.fitViewportToContent();
|
| 2002 |
-
dagHandle.clearNodeSelection();
|
| 2003 |
}
|
| 2004 |
|
| 2005 |
const themeManager = initThemeManager(
|
|
|
|
| 13 |
import { PANEL_SPLIT_STORAGE_KEY_GEN_ATTRIBUTE } from './utils/panelSplitStorage';
|
| 14 |
import { TextInputController } from './controllers/textInputController';
|
| 15 |
import { initializeCommonApp } from './appInitializer';
|
| 16 |
+
import { setPageOptsGetter } from './utils/clientActivityPing';
|
| 17 |
import { showAlertDialog } from './ui/dialog';
|
| 18 |
import URLHandler from './utils/URLHandler';
|
| 19 |
import { createToast } from './ui/toast';
|
|
|
|
| 27 |
import {
|
| 28 |
initGenAttributeDagView,
|
| 29 |
setDagNodeCiVisualScaleEnabled,
|
| 30 |
+
setDagDecayAttributionToHighSurprisalTargetEnabled,
|
| 31 |
type DagLayoutMode,
|
| 32 |
clampDagCompactness,
|
| 33 |
clampLinearArcAdjacentGap,
|
|
|
|
| 107 |
const GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_replay_pacing_mode';
|
| 108 |
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_total_s';
|
| 109 |
const GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY = 'info_radar_gen_attr_dag_node_ci_visual_scale';
|
| 110 |
+
const GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY =
|
| 111 |
+
'info_radar_gen_attr_dag_decay_attribution_high_surprisal';
|
| 112 |
+
/** @deprecated 读取迁移用 */
|
| 113 |
+
const GEN_ATTR_DAG_EDGE_WEAKEN_HIGH_SURPRISAL_STORAGE_KEY_LEGACY =
|
| 114 |
+
'info_radar_gen_attr_dag_edge_weaken_high_surprisal';
|
| 115 |
const GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY = 'info_radar_gen_attr_dag_hide_inactive_edges';
|
| 116 |
+
const GEN_ATTR_DAG_SHOW_DOWNSTREAM_INFLUENCE_STORAGE_KEY =
|
| 117 |
+
'info_radar_gen_attr_dag_show_downstream_influence';
|
| 118 |
+
const GEN_ATTR_DAG_RECURSIVE_ATTRIBUTION_STORAGE_KEY = 'info_radar_gen_attr_dag_recursive_attribution';
|
| 119 |
const GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY = 'info_radar_gen_attr_dag_hide_excluded_tokens';
|
| 120 |
+
const GEN_ATTR_DAG_SHOW_TOPK_ON_SELECTED_STORAGE_KEY = 'info_radar_gen_attr_dag_show_topk_on_selected';
|
| 121 |
const GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY =
|
| 122 |
'info_radar_gen_attr_dag_linear_arc_adjacent_gap';
|
| 123 |
const GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY = 'info_radar_gen_attr_dag_compactness';
|
|
|
|
| 145 |
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MIN = 1;
|
| 146 |
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MAX = 3600;
|
| 147 |
|
| 148 |
+
/** 与无 demoUiOptions 本地缓存时「读出默认」对齐,供重置与可读性单一的来源 */
|
| 149 |
+
const DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS: GenAttrDemoUiOptions = {
|
| 150 |
+
layoutMode: 'text-flow',
|
| 151 |
+
measureWidthPx: GEN_ATTR_DAG_MEASURE_WIDTH_DEFAULT,
|
| 152 |
+
dagCompactness: DAG_COMPACTNESS_DEFAULT,
|
| 153 |
+
linearArcAdjacentGapPx: LINEAR_ARC_ADJACENT_GAP_DEFAULT,
|
| 154 |
+
hideExcludedTokens: false,
|
| 155 |
+
edgeTopPCoverage: DAG_EDGE_TOP_P_COVERAGE_DEFAULT,
|
| 156 |
+
nodeCiVisualScaleEnabled: true,
|
| 157 |
+
decayAttributionToHighSurprisalTargetEnabled: true,
|
| 158 |
+
hideInactiveEdges: false,
|
| 159 |
+
showDownstreamInfluence: false,
|
| 160 |
+
recursiveAttributionEnabled: false,
|
| 161 |
+
showTokenInfoOnSelected: false,
|
| 162 |
+
replayPacingMode: 'total',
|
| 163 |
+
playbackTotalS: GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT,
|
| 164 |
+
playbackStepMs: GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT,
|
| 165 |
+
excludePromptPatternsEnabled: true,
|
| 166 |
+
excludePromptPatternsText: DEFAULT_EXCLUDE_PROMPT_PATTERNS_TEXT,
|
| 167 |
+
excludeGeneratedPatternsEnabled: true,
|
| 168 |
+
excludeGeneratedPatternsText: DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT,
|
| 169 |
+
};
|
| 170 |
+
|
| 171 |
const GENERATE_BTN_LABEL = 'Start';
|
| 172 |
const STOP_BTN_LABEL = 'Stop';
|
| 173 |
|
|
|
|
| 292 |
} catch {
|
| 293 |
// ignore
|
| 294 |
}
|
| 295 |
+
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.replayPacingMode;
|
| 296 |
}
|
| 297 |
|
| 298 |
function readStoredDagLayoutMode(): DagLayoutMode {
|
| 299 |
try {
|
| 300 |
const v = localStorage.getItem(GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY);
|
| 301 |
+
if (
|
| 302 |
+
v === 'text-flow' ||
|
| 303 |
+
v === 'linear-arc' ||
|
| 304 |
+
v === 'linear-arc-step-down' ||
|
| 305 |
+
v === 'spiral'
|
| 306 |
+
) {
|
| 307 |
+
return v;
|
| 308 |
+
}
|
| 309 |
} catch {
|
| 310 |
// ignore
|
| 311 |
}
|
| 312 |
+
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.layoutMode;
|
| 313 |
}
|
| 314 |
|
| 315 |
const apiPrefix = URLHandler.parameters['api'] || '';
|
|
|
|
| 414 |
|
| 415 |
function currentDagLayoutMode(): DagLayoutMode {
|
| 416 |
const v = dagLayoutModeSelect?.value;
|
| 417 |
+
if (v === 'linear-arc' || v === 'linear-arc-step-down' || v === 'spiral') return v;
|
| 418 |
return 'text-flow';
|
| 419 |
}
|
| 420 |
|
| 421 |
function applyDagLayoutModeUi(): void {
|
| 422 |
const mode = currentDagLayoutMode();
|
| 423 |
if (dagCompactnessGroup) {
|
| 424 |
+
/** text-flow / spiral 均使用 display-scale 驱动的节点宽高与边回缩;linear-arc 家族不适用。 */
|
| 425 |
+
dagCompactnessGroup.hidden = mode === 'linear-arc' || mode === 'linear-arc-step-down';
|
| 426 |
}
|
| 427 |
if (dagMeasureWidthGroup) {
|
| 428 |
dagMeasureWidthGroup.hidden = mode !== 'text-flow';
|
| 429 |
}
|
| 430 |
if (dagLinearArcIntervalGroup) {
|
| 431 |
+
dagLinearArcIntervalGroup.hidden = mode !== 'linear-arc' && mode !== 'linear-arc-step-down';
|
| 432 |
}
|
| 433 |
}
|
| 434 |
|
| 435 |
const dagHideExcludedTokensInput = document.getElementById(
|
| 436 |
'gen_attr_dag_hide_excluded_tokens'
|
| 437 |
) as HTMLInputElement | null;
|
| 438 |
+
const dagShowTopkOnSelectedInput = document.getElementById(
|
| 439 |
+
'gen_attr_dag_show_topk_on_selected'
|
| 440 |
+
) as HTMLInputElement | null;
|
| 441 |
const dagNodeCiVisualScaleInput = document.getElementById(
|
| 442 |
'gen_attr_dag_node_ci_visual_scale'
|
| 443 |
) as HTMLInputElement | null;
|
| 444 |
+
const dagDecayAttributionHighSurprisalInput = document.getElementById(
|
| 445 |
+
'gen_attr_dag_decay_attribution_high_surprisal'
|
| 446 |
) as HTMLInputElement | null;
|
| 447 |
const dagHideInactiveEdgesInput = document.getElementById(
|
| 448 |
'gen_attr_dag_hide_inactive_edges'
|
| 449 |
) as HTMLInputElement | null;
|
| 450 |
+
const dagShowDownstreamInfluenceInput = document.getElementById(
|
| 451 |
+
'gen_attr_dag_show_downstream_influence'
|
| 452 |
+
) as HTMLInputElement | null;
|
| 453 |
+
const dagShowDownstreamInfluenceGroup = document.getElementById(
|
| 454 |
+
'gen_attr_dag_show_downstream_influence_group'
|
| 455 |
+
);
|
| 456 |
+
const dagRecursiveAttributionInput = document.getElementById(
|
| 457 |
+
'gen_attr_dag_recursive_attribution'
|
| 458 |
+
) as HTMLInputElement | null;
|
| 459 |
const genAttrExcludePromptPatternsTa = document.getElementById(
|
| 460 |
'gen_attr_exclude_prompt_patterns'
|
| 461 |
) as HTMLTextAreaElement | null;
|
|
|
|
| 468 |
const genAttrExcludeGeneratedPatternsEnable = document.getElementById(
|
| 469 |
'gen_attr_exclude_generated_patterns_enable'
|
| 470 |
) as HTMLInputElement | null;
|
| 471 |
+
const genAttrResetUiOptionsBtn = document.getElementById(
|
| 472 |
+
'gen_attr_reset_ui_options_btn',
|
| 473 |
+
) as HTMLButtonElement | null;
|
| 474 |
const completeReasonEl = d3.select('#gen_attr_complete_reason');
|
| 475 |
|
| 476 |
function syncGenAttrExcludePatternTextareasDisabled(): void {
|
|
|
|
| 549 |
function readStoredDagNodeCiVisualScale(): boolean {
|
| 550 |
try {
|
| 551 |
const v = localStorage.getItem(GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY);
|
| 552 |
+
if (v !== null) return v === '1';
|
| 553 |
} catch {
|
| 554 |
+
// ignore
|
| 555 |
}
|
| 556 |
+
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.nodeCiVisualScaleEnabled;
|
| 557 |
}
|
| 558 |
const initialDagNodeCiVisualScale = readStoredDagNodeCiVisualScale();
|
| 559 |
if (dagNodeCiVisualScaleInput) dagNodeCiVisualScaleInput.checked = initialDagNodeCiVisualScale;
|
|
|
|
| 569 |
tryResetAndReplayDag();
|
| 570 |
});
|
| 571 |
|
| 572 |
+
function readStoredDagDecayAttributionToHighSurprisalTarget(): boolean {
|
| 573 |
try {
|
| 574 |
+
const v = localStorage.getItem(GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY);
|
| 575 |
+
if (v !== null) return v === '1';
|
| 576 |
+
const legacy = localStorage.getItem(GEN_ATTR_DAG_EDGE_WEAKEN_HIGH_SURPRISAL_STORAGE_KEY_LEGACY);
|
| 577 |
+
if (legacy !== null) return legacy === '1';
|
| 578 |
} catch {
|
| 579 |
+
// ignore
|
| 580 |
}
|
| 581 |
+
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.decayAttributionToHighSurprisalTargetEnabled;
|
| 582 |
+
}
|
| 583 |
+
const initialDagDecayAttributionHighSurprisal = readStoredDagDecayAttributionToHighSurprisalTarget();
|
| 584 |
+
if (dagDecayAttributionHighSurprisalInput) {
|
| 585 |
+
dagDecayAttributionHighSurprisalInput.checked = initialDagDecayAttributionHighSurprisal;
|
| 586 |
}
|
| 587 |
+
setDagDecayAttributionToHighSurprisalTargetEnabled(initialDagDecayAttributionHighSurprisal);
|
| 588 |
+
dagDecayAttributionHighSurprisalInput?.addEventListener('change', () => {
|
| 589 |
+
const enabled = dagDecayAttributionHighSurprisalInput.checked;
|
|
|
|
|
|
|
| 590 |
try {
|
| 591 |
+
localStorage.setItem(GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY, enabled ? '1' : '0');
|
| 592 |
} catch {
|
| 593 |
/* ignore */
|
| 594 |
}
|
| 595 |
+
setDagDecayAttributionToHighSurprisalTargetEnabled(enabled);
|
| 596 |
tryResetAndReplayDag();
|
| 597 |
});
|
| 598 |
|
|
|
|
| 602 |
}
|
| 603 |
function readStoredDagHideInactiveEdges(): boolean {
|
| 604 |
try {
|
| 605 |
+
const v = localStorage.getItem(GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY);
|
| 606 |
+
if (v !== null) return v === '1';
|
| 607 |
} catch {
|
| 608 |
+
// ignore
|
| 609 |
}
|
| 610 |
+
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.hideInactiveEdges;
|
| 611 |
}
|
| 612 |
const initialDagHideInactiveEdges = readStoredDagHideInactiveEdges();
|
| 613 |
if (dagHideInactiveEdgesInput) dagHideInactiveEdgesInput.checked = initialDagHideInactiveEdges;
|
|
|
|
| 622 |
applyDagHideInactiveEdges(hide);
|
| 623 |
});
|
| 624 |
|
| 625 |
+
function readStoredDagShowDownstreamInfluence(): boolean {
|
| 626 |
+
try {
|
| 627 |
+
const v = localStorage.getItem(GEN_ATTR_DAG_SHOW_DOWNSTREAM_INFLUENCE_STORAGE_KEY);
|
| 628 |
+
if (v !== null) return v === '1';
|
| 629 |
+
} catch {
|
| 630 |
+
// ignore
|
| 631 |
+
}
|
| 632 |
+
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.showDownstreamInfluence;
|
| 633 |
+
}
|
| 634 |
+
const initialDagShowDownstreamInfluence = readStoredDagShowDownstreamInfluence();
|
| 635 |
+
if (dagShowDownstreamInfluenceInput) {
|
| 636 |
+
dagShowDownstreamInfluenceInput.checked = initialDagShowDownstreamInfluence;
|
| 637 |
+
}
|
| 638 |
+
dagShowDownstreamInfluenceInput?.addEventListener('change', () => {
|
| 639 |
+
const show = dagShowDownstreamInfluenceInput.checked;
|
| 640 |
+
try {
|
| 641 |
+
localStorage.setItem(GEN_ATTR_DAG_SHOW_DOWNSTREAM_INFLUENCE_STORAGE_KEY, show ? '1' : '0');
|
| 642 |
+
} catch {
|
| 643 |
+
/* ignore */
|
| 644 |
+
}
|
| 645 |
+
dagHandle.setShowDownstreamInfluence(show);
|
| 646 |
+
});
|
| 647 |
+
|
| 648 |
+
/** 传播归因(UI: Propagated attribution mode;`recursiveAttributionEnabled`)开启时隐藏 downstream influence 选项(该模式仅展示上游链)。 */
|
| 649 |
+
function applyDagDownstreamInfluenceUi(): void {
|
| 650 |
+
const recursive = dagRecursiveAttributionInput?.checked ?? false;
|
| 651 |
+
if (dagShowDownstreamInfluenceGroup) {
|
| 652 |
+
dagShowDownstreamInfluenceGroup.hidden = recursive;
|
| 653 |
+
}
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
function readStoredDagRecursiveAttribution(): boolean {
|
| 657 |
+
try {
|
| 658 |
+
const v = localStorage.getItem(GEN_ATTR_DAG_RECURSIVE_ATTRIBUTION_STORAGE_KEY);
|
| 659 |
+
if (v !== null) return v === '1';
|
| 660 |
+
} catch {
|
| 661 |
+
// ignore
|
| 662 |
+
}
|
| 663 |
+
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.recursiveAttributionEnabled;
|
| 664 |
+
}
|
| 665 |
+
const initialDagRecursiveAttribution = readStoredDagRecursiveAttribution();
|
| 666 |
+
if (dagRecursiveAttributionInput) dagRecursiveAttributionInput.checked = initialDagRecursiveAttribution;
|
| 667 |
+
applyDagDownstreamInfluenceUi();
|
| 668 |
+
dagRecursiveAttributionInput?.addEventListener('change', () => {
|
| 669 |
+
const enabled = dagRecursiveAttributionInput.checked;
|
| 670 |
+
try {
|
| 671 |
+
localStorage.setItem(GEN_ATTR_DAG_RECURSIVE_ATTRIBUTION_STORAGE_KEY, enabled ? '1' : '0');
|
| 672 |
+
} catch {
|
| 673 |
+
/* ignore */
|
| 674 |
+
}
|
| 675 |
+
applyDagDownstreamInfluenceUi();
|
| 676 |
+
dagHandle.setRecursiveAttributionEnabled(enabled);
|
| 677 |
+
});
|
| 678 |
+
|
| 679 |
function readStoredDagHideExcludedTokens(): boolean {
|
| 680 |
try {
|
| 681 |
+
const v = localStorage.getItem(GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY);
|
| 682 |
+
if (v !== null) return v === '1';
|
| 683 |
} catch {
|
| 684 |
+
// ignore
|
| 685 |
}
|
| 686 |
+
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.hideExcludedTokens;
|
| 687 |
}
|
| 688 |
const initialDagHideExcludedTokens = readStoredDagHideExcludedTokens();
|
| 689 |
if (dagHideExcludedTokensInput) dagHideExcludedTokensInput.checked = initialDagHideExcludedTokens;
|
| 690 |
+
function readStoredDagShowTopkOnSelected(): boolean {
|
| 691 |
+
try {
|
| 692 |
+
const v = localStorage.getItem(GEN_ATTR_DAG_SHOW_TOPK_ON_SELECTED_STORAGE_KEY);
|
| 693 |
+
if (v !== null) return v === '1';
|
| 694 |
+
} catch {
|
| 695 |
+
// ignore
|
| 696 |
+
}
|
| 697 |
+
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.showTokenInfoOnSelected;
|
| 698 |
+
}
|
| 699 |
+
const initialDagShowTopkOnSelected = readStoredDagShowTopkOnSelected();
|
| 700 |
+
if (dagShowTopkOnSelectedInput) dagShowTopkOnSelectedInput.checked = initialDagShowTopkOnSelected;
|
| 701 |
dagHideExcludedTokensInput?.addEventListener('change', () => {
|
| 702 |
const hide = dagHideExcludedTokensInput.checked;
|
| 703 |
try {
|
|
|
|
| 708 |
dagHandle.setHideExcludedTokens(hide);
|
| 709 |
});
|
| 710 |
|
| 711 |
+
dagShowTopkOnSelectedInput?.addEventListener('change', () => {
|
| 712 |
+
const show = dagShowTopkOnSelectedInput.checked;
|
| 713 |
+
try {
|
| 714 |
+
localStorage.setItem(GEN_ATTR_DAG_SHOW_TOPK_ON_SELECTED_STORAGE_KEY, show ? '1' : '0');
|
| 715 |
+
} catch {
|
| 716 |
+
/* ignore */
|
| 717 |
+
}
|
| 718 |
+
dagHandle.setShowTokenInfoOnSelected(show);
|
| 719 |
+
});
|
| 720 |
+
|
| 721 |
+
setPageOptsGetter(() => {
|
| 722 |
+
const mode = currentDagLayoutMode();
|
| 723 |
+
return {
|
| 724 |
+
layout_linear_arc: mode === 'linear-arc',
|
| 725 |
+
layout_step_down: mode === 'linear-arc-step-down',
|
| 726 |
+
layout_spiral: mode === 'spiral',
|
| 727 |
+
propagated: dagRecursiveAttributionInput?.checked ?? false,
|
| 728 |
+
downstream: dagShowDownstreamInfluenceInput?.checked ?? false,
|
| 729 |
+
token_tooltip: dagShowTopkOnSelectedInput?.checked ?? false,
|
| 730 |
+
};
|
| 731 |
+
});
|
| 732 |
+
|
| 733 |
modelVariantSelect?.addEventListener('change', () => {
|
| 734 |
try {
|
| 735 |
localStorage.setItem(GEN_ATTR_MODEL_VARIANT_STORAGE_KEY, currentModelVariant());
|
|
|
|
| 1149 |
dagCompactness: initialDagCompactness,
|
| 1150 |
linearArcAdjacentGapPx: initialDagLinearArcGap,
|
| 1151 |
hideExcludedTokens: initialDagHideExcludedTokens,
|
| 1152 |
+
showTokenInfoOnSelected: initialDagShowTopkOnSelected,
|
| 1153 |
+
showDownstreamInfluence: initialDagShowDownstreamInfluence,
|
| 1154 |
+
recursiveAttributionEnabled: initialDagRecursiveAttribution,
|
| 1155 |
edgeTopPCoverage: initialDagEdgeTopPCoverage,
|
| 1156 |
onFullscreenError: (message) => showToast(message, 'error'),
|
| 1157 |
getEffectiveExcludePromptPatternsText: genAttrEffectiveExcludePromptPatternsText,
|
|
|
|
| 1178 |
return inFlight || dagPlaybackTimer !== null || dagLastTokenDwellTimer !== null;
|
| 1179 |
}
|
| 1180 |
|
| 1181 |
+
/**
|
| 1182 |
+
* 非忙状态下 reset + replay + fit,供各设置项切换后复用。忙时为 no-op。
|
| 1183 |
+
* 默认保留 DAG 选中节点;整页重置 UI 等场景传 `preserveNodeSelection: false`。
|
| 1184 |
+
*/
|
| 1185 |
+
function tryResetAndReplayDag(opts?: { preserveNodeSelection?: boolean }): void {
|
| 1186 |
if (isDagBusy()) return;
|
| 1187 |
+
const preserveSelection = opts?.preserveNodeSelection !== false;
|
| 1188 |
+
const preservedSelectedId = preserveSelection ? dagHandle.getSelectedNodeId() : null;
|
| 1189 |
const h = runnerHandle;
|
| 1190 |
dagHandle.reset();
|
| 1191 |
if (h && h.tokenCount > 0) {
|
| 1192 |
replayRunnerStepsIntoDag(h, currentRunPromptSpans.length > 0 ? currentRunPromptSpans : undefined);
|
| 1193 |
}
|
| 1194 |
dagHandle.fitViewportToContent();
|
| 1195 |
+
if (preservedSelectedId != null) {
|
| 1196 |
+
dagHandle.setSelectedNodeId(preservedSelectedId);
|
| 1197 |
+
} else {
|
| 1198 |
+
dagHandle.clearNodeSelection();
|
| 1199 |
+
}
|
| 1200 |
}
|
| 1201 |
|
| 1202 |
dagMeasureWidthInput?.addEventListener('change', () => {
|
|
|
|
| 1290 |
hideExcludedTokens: dagHideExcludedTokensInput?.checked ?? false,
|
| 1291 |
edgeTopPCoverage,
|
| 1292 |
nodeCiVisualScaleEnabled: dagNodeCiVisualScaleInput?.checked ?? true,
|
| 1293 |
+
decayAttributionToHighSurprisalTargetEnabled:
|
| 1294 |
+
dagDecayAttributionHighSurprisalInput?.checked ?? true,
|
| 1295 |
hideInactiveEdges: dagHideInactiveEdgesInput?.checked ?? false,
|
| 1296 |
+
showDownstreamInfluence: dagShowDownstreamInfluenceInput?.checked ?? false,
|
| 1297 |
+
recursiveAttributionEnabled: dagRecursiveAttributionInput?.checked ?? false,
|
| 1298 |
+
showTokenInfoOnSelected: dagShowTopkOnSelectedInput?.checked ?? false,
|
| 1299 |
replayPacingMode: currentDagReplayPacingMode(),
|
| 1300 |
playbackTotalS,
|
| 1301 |
playbackStepMs,
|
|
|
|
| 1303 |
excludePromptPatternsText: genAttrExcludePromptPatternsTa?.value ?? '',
|
| 1304 |
excludeGeneratedPatternsEnabled: genAttrExcludeGeneratedPatternsEnable?.checked ?? true,
|
| 1305 |
excludeGeneratedPatternsText: genAttrExcludeGeneratedPatternsTa?.value ?? '',
|
| 1306 |
+
selectedNodeId: dagHandle.getSelectedNodeId(),
|
| 1307 |
};
|
| 1308 |
}
|
| 1309 |
|
| 1310 |
+
function genAttrDemoUiOptionsMatchesDefaults(current: GenAttrDemoUiOptions): boolean {
|
| 1311 |
+
const base = DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS;
|
| 1312 |
+
for (const key of Object.keys(base) as (keyof GenAttrDemoUiOptions)[]) {
|
| 1313 |
+
const c = current[key];
|
| 1314 |
+
const b = base[key];
|
| 1315 |
+
if (typeof c === 'number' && typeof b === 'number') {
|
| 1316 |
+
if (Math.abs(c - b) >= 1e-6) return false;
|
| 1317 |
+
} else if (c !== b) {
|
| 1318 |
+
return false;
|
| 1319 |
+
}
|
| 1320 |
+
}
|
| 1321 |
+
return true;
|
| 1322 |
+
}
|
| 1323 |
+
|
| 1324 |
+
function syncGenAttrResetUiOptionsButtonState(): void {
|
| 1325 |
+
if (!genAttrResetUiOptionsBtn) return;
|
| 1326 |
+
genAttrResetUiOptionsBtn.disabled = genAttrDemoUiOptionsMatchesDefaults(
|
| 1327 |
+
readGenAttrDemoUiOptionsFromControls(),
|
| 1328 |
+
);
|
| 1329 |
+
}
|
| 1330 |
+
|
| 1331 |
/**
|
| 1332 |
* 从 `demoUiOptions` 还原排除控件(仅 DOM);与施加 DAG demo 不回写 GEN_ATTR_LS 的策略一致,`replay` 读当前控件生效。
|
| 1333 |
*/
|
|
|
|
| 1361 |
syncGenAttrExcludePatternTextareasDisabled();
|
| 1362 |
}
|
| 1363 |
|
| 1364 |
+
/** 按 `demoUiOptions` 逐项还原 DAG 面板与排除控件(后者仅 DOM);未覆盖字段不改变。 */
|
| 1365 |
+
function applyGenAttrDemoUiOptionsSnap(snap: Partial<GenAttrDemoUiOptions>): void {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1366 |
const mode = snap.layoutMode;
|
| 1367 |
if (mode) {
|
| 1368 |
if (dagLayoutModeSelect) {
|
|
|
|
| 1400 |
if (dagNodeCiVisualScaleInput) dagNodeCiVisualScaleInput.checked = snap.nodeCiVisualScaleEnabled;
|
| 1401 |
setDagNodeCiVisualScaleEnabled(snap.nodeCiVisualScaleEnabled);
|
| 1402 |
}
|
| 1403 |
+
const decayAttributionHighSurprisal =
|
| 1404 |
+
snap.decayAttributionToHighSurprisalTargetEnabled ??
|
| 1405 |
+
(snap as { edgeWeakenHighSurprisalEnabled?: boolean }).edgeWeakenHighSurprisalEnabled;
|
| 1406 |
+
if (decayAttributionHighSurprisal !== undefined) {
|
| 1407 |
+
if (dagDecayAttributionHighSurprisalInput) {
|
| 1408 |
+
dagDecayAttributionHighSurprisalInput.checked = decayAttributionHighSurprisal;
|
| 1409 |
+
}
|
| 1410 |
+
setDagDecayAttributionToHighSurprisalTargetEnabled(decayAttributionHighSurprisal);
|
| 1411 |
}
|
| 1412 |
if (snap.hideInactiveEdges !== undefined) {
|
| 1413 |
if (dagHideInactiveEdgesInput) dagHideInactiveEdgesInput.checked = snap.hideInactiveEdges;
|
| 1414 |
applyDagHideInactiveEdges(snap.hideInactiveEdges);
|
| 1415 |
}
|
| 1416 |
+
if (snap.showDownstreamInfluence !== undefined) {
|
| 1417 |
+
if (dagShowDownstreamInfluenceInput) {
|
| 1418 |
+
dagShowDownstreamInfluenceInput.checked = snap.showDownstreamInfluence;
|
| 1419 |
+
}
|
| 1420 |
+
dagHandle.setShowDownstreamInfluence(snap.showDownstreamInfluence);
|
| 1421 |
+
}
|
| 1422 |
+
if (snap.recursiveAttributionEnabled !== undefined) {
|
| 1423 |
+
if (dagRecursiveAttributionInput) {
|
| 1424 |
+
dagRecursiveAttributionInput.checked = snap.recursiveAttributionEnabled;
|
| 1425 |
+
}
|
| 1426 |
+
applyDagDownstreamInfluenceUi();
|
| 1427 |
+
dagHandle.setRecursiveAttributionEnabled(snap.recursiveAttributionEnabled);
|
| 1428 |
+
}
|
| 1429 |
+
if (snap.showTokenInfoOnSelected !== undefined) {
|
| 1430 |
+
if (dagShowTopkOnSelectedInput) dagShowTopkOnSelectedInput.checked = snap.showTokenInfoOnSelected;
|
| 1431 |
+
dagHandle.setShowTokenInfoOnSelected(snap.showTokenInfoOnSelected);
|
| 1432 |
+
}
|
| 1433 |
if (snap.replayPacingMode !== undefined) {
|
| 1434 |
if (dagReplayModeSelect) dagReplayModeSelect.value = snap.replayPacingMode;
|
| 1435 |
applyDagReplaySpeedUi();
|
|
|
|
| 1444 |
}
|
| 1445 |
|
| 1446 |
applyGenAttrExcludePatternsFromDemoUiSnap(snap);
|
| 1447 |
+
syncGenAttrResetUiOptionsButtonState();
|
| 1448 |
+
}
|
| 1449 |
+
|
| 1450 |
+
/** replay 完成后按 `demoUiOptions.selectedNodeId` 恢复 DAG 焦点;无效或缺失则清除选中。 */
|
| 1451 |
+
function restoreGenAttrDagFocusFromDemoUiOptions(snap: Partial<GenAttrDemoUiOptions> | undefined): void {
|
| 1452 |
+
const focusId = snap?.selectedNodeId;
|
| 1453 |
+
if (typeof focusId === 'string' && focusId.length > 0) {
|
| 1454 |
+
try {
|
| 1455 |
+
dagHandle.setSelectedNodeId(focusId);
|
| 1456 |
+
return;
|
| 1457 |
+
} catch {
|
| 1458 |
+
/* demo 快照与当前图不一致时忽略 */
|
| 1459 |
+
}
|
| 1460 |
+
}
|
| 1461 |
+
dagHandle.clearNodeSelection();
|
| 1462 |
+
}
|
| 1463 |
+
|
| 1464 |
+
function applyGenAttrDemoUiOptionsFromRecord(rec: GenAttrCachedRun): void {
|
| 1465 |
+
if (!rec.demoUiOptions) return;
|
| 1466 |
+
applyGenAttrDemoUiOptionsSnap(rec.demoUiOptions);
|
| 1467 |
+
}
|
| 1468 |
+
|
| 1469 |
+
/** Gen Attribute demo-UI scope:与 {@link readGenAttrDemoUiOptionsFromControls} / IndexedDB demo 快照一致(不含 Model、Max tokens、prompt 正文)。 */
|
| 1470 |
+
const GEN_ATTR_DEMO_UI_LOCAL_STORAGE_KEYS: readonly string[] = [
|
| 1471 |
+
GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY,
|
| 1472 |
+
GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY,
|
| 1473 |
+
GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY,
|
| 1474 |
+
GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY,
|
| 1475 |
+
GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY,
|
| 1476 |
+
GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY,
|
| 1477 |
+
GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY,
|
| 1478 |
+
GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY,
|
| 1479 |
+
GEN_ATTR_DAG_SHOW_DOWNSTREAM_INFLUENCE_STORAGE_KEY,
|
| 1480 |
+
GEN_ATTR_DAG_RECURSIVE_ATTRIBUTION_STORAGE_KEY,
|
| 1481 |
+
GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY,
|
| 1482 |
+
GEN_ATTR_DAG_SHOW_TOPK_ON_SELECTED_STORAGE_KEY,
|
| 1483 |
+
GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY,
|
| 1484 |
+
GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY,
|
| 1485 |
+
GEN_ATTR_DAG_EDGE_TOP_P_COVERAGE_STORAGE_KEY,
|
| 1486 |
+
GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY,
|
| 1487 |
+
GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY,
|
| 1488 |
+
GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY,
|
| 1489 |
+
GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY,
|
| 1490 |
+
];
|
| 1491 |
+
|
| 1492 |
+
function removeGenAttrDemoUiOptionsFromLocalStorage(): void {
|
| 1493 |
+
try {
|
| 1494 |
+
for (const k of GEN_ATTR_DEMO_UI_LOCAL_STORAGE_KEYS) {
|
| 1495 |
+
localStorage.removeItem(k);
|
| 1496 |
+
}
|
| 1497 |
+
} catch {
|
| 1498 |
+
/* ignore */
|
| 1499 |
+
}
|
| 1500 |
+
}
|
| 1501 |
+
|
| 1502 |
+
/** 重置「DAG 演示用 UI」:清 LS 后以 {@link DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS} 全量套用。 */
|
| 1503 |
+
function resetGenAttrDemoUiOptionsToDefaults(): void {
|
| 1504 |
+
stopDagPlayback();
|
| 1505 |
+
removeGenAttrDemoUiOptionsFromLocalStorage();
|
| 1506 |
+
applyGenAttrDemoUiOptionsSnap(DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS);
|
| 1507 |
+
tryResetAndReplayDag({ preserveNodeSelection: false });
|
| 1508 |
}
|
| 1509 |
|
| 1510 |
+
genAttrResetUiOptionsBtn?.addEventListener('click', resetGenAttrDemoUiOptionsToDefaults);
|
| 1511 |
+
|
| 1512 |
+
(() => {
|
| 1513 |
+
const panel = document.querySelector('.gen-attribute-page .input-section');
|
| 1514 |
+
if (!panel) return;
|
| 1515 |
+
const sync = () => syncGenAttrResetUiOptionsButtonState();
|
| 1516 |
+
panel.addEventListener('change', sync);
|
| 1517 |
+
panel.addEventListener('input', sync);
|
| 1518 |
+
sync();
|
| 1519 |
+
})();
|
| 1520 |
+
|
| 1521 |
window.addEventListener('pagehide', (ev) => {
|
| 1522 |
if (ev.persisted) return;
|
| 1523 |
dagHandle.detach();
|
|
|
|
| 1526 |
function onExcludePatternsEffectiveChange(): void {
|
| 1527 |
const h = runnerHandle;
|
| 1528 |
if (!h || h.tokenCount === 0) return;
|
| 1529 |
+
tryResetAndReplayDag();
|
|
|
|
|
|
|
| 1530 |
}
|
| 1531 |
|
| 1532 |
function bindExcludePatternControls(
|
|
|
|
| 1853 |
stopDagPlayback();
|
| 1854 |
dagHandle.reset();
|
| 1855 |
applyGenAttrDemoUiOptionsFromRecord(rec);
|
| 1856 |
+
syncGenAttrResetUiOptionsButtonState();
|
| 1857 |
runnerHandle = createHydratedTokenGenHandle(rec.steps);
|
| 1858 |
lastRunInitialContext = rec.initialContext;
|
| 1859 |
lastRunInputSnapshot = getInputSnapshotForRun();
|
|
|
|
| 1863 |
currentRunPromptSpans = replayPromptSpans;
|
| 1864 |
replayRunnerStepsIntoDag(runnerHandle, replayPromptSpans);
|
| 1865 |
dagHandle.fitViewportToContent();
|
| 1866 |
+
restoreGenAttrDagFocusFromDemoUiOptions(rec.demoUiOptions);
|
| 1867 |
const n = runnerHandle.tokenCount;
|
| 1868 |
setGenAttrUsageMetric(initialPromptTokensFromFirstStep(rec.steps[0]!), n);
|
| 1869 |
if (validateMetricsElements(metricModel) && n > 0) {
|
|
|
|
| 2271 |
function refreshDagForThemeChange(): void {
|
| 2272 |
stopDagPlayback();
|
| 2273 |
const h = runnerHandle;
|
| 2274 |
+
if (!h || h.tokenCount === 0) return;
|
| 2275 |
+
tryResetAndReplayDag();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2276 |
}
|
| 2277 |
|
| 2278 |
const themeManager = initThemeManager(
|
client/src/ts/lang/translations.ts
CHANGED
|
@@ -52,11 +52,17 @@ export const translations: Translations = {
|
|
| 52 |
'Coverage 指每一步在 Top-N 候选池内的累计质量份额(先入池、池内归一后按强度排序再累加)。数值越大保留的 DAG 入边越多;分母仅为该候选池,不是该步 API 返回的全部归因 token。',
|
| 53 |
'When checked, gray DAG edges not adjacent to the hovered or selected node are hidden.':
|
| 54 |
'勾选后,DAG 中未与当前悬浮/选中节点相邻的灰色边将被隐藏。',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
'Width (px) of the invisible measurement layer used for DAG layout. Only this width affects wrapping and node positions. When idle, changes replay and fit automatically; during generation or DAG playback, the setting updates for the next run or refresh.':
|
| 56 |
'DAG 节点几何所基于的不可见测量层宽度(px)。只有测量层宽度会影响节点折行/位置。修改后:稳态下自动按新宽度重放并 fit;若正在生成或 DAG 播放中,仅更新设置,下次刷新/生成时生效。',
|
| 57 |
'Token distance': 'Token distance 间距',
|
|
|
|
|
|
|
| 58 |
'Horizontal gap (px) between the outer left/right edges of adjacent token nodes in linear-arc layout only. When idle, the DAG refits; during generation or DAG playback, the value is stored and applied on the next sync.':
|
| 59 |
-
'仅 linear-arc 布局下生效:相邻 token 节点矩形外侧边之间的水平间隙(px)。修改后:稳态下立即重绘并 fit;若正在生成或 DAG 播放中,仅写入存储,下一轮同步时再反映。',
|
| 60 |
'Compactness': 'DAG 紧凑度',
|
| 61 |
'Scales DAG node boxes and labels relative to the measurement layer; 1 matches full readout scale. When idle, changes replay and fit automatically; during generation or DAG playback, the setting updates for the next run or refresh.':
|
| 62 |
'相对测量层缩放 DAG 节点框与标签;1 与正文阅读比例一致。修改后:稳态下自动重放并 fit;若正在生成或 DAG 播放中,仅更新设置,下次运行或刷新时生效。',
|
|
@@ -115,6 +121,9 @@ export const translations: Translations = {
|
|
| 115 |
'Cached result not found (link may be expired)': '未找到该条归因缓存(分享链接可能已过期)',
|
| 116 |
'Cached run not found': '未找到该条生成缓存,可能已被删除',
|
| 117 |
'Cached run not found (link may be expired)': '未找到该条生成缓存(分享链接可能已过期)',
|
|
|
|
|
|
|
|
|
|
| 118 |
'Cached history': '历史缓存',
|
| 119 |
'Cached demos': '示例缓存',
|
| 120 |
'Demo not found': '未找到该示例缓存',
|
|
@@ -305,6 +314,9 @@ export const translations: Translations = {
|
|
| 305 |
'log perplexity:': '对数困惑度:',
|
| 306 |
'Top-k data not available.': '未提供 Top-k 数据。',
|
| 307 |
'UTF-8 size:': 'UTF-8大小:',
|
|
|
|
|
|
|
|
|
|
| 308 |
|
| 309 |
// ========== 统计信息 ==========
|
| 310 |
'bytes': '字节',
|
|
@@ -386,6 +398,8 @@ export const translations: Translations = {
|
|
| 386 |
'无法使用加密API(crypto.subtle),保存到本地缓存功能不可用。',
|
| 387 |
'URL text extraction failed': 'URL 文本提取失败',
|
| 388 |
'Semantic analysis failed': '语义分析失败',
|
|
|
|
|
|
|
| 389 |
'Tokenizer results inconsistent: semantic and info-density token boundaries differ.': 'Tokenizer 结果不一致:语义分析与信息密度的 token 边界存在差异,属预期外情况。',
|
| 390 |
'No data to analyze, please analyze text first': '没有可分析的数据,请先分析文本',
|
| 391 |
'User cancelled file selection': '用户取消了文件选择',
|
|
|
|
| 52 |
'Coverage 指每一步在 Top-N 候选池内的累计质量份额(先入池、池内归一后按强度排序再累加)。数值越大保留的 DAG 入边越多;分母仅为该候选池,不是该步 API 返回的全部归因 token。',
|
| 53 |
'When checked, gray DAG edges not adjacent to the hovered or selected node are hidden.':
|
| 54 |
'勾选后,DAG 中未与当前悬浮/选中节点相邻的灰色边将被隐藏。',
|
| 55 |
+
'Show token tooltip':
|
| 56 |
+
'显示 token 提示',
|
| 57 |
+
'When checked, selecting or hovering a token node shows token information in the results area.':
|
| 58 |
+
'勾选后,选中或悬浮 token 节点时,在右侧结果区展示该 token 的信息(信息量、预测分布、归因份额等)。',
|
| 59 |
'Width (px) of the invisible measurement layer used for DAG layout. Only this width affects wrapping and node positions. When idle, changes replay and fit automatically; during generation or DAG playback, the setting updates for the next run or refresh.':
|
| 60 |
'DAG 节点几何所基于的不可见测量层宽度(px)。只有测量层宽度会影响节点折行/位置。修改后:稳态下自动按新宽度重放并 fit;若正在生成或 DAG 播放中,仅更新设置,下次刷新/生成时生效。',
|
| 61 |
'Token distance': 'Token distance 间距',
|
| 62 |
+
'Horizontal gap (px) between the outer left/right edges of adjacent token nodes in linear-arc / linear-arc-step-down layout only. When idle, the DAG refits; during generation or DAG playback, the value is stored and applied on the next sync.':
|
| 63 |
+
'仅 linear-arc / linear-arc-step-down 布局下生效:相邻 token 节点矩形外侧边之间的水平间隙(px)。修改后:稳态下立即重绘并 fit;若正在生成或 DAG 播放中,仅写入存储,下一轮同步时再反映。',
|
| 64 |
'Horizontal gap (px) between the outer left/right edges of adjacent token nodes in linear-arc layout only. When idle, the DAG refits; during generation or DAG playback, the value is stored and applied on the next sync.':
|
| 65 |
+
'仅 linear-arc / linear-arc-step-down 布局下生效:相邻 token 节点矩形外侧边之间的水平间隙(px)。修改后:稳态下立即重绘并 fit;若正在生成或 DAG 播放中,仅写入存储,下一轮同步时再反映。',
|
| 66 |
'Compactness': 'DAG 紧凑度',
|
| 67 |
'Scales DAG node boxes and labels relative to the measurement layer; 1 matches full readout scale. When idle, changes replay and fit automatically; during generation or DAG playback, the setting updates for the next run or refresh.':
|
| 68 |
'相对测量层缩放 DAG 节点框与标签;1 与正文阅读比例一致。修改后:稳态下自动重放并 fit;若正在生成或 DAG 播放中,仅更新设置,下次运行或刷新时生效。',
|
|
|
|
| 121 |
'Cached result not found (link may be expired)': '未找到该条归因缓存(分享链接可能已过期)',
|
| 122 |
'Cached run not found': '未找到该条生成缓存,可能已被删除',
|
| 123 |
'Cached run not found (link may be expired)': '未找到该条生成缓存(分享链接可能已过期)',
|
| 124 |
+
'Reset UI options': '重置界面选项',
|
| 125 |
+
'Restore DAG options, replay speed, exclusions, etc. to defaults and clear saved preferences for those controls.':
|
| 126 |
+
'将 DAG 参数、回放速度、排除正则等恢复为默认值,并清除这些控件的本地保存项。',
|
| 127 |
'Cached history': '历史缓存',
|
| 128 |
'Cached demos': '示例缓存',
|
| 129 |
'Demo not found': '未找到该示例缓存',
|
|
|
|
| 314 |
'log perplexity:': '对数困惑度:',
|
| 315 |
'Top-k data not available.': '未提供 Top-k 数据。',
|
| 316 |
'UTF-8 size:': 'UTF-8大小:',
|
| 317 |
+
'Attribution share:': '归因份额:',
|
| 318 |
+
'Attribution share (Total):': '归因份额(总计):',
|
| 319 |
+
'Attribution share (Self):': '归因份额(自身):',
|
| 320 |
|
| 321 |
// ========== 统计信息 ==========
|
| 322 |
'bytes': '字节',
|
|
|
|
| 398 |
'无法使用加密API(crypto.subtle),保存到本地缓存功能不可用。',
|
| 399 |
'URL text extraction failed': 'URL 文本提取失败',
|
| 400 |
'Semantic analysis failed': '语义分析失败',
|
| 401 |
+
'Semantic Query(Beta)': '语义查询(Beta)',
|
| 402 |
+
'Enter query question or topic': '请输入查询问题或主题',
|
| 403 |
'Tokenizer results inconsistent: semantic and info-density token boundaries differ.': 'Tokenizer 结果不一致:语义分析与信息密度的 token 边界存在差异,属预期外情况。',
|
| 404 |
'No data to analyze, please analyze text first': '没有可分析的数据,请先分析文本',
|
| 405 |
'User cancelled file selection': '用户取消了文件选择',
|
client/src/ts/storage/genAttributeRunCache.ts
CHANGED
|
@@ -67,8 +67,13 @@ export type GenAttrDemoUiOptions = {
|
|
| 67 |
hideExcludedTokens: boolean;
|
| 68 |
edgeTopPCoverage: number;
|
| 69 |
nodeCiVisualScaleEnabled: boolean;
|
| 70 |
-
|
| 71 |
hideInactiveEdges: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
replayPacingMode: 'total' | 'step';
|
| 73 |
playbackTotalS: number;
|
| 74 |
playbackStepMs: number;
|
|
@@ -78,6 +83,8 @@ export type GenAttrDemoUiOptions = {
|
|
| 78 |
/** 排除生成 token 归因:使能与正则文本(`info_radar_gen_attr_exclude_generated_*`)。 */
|
| 79 |
excludeGeneratedPatternsEnabled: boolean;
|
| 80 |
excludeGeneratedPatternsText: string;
|
|
|
|
|
|
|
| 81 |
};
|
| 82 |
|
| 83 |
/** 单条记录 JSON:内容字段 + 可选 `demoUiOptions`(仅导出 demo 写入)。 */
|
|
@@ -193,7 +200,12 @@ function isValidGenAttrRunDraftPayload(v: unknown): boolean {
|
|
| 193 |
}
|
| 194 |
|
| 195 |
function isDagLayoutModePayload(v: unknown): v is DagLayoutMode {
|
| 196 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
}
|
| 198 |
|
| 199 |
function isValidDemoUiOptionsPayload(v: unknown): v is Partial<GenAttrDemoUiOptions> {
|
|
@@ -222,10 +234,35 @@ function isValidDemoUiOptionsPayload(v: unknown): v is Partial<GenAttrDemoUiOpti
|
|
| 222 |
if (d.nodeCiVisualScaleEnabled !== undefined && typeof d.nodeCiVisualScaleEnabled !== 'boolean') {
|
| 223 |
return false;
|
| 224 |
}
|
| 225 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
return false;
|
| 227 |
}
|
| 228 |
if (d.hideInactiveEdges !== undefined && typeof d.hideInactiveEdges !== 'boolean') return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
if (d.replayPacingMode !== undefined && d.replayPacingMode !== 'total' && d.replayPacingMode !== 'step') {
|
| 230 |
return false;
|
| 231 |
}
|
|
@@ -256,6 +293,13 @@ function isValidDemoUiOptionsPayload(v: unknown): v is Partial<GenAttrDemoUiOpti
|
|
| 256 |
) {
|
| 257 |
return false;
|
| 258 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
return true;
|
| 260 |
}
|
| 261 |
|
|
|
|
| 67 |
hideExcludedTokens: boolean;
|
| 68 |
edgeTopPCoverage: number;
|
| 69 |
nodeCiVisualScaleEnabled: boolean;
|
| 70 |
+
decayAttributionToHighSurprisalTargetEnabled: boolean;
|
| 71 |
hideInactiveEdges: boolean;
|
| 72 |
+
showDownstreamInfluence: boolean;
|
| 73 |
+
/** 传播归因(UI: Propagated attribution mode;与 `recursiveAttribution*` 同义)。 */
|
| 74 |
+
recursiveAttributionEnabled: boolean;
|
| 75 |
+
/** 是否显示 token tooltip(UI: Show token tooltip;`showTokenInfoOnSelected`)。 */
|
| 76 |
+
showTokenInfoOnSelected: boolean;
|
| 77 |
replayPacingMode: 'total' | 'step';
|
| 78 |
playbackTotalS: number;
|
| 79 |
playbackStepMs: number;
|
|
|
|
| 83 |
/** 排除生成 token 归因:使能与正则文本(`info_radar_gen_attr_exclude_generated_*`)。 */
|
| 84 |
excludeGeneratedPatternsEnabled: boolean;
|
| 85 |
excludeGeneratedPatternsText: string;
|
| 86 |
+
/** DAG 选中节点(offset id:`"${start}_${end}"`);无选中时为 `null`。 */
|
| 87 |
+
selectedNodeId?: string | null;
|
| 88 |
};
|
| 89 |
|
| 90 |
/** 单条记录 JSON:内容字段 + 可选 `demoUiOptions`(仅导出 demo 写入)。 */
|
|
|
|
| 200 |
}
|
| 201 |
|
| 202 |
function isDagLayoutModePayload(v: unknown): v is DagLayoutMode {
|
| 203 |
+
return (
|
| 204 |
+
v === 'text-flow' ||
|
| 205 |
+
v === 'linear-arc' ||
|
| 206 |
+
v === 'linear-arc-step-down' ||
|
| 207 |
+
v === 'spiral'
|
| 208 |
+
);
|
| 209 |
}
|
| 210 |
|
| 211 |
function isValidDemoUiOptionsPayload(v: unknown): v is Partial<GenAttrDemoUiOptions> {
|
|
|
|
| 234 |
if (d.nodeCiVisualScaleEnabled !== undefined && typeof d.nodeCiVisualScaleEnabled !== 'boolean') {
|
| 235 |
return false;
|
| 236 |
}
|
| 237 |
+
if (
|
| 238 |
+
d.decayAttributionToHighSurprisalTargetEnabled !== undefined &&
|
| 239 |
+
typeof d.decayAttributionToHighSurprisalTargetEnabled !== 'boolean'
|
| 240 |
+
) {
|
| 241 |
+
return false;
|
| 242 |
+
}
|
| 243 |
+
const legacyDecay = (d as { edgeWeakenHighSurprisalEnabled?: unknown }).edgeWeakenHighSurprisalEnabled;
|
| 244 |
+
if (legacyDecay !== undefined && typeof legacyDecay !== 'boolean') {
|
| 245 |
return false;
|
| 246 |
}
|
| 247 |
if (d.hideInactiveEdges !== undefined && typeof d.hideInactiveEdges !== 'boolean') return false;
|
| 248 |
+
if (
|
| 249 |
+
d.showDownstreamInfluence !== undefined &&
|
| 250 |
+
typeof d.showDownstreamInfluence !== 'boolean'
|
| 251 |
+
) {
|
| 252 |
+
return false;
|
| 253 |
+
}
|
| 254 |
+
if (
|
| 255 |
+
d.recursiveAttributionEnabled !== undefined &&
|
| 256 |
+
typeof d.recursiveAttributionEnabled !== 'boolean'
|
| 257 |
+
) {
|
| 258 |
+
return false;
|
| 259 |
+
}
|
| 260 |
+
if (
|
| 261 |
+
d.showTokenInfoOnSelected !== undefined &&
|
| 262 |
+
typeof d.showTokenInfoOnSelected !== 'boolean'
|
| 263 |
+
) {
|
| 264 |
+
return false;
|
| 265 |
+
}
|
| 266 |
if (d.replayPacingMode !== undefined && d.replayPacingMode !== 'total' && d.replayPacingMode !== 'step') {
|
| 267 |
return false;
|
| 268 |
}
|
|
|
|
| 293 |
) {
|
| 294 |
return false;
|
| 295 |
}
|
| 296 |
+
if (
|
| 297 |
+
d.selectedNodeId !== undefined &&
|
| 298 |
+
d.selectedNodeId !== null &&
|
| 299 |
+
typeof d.selectedNodeId !== 'string'
|
| 300 |
+
) {
|
| 301 |
+
return false;
|
| 302 |
+
}
|
| 303 |
return true;
|
| 304 |
}
|
| 305 |
|
client/src/ts/utils/clientActivityPing.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { AdminManager } from './adminManager';
|
|
| 3 |
|
| 4 |
const S = 10, FIRST = 2;
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
export type ReportedClientOs = 'ios' | 'android' | 'windows' | 'macos' | 'linux' | 'unknown';
|
| 7 |
|
| 8 |
/** UA 粗略归类;仅在首轮心跳(cum === FIRST)顺带上报一次。 */
|
|
@@ -39,6 +45,10 @@ export function initClientActivityPing(apiPrefix: string | null | undefined): vo
|
|
| 39 |
delta_active_sec,
|
| 40 |
};
|
| 41 |
if (cum === FIRST) payload.client_os = detectInitialClientOs();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
const body = JSON.stringify(payload);
|
| 43 |
void fetch(u, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, keepalive: true })
|
| 44 |
.then((r) => { if (r.ok) reportedTotal = cum; })
|
|
|
|
| 3 |
|
| 4 |
const S = 10, FIRST = 2;
|
| 5 |
|
| 6 |
+
let _pageOptsGetter: (() => Record<string, boolean>) | undefined;
|
| 7 |
+
/** gen_attribute.html 等页面注册当前选项状态,供心跳上报时附带。 */
|
| 8 |
+
export function setPageOptsGetter(fn: () => Record<string, boolean>): void {
|
| 9 |
+
_pageOptsGetter = fn;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
export type ReportedClientOs = 'ios' | 'android' | 'windows' | 'macos' | 'linux' | 'unknown';
|
| 13 |
|
| 14 |
/** UA 粗略归类;仅在首轮心跳(cum === FIRST)顺带上报一次。 */
|
|
|
|
| 45 |
delta_active_sec,
|
| 46 |
};
|
| 47 |
if (cum === FIRST) payload.client_os = detectInitialClientOs();
|
| 48 |
+
if (_pageOptsGetter) {
|
| 49 |
+
const active = Object.fromEntries(Object.entries(_pageOptsGetter()).filter(([, v]) => v));
|
| 50 |
+
if (Object.keys(active).length > 0) payload.page_opts = active;
|
| 51 |
+
}
|
| 52 |
const body = JSON.stringify(payload);
|
| 53 |
void fetch(u, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, keepalive: true })
|
| 54 |
.then((r) => { if (r.ok) reportedTotal = cum; })
|
client/src/ts/utils/surprisalMath.ts
CHANGED
|
@@ -14,12 +14,18 @@ export const ZERO_CONFIDENCE_PROBABILITY_BASELINE = 2 ** -18;
|
|
| 14 |
export const REFERENCE_MAX_SURPRISAL_BITS = Math.log2(1 / ZERO_CONFIDENCE_PROBABILITY_BASELINE);
|
| 15 |
|
| 16 |
/**
|
| 17 |
-
* 全信心概率阈值 p₁:surprisal 低
|
| 18 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
*/
|
| 20 |
export const FULL_CONFIDENCE_PROBABILITY_BASELINE = 2 ** -3;
|
| 21 |
|
| 22 |
-
/** 与 p₁ 对应的 surprisal
|
| 23 |
export const REFERENCE_NO_SURPRISAL_BITS = Math.log2(1 / FULL_CONFIDENCE_PROBABILITY_BASELINE);
|
| 24 |
|
| 25 |
function clamp01(n: number): number {
|
|
@@ -49,3 +55,44 @@ export function computeConditionalInformationRatio(targetProb: number | undefine
|
|
| 49 |
if (!Number.isFinite(targetProb) || targetProb <= 0) return 1;
|
| 50 |
return clamp01(-Math.log2(targetProb) / REFERENCE_MAX_SURPRISAL_BITS);
|
| 51 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
export const REFERENCE_MAX_SURPRISAL_BITS = Math.log2(1 / ZERO_CONFIDENCE_PROBABILITY_BASELINE);
|
| 15 |
|
| 16 |
/**
|
| 17 |
+
* 全信心概率阈值 p₁(3 bit,p > 1/8):surprisal 足够低时视为充分自信、DAG 传导节点(非信息来源)。
|
| 18 |
+
*
|
| 19 |
+
* 硬截断仅经本文件三个 `dag*` 函数进入 Generate & Attribute DAG:
|
| 20 |
+
* - {@link dagCiVisualScaleFromTargetProb} → `genAttributeDagView`:生成节点框/标签不放大(1×)
|
| 21 |
+
* - {@link dagStepDownEffectiveCiRatio} → `genAttributeDagViewLinearArcMode`:`linear-arc-step-down` 无竖直台阶
|
| 22 |
+
* - {@link dagPropagationMiRatio} → `genAttributeDagView` `nodePropagationMiRatio`:递归链满额传导、无 stay 描边
|
| 23 |
+
*
|
| 24 |
+
* 不用于 tooltip 与边上的 CI/MI 展示(仍 {@link computeMutualInformationRatio} / {@link computeConditionalInformationRatio})。
|
| 25 |
*/
|
| 26 |
export const FULL_CONFIDENCE_PROBABILITY_BASELINE = 2 ** -3;
|
| 27 |
|
| 28 |
+
/** 与 p₁ 对应的 surprisal 上界(bit);surprisal ≤ 此值即满足「充分自信」截断条件。 */
|
| 29 |
export const REFERENCE_NO_SURPRISAL_BITS = Math.log2(1 / FULL_CONFIDENCE_PROBABILITY_BASELINE);
|
| 30 |
|
| 31 |
function clamp01(n: number): number {
|
|
|
|
| 55 |
if (!Number.isFinite(targetProb) || targetProb <= 0) return 1;
|
| 56 |
return clamp01(-Math.log2(targetProb) / REFERENCE_MAX_SURPRISAL_BITS);
|
| 57 |
}
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* DAG 生成节点 CI 视觉缩放倍数(约 `[1, 2]`):语义为 `1 +` 有效 CI。
|
| 61 |
+
* `ciVisualScaleEnabled === false` 或 `p > {@link FULL_CONFIDENCE_PROBABILITY_BASELINE}` 时为 `1×`;
|
| 62 |
+
* 否则为 `1 + {@link computeConditionalInformationRatio}(p)`。
|
| 63 |
+
*/
|
| 64 |
+
export function dagCiVisualScaleFromTargetProb(
|
| 65 |
+
targetProb: number | undefined,
|
| 66 |
+
ciVisualScaleEnabled: boolean
|
| 67 |
+
): number {
|
| 68 |
+
if (!ciVisualScaleEnabled) return 1;
|
| 69 |
+
if (targetProb !== undefined && Number.isFinite(targetProb) && targetProb > FULL_CONFIDENCE_PROBABILITY_BASELINE) {
|
| 70 |
+
return 1;
|
| 71 |
+
}
|
| 72 |
+
return 1 + computeConditionalInformationRatio(targetProb);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/**
|
| 76 |
+
* 仅用于 DAG「下台阶」布局的有效 CI(`[0,1]`):与 {@link computeConditionalInformationRatio} 同源,
|
| 77 |
+
* 但 `p > {@link FULL_CONFIDENCE_PROBABILITY_BASELINE}` 时为 0(与节点「高置信 1×」截断一致)。
|
| 78 |
+
*
|
| 79 |
+
* 不受「关闭 CI 视觉放大」开关影响——该开关只缩节点框,不应关掉按不确定度的竖直落差。
|
| 80 |
+
*/
|
| 81 |
+
export function dagStepDownEffectiveCiRatio(targetProb: number | undefined): number {
|
| 82 |
+
if (targetProb !== undefined && Number.isFinite(targetProb) && targetProb > FULL_CONFIDENCE_PROBABILITY_BASELINE) {
|
| 83 |
+
return 0;
|
| 84 |
+
}
|
| 85 |
+
return computeConditionalInformationRatio(targetProb);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/**
|
| 89 |
+
* DAG 递归归因链的传导系数(`[0,1]`):与 {@link computeMutualInformationRatio} 同源,
|
| 90 |
+
* 但 `p > {@link FULL_CONFIDENCE_PROBABILITY_BASELINE}` 时截断为 1(纯传导,不衰减预算、不留 stay)。
|
| 91 |
+
* 与节点视觉(不放大)、下台阶(不下沉)的「充分自信」语义保持一致。
|
| 92 |
+
*/
|
| 93 |
+
export function dagPropagationMiRatio(targetProb: number | undefined): number {
|
| 94 |
+
if (targetProb !== undefined && Number.isFinite(targetProb) && targetProb > FULL_CONFIDENCE_PROBABILITY_BASELINE) {
|
| 95 |
+
return 1;
|
| 96 |
+
}
|
| 97 |
+
return computeMutualInformationRatio(targetProb);
|
| 98 |
+
}
|
client/src/ts/utils/tokenDisplayUtils.ts
CHANGED
|
@@ -80,7 +80,7 @@ function visualizeSpecialCharsImpl(text: string, options?: VisualizeSpecialChars
|
|
| 80 |
} else if (inBracket) {
|
| 81 |
processed.push(char);
|
| 82 |
} else {
|
| 83 |
-
// 保留的空格不能走下方「不可打印 →
|
| 84 |
if (char === ' ') {
|
| 85 |
processed.push(char);
|
| 86 |
} else if (isPrintableChar(char)) {
|
|
@@ -88,8 +88,8 @@ function visualizeSpecialCharsImpl(text: string, options?: VisualizeSpecialChars
|
|
| 88 |
} else {
|
| 89 |
const codePoint = char.codePointAt(0);
|
| 90 |
if (codePoint !== undefined) {
|
| 91 |
-
const hexCode = codePoint.toString(16).
|
| 92 |
-
processed.push(`[
|
| 93 |
} else {
|
| 94 |
processed.push(char);
|
| 95 |
}
|
|
|
|
| 80 |
} else if (inBracket) {
|
| 81 |
processed.push(char);
|
| 82 |
} else {
|
| 83 |
+
// 保留的空格不能走下方「不可打印 → 码点」分支,否则会变成 [0020]
|
| 84 |
if (char === ' ') {
|
| 85 |
processed.push(char);
|
| 86 |
} else if (isPrintableChar(char)) {
|
|
|
|
| 88 |
} else {
|
| 89 |
const codePoint = char.codePointAt(0);
|
| 90 |
if (codePoint !== undefined) {
|
| 91 |
+
const hexCode = codePoint.toString(16).toLowerCase().padStart(4, '0');
|
| 92 |
+
processed.push(`[${hexCode}]`);
|
| 93 |
} else {
|
| 94 |
processed.push(char);
|
| 95 |
}
|
client/src/ts/utils/topkChartUtils.ts
CHANGED
|
@@ -123,8 +123,8 @@ export function renderTopkChartHtml(
|
|
| 123 |
const maxBar = options?.maxBarWidth ?? MAX_BAR_WIDTH;
|
| 124 |
const numF = options?.numFormat ?? formatTopkTooltipProbabilityPercent;
|
| 125 |
|
| 126 |
-
|
| 127 |
-
const scale = d3.scaleLinear().domain([0,
|
| 128 |
const barCellW = options?.barCellWidth ?? 110;
|
| 129 |
|
| 130 |
const pickable = options?.interactivePickable === true;
|
|
|
|
| 123 |
const maxBar = options?.maxBarWidth ?? MAX_BAR_WIDTH;
|
| 124 |
const numF = options?.numFormat ?? formatTopkTooltipProbabilityPercent;
|
| 125 |
|
| 126 |
+
/** 条形满宽对应概率 100%(1),与显示的百分比刻度一致 */
|
| 127 |
+
const scale = d3.scaleLinear().domain([0, 1]).range([0, maxBar]);
|
| 128 |
const barCellW = options?.barCellWidth ?? 110;
|
| 129 |
|
| 130 |
const pickable = options?.interactivePickable === true;
|
client/src/ts/utils/visitStatsDialog.ts
CHANGED
|
@@ -28,6 +28,22 @@ const API_ORDER = [
|
|
| 28 |
|
| 29 |
const OS_ORDER = ['ios', 'android', 'windows', 'macos', 'linux', 'unknown'] as const;
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
type VisitStatsRow = NonNullable<Awaited<ReturnType<TextAnalysisAPI['getVisitStats']>>>;
|
| 32 |
|
| 33 |
function orderedKeysGt0(primary: readonly string[], rec: Record<string, number>): string[] {
|
|
@@ -74,7 +90,8 @@ function visitStatsHtml(data: VisitStatsRow): string {
|
|
| 74 |
const pg = data.page_sec ?? {};
|
| 75 |
const ap = data.api ?? {};
|
| 76 |
const os = data.os ?? {};
|
| 77 |
-
const
|
|
|
|
| 78 |
const linesJoined = (keys: string[], cur: Record<string, number>, base: Record<string, number>): string[] => {
|
| 79 |
if (!keys.length) return ['(none)'];
|
| 80 |
return keys.map((k) => {
|
|
@@ -84,7 +101,6 @@ function visitStatsHtml(data: VisitStatsRow): string {
|
|
| 84 |
};
|
| 85 |
const linesJoinedPageSec = (keys: string[], cur: Record<string, number>, base: Record<string, number>): string[] => {
|
| 86 |
if (!keys.length) return ['(none)'];
|
| 87 |
-
const hasBase = Object.keys(sb).length > 0;
|
| 88 |
return keys.map((k) => {
|
| 89 |
const v = cur[k] ?? 0;
|
| 90 |
const main = hasBase ? formatDurationSec(v) : 'unknown';
|
|
@@ -92,6 +108,20 @@ function visitStatsHtml(data: VisitStatsRow): string {
|
|
| 92 |
});
|
| 93 |
};
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
return [
|
| 96 |
`Last delta reset: ${esc(data.reset_at ? new Date(data.reset_at).toLocaleString() : 'unknown')}`,
|
| 97 |
`Last persisted: ${esc(data.saved_at ? new Date(data.saved_at).toLocaleString() : 'unknown')}`,
|
|
@@ -108,6 +138,9 @@ function visitStatsHtml(data: VisitStatsRow): string {
|
|
| 108 |
'',
|
| 109 |
'[API]',
|
| 110 |
...linesJoined(orderedKeysGt0(API_ORDER, ap), ap, sb.api ?? {}),
|
|
|
|
|
|
|
|
|
|
| 111 |
].join('\n');
|
| 112 |
}
|
| 113 |
|
|
|
|
| 28 |
|
| 29 |
const OS_ORDER = ['ios', 'android', 'windows', 'macos', 'linux', 'unknown'] as const;
|
| 30 |
|
| 31 |
+
const GEN_ATTR_OPT_ORDER = [
|
| 32 |
+
'layout_linear_arc', 'layout_step_down', 'layout_spiral',
|
| 33 |
+
'propagated',
|
| 34 |
+
'downstream', 'token_tooltip',
|
| 35 |
+
] as const;
|
| 36 |
+
|
| 37 |
+
/** gen_attribute.html UI 原文;key 与上报/存储一致 */
|
| 38 |
+
const GEN_ATTR_OPT_LABELS: Record<(typeof GEN_ATTR_OPT_ORDER)[number], string> = {
|
| 39 |
+
propagated: 'Propagated attribution mode',
|
| 40 |
+
layout_linear_arc: 'DAG layout mode/linear_arc',
|
| 41 |
+
layout_step_down: 'DAG layout mode/step-down',
|
| 42 |
+
layout_spiral: 'DAG layout mode/spiral',
|
| 43 |
+
downstream: 'Show downstream influence',
|
| 44 |
+
token_tooltip: 'Show token tooltip',
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
type VisitStatsRow = NonNullable<Awaited<ReturnType<TextAnalysisAPI['getVisitStats']>>>;
|
| 48 |
|
| 49 |
function orderedKeysGt0(primary: readonly string[], rec: Record<string, number>): string[] {
|
|
|
|
| 90 |
const pg = data.page_sec ?? {};
|
| 91 |
const ap = data.api ?? {};
|
| 92 |
const os = data.os ?? {};
|
| 93 |
+
const hasBase = Object.keys(sb).length > 0;
|
| 94 |
+
const fmtTotal = (v: number) => (hasBase ? String(v) : 'unknown');
|
| 95 |
const linesJoined = (keys: string[], cur: Record<string, number>, base: Record<string, number>): string[] => {
|
| 96 |
if (!keys.length) return ['(none)'];
|
| 97 |
return keys.map((k) => {
|
|
|
|
| 101 |
};
|
| 102 |
const linesJoinedPageSec = (keys: string[], cur: Record<string, number>, base: Record<string, number>): string[] => {
|
| 103 |
if (!keys.length) return ['(none)'];
|
|
|
|
| 104 |
return keys.map((k) => {
|
| 105 |
const v = cur[k] ?? 0;
|
| 106 |
const main = hasBase ? formatDurationSec(v) : 'unknown';
|
|
|
|
| 108 |
});
|
| 109 |
};
|
| 110 |
|
| 111 |
+
const genAttrOpts = data.gen_attr_opt_sec ?? {};
|
| 112 |
+
const genAttrTotalSec = pg['gen_attribute.html'] ?? 0;
|
| 113 |
+
const genAttrOptKeys = orderedKeysGt0(GEN_ATTR_OPT_ORDER, genAttrOpts);
|
| 114 |
+
const genAttrOptLines: string[] = genAttrOptKeys.length > 0 && genAttrTotalSec > 0
|
| 115 |
+
? genAttrOptKeys.map((k) => {
|
| 116 |
+
const v = genAttrOpts[k] ?? 0;
|
| 117 |
+
const pct = Math.round(v / genAttrTotalSec * 100);
|
| 118 |
+
const main = hasBase ? `${formatDurationSec(v)} (${pct}%)` : 'unknown';
|
| 119 |
+
const bv = (sb.gen_attr_opt_sec ?? {})[k] ?? 0;
|
| 120 |
+
const label = GEN_ATTR_OPT_LABELS[k as (typeof GEN_ATTR_OPT_ORDER)[number]] ?? k;
|
| 121 |
+
return `${esc(label)}: ${main}${deltaSuffixDuration(v - bv)}`;
|
| 122 |
+
})
|
| 123 |
+
: ['(none)'];
|
| 124 |
+
|
| 125 |
return [
|
| 126 |
`Last delta reset: ${esc(data.reset_at ? new Date(data.reset_at).toLocaleString() : 'unknown')}`,
|
| 127 |
`Last persisted: ${esc(data.saved_at ? new Date(data.saved_at).toLocaleString() : 'unknown')}`,
|
|
|
|
| 138 |
'',
|
| 139 |
'[API]',
|
| 140 |
...linesJoined(orderedKeysGt0(API_ORDER, ap), ap, sb.api ?? {}),
|
| 141 |
+
'',
|
| 142 |
+
'[gen_attribute options (% active time)]',
|
| 143 |
+
...genAttrOptLines,
|
| 144 |
].join('\n');
|
| 145 |
}
|
| 146 |
|
client/src/ts/utils/visualizationUpdater.ts
CHANGED
|
@@ -272,16 +272,23 @@ export class VisualizationUpdater {
|
|
| 272 |
|
| 273 |
const infoText = this.currentState.infoDensityData?.request?.text ?? '';
|
| 274 |
const semText = this.currentState.semanticData?.text ?? '';
|
|
|
|
| 275 |
|
| 276 |
let showInfoDensity = false;
|
| 277 |
let showSemantic = false;
|
| 278 |
|
| 279 |
if (mode === 'infoDensity') {
|
| 280 |
-
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
} else {
|
| 283 |
showSemantic = true;
|
| 284 |
-
showInfoDensity =
|
|
|
|
|
|
|
| 285 |
}
|
| 286 |
|
| 287 |
if (tokenHistogramItem) tokenHistogramItem.style.display = showInfoDensity ? '' : 'none';
|
|
@@ -327,18 +334,20 @@ export class VisualizationUpdater {
|
|
| 327 |
|
| 328 |
/**
|
| 329 |
* 重新渲染直方图(内部方法)
|
| 330 |
-
* 仅信息密度
|
| 331 |
* @param skipLmfUpdate 为 true 时跳过 lmf.update(主题切换时由 rerenderOnThemeChange 统一重绘,避免竞态)
|
| 332 |
*/
|
| 333 |
private updateVisualizationInternal(skipLmfUpdate = false): void {
|
| 334 |
const hasInfoDensity = !!this.currentState.infoDensityData;
|
| 335 |
const displayResult = this.computeDisplayResult();
|
|
|
|
|
|
|
| 336 |
|
| 337 |
const tokenHistogramItem = document.getElementById('token_histogram_item');
|
| 338 |
const surprisalProgressItem = document.getElementById('surprisal_progress_item');
|
| 339 |
const rawScoreNormedItem = document.getElementById('raw_score_normed_histogram_item');
|
| 340 |
|
| 341 |
-
if (
|
| 342 |
const currentSurprisals = this.currentState.currentSurprisals;
|
| 343 |
const currentTokenAvg = this.currentState.currentTokenAvg;
|
| 344 |
const currentTokenP90 = this.currentState.currentTokenP90;
|
|
@@ -375,7 +384,6 @@ export class VisualizationUpdater {
|
|
| 375 |
|
| 376 |
const rawScoresNormed = displayResult?.rawScoresNormed;
|
| 377 |
const validRawScoresNormed = rawScoresNormed?.filter((s) => typeof s === 'number' && isFinite(s));
|
| 378 |
-
const sem = this.currentState.semanticData;
|
| 379 |
const signalFitResult = sem?.signalFitResult ?? null;
|
| 380 |
const chunkInfos = sem?.chunkInfos;
|
| 381 |
const isChunkMode = (chunkInfos?.length ?? 0) > 0;
|
|
@@ -614,7 +622,6 @@ export class VisualizationUpdater {
|
|
| 614 |
this.deps.highlightController.updateCurrentData(null);
|
| 615 |
d3.select('#all_result').style('opacity', 0);
|
| 616 |
this.updateSemanticDebugInfo();
|
| 617 |
-
this.syncDigitsMergeUi();
|
| 618 |
}
|
| 619 |
|
| 620 |
/**
|
|
@@ -627,24 +634,10 @@ export class VisualizationUpdater {
|
|
| 627 |
const matchScoreProgressItem = document.getElementById('match_score_progress_item');
|
| 628 |
if (matchScoreProgressItem) matchScoreProgressItem.style.display = 'none';
|
| 629 |
this.updateSemanticDebugInfo();
|
| 630 |
-
this.syncDigitsMergeUi();
|
| 631 |
}
|
| 632 |
|
| 633 |
/**
|
| 634 |
-
*
|
| 635 |
-
*/
|
| 636 |
-
private syncDigitsMergeUi(): void {
|
| 637 |
-
const el = document.getElementById('enable_digits_merge_toggle') as HTMLInputElement | null;
|
| 638 |
-
if (!el) return;
|
| 639 |
-
const disabled = !!this.currentState.semanticData?.chunkInfos?.length;
|
| 640 |
-
el.disabled = disabled;
|
| 641 |
-
el.title = disabled
|
| 642 |
-
? 'Chunked semantic analysis locks digit merge; clear semantic data or use non-chunked mode to toggle.'
|
| 643 |
-
: '';
|
| 644 |
-
}
|
| 645 |
-
|
| 646 |
-
/**
|
| 647 |
-
* digit merge 开关变化时:从 originalTokens / API attention 重算合并并刷新文本与图表
|
| 648 |
*/
|
| 649 |
public applyDigitsMergeSetting(): void {
|
| 650 |
const digitMerge = getDigitsMergeEnabled();
|
|
@@ -678,7 +671,6 @@ export class VisualizationUpdater {
|
|
| 678 |
this.currentState.currentTokenAvg = computeAverage(mergedSurprisals);
|
| 679 |
this.currentState.currentTokenP90 = computeP90(mergedSurprisals);
|
| 680 |
}
|
| 681 |
-
this.syncDigitsMergeUi();
|
| 682 |
let displayResult: ReturnType<VisualizationUpdater['computeDisplayResult']>;
|
| 683 |
try {
|
| 684 |
displayResult = this.computeDisplayResult();
|
|
@@ -707,7 +699,7 @@ export class VisualizationUpdater {
|
|
| 707 |
if (el) el.style.display = enabled ? '' : 'none';
|
| 708 |
this.deps.lmf.updateOptions({ semanticAnalysisMode: enabled }, false);
|
| 709 |
if (!enabled) {
|
| 710 |
-
// 关闭时清除语义数据
|
| 711 |
this.currentState.semanticData = null;
|
| 712 |
const rawScoreNormedItem = document.getElementById('raw_score_normed_histogram_item');
|
| 713 |
if (rawScoreNormedItem) rawScoreNormedItem.style.display = 'none';
|
|
@@ -720,10 +712,9 @@ export class VisualizationUpdater {
|
|
| 720 |
d3.select('#all_result').style('opacity', 0);
|
| 721 |
this.deps.appStateManager.updateState({ hasValidData: false });
|
| 722 |
}
|
| 723 |
-
// 关闭语义模式后立刻按当前数据重绘,确保语义着色和相关图表不残留
|
| 724 |
-
this.updateVisualizationInternal(false);
|
| 725 |
}
|
| 726 |
-
|
|
|
|
| 727 |
// 语义分析配置影响 Upload/Save 的 dataReadyForSave 条件,需始终更新按钮状态
|
| 728 |
this.deps.appStateManager.updateButtonStates();
|
| 729 |
}
|
|
@@ -881,7 +872,6 @@ export class VisualizationUpdater {
|
|
| 881 |
this.deps.appStateManager.updateState({ hasValidData: true });
|
| 882 |
|
| 883 |
this.syncSemanticUiFromConfig();
|
| 884 |
-
this.syncDigitsMergeUi();
|
| 885 |
}
|
| 886 |
|
| 887 |
/**
|
|
@@ -986,7 +976,6 @@ export class VisualizationUpdater {
|
|
| 986 |
this.updateVisualizationInternal();
|
| 987 |
|
| 988 |
this.updateSemanticDebugInfo(res.debug_info);
|
| 989 |
-
this.syncDigitsMergeUi();
|
| 990 |
return true;
|
| 991 |
}
|
| 992 |
|
|
|
|
| 272 |
|
| 273 |
const infoText = this.currentState.infoDensityData?.request?.text ?? '';
|
| 274 |
const semText = this.currentState.semanticData?.text ?? '';
|
| 275 |
+
const semanticQueryOn = getSemanticAnalysisEnabled();
|
| 276 |
|
| 277 |
let showInfoDensity = false;
|
| 278 |
let showSemantic = false;
|
| 279 |
|
| 280 |
if (mode === 'infoDensity') {
|
| 281 |
+
/** Semantic Query 勾选时统计区不出现信息密度图占位 */
|
| 282 |
+
showInfoDensity = !semanticQueryOn;
|
| 283 |
+
showSemantic =
|
| 284 |
+
semanticQueryOn &&
|
| 285 |
+
hasSemanticData(this.currentState.semanticData) &&
|
| 286 |
+
semText === text;
|
| 287 |
} else {
|
| 288 |
showSemantic = true;
|
| 289 |
+
showInfoDensity =
|
| 290 |
+
!semanticQueryOn &&
|
| 291 |
+
!!(this.currentState.infoDensityData && infoText === text);
|
| 292 |
}
|
| 293 |
|
| 294 |
if (tokenHistogramItem) tokenHistogramItem.style.display = showInfoDensity ? '' : 'none';
|
|
|
|
| 334 |
|
| 335 |
/**
|
| 336 |
* 重新渲染直方图(内部方法)
|
| 337 |
+
* Semantic Query 勾选:仅语义相关图;未勾选:有信息密度数据时显示 token + surprisal
|
| 338 |
* @param skipLmfUpdate 为 true 时跳过 lmf.update(主题切换时由 rerenderOnThemeChange 统一重绘,避免竞态)
|
| 339 |
*/
|
| 340 |
private updateVisualizationInternal(skipLmfUpdate = false): void {
|
| 341 |
const hasInfoDensity = !!this.currentState.infoDensityData;
|
| 342 |
const displayResult = this.computeDisplayResult();
|
| 343 |
+
const sem = this.currentState.semanticData;
|
| 344 |
+
const showInfoDensityCharts = hasInfoDensity && !getSemanticAnalysisEnabled();
|
| 345 |
|
| 346 |
const tokenHistogramItem = document.getElementById('token_histogram_item');
|
| 347 |
const surprisalProgressItem = document.getElementById('surprisal_progress_item');
|
| 348 |
const rawScoreNormedItem = document.getElementById('raw_score_normed_histogram_item');
|
| 349 |
|
| 350 |
+
if (showInfoDensityCharts) {
|
| 351 |
const currentSurprisals = this.currentState.currentSurprisals;
|
| 352 |
const currentTokenAvg = this.currentState.currentTokenAvg;
|
| 353 |
const currentTokenP90 = this.currentState.currentTokenP90;
|
|
|
|
| 384 |
|
| 385 |
const rawScoresNormed = displayResult?.rawScoresNormed;
|
| 386 |
const validRawScoresNormed = rawScoresNormed?.filter((s) => typeof s === 'number' && isFinite(s));
|
|
|
|
| 387 |
const signalFitResult = sem?.signalFitResult ?? null;
|
| 388 |
const chunkInfos = sem?.chunkInfos;
|
| 389 |
const isChunkMode = (chunkInfos?.length ?? 0) > 0;
|
|
|
|
| 622 |
this.deps.highlightController.updateCurrentData(null);
|
| 623 |
d3.select('#all_result').style('opacity', 0);
|
| 624 |
this.updateSemanticDebugInfo();
|
|
|
|
| 625 |
}
|
| 626 |
|
| 627 |
/**
|
|
|
|
| 634 |
const matchScoreProgressItem = document.getElementById('match_score_progress_item');
|
| 635 |
if (matchScoreProgressItem) matchScoreProgressItem.style.display = 'none';
|
| 636 |
this.updateSemanticDebugInfo();
|
|
|
|
| 637 |
}
|
| 638 |
|
| 639 |
/**
|
| 640 |
+
* digit merge 用户偏好变化时:对信息密度与整段语义从可重算数据源刷新;分块语义无副本则保持当前展示不变
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
*/
|
| 642 |
public applyDigitsMergeSetting(): void {
|
| 643 |
const digitMerge = getDigitsMergeEnabled();
|
|
|
|
| 671 |
this.currentState.currentTokenAvg = computeAverage(mergedSurprisals);
|
| 672 |
this.currentState.currentTokenP90 = computeP90(mergedSurprisals);
|
| 673 |
}
|
|
|
|
| 674 |
let displayResult: ReturnType<VisualizationUpdater['computeDisplayResult']>;
|
| 675 |
try {
|
| 676 |
displayResult = this.computeDisplayResult();
|
|
|
|
| 699 |
if (el) el.style.display = enabled ? '' : 'none';
|
| 700 |
this.deps.lmf.updateOptions({ semanticAnalysisMode: enabled }, false);
|
| 701 |
if (!enabled) {
|
| 702 |
+
// 关闭时清除语义数据;统计图由下方 updateVisualizationInternal 统一刷新
|
| 703 |
this.currentState.semanticData = null;
|
| 704 |
const rawScoreNormedItem = document.getElementById('raw_score_normed_histogram_item');
|
| 705 |
if (rawScoreNormedItem) rawScoreNormedItem.style.display = 'none';
|
|
|
|
| 712 |
d3.select('#all_result').style('opacity', 0);
|
| 713 |
this.deps.appStateManager.updateState({ hasValidData: false });
|
| 714 |
}
|
|
|
|
|
|
|
| 715 |
}
|
| 716 |
+
/** 勾选 / 关闭 Semantic Query 后立即刷新统计图显隐(与 getSemanticAnalysisEnabled 一致) */
|
| 717 |
+
this.updateVisualizationInternal(false);
|
| 718 |
// 语义分析配置影响 Upload/Save 的 dataReadyForSave 条件,需始终更新按钮状态
|
| 719 |
this.deps.appStateManager.updateButtonStates();
|
| 720 |
}
|
|
|
|
| 872 |
this.deps.appStateManager.updateState({ hasValidData: true });
|
| 873 |
|
| 874 |
this.syncSemanticUiFromConfig();
|
|
|
|
| 875 |
}
|
| 876 |
|
| 877 |
/**
|
|
|
|
| 976 |
this.updateVisualizationInternal();
|
| 977 |
|
| 978 |
this.updateSemanticDebugInfo(res.debug_info);
|
|
|
|
| 979 |
return true;
|
| 980 |
}
|
| 981 |
|
client/src/ts/vis/ToolTip.ts
CHANGED
|
@@ -13,9 +13,30 @@ import {
|
|
| 13 |
|
| 14 |
const SEPARATOR = '─────────────';
|
| 15 |
|
|
|
|
|
|
|
|
|
|
| 16 |
export type ToolTipOptions = {
|
| 17 |
/** 真实 top-k 下 surprisal 行的标签(默认「信息量」) */
|
| 18 |
surprisalRowLabel?: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
};
|
| 20 |
|
| 21 |
type DetailField = { label: string; value: string; valueColor?: boolean };
|
|
@@ -43,15 +64,30 @@ export class ToolTip {
|
|
| 43 |
|
| 44 |
// 防抖:pending 的更新任务
|
| 45 |
private pendingUpdate: number | null = null;
|
| 46 |
-
private pendingData: {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
// 主题监听器
|
| 49 |
private themeObserver: MutationObserver | null = null;
|
| 50 |
|
| 51 |
private readonly surprisalRowLabel: string;
|
|
|
|
|
|
|
| 52 |
|
| 53 |
constructor(private parent: D3Sel, private eh: SimpleEventHandler, options?: ToolTipOptions) {
|
| 54 |
this.surprisalRowLabel = options?.surprisalRowLabel ?? tr('information:');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
this._init();
|
| 56 |
this._setupThemeObserver();
|
| 57 |
this._updateThemeColors();
|
|
@@ -62,20 +98,19 @@ export class ToolTip {
|
|
| 62 |
this.predictions = this.parent.select('.predictions');
|
| 63 |
this.myDetail = this.parent.select('.myDetail');
|
| 64 |
this.currentToken = this.parent.select('.currentToken');
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
});
|
| 79 |
}
|
| 80 |
|
| 81 |
/**
|
|
@@ -140,59 +175,23 @@ export class ToolTip {
|
|
| 140 |
}
|
| 141 |
|
| 142 |
/**
|
| 143 |
-
* 从
|
| 144 |
-
* 解决事件绑定在group上时,target可能是group而不是rect的问题
|
| 145 |
-
* @param target 事件目标元素
|
| 146 |
-
* @param mouseX 鼠标X坐标
|
| 147 |
-
* @param mouseY 鼠标Y坐标
|
| 148 |
-
* @returns 找到的SVG rect元素,如果没找到则返回null
|
| 149 |
*/
|
| 150 |
-
private
|
| 151 |
-
if (!target) return null;
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
// 如果target本身就是rect,直接返回
|
| 156 |
-
if (element instanceof SVGRectElement) {
|
| 157 |
-
return element;
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
// 如果target是group,查找包含鼠标位置的rect
|
| 161 |
if (element instanceof SVGGElement) {
|
| 162 |
-
|
| 163 |
-
// 优先查找包含鼠标位置的rect
|
| 164 |
-
for (const rect of rects) {
|
| 165 |
-
const rectBounds = rect.getBoundingClientRect();
|
| 166 |
-
if (mouseX >= rectBounds.left && mouseX <= rectBounds.right &&
|
| 167 |
-
mouseY >= rectBounds.top && mouseY <= rectBounds.bottom) {
|
| 168 |
-
return rect;
|
| 169 |
-
}
|
| 170 |
-
}
|
| 171 |
-
// 如果没找到包含鼠标的rect,返回第一个rect(fallback)
|
| 172 |
-
return rects[0] || null;
|
| 173 |
}
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
return parent;
|
| 180 |
}
|
| 181 |
-
|
| 182 |
-
// 在group中查找包含鼠标位置的rect
|
| 183 |
-
const rects = parent.querySelectorAll('rect');
|
| 184 |
-
for (const rect of rects) {
|
| 185 |
-
const rectBounds = rect.getBoundingClientRect();
|
| 186 |
-
if (mouseX >= rectBounds.left && mouseX <= rectBounds.right &&
|
| 187 |
-
mouseY >= rectBounds.top && mouseY <= rectBounds.bottom) {
|
| 188 |
-
return rect;
|
| 189 |
-
}
|
| 190 |
-
}
|
| 191 |
-
return rects[0] || null;
|
| 192 |
-
}
|
| 193 |
-
parent = parent.parentElement;
|
| 194 |
}
|
| 195 |
-
|
| 196 |
return null;
|
| 197 |
}
|
| 198 |
|
|
@@ -222,37 +221,71 @@ export class ToolTip {
|
|
| 222 |
this.pendingData = null;
|
| 223 |
this.visibility = false;
|
| 224 |
if (node) {
|
| 225 |
-
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
}
|
| 228 |
}
|
| 229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
set visibility(vis: boolean) {
|
|
|
|
| 231 |
if (vis == true) {
|
| 232 |
-
|
| 233 |
-
|
|
|
|
| 234 |
} else {
|
|
|
|
| 235 |
this.parent.style('opacity', 0);
|
| 236 |
this.parent.style('pointer-events', 'none'); // 关闭时禁止点击,让事件穿透
|
| 237 |
}
|
| 238 |
}
|
| 239 |
|
| 240 |
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
// 防抖:取消之前的更新任务
|
| 243 |
if (this.pendingUpdate !== null) {
|
| 244 |
cancelAnimationFrame(this.pendingUpdate);
|
| 245 |
}
|
| 246 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
// 保存最新的数据
|
| 248 |
-
this.pendingData = { ri,
|
| 249 |
|
| 250 |
// 先将 tooltip 移到屏幕外,避免在位置计算完成前显示在旧位置
|
| 251 |
// 这可以解决 iOS Safari 上触摸时的抖动问题:
|
| 252 |
// 如果旧位置在触摸点下方,会触发 tooltip 的 touchstart 导致关闭
|
| 253 |
const node = this.parent.node() as HTMLElement;
|
| 254 |
if (node) {
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
}
|
| 257 |
this.visibility = true;
|
| 258 |
|
|
@@ -261,14 +294,14 @@ export class ToolTip {
|
|
| 261 |
this.pendingUpdate = null;
|
| 262 |
if (!this.pendingData) return;
|
| 263 |
|
| 264 |
-
const { ri: currentRi,
|
| 265 |
this.pendingData = null;
|
| 266 |
|
| 267 |
// 更新内容
|
| 268 |
-
this._updateContent(currentRi);
|
| 269 |
|
| 270 |
// 立即计算位置(DOM已更新,getBoundingClientRect 能获取准确值)
|
| 271 |
-
this._updatePosition(
|
| 272 |
});
|
| 273 |
}
|
| 274 |
|
|
@@ -276,7 +309,7 @@ export class ToolTip {
|
|
| 276 |
* 更新tooltip内容
|
| 277 |
* 统一结构:语义区块(上) + 分隔线 + 信息密度区块(下,含汇总指标 + top-k 表格)
|
| 278 |
*/
|
| 279 |
-
private _updateContent(ri: GLTR_RenderItem): void {
|
| 280 |
const { selectedColor, detailColor, valueColor } = this.themeColors;
|
| 281 |
|
| 282 |
// 更新当前token显示(第一行)
|
|
@@ -296,40 +329,47 @@ export class ToolTip {
|
|
| 296 |
(s.chunkIndex !== undefined && s.chunkMatchDegree !== undefined));
|
| 297 |
const { hasRealTopk } = getFrontendTokenTopkState(tokenData);
|
| 298 |
|
| 299 |
-
// 1. 构建
|
| 300 |
-
|
|
|
|
| 301 |
if (hasSemantic && s) {
|
| 302 |
-
if (s.pwScore !== undefined)
|
| 303 |
-
if (s.signalProb !== undefined)
|
| 304 |
-
if (s.rawScoreNormed !== undefined)
|
| 305 |
-
if (s.rawScore !== undefined)
|
| 306 |
if (s.chunkIndex !== undefined && s.chunkMatchDegree !== undefined) {
|
| 307 |
-
|
| 308 |
label: `chunk #${s.chunkIndex} match score:`,
|
| 309 |
value: (s.chunkMatchDegree * 100).toFixed(1) + '%'
|
| 310 |
}, detailColor, valueColor));
|
| 311 |
}
|
| 312 |
}
|
|
|
|
|
|
|
|
|
|
| 313 |
|
| 314 |
-
// 2. 构建信息密度区块
|
| 315 |
const infoRows: string[] = [];
|
| 316 |
if (hasRealTopk) {
|
| 317 |
const prob = tokenData.real_topk![1];
|
| 318 |
const surprisal = calculateSurprisal(prob);
|
| 319 |
const isClassic = getTokenRenderStyle() === 'classic';
|
| 320 |
-
infoRows.push(renderField({ label: this.surprisalRowLabel, value: `${this.significantF(surprisal)} bits` }, detailColor, valueColor));
|
| 321 |
if (!isClassic) {
|
| 322 |
const informationDensity = calculateSurprisalDensity(tokenData);
|
| 323 |
const utf8Size = new TextEncoder().encode(tokenData.raw).length;
|
| 324 |
-
infoRows.
|
| 325 |
-
infoRows.
|
| 326 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
}
|
| 328 |
|
| 329 |
-
// 3. 合并 myDetail:
|
| 330 |
const detailParts: string[] = [];
|
| 331 |
-
if (
|
| 332 |
-
if (
|
| 333 |
if (infoRows.length) detailParts.push(infoRows.join('<br/>'));
|
| 334 |
this.myDetail.html(detailParts.join('<br/>'));
|
| 335 |
|
|
@@ -343,12 +383,17 @@ export class ToolTip {
|
|
| 343 |
}
|
| 344 |
|
| 345 |
/**
|
| 346 |
-
* 更新tooltip位置
|
| 347 |
*/
|
| 348 |
-
private _updatePosition(
|
| 349 |
const tooltipNode = this.parent.node() as HTMLElement;
|
| 350 |
if (!tooltipNode) return;
|
| 351 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
// 获取视口信息(用于边界检查)
|
| 353 |
const viewport = this._getViewportInfo();
|
| 354 |
|
|
@@ -374,15 +419,10 @@ export class ToolTip {
|
|
| 374 |
anchorRect = anchor.getBoundingClientRect();
|
| 375 |
}
|
| 376 |
|
| 377 |
-
|
| 378 |
-
throw new Error('[ToolTip] 更新位置需要 pointer 事件(缺少 MouseEvent)');
|
| 379 |
-
}
|
| 380 |
-
const mouseX = event.clientX;
|
| 381 |
-
const mouseY = event.clientY;
|
| 382 |
-
const tokenRectElement = this._findTokenRect(event.target, mouseX, mouseY);
|
| 383 |
if (!tokenRectElement) {
|
| 384 |
throw new Error(
|
| 385 |
-
'[ToolTip] 无法从
|
| 386 |
);
|
| 387 |
}
|
| 388 |
|
|
|
|
| 13 |
|
| 14 |
const SEPARATOR = '─────────────';
|
| 15 |
|
| 16 |
+
/** 贴在定位包含块角落时的留白(px);{@link ToolTipOptions.placement} `parent-bottom-right` 使用 */
|
| 17 |
+
const CORNER_INSET_PX = 0;
|
| 18 |
+
|
| 19 |
export type ToolTipOptions = {
|
| 20 |
/** 真实 top-k 下 surprisal 行的标签(默认「信息量」) */
|
| 21 |
surprisalRowLabel?: string;
|
| 22 |
+
/**
|
| 23 |
+
* `parent-bottom-right`:`position:absolute`,贴在定位包含块(`offsetParent`)右下角(DAG Top‑K 作为 `#results` 直接子节点时即为 results 内侧右下)。
|
| 24 |
+
* 该 HUD 模式不依赖锚点几何;面板应由页面 CSS(如 `.gen-attr-dag-topk-tooltip`)约束宽高与内部滚动,而非按内容 shrink-wrap。
|
| 25 |
+
* 默认 `anchor`:沿用原有相对 token rect 的定位。
|
| 26 |
+
*/
|
| 27 |
+
placement?: 'anchor' | 'parent-bottom-right';
|
| 28 |
+
/**
|
| 29 |
+
* false:面板不参与命中测试(`pointer-events: none`),避免盖住底层 SVG 时在节点上反复 `mouseleave`/闪动;
|
| 30 |
+
* 同时不注册点击/触摸收起,且不接收内部滚动交互。
|
| 31 |
+
*/
|
| 32 |
+
pointerInteractive?: boolean;
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
/** {@link ToolTip.updateData} 可选增补(如 DAG:CI/MI 行紧跟 surprisal 之后) */
|
| 36 |
+
export type ToolTipUpdateAugment = {
|
| 37 |
+
/** 在 surprisal / 信息密度行之前渲染(紧跟 token 文字,位于所有 info 行上方) */
|
| 38 |
+
rowsBeforeInfo?: Array<{ label: string; value: string; valueColor?: boolean }>;
|
| 39 |
+
rowsAfterSurprisal?: Array<{ label: string; value: string; valueColor?: boolean }>;
|
| 40 |
};
|
| 41 |
|
| 42 |
type DetailField = { label: string; value: string; valueColor?: boolean };
|
|
|
|
| 64 |
|
| 65 |
// 防抖:pending 的更新任务
|
| 66 |
private pendingUpdate: number | null = null;
|
| 67 |
+
private pendingData: {
|
| 68 |
+
ri: GLTR_RenderItem;
|
| 69 |
+
anchorTarget: EventTarget | null;
|
| 70 |
+
augment?: ToolTipUpdateAugment;
|
| 71 |
+
} | null = null;
|
| 72 |
|
| 73 |
// 主题监听器
|
| 74 |
private themeObserver: MutationObserver | null = null;
|
| 75 |
|
| 76 |
private readonly surprisalRowLabel: string;
|
| 77 |
+
private readonly placement: NonNullable<ToolTipOptions['placement']>;
|
| 78 |
+
private readonly pointerInteractive: boolean;
|
| 79 |
|
| 80 |
constructor(private parent: D3Sel, private eh: SimpleEventHandler, options?: ToolTipOptions) {
|
| 81 |
this.surprisalRowLabel = options?.surprisalRowLabel ?? tr('information:');
|
| 82 |
+
this.placement = options?.placement ?? 'anchor';
|
| 83 |
+
this.pointerInteractive = options?.pointerInteractive ?? true;
|
| 84 |
+
if (!this.pointerInteractive) {
|
| 85 |
+
this.parent.classed('tooltip-no-pointer-hit', true);
|
| 86 |
+
}
|
| 87 |
+
const el = this.parent.node() as HTMLElement | null;
|
| 88 |
+
if (el && this.placement === 'parent-bottom-right') {
|
| 89 |
+
el.style.position = 'absolute';
|
| 90 |
+
}
|
| 91 |
this._init();
|
| 92 |
this._setupThemeObserver();
|
| 93 |
this._updateThemeColors();
|
|
|
|
| 98 |
this.predictions = this.parent.select('.predictions');
|
| 99 |
this.myDetail = this.parent.select('.myDetail');
|
| 100 |
this.currentToken = this.parent.select('.currentToken');
|
| 101 |
+
|
| 102 |
+
if (this.pointerInteractive) {
|
| 103 |
+
this.parent.on('click', (event) => {
|
| 104 |
+
event.stopPropagation();
|
| 105 |
+
event.preventDefault();
|
| 106 |
+
this.visibility = false;
|
| 107 |
+
});
|
| 108 |
+
this.parent.on('touchstart', (event) => {
|
| 109 |
+
event.stopPropagation();
|
| 110 |
+
event.preventDefault();
|
| 111 |
+
this.visibility = false;
|
| 112 |
+
});
|
| 113 |
+
}
|
|
|
|
| 114 |
}
|
| 115 |
|
| 116 |
/**
|
|
|
|
| 175 |
}
|
| 176 |
|
| 177 |
/**
|
| 178 |
+
* 从事件目标解析用于定位的 SVG rect(与指针坐标无关):rect 自身、或容器 `g` 内首个 `rect`、或向上追溯。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
*/
|
| 180 |
+
private _resolveAnchorRectElement(target: EventTarget | null): SVGRectElement | null {
|
| 181 |
+
if (!target || !(target instanceof Element)) return null;
|
| 182 |
+
let element: Element | null = target;
|
| 183 |
+
if (element instanceof SVGRectElement) return element;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
if (element instanceof SVGGElement) {
|
| 185 |
+
return element.querySelector('rect');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
}
|
| 187 |
+
while (element) {
|
| 188 |
+
if (element instanceof SVGRectElement) return element;
|
| 189 |
+
if (element instanceof SVGGElement) {
|
| 190 |
+
const r = element.querySelector('rect');
|
| 191 |
+
if (r) return r;
|
|
|
|
| 192 |
}
|
| 193 |
+
element = element.parentElement;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
}
|
|
|
|
| 195 |
return null;
|
| 196 |
}
|
| 197 |
|
|
|
|
| 221 |
this.pendingData = null;
|
| 222 |
this.visibility = false;
|
| 223 |
if (node) {
|
| 224 |
+
if (this.placement === 'parent-bottom-right') {
|
| 225 |
+
this._placeParentBottomRight(node);
|
| 226 |
+
} else {
|
| 227 |
+
node.style.top = '0px';
|
| 228 |
+
node.style.left = '0px';
|
| 229 |
+
}
|
| 230 |
}
|
| 231 |
}
|
| 232 |
|
| 233 |
+
/** 固定在 offsetParent(含隐藏态占位)右下角 */
|
| 234 |
+
private _placeParentBottomRight(node: HTMLElement): void {
|
| 235 |
+
node.style.position = 'absolute';
|
| 236 |
+
node.style.right = `${CORNER_INSET_PX}px`;
|
| 237 |
+
node.style.left = 'auto';
|
| 238 |
+
node.style.top = 'auto';
|
| 239 |
+
node.style.bottom = `${CORNER_INSET_PX}px`;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
set visibility(vis: boolean) {
|
| 243 |
+
const node = this.parent.node() as HTMLElement | null;
|
| 244 |
if (vis == true) {
|
| 245 |
+
node?.classList.add('tooltip-visible');
|
| 246 |
+
node?.style.removeProperty('opacity');
|
| 247 |
+
this.parent.style('pointer-events', this.pointerInteractive ? 'auto' : 'none');
|
| 248 |
} else {
|
| 249 |
+
node?.classList.remove('tooltip-visible');
|
| 250 |
this.parent.style('opacity', 0);
|
| 251 |
this.parent.style('pointer-events', 'none'); // 关闭时禁止点击,让事件穿透
|
| 252 |
}
|
| 253 |
}
|
| 254 |
|
| 255 |
|
| 256 |
+
/**
|
| 257 |
+
* @param eventOrAnchor 指针事件(使用 `target`)或直接传入用作锚点的元素(如 SVG `rect` / `g`)
|
| 258 |
+
*/
|
| 259 |
+
updateData(
|
| 260 |
+
ri: GLTR_RenderItem,
|
| 261 |
+
eventOrAnchor?: MouseEvent | TouchEvent | Element | null,
|
| 262 |
+
augment?: ToolTipUpdateAugment
|
| 263 |
+
) {
|
| 264 |
// 防抖:取消之前的更新任务
|
| 265 |
if (this.pendingUpdate !== null) {
|
| 266 |
cancelAnimationFrame(this.pendingUpdate);
|
| 267 |
}
|
| 268 |
|
| 269 |
+
const anchorTarget =
|
| 270 |
+
eventOrAnchor instanceof Element
|
| 271 |
+
? eventOrAnchor
|
| 272 |
+
: eventOrAnchor && 'target' in eventOrAnchor
|
| 273 |
+
? (eventOrAnchor.target as EventTarget | null)
|
| 274 |
+
: null;
|
| 275 |
+
|
| 276 |
// 保存最新的数据
|
| 277 |
+
this.pendingData = { ri, anchorTarget, augment };
|
| 278 |
|
| 279 |
// 先将 tooltip 移到屏幕外,避免在位置计算完成前显示在旧位置
|
| 280 |
// 这可以解决 iOS Safari 上触摸时的抖动问题:
|
| 281 |
// 如果旧位置在触摸点下方,会触发 tooltip 的 touchstart 导致关闭
|
| 282 |
const node = this.parent.node() as HTMLElement;
|
| 283 |
if (node) {
|
| 284 |
+
if (this.placement === 'parent-bottom-right') {
|
| 285 |
+
this._placeParentBottomRight(node);
|
| 286 |
+
} else {
|
| 287 |
+
node.style.left = '-9999px';
|
| 288 |
+
}
|
| 289 |
}
|
| 290 |
this.visibility = true;
|
| 291 |
|
|
|
|
| 294 |
this.pendingUpdate = null;
|
| 295 |
if (!this.pendingData) return;
|
| 296 |
|
| 297 |
+
const { ri: currentRi, anchorTarget: at, augment } = this.pendingData;
|
| 298 |
this.pendingData = null;
|
| 299 |
|
| 300 |
// 更新内容
|
| 301 |
+
this._updateContent(currentRi, augment);
|
| 302 |
|
| 303 |
// 立即计算位置(DOM已更新,getBoundingClientRect 能获取准确值)
|
| 304 |
+
this._updatePosition(at);
|
| 305 |
});
|
| 306 |
}
|
| 307 |
|
|
|
|
| 309 |
* 更新tooltip内容
|
| 310 |
* 统一结构:语义区块(上) + 分隔线 + 信息密度区块(下,含汇总指标 + top-k 表格)
|
| 311 |
*/
|
| 312 |
+
private _updateContent(ri: GLTR_RenderItem, augment?: ToolTipUpdateAugment): void {
|
| 313 |
const { selectedColor, detailColor, valueColor } = this.themeColors;
|
| 314 |
|
| 315 |
// 更新当前token显示(第一行)
|
|
|
|
| 329 |
(s.chunkIndex !== undefined && s.chunkMatchDegree !== undefined));
|
| 330 |
const { hasRealTopk } = getFrontendTokenTopkState(tokenData);
|
| 331 |
|
| 332 |
+
// 1. 构建上区块:语义行 + rowsBeforeInfo(DAG 归因份额等附加行)
|
| 333 |
+
// 二者在视觉上同属"token 语义信息",与下方信息密度区块以分隔线隔开
|
| 334 |
+
const topRows: string[] = [];
|
| 335 |
if (hasSemantic && s) {
|
| 336 |
+
if (s.pwScore !== undefined) topRows.push(renderField({ label: tr('pw score:'), value: this.numF(s.pwScore) }, detailColor, valueColor));
|
| 337 |
+
if (s.signalProb !== undefined) topRows.push(renderField({ label: tr('signal probability:'), value: this.numF(s.signalProb) }, detailColor, valueColor));
|
| 338 |
+
if (s.rawScoreNormed !== undefined) topRows.push(renderField({ label: tr('raw score normed:'), value: this.numF(s.rawScoreNormed) }, detailColor, valueColor));
|
| 339 |
+
if (s.rawScore !== undefined) topRows.push(renderField({ label: tr('raw score:'), value: d3.format('.6f')(s.rawScore), valueColor: false }, detailColor, valueColor));
|
| 340 |
if (s.chunkIndex !== undefined && s.chunkMatchDegree !== undefined) {
|
| 341 |
+
topRows.push(renderField({
|
| 342 |
label: `chunk #${s.chunkIndex} match score:`,
|
| 343 |
value: (s.chunkMatchDegree * 100).toFixed(1) + '%'
|
| 344 |
}, detailColor, valueColor));
|
| 345 |
}
|
| 346 |
}
|
| 347 |
+
for (const f of augment?.rowsBeforeInfo ?? []) {
|
| 348 |
+
topRows.push(renderField(f, detailColor, valueColor));
|
| 349 |
+
}
|
| 350 |
|
| 351 |
+
// 2. 构建信息密度区块:按明确顺序追加,避免脆弱的 unshift/splice
|
| 352 |
const infoRows: string[] = [];
|
| 353 |
if (hasRealTopk) {
|
| 354 |
const prob = tokenData.real_topk![1];
|
| 355 |
const surprisal = calculateSurprisal(prob);
|
| 356 |
const isClassic = getTokenRenderStyle() === 'classic';
|
|
|
|
| 357 |
if (!isClassic) {
|
| 358 |
const informationDensity = calculateSurprisalDensity(tokenData);
|
| 359 |
const utf8Size = new TextEncoder().encode(tokenData.raw).length;
|
| 360 |
+
infoRows.push(renderField({ label: tr('information density:'), value: `${this.significantF(informationDensity)} ${tr('bits/Byte')}` }, detailColor, valueColor));
|
| 361 |
+
infoRows.push(renderField({ label: tr('UTF-8 size:'), value: `${utf8Size} ${tr('bytes')}`, valueColor: false }, detailColor, valueColor));
|
| 362 |
}
|
| 363 |
+
infoRows.push(renderField({ label: this.surprisalRowLabel, value: `${this.significantF(surprisal)} bits` }, detailColor, valueColor));
|
| 364 |
+
}
|
| 365 |
+
for (const f of augment?.rowsAfterSurprisal ?? []) {
|
| 366 |
+
infoRows.push(renderField(f, detailColor, valueColor));
|
| 367 |
}
|
| 368 |
|
| 369 |
+
// 3. 合并 myDetail:上区块 + 分隔线(仅当两区块都有时) + 信息密度区块
|
| 370 |
const detailParts: string[] = [];
|
| 371 |
+
if (topRows.length) detailParts.push(topRows.join('<br/>'));
|
| 372 |
+
if (topRows.length && infoRows.length) detailParts.push(`<span style="color:${detailColor}">${SEPARATOR}</span>`);
|
| 373 |
if (infoRows.length) detailParts.push(infoRows.join('<br/>'));
|
| 374 |
this.myDetail.html(detailParts.join('<br/>'));
|
| 375 |
|
|
|
|
| 383 |
}
|
| 384 |
|
| 385 |
/**
|
| 386 |
+
* 更新tooltip位置(相对锚点元素几何,不依赖指针坐标)
|
| 387 |
*/
|
| 388 |
+
private _updatePosition(anchorTarget: EventTarget | null): void {
|
| 389 |
const tooltipNode = this.parent.node() as HTMLElement;
|
| 390 |
if (!tooltipNode) return;
|
| 391 |
|
| 392 |
+
if (this.placement === 'parent-bottom-right') {
|
| 393 |
+
this._placeParentBottomRight(tooltipNode);
|
| 394 |
+
return;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
// 获取视口信息(用于边界检查)
|
| 398 |
const viewport = this._getViewportInfo();
|
| 399 |
|
|
|
|
| 419 |
anchorRect = anchor.getBoundingClientRect();
|
| 420 |
}
|
| 421 |
|
| 422 |
+
const tokenRectElement = this._resolveAnchorRectElement(anchorTarget);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
if (!tokenRectElement) {
|
| 424 |
throw new Error(
|
| 425 |
+
'[ToolTip] 无法从锚点解析到 SVG rect,请传入 token rect、g 或其它含 rect 的祖先元素'
|
| 426 |
);
|
| 427 |
}
|
| 428 |
|
client/src/ts/vis/constants.ts
CHANGED
|
@@ -13,7 +13,7 @@ export const HIGHLIGHT_CONSTANTS = {
|
|
| 13 |
/** chunk 字符半开区间下划线(由 DOM Range 推算,不依赖 token rect) */
|
| 14 |
INTERVAL_UNDERLINE_CLASS: 'chunk-interval-underline',
|
| 15 |
/** 高亮边框颜色(CSS变量) */
|
| 16 |
-
HIGHLIGHT_COLOR: 'var(--
|
| 17 |
/** 边框宽度 */
|
| 18 |
BORDER_WIDTH: '1.5',
|
| 19 |
/** 下划线宽度 */
|
|
|
|
| 13 |
/** chunk 字符半开区间下划线(由 DOM Range 推算,不依赖 token rect) */
|
| 14 |
INTERVAL_UNDERLINE_CLASS: 'chunk-interval-underline',
|
| 15 |
/** 高亮边框颜色(CSS变量) */
|
| 16 |
+
HIGHLIGHT_COLOR: 'var(--accent-color, #1e6fff)',
|
| 17 |
/** 边框宽度 */
|
| 18 |
BORDER_WIDTH: '1.5',
|
| 19 |
/** 下划线宽度 */
|