Spaces:
Running
Running
| <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% ; 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()">×</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}®enerate_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> | |