支持总时间和单步时间设置;追赶调度,排除渲染时间带来的误差;日志改进
Browse files- backend/access_log.py +10 -12
- backend/api/prediction_attribute.py +2 -1
- backend/completion_generator.py +7 -9
- backend/visit_stats.py +4 -3
- client/src/css/gen_attribute.scss +23 -0
- client/src/gen_attribute.html +21 -6
- client/src/ts/gen_attribute.ts +132 -13
backend/access_log.py
CHANGED
|
@@ -174,13 +174,14 @@ def log_openai_completions_request(
|
|
| 174 |
_request_counter += 1
|
| 175 |
request_id = _request_counter
|
| 176 |
|
| 177 |
-
preview =
|
| 178 |
p_preview = prompt[:preview] + "..." if len(prompt) > preview else prompt
|
| 179 |
details = (
|
| 180 |
f"req_id={request_id}, model='{model}', "
|
| 181 |
f"prompt='{p_preview}', chars={len(prompt)}"
|
| 182 |
)
|
| 183 |
_log_request("📥 openai completions 请求", details, client_ip)
|
|
|
|
| 184 |
return request_id
|
| 185 |
|
| 186 |
|
|
@@ -202,18 +203,15 @@ def log_prediction_attribute_request(
|
|
| 202 |
_request_counter += 1
|
| 203 |
request_id = _request_counter
|
| 204 |
|
| 205 |
-
|
| 206 |
-
c_preview =
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
if len(target_prediction) > preview
|
| 213 |
-
else target_prediction
|
| 214 |
-
)
|
| 215 |
details = (
|
| 216 |
-
f"req_id={request_id}, model={model!r}, context='{c_preview}', target='{
|
| 217 |
f"context_chars={len(context)}"
|
| 218 |
)
|
| 219 |
_log_request("📥 prediction_attribute 请求", details, client_ip)
|
|
|
|
| 174 |
_request_counter += 1
|
| 175 |
request_id = _request_counter
|
| 176 |
|
| 177 |
+
preview = 100
|
| 178 |
p_preview = prompt[:preview] + "..." if len(prompt) > preview else prompt
|
| 179 |
details = (
|
| 180 |
f"req_id={request_id}, model='{model}', "
|
| 181 |
f"prompt='{p_preview}', chars={len(prompt)}"
|
| 182 |
)
|
| 183 |
_log_request("📥 openai completions 请求", details, client_ip)
|
| 184 |
+
_hit_api("chat")
|
| 185 |
return request_id
|
| 186 |
|
| 187 |
|
|
|
|
| 203 |
_request_counter += 1
|
| 204 |
request_id = _request_counter
|
| 205 |
|
| 206 |
+
context_preview = 150
|
| 207 |
+
c_preview = (
|
| 208 |
+
context[:context_preview] + "..."
|
| 209 |
+
if len(context) > context_preview
|
| 210 |
+
else context
|
| 211 |
+
)
|
| 212 |
+
target_show = "<top-1>" if target_prediction is None else target_prediction
|
|
|
|
|
|
|
|
|
|
| 213 |
details = (
|
| 214 |
+
f"req_id={request_id}, model={model!r}, context='{c_preview}', target='{target_show}', "
|
| 215 |
f"context_chars={len(context)}"
|
| 216 |
)
|
| 217 |
_log_request("📥 prediction_attribute 请求", details, client_ip)
|
backend/api/prediction_attribute.py
CHANGED
|
@@ -71,9 +71,10 @@ def prediction_attribute(attribution_request):
|
|
| 71 |
|
| 72 |
elapsed = time.perf_counter() - start_time
|
| 73 |
tokens = len(result.get("token_attribution", []))
|
|
|
|
| 74 |
print(
|
| 75 |
f"\t📤 API prediction_attribute response: req_id={request_id}, "
|
| 76 |
-
f"tokens={tokens}, response_time={elapsed:.4f}s"
|
| 77 |
)
|
| 78 |
|
| 79 |
return {"success": True, **result}, 200
|
|
|
|
| 71 |
|
| 72 |
elapsed = time.perf_counter() - start_time
|
| 73 |
tokens = len(result.get("token_attribution", []))
|
| 74 |
+
target_token = result.get("target_token")
|
| 75 |
print(
|
| 76 |
f"\t📤 API prediction_attribute response: req_id={request_id}, "
|
| 77 |
+
f"target={target_token!r}, tokens={tokens}, response_time={elapsed:.4f}s"
|
| 78 |
)
|
| 79 |
|
| 80 |
return {"success": True, **result}, 200
|
backend/completion_generator.py
CHANGED
|
@@ -78,9 +78,7 @@ def _completion_without_generate(
|
|
| 78 |
|
| 79 |
def _print_completion_stream_delta(text: str, stream_end: bool) -> None:
|
| 80 |
"""接收 TextStreamer 切分好的增量片段,由本模块打印(与默认 TextStreamer 输出一致)。"""
|
| 81 |
-
|
| 82 |
-
if get_verbose():
|
| 83 |
-
print(text, flush=True, end="" if not stream_end else None)
|
| 84 |
|
| 85 |
|
| 86 |
def _compose_stream_delta(
|
|
@@ -429,12 +427,12 @@ def core_generate_from_text(
|
|
| 429 |
effective_max_new = remaining
|
| 430 |
else:
|
| 431 |
effective_max_new = min(max_tokens, remaining)
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
|
| 439 |
prompt_tokens = int(input_len)
|
| 440 |
# 主要防止:排队等推理锁期间用户已取消,拿到锁后在此短路,避免无意义进入 generate。
|
|
|
|
| 78 |
|
| 79 |
def _print_completion_stream_delta(text: str, stream_end: bool) -> None:
|
| 80 |
"""接收 TextStreamer 切分好的增量片段,由本模块打印(与默认 TextStreamer 输出一致)。"""
|
| 81 |
+
print(text, flush=True, end="" if not stream_end else None)
|
|
|
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
def _compose_stream_delta(
|
|
|
|
| 427 |
effective_max_new = remaining
|
| 428 |
else:
|
| 429 |
effective_max_new = min(max_tokens, remaining)
|
| 430 |
+
|
| 431 |
+
print(
|
| 432 |
+
f"📌 completion: 推理原文 (tokens={input_len}, ctx_limit={ctx_limit}, max_new={effective_max_new}):\n"
|
| 433 |
+
f"{formatted_text}",
|
| 434 |
+
end="", # 不换行, 用于和后续打印推理结果拼在一起
|
| 435 |
+
)
|
| 436 |
|
| 437 |
prompt_tokens = int(input_len)
|
| 438 |
# 主要防止:排队等推理锁期间用户已取消,拿到锁后在此短路,避免无意义进入 generate。
|
backend/visit_stats.py
CHANGED
|
@@ -63,12 +63,13 @@ def print_visit_summary():
|
|
| 63 |
os_cnt[o] += 1
|
| 64 |
os_order = ("ios", "android", "windows", "macos", "linux", "unknown")
|
| 65 |
os_pg = [f" {k}: {os_cnt[k]}" for k in os_order if os_cnt[k]]
|
|
|
|
| 66 |
|
| 67 |
-
body = ["========== [访问统计] ==========",
|
| 68 |
f"进程约 {h:.2f}h | 页面访问IP:{n_ip} | 真实活跃IP:{n_act}", "--- 活跃IP中OS统计 ---",
|
| 69 |
*(os_pg or [" (尚无)"]), "--- 页面活跃时间统计(秒) ---",
|
| 70 |
-
*(pg or [" (尚无)"]), "---
|
| 71 |
-
*[f" {k}: {apis.get(k, 0)}" for k in ("analyze", "analyze_semantic", "prediction_attribute")],
|
| 72 |
"=" * 42]
|
| 73 |
print("\n".join(body), flush=True)
|
| 74 |
|
|
|
|
| 63 |
os_cnt[o] += 1
|
| 64 |
os_order = ("ios", "android", "windows", "macos", "linux", "unknown")
|
| 65 |
os_pg = [f" {k}: {os_cnt[k]}" for k in os_order if os_cnt[k]]
|
| 66 |
+
now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
| 67 |
|
| 68 |
+
body = [f"========== [访问统计] {now} ==========",
|
| 69 |
f"进程约 {h:.2f}h | 页面访问IP:{n_ip} | 真实活跃IP:{n_act}", "--- 活跃IP中OS统计 ---",
|
| 70 |
*(os_pg or [" (尚无)"]), "--- 页面活跃时间统计(秒) ---",
|
| 71 |
+
*(pg or [" (尚无)"]), "--- API调用统计 ---",
|
| 72 |
+
*[f" {k}: {apis.get(k, 0)}" for k in ("analyze", "analyze_semantic", "prediction_attribute", "chat")],
|
| 73 |
"=" * 42]
|
| 74 |
print("\n".join(body), flush=True)
|
| 75 |
|
client/src/css/gen_attribute.scss
CHANGED
|
@@ -345,6 +345,29 @@
|
|
| 345 |
text-align: right;
|
| 346 |
}
|
| 347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
.attribution-exclude-prompt-patterns-input {
|
| 349 |
width: 100%;
|
| 350 |
box-sizing: border-box;
|
|
|
|
| 345 |
text-align: right;
|
| 346 |
}
|
| 347 |
|
| 348 |
+
.gen-attr-dag-replay-speed-row {
|
| 349 |
+
display: flex;
|
| 350 |
+
flex-wrap: wrap;
|
| 351 |
+
align-items: center;
|
| 352 |
+
gap: 6px 10px;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
select.gen-attr-dag-replay-mode-select {
|
| 356 |
+
width: auto;
|
| 357 |
+
min-width: 7.5rem;
|
| 358 |
+
text-align: left;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.gen-attr-dag-replay-value-wrap {
|
| 362 |
+
align-items: center;
|
| 363 |
+
gap: 4px;
|
| 364 |
+
|
| 365 |
+
// 仅非 hidden 时设 flex,否则会覆盖浏览器对 [hidden] 的 display:none(两列输入会同时出现)
|
| 366 |
+
&:not([hidden]) {
|
| 367 |
+
display: inline-flex;
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
.attribution-exclude-prompt-patterns-input {
|
| 372 |
width: 100%;
|
| 373 |
box-sizing: border-box;
|
client/src/gen_attribute.html
CHANGED
|
@@ -211,13 +211,28 @@
|
|
| 211 |
</span>
|
| 212 |
</div>
|
| 213 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 214 |
-
<span class="semantic-submode-group">
|
| 215 |
-
<label class="semantic-submode-label" for="
|
| 216 |
-
<
|
| 217 |
-
|
| 218 |
-
title="Delay in milliseconds between steps during DAG playback. Stored locally; the value is read when you press play—changing it mid-playback does not affect the current run."
|
| 219 |
data-i18n="title">
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
</span>
|
| 222 |
</div>
|
| 223 |
</section>
|
|
|
|
| 211 |
</span>
|
| 212 |
</div>
|
| 213 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 214 |
+
<span class="semantic-submode-group gen-attr-dag-replay-speed-row">
|
| 215 |
+
<label class="semantic-submode-label" for="gen_attr_dag_replay_mode" data-i18n>DAG replay speed</label>
|
| 216 |
+
<select id="gen_attr_dag_replay_mode" class="gen-attr-dag-replay-mode-select gen-attr-dag-measure-width-input"
|
| 217 |
+
title="Total duration vs. fixed delay per step. Waits shrink if a step is slow; long idle realigns the beat."
|
|
|
|
| 218 |
data-i18n="title">
|
| 219 |
+
<option value="total" data-i18n>Total time</option>
|
| 220 |
+
<option value="step" data-i18n>Step time</option>
|
| 221 |
+
</select>
|
| 222 |
+
<span id="gen_attr_dag_replay_total_wrap" class="gen-attr-dag-replay-value-wrap">
|
| 223 |
+
<input type="number" id="gen_attr_dag_playback_total_s" class="gen-attr-dag-measure-width-input"
|
| 224 |
+
value="7" min="1" max="3600" step="1"
|
| 225 |
+
title="Nominal delay = total seconds ÷ (steps − 1). Saved locally; applied when you press play."
|
| 226 |
+
data-i18n="title">
|
| 227 |
+
<span class="semantic-submode-label">s</span>
|
| 228 |
+
</span>
|
| 229 |
+
<span id="gen_attr_dag_replay_step_wrap" class="gen-attr-dag-replay-value-wrap" hidden>
|
| 230 |
+
<input type="number" id="gen_attr_dag_playback_step_ms" class="gen-attr-dag-measure-width-input"
|
| 231 |
+
value="200" min="0" max="10000" step="10"
|
| 232 |
+
title="Nominal ms between steps. Saved locally; applied when you press play (not mid-run)."
|
| 233 |
+
data-i18n="title">
|
| 234 |
+
<span class="semantic-submode-label">ms</span>
|
| 235 |
+
</span>
|
| 236 |
</span>
|
| 237 |
</div>
|
| 238 |
</section>
|
client/src/ts/gen_attribute.ts
CHANGED
|
@@ -78,8 +78,13 @@ const GEN_ATTR_MAX_TOKENS_STORAGE_KEY = 'info_radar_gen_attr_max_tokens';
|
|
| 78 |
const GEN_ATTR_MAX_TOKENS_DEFAULT = 100;
|
| 79 |
const GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY = 'info_radar_gen_attr_dag_measure_width';
|
| 80 |
const GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_step_ms';
|
|
|
|
|
|
|
| 81 |
const GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY = 'info_radar_gen_attr_dag_hide_inactive_edges';
|
| 82 |
|
|
|
|
|
|
|
|
|
|
| 83 |
const GEN_ATTR_DAG_MEASURE_WIDTH_DEFAULT = 500;
|
| 84 |
const GEN_ATTR_DAG_MEASURE_WIDTH_MIN = 200;
|
| 85 |
const GEN_ATTR_DAG_MEASURE_WIDTH_MAX = 4000;
|
|
@@ -88,6 +93,10 @@ const GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT = 200;
|
|
| 88 |
const GEN_ATTR_DAG_PLAYBACK_STEP_MS_MIN = 0;
|
| 89 |
const GEN_ATTR_DAG_PLAYBACK_STEP_MS_MAX = 10000;
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
const GENERATE_BTN_LABEL = 'Start';
|
| 92 |
const STOP_BTN_LABEL = 'Stop';
|
| 93 |
|
|
@@ -148,6 +157,34 @@ function readStoredDagPlaybackStepMs(): number {
|
|
| 148 |
return GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT;
|
| 149 |
}
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
const apiPrefix = URLHandler.parameters['api'] || '';
|
| 152 |
const bodyElement = d3.select('body').node() as Element;
|
| 153 |
const { totalSurprisalFormat, api } = initializeCommonApp(apiPrefix, bodyElement);
|
|
@@ -203,9 +240,31 @@ const maxTokensInput = document.getElementById('gen_attr_max_tokens') as HTMLInp
|
|
| 203 |
const dagMeasureWidthInput = document.getElementById(
|
| 204 |
'gen_attr_dag_measure_width'
|
| 205 |
) as HTMLInputElement | null;
|
|
|
|
| 206 |
const dagPlaybackStepMsInput = document.getElementById(
|
| 207 |
'gen_attr_dag_playback_step_ms'
|
| 208 |
) as HTMLInputElement | null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
const dagHideInactiveEdgesInput = document.getElementById(
|
| 210 |
'gen_attr_dag_hide_inactive_edges'
|
| 211 |
) as HTMLInputElement | null;
|
|
@@ -215,8 +274,15 @@ if (modelVariantSelect) modelVariantSelect.value = readStoredModelVariant();
|
|
| 215 |
if (maxTokensInput) maxTokensInput.value = String(readStoredMaxTokens());
|
| 216 |
const initialDagMeasureWidth = readStoredDagMeasureWidth();
|
| 217 |
if (dagMeasureWidthInput) dagMeasureWidthInput.value = String(initialDagMeasureWidth);
|
|
|
|
|
|
|
| 218 |
const initialDagPlaybackStepMs = readStoredDagPlaybackStepMs();
|
| 219 |
if (dagPlaybackStepMsInput) dagPlaybackStepMsInput.value = String(initialDagPlaybackStepMs);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
const genAttrResultsNode = genAttrResultsEl.node() as HTMLElement | null;
|
| 222 |
function applyDagHideInactiveEdges(hide: boolean): void {
|
|
@@ -265,6 +331,7 @@ maxTokensInput?.addEventListener('change', () => {
|
|
| 265 |
syncSubmitButtonState();
|
| 266 |
});
|
| 267 |
|
|
|
|
| 268 |
dagPlaybackStepMsInput?.addEventListener('change', () => {
|
| 269 |
const raw = parseInt(dagPlaybackStepMsInput.value, 10);
|
| 270 |
const ms = Number.isFinite(raw)
|
|
@@ -278,6 +345,29 @@ dagPlaybackStepMsInput?.addEventListener('change', () => {
|
|
| 278 |
}
|
| 279 |
});
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
function isSkipChatTemplate(): boolean {
|
| 282 |
return skipChatTemplateInput?.checked ?? false;
|
| 283 |
}
|
|
@@ -419,14 +509,30 @@ function scheduleDagLastTokenDwell(action: () => void, dwellMs: number = DAG_LAS
|
|
| 419 |
}, dwellMs);
|
| 420 |
}
|
| 421 |
|
| 422 |
-
/**
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
if (
|
| 429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
}
|
| 431 |
|
| 432 |
function stopDagPlayback(): void {
|
|
@@ -456,9 +562,12 @@ function handleDagPlaybackToggle(wantPlay: boolean): void {
|
|
| 456 |
dagHandle.reset(true);
|
| 457 |
dagPlaybackNextIndex = 0;
|
| 458 |
}
|
| 459 |
-
const
|
| 460 |
dagHandle.setDagPlaybackPlaying(true);
|
| 461 |
|
|
|
|
|
|
|
|
|
|
| 462 |
const isStalePlaybackHandle = (): boolean => {
|
| 463 |
if (runnerHandle === h) return false;
|
| 464 |
dagPlaybackTimer = null;
|
|
@@ -473,12 +582,22 @@ function handleDagPlaybackToggle(wantPlay: boolean): void {
|
|
| 473 |
dagHandle.setDagPlaybackPlaying(false);
|
| 474 |
};
|
| 475 |
|
| 476 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
dagPlaybackTimer = setTimeout(() => {
|
| 478 |
dagPlaybackTimer = null;
|
| 479 |
if (isStalePlaybackHandle()) return;
|
| 480 |
-
|
| 481 |
-
},
|
| 482 |
};
|
| 483 |
|
| 484 |
const tick = (): void => {
|
|
@@ -503,7 +622,7 @@ function handleDagPlaybackToggle(wantPlay: boolean): void {
|
|
| 503 |
});
|
| 504 |
return;
|
| 505 |
}
|
| 506 |
-
|
| 507 |
};
|
| 508 |
tick();
|
| 509 |
}
|
|
|
|
| 78 |
const GEN_ATTR_MAX_TOKENS_DEFAULT = 100;
|
| 79 |
const GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY = 'info_radar_gen_attr_dag_measure_width';
|
| 80 |
const GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_step_ms';
|
| 81 |
+
const GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_replay_pacing_mode';
|
| 82 |
+
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_total_s';
|
| 83 |
const GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY = 'info_radar_gen_attr_dag_hide_inactive_edges';
|
| 84 |
|
| 85 |
+
/** 步进回放节奏:`total`=整段剩余回放总时长内均分间隔;`step`=固定每步间隔(ms)。 */
|
| 86 |
+
type DagReplayPacingMode = 'total' | 'step';
|
| 87 |
+
|
| 88 |
const GEN_ATTR_DAG_MEASURE_WIDTH_DEFAULT = 500;
|
| 89 |
const GEN_ATTR_DAG_MEASURE_WIDTH_MIN = 200;
|
| 90 |
const GEN_ATTR_DAG_MEASURE_WIDTH_MAX = 4000;
|
|
|
|
| 93 |
const GEN_ATTR_DAG_PLAYBACK_STEP_MS_MIN = 0;
|
| 94 |
const GEN_ATTR_DAG_PLAYBACK_STEP_MS_MAX = 10000;
|
| 95 |
|
| 96 |
+
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT = 7;
|
| 97 |
+
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MIN = 1;
|
| 98 |
+
const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MAX = 3600;
|
| 99 |
+
|
| 100 |
const GENERATE_BTN_LABEL = 'Start';
|
| 101 |
const STOP_BTN_LABEL = 'Stop';
|
| 102 |
|
|
|
|
| 157 |
return GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT;
|
| 158 |
}
|
| 159 |
|
| 160 |
+
function clampDagPlaybackTotalS(n: number): number {
|
| 161 |
+
return Math.max(
|
| 162 |
+
GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MIN,
|
| 163 |
+
Math.min(GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MAX, Math.round(n))
|
| 164 |
+
);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
function readStoredDagPlaybackTotalS(): number {
|
| 168 |
+
try {
|
| 169 |
+
const v = localStorage.getItem(GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY);
|
| 170 |
+
const n = v !== null ? parseInt(v, 10) : NaN;
|
| 171 |
+
if (Number.isFinite(n)) return clampDagPlaybackTotalS(n);
|
| 172 |
+
} catch {
|
| 173 |
+
// ignore
|
| 174 |
+
}
|
| 175 |
+
return GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
function readStoredDagReplayPacingMode(): DagReplayPacingMode {
|
| 179 |
+
try {
|
| 180 |
+
const v = localStorage.getItem(GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY);
|
| 181 |
+
if (v === 'total' || v === 'step') return v;
|
| 182 |
+
} catch {
|
| 183 |
+
// ignore
|
| 184 |
+
}
|
| 185 |
+
return 'total';
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
const apiPrefix = URLHandler.parameters['api'] || '';
|
| 189 |
const bodyElement = d3.select('body').node() as Element;
|
| 190 |
const { totalSurprisalFormat, api } = initializeCommonApp(apiPrefix, bodyElement);
|
|
|
|
| 240 |
const dagMeasureWidthInput = document.getElementById(
|
| 241 |
'gen_attr_dag_measure_width'
|
| 242 |
) as HTMLInputElement | null;
|
| 243 |
+
/** 步进回放:固定间隔(ms)或总时长(s),由 {@link DagReplayPacingMode} 选择。 */
|
| 244 |
const dagPlaybackStepMsInput = document.getElementById(
|
| 245 |
'gen_attr_dag_playback_step_ms'
|
| 246 |
) as HTMLInputElement | null;
|
| 247 |
+
const dagReplayModeSelect = document.getElementById(
|
| 248 |
+
'gen_attr_dag_replay_mode'
|
| 249 |
+
) as HTMLSelectElement | null;
|
| 250 |
+
const dagPlaybackTotalSInput = document.getElementById(
|
| 251 |
+
'gen_attr_dag_playback_total_s'
|
| 252 |
+
) as HTMLInputElement | null;
|
| 253 |
+
const dagReplayTotalWrap = document.getElementById('gen_attr_dag_replay_total_wrap');
|
| 254 |
+
const dagReplayStepWrap = document.getElementById('gen_attr_dag_replay_step_wrap');
|
| 255 |
+
|
| 256 |
+
/** 与 `#gen_attr_dag_replay_mode` 同步;非法或缺失时视为 `total`。 */
|
| 257 |
+
function currentDagReplayPacingMode(): DagReplayPacingMode {
|
| 258 |
+
return dagReplayModeSelect?.value === 'step' ? 'step' : 'total';
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/** 切换下拉时更新 `hidden`;样式见 `.gen-attr-dag-replay-value-wrap:not([hidden])`。 */
|
| 262 |
+
function applyDagReplaySpeedUi(): void {
|
| 263 |
+
const mode = currentDagReplayPacingMode();
|
| 264 |
+
if (dagReplayTotalWrap) dagReplayTotalWrap.hidden = mode !== 'total';
|
| 265 |
+
if (dagReplayStepWrap) dagReplayStepWrap.hidden = mode !== 'step';
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
const dagHideInactiveEdgesInput = document.getElementById(
|
| 269 |
'gen_attr_dag_hide_inactive_edges'
|
| 270 |
) as HTMLInputElement | null;
|
|
|
|
| 274 |
if (maxTokensInput) maxTokensInput.value = String(readStoredMaxTokens());
|
| 275 |
const initialDagMeasureWidth = readStoredDagMeasureWidth();
|
| 276 |
if (dagMeasureWidthInput) dagMeasureWidthInput.value = String(initialDagMeasureWidth);
|
| 277 |
+
|
| 278 |
+
// DAG 回放节奏:步长 / 总时长 / 模式下拉 — 自 localStorage 恢复后再同步展示哪块输入
|
| 279 |
const initialDagPlaybackStepMs = readStoredDagPlaybackStepMs();
|
| 280 |
if (dagPlaybackStepMsInput) dagPlaybackStepMsInput.value = String(initialDagPlaybackStepMs);
|
| 281 |
+
const initialDagReplayPacingMode = readStoredDagReplayPacingMode();
|
| 282 |
+
if (dagReplayModeSelect) dagReplayModeSelect.value = initialDagReplayPacingMode;
|
| 283 |
+
const initialDagPlaybackTotalS = readStoredDagPlaybackTotalS();
|
| 284 |
+
if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = String(initialDagPlaybackTotalS);
|
| 285 |
+
applyDagReplaySpeedUi();
|
| 286 |
|
| 287 |
const genAttrResultsNode = genAttrResultsEl.node() as HTMLElement | null;
|
| 288 |
function applyDagHideInactiveEdges(hide: boolean): void {
|
|
|
|
| 331 |
syncSubmitButtonState();
|
| 332 |
});
|
| 333 |
|
| 334 |
+
// DAG 回放节奏(与上节「DAG 测量宽度」无关;宽度 listener 在后文)
|
| 335 |
dagPlaybackStepMsInput?.addEventListener('change', () => {
|
| 336 |
const raw = parseInt(dagPlaybackStepMsInput.value, 10);
|
| 337 |
const ms = Number.isFinite(raw)
|
|
|
|
| 345 |
}
|
| 346 |
});
|
| 347 |
|
| 348 |
+
dagReplayModeSelect?.addEventListener('change', () => {
|
| 349 |
+
const mode = currentDagReplayPacingMode();
|
| 350 |
+
try {
|
| 351 |
+
localStorage.setItem(GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY, mode);
|
| 352 |
+
} catch {
|
| 353 |
+
/* ignore */
|
| 354 |
+
}
|
| 355 |
+
applyDagReplaySpeedUi();
|
| 356 |
+
});
|
| 357 |
+
|
| 358 |
+
dagPlaybackTotalSInput?.addEventListener('change', () => {
|
| 359 |
+
const raw = parseInt(dagPlaybackTotalSInput.value, 10);
|
| 360 |
+
const s = Number.isFinite(raw)
|
| 361 |
+
? clampDagPlaybackTotalS(raw)
|
| 362 |
+
: GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT;
|
| 363 |
+
dagPlaybackTotalSInput.value = String(s);
|
| 364 |
+
try {
|
| 365 |
+
localStorage.setItem(GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY, String(s));
|
| 366 |
+
} catch {
|
| 367 |
+
/* ignore */
|
| 368 |
+
}
|
| 369 |
+
});
|
| 370 |
+
|
| 371 |
function isSkipChatTemplate(): boolean {
|
| 372 |
return skipChatTemplateInput?.checked ?? false;
|
| 373 |
}
|
|
|
|
| 509 |
}, dwellMs);
|
| 510 |
}
|
| 511 |
|
| 512 |
+
/**
|
| 513 |
+
* 点击播放时:读界面值并写回规范化结果,得到本轮「相邻两步 DAG 更新」之间的延时(ms)。
|
| 514 |
+
* - `step`:固定间隔。
|
| 515 |
+
* - `total`:`totalS` 按**整段 DAG 步数**均分间隔,与「从头回放」相同(`fullStepCount - 1` 段);不管当前 `dagPlaybackNextIndex`。首步立即执行,与末 token dwell 无关。
|
| 516 |
+
*/
|
| 517 |
+
function resolveDagPlaybackStepDelayMsOnPlay(fullStepCount: number): number {
|
| 518 |
+
if (currentDagReplayPacingMode() === 'step') {
|
| 519 |
+
const raw = parseInt(dagPlaybackStepMsInput?.value ?? '', 10);
|
| 520 |
+
const ms = Number.isFinite(raw)
|
| 521 |
+
? clampDagPlaybackStepMs(raw)
|
| 522 |
+
: readStoredDagPlaybackStepMs();
|
| 523 |
+
if (dagPlaybackStepMsInput) dagPlaybackStepMsInput.value = String(ms);
|
| 524 |
+
return ms;
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
const rawS = parseInt(dagPlaybackTotalSInput?.value ?? '', 10);
|
| 528 |
+
const totalS = Number.isFinite(rawS)
|
| 529 |
+
? clampDagPlaybackTotalS(rawS)
|
| 530 |
+
: readStoredDagPlaybackTotalS();
|
| 531 |
+
if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = String(totalS);
|
| 532 |
+
|
| 533 |
+
const transitionCount = Math.max(0, fullStepCount - 1);
|
| 534 |
+
if (transitionCount <= 0) return 0;
|
| 535 |
+
return Math.round((totalS * 1000) / transitionCount);
|
| 536 |
}
|
| 537 |
|
| 538 |
function stopDagPlayback(): void {
|
|
|
|
| 562 |
dagHandle.reset(true);
|
| 563 |
dagPlaybackNextIndex = 0;
|
| 564 |
}
|
| 565 |
+
const stepDelayMs = resolveDagPlaybackStepDelayMsOnPlay(steps.length);
|
| 566 |
dagHandle.setDagPlaybackPlaying(true);
|
| 567 |
|
| 568 |
+
/** 相邻两步「理想触发」之间的名义间隔;与 {@link resolveDagPlaybackStepDelayMsOnPlay} 一致。 */
|
| 569 |
+
let nextDue = performance.now();
|
| 570 |
+
|
| 571 |
const isStalePlaybackHandle = (): boolean => {
|
| 572 |
if (runnerHandle === h) return false;
|
| 573 |
dagPlaybackTimer = null;
|
|
|
|
| 582 |
dagHandle.setDagPlaybackPlaying(false);
|
| 583 |
};
|
| 584 |
|
| 585 |
+
/**
|
| 586 |
+
* 步间节拍:理想时刻 `nextDue` 每次前进 `stepDelayMs`,实际等待 `max(0, nextDue - now)`。
|
| 587 |
+
* 若已迟到(`delay === 0`),则 `nextDue = now + stepDelayMs` 重锚,避免长时间暂停 / 后台节流后连发多步。
|
| 588 |
+
*/
|
| 589 |
+
const scheduleNextPlaybackTick = (): void => {
|
| 590 |
+
const now = performance.now();
|
| 591 |
+
nextDue += stepDelayMs;
|
| 592 |
+
let delay = Math.max(0, nextDue - now);
|
| 593 |
+
if (delay === 0) {
|
| 594 |
+
nextDue = now + stepDelayMs;
|
| 595 |
+
}
|
| 596 |
dagPlaybackTimer = setTimeout(() => {
|
| 597 |
dagPlaybackTimer = null;
|
| 598 |
if (isStalePlaybackHandle()) return;
|
| 599 |
+
tick();
|
| 600 |
+
}, delay);
|
| 601 |
};
|
| 602 |
|
| 603 |
const tick = (): void => {
|
|
|
|
| 622 |
});
|
| 623 |
return;
|
| 624 |
}
|
| 625 |
+
scheduleNextPlaybackTick();
|
| 626 |
};
|
| 627 |
tick();
|
| 628 |
}
|