Popiai-skill仓库
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.
 
 
 
 

251 lines
9.1 KiB

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>原声蓝图审阅</title>
<style>
:root {
--bg: #0d1117; --surface: #161b22; --border: #30363d;
--text: #e6edf3; --text-secondary: #8b949e; --accent: #58a6ff;
--warm: #f0883e; --cool: #58a6ff; --tense: #f85149;
--romantic: #d2a8ff; --comedy: #3fb950; --epic: #bc8cff;
--sad: #79c0ff; --action: #ff7b72;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 24px; }
h1 { font-size: 24px; margin-bottom: 8px; }
.subtitle { color: var(--text-secondary); margin-bottom: 32px; }
.section { margin-bottom: 40px; }
.section-title {
font-size: 18px; font-weight: 600; margin-bottom: 16px;
padding-bottom: 8px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 8px;
}
.section-badge {
font-size: 12px; padding: 2px 8px; border-radius: 12px;
background: var(--accent); color: var(--bg); font-weight: 600;
}
.track-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 20px; margin-bottom: 12px;
position: relative; transition: border-color 0.2s;
}
.track-card:hover { border-color: var(--accent); }
.track-name { font-size: 16px; font-weight: 600; margin-bottom: 12px; }
.tag-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.tag {
font-size: 11px; padding: 3px 10px; border-radius: 10px;
background: rgba(88, 166, 255, 0.15); color: var(--accent);
}
.tag.instrument { background: rgba(63, 185, 80, 0.15); color: var(--comedy); }
.tag.vocal { background: rgba(210, 168, 255, 0.15); color: var(--romantic); }
.field { margin-bottom: 8px; font-size: 14px; line-height: 1.6; }
.field-label { color: var(--text-secondary); font-weight: 500; }
.field-value { color: var(--text); }
.rationale {
margin-top: 12px; padding: 10px 14px; border-radius: 8px;
background: rgba(88, 166, 255, 0.06); border-left: 3px solid var(--accent);
font-size: 13px; color: var(--text-secondary); line-height: 1.5;
}
.cite-btn {
position: absolute; top: 12px; right: 12px;
background: var(--border); border: none; color: var(--text-secondary);
width: 28px; height: 28px; border-radius: 6px; cursor: pointer;
font-size: 14px; display: flex; align-items: center; justify-content: center;
}
.cite-btn:hover { background: var(--accent); color: var(--bg); }
.emotion-indicator {
display: inline-block; font-size: 12px; padding: 2px 8px;
border-radius: 10px; margin-left: 8px;
}
.emotion-indicator.tense { background: rgba(248,81,73,0.2); color: var(--tense); }
.emotion-indicator.romantic { background: rgba(210,168,255,0.2); color: var(--romantic); }
.emotion-indicator.comedy { background: rgba(63,185,80,0.2); color: var(--comedy); }
.emotion-indicator.epic { background: rgba(188,140,255,0.2); color: var(--epic); }
.emotion-indicator.sad { background: rgba(121,192,255,0.2); color: var(--sad); }
.emotion-indicator.action { background: rgba(255,123,114,0.2); color: var(--action); }
.emotion-indicator.warm { background: rgba(240,136,62,0.2); color: var(--warm); }
/* Feedback bar */
.feedback-bar {
position: fixed; bottom: 0; left: 0; right: 0;
background: var(--surface); border-top: 1px solid var(--border);
padding: 12px 24px; display: flex; gap: 12px; align-items: center;
z-index: 100;
}
.feedback-bar textarea {
flex: 1; background: var(--bg); border: 1px solid var(--border);
color: var(--text); border-radius: 8px; padding: 10px 14px;
font-size: 14px; resize: none; height: 44px; line-height: 24px;
font-family: inherit;
}
.feedback-bar textarea:focus { outline: none; border-color: var(--accent); }
.feedback-bar button {
background: var(--accent); color: var(--bg); border: none;
padding: 10px 20px; border-radius: 8px; font-weight: 600;
cursor: pointer; font-size: 14px; white-space: nowrap;
}
.feedback-bar button:hover { opacity: 0.9; }
.feedback-bar button.lgtm {
background: var(--comedy); color: var(--bg);
}
body { padding-bottom: 80px; }
</style>
</head>
<body>
<script>
const PREVIEW_DATA = null; /* __PREVIEW_DATA__ */
</script>
<div id="app"></div>
<div class="feedback-bar">
<textarea id="feedbackText" placeholder="输入修改建议… 点击卡片右上角 @ 可引用特定曲目"></textarea>
<button class="lgtm" onclick="submitFeedback('LGTM')">LGTM</button>
<button onclick="submitFeedback()">提交建议</button>
</div>
<script>
const data = JSON.parse(PREVIEW_DATA);
const emotionMap = {
'紧张': 'tense', '权谋': 'tense', '悬疑': 'tense',
'浪漫': 'romantic', '心动': 'romantic', '深情': 'romantic',
'喜剧': 'comedy', '搞笑': 'comedy', '欢快': 'comedy',
'史诗': 'epic', '宏大': 'epic', '壮烈': 'epic',
'悲伤': 'sad', '不舍': 'sad', '忧伤': 'sad', '绝望': 'sad',
'打斗': 'action', '激烈': 'action', '爆发': 'action',
'温暖': 'warm', '温情': 'warm', '重逢': 'warm',
'震惊': 'tense', '背叛': 'tense', '牺牲': 'epic',
'惊艳': 'romantic', '抉择': 'tense', '坚定': 'warm',
'遗憾': 'sad', '释然': 'warm',
};
function getEmotionClass(text) {
if (!text) return 'warm';
for (const [keyword, cls] of Object.entries(emotionMap)) {
if (text.includes(keyword)) return cls;
}
return 'warm';
}
function renderTags(styleTags, instruments, vocalStyle) {
let html = '<div class="tag-row">';
if (styleTags) {
styleTags.split(',').forEach(t => {
html += `<span class="tag">${t.trim()}</span>`;
});
}
if (instruments) {
instruments.split('、').slice(0, 4).forEach(t => {
html += `<span class="tag instrument">${t.trim()}</span>`;
});
}
if (vocalStyle) {
html += `<span class="tag vocal">${vocalStyle.split(',')[0].trim()}</span>`;
}
html += '</div>';
return html;
}
function renderTrackCard(track, category) {
const citeRef = `@${category}/${track.name}`;
let html = `<div class="track-card">`;
html += `<button class="cite-btn" title="引用" onclick="cite('${citeRef}')">@</button>`;
html += `<div class="track-name">${track.name}`;
if (track.emotion_type) {
const cls = getEmotionClass(track.emotion_type);
html += `<span class="emotion-indicator ${cls}">${track.emotion_type}</span>`;
}
html += `</div>`;
html += renderTags(track.style_tags, track.instruments, track.vocal_style);
const fields = [
['情感基调', track.emotion],
['人声风格', track.vocal_style],
['歌词方向', track.lyrics_direction],
['节奏', track.tempo],
['Hook 策略', track.hook_strategy],
['场景描述', track.scene_description],
['时长预估', track.duration_estimate],
];
fields.forEach(([label, value]) => {
if (value) {
html += `<div class="field"><span class="field-label">${label}:</span><span class="field-value">${value}</span></div>`;
}
});
if (track.rationale) {
html += `<div class="rationale">${track.rationale}</div>`;
}
html += '</div>';
return html;
}
function render() {
let html = `<h1>${data.title || '原声音乐蓝图'}</h1>`;
html += `<div class="subtitle">审阅所有计划曲目的音乐方向,确认后进入生成阶段</div>`;
if (data.character_themes && data.character_themes.length) {
html += `<div class="section"><div class="section-title">角色主题曲 <span class="section-badge">${data.character_themes.length} 首</span></div>`;
data.character_themes.forEach(t => { html += renderTrackCard(t, '角色曲'); });
html += '</div>';
}
if (data.opening) {
html += `<div class="section"><div class="section-title">片头曲 (OP)</div>`;
html += renderTrackCard(data.opening, 'OP');
html += '</div>';
}
if (data.ending) {
html += `<div class="section"><div class="section-title">片尾曲 (ED)</div>`;
html += renderTrackCard(data.ending, 'ED');
html += '</div>';
}
if (data.bgm && data.bgm.length) {
html += `<div class="section"><div class="section-title">场景配乐 <span class="section-badge">${data.bgm.length} 首</span></div>`;
data.bgm.forEach(t => { html += renderTrackCard(t, 'BGM'); });
html += '</div>';
}
document.getElementById('app').innerHTML = html;
}
function cite(ref) {
const ta = document.getElementById('feedbackText');
ta.value += (ta.value ? ' ' : '') + ref;
ta.focus();
}
async function submitFeedback(override) {
const text = override || document.getElementById('feedbackText').value.trim();
if (!text) return;
try {
const res = await fetch('/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ feedback: text }),
});
const data = await res.json();
if (data.ok) {
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-size:20px;color:#3fb950;">已提交,窗口即将关闭</div>';
}
} catch (e) {
alert('提交失败: ' + e.message);
}
}
render();
</script>
</body>
</html>