返回 DeepSeek-Reasonix
EffortSwitcher.tsx
根目录 / desktop / frontend / src / components / EffortSwitcher.tsx
1 import { useCallback, useEffect, useRef, useState } from "react";
2 import { Check, ChevronsUpDown, Gauge } from "lucide-react";
3 import { asArray } from "../lib/array";
4 import type { EffortInfo } from "../lib/types";
5 import { ANCHORED_POPOVER_CLOSE_MS, AnchoredPopover } from "./AnchoredPopover";
6
7 export function EffortSwitcher({
8 effort,
9 disabled,
10 onPick,
11 }: {
12 effort?: EffortInfo;
13 disabled: boolean;
14 onPick: (level: string) => void;
15 }) {
16 const [open, setOpen] = useState(false);
17 const [closing, setClosing] = useState(false);
18 const triggerRef = useRef<HTMLButtonElement>(null);
19 const closeTimerRef = useRef<number | null>(null);
20 const levels = asArray(effort?.levels);
21 const current = effort?.current || "auto";
22
23 const clearCloseTimer = useCallback(() => {
24 if (closeTimerRef.current === null) return;
25 window.clearTimeout(closeTimerRef.current);
26 closeTimerRef.current = null;
27 }, []);
28
29 const openMenu = useCallback(() => {
30 clearCloseTimer();
31 setClosing(false);
32 setOpen(true);
33 }, [clearCloseTimer]);
34
35 const closeMenu = useCallback((afterClose?: () => void) => {
36 clearCloseTimer();
37 setClosing(true);
38 window.requestAnimationFrame(() => setOpen(false));
39 const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
40 closeTimerRef.current = window.setTimeout(() => {
41 closeTimerRef.current = null;
42 setClosing(false);
43 afterClose?.();
44 }, reduceMotion ? 0 : ANCHORED_POPOVER_CLOSE_MS);
45 }, [clearCloseTimer]);
46
47 useEffect(() => () => clearCloseTimer(), [clearCloseTimer]);
48
49 const pick = (level: string) => {
50 closeMenu(() => {
51 if (level !== current) onPick(level);
52 });
53 };
54
55 if (!effort?.supported || levels.length === 0) return null;
56
57 return (
58 <div className="modelsw effortsw">
59 <button
60 ref={triggerRef}
61 type="button"
62 className={`modelsw__trigger effortsw__trigger ${current !== "auto" ? "effortsw__trigger--explicit" : ""}`}
63 disabled={disabled}
64 aria-expanded={open && !closing}
65 onClick={() => (open || closing ? closeMenu() : openMenu())}
66 >
67 <Gauge size={14} className="modelsw__kind" />
68 <span className="modelsw__label">{current}</span>
69 <ChevronsUpDown size={11} />
70 </button>
71 <AnchoredPopover
72 open={open && !disabled}
73 closing={closing}
74 anchorRef={triggerRef}
75 onClose={() => closeMenu()}
76 className="modelsw__menu modelsw__menu--portal effortsw__menu"
77 align="end"
78 >
79 <div role="listbox">
80 {levels.map((level) => (
81 <button
82 key={level}
83 type="button"
84 role="option"
85 aria-selected={level === current}
86 className={`modelsw__item ${level === current ? "modelsw__item--current" : ""}`}
87 onClick={() => pick(level)}
88 >
89 <span className="modelsw__model">{level}</span>
90 {level === current && <Check size={13} className="modelsw__check" />}
91 </button>
92 ))}
93 </div>
94 </AnchoredPopover>
95 </div>
96 );
97 }
98
98 lines Plain Text