dag支持thinking模式;动画速度改进;其它小改进
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +2 -2
- backend/api/analyze.py +4 -4
- backend/api/client_activity.py +1 -1
- backend/api/model_switch.py +3 -3
- backend/api/openai_completions.py +14 -3
- backend/core/completion_generator.py +6 -5
- backend/core/language_checker.py +5 -5
- backend/core/prediction_attributor.py +6 -6
- backend/core/semantic_analyzer.py +5 -5
- backend/models/model_manager.py +58 -69
- backend/platform/access_log.py +6 -2
- backend/platform/app_context.py +19 -7
- backend/platform/visit_stats.py +2 -1
- client/src/assets/demos/causal_flow/CoT | 多“跳”推理.json +0 -0
- client/src/assets/demos/causal_flow/CoT | 苏州所在省的省会.json +8 -5
- client/src/assets/demos/causal_flow/order.json +26 -0
- client/src/assets/images/{dag-cot.png → dag-cot.mov} +2 -2
- client/src/causal_flow.html +14 -7
- client/src/chat.html +13 -5
- client/src/css/base/_narrow-ios-form-font-tail.scss +14 -0
- client/src/css/base/_responsive.scss +32 -12
- client/src/css/components/_chat-input-panel.scss +39 -0
- client/src/css/components/_semantic-analysis.scss +0 -8
- client/src/css/components/dialog.scss +17 -0
- client/src/css/pages/analysis.scss +4 -0
- client/src/css/pages/attribution.scss +4 -1
- client/src/css/pages/causal_flow.scss +7 -46
- client/src/css/pages/chat.scss +4 -0
- client/src/css/pages/compare.scss +4 -0
- client/src/features/analysis/infoDensityRenderManager.ts +6 -5
- client/src/features/causal_flow/bundledDemos.ts +2 -2
- client/src/features/causal_flow/genAttributeBundledDemoManifest.generated.ts +2 -1
- client/src/features/chat/chatPromptTemplateMode.ts +4 -19
- client/src/pages/analysis/index.ts +10 -9
- client/src/pages/attribution/index.ts +3 -12
- client/src/pages/causal_flow/index.ts +243 -334
- client/src/pages/chat/index.ts +23 -5
- client/src/pages/home/index.ts +8 -5
- client/src/scripts/genAttributeDemoManifestPlugin.js +85 -10
- client/src/scripts/injectPageMetaIntoHtml.js +2 -2
- client/src/shared/api/completionsClient.ts +10 -2
- client/src/shared/controllers/serverDemoController.ts +4 -3
- client/src/shared/core/responsive.ts +5 -3
- client/src/shared/cross/adminManager.ts +8 -6
- client/src/shared/cross/digitsMergeManager.ts +4 -2
- client/src/shared/cross/panelSplitStorage.ts +11 -17
- client/src/shared/cross/queryHistory.ts +4 -3
- client/src/shared/cross/semanticResultCache.ts +12 -12
- client/src/shared/cross/semanticThresholdManager.ts +3 -2
- client/src/shared/cross/settingsMenuManager.ts +4 -1
Dockerfile
CHANGED
|
@@ -63,5 +63,5 @@ EXPOSE 7860
|
|
| 63 |
# 在CPU basic 上使用0.6b模型能达到及格的速度
|
| 64 |
# 在CPU upgrade 上使用1.7b模型能达到及格的速度
|
| 65 |
# 在本地M5 16G芯片上使用4b模型能达到及格的速度(瓶颈是内存大小);M5 16G内存仅能同时支持一种分析模型(信息密度分析或语义分析)
|
| 66 |
-
CMD ["python", "run.py", "--no_auto_load", "--port", "7860", "--
|
| 67 |
-
# CMD ["python", "run.py", "--no_auto_load", "--port", "7860", "--
|
|
|
|
| 63 |
# 在CPU basic 上使用0.6b模型能达到及格的速度
|
| 64 |
# 在CPU upgrade 上使用1.7b模型能达到及格的速度
|
| 65 |
# 在本地M5 16G芯片上使用4b模型能达到及格的速度(瓶颈是内存大小);M5 16G内存仅能同时支持一种分析模型(信息密度分析或语义分析)
|
| 66 |
+
CMD ["python", "run.py", "--no_auto_load", "--port", "7860", "--base_model", "qwen3-1.7b", "--instruct_model", "qwen3-1.7b-instruct"]
|
| 67 |
+
# CMD ["python", "run.py", "--no_auto_load", "--port", "7860", "--base_model", "qwen3-0.6b", "--instruct_model", "qwen3-0.6b-instruct"]
|
backend/api/analyze.py
CHANGED
|
@@ -6,7 +6,7 @@ import queue
|
|
| 6 |
import threading
|
| 7 |
from typing import Optional
|
| 8 |
from backend.platform.schemas import create_empty_analysis_result
|
| 9 |
-
from backend.models.model_manager import project_registry,
|
| 10 |
from model_paths import resolve_hf_path
|
| 11 |
from backend.platform.oom import exit_if_oom
|
| 12 |
from backend.api.sse_utils import (
|
|
@@ -86,7 +86,7 @@ def _validate_and_prepare_request(analyze_request):
|
|
| 86 |
# 获取默认模型(使用模块级上下文以获取持久化的当前活动模型)
|
| 87 |
from backend.platform.app_context import get_app_context
|
| 88 |
context = get_app_context(prefer_module_context=True)
|
| 89 |
-
default_model = context.
|
| 90 |
|
| 91 |
# 处理 default、None 或空字符串,使用默认模型
|
| 92 |
if not model or model == 'default' or model == '':
|
|
@@ -119,7 +119,7 @@ def _load_project_with_error_handling(model):
|
|
| 119 |
p = project_registry.get(model)
|
| 120 |
if p is None:
|
| 121 |
from backend.platform.app_context import get_app_context
|
| 122 |
-
from backend.models.model_manager import
|
| 123 |
|
| 124 |
context = get_app_context(prefer_module_context=True)
|
| 125 |
if context.model_loading:
|
|
@@ -129,7 +129,7 @@ def _load_project_with_error_handling(model):
|
|
| 129 |
# 懒加载模式 (--no_auto_load):首次请求仅初始化主槽位(权重 + QwenLM 项目)
|
| 130 |
if getattr(context.args, 'no_auto_load', False):
|
| 131 |
try:
|
| 132 |
-
|
| 133 |
p = project_registry.get(model)
|
| 134 |
except Exception as e: # noqa: BLE001
|
| 135 |
import traceback
|
|
|
|
| 6 |
import threading
|
| 7 |
from typing import Optional
|
| 8 |
from backend.platform.schemas import create_empty_analysis_result
|
| 9 |
+
from backend.models.model_manager import project_registry, DEFAULT_BASE_MODEL, inference_lock
|
| 10 |
from model_paths import resolve_hf_path
|
| 11 |
from backend.platform.oom import exit_if_oom
|
| 12 |
from backend.api.sse_utils import (
|
|
|
|
| 86 |
# 获取默认模型(使用模块级上下文以获取持久化的当前活动模型)
|
| 87 |
from backend.platform.app_context import get_app_context
|
| 88 |
context = get_app_context(prefer_module_context=True)
|
| 89 |
+
default_model = context.base_model_id if context.base_model_id else DEFAULT_BASE_MODEL
|
| 90 |
|
| 91 |
# 处理 default、None 或空字符串,使用默认模型
|
| 92 |
if not model or model == 'default' or model == '':
|
|
|
|
| 119 |
p = project_registry.get(model)
|
| 120 |
if p is None:
|
| 121 |
from backend.platform.app_context import get_app_context
|
| 122 |
+
from backend.models.model_manager import ensure_base_slot_ready
|
| 123 |
|
| 124 |
context = get_app_context(prefer_module_context=True)
|
| 125 |
if context.model_loading:
|
|
|
|
| 129 |
# 懒加载模式 (--no_auto_load):首次请求仅初始化主槽位(权重 + QwenLM 项目)
|
| 130 |
if getattr(context.args, 'no_auto_load', False):
|
| 131 |
try:
|
| 132 |
+
ensure_base_slot_ready()
|
| 133 |
p = project_registry.get(model)
|
| 134 |
except Exception as e: # noqa: BLE001
|
| 135 |
import traceback
|
backend/api/client_activity.py
CHANGED
|
@@ -47,6 +47,6 @@ def client_activity_report(activity_body=None):
|
|
| 47 |
if _sparse_page_activity_log_cum(cum):
|
| 48 |
log_request(
|
| 49 |
"📄 页面活跃",
|
| 50 |
-
f"path={log_path!r} total_sec={cum} delta_sec={dlt}",
|
| 51 |
)
|
| 52 |
return {"ok": True}
|
|
|
|
| 47 |
if _sparse_page_activity_log_cum(cum):
|
| 48 |
log_request(
|
| 49 |
"📄 页面活跃",
|
| 50 |
+
f"path(sampled)={log_path!r} total_sec={cum} delta_sec={dlt}",
|
| 51 |
)
|
| 52 |
return {"ok": True}
|
backend/api/model_switch.py
CHANGED
|
@@ -49,7 +49,7 @@ def get_current_model():
|
|
| 49 |
|
| 50 |
return {
|
| 51 |
'success': True,
|
| 52 |
-
'model': context.
|
| 53 |
'loading': context.model_loading,
|
| 54 |
'device_type': device_type,
|
| 55 |
'use_int8': os.environ.get('FORCE_INT8') == '1',
|
|
@@ -115,7 +115,7 @@ def switch_model(switch_request):
|
|
| 115 |
|
| 116 |
# 使用模块级上下文以确保状态修改持久化(不会被后续请求重置)
|
| 117 |
context = get_app_context(prefer_module_context=True)
|
| 118 |
-
current_model = context.
|
| 119 |
|
| 120 |
# 保存当前环境变量配置(用于回滚)
|
| 121 |
old_force_int8 = os.environ.get('FORCE_INT8')
|
|
@@ -223,7 +223,7 @@ def switch_model(switch_request):
|
|
| 223 |
return (
|
| 224 |
{
|
| 225 |
'success': False,
|
| 226 |
-
'message': '在线模型切换已禁用,请通过命令行 --
|
| 227 |
},
|
| 228 |
501,
|
| 229 |
)
|
|
|
|
| 49 |
|
| 50 |
return {
|
| 51 |
'success': True,
|
| 52 |
+
'model': context.base_model_id,
|
| 53 |
'loading': context.model_loading,
|
| 54 |
'device_type': device_type,
|
| 55 |
'use_int8': os.environ.get('FORCE_INT8') == '1',
|
|
|
|
| 115 |
|
| 116 |
# 使用模块级上下文以确保状态修改持久化(不会被后续请求重置)
|
| 117 |
context = get_app_context(prefer_module_context=True)
|
| 118 |
+
current_model = context.base_model_id
|
| 119 |
|
| 120 |
# 保存当前环境变量配置(用于回滚)
|
| 121 |
old_force_int8 = os.environ.get('FORCE_INT8')
|
|
|
|
| 223 |
return (
|
| 224 |
{
|
| 225 |
'success': False,
|
| 226 |
+
'message': '在线模型切换已禁用,请通过命令行 --base_model / --instruct_model 指定后重启服务',
|
| 227 |
},
|
| 228 |
501,
|
| 229 |
)
|
backend/api/openai_completions.py
CHANGED
|
@@ -7,7 +7,7 @@ import time
|
|
| 7 |
import traceback
|
| 8 |
from typing import Any, Callable, Dict, List, Optional, Tuple
|
| 9 |
|
| 10 |
-
from backend.models.model_manager import inference_lock,
|
| 11 |
from backend.core.prediction_attributor import slot_for_prediction_attr_model
|
| 12 |
from backend.platform.oom import exit_if_oom, is_oom_error
|
| 13 |
from backend.core.completion_generator import (
|
|
@@ -54,7 +54,7 @@ def _build_response(
|
|
| 54 |
"id": "cmpl-stub-info-radar",
|
| 55 |
"object": "text_completion",
|
| 56 |
"created": int(time.time()),
|
| 57 |
-
"model":
|
| 58 |
"choices": [
|
| 59 |
{
|
| 60 |
"text": completion_text,
|
|
@@ -322,6 +322,14 @@ def completions_prompt(completions_prompt_request):
|
|
| 322 |
return {"success": False, "message": "system 必须为字符串"}, 400
|
| 323 |
system_opt = system_raw
|
| 324 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
client_ip = get_client_ip()
|
| 326 |
from backend.platform.access_log import log_openai_completions_prompt_request
|
| 327 |
|
|
@@ -329,6 +337,7 @@ def completions_prompt(completions_prompt_request):
|
|
| 329 |
model,
|
| 330 |
user_prompt=prompt,
|
| 331 |
system=system_opt,
|
|
|
|
| 332 |
client_ip=client_ip,
|
| 333 |
)
|
| 334 |
|
|
@@ -338,7 +347,9 @@ def completions_prompt(completions_prompt_request):
|
|
| 338 |
return {"success": False, "message": str(e)}, 400
|
| 339 |
|
| 340 |
try:
|
| 341 |
-
prompt_used = apply_chat_template_for_completion(
|
|
|
|
|
|
|
| 342 |
except PromptTooLongError as e:
|
| 343 |
return {"success": False, "message": str(e)}, 400
|
| 344 |
|
|
|
|
| 7 |
import traceback
|
| 8 |
from typing import Any, Callable, Dict, List, Optional, Tuple
|
| 9 |
|
| 10 |
+
from backend.models.model_manager import inference_lock, get_instruct_model_display_name
|
| 11 |
from backend.core.prediction_attributor import slot_for_prediction_attr_model
|
| 12 |
from backend.platform.oom import exit_if_oom, is_oom_error
|
| 13 |
from backend.core.completion_generator import (
|
|
|
|
| 54 |
"id": "cmpl-stub-info-radar",
|
| 55 |
"object": "text_completion",
|
| 56 |
"created": int(time.time()),
|
| 57 |
+
"model": get_instruct_model_display_name(),
|
| 58 |
"choices": [
|
| 59 |
{
|
| 60 |
"text": completion_text,
|
|
|
|
| 322 |
return {"success": False, "message": "system 必须为字符串"}, 400
|
| 323 |
system_opt = system_raw
|
| 324 |
|
| 325 |
+
enable_thinking_raw = completions_prompt_request.get("enable_thinking")
|
| 326 |
+
if enable_thinking_raw is None:
|
| 327 |
+
enable_thinking = False
|
| 328 |
+
elif not isinstance(enable_thinking_raw, bool):
|
| 329 |
+
return {"success": False, "message": "enable_thinking 必须为布尔值"}, 400
|
| 330 |
+
else:
|
| 331 |
+
enable_thinking = enable_thinking_raw
|
| 332 |
+
|
| 333 |
client_ip = get_client_ip()
|
| 334 |
from backend.platform.access_log import log_openai_completions_prompt_request
|
| 335 |
|
|
|
|
| 337 |
model,
|
| 338 |
user_prompt=prompt,
|
| 339 |
system=system_opt,
|
| 340 |
+
enable_thinking=enable_thinking,
|
| 341 |
client_ip=client_ip,
|
| 342 |
)
|
| 343 |
|
|
|
|
| 347 |
return {"success": False, "message": str(e)}, 400
|
| 348 |
|
| 349 |
try:
|
| 350 |
+
prompt_used = apply_chat_template_for_completion(
|
| 351 |
+
prompt, system_opt, slot=slot, enable_thinking=enable_thinking
|
| 352 |
+
)
|
| 353 |
except PromptTooLongError as e:
|
| 354 |
return {"success": False, "message": str(e)}, 400
|
| 355 |
|
backend/core/completion_generator.py
CHANGED
|
@@ -19,7 +19,7 @@ from transformers import StoppingCriteria, StoppingCriteriaList, TextStreamer
|
|
| 19 |
from backend.platform.format import round_to_sig_figs
|
| 20 |
from backend.platform.app_context import get_verbose
|
| 21 |
from backend.models.device import DeviceManager
|
| 22 |
-
from backend.models.model_manager import ModelSlot,
|
| 23 |
from .pred_topk_format import pred_topk_pairs_from_probs_1d
|
| 24 |
from backend.platform.runtime_config import DEFAULT_TOPK
|
| 25 |
|
|
@@ -408,7 +408,7 @@ def core_generate_from_text(
|
|
| 408 |
(续写文本, finish_reason, prompt_tokens, completion_tokens, 续写段 bpe_strings, ttft_s)。
|
| 409 |
ttft_s 为自 ``model.generate`` 起至首次产出续写片段的秒数;仅取消时为 ``None``。
|
| 410 |
"""
|
| 411 |
-
tokenizer, model, device =
|
| 412 |
ctx_limit = completion_max_token_length
|
| 413 |
|
| 414 |
model.eval()
|
|
@@ -518,14 +518,15 @@ def apply_chat_template_for_completion(
|
|
| 518 |
user_content: str,
|
| 519 |
system: Optional[str] = None,
|
| 520 |
*,
|
| 521 |
-
slot: ModelSlot = ModelSlot.
|
|
|
|
| 522 |
) -> str:
|
| 523 |
"""
|
| 524 |
将单条 user 文本套用到 tokenizer chat template,返回实际送入 core_generate_from_text 的字符串。
|
| 525 |
|
| 526 |
调用方未传入 ``system``(即 ``None``)时仅拼装单条 user 消息;传入字符串时(含 ``\"\"``、仅空白)
|
| 527 |
原样作为 chat template 的 system 段,不做裁剪或改写。长度与上下文上限由 ``core_generate_from_text``
|
| 528 |
-
在生成前校验。slot 控制使用哪个槽位的 tokenizer(base 传 ModelSlot.
|
| 529 |
"""
|
| 530 |
tokenizer, _, _ = ensure_slot_weights_loaded(slot)
|
| 531 |
if system is None:
|
|
@@ -539,7 +540,7 @@ def apply_chat_template_for_completion(
|
|
| 539 |
messages,
|
| 540 |
tokenize=False,
|
| 541 |
add_generation_prompt=True,
|
| 542 |
-
enable_thinking=
|
| 543 |
)
|
| 544 |
|
| 545 |
|
|
|
|
| 19 |
from backend.platform.format import round_to_sig_figs
|
| 20 |
from backend.platform.app_context import get_verbose
|
| 21 |
from backend.models.device import DeviceManager
|
| 22 |
+
from backend.models.model_manager import ModelSlot, ensure_instruct_slot_ready, ensure_slot_weights_loaded
|
| 23 |
from .pred_topk_format import pred_topk_pairs_from_probs_1d
|
| 24 |
from backend.platform.runtime_config import DEFAULT_TOPK
|
| 25 |
|
|
|
|
| 408 |
(续写文本, finish_reason, prompt_tokens, completion_tokens, 续写段 bpe_strings, ttft_s)。
|
| 409 |
ttft_s 为自 ``model.generate`` 起至首次产出续写片段的秒数;仅取消时为 ``None``。
|
| 410 |
"""
|
| 411 |
+
tokenizer, model, device = ensure_instruct_slot_ready()
|
| 412 |
ctx_limit = completion_max_token_length
|
| 413 |
|
| 414 |
model.eval()
|
|
|
|
| 518 |
user_content: str,
|
| 519 |
system: Optional[str] = None,
|
| 520 |
*,
|
| 521 |
+
slot: ModelSlot = ModelSlot.INSTRUCT,
|
| 522 |
+
enable_thinking: bool = False,
|
| 523 |
) -> str:
|
| 524 |
"""
|
| 525 |
将单条 user 文本套用到 tokenizer chat template,返回实际送入 core_generate_from_text 的字符串。
|
| 526 |
|
| 527 |
调用方未传入 ``system``(即 ``None``)时仅拼装单条 user 消息;传入字符串时(含 ``\"\"``、仅空白)
|
| 528 |
原样作为 chat template 的 system 段,不做裁剪或改写。长度与上下文上限由 ``core_generate_from_text``
|
| 529 |
+
在生成前校验。slot 控制使用哪个槽位的 tokenizer(base 传 ModelSlot.BASE)。
|
| 530 |
"""
|
| 531 |
tokenizer, _, _ = ensure_slot_weights_loaded(slot)
|
| 532 |
if system is None:
|
|
|
|
| 540 |
messages,
|
| 541 |
tokenize=False,
|
| 542 |
add_generation_prompt=True,
|
| 543 |
+
enable_thinking=enable_thinking,
|
| 544 |
)
|
| 545 |
|
| 546 |
|
backend/core/language_checker.py
CHANGED
|
@@ -8,7 +8,7 @@ from backend.models.class_register import register_model, REGISTERED_MODELS
|
|
| 8 |
from backend.models.device import DeviceManager
|
| 9 |
from backend.models.model_manager import ensure_model_loaded
|
| 10 |
from backend.platform.runtime_config import load_runtime_config, DEFAULT_TOPK
|
| 11 |
-
from model_paths import
|
| 12 |
|
| 13 |
# 按 id(model) 缓存「仅含 BOS/等价起始符一步 forward」得到的末位词表 logits(全词表,不随分析文本变)
|
| 14 |
_bos_first_position_logits_cache: Dict[int, torch.Tensor] = {}
|
|
@@ -109,7 +109,7 @@ class QwenLM(AbstractLanguageChecker):
|
|
| 109 |
"""
|
| 110 |
def __init__(self, model_path=None, model_name=None):
|
| 111 |
super(QwenLM, self).__init__()
|
| 112 |
-
model_name = model_name or getattr(self.__class__, '_registered_model_name',
|
| 113 |
if model_path is not None and str(model_path).strip():
|
| 114 |
resolved = str(model_path).strip()
|
| 115 |
else:
|
|
@@ -392,13 +392,13 @@ class QwenLM(AbstractLanguageChecker):
|
|
| 392 |
|
| 393 |
|
| 394 |
# ============================================================
|
| 395 |
-
# 自动注册:根据 MODEL_PATHS 与
|
| 396 |
# ============================================================
|
| 397 |
# 只需要在 model_paths.py 中添加模型路径,即可自动注册
|
| 398 |
# 无需手动创建子类,实现 DRY 原则
|
| 399 |
def _auto_register_models():
|
| 400 |
-
"""自动注册 MODEL_PATHS 与
|
| 401 |
-
for model_name in (*MODEL_PATHS.keys(), *
|
| 402 |
if model_name not in REGISTERED_MODELS:
|
| 403 |
# 动态创建模型类并注册
|
| 404 |
# 使用闭包捕获当前 model_name
|
|
|
|
| 8 |
from backend.models.device import DeviceManager
|
| 9 |
from backend.models.model_manager import ensure_model_loaded
|
| 10 |
from backend.platform.runtime_config import load_runtime_config, DEFAULT_TOPK
|
| 11 |
+
from model_paths import DEFAULT_BASE_MODEL, INSTRUCT_MODEL_PATHS, MODEL_PATHS, resolve_hf_path
|
| 12 |
|
| 13 |
# 按 id(model) 缓存「仅含 BOS/等价起始符一步 forward」得到的末位词表 logits(全词表,不随分析文本变)
|
| 14 |
_bos_first_position_logits_cache: Dict[int, torch.Tensor] = {}
|
|
|
|
| 109 |
"""
|
| 110 |
def __init__(self, model_path=None, model_name=None):
|
| 111 |
super(QwenLM, self).__init__()
|
| 112 |
+
model_name = model_name or getattr(self.__class__, '_registered_model_name', DEFAULT_BASE_MODEL)
|
| 113 |
if model_path is not None and str(model_path).strip():
|
| 114 |
resolved = str(model_path).strip()
|
| 115 |
else:
|
|
|
|
| 392 |
|
| 393 |
|
| 394 |
# ============================================================
|
| 395 |
+
# 自动注册:根据 MODEL_PATHS 与 INSTRUCT_MODEL_PATHS 自动注册所有模型
|
| 396 |
# ============================================================
|
| 397 |
# 只需要在 model_paths.py 中添加模型路径,即可自动注册
|
| 398 |
# 无需手动创建子类,实现 DRY 原则
|
| 399 |
def _auto_register_models():
|
| 400 |
+
"""自动注册 MODEL_PATHS 与 INSTRUCT_MODEL_PATHS 中的所有模型"""
|
| 401 |
+
for model_name in (*MODEL_PATHS.keys(), *INSTRUCT_MODEL_PATHS.keys()):
|
| 402 |
if model_name not in REGISTERED_MODELS:
|
| 403 |
# 动态创建模型类并注册
|
| 404 |
# 使用闭包捕获当前 model_name
|
backend/core/prediction_attributor.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
预测归因:对任意上下文的下一个 token 预测,计算指定候选 token 的 logit
|
| 3 |
对输入各 token embedding 的梯度,以梯度 L2 范数作为归因分。
|
| 4 |
|
| 5 |
-
由请求参数 `model` 选择权重槽位:base 为主槽位(--
|
| 6 |
"""
|
| 7 |
|
| 8 |
import math
|
|
@@ -15,8 +15,8 @@ from backend.models.device import DeviceManager
|
|
| 15 |
from backend.models.model_manager import (
|
| 16 |
ModelSlot,
|
| 17 |
ensure_slot_weights_loaded,
|
| 18 |
-
|
| 19 |
-
|
| 20 |
)
|
| 21 |
from .next_token_topk import decode_topk_ids_to_strings_and_rounded_probs, DEFAULT_NEXT_TOKEN_TOPK
|
| 22 |
|
|
@@ -41,9 +41,9 @@ PREDICTION_ATTR_MODEL_INSTRUCT = "instruct"
|
|
| 41 |
|
| 42 |
def slot_for_prediction_attr_model(model: str) -> ModelSlot:
|
| 43 |
if model == PREDICTION_ATTR_MODEL_BASE:
|
| 44 |
-
return ModelSlot.
|
| 45 |
if model == PREDICTION_ATTR_MODEL_INSTRUCT:
|
| 46 |
-
return ModelSlot.
|
| 47 |
raise ValueError(
|
| 48 |
f"Unsupported model {model!r}; only {PREDICTION_ATTR_MODEL_BASE!r} and "
|
| 49 |
f"{PREDICTION_ATTR_MODEL_INSTRUCT!r} are supported."
|
|
@@ -80,7 +80,7 @@ def analyze_prediction_attribution(
|
|
| 80 |
slot = slot_for_prediction_attr_model(model)
|
| 81 |
tokenizer, hf_model, device = ensure_slot_weights_loaded(slot)
|
| 82 |
model_display = (
|
| 83 |
-
|
| 84 |
)
|
| 85 |
|
| 86 |
if target_prediction is not None and target_token_id is not None:
|
|
|
|
| 2 |
预测归因:对任意上下文的下一个 token 预测,计算指定候选 token 的 logit
|
| 3 |
对输入各 token embedding 的梯度,以梯度 L2 范数作为归因分。
|
| 4 |
|
| 5 |
+
由请求参数 `model` 选择权重槽位:base 为主槽位(--base_model),instruct 为 instruct 槽位(--instruct_model)。
|
| 6 |
"""
|
| 7 |
|
| 8 |
import math
|
|
|
|
| 15 |
from backend.models.model_manager import (
|
| 16 |
ModelSlot,
|
| 17 |
ensure_slot_weights_loaded,
|
| 18 |
+
get_base_model_display_name,
|
| 19 |
+
get_instruct_model_display_name,
|
| 20 |
)
|
| 21 |
from .next_token_topk import decode_topk_ids_to_strings_and_rounded_probs, DEFAULT_NEXT_TOKEN_TOPK
|
| 22 |
|
|
|
|
| 41 |
|
| 42 |
def slot_for_prediction_attr_model(model: str) -> ModelSlot:
|
| 43 |
if model == PREDICTION_ATTR_MODEL_BASE:
|
| 44 |
+
return ModelSlot.BASE
|
| 45 |
if model == PREDICTION_ATTR_MODEL_INSTRUCT:
|
| 46 |
+
return ModelSlot.INSTRUCT
|
| 47 |
raise ValueError(
|
| 48 |
f"Unsupported model {model!r}; only {PREDICTION_ATTR_MODEL_BASE!r} and "
|
| 49 |
f"{PREDICTION_ATTR_MODEL_INSTRUCT!r} are supported."
|
|
|
|
| 80 |
slot = slot_for_prediction_attr_model(model)
|
| 81 |
tokenizer, hf_model, device = ensure_slot_weights_loaded(slot)
|
| 82 |
model_display = (
|
| 83 |
+
get_base_model_display_name() if slot == ModelSlot.BASE else get_instruct_model_display_name()
|
| 84 |
)
|
| 85 |
|
| 86 |
if target_prediction is not None and target_token_id is not None:
|
backend/core/semantic_analyzer.py
CHANGED
|
@@ -8,7 +8,7 @@ Semantic analysis:基于 instruct 模型提取原文 token 与 query 的相关
|
|
| 8 |
|
| 9 |
count/fill_blank 按概率加权(Σ pᵢ·zᵢ)。
|
| 10 |
|
| 11 |
-
模型由 --
|
| 12 |
"""
|
| 13 |
|
| 14 |
import gc
|
|
@@ -19,7 +19,7 @@ import torch
|
|
| 19 |
|
| 20 |
from backend.platform.format import round_to_sig_figs
|
| 21 |
from backend.models.device import DeviceManager
|
| 22 |
-
from backend.models.model_manager import
|
| 23 |
from .next_token_topk import decode_topk_ids_to_strings_and_rounded_probs, DEFAULT_NEXT_TOKEN_TOPK
|
| 24 |
from backend.platform.runtime_config import get_semantic_max_token_length
|
| 25 |
|
|
@@ -178,7 +178,7 @@ def _analyze_logits_gradient(
|
|
| 178 |
|
| 179 |
if full_match_degree_only:
|
| 180 |
return {
|
| 181 |
-
"model":
|
| 182 |
"token_attention": [],
|
| 183 |
"full_match_degree": full_match_degree,
|
| 184 |
}
|
|
@@ -235,7 +235,7 @@ def _analyze_logits_gradient(
|
|
| 235 |
print(f"⚠️ token_attention 中有 {nan_count} 个 score 为 NaN/Inf,已替换为 0。")
|
| 236 |
|
| 237 |
out = {
|
| 238 |
-
"model":
|
| 239 |
"token_attention": token_attention,
|
| 240 |
"full_match_degree": full_match_degree,
|
| 241 |
}
|
|
@@ -270,7 +270,7 @@ def analyze_semantic(
|
|
| 270 |
Returns:
|
| 271 |
{"model", "token_attention", "full_match_degree"};debug_info=True 时包含 debug_info 对象
|
| 272 |
"""
|
| 273 |
-
tokenizer, model, device =
|
| 274 |
return _analyze_logits_gradient(
|
| 275 |
query, text, tokenizer, model, device,
|
| 276 |
submode_override=submode_override,
|
|
|
|
| 8 |
|
| 9 |
count/fill_blank 按概率加权(Σ pᵢ·zᵢ)。
|
| 10 |
|
| 11 |
+
模型由 --instruct_model 参数指定,默认 qwen3-0.6b-instruct
|
| 12 |
"""
|
| 13 |
|
| 14 |
import gc
|
|
|
|
| 19 |
|
| 20 |
from backend.platform.format import round_to_sig_figs
|
| 21 |
from backend.models.device import DeviceManager
|
| 22 |
+
from backend.models.model_manager import ensure_instruct_slot_ready, get_instruct_model_display_name
|
| 23 |
from .next_token_topk import decode_topk_ids_to_strings_and_rounded_probs, DEFAULT_NEXT_TOKEN_TOPK
|
| 24 |
from backend.platform.runtime_config import get_semantic_max_token_length
|
| 25 |
|
|
|
|
| 178 |
|
| 179 |
if full_match_degree_only:
|
| 180 |
return {
|
| 181 |
+
"model": get_instruct_model_display_name(),
|
| 182 |
"token_attention": [],
|
| 183 |
"full_match_degree": full_match_degree,
|
| 184 |
}
|
|
|
|
| 235 |
print(f"⚠️ token_attention 中有 {nan_count} 个 score 为 NaN/Inf,已替换为 0。")
|
| 236 |
|
| 237 |
out = {
|
| 238 |
+
"model": get_instruct_model_display_name(),
|
| 239 |
"token_attention": token_attention,
|
| 240 |
"full_match_degree": full_match_degree,
|
| 241 |
}
|
|
|
|
| 270 |
Returns:
|
| 271 |
{"model", "token_attention", "full_match_degree"};debug_info=True 时包含 debug_info 对象
|
| 272 |
"""
|
| 273 |
+
tokenizer, model, device = ensure_instruct_slot_ready()
|
| 274 |
return _analyze_logits_gradient(
|
| 275 |
query, text, tokenizer, model, device,
|
| 276 |
submode_override=submode_override,
|
backend/models/model_manager.py
CHANGED
|
@@ -1,4 +1,10 @@
|
|
| 1 |
-
"""模型管理
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from enum import Enum
|
| 3 |
import threading
|
| 4 |
|
|
@@ -7,55 +13,52 @@ from backend.models.project_registry import ModelRegistry
|
|
| 7 |
from backend.models.device import DeviceManager
|
| 8 |
from backend.models.model_loader import attn_implementation_for_device, load_causal_lm, load_tokenizer
|
| 9 |
|
| 10 |
-
from model_paths import
|
| 11 |
|
| 12 |
project_registry = ModelRegistry(REGISTERED_MODELS)
|
| 13 |
_init_lock = threading.Lock()
|
| 14 |
|
| 15 |
-
# 统一推理锁:信息密度分析与
|
| 16 |
inference_lock = threading.Lock()
|
| 17 |
|
| 18 |
-
# 按 HuggingFace 路径去重的已加载模型缓存(
|
| 19 |
_hf_load_lock = threading.Lock()
|
| 20 |
_hf_loaded: dict[str, tuple] = {}
|
| 21 |
|
| 22 |
|
| 23 |
class ModelSlot(str, Enum):
|
| 24 |
-
"""与 CLI --
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
|
| 29 |
|
| 30 |
-
|
| 31 |
-
CONFIGURED_SLOTS: tuple[ModelSlot, ...] = (ModelSlot.MAIN, ModelSlot.SEMANTIC)
|
| 32 |
|
| 33 |
|
| 34 |
def _resolved_hf_path_for_slot(slot: ModelSlot) -> str:
|
| 35 |
"""由应用上下文解析槽位对应的 HuggingFace 路径(或本地路径字符串)。"""
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
raw = DEFAULT_SEMANTIC_MODEL
|
| 52 |
-
return resolve_hf_path(raw)
|
| 53 |
raise ValueError(f"unknown ModelSlot: {slot!r}")
|
| 54 |
|
| 55 |
|
| 56 |
def ensure_slot_weights_loaded(slot: ModelSlot):
|
| 57 |
"""
|
| 58 |
-
加载指定槽位权重(若未缓存)
|
| 59 |
返回 (tokenizer, model, device)。
|
| 60 |
"""
|
| 61 |
return ensure_model_loaded(_resolved_hf_path_for_slot(slot))
|
|
@@ -99,24 +102,23 @@ def ensure_project_loaded(project_name: str):
|
|
| 99 |
try:
|
| 100 |
return project_registry.ensure_loaded(project_name)
|
| 101 |
except KeyError:
|
| 102 |
-
# Re-raise to allow caller to format message uniformly.
|
| 103 |
raise
|
| 104 |
except Exception as exc: # noqa: BLE001 - propagate detailed message
|
| 105 |
raise RuntimeError(f"模型 '{project_name}' 加载失败: {exc}") from exc
|
| 106 |
|
| 107 |
|
| 108 |
-
def
|
| 109 |
"""
|
| 110 |
-
信息密度路径:在
|
| 111 |
-
|
| 112 |
"""
|
| 113 |
from backend.platform.app_context import get_app_context
|
| 114 |
|
| 115 |
context = get_app_context(prefer_module_context=True)
|
| 116 |
-
selected_name = context.
|
| 117 |
|
| 118 |
if not selected_name:
|
| 119 |
-
raise ValueError("未指定模型
|
| 120 |
|
| 121 |
if selected_name in project_registry:
|
| 122 |
_ensure_default_project_ready(selected_name)
|
|
@@ -135,7 +137,7 @@ def _register_main_qwenlm_if_needed():
|
|
| 135 |
def preload_all_slots():
|
| 136 |
"""
|
| 137 |
启动预载(非 --no_auto_load):对 CONFIGURED_SLOTS 各解析 HF 路径,去重后加载全部权重,
|
| 138 |
-
再注册
|
| 139 |
"""
|
| 140 |
from backend.platform.app_context import get_app_context
|
| 141 |
|
|
@@ -146,45 +148,43 @@ def preload_all_slots():
|
|
| 146 |
with _init_lock:
|
| 147 |
for path in paths:
|
| 148 |
ensure_model_loaded(path)
|
| 149 |
-
|
| 150 |
|
| 151 |
|
| 152 |
def ensure_slot_ready(slot: ModelSlot):
|
| 153 |
"""
|
| 154 |
-
槽位业务就绪
|
| 155 |
|
| 156 |
- 两槽位均先保证 HF 权重已加载,返回 (tokenizer, model, device)。
|
| 157 |
-
-
|
| 158 |
-
|
| 159 |
-
懒加载时:信息密度调 ensure_main_slot_ready();语义/续写调 ensure_semantic_slot_ready()。
|
| 160 |
"""
|
| 161 |
from backend.platform.app_context import get_app_context
|
| 162 |
|
| 163 |
get_app_context(prefer_module_context=True)
|
| 164 |
|
| 165 |
-
if slot == ModelSlot.
|
| 166 |
with _init_lock:
|
| 167 |
-
out = ensure_slot_weights_loaded(ModelSlot.
|
| 168 |
-
|
| 169 |
return out
|
| 170 |
-
if slot == ModelSlot.
|
| 171 |
-
return ensure_slot_weights_loaded(ModelSlot.
|
| 172 |
raise ValueError(f"unknown ModelSlot: {slot!r}")
|
| 173 |
|
| 174 |
|
| 175 |
-
def
|
| 176 |
-
"""
|
| 177 |
-
return ensure_slot_ready(ModelSlot.
|
| 178 |
|
| 179 |
|
| 180 |
-
def
|
| 181 |
-
"""
|
| 182 |
-
return ensure_slot_ready(ModelSlot.
|
| 183 |
|
| 184 |
|
| 185 |
def get_current_model_max_token_length() -> int:
|
| 186 |
"""
|
| 187 |
-
查询当前生效模型的 max_token_length 参数。
|
| 188 |
优先从已加载的模型实例获取,未加载时取 default_model.default_cpu_machine 配置。
|
| 189 |
"""
|
| 190 |
from backend.platform.app_context import get_app_context
|
|
@@ -192,7 +192,7 @@ def get_current_model_max_token_length() -> int:
|
|
| 192 |
|
| 193 |
try:
|
| 194 |
context = get_app_context(prefer_module_context=True)
|
| 195 |
-
model_name = context.
|
| 196 |
except RuntimeError:
|
| 197 |
model_name = "default_model"
|
| 198 |
|
|
@@ -212,22 +212,11 @@ def _ensure_default_project_ready(selected_name: str):
|
|
| 212 |
project_registry.ensure_loaded(selected_name)
|
| 213 |
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
def get_semantic_model_display_name() -> str:
|
| 220 |
-
"""返回 semantic 槽位 HuggingFace 路径(用于结果中的 model 字段)"""
|
| 221 |
-
return _resolved_hf_path_for_slot(ModelSlot.SEMANTIC)
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
def ensure_main_model_loaded():
|
| 225 |
-
"""
|
| 226 |
-
仅需主模型前向、且不必经过 project_registry 时(如 attribution):MAIN 槽位权重。
|
| 227 |
-
"""
|
| 228 |
-
return ensure_slot_weights_loaded(ModelSlot.MAIN)
|
| 229 |
|
| 230 |
|
| 231 |
-
def
|
| 232 |
-
"""返回
|
| 233 |
-
return _resolved_hf_path_for_slot(ModelSlot.
|
|
|
|
| 1 |
+
"""模型管理:base / instruct 双槽位,HF 权重缓存共用。
|
| 2 |
+
|
| 3 |
+
加载约定(由简到繁):
|
| 4 |
+
- ``ensure_slot_weights_loaded(slot)``:仅保证该槽位 HF 权重在 ``_hf_loaded`` 中(归因、tokenize)。
|
| 5 |
+
- ``ensure_slot_ready(slot)``:槽位可推理;base 另挂 ``project_registry`` / QwenLM(信息密度)。
|
| 6 |
+
- 业务入口:信息密度 ``ensure_base_slot_ready()``;语义 / 续写 ``ensure_instruct_slot_ready()``。
|
| 7 |
+
"""
|
| 8 |
from enum import Enum
|
| 9 |
import threading
|
| 10 |
|
|
|
|
| 13 |
from backend.models.device import DeviceManager
|
| 14 |
from backend.models.model_loader import attn_implementation_for_device, load_causal_lm, load_tokenizer
|
| 15 |
|
| 16 |
+
from model_paths import DEFAULT_BASE_MODEL, DEFAULT_INSTRUCT_MODEL, resolve_hf_path
|
| 17 |
|
| 18 |
project_registry = ModelRegistry(REGISTERED_MODELS)
|
| 19 |
_init_lock = threading.Lock()
|
| 20 |
|
| 21 |
+
# 统一推理锁:信息密度分析与 instruct 路径共用,确保模型推理串行执行
|
| 22 |
inference_lock = threading.Lock()
|
| 23 |
|
| 24 |
+
# 按 HuggingFace 路径去重的已加载模型缓存(两槽位共用)
|
| 25 |
_hf_load_lock = threading.Lock()
|
| 26 |
_hf_loaded: dict[str, tuple] = {}
|
| 27 |
|
| 28 |
|
| 29 |
class ModelSlot(str, Enum):
|
| 30 |
+
"""与 CLI --base_model / --instruct_model 对应的两个对等槽位。"""
|
| 31 |
|
| 32 |
+
BASE = "base"
|
| 33 |
+
INSTRUCT = "instruct"
|
| 34 |
|
| 35 |
|
| 36 |
+
CONFIGURED_SLOTS: tuple[ModelSlot, ...] = (ModelSlot.BASE, ModelSlot.INSTRUCT)
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
def _resolved_hf_path_for_slot(slot: ModelSlot) -> str:
|
| 40 |
"""由应用上下文解析槽位对应的 HuggingFace 路径(或本地路径字符串)。"""
|
| 41 |
+
from backend.platform.app_context import get_app_context
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
context = get_app_context(prefer_module_context=True)
|
| 45 |
+
except RuntimeError:
|
| 46 |
+
if slot == ModelSlot.BASE:
|
| 47 |
+
return resolve_hf_path(DEFAULT_BASE_MODEL)
|
| 48 |
+
if slot == ModelSlot.INSTRUCT:
|
| 49 |
+
return resolve_hf_path(DEFAULT_INSTRUCT_MODEL)
|
| 50 |
+
raise ValueError(f"unknown ModelSlot: {slot!r}") from None
|
| 51 |
+
|
| 52 |
+
if slot == ModelSlot.BASE:
|
| 53 |
+
return resolve_hf_path(context.base_model_id or DEFAULT_BASE_MODEL)
|
| 54 |
+
if slot == ModelSlot.INSTRUCT:
|
| 55 |
+
return resolve_hf_path(context.instruct_model_id or DEFAULT_INSTRUCT_MODEL)
|
|
|
|
|
|
|
| 56 |
raise ValueError(f"unknown ModelSlot: {slot!r}")
|
| 57 |
|
| 58 |
|
| 59 |
def ensure_slot_weights_loaded(slot: ModelSlot):
|
| 60 |
"""
|
| 61 |
+
加载指定槽位权重(若未缓存)。
|
| 62 |
返回 (tokenizer, model, device)。
|
| 63 |
"""
|
| 64 |
return ensure_model_loaded(_resolved_hf_path_for_slot(slot))
|
|
|
|
| 102 |
try:
|
| 103 |
return project_registry.ensure_loaded(project_name)
|
| 104 |
except KeyError:
|
|
|
|
| 105 |
raise
|
| 106 |
except Exception as exc: # noqa: BLE001 - propagate detailed message
|
| 107 |
raise RuntimeError(f"模型 '{project_name}' 加载失败: {exc}") from exc
|
| 108 |
|
| 109 |
|
| 110 |
+
def _register_base_qwenlm_if_needed():
|
| 111 |
"""
|
| 112 |
+
信息密度路径:在 base 槽位权重已就绪后,注册 project_registry 中的 QwenLM 实例。
|
| 113 |
+
instruct 槽位无对应 registry 包装。
|
| 114 |
"""
|
| 115 |
from backend.platform.app_context import get_app_context
|
| 116 |
|
| 117 |
context = get_app_context(prefer_module_context=True)
|
| 118 |
+
selected_name = context.base_model_id
|
| 119 |
|
| 120 |
if not selected_name:
|
| 121 |
+
raise ValueError("未指定 base 模型 id")
|
| 122 |
|
| 123 |
if selected_name in project_registry:
|
| 124 |
_ensure_default_project_ready(selected_name)
|
|
|
|
| 137 |
def preload_all_slots():
|
| 138 |
"""
|
| 139 |
启动预载(非 --no_auto_load):对 CONFIGURED_SLOTS 各解析 HF 路径,去重后加载全部权重,
|
| 140 |
+
再注册 base 槽位 QwenLM 项目。
|
| 141 |
"""
|
| 142 |
from backend.platform.app_context import get_app_context
|
| 143 |
|
|
|
|
| 148 |
with _init_lock:
|
| 149 |
for path in paths:
|
| 150 |
ensure_model_loaded(path)
|
| 151 |
+
_register_base_qwenlm_if_needed()
|
| 152 |
|
| 153 |
|
| 154 |
def ensure_slot_ready(slot: ModelSlot):
|
| 155 |
"""
|
| 156 |
+
槽位业务就绪:保证该槽位后续推理所需状态已备好。
|
| 157 |
|
| 158 |
- 两槽位均先保证 HF 权重已加载,返回 (tokenizer, model, device)。
|
| 159 |
+
- base 另需将 QwenLM 挂入 project_registry(信息密度);instruct 无 registry 步骤。
|
|
|
|
|
|
|
| 160 |
"""
|
| 161 |
from backend.platform.app_context import get_app_context
|
| 162 |
|
| 163 |
get_app_context(prefer_module_context=True)
|
| 164 |
|
| 165 |
+
if slot == ModelSlot.BASE:
|
| 166 |
with _init_lock:
|
| 167 |
+
out = ensure_slot_weights_loaded(ModelSlot.BASE)
|
| 168 |
+
_register_base_qwenlm_if_needed()
|
| 169 |
return out
|
| 170 |
+
if slot == ModelSlot.INSTRUCT:
|
| 171 |
+
return ensure_slot_weights_loaded(ModelSlot.INSTRUCT)
|
| 172 |
raise ValueError(f"unknown ModelSlot: {slot!r}")
|
| 173 |
|
| 174 |
|
| 175 |
+
def ensure_base_slot_ready():
|
| 176 |
+
"""信息密度等业务:``ensure_slot_ready(ModelSlot.BASE)``。"""
|
| 177 |
+
return ensure_slot_ready(ModelSlot.BASE)
|
| 178 |
|
| 179 |
|
| 180 |
+
def ensure_instruct_slot_ready():
|
| 181 |
+
"""语义分析 / 续写:``ensure_slot_ready(ModelSlot.INSTRUCT)``。"""
|
| 182 |
+
return ensure_slot_ready(ModelSlot.INSTRUCT)
|
| 183 |
|
| 184 |
|
| 185 |
def get_current_model_max_token_length() -> int:
|
| 186 |
"""
|
| 187 |
+
查询当前生效 base 模型的 max_token_length 参数。
|
| 188 |
优先从已加载的模型实例获取,未加载时取 default_model.default_cpu_machine 配置。
|
| 189 |
"""
|
| 190 |
from backend.platform.app_context import get_app_context
|
|
|
|
| 192 |
|
| 193 |
try:
|
| 194 |
context = get_app_context(prefer_module_context=True)
|
| 195 |
+
model_name = context.base_model_id or DEFAULT_BASE_MODEL
|
| 196 |
except RuntimeError:
|
| 197 |
model_name = "default_model"
|
| 198 |
|
|
|
|
| 212 |
project_registry.ensure_loaded(selected_name)
|
| 213 |
|
| 214 |
|
| 215 |
+
def get_instruct_model_display_name() -> str:
|
| 216 |
+
"""返回 instruct 槽位 HuggingFace 路径(用于结果中的 model 字段)。"""
|
| 217 |
+
return _resolved_hf_path_for_slot(ModelSlot.INSTRUCT)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
|
| 220 |
+
def get_base_model_display_name() -> str:
|
| 221 |
+
"""返回 base 槽位 HuggingFace 路径(用于结果中的 model 字段)。"""
|
| 222 |
+
return _resolved_hf_path_for_slot(ModelSlot.BASE)
|
backend/platform/access_log.py
CHANGED
|
@@ -270,6 +270,7 @@ def log_openai_completions_prompt_request(
|
|
| 270 |
model: str,
|
| 271 |
user_prompt: str,
|
| 272 |
system: Optional[str] = None,
|
|
|
|
| 273 |
client_ip: str = None,
|
| 274 |
) -> None:
|
| 275 |
"""记录 POST /v1/completions/prompt(仅拼装 chat template,不分配 req_id)。"""
|
|
@@ -277,8 +278,11 @@ def log_openai_completions_prompt_request(
|
|
| 277 |
|
| 278 |
up = _log_str_preview(user_prompt, preview)
|
| 279 |
if system is None:
|
| 280 |
-
details = f"model='{model}', user_prompt='{up}'"
|
| 281 |
else:
|
| 282 |
-
details =
|
|
|
|
|
|
|
|
|
|
| 283 |
log_request("📥 openai completions/prompt 请求", details, client_ip)
|
| 284 |
|
|
|
|
| 270 |
model: str,
|
| 271 |
user_prompt: str,
|
| 272 |
system: Optional[str] = None,
|
| 273 |
+
enable_thinking: bool = False,
|
| 274 |
client_ip: str = None,
|
| 275 |
) -> None:
|
| 276 |
"""记录 POST /v1/completions/prompt(仅拼装 chat template,不分配 req_id)。"""
|
|
|
|
| 278 |
|
| 279 |
up = _log_str_preview(user_prompt, preview)
|
| 280 |
if system is None:
|
| 281 |
+
details = f"model='{model}', enable_thinking={enable_thinking}, user_prompt='{up}'"
|
| 282 |
else:
|
| 283 |
+
details = (
|
| 284 |
+
f"model='{model}', enable_thinking={enable_thinking}, "
|
| 285 |
+
f"system='{_log_str_preview(system, preview)}', user_prompt='{up}'"
|
| 286 |
+
)
|
| 287 |
log_request("📥 openai completions/prompt 请求", details, client_ip)
|
| 288 |
|
backend/platform/app_context.py
CHANGED
|
@@ -8,6 +8,8 @@ from pathlib import Path
|
|
| 8 |
from typing import Optional
|
| 9 |
from argparse import Namespace
|
| 10 |
|
|
|
|
|
|
|
| 11 |
|
| 12 |
class AppContext:
|
| 13 |
"""
|
|
@@ -54,21 +56,31 @@ class AppContext:
|
|
| 54 |
self.args = args
|
| 55 |
self.data_dir = data_dir
|
| 56 |
self._model_loading = True # 初始时处于加载状态
|
| 57 |
-
self.
|
|
|
|
| 58 |
|
| 59 |
@property
|
| 60 |
-
def
|
| 61 |
-
"""当前模型
|
| 62 |
-
return self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
@property
|
| 65 |
def model_loading(self) -> bool:
|
| 66 |
"""模型是否正在加载"""
|
| 67 |
return self._model_loading
|
| 68 |
|
| 69 |
-
def
|
| 70 |
-
"""设置
|
| 71 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
def set_model_loading(self, loading: bool):
|
| 74 |
"""设置模型加载状态"""
|
|
|
|
| 8 |
from typing import Optional
|
| 9 |
from argparse import Namespace
|
| 10 |
|
| 11 |
+
from model_paths import DEFAULT_BASE_MODEL, DEFAULT_INSTRUCT_MODEL
|
| 12 |
+
|
| 13 |
|
| 14 |
class AppContext:
|
| 15 |
"""
|
|
|
|
| 56 |
self.args = args
|
| 57 |
self.data_dir = data_dir
|
| 58 |
self._model_loading = True # 初始时处于加载状态
|
| 59 |
+
self._base_model_id = getattr(args, "base_model", None) or DEFAULT_BASE_MODEL
|
| 60 |
+
self._instruct_model_id = getattr(args, "instruct_model", None) or DEFAULT_INSTRUCT_MODEL
|
| 61 |
|
| 62 |
@property
|
| 63 |
+
def base_model_id(self) -> str:
|
| 64 |
+
"""当前 base 槽位 CLI 模型 id(信息密度主模型)。"""
|
| 65 |
+
return self._base_model_id
|
| 66 |
+
|
| 67 |
+
@property
|
| 68 |
+
def instruct_model_id(self) -> str:
|
| 69 |
+
"""当前 instruct 槽位 CLI 模型 id(语义 / 续写)。"""
|
| 70 |
+
return self._instruct_model_id
|
| 71 |
|
| 72 |
@property
|
| 73 |
def model_loading(self) -> bool:
|
| 74 |
"""模型是否正在加载"""
|
| 75 |
return self._model_loading
|
| 76 |
|
| 77 |
+
def set_base_model_id(self, model_id: str):
|
| 78 |
+
"""设置 base 槽位 CLI 模型 id(如在线切换)。"""
|
| 79 |
+
self._base_model_id = model_id
|
| 80 |
+
|
| 81 |
+
def set_current_model(self, model_id: str):
|
| 82 |
+
"""兼容旧名:同 set_base_model_id。"""
|
| 83 |
+
self.set_base_model_id(model_id)
|
| 84 |
|
| 85 |
def set_model_loading(self, loading: bool):
|
| 86 |
"""设置模型加载状态"""
|
backend/platform/visit_stats.py
CHANGED
|
@@ -41,7 +41,8 @@ _STATS_API_ORDER = (
|
|
| 41 |
_STATS_OS_ORDER = ("ios", "android", "windows", "macos", "linux", "unknown")
|
| 42 |
_STATS_GEN_ATTR_OPT_ORDER = (
|
| 43 |
"layout_linear_arc", "layout_step_down", "layout_spiral",
|
| 44 |
-
"propagated", "
|
|
|
|
| 45 |
)
|
| 46 |
|
| 47 |
# RLock:_persist_tick 在已持锁时调用 _sample_locked_counters,同线程需可重入。
|
|
|
|
| 41 |
_STATS_OS_ORDER = ("ios", "android", "windows", "macos", "linux", "unknown")
|
| 42 |
_STATS_GEN_ATTR_OPT_ORDER = (
|
| 43 |
"layout_linear_arc", "layout_step_down", "layout_spiral",
|
| 44 |
+
"propagated", "propagated_anim_backward",
|
| 45 |
+
"downstream", "token_tooltip",
|
| 46 |
)
|
| 47 |
|
| 48 |
# RLock:_persist_tick 在已持锁时调用 _sample_locked_counters,同线程需可重入。
|
client/src/assets/demos/causal_flow/CoT | 多“跳”推理.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
client/src/assets/demos/causal_flow/CoT | 苏州所在省的省会.json
CHANGED
|
@@ -29624,12 +29624,13 @@
|
|
| 29624 |
"maxTokens": 200,
|
| 29625 |
"system": "You are a helpful assistant.",
|
| 29626 |
"user": "苏州所在省的省会是哪个?先一步步推理再回答。",
|
| 29627 |
-
"useSystem": true
|
|
|
|
| 29628 |
},
|
| 29629 |
"demoUiOptions": {
|
| 29630 |
"layoutMode": "text-flow",
|
| 29631 |
"measureWidthPx": 500,
|
| 29632 |
-
"dagCompactness": 0.
|
| 29633 |
"linearArcAdjacentGapPx": 0,
|
| 29634 |
"hideExcludedTokens": false,
|
| 29635 |
"edgeTopPCoverage": 0.4,
|
|
@@ -29638,14 +29639,16 @@
|
|
| 29638 |
"hideInactiveEdges": true,
|
| 29639 |
"showDownstreamInfluence": false,
|
| 29640 |
"recursiveAttributionEnabled": true,
|
|
|
|
|
|
|
| 29641 |
"showTokenInfoOnSelected": true,
|
| 29642 |
-
"replayPacingMode": "
|
| 29643 |
-
"playbackTotalS":
|
| 29644 |
"playbackStepMs": 200,
|
| 29645 |
"excludePromptPatternsEnabled": true,
|
| 29646 |
"excludePromptPatternsText": "先一步步推理再回答。\n#comment# use '#comment#' to comment lines; support regex\n<\\|im_start\\|>system\\n\n<\\|im_start\\|>user\\n\n<\\|im_start\\|>assistant\\n\n<\\|im_start\\|>assistant\\n\\n\n<\\|im_end\\|>\\n\n<think>\\n\\n\n</think>\\n\\n\n<\\|im_start\\|>system\\n[\\s\\S]*?<\\|im_end\\|>#comment# all system prompt",
|
| 29647 |
"excludeGeneratedPatternsEnabled": true,
|
| 29648 |
-
"excludeGeneratedPatternsText": "<think>\\n\n</think>\\n\\n",
|
| 29649 |
"selectedNodeId": "237_239"
|
| 29650 |
}
|
| 29651 |
}
|
|
|
|
| 29624 |
"maxTokens": 200,
|
| 29625 |
"system": "You are a helpful assistant.",
|
| 29626 |
"user": "苏州所在省的省会是哪个?先一步步推理再回答。",
|
| 29627 |
+
"useSystem": true,
|
| 29628 |
+
"enableThinking": false
|
| 29629 |
},
|
| 29630 |
"demoUiOptions": {
|
| 29631 |
"layoutMode": "text-flow",
|
| 29632 |
"measureWidthPx": 500,
|
| 29633 |
+
"dagCompactness": 0.9,
|
| 29634 |
"linearArcAdjacentGapPx": 0,
|
| 29635 |
"hideExcludedTokens": false,
|
| 29636 |
"edgeTopPCoverage": 0.4,
|
|
|
|
| 29639 |
"hideInactiveEdges": true,
|
| 29640 |
"showDownstreamInfluence": false,
|
| 29641 |
"recursiveAttributionEnabled": true,
|
| 29642 |
+
"recursiveEdgeBatchAnimationEnabled": true,
|
| 29643 |
+
"recursiveEdgeBatchAnimationDirection": "forward",
|
| 29644 |
"showTokenInfoOnSelected": true,
|
| 29645 |
+
"replayPacingMode": "total",
|
| 29646 |
+
"playbackTotalS": 6,
|
| 29647 |
"playbackStepMs": 200,
|
| 29648 |
"excludePromptPatternsEnabled": true,
|
| 29649 |
"excludePromptPatternsText": "先一步步推理再回答。\n#comment# use '#comment#' to comment lines; support regex\n<\\|im_start\\|>system\\n\n<\\|im_start\\|>user\\n\n<\\|im_start\\|>assistant\\n\n<\\|im_start\\|>assistant\\n\\n\n<\\|im_end\\|>\\n\n<think>\\n\\n\n</think>\\n\\n\n<\\|im_start\\|>system\\n[\\s\\S]*?<\\|im_end\\|>#comment# all system prompt",
|
| 29650 |
"excludeGeneratedPatternsEnabled": true,
|
| 29651 |
+
"excludeGeneratedPatternsText": "[\\s\\S]*?推理[\\s\\S]*?:\\n\\n\n<think>\\n\n</think>\\n\\n",
|
| 29652 |
"selectedNodeId": "237_239"
|
| 29653 |
}
|
| 29654 |
}
|
client/src/assets/demos/causal_flow/order.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"slug": "Write a sonnet about love",
|
| 4 |
+
"label": "Poem | Write a sonnet about love"
|
| 5 |
+
},
|
| 6 |
+
{
|
| 7 |
+
"slug": "写一首绝句,主题是春天",
|
| 8 |
+
"label": "写诗 | 写一首绝句,主题是春天"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"slug": "CoT | 苏州所在省的省会",
|
| 12 |
+
"label": "CoT | 苏州所在省的省会"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"slug": "CoT | 多“跳”推理",
|
| 16 |
+
"label": "CoT | 多“跳”推理"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"slug": "过拟合|李白 将进酒",
|
| 20 |
+
"label": "过拟合|李白 将进酒"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"slug": "CN->EN翻译",
|
| 24 |
+
"label": "CN->EN | 翻译"
|
| 25 |
+
}
|
| 26 |
+
]
|
client/src/assets/images/{dag-cot.png → dag-cot.mov}
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:47a48bf00f0d2cbd8347915deba2a3f9e0e843e7a57e5b0d69e75ac8d0a984e7
|
| 3 |
+
size 65679
|
client/src/causal_flow.html
CHANGED
|
@@ -119,6 +119,14 @@
|
|
| 119 |
</div>
|
| 120 |
</div>
|
| 121 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
</div>
|
| 123 |
|
| 124 |
<div class="attribution-exclude-prompt-patterns-row">
|
|
@@ -187,7 +195,6 @@
|
|
| 187 |
<div class="loadersmall"></div>
|
| 188 |
<span id="gen_attr_complete_reason" class="generation-end-reason"></span>
|
| 189 |
</div>
|
| 190 |
-
<span id="gen_attr_analyze_progress" class="analyze-progress"></span>
|
| 191 |
</div>
|
| 192 |
</div>
|
| 193 |
<div id="gen_attr_text_metrics" class="text-metrics text-metrics-chat">
|
|
@@ -200,7 +207,7 @@
|
|
| 200 |
|
| 201 |
<div class="gen-attr-reset-ui-options-row">
|
| 202 |
<button type="button" id="gen_attr_reset_ui_options_btn" class="text-action-btn"
|
| 203 |
-
title="Restore DAG options,
|
| 204 |
data-i18n="text,title">
|
| 205 |
Reset UI options
|
| 206 |
</button>
|
|
@@ -290,8 +297,8 @@
|
|
| 290 |
<select id="gen_attr_dag_recursive_edge_animation_direction" class="semantic-submode-select"
|
| 291 |
title="Choose one direction for propagated-edge batch animation."
|
| 292 |
data-i18n="title">
|
| 293 |
-
<option value="backward">backward</option>
|
| 294 |
<option value="forward">forward</option>
|
|
|
|
| 295 |
</select>
|
| 296 |
</span>
|
| 297 |
<span class="semantic-submode-group" id="gen_attr_dag_show_downstream_influence_group">
|
|
@@ -379,10 +386,10 @@
|
|
| 379 |
</div>
|
| 380 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 381 |
<span class="semantic-submode-group gen-attr-dag-replay-speed-row semantic-submode-group--emphasis">
|
| 382 |
-
<label class="semantic-submode-label" for="gen_attr_dag_replay_mode" data-i18n>
|
| 383 |
<select id="gen_attr_dag_replay_mode"
|
| 384 |
class="semantic-submode-select gen-attr-dag-replay-mode-select"
|
| 385 |
-
title="Total duration
|
| 386 |
data-i18n="title">
|
| 387 |
<option value="total" data-i18n>total time</option>
|
| 388 |
<option value="step" data-i18n>step time</option>
|
|
@@ -390,14 +397,14 @@
|
|
| 390 |
<span id="gen_attr_dag_replay_total_wrap" class="gen-attr-dag-replay-value-wrap">
|
| 391 |
<input type="number" id="gen_attr_dag_playback_total_s" class="gen-attr-dag-measure-width-input"
|
| 392 |
value="7" min="1" max="3600" step="1"
|
| 393 |
-
title="
|
| 394 |
data-i18n="title">
|
| 395 |
<span class="semantic-submode-label">s</span>
|
| 396 |
</span>
|
| 397 |
<span id="gen_attr_dag_replay_step_wrap" class="gen-attr-dag-replay-value-wrap" hidden>
|
| 398 |
<input type="number" id="gen_attr_dag_playback_step_ms" class="gen-attr-dag-measure-width-input"
|
| 399 |
value="200" min="0" max="10000" step="10"
|
| 400 |
-
title="
|
| 401 |
data-i18n="title">
|
| 402 |
<span class="semantic-submode-label">ms</span>
|
| 403 |
</span>
|
|
|
|
| 119 |
</div>
|
| 120 |
</div>
|
| 121 |
</div>
|
| 122 |
+
<div class="semantic-submode-row chat-enable-thinking-row">
|
| 123 |
+
<span class="semantic-submode-group">
|
| 124 |
+
<label class="semantic-submode-label" for="gen_attr_enable_thinking">
|
| 125 |
+
<input type="checkbox" id="gen_attr_enable_thinking" />
|
| 126 |
+
<span data-i18n>Enable thinking</span>
|
| 127 |
+
</label>
|
| 128 |
+
</span>
|
| 129 |
+
</div>
|
| 130 |
</div>
|
| 131 |
|
| 132 |
<div class="attribution-exclude-prompt-patterns-row">
|
|
|
|
| 195 |
<div class="loadersmall"></div>
|
| 196 |
<span id="gen_attr_complete_reason" class="generation-end-reason"></span>
|
| 197 |
</div>
|
|
|
|
| 198 |
</div>
|
| 199 |
</div>
|
| 200 |
<div id="gen_attr_text_metrics" class="text-metrics text-metrics-chat">
|
|
|
|
| 207 |
|
| 208 |
<div class="gen-attr-reset-ui-options-row">
|
| 209 |
<button type="button" id="gen_attr_reset_ui_options_btn" class="text-action-btn"
|
| 210 |
+
title="Restore DAG options, play speed, exclusions, etc. to defaults and clear saved preferences for those controls."
|
| 211 |
data-i18n="text,title">
|
| 212 |
Reset UI options
|
| 213 |
</button>
|
|
|
|
| 297 |
<select id="gen_attr_dag_recursive_edge_animation_direction" class="semantic-submode-select"
|
| 298 |
title="Choose one direction for propagated-edge batch animation."
|
| 299 |
data-i18n="title">
|
|
|
|
| 300 |
<option value="forward">forward</option>
|
| 301 |
+
<option value="backward">backward</option>
|
| 302 |
</select>
|
| 303 |
</span>
|
| 304 |
<span class="semantic-submode-group" id="gen_attr_dag_show_downstream_influence_group">
|
|
|
|
| 386 |
</div>
|
| 387 |
<div class="gen-attr-dag-measure-width-row semantic-submode-row">
|
| 388 |
<span class="semantic-submode-group gen-attr-dag-replay-speed-row semantic-submode-group--emphasis">
|
| 389 |
+
<label class="semantic-submode-label" for="gen_attr_dag_replay_mode" data-i18n>Play speed</label>
|
| 390 |
<select id="gen_attr_dag_replay_mode"
|
| 391 |
class="semantic-submode-select gen-attr-dag-replay-mode-select"
|
| 392 |
+
title="Total duration or per-step delay. DAG Play uses equal steps or a fixed step interval; focus-chain animation scales each step by attribution weight."
|
| 393 |
data-i18n="title">
|
| 394 |
<option value="total" data-i18n>total time</option>
|
| 395 |
<option value="step" data-i18n>step time</option>
|
|
|
|
| 397 |
<span id="gen_attr_dag_replay_total_wrap" class="gen-attr-dag-replay-value-wrap">
|
| 398 |
<input type="number" id="gen_attr_dag_playback_total_s" class="gen-attr-dag-measure-width-input"
|
| 399 |
value="7" min="1" max="3600" step="1"
|
| 400 |
+
title="Total seconds. DAG Play divides evenly across steps; focus-chain animation splits by layer weight. Saved locally; applied when you press Play or select a focus node."
|
| 401 |
data-i18n="title">
|
| 402 |
<span class="semantic-submode-label">s</span>
|
| 403 |
</span>
|
| 404 |
<span id="gen_attr_dag_replay_step_wrap" class="gen-attr-dag-replay-value-wrap" hidden>
|
| 405 |
<input type="number" id="gen_attr_dag_playback_step_ms" class="gen-attr-dag-measure-width-input"
|
| 406 |
value="200" min="0" max="10000" step="10"
|
| 407 |
+
title="Milliseconds per step. DAG Play uses this fixed interval; focus-chain animation multiplies by layer weight. Saved locally; applied when you press Play or select a focus node."
|
| 408 |
data-i18n="title">
|
| 409 |
<span class="semantic-submode-label">ms</span>
|
| 410 |
</span>
|
client/src/chat.html
CHANGED
|
@@ -39,7 +39,7 @@
|
|
| 39 |
<section class="input-section">
|
| 40 |
<div class="semantic-submode-row chat-raw-prompt-mode-row">
|
| 41 |
<span class="semantic-submode-group">
|
| 42 |
-
<label for="chat_skip_chat_template">
|
| 43 |
<input type="checkbox" id="chat_skip_chat_template" />
|
| 44 |
<span data-i18n>Raw prompt mode</span>
|
| 45 |
</label>
|
|
@@ -47,7 +47,7 @@
|
|
| 47 |
</div>
|
| 48 |
<div id="raw_input_panel" class="chat-prompt-panel">
|
| 49 |
<div class="input-header">
|
| 50 |
-
<span
|
| 51 |
<div class="text-action-buttons-top">
|
| 52 |
<div class="textarea-counter" id="text_count_display">
|
| 53 |
<span id="text_count_value">0</span> <span data-i18n>chars</span>
|
|
@@ -67,9 +67,9 @@
|
|
| 67 |
<div id="chat_input_panel" hidden>
|
| 68 |
<div class="chat-prompt-panel" id="chat_system_prompt_panel">
|
| 69 |
<div class="input-header">
|
| 70 |
-
<label class="chat-use-system-label">
|
| 71 |
<input type="checkbox" id="chat_use_system_prompt" checked />
|
| 72 |
-
<span
|
| 73 |
</label>
|
| 74 |
<div class="text-action-buttons-top">
|
| 75 |
<div class="textarea-counter" id="chat_system_text_count_display">
|
|
@@ -89,7 +89,7 @@
|
|
| 89 |
</div>
|
| 90 |
<div class="chat-prompt-panel">
|
| 91 |
<div class="input-header">
|
| 92 |
-
<span
|
| 93 |
<div class="text-action-buttons-top">
|
| 94 |
<div class="textarea-counter" id="chat_user_text_count_display">
|
| 95 |
<span id="chat_user_text_count_value">0</span> <span data-i18n>chars</span>
|
|
@@ -106,6 +106,14 @@
|
|
| 106 |
</div>
|
| 107 |
</div>
|
| 108 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
</div>
|
| 110 |
<div class="textarea-wrapper chat-prompt-actions-row">
|
| 111 |
<div class="semantic-submode-row chat-completion-options-row">
|
|
|
|
| 39 |
<section class="input-section">
|
| 40 |
<div class="semantic-submode-row chat-raw-prompt-mode-row">
|
| 41 |
<span class="semantic-submode-group">
|
| 42 |
+
<label class="semantic-submode-label" for="chat_skip_chat_template">
|
| 43 |
<input type="checkbox" id="chat_skip_chat_template" />
|
| 44 |
<span data-i18n>Raw prompt mode</span>
|
| 45 |
</label>
|
|
|
|
| 47 |
</div>
|
| 48 |
<div id="raw_input_panel" class="chat-prompt-panel">
|
| 49 |
<div class="input-header">
|
| 50 |
+
<span class="semantic-submode-label" data-i18n>Raw prompt</span>
|
| 51 |
<div class="text-action-buttons-top">
|
| 52 |
<div class="textarea-counter" id="text_count_display">
|
| 53 |
<span id="text_count_value">0</span> <span data-i18n>chars</span>
|
|
|
|
| 67 |
<div id="chat_input_panel" hidden>
|
| 68 |
<div class="chat-prompt-panel" id="chat_system_prompt_panel">
|
| 69 |
<div class="input-header">
|
| 70 |
+
<label class="chat-use-system-label semantic-submode-label" for="chat_use_system_prompt">
|
| 71 |
<input type="checkbox" id="chat_use_system_prompt" checked />
|
| 72 |
+
<span data-i18n>System</span>
|
| 73 |
</label>
|
| 74 |
<div class="text-action-buttons-top">
|
| 75 |
<div class="textarea-counter" id="chat_system_text_count_display">
|
|
|
|
| 89 |
</div>
|
| 90 |
<div class="chat-prompt-panel">
|
| 91 |
<div class="input-header">
|
| 92 |
+
<span class="semantic-submode-label" data-i18n>User</span>
|
| 93 |
<div class="text-action-buttons-top">
|
| 94 |
<div class="textarea-counter" id="chat_user_text_count_display">
|
| 95 |
<span id="chat_user_text_count_value">0</span> <span data-i18n>chars</span>
|
|
|
|
| 106 |
</div>
|
| 107 |
</div>
|
| 108 |
</div>
|
| 109 |
+
<div class="semantic-submode-row chat-enable-thinking-row">
|
| 110 |
+
<span class="semantic-submode-group">
|
| 111 |
+
<label class="semantic-submode-label" for="chat_enable_thinking">
|
| 112 |
+
<input type="checkbox" id="chat_enable_thinking" />
|
| 113 |
+
<span data-i18n>Enable thinking</span>
|
| 114 |
+
</label>
|
| 115 |
+
</span>
|
| 116 |
+
</div>
|
| 117 |
</div>
|
| 118 |
<div class="textarea-wrapper chat-prompt-actions-row">
|
| 119 |
<div class="semantic-submode-row chat-completion-options-row">
|
client/src/css/base/_narrow-ios-form-font-tail.scss
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 子页 bundle 末尾:@use '../base/narrow-ios-form-font-tail' as ios-form-font; @include ios-form-font.apply;
|
| 2 |
+
// 设计说明见 responsive.narrow-ios-form-font
|
| 3 |
+
@use 'breakpoints' as *;
|
| 4 |
+
@use 'responsive';
|
| 5 |
+
|
| 6 |
+
@mixin apply {
|
| 7 |
+
@media (max-width: $breakpoint-mobile) {
|
| 8 |
+
@include responsive.narrow-ios-form-font('html');
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
@media (min-width: $breakpoint-tablet) {
|
| 12 |
+
@include responsive.narrow-ios-form-font('html[data-force-narrow]:has(.main_frame)');
|
| 13 |
+
}
|
| 14 |
+
}
|
client/src/css/base/_responsive.scss
CHANGED
|
@@ -24,6 +24,38 @@
|
|
| 24 |
box-sizing: border-box;
|
| 25 |
}
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
// 窄屏布局(视口 ≤767px 与设置「强制窄屏」共用)
|
| 28 |
@mixin narrow-app-layout($root) {
|
| 29 |
#{$root} {
|
|
@@ -164,18 +196,6 @@
|
|
| 164 |
|
| 165 |
#{$root} .textarea-wrapper textarea {
|
| 166 |
min-height: 80px;
|
| 167 |
-
font-size: 16px; // 设置为至少 16px,防止 iOS 自动缩放
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
#{$root} input[type="text"],
|
| 171 |
-
#{$root} input[type="url"],
|
| 172 |
-
#{$root} input[type="search"],
|
| 173 |
-
#{$root} input[type="email"],
|
| 174 |
-
#{$root} input[type="number"],
|
| 175 |
-
#{$root} input[type="tel"],
|
| 176 |
-
#{$root} input[type="password"],
|
| 177 |
-
#{$root} select {
|
| 178 |
-
font-size: 16px; // 所有文本类输入控件在移动端统一使用 16px
|
| 179 |
}
|
| 180 |
}
|
| 181 |
|
|
|
|
| 24 |
box-sizing: border-box;
|
| 25 |
}
|
| 26 |
|
| 27 |
+
// --- 窄屏表单字号(防 iOS 聚焦自动放大)---
|
| 28 |
+
//
|
| 29 |
+
// 背景:iOS Safari 在可编辑控件 computed font-size < 16px 时,聚焦常会整页 zoom。
|
| 30 |
+
//
|
| 31 |
+
// 约定:
|
| 32 |
+
// - 窄屏(≤$breakpoint-mobile)与「强制窄屏」下,input/textarea/select 统一 16px(不用 viewport 禁缩放)。
|
| 33 |
+
// - 桌面端仍可用 9pt 等紧凑字号;弹窗表单见 dialog.scss(全宽 16px,与窄屏无关)。
|
| 34 |
+
// - 页面 SCSS 勿对表单写 font-size < 16px;布局类名、padding 不受限。小字号请用于 label/说明,勿用于可编辑控件。
|
| 35 |
+
//
|
| 36 |
+
// 实现注意:
|
| 37 |
+
// - 本 mixin 须在子页 SCSS 最后生效:各 pages/*.scss 末尾 @include narrow-ios-form-font-tail.apply
|
| 38 |
+
// (若只写在 narrow-app-layout 里,会被同文件后加载的 .foo-input { font-size: 9pt } 盖住)。
|
| 39 |
+
// - 选择器含 textarea[class] 等,用于压过带 class 的页面规则;不要改回仅 .textarea-wrapper textarea。
|
| 40 |
+
// - 新增子页(@use app-pages)时,记得在文件末尾加上述 tail include。
|
| 41 |
+
//
|
| 42 |
+
@mixin narrow-ios-form-font($root) {
|
| 43 |
+
#{$root} textarea,
|
| 44 |
+
#{$root} textarea[class],
|
| 45 |
+
#{$root} select,
|
| 46 |
+
#{$root} select[class],
|
| 47 |
+
#{$root} input[type="text"],
|
| 48 |
+
#{$root} input[type="url"],
|
| 49 |
+
#{$root} input[type="search"],
|
| 50 |
+
#{$root} input[type="email"],
|
| 51 |
+
#{$root} input[type="number"],
|
| 52 |
+
#{$root} input[type="tel"],
|
| 53 |
+
#{$root} input[type="password"],
|
| 54 |
+
#{$root} input[class] {
|
| 55 |
+
font-size: 16px;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
// 窄屏布局(视口 ≤767px 与设置「强制窄屏」共用)
|
| 60 |
@mixin narrow-app-layout($root) {
|
| 61 |
#{$root} {
|
|
|
|
| 196 |
|
| 197 |
#{$root} .textarea-wrapper textarea {
|
| 198 |
min-height: 80px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
}
|
| 200 |
}
|
| 201 |
|
client/src/css/components/_chat-input-panel.scss
CHANGED
|
@@ -6,6 +6,45 @@
|
|
| 6 |
margin-bottom: 0;
|
| 7 |
}
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
.input-section .textarea-wrapper.chat-prompt-actions-row .chat-completion-options-row {
|
| 10 |
margin-bottom: 8px;
|
| 11 |
width: 100%;
|
|
|
|
| 6 |
margin-bottom: 0;
|
| 7 |
}
|
| 8 |
|
| 9 |
+
// Prompt 区选项标签:panel 标题、勾选行同系(9pt + 勾选主次色)
|
| 10 |
+
.input-section {
|
| 11 |
+
span.semantic-submode-label {
|
| 12 |
+
color: var(--text-muted);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
.semantic-submode-row:not(.attribution-exclude-prompt-patterns-header):not(.attribution-exclude-generated-patterns-header) {
|
| 16 |
+
label.semantic-submode-label {
|
| 17 |
+
color: var(--text-primary);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
label.semantic-submode-label:has(> input[type='checkbox']:not(:checked)) {
|
| 21 |
+
color: var(--text-muted);
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.chat-prompt-panel > .input-header > .semantic-submode-label {
|
| 26 |
+
font-size: 9pt;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// System 在 input-header 内(非 semantic-submode-row)
|
| 30 |
+
.chat-prompt-panel > .input-header label.chat-use-system-label.semantic-submode-label {
|
| 31 |
+
font-size: 9pt;
|
| 32 |
+
|
| 33 |
+
input[type='checkbox'] {
|
| 34 |
+
margin: 0;
|
| 35 |
+
flex-shrink: 0;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
&:has(> input[type='checkbox']:not(:checked)) {
|
| 39 |
+
color: var(--text-muted);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
&:has(> input[type='checkbox']:checked) {
|
| 43 |
+
color: var(--text-primary);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
.input-section .textarea-wrapper.chat-prompt-actions-row .chat-completion-options-row {
|
| 49 |
margin-bottom: 8px;
|
| 50 |
width: 100%;
|
client/src/css/components/_semantic-analysis.scss
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
// 语义分析相关样式(section、查询输入、历史下拉、loader)
|
| 2 |
|
| 3 |
@use "loaders";
|
| 4 |
-
@use "breakpoints" as *;
|
| 5 |
@use "query-history-dropdown" as qh;
|
| 6 |
@use "lmf-readout" as lmf;
|
| 7 |
|
|
@@ -263,10 +262,3 @@
|
|
| 263 |
color: var(--text-muted, #999);
|
| 264 |
}
|
| 265 |
}
|
| 266 |
-
|
| 267 |
-
// 窄屏:与 _responsive 中 input[type=text] 的 16px 一致;上方三层嵌套选择器优先级高于 input[type=text],故在此用同权重规则 + 源顺序覆盖
|
| 268 |
-
@media (max-width: $breakpoint-mobile) {
|
| 269 |
-
.semantic-analysis-section .semantic-analysis-controls .semantic-search-input {
|
| 270 |
-
font-size: 16px;
|
| 271 |
-
}
|
| 272 |
-
}
|
|
|
|
| 1 |
// 语义分析相关样式(section、查询输入、历史下拉、loader)
|
| 2 |
|
| 3 |
@use "loaders";
|
|
|
|
| 4 |
@use "query-history-dropdown" as qh;
|
| 5 |
@use "lmf-readout" as lmf;
|
| 6 |
|
|
|
|
| 262 |
color: var(--text-muted, #999);
|
| 263 |
}
|
| 264 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client/src/css/components/dialog.scss
CHANGED
|
@@ -149,6 +149,22 @@
|
|
| 149 |
// 对话框表单元素样式
|
| 150 |
.dialog-form-container {
|
| 151 |
margin-bottom: 15px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
.dialog-label {
|
|
@@ -171,6 +187,7 @@
|
|
| 171 |
}
|
| 172 |
}
|
| 173 |
|
|
|
|
| 174 |
.dialog-input {
|
| 175 |
width: 100%;
|
| 176 |
padding: 8px;
|
|
|
|
| 149 |
// 对话框表单元素样式
|
| 150 |
.dialog-form-container {
|
| 151 |
margin-bottom: 15px;
|
| 152 |
+
|
| 153 |
+
// 长内容弹窗:占满 .dialog-content,正文在 .dialog-scroll-region 内滚动
|
| 154 |
+
&--fill {
|
| 155 |
+
display: flex;
|
| 156 |
+
flex-direction: column;
|
| 157 |
+
flex: 1;
|
| 158 |
+
min-height: 0;
|
| 159 |
+
margin-bottom: 0;
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.dialog-scroll-region {
|
| 164 |
+
flex: 1;
|
| 165 |
+
min-height: 0;
|
| 166 |
+
overflow-y: auto;
|
| 167 |
+
-webkit-overflow-scrolling: touch;
|
| 168 |
}
|
| 169 |
|
| 170 |
.dialog-label {
|
|
|
|
| 187 |
}
|
| 188 |
}
|
| 189 |
|
| 190 |
+
// 弹窗表单任意屏宽 16px(iOS 防放大);子页左栏窄屏见 responsive.narrow-ios-form-font
|
| 191 |
.dialog-input {
|
| 192 |
width: 100%;
|
| 193 |
padding: 8px;
|
client/src/css/pages/analysis.scss
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
// Analysis 页专有样式(intro / 统计图 / 分析进度)
|
| 2 |
@use 'app-pages';
|
|
|
|
| 3 |
|
| 4 |
// 介绍区域样式
|
| 5 |
.intro-section {
|
|
@@ -191,3 +192,6 @@
|
|
| 191 |
box-sizing: border-box; // 确保宽度计算正确
|
| 192 |
}
|
| 193 |
}
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
// Analysis 页专有样式(intro / 统计图 / 分析进度)
|
| 2 |
@use 'app-pages';
|
| 3 |
+
@use '../base/narrow-ios-form-font-tail' as ios-form-font;
|
| 4 |
|
| 5 |
// 介绍区域样式
|
| 6 |
.intro-section {
|
|
|
|
| 192 |
box-sizing: border-box; // 确保宽度计算正确
|
| 193 |
}
|
| 194 |
}
|
| 195 |
+
|
| 196 |
+
// 保持在本文件末尾;说明见 responsive.narrow-ios-form-font
|
| 197 |
+
@include ios-form-font.apply;
|
client/src/css/pages/attribution.scss
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
@use "attribution-inspector";
|
| 4 |
@use "chat-input-panel";
|
| 5 |
@use "breakpoints" as *;
|
|
|
|
| 6 |
|
| 7 |
// 桌面端:覆盖全局 min-height:100%,使 #results 按内容收缩(debug 面板紧跟其后)
|
| 8 |
@media (min-width: $breakpoint-tablet) {
|
|
@@ -116,7 +117,6 @@
|
|
| 116 |
.attribution-exclude-prompt-patterns-input {
|
| 117 |
width: 100%;
|
| 118 |
box-sizing: border-box;
|
| 119 |
-
font-size: 9pt;
|
| 120 |
padding: 6px 8px;
|
| 121 |
border: 1px solid var(--input-border);
|
| 122 |
border-radius: 4px;
|
|
@@ -128,3 +128,6 @@
|
|
| 128 |
cursor: not-allowed;
|
| 129 |
}
|
| 130 |
}
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
@use "attribution-inspector";
|
| 4 |
@use "chat-input-panel";
|
| 5 |
@use "breakpoints" as *;
|
| 6 |
+
@use '../base/narrow-ios-form-font-tail' as ios-form-font;
|
| 7 |
|
| 8 |
// 桌面端:覆盖全局 min-height:100%,使 #results 按内容收缩(debug 面板紧跟其后)
|
| 9 |
@media (min-width: $breakpoint-tablet) {
|
|
|
|
| 117 |
.attribution-exclude-prompt-patterns-input {
|
| 118 |
width: 100%;
|
| 119 |
box-sizing: border-box;
|
|
|
|
| 120 |
padding: 6px 8px;
|
| 121 |
border: 1px solid var(--input-border);
|
| 122 |
border-radius: 4px;
|
|
|
|
| 128 |
cursor: not-allowed;
|
| 129 |
}
|
| 130 |
}
|
| 131 |
+
|
| 132 |
+
// 保持在本文件末尾;说明见 responsive.narrow-ios-form-font
|
| 133 |
+
@include ios-form-font.apply;
|
client/src/css/pages/causal_flow.scss
CHANGED
|
@@ -4,6 +4,7 @@
|
|
| 4 |
@use "chat-input-panel";
|
| 5 |
@use "generation-status";
|
| 6 |
@use "lmf-readout" as lmf;
|
|
|
|
| 7 |
|
| 8 |
$gen-attr-option-row-gap: 12px;
|
| 9 |
$gen-attr-cached-demos-panel-width: 18em;
|
|
@@ -345,7 +346,7 @@ $gen-attr-cached-demos-panel-width: 18em;
|
|
| 345 |
#results.gen-attr-results-surface.LMF.css-pseudo-fullscreen-target {
|
| 346 |
width: 100vw;
|
| 347 |
height: 100vh;
|
| 348 |
-
padding:
|
| 349 |
background: var(--text-area-bg);
|
| 350 |
color: var(--text-color);
|
| 351 |
overflow: auto;
|
|
@@ -421,50 +422,8 @@ $gen-attr-cached-demos-panel-width: 18em;
|
|
| 421 |
max-height: 250px;
|
| 422 |
}
|
| 423 |
|
| 424 |
-
body.gen-attribute-page .input-section {
|
| 425 |
-
|
| 426 |
-
color: var(--text-muted);
|
| 427 |
-
}
|
| 428 |
-
|
| 429 |
-
.semantic-submode-row:not(.attribution-exclude-prompt-patterns-header):not(.attribution-exclude-generated-patterns-header) {
|
| 430 |
-
label.semantic-submode-label {
|
| 431 |
-
color: var(--text-primary);
|
| 432 |
-
}
|
| 433 |
-
|
| 434 |
-
label.semantic-submode-label:has(> input[type='checkbox']:not(:checked)) {
|
| 435 |
-
color: var(--text-muted);
|
| 436 |
-
}
|
| 437 |
-
}
|
| 438 |
-
|
| 439 |
-
// Start 上方的 prompt 区标题与同系字号(非 .semantic-submode-row 后代时补齐 9pt)
|
| 440 |
-
> .semantic-submode-row.chat-raw-prompt-mode-row label.semantic-submode-label {
|
| 441 |
-
display: inline-flex;
|
| 442 |
-
align-items: center;
|
| 443 |
-
gap: 6px;
|
| 444 |
-
cursor: pointer;
|
| 445 |
-
user-select: none;
|
| 446 |
-
}
|
| 447 |
-
|
| 448 |
-
.chat-prompt-panel > .input-header > .semantic-submode-label {
|
| 449 |
-
font-size: 9pt;
|
| 450 |
-
}
|
| 451 |
-
|
| 452 |
-
// System:在 input-header 内(非 semantic-submode-row),勾选主次色与勾选行一致
|
| 453 |
-
#gen_attr_system_prompt_panel.chat-prompt-panel > .input-header label.chat-use-system-label.semantic-submode-label {
|
| 454 |
-
font-size: 9pt;
|
| 455 |
-
|
| 456 |
-
&:has(> input[type='checkbox']:not(:checked)) {
|
| 457 |
-
color: var(--text-muted);
|
| 458 |
-
}
|
| 459 |
-
|
| 460 |
-
&:has(> input[type='checkbox']:checked) {
|
| 461 |
-
color: var(--text-primary);
|
| 462 |
-
}
|
| 463 |
-
}
|
| 464 |
-
|
| 465 |
-
.textarea-wrapper.chat-prompt-actions-row > .chat-completion-options-row.semantic-submode-row {
|
| 466 |
-
gap: $gen-attr-option-row-gap;
|
| 467 |
-
}
|
| 468 |
}
|
| 469 |
|
| 470 |
// 与 Attribution 页 Exclude prompt patterns 同形;generated 仅本页有 UI(持久化键见 attributionExclude*PatternsStorage)
|
|
@@ -583,7 +542,6 @@ body.gen-attribute-page .input-section {
|
|
| 583 |
.attribution-exclude-prompt-patterns-input {
|
| 584 |
width: 100%;
|
| 585 |
box-sizing: border-box;
|
| 586 |
-
font-size: 9pt;
|
| 587 |
padding: 6px 8px;
|
| 588 |
border: 1px solid var(--input-border);
|
| 589 |
border-radius: 4px;
|
|
@@ -600,3 +558,6 @@ body.gen-attribute-page .input-section {
|
|
| 600 |
body.css-pseudo-fullscreen-body-lock {
|
| 601 |
overflow: hidden;
|
| 602 |
}
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
@use "chat-input-panel";
|
| 5 |
@use "generation-status";
|
| 6 |
@use "lmf-readout" as lmf;
|
| 7 |
+
@use '../base/narrow-ios-form-font-tail' as ios-form-font;
|
| 8 |
|
| 9 |
$gen-attr-option-row-gap: 12px;
|
| 10 |
$gen-attr-cached-demos-panel-width: 18em;
|
|
|
|
| 346 |
#results.gen-attr-results-surface.LMF.css-pseudo-fullscreen-target {
|
| 347 |
width: 100vw;
|
| 348 |
height: 100vh;
|
| 349 |
+
padding: 6px;
|
| 350 |
background: var(--text-area-bg);
|
| 351 |
color: var(--text-color);
|
| 352 |
overflow: auto;
|
|
|
|
| 422 |
max-height: 250px;
|
| 423 |
}
|
| 424 |
|
| 425 |
+
body.gen-attribute-page .input-section .textarea-wrapper.chat-prompt-actions-row > .chat-completion-options-row.semantic-submode-row {
|
| 426 |
+
gap: $gen-attr-option-row-gap;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
}
|
| 428 |
|
| 429 |
// 与 Attribution 页 Exclude prompt patterns 同形;generated 仅本页有 UI(持久化键见 attributionExclude*PatternsStorage)
|
|
|
|
| 542 |
.attribution-exclude-prompt-patterns-input {
|
| 543 |
width: 100%;
|
| 544 |
box-sizing: border-box;
|
|
|
|
| 545 |
padding: 6px 8px;
|
| 546 |
border: 1px solid var(--input-border);
|
| 547 |
border-radius: 4px;
|
|
|
|
| 558 |
body.css-pseudo-fullscreen-body-lock {
|
| 559 |
overflow: hidden;
|
| 560 |
}
|
| 561 |
+
|
| 562 |
+
// 保持在本文件末尾;说明见 responsive.narrow-ios-form-font
|
| 563 |
+
@include ios-form-font.apply;
|
client/src/css/pages/chat.scss
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
// Chat 页:共用左栏见 _chat-input-panel.scss;此处仅右栏续写栈与 Chat 专有控件
|
| 2 |
@use 'app-pages';
|
|
|
|
| 3 |
|
| 4 |
@use 'chat-input-panel';
|
| 5 |
@use 'breakpoints' as *;
|
|
@@ -102,3 +103,6 @@
|
|
| 102 |
padding-right: calc(12px + var(--minimap-width));
|
| 103 |
}
|
| 104 |
}
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
// Chat 页:共用左栏见 _chat-input-panel.scss;此处仅右栏续写栈与 Chat 专有控件
|
| 2 |
@use 'app-pages';
|
| 3 |
+
@use '../base/narrow-ios-form-font-tail' as ios-form-font;
|
| 4 |
|
| 5 |
@use 'chat-input-panel';
|
| 6 |
@use 'breakpoints' as *;
|
|
|
|
| 103 |
padding-right: calc(12px + var(--minimap-width));
|
| 104 |
}
|
| 105 |
}
|
| 106 |
+
|
| 107 |
+
// 保持在本文件末尾;说明见 responsive.narrow-ios-form-font
|
| 108 |
+
@include ios-form-font.apply;
|
client/src/css/pages/compare.scss
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
// 对比页面样式 - app-pages 提供 chrome / demo / tooltip 等共用样式
|
| 2 |
@use 'app-pages';
|
| 3 |
@use "lmf-readout" as lmf;
|
|
|
|
| 4 |
|
| 5 |
// 主框架样式
|
| 6 |
.main_frame {
|
|
@@ -401,3 +402,6 @@
|
|
| 401 |
}
|
| 402 |
}
|
| 403 |
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
// 对比页面样式 - app-pages 提供 chrome / demo / tooltip 等共用样式
|
| 2 |
@use 'app-pages';
|
| 3 |
@use "lmf-readout" as lmf;
|
| 4 |
+
@use '../base/narrow-ios-form-font-tail' as ios-form-font;
|
| 5 |
|
| 6 |
// 主框架样式
|
| 7 |
.main_frame {
|
|
|
|
| 402 |
}
|
| 403 |
}
|
| 404 |
|
| 405 |
+
// 保持在本文件末尾;说明见 responsive.narrow-ios-form-font
|
| 406 |
+
@include ios-form-font.apply;
|
| 407 |
+
|
client/src/features/analysis/infoDensityRenderManager.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
| 3 |
|
| 4 |
export function getInfoDensityRenderDisabled(): boolean {
|
| 5 |
-
|
| 6 |
-
return v === 'true';
|
| 7 |
}
|
| 8 |
|
| 9 |
export function setInfoDensityRenderDisabled(disabled: boolean): void {
|
| 10 |
-
|
| 11 |
}
|
|
|
|
| 1 |
+
import { lsReadBool, lsWriteBool } from '../../shared/storage/localStorageHelpers';
|
| 2 |
+
|
| 3 |
+
/** 信息密度底色渲染开关 key:为 true 时关闭信息密度/classic 底色(语义叠加层不受影响) */
|
| 4 |
+
export const INFO_DENSITY_RENDER_DISABLED_KEY = 'info_radar_disable_info_density_render';
|
| 5 |
|
| 6 |
export function getInfoDensityRenderDisabled(): boolean {
|
| 7 |
+
return lsReadBool(INFO_DENSITY_RENDER_DISABLED_KEY, false);
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
export function setInfoDensityRenderDisabled(disabled: boolean): void {
|
| 11 |
+
lsWriteBool(INFO_DENSITY_RENDER_DISABLED_KEY, disabled);
|
| 12 |
}
|
client/src/features/causal_flow/bundledDemos.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
| 7 |
parseGenAttrCachedRunPayload,
|
| 8 |
type GenAttrCachedRun,
|
| 9 |
} from '../../shared/storage/genAttributeRunCache';
|
| 10 |
-
import {
|
| 11 |
|
| 12 |
const BASE = 'assets/demos/causal_flow/';
|
| 13 |
|
|
@@ -28,7 +28,7 @@ export type BundledDemoListEntry = { id: string; label: string };
|
|
| 28 |
|
| 29 |
/** 构建期固定的 bundled demo 列表(与当前 JS 同版本)。 */
|
| 30 |
export function getBundledGenAttributeDemoList(): readonly BundledDemoListEntry[] {
|
| 31 |
-
return
|
| 32 |
}
|
| 33 |
|
| 34 |
/**
|
|
|
|
| 7 |
parseGenAttrCachedRunPayload,
|
| 8 |
type GenAttrCachedRun,
|
| 9 |
} from '../../shared/storage/genAttributeRunCache';
|
| 10 |
+
import { GEN_ATTRIBUTE_BUNDLED_DEMOS } from './genAttributeBundledDemoManifest.generated';
|
| 11 |
|
| 12 |
const BASE = 'assets/demos/causal_flow/';
|
| 13 |
|
|
|
|
| 28 |
|
| 29 |
/** 构建期固定的 bundled demo 列表(与当前 JS 同版本)。 */
|
| 30 |
export function getBundledGenAttributeDemoList(): readonly BundledDemoListEntry[] {
|
| 31 |
+
return GEN_ATTRIBUTE_BUNDLED_DEMOS.map(({ slug, label }) => ({ id: slug, label }));
|
| 32 |
}
|
| 33 |
|
| 34 |
/**
|
client/src/features/causal_flow/genAttributeBundledDemoManifest.generated.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
/**
|
| 2 |
* Generated by GenAttributeDemoManifestPlugin — do not edit.
|
| 3 |
*/
|
| 4 |
-
export
|
|
|
|
|
|
| 1 |
/**
|
| 2 |
* Generated by GenAttributeDemoManifestPlugin — do not edit.
|
| 3 |
*/
|
| 4 |
+
export type GenAttributeBundledDemoManifestEntry = { readonly slug: string; readonly label: string };
|
| 5 |
+
export const GEN_ATTRIBUTE_BUNDLED_DEMOS: readonly GenAttributeBundledDemoManifestEntry[] = [{"slug":"Write a sonnet about love","label":"Poem | Write a sonnet about love"},{"slug":"写一首绝句,主题是春天","label":"写诗 | 写一首绝句,主题是春天"},{"slug":"CoT | 苏州所在省的省会","label":"CoT | 苏州所在省的省会"},{"slug":"CoT | 多“跳”推理","label":"CoT | 多“跳”推理"},{"slug":"过拟合|李白 将进酒","label":"过拟合|李白 将进酒"},{"slug":"CN->EN翻译","label":"CN->EN | 翻译"}];
|
client/src/features/chat/chatPromptTemplateMode.ts
CHANGED
|
@@ -1,21 +1,6 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* 与 Chat 页共用的「Raw prompt mode」开关(localStorage)。
|
| 3 |
-
* 在 Generate & Attribute 与 Chat 之间切换时保持一致。
|
| 4 |
-
*/
|
| 5 |
export const LS_SKIP_CHAT_TEMPLATE = 'chat_skip_chat_template';
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
} catch {
|
| 11 |
-
return false;
|
| 12 |
-
}
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
export function writeSkipChatTemplateToStorage(value: boolean): void {
|
| 16 |
-
try {
|
| 17 |
-
localStorage.setItem(LS_SKIP_CHAT_TEMPLATE, value ? 'true' : 'false');
|
| 18 |
-
} catch {
|
| 19 |
-
/* ignore quota / private mode */
|
| 20 |
-
}
|
| 21 |
-
}
|
|
|
|
| 1 |
+
/** Chat / Generate & Attribute 共用的「Raw prompt mode」开关 storage key */
|
|
|
|
|
|
|
|
|
|
| 2 |
export const LS_SKIP_CHAT_TEMPLATE = 'chat_skip_chat_template';
|
| 3 |
|
| 4 |
+
/** Enable thinking 开关(Chat / Causal Flow 各页独立 key,仅在 Chat template 模式下生效) */
|
| 5 |
+
export const CHAT_ENABLE_THINKING_STORAGE_KEY = 'info_radar_chat_enable_thinking';
|
| 6 |
+
export const GEN_ATTR_ENABLE_THINKING_STORAGE_KEY = 'info_radar_gen_attr_enable_thinking';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client/src/pages/analysis/index.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { saveHistory, initQueryHistoryDropdown } from '../../shared/cross/queryH
|
|
| 52 |
import { removeByQuery as removeSemanticCacheByQuery } from '../../shared/cross/semanticResultCache';
|
| 53 |
import { playAnalysisCompleteSound } from '../../shared/cross/soundNotification';
|
| 54 |
import { getSemanticMatchThreshold, setSemanticMatchThreshold } from '../../shared/cross/semanticThresholdManager';
|
|
|
|
| 55 |
import { SEMANTIC_MATCH_THRESHOLD } from '../../shared/core/constants';
|
| 56 |
import { SemanticSearchController } from '../../shared/controllers/semanticSearchController';
|
| 57 |
import { initDensityAttributionSidebar } from '../../shared/prediction_attribution/density_sidebar/densityAttributionSidebar';
|
|
@@ -137,7 +138,7 @@ window.onload = () => {
|
|
| 137 |
loadHomeContent('home-intro-content');
|
| 138 |
|
| 139 |
// minimap启用状态(优先使用localStorage,否则根据设备类型判断:移动端默认为false,桌面端默认为true)
|
| 140 |
-
const storedMinimap =
|
| 141 |
let enableMinimap: boolean = storedMinimap !== null
|
| 142 |
? storedMinimap === '1'
|
| 143 |
: !isMobileDevice();
|
|
@@ -257,9 +258,9 @@ window.onload = () => {
|
|
| 257 |
const validSubmodes = ['count', 'fill_blank', 'hybrid'];
|
| 258 |
const validColorSources = ['raw_score_normed', 'signal_probability', 'pw_score'];
|
| 259 |
const query = URLHandler.parameters['semantic_query'] ?? '';
|
| 260 |
-
const submode =
|
| 261 |
-
const chunked =
|
| 262 |
-
const colorSource =
|
| 263 |
const queryEl = document.getElementById('semantic_search_input') as HTMLInputElement | null;
|
| 264 |
if (queryEl) queryEl.value = typeof query === 'string' ? query : '';
|
| 265 |
const submodeEl = document.getElementById('semantic_submode_select') as HTMLSelectElement | null;
|
|
@@ -276,9 +277,9 @@ window.onload = () => {
|
|
| 276 |
const chunkedEl = document.getElementById('semantic_chunked_mode') as HTMLInputElement | null;
|
| 277 |
const colorEl = document.getElementById('semantic_color_source_select') as HTMLSelectElement | null;
|
| 278 |
const thresholdEl = document.getElementById('semantic_threshold_input') as HTMLInputElement | null;
|
| 279 |
-
|
| 280 |
-
if (chunkedEl)
|
| 281 |
-
if (colorEl)
|
| 282 |
if (thresholdEl) {
|
| 283 |
const v = parseFloat(thresholdEl.value);
|
| 284 |
if (Number.isFinite(v)) {
|
|
@@ -315,7 +316,7 @@ window.onload = () => {
|
|
| 315 |
lmf.updateOptions({
|
| 316 |
enableMinimap: enableMinimap
|
| 317 |
}, false);
|
| 318 |
-
|
| 319 |
},
|
| 320 |
onSemanticAnalysisToggle: (_enabled: boolean) => {
|
| 321 |
// 打开/关闭时都清除 query,并将 submode/chunked/color/阈值 重置为默认值并写回 localStorage
|
|
@@ -709,7 +710,7 @@ window.onload = () => {
|
|
| 709 |
return;
|
| 710 |
}
|
| 711 |
|
| 712 |
-
const lastPath =
|
| 713 |
const { options: folderOptions, defaultPath } = buildFolderOptions(folders, lastPath);
|
| 714 |
const defaultName = getDefaultDemoName(null, prefillText);
|
| 715 |
|
|
|
|
| 52 |
import { removeByQuery as removeSemanticCacheByQuery } from '../../shared/cross/semanticResultCache';
|
| 53 |
import { playAnalysisCompleteSound } from '../../shared/cross/soundNotification';
|
| 54 |
import { getSemanticMatchThreshold, setSemanticMatchThreshold } from '../../shared/cross/semanticThresholdManager';
|
| 55 |
+
import { lsGet, lsSet, lsWriteBool } from '../../shared/storage/localStorageHelpers';
|
| 56 |
import { SEMANTIC_MATCH_THRESHOLD } from '../../shared/core/constants';
|
| 57 |
import { SemanticSearchController } from '../../shared/controllers/semanticSearchController';
|
| 58 |
import { initDensityAttributionSidebar } from '../../shared/prediction_attribution/density_sidebar/densityAttributionSidebar';
|
|
|
|
| 138 |
loadHomeContent('home-intro-content');
|
| 139 |
|
| 140 |
// minimap启用状态(优先使用localStorage,否则根据设备类型判断:移动端默认为false,桌面端默认为true)
|
| 141 |
+
const storedMinimap = lsGet('minimap_enabled');
|
| 142 |
let enableMinimap: boolean = storedMinimap !== null
|
| 143 |
? storedMinimap === '1'
|
| 144 |
: !isMobileDevice();
|
|
|
|
| 258 |
const validSubmodes = ['count', 'fill_blank', 'hybrid'];
|
| 259 |
const validColorSources = ['raw_score_normed', 'signal_probability', 'pw_score'];
|
| 260 |
const query = URLHandler.parameters['semantic_query'] ?? '';
|
| 261 |
+
const submode = lsGet(SEMANTIC_KEYS.submode) ?? 'hybrid';
|
| 262 |
+
const chunked = lsGet(SEMANTIC_KEYS.chunked) !== '0';
|
| 263 |
+
const colorSource = lsGet(SEMANTIC_KEYS.colorSource) ?? 'pw_score';
|
| 264 |
const queryEl = document.getElementById('semantic_search_input') as HTMLInputElement | null;
|
| 265 |
if (queryEl) queryEl.value = typeof query === 'string' ? query : '';
|
| 266 |
const submodeEl = document.getElementById('semantic_submode_select') as HTMLSelectElement | null;
|
|
|
|
| 277 |
const chunkedEl = document.getElementById('semantic_chunked_mode') as HTMLInputElement | null;
|
| 278 |
const colorEl = document.getElementById('semantic_color_source_select') as HTMLSelectElement | null;
|
| 279 |
const thresholdEl = document.getElementById('semantic_threshold_input') as HTMLInputElement | null;
|
| 280 |
+
lsSet(SEMANTIC_KEYS.submode, submodeEl?.value ?? 'hybrid');
|
| 281 |
+
if (chunkedEl) lsWriteBool(SEMANTIC_KEYS.chunked, chunkedEl.checked, '1');
|
| 282 |
+
if (colorEl) lsSet(SEMANTIC_KEYS.colorSource, colorEl.value);
|
| 283 |
if (thresholdEl) {
|
| 284 |
const v = parseFloat(thresholdEl.value);
|
| 285 |
if (Number.isFinite(v)) {
|
|
|
|
| 316 |
lmf.updateOptions({
|
| 317 |
enableMinimap: enableMinimap
|
| 318 |
}, false);
|
| 319 |
+
lsWriteBool('minimap_enabled', enableMinimap, '1');
|
| 320 |
},
|
| 321 |
onSemanticAnalysisToggle: (_enabled: boolean) => {
|
| 322 |
// 打开/关闭时都清除 query,并将 submode/chunked/color/阈值 重置为默认值并写回 localStorage
|
|
|
|
| 710 |
return;
|
| 711 |
}
|
| 712 |
|
| 713 |
+
const lastPath = lsGet(LAST_SAVE_PATH_KEY);
|
| 714 |
const { options: folderOptions, defaultPath } = buildFolderOptions(folders, lastPath);
|
| 715 |
const defaultName = getDefaultDemoName(null, prefillText);
|
| 716 |
|
client/src/pages/attribution/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { loadPredictionAttributeWithCache } from '../../shared/prediction_attrib
|
|
| 40 |
import { readStoredEffectiveExcludePromptPatternsText } from '../../shared/prediction_attribution/core/attributionExcludePromptPatternsStorage';
|
| 41 |
import { bindExcludePromptPatternsUi } from '../../shared/prediction_attribution/core/excludePromptPatternsUi';
|
| 42 |
import { syncDraftCommittedButtonPair } from '../../shared/cross/syncDraftCommittedButtonPair';
|
|
|
|
| 43 |
|
| 44 |
d3.selectAll('.loadersmall').style('display', 'none');
|
| 45 |
|
|
@@ -52,13 +53,7 @@ const TARGET_HISTORY_KEY = 'info_radar_attribution_target_history';
|
|
| 52 |
const ATTRIBUTION_MODEL_VARIANT_STORAGE_KEY = 'info_radar_attribution_model_variant';
|
| 53 |
|
| 54 |
function readStoredAttributionPageModelVariant(): PredictionAttributeModelVariant {
|
| 55 |
-
|
| 56 |
-
const v = localStorage.getItem(ATTRIBUTION_MODEL_VARIANT_STORAGE_KEY);
|
| 57 |
-
if (v === 'base' || v === 'instruct') return v;
|
| 58 |
-
} catch {
|
| 59 |
-
// ignore
|
| 60 |
-
}
|
| 61 |
-
return 'instruct';
|
| 62 |
}
|
| 63 |
|
| 64 |
const apiPrefix = URLHandler.parameters['api'] || '';
|
|
@@ -101,11 +96,7 @@ function currentAttributionModelVariant(): PredictionAttributeModelVariant {
|
|
| 101 |
}
|
| 102 |
|
| 103 |
modelVariantSelect?.addEventListener('change', () => {
|
| 104 |
-
|
| 105 |
-
localStorage.setItem(ATTRIBUTION_MODEL_VARIANT_STORAGE_KEY, currentAttributionModelVariant());
|
| 106 |
-
} catch {
|
| 107 |
-
// ignore
|
| 108 |
-
}
|
| 109 |
});
|
| 110 |
|
| 111 |
// --- TextInputController ---
|
|
|
|
| 40 |
import { readStoredEffectiveExcludePromptPatternsText } from '../../shared/prediction_attribution/core/attributionExcludePromptPatternsStorage';
|
| 41 |
import { bindExcludePromptPatternsUi } from '../../shared/prediction_attribution/core/excludePromptPatternsUi';
|
| 42 |
import { syncDraftCommittedButtonPair } from '../../shared/cross/syncDraftCommittedButtonPair';
|
| 43 |
+
import { lsReadEnum, lsWriteString } from '../../shared/storage/localStorageHelpers';
|
| 44 |
|
| 45 |
d3.selectAll('.loadersmall').style('display', 'none');
|
| 46 |
|
|
|
|
| 53 |
const ATTRIBUTION_MODEL_VARIANT_STORAGE_KEY = 'info_radar_attribution_model_variant';
|
| 54 |
|
| 55 |
function readStoredAttributionPageModelVariant(): PredictionAttributeModelVariant {
|
| 56 |
+
return lsReadEnum(ATTRIBUTION_MODEL_VARIANT_STORAGE_KEY, ['base', 'instruct'] as const, 'instruct');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
const apiPrefix = URLHandler.parameters['api'] || '';
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
modelVariantSelect?.addEventListener('change', () => {
|
| 99 |
+
lsWriteString(ATTRIBUTION_MODEL_VARIANT_STORAGE_KEY, currentAttributionModelVariant());
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
});
|
| 101 |
|
| 102 |
// --- TextInputController ---
|
client/src/pages/causal_flow/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
| 33 |
DAG_COMPACTNESS_DEFAULT,
|
| 34 |
LINEAR_ARC_ADJACENT_GAP_DEFAULT,
|
| 35 |
} from '../../shared/prediction_attribution/causal_flow/genAttributeDagView';
|
|
|
|
| 36 |
import {
|
| 37 |
createHydratedTokenGenHandle,
|
| 38 |
startTokenGenAttribution,
|
|
@@ -85,11 +86,21 @@ import {
|
|
| 85 |
saveHistory,
|
| 86 |
} from '../../shared/cross/queryHistory';
|
| 87 |
import {
|
| 88 |
-
|
| 89 |
-
|
| 90 |
} from '../../features/chat/chatPromptTemplateMode';
|
| 91 |
import { postCompletionsPrompt, postCompletionsStop } from '../../shared/api/completionsClient';
|
| 92 |
import { updateApiUsageDisplay, updateModel, validateMetricsElements } from '../../shared/cross/textMetricsUpdater';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
d3.selectAll('.loadersmall').style('display', 'none');
|
| 95 |
|
|
@@ -162,7 +173,7 @@ const DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS: GenAttrDemoUiOptions = {
|
|
| 162 |
showDownstreamInfluence: false,
|
| 163 |
recursiveAttributionEnabled: false,
|
| 164 |
recursiveEdgeBatchAnimationEnabled: true,
|
| 165 |
-
recursiveEdgeBatchAnimationDirection: '
|
| 166 |
showTokenInfoOnSelected: false,
|
| 167 |
replayPacingMode: 'total',
|
| 168 |
playbackTotalS: GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT,
|
|
@@ -183,24 +194,13 @@ function createFlowId(): string {
|
|
| 183 |
}
|
| 184 |
|
| 185 |
function readStoredModelVariant(): PredictionAttributeModelVariant {
|
| 186 |
-
|
| 187 |
-
const v = localStorage.getItem(GEN_ATTR_MODEL_VARIANT_STORAGE_KEY);
|
| 188 |
-
if (v === 'base' || v === 'instruct') return v;
|
| 189 |
-
} catch {
|
| 190 |
-
// ignore
|
| 191 |
-
}
|
| 192 |
-
return 'instruct';
|
| 193 |
}
|
| 194 |
|
| 195 |
function readStoredMaxTokens(): number {
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
if (Number.isFinite(n) && n >= 1 && n <= 500) return n;
|
| 200 |
-
} catch {
|
| 201 |
-
// ignore
|
| 202 |
-
}
|
| 203 |
-
return GEN_ATTR_MAX_TOKENS_DEFAULT;
|
| 204 |
}
|
| 205 |
|
| 206 |
function clampDagMeasureWidth(n: number): number {
|
|
@@ -211,47 +211,32 @@ function clampDagMeasureWidth(n: number): number {
|
|
| 211 |
}
|
| 212 |
|
| 213 |
function readStoredDagMeasureWidth(): number {
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
if (Number.isFinite(n)) return clampDagMeasureWidth(n);
|
| 218 |
-
} catch {
|
| 219 |
-
// ignore
|
| 220 |
-
}
|
| 221 |
-
return GEN_ATTR_DAG_MEASURE_WIDTH_DEFAULT;
|
| 222 |
}
|
| 223 |
|
| 224 |
function readStoredDagCompactness(): number {
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
} catch {
|
| 230 |
-
// ignore
|
| 231 |
-
}
|
| 232 |
-
return DAG_COMPACTNESS_DEFAULT;
|
| 233 |
}
|
| 234 |
|
| 235 |
function readStoredDagEdgeTopPCoverage(): number {
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
// ignore
|
| 242 |
-
}
|
| 243 |
-
return DAG_EDGE_TOP_P_COVERAGE_DEFAULT;
|
| 244 |
}
|
| 245 |
|
| 246 |
function readStoredDagLinearArcAdjacentGap(): number {
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
// ignore
|
| 253 |
-
}
|
| 254 |
-
return LINEAR_ARC_ADJACENT_GAP_DEFAULT;
|
| 255 |
}
|
| 256 |
|
| 257 |
function clampDagPlaybackStepMs(n: number): number {
|
|
@@ -262,14 +247,11 @@ function clampDagPlaybackStepMs(n: number): number {
|
|
| 262 |
}
|
| 263 |
|
| 264 |
function readStoredDagPlaybackStepMs(): number {
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
// ignore
|
| 271 |
-
}
|
| 272 |
-
return GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT;
|
| 273 |
}
|
| 274 |
|
| 275 |
function clampDagPlaybackTotalS(n: number): number {
|
|
@@ -280,41 +262,27 @@ function clampDagPlaybackTotalS(n: number): number {
|
|
| 280 |
}
|
| 281 |
|
| 282 |
function readStoredDagPlaybackTotalS(): number {
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
// ignore
|
| 289 |
-
}
|
| 290 |
-
return GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT;
|
| 291 |
}
|
| 292 |
|
| 293 |
function readStoredDagReplayPacingMode(): DagReplayPacingMode {
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
}
|
| 300 |
-
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.replayPacingMode;
|
| 301 |
}
|
| 302 |
|
| 303 |
function readStoredDagLayoutMode(): DagLayoutMode {
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
v === 'linear-arc-step-down' ||
|
| 310 |
-
v === 'spiral'
|
| 311 |
-
) {
|
| 312 |
-
return v;
|
| 313 |
-
}
|
| 314 |
-
} catch {
|
| 315 |
-
// ignore
|
| 316 |
-
}
|
| 317 |
-
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.layoutMode;
|
| 318 |
}
|
| 319 |
|
| 320 |
const apiPrefix = URLHandler.parameters['api'] || '';
|
|
@@ -366,10 +334,12 @@ const genAttrTeacherForcingBlock = document.getElementById('gen_attr_teacher_for
|
|
| 366 |
const genAttrStopAfterTeacherForcing = document.getElementById(
|
| 367 |
'gen_attr_stop_after_teacher_forcing'
|
| 368 |
) as HTMLInputElement | null;
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
const submitBtn = d3.select('#gen_attr_submit_btn');
|
| 371 |
const loaderSmall = d3.select('.loadersmall');
|
| 372 |
-
const analyzeProgressEl = d3.select('#gen_attr_analyze_progress');
|
| 373 |
const metricUsage = d3.select('#gen_attr_metric_usage');
|
| 374 |
const metricModel = d3.select('#gen_attr_metric_model');
|
| 375 |
const genAttrResultsEl = d3.select('#results.gen-attr-results-surface');
|
|
@@ -410,6 +380,23 @@ function currentDagReplayPacingMode(): DagReplayPacingMode {
|
|
| 410 |
return dagReplayModeSelect?.value === 'step' ? 'step' : 'total';
|
| 411 |
}
|
| 412 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
/** 切换下拉时更新 `hidden`;样式见 `.gen-attr-dag-replay-value-wrap:not([hidden])`。 */
|
| 414 |
function applyDagReplaySpeedUi(): void {
|
| 415 |
const mode = currentDagReplayPacingMode();
|
|
@@ -504,28 +491,30 @@ function syncGenAttrExcludePatternTextareasDisabled(): void {
|
|
| 504 |
}
|
| 505 |
|
| 506 |
function hydrateGenAttrExcludePatternsFromGenAttrStorage(): void {
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
|
|
|
|
|
|
| 517 |
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
}
|
| 530 |
syncGenAttrExcludePatternTextareasDisabled();
|
| 531 |
}
|
|
@@ -543,7 +532,6 @@ function genAttrEffectiveExcludeGeneratedPatternsText(): string {
|
|
| 543 |
return genAttrExcludeGeneratedPatternsTa?.value ?? '';
|
| 544 |
}
|
| 545 |
|
| 546 |
-
if (modelVariantSelect) modelVariantSelect.value = readStoredModelVariant();
|
| 547 |
if (maxTokensInput) maxTokensInput.value = String(readStoredMaxTokens());
|
| 548 |
const initialDagLayoutMode = readStoredDagLayoutMode();
|
| 549 |
if (dagLayoutModeSelect) dagLayoutModeSelect.value = initialDagLayoutMode;
|
|
@@ -568,37 +556,27 @@ applyDagReplaySpeedUi();
|
|
| 568 |
|
| 569 |
const genAttrResultsNode = genAttrResultsEl.node() as HTMLElement | null;
|
| 570 |
function readStoredDagNodeCiVisualScale(): boolean {
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
}
|
| 577 |
-
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.nodeCiVisualScaleEnabled;
|
| 578 |
}
|
| 579 |
const initialDagNodeCiVisualScale = readStoredDagNodeCiVisualScale();
|
| 580 |
if (dagNodeCiVisualScaleInput) dagNodeCiVisualScaleInput.checked = initialDagNodeCiVisualScale;
|
| 581 |
setDagNodeCiVisualScaleEnabled(initialDagNodeCiVisualScale);
|
| 582 |
dagNodeCiVisualScaleInput?.addEventListener('change', () => {
|
| 583 |
const enabled = dagNodeCiVisualScaleInput.checked;
|
| 584 |
-
|
| 585 |
-
localStorage.setItem(GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY, enabled ? '1' : '0');
|
| 586 |
-
} catch {
|
| 587 |
-
/* ignore */
|
| 588 |
-
}
|
| 589 |
setDagNodeCiVisualScaleEnabled(enabled);
|
| 590 |
tryResetAndReplayDag();
|
| 591 |
});
|
| 592 |
|
| 593 |
function readStoredDagDecayAttributionToHighSurprisalTarget(): boolean {
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
if (legacy !== null) return legacy === '1';
|
| 599 |
-
} catch {
|
| 600 |
-
// ignore
|
| 601 |
-
}
|
| 602 |
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.decayAttributionToHighSurprisalTargetEnabled;
|
| 603 |
}
|
| 604 |
const initialDagDecayAttributionHighSurprisal = readStoredDagDecayAttributionToHighSurprisalTarget();
|
|
@@ -608,13 +586,9 @@ if (dagDecayAttributionHighSurprisalInput) {
|
|
| 608 |
setDagDecayAttributionToHighSurprisalTargetEnabled(initialDagDecayAttributionHighSurprisal);
|
| 609 |
dagDecayAttributionHighSurprisalInput?.addEventListener('change', () => {
|
| 610 |
const enabled = dagDecayAttributionHighSurprisalInput.checked;
|
| 611 |
-
|
| 612 |
-
localStorage.setItem(GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY, enabled ? '1' : '0');
|
| 613 |
-
} catch {
|
| 614 |
-
/* ignore */
|
| 615 |
-
}
|
| 616 |
setDagDecayAttributionToHighSurprisalTargetEnabled(enabled);
|
| 617 |
-
tryResetAndReplayDag();
|
| 618 |
});
|
| 619 |
|
| 620 |
function applyDagHideInactiveEdges(hide: boolean): void {
|
|
@@ -622,35 +596,27 @@ function applyDagHideInactiveEdges(hide: boolean): void {
|
|
| 622 |
genAttrResultsNode.classList.toggle('gen-attr-dag-hide-inactive-edges', hide);
|
| 623 |
}
|
| 624 |
function readStoredDagHideInactiveEdges(): boolean {
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
}
|
| 631 |
-
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.hideInactiveEdges;
|
| 632 |
}
|
| 633 |
const initialDagHideInactiveEdges = readStoredDagHideInactiveEdges();
|
| 634 |
if (dagHideInactiveEdgesInput) dagHideInactiveEdgesInput.checked = initialDagHideInactiveEdges;
|
| 635 |
applyDagHideInactiveEdges(initialDagHideInactiveEdges);
|
| 636 |
dagHideInactiveEdgesInput?.addEventListener('change', () => {
|
| 637 |
const hide = dagHideInactiveEdgesInput.checked;
|
| 638 |
-
|
| 639 |
-
localStorage.setItem(GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY, hide ? '1' : '0');
|
| 640 |
-
} catch {
|
| 641 |
-
/* ignore */
|
| 642 |
-
}
|
| 643 |
applyDagHideInactiveEdges(hide);
|
| 644 |
});
|
| 645 |
|
| 646 |
function readStoredDagShowDownstreamInfluence(): boolean {
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
}
|
| 653 |
-
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.showDownstreamInfluence;
|
| 654 |
}
|
| 655 |
const initialDagShowDownstreamInfluence = readStoredDagShowDownstreamInfluence();
|
| 656 |
if (dagShowDownstreamInfluenceInput) {
|
|
@@ -658,11 +624,7 @@ if (dagShowDownstreamInfluenceInput) {
|
|
| 658 |
}
|
| 659 |
dagShowDownstreamInfluenceInput?.addEventListener('change', () => {
|
| 660 |
const show = dagShowDownstreamInfluenceInput.checked;
|
| 661 |
-
|
| 662 |
-
localStorage.setItem(GEN_ATTR_DAG_SHOW_DOWNSTREAM_INFLUENCE_STORAGE_KEY, show ? '1' : '0');
|
| 663 |
-
} catch {
|
| 664 |
-
/* ignore */
|
| 665 |
-
}
|
| 666 |
dagHandle.setShowDownstreamInfluence(show);
|
| 667 |
});
|
| 668 |
|
|
@@ -682,35 +644,29 @@ function applyDagRecursiveAttributionSubmodeUi(): void {
|
|
| 682 |
}
|
| 683 |
|
| 684 |
function readStoredDagRecursiveAttribution(): boolean {
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
}
|
| 691 |
-
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.recursiveAttributionEnabled;
|
| 692 |
}
|
| 693 |
const initialDagRecursiveAttribution = readStoredDagRecursiveAttribution();
|
| 694 |
if (dagRecursiveAttributionInput) dagRecursiveAttributionInput.checked = initialDagRecursiveAttribution;
|
| 695 |
|
| 696 |
function readStoredDagRecursiveEdgeAnimationEnabled(): boolean {
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
}
|
| 703 |
-
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.recursiveEdgeBatchAnimationEnabled;
|
| 704 |
}
|
| 705 |
|
| 706 |
function readStoredDagRecursiveEdgeAnimationDirection(): DagRecursiveEdgeAnimationDirection {
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
}
|
| 713 |
-
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.recursiveEdgeBatchAnimationDirection;
|
| 714 |
}
|
| 715 |
|
| 716 |
const initialDagRecursiveEdgeAnimationEnabled = readStoredDagRecursiveEdgeAnimationEnabled();
|
|
@@ -723,74 +679,50 @@ if (dagRecursiveEdgeAnimationDirectionSelect) {
|
|
| 723 |
applyDagRecursiveAttributionSubmodeUi();
|
| 724 |
dagRecursiveAttributionInput?.addEventListener('change', () => {
|
| 725 |
const enabled = dagRecursiveAttributionInput.checked;
|
| 726 |
-
|
| 727 |
-
localStorage.setItem(GEN_ATTR_DAG_RECURSIVE_ATTRIBUTION_STORAGE_KEY, enabled ? '1' : '0');
|
| 728 |
-
} catch {
|
| 729 |
-
/* ignore */
|
| 730 |
-
}
|
| 731 |
applyDagRecursiveAttributionSubmodeUi();
|
| 732 |
dagHandle.setRecursiveAttributionEnabled(enabled);
|
| 733 |
});
|
| 734 |
dagRecursiveEdgeAnimationInput?.addEventListener('change', () => {
|
| 735 |
const enabled = dagRecursiveEdgeAnimationInput.checked;
|
| 736 |
-
|
| 737 |
-
localStorage.setItem(GEN_ATTR_DAG_RECURSIVE_EDGE_ANIMATION_STORAGE_KEY, enabled ? '1' : '0');
|
| 738 |
-
} catch {
|
| 739 |
-
/* ignore */
|
| 740 |
-
}
|
| 741 |
applyDagRecursiveAttributionSubmodeUi();
|
| 742 |
dagHandle.setRecursiveEdgeBatchAnimationEnabled(enabled);
|
| 743 |
});
|
| 744 |
dagRecursiveEdgeAnimationDirectionSelect?.addEventListener('change', () => {
|
| 745 |
const direction = currentDagRecursiveEdgeAnimationDirection();
|
| 746 |
dagRecursiveEdgeAnimationDirectionSelect.value = direction;
|
| 747 |
-
|
| 748 |
-
localStorage.setItem(GEN_ATTR_DAG_RECURSIVE_EDGE_ANIMATION_DIRECTION_STORAGE_KEY, direction);
|
| 749 |
-
} catch {
|
| 750 |
-
/* ignore */
|
| 751 |
-
}
|
| 752 |
dagHandle.setRecursiveEdgeBatchAnimationDirection(direction);
|
| 753 |
});
|
| 754 |
|
| 755 |
function readStoredDagHideExcludedTokens(): boolean {
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
}
|
| 762 |
-
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.hideExcludedTokens;
|
| 763 |
}
|
| 764 |
const initialDagHideExcludedTokens = readStoredDagHideExcludedTokens();
|
| 765 |
if (dagHideExcludedTokensInput) dagHideExcludedTokensInput.checked = initialDagHideExcludedTokens;
|
| 766 |
function readStoredDagShowTopkOnSelected(): boolean {
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
}
|
| 773 |
-
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.showTokenInfoOnSelected;
|
| 774 |
}
|
| 775 |
const initialDagShowTopkOnSelected = readStoredDagShowTopkOnSelected();
|
| 776 |
if (dagShowTopkOnSelectedInput) dagShowTopkOnSelectedInput.checked = initialDagShowTopkOnSelected;
|
| 777 |
dagHideExcludedTokensInput?.addEventListener('change', () => {
|
| 778 |
const hide = dagHideExcludedTokensInput.checked;
|
| 779 |
-
|
| 780 |
-
localStorage.setItem(GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY, hide ? '1' : '0');
|
| 781 |
-
} catch {
|
| 782 |
-
/* ignore */
|
| 783 |
-
}
|
| 784 |
dagHandle.setHideExcludedTokens(hide);
|
| 785 |
});
|
| 786 |
|
| 787 |
dagShowTopkOnSelectedInput?.addEventListener('change', () => {
|
| 788 |
const show = dagShowTopkOnSelectedInput.checked;
|
| 789 |
-
|
| 790 |
-
localStorage.setItem(GEN_ATTR_DAG_SHOW_TOPK_ON_SELECTED_STORAGE_KEY, show ? '1' : '0');
|
| 791 |
-
} catch {
|
| 792 |
-
/* ignore */
|
| 793 |
-
}
|
| 794 |
dagHandle.setShowTokenInfoOnSelected(show);
|
| 795 |
});
|
| 796 |
|
|
@@ -802,31 +734,24 @@ setPageOptsGetter(() => {
|
|
| 802 |
layout_spiral: mode === 'spiral',
|
| 803 |
propagated: dagRecursiveAttributionInput?.checked ?? false,
|
| 804 |
propagated_anim: dagRecursiveEdgeAnimationInput?.checked ?? true,
|
| 805 |
-
|
| 806 |
downstream: dagShowDownstreamInfluenceInput?.checked ?? false,
|
| 807 |
token_tooltip: dagShowTopkOnSelectedInput?.checked ?? false,
|
| 808 |
};
|
| 809 |
});
|
| 810 |
|
| 811 |
modelVariantSelect?.addEventListener('change', () => {
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
} catch {
|
| 815 |
-
/* ignore */
|
| 816 |
-
}
|
| 817 |
syncIdleModelMetric();
|
| 818 |
syncSubmitButtonState();
|
| 819 |
});
|
| 820 |
|
| 821 |
maxTokensInput?.addEventListener('change', () => {
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
);
|
| 827 |
-
} catch {
|
| 828 |
-
/* ignore */
|
| 829 |
-
}
|
| 830 |
syncSubmitButtonState();
|
| 831 |
});
|
| 832 |
|
|
@@ -837,20 +762,12 @@ dagPlaybackStepMsInput?.addEventListener('change', () => {
|
|
| 837 |
? clampDagPlaybackStepMs(raw)
|
| 838 |
: GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT;
|
| 839 |
dagPlaybackStepMsInput.value = String(ms);
|
| 840 |
-
|
| 841 |
-
localStorage.setItem(GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY, String(ms));
|
| 842 |
-
} catch {
|
| 843 |
-
/* ignore */
|
| 844 |
-
}
|
| 845 |
});
|
| 846 |
|
| 847 |
dagReplayModeSelect?.addEventListener('change', () => {
|
| 848 |
const mode = currentDagReplayPacingMode();
|
| 849 |
-
|
| 850 |
-
localStorage.setItem(GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY, mode);
|
| 851 |
-
} catch {
|
| 852 |
-
/* ignore */
|
| 853 |
-
}
|
| 854 |
applyDagReplaySpeedUi();
|
| 855 |
});
|
| 856 |
|
|
@@ -860,11 +777,7 @@ dagPlaybackTotalSInput?.addEventListener('change', () => {
|
|
| 860 |
? clampDagPlaybackTotalS(raw)
|
| 861 |
: GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT;
|
| 862 |
dagPlaybackTotalSInput.value = String(s);
|
| 863 |
-
|
| 864 |
-
localStorage.setItem(GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY, String(s));
|
| 865 |
-
} catch {
|
| 866 |
-
/* ignore */
|
| 867 |
-
}
|
| 868 |
});
|
| 869 |
|
| 870 |
function isSkipChatTemplate(): boolean {
|
|
@@ -875,6 +788,10 @@ function isGenAttrUseSystemPrompt(): boolean {
|
|
| 875 |
return genAttrUseSystemPromptInput?.checked ?? true;
|
| 876 |
}
|
| 877 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
function syncGenAttrSystemPromptSuppressedUi(): void {
|
| 879 |
const on = isGenAttrUseSystemPrompt();
|
| 880 |
genAttrSystemPromptPanel?.classList.toggle('chat-system-prompt-suppressed', !on);
|
|
@@ -896,6 +813,20 @@ function syncPromptPanelVisibility(): void {
|
|
| 896 |
if (chatInputPanel) chatInputPanel.hidden = skip;
|
| 897 |
}
|
| 898 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
function getActivePromptValue(): string {
|
| 900 |
if (isSkipChatTemplate()) {
|
| 901 |
return (rawTextField.node() as HTMLTextAreaElement | null)?.value ?? '';
|
|
@@ -947,6 +878,7 @@ function buildGenAttrRunDraftForCache(): GenAttrRunDraft {
|
|
| 947 |
system: systemPromptTextarea?.value ?? '',
|
| 948 |
user: userPromptTextarea?.value ?? '',
|
| 949 |
useSystem: isGenAttrUseSystemPrompt(),
|
|
|
|
| 950 |
...tfDraftFields,
|
| 951 |
};
|
| 952 |
}
|
|
@@ -1093,25 +1025,13 @@ function scheduleDagLastTokenDwell(action: () => void, dwellMs: number = DAG_LAS
|
|
| 1093 |
* `fullStepCount` 即生成 token 步数;prompt 帧 → step0 占一段,step0 → step1 占一段,依此类推。
|
| 1094 |
*/
|
| 1095 |
function resolveDagPlaybackStepDelayMsOnPlay(fullStepCount: number): number {
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
const ms = Number.isFinite(raw)
|
| 1099 |
-
? clampDagPlaybackStepMs(raw)
|
| 1100 |
-
: readStoredDagPlaybackStepMs();
|
| 1101 |
-
if (dagPlaybackStepMsInput) dagPlaybackStepMsInput.value = String(ms);
|
| 1102 |
-
return ms;
|
| 1103 |
-
}
|
| 1104 |
-
|
| 1105 |
-
const rawS = parseInt(dagPlaybackTotalSInput?.value ?? '', 10);
|
| 1106 |
-
const totalS = Number.isFinite(rawS)
|
| 1107 |
-
? clampDagPlaybackTotalS(rawS)
|
| 1108 |
-
: readStoredDagPlaybackTotalS();
|
| 1109 |
-
if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = String(totalS);
|
| 1110 |
|
| 1111 |
// prompt 帧作为等权第一段,共 fullStepCount 段(比原来的 fullStepCount-1 多一段)
|
| 1112 |
const transitionCount = Math.max(0, fullStepCount);
|
| 1113 |
if (transitionCount <= 0) return 0;
|
| 1114 |
-
return Math.round((totalS * 1000) / transitionCount);
|
| 1115 |
}
|
| 1116 |
|
| 1117 |
function stopDagPlayback(): void {
|
|
@@ -1232,6 +1152,7 @@ const dagHandle = initGenAttributeDagView(d3.select('#results'), {
|
|
| 1232 |
recursiveAttributionEnabled: initialDagRecursiveAttribution,
|
| 1233 |
recursiveEdgeBatchAnimationEnabled: initialDagRecursiveEdgeAnimationEnabled,
|
| 1234 |
recursiveEdgeBatchAnimationDirection: initialDagRecursiveEdgeAnimationDirection,
|
|
|
|
| 1235 |
edgeTopPCoverage: initialDagEdgeTopPCoverage,
|
| 1236 |
onFullscreenError: (message) => showToast(message, 'error'),
|
| 1237 |
getEffectiveExcludePromptPatternsText: genAttrEffectiveExcludePromptPatternsText,
|
|
@@ -1240,11 +1161,7 @@ const dagHandle = initGenAttributeDagView(d3.select('#results'), {
|
|
| 1240 |
|
| 1241 |
dagLayoutModeSelect?.addEventListener('change', () => {
|
| 1242 |
const mode = currentDagLayoutMode();
|
| 1243 |
-
|
| 1244 |
-
localStorage.setItem(GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY, mode);
|
| 1245 |
-
} catch {
|
| 1246 |
-
/* ignore */
|
| 1247 |
-
}
|
| 1248 |
applyDagLayoutModeUi();
|
| 1249 |
dagHandle.setLayoutMode(mode);
|
| 1250 |
});
|
|
@@ -1259,19 +1176,23 @@ function isDagBusy(): boolean {
|
|
| 1259 |
}
|
| 1260 |
|
| 1261 |
/**
|
| 1262 |
-
* 非忙状态下 reset + replay
|
| 1263 |
* 默认保留 DAG 选中节点;整页重置 UI 等场景传 `preserveNodeSelection: false`。
|
|
|
|
| 1264 |
*/
|
| 1265 |
-
function tryResetAndReplayDag(opts?: { preserveNodeSelection?: boolean }): void {
|
| 1266 |
if (isDagBusy()) return;
|
|
|
|
| 1267 |
const preserveSelection = opts?.preserveNodeSelection !== false;
|
| 1268 |
const preservedSelectedId = preserveSelection ? dagHandle.getSelectedNodeId() : null;
|
| 1269 |
const h = runnerHandle;
|
| 1270 |
-
dagHandle.reset();
|
| 1271 |
if (h && h.tokenCount > 0) {
|
| 1272 |
replayRunnerStepsIntoDag(h, currentRunPromptSpans.length > 0 ? currentRunPromptSpans : undefined);
|
| 1273 |
}
|
| 1274 |
-
|
|
|
|
|
|
|
| 1275 |
if (preservedSelectedId != null) {
|
| 1276 |
dagHandle.setSelectedNodeId(preservedSelectedId);
|
| 1277 |
} else {
|
|
@@ -1285,11 +1206,7 @@ dagMeasureWidthInput?.addEventListener('change', () => {
|
|
| 1285 |
? clampDagMeasureWidth(raw)
|
| 1286 |
: GEN_ATTR_DAG_MEASURE_WIDTH_DEFAULT;
|
| 1287 |
dagMeasureWidthInput.value = String(w);
|
| 1288 |
-
|
| 1289 |
-
localStorage.setItem(GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY, String(w));
|
| 1290 |
-
} catch {
|
| 1291 |
-
/* ignore */
|
| 1292 |
-
}
|
| 1293 |
dagHandle.setMeasureWidthPx(w);
|
| 1294 |
tryResetAndReplayDag();
|
| 1295 |
});
|
|
@@ -1298,11 +1215,7 @@ dagCompactnessInput?.addEventListener('change', () => {
|
|
| 1298 |
const raw = parseFloat(dagCompactnessInput.value);
|
| 1299 |
const c = Number.isFinite(raw) ? clampDagCompactness(raw) : DAG_COMPACTNESS_DEFAULT;
|
| 1300 |
dagCompactnessInput.value = String(c);
|
| 1301 |
-
|
| 1302 |
-
localStorage.setItem(GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY, String(c));
|
| 1303 |
-
} catch {
|
| 1304 |
-
/* ignore */
|
| 1305 |
-
}
|
| 1306 |
dagHandle.setDagCompactness(c);
|
| 1307 |
tryResetAndReplayDag();
|
| 1308 |
});
|
|
@@ -1313,13 +1226,9 @@ dagEdgeTopPCoverageInput?.addEventListener('change', () => {
|
|
| 1313 |
? clampDagEdgeTopPCoverage(raw)
|
| 1314 |
: DAG_EDGE_TOP_P_COVERAGE_DEFAULT;
|
| 1315 |
dagEdgeTopPCoverageInput.value = String(c);
|
| 1316 |
-
|
| 1317 |
-
localStorage.setItem(GEN_ATTR_DAG_EDGE_TOP_P_COVERAGE_STORAGE_KEY, String(c));
|
| 1318 |
-
} catch {
|
| 1319 |
-
/* ignore */
|
| 1320 |
-
}
|
| 1321 |
dagHandle.setEdgeTopPCoverage(c);
|
| 1322 |
-
tryResetAndReplayDag();
|
| 1323 |
});
|
| 1324 |
|
| 1325 |
dagLinearArcIntervalInput?.addEventListener('change', () => {
|
|
@@ -1328,11 +1237,7 @@ dagLinearArcIntervalInput?.addEventListener('change', () => {
|
|
| 1328 |
? clampLinearArcAdjacentGap(raw)
|
| 1329 |
: LINEAR_ARC_ADJACENT_GAP_DEFAULT;
|
| 1330 |
dagLinearArcIntervalInput.value = String(n);
|
| 1331 |
-
|
| 1332 |
-
localStorage.setItem(GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY, String(n));
|
| 1333 |
-
} catch {
|
| 1334 |
-
/* ignore */
|
| 1335 |
-
}
|
| 1336 |
dagHandle.setLinearArcAdjacentGapPx(n, { skipRefit: isDagBusy() });
|
| 1337 |
});
|
| 1338 |
|
|
@@ -1354,14 +1259,11 @@ function readGenAttrDemoUiOptionsFromControls(): GenAttrDemoUiOptions {
|
|
| 1354 |
const edgeTopPCoverage = Number.isFinite(rawTop)
|
| 1355 |
? clampDagEdgeTopPCoverage(rawTop)
|
| 1356 |
: DAG_EDGE_TOP_P_COVERAGE_DEFAULT;
|
| 1357 |
-
const
|
| 1358 |
-
|
| 1359 |
-
|
| 1360 |
-
:
|
| 1361 |
-
|
| 1362 |
-
const playbackStepMs = Number.isFinite(rawStep)
|
| 1363 |
-
? clampDagPlaybackStepMs(rawStep)
|
| 1364 |
-
: GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT;
|
| 1365 |
return {
|
| 1366 |
layoutMode: currentDagLayoutMode(),
|
| 1367 |
measureWidthPx,
|
|
@@ -1378,7 +1280,7 @@ function readGenAttrDemoUiOptionsFromControls(): GenAttrDemoUiOptions {
|
|
| 1378 |
recursiveEdgeBatchAnimationEnabled: dagRecursiveEdgeAnimationInput?.checked ?? true,
|
| 1379 |
recursiveEdgeBatchAnimationDirection: currentDagRecursiveEdgeAnimationDirection(),
|
| 1380 |
showTokenInfoOnSelected: dagShowTopkOnSelectedInput?.checked ?? false,
|
| 1381 |
-
replayPacingMode
|
| 1382 |
playbackTotalS,
|
| 1383 |
playbackStepMs,
|
| 1384 |
excludePromptPatternsEnabled: genAttrExcludePromptPatternsEnable?.checked ?? true,
|
|
@@ -1600,12 +1502,8 @@ const GEN_ATTR_DEMO_UI_LOCAL_STORAGE_KEYS: readonly string[] = [
|
|
| 1600 |
];
|
| 1601 |
|
| 1602 |
function removeGenAttrDemoUiOptionsFromLocalStorage(): void {
|
| 1603 |
-
|
| 1604 |
-
|
| 1605 |
-
localStorage.removeItem(k);
|
| 1606 |
-
}
|
| 1607 |
-
} catch {
|
| 1608 |
-
/* ignore */
|
| 1609 |
}
|
| 1610 |
}
|
| 1611 |
|
|
@@ -1646,17 +1544,13 @@ function bindExcludePatternControls(
|
|
| 1646 |
enabledKey: string,
|
| 1647 |
): void {
|
| 1648 |
enableEl?.addEventListener('change', () => {
|
| 1649 |
-
|
| 1650 |
-
|
| 1651 |
-
localStorage.setItem(enabledKey, enableEl.checked ? '1' : '0');
|
| 1652 |
-
} catch { /* ignore */ }
|
| 1653 |
syncGenAttrExcludePatternTextareasDisabled();
|
| 1654 |
onExcludePatternsEffectiveChange();
|
| 1655 |
});
|
| 1656 |
textEl?.addEventListener('blur', () => {
|
| 1657 |
-
|
| 1658 |
-
localStorage.setItem(textKey, textEl.value);
|
| 1659 |
-
} catch { /* ignore */ }
|
| 1660 |
onExcludePatternsEffectiveChange();
|
| 1661 |
});
|
| 1662 |
}
|
|
@@ -1675,6 +1569,7 @@ bindExcludePatternControls(
|
|
| 1675 |
);
|
| 1676 |
|
| 1677 |
function currentModelVariant(): PredictionAttributeModelVariant {
|
|
|
|
| 1678 |
const v = modelVariantSelect?.value;
|
| 1679 |
return v === 'base' || v === 'instruct' ? v : 'instruct';
|
| 1680 |
}
|
|
@@ -1727,6 +1622,7 @@ function getInputSnapshotForRun(): string {
|
|
| 1727 |
useSys: isGenAttrUseSystemPrompt(),
|
| 1728 |
sys: (systemTextField.node() as HTMLTextAreaElement | null)?.value ?? '',
|
| 1729 |
user: (userTextField.node() as HTMLTextAreaElement | null)?.value ?? '',
|
|
|
|
| 1730 |
...runOpts,
|
| 1731 |
});
|
| 1732 |
}
|
|
@@ -1735,9 +1631,6 @@ function setGenLoading(loading: boolean): void {
|
|
| 1735 |
inFlight = loading;
|
| 1736 |
loaderSmall.style('display', loading ? null : 'none');
|
| 1737 |
genAttrResultsEl.classed('gen-attr-in-flight', loading);
|
| 1738 |
-
if (!loading) {
|
| 1739 |
-
analyzeProgressEl.text('').style('display', 'none');
|
| 1740 |
-
}
|
| 1741 |
syncSubmitButtonState();
|
| 1742 |
}
|
| 1743 |
|
|
@@ -1790,15 +1683,24 @@ function bindInputsForSync(): void {
|
|
| 1790 |
}
|
| 1791 |
|
| 1792 |
if (skipChatTemplateInput) {
|
| 1793 |
-
skipChatTemplateInput.checked =
|
| 1794 |
skipChatTemplateInput.addEventListener('change', () => {
|
| 1795 |
-
|
| 1796 |
syncPromptPanelVisibility();
|
| 1797 |
syncGenAttrSystemPromptSuppressedUi();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1798 |
syncSubmitButtonState();
|
| 1799 |
});
|
| 1800 |
}
|
| 1801 |
syncPromptPanelVisibility();
|
|
|
|
| 1802 |
syncGenAttrSystemPromptSuppressedUi();
|
| 1803 |
genAttrUseSystemPromptInput?.addEventListener('change', () => {
|
| 1804 |
syncGenAttrSystemPromptSuppressedUi();
|
|
@@ -1914,28 +1816,39 @@ async function applyGenAttrCachedRun(
|
|
| 1914 |
}
|
| 1915 |
if (skipChatTemplateInput) {
|
| 1916 |
skipChatTemplateInput.checked = false;
|
| 1917 |
-
|
| 1918 |
syncPromptPanelVisibility();
|
| 1919 |
syncGenAttrSystemPromptSuppressedUi();
|
|
|
|
| 1920 |
}
|
| 1921 |
systemTextField.property('value', draft.system ?? '');
|
| 1922 |
systemPromptTextarea?.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1923 |
userTextField.property('value', draft.user ?? '');
|
| 1924 |
userPromptTextarea?.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1925 |
} else {
|
| 1926 |
if (skipChatTemplateInput) {
|
| 1927 |
skipChatTemplateInput.checked = true;
|
| 1928 |
-
|
| 1929 |
syncPromptPanelVisibility();
|
|
|
|
| 1930 |
}
|
| 1931 |
rawTextField.property('value', rec.initialContext);
|
| 1932 |
rawTextarea?.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1933 |
}
|
| 1934 |
|
| 1935 |
// 恢复 model / maxTokens(必须在 getInputSnapshotForRun() 之前,使快照与实际一致)
|
| 1936 |
-
if (draft?.model && modelVariantSelect) {
|
| 1937 |
modelVariantSelect.value = draft.model;
|
|
|
|
| 1938 |
}
|
|
|
|
| 1939 |
if (draft?.maxTokens != null && maxTokensInput) {
|
| 1940 |
maxTokensInput.value = String(draft.maxTokens);
|
| 1941 |
}
|
|
@@ -2107,11 +2020,7 @@ initQueryHistoryDropdown({
|
|
| 2107 |
refreshGenAttrBundledDemoEntriesList();
|
| 2108 |
syncGenAttrCachedDemosValueDisplay();
|
| 2109 |
|
| 2110 |
-
// ---
|
| 2111 |
-
function showProgress(current: number, total: number): void {
|
| 2112 |
-
analyzeProgressEl.text(`${current} / ${total}`).style('display', null);
|
| 2113 |
-
}
|
| 2114 |
-
|
| 2115 |
/** 首步 `token_attribution.length` ≈ 初始 prompt 子词数(与 Chat 展示同形,无需后端 usage) */
|
| 2116 |
function initialPromptTokensFromFirstStep(step: TokenGenStep): number | undefined {
|
| 2117 |
const n = step.response.token_attribution?.length;
|
|
@@ -2213,13 +2122,16 @@ async function resolveInitialContext(signal: AbortSignal): Promise<string> {
|
|
| 2213 |
const user = (userTextField.node() as HTMLTextAreaElement | null)?.value ?? '';
|
| 2214 |
const useSystem = isGenAttrUseSystemPrompt();
|
| 2215 |
const systemRaw = (systemTextField.node() as HTMLTextAreaElement | null)?.value ?? '';
|
| 2216 |
-
const promptReq: { model: string; prompt: string; system?: string } = {
|
| 2217 |
model: currentModelVariant(),
|
| 2218 |
prompt: user,
|
| 2219 |
};
|
| 2220 |
if (useSystem) {
|
| 2221 |
promptReq.system = systemRaw;
|
| 2222 |
}
|
|
|
|
|
|
|
|
|
|
| 2223 |
const assembled = await postCompletionsPrompt(promptReq, { signal });
|
| 2224 |
return assembled.prompt_used;
|
| 2225 |
}
|
|
@@ -2277,7 +2189,6 @@ async function runGeneration(): Promise<void> {
|
|
| 2277 |
const tokenizeModel = currentModelVariant();
|
| 2278 |
const runDraft = buildGenAttrRunDraftForCache();
|
| 2279 |
const prompt = getActivePromptValue();
|
| 2280 |
-
analyzeProgressEl.text('Assembling prompt…').style('display', null);
|
| 2281 |
initialContext = await resolveInitialContext(signal);
|
| 2282 |
lastRunInitialContext = initialContext;
|
| 2283 |
lastRunInputSnapshot = getInputSnapshotForRun();
|
|
@@ -2301,7 +2212,6 @@ async function runGeneration(): Promise<void> {
|
|
| 2301 |
let initialPromptTokens: number | undefined;
|
| 2302 |
currentRunPromptSpans = [];
|
| 2303 |
setGenAttrUsageMetric(undefined, 0);
|
| 2304 |
-
showProgress(0, maxTokens);
|
| 2305 |
|
| 2306 |
dagHandle.reset();
|
| 2307 |
void fetchTokenize(apiBaseForRequests, initialContext, tokenizeModel).then((spans) => {
|
|
@@ -2332,7 +2242,6 @@ async function runGeneration(): Promise<void> {
|
|
| 2332 |
const excludeCtx = excludeIntervalContextFromSteps(h.getAllSteps());
|
| 2333 |
pushDagFromPreprocess(step, stepIndex, true, excludeCtx);
|
| 2334 |
dagPlaybackNextIndex = stepIndex + 1;
|
| 2335 |
-
showProgress(stepIndex + 1, maxTokens);
|
| 2336 |
setGenAttrUsageMetric(initialPromptTokens, stepIndex + 1);
|
| 2337 |
showAttributionForStepIndex(stepIndex);
|
| 2338 |
},
|
|
@@ -2402,7 +2311,7 @@ function refreshDagForThemeChange(): void {
|
|
| 2402 |
stopDagPlayback();
|
| 2403 |
const h = runnerHandle;
|
| 2404 |
if (!h || h.tokenCount === 0) return;
|
| 2405 |
-
tryResetAndReplayDag();
|
| 2406 |
}
|
| 2407 |
|
| 2408 |
const themeManager = initThemeManager(
|
|
|
|
| 33 |
DAG_COMPACTNESS_DEFAULT,
|
| 34 |
LINEAR_ARC_ADJACENT_GAP_DEFAULT,
|
| 35 |
} from '../../shared/prediction_attribution/causal_flow/genAttributeDagView';
|
| 36 |
+
import type { DagRecursiveEdgeReplayPacing } from '../../shared/prediction_attribution/causal_flow/genAttributeDagRecursiveEdgeAnimation';
|
| 37 |
import {
|
| 38 |
createHydratedTokenGenHandle,
|
| 39 |
startTokenGenAttribution,
|
|
|
|
| 86 |
saveHistory,
|
| 87 |
} from '../../shared/cross/queryHistory';
|
| 88 |
import {
|
| 89 |
+
GEN_ATTR_ENABLE_THINKING_STORAGE_KEY,
|
| 90 |
+
LS_SKIP_CHAT_TEMPLATE,
|
| 91 |
} from '../../features/chat/chatPromptTemplateMode';
|
| 92 |
import { postCompletionsPrompt, postCompletionsStop } from '../../shared/api/completionsClient';
|
| 93 |
import { updateApiUsageDisplay, updateModel, validateMetricsElements } from '../../shared/cross/textMetricsUpdater';
|
| 94 |
+
import {
|
| 95 |
+
lsGet,
|
| 96 |
+
lsReadBool,
|
| 97 |
+
lsReadEnum,
|
| 98 |
+
lsReadNumber,
|
| 99 |
+
lsRemove,
|
| 100 |
+
lsSet,
|
| 101 |
+
lsWriteBool,
|
| 102 |
+
lsWriteString,
|
| 103 |
+
} from '../../shared/storage/localStorageHelpers';
|
| 104 |
|
| 105 |
d3.selectAll('.loadersmall').style('display', 'none');
|
| 106 |
|
|
|
|
| 173 |
showDownstreamInfluence: false,
|
| 174 |
recursiveAttributionEnabled: false,
|
| 175 |
recursiveEdgeBatchAnimationEnabled: true,
|
| 176 |
+
recursiveEdgeBatchAnimationDirection: 'forward',
|
| 177 |
showTokenInfoOnSelected: false,
|
| 178 |
replayPacingMode: 'total',
|
| 179 |
playbackTotalS: GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT,
|
|
|
|
| 194 |
}
|
| 195 |
|
| 196 |
function readStoredModelVariant(): PredictionAttributeModelVariant {
|
| 197 |
+
return lsReadEnum(GEN_ATTR_MODEL_VARIANT_STORAGE_KEY, ['base', 'instruct'] as const, 'instruct');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
}
|
| 199 |
|
| 200 |
function readStoredMaxTokens(): number {
|
| 201 |
+
return lsReadNumber(GEN_ATTR_MAX_TOKENS_STORAGE_KEY, GEN_ATTR_MAX_TOKENS_DEFAULT, {
|
| 202 |
+
validate: (n) => n >= 1 && n <= 500,
|
| 203 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
}
|
| 205 |
|
| 206 |
function clampDagMeasureWidth(n: number): number {
|
|
|
|
| 211 |
}
|
| 212 |
|
| 213 |
function readStoredDagMeasureWidth(): number {
|
| 214 |
+
return lsReadNumber(GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY, GEN_ATTR_DAG_MEASURE_WIDTH_DEFAULT, {
|
| 215 |
+
clamp: clampDagMeasureWidth,
|
| 216 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
}
|
| 218 |
|
| 219 |
function readStoredDagCompactness(): number {
|
| 220 |
+
return lsReadNumber(GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY, DAG_COMPACTNESS_DEFAULT, {
|
| 221 |
+
parse: 'float',
|
| 222 |
+
clamp: clampDagCompactness,
|
| 223 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
}
|
| 225 |
|
| 226 |
function readStoredDagEdgeTopPCoverage(): number {
|
| 227 |
+
return lsReadNumber(
|
| 228 |
+
GEN_ATTR_DAG_EDGE_TOP_P_COVERAGE_STORAGE_KEY,
|
| 229 |
+
DAG_EDGE_TOP_P_COVERAGE_DEFAULT,
|
| 230 |
+
{ parse: 'float', clamp: clampDagEdgeTopPCoverage },
|
| 231 |
+
);
|
|
|
|
|
|
|
|
|
|
| 232 |
}
|
| 233 |
|
| 234 |
function readStoredDagLinearArcAdjacentGap(): number {
|
| 235 |
+
return lsReadNumber(
|
| 236 |
+
GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY,
|
| 237 |
+
LINEAR_ARC_ADJACENT_GAP_DEFAULT,
|
| 238 |
+
{ clamp: clampLinearArcAdjacentGap },
|
| 239 |
+
);
|
|
|
|
|
|
|
|
|
|
| 240 |
}
|
| 241 |
|
| 242 |
function clampDagPlaybackStepMs(n: number): number {
|
|
|
|
| 247 |
}
|
| 248 |
|
| 249 |
function readStoredDagPlaybackStepMs(): number {
|
| 250 |
+
return lsReadNumber(
|
| 251 |
+
GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY,
|
| 252 |
+
GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT,
|
| 253 |
+
{ clamp: clampDagPlaybackStepMs },
|
| 254 |
+
);
|
|
|
|
|
|
|
|
|
|
| 255 |
}
|
| 256 |
|
| 257 |
function clampDagPlaybackTotalS(n: number): number {
|
|
|
|
| 262 |
}
|
| 263 |
|
| 264 |
function readStoredDagPlaybackTotalS(): number {
|
| 265 |
+
return lsReadNumber(
|
| 266 |
+
GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY,
|
| 267 |
+
GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT,
|
| 268 |
+
{ clamp: clampDagPlaybackTotalS },
|
| 269 |
+
);
|
|
|
|
|
|
|
|
|
|
| 270 |
}
|
| 271 |
|
| 272 |
function readStoredDagReplayPacingMode(): DagReplayPacingMode {
|
| 273 |
+
return lsReadEnum(
|
| 274 |
+
GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY,
|
| 275 |
+
['total', 'step'] as const,
|
| 276 |
+
DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.replayPacingMode,
|
| 277 |
+
);
|
|
|
|
|
|
|
| 278 |
}
|
| 279 |
|
| 280 |
function readStoredDagLayoutMode(): DagLayoutMode {
|
| 281 |
+
return lsReadEnum(
|
| 282 |
+
GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY,
|
| 283 |
+
['text-flow', 'linear-arc', 'linear-arc-step-down', 'spiral'] as const,
|
| 284 |
+
DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.layoutMode,
|
| 285 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
}
|
| 287 |
|
| 288 |
const apiPrefix = URLHandler.parameters['api'] || '';
|
|
|
|
| 334 |
const genAttrStopAfterTeacherForcing = document.getElementById(
|
| 335 |
'gen_attr_stop_after_teacher_forcing'
|
| 336 |
) as HTMLInputElement | null;
|
| 337 |
+
const genAttrEnableThinkingInput = document.getElementById(
|
| 338 |
+
'gen_attr_enable_thinking'
|
| 339 |
+
) as HTMLInputElement | null;
|
| 340 |
|
| 341 |
const submitBtn = d3.select('#gen_attr_submit_btn');
|
| 342 |
const loaderSmall = d3.select('.loadersmall');
|
|
|
|
| 343 |
const metricUsage = d3.select('#gen_attr_metric_usage');
|
| 344 |
const metricModel = d3.select('#gen_attr_metric_model');
|
| 345 |
const genAttrResultsEl = d3.select('#results.gen-attr-results-surface');
|
|
|
|
| 380 |
return dagReplayModeSelect?.value === 'step' ? 'step' : 'total';
|
| 381 |
}
|
| 382 |
|
| 383 |
+
/** DAG replay speed 控件 → 规范化节奏;生成回放、传播链动画、demo 导出共用。 */
|
| 384 |
+
function readDagReplayPacingFromControls(options?: { writeBack?: boolean }): DagRecursiveEdgeReplayPacing {
|
| 385 |
+
const rawStep = parseInt(dagPlaybackStepMsInput?.value ?? '', 10);
|
| 386 |
+
const stepMs = Number.isFinite(rawStep)
|
| 387 |
+
? clampDagPlaybackStepMs(rawStep)
|
| 388 |
+
: readStoredDagPlaybackStepMs();
|
| 389 |
+
const rawS = parseInt(dagPlaybackTotalSInput?.value ?? '', 10);
|
| 390 |
+
const totalS = Number.isFinite(rawS)
|
| 391 |
+
? clampDagPlaybackTotalS(rawS)
|
| 392 |
+
: readStoredDagPlaybackTotalS();
|
| 393 |
+
if (options?.writeBack) {
|
| 394 |
+
if (dagPlaybackStepMsInput) dagPlaybackStepMsInput.value = String(stepMs);
|
| 395 |
+
if (dagPlaybackTotalSInput) dagPlaybackTotalSInput.value = String(totalS);
|
| 396 |
+
}
|
| 397 |
+
return { mode: currentDagReplayPacingMode(), stepMs, totalS };
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
/** 切换下拉时更新 `hidden`;样式见 `.gen-attr-dag-replay-value-wrap:not([hidden])`。 */
|
| 401 |
function applyDagReplaySpeedUi(): void {
|
| 402 |
const mode = currentDagReplayPacingMode();
|
|
|
|
| 491 |
}
|
| 492 |
|
| 493 |
function hydrateGenAttrExcludePatternsFromGenAttrStorage(): void {
|
| 494 |
+
const savedPrompt = lsGet(GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_STORAGE_KEY);
|
| 495 |
+
if (genAttrExcludePromptPatternsTa) {
|
| 496 |
+
genAttrExcludePromptPatternsTa.value =
|
| 497 |
+
savedPrompt !== null ? savedPrompt : DEFAULT_EXCLUDE_PROMPT_PATTERNS_TEXT;
|
| 498 |
+
}
|
| 499 |
+
if (genAttrExcludePromptPatternsEnable) {
|
| 500 |
+
genAttrExcludePromptPatternsEnable.checked = lsReadBool(
|
| 501 |
+
GEN_ATTR_EXCLUDE_PROMPT_PATTERNS_ENABLED_STORAGE_KEY,
|
| 502 |
+
true,
|
| 503 |
+
{ encoding: '1' },
|
| 504 |
+
);
|
| 505 |
+
}
|
| 506 |
|
| 507 |
+
const savedGen = lsGet(GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_STORAGE_KEY);
|
| 508 |
+
if (genAttrExcludeGeneratedPatternsTa) {
|
| 509 |
+
genAttrExcludeGeneratedPatternsTa.value =
|
| 510 |
+
savedGen !== null ? savedGen : DEFAULT_EXCLUDE_GENERATED_PATTERNS_TEXT;
|
| 511 |
+
}
|
| 512 |
+
if (genAttrExcludeGeneratedPatternsEnable) {
|
| 513 |
+
genAttrExcludeGeneratedPatternsEnable.checked = lsReadBool(
|
| 514 |
+
GEN_ATTR_EXCLUDE_GENERATED_PATTERNS_ENABLED_STORAGE_KEY,
|
| 515 |
+
true,
|
| 516 |
+
{ encoding: '1' },
|
| 517 |
+
);
|
| 518 |
}
|
| 519 |
syncGenAttrExcludePatternTextareasDisabled();
|
| 520 |
}
|
|
|
|
| 532 |
return genAttrExcludeGeneratedPatternsTa?.value ?? '';
|
| 533 |
}
|
| 534 |
|
|
|
|
| 535 |
if (maxTokensInput) maxTokensInput.value = String(readStoredMaxTokens());
|
| 536 |
const initialDagLayoutMode = readStoredDagLayoutMode();
|
| 537 |
if (dagLayoutModeSelect) dagLayoutModeSelect.value = initialDagLayoutMode;
|
|
|
|
| 556 |
|
| 557 |
const genAttrResultsNode = genAttrResultsEl.node() as HTMLElement | null;
|
| 558 |
function readStoredDagNodeCiVisualScale(): boolean {
|
| 559 |
+
return lsReadBool(
|
| 560 |
+
GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY,
|
| 561 |
+
DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.nodeCiVisualScaleEnabled,
|
| 562 |
+
{ encoding: '1' },
|
| 563 |
+
);
|
|
|
|
|
|
|
| 564 |
}
|
| 565 |
const initialDagNodeCiVisualScale = readStoredDagNodeCiVisualScale();
|
| 566 |
if (dagNodeCiVisualScaleInput) dagNodeCiVisualScaleInput.checked = initialDagNodeCiVisualScale;
|
| 567 |
setDagNodeCiVisualScaleEnabled(initialDagNodeCiVisualScale);
|
| 568 |
dagNodeCiVisualScaleInput?.addEventListener('change', () => {
|
| 569 |
const enabled = dagNodeCiVisualScaleInput.checked;
|
| 570 |
+
lsWriteBool(GEN_ATTR_DAG_NODE_CI_VISUAL_SCALE_STORAGE_KEY, enabled, '1');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
setDagNodeCiVisualScaleEnabled(enabled);
|
| 572 |
tryResetAndReplayDag();
|
| 573 |
});
|
| 574 |
|
| 575 |
function readStoredDagDecayAttributionToHighSurprisalTarget(): boolean {
|
| 576 |
+
const v = lsGet(GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY);
|
| 577 |
+
if (v !== null) return v === '1';
|
| 578 |
+
const legacy = lsGet(GEN_ATTR_DAG_EDGE_WEAKEN_HIGH_SURPRISAL_STORAGE_KEY_LEGACY);
|
| 579 |
+
if (legacy !== null) return legacy === '1';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
return DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.decayAttributionToHighSurprisalTargetEnabled;
|
| 581 |
}
|
| 582 |
const initialDagDecayAttributionHighSurprisal = readStoredDagDecayAttributionToHighSurprisalTarget();
|
|
|
|
| 586 |
setDagDecayAttributionToHighSurprisalTargetEnabled(initialDagDecayAttributionHighSurprisal);
|
| 587 |
dagDecayAttributionHighSurprisalInput?.addEventListener('change', () => {
|
| 588 |
const enabled = dagDecayAttributionHighSurprisalInput.checked;
|
| 589 |
+
lsWriteBool(GEN_ATTR_DAG_DECAY_ATTRIBUTION_HIGH_SURPRISAL_STORAGE_KEY, enabled, '1');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
setDagDecayAttributionToHighSurprisalTargetEnabled(enabled);
|
| 591 |
+
tryResetAndReplayDag({ refit: false });
|
| 592 |
});
|
| 593 |
|
| 594 |
function applyDagHideInactiveEdges(hide: boolean): void {
|
|
|
|
| 596 |
genAttrResultsNode.classList.toggle('gen-attr-dag-hide-inactive-edges', hide);
|
| 597 |
}
|
| 598 |
function readStoredDagHideInactiveEdges(): boolean {
|
| 599 |
+
return lsReadBool(
|
| 600 |
+
GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY,
|
| 601 |
+
DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.hideInactiveEdges,
|
| 602 |
+
{ encoding: '1' },
|
| 603 |
+
);
|
|
|
|
|
|
|
| 604 |
}
|
| 605 |
const initialDagHideInactiveEdges = readStoredDagHideInactiveEdges();
|
| 606 |
if (dagHideInactiveEdgesInput) dagHideInactiveEdgesInput.checked = initialDagHideInactiveEdges;
|
| 607 |
applyDagHideInactiveEdges(initialDagHideInactiveEdges);
|
| 608 |
dagHideInactiveEdgesInput?.addEventListener('change', () => {
|
| 609 |
const hide = dagHideInactiveEdgesInput.checked;
|
| 610 |
+
lsWriteBool(GEN_ATTR_DAG_HIDE_INACTIVE_EDGES_STORAGE_KEY, hide, '1');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
applyDagHideInactiveEdges(hide);
|
| 612 |
});
|
| 613 |
|
| 614 |
function readStoredDagShowDownstreamInfluence(): boolean {
|
| 615 |
+
return lsReadBool(
|
| 616 |
+
GEN_ATTR_DAG_SHOW_DOWNSTREAM_INFLUENCE_STORAGE_KEY,
|
| 617 |
+
DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.showDownstreamInfluence,
|
| 618 |
+
{ encoding: '1' },
|
| 619 |
+
);
|
|
|
|
|
|
|
| 620 |
}
|
| 621 |
const initialDagShowDownstreamInfluence = readStoredDagShowDownstreamInfluence();
|
| 622 |
if (dagShowDownstreamInfluenceInput) {
|
|
|
|
| 624 |
}
|
| 625 |
dagShowDownstreamInfluenceInput?.addEventListener('change', () => {
|
| 626 |
const show = dagShowDownstreamInfluenceInput.checked;
|
| 627 |
+
lsWriteBool(GEN_ATTR_DAG_SHOW_DOWNSTREAM_INFLUENCE_STORAGE_KEY, show, '1');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
dagHandle.setShowDownstreamInfluence(show);
|
| 629 |
});
|
| 630 |
|
|
|
|
| 644 |
}
|
| 645 |
|
| 646 |
function readStoredDagRecursiveAttribution(): boolean {
|
| 647 |
+
return lsReadBool(
|
| 648 |
+
GEN_ATTR_DAG_RECURSIVE_ATTRIBUTION_STORAGE_KEY,
|
| 649 |
+
DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.recursiveAttributionEnabled,
|
| 650 |
+
{ encoding: '1' },
|
| 651 |
+
);
|
|
|
|
|
|
|
| 652 |
}
|
| 653 |
const initialDagRecursiveAttribution = readStoredDagRecursiveAttribution();
|
| 654 |
if (dagRecursiveAttributionInput) dagRecursiveAttributionInput.checked = initialDagRecursiveAttribution;
|
| 655 |
|
| 656 |
function readStoredDagRecursiveEdgeAnimationEnabled(): boolean {
|
| 657 |
+
return lsReadBool(
|
| 658 |
+
GEN_ATTR_DAG_RECURSIVE_EDGE_ANIMATION_STORAGE_KEY,
|
| 659 |
+
DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.recursiveEdgeBatchAnimationEnabled,
|
| 660 |
+
{ encoding: '1' },
|
| 661 |
+
);
|
|
|
|
|
|
|
| 662 |
}
|
| 663 |
|
| 664 |
function readStoredDagRecursiveEdgeAnimationDirection(): DagRecursiveEdgeAnimationDirection {
|
| 665 |
+
return lsReadEnum(
|
| 666 |
+
GEN_ATTR_DAG_RECURSIVE_EDGE_ANIMATION_DIRECTION_STORAGE_KEY,
|
| 667 |
+
['backward', 'forward'] as const,
|
| 668 |
+
DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.recursiveEdgeBatchAnimationDirection,
|
| 669 |
+
);
|
|
|
|
|
|
|
| 670 |
}
|
| 671 |
|
| 672 |
const initialDagRecursiveEdgeAnimationEnabled = readStoredDagRecursiveEdgeAnimationEnabled();
|
|
|
|
| 679 |
applyDagRecursiveAttributionSubmodeUi();
|
| 680 |
dagRecursiveAttributionInput?.addEventListener('change', () => {
|
| 681 |
const enabled = dagRecursiveAttributionInput.checked;
|
| 682 |
+
lsWriteBool(GEN_ATTR_DAG_RECURSIVE_ATTRIBUTION_STORAGE_KEY, enabled, '1');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
applyDagRecursiveAttributionSubmodeUi();
|
| 684 |
dagHandle.setRecursiveAttributionEnabled(enabled);
|
| 685 |
});
|
| 686 |
dagRecursiveEdgeAnimationInput?.addEventListener('change', () => {
|
| 687 |
const enabled = dagRecursiveEdgeAnimationInput.checked;
|
| 688 |
+
lsWriteBool(GEN_ATTR_DAG_RECURSIVE_EDGE_ANIMATION_STORAGE_KEY, enabled, '1');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
applyDagRecursiveAttributionSubmodeUi();
|
| 690 |
dagHandle.setRecursiveEdgeBatchAnimationEnabled(enabled);
|
| 691 |
});
|
| 692 |
dagRecursiveEdgeAnimationDirectionSelect?.addEventListener('change', () => {
|
| 693 |
const direction = currentDagRecursiveEdgeAnimationDirection();
|
| 694 |
dagRecursiveEdgeAnimationDirectionSelect.value = direction;
|
| 695 |
+
lsWriteString(GEN_ATTR_DAG_RECURSIVE_EDGE_ANIMATION_DIRECTION_STORAGE_KEY, direction);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
dagHandle.setRecursiveEdgeBatchAnimationDirection(direction);
|
| 697 |
});
|
| 698 |
|
| 699 |
function readStoredDagHideExcludedTokens(): boolean {
|
| 700 |
+
return lsReadBool(
|
| 701 |
+
GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY,
|
| 702 |
+
DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.hideExcludedTokens,
|
| 703 |
+
{ encoding: '1' },
|
| 704 |
+
);
|
|
|
|
|
|
|
| 705 |
}
|
| 706 |
const initialDagHideExcludedTokens = readStoredDagHideExcludedTokens();
|
| 707 |
if (dagHideExcludedTokensInput) dagHideExcludedTokensInput.checked = initialDagHideExcludedTokens;
|
| 708 |
function readStoredDagShowTopkOnSelected(): boolean {
|
| 709 |
+
return lsReadBool(
|
| 710 |
+
GEN_ATTR_DAG_SHOW_TOPK_ON_SELECTED_STORAGE_KEY,
|
| 711 |
+
DEFAULT_GEN_ATTR_DEMO_UI_OPTIONS.showTokenInfoOnSelected,
|
| 712 |
+
{ encoding: '1' },
|
| 713 |
+
);
|
|
|
|
|
|
|
| 714 |
}
|
| 715 |
const initialDagShowTopkOnSelected = readStoredDagShowTopkOnSelected();
|
| 716 |
if (dagShowTopkOnSelectedInput) dagShowTopkOnSelectedInput.checked = initialDagShowTopkOnSelected;
|
| 717 |
dagHideExcludedTokensInput?.addEventListener('change', () => {
|
| 718 |
const hide = dagHideExcludedTokensInput.checked;
|
| 719 |
+
lsWriteBool(GEN_ATTR_DAG_HIDE_EXCLUDED_TOKENS_STORAGE_KEY, hide, '1');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
dagHandle.setHideExcludedTokens(hide);
|
| 721 |
});
|
| 722 |
|
| 723 |
dagShowTopkOnSelectedInput?.addEventListener('change', () => {
|
| 724 |
const show = dagShowTopkOnSelectedInput.checked;
|
| 725 |
+
lsWriteBool(GEN_ATTR_DAG_SHOW_TOPK_ON_SELECTED_STORAGE_KEY, show, '1');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
dagHandle.setShowTokenInfoOnSelected(show);
|
| 727 |
});
|
| 728 |
|
|
|
|
| 734 |
layout_spiral: mode === 'spiral',
|
| 735 |
propagated: dagRecursiveAttributionInput?.checked ?? false,
|
| 736 |
propagated_anim: dagRecursiveEdgeAnimationInput?.checked ?? true,
|
| 737 |
+
propagated_anim_backward: currentDagRecursiveEdgeAnimationDirection() === 'backward',
|
| 738 |
downstream: dagShowDownstreamInfluenceInput?.checked ?? false,
|
| 739 |
token_tooltip: dagShowTopkOnSelectedInput?.checked ?? false,
|
| 740 |
};
|
| 741 |
});
|
| 742 |
|
| 743 |
modelVariantSelect?.addEventListener('change', () => {
|
| 744 |
+
if (!isSkipChatTemplate()) return;
|
| 745 |
+
lsWriteString(GEN_ATTR_MODEL_VARIANT_STORAGE_KEY, currentModelVariant());
|
|
|
|
|
|
|
|
|
|
| 746 |
syncIdleModelMetric();
|
| 747 |
syncSubmitButtonState();
|
| 748 |
});
|
| 749 |
|
| 750 |
maxTokensInput?.addEventListener('change', () => {
|
| 751 |
+
lsSet(
|
| 752 |
+
GEN_ATTR_MAX_TOKENS_STORAGE_KEY,
|
| 753 |
+
maxTokensInput?.value ?? String(GEN_ATTR_MAX_TOKENS_DEFAULT),
|
| 754 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
syncSubmitButtonState();
|
| 756 |
});
|
| 757 |
|
|
|
|
| 762 |
? clampDagPlaybackStepMs(raw)
|
| 763 |
: GEN_ATTR_DAG_PLAYBACK_STEP_MS_DEFAULT;
|
| 764 |
dagPlaybackStepMsInput.value = String(ms);
|
| 765 |
+
lsSet(GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY, String(ms));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
});
|
| 767 |
|
| 768 |
dagReplayModeSelect?.addEventListener('change', () => {
|
| 769 |
const mode = currentDagReplayPacingMode();
|
| 770 |
+
lsWriteString(GEN_ATTR_DAG_REPLAY_PACING_MODE_STORAGE_KEY, mode);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 771 |
applyDagReplaySpeedUi();
|
| 772 |
});
|
| 773 |
|
|
|
|
| 777 |
? clampDagPlaybackTotalS(raw)
|
| 778 |
: GEN_ATTR_DAG_PLAYBACK_TOTAL_S_DEFAULT;
|
| 779 |
dagPlaybackTotalSInput.value = String(s);
|
| 780 |
+
lsSet(GEN_ATTR_DAG_PLAYBACK_TOTAL_S_STORAGE_KEY, String(s));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
});
|
| 782 |
|
| 783 |
function isSkipChatTemplate(): boolean {
|
|
|
|
| 788 |
return genAttrUseSystemPromptInput?.checked ?? true;
|
| 789 |
}
|
| 790 |
|
| 791 |
+
function isEnableThinking(): boolean {
|
| 792 |
+
return genAttrEnableThinkingInput?.checked ?? false;
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
function syncGenAttrSystemPromptSuppressedUi(): void {
|
| 796 |
const on = isGenAttrUseSystemPrompt();
|
| 797 |
genAttrSystemPromptPanel?.classList.toggle('chat-system-prompt-suppressed', !on);
|
|
|
|
| 813 |
if (chatInputPanel) chatInputPanel.hidden = skip;
|
| 814 |
}
|
| 815 |
|
| 816 |
+
/** Chat template 下 model 恒为 instruct 且下拉仅展示;Raw 下读写 localStorage 偏好。 */
|
| 817 |
+
function syncModelVariantUi(): void {
|
| 818 |
+
if (!modelVariantSelect) return;
|
| 819 |
+
const skip = isSkipChatTemplate();
|
| 820 |
+
if (skip) {
|
| 821 |
+
modelVariantSelect.disabled = false;
|
| 822 |
+
modelVariantSelect.value = readStoredModelVariant();
|
| 823 |
+
} else {
|
| 824 |
+
modelVariantSelect.disabled = true;
|
| 825 |
+
modelVariantSelect.value = 'instruct';
|
| 826 |
+
}
|
| 827 |
+
syncIdleModelMetric();
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
function getActivePromptValue(): string {
|
| 831 |
if (isSkipChatTemplate()) {
|
| 832 |
return (rawTextField.node() as HTMLTextAreaElement | null)?.value ?? '';
|
|
|
|
| 878 |
system: systemPromptTextarea?.value ?? '',
|
| 879 |
user: userPromptTextarea?.value ?? '',
|
| 880 |
useSystem: isGenAttrUseSystemPrompt(),
|
| 881 |
+
enableThinking: isEnableThinking(),
|
| 882 |
...tfDraftFields,
|
| 883 |
};
|
| 884 |
}
|
|
|
|
| 1025 |
* `fullStepCount` 即生成 token 步数;prompt 帧 → step0 占一段,step0 → step1 占一段,依此类推。
|
| 1026 |
*/
|
| 1027 |
function resolveDagPlaybackStepDelayMsOnPlay(fullStepCount: number): number {
|
| 1028 |
+
const pacing = readDagReplayPacingFromControls({ writeBack: true });
|
| 1029 |
+
if (pacing.mode === 'step') return pacing.stepMs;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1030 |
|
| 1031 |
// prompt 帧作为等权第一段,共 fullStepCount 段(比原来的 fullStepCount-1 多一段)
|
| 1032 |
const transitionCount = Math.max(0, fullStepCount);
|
| 1033 |
if (transitionCount <= 0) return 0;
|
| 1034 |
+
return Math.round((pacing.totalS * 1000) / transitionCount);
|
| 1035 |
}
|
| 1036 |
|
| 1037 |
function stopDagPlayback(): void {
|
|
|
|
| 1152 |
recursiveAttributionEnabled: initialDagRecursiveAttribution,
|
| 1153 |
recursiveEdgeBatchAnimationEnabled: initialDagRecursiveEdgeAnimationEnabled,
|
| 1154 |
recursiveEdgeBatchAnimationDirection: initialDagRecursiveEdgeAnimationDirection,
|
| 1155 |
+
getReplayPacing: readDagReplayPacingFromControls,
|
| 1156 |
edgeTopPCoverage: initialDagEdgeTopPCoverage,
|
| 1157 |
onFullscreenError: (message) => showToast(message, 'error'),
|
| 1158 |
getEffectiveExcludePromptPatternsText: genAttrEffectiveExcludePromptPatternsText,
|
|
|
|
| 1161 |
|
| 1162 |
dagLayoutModeSelect?.addEventListener('change', () => {
|
| 1163 |
const mode = currentDagLayoutMode();
|
| 1164 |
+
lsWriteString(GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY, mode);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1165 |
applyDagLayoutModeUi();
|
| 1166 |
dagHandle.setLayoutMode(mode);
|
| 1167 |
});
|
|
|
|
| 1176 |
}
|
| 1177 |
|
| 1178 |
/**
|
| 1179 |
+
* 非忙状态下 reset + replay,按需 fit,供各设置项切换后复用。忙时为 no-op。
|
| 1180 |
* 默认保留 DAG 选中节点;整页重置 UI 等场景传 `preserveNodeSelection: false`。
|
| 1181 |
+
* `refit: false` 时 `reset(true)` 保留 pan/zoom(仅边集/样式类变更)。
|
| 1182 |
*/
|
| 1183 |
+
function tryResetAndReplayDag(opts?: { preserveNodeSelection?: boolean; refit?: boolean }): void {
|
| 1184 |
if (isDagBusy()) return;
|
| 1185 |
+
const refit = opts?.refit !== false;
|
| 1186 |
const preserveSelection = opts?.preserveNodeSelection !== false;
|
| 1187 |
const preservedSelectedId = preserveSelection ? dagHandle.getSelectedNodeId() : null;
|
| 1188 |
const h = runnerHandle;
|
| 1189 |
+
dagHandle.reset(!refit);
|
| 1190 |
if (h && h.tokenCount > 0) {
|
| 1191 |
replayRunnerStepsIntoDag(h, currentRunPromptSpans.length > 0 ? currentRunPromptSpans : undefined);
|
| 1192 |
}
|
| 1193 |
+
if (refit) {
|
| 1194 |
+
dagHandle.fitViewportToContent();
|
| 1195 |
+
}
|
| 1196 |
if (preservedSelectedId != null) {
|
| 1197 |
dagHandle.setSelectedNodeId(preservedSelectedId);
|
| 1198 |
} else {
|
|
|
|
| 1206 |
? clampDagMeasureWidth(raw)
|
| 1207 |
: GEN_ATTR_DAG_MEASURE_WIDTH_DEFAULT;
|
| 1208 |
dagMeasureWidthInput.value = String(w);
|
| 1209 |
+
lsSet(GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY, String(w));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1210 |
dagHandle.setMeasureWidthPx(w);
|
| 1211 |
tryResetAndReplayDag();
|
| 1212 |
});
|
|
|
|
| 1215 |
const raw = parseFloat(dagCompactnessInput.value);
|
| 1216 |
const c = Number.isFinite(raw) ? clampDagCompactness(raw) : DAG_COMPACTNESS_DEFAULT;
|
| 1217 |
dagCompactnessInput.value = String(c);
|
| 1218 |
+
lsSet(GEN_ATTR_DAG_COMPACTNESS_STORAGE_KEY, String(c));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1219 |
dagHandle.setDagCompactness(c);
|
| 1220 |
tryResetAndReplayDag();
|
| 1221 |
});
|
|
|
|
| 1226 |
? clampDagEdgeTopPCoverage(raw)
|
| 1227 |
: DAG_EDGE_TOP_P_COVERAGE_DEFAULT;
|
| 1228 |
dagEdgeTopPCoverageInput.value = String(c);
|
| 1229 |
+
lsSet(GEN_ATTR_DAG_EDGE_TOP_P_COVERAGE_STORAGE_KEY, String(c));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1230 |
dagHandle.setEdgeTopPCoverage(c);
|
| 1231 |
+
tryResetAndReplayDag({ refit: false });
|
| 1232 |
});
|
| 1233 |
|
| 1234 |
dagLinearArcIntervalInput?.addEventListener('change', () => {
|
|
|
|
| 1237 |
? clampLinearArcAdjacentGap(raw)
|
| 1238 |
: LINEAR_ARC_ADJACENT_GAP_DEFAULT;
|
| 1239 |
dagLinearArcIntervalInput.value = String(n);
|
| 1240 |
+
lsSet(GEN_ATTR_DAG_LINEAR_ARC_GAP_STORAGE_KEY, String(n));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1241 |
dagHandle.setLinearArcAdjacentGapPx(n, { skipRefit: isDagBusy() });
|
| 1242 |
});
|
| 1243 |
|
|
|
|
| 1259 |
const edgeTopPCoverage = Number.isFinite(rawTop)
|
| 1260 |
? clampDagEdgeTopPCoverage(rawTop)
|
| 1261 |
: DAG_EDGE_TOP_P_COVERAGE_DEFAULT;
|
| 1262 |
+
const {
|
| 1263 |
+
mode: replayPacingMode,
|
| 1264 |
+
stepMs: playbackStepMs,
|
| 1265 |
+
totalS: playbackTotalS,
|
| 1266 |
+
} = readDagReplayPacingFromControls();
|
|
|
|
|
|
|
|
|
|
| 1267 |
return {
|
| 1268 |
layoutMode: currentDagLayoutMode(),
|
| 1269 |
measureWidthPx,
|
|
|
|
| 1280 |
recursiveEdgeBatchAnimationEnabled: dagRecursiveEdgeAnimationInput?.checked ?? true,
|
| 1281 |
recursiveEdgeBatchAnimationDirection: currentDagRecursiveEdgeAnimationDirection(),
|
| 1282 |
showTokenInfoOnSelected: dagShowTopkOnSelectedInput?.checked ?? false,
|
| 1283 |
+
replayPacingMode,
|
| 1284 |
playbackTotalS,
|
| 1285 |
playbackStepMs,
|
| 1286 |
excludePromptPatternsEnabled: genAttrExcludePromptPatternsEnable?.checked ?? true,
|
|
|
|
| 1502 |
];
|
| 1503 |
|
| 1504 |
function removeGenAttrDemoUiOptionsFromLocalStorage(): void {
|
| 1505 |
+
for (const k of GEN_ATTR_DEMO_UI_LOCAL_STORAGE_KEYS) {
|
| 1506 |
+
lsRemove(k);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1507 |
}
|
| 1508 |
}
|
| 1509 |
|
|
|
|
| 1544 |
enabledKey: string,
|
| 1545 |
): void {
|
| 1546 |
enableEl?.addEventListener('change', () => {
|
| 1547 |
+
if (textEl) lsSet(textKey, textEl.value);
|
| 1548 |
+
lsWriteBool(enabledKey, enableEl.checked, '1');
|
|
|
|
|
|
|
| 1549 |
syncGenAttrExcludePatternTextareasDisabled();
|
| 1550 |
onExcludePatternsEffectiveChange();
|
| 1551 |
});
|
| 1552 |
textEl?.addEventListener('blur', () => {
|
| 1553 |
+
lsSet(textKey, textEl.value);
|
|
|
|
|
|
|
| 1554 |
onExcludePatternsEffectiveChange();
|
| 1555 |
});
|
| 1556 |
}
|
|
|
|
| 1569 |
);
|
| 1570 |
|
| 1571 |
function currentModelVariant(): PredictionAttributeModelVariant {
|
| 1572 |
+
if (!isSkipChatTemplate()) return 'instruct';
|
| 1573 |
const v = modelVariantSelect?.value;
|
| 1574 |
return v === 'base' || v === 'instruct' ? v : 'instruct';
|
| 1575 |
}
|
|
|
|
| 1622 |
useSys: isGenAttrUseSystemPrompt(),
|
| 1623 |
sys: (systemTextField.node() as HTMLTextAreaElement | null)?.value ?? '',
|
| 1624 |
user: (userTextField.node() as HTMLTextAreaElement | null)?.value ?? '',
|
| 1625 |
+
think: isEnableThinking(),
|
| 1626 |
...runOpts,
|
| 1627 |
});
|
| 1628 |
}
|
|
|
|
| 1631 |
inFlight = loading;
|
| 1632 |
loaderSmall.style('display', loading ? null : 'none');
|
| 1633 |
genAttrResultsEl.classed('gen-attr-in-flight', loading);
|
|
|
|
|
|
|
|
|
|
| 1634 |
syncSubmitButtonState();
|
| 1635 |
}
|
| 1636 |
|
|
|
|
| 1683 |
}
|
| 1684 |
|
| 1685 |
if (skipChatTemplateInput) {
|
| 1686 |
+
skipChatTemplateInput.checked = lsReadBool(LS_SKIP_CHAT_TEMPLATE, false);
|
| 1687 |
skipChatTemplateInput.addEventListener('change', () => {
|
| 1688 |
+
lsWriteBool(LS_SKIP_CHAT_TEMPLATE, skipChatTemplateInput.checked);
|
| 1689 |
syncPromptPanelVisibility();
|
| 1690 |
syncGenAttrSystemPromptSuppressedUi();
|
| 1691 |
+
syncModelVariantUi();
|
| 1692 |
+
syncSubmitButtonState();
|
| 1693 |
+
});
|
| 1694 |
+
}
|
| 1695 |
+
if (genAttrEnableThinkingInput) {
|
| 1696 |
+
genAttrEnableThinkingInput.checked = lsReadBool(GEN_ATTR_ENABLE_THINKING_STORAGE_KEY, false);
|
| 1697 |
+
genAttrEnableThinkingInput.addEventListener('change', () => {
|
| 1698 |
+
lsWriteBool(GEN_ATTR_ENABLE_THINKING_STORAGE_KEY, genAttrEnableThinkingInput.checked);
|
| 1699 |
syncSubmitButtonState();
|
| 1700 |
});
|
| 1701 |
}
|
| 1702 |
syncPromptPanelVisibility();
|
| 1703 |
+
syncModelVariantUi();
|
| 1704 |
syncGenAttrSystemPromptSuppressedUi();
|
| 1705 |
genAttrUseSystemPromptInput?.addEventListener('change', () => {
|
| 1706 |
syncGenAttrSystemPromptSuppressedUi();
|
|
|
|
| 1816 |
}
|
| 1817 |
if (skipChatTemplateInput) {
|
| 1818 |
skipChatTemplateInput.checked = false;
|
| 1819 |
+
lsWriteBool(LS_SKIP_CHAT_TEMPLATE, false);
|
| 1820 |
syncPromptPanelVisibility();
|
| 1821 |
syncGenAttrSystemPromptSuppressedUi();
|
| 1822 |
+
syncModelVariantUi();
|
| 1823 |
}
|
| 1824 |
systemTextField.property('value', draft.system ?? '');
|
| 1825 |
systemPromptTextarea?.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1826 |
userTextField.property('value', draft.user ?? '');
|
| 1827 |
userPromptTextarea?.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1828 |
+
if (genAttrEnableThinkingInput) {
|
| 1829 |
+
genAttrEnableThinkingInput.checked = draft.enableThinking ?? false;
|
| 1830 |
+
lsWriteBool(
|
| 1831 |
+
GEN_ATTR_ENABLE_THINKING_STORAGE_KEY,
|
| 1832 |
+
genAttrEnableThinkingInput.checked,
|
| 1833 |
+
);
|
| 1834 |
+
}
|
| 1835 |
} else {
|
| 1836 |
if (skipChatTemplateInput) {
|
| 1837 |
skipChatTemplateInput.checked = true;
|
| 1838 |
+
lsWriteBool(LS_SKIP_CHAT_TEMPLATE, true);
|
| 1839 |
syncPromptPanelVisibility();
|
| 1840 |
+
syncModelVariantUi();
|
| 1841 |
}
|
| 1842 |
rawTextField.property('value', rec.initialContext);
|
| 1843 |
rawTextarea?.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1844 |
}
|
| 1845 |
|
| 1846 |
// 恢复 model / maxTokens(必须在 getInputSnapshotForRun() 之前,使快照与实际一致)
|
| 1847 |
+
if (draft?.mode === 'raw' && draft.model && modelVariantSelect) {
|
| 1848 |
modelVariantSelect.value = draft.model;
|
| 1849 |
+
lsWriteString(GEN_ATTR_MODEL_VARIANT_STORAGE_KEY, draft.model);
|
| 1850 |
}
|
| 1851 |
+
syncModelVariantUi();
|
| 1852 |
if (draft?.maxTokens != null && maxTokensInput) {
|
| 1853 |
maxTokensInput.value = String(draft.maxTokens);
|
| 1854 |
}
|
|
|
|
| 2020 |
refreshGenAttrBundledDemoEntriesList();
|
| 2021 |
syncGenAttrCachedDemosValueDisplay();
|
| 2022 |
|
| 2023 |
+
// --- 指标 ---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2024 |
/** 首步 `token_attribution.length` ≈ 初始 prompt 子词数(与 Chat 展示同形,无需后端 usage) */
|
| 2025 |
function initialPromptTokensFromFirstStep(step: TokenGenStep): number | undefined {
|
| 2026 |
const n = step.response.token_attribution?.length;
|
|
|
|
| 2122 |
const user = (userTextField.node() as HTMLTextAreaElement | null)?.value ?? '';
|
| 2123 |
const useSystem = isGenAttrUseSystemPrompt();
|
| 2124 |
const systemRaw = (systemTextField.node() as HTMLTextAreaElement | null)?.value ?? '';
|
| 2125 |
+
const promptReq: { model: string; prompt: string; system?: string; enable_thinking?: boolean } = {
|
| 2126 |
model: currentModelVariant(),
|
| 2127 |
prompt: user,
|
| 2128 |
};
|
| 2129 |
if (useSystem) {
|
| 2130 |
promptReq.system = systemRaw;
|
| 2131 |
}
|
| 2132 |
+
if (isEnableThinking()) {
|
| 2133 |
+
promptReq.enable_thinking = true;
|
| 2134 |
+
}
|
| 2135 |
const assembled = await postCompletionsPrompt(promptReq, { signal });
|
| 2136 |
return assembled.prompt_used;
|
| 2137 |
}
|
|
|
|
| 2189 |
const tokenizeModel = currentModelVariant();
|
| 2190 |
const runDraft = buildGenAttrRunDraftForCache();
|
| 2191 |
const prompt = getActivePromptValue();
|
|
|
|
| 2192 |
initialContext = await resolveInitialContext(signal);
|
| 2193 |
lastRunInitialContext = initialContext;
|
| 2194 |
lastRunInputSnapshot = getInputSnapshotForRun();
|
|
|
|
| 2212 |
let initialPromptTokens: number | undefined;
|
| 2213 |
currentRunPromptSpans = [];
|
| 2214 |
setGenAttrUsageMetric(undefined, 0);
|
|
|
|
| 2215 |
|
| 2216 |
dagHandle.reset();
|
| 2217 |
void fetchTokenize(apiBaseForRequests, initialContext, tokenizeModel).then((spans) => {
|
|
|
|
| 2242 |
const excludeCtx = excludeIntervalContextFromSteps(h.getAllSteps());
|
| 2243 |
pushDagFromPreprocess(step, stepIndex, true, excludeCtx);
|
| 2244 |
dagPlaybackNextIndex = stepIndex + 1;
|
|
|
|
| 2245 |
setGenAttrUsageMetric(initialPromptTokens, stepIndex + 1);
|
| 2246 |
showAttributionForStepIndex(stepIndex);
|
| 2247 |
},
|
|
|
|
| 2311 |
stopDagPlayback();
|
| 2312 |
const h = runnerHandle;
|
| 2313 |
if (!h || h.tokenCount === 0) return;
|
| 2314 |
+
tryResetAndReplayDag({ refit: false });
|
| 2315 |
}
|
| 2316 |
|
| 2317 |
const themeManager = initThemeManager(
|
client/src/pages/chat/index.ts
CHANGED
|
@@ -53,9 +53,10 @@ import {
|
|
| 53 |
} from '../../shared/cross/contentUrl';
|
| 54 |
import { CHAT_SURPRISAL_COLOR_MAP_MAX } from '../../shared/cross/SurprisalColorConfig';
|
| 55 |
import { updateChatCompletionMetrics } from '../../shared/cross/textMetricsUpdater';
|
|
|
|
| 56 |
import {
|
| 57 |
-
|
| 58 |
-
|
| 59 |
} from '../../features/chat/chatPromptTemplateMode';
|
| 60 |
import { createToast } from '../../shared/ui/toast';
|
| 61 |
import { initDensityAttributionSidebar } from '../../shared/prediction_attribution/density_sidebar/densityAttributionSidebar';
|
|
@@ -110,6 +111,9 @@ const chatUseSystemPromptInput = document.getElementById(
|
|
| 110 |
'chat_use_system_prompt'
|
| 111 |
) as HTMLInputElement | null;
|
| 112 |
const chatSystemPromptPanel = document.getElementById('chat_system_prompt_panel');
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
function isSkipChatTemplate(): boolean {
|
| 115 |
return skipChatTemplateInput?.checked ?? false;
|
|
@@ -119,6 +123,10 @@ function isChatUseSystemPrompt(): boolean {
|
|
| 119 |
return chatUseSystemPromptInput?.checked ?? true;
|
| 120 |
}
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
function syncChatSystemPromptSuppressedUi(): void {
|
| 123 |
const on = isChatUseSystemPrompt();
|
| 124 |
chatSystemPromptPanel?.classList.toggle('chat-system-prompt-suppressed', !on);
|
|
@@ -450,13 +458,16 @@ const runAsk = async (options?: { forceRefresh?: boolean }): Promise<void> => {
|
|
| 450 |
const useSystem = isChatUseSystemPrompt();
|
| 451 |
const systemRaw =
|
| 452 |
(chatSystemTextField.node() as HTMLTextAreaElement | null)?.value ?? '';
|
| 453 |
-
const promptReq: { model: string; prompt: string; system?: string } = {
|
| 454 |
model: completionModel,
|
| 455 |
prompt
|
| 456 |
};
|
| 457 |
if (useSystem) {
|
| 458 |
promptReq.system = systemRaw;
|
| 459 |
}
|
|
|
|
|
|
|
|
|
|
| 460 |
const assembled = await postCompletionsPrompt(promptReq, {
|
| 461 |
signal: askAbort.signal
|
| 462 |
});
|
|
@@ -526,14 +537,21 @@ const runAsk = async (options?: { forceRefresh?: boolean }): Promise<void> => {
|
|
| 526 |
};
|
| 527 |
|
| 528 |
if (skipChatTemplateInput) {
|
| 529 |
-
skipChatTemplateInput.checked =
|
| 530 |
skipChatTemplateInput.addEventListener('change', () => {
|
| 531 |
-
|
| 532 |
syncPromptPanelVisibility();
|
| 533 |
syncChatSystemPromptSuppressedUi();
|
| 534 |
syncAskButtonState();
|
| 535 |
});
|
| 536 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
syncPromptPanelVisibility();
|
| 538 |
syncChatSystemPromptSuppressedUi();
|
| 539 |
chatUseSystemPromptInput?.addEventListener('change', () => {
|
|
|
|
| 53 |
} from '../../shared/cross/contentUrl';
|
| 54 |
import { CHAT_SURPRISAL_COLOR_MAP_MAX } from '../../shared/cross/SurprisalColorConfig';
|
| 55 |
import { updateChatCompletionMetrics } from '../../shared/cross/textMetricsUpdater';
|
| 56 |
+
import { lsReadBool, lsWriteBool } from '../../shared/storage/localStorageHelpers';
|
| 57 |
import {
|
| 58 |
+
CHAT_ENABLE_THINKING_STORAGE_KEY,
|
| 59 |
+
LS_SKIP_CHAT_TEMPLATE,
|
| 60 |
} from '../../features/chat/chatPromptTemplateMode';
|
| 61 |
import { createToast } from '../../shared/ui/toast';
|
| 62 |
import { initDensityAttributionSidebar } from '../../shared/prediction_attribution/density_sidebar/densityAttributionSidebar';
|
|
|
|
| 111 |
'chat_use_system_prompt'
|
| 112 |
) as HTMLInputElement | null;
|
| 113 |
const chatSystemPromptPanel = document.getElementById('chat_system_prompt_panel');
|
| 114 |
+
const enableThinkingInput = document.getElementById(
|
| 115 |
+
'chat_enable_thinking'
|
| 116 |
+
) as HTMLInputElement | null;
|
| 117 |
|
| 118 |
function isSkipChatTemplate(): boolean {
|
| 119 |
return skipChatTemplateInput?.checked ?? false;
|
|
|
|
| 123 |
return chatUseSystemPromptInput?.checked ?? true;
|
| 124 |
}
|
| 125 |
|
| 126 |
+
function isEnableThinking(): boolean {
|
| 127 |
+
return enableThinkingInput?.checked ?? false;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
function syncChatSystemPromptSuppressedUi(): void {
|
| 131 |
const on = isChatUseSystemPrompt();
|
| 132 |
chatSystemPromptPanel?.classList.toggle('chat-system-prompt-suppressed', !on);
|
|
|
|
| 458 |
const useSystem = isChatUseSystemPrompt();
|
| 459 |
const systemRaw =
|
| 460 |
(chatSystemTextField.node() as HTMLTextAreaElement | null)?.value ?? '';
|
| 461 |
+
const promptReq: { model: string; prompt: string; system?: string; enable_thinking?: boolean } = {
|
| 462 |
model: completionModel,
|
| 463 |
prompt
|
| 464 |
};
|
| 465 |
if (useSystem) {
|
| 466 |
promptReq.system = systemRaw;
|
| 467 |
}
|
| 468 |
+
if (isEnableThinking()) {
|
| 469 |
+
promptReq.enable_thinking = true;
|
| 470 |
+
}
|
| 471 |
const assembled = await postCompletionsPrompt(promptReq, {
|
| 472 |
signal: askAbort.signal
|
| 473 |
});
|
|
|
|
| 537 |
};
|
| 538 |
|
| 539 |
if (skipChatTemplateInput) {
|
| 540 |
+
skipChatTemplateInput.checked = lsReadBool(LS_SKIP_CHAT_TEMPLATE, false);
|
| 541 |
skipChatTemplateInput.addEventListener('change', () => {
|
| 542 |
+
lsWriteBool(LS_SKIP_CHAT_TEMPLATE, skipChatTemplateInput.checked);
|
| 543 |
syncPromptPanelVisibility();
|
| 544 |
syncChatSystemPromptSuppressedUi();
|
| 545 |
syncAskButtonState();
|
| 546 |
});
|
| 547 |
}
|
| 548 |
+
if (enableThinkingInput) {
|
| 549 |
+
enableThinkingInput.checked = lsReadBool(CHAT_ENABLE_THINKING_STORAGE_KEY, false);
|
| 550 |
+
enableThinkingInput.addEventListener('change', () => {
|
| 551 |
+
lsWriteBool(CHAT_ENABLE_THINKING_STORAGE_KEY, enableThinkingInput.checked);
|
| 552 |
+
syncAskButtonState();
|
| 553 |
+
});
|
| 554 |
+
}
|
| 555 |
syncPromptPanelVisibility();
|
| 556 |
syncChatSystemPromptSuppressedUi();
|
| 557 |
chatUseSystemPromptInput?.addEventListener('change', () => {
|
client/src/pages/home/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import '../../shared/core/d3-polyfill';
|
|
| 5 |
import dagPreviewLight from '../../assets/images/dag.mov';
|
| 6 |
import dagPreviewDark from '../../assets/images/dag-dark.mov';
|
| 7 |
import dagSpiralPreview from '../../assets/images/dag-spiral.mov';
|
| 8 |
-
import dagCotPreview from '../../assets/images/dag-cot.
|
| 9 |
import '../../css/pages/home.scss';
|
| 10 |
|
| 11 |
import { initThemeManager, type Theme } from '../../shared/ui/theme';
|
|
@@ -43,7 +43,7 @@ function applyGenAttributeNavCardHref(): void {
|
|
| 43 |
});
|
| 44 |
}
|
| 45 |
|
| 46 |
-
const GEN_ATTRIBUTE_BADGE_LINK = '
|
| 47 |
|
| 48 |
const DAG_PREVIEW_BY_THEME: Record<Theme, string> = {
|
| 49 |
light: dagPreviewLight,
|
|
@@ -61,14 +61,14 @@ function initGenAttributeCardCarousel(): (theme: Theme) => void {
|
|
| 61 |
const dots = card?.querySelectorAll<HTMLButtonElement>('.nav-landing-card-carousel-dots button');
|
| 62 |
const flowVideo = card?.querySelector<HTMLVideoElement>('[data-slide="flow"] video');
|
| 63 |
const spiralVideo = card?.querySelector<HTMLVideoElement>('[data-slide="spiral"] video');
|
| 64 |
-
const
|
| 65 |
|
| 66 |
-
if (!card || !viewport || slides.length === 0 || !dots?.length || !flowVideo || !spiralVideo || !
|
| 67 |
return () => {};
|
| 68 |
}
|
| 69 |
|
| 70 |
spiralVideo.src = dagSpiralPreview;
|
| 71 |
-
|
| 72 |
|
| 73 |
let index = 0;
|
| 74 |
let paused = false;
|
|
@@ -98,10 +98,13 @@ function initGenAttributeCardCarousel(): (theme: Theme) => void {
|
|
| 98 |
const syncSlideVideos = (): void => {
|
| 99 |
flowVideo.pause();
|
| 100 |
spiralVideo.pause();
|
|
|
|
| 101 |
if (index === 0) {
|
| 102 |
void flowVideo.play().catch(() => {});
|
| 103 |
} else if (index === 1) {
|
| 104 |
void spiralVideo.play().catch(() => {});
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
};
|
| 107 |
|
|
|
|
| 5 |
import dagPreviewLight from '../../assets/images/dag.mov';
|
| 6 |
import dagPreviewDark from '../../assets/images/dag-dark.mov';
|
| 7 |
import dagSpiralPreview from '../../assets/images/dag-spiral.mov';
|
| 8 |
+
import dagCotPreview from '../../assets/images/dag-cot.mov';
|
| 9 |
import '../../css/pages/home.scss';
|
| 10 |
|
| 11 |
import { initThemeManager, type Theme } from '../../shared/ui/theme';
|
|
|
|
| 43 |
});
|
| 44 |
}
|
| 45 |
|
| 46 |
+
const GEN_ATTRIBUTE_BADGE_LINK = 'https://xhslink.com/m/PwoOhuuhtV';
|
| 47 |
|
| 48 |
const DAG_PREVIEW_BY_THEME: Record<Theme, string> = {
|
| 49 |
light: dagPreviewLight,
|
|
|
|
| 61 |
const dots = card?.querySelectorAll<HTMLButtonElement>('.nav-landing-card-carousel-dots button');
|
| 62 |
const flowVideo = card?.querySelector<HTMLVideoElement>('[data-slide="flow"] video');
|
| 63 |
const spiralVideo = card?.querySelector<HTMLVideoElement>('[data-slide="spiral"] video');
|
| 64 |
+
const cotVideo = card?.querySelector<HTMLVideoElement>('[data-slide="cot"] video');
|
| 65 |
|
| 66 |
+
if (!card || !viewport || slides.length === 0 || !dots?.length || !flowVideo || !spiralVideo || !cotVideo) {
|
| 67 |
return () => {};
|
| 68 |
}
|
| 69 |
|
| 70 |
spiralVideo.src = dagSpiralPreview;
|
| 71 |
+
cotVideo.src = dagCotPreview;
|
| 72 |
|
| 73 |
let index = 0;
|
| 74 |
let paused = false;
|
|
|
|
| 98 |
const syncSlideVideos = (): void => {
|
| 99 |
flowVideo.pause();
|
| 100 |
spiralVideo.pause();
|
| 101 |
+
cotVideo.pause();
|
| 102 |
if (index === 0) {
|
| 103 |
void flowVideo.play().catch(() => {});
|
| 104 |
} else if (index === 1) {
|
| 105 |
void spiralVideo.play().catch(() => {});
|
| 106 |
+
} else if (index === 2) {
|
| 107 |
+
void cotVideo.play().catch(() => {});
|
| 108 |
}
|
| 109 |
};
|
| 110 |
|
client/src/scripts/genAttributeDemoManifestPlugin.js
CHANGED
|
@@ -1,31 +1,106 @@
|
|
| 1 |
/**
|
| 2 |
-
* 构建前扫描 `assets/demos/causal_flow/*.json`,写入 `features/causal_flow/genAttributeBundledDemoManifest.generated.ts`,供 bundle 内联
|
| 3 |
-
*
|
|
|
|
|
|
|
| 4 |
*/
|
| 5 |
const path = require('path');
|
| 6 |
const fs = require('fs');
|
| 7 |
|
| 8 |
const REL_DIR = 'assets/demos/causal_flow';
|
| 9 |
const GENERATED_BASENAME = 'genAttributeBundledDemoManifest.generated.ts';
|
|
|
|
| 10 |
|
| 11 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
if (!fs.existsSync(srcDir)) return [];
|
| 13 |
-
|
| 14 |
-
|
|
|
|
| 15 |
.map((f) => f.replace(/\.json$/i, ''))
|
| 16 |
.filter((s) => s.length > 0);
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
function writeGeneratedModule(srcDir, outPath) {
|
| 23 |
-
const
|
| 24 |
const content =
|
| 25 |
'/**\n' +
|
| 26 |
' * Generated by GenAttributeDemoManifestPlugin — do not edit.\n' +
|
| 27 |
' */\n' +
|
| 28 |
-
|
|
|
|
| 29 |
if (fs.existsSync(outPath) && fs.readFileSync(outPath, 'utf8') === content) return;
|
| 30 |
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
| 31 |
fs.writeFileSync(outPath, content, 'utf8');
|
|
|
|
| 1 |
/**
|
| 2 |
+
* 构建前扫描 `assets/demos/causal_flow/*.json`,写入 `features/causal_flow/genAttributeBundledDemoManifest.generated.ts`,供 bundle 内联 demo 列表。
|
| 3 |
+
* 顺序与 UI 名:`order.json` 数组;项为 slug 字符串或 `{ slug, label? }`(无 label 则 UI 显示 slug)。
|
| 4 |
+
* 未列入 order 的 demo 按 UTF-16 码元序追加到末尾,label 同 slug。
|
| 5 |
+
* order 中的 slug 必须对应目录内已有 demo JSON;重复 slug 亦会在构建时报错。
|
| 6 |
*/
|
| 7 |
const path = require('path');
|
| 8 |
const fs = require('fs');
|
| 9 |
|
| 10 |
const REL_DIR = 'assets/demos/causal_flow';
|
| 11 |
const GENERATED_BASENAME = 'genAttributeBundledDemoManifest.generated.ts';
|
| 12 |
+
const ORDER_FILENAME = 'order.json';
|
| 13 |
|
| 14 |
+
function utf16Sort(slugs) {
|
| 15 |
+
return [...slugs].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function discoverSlugs(srcDir) {
|
| 19 |
if (!fs.existsSync(srcDir)) return [];
|
| 20 |
+
return fs
|
| 21 |
+
.readdirSync(srcDir)
|
| 22 |
+
.filter((f) => f.endsWith('.json') && f !== ORDER_FILENAME)
|
| 23 |
.map((f) => f.replace(/\.json$/i, ''))
|
| 24 |
.filter((s) => s.length > 0);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/** @returns {{ slug: string, label: string | null } | null} */
|
| 28 |
+
function parseOrderEntry(entry, index) {
|
| 29 |
+
const at = `${ORDER_FILENAME}[${index}]`;
|
| 30 |
+
if (typeof entry === 'string') {
|
| 31 |
+
const slug = entry.trim();
|
| 32 |
+
return slug ? { slug, label: null } : null;
|
| 33 |
+
}
|
| 34 |
+
if (entry && typeof entry === 'object' && typeof entry.slug === 'string') {
|
| 35 |
+
const slug = entry.slug.trim();
|
| 36 |
+
if (!slug) return null;
|
| 37 |
+
const label =
|
| 38 |
+
typeof entry.label === 'string' && entry.label.trim().length > 0
|
| 39 |
+
? entry.label.trim()
|
| 40 |
+
: null;
|
| 41 |
+
return { slug, label };
|
| 42 |
+
}
|
| 43 |
+
throw new Error(
|
| 44 |
+
`${at}: expected a slug string or { "slug": "...", "label"?: "..." }`
|
| 45 |
+
);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function readOrderEntries(srcDir) {
|
| 49 |
+
const orderPath = path.join(srcDir, ORDER_FILENAME);
|
| 50 |
+
if (!fs.existsSync(orderPath)) return null;
|
| 51 |
+
let raw;
|
| 52 |
+
try {
|
| 53 |
+
raw = JSON.parse(fs.readFileSync(orderPath, 'utf8'));
|
| 54 |
+
} catch (e) {
|
| 55 |
+
throw new Error(`${ORDER_FILENAME}: invalid JSON (${e.message})`);
|
| 56 |
+
}
|
| 57 |
+
if (!Array.isArray(raw)) {
|
| 58 |
+
throw new Error(`${ORDER_FILENAME}: expected a JSON array`);
|
| 59 |
+
}
|
| 60 |
+
return raw.map((entry, i) => parseOrderEntry(entry, i)).filter(Boolean);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function resolveLabel(slug, label) {
|
| 64 |
+
return label ?? slug;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function collectDemoEntries(srcDir) {
|
| 68 |
+
const discovered = new Set(discoverSlugs(srcDir));
|
| 69 |
+
const order = readOrderEntries(srcDir);
|
| 70 |
+
if (order == null) {
|
| 71 |
+
return utf16Sort([...discovered]).map((slug) => ({
|
| 72 |
+
slug,
|
| 73 |
+
label: slug,
|
| 74 |
+
}));
|
| 75 |
+
}
|
| 76 |
+
const seen = new Set();
|
| 77 |
+
const result = [];
|
| 78 |
+
for (const { slug, label } of order) {
|
| 79 |
+
if (seen.has(slug)) {
|
| 80 |
+
throw new Error(`${ORDER_FILENAME}: duplicate slug ${JSON.stringify(slug)}`);
|
| 81 |
+
}
|
| 82 |
+
if (!discovered.has(slug)) {
|
| 83 |
+
throw new Error(
|
| 84 |
+
`${ORDER_FILENAME}: unknown slug ${JSON.stringify(slug)} (no ${slug}.json under ${REL_DIR})`,
|
| 85 |
+
);
|
| 86 |
+
}
|
| 87 |
+
seen.add(slug);
|
| 88 |
+
result.push({ slug, label: resolveLabel(slug, label) });
|
| 89 |
+
}
|
| 90 |
+
for (const slug of utf16Sort([...discovered].filter((s) => !seen.has(s)))) {
|
| 91 |
+
result.push({ slug, label: slug });
|
| 92 |
+
}
|
| 93 |
+
return result;
|
| 94 |
}
|
| 95 |
|
| 96 |
function writeGeneratedModule(srcDir, outPath) {
|
| 97 |
+
const entries = collectDemoEntries(srcDir);
|
| 98 |
const content =
|
| 99 |
'/**\n' +
|
| 100 |
' * Generated by GenAttributeDemoManifestPlugin — do not edit.\n' +
|
| 101 |
' */\n' +
|
| 102 |
+
'export type GenAttributeBundledDemoManifestEntry = { readonly slug: string; readonly label: string };\n' +
|
| 103 |
+
`export const GEN_ATTRIBUTE_BUNDLED_DEMOS: readonly GenAttributeBundledDemoManifestEntry[] = ${JSON.stringify(entries)};\n`;
|
| 104 |
if (fs.existsSync(outPath) && fs.readFileSync(outPath, 'utf8') === content) return;
|
| 105 |
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
| 106 |
fs.writeFileSync(outPath, content, 'utf8');
|
client/src/scripts/injectPageMetaIntoHtml.js
CHANGED
|
@@ -97,7 +97,7 @@ function injectPageMeta(html, pageKey, doc) {
|
|
| 97 |
: `<div class="nav-landing-card-shot" aria-hidden="true"></div>`;
|
| 98 |
const badge =
|
| 99 |
navKey === 'causalFlow'
|
| 100 |
-
? `<span class="nav-landing-card-badge" title="Go to demo on RedNote: xhslink.com
|
| 101 |
: '';
|
| 102 |
|
| 103 |
if (navKey === 'causalFlow') {
|
|
@@ -117,7 +117,7 @@ function injectPageMeta(html, pageKey, doc) {
|
|
| 117 |
`<div class="nav-landing-card-carousel-viewport">` +
|
| 118 |
`<div class="nav-landing-card-slide" data-slide="flow">${slideLink('flow', '<video muted loop playsinline preload="metadata"></video>')}</div>` +
|
| 119 |
`<div class="nav-landing-card-slide" data-slide="spiral">${slideLink('spiral', '<video muted loop playsinline preload="none"></video>')}</div>` +
|
| 120 |
-
`<div class="nav-landing-card-slide" data-slide="cot">${slideLink('cot', '<
|
| 121 |
`</div>` +
|
| 122 |
`<button type="button" class="nav-landing-card-carousel-arrow nav-landing-card-carousel-arrow--prev" aria-label="Previous preview">‹</button>` +
|
| 123 |
`<button type="button" class="nav-landing-card-carousel-arrow nav-landing-card-carousel-arrow--next" aria-label="Next preview">›</button>` +
|
|
|
|
| 97 |
: `<div class="nav-landing-card-shot" aria-hidden="true"></div>`;
|
| 98 |
const badge =
|
| 99 |
navKey === 'causalFlow'
|
| 100 |
+
? `<span class="nav-landing-card-badge" title="Go to demo on RedNote: xhslink.com" data-i18n="text,title">500K+ plays on RedNote</span>`
|
| 101 |
: '';
|
| 102 |
|
| 103 |
if (navKey === 'causalFlow') {
|
|
|
|
| 117 |
`<div class="nav-landing-card-carousel-viewport">` +
|
| 118 |
`<div class="nav-landing-card-slide" data-slide="flow">${slideLink('flow', '<video muted loop playsinline preload="metadata"></video>')}</div>` +
|
| 119 |
`<div class="nav-landing-card-slide" data-slide="spiral">${slideLink('spiral', '<video muted loop playsinline preload="none"></video>')}</div>` +
|
| 120 |
+
`<div class="nav-landing-card-slide" data-slide="cot">${slideLink('cot', '<video muted loop playsinline preload="none"></video>')}</div>` +
|
| 121 |
`</div>` +
|
| 122 |
`<button type="button" class="nav-landing-card-carousel-arrow nav-landing-card-carousel-arrow--prev" aria-label="Previous preview">‹</button>` +
|
| 123 |
`<button type="button" class="nav-landing-card-carousel-arrow nav-landing-card-carousel-arrow--next" aria-label="Next preview">›</button>` +
|
client/src/shared/api/completionsClient.ts
CHANGED
|
@@ -68,18 +68,26 @@ export type PostCompletionsPromptOptions = {
|
|
| 68 |
* POST /v1/completions/prompt:将用户原文套用 chat template,返回实际送入续写的完整 prompt。
|
| 69 |
*/
|
| 70 |
export async function postCompletionsPrompt(
|
| 71 |
-
body: { model: string; prompt: string; system?: string },
|
| 72 |
options: PostCompletionsPromptOptions = {}
|
| 73 |
): Promise<{ prompt_used: string }> {
|
| 74 |
const { signal } = options;
|
| 75 |
const url = URLHandler.basicURL() + COMPLETIONS_PROMPT_PATH;
|
| 76 |
-
const payload: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
model: body.model,
|
| 78 |
prompt: body.prompt
|
| 79 |
};
|
| 80 |
if (body.system !== undefined) {
|
| 81 |
payload.system = body.system;
|
| 82 |
}
|
|
|
|
|
|
|
|
|
|
| 83 |
const res = await fetch(url, {
|
| 84 |
method: 'POST',
|
| 85 |
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
|
|
|
|
| 68 |
* POST /v1/completions/prompt:将用户原文套用 chat template,返回实际送入续写的完整 prompt。
|
| 69 |
*/
|
| 70 |
export async function postCompletionsPrompt(
|
| 71 |
+
body: { model: string; prompt: string; system?: string; enable_thinking?: boolean },
|
| 72 |
options: PostCompletionsPromptOptions = {}
|
| 73 |
): Promise<{ prompt_used: string }> {
|
| 74 |
const { signal } = options;
|
| 75 |
const url = URLHandler.basicURL() + COMPLETIONS_PROMPT_PATH;
|
| 76 |
+
const payload: {
|
| 77 |
+
model: string;
|
| 78 |
+
prompt: string;
|
| 79 |
+
system?: string;
|
| 80 |
+
enable_thinking?: boolean;
|
| 81 |
+
} = {
|
| 82 |
model: body.model,
|
| 83 |
prompt: body.prompt
|
| 84 |
};
|
| 85 |
if (body.system !== undefined) {
|
| 86 |
payload.system = body.system;
|
| 87 |
}
|
| 88 |
+
if (body.enable_thinking === true) {
|
| 89 |
+
payload.enable_thinking = true;
|
| 90 |
+
}
|
| 91 |
const res = await fetch(url, {
|
| 92 |
method: 'POST',
|
| 93 |
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
|
client/src/shared/controllers/serverDemoController.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
| 12 |
buildFolderOptions
|
| 13 |
} from '../../features/demo/demoPathUtils';
|
| 14 |
import { tr, trf } from '../../shared/lang/i18n-lite';
|
|
|
|
| 15 |
|
| 16 |
/**
|
| 17 |
* 从保存结果中提取文件名,如果不存在则根据名称生成
|
|
@@ -40,7 +41,7 @@ export const showDemoNameInput = (
|
|
| 40 |
const folders = result.folders || [];
|
| 41 |
|
| 42 |
// 获取上次保存的路径(从 localStorage)
|
| 43 |
-
const lastSavePath =
|
| 44 |
|
| 45 |
// 使用统一的 buildFolderOptions 函数
|
| 46 |
const { options: selectOptions, defaultPath } = buildFolderOptions(folders, lastSavePath);
|
|
@@ -59,7 +60,7 @@ export const showDemoNameInput = (
|
|
| 59 |
if (value && value.input) {
|
| 60 |
// 保存选择的路径到 localStorage
|
| 61 |
const selectedPath = value.select || '/';
|
| 62 |
-
|
| 63 |
resolve({ name: value.input, path: selectedPath });
|
| 64 |
} else {
|
| 65 |
resolve(null);
|
|
@@ -161,7 +162,7 @@ export const handleServerDemoSave = async (options: ServerDemoSaveOptions): Prom
|
|
| 161 |
result = { name: presetSaveInfo.name.trim(), path: normalizedPath };
|
| 162 |
// 记录最近路径
|
| 163 |
if (normalizedPath) {
|
| 164 |
-
|
| 165 |
}
|
| 166 |
} else {
|
| 167 |
const defaultName = getDefaultDemoName(currentData, textFieldValue, currentFileName);
|
|
|
|
| 12 |
buildFolderOptions
|
| 13 |
} from '../../features/demo/demoPathUtils';
|
| 14 |
import { tr, trf } from '../../shared/lang/i18n-lite';
|
| 15 |
+
import { lsGet, lsSet } from '../../shared/storage/localStorageHelpers';
|
| 16 |
|
| 17 |
/**
|
| 18 |
* 从保存结果中提取文件名,如果不存在则根据名称生成
|
|
|
|
| 41 |
const folders = result.folders || [];
|
| 42 |
|
| 43 |
// 获取上次保存的路径(从 localStorage)
|
| 44 |
+
const lastSavePath = lsGet(LAST_SAVE_PATH_KEY);
|
| 45 |
|
| 46 |
// 使用统一的 buildFolderOptions 函数
|
| 47 |
const { options: selectOptions, defaultPath } = buildFolderOptions(folders, lastSavePath);
|
|
|
|
| 60 |
if (value && value.input) {
|
| 61 |
// 保存选择的路径到 localStorage
|
| 62 |
const selectedPath = value.select || '/';
|
| 63 |
+
lsSet(LAST_SAVE_PATH_KEY, selectedPath);
|
| 64 |
resolve({ name: value.input, path: selectedPath });
|
| 65 |
} else {
|
| 66 |
resolve(null);
|
|
|
|
| 162 |
result = { name: presetSaveInfo.name.trim(), path: normalizedPath };
|
| 163 |
// 记录最近路径
|
| 164 |
if (normalizedPath) {
|
| 165 |
+
lsSet(LAST_SAVE_PATH_KEY, normalizedPath);
|
| 166 |
}
|
| 167 |
} else {
|
| 168 |
const defaultName = getDefaultDemoName(currentData, textFieldValue, currentFileName);
|
client/src/shared/core/responsive.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
| 3 |
* 使用 CSS 变量和 matchMedia API 实现单一数据源
|
| 4 |
*/
|
| 5 |
|
|
|
|
|
|
|
| 6 |
/**
|
| 7 |
* 从 CSS 变量获取断点值
|
| 8 |
*/
|
|
@@ -46,7 +48,7 @@ export const FORCE_NARROW_STORAGE_KEY = 'info_radar_force_narrow';
|
|
| 46 |
export const FORCE_NARROW_CHANGE_EVENT = 'force-narrow-change';
|
| 47 |
|
| 48 |
export const getForceNarrowScreen = (): boolean =>
|
| 49 |
-
|
| 50 |
|
| 51 |
export const syncForceNarrowAttribute = (): void => {
|
| 52 |
const root = document.documentElement;
|
|
@@ -70,8 +72,8 @@ export const initForceNarrowFromStorage = (): void => {
|
|
| 70 |
};
|
| 71 |
|
| 72 |
export const setForceNarrowScreen = (enabled: boolean): void => {
|
| 73 |
-
if (enabled)
|
| 74 |
-
else
|
| 75 |
syncForceNarrowAttribute();
|
| 76 |
window.dispatchEvent(new Event(FORCE_NARROW_CHANGE_EVENT));
|
| 77 |
window.dispatchEvent(new Event('resize'));
|
|
|
|
| 3 |
* 使用 CSS 变量和 matchMedia API 实现单一数据源
|
| 4 |
*/
|
| 5 |
|
| 6 |
+
import { lsGet, lsRemove, lsSet } from '../storage/localStorageHelpers';
|
| 7 |
+
|
| 8 |
/**
|
| 9 |
* 从 CSS 变量获取断点值
|
| 10 |
*/
|
|
|
|
| 48 |
export const FORCE_NARROW_CHANGE_EVENT = 'force-narrow-change';
|
| 49 |
|
| 50 |
export const getForceNarrowScreen = (): boolean =>
|
| 51 |
+
lsGet(FORCE_NARROW_STORAGE_KEY) === '1';
|
| 52 |
|
| 53 |
export const syncForceNarrowAttribute = (): void => {
|
| 54 |
const root = document.documentElement;
|
|
|
|
| 72 |
};
|
| 73 |
|
| 74 |
export const setForceNarrowScreen = (enabled: boolean): void => {
|
| 75 |
+
if (enabled) lsSet(FORCE_NARROW_STORAGE_KEY, '1');
|
| 76 |
+
else lsRemove(FORCE_NARROW_STORAGE_KEY);
|
| 77 |
syncForceNarrowAttribute();
|
| 78 |
window.dispatchEvent(new Event(FORCE_NARROW_CHANGE_EVENT));
|
| 79 |
window.dispatchEvent(new Event('resize'));
|
client/src/shared/cross/adminManager.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
| 2 |
* 管理员状态管理模块
|
| 3 |
*/
|
| 4 |
|
|
|
|
|
|
|
| 5 |
const INFORADAR_ADMIN_TOKEN_KEY = 'admin_token';
|
| 6 |
const ADMIN_MODE_KEY = 'is_admin_mode';
|
| 7 |
|
|
@@ -12,8 +14,8 @@ export class AdminManager {
|
|
| 12 |
|
| 13 |
private constructor() {
|
| 14 |
// 从localStorage恢复状态
|
| 15 |
-
const savedToken =
|
| 16 |
-
const savedMode =
|
| 17 |
|
| 18 |
if (savedToken) {
|
| 19 |
this.adminToken = savedToken;
|
|
@@ -66,8 +68,8 @@ export class AdminManager {
|
|
| 66 |
if (result.success) {
|
| 67 |
this.adminToken = token;
|
| 68 |
this.isAdminMode = true;
|
| 69 |
-
|
| 70 |
-
|
| 71 |
return {
|
| 72 |
success: true,
|
| 73 |
message: result.message
|
|
@@ -98,8 +100,8 @@ export class AdminManager {
|
|
| 98 |
public clearAdminToken(): void {
|
| 99 |
this.adminToken = null;
|
| 100 |
this.isAdminMode = false;
|
| 101 |
-
|
| 102 |
-
|
| 103 |
}
|
| 104 |
|
| 105 |
/**
|
|
|
|
| 2 |
* 管理员状态管理模块
|
| 3 |
*/
|
| 4 |
|
| 5 |
+
import { lsGet, lsRemove, lsSet } from '../storage/localStorageHelpers';
|
| 6 |
+
|
| 7 |
const INFORADAR_ADMIN_TOKEN_KEY = 'admin_token';
|
| 8 |
const ADMIN_MODE_KEY = 'is_admin_mode';
|
| 9 |
|
|
|
|
| 14 |
|
| 15 |
private constructor() {
|
| 16 |
// 从localStorage恢复状态
|
| 17 |
+
const savedToken = lsGet(INFORADAR_ADMIN_TOKEN_KEY);
|
| 18 |
+
const savedMode = lsGet(ADMIN_MODE_KEY);
|
| 19 |
|
| 20 |
if (savedToken) {
|
| 21 |
this.adminToken = savedToken;
|
|
|
|
| 68 |
if (result.success) {
|
| 69 |
this.adminToken = token;
|
| 70 |
this.isAdminMode = true;
|
| 71 |
+
lsSet(INFORADAR_ADMIN_TOKEN_KEY, token);
|
| 72 |
+
lsSet(ADMIN_MODE_KEY, 'true');
|
| 73 |
return {
|
| 74 |
success: true,
|
| 75 |
message: result.message
|
|
|
|
| 100 |
public clearAdminToken(): void {
|
| 101 |
this.adminToken = null;
|
| 102 |
this.isAdminMode = false;
|
| 103 |
+
lsRemove(INFORADAR_ADMIN_TOKEN_KEY);
|
| 104 |
+
lsRemove(ADMIN_MODE_KEY);
|
| 105 |
}
|
| 106 |
|
| 107 |
/**
|
client/src/shared/cross/digitsMergeManager.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
/** 数字段合并(digit merge)开关,与 BPE overlap 合并独立;默认开启以保持既有行为 */
|
|
|
|
|
|
|
| 2 |
export const DIGITS_MERGE_STORAGE_KEY = 'info_radar_digits_merge_enabled';
|
| 3 |
|
| 4 |
const renderListeners = new Set<() => void>();
|
|
@@ -39,12 +41,12 @@ export function addDigitsMergeRenderListener(callback: () => void): void {
|
|
| 39 |
}
|
| 40 |
|
| 41 |
export function getDigitsMergeEnabled(): boolean {
|
| 42 |
-
const v =
|
| 43 |
if (v === null) return true;
|
| 44 |
return v === 'true';
|
| 45 |
}
|
| 46 |
|
| 47 |
export function setDigitsMergeEnabled(enabled: boolean): void {
|
| 48 |
-
|
| 49 |
notifyDigitsMergeRenderListeners();
|
| 50 |
}
|
|
|
|
| 1 |
/** 数字段合并(digit merge)开关,与 BPE overlap 合并独立;默认开启以保持既有行为 */
|
| 2 |
+
import { lsGet, lsWriteBool } from '../storage/localStorageHelpers';
|
| 3 |
+
|
| 4 |
export const DIGITS_MERGE_STORAGE_KEY = 'info_radar_digits_merge_enabled';
|
| 5 |
|
| 6 |
const renderListeners = new Set<() => void>();
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
export function getDigitsMergeEnabled(): boolean {
|
| 44 |
+
const v = lsGet(DIGITS_MERGE_STORAGE_KEY);
|
| 45 |
if (v === null) return true;
|
| 46 |
return v === 'true';
|
| 47 |
}
|
| 48 |
|
| 49 |
export function setDigitsMergeEnabled(enabled: boolean): void {
|
| 50 |
+
lsWriteBool(DIGITS_MERGE_STORAGE_KEY, enabled);
|
| 51 |
notifyDigitsMergeRenderListeners();
|
| 52 |
}
|
client/src/shared/cross/panelSplitStorage.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
/** 左栏宽度占「主区宽度 − resizer(8px)」的比例,与 LayoutController / chatPanelLayout 内逻辑一致 */
|
| 2 |
|
|
|
|
|
|
|
| 3 |
const DEFAULT_RATIO = 0.5;
|
| 4 |
const MIN_RATIO = 0.1;
|
| 5 |
const MAX_RATIO = 0.9;
|
|
@@ -10,26 +12,18 @@ export const PANEL_SPLIT_STORAGE_KEY_ATTRIBUTION = 'info_radar_panel_split_attri
|
|
| 10 |
export const PANEL_SPLIT_STORAGE_KEY_GEN_ATTRIBUTE = 'info_radar_panel_split_gen_attribute';
|
| 11 |
|
| 12 |
export function readPanelSplitRatio(storageKey: string): number {
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
if (!Number.isFinite(n)) {
|
| 20 |
-
return DEFAULT_RATIO;
|
| 21 |
-
}
|
| 22 |
-
return Math.max(MIN_RATIO, Math.min(MAX_RATIO, n));
|
| 23 |
-
} catch {
|
| 24 |
return DEFAULT_RATIO;
|
| 25 |
}
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
export function writePanelSplitRatio(storageKey: string, ratio: number): void {
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
localStorage.setItem(storageKey, String(clamped));
|
| 32 |
-
} catch {
|
| 33 |
-
// ignore quota / private mode
|
| 34 |
-
}
|
| 35 |
}
|
|
|
|
| 1 |
/** 左栏宽度占「主区宽度 − resizer(8px)」的比例,与 LayoutController / chatPanelLayout 内逻辑一致 */
|
| 2 |
|
| 3 |
+
import { lsGet, lsSet } from '../storage/localStorageHelpers';
|
| 4 |
+
|
| 5 |
const DEFAULT_RATIO = 0.5;
|
| 6 |
const MIN_RATIO = 0.1;
|
| 7 |
const MAX_RATIO = 0.9;
|
|
|
|
| 12 |
export const PANEL_SPLIT_STORAGE_KEY_GEN_ATTRIBUTE = 'info_radar_panel_split_gen_attribute';
|
| 13 |
|
| 14 |
export function readPanelSplitRatio(storageKey: string): number {
|
| 15 |
+
const raw = lsGet(storageKey);
|
| 16 |
+
if (raw === null) {
|
| 17 |
+
return DEFAULT_RATIO;
|
| 18 |
+
}
|
| 19 |
+
const n = Number(raw);
|
| 20 |
+
if (!Number.isFinite(n)) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
return DEFAULT_RATIO;
|
| 22 |
}
|
| 23 |
+
return Math.max(MIN_RATIO, Math.min(MAX_RATIO, n));
|
| 24 |
}
|
| 25 |
|
| 26 |
export function writePanelSplitRatio(storageKey: string, ratio: number): void {
|
| 27 |
+
const clamped = Math.max(MIN_RATIO, Math.min(MAX_RATIO, ratio));
|
| 28 |
+
lsSet(storageKey, String(clamped));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
client/src/shared/cross/queryHistory.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
*/
|
| 4 |
|
| 5 |
import { tr } from '../../shared/lang/i18n-lite';
|
|
|
|
| 6 |
|
| 7 |
/** 首页语义搜索等默认使用 */
|
| 8 |
export const SEMANTIC_QUERY_HISTORY_KEY = 'info_radar_query_search_history';
|
|
@@ -49,7 +50,7 @@ function shouldTouchLinkedMru(applyHistoryOnHover: boolean, fromHover: boolean):
|
|
| 49 |
|
| 50 |
function load(storageKey: string): string[] {
|
| 51 |
try {
|
| 52 |
-
const raw =
|
| 53 |
if (!raw) return [];
|
| 54 |
const parsed = JSON.parse(raw);
|
| 55 |
if (!Array.isArray(parsed)) return [];
|
|
@@ -68,12 +69,12 @@ function load(storageKey: string): string[] {
|
|
| 68 |
|
| 69 |
function remove(storageKey: string, query: string): void {
|
| 70 |
const list = load(storageKey).filter((s) => s !== query);
|
| 71 |
-
|
| 72 |
}
|
| 73 |
|
| 74 |
export function saveHistory(query: string, storageKey: string = SEMANTIC_QUERY_HISTORY_KEY): void {
|
| 75 |
const list = [query, ...load(storageKey).filter((s) => s !== query)].slice(0, MAX);
|
| 76 |
-
|
| 77 |
}
|
| 78 |
|
| 79 |
export interface InitQueryHistoryDropdownOptions {
|
|
|
|
| 3 |
*/
|
| 4 |
|
| 5 |
import { tr } from '../../shared/lang/i18n-lite';
|
| 6 |
+
import { lsGet, lsSet } from '../storage/localStorageHelpers';
|
| 7 |
|
| 8 |
/** 首页语义搜索等默认使用 */
|
| 9 |
export const SEMANTIC_QUERY_HISTORY_KEY = 'info_radar_query_search_history';
|
|
|
|
| 50 |
|
| 51 |
function load(storageKey: string): string[] {
|
| 52 |
try {
|
| 53 |
+
const raw = lsGet(storageKey);
|
| 54 |
if (!raw) return [];
|
| 55 |
const parsed = JSON.parse(raw);
|
| 56 |
if (!Array.isArray(parsed)) return [];
|
|
|
|
| 69 |
|
| 70 |
function remove(storageKey: string, query: string): void {
|
| 71 |
const list = load(storageKey).filter((s) => s !== query);
|
| 72 |
+
lsSet(storageKey, JSON.stringify(list));
|
| 73 |
}
|
| 74 |
|
| 75 |
export function saveHistory(query: string, storageKey: string = SEMANTIC_QUERY_HISTORY_KEY): void {
|
| 76 |
const list = [query, ...load(storageKey).filter((s) => s !== query)].slice(0, MAX);
|
| 77 |
+
lsSet(storageKey, JSON.stringify(list));
|
| 78 |
}
|
| 79 |
|
| 80 |
export interface InitQueryHistoryDropdownOptions {
|
client/src/shared/cross/semanticResultCache.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
| 3 |
* 持久化到 localStorage,刷新后保留。删除查询历史时需调用 removeByQuery 清理对应缓存。
|
| 4 |
*/
|
| 5 |
|
|
|
|
|
|
|
| 6 |
const MAX_SIZE = 50;
|
| 7 |
const STORAGE_KEY = 'info_radar_semantic_result_cache';
|
| 8 |
|
|
@@ -35,7 +37,7 @@ let keyOrder: string[] = [];
|
|
| 35 |
|
| 36 |
function load(): void {
|
| 37 |
try {
|
| 38 |
-
const raw =
|
| 39 |
if (!raw) return;
|
| 40 |
const parsed = JSON.parse(raw) as { entries?: Record<string, StoredEntry>; keyOrder?: string[] };
|
| 41 |
if (!parsed?.entries || typeof parsed.entries !== 'object') return;
|
|
@@ -55,17 +57,15 @@ function load(): void {
|
|
| 55 |
load();
|
| 56 |
|
| 57 |
function persist(): void {
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
console.warn('[semanticResultCache] 持久化失败,刷新后缓存可能丢失。原因:', reason);
|
| 68 |
-
}
|
| 69 |
}
|
| 70 |
|
| 71 |
function evictOne(): void {
|
|
|
|
| 3 |
* 持久化到 localStorage,刷新后保留。删除查询历史时需调用 removeByQuery 清理对应缓存。
|
| 4 |
*/
|
| 5 |
|
| 6 |
+
import { lsGet, lsSetCatch } from '../storage/localStorageHelpers';
|
| 7 |
+
|
| 8 |
const MAX_SIZE = 50;
|
| 9 |
const STORAGE_KEY = 'info_radar_semantic_result_cache';
|
| 10 |
|
|
|
|
| 37 |
|
| 38 |
function load(): void {
|
| 39 |
try {
|
| 40 |
+
const raw = lsGet(STORAGE_KEY);
|
| 41 |
if (!raw) return;
|
| 42 |
const parsed = JSON.parse(raw) as { entries?: Record<string, StoredEntry>; keyOrder?: string[] };
|
| 43 |
if (!parsed?.entries || typeof parsed.entries !== 'object') return;
|
|
|
|
| 57 |
load();
|
| 58 |
|
| 59 |
function persist(): void {
|
| 60 |
+
const entries: Record<string, StoredEntry> = {};
|
| 61 |
+
for (const [k, v] of cache) entries[k] = v;
|
| 62 |
+
const err = lsSetCatch(STORAGE_KEY, JSON.stringify({ entries, keyOrder }));
|
| 63 |
+
if (err === undefined) return;
|
| 64 |
+
const reason =
|
| 65 |
+
err instanceof DOMException && err.name === 'QuotaExceededError'
|
| 66 |
+
? 'localStorage 配额已满(Chrome 约 5MB/域名),建议减少 MAX_SIZE 或清理其他站点数据'
|
| 67 |
+
: String(err);
|
| 68 |
+
console.warn('[semanticResultCache] 持久化失败,刷新后缓存可能丢失。原因:', reason);
|
|
|
|
|
|
|
| 69 |
}
|
| 70 |
|
| 71 |
function evictOne(): void {
|
client/src/shared/cross/semanticThresholdManager.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
/** 语义匹配度阈值:用户可配置,持久化到 localStorage,与 Semantic analysis 同方式 */
|
| 2 |
import { SEMANTIC_MATCH_THRESHOLD } from '../core/constants';
|
|
|
|
| 3 |
|
| 4 |
const KEY = 'info_radar_semantic_match_threshold';
|
| 5 |
|
| 6 |
export function getSemanticMatchThreshold(): number {
|
| 7 |
-
const v =
|
| 8 |
if (v == null) return SEMANTIC_MATCH_THRESHOLD;
|
| 9 |
const n = parseFloat(v);
|
| 10 |
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : SEMANTIC_MATCH_THRESHOLD;
|
|
@@ -12,5 +13,5 @@ export function getSemanticMatchThreshold(): number {
|
|
| 12 |
|
| 13 |
export function setSemanticMatchThreshold(value: number): void {
|
| 14 |
const clamped = Math.max(0, Math.min(1, value));
|
| 15 |
-
|
| 16 |
}
|
|
|
|
| 1 |
/** 语义匹配度阈值:用户可配置,持久化到 localStorage,与 Semantic analysis 同方式 */
|
| 2 |
import { SEMANTIC_MATCH_THRESHOLD } from '../core/constants';
|
| 3 |
+
import { lsGet, lsSet } from '../storage/localStorageHelpers';
|
| 4 |
|
| 5 |
const KEY = 'info_radar_semantic_match_threshold';
|
| 6 |
|
| 7 |
export function getSemanticMatchThreshold(): number {
|
| 8 |
+
const v = lsGet(KEY);
|
| 9 |
if (v == null) return SEMANTIC_MATCH_THRESHOLD;
|
| 10 |
const n = parseFloat(v);
|
| 11 |
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : SEMANTIC_MATCH_THRESHOLD;
|
|
|
|
| 13 |
|
| 14 |
export function setSemanticMatchThreshold(value: number): void {
|
| 15 |
const clamped = Math.max(0, Math.min(1, value));
|
| 16 |
+
lsSet(KEY, String(clamped));
|
| 17 |
}
|
client/src/shared/cross/settingsMenuManager.ts
CHANGED
|
@@ -14,7 +14,10 @@ import { getSemanticAnalysisEnabled, setSemanticAnalysisEnabled } from './semant
|
|
| 14 |
import { getDigitsMergeEnabled, setDigitsMergeEnabled } from './digitsMergeManager';
|
| 15 |
import { getForceNarrowScreen, setForceNarrowScreen, FORCE_NARROW_CHANGE_EVENT } from '../core/responsive';
|
| 16 |
import { getSemanticMatchThreshold } from './semanticThresholdManager';
|
| 17 |
-
import {
|
|
|
|
|
|
|
|
|
|
| 18 |
import { showVisitStatsDialog } from './visitStatsDialog';
|
| 19 |
import { showModelManageDialog } from './modelManageDialog';
|
| 20 |
|
|
|
|
| 14 |
import { getDigitsMergeEnabled, setDigitsMergeEnabled } from './digitsMergeManager';
|
| 15 |
import { getForceNarrowScreen, setForceNarrowScreen, FORCE_NARROW_CHANGE_EVENT } from '../core/responsive';
|
| 16 |
import { getSemanticMatchThreshold } from './semanticThresholdManager';
|
| 17 |
+
import {
|
| 18 |
+
getInfoDensityRenderDisabled,
|
| 19 |
+
setInfoDensityRenderDisabled,
|
| 20 |
+
} from '../../features/analysis/infoDensityRenderManager';
|
| 21 |
import { showVisitStatsDialog } from './visitStatsDialog';
|
| 22 |
import { showModelManageDialog } from './modelManageDialog';
|
| 23 |
|