You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
652 lines
24 KiB
652 lines
24 KiB
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Mix Preview</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117; --surface: #161b22; --border: #30363d;
|
|
--text: #e6edf3; --text-secondary: #8b949e; --accent: #58a6ff;
|
|
--success: #3fb950; --danger: #f85149;
|
|
--voice-color: #d2a8ff; --nature-color: #3fb950;
|
|
--music-color: #f0883e; --mixed-color: #58a6ff;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
background: var(--bg); color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
padding: 24px; max-width: 720px; margin: 0 auto;
|
|
}
|
|
h1 { font-size: 22px; margin-bottom: 4px; }
|
|
.subtitle { color: var(--text-secondary); margin-bottom: 24px; font-size: 13px; }
|
|
|
|
/* Hero player */
|
|
.hero {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: 14px; padding: 24px; margin-bottom: 24px;
|
|
border-left: 3px solid var(--mixed-color);
|
|
}
|
|
.hero-label {
|
|
display: flex; align-items: center; gap: 8px; margin-bottom: 14px;
|
|
}
|
|
.hero-icon {
|
|
width: 32px; height: 32px; border-radius: 10px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 16px; background: rgba(88,166,255,0.15); color: var(--mixed-color);
|
|
}
|
|
.hero-title { font-size: 17px; font-weight: 600; }
|
|
.hero audio { width: 100%; height: 40px; border-radius: 8px; }
|
|
.hero-meta {
|
|
display: flex; gap: 16px; margin-top: 10px; font-size: 12px; color: var(--text-secondary);
|
|
}
|
|
|
|
/* Adjustment section */
|
|
.section-title {
|
|
font-size: 14px; font-weight: 600; margin-bottom: 12px;
|
|
color: var(--text-secondary); letter-spacing: 0.5px;
|
|
}
|
|
|
|
.adj-grid { display: flex; flex-direction: column; gap: 8px; margin-bottom: 24px; }
|
|
|
|
.adj-card {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: 10px; overflow: hidden; transition: border-color 0.15s;
|
|
}
|
|
.adj-card.active { border-color: var(--accent); }
|
|
|
|
.adj-header {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 12px 16px; cursor: pointer; user-select: none;
|
|
}
|
|
.adj-header:hover { background: rgba(255,255,255,0.02); }
|
|
|
|
.adj-check {
|
|
width: 20px; height: 20px; border-radius: 6px;
|
|
border: 2px solid var(--border); display: flex;
|
|
align-items: center; justify-content: center;
|
|
flex-shrink: 0; transition: all 0.15s; font-size: 12px;
|
|
}
|
|
.adj-card.active .adj-check {
|
|
background: var(--accent); border-color: var(--accent); color: #fff;
|
|
}
|
|
|
|
.adj-label { font-size: 14px; flex: 1; }
|
|
.adj-tag {
|
|
font-size: 11px; padding: 2px 8px; border-radius: 10px;
|
|
background: rgba(255,255,255,0.06); color: var(--text-secondary);
|
|
}
|
|
|
|
.adj-body {
|
|
display: none; padding: 0 16px 14px 46px;
|
|
}
|
|
.adj-card.active .adj-body { display: block; }
|
|
|
|
/* Choice pills inside adj */
|
|
.choice-row {
|
|
display: flex; gap: 8px; margin-bottom: 8px;
|
|
}
|
|
.choice-pill {
|
|
flex: 1; padding: 8px 12px; border: 1px solid var(--border);
|
|
border-radius: 8px; background: var(--bg); color: var(--text-secondary);
|
|
font-size: 13px; cursor: pointer; text-align: center;
|
|
transition: all 0.15s; user-select: none;
|
|
}
|
|
.choice-pill:hover { border-color: var(--accent); color: var(--text); }
|
|
.choice-pill.selected {
|
|
border-color: var(--accent); background: rgba(88,166,255,0.12);
|
|
color: var(--accent); font-weight: 600;
|
|
}
|
|
|
|
.adj-hint {
|
|
font-size: 12px; color: var(--text-secondary); margin-bottom: 8px; line-height: 1.5;
|
|
}
|
|
|
|
textarea {
|
|
width: 100%; min-height: 48px; background: var(--bg);
|
|
border: 1px solid var(--border); border-radius: 8px;
|
|
padding: 8px 10px; color: var(--text); font-size: 13px;
|
|
font-family: inherit; resize: vertical; outline: none;
|
|
}
|
|
textarea:focus { border-color: var(--accent); }
|
|
textarea::placeholder { color: var(--text-secondary); }
|
|
|
|
/* Reference tracks */
|
|
.ref-section { margin-bottom: 24px; }
|
|
.ref-toggle {
|
|
font-size: 13px; color: var(--text-secondary); cursor: pointer;
|
|
padding: 8px 0; user-select: none; display: flex; align-items: center; gap: 6px;
|
|
}
|
|
.ref-toggle:hover { color: var(--text); }
|
|
.ref-arrow { font-size: 10px; transition: transform 0.15s; }
|
|
.ref-arrow.open { transform: rotate(90deg); }
|
|
.ref-body { display: none; padding-top: 8px; }
|
|
.ref-body.open { display: block; }
|
|
.ref-track {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: 8px; padding: 12px 14px; margin-bottom: 8px;
|
|
}
|
|
.ref-track-label {
|
|
font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;
|
|
display: flex; align-items: center; gap: 6px;
|
|
}
|
|
.ref-dot {
|
|
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
|
}
|
|
.ref-track audio { width: 100%; height: 32px; }
|
|
.ref-params {
|
|
display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px;
|
|
}
|
|
.ref-param {
|
|
font-size: 11px; background: var(--bg); padding: 3px 8px;
|
|
border-radius: 6px; color: var(--text-secondary);
|
|
}
|
|
|
|
/* Actions */
|
|
.actions { display: flex; gap: 12px; }
|
|
.btn {
|
|
flex: 1; padding: 14px 20px; border: none; border-radius: 10px;
|
|
font-size: 15px; font-weight: 600; cursor: pointer;
|
|
font-family: inherit; transition: opacity 0.15s;
|
|
}
|
|
.btn:hover { opacity: 0.85; }
|
|
.btn:active { opacity: 0.7; }
|
|
.btn-success { background: var(--success); color: #fff; }
|
|
.btn-primary { background: var(--accent); color: #fff; }
|
|
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
.btn-count {
|
|
font-size: 12px; font-weight: 400; opacity: 0.8; margin-left: 4px;
|
|
}
|
|
|
|
.success-msg { text-align: center; padding: 60px 20px; }
|
|
.success-msg h2 { font-size: 20px; margin-bottom: 8px; }
|
|
.success-msg p { color: var(--text-secondary); font-size: 14px; }
|
|
|
|
@media (max-width: 600px) {
|
|
body { padding: 16px; }
|
|
.actions { flex-direction: column; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="hero"></div>
|
|
<div id="adj"></div>
|
|
|
|
<script>
|
|
const PREVIEW_TYPE = null; /* __PREVIEW_TYPE__ */
|
|
const PREVIEW_DATA = null; /* __PREVIEW_DATA__ */
|
|
|
|
const data = typeof PREVIEW_DATA === 'string' ? JSON.parse(PREVIEW_DATA) : PREVIEW_DATA;
|
|
|
|
/* ── i18n strings ── */
|
|
const STRINGS = {
|
|
en: {
|
|
subtitle_asmr: 'ASMR',
|
|
subtitle_meditation: 'Guided Meditation',
|
|
subtitle_story: 'Bedtime Story',
|
|
listen_hint: 'Listen first, then check items below for adjustments',
|
|
hero_title: 'Final Mix',
|
|
voice_label: 'Voice',
|
|
speed_label: 'Speed',
|
|
has_music: 'Has BGM',
|
|
no_music: 'No BGM',
|
|
section_adj: 'What needs adjustment?',
|
|
adj_voice_speed_label: 'Voice speed issue',
|
|
adj_voice_speed_tag: 'Voice',
|
|
adj_voice_speed_hint: 'Current speed {speed}x.',
|
|
adj_voice_speed_options: ['Too fast', 'Too slow'],
|
|
adj_voice_tone_label: 'Change voice',
|
|
adj_voice_tone_tag: 'Voice',
|
|
adj_voice_tone_hint: 'Current voice: {voiceId}. Describe what you want.',
|
|
adj_voice_tone_placeholder: 'e.g.: deeper / softer / male / no breathy',
|
|
adj_voice_volume_label: 'Voice volume issue',
|
|
adj_voice_volume_tag: 'Voice',
|
|
adj_voice_volume_hint: 'Current gain {gain}x.',
|
|
adj_voice_volume_options: ['Too loud', 'Too quiet'],
|
|
adj_nature_volume_label: 'Background sound volume',
|
|
adj_nature_volume_tag: 'Ambient',
|
|
adj_nature_volume_hint: 'Current {natureCategory} volume {natureVol}x.',
|
|
adj_nature_volume_options: ['Too loud', 'Too quiet'],
|
|
adj_nature_swap_label: 'Change ambient sound',
|
|
adj_nature_swap_tag: 'Ambient',
|
|
adj_nature_swap_hint: 'Currently using "{natureCategory}". You can switch to another ambient sound.',
|
|
adj_nature_swap_placeholder: 'e.g.: switch to rain / ocean / creek',
|
|
adj_music_volume_label: 'Music volume issue',
|
|
adj_music_volume_tag: 'Music',
|
|
adj_music_volume_hint: 'Current music volume {musicVol}x.',
|
|
adj_music_volume_options: ['Too loud', 'Too quiet'],
|
|
adj_music_style_label: 'Music style issue',
|
|
adj_music_style_tag: 'Music',
|
|
adj_music_style_hint: 'Describe the music feel you want; it will be regenerated.',
|
|
adj_music_style_placeholder: 'e.g.: too lively / more ethereal / no piano',
|
|
adj_music_toggle_label_has: 'Remove BGM',
|
|
adj_music_toggle_label_no: 'Add BGM',
|
|
adj_music_toggle_tag: 'Music',
|
|
adj_music_toggle_hint_has: 'Remove background music, keep only voice and ambient sound.',
|
|
adj_music_toggle_hint_no: 'Add a background music track to this project.',
|
|
adj_voice_pause_label: 'Pause/breathing feels off',
|
|
adj_voice_pause_tag: 'Rhythm',
|
|
adj_voice_pause_hint: 'Controls the overall duration of all pauses (between sentences, paragraphs, breathing gaps).',
|
|
adj_voice_pause_options: ['Longer pauses', 'Shorter pauses'],
|
|
adj_other_label: 'Other issues',
|
|
adj_other_tag: 'Other',
|
|
adj_other_hint: 'Describe any issue not covered above.',
|
|
adj_other_placeholder: 'e.g.: fade out too fast / too long / noise somewhere',
|
|
ref_toggle: 'Reference tracks (expand to compare individual tracks)',
|
|
ref_voice: 'Voice',
|
|
ref_bgm: 'BGM',
|
|
ref_speed: 'Speed',
|
|
ref_gain: 'Gain',
|
|
ref_delay: 'Delay',
|
|
ref_volume: 'Volume',
|
|
btn_export: 'Satisfied, export',
|
|
btn_submit: 'Submit changes',
|
|
btn_count_suffix: ' items',
|
|
success_export_title: 'Export confirmed',
|
|
success_export_desc: 'Exporting final audio...',
|
|
success_adjust_title: 'Feedback submitted',
|
|
success_adjust_desc: 'Regenerating based on your feedback...',
|
|
confirm_hint: 'Check to confirm this change',
|
|
submit_error: 'Submission failed, please retry',
|
|
feedback_satisfied: 'Satisfied, ready to export',
|
|
feedback_checked_no_choice: '(checked, no direction selected)',
|
|
feedback_checked_no_detail: '(checked, no detail provided)',
|
|
feedback_needs_adjustment: 'Needs adjustment',
|
|
},
|
|
zh: {
|
|
subtitle_asmr: 'ASMR',
|
|
subtitle_meditation: '引导冥想',
|
|
subtitle_story: '睡前故事',
|
|
listen_hint: '先听成品,不满意的地方勾选下方对应项',
|
|
hero_title: '混合成品',
|
|
voice_label: '音色',
|
|
speed_label: '语速',
|
|
has_music: '有背景音乐',
|
|
no_music: '无背景音乐',
|
|
section_adj: '哪里需要调整?',
|
|
adj_voice_speed_label: '语速不合适',
|
|
adj_voice_speed_tag: '语音',
|
|
adj_voice_speed_hint: '当前语速 {speed}x。',
|
|
adj_voice_speed_options: ['太快了', '太慢了'],
|
|
adj_voice_tone_label: '想换音色',
|
|
adj_voice_tone_tag: '语音',
|
|
adj_voice_tone_hint: '当前音色:{voiceId}。描述你想要的感觉即可。',
|
|
adj_voice_tone_placeholder: '例如:更低沉 / 更温柔 / 男声 / 不要气声',
|
|
adj_voice_volume_label: '人声音量不对',
|
|
adj_voice_volume_tag: '语音',
|
|
adj_voice_volume_hint: '当前增益 {gain}x。',
|
|
adj_voice_volume_options: ['人声太大', '人声太小'],
|
|
adj_nature_volume_label: '背景声音量不对',
|
|
adj_nature_volume_tag: '环境音',
|
|
adj_nature_volume_hint: '当前 {natureCategory} 音量 {natureVol}x。',
|
|
adj_nature_volume_options: ['太大', '太小'],
|
|
adj_nature_swap_label: '想换背景声音',
|
|
adj_nature_swap_tag: '环境音',
|
|
adj_nature_swap_hint: '当前使用的是「{natureCategory}」。可以换成其他自然声音。',
|
|
adj_nature_swap_placeholder: '例如:换成雨声 / 海浪 / 小溪',
|
|
adj_music_volume_label: '音乐音量不对',
|
|
adj_music_volume_tag: '音乐',
|
|
adj_music_volume_hint: '当前音乐音量 {musicVol}x。',
|
|
adj_music_volume_options: ['太大', '太小'],
|
|
adj_music_style_label: '音乐风格不对',
|
|
adj_music_style_tag: '音乐',
|
|
adj_music_style_hint: '描述你想要的音乐感觉,会重新生成。',
|
|
adj_music_style_placeholder: '例如:太活泼了 / 想要更空灵 / 不要钢琴',
|
|
adj_music_toggle_label_has: '不想要背景音乐',
|
|
adj_music_toggle_label_no: '想加背景音乐',
|
|
adj_music_toggle_tag: '音乐',
|
|
adj_music_toggle_hint_has: '去掉背景音乐,只保留语音和环境音。',
|
|
adj_music_toggle_hint_no: '为这个项目加一段背景音乐。',
|
|
adj_voice_pause_label: '停顿气口感觉不对',
|
|
adj_voice_pause_tag: '节奏',
|
|
adj_voice_pause_hint: '指脚本中所有停顿(句间、段落、呼吸留白)的整体时长。',
|
|
adj_voice_pause_options: ['延长气口', '缩短气口'],
|
|
adj_other_label: '其他问题',
|
|
adj_other_tag: '自由',
|
|
adj_other_hint: '描述任何上面没覆盖到的问题。',
|
|
adj_other_placeholder: '例如:结尾淡出太快 / 整体太长了 / 某处有杂音',
|
|
ref_toggle: '分轨参考(可展开对比单独轨道)',
|
|
ref_voice: '语音',
|
|
ref_bgm: '背景音乐',
|
|
ref_speed: '语速',
|
|
ref_gain: '增益',
|
|
ref_delay: '延迟',
|
|
ref_volume: '音量',
|
|
btn_export: '满意,直接导出',
|
|
btn_submit: '提交调整',
|
|
btn_count_suffix: '项',
|
|
success_export_title: '已确认导出',
|
|
success_export_desc: '正在导出最终音频…',
|
|
success_adjust_title: '调整意见已提交',
|
|
success_adjust_desc: '正在根据你的反馈重新生成…',
|
|
confirm_hint: '勾选即表示确认此调整',
|
|
submit_error: '提交失败,请重试',
|
|
feedback_satisfied: '满意,可以导出',
|
|
feedback_checked_no_choice: '(已勾选,未选择方向)',
|
|
feedback_checked_no_detail: '(已勾选,未填写详情)',
|
|
feedback_needs_adjustment: '需要调整',
|
|
},
|
|
};
|
|
|
|
const locale = (data && data.locale) || 'en';
|
|
const S = STRINGS[locale];
|
|
|
|
document.documentElement.lang = locale === 'zh' ? 'zh-CN' : 'en';
|
|
|
|
/* Build ADJUSTMENTS from i18n strings */
|
|
function buildAdjustments() {
|
|
return [
|
|
{
|
|
id: 'voice_speed', label: S.adj_voice_speed_label, tag: S.adj_voice_speed_tag,
|
|
tagColor: '#d2a8ff',
|
|
hint: S.adj_voice_speed_hint,
|
|
type: 'choice', options: S.adj_voice_speed_options,
|
|
},
|
|
{
|
|
id: 'voice_tone', label: S.adj_voice_tone_label, tag: S.adj_voice_tone_tag,
|
|
tagColor: '#d2a8ff',
|
|
hint: S.adj_voice_tone_hint,
|
|
type: 'text', placeholder: S.adj_voice_tone_placeholder,
|
|
},
|
|
{
|
|
id: 'voice_volume', label: S.adj_voice_volume_label, tag: S.adj_voice_volume_tag,
|
|
tagColor: '#d2a8ff',
|
|
hint: S.adj_voice_volume_hint,
|
|
type: 'choice', options: S.adj_voice_volume_options,
|
|
},
|
|
{
|
|
id: 'nature_volume', label: S.adj_nature_volume_label, tag: S.adj_nature_volume_tag,
|
|
tagColor: '#3fb950',
|
|
hint: S.adj_nature_volume_hint,
|
|
type: 'choice', options: S.adj_nature_volume_options,
|
|
hidden: () => !data.nature,
|
|
},
|
|
{
|
|
id: 'nature_swap', label: S.adj_nature_swap_label, tag: S.adj_nature_swap_tag,
|
|
tagColor: '#3fb950',
|
|
hint: S.adj_nature_swap_hint,
|
|
type: 'text', placeholder: S.adj_nature_swap_placeholder,
|
|
hidden: () => !data.nature,
|
|
},
|
|
{
|
|
id: 'music_volume', label: S.adj_music_volume_label, tag: S.adj_music_volume_tag,
|
|
tagColor: '#f0883e',
|
|
hint: S.adj_music_volume_hint,
|
|
type: 'choice', options: S.adj_music_volume_options,
|
|
hidden: () => !data.music,
|
|
},
|
|
{
|
|
id: 'music_style', label: S.adj_music_style_label, tag: S.adj_music_style_tag,
|
|
tagColor: '#f0883e',
|
|
hint: S.adj_music_style_hint,
|
|
type: 'text', placeholder: S.adj_music_style_placeholder,
|
|
hidden: () => !data.music,
|
|
},
|
|
{
|
|
id: 'music_toggle',
|
|
label: data.music ? S.adj_music_toggle_label_has : S.adj_music_toggle_label_no,
|
|
tag: S.adj_music_toggle_tag,
|
|
tagColor: '#f0883e',
|
|
hint: data.music ? S.adj_music_toggle_hint_has : S.adj_music_toggle_hint_no,
|
|
type: 'confirm',
|
|
},
|
|
{
|
|
id: 'voice_pause', label: S.adj_voice_pause_label, tag: S.adj_voice_pause_tag,
|
|
tagColor: '#58a6ff',
|
|
hint: S.adj_voice_pause_hint,
|
|
type: 'choice', options: S.adj_voice_pause_options,
|
|
},
|
|
{
|
|
id: 'other', label: S.adj_other_label, tag: S.adj_other_tag,
|
|
tagColor: '#8b949e',
|
|
hint: S.adj_other_hint,
|
|
type: 'text', placeholder: S.adj_other_placeholder,
|
|
},
|
|
];
|
|
}
|
|
|
|
const ADJUSTMENTS = buildAdjustments();
|
|
|
|
const state = {
|
|
active: {}, // id -> true/false
|
|
values: {}, // id -> slider value or text value
|
|
refOpen: false,
|
|
};
|
|
|
|
/* Init values */
|
|
ADJUSTMENTS.forEach(a => {
|
|
state.active[a.id] = false;
|
|
if (a.type === 'choice') state.values[a.id] = null; // no option selected yet
|
|
else if (a.type === 'text') state.values[a.id] = '';
|
|
else if (a.type === 'confirm') state.values[a.id] = false;
|
|
});
|
|
|
|
function esc(text) {
|
|
const d = document.createElement('div');
|
|
d.textContent = text;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function fillHint(hint) {
|
|
return hint
|
|
.replace('{speed}', data.voice.speed)
|
|
.replace('{voiceId}', data.voice.voiceId)
|
|
.replace('{gain}', data.voice.gain)
|
|
.replace('{natureCategory}', data.nature ? data.nature.category : '')
|
|
.replace('{natureVol}', data.nature ? data.nature.volume : '')
|
|
.replace('{musicVol}', data.music ? data.music.volume : '')
|
|
.replace('{delay}', data.voiceDelay);
|
|
}
|
|
|
|
function activeCount() {
|
|
return Object.values(state.active).filter(Boolean).length;
|
|
}
|
|
|
|
function render() {
|
|
renderHero();
|
|
renderAdj();
|
|
}
|
|
|
|
function renderHero() {
|
|
const typeName = data.contentType === 'asmr' ? S.subtitle_asmr
|
|
: data.contentType === 'meditation' ? S.subtitle_meditation
|
|
: S.subtitle_story;
|
|
document.getElementById('hero').innerHTML = `
|
|
<h1>${esc(data.projectName)}</h1>
|
|
<p class="subtitle">${typeName} | ${S.listen_hint}</p>
|
|
<div class="hero">
|
|
<div class="hero-label">
|
|
<div class="hero-icon">▶</div>
|
|
<span class="hero-title">${S.hero_title}</span>
|
|
</div>
|
|
<audio controls preload="metadata" src="${data.mixed.audioUrl}"></audio>
|
|
<div class="hero-meta">
|
|
<span>${S.voice_label} ${esc(data.voice.voiceId)}</span>
|
|
<span>${S.speed_label} ${data.voice.speed}x</span>
|
|
${data.nature ? `<span>${esc(data.nature.category)}</span>` : ''}
|
|
${data.music ? `<span>${S.has_music}</span>` : `<span>${S.no_music}</span>`}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderAdj() {
|
|
const visibleAdj = ADJUSTMENTS.filter(a => !a.hidden || !a.hidden());
|
|
let html = `
|
|
<div class="section-title">${S.section_adj}</div>
|
|
<div class="adj-grid">
|
|
`;
|
|
|
|
visibleAdj.forEach(a => {
|
|
const isActive = state.active[a.id];
|
|
html += `
|
|
<div class="adj-card ${isActive ? 'active' : ''}" data-id="${a.id}">
|
|
<div class="adj-header" onclick="toggleAdj('${a.id}')">
|
|
<div class="adj-check">${isActive ? '✓' : ''}</div>
|
|
<span class="adj-label">${a.label}</span>
|
|
<span class="adj-tag" style="color:${a.tagColor};border:1px solid ${a.tagColor}33;background:${a.tagColor}11">${a.tag}</span>
|
|
</div>
|
|
<div class="adj-body">
|
|
<div class="adj-hint">${fillHint(a.hint)}</div>
|
|
`;
|
|
|
|
if (a.type === 'choice') {
|
|
html += `<div class="choice-row">`;
|
|
a.options.forEach(opt => {
|
|
const selected = state.values[a.id] === opt ? 'selected' : '';
|
|
html += `<div class="choice-pill ${selected}" onclick="onChoice('${a.id}','${opt}')">${opt}</div>`;
|
|
});
|
|
html += `</div>`;
|
|
} else if (a.type === 'text') {
|
|
html += `
|
|
<textarea placeholder="${a.placeholder || ''}"
|
|
oninput="state.values['${a.id}']=this.value">${esc(state.values[a.id])}</textarea>
|
|
`;
|
|
} else if (a.type === 'confirm') {
|
|
html += `<div class="adj-hint" style="color:var(--accent)">${S.confirm_hint}</div>`;
|
|
}
|
|
|
|
html += `</div></div>`;
|
|
});
|
|
|
|
html += `</div>`;
|
|
|
|
/* Reference tracks (collapsed) */
|
|
html += `
|
|
<div class="ref-section">
|
|
<div class="ref-toggle" onclick="toggleRef()">
|
|
<span class="ref-arrow ${state.refOpen ? 'open' : ''}">▶</span>
|
|
${S.ref_toggle}
|
|
</div>
|
|
<div class="ref-body ${state.refOpen ? 'open' : ''}">
|
|
<div class="ref-track">
|
|
<div class="ref-track-label"><div class="ref-dot" style="background:var(--voice-color)"></div>${S.ref_voice}</div>
|
|
<audio controls preload="none" src="${data.voice.audioUrl}"></audio>
|
|
<div class="ref-params">
|
|
<span class="ref-param">${esc(data.voice.voiceId)}</span>
|
|
<span class="ref-param">${S.ref_speed} ${data.voice.speed}x</span>
|
|
<span class="ref-param">${S.ref_gain} ${data.voice.gain}x</span>
|
|
<span class="ref-param">${S.ref_delay} ${data.voiceDelay}s</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
if (data.nature) {
|
|
html += `
|
|
<div class="ref-track">
|
|
<div class="ref-track-label"><div class="ref-dot" style="background:var(--nature-color)"></div>${esc(data.nature.category)}</div>
|
|
<audio controls preload="none" src="${data.nature.audioUrl}"></audio>
|
|
<div class="ref-params"><span class="ref-param">${S.ref_volume} ${data.nature.volume}x</span></div>
|
|
</div>
|
|
`;
|
|
}
|
|
if (data.music) {
|
|
html += `
|
|
<div class="ref-track">
|
|
<div class="ref-track-label"><div class="ref-dot" style="background:var(--music-color)"></div>${S.ref_bgm}</div>
|
|
<audio controls preload="none" src="${data.music.audioUrl}"></audio>
|
|
<div class="ref-params"><span class="ref-param">${S.ref_volume} ${data.music.volume}x</span></div>
|
|
</div>
|
|
`;
|
|
}
|
|
html += `</div></div>`;
|
|
|
|
/* Action buttons */
|
|
const n = activeCount();
|
|
html += `
|
|
<div class="actions">
|
|
<button class="btn btn-success" id="btn-lgtm" onclick="submitFeedback(true)">${S.btn_export}</button>
|
|
<button class="btn btn-primary" id="btn-submit" onclick="submitFeedback(false)" ${n === 0 ? 'disabled' : ''}>
|
|
${S.btn_submit}<span class="btn-count">${n > 0 ? '(' + n + S.btn_count_suffix + ')' : ''}</span>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('adj').innerHTML = html;
|
|
}
|
|
|
|
function toggleAdj(id) {
|
|
state.active[id] = !state.active[id];
|
|
renderAdj();
|
|
}
|
|
|
|
function onChoice(id, opt) {
|
|
state.values[id] = state.values[id] === opt ? null : opt; // toggle
|
|
renderAdj();
|
|
}
|
|
|
|
function toggleRef() {
|
|
state.refOpen = !state.refOpen;
|
|
const arrow = document.querySelector('.ref-arrow');
|
|
const body = document.querySelector('.ref-body');
|
|
arrow.classList.toggle('open');
|
|
body.classList.toggle('open');
|
|
}
|
|
|
|
function collectFeedback(isLGTM) {
|
|
const lines = [];
|
|
const settings = { overall_satisfied: isLGTM };
|
|
|
|
if (isLGTM) {
|
|
return { feedback: S.feedback_satisfied, settings };
|
|
}
|
|
|
|
ADJUSTMENTS.forEach(a => {
|
|
if (!state.active[a.id]) return;
|
|
if (a.hidden && a.hidden()) return;
|
|
|
|
if (a.type === 'choice') {
|
|
const chosen = state.values[a.id];
|
|
settings[a.id] = chosen;
|
|
lines.push(`- **${a.label}**:${chosen || S.feedback_checked_no_choice}`);
|
|
} else if (a.type === 'text') {
|
|
const text = (state.values[a.id] || '').trim();
|
|
settings[a.id] = text;
|
|
lines.push(`- **${a.label}**:${text || S.feedback_checked_no_detail}`);
|
|
} else if (a.type === 'confirm') {
|
|
settings[a.id] = true;
|
|
lines.push(`- **${a.label}**`);
|
|
}
|
|
});
|
|
|
|
return {
|
|
feedback: lines.length > 0 ? lines.join('\n') : S.feedback_needs_adjustment,
|
|
settings,
|
|
};
|
|
}
|
|
|
|
async function submitFeedback(isLGTM) {
|
|
const btnLgtm = document.getElementById('btn-lgtm');
|
|
const btnSubmit = document.getElementById('btn-submit');
|
|
btnLgtm.disabled = true;
|
|
btnSubmit.disabled = true;
|
|
|
|
const { feedback, settings } = collectFeedback(isLGTM);
|
|
|
|
try {
|
|
const res = await fetch('/feedback', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ feedback, settings })
|
|
});
|
|
const result = await res.json();
|
|
if (result.ok) {
|
|
document.getElementById('hero').innerHTML = `
|
|
<div class="success-msg">
|
|
<h2>${isLGTM ? S.success_export_title : S.success_adjust_title}</h2>
|
|
<p>${isLGTM ? S.success_export_desc : S.success_adjust_desc}</p>
|
|
</div>
|
|
`;
|
|
document.getElementById('adj').innerHTML = '';
|
|
}
|
|
} catch (e) {
|
|
btnLgtm.disabled = false;
|
|
btnSubmit.disabled = false;
|
|
alert(S.submit_error);
|
|
}
|
|
}
|
|
|
|
render();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|