新增高惊讶度节点视觉放大选项;新增高惊讶度目标弱化入边选项;文案、UI改进
Browse files- backend/visit_stats.py +41 -4
- client/src/css/gen_attribute.scss +10 -1
- client/src/css/home.scss +43 -1
- client/src/gen_attribute.html +20 -0
- client/src/scripts/injectPageMetaIntoHtml.js +5 -0
- client/src/ts/attribution/genAttributeDagLinkSegment.ts +4 -4
- client/src/ts/attribution/genAttributeDagTextMeasure.ts +5 -5
- client/src/ts/attribution/genAttributeDagView.ts +82 -45
- client/src/ts/attribution/genAttributeDagViewLinearArcMode.ts +4 -3
- client/src/ts/attribution/genAttributeDagViewSpiralMode.ts +15 -12
- client/src/ts/attribution/genAttributeDagViewTextFlowMode.ts +3 -3
- client/src/ts/gen_attribute.ts +70 -25
- client/src/ts/home.ts +14 -0
- client/src/ts/lang/translations.ts +2 -0
- client/src/ts/utils/SurprisalColorConfig.ts +2 -1
- client/src/ts/utils/cachedHistoryUi.ts +4 -5
- client/src/ts/utils/settingsMenuManager.ts +15 -9
- client/src/ts/utils/surprisalMath.ts +51 -0
- data/demo/public/CN/政府工作报告极简版.json +0 -0
backend/visit_stats.py
CHANGED
|
@@ -16,6 +16,27 @@ _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 |
# RLock:_persist_tick 在已持锁时调用 _sample_locked_counters,同线程需可重入。
|
| 20 |
_LOCK = threading.RLock()
|
| 21 |
|
|
@@ -32,6 +53,8 @@ _HF_REPO = "dqy08/info-lens-stats"
|
|
| 32 |
_HF_TOKEN = os.environ.get("HF_TOKEN_stats_write")
|
| 33 |
_HF_TOTAL_FILE = "stats_total.json"
|
| 34 |
_HF_DELTA_DIR = "stats_delta"
|
|
|
|
|
|
|
| 35 |
def _stats_record(saved_at: str, body: dict) -> dict:
|
| 36 |
"""total / delta 磁盘与仓库共用:saved_at + 计数字段 + server_platform(若有)"""
|
| 37 |
return {"saved_at": saved_at, **body}
|
|
@@ -73,6 +96,13 @@ def _restart_log_repo_path() -> str:
|
|
| 73 |
return f"{_HF_DELTA_DIR}/{_delta_time_slug()}.restart.log"
|
| 74 |
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
def _download_stats_total() -> dict | None:
|
| 77 |
"""从 HF Dataset 读取 stats_total.json,失败返回 None。"""
|
| 78 |
if not _HF_TOKEN:
|
|
@@ -310,6 +340,13 @@ def _merge_from_sample(s: dict) -> tuple[dict, dict, dict]:
|
|
| 310 |
for k in set(bpo) | set(so)
|
| 311 |
}
|
| 312 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
tpl, tav = s["bp"] + s["sw_pl"], s["bav"] + s["sw_av"]
|
| 314 |
|
| 315 |
public = {
|
|
@@ -323,16 +360,16 @@ def _merge_from_sample(s: dict) -> tuple[dict, dict, dict]:
|
|
| 323 |
stats_body = {
|
| 324 |
"page_loads": tpl,
|
| 325 |
"active_visits": tav,
|
|
|
|
| 326 |
"page_sec": total_page_sec,
|
| 327 |
"api": total_api,
|
| 328 |
-
"os": total_os,
|
| 329 |
}
|
| 330 |
delta_body = {
|
| 331 |
"page_loads": s["sw_pl"],
|
| 332 |
"active_visits": s["sw_av"],
|
| 333 |
-
"
|
| 334 |
-
"
|
| 335 |
-
"
|
| 336 |
}
|
| 337 |
return public, stats_body, delta_body
|
| 338 |
|
|
|
|
| 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
|
| 20 |
+
_STATS_PAGE_ORDER = (
|
| 21 |
+
"index.html",
|
| 22 |
+
"analysis.html",
|
| 23 |
+
"compare.html",
|
| 24 |
+
"chat.html",
|
| 25 |
+
"attribution.html",
|
| 26 |
+
"gen_attribute.html",
|
| 27 |
+
)
|
| 28 |
+
_STATS_API_ORDER = (
|
| 29 |
+
"analyze",
|
| 30 |
+
"analyze_semantic",
|
| 31 |
+
"chat",
|
| 32 |
+
"causal_flow",
|
| 33 |
+
"prediction_attribute",
|
| 34 |
+
"prediction_attribute__attribution.html",
|
| 35 |
+
"prediction_attribute__chat.html",
|
| 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()
|
| 42 |
|
|
|
|
| 53 |
_HF_TOKEN = os.environ.get("HF_TOKEN_stats_write")
|
| 54 |
_HF_TOTAL_FILE = "stats_total.json"
|
| 55 |
_HF_DELTA_DIR = "stats_delta"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
def _stats_record(saved_at: str, body: dict) -> dict:
|
| 59 |
"""total / delta 磁盘与仓库共用:saved_at + 计数字段 + server_platform(若有)"""
|
| 60 |
return {"saved_at": saved_at, **body}
|
|
|
|
| 96 |
return f"{_HF_DELTA_DIR}/{_delta_time_slug()}.restart.log"
|
| 97 |
|
| 98 |
|
| 99 |
+
def _ordered_str_int_map(primary: tuple[str, ...], m: Mapping[str, object]) -> dict[str, int]:
|
| 100 |
+
primary_set = frozenset(primary)
|
| 101 |
+
head = [k for k in primary if k in m]
|
| 102 |
+
tail = sorted(k for k in m if k not in primary_set)
|
| 103 |
+
return {k: int(m[k]) for k in (*head, *tail)}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
def _download_stats_total() -> dict | None:
|
| 107 |
"""从 HF Dataset 读取 stats_total.json,失败返回 None。"""
|
| 108 |
if not _HF_TOKEN:
|
|
|
|
| 340 |
for k in set(bpo) | set(so)
|
| 341 |
}
|
| 342 |
|
| 343 |
+
total_page_sec = _ordered_str_int_map(_STATS_PAGE_ORDER, total_page_sec)
|
| 344 |
+
total_api = _ordered_str_int_map(_STATS_API_ORDER, total_api)
|
| 345 |
+
total_os = _ordered_str_int_map(_STATS_OS_ORDER, total_os)
|
| 346 |
+
ord_pg = _ordered_str_int_map(_STATS_PAGE_ORDER, sp)
|
| 347 |
+
ord_api = _ordered_str_int_map(_STATS_API_ORDER, sa)
|
| 348 |
+
ord_os = _ordered_str_int_map(_STATS_OS_ORDER, so)
|
| 349 |
+
|
| 350 |
tpl, tav = s["bp"] + s["sw_pl"], s["bav"] + s["sw_av"]
|
| 351 |
|
| 352 |
public = {
|
|
|
|
| 360 |
stats_body = {
|
| 361 |
"page_loads": tpl,
|
| 362 |
"active_visits": tav,
|
| 363 |
+
"os": total_os,
|
| 364 |
"page_sec": total_page_sec,
|
| 365 |
"api": total_api,
|
|
|
|
| 366 |
}
|
| 367 |
delta_body = {
|
| 368 |
"page_loads": s["sw_pl"],
|
| 369 |
"active_visits": s["sw_av"],
|
| 370 |
+
"os": ord_os,
|
| 371 |
+
"page_sec": ord_pg,
|
| 372 |
+
"api": ord_api,
|
| 373 |
}
|
| 374 |
return public, stats_body, delta_body
|
| 375 |
|
client/src/css/gen_attribute.scss
CHANGED
|
@@ -103,7 +103,8 @@
|
|
| 103 |
// 节点标签:与测量层同源 1em × display-scale × node-text-font-scale(后者略小于 1 替代固定 padding)
|
| 104 |
#results.gen-attr-results-surface.LMF .gen-attr-dag-svg .gen-attr-dag-node-text {
|
| 105 |
font-size: calc(
|
| 106 |
-
1em * var(--gen-attr-dag-display-scale, 1) *
|
|
|
|
| 107 |
);
|
| 108 |
font-family: inherit;
|
| 109 |
font-weight: inherit;
|
|
@@ -387,6 +388,14 @@ body.gen-attribute-page .input-section {
|
|
| 387 |
margin-top: 10px;
|
| 388 |
}
|
| 389 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
.attribution-exclude-prompt-patterns-header {
|
| 391 |
flex-wrap: wrap;
|
| 392 |
|
|
|
|
| 103 |
// 节点标签:与测量层同源 1em × display-scale × node-text-font-scale(后者略小于 1 替代固定 padding)
|
| 104 |
#results.gen-attr-results-surface.LMF .gen-attr-dag-svg .gen-attr-dag-node-text {
|
| 105 |
font-size: calc(
|
| 106 |
+
1em * var(--gen-attr-dag-display-scale, 1) *
|
| 107 |
+
var(--gen-attr-dag-node-text-font-scale, 0.9) * var(--gen-attr-dag-node-ci-visual-scale, 1)
|
| 108 |
);
|
| 109 |
font-family: inherit;
|
| 110 |
font-weight: inherit;
|
|
|
|
| 388 |
margin-top: 10px;
|
| 389 |
}
|
| 390 |
|
| 391 |
+
// Start / Model 行与首行 DAG 参数(含 layout mode)之间略加大,避免与按钮挤在一起
|
| 392 |
+
.gen-attribute-page
|
| 393 |
+
.input-section
|
| 394 |
+
> .textarea-wrapper.chat-prompt-actions-row
|
| 395 |
+
+ .gen-attr-dag-measure-width-row {
|
| 396 |
+
margin-top: 30px;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
.attribution-exclude-prompt-patterns-header {
|
| 400 |
flex-wrap: wrap;
|
| 401 |
|
client/src/css/home.scss
CHANGED
|
@@ -185,6 +185,7 @@ html[data-theme='dark'] body.nav-landing-page {
|
|
| 185 |
.nav-landing-card {
|
| 186 |
display: flex;
|
| 187 |
flex-direction: column;
|
|
|
|
| 188 |
gap: $nav-card-gap;
|
| 189 |
padding: $nav-card-pad-block $nav-card-pad-inline;
|
| 190 |
border-radius: $nav-card-radius;
|
|
@@ -196,11 +197,52 @@ html[data-theme='dark'] body.nav-landing-page {
|
|
| 196 |
transition: background 0.15s ease, border-color 0.15s ease;
|
| 197 |
box-sizing: border-box;
|
| 198 |
|
| 199 |
-
&:hover {
|
| 200 |
background: var(--nav-landing-card-bg-hover, #dcdcdc);
|
| 201 |
}
|
| 202 |
}
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
html[data-theme='dark'] .nav-landing-card {
|
| 205 |
border-color: var(--border-color, #333);
|
| 206 |
}
|
|
|
|
| 185 |
.nav-landing-card {
|
| 186 |
display: flex;
|
| 187 |
flex-direction: column;
|
| 188 |
+
position: relative;
|
| 189 |
gap: $nav-card-gap;
|
| 190 |
padding: $nav-card-pad-block $nav-card-pad-inline;
|
| 191 |
border-radius: $nav-card-radius;
|
|
|
|
| 197 |
transition: background 0.15s ease, border-color 0.15s ease;
|
| 198 |
box-sizing: border-box;
|
| 199 |
|
| 200 |
+
&:hover:not(:has(.nav-landing-card-badge:hover, .nav-landing-card-badge:focus-visible)) {
|
| 201 |
background: var(--nav-landing-card-bg-hover, #dcdcdc);
|
| 202 |
}
|
| 203 |
}
|
| 204 |
|
| 205 |
+
.nav-landing-card-badge {
|
| 206 |
+
position: absolute;
|
| 207 |
+
top: -10px;
|
| 208 |
+
right: 12px;
|
| 209 |
+
z-index: 2;
|
| 210 |
+
height: 22px;
|
| 211 |
+
padding: 0 10px;
|
| 212 |
+
border-radius: 9999px;
|
| 213 |
+
border: 1px solid rgba(212, 107, 8, 0.35);
|
| 214 |
+
background: rgba(255, 247, 230, 0.78);
|
| 215 |
+
color: rgba(141, 73, 0, 0.86);
|
| 216 |
+
font-size: 12px;
|
| 217 |
+
font-weight: 600;
|
| 218 |
+
line-height: 22px;
|
| 219 |
+
white-space: nowrap;
|
| 220 |
+
cursor: pointer;
|
| 221 |
+
opacity: 0.82;
|
| 222 |
+
transition: opacity 0.16s ease, background-color 0.16s ease, border-color 0.16s ease, color 0.16s ease;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.nav-landing-card-badge:hover,
|
| 226 |
+
.nav-landing-card-badge:focus-visible {
|
| 227 |
+
opacity: 1;
|
| 228 |
+
border-color: #ffb86b;
|
| 229 |
+
background: #fff2d9;
|
| 230 |
+
color: #ad4e00;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
html[data-theme='dark'] .nav-landing-card-badge {
|
| 234 |
+
border-color: rgba(212, 107, 8, 0.42);
|
| 235 |
+
background: #383028;
|
| 236 |
+
color: rgba(237, 214, 192, 0.95);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
html[data-theme='dark'] .nav-landing-card-badge:hover,
|
| 240 |
+
html[data-theme='dark'] .nav-landing-card-badge:focus-visible {
|
| 241 |
+
border-color: #ffb86b;
|
| 242 |
+
background: #4a4034;
|
| 243 |
+
color: #fff2e8;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
html[data-theme='dark'] .nav-landing-card {
|
| 247 |
border-color: var(--border-color, #333);
|
| 248 |
}
|
client/src/gen_attribute.html
CHANGED
|
@@ -225,6 +225,26 @@
|
|
| 225 |
<span class="semantic-submode-label">px</span>
|
| 226 |
</span>
|
| 227 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 229 |
<span class="semantic-submode-group">
|
| 230 |
<label class="semantic-submode-label">
|
|
|
|
| 225 |
<span class="semantic-submode-label">px</span>
|
| 226 |
</span>
|
| 227 |
</div>
|
| 228 |
+
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 229 |
+
<span class="semantic-submode-group">
|
| 230 |
+
<label class="semantic-submode-label">
|
| 231 |
+
<input type="checkbox" id="gen_attr_dag_node_ci_visual_scale" checked
|
| 232 |
+
title="When checked, generated nodes are visually enlarged based on their conditional information (surprisal). Takes effect on next generation."
|
| 233 |
+
data-i18n="title">
|
| 234 |
+
Enlarge high-surprisal nodes
|
| 235 |
+
</label>
|
| 236 |
+
</span>
|
| 237 |
+
</div>
|
| 238 |
+
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 239 |
+
<span class="semantic-submode-group">
|
| 240 |
+
<label class="semantic-submode-label">
|
| 241 |
+
<input type="checkbox" id="gen_attr_dag_edge_weaken_high_surprisal" checked
|
| 242 |
+
title="When checked, edges leading to high-surprisal (uncertain) target tokens are visually weakened by the mutual information ratio. Takes effect on next generation."
|
| 243 |
+
data-i18n="title">
|
| 244 |
+
Weaken attribution to high-surprisal target
|
| 245 |
+
</label>
|
| 246 |
+
</span>
|
| 247 |
+
</div>
|
| 248 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 249 |
<span class="semantic-submode-group">
|
| 250 |
<label class="semantic-submode-label">
|
client/src/scripts/injectPageMetaIntoHtml.js
CHANGED
|
@@ -114,7 +114,12 @@ function injectPageMeta(html, pageKey, doc) {
|
|
| 114 |
navKey === 'genAttribute'
|
| 115 |
? `<video class="nav-landing-card-shot" muted loop playsinline autoplay preload="metadata" aria-hidden="true"></video>`
|
| 116 |
: `<div class="nav-landing-card-shot" aria-hidden="true"></div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
const inner =
|
|
|
|
| 118 |
`<div class="nav-landing-card-text">` +
|
| 119 |
`<span class="nav-landing-card-title" data-i18n>${escapeHtmlText(navMeta.title)}</span>` +
|
| 120 |
`<span class="nav-landing-card-subtitle" data-i18n>${escapeHtmlText(navMeta.subtitle)}</span>` +
|
|
|
|
| 114 |
navKey === 'genAttribute'
|
| 115 |
? `<video class="nav-landing-card-shot" muted loop playsinline autoplay preload="metadata" aria-hidden="true"></video>`
|
| 116 |
: `<div class="nav-landing-card-shot" aria-hidden="true"></div>`;
|
| 117 |
+
const badge =
|
| 118 |
+
navKey === 'genAttribute'
|
| 119 |
+
? `<span class="nav-landing-card-badge" title="Go to demo on RedNote: xhslink.com/o/A7VLi99aBvG" data-i18n="text,title">100K+ plays on RedNote</span>`
|
| 120 |
+
: '';
|
| 121 |
const inner =
|
| 122 |
+
badge +
|
| 123 |
`<div class="nav-landing-card-text">` +
|
| 124 |
`<span class="nav-landing-card-title" data-i18n>${escapeHtmlText(navMeta.title)}</span>` +
|
| 125 |
`<span class="nav-landing-card-subtitle" data-i18n>${escapeHtmlText(navMeta.subtitle)}</span>` +
|
client/src/ts/attribution/genAttributeDagLinkSegment.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
/** 轴对齐矩形节点:用于连线从边界起止
|
| 2 |
export type DagLinkRectNode = {
|
| 3 |
-
|
| 4 |
-
|
| 5 |
nodeW: number;
|
| 6 |
nodeH: number;
|
| 7 |
};
|
| 8 |
|
| 9 |
function nodeCenter(n: DagLinkRectNode): { cx: number; cy: number } {
|
| 10 |
-
return { cx: n.
|
| 11 |
}
|
| 12 |
|
| 13 |
/** 轴对齐矩形(半宽 hw、半高 hh)中心沿单位向量 (ux,uy) 到边界的距离。 */
|
|
|
|
| 1 |
+
/** 轴对齐矩形节点:用于连线从边界起止,以矩形中心坐标表示。 */
|
| 2 |
export type DagLinkRectNode = {
|
| 3 |
+
cx: number;
|
| 4 |
+
cy: number;
|
| 5 |
nodeW: number;
|
| 6 |
nodeH: number;
|
| 7 |
};
|
| 8 |
|
| 9 |
function nodeCenter(n: DagLinkRectNode): { cx: number; cy: number } {
|
| 10 |
+
return { cx: n.cx, cy: n.cy };
|
| 11 |
}
|
| 12 |
|
| 13 |
/** 轴对齐矩形(半宽 hw、半高 hh)中心沿单位向量 (ux,uy) 到边界的距离。 */
|
client/src/ts/attribution/genAttributeDagTextMeasure.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { visualizeSpecialChars } from '../utils/tokenDisplayUtils';
|
|
| 6 |
import type { PromptTokenSpan } from './genAttributeDagPreprocess';
|
| 7 |
|
| 8 |
export type GenAttrDagTokenGeom = {
|
| 9 |
-
/**
|
| 10 |
-
|
| 11 |
-
|
| 12 |
width: number;
|
| 13 |
height: number;
|
| 14 |
};
|
|
@@ -93,8 +93,8 @@ function geomFromTokenFragments(frags: TokenFragmentRect[], raw: string): GenAtt
|
|
| 93 |
: 1;
|
| 94 |
const widthSum = Math.max(geomWidthSum, expandedFloor);
|
| 95 |
return {
|
| 96 |
-
|
| 97 |
-
|
| 98 |
width: widthSum,
|
| 99 |
height: hFirst,
|
| 100 |
};
|
|
|
|
| 6 |
import type { PromptTokenSpan } from './genAttributeDagPreprocess';
|
| 7 |
|
| 8 |
export type GenAttrDagTokenGeom = {
|
| 9 |
+
/** token 基础矩形(1× 尺寸)的中心坐标;同行 token 的 cy 相同,与 CI 缩放无关。 */
|
| 10 |
+
cx: number;
|
| 11 |
+
cy: number;
|
| 12 |
width: number;
|
| 13 |
height: number;
|
| 14 |
};
|
|
|
|
| 93 |
: 1;
|
| 94 |
const widthSum = Math.max(geomWidthSum, expandedFloor);
|
| 95 |
return {
|
| 96 |
+
cx: first.x + widthSum / 2,
|
| 97 |
+
cy: first.y + hFirst / 2,
|
| 98 |
width: widthSum,
|
| 99 |
height: hFirst,
|
| 100 |
};
|
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,
|
|
@@ -11,6 +11,11 @@ import {
|
|
| 11 |
type PromptTokenSpan,
|
| 12 |
} from './genAttributeDagPreprocess';
|
| 13 |
import { DAG_EDGE_MIN_DISPLAY_OPACITY } from './genAttributeDagEdgeDisplay';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
import { isOffsetSpanFullyExcluded } from './attributionDisplayModel';
|
| 15 |
import {
|
| 16 |
alignAndAggregateByNode,
|
|
@@ -39,6 +44,9 @@ import { paintTextFlowLayout } from './genAttributeDagViewTextFlowMode';
|
|
| 39 |
import { paintSpiralLayout } from './genAttributeDagViewSpiralMode';
|
| 40 |
import { tr } from '../lang/i18n-lite';
|
| 41 |
|
|
|
|
|
|
|
|
|
|
| 42 |
/** 再次挂载前执行上一轮 detach(当前为空操作,保留扩展点) */
|
| 43 |
const detachGenAttributeDagPanel = new WeakMap<HTMLElement, () => void>();
|
| 44 |
|
|
@@ -55,35 +63,39 @@ export function clampDagCompactness(n: number): number {
|
|
| 55 |
return Math.min(DAG_COMPACTNESS_MAX, Math.max(DAG_COMPACTNESS_MIN, n));
|
| 56 |
}
|
| 57 |
|
| 58 |
-
/**
|
| 59 |
-
* 零信心概率基准 p₀:surprisal log₂(1/p₀) 视作单 token 的绝对信息量参照(此处 20 bit)。
|
| 60 |
-
* p = p₀ 时 {@link computeMutualInformationRatio} 为 0。
|
| 61 |
-
*/
|
| 62 |
-
const ZERO_CONFIDENCE_PROBABILITY_BASELINE = 2 ** -20;
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
}
|
| 67 |
|
| 68 |
/**
|
| 69 |
-
*
|
| 70 |
-
*
|
| 71 |
-
*
|
| 72 |
*/
|
| 73 |
-
function
|
| 74 |
-
if (
|
| 75 |
-
if (!Number.isFinite(targetProb)
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
);
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
-
/**
|
| 84 |
-
* 节点/边原生 `<title>` 中互信息率 α 的展示:α∈[0,1] 转为百分号字符串,
|
| 85 |
-
* 与 analysis 主视图 Tooltip 中 Top-K 概率列 {@link formatTopkTooltipProbabilityPercent} 同形。
|
| 86 |
-
*/
|
| 87 |
function formatMutualInformationRatioForTooltip(miRatio: number): string {
|
| 88 |
if (!Number.isFinite(miRatio)) return String(miRatio);
|
| 89 |
return formatTopkTooltipProbabilityPercent(miRatio);
|
|
@@ -109,12 +121,17 @@ type DagNodeAttrs = {
|
|
| 109 |
*/
|
| 110 |
start: number;
|
| 111 |
end: number;
|
| 112 |
-
/**
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
| 116 |
nodeW: number;
|
| 117 |
nodeH: number;
|
|
|
|
|
|
|
| 118 |
/** {@link visualizeSpecialChars}(DAG:仅「空格后是 [A-Za-z0-9]」保留空格,其余空格为 ·),建点后不变 */
|
| 119 |
displayLabel: string;
|
| 120 |
/** 原生 `<title>` 全文(与 `DISABLE_DAG_NODE_TOOLTIPS` 无关,便于切换时不必重算) */
|
|
@@ -141,9 +158,10 @@ type DagLink = {
|
|
| 141 |
titleText: string;
|
| 142 |
};
|
| 143 |
|
| 144 |
-
/** 与 {@link refreshNodeLinkHighlight} 中边的 `stroke-opacity` 一致:`normalizedScore × mutualInformationRatio`。 */
|
| 145 |
function dagLinkStrokeOpacity(d: Pick<DagLink, 'normalizedScore' | 'mutualInformationRatio'>): number {
|
| 146 |
-
|
|
|
|
| 147 |
}
|
| 148 |
|
| 149 |
function dagLinkEndpointKey(source: string, target: string): string {
|
|
@@ -295,7 +313,7 @@ function linkEndInsetBaseAtUnitScalePx(measureLayerEl: HTMLElement): number {
|
|
| 295 |
}
|
| 296 |
|
| 297 |
function nodeRx(d: DagNode): number {
|
| 298 |
-
return Math.min(
|
| 299 |
}
|
| 300 |
|
| 301 |
export type GenAttributeDagHandle = {
|
|
@@ -401,8 +419,14 @@ function buildNodeNativeTitleText(
|
|
| 401 |
];
|
| 402 |
const { targetProb } = d;
|
| 403 |
if (targetProb !== undefined && Number.isFinite(targetProb)) {
|
| 404 |
-
lines.push(`
|
| 405 |
-
lines.push(`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
}
|
| 407 |
return lines.join('\n');
|
| 408 |
}
|
|
@@ -445,15 +469,15 @@ function buildLinkTitleText(
|
|
| 445 |
const GLUE_EDGE_CHAR = /^(?:(?!\p{Script=Han})\p{L}|['\-_])$/u;
|
| 446 |
|
| 447 |
/**
|
| 448 |
-
* 子词拼接:offset 紧贴、同行、prev 末码点与当前首码点均满足 {@link GLUE_EDGE_CHAR}
|
| 449 |
-
* →
|
| 450 |
*/
|
| 451 |
function snapSubwordNode(node: DagNode, prev: DagNode | null): void {
|
| 452 |
-
if (!prev || prev.end !== node.start || node.
|
| 453 |
const last = [...prev.label].at(-1) ?? '';
|
| 454 |
const first = [...node.label][0] ?? '';
|
| 455 |
if (!GLUE_EDGE_CHAR.test(last) || !GLUE_EDGE_CHAR.test(first)) return;
|
| 456 |
-
node.
|
| 457 |
}
|
| 458 |
|
| 459 |
/** 焦点 + 一层入邻(直接祖先)+ 一层出邻(直接后代),用于选中/悬停高亮范围 */
|
|
@@ -801,6 +825,7 @@ export function initGenAttributeDagView(
|
|
| 801 |
});
|
| 802 |
}
|
| 803 |
|
|
|
|
| 804 |
const drag = d3
|
| 805 |
.drag<SVGGElement, DagNode>()
|
| 806 |
// 与 d3 默认 filter 一致,并仅在「当前节点已单击选中」时允许拖动手势生效,减少误拖
|
|
@@ -812,16 +837,22 @@ export function initGenAttributeDagView(
|
|
| 812 |
selectedId === d.id &&
|
| 813 |
layoutMode === 'text-flow'
|
| 814 |
)
|
| 815 |
-
.on('start', (event) => {
|
| 816 |
event.sourceEvent?.stopPropagation();
|
|
|
|
|
|
|
| 817 |
})
|
| 818 |
.on('drag', (event, d) => {
|
| 819 |
layoutDirty = true;
|
| 820 |
userDraggedNodes = true;
|
| 821 |
const [x, y] = d3.pointer(event, rootG.node());
|
| 822 |
-
|
| 823 |
-
d.
|
|
|
|
| 824 |
paint();
|
|
|
|
|
|
|
|
|
|
| 825 |
});
|
| 826 |
|
| 827 |
/**
|
|
@@ -949,7 +980,10 @@ export function initGenAttributeDagView(
|
|
| 949 |
// 节点身份 append-only、几何(nodeW/nodeH)一旦建立不再变化(drag 仅改 x/y,
|
| 950 |
// 由 paint 通过 transform 处理),故与几何相关的属性���在 enter 写一次即可;
|
| 951 |
// 同理 `--prompt` class 依据 step === -1,step 初始化后不变。
|
| 952 |
-
const g = enter
|
|
|
|
|
|
|
|
|
|
| 953 |
g.classed('gen-attr-dag-node--prompt', (d: DagNode) => d.step === -1);
|
| 954 |
if (!DISABLE_DAG_NODE_TOOLTIPS) {
|
| 955 |
g.append('title').text((d: DagNode) => d.nativeTitleText);
|
|
@@ -1034,10 +1068,11 @@ export function initGenAttributeDagView(
|
|
| 1034 |
step: -1,
|
| 1035 |
start: ns,
|
| 1036 |
end: ne,
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
nodeW: g.width * displayScale,
|
| 1040 |
nodeH: g.height * displayScale,
|
|
|
|
| 1041 |
displayLabel,
|
| 1042 |
nativeTitleText: buildNodeNativeTitleText({
|
| 1043 |
displayLabel,
|
|
@@ -1081,16 +1116,18 @@ export function initGenAttributeDagView(
|
|
| 1081 |
const displayLabel = visualizeSpecialChars(token, {
|
| 1082 |
spaceDotExceptBeforeAsciiLetterOrNumber: true,
|
| 1083 |
});
|
|
|
|
| 1084 |
const targetNode: DagNode = {
|
| 1085 |
id: targetId,
|
| 1086 |
label: token,
|
| 1087 |
step: stepProcessed,
|
| 1088 |
start: targetStart,
|
| 1089 |
end: targetEnd,
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
nodeW: g.width * displayScale,
|
| 1093 |
-
nodeH: g.height * displayScale,
|
|
|
|
| 1094 |
displayLabel,
|
| 1095 |
nativeTitleText: buildNodeNativeTitleText({
|
| 1096 |
displayLabel,
|
|
|
|
| 1 |
import * as d3 from 'd3';
|
| 2 |
import { DirectedGraph } from 'graphology';
|
| 3 |
+
import { calculateSurprisal, type D3Sel } from '../utils/Util';
|
| 4 |
import { visualizeSpecialChars } from '../utils/tokenDisplayUtils';
|
| 5 |
import {
|
| 6 |
clampDagEdgeTopPCoverage,
|
|
|
|
| 11 |
type PromptTokenSpan,
|
| 12 |
} from './genAttributeDagPreprocess';
|
| 13 |
import { DAG_EDGE_MIN_DISPLAY_OPACITY } from './genAttributeDagEdgeDisplay';
|
| 14 |
+
import {
|
| 15 |
+
computeMutualInformationRatio,
|
| 16 |
+
computeConditionalInformationRatio,
|
| 17 |
+
FULL_CONFIDENCE_PROBABILITY_BASELINE,
|
| 18 |
+
} from '../utils/surprisalMath';
|
| 19 |
import { isOffsetSpanFullyExcluded } from './attributionDisplayModel';
|
| 20 |
import {
|
| 21 |
alignAndAggregateByNode,
|
|
|
|
| 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 |
|
|
|
|
| 63 |
return Math.min(DAG_COMPACTNESS_MAX, Math.max(DAG_COMPACTNESS_MIN, n));
|
| 64 |
}
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
+
|
| 68 |
+
/** 节点 CI 视觉放大开关;`false` 时所有生成节点 ciVisualScale 恒为 1×,下次 update() 起生效。 */
|
| 69 |
+
let dagNodeCiVisualScaleEnabled = true;
|
| 70 |
+
export function setDagNodeCiVisualScaleEnabled(enabled: boolean): void {
|
| 71 |
+
dagNodeCiVisualScaleEnabled = enabled;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/** 高惊讶度目标边弱化开关;`false` 时 mutualInformationRatio 恒为 1(不弱化),下次 update() 起生效。 */
|
| 75 |
+
let dagEdgeWeakenHighSurprisalEnabled = true;
|
| 76 |
+
export function setDagEdgeWeakenHighSurprisalEnabled(enabled: boolean): void {
|
| 77 |
+
dagEdgeWeakenHighSurprisalEnabled = enabled;
|
| 78 |
}
|
| 79 |
|
| 80 |
/**
|
| 81 |
+
* DAG 生成节点矩形/标签缩放:CI=0→1×,CI=1→2×(prompt 节点恒用 1,见建点处)。
|
| 82 |
+
* p > {@link FULL_CONFIDENCE_PROBABILITY_BASELINE}(surprisal < 3 bit)时截断为 1×,不放大。
|
| 83 |
+
* {@link dagNodeCiVisualScaleEnabled} 为 false 时恒返回 1。
|
| 84 |
*/
|
| 85 |
+
function dagGeneratedNodeCiVisualScale(targetProb: number | undefined): number {
|
| 86 |
+
if (!dagNodeCiVisualScaleEnabled) return 1;
|
| 87 |
+
if (targetProb !== undefined && Number.isFinite(targetProb) && targetProb > FULL_CONFIDENCE_PROBABILITY_BASELINE) return 1;
|
| 88 |
+
return 1 + computeConditionalInformationRatio(targetProb);
|
| 89 |
+
}
|
| 90 |
|
| 91 |
+
/** 原生 `<title>` 中 CI/MI 百分号展示,与 Top-K 概率列 {@link formatTopkTooltipProbabilityPercent} 同形。 */
|
| 92 |
+
function formatCiMiRatiosLineForTooltip(ciRatio: number, miRatio: number): string {
|
| 93 |
+
const ci = Number.isFinite(ciRatio) ? formatTopkTooltipProbabilityPercent(ciRatio) : String(ciRatio);
|
| 94 |
+
const mi = Number.isFinite(miRatio) ? formatTopkTooltipProbabilityPercent(miRatio) : String(miRatio);
|
| 95 |
+
return `CI/MI: ${ci} / ${mi}`;
|
| 96 |
}
|
| 97 |
|
| 98 |
+
/** 边原生 `<title>` 中互信息率 α 的展示(节点 title 改用 {@link formatCiMiRatiosLineForTooltip})。 */
|
|
|
|
|
|
|
|
|
|
| 99 |
function formatMutualInformationRatioForTooltip(miRatio: number): string {
|
| 100 |
if (!Number.isFinite(miRatio)) return String(miRatio);
|
| 101 |
return formatTopkTooltipProbabilityPercent(miRatio);
|
|
|
|
| 121 |
*/
|
| 122 |
start: number;
|
| 123 |
end: number;
|
| 124 |
+
/**
|
| 125 |
+
* 节点矩形中心坐标。center 不随 CI 缩放变化,故同行 token 的 cy 始终相等,
|
| 126 |
+
* 可直接用于 {@link snapSubwordNode} 同行��测,无需额外 baseY 字段。
|
| 127 |
+
*/
|
| 128 |
+
cx: number;
|
| 129 |
+
cy: number;
|
| 130 |
+
/** 测量层几何 × display-scale × CI 缩放 后的宽、高 */
|
| 131 |
nodeW: number;
|
| 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 |
/** 原生 `<title>` 全文(与 `DISABLE_DAG_NODE_TOOLTIPS` 无关,便于切换时不必重算) */
|
|
|
|
| 158 |
titleText: string;
|
| 159 |
};
|
| 160 |
|
| 161 |
+
/** 与 {@link refreshNodeLinkHighlight} 中边的 `stroke-opacity` 一致:`normalizedScore × mutualInformationRatio`(开关关闭时 MI 系数恒为 1)。 */
|
| 162 |
function dagLinkStrokeOpacity(d: Pick<DagLink, 'normalizedScore' | 'mutualInformationRatio'>): number {
|
| 163 |
+
const mi = dagEdgeWeakenHighSurprisalEnabled ? (d.mutualInformationRatio ?? 1) : 1;
|
| 164 |
+
return (d.normalizedScore ?? 1) * mi;
|
| 165 |
}
|
| 166 |
|
| 167 |
function dagLinkEndpointKey(source: string, target: string): string {
|
|
|
|
| 313 |
}
|
| 314 |
|
| 315 |
function nodeRx(d: DagNode): number {
|
| 316 |
+
return Math.min(d.nodeW / 2, d.nodeH / 2);
|
| 317 |
}
|
| 318 |
|
| 319 |
export type GenAttributeDagHandle = {
|
|
|
|
| 419 |
];
|
| 420 |
const { targetProb } = d;
|
| 421 |
if (targetProb !== undefined && Number.isFinite(targetProb)) {
|
| 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 |
}
|
|
|
|
| 469 |
const GLUE_EDGE_CHAR = /^(?:(?!\p{Script=Han})\p{L}|['\-_])$/u;
|
| 470 |
|
| 471 |
/**
|
| 472 |
+
* 子词拼接:offset 紧贴、同行(cy 相等)、prev 末码点与当前首码点均满足 {@link GLUE_EDGE_CHAR}
|
| 473 |
+
* → 将当前节点中心 cx 紧贴 prev 右缘(链式调用时 prev.cx 已调整,自动支持多段续片)。
|
| 474 |
*/
|
| 475 |
function snapSubwordNode(node: DagNode, prev: DagNode | null): void {
|
| 476 |
+
if (!prev || prev.end !== node.start || node.cy !== prev.cy) return;
|
| 477 |
const last = [...prev.label].at(-1) ?? '';
|
| 478 |
const first = [...node.label][0] ?? '';
|
| 479 |
if (!GLUE_EDGE_CHAR.test(last) || !GLUE_EDGE_CHAR.test(first)) return;
|
| 480 |
+
node.cx = prev.cx + (prev.nodeW + node.nodeW) / 2;
|
| 481 |
}
|
| 482 |
|
| 483 |
/** 焦点 + 一层入邻(直接祖先)+ 一层出邻(直接后代),用于选中/悬停高亮范围 */
|
|
|
|
| 825 |
});
|
| 826 |
}
|
| 827 |
|
| 828 |
+
let dragPointerOffset: { x: number; y: number } | null = null;
|
| 829 |
const drag = d3
|
| 830 |
.drag<SVGGElement, DagNode>()
|
| 831 |
// 与 d3 默认 filter 一致,并仅在「当前节点已单击选中」时允许拖动手势生效,减少误拖
|
|
|
|
| 837 |
selectedId === d.id &&
|
| 838 |
layoutMode === 'text-flow'
|
| 839 |
)
|
| 840 |
+
.on('start', (event, d) => {
|
| 841 |
event.sourceEvent?.stopPropagation();
|
| 842 |
+
const [x, y] = d3.pointer(event, rootG.node());
|
| 843 |
+
dragPointerOffset = { x: x - d.cx, y: y - d.cy };
|
| 844 |
})
|
| 845 |
.on('drag', (event, d) => {
|
| 846 |
layoutDirty = true;
|
| 847 |
userDraggedNodes = true;
|
| 848 |
const [x, y] = d3.pointer(event, rootG.node());
|
| 849 |
+
const offset = dragPointerOffset ?? { x: 0, y: 0 };
|
| 850 |
+
d.cx = x - offset.x;
|
| 851 |
+
d.cy = y - offset.y;
|
| 852 |
paint();
|
| 853 |
+
})
|
| 854 |
+
.on('end', () => {
|
| 855 |
+
dragPointerOffset = null;
|
| 856 |
});
|
| 857 |
|
| 858 |
/**
|
|
|
|
| 980 |
// 节点身份 append-only、几何(nodeW/nodeH)一旦建立不再变化(drag 仅改 x/y,
|
| 981 |
// 由 paint 通过 transform 处理),故与几何相关的属性���在 enter 写一次即可;
|
| 982 |
// 同理 `--prompt` class 依据 step === -1,step 初始化后不变。
|
| 983 |
+
const g = enter
|
| 984 |
+
.append('g')
|
| 985 |
+
.attr('class', 'gen-attr-dag-node')
|
| 986 |
+
.style('--gen-attr-dag-node-ci-visual-scale', (d: DagNode) => String(d.ciVisualScale));
|
| 987 |
g.classed('gen-attr-dag-node--prompt', (d: DagNode) => d.step === -1);
|
| 988 |
if (!DISABLE_DAG_NODE_TOOLTIPS) {
|
| 989 |
g.append('title').text((d: DagNode) => d.nativeTitleText);
|
|
|
|
| 1068 |
step: -1,
|
| 1069 |
start: ns,
|
| 1070 |
end: ne,
|
| 1071 |
+
cx: g.cx,
|
| 1072 |
+
cy: g.cy,
|
| 1073 |
nodeW: g.width * displayScale,
|
| 1074 |
nodeH: g.height * displayScale,
|
| 1075 |
+
ciVisualScale: 1,
|
| 1076 |
displayLabel,
|
| 1077 |
nativeTitleText: buildNodeNativeTitleText({
|
| 1078 |
displayLabel,
|
|
|
|
| 1116 |
const displayLabel = visualizeSpecialChars(token, {
|
| 1117 |
spaceDotExceptBeforeAsciiLetterOrNumber: true,
|
| 1118 |
});
|
| 1119 |
+
const ciVisualScale = dagGeneratedNodeCiVisualScale(response.target_prob);
|
| 1120 |
const targetNode: DagNode = {
|
| 1121 |
id: targetId,
|
| 1122 |
label: token,
|
| 1123 |
step: stepProcessed,
|
| 1124 |
start: targetStart,
|
| 1125 |
end: targetEnd,
|
| 1126 |
+
cx: g.cx,
|
| 1127 |
+
cy: g.cy,
|
| 1128 |
+
nodeW: g.width * displayScale * ciVisualScale,
|
| 1129 |
+
nodeH: g.height * displayScale * ciVisualScale,
|
| 1130 |
+
ciVisualScale,
|
| 1131 |
displayLabel,
|
| 1132 |
nativeTitleText: buildNodeNativeTitleText({
|
| 1133 |
displayLabel,
|
client/src/ts/attribution/genAttributeDagViewLinearArcMode.ts
CHANGED
|
@@ -25,7 +25,7 @@ export function clampLinearArcAdjacentGap(px: number): number {
|
|
| 25 |
);
|
| 26 |
}
|
| 27 |
|
| 28 |
-
type LinearArcNodeLike = { nodeW: number };
|
| 29 |
|
| 30 |
/** `step === -1` 表示 prompt(与 `genAttributeDagView` 中 `DagNode.step` 约定一致) */
|
| 31 |
type LinearArcSteppedNode = LinearArcNodeLike & { step: number };
|
|
@@ -76,7 +76,8 @@ export function paintLinearArcLayout<
|
|
| 76 |
if (srcCx === undefined || tgtCx === undefined) {
|
| 77 |
throw new Error('paintLinearArcLayout: link endpoint not in linear node list');
|
| 78 |
}
|
| 79 |
-
|
|
|
|
| 80 |
const dx = Math.abs(tgtCx - srcCx);
|
| 81 |
const arcH = dx * 0.4;
|
| 82 |
const upY = y - arcH;
|
|
@@ -98,6 +99,6 @@ export function paintLinearArcLayout<
|
|
| 98 |
nodeSel.attr('transform', (d) => {
|
| 99 |
const cx = centerXByNode.get(d);
|
| 100 |
if (cx === undefined) return null; // 不在布局列表中(已 display:none),不更新 transform
|
| 101 |
-
return `translate(${cx - d.nodeW / 2},${LINEAR_ARC_BASELINE_Y})`;
|
| 102 |
});
|
| 103 |
}
|
|
|
|
| 25 |
);
|
| 26 |
}
|
| 27 |
|
| 28 |
+
type LinearArcNodeLike = { nodeW: number; nodeH: number; ciVisualScale: number };
|
| 29 |
|
| 30 |
/** `step === -1` 表示 prompt(与 `genAttributeDagView` 中 `DagNode.step` 约定一致) */
|
| 31 |
type LinearArcSteppedNode = LinearArcNodeLike & { step: number };
|
|
|
|
| 76 |
if (srcCx === undefined || tgtCx === undefined) {
|
| 77 |
throw new Error('paintLinearArcLayout: link endpoint not in linear node list');
|
| 78 |
}
|
| 79 |
+
// 用未放大的半高(nodeH / ciVisualScale / 2)定位弧端点,使所有节点顶部对齐同一 y 基线。
|
| 80 |
+
const y = LINEAR_ARC_BASELINE_Y - src.nodeH / (2 * src.ciVisualScale);
|
| 81 |
const dx = Math.abs(tgtCx - srcCx);
|
| 82 |
const arcH = dx * 0.4;
|
| 83 |
const upY = y - arcH;
|
|
|
|
| 99 |
nodeSel.attr('transform', (d) => {
|
| 100 |
const cx = centerXByNode.get(d);
|
| 101 |
if (cx === undefined) return null; // 不在布局列表中(已 display:none),不更新 transform
|
| 102 |
+
return `translate(${cx - d.nodeW / 2},${LINEAR_ARC_BASELINE_Y - d.nodeH / 2})`;
|
| 103 |
});
|
| 104 |
}
|
client/src/ts/attribution/genAttributeDagViewSpiralMode.ts
CHANGED
|
@@ -10,6 +10,8 @@ const SPIRAL_SPACING = 60;
|
|
| 10 |
const SPIRAL_ARC_STEP = 40;
|
| 11 |
/** 螺旋旋转相位(弧度):控制螺旋臂展开方向。0 = 向右,-Math.PI/2 = 向上。 */
|
| 12 |
const SPIRAL_PHASE = Math.PI * 0.6;
|
|
|
|
|
|
|
| 13 |
// ────────────────────────────────────────────────────────────────────────────
|
| 14 |
|
| 15 |
type SpiralNodeLike = { nodeW: number; nodeH: number };
|
|
@@ -63,10 +65,19 @@ export function paintSpiralLayout<
|
|
| 63 |
positionByNode.set(nodes[i]!, rawPos[i]!);
|
| 64 |
}
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
// 节点:中心落在螺旋点,矩形保持水平
|
| 67 |
nodeSel.attr('transform', (d) => {
|
| 68 |
const pos = positionByNode.get(d);
|
| 69 |
if (pos === undefined) return null;
|
|
|
|
|
|
|
|
|
|
| 70 |
return `translate(${pos.cx - d.nodeW / 2},${pos.cy - d.nodeH / 2})`;
|
| 71 |
});
|
| 72 |
|
|
@@ -76,18 +87,10 @@ export function paintSpiralLayout<
|
|
| 76 |
const pa = positionByNode.get(src);
|
| 77 |
const pb = positionByNode.get(tgt);
|
| 78 |
if (pa === undefined || pb === undefined) return;
|
| 79 |
-
const
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
nodeH: src.nodeH,
|
| 84 |
-
};
|
| 85 |
-
const tgtRect = {
|
| 86 |
-
x: pb.cx - tgt.nodeW / 2,
|
| 87 |
-
y: pb.cy - tgt.nodeH / 2,
|
| 88 |
-
nodeW: tgt.nodeW,
|
| 89 |
-
nodeH: tgt.nodeH,
|
| 90 |
-
};
|
| 91 |
const seg = linkSegmentThroughNodeRects(srcRect, tgtRect, linkEndInsetPx);
|
| 92 |
d3.select(this)
|
| 93 |
.selectAll('path.gen-attr-dag-link-visible')
|
|
|
|
| 10 |
const SPIRAL_ARC_STEP = 40;
|
| 11 |
/** 螺旋旋转相位(弧度):控制螺旋臂展开方向。0 = 向右,-Math.PI/2 = 向上。 */
|
| 12 |
const SPIRAL_PHASE = Math.PI * 0.6;
|
| 13 |
+
/** 螺旋上第一个(起始位置)token 的相对视觉放大倍数(仅 spiral 布局)。 */
|
| 14 |
+
const SPIRAL_FIRST_TOKEN_SCALE = 2;
|
| 15 |
// ────────────────────────────────────────────────────────────────────────────
|
| 16 |
|
| 17 |
type SpiralNodeLike = { nodeW: number; nodeH: number };
|
|
|
|
| 65 |
positionByNode.set(nodes[i]!, rawPos[i]!);
|
| 66 |
}
|
| 67 |
|
| 68 |
+
const firstSpiralNode = nodes.length > 0 ? nodes[0]! : null;
|
| 69 |
+
const effNodeSize = (n: NodeDatum) =>
|
| 70 |
+
firstSpiralNode !== null && n === firstSpiralNode
|
| 71 |
+
? { nodeW: n.nodeW * SPIRAL_FIRST_TOKEN_SCALE, nodeH: n.nodeH * SPIRAL_FIRST_TOKEN_SCALE }
|
| 72 |
+
: { nodeW: n.nodeW, nodeH: n.nodeH };
|
| 73 |
+
|
| 74 |
// 节点:中心落在螺旋点,矩形保持水平
|
| 75 |
nodeSel.attr('transform', (d) => {
|
| 76 |
const pos = positionByNode.get(d);
|
| 77 |
if (pos === undefined) return null;
|
| 78 |
+
if (firstSpiralNode !== null && d === firstSpiralNode) {
|
| 79 |
+
return `translate(${pos.cx},${pos.cy}) scale(${SPIRAL_FIRST_TOKEN_SCALE}) translate(${-d.nodeW / 2},${-d.nodeH / 2})`;
|
| 80 |
+
}
|
| 81 |
return `translate(${pos.cx - d.nodeW / 2},${pos.cy - d.nodeH / 2})`;
|
| 82 |
});
|
| 83 |
|
|
|
|
| 87 |
const pa = positionByNode.get(src);
|
| 88 |
const pb = positionByNode.get(tgt);
|
| 89 |
if (pa === undefined || pb === undefined) return;
|
| 90 |
+
const sw = effNodeSize(src);
|
| 91 |
+
const tw = effNodeSize(tgt);
|
| 92 |
+
const srcRect = { cx: pa.cx, cy: pa.cy, nodeW: sw.nodeW, nodeH: sw.nodeH };
|
| 93 |
+
const tgtRect = { cx: pb.cx, cy: pb.cy, nodeW: tw.nodeW, nodeH: tw.nodeH };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
const seg = linkSegmentThroughNodeRects(srcRect, tgtRect, linkEndInsetPx);
|
| 95 |
d3.select(this)
|
| 96 |
.selectAll('path.gen-attr-dag-link-visible')
|
client/src/ts/attribution/genAttributeDagViewTextFlowMode.ts
CHANGED
|
@@ -2,8 +2,8 @@ import * as d3 from 'd3';
|
|
| 2 |
import { linkSegmentThroughNodeRects } from './genAttributeDagLinkSegment';
|
| 3 |
|
| 4 |
type TextFlowNodeLike = {
|
| 5 |
-
|
| 6 |
-
|
| 7 |
nodeW: number;
|
| 8 |
nodeH: number;
|
| 9 |
};
|
|
@@ -23,5 +23,5 @@ export function paintTextFlowLayout<LinkDatum, NodeDatum extends TextFlowNodeLik
|
|
| 23 |
.selectAll('path.gen-attr-dag-link-visible')
|
| 24 |
.attr('d', `M ${seg.x1} ${seg.y1} L ${seg.x2} ${seg.y2}`);
|
| 25 |
});
|
| 26 |
-
nodeSel.attr('transform', (d) => `translate(${d.
|
| 27 |
}
|
|
|
|
| 2 |
import { linkSegmentThroughNodeRects } from './genAttributeDagLinkSegment';
|
| 3 |
|
| 4 |
type TextFlowNodeLike = {
|
| 5 |
+
cx: number;
|
| 6 |
+
cy: number;
|
| 7 |
nodeW: number;
|
| 8 |
nodeH: number;
|
| 9 |
};
|
|
|
|
| 23 |
.selectAll('path.gen-attr-dag-link-visible')
|
| 24 |
.attr('d', `M ${seg.x1} ${seg.y1} L ${seg.x2} ${seg.y2}`);
|
| 25 |
});
|
| 26 |
+
nodeSel.attr('transform', (d) => `translate(${d.cx - d.nodeW / 2},${d.cy - d.nodeH / 2})`);
|
| 27 |
}
|
client/src/ts/gen_attribute.ts
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
| 25 |
} from './attribution/genAttributeDagPreprocess';
|
| 26 |
import {
|
| 27 |
initGenAttributeDagView,
|
|
|
|
|
|
|
| 28 |
type DagLayoutMode,
|
| 29 |
clampDagCompactness,
|
| 30 |
clampLinearArcAdjacentGap,
|
|
@@ -97,6 +99,8 @@ const GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_layout_mod
|
|
| 97 |
const GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_step_ms';
|
| 98 |
const GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_replay_pacing_mode';
|
| 99 |
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_total_s';
|
|
|
|
|
|
|
| 100 |
const GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY = 'info_radar_gen_attr_dag_hide_inactive_edges';
|
| 101 |
const GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY = 'info_radar_gen_attr_dag_hide_excluded_tokens';
|
| 102 |
const GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY =
|
|
@@ -379,6 +383,12 @@ function applyDagLayoutModeUi(): void {
|
|
| 379 |
const dagHideExcludedTokensInput = document.getElementById(
|
| 380 |
'gen_attr_dag_hide_excluded_tokens'
|
| 381 |
) as HTMLInputElement | null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
const dagHideInactiveEdgesInput = document.getElementById(
|
| 383 |
'gen_attr_dag_hide_inactive_edges'
|
| 384 |
) as HTMLInputElement | null;
|
|
@@ -408,6 +418,50 @@ if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = String(initialDagPlay
|
|
| 408 |
applyDagReplaySpeedUi();
|
| 409 |
|
| 410 |
const genAttrResultsNode = genAttrResultsEl.node() as HTMLElement | null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
function applyDagHideInactiveEdges(hide: boolean): void {
|
| 412 |
if (!genAttrResultsNode) return;
|
| 413 |
genAttrResultsNode.classList.toggle('gen-attr-dag-hide-inactive-edges', hide);
|
|
@@ -871,6 +925,18 @@ function isDagBusy(): boolean {
|
|
| 871 |
return inFlight || dagPlaybackTimer !== null || dagLastTokenDwellTimer !== null;
|
| 872 |
}
|
| 873 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 874 |
dagMeasureWidthInput?.addEventListener('change', () => {
|
| 875 |
const raw = parseInt(dagMeasureWidthInput.value, 10);
|
| 876 |
const w = Number.isFinite(raw)
|
|
@@ -883,14 +949,7 @@ dagMeasureWidthInput?.addEventListener('change', () => {
|
|
| 883 |
/* ignore */
|
| 884 |
}
|
| 885 |
dagHandle.setMeasureWidthPx(w);
|
| 886 |
-
|
| 887 |
-
const h = runnerHandle;
|
| 888 |
-
dagHandle.reset();
|
| 889 |
-
if (h && h.tokenCount > 0) {
|
| 890 |
-
replayRunnerStepsIntoDag(h, currentRunPromptSpans.length > 0 ? currentRunPromptSpans : undefined);
|
| 891 |
-
}
|
| 892 |
-
dagHandle.fitViewportToContent();
|
| 893 |
-
dagHandle.clearNodeSelection();
|
| 894 |
});
|
| 895 |
|
| 896 |
dagCompactnessInput?.addEventListener('change', () => {
|
|
@@ -903,14 +962,7 @@ dagCompactnessInput?.addEventListener('change', () => {
|
|
| 903 |
/* ignore */
|
| 904 |
}
|
| 905 |
dagHandle.setDagCompactness(c);
|
| 906 |
-
|
| 907 |
-
const h = runnerHandle;
|
| 908 |
-
dagHandle.reset();
|
| 909 |
-
if (h && h.tokenCount > 0) {
|
| 910 |
-
replayRunnerStepsIntoDag(h, currentRunPromptSpans.length > 0 ? currentRunPromptSpans : undefined);
|
| 911 |
-
}
|
| 912 |
-
dagHandle.fitViewportToContent();
|
| 913 |
-
dagHandle.clearNodeSelection();
|
| 914 |
});
|
| 915 |
|
| 916 |
dagEdgeTopPCoverageInput?.addEventListener('change', () => {
|
|
@@ -925,14 +977,7 @@ dagEdgeTopPCoverageInput?.addEventListener('change', () => {
|
|
| 925 |
/* ignore */
|
| 926 |
}
|
| 927 |
dagHandle.setEdgeTopPCoverage(c);
|
| 928 |
-
|
| 929 |
-
const h = runnerHandle;
|
| 930 |
-
dagHandle.reset();
|
| 931 |
-
if (h && h.tokenCount > 0) {
|
| 932 |
-
replayRunnerStepsIntoDag(h, currentRunPromptSpans.length > 0 ? currentRunPromptSpans : undefined);
|
| 933 |
-
}
|
| 934 |
-
dagHandle.fitViewportToContent();
|
| 935 |
-
dagHandle.clearNodeSelection();
|
| 936 |
});
|
| 937 |
|
| 938 |
dagLinearArcIntervalInput?.addEventListener('change', () => {
|
|
@@ -1300,7 +1345,7 @@ async function applyGenAttrCachedRun(
|
|
| 1300 |
}
|
| 1301 |
}
|
| 1302 |
|
| 1303 |
-
/**
|
| 1304 |
async function restoreGenAttrFromCachedRun(
|
| 1305 |
contentKey: string,
|
| 1306 |
shouldTouch: boolean,
|
|
|
|
| 25 |
} from './attribution/genAttributeDagPreprocess';
|
| 26 |
import {
|
| 27 |
initGenAttributeDagView,
|
| 28 |
+
setDagNodeCiVisualScaleEnabled,
|
| 29 |
+
setDagEdgeWeakenHighSurprisalEnabled,
|
| 30 |
type DagLayoutMode,
|
| 31 |
clampDagCompactness,
|
| 32 |
clampLinearArcAdjacentGap,
|
|
|
|
| 99 |
const GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_step_ms';
|
| 100 |
const GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_replay_pacing_mode';
|
| 101 |
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_total_s';
|
| 102 |
+
const GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY = 'info_radar_gen_attr_dag_node_ci_visual_scale';
|
| 103 |
+
const GEN_ATTR_DAG_EDGE_WEAKEN_HIGH_SURPRISAL_STORAGE_KEY = 'info_radar_gen_attr_dag_edge_weaken_high_surprisal';
|
| 104 |
const GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY = 'info_radar_gen_attr_dag_hide_inactive_edges';
|
| 105 |
const GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY = 'info_radar_gen_attr_dag_hide_excluded_tokens';
|
| 106 |
const GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY =
|
|
|
|
| 383 |
const dagHideExcludedTokensInput = document.getElementById(
|
| 384 |
'gen_attr_dag_hide_excluded_tokens'
|
| 385 |
) as HTMLInputElement | null;
|
| 386 |
+
const dagNodeCiVisualScaleInput = document.getElementById(
|
| 387 |
+
'gen_attr_dag_node_ci_visual_scale'
|
| 388 |
+
) as HTMLInputElement | null;
|
| 389 |
+
const dagEdgeWeakenHighSurprisalInput = document.getElementById(
|
| 390 |
+
'gen_attr_dag_edge_weaken_high_surprisal'
|
| 391 |
+
) as HTMLInputElement | null;
|
| 392 |
const dagHideInactiveEdgesInput = document.getElementById(
|
| 393 |
'gen_attr_dag_hide_inactive_edges'
|
| 394 |
) as HTMLInputElement | null;
|
|
|
|
| 418 |
applyDagReplaySpeedUi();
|
| 419 |
|
| 420 |
const genAttrResultsNode = genAttrResultsEl.node() as HTMLElement | null;
|
| 421 |
+
function readStoredDagNodeCiVisualScale(): boolean {
|
| 422 |
+
try {
|
| 423 |
+
const v = localStorage.getItem(GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY);
|
| 424 |
+
return v === null ? true : v === '1';
|
| 425 |
+
} catch {
|
| 426 |
+
return true;
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
const initialDagNodeCiVisualScale = readStoredDagNodeCiVisualScale();
|
| 430 |
+
if (dagNodeCiVisualScaleInput) dagNodeCiVisualScaleInput.checked = initialDagNodeCiVisualScale;
|
| 431 |
+
setDagNodeCiVisualScaleEnabled(initialDagNodeCiVisualScale);
|
| 432 |
+
dagNodeCiVisualScaleInput?.addEventListener('change', () => {
|
| 433 |
+
const enabled = dagNodeCiVisualScaleInput.checked;
|
| 434 |
+
try {
|
| 435 |
+
localStorage.setItem(GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY, enabled ? '1' : '0');
|
| 436 |
+
} catch {
|
| 437 |
+
/* ignore */
|
| 438 |
+
}
|
| 439 |
+
setDagNodeCiVisualScaleEnabled(enabled);
|
| 440 |
+
tryResetAndReplayDag();
|
| 441 |
+
});
|
| 442 |
+
|
| 443 |
+
function readStoredDagEdgeWeakenHighSurprisal(): boolean {
|
| 444 |
+
try {
|
| 445 |
+
const v = localStorage.getItem(GEN_ATTR_DAG_EDGE_WEAKEN_HIGH_SURPRISAL_STORAGE_KEY);
|
| 446 |
+
return v === null ? true : v === '1';
|
| 447 |
+
} catch {
|
| 448 |
+
return true;
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
const initialDagEdgeWeakenHighSurprisal = readStoredDagEdgeWeakenHighSurprisal();
|
| 452 |
+
if (dagEdgeWeakenHighSurprisalInput) dagEdgeWeakenHighSurprisalInput.checked = initialDagEdgeWeakenHighSurprisal;
|
| 453 |
+
setDagEdgeWeakenHighSurprisalEnabled(initialDagEdgeWeakenHighSurprisal);
|
| 454 |
+
dagEdgeWeakenHighSurprisalInput?.addEventListener('change', () => {
|
| 455 |
+
const enabled = dagEdgeWeakenHighSurprisalInput.checked;
|
| 456 |
+
try {
|
| 457 |
+
localStorage.setItem(GEN_ATTR_DAG_EDGE_WEAKEN_HIGH_SURPRISAL_STORAGE_KEY, enabled ? '1' : '0');
|
| 458 |
+
} catch {
|
| 459 |
+
/* ignore */
|
| 460 |
+
}
|
| 461 |
+
setDagEdgeWeakenHighSurprisalEnabled(enabled);
|
| 462 |
+
tryResetAndReplayDag();
|
| 463 |
+
});
|
| 464 |
+
|
| 465 |
function applyDagHideInactiveEdges(hide: boolean): void {
|
| 466 |
if (!genAttrResultsNode) return;
|
| 467 |
genAttrResultsNode.classList.toggle('gen-attr-dag-hide-inactive-edges', hide);
|
|
|
|
| 925 |
return inFlight || dagPlaybackTimer !== null || dagLastTokenDwellTimer !== null;
|
| 926 |
}
|
| 927 |
|
| 928 |
+
/** 非忙状态下 reset + replay + fit,供各设置项切换后复用。忙时为 no-op。 */
|
| 929 |
+
function tryResetAndReplayDag(): void {
|
| 930 |
+
if (isDagBusy()) return;
|
| 931 |
+
const h = runnerHandle;
|
| 932 |
+
dagHandle.reset();
|
| 933 |
+
if (h && h.tokenCount > 0) {
|
| 934 |
+
replayRunnerStepsIntoDag(h, currentRunPromptSpans.length > 0 ? currentRunPromptSpans : undefined);
|
| 935 |
+
}
|
| 936 |
+
dagHandle.fitViewportToContent();
|
| 937 |
+
dagHandle.clearNodeSelection();
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
dagMeasureWidthInput?.addEventListener('change', () => {
|
| 941 |
const raw = parseInt(dagMeasureWidthInput.value, 10);
|
| 942 |
const w = Number.isFinite(raw)
|
|
|
|
| 949 |
/* ignore */
|
| 950 |
}
|
| 951 |
dagHandle.setMeasureWidthPx(w);
|
| 952 |
+
tryResetAndReplayDag();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 953 |
});
|
| 954 |
|
| 955 |
dagCompactnessInput?.addEventListener('change', () => {
|
|
|
|
| 962 |
/* ignore */
|
| 963 |
}
|
| 964 |
dagHandle.setDagCompactness(c);
|
| 965 |
+
tryResetAndReplayDag();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 966 |
});
|
| 967 |
|
| 968 |
dagEdgeTopPCoverageInput?.addEventListener('change', () => {
|
|
|
|
| 977 |
/* ignore */
|
| 978 |
}
|
| 979 |
dagHandle.setEdgeTopPCoverage(c);
|
| 980 |
+
tryResetAndReplayDag();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 981 |
});
|
| 982 |
|
| 983 |
dagLinearArcIntervalInput?.addEventListener('change', () => {
|
|
|
|
| 1345 |
}
|
| 1346 |
}
|
| 1347 |
|
| 1348 |
+
/** 从缓存恢复运行;`shouldTouch` 为 true 时 touch MRU(下拉选中恒为 false,↑ 置顶走单独路径)。 */
|
| 1349 |
async function restoreGenAttrFromCachedRun(
|
| 1350 |
contentKey: string,
|
| 1351 |
shouldTouch: boolean,
|
client/src/ts/home.ts
CHANGED
|
@@ -22,6 +22,7 @@ const GEN_ATTRIBUTE_HOME_DEMO_SLUG: Record<'en' | 'zh', string> = {
|
|
| 22 |
en: 'Write a sonnet about love',
|
| 23 |
zh: '写一首绝句,主题是春天',
|
| 24 |
};
|
|
|
|
| 25 |
|
| 26 |
function applyGenAttributeNavCardHref(): void {
|
| 27 |
const a = document.querySelector<HTMLAnchorElement>('a.nav-landing-card[data-nav-page="genAttribute"]');
|
|
@@ -43,7 +44,20 @@ function syncGenAttributeCardPreviewVideo(theme: Theme): void {
|
|
| 43 |
v.src = DAG_PREVIEW_BY_THEME[theme];
|
| 44 |
}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
applyGenAttributeNavCardHref();
|
|
|
|
| 47 |
|
| 48 |
const apiPrefix = URLHandler.parameters['api'] || '';
|
| 49 |
const { api } = initializeCommonApp(apiPrefix);
|
|
|
|
| 22 |
en: 'Write a sonnet about love',
|
| 23 |
zh: '写一首绝句,主题是春天',
|
| 24 |
};
|
| 25 |
+
const GEN_ATTRIBUTE_BADGE_LINK = 'http://xhslink.com/o/A7VLi99aBvG';
|
| 26 |
|
| 27 |
function applyGenAttributeNavCardHref(): void {
|
| 28 |
const a = document.querySelector<HTMLAnchorElement>('a.nav-landing-card[data-nav-page="genAttribute"]');
|
|
|
|
| 44 |
v.src = DAG_PREVIEW_BY_THEME[theme];
|
| 45 |
}
|
| 46 |
|
| 47 |
+
function bindGenAttributeBadgeLink(): void {
|
| 48 |
+
const badge = document.querySelector<HTMLElement>(
|
| 49 |
+
'a.nav-landing-card[data-nav-page="genAttribute"] .nav-landing-card-badge'
|
| 50 |
+
);
|
| 51 |
+
if (!badge) return;
|
| 52 |
+
badge.addEventListener('click', (event: MouseEvent) => {
|
| 53 |
+
event.preventDefault();
|
| 54 |
+
event.stopPropagation();
|
| 55 |
+
window.open(GEN_ATTRIBUTE_BADGE_LINK, '_blank', 'noopener');
|
| 56 |
+
});
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
applyGenAttributeNavCardHref();
|
| 60 |
+
bindGenAttributeBadgeLink();
|
| 61 |
|
| 62 |
const apiPrefix = URLHandler.parameters['api'] || '';
|
| 63 |
const { api } = initializeCommonApp(apiPrefix);
|
client/src/ts/lang/translations.ts
CHANGED
|
@@ -29,6 +29,8 @@ export const translations: Translations = {
|
|
| 29 |
'- attribute a predicted token to its context': '- 将预测 token 归因到上下文',
|
| 30 |
'LLM Causal Flow': 'LLM Causal Flow 因果流',
|
| 31 |
'- explore the context-attribution DAG': '- 探索上下文归因的 DAG 关系图',
|
|
|
|
|
|
|
| 32 |
// 合成标题串(<title>):英文 key 与 injectPageMeta documentTitleEn 拼接一致
|
| 33 |
'Info Lens - A toolbox for exploring the informational nature of LLMs and language': 'Info Lens 信息透镜 - 用于探索 LLM 与语言的信息本质的工具箱',
|
| 34 |
"Info Highlight - highlight the 'informative' parts": 'Info Highlight 信息高亮 - 高亮“信息量大”的地方',
|
|
|
|
| 29 |
'- attribute a predicted token to its context': '- 将预测 token 归因到上下文',
|
| 30 |
'LLM Causal Flow': 'LLM Causal Flow 因果流',
|
| 31 |
'- explore the context-attribution DAG': '- 探索上下文归因的 DAG 关系图',
|
| 32 |
+
'100K+ plays on RedNote': '小红书 10万+次播放',
|
| 33 |
+
'Go to demo on RedNote: xhslink.com/o/A7VLi99aBvG': '在小红书打开demo:xhslink.com/o/A7VLi99aBvG)',
|
| 34 |
// 合成标题串(<title>):英文 key 与 injectPageMeta documentTitleEn 拼接一致
|
| 35 |
'Info Lens - A toolbox for exploring the informational nature of LLMs and language': 'Info Lens 信息透镜 - 用于探索 LLM 与语言的信息本质的工具箱',
|
| 36 |
"Info Highlight - highlight the 'informative' parts": 'Info Highlight 信息高亮 - 高亮“信息量大”的地方',
|
client/src/ts/utils/SurprisalColorConfig.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import * as d3 from "d3";
|
| 2 |
import { isFiniteNumber } from "./Util";
|
|
|
|
| 3 |
|
| 4 |
/**
|
| 5 |
* 惊讶度颜色配置模块
|
|
@@ -11,7 +12,7 @@ import { isFiniteNumber } from "./Util";
|
|
| 11 |
// ==========================================
|
| 12 |
|
| 13 |
/** Token surprisal 的最大值,用于颜色映射(默认上限,可被调用方传入的可选 max 覆盖) */
|
| 14 |
-
const TOKEN_SURPRISAL_MAX =
|
| 15 |
|
| 16 |
/** Byte surprisal 的最大值,用于颜色映射 */
|
| 17 |
const BYTE_SURPRISAL_MAX = 6;
|
|
|
|
| 1 |
import * as d3 from "d3";
|
| 2 |
import { isFiniteNumber } from "./Util";
|
| 3 |
+
import { REFERENCE_MAX_SURPRISAL_BITS } from "./surprisalMath";
|
| 4 |
|
| 5 |
/**
|
| 6 |
* 惊讶度颜色配置模块
|
|
|
|
| 12 |
// ==========================================
|
| 13 |
|
| 14 |
/** Token surprisal 的最大值,用于颜色映射(默认上限,可被调用方传入的可选 max 覆盖) */
|
| 15 |
+
const TOKEN_SURPRISAL_MAX = REFERENCE_MAX_SURPRISAL_BITS;
|
| 16 |
|
| 17 |
/** Byte surprisal 的最大值,用于颜色映射 */
|
| 18 |
const BYTE_SURPRISAL_MAX = 6;
|
client/src/ts/utils/cachedHistoryUi.ts
CHANGED
|
@@ -27,8 +27,7 @@ export type InitCachedHistoryQueryDropdownOptions = {
|
|
| 27 |
listMru: () => Promise<CachedHistoryListRow[]>;
|
| 28 |
/**
|
| 29 |
* 第一参为 {@link CachedHistoryListRow.contentKey}(与 `?content=` 一致)。
|
| 30 |
-
* 第二参
|
| 31 |
-
* 是否应对关联 MRU 执行 touch(悬停为 false)。
|
| 32 |
*/
|
| 33 |
onSelectEntry: (
|
| 34 |
contentKey: string,
|
|
@@ -40,7 +39,7 @@ export type InitCachedHistoryQueryDropdownOptions = {
|
|
| 40 |
};
|
| 41 |
|
| 42 |
/**
|
| 43 |
-
* 三页 Cached history 共用的「无 input + MRU 异步刷新 + 悬停预览」接线。
|
| 44 |
* 返回 `refreshList` 供 URL hydrate 等与下拉无关的路径刷新内存列表。
|
| 45 |
*/
|
| 46 |
export function initCachedHistoryQueryDropdown(
|
|
@@ -60,8 +59,8 @@ export function initCachedHistoryQueryDropdown(
|
|
| 60 |
filterHistoryByInput: false,
|
| 61 |
onSelect: () => {},
|
| 62 |
fillInputOnSelect: false,
|
| 63 |
-
onHistorySelect: (contentKey
|
| 64 |
-
void Promise.resolve(options.onSelectEntry(contentKey,
|
| 65 |
},
|
| 66 |
onRemove: options.onRemove,
|
| 67 |
onPromote: options.onPromote,
|
|
|
|
| 27 |
listMru: () => Promise<CachedHistoryListRow[]>;
|
| 28 |
/**
|
| 29 |
* 第一参为 {@link CachedHistoryListRow.contentKey}(与 `?content=` 一致)。
|
| 30 |
+
* 第二参恒为 false:列表点击或悬停预览均不 bump MRU,仅 {@link onPromote}(↑)会 touch。
|
|
|
|
| 31 |
*/
|
| 32 |
onSelectEntry: (
|
| 33 |
contentKey: string,
|
|
|
|
| 39 |
};
|
| 40 |
|
| 41 |
/**
|
| 42 |
+
* 三页 Cached history 共用的「无 input + MRU 异步刷新 + 悬停预览」接线;选中条目不写 MRU。
|
| 43 |
* 返回 `refreshList` 供 URL hydrate 等与下拉无关的路径刷新内存列表。
|
| 44 |
*/
|
| 45 |
export function initCachedHistoryQueryDropdown(
|
|
|
|
| 59 |
filterHistoryByInput: false,
|
| 60 |
onSelect: () => {},
|
| 61 |
fillInputOnSelect: false,
|
| 62 |
+
onHistorySelect: (contentKey) => {
|
| 63 |
+
void Promise.resolve(options.onSelectEntry(contentKey, false, ctx));
|
| 64 |
},
|
| 65 |
onRemove: options.onRemove,
|
| 66 |
onPromote: options.onPromote,
|
client/src/ts/utils/settingsMenuManager.ts
CHANGED
|
@@ -374,7 +374,7 @@ export class SettingsMenuManager {
|
|
| 374 |
}
|
| 375 |
|
| 376 |
private async handleVisitStatsClick(): Promise<void> {
|
| 377 |
-
/
|
| 378 |
const PAGE_ORDER = [
|
| 379 |
'index.html',
|
| 380 |
'analysis.html',
|
|
@@ -445,14 +445,19 @@ export class SettingsMenuManager {
|
|
| 445 |
};
|
| 446 |
|
| 447 |
const fetchAndRender = async (container: d3.Selection<HTMLDivElement, unknown, HTMLElement, any>) => {
|
| 448 |
-
container.
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
try {
|
| 457 |
const data = await this.api.getVisitStats();
|
| 458 |
if (!data?.success) throw new Error('bad');
|
|
@@ -460,6 +465,7 @@ export class SettingsMenuManager {
|
|
| 460 |
} catch {
|
| 461 |
block.text('Failed to load stats.');
|
| 462 |
}
|
|
|
|
| 463 |
};
|
| 464 |
|
| 465 |
showDialog({
|
|
|
|
| 374 |
}
|
| 375 |
|
| 376 |
private async handleVisitStatsClick(): Promise<void> {
|
| 377 |
+
// backend/visit_stats.py:_STATS_PAGE_ORDER / _STATS_API_ORDER / _STATS_OS_ORDER
|
| 378 |
const PAGE_ORDER = [
|
| 379 |
'index.html',
|
| 380 |
'analysis.html',
|
|
|
|
| 445 |
};
|
| 446 |
|
| 447 |
const fetchAndRender = async (container: d3.Selection<HTMLDivElement, unknown, HTMLElement, any>) => {
|
| 448 |
+
let block = container.select<HTMLDivElement>('div.visit-stats-body');
|
| 449 |
+
if (block.empty()) {
|
| 450 |
+
block = container
|
| 451 |
+
.append('div')
|
| 452 |
+
.attr('class', 'visit-stats-body')
|
| 453 |
+
.style('margin', '0')
|
| 454 |
+
.style('white-space', 'pre-wrap')
|
| 455 |
+
.style('font', 'inherit')
|
| 456 |
+
.style('font-size', '13px');
|
| 457 |
+
} else {
|
| 458 |
+
// 与 Model Management 一致:用透明度保留占位,避免清空 DOM 导致弹窗高度塌陷抖动
|
| 459 |
+
block.style('opacity', '0');
|
| 460 |
+
}
|
| 461 |
try {
|
| 462 |
const data = await this.api.getVisitStats();
|
| 463 |
if (!data?.success) throw new Error('bad');
|
|
|
|
| 465 |
} catch {
|
| 466 |
block.text('Failed to load stats.');
|
| 467 |
}
|
| 468 |
+
block.style('opacity', '1');
|
| 469 |
};
|
| 470 |
|
| 471 |
showDialog({
|
client/src/ts/utils/surprisalMath.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* 惊讶度与信息量的数学基础模块。
|
| 3 |
+
* 颜色映射相关常量见 {@link SurprisalColorConfig}。
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* 零信心概率基准 p₀:surprisal log₂(1/p₀) 视作单 token 的绝对信息量参照。
|
| 8 |
+
* 超过此值视为模型已无法有效预测,各处可视化统一在此封顶。
|
| 9 |
+
* 此处为 18 bit,大致对应模型的词表大小256K时的平均token概率。
|
| 10 |
+
*/
|
| 11 |
+
export const ZERO_CONFIDENCE_PROBABILITY_BASELINE = 2 ** -18;
|
| 12 |
+
|
| 13 |
+
/** 与 p₀ 对应的参照 surprisal 上界(bit);同时作为 token 着色标尺上限。 */
|
| 14 |
+
export const REFERENCE_MAX_SURPRISAL_BITS = Math.log2(1 / ZERO_CONFIDENCE_PROBABILITY_BASELINE);
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* 全信心概率阈值 p₁:surprisal 低于对应 bit 数时视作模型已充分自信,视觉上不放大节点。
|
| 18 |
+
* 此处为 3 bit,对应概率 > 1/8(约 12.5%)。
|
| 19 |
+
*/
|
| 20 |
+
export const FULL_CONFIDENCE_PROBABILITY_BASELINE = 2 ** -3;
|
| 21 |
+
|
| 22 |
+
/** 与 p₁ 对应的 surprisal 下界(bit);低于此值的节点 ciVisualScale 截断为 1×。 */
|
| 23 |
+
export const REFERENCE_NO_SURPRISAL_BITS = Math.log2(1 / FULL_CONFIDENCE_PROBABILITY_BASELINE);
|
| 24 |
+
|
| 25 |
+
function clamp01(n: number): number {
|
| 26 |
+
return Math.min(1, Math.max(0, n));
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/**
|
| 30 |
+
* 互信息率 α:在参照熵 log₂(1/p₀) 下,将「前文与目标 token 的可对齐程度」
|
| 31 |
+
* (log₂(1/p₀) − log₂(1/p)) / log₂(1/p₀) = log₂(p/p₀) / log₂(1/p₀) clamp 到 [0,1]。
|
| 32 |
+
* 低 surprisal → 高 α;仅用于本步入边透明度,不参与边筛选。缺省 `target_prob` 时返回 1(兼容旧缓存)。
|
| 33 |
+
*/
|
| 34 |
+
export function computeMutualInformationRatio(targetProb: number | undefined): number {
|
| 35 |
+
if (targetProb === undefined) return 1;
|
| 36 |
+
if (!Number.isFinite(targetProb) || targetProb <= 0) return 0;
|
| 37 |
+
return clamp01(
|
| 38 |
+
Math.log2(targetProb / ZERO_CONFIDENCE_PROBABILITY_BASELINE) / REFERENCE_MAX_SURPRISAL_BITS
|
| 39 |
+
);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* 条件信息量比率 CI:surprisal/max = (−log₂ p) / log₂(1/p₀) clamp 到 [0,1],
|
| 44 |
+
* 与 {@link computeMutualInformationRatio} 对称(同 p 下 CI + MI = 1)。
|
| 45 |
+
* 缺省 `target_prob` 时返回 0;非法或 p≤0 时返回 1。
|
| 46 |
+
*/
|
| 47 |
+
export function computeConditionalInformationRatio(targetProb: number | undefined): number {
|
| 48 |
+
if (targetProb === undefined) return 0;
|
| 49 |
+
if (!Number.isFinite(targetProb) || targetProb <= 0) return 1;
|
| 50 |
+
return clamp01(-Math.log2(targetProb) / REFERENCE_MAX_SURPRISAL_BITS);
|
| 51 |
+
}
|
data/demo/public/CN/政府工作报告极简版.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|