Merge branch 'main' of 8.163.11.238:audio/x8-controller-v2
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
VITE_AI_API_URL=https://v2ai.luxsin.com.cn
|
# VITE_AI_API_URL=https://v2ai.luxsin.com.cn
|
||||||
|
VITE_AI_API_URL=http://localhost:8000
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ build/
|
|||||||
*.dist
|
*.dist
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
|
|||||||
+28
-3
@@ -80,11 +80,36 @@ export interface SSEEvent {
|
|||||||
content?: string | ToolUseBlock | null;
|
content?: string | ToolUseBlock | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 与后端 `DeviceSetting` 对齐,字段来自 `deviceState`。允许其它透传字段。 */
|
||||||
|
export interface DeviceSettingPayload {
|
||||||
|
mac: string;
|
||||||
|
language: number;
|
||||||
|
device: string;
|
||||||
|
volume?: number;
|
||||||
|
msgCount?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与后端 `DevicePEQ` 对齐,由 `api.getPeqState()` 拼装。 */
|
||||||
|
export interface DevicePEQPayload {
|
||||||
|
peqSelect: number;
|
||||||
|
peqEnable: number;
|
||||||
|
peq: Array<{
|
||||||
|
name: string;
|
||||||
|
brand?: string;
|
||||||
|
model?: string;
|
||||||
|
filters: Array<{ type: number | string; fc: number; gain: number; q: number }> | string;
|
||||||
|
preamp?: number;
|
||||||
|
canDel?: number;
|
||||||
|
autoPre?: number;
|
||||||
|
}>;
|
||||||
|
msgCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QuestionPayload {
|
export interface QuestionPayload {
|
||||||
question: string;
|
question: string;
|
||||||
language: number;
|
device_setting: DeviceSettingPayload;
|
||||||
mac: string;
|
device_peq: DevicePEQPayload;
|
||||||
device: string;
|
|
||||||
chat_id?: string | null;
|
chat_id?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -535,6 +535,9 @@
|
|||||||
"title": "EQ comparison",
|
"title": "EQ comparison",
|
||||||
"legendBefore": "Before",
|
"legendBefore": "Before",
|
||||||
"legendAfter": "After",
|
"legendAfter": "After",
|
||||||
|
"legendCurrent": "Current",
|
||||||
|
"legendAfterApply": "After apply",
|
||||||
|
"legendAfterRollback": "After rollback",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"apply": "Apply this optimization",
|
"apply": "Apply this optimization",
|
||||||
"rollback": "Rollback to original",
|
"rollback": "Rollback to original",
|
||||||
|
|||||||
@@ -349,6 +349,9 @@
|
|||||||
"title": "EQ 對比",
|
"title": "EQ 對比",
|
||||||
"legendBefore": "優化前",
|
"legendBefore": "優化前",
|
||||||
"legendAfter": "優化後",
|
"legendAfter": "優化後",
|
||||||
|
"legendCurrent": "當前",
|
||||||
|
"legendAfterApply": "套用後",
|
||||||
|
"legendAfterRollback": "回滾後",
|
||||||
"close": "關閉",
|
"close": "關閉",
|
||||||
"apply": "套用此優化",
|
"apply": "套用此優化",
|
||||||
"rollback": "回滾到優化前",
|
"rollback": "回滾到優化前",
|
||||||
|
|||||||
@@ -350,6 +350,9 @@
|
|||||||
"title": "EQ 对比",
|
"title": "EQ 对比",
|
||||||
"legendBefore": "优化前",
|
"legendBefore": "优化前",
|
||||||
"legendAfter": "优化后",
|
"legendAfter": "优化后",
|
||||||
|
"legendCurrent": "当前",
|
||||||
|
"legendAfterApply": "应用后",
|
||||||
|
"legendAfterRollback": "回滚后",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"apply": "应用此优化",
|
"apply": "应用此优化",
|
||||||
"rollback": "回滚到优化前",
|
"rollback": "回滚到优化前",
|
||||||
|
|||||||
+155
-51
@@ -180,22 +180,13 @@ export default function AIPage() {
|
|||||||
}
|
}
|
||||||
}, [aiText, mac]);
|
}, [aiText, mac]);
|
||||||
|
|
||||||
// ── 把后端消息行映射为 UI 消息(type=0 聊天,type=2 优化卡片) ──
|
// ── 把后端消息行映射为 UI 消息 ──
|
||||||
|
// 只要助手消息带有 before_peq + after_peq,就展示为 AB 对比卡片;
|
||||||
|
// 其余按 type=0 聊天消息处理;用户侧的 type=2 仍然只是内部 prompt,跳过。
|
||||||
const mapRowsToUI = useCallback((rows: MessageRead[]): UIMessage[] => {
|
const mapRowsToUI = useCallback((rows: MessageRead[]): UIMessage[] => {
|
||||||
const result: UIMessage[] = [];
|
const result: UIMessage[] = [];
|
||||||
for (const r of rows) {
|
for (const r of rows) {
|
||||||
if (r.type === 0) {
|
if (r.role === "user" && r.before_peq && r.after_peq) {
|
||||||
const { text, tools } = extractText(r.content);
|
|
||||||
if (!text && tools.length === 0) continue;
|
|
||||||
result.push({
|
|
||||||
kind: "chat",
|
|
||||||
id: r.id,
|
|
||||||
role: r.role,
|
|
||||||
text,
|
|
||||||
tools: tools.map((t) => ({ ...t, ok: true })),
|
|
||||||
});
|
|
||||||
} else if (r.type === 2 && r.role === "assistant" && r.before_peq && r.after_peq) {
|
|
||||||
// 只展示助手侧的优化记录;用户侧(type=2, role=user)只是内部 prompt
|
|
||||||
result.push({
|
result.push({
|
||||||
kind: "optimize",
|
kind: "optimize",
|
||||||
id: r.id,
|
id: r.id,
|
||||||
@@ -205,6 +196,16 @@ export default function AIPage() {
|
|||||||
applied: !!r.applied,
|
applied: !!r.applied,
|
||||||
appliedAt: r.applied_at ?? null,
|
appliedAt: r.applied_at ?? null,
|
||||||
});
|
});
|
||||||
|
} else if (r.type === 0) {
|
||||||
|
const { text, tools } = extractText(r.content);
|
||||||
|
if (!text && tools.length === 0) continue;
|
||||||
|
result.push({
|
||||||
|
kind: "chat",
|
||||||
|
id: r.id,
|
||||||
|
role: r.role,
|
||||||
|
text,
|
||||||
|
tools: tools.map((t) => ({ ...t, ok: true })),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -267,7 +268,7 @@ export default function AIPage() {
|
|||||||
(question: string) => {
|
(question: string) => {
|
||||||
const trimmed = question.trim();
|
const trimmed = question.trim();
|
||||||
if (!trimmed || sending) return;
|
if (!trimmed || sending) return;
|
||||||
if (!mac) {
|
if (!mac || !deviceState || !api) {
|
||||||
toast.error(aiText.deviceRequired);
|
toast.error(aiText.deviceRequired);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -302,12 +303,42 @@ export default function AIPage() {
|
|||||||
// 工具执行后续调用时需要保留当前 chat_id,先捕获一下
|
// 工具执行后续调用时需要保留当前 chat_id,先捕获一下
|
||||||
let currentChatId = chatId;
|
let currentChatId = chatId;
|
||||||
|
|
||||||
|
// 组装新版 /sse/question 请求体
|
||||||
|
const buildAndStream = async () => {
|
||||||
|
let devicePeq: any;
|
||||||
|
try {
|
||||||
|
const peqState = await api.getPeqState();
|
||||||
|
devicePeq = {
|
||||||
|
peqSelect: peqState.peqSelect ?? deviceState.peqSelect ?? 0,
|
||||||
|
peqEnable: deviceState.peqEnable ?? 1,
|
||||||
|
peq: (peqState.peq ?? []).map((p) => ({
|
||||||
|
name: p.name,
|
||||||
|
brand: p.brand,
|
||||||
|
model: p.model,
|
||||||
|
filters: p.filters ?? [],
|
||||||
|
preamp: p.preamp,
|
||||||
|
canDel: p.canDel,
|
||||||
|
autoPre: p.autoPre,
|
||||||
|
})),
|
||||||
|
msgCount: deviceState.msgCount ?? 0,
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
updateAssistant((m) => ({
|
||||||
|
...m,
|
||||||
|
pending: false,
|
||||||
|
error: true,
|
||||||
|
text: aiText.requestFailed,
|
||||||
|
}));
|
||||||
|
setSending(false);
|
||||||
|
toast.error(formatText(aiText.requestFailed, { message: e?.message ?? e }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const controller = streamQuestion(
|
const controller = streamQuestion(
|
||||||
{
|
{
|
||||||
question: trimmed,
|
question: trimmed,
|
||||||
language,
|
device_setting: { ...deviceState },
|
||||||
mac,
|
device_peq: devicePeq,
|
||||||
device: deviceName,
|
|
||||||
chat_id: currentChatId,
|
chat_id: currentChatId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -373,8 +404,10 @@ export default function AIPage() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
abortRef.current = controller;
|
abortRef.current = controller;
|
||||||
|
};
|
||||||
|
void buildAndStream();
|
||||||
},
|
},
|
||||||
[aiText, api, chatId, deviceName, language, mac, refreshCurrentChat, sending],
|
[aiText, api, chatId, deviceState, mac, refreshCurrentChat, sending],
|
||||||
);
|
);
|
||||||
|
|
||||||
const stop = useCallback(() => {
|
const stop = useCallback(() => {
|
||||||
@@ -417,6 +450,44 @@ export default function AIPage() {
|
|||||||
// ── 优化记录:对比对话框 & 应用/回滚 ──
|
// ── 优化记录:对比对话框 & 应用/回滚 ──
|
||||||
const [compareMsg, setCompareMsg] = useState<OptimizeUIMessage | null>(null);
|
const [compareMsg, setCompareMsg] = useState<OptimizeUIMessage | null>(null);
|
||||||
const [applyingId, setApplyingId] = useState<string | null>(null);
|
const [applyingId, setApplyingId] = useState<string | null>(null);
|
||||||
|
const [currentDevicePeq, setCurrentDevicePeq] = useState<OptimizePeqPayload | null>(null);
|
||||||
|
|
||||||
|
// 拉取当前设备 PEQ,作为"当前"参考曲线与"应用/回滚"后的对比基准
|
||||||
|
const refreshCurrentDevicePeq = useCallback(async () => {
|
||||||
|
if (!api) return;
|
||||||
|
try {
|
||||||
|
const peqState = await api.getPeqState();
|
||||||
|
const selectedIdx = peqState.peqSelect ?? 0;
|
||||||
|
const selected = peqState.peq?.[selectedIdx];
|
||||||
|
const selectedFilters = (() => {
|
||||||
|
const f = selected?.filters;
|
||||||
|
if (!f) return undefined;
|
||||||
|
if (typeof f === "string") {
|
||||||
|
try {
|
||||||
|
return JSON.parse(f);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
})();
|
||||||
|
setCurrentDevicePeq({
|
||||||
|
name: selected?.name,
|
||||||
|
preamp: selected?.preamp,
|
||||||
|
canDel: selected?.canDel,
|
||||||
|
autoPre: selected?.autoPre,
|
||||||
|
brand: selected?.brand,
|
||||||
|
model: selected?.model,
|
||||||
|
filters: (selectedFilters ?? peqState.filters ?? []) as OptimizePeqPayload["filters"],
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// 静默失败,保留上一次的值
|
||||||
|
}
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isConnected) void refreshCurrentDevicePeq();
|
||||||
|
}, [isConnected, refreshCurrentDevicePeq]);
|
||||||
|
|
||||||
const handleApplyToggle = useCallback(
|
const handleApplyToggle = useCallback(
|
||||||
async (msg: OptimizeUIMessage) => {
|
async (msg: OptimizeUIMessage) => {
|
||||||
@@ -453,6 +524,7 @@ export default function AIPage() {
|
|||||||
setCompareMsg((cur) =>
|
setCompareMsg((cur) =>
|
||||||
cur && cur.id === msg.id ? { ...cur, applied: nextApplied } : cur,
|
cur && cur.id === msg.id ? { ...cur, applied: nextApplied } : cur,
|
||||||
);
|
);
|
||||||
|
void refreshCurrentDevicePeq();
|
||||||
toast.success(
|
toast.success(
|
||||||
nextApplied ? aiText.optimize.applySuccess : aiText.optimize.rollbackSuccess,
|
nextApplied ? aiText.optimize.applySuccess : aiText.optimize.rollbackSuccess,
|
||||||
);
|
);
|
||||||
@@ -462,7 +534,15 @@ export default function AIPage() {
|
|||||||
setApplyingId(null);
|
setApplyingId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[aiText, api],
|
[aiText, api, refreshCurrentDevicePeq],
|
||||||
|
);
|
||||||
|
|
||||||
|
const openCompare = useCallback(
|
||||||
|
(m: OptimizeUIMessage) => {
|
||||||
|
setCompareMsg(m);
|
||||||
|
void refreshCurrentDevicePeq();
|
||||||
|
},
|
||||||
|
[refreshCurrentDevicePeq],
|
||||||
);
|
);
|
||||||
|
|
||||||
const headerTitle = aiText.title;
|
const headerTitle = aiText.title;
|
||||||
@@ -508,7 +588,6 @@ export default function AIPage() {
|
|||||||
|
|
||||||
{/* ── Messages / Welcome ── */}
|
{/* ── Messages / Welcome ── */}
|
||||||
<div ref={scrollRef} className="flex-1 min-h-0 overflow-y-auto px-4">
|
<div ref={scrollRef} className="flex-1 min-h-0 overflow-y-auto px-4">
|
||||||
<div className="mx-auto w-full max-w-[720px]">
|
|
||||||
{isEmpty ? (
|
{isEmpty ? (
|
||||||
<WelcomeView
|
<WelcomeView
|
||||||
aiText={aiText}
|
aiText={aiText}
|
||||||
@@ -528,8 +607,9 @@ export default function AIPage() {
|
|||||||
key={m.id}
|
key={m.id}
|
||||||
aiText={aiText}
|
aiText={aiText}
|
||||||
msg={m}
|
msg={m}
|
||||||
|
currentPeq={currentDevicePeq}
|
||||||
applying={applyingId === m.id}
|
applying={applyingId === m.id}
|
||||||
onCompare={() => setCompareMsg(m)}
|
onCompare={() => openCompare(m)}
|
||||||
onApplyToggle={() => handleApplyToggle(m)}
|
onApplyToggle={() => handleApplyToggle(m)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -544,7 +624,6 @@ export default function AIPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Input bar ── */}
|
{/* ── Input bar ── */}
|
||||||
<div
|
<div
|
||||||
@@ -558,7 +637,7 @@ export default function AIPage() {
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
className="flex items-end gap-2 rounded-[22px] px-3 py-2 mx-auto w-full max-w-[720px]"
|
className="flex items-end gap-2 rounded-[22px] px-3 py-2 w-full"
|
||||||
style={{
|
style={{
|
||||||
background: "rgba(28,28,30,0.92)",
|
background: "rgba(28,28,30,0.92)",
|
||||||
border: "1px solid rgba(255,255,255,0.08)",
|
border: "1px solid rgba(255,255,255,0.08)",
|
||||||
@@ -611,7 +690,7 @@ export default function AIPage() {
|
|||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
{chatId && messages.length > 0 && (
|
{chatId && messages.length > 0 && (
|
||||||
<div className="flex justify-center mt-2 mx-auto w-full max-w-[720px]">
|
<div className="flex justify-center mt-2">
|
||||||
<button
|
<button
|
||||||
onClick={clearCurrent}
|
onClick={clearCurrent}
|
||||||
className="text-[11px] text-white/35 active:text-white/70 transition-colors px-2"
|
className="text-[11px] text-white/35 active:text-white/70 transition-colors px-2"
|
||||||
@@ -639,6 +718,7 @@ export default function AIPage() {
|
|||||||
<CompareDialog
|
<CompareDialog
|
||||||
aiText={aiText}
|
aiText={aiText}
|
||||||
msg={compareMsg}
|
msg={compareMsg}
|
||||||
|
currentPeq={currentDevicePeq}
|
||||||
applying={applyingId === compareMsg.id}
|
applying={applyingId === compareMsg.id}
|
||||||
onClose={() => setCompareMsg(null)}
|
onClose={() => setCompareMsg(null)}
|
||||||
onApplyToggle={() => handleApplyToggle(compareMsg)}
|
onApplyToggle={() => handleApplyToggle(compareMsg)}
|
||||||
@@ -899,18 +979,23 @@ function HistoryPanel({
|
|||||||
function OptimizeCard({
|
function OptimizeCard({
|
||||||
aiText,
|
aiText,
|
||||||
msg,
|
msg,
|
||||||
|
currentPeq,
|
||||||
applying,
|
applying,
|
||||||
onCompare,
|
onCompare,
|
||||||
onApplyToggle,
|
onApplyToggle,
|
||||||
}: {
|
}: {
|
||||||
aiText: AILocale;
|
aiText: AILocale;
|
||||||
msg: OptimizeUIMessage;
|
msg: OptimizeUIMessage;
|
||||||
|
currentPeq: OptimizePeqPayload | null;
|
||||||
applying: boolean;
|
applying: boolean;
|
||||||
onCompare: () => void;
|
onCompare: () => void;
|
||||||
onApplyToggle: () => void;
|
onApplyToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
const filterCount = msg.afterPeq.filters?.length ?? 0;
|
// 当前未应用 → 目标是 after_peq(点击应用即切到 after)
|
||||||
const preamp = msg.afterPeq.preamp ?? 0;
|
// 当前已应用 → 目标是 before_peq(点击回滚即切回 before)
|
||||||
|
const targetPeq = msg.applied ? msg.beforePeq : msg.afterPeq;
|
||||||
|
const filterCount = targetPeq.filters?.length ?? 0;
|
||||||
|
const preamp = targetPeq.preamp ?? 0;
|
||||||
const summary = formatText(aiText.optimize.summary, {
|
const summary = formatText(aiText.optimize.summary, {
|
||||||
count: filterCount,
|
count: filterCount,
|
||||||
preamp: `${preamp > 0 ? "+" : ""}${preamp.toFixed(1)}`,
|
preamp: `${preamp > 0 ? "+" : ""}${preamp.toFixed(1)}`,
|
||||||
@@ -958,10 +1043,10 @@ function OptimizeCard({
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-[12px] text-white/45 mt-0.5">{summary}</div>
|
<div className="text-[12px] text-white/45 mt-0.5">{summary}</div>
|
||||||
|
|
||||||
{/* 缩略对比曲线 */}
|
{/* 缩略对比曲线:当前设备 vs 应用/回滚后 */}
|
||||||
<ComparePreview
|
<ComparePreview
|
||||||
beforePeq={msg.beforePeq}
|
currentPeq={currentPeq}
|
||||||
afterPeq={msg.afterPeq}
|
targetPeq={targetPeq}
|
||||||
width={260}
|
width={260}
|
||||||
height={70}
|
height={70}
|
||||||
/>
|
/>
|
||||||
@@ -1073,18 +1158,21 @@ function CurveSvg({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ComparePreview({
|
function ComparePreview({
|
||||||
beforePeq,
|
currentPeq,
|
||||||
afterPeq,
|
targetPeq,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
}: {
|
}: {
|
||||||
beforePeq: OptimizePeqPayload;
|
currentPeq: OptimizePeqPayload | null;
|
||||||
afterPeq: OptimizePeqPayload;
|
targetPeq: OptimizePeqPayload;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
}) {
|
}) {
|
||||||
const beforeBands = useMemo(() => peqToBands(beforePeq), [beforePeq]);
|
const currentBands = useMemo(
|
||||||
const afterBands = useMemo(() => peqToBands(afterPeq), [afterPeq]);
|
() => (currentPeq ? peqToBands(currentPeq) : []),
|
||||||
|
[currentPeq],
|
||||||
|
);
|
||||||
|
const targetBands = useMemo(() => peqToBands(targetPeq), [targetPeq]);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative mt-3 rounded-[10px] overflow-hidden"
|
className="relative mt-3 rounded-[10px] overflow-hidden"
|
||||||
@@ -1093,11 +1181,13 @@ function ComparePreview({
|
|||||||
border: "1px solid rgba(255,255,255,0.05)",
|
border: "1px solid rgba(255,255,255,0.05)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{currentBands.length > 0 && (
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
<CurveSvg bands={beforeBands} width={width} height={height} stroke="rgba(255,255,255,0.35)" />
|
<CurveSvg bands={currentBands} width={width} height={height} stroke="rgba(255,255,255,0.35)" />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CurveSvg bands={afterBands} width={width} height={height} stroke="#00FFF6" fill="rgba(0,255,246,0.08)" />
|
<CurveSvg bands={targetBands} width={width} height={height} stroke="#00FFF6" fill="rgba(0,255,246,0.08)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1109,18 +1199,30 @@ function ComparePreview({
|
|||||||
function CompareDialog({
|
function CompareDialog({
|
||||||
aiText,
|
aiText,
|
||||||
msg,
|
msg,
|
||||||
|
currentPeq,
|
||||||
applying,
|
applying,
|
||||||
onClose,
|
onClose,
|
||||||
onApplyToggle,
|
onApplyToggle,
|
||||||
}: {
|
}: {
|
||||||
aiText: AILocale;
|
aiText: AILocale;
|
||||||
msg: OptimizeUIMessage;
|
msg: OptimizeUIMessage;
|
||||||
|
currentPeq: OptimizePeqPayload | null;
|
||||||
applying: boolean;
|
applying: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onApplyToggle: () => void;
|
onApplyToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
const beforeBands = useMemo(() => peqToBands(msg.beforePeq), [msg.beforePeq]);
|
// 未应用 → 目标 = after_peq,图例右侧显示“应用后”
|
||||||
const afterBands = useMemo(() => peqToBands(msg.afterPeq), [msg.afterPeq]);
|
// 已应用 → 目标 = before_peq,图例右侧显示“回滚后”
|
||||||
|
const targetPeq = msg.applied ? msg.beforePeq : msg.afterPeq;
|
||||||
|
const currentBands = useMemo(
|
||||||
|
() => (currentPeq ? peqToBands(currentPeq) : []),
|
||||||
|
[currentPeq],
|
||||||
|
);
|
||||||
|
const targetBands = useMemo(() => peqToBands(targetPeq), [targetPeq]);
|
||||||
|
const currentLabel = aiText.compareDialog.legendCurrent;
|
||||||
|
const targetLabel = msg.applied
|
||||||
|
? aiText.compareDialog.legendAfterRollback
|
||||||
|
: aiText.compareDialog.legendAfterApply;
|
||||||
const chartWidth = 340;
|
const chartWidth = 340;
|
||||||
const chartHeight = 160;
|
const chartHeight = 160;
|
||||||
|
|
||||||
@@ -1173,25 +1275,27 @@ function CompareDialog({
|
|||||||
className="block w-3 h-[2px] rounded"
|
className="block w-3 h-[2px] rounded"
|
||||||
style={{ background: "rgba(255,255,255,0.45)" }}
|
style={{ background: "rgba(255,255,255,0.45)" }}
|
||||||
/>
|
/>
|
||||||
{aiText.compareDialog.legendBefore}
|
{currentLabel}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center gap-1.5" style={{ color: "#00FFF6" }}>
|
<span className="inline-flex items-center gap-1.5" style={{ color: "#00FFF6" }}>
|
||||||
<span className="block w-3 h-[2px] rounded" style={{ background: "#00FFF6" }} />
|
<span className="block w-3 h-[2px] rounded" style={{ background: "#00FFF6" }} />
|
||||||
{aiText.compareDialog.legendAfter}
|
{targetLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative" style={{ height: chartHeight }}>
|
<div className="relative" style={{ height: chartHeight }}>
|
||||||
|
{currentBands.length > 0 && (
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
<CurveSvg
|
<CurveSvg
|
||||||
bands={beforeBands}
|
bands={currentBands}
|
||||||
width={chartWidth}
|
width={chartWidth}
|
||||||
height={chartHeight}
|
height={chartHeight}
|
||||||
stroke="rgba(255,255,255,0.45)"
|
stroke="rgba(255,255,255,0.45)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
<CurveSvg
|
<CurveSvg
|
||||||
bands={afterBands}
|
bands={targetBands}
|
||||||
width={chartWidth}
|
width={chartWidth}
|
||||||
height={chartHeight}
|
height={chartHeight}
|
||||||
stroke="#00FFF6"
|
stroke="#00FFF6"
|
||||||
@@ -1209,18 +1313,18 @@ function CompareDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 参数对比表 */}
|
{/* 参数对比表:当前设备 vs 应用/回滚后 */}
|
||||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||||
<ParamCard
|
<ParamCard
|
||||||
aiText={aiText}
|
aiText={aiText}
|
||||||
label={aiText.compareDialog.legendBefore}
|
label={currentLabel}
|
||||||
peq={msg.beforePeq}
|
peq={currentPeq}
|
||||||
muted
|
muted
|
||||||
/>
|
/>
|
||||||
<ParamCard
|
<ParamCard
|
||||||
aiText={aiText}
|
aiText={aiText}
|
||||||
label={aiText.compareDialog.legendAfter}
|
label={targetLabel}
|
||||||
peq={msg.afterPeq}
|
peq={targetPeq}
|
||||||
highlight
|
highlight
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1283,14 +1387,14 @@ function ParamCard({
|
|||||||
}: {
|
}: {
|
||||||
aiText: AILocale;
|
aiText: AILocale;
|
||||||
label: string;
|
label: string;
|
||||||
peq: OptimizePeqPayload;
|
peq: OptimizePeqPayload | null;
|
||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const filters = peq.filters ?? [];
|
const filters = peq?.filters ?? [];
|
||||||
const summary = formatText(aiText.compareDialog.paramSummary, {
|
const summary = formatText(aiText.compareDialog.paramSummary, {
|
||||||
count: filters.length,
|
count: filters.length,
|
||||||
preamp: (peq.preamp ?? 0).toFixed(1),
|
preamp: (peq?.preamp ?? 0).toFixed(1),
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user