Files
Popiai-skill/audio-creation/asmr-ambient/html/preview-mix_feedback.html
T

653 lines
24 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="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">&#9654;</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>