bookworm-smart-assistant/scripts/gen_git_ui.py

162 lines
9.8 KiB
Python
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.

#!/usr/bin/env python3
# Generates the git history UI JavaScript and appends to app.js
# Using chr() for quote characters to avoid shell/heredoc quoting issues
sq = chr(39) # single quote
dq = chr(34) # double quote
nl = chr(10) # newline
bs = chr(92) # backslash
lines = [
'',
'// --- Git 版本历史 UI ---',
'',
'/** 打开 Git 版本历史面板 */',
'async function showGitHistory() {',
' if (!_currentProjectId) return;',
' const overlay = document.createElement(' + sq + 'div' + sq + ');',
' overlay.className = ' + sq + 'modal-overlay' + sq + ';',
' overlay.id = ' + sq + 'gitHistoryOverlay' + sq + ';',
' overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };',
' overlay.innerHTML = `',
' <div class="modal-content" style="max-width:600px;max-height:90vh;overflow-y:auto">',
' <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">',
' <h3 style="margin:0;font-size:16px;color:var(--text-1)">版本历史</h3>',
' <button class="btn btn-ghost" style="padding:2px 8px;font-size:18px"',
' onclick="document.getElementById(' + sq + 'gitHistoryOverlay' + sq + ').remove()">&#215;</button>',
' </div>',
' <div id="gitLogList" style="display:flex;flex-direction:column;gap:8px">',
' <p style="color:var(--text-3);font-size:13px;text-align:center;padding:20px">加载中...</p>',
' </div>',
' <div id="gitDiffArea" style="display:none;margin-top:14px;background:var(--bg-2);border-radius:8px;padding:14px">',
' <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">',
' <span id="gitDiffTitle" style="font-size:12px;font-weight:600;color:var(--text-2);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>',
' <button class="btn btn-danger" id="gitRevertBtn" style="font-size:11px;padding:4px 10px;margin-left:10px;flex-shrink:0"',
' onclick="confirmRevert()">回滚到此版本</button>',
' </div>',
' <pre id="gitDiffContent" style="font-family:monospace;font-size:11px;line-height:1.6;',
' white-space:pre-wrap;word-break:break-all;max-height:360px;overflow-y:auto;',
' background:var(--bg-3);padding:10px;border-radius:6px;margin:0;color:var(--text-1)"></pre>',
' </div>',
' </div>`;',
' document.body.appendChild(overlay);',
' await _loadGitLog();',
'}',
'',
'// 当前选中的 commit hash用于回滚',
'let _selectedCommitHash = null;',
'',
'/** 加载提交历史列表 */',
'async function _loadGitLog() {',
' const listEl = document.getElementById(' + sq + 'gitLogList' + sq + ');',
' if (!listEl) return;',
' const { status, data } = await api(' + sq + 'GET' + sq + ', ' + sq + '/v1/projects/git/log?id=' + sq + ' + _currentProjectId + ' + sq + '&limit=30' + sq + ');',
' if (status !== 200 || !data.commits) {',
' listEl.innerHTML = ' + sq + '<p style="color:var(--error);font-size:13px;text-align:center;padding:20px">加载失败</p>' + sq + ';',
' return;',
' }',
' const commits = data.commits;',
' if (commits.length === 0) {',
' listEl.innerHTML = ' + sq + '<p style="color:var(--text-3);font-size:13px;text-align:center;padding:20px">暂无提交记录</p>' + sq + ';',
' return;',
' }',
' listEl.innerHTML = commits.map((c, idx) => `',
' <div class="git-commit-item" data-hash="${c.hash}"',
' onclick="_showCommitDiff(this.dataset.hash, this.dataset.msg)" data-msg="${_escHtml(c.message)}"',
' style="display:flex;align-items:flex-start;gap:12px;padding:10px 12px;background:var(--bg-2);',
' border-radius:8px;cursor:pointer;transition:background .15s;border:1px solid transparent"',
' onmouseover="this.style.borderColor=\'var(--border-d)\'" onmouseout="this.style.borderColor=\'transparent\'">',
' <div style="flex-shrink:0;width:8px;height:8px;border-radius:50%;background:var(--primary);margin-top:5px"></div>',
' <div style="flex:1;min-width:0">',
' <div style="font-size:13px;color:var(--text-1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${_escHtml(c.message)}</div>',
' <div style="font-size:11px;color:var(--text-3);margin-top:2px">',
' <code style="background:var(--bg-3);padding:1px 5px;border-radius:3px;font-size:10px">${c.hash}</code>',
' &nbsp;${_fmtDate(c.date)}&nbsp;&#183;&nbsp;${_escHtml(c.author)}',
' </div>',
' </div>',
' ${idx === 0 ? ' + sq + '<span style="font-size:10px;color:var(--primary);background:rgba(99,102,241,.12);padding:1px 6px;border-radius:10px;flex-shrink:0;margin-top:2px">最新</span>' + sq + ' : ' + sq + sq + '}',
' </div>`).join(' + sq + sq + ');',
'}',
'',
'/** 展示某次提交的 diff */',
'async function _showCommitDiff(hash, message) {',
' _selectedCommitHash = hash;',
' const diffArea = document.getElementById(' + sq + 'gitDiffArea' + sq + ');',
' const diffContent = document.getElementById(' + sq + 'gitDiffContent' + sq + ');',
' const diffTitle = document.getElementById(' + sq + 'gitDiffTitle' + sq + ');',
' if (!diffArea) return;',
' diffTitle.textContent = hash + ' + sq + ' ' + sq + ' + (message || ' + sq + sq + ');',
' diffContent.textContent = ' + sq + '加载中...' + sq + ';',
' diffArea.style.display = ' + sq + 'block' + sq + ';',
' // 高亮选中行',
' document.querySelectorAll(' + sq + '.git-commit-item' + sq + ').forEach(el => {',
' el.style.background = el.dataset.hash === hash ? ' + sq + 'var(--bg-3)' + sq + ' : ' + sq + 'var(--bg-2)' + sq + ';',
' });',
' const { status, data } = await api(' + sq + 'GET' + sq + ', ' + sq + '/v1/projects/git/diff?id=' + sq + ' + _currentProjectId + ' + sq + '&hash=' + sq + ' + encodeURIComponent(hash));',
' if (status !== 200) { diffContent.textContent = ' + sq + '获取 diff 失败' + sq + '; return; }',
' const rawText = (data.stat || ' + sq + sq + ') + ' + sq + bs + 'n' + bs + 'n' + sq + ' + (data.diff || ' + sq + '(无变更)' + sq + ');',
' // 简单着色: + / -',
' diffContent.innerHTML = rawText',
' .split(' + sq + bs + 'n' + sq + ')',
' .map(ln => {',
' const safe = _escHtml(ln);',
' if (ln.startsWith(' + sq + '+++' + sq + ') || ln.startsWith(' + sq + '---' + sq + ')) return ' + sq + '<span style="color:var(--text-2)">' + sq + ' + safe + ' + sq + '</span>' + sq + ';',
' if (ln.startsWith(' + sq + '+' + sq + ')) return ' + sq + '<span style="color:#4ade80">' + sq + ' + safe + ' + sq + '</span>' + sq + ';',
' if (ln.startsWith(' + sq + '-' + sq + ')) return ' + sq + '<span style="color:#f87171">' + sq + ' + safe + ' + sq + '</span>' + sq + ';',
' return safe;',
' })',
' .join(' + sq + bs + 'n' + sq + ');',
'}',
'',
'/** 确认并执行回滚 */',
'async function confirmRevert() {',
' if (!_selectedCommitHash || !_currentProjectId) return;',
' const h = _selectedCommitHash;',
' if (!confirm(' + sq + '确定回滚到版本 ' + sq + ' + h + ' + sq + ' 吗?当前未保存的变更将被覆盖。此操作产生新提交,不破坏历史记录。' + sq + ')) return;',
' const btn = document.getElementById(' + sq + 'gitRevertBtn' + sq + ');',
' if (btn) { btn.disabled = true; btn.textContent = ' + sq + '回滚中...' + sq + '; }',
' const { status, data } = await api(' + sq + 'POST' + sq + ', ' + sq + '/v1/projects/git/revert' + sq + ', { projectId: _currentProjectId, hash: h });',
' if (btn) { btn.disabled = false; btn.textContent = ' + sq + '回滚到此版本' + sq + '; }',
' if (status === 200) {',
' toast(' + sq + '已回滚到版本 ' + sq + ' + h, ' + sq + 'success' + sq + ');',
' document.getElementById(' + sq + 'gitHistoryOverlay' + sq + ')?.remove();',
' if (typeof openProject === ' + sq + 'function' + sq + ') openProject(_currentProjectId);',
' } else {',
' toast((data && data.error) || ' + sq + '回滚失败' + sq + ', ' + sq + 'error' + sq + ');',
' }',
'}',
'',
'/** HTML 转义辅助 */',
'function _escHtml(s) {',
' return String(s || ' + sq + sq + ')',
' .replace(/&/g, ' + sq + '&amp;' + sq + ')',
' .replace(/</g, ' + sq + '&lt;' + sq + ')',
' .replace(/>/g, ' + sq + '&gt;' + sq + ')',
' .replace(/"/g, ' + sq + '&quot;' + sq + ');',
'}',
'',
'/** 格式化 ISO 日期为简短可读格式 */',
'function _fmtDate(iso) {',
' try {',
' const d = new Date(iso);',
' const now = new Date();',
' const diff = (now - d) / 1000;',
' if (diff < 60) return ' + sq + '刚刚' + sq + ';',
' if (diff < 3600) return Math.floor(diff / 60) + ' + sq + ' 分钟前' + sq + ';',
' if (diff < 86400) return Math.floor(diff / 3600) + ' + sq + ' 小时前' + sq + ';',
' if (diff < 2592000) return Math.floor(diff / 86400) + ' + sq + ' 天前' + sq + ';',
' return d.toLocaleDateString(' + sq + 'zh-CN' + sq + ', { year: ' + sq + 'numeric' + sq + ', month: ' + sq + '2-digit' + sq + ', day: ' + sq + '2-digit' + sq + ' });',
' } catch(e) { return iso || ' + sq + sq + '; }',
'}',
'',
]
content = nl.join(lines) + nl
import tempfile, os
out_path = os.path.join(tempfile.gettempdir(), 'git_ui_append.js')
with open(out_path, 'w', encoding='utf-8') as f:
f.write(content)
print('Generated %s (%d bytes, %d lines)' % (out_path, len(content), content.count(nl)))