agent-Specialization/demo/reasoning_effort_slider.html
JOJO c9c3eedd82 feat: 移除实验性间隔思考模式,推理强度前置到前端
- 彻底移除「每 N 次请求强制思考」的间隔调度机制(思考/非思考交替导致缓存命中率极低)
- 运行模式收敛为 fast/thinking:原 deep 改名 thinking,历史数据读取时映射
- 新增推理强度五档滑块(low/medium/high/xhigh/max + 默认不传参),置于模型/模式弹窗
- 模型配置新增 reasoning_effort 开关;档位会话级持久化到对话 meta,随对话保存/恢复
- 个人空间新增「默认推理强度」;新用户默认运行模式改为 thinking
- 滑块交互:JS 插值驱动(点击/拖动/吸附全程平滑),首帧档位语义定位无闪烁
- 保存链路:前端防抖 600ms + conversation_id 兜底,修复 /new 发消息丢档位、切对话恢复错误
2026-07-21 13:47:53 +08:00

474 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>推理强度选择器 Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--page-bg: #1a1a1c;
--card-bg: #2c2c2e;
--track-bg: #3a3a3c;
--text-primary: #f2f2f4;
--text-secondary: #9c9ca0;
--knob-bg: #ffffff;
--dot-on: rgba(255, 255, 255, 0.55);
--dot-off: rgba(0, 0, 0, 0.28);
}
html, body {
height: 100%;
background: var(--page-bg);
font-family: -apple-system, "PingFang SC", "Helvetica Neue", sans-serif;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 24px;
}
.demo-title {
color: var(--text-secondary);
font-size: 13px;
letter-spacing: 0.5px;
}
/* ========== 卡片(模拟弹窗内的一块区域) ========== */
.effort-card {
width: 460px;
background: var(--card-bg);
border-radius: 18px;
padding: 22px 26px 20px;
user-select: none;
-webkit-user-select: none;
}
/* ========== 顶部:两种状态文字叠在一起,淡入淡出切换 ========== */
.effort-header {
position: relative;
height: 26px;
margin-bottom: 18px;
}
.header-state {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: space-between;
transition: opacity 0.18s ease, transform 0.18s ease;
}
.header-idle { opacity: 1; }
.header-drag {
opacity: 0;
transform: translateY(4px);
pointer-events: none;
color: var(--text-secondary);
font-size: 16px;
}
.effort-card.live-drag .header-idle {
opacity: 0;
transform: translateY(-4px);
pointer-events: none;
}
.effort-card.live-drag .header-drag {
opacity: 1;
transform: translateY(0);
}
.header-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
/* ========== 「默认」打勾框:个人空间 fancy-check 缩小版 ========== */
.default-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.default-label {
font-size: 13px;
color: var(--text-secondary);
}
.default-toggle .fancy-check {
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.default-toggle .fancy-check svg {
width: 16px;
height: 16px;
overflow: visible;
}
.fancy-path {
fill: none;
stroke: var(--text-secondary);
stroke-width: 5;
stroke-linecap: round;
stroke-linejoin: round;
transition:
stroke-dasharray 0.5s ease,
stroke-dashoffset 0.5s ease,
stroke 0.2s ease;
stroke-dasharray: 241 9999999;
stroke-dashoffset: 0;
}
.default-toggle.checked .fancy-path {
stroke-dasharray: 70.5096664428711 9999999;
stroke-dashoffset: -262.2723388671875;
}
/* ========== 滑块区域 ========== */
.slider-zone {
position: relative;
transition: filter 0.25s ease, opacity 0.25s ease;
}
/* 默认勾选时:整体置灰,且停掉所有动画 */
.effort-card.is-default .slider-zone {
filter: grayscale(1) brightness(0.72);
opacity: 0.55;
}
.effort-card.is-default .fill-layer {
animation: none;
}
.effort-card.is-default .fill-layer::after {
animation: none;
}
.track {
position: relative;
height: 34px;
border-radius: 17px;
background: var(--track-bg);
cursor: pointer;
touch-action: none;
}
/* 填充5 层叠放,当前档淡入、其余淡出 —— 任何颜色切换都是平滑 crossfade */
.fill-stack {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 23px;
border-radius: 17px;
overflow: hidden;
}
.fill-layer {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
opacity: 0;
transition: opacity 0.35s ease;
}
.fill-layer.on { opacity: 1; }
.fill-layer.lv-0 { background: #f5c542; }
.fill-layer.lv-1 { background: #3fd07c; }
.fill-layer.lv-2 { background: #4f8ef7; }
.fill-layer.lv-3 { background: #a55df0; }
/* xhigh白光扫过90deg 整列渐变无硬边;位移动画两端完全出画后回绕 → 无缝) */
.fill-layer.lv-3::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -50%;
width: 45%;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.55) 50%,
transparent 100%
);
animation: shine-sweep 1.8s linear infinite;
}
@keyframes shine-sweep {
from { left: -50%; }
to { left: 130%; }
}
/* max彩虹流动200% 宽 + 位移整数个循环单元 → 真无缝) */
.fill-layer.lv-4 {
width: 200%;
background: repeating-linear-gradient(
90deg,
#ff3b5c 0%,
#ff9f2e 7.15%,
#ffe93b 14.3%,
#3ddc68 21.45%,
#2fd4ff 28.6%,
#7a5cff 35.75%,
#ff4fd8 42.9%,
#ff3b5c 50%
);
animation: rainbow-flow 2.4s linear infinite;
}
/* 位移方向:色块随条从左向右流动;位移为整数个循环单元 → 真无缝 */
@keyframes rainbow-flow {
from { transform: translateX(-50%); }
to { transform: translateX(0); }
}
/* 圆钮与填充transition 常驻 —— 点击也平滑滑动;仅真实拖动时关闭跟随 */
.knob, .fill-stack {
transition: left 0.2s ease, width 0.2s ease;
}
.effort-card.live-drag .knob,
.effort-card.live-drag .fill-stack {
transition: none;
}
/* 档位圆点 */
.dot {
position: absolute;
top: 50%;
width: 7px;
height: 7px;
border-radius: 50%;
transform: translate(-50%, -50%);
background: var(--dot-off);
transition: background 0.2s ease;
pointer-events: none;
}
.dot.covered { background: var(--dot-on); }
/* 拖柄 */
.knob {
position: absolute;
top: 50%;
width: 46px;
height: 46px;
border-radius: 50%;
background: var(--knob-bg);
transform: translate(-50%, -50%);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
cursor: grab;
z-index: 2;
}
.effort-card.live-drag .knob { cursor: grabbing; }
/* ========== 档位文字 ========== */
.level-labels {
position: relative;
height: 20px;
margin-top: 12px;
}
.level-label {
position: absolute;
transform: translateX(-50%);
font-size: 12px;
color: var(--text-secondary);
transition: color 0.2s ease, font-weight 0.2s ease;
white-space: nowrap;
}
.level-label.active {
color: var(--text-primary);
font-weight: 600;
}
/* demo 辅助:当前状态输出 */
.demo-state {
color: var(--text-secondary);
font-size: 12px;
font-family: ui-monospace, monospace;
}
</style>
</head>
<body>
<div class="demo-title">推理强度选择器 Demo · 拖动滑块 / 点击档位 / 勾选「默认」试试</div>
<div class="effort-card" id="card">
<!-- 顶部:非拖动 = 推理强度 + 默认;拖动中 = 更高效 / 更智能 -->
<div class="effort-header">
<div class="header-state header-idle">
<span class="header-title">推理强度</span>
<span class="default-toggle checked" id="defaultToggle">
<span class="default-label">默认</span>
<span class="fancy-check" aria-hidden="true">
<svg viewBox="0 0 64 64">
<path
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
pathLength="575.0541381835938"
class="fancy-path"
></path>
</svg>
</span>
</span>
</div>
<div class="header-state header-drag">
<span>更高效</span>
<span>更智能</span>
</div>
</div>
<!-- 滑块 -->
<div class="slider-zone">
<div class="track" id="track">
<div class="fill-stack" id="fillStack">
<div class="fill-layer lv-0"></div>
<div class="fill-layer lv-1"></div>
<div class="fill-layer lv-2"></div>
<div class="fill-layer lv-3"></div>
<div class="fill-layer lv-4"></div>
</div>
<!-- dots 由 JS 生成 -->
<div class="knob" id="knob"></div>
</div>
<div class="level-labels" id="labels"></div>
</div>
</div>
<div class="demo-state" id="stateOut"></div>
<script>
(function () {
const LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
// 轨道内两端档位的圆钮中心 inset比圆钮半径(23)小 3px
// 让圆钮在两端时外凸 3px完整盖住填充条端头避免相切处露出杂边
const EDGE = 20;
const card = document.getElementById('card');
const track = document.getElementById('track');
const fillStack = document.getElementById('fillStack');
const layers = Array.from(fillStack.querySelectorAll('.fill-layer'));
const knob = document.getElementById('knob');
const labelsBox = document.getElementById('labels');
const defaultToggle = document.getElementById('defaultToggle');
const stateOut = document.getElementById('stateOut');
let levelIndex = 2; // 当前档位(记忆值,勾选默认时也保留)
let isDefault = true; // 是否勾选「默认」
let dragging = false; // 按下
let liveDrag = false; // 按下且已移动(真实拖动中)
let downX = 0;
const dots = [];
const labelEls = [];
// 生成 5 个档位点 + 5 个文字标签
LEVELS.forEach((name) => {
const dot = document.createElement('div');
dot.className = 'dot';
track.insertBefore(dot, knob);
dots.push(dot);
const label = document.createElement('span');
label.className = 'level-label';
label.textContent = name;
label.style.left = posPct(dots.length - 1);
labelsBox.appendChild(label);
labelEls.push(label);
});
// 档位 i 在轨道内的位置(百分比字符串,供标签/圆点用)
function posPct(i) {
const ratio = i / (LEVELS.length - 1);
return `calc(${EDGE}px + (100% - ${EDGE * 2}px) * ${ratio})`;
}
// 档位 i 在轨道内的像素 x
function posPx(i) {
const w = track.clientWidth;
return EDGE + (w - EDGE * 2) * (i / (LEVELS.length - 1));
}
// 像素 x → 最近档位
function pxToLevel(x) {
const w = track.clientWidth;
const ratio = (x - EDGE) / (w - EDGE * 2);
return Math.max(0, Math.min(LEVELS.length - 1, Math.round(ratio * (LEVELS.length - 1))));
}
function render(previewX) {
// 拖动时用连续像素位置否则吸附到档位点CSS transition 负责平滑)
const x = typeof previewX === 'number' ? previewX : posPx(levelIndex);
const activeLv = typeof previewX === 'number' ? pxToLevel(previewX) : levelIndex;
fillStack.style.width = x + 'px';
knob.style.left = x + 'px';
layers.forEach((layer, i) => layer.classList.toggle('on', i === activeLv));
dots.forEach((dot, i) => {
dot.style.left = posPct(i);
dot.classList.toggle('covered', posPx(i) <= x);
});
labelEls.forEach((el, i) => el.classList.toggle('active', i === activeLv));
card.classList.toggle('is-default', isDefault);
card.classList.toggle('live-drag', liveDrag);
defaultToggle.classList.toggle('checked', isDefault);
stateOut.textContent =
`level = ${isDefault ? '(默认,不传参)' : LEVELS[levelIndex]} | dragging = ${liveDrag}`;
}
// ---- 交互 ----
function onDown(e) {
dragging = true;
liveDrag = false;
downX = e.clientX;
track.setPointerCapture(e.pointerId);
// 点击含置灰状态解除默认并平滑滑到目标档transition 生效)
if (isDefault) isDefault = false;
levelIndex = pxToLevel(clampX(e));
render();
}
function onMove(e) {
if (!dragging) return;
if (!liveDrag && Math.abs(e.clientX - downX) > 3) liveDrag = true;
if (liveDrag) render(clampX(e)); // 真实拖动:无 transition 连续跟随
}
function onUp(e) {
if (!dragging) return;
dragging = false;
if (liveDrag) {
// 拖动结束吸附最近档。先用松手点的连续位置渲染knob 不动),
// 同时移除 live-drag 恢复 transition下一帧再吸附到档位点
// 避免 transition 恢复与位置变化同帧导致瞬间跳变
const x = clampX(e);
levelIndex = pxToLevel(x);
liveDrag = false;
render(x);
requestAnimationFrame(() => render());
return;
}
render();
}
function clampX(e) {
const rect = track.getBoundingClientRect();
const x = e.clientX - rect.left;
return Math.max(EDGE, Math.min(rect.width - EDGE, x));
}
track.addEventListener('pointerdown', onDown);
track.addEventListener('pointermove', onMove);
track.addEventListener('pointerup', onUp);
track.addEventListener('pointercancel', onUp);
// 勾选/取消「默认」
defaultToggle.addEventListener('click', () => {
isDefault = !isDefault;
render();
});
window.addEventListener('resize', () => render());
render();
})();
</script>
</body>
</html>