RepoReaper / frontend /index.html
GitHub Actions Bot
deploy: auto-inject hf config & sync
4e98fb0
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub RAG Agent</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<style>
:root {
/* === 配色微调 (清新风格) === */
--primary-color: #2563eb;
--bg-color: #f8fafc;
--panel-bg: #ffffff;
--border-color: #e2e8f0;
--text-primary: #334155;
--text-secondary: #64748b;
/* === 聊天气泡新配色 === */
--chat-user-bg: #e0f2fe; /* 用户:清新的浅天蓝 */
--chat-user-text: #0c4a6e; /* 用户:深蓝文字 */
--chat-ai-bg: #ffffff; /* AI:纯白底 */
/* === 代码新配色 === */
--code-inline-color: #0284c7; /* 行内代码:清新的湖蓝色 */
--code-inline-bg: #f0f9ff; /* 行内代码:极淡的蓝背景 */
--code-block-bg: #f1f5f9; /* 代码块:清爽的灰白背景 */
--code-block-border: #cbd5e1;
/* === 字体设置 === */
--base-font-size: 16px;
}
body, html {
margin: 0; padding: 0; height: 100%; width: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
font-size: var(--base-font-size);
overflow: hidden;
}
/* 顶部导航 */
.header {
height: 60px;
background: var(--panel-bg);
border-bottom: 1px solid var(--border-color);
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; flex-shrink: 0;
}
.header h1 { margin: 0; font-size: 1.25rem; font-weight: 600; display: flex; align-items: center; gap: 12px; color: var(--text-primary); }
.badge { background: var(--primary-color); color: white; font-size: 0.8rem; padding: 4px 10px; border-radius: 20px; font-weight: 500; }
/* 主布局 */
.main-container { display: flex; height: calc(100vh - 60px); width: 100%; }
.left-panel { width: 50%; min-width: 320px; background: var(--panel-bg); display: flex; flex-direction: column; border-right: 1px solid var(--border-color); }
.right-panel { flex: 1; min-width: 320px; background: var(--bg-color); display: flex; flex-direction: column; }
/* 拖拽条 */
.resizer { width: 10px; background: #f1f5f9; cursor: col-resize; display: flex; align-items: center; justify-content: center; flex-shrink: 0; border-left: 1px solid var(--border-color); border-right: 1px solid var(--border-color); transition: background 0.2s; }
.resizer:hover, .resizer.active { background: #e2e8f0; }
.resizer::after { content: "⋮"; color: #94a3b8; font-size: 14px; pointer-events: none; }
.panel-content { padding: 24px; overflow-y: auto; flex: 1; }
/* 输入栏 */
.input-bar {
padding: 20px;
border-bottom: 1px solid var(--border-color);
background: #ffffff;
display: flex;
gap: 12px;
align-items: center; /* 垂直居中对齐 */
}
.chat-input-bar { padding: 20px; border-top: 1px solid var(--border-color); background: #ffffff; display: flex; gap: 12px; }
input[type="text"] {
flex: 1; padding: 12px 16px; border: 1px solid var(--border-color); border-radius: 8px; outline: none; font-size: 16px; transition: all 0.2s;
background: #f8fafc;
}
input[type="text"]:focus { border-color: var(--primary-color); background: #fff; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); }
button {
background: var(--primary-color); color: white; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-weight: 500; font-size: 16px; transition: background 0.2s; white-space: nowrap;
}
button:hover { background: #1d4ed8; }
button:disabled { background: #93c5fd; cursor: not-allowed; }
/* === 中止按钮样式 === */
button.btn-stop {
background: #dc2626;
}
button.btn-stop:hover {
background: #b91c1c;
}
/* === 智能按钮状态样式 === */
button.btn-analyze {
background: linear-gradient(135deg, #2563eb, #1d4ed8);
}
button.btn-analyze:hover {
background: linear-gradient(135deg, #1d4ed8, #1e40af);
}
button.btn-generate {
background: linear-gradient(135deg, #10b981, #059669);
}
button.btn-generate:hover {
background: linear-gradient(135deg, #059669, #047857);
}
button.btn-reanalyze {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
button.btn-reanalyze:hover {
background: linear-gradient(135deg, #d97706, #b45309);
}
button.btn-checking {
background: #94a3b8;
cursor: wait;
}
/* === [新增] 美观的语言切换开关样式 === */
.lang-toggle {
display: flex;
background: #f1f5f9;
padding: 4px;
border-radius: 10px;
border: 1px solid var(--border-color);
flex-shrink: 0;
}
.lang-toggle input[type="radio"] {
display: none;
}
.lang-toggle label {
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
border-radius: 8px;
color: var(--text-secondary);
font-weight: 500;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
}
.lang-toggle label:hover {
color: var(--primary-color);
}
/* 选中状态 */
.lang-toggle input[type="radio"]:checked + label {
background: #ffffff;
color: var(--primary-color);
box-shadow: 0 2px 4px rgba(0,0,0,0.06);
transform: scale(1.02);
}
/* ==================================== */
/* 日志区域 */
.logs-area {
background: #f1f5f9; color: #475569; padding: 16px; border-radius: 8px; height: 140px; overflow-y: auto; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 15px; margin-bottom: 12px; line-height: 1.6; flex-shrink: 0; border: 1px solid var(--border-color);
}
/* === 智能提示区域样式 === */
.smart-hint {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 16px;
margin-bottom: 12px;
border-radius: 8px;
font-size: 14px;
line-height: 1.5;
animation: slideIn 0.3s ease-out;
}
.smart-hint.hint-info {
background: linear-gradient(135deg, #e0f2fe, #f0f9ff);
border: 1px solid #7dd3fc;
color: #0369a1;
}
.smart-hint.hint-success {
background: linear-gradient(135deg, #d1fae5, #ecfdf5);
border: 1px solid #6ee7b7;
color: #047857;
}
.smart-hint.hint-warning {
background: linear-gradient(135deg, #fef3c7, #fffbeb);
border: 1px solid #fcd34d;
color: #92400e;
}
.smart-hint .hint-icon {
font-size: 18px;
flex-shrink: 0;
}
.smart-hint .hint-text {
flex: 1;
}
.smart-hint .hint-text strong {
font-weight: 600;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 报告下载工具栏 */
.report-toolbar {
display: flex;
gap: 12px;
padding: 10px 16px;
background: #f8fafc;
border-bottom: 1px solid var(--border-color);
border-radius: 8px 8px 0 0;
}
.download-btn {
padding: 6px 14px;
font-size: 13px;
background: #e2e8f0;
color: #334155;
border: 1px solid #cbd5e1;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.download-btn:hover {
background: #cbd5e1;
border-color: #94a3b8;
}
/* === Markdown 内容样式增强 (表格与结构) === */
.markdown-body { font-size: 16px; line-height: 1.75; color: var(--text-primary); }
.markdown-body h1, .markdown-body h2, .markdown-body h3 { margin-top: 1.5em; color: #1e293b; font-weight: 600; }
/* 列表样式 */
.markdown-body ul { padding-left: 1.5em; }
.markdown-body li { margin: 0.5em 0; }
/* 表格样式 (Table) */
.markdown-body table {
width: 100%;
border-collapse: collapse;
margin: 1.5em 0;
display: block; /* 允许横向滚动 */
overflow-x: auto;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.markdown-body th, .markdown-body td {
padding: 12px 16px;
border: 1px solid var(--border-color);
text-align: left;
}
.markdown-body th {
background-color: #f1f5f9;
font-weight: 600;
color: var(--text-primary);
}
.markdown-body tr:nth-child(2n) {
background-color: #f8fafc;
}
.markdown-body tr:hover {
background-color: #f0f9ff;
}
/* 代码样式 */
.markdown-body code {
background: var(--code-inline-bg);
color: var(--code-inline-color);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.9em;
border: 1px solid #bae6fd;
}
.markdown-body pre {
background: var(--code-block-bg);
padding: 20px;
border-radius: 8px;
border: 1px solid var(--code-block-border);
border-left: 4px solid var(--primary-color);
overflow-x: auto;
margin: 1.5em 0;
}
.markdown-body pre code { background: none; color: inherit; padding: 0; border: none; font-size: 14px; }
/* Mermaid 图表居中与交互 */
.mermaid {
display: flex;
justify-content: center;
margin: 20px 0;
background: var(--bg-color);
padding: 10px;
border-radius: 8px;
cursor: zoom-in; /* 提示可点击放大 */
transition: transform 0.2s;
overflow-x: auto; /* 允许小幅横向滚动 */
}
.mermaid:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
/* Mermaid 加载中样式 */
.mermaid-pending {
display: flex;
justify-content: center;
align-items: center;
margin: 20px 0;
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
padding: 40px;
border-radius: 8px;
border: 1px dashed #7dd3fc;
cursor: default;
}
.mermaid-loading {
color: #0369a1;
font-size: 14px;
animation: mermaidPulse 1.5s ease-in-out infinite;
}
@keyframes mermaidPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Mermaid 错误样式 */
.mermaid-error {
margin: 20px 0;
padding: 16px;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
}
.mermaid-error-header {
color: #dc2626;
font-weight: 600;
margin-bottom: 12px;
}
.mermaid-error details { margin: 8px 0; }
.mermaid-error summary {
cursor: pointer;
color: #4b5563;
font-size: 14px;
}
.mermaid-source {
background: #1f2937;
color: #e5e7eb;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin-top: 8px;
font-size: 13px;
}
.mermaid-error-tip {
color: #6b7280;
font-size: 13px;
margin-top: 8px;
font-style: italic;
}
/* 聊天气泡样式 */
.chat-container { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 24px; }
.msg {
max-width: 85%;
padding: 14px 20px;
border-radius: 16px;
font-size: 16px; position: relative; word-wrap: break-word;
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
line-height: 1.6;
}
.msg.user {
align-self: flex-end;
background: var(--chat-user-bg);
color: var(--chat-user-text);
border: 1px solid #bae6fd;
border-bottom-right-radius: 4px;
}
.msg.ai {
align-self: flex-start;
background: var(--chat-ai-bg);
border: 1px solid var(--border-color);
border-bottom-left-radius: 4px;
}
.source-tag { font-size: 13px; color: var(--text-secondary); margin-top: 12px; padding-top: 12px; border-top: 1px solid #f1f5f9; }
/* === 模态框 (Lightbox) 样式 === */
.modal {
display: none; /* 默认隐藏 */
position: fixed;
z-index: 1000;
left: 0; top: 0; width: 100%; height: 100%;
background-color: rgba(0,0,0,0.8); /* 半透明黑背景 */
backdrop-filter: blur(5px);
overflow: hidden;
justify-content: center;
align-items: center;
}
.modal-content-wrapper {
position: relative;
width: 95%; /* 稍微加宽一点 */
height: 95%;
background: white;
border-radius: 8px;
/* === 核心修改 === */
overflow: auto; /* 开启滚动条 */
display: block; /* 改为 block,避免 flex 居中导致的左侧裁切问题 */
text-align: center; /* 让小图居中,大图自然向右延伸 */
padding: 20px; /* 减小一点内边距,给图更多空间 */
}
/* 确保内部的 div (承载 svg 的容器) 能够自适应 */
#modalContent {
display: inline-block; /* 配合父级的 text-align: center 实现居中 */
min-width: 100%; /* 至少占满容器 */
text-align: left; /* 内部内容恢复左对齐 */
}
.close-btn {
position: absolute; top: 20px; right: 30px;
color: #f1f1f1; font-size: 40px; font-weight: bold;
cursor: pointer; z-index: 1001;
}
.close-btn:hover { color: #bbb; }
@media (max-width: 768px) {
.main-container { flex-direction: column; overflow-y: auto; }
.left-panel, .right-panel { width: 100% !important; height: auto; min-height: 50vh; }
.resizer { display: none; }
.panel-content { min-height: 300px; }
body { overflow: auto; }
.header { padding: 0 16px; }
}
</style>
</head>
<body>
<div class="header">
<h1><span>🧠</span> GitHub RAG Agent <span class="badge">v5.4 Smart Hints</span></h1>
<div style="font-size: 14px; color: var(--text-secondary);">Session: <span id="sessionIdDisplay">...</span></div>
</div>
<div class="main-container" id="mainContainer">
<div class="left-panel" id="leftPanel">
<div class="input-bar">
<form onsubmit="return false;" style="display: contents;">
<input type="text" id="repoUrl" value=""
placeholder="Enter GitHub Repo URL (e.g., https://github.com/owner/repo)"
onkeypress="handleRepoKeyPress(event)"
oninput="onUrlChange()">
<div class="lang-toggle">
<input type="radio" id="lang-en" name="lang" value="en" checked onchange="onLanguageChange()">
<label for="lang-en" title="Generate report in English">ENG</label>
<input type="radio" id="lang-zh" name="lang" value="zh" onchange="onLanguageChange()">
<label for="lang-zh" title="使用中文生成报告">中文</label>
</div>
<button type="button" onclick="handleAnalyzeClick()" id="btn-analyze" class="btn-analyze">🔍 Analyze</button>
</form>
</div>
<div class="panel-content">
<div class="logs-area" id="logs">
<div>[System] Ready to enter...</div>
</div>
<!-- 智能提示区域 -->
<div class="smart-hint" id="smartHint" style="display: none;">
<span class="hint-icon">💡</span>
<span class="hint-text" id="hintText"></span>
</div>
<div class="report-toolbar" id="reportToolbar" style="display: none; padding: 8px 16px; border-bottom: 1px solid #334155; background: #1e293b;">
<button onclick="downloadMarkdown()" class="download-btn" title="Download as Markdown">
📄 Markdown
</button>
<button onclick="printReport()" class="download-btn" title="Print / Save as PDF">
🖨️ Print/PDF
</button>
</div>
<div class="markdown-body" id="report">
<div style="text-align: center; color: #94a3b8; margin-top: 80px; font-size: 18px;">
📊 The project architecture report will be generated here.
</div>
</div>
</div>
</div>
<div class="resizer" id="resizer"></div>
<div class="right-panel" id="rightPanel">
<div class="chat-container" id="chat-history">
<div class="msg ai">👋 Hi! Once the analysis is done, ask me anything about the code.</div>
</div>
<div class="chat-input-bar">
<input type="text" id="chatInput" placeholder="e.g., How does generate_unique_id work?" onkeypress="handleKeyPress(event)" disabled>
<button onclick="handleChatButton()" id="btn-chat" disabled>Send</button>
</div>
</div>
</div>
<div id="imgModal" class="modal" onclick="closeModal()">
<span class="close-btn" onclick="closeModal()">&times;</span>
<div class="modal-content-wrapper" onclick="event.stopPropagation()">
<div id="modalContent"></div>
</div>
</div>
<script>
// === Mermaid 初始化 ===
mermaid.initialize({
startOnLoad: false,
theme: 'neutral',
securityLevel: 'loose',
// 改进的配置以支持中文
flowchart: {
htmlLabels: true,
useMaxWidth: true
},
sequence: {
useMaxWidth: true
}
});
const API_BASE = "";
let fullReportMarkdown = "";
let currentSessionId = null; // 基于仓库 URL 的 Session ID
let currentRepoUrl = ""; // 当前仓库 URL(已分析的)
let cachedReports = {}; // 缓存已加载的报告 { "en": "...", "zh": "..." }
let lastCheckedUrl = ""; // 上次检查的 URL
let lastCheckResult = null; // 上次检查结果缓存
let urlCheckTimeout = null; // URL 变化防抖定时器
// === 按钮状态枚举 ===
const BTN_STATE = {
ANALYZE: 'analyze', // 蓝色:全新分析
GENERATE: 'generate', // 绿色:仅生成报告(复用索引)
REANALYZE: 'reanalyze', // 橙色:强制重新分析
CHECKING: 'checking', // 灰色:检查中
ANALYZING: 'analyzing' // 禁用:分析中
};
let currentBtnState = BTN_STATE.ANALYZE;
// === 获取当前语言 ===
function getCurrentLang() {
return document.querySelector('input[name="lang"]:checked').value;
}
// === 双语提示消息 ===
const HINTS = {
// 有报告可用
reportReady: {
en: '<strong>Report ready!</strong> Switch language to view another version, or click <strong>Reanalyze</strong> to regenerate.',
zh: '<strong>报告已加载!</strong> 可切换语言查看其他版本,或点击 <strong>Reanalyze</strong> 重新生成。'
},
// 可以快速生成
canGenerate: {
en: '<strong>Index found!</strong> Click <strong>Generate</strong> to quickly create a report (no re-indexing needed).',
zh: '<strong>已有索引!</strong> 点击 <strong>Generate</strong> 可快速生成报告(无需重新索引)。'
},
// 需要完整分析
needAnalyze: {
en: '<strong>New repository.</strong> Click <strong>Analyze</strong> to start code indexing and report generation.',
zh: '<strong>新仓库。</strong> 点击 <strong>Analyze</strong> 开始代码索引和报告生成。'
},
// 语言切换 - 有缓存
langSwitched: {
en: '<strong>Switched to English report</strong> (from cache).',
zh: '<strong>已切换到中文报告</strong>(来自缓存)。'
},
// 语言切换 - 需要生成
langNeedGenerate: {
en: '<strong>No English report yet.</strong> Click <strong>Generate EN</strong> to create one (fast, uses existing index).',
zh: '<strong>暂无中文报告。</strong> 点击 <strong>Generate 中文</strong> 快速生成(复用现有索引)。'
}
};
// === 显示智能提示 ===
function showHint(hintKey, type = 'info', langOverride = null) {
const hintDiv = document.getElementById('smartHint');
const hintText = document.getElementById('hintText');
const lang = langOverride || getCurrentLang();
if (!HINTS[hintKey]) {
hideHint();
return;
}
const message = HINTS[hintKey][lang] || HINTS[hintKey]['en'];
hintDiv.className = 'smart-hint hint-' + type;
hintText.innerHTML = message;
hintDiv.style.display = 'flex';
}
// === 隐藏提示 ===
function hideHint() {
document.getElementById('smartHint').style.display = 'none';
}
// === 设置按钮状态 ===
function setButtonState(state, customText = null) {
const btn = document.getElementById('btn-analyze');
const lang = getCurrentLang();
const langLabel = lang === 'zh' ? '中文' : 'EN';
// 移除所有状态类
btn.classList.remove('btn-analyze', 'btn-generate', 'btn-reanalyze', 'btn-checking');
btn.disabled = false;
currentBtnState = state;
switch (state) {
case BTN_STATE.ANALYZE:
btn.classList.add('btn-analyze');
btn.textContent = customText || '🔍 Analyze';
break;
case BTN_STATE.GENERATE:
btn.classList.add('btn-generate');
btn.textContent = customText || `🌐 Generate ${langLabel}`;
break;
case BTN_STATE.REANALYZE:
btn.classList.add('btn-reanalyze');
btn.textContent = customText || '🔄 Reanalyze';
break;
case BTN_STATE.CHECKING:
btn.classList.add('btn-checking');
btn.textContent = '⏳ Checking...';
btn.disabled = true;
break;
case BTN_STATE.ANALYZING:
btn.classList.add('btn-checking');
btn.textContent = '⏳ Analyzing...';
btn.disabled = true;
break;
}
}
// === URL 变化监听(防抖)===
function onUrlChange() {
clearTimeout(urlCheckTimeout);
hideHint(); // URL 变化时先隐藏提示
urlCheckTimeout = setTimeout(() => {
checkUrlAndUpdateState();
}, 500); // 500ms 防抖
}
// === 检查 URL 并更新按钮状态 ===
async function checkUrlAndUpdateState() {
const url = document.getElementById('repoUrl').value.trim();
const lang = document.querySelector('input[name="lang"]:checked').value;
if (!url) {
setButtonState(BTN_STATE.ANALYZE);
return;
}
// URL 变化,清空本地缓存
if (url !== currentRepoUrl) {
cachedReports = {};
}
// 如果 URL 没变且有缓存结果,直接使用
if (url === lastCheckedUrl && lastCheckResult) {
applyCheckResult(lastCheckResult, lang);
return;
}
setButtonState(BTN_STATE.CHECKING);
logAppend(`🔍 Checking repository status...`, "#64748b");
try {
const result = await checkRepoSession(url, lang);
lastCheckedUrl = url;
lastCheckResult = result;
currentSessionId = result.session_id;
updateSessionDisplay(currentSessionId);
applyCheckResult(result, lang);
} catch (e) {
console.error('Check failed:', e);
setButtonState(BTN_STATE.ANALYZE);
}
}
// === 应用检查结果更新 UI ===
function applyCheckResult(result, lang) {
const reportDiv = document.getElementById('report');
const toolbar = document.getElementById('reportToolbar');
if (result.exists && result.report) {
// 有该语言的报告 → 显示报告,按钮为 Reanalyze
fullReportMarkdown = result.report;
cachedReports[lang] = result.report;
reportDiv.innerHTML = marked.parse(fullReportMarkdown);
renderMermaidDiagrams(reportDiv);
showReportToolbar();
toggleChat(true);
setButtonState(BTN_STATE.REANALYZE);
showHint('reportReady', 'success');
logAppend(`✅ Found ${lang.toUpperCase()} report (cached)`, "#15803d");
} else if (result.has_index) {
// 有索引但没有该语言报告 → 按钮为 Generate
const availableLangs = result.available_languages || [];
reportDiv.innerHTML = `<div style="text-align: center; color: #94a3b8; margin-top: 80px; font-size: 16px;">
📚 Code index exists. Available reports: [${availableLangs.join(', ') || 'none'}]<br><br>
<span style="font-size: 14px;">Click <strong>Generate</strong> to create a ${lang.toUpperCase()} report.</span>
</div>`;
if (toolbar) toolbar.style.display = 'none';
toggleChat(false);
setButtonState(BTN_STATE.GENERATE);
showHint('canGenerate', 'info');
logAppend(`📚 Index found. Click Generate to create ${lang.toUpperCase()} report.`, "#0ea5e9");
} else {
// 全新仓库 → 按钮为 Analyze
reportDiv.innerHTML = `<div style="text-align: center; color: #94a3b8; margin-top: 80px; font-size: 18px;">
📊 The project architecture report will be generated here.
</div>`;
if (toolbar) toolbar.style.display = 'none';
toggleChat(false);
setButtonState(BTN_STATE.ANALYZE);
showHint('needAnalyze', 'warning');
logAppend(`🆕 New repository. Click Analyze to start.`, "#64748b");
}
}
// === 语言切换事件 ===
async function onLanguageChange() {
const newLang = document.querySelector('input[name="lang"]:checked').value;
const url = document.getElementById('repoUrl').value.trim();
if (!url) return;
const reportDiv = document.getElementById('report');
const toolbar = document.getElementById('reportToolbar');
// 1. 先检查本地缓存
if (cachedReports[newLang]) {
fullReportMarkdown = cachedReports[newLang];
reportDiv.innerHTML = marked.parse(fullReportMarkdown);
renderMermaidDiagrams(reportDiv);
showReportToolbar();
toggleChat(true);
setButtonState(BTN_STATE.REANALYZE);
showHint('langSwitched', 'success', newLang);
logAppend(`🔄 Switched to ${newLang.toUpperCase()} report (from cache)`, "#0ea5e9");
return;
}
// 2. 没有本地缓存,检查后端
setButtonState(BTN_STATE.CHECKING);
hideHint();
logAppend(`🔍 Checking ${newLang.toUpperCase()} report...`, "#64748b");
try {
const result = await checkRepoSession(url, newLang);
lastCheckedUrl = url;
lastCheckResult = result;
currentSessionId = result.session_id;
if (result.exists && result.report) {
// 后端有该语言报告
cachedReports[newLang] = result.report;
fullReportMarkdown = result.report;
reportDiv.innerHTML = marked.parse(fullReportMarkdown);
renderMermaidDiagrams(reportDiv);
showReportToolbar();
toggleChat(true);
setButtonState(BTN_STATE.REANALYZE);
showHint('langSwitched', 'success', newLang);
logAppend(`📦 Loaded ${newLang.toUpperCase()} report`, "#15803d");
} else if (result.has_index) {
// 有索引,无该语言报告
reportDiv.innerHTML = `<div style="text-align: center; color: #94a3b8; margin-top: 80px; font-size: 16px;">
📝 No ${newLang.toUpperCase()} report available yet.<br><br>
<span style="font-size: 14px;">Click <strong>Generate</strong> to create a ${newLang.toUpperCase()} report.</span>
</div>`;
if (toolbar) toolbar.style.display = 'none';
toggleChat(false);
setButtonState(BTN_STATE.GENERATE);
showHint('langNeedGenerate', 'info', newLang);
logAppend(`ℹ️ No ${newLang.toUpperCase()} report. Click Generate.`, "#f59e0b");
} else {
// 无索引
reportDiv.innerHTML = `<div style="text-align: center; color: #94a3b8; margin-top: 80px; font-size: 18px;">
📊 The project architecture report will be generated here.
</div>`;
if (toolbar) toolbar.style.display = 'none';
toggleChat(false);
setButtonState(BTN_STATE.ANALYZE);
showHint('needAnalyze', 'warning', newLang);
}
} catch (e) {
console.error('Language switch check failed:', e);
setButtonState(BTN_STATE.ANALYZE);
}
}
// === 基于仓库 URL 生成 Session ID ===
function generateRepoSessionId(repoUrl) {
// 标准化 URL
let normalized = repoUrl.trim().toLowerCase();
// 移除 .git 后缀
if (normalized.endsWith('.git')) {
normalized = normalized.slice(0, -4);
}
// 提取 owner/repo
const match = normalized.match(/github\.com[\/:]([^\/]+)\/([^\/\?#]+)/);
if (!match) return null;
const owner = match[1];
const repo = match[2];
// 生成简单 hash
const hashInput = `https://github.com/${owner}/${repo}`;
let hash = 0;
for (let i = 0; i < hashInput.length; i++) {
const char = hashInput.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
const shortHash = Math.abs(hash).toString(16).substring(0, 8);
return `repo_${shortHash}_${owner.substring(0, 10)}_${repo.substring(0, 15)}`;
}
// === 检查仓库是否已分析(包含语言参数)===
async function checkRepoSession(repoUrl, language = 'en') {
try {
const response = await fetch(`${API_BASE}/api/repo/check`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: repoUrl, language: language })
});
return await response.json();
} catch (e) {
console.error("Check repo session failed:", e);
return { exists: false, has_index: false, available_languages: [] };
}
}
// 更新 Session 显示
function updateSessionDisplay(sessionId) {
if (sessionId) {
document.getElementById('sessionIdDisplay').innerText = sessionId.substring(sessionId.length - 8);
}
}
// 拖拽逻辑
const resizer = document.getElementById('resizer');
const leftPanel = document.getElementById('leftPanel');
const container = document.getElementById('mainContainer');
let isResizing = false;
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
resizer.classList.add('active');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const newLeftWidth = (e.clientX / container.offsetWidth) * 100;
if (newLeftWidth > 20 && newLeftWidth < 80) leftPanel.style.width = `${newLeftWidth}%`;
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
resizer.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
});
// === 模态框逻辑 ===
function openModal(svgContent) {
const modal = document.getElementById('imgModal');
const content = document.getElementById('modalContent');
// 复制 SVG 内容
content.innerHTML = svgContent;
// 调整样式以适应全屏
const svg = content.querySelector('svg');
if (svg) {
// 1. 移除最大宽度限制,允许它撑开
svg.style.maxWidth = 'none';
svg.style.height = 'auto';
// 2. 核心修复:依据 viewBox 强制设置宽度
// 如果 SVG 有定义原始尺寸 (viewBox),就用原始尺寸作为 CSS width
// 这样可以确保大图(如流程图)以原始分辨率渲染,从而触发滚动条
if(svg.viewBox && svg.viewBox.baseVal) {
// 加上 20px padding 避免贴边
const naturalWidth = svg.viewBox.baseVal.width;
// 逻辑:如果原始宽度小于屏幕,就用 100% 撑满;如果原始宽度大于屏幕,就用原始宽度
// 这样既能保证小图不模糊,也能保证大图能看清且可滚动
svg.style.width = `max(100%, ${naturalWidth}px)`;
} else {
// 兜底:如果没有 viewBox,直接设为 100%
svg.style.width = '100%';
}
}
modal.style.display = "flex";
}
function closeModal() {
document.getElementById('imgModal').style.display = "none";
}
document.addEventListener('keydown', function(event) {
if (event.key === "Escape") closeModal();
});
// === Mermaid 中文预处理 ===
function sanitizeMermaidCode(code) {
let lines = code.split('\n');
return lines.map(line => {
// 跳过注释和空行
if (line.trim().startsWith('%%') || line.trim() === '') {
return line;
}
// 处理 graph/flowchart 节点定义: A[文本] -> A["文本"]
line = line.replace(/(\w+)\[([^\]"]+)\]/g, (match, id, text) => {
if (/[\u4e00-\u9fa5]/.test(text) || /[()()::,,]/.test(text)) {
return `${id}["${text}"]`;
}
return match;
});
// 处理圆角节点 A(文本)
line = line.replace(/(\w+)\(([^)"]+)\)/g, (match, id, text) => {
if (/[\u4e00-\u9fa5]/.test(text) || /[[\]{}::,,]/.test(text)) {
return `${id}("${text}")`;
}
return match;
});
// 处理菱形节点 A{文本}
line = line.replace(/(\w+)\{([^}"]+)\}/g, (match, id, text) => {
if (/[\u4e00-\u9fa5]/.test(text) || /[[\]()::,,]/.test(text)) {
return `${id}{"${text}"}`;
}
return match;
});
// 处理连线标签 -->|文本|
line = line.replace(/(\|)([^|"]+)(\|)/g, (match, p1, text, p2) => {
if (/[\u4e00-\u9fa5]/.test(text)) {
return `|"${text}"|`;
}
return match;
});
// 处理 sequenceDiagram 消息文本
line = line.replace(/(->|-->>?|<<--)([^:]+):\s*([^"'\n]+)$/g, (match, arrow, target, msg) => {
if (/[\u4e00-\u9fa5]/.test(msg) && !msg.startsWith('"')) {
return `${arrow}${target}: "${msg.trim()}"`;
}
return match;
});
return line;
}).join('\n');
}
// === Mermaid 渲染状态管理(修正版)===
let isMermaidRendering = false;
let mermaidCheckTimeout = null;
const MERMAID_CHECK_INTERVAL = 350; // 检测间隔
// 存储已渲染的代码块 - key 是代码内容,value 是渲染后的 HTML
const renderedMermaidCache = new Map();
/**
* 获取 markdown 中所有完整的 mermaid 代码块内容集合
*/
function getCompleteMermaidCodes(markdown) {
if (!markdown) return new Set();
const codes = new Set();
const mermaidBlockRegex = /```mermaid\s*\n([\s\S]*?)```/g;
let match;
while ((match = mermaidBlockRegex.exec(markdown)) !== null) {
const code = match[1].trim();
if (code.length > 0) {
codes.add(code);
}
}
return codes;
}
/**
* 启动增量渲染检测(流式输出期间调用)
*/
function startIncrementalMermaidCheck() {
if (mermaidCheckTimeout) return;
mermaidCheckTimeout = setInterval(() => {
renderAllCompleteMermaidBlocks();
}, MERMAID_CHECK_INTERVAL);
}
/**
* 停止增量渲染检测
*/
function stopIncrementalMermaidCheck() {
if (mermaidCheckTimeout) {
clearInterval(mermaidCheckTimeout);
mermaidCheckTimeout = null;
}
}
/**
* 渲染所有完整的 Mermaid 代码块
* 核心逻辑:
* 1. 从 markdown 源码中提取所有完整的代码块内容
* 2. 查找 DOM 中所有 code.language-mermaid 元素
* 3. 只渲染内容在完整列表中的代码块
*/
async function renderAllCompleteMermaidBlocks() {
const reportDiv = document.getElementById('report');
if (!reportDiv) return;
if (isMermaidRendering) return;
// 获取 markdown 中所有完整的代码块内容
const completeCodes = getCompleteMermaidCodes(fullReportMarkdown);
if (completeCodes.size === 0) return;
// 查找 DOM 中所有未渲染的 code.language-mermaid 元素
const codeBlocks = reportDiv.querySelectorAll('code.language-mermaid');
if (codeBlocks.length === 0) return;
// 找出需要渲染的代码块(内容在完整列表中的)
const blocksToRender = [];
for (const codeBlock of codeBlocks) {
const code = codeBlock.textContent.trim();
if (completeCodes.has(code)) {
blocksToRender.push(codeBlock);
}
}
if (blocksToRender.length === 0) return;
console.log(`[Mermaid] Rendering ${blocksToRender.length} complete block(s)...`);
isMermaidRendering = true;
try {
for (let i = 0; i < blocksToRender.length; i++) {
const codeBlock = blocksToRender[i];
// 检查元素是否还在 DOM 中
if (!codeBlock.parentElement) continue;
// 让出主线程
await new Promise(resolve => {
if (window.requestIdleCallback) {
requestIdleCallback(resolve, { timeout: 50 });
} else {
setTimeout(resolve, 10);
}
});
await renderSingleMermaidBlock(codeBlock);
}
console.log('[Mermaid] Render complete');
} catch (e) {
console.error('[Mermaid] Render error:', e);
} finally {
isMermaidRendering = false;
}
}
/**
* 渲染单个代码块
*/
async function renderSingleMermaidBlock(codeBlock) {
const originalCode = codeBlock.textContent.trim();
const pre = codeBlock.parentElement;
if (!pre || pre.tagName !== 'PRE') return;
// 检查缓存 - 如果这段代码已经渲染过,直接使用缓存的 HTML
if (renderedMermaidCache.has(originalCode)) {
console.log('[Mermaid] Using cached render result');
const cachedHtml = renderedMermaidCache.get(originalCode);
const div = document.createElement('div');
div.className = 'mermaid';
div.innerHTML = cachedHtml;
div.style.cursor = 'zoom-in';
div.style.overflowX = 'auto';
div.onclick = function() {
openModal(div.innerHTML);
};
pre.replaceWith(div);
return;
}
const code = sanitizeMermaidCode(originalCode);
const div = document.createElement('div');
div.id = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
div.className = 'mermaid';
div.dataset.originalCode = originalCode;
div.textContent = code;
pre.replaceWith(div);
try {
await mermaid.run({ nodes: [div] });
const svg = div.querySelector('svg');
if (svg) {
div.onclick = function() {
openModal(div.innerHTML);
};
div.style.overflowX = 'auto';
div.style.cursor = 'zoom-in';
svg.style.maxWidth = '100%';
// 缓存渲染结果
renderedMermaidCache.set(originalCode, div.innerHTML);
console.log('[Mermaid] Block rendered and cached successfully');
}
} catch (e) {
console.error('[Mermaid] Block render failed:', e);
div.classList.add('mermaid-error');
div.innerHTML = `
<div class="mermaid-error-header">⚠️ 图表渲染失败</div>
<details>
<summary>查看原始 Mermaid 代码</summary>
<pre class="mermaid-source"><code>${escapeHtmlForMermaid(originalCode)}</code></pre>
</details>
<div class="mermaid-error-tip">提示: 请检查代码语法,中文文本需用双引号包裹</div>
`;
}
}
/**
* 完整渲染(流结束时调用)
*/
async function renderMermaidDiagrams(rootNode) {
stopIncrementalMermaidCheck();
await renderAllCompleteMermaidBlocks();
console.log('[Mermaid] Final render complete');
}
/**
* 重置渲染状态(新分析开始时调用)
*/
function resetMermaidState() {
stopIncrementalMermaidCheck();
isMermaidRendering = false;
renderedMermaidCache.clear();
console.log('[Mermaid] State reset, cache cleared');
}
// === HTML 转义函数 ===
function escapeHtmlForMermaid(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// === 主按钮点击处理 ===
function handleAnalyzeClick() {
const url = document.getElementById('repoUrl').value.trim();
if (!url) {
alert("Please enter a GitHub repository URL");
return;
}
const lang = document.querySelector('input[name="lang"]:checked').value;
switch (currentBtnState) {
case BTN_STATE.ANALYZE:
// 全新分析
startAnalysis(false);
break;
case BTN_STATE.GENERATE:
// 复用索引,仅生成报告
startAnalysis(true);
break;
case BTN_STATE.REANALYZE:
// 强制重新分析
if (confirm(`This will re-analyze the repository from scratch.\n\nContinue?`)) {
// 清空该语言的缓存
delete cachedReports[lang];
startAnalysis(false);
}
break;
default:
// CHECKING/ANALYZING 状态,按钮应该已禁用
break;
}
}
// === 核心分析逻辑 ===
async function startAnalysis(regenerateOnly = false) {
const url = document.getElementById('repoUrl').value.trim();
const lang = document.querySelector('input[name="lang"]:checked').value;
const reportDiv = document.getElementById('report');
const toolbar = document.getElementById('reportToolbar');
// 隐藏工具栏和提示
if (toolbar) toolbar.style.display = 'none';
hideHint();
// 设置按钮为分析中状态
setButtonState(BTN_STATE.ANALYZING);
// 获取 session ID
if (!currentSessionId || url !== currentRepoUrl) {
const checkResult = await checkRepoSession(url, lang);
currentSessionId = checkResult.session_id;
}
currentRepoUrl = url;
updateSessionDisplay(currentSessionId);
// 清空并显示加载提示
fullReportMarkdown = "";
resetMermaidState(); // 重置 Mermaid 渲染状态
const actionText = regenerateOnly ? 'Generating report' : 'Analyzing code';
reportDiv.innerHTML = `<div style='color:#94a3b8; text-align:center; margin-top:40px'>⏳ ${actionText} (${lang.toUpperCase()})...</div>`;
toggleChat(false);
const logAction = regenerateOnly ? '📝 Generating report (reusing index)' : '🚀 Starting full analysis';
document.getElementById('logs').innerHTML = `<div>[System] ${logAction} (${lang.toUpperCase()})...</div>`;
// 开始 SSE 流
const eventSource = new EventSource(
`${API_BASE}/analyze?url=${encodeURIComponent(url)}&session_id=${currentSessionId}&language=${lang}&regenerate_only=${regenerateOnly}`
);
// 启动增量渲染检测
startIncrementalMermaidCheck();
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.step === 'report_chunk') {
fullReportMarkdown += data.chunk;
reportDiv.innerHTML = marked.parse(fullReportMarkdown);
const panelContent = reportDiv.parentElement;
panelContent.scrollTop = panelContent.scrollHeight;
} else if (data.step === 'finish') {
logAppend(`✅ ${data.message}`, "#15803d");
eventSource.close();
// 停止增量检测并进行最终渲染
stopIncrementalMermaidCheck();
// 缓存报告
cachedReports[lang] = fullReportMarkdown;
// 更新按钮状态
setButtonState(BTN_STATE.REANALYZE);
showHint('reportReady', 'success');
toggleChat(true);
showReportToolbar();
renderMermaidDiagrams(reportDiv);
addMsg("ai", "🎉 Analysis complete! You can ask questions now.");
} else if (data.step === 'error') {
logAppend(`❌ ${data.message}`, "#b91c1c");
eventSource.close();
stopIncrementalMermaidCheck(); // 停止增量检测
// 错误时恢复到适当状态
if (lastCheckResult && lastCheckResult.has_index) {
setButtonState(BTN_STATE.GENERATE);
} else {
setButtonState(BTN_STATE.ANALYZE);
}
} else {
logAppend(`👉 ${data.message}`);
}
};
eventSource.onerror = function(err) {
logAppend("❌ Connection lost", "#b91c1c");
eventSource.close();
stopIncrementalMermaidCheck(); // 停止增量检测
setButtonState(BTN_STATE.ANALYZE);
};
}
// === 报告工具栏 ===
function showReportToolbar() {
const toolbar = document.getElementById('reportToolbar');
if (toolbar) toolbar.style.display = 'flex';
}
// === 下载 Markdown ===
function downloadMarkdown() {
if (!fullReportMarkdown) {
alert("No report to download");
return;
}
const blob = new Blob([fullReportMarkdown], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const repoName = currentRepoUrl.split('/').pop() || 'report';
a.download = `${repoName}_analysis.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// === 下载 PDF (使用浏览器打印功能) ===
function printReport() {
if (!fullReportMarkdown) {
alert("No report to print");
return;
}
const repoName = currentRepoUrl.split('/').pop() || 'report';
// 处理 Mermaid 图表占位符
const processedHtml = marked.parse(fullReportMarkdown).replace(
/<pre class="mermaid">[\s\S]*?<\/pre>/g,
'<div class="mermaid-placeholder">📊 Mermaid diagram (view in browser)<\/div>'
);
// 创建打印窗口
const printWindow = window.open('', '_blank');
// 避免在模板字符串中出现 </body></html> 导致解析问题
const htmlContent = [
'<!DOCTYPE html>',
'<html>',
'<head>',
'<title>' + repoName + ' - Analysis Report<\/title>',
'<style>',
'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; max-width: 900px; margin: 0 auto; padding: 40px; color: #1e293b; }',
'h1, h2, h3 { color: #0f172a; margin-top: 1.5em; }',
'h1 { border-bottom: 2px solid #e2e8f0; padding-bottom: 0.3em; }',
'h2 { border-bottom: 1px solid #e2e8f0; padding-bottom: 0.2em; }',
'code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }',
'pre { background: #f8fafc; padding: 16px; border-radius: 8px; overflow-x: auto; border: 1px solid #e2e8f0; }',
'pre code { background: none; padding: 0; }',
'table { width: 100%; border-collapse: collapse; margin: 1em 0; }',
'th, td { border: 1px solid #e2e8f0; padding: 10px 12px; text-align: left; }',
'th { background: #f8fafc; font-weight: 600; }',
'blockquote { border-left: 4px solid #3b82f6; margin: 1em 0; padding-left: 1em; color: #64748b; }',
'ul, ol { padding-left: 1.5em; }',
'li { margin: 0.3em 0; }',
'.mermaid-placeholder { background: #fef3c7; border: 1px dashed #f59e0b; padding: 20px; text-align: center; color: #92400e; border-radius: 8px; margin: 1em 0; }',
'@media print { body { padding: 20px; } pre { white-space: pre-wrap; word-wrap: break-word; } }',
'<\/style>',
'<\/head>',
'<body>',
processedHtml,
'<script>window.print();<\/script>',
'<\/body>',
'<\/html>'
].join('\n');
printWindow.document.write(htmlContent);
printWindow.document.close();
}
// === 聊天中止控制 ===
let chatAbortController = null;
let isChatGenerating = false;
function handleChatButton() {
if (isChatGenerating) {
stopChat();
} else {
sendChat();
}
}
function stopChat() {
if (chatAbortController) {
chatAbortController.abort();
chatAbortController = null;
}
}
function setChatButtonState(isGenerating) {
const btnChat = document.getElementById('btn-chat');
const input = document.getElementById('chatInput');
isChatGenerating = isGenerating;
if (isGenerating) {
btnChat.textContent = "Stop";
btnChat.classList.add('btn-stop');
btnChat.disabled = false;
input.disabled = true;
} else {
btnChat.textContent = "Send";
btnChat.classList.remove('btn-stop');
btnChat.disabled = false;
input.disabled = false;
input.focus();
}
}
async function sendChat() {
const input = document.getElementById('chatInput');
const query = input.value.trim();
if (!query) return;
addMsg("user", query);
input.value = "";
// 创建中止控制器
chatAbortController = new AbortController();
setChatButtonState(true);
let msgId;
setTimeout(() => {
msgId = addMsg("ai", "...");
}, 10);
// 获取当前仓库 URL(用于评估)
const repoUrl = document.getElementById('repoUrl').value.trim();
// 确保有有效的 Session ID
if (!currentSessionId) {
updateMsg(msgId, "❌ Please analyze a repository first.");
setChatButtonState(false);
return;
}
try {
const res = await fetch(`${API_BASE}/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: query, session_id: currentSessionId, repo_url: repoUrl }),
signal: chatAbortController.signal
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let fullText = "";
const msgDiv = await waitForElement(() => document.getElementById(msgId));
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
fullText += chunk;
msgDiv.innerHTML = marked.parse(fullText);
const history = document.getElementById('chat-history');
history.scrollTop = history.scrollHeight;
}
// 渲染对话气泡中的图表
await renderMermaidDiagrams(msgDiv);
} catch (err) {
if (err.name === 'AbortError') {
// 用户主动中止,添加提示
const div = document.getElementById(msgId);
if (div && div.innerHTML !== '...') {
div.innerHTML += '<br><span style="color:#6b7280; font-style:italic">⏹️ Stop</span>';
} else if (div) {
div.innerHTML = '<span style="color:#6b7280; font-style:italic">⏹️ Stop</span>';
}
} else {
if (!msgId) msgId = addMsg("ai", "");
const div = document.getElementById(msgId);
if(div) div.innerHTML += `<br><span style="color:red">❌ Error: ${err.message}</span>`;
}
} finally {
chatAbortController = null;
setChatButtonState(false);
}
}
function waitForElement(getter, timeout=2000) {
return new Promise((resolve, reject) => {
const start = Date.now();
const check = () => {
const el = getter();
if (el) resolve(el);
else if (Date.now() - start > timeout) reject(new Error("Timeout waiting for message bubble"));
else requestAnimationFrame(check);
};
check();
});
}
function logAppend(text, color = "inherit") {
const div = document.createElement('div');
div.textContent = text;
div.style.color = color;
const container = document.getElementById('logs');
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function addMsg(role, content) {
const history = document.getElementById('chat-history');
const div = document.createElement('div');
div.className = `msg ${role} markdown-body`;
div.id = "msg-" + Date.now() + "-" + Math.random().toString(36).substr(2, 9);
div.innerHTML = marked.parse(content);
history.appendChild(div);
history.scrollTop = history.scrollHeight;
return div.id;
}
function toggleChat(enabled) {
document.getElementById('chatInput').disabled = !enabled;
document.getElementById('btn-chat').disabled = !enabled;
document.getElementById('chatInput').placeholder = enabled ? "Please enter your question..." : "Waiting for analysis to complete...";
}
// app/index.html 底部 script 区域
function handleRepoKeyPress(e) {
if (e.key === 'Enter') {
// 1. 阻止默认行为(防止刷新)
e.preventDefault();
// 2. 阻止事件冒泡(防止触发父级 form 提交)
e.stopPropagation();
// 3. 手动触发点击
document.getElementById('btn-analyze').click();
return false;
}
}
function handleKeyPress(e) {
if (e.key === 'Enter' && !isChatGenerating) sendChat();
}
</script>
</body>
</html>