Spaces:
Runtime error
Runtime error
Fix final UI export syntax issue
Browse files
app/deployment/hf_status.py
CHANGED
|
@@ -6213,3 +6213,8 @@ window.sendMessage = async function() {
|
|
| 6213 |
html = html.replace("</body>", js + "\n</body>")
|
| 6214 |
|
| 6215 |
return html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6213 |
html = html.replace("</body>", js + "\n</body>")
|
| 6214 |
|
| 6215 |
return html
|
| 6216 |
+
# =====================================================
|
| 6217 |
+
# Phase 38 final stable UI export
|
| 6218 |
+
# This keeps old helper functions but forces / and /app to use the clean final UI.
|
| 6219 |
+
# =====================================================
|
| 6220 |
+
from app.product.final_product_ui import get_home_html, get_product_app_html
|
app/product/final_product_ui.py
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
def get_home_html() -> str:
|
| 3 |
+
return """
|
| 4 |
+
<!DOCTYPE html>
|
| 5 |
+
<html>
|
| 6 |
+
<head>
|
| 7 |
+
<title>GraphResearcher</title>
|
| 8 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 9 |
+
<style>
|
| 10 |
+
* { box-sizing: border-box; }
|
| 11 |
+
body { margin: 0; font-family: Inter, Arial, sans-serif; background: #f8fafc; color: #0f172a; }
|
| 12 |
+
.nav { height: 72px; display: flex; align-items: center; justify-content: space-between; padding: 0 56px; background: white; border-bottom: 1px solid #e5e7eb; }
|
| 13 |
+
.brand { font-weight: 900; font-size: 24px; letter-spacing: -0.7px; }
|
| 14 |
+
.nav a { text-decoration: none; color: #334155; margin-left: 22px; font-weight: 800; }
|
| 15 |
+
.hero { min-height: calc(100vh - 72px); display: grid; grid-template-columns: 1fr 0.9fr; gap: 52px; align-items: center; padding: 64px 72px; }
|
| 16 |
+
.badge { display: inline-block; background: #dbeafe; color: #1d4ed8; padding: 8px 12px; border-radius: 999px; font-weight: 900; font-size: 13px; margin-bottom: 18px; }
|
| 17 |
+
h1 { font-size: 58px; line-height: 1.04; margin: 0 0 20px; letter-spacing: -2px; }
|
| 18 |
+
p { font-size: 19px; line-height: 1.7; color: #475569; max-width: 720px; }
|
| 19 |
+
.actions { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 32px; }
|
| 20 |
+
.btn { display: inline-block; text-decoration: none; background: #2563eb; color: white; padding: 14px 22px; border-radius: 12px; font-weight: 900; }
|
| 21 |
+
.btn.dark { background: #0f172a; }
|
| 22 |
+
.preview { background: #0f172a; color: white; border-radius: 26px; padding: 24px; box-shadow: 0 28px 70px rgba(15,23,42,0.24); }
|
| 23 |
+
.preview-card { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.12); border-radius: 18px; padding: 16px; margin-bottom: 14px; line-height: 1.55; }
|
| 24 |
+
.preview-card b { color: #bfdbfe; }
|
| 25 |
+
.features { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; padding: 0 72px 58px; }
|
| 26 |
+
.feature { background: white; border: 1px solid #e5e7eb; border-radius: 18px; padding: 24px; }
|
| 27 |
+
.feature p { font-size: 15px; color: #64748b; }
|
| 28 |
+
@media(max-width: 950px) {
|
| 29 |
+
.nav { padding: 0 22px; }
|
| 30 |
+
.hero { grid-template-columns: 1fr; padding: 42px 24px; }
|
| 31 |
+
h1 { font-size: 42px; }
|
| 32 |
+
.features { grid-template-columns: 1fr; padding: 0 24px 44px; }
|
| 33 |
+
}
|
| 34 |
+
</style>
|
| 35 |
+
</head>
|
| 36 |
+
<body>
|
| 37 |
+
<div class="nav">
|
| 38 |
+
<div class="brand">GraphResearcher</div>
|
| 39 |
+
<div><a href="/app">Launch App</a></div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<section class="hero">
|
| 43 |
+
<div>
|
| 44 |
+
<div class="badge">Document Chat + GraphRAG</div>
|
| 45 |
+
<h1>Upload documents. Ask questions. Verify every answer.</h1>
|
| 46 |
+
<p>
|
| 47 |
+
GraphResearcher lets users chat with PDFs and reports using citation-grounded retrieval,
|
| 48 |
+
graph context, source verification, graph view, and document comparison.
|
| 49 |
+
</p>
|
| 50 |
+
<div class="actions">
|
| 51 |
+
<a class="btn" href="/app">Start Chatting</a>
|
| 52 |
+
<a class="btn dark" href="/login">Login</a>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<div class="preview">
|
| 57 |
+
<div class="preview-card"><b>1. Upload</b><br>Add a document from the app sidebar. No document ID needed.</div>
|
| 58 |
+
<div class="preview-card"><b>2. Chat</b><br>Ask natural questions and get source-backed answers.</div>
|
| 59 |
+
<div class="preview-card"><b>3. Verify</b><br>Open sources, see page/chunk metadata, and inspect the graph.</div>
|
| 60 |
+
<div class="preview-card"><b>4. Compare</b><br>Select a second document and compare them side by side.</div>
|
| 61 |
+
</div>
|
| 62 |
+
</section>
|
| 63 |
+
|
| 64 |
+
<section class="features">
|
| 65 |
+
<div class="feature"><h3>Simple workspace</h3><p>Upload, select, delete, and re-index documents from one clean UI.</p></div>
|
| 66 |
+
<div class="feature"><h3>Grounded answers</h3><p>Answers show evidence boxes and source cards for verification.</p></div>
|
| 67 |
+
<div class="feature"><h3>GraphRAG inside</h3><p>Graph view and graph retrieval stay available without exposing developer tools.</p></div>
|
| 68 |
+
</section>
|
| 69 |
+
</body>
|
| 70 |
+
</html>
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def get_product_app_html() -> str:
|
| 75 |
+
return """
|
| 76 |
+
<!DOCTYPE html>
|
| 77 |
+
<html>
|
| 78 |
+
<head>
|
| 79 |
+
<title>GraphResearcher App</title>
|
| 80 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 81 |
+
|
| 82 |
+
<style>
|
| 83 |
+
* { box-sizing: border-box; }
|
| 84 |
+
body { margin: 0; font-family: Inter, Arial, sans-serif; background: #f8fafc; color: #111827; }
|
| 85 |
+
.app { display: grid; grid-template-columns: 310px 1fr 370px; height: 100vh; }
|
| 86 |
+
.sidebar { background: #0f172a; color: white; padding: 18px; overflow-y: auto; }
|
| 87 |
+
.brand { font-size: 24px; font-weight: 900; margin-bottom: 18px; letter-spacing: -0.7px; }
|
| 88 |
+
.upload-box { border: 1px dashed rgba(255,255,255,0.35); border-radius: 16px; padding: 14px; background: rgba(255,255,255,0.06); margin-bottom: 20px; }
|
| 89 |
+
.upload-box input { width: 100%; font-size: 12px; margin: 10px 0; }
|
| 90 |
+
button { border: none; border-radius: 10px; padding: 10px 13px; cursor: pointer; background: #2563eb; color: white; font-weight: 800; }
|
| 91 |
+
button:hover { background: #1d4ed8; }
|
| 92 |
+
button.secondary { background: #334155; }
|
| 93 |
+
button.green { background: #059669; }
|
| 94 |
+
button.red { background: #dc2626; }
|
| 95 |
+
button.light { background: #f1f5f9; color: #0f172a; border: 1px solid #cbd5e1; }
|
| 96 |
+
.full { width: 100%; margin-bottom: 9px; }
|
| 97 |
+
.small { font-size: 12px; color: #94a3b8; line-height: 1.5; }
|
| 98 |
+
.doc-card { padding: 12px; border-radius: 13px; background: rgba(255,255,255,0.07); margin-bottom: 9px; cursor: pointer; border: 1px solid transparent; }
|
| 99 |
+
.doc-card.active { background: #2563eb; border-color: #93c5fd; }
|
| 100 |
+
.doc-title { font-size: 14px; font-weight: 800; word-break: break-word; }
|
| 101 |
+
.doc-meta { font-size: 11px; color: #cbd5e1; margin-top: 4px; }
|
| 102 |
+
.main { display: flex; flex-direction: column; height: 100vh; }
|
| 103 |
+
.topbar { padding: 14px 20px; border-bottom: 1px solid #e5e7eb; background: white; display: flex; justify-content: space-between; gap: 14px; align-items: center; }
|
| 104 |
+
.selected-doc { font-weight: 900; font-size: 17px; }
|
| 105 |
+
.selected-doc span { display: block; color: #64748b; font-weight: 500; font-size: 12px; margin-top: 2px; }
|
| 106 |
+
.status-pill { font-size: 12px; padding: 7px 10px; border-radius: 999px; background: #e0f2fe; color: #075985; white-space: nowrap; font-weight: 800; }
|
| 107 |
+
.messages { flex: 1; overflow-y: auto; padding: 26px; }
|
| 108 |
+
.message { max-width: 940px; margin-bottom: 18px; display: flex; }
|
| 109 |
+
.message.user { margin-left: auto; justify-content: flex-end; }
|
| 110 |
+
.bubble { padding: 15px 17px; border-radius: 17px; line-height: 1.65; white-space: pre-wrap; font-size: 15.5px; }
|
| 111 |
+
.user .bubble { background: #2563eb; color: white; border-bottom-right-radius: 4px; }
|
| 112 |
+
.assistant .bubble { background: white; border: 1px solid #e5e7eb; color: #111827; border-bottom-left-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
| 113 |
+
.empty { max-width: 760px; margin: 70px auto; text-align: center; color: #475569; }
|
| 114 |
+
.empty h1 { color: #0f172a; }
|
| 115 |
+
.composer { padding: 16px 20px; background: white; border-top: 1px solid #e5e7eb; }
|
| 116 |
+
.composer-box { display: flex; gap: 10px; align-items: flex-end; }
|
| 117 |
+
textarea { flex: 1; resize: none; min-height: 54px; max-height: 160px; padding: 13px; border: 1px solid #cbd5e1; border-radius: 12px; font-size: 15px; }
|
| 118 |
+
select { width: 100%; padding: 9px; border-radius: 9px; border: 1px solid #cbd5e1; background: white; }
|
| 119 |
+
.right-panel { background: white; border-left: 1px solid #e5e7eb; padding: 16px; overflow-y: auto; }
|
| 120 |
+
.panel-section { margin-bottom: 20px; }
|
| 121 |
+
.panel-section h3 { margin-bottom: 8px; font-size: 16px; }
|
| 122 |
+
.toggle-row { display: flex; flex-direction: column; gap: 8px; font-size: 14px; }
|
| 123 |
+
.metric { display: inline-block; background: #eef2ff; color: #3730a3; padding: 5px 8px; border-radius: 999px; font-size: 12px; margin: 3px; }
|
| 124 |
+
.citation-card { border: 1px solid #e5e7eb; background: #f8fafc; border-radius: 12px; padding: 12px; margin-bottom: 11px; font-size: 13px; }
|
| 125 |
+
.source-line { margin-top: 5px; color: #475569; }
|
| 126 |
+
.preview-text { margin-top: 8px; color: #64748b; font-size: 12px; line-height: 1.5; }
|
| 127 |
+
.answer-card { line-height: 1.72; }
|
| 128 |
+
.answer-card h2 { margin: 0 0 10px; font-size: 18px; color: #0f172a; }
|
| 129 |
+
.answer-card h3 { margin: 18px 0 8px; font-size: 15px; color: #1d4ed8; }
|
| 130 |
+
.answer-card p { margin: 8px 0; }
|
| 131 |
+
.evidence-box { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 12px; padding: 11px; margin-top: 9px; font-size: 13px; color: #475569; }
|
| 132 |
+
.source-chip { display: inline-block; background: #eef2ff; color: #3730a3; padding: 3px 7px; border-radius: 999px; font-size: 12px; margin: 2px; font-weight: 700; }
|
| 133 |
+
.warning { background: #fff7ed; border: 1px solid #fed7aa; color: #9a3412; padding: 10px; border-radius: 12px; margin: 10px 0; }
|
| 134 |
+
.compare-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 12px; }
|
| 135 |
+
.compare-card { border: 1px solid #e5e7eb; background: #f8fafc; border-radius: 14px; padding: 14px; line-height: 1.6; }
|
| 136 |
+
.danger-zone { border-top: 1px solid #e5e7eb; padding-top: 14px; }
|
| 137 |
+
@media (max-width: 1150px) { .app { grid-template-columns: 290px 1fr; } .right-panel { display: none; } }
|
| 138 |
+
@media (max-width: 850px) { .compare-grid { grid-template-columns: 1fr; } }
|
| 139 |
+
@media (max-width: 760px) { .app { grid-template-columns: 1fr; } .sidebar { display: none; } }
|
| 140 |
+
</style>
|
| 141 |
+
</head>
|
| 142 |
+
|
| 143 |
+
<body>
|
| 144 |
+
<div class="app">
|
| 145 |
+
<aside class="sidebar">
|
| 146 |
+
<div class="brand">GraphResearcher</div>
|
| 147 |
+
|
| 148 |
+
<div class="upload-box">
|
| 149 |
+
<b>Upload document</b>
|
| 150 |
+
<p class="small">Upload, select, and chat. If Hugging Face rebuilt recently, clear cache and re-upload.</p>
|
| 151 |
+
<input id="fileInput" type="file">
|
| 152 |
+
<button class="full green" onclick="uploadDocument()">Upload & Select</button>
|
| 153 |
+
<button class="full secondary" onclick="refreshDocuments()">Refresh Documents</button>
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
<div class="small" style="margin-bottom:8px;">My Documents</div>
|
| 157 |
+
<div id="documentList"></div>
|
| 158 |
+
|
| 159 |
+
<hr style="border-color:rgba(255,255,255,0.15); margin:18px 0;">
|
| 160 |
+
|
| 161 |
+
<button class="full secondary" onclick="newChat()">+ New Chat</button>
|
| 162 |
+
<button class="full secondary" onclick="reindexSelectedDocument()">Re-index Selected</button>
|
| 163 |
+
<button class="full secondary" onclick="buildGraph()">Build / Rebuild Graph</button>
|
| 164 |
+
<button class="full secondary" onclick="openGraphViewer()">View Graph</button>
|
| 165 |
+
<button class="full secondary" onclick="clearWorkspaceCache()">Clear Workspace Cache</button>
|
| 166 |
+
<button class="full secondary" onclick="window.location.href='/'">Home</button>
|
| 167 |
+
</aside>
|
| 168 |
+
|
| 169 |
+
<main class="main">
|
| 170 |
+
<div class="topbar">
|
| 171 |
+
<div class="selected-doc" id="selectedDocTitle">
|
| 172 |
+
No document selected
|
| 173 |
+
<span>Upload or select a document from the left sidebar.</span>
|
| 174 |
+
</div>
|
| 175 |
+
<span id="appStatus" class="status-pill">Ready</span>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<div id="messages" class="messages"></div>
|
| 179 |
+
|
| 180 |
+
<div class="composer">
|
| 181 |
+
<div class="composer-box">
|
| 182 |
+
<textarea id="messageInput" placeholder="Ask about the selected document..." onkeydown="handleKeyDown(event)"></textarea>
|
| 183 |
+
<button onclick="sendMessage()">Send</button>
|
| 184 |
+
</div>
|
| 185 |
+
<div class="small" style="margin-top:8px;color:#64748b;">
|
| 186 |
+
Tip: choose a second document in the right panel to compare two documents.
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
</main>
|
| 190 |
+
|
| 191 |
+
<aside class="right-panel">
|
| 192 |
+
<div class="panel-section">
|
| 193 |
+
<h3>Selected Document</h3>
|
| 194 |
+
<div id="docDetails" class="small" style="color:#64748b;">No document selected.</div>
|
| 195 |
+
</div>
|
| 196 |
+
|
| 197 |
+
<div class="panel-section">
|
| 198 |
+
<h3>Compare With</h3>
|
| 199 |
+
<select id="compareDocumentSelect" onchange="renderSelectedDocument()">
|
| 200 |
+
<option value="">No comparison</option>
|
| 201 |
+
</select>
|
| 202 |
+
<p class="small" style="color:#64748b;">Choose another document for side-by-side comparison.</p>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<div class="panel-section">
|
| 206 |
+
<h3>Graph</h3>
|
| 207 |
+
<button class="green" onclick="buildGraph()">Build / Rebuild Graph</button>
|
| 208 |
+
<button class="secondary" onclick="openGraphViewer()">View Graph</button>
|
| 209 |
+
</div>
|
| 210 |
+
|
| 211 |
+
<div class="panel-section">
|
| 212 |
+
<h3>Answer Style</h3>
|
| 213 |
+
<select id="answerStyle">
|
| 214 |
+
<option value="detailed" selected>Detailed</option>
|
| 215 |
+
<option value="step_by_step">Step-by-step</option>
|
| 216 |
+
<option value="concise">Concise</option>
|
| 217 |
+
<option value="research">Research style</option>
|
| 218 |
+
<option value="comparison">Comparison focused</option>
|
| 219 |
+
</select>
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<div class="panel-section">
|
| 223 |
+
<h3>Advanced Settings</h3>
|
| 224 |
+
<div class="toggle-row">
|
| 225 |
+
<label><input type="checkbox" id="useLLM" checked> Use LLM</label>
|
| 226 |
+
<label><input type="checkbox" id="useReranker" checked> Use reranker</label>
|
| 227 |
+
<label><input type="checkbox" id="useGraph" checked> Use graph context</label>
|
| 228 |
+
<label><input type="checkbox" id="useGraphRetrieval" checked> Use graph retrieval fusion</label>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
|
| 232 |
+
<div class="panel-section">
|
| 233 |
+
<h3>Last Answer Metrics</h3>
|
| 234 |
+
<div id="metricsBox"><span class="metric">No answer yet</span></div>
|
| 235 |
+
</div>
|
| 236 |
+
|
| 237 |
+
<div class="panel-section">
|
| 238 |
+
<h3>Sources</h3>
|
| 239 |
+
<div id="citationsBox" class="small" style="color:#64748b;">Sources will appear here after an answer.</div>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<div class="panel-section danger-zone">
|
| 243 |
+
<h3>Danger Zone</h3>
|
| 244 |
+
<button class="red" onclick="deleteSelectedDocument()">Delete Selected Document</button>
|
| 245 |
+
</div>
|
| 246 |
+
</aside>
|
| 247 |
+
</div>
|
| 248 |
+
|
| 249 |
+
<script>
|
| 250 |
+
let documents = JSON.parse(localStorage.getItem("graphrag_documents") || "[]");
|
| 251 |
+
let selectedDocumentId = localStorage.getItem("graphrag_selected_document_id") || null;
|
| 252 |
+
let conversations = JSON.parse(localStorage.getItem("graphrag_final_conversations") || "{}");
|
| 253 |
+
let lastSources = [];
|
| 254 |
+
|
| 255 |
+
function saveDocuments() {
|
| 256 |
+
localStorage.setItem("graphrag_documents", JSON.stringify(documents));
|
| 257 |
+
if (selectedDocumentId) localStorage.setItem("graphrag_selected_document_id", selectedDocumentId);
|
| 258 |
+
}
|
| 259 |
+
function saveConversations() { localStorage.setItem("graphrag_final_conversations", JSON.stringify(conversations)); }
|
| 260 |
+
function setStatus(text) { document.getElementById("appStatus").textContent = text; }
|
| 261 |
+
function getSelectedDocument() { return documents.find(d => d.id === selectedDocumentId) || null; }
|
| 262 |
+
function getCompareDocument() {
|
| 263 |
+
const id = document.getElementById("compareDocumentSelect")?.value || "";
|
| 264 |
+
if (!id) return null;
|
| 265 |
+
return documents.find(d => d.id === id) || null;
|
| 266 |
+
}
|
| 267 |
+
function escapeHtml(value) {
|
| 268 |
+
return String(value || "").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""");
|
| 269 |
+
}
|
| 270 |
+
function parseDocumentId(data) {
|
| 271 |
+
return data.document_id || data.id || data.doc_id || data.documentId ||
|
| 272 |
+
data.document?.document_id || data.document?.id ||
|
| 273 |
+
data.file?.document_id || data.result?.document_id || data.data?.document_id;
|
| 274 |
+
}
|
| 275 |
+
async function tryEndpoints(candidates, optionsFactory) {
|
| 276 |
+
let lastError = null;
|
| 277 |
+
for (const candidate of candidates) {
|
| 278 |
+
try {
|
| 279 |
+
const response = await fetch(candidate, optionsFactory(candidate));
|
| 280 |
+
const contentType = response.headers.get("content-type") || "";
|
| 281 |
+
const data = contentType.includes("application/json") ? await response.json() : await response.text();
|
| 282 |
+
if (response.ok) return { ok: true, endpoint: candidate, data };
|
| 283 |
+
lastError = { endpoint: candidate, status: response.status, data };
|
| 284 |
+
} catch (error) {
|
| 285 |
+
lastError = { endpoint: candidate, error: error.message };
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
return { ok: false, error: lastError };
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
function renderDocuments() {
|
| 292 |
+
const list = document.getElementById("documentList");
|
| 293 |
+
list.innerHTML = "";
|
| 294 |
+
|
| 295 |
+
if (documents.length === 0) list.innerHTML = `<div class="small">No documents yet. Upload one above.</div>`;
|
| 296 |
+
|
| 297 |
+
documents.forEach(doc => {
|
| 298 |
+
const div = document.createElement("div");
|
| 299 |
+
div.className = "doc-card" + (doc.id === selectedDocumentId ? " active" : "");
|
| 300 |
+
div.onclick = () => selectDocument(doc.id);
|
| 301 |
+
div.innerHTML = `<div class="doc-title">${escapeHtml(doc.name || "Untitled document")}</div>
|
| 302 |
+
<div class="doc-meta">${escapeHtml(doc.status || "uploaded")} • ${escapeHtml(doc.graphStatus || "graph not built")}</div>`;
|
| 303 |
+
list.appendChild(div);
|
| 304 |
+
});
|
| 305 |
+
|
| 306 |
+
renderCompareDropdown();
|
| 307 |
+
renderSelectedDocument();
|
| 308 |
+
}
|
| 309 |
+
function renderCompareDropdown() {
|
| 310 |
+
const select = document.getElementById("compareDocumentSelect");
|
| 311 |
+
if (!select) return;
|
| 312 |
+
|
| 313 |
+
const oldValue = select.value;
|
| 314 |
+
select.innerHTML = `<option value="">No comparison</option>`;
|
| 315 |
+
|
| 316 |
+
documents.filter(d => d.id !== selectedDocumentId).forEach(doc => {
|
| 317 |
+
const option = document.createElement("option");
|
| 318 |
+
option.value = doc.id;
|
| 319 |
+
option.textContent = doc.name || "Untitled document";
|
| 320 |
+
select.appendChild(option);
|
| 321 |
+
});
|
| 322 |
+
|
| 323 |
+
if ([...select.options].some(o => o.value === oldValue)) select.value = oldValue;
|
| 324 |
+
}
|
| 325 |
+
function renderSelectedDocument() {
|
| 326 |
+
const doc = getSelectedDocument();
|
| 327 |
+
const compareDoc = getCompareDocument();
|
| 328 |
+
const title = document.getElementById("selectedDocTitle");
|
| 329 |
+
const details = document.getElementById("docDetails");
|
| 330 |
+
|
| 331 |
+
if (!doc) {
|
| 332 |
+
title.innerHTML = `No document selected <span>Upload or select a document from the left sidebar.</span>`;
|
| 333 |
+
details.textContent = "No document selected.";
|
| 334 |
+
renderMessages();
|
| 335 |
+
return;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
const subtitle = compareDoc ? `Compare mode active with ${compareDoc.name || "second document"}.` : "Ready for document chat.";
|
| 339 |
+
title.innerHTML = `${escapeHtml(doc.name || "Untitled document")} <span>${escapeHtml(subtitle)}</span>`;
|
| 340 |
+
details.innerHTML = `<b>Name:</b> ${escapeHtml(doc.name || "Untitled")}<br>
|
| 341 |
+
<b>Status:</b> ${escapeHtml(doc.status || "uploaded")}<br>
|
| 342 |
+
<b>Graph:</b> ${escapeHtml(doc.graphStatus || "not built")}<br>
|
| 343 |
+
<b>Uploaded:</b> ${escapeHtml(doc.uploadedAt || "unknown")}
|
| 344 |
+
${compareDoc ? `<br><br><b>Comparing with:</b> ${escapeHtml(compareDoc.name || "Untitled")}` : ""}`;
|
| 345 |
+
renderMessages();
|
| 346 |
+
}
|
| 347 |
+
function selectDocument(id) {
|
| 348 |
+
selectedDocumentId = id;
|
| 349 |
+
saveDocuments();
|
| 350 |
+
renderDocuments();
|
| 351 |
+
}
|
| 352 |
+
function getConversationKey() {
|
| 353 |
+
const compareDoc = getCompareDocument();
|
| 354 |
+
if (compareDoc) return `${selectedDocumentId}__compare__${compareDoc.id}`;
|
| 355 |
+
return selectedDocumentId || "default";
|
| 356 |
+
}
|
| 357 |
+
function getConversation() {
|
| 358 |
+
const key = getConversationKey();
|
| 359 |
+
if (!conversations[key]) conversations[key] = [];
|
| 360 |
+
return conversations[key];
|
| 361 |
+
}
|
| 362 |
+
function newChat() {
|
| 363 |
+
if (!selectedDocumentId) return alert("Select a document first.");
|
| 364 |
+
conversations[getConversationKey()] = [];
|
| 365 |
+
saveConversations();
|
| 366 |
+
renderMessages();
|
| 367 |
+
}
|
| 368 |
+
function renderMessages() {
|
| 369 |
+
const box = document.getElementById("messages");
|
| 370 |
+
const doc = getSelectedDocument();
|
| 371 |
+
const compareDoc = getCompareDocument();
|
| 372 |
+
|
| 373 |
+
if (!doc) {
|
| 374 |
+
box.innerHTML = `<div class="empty"><h1>Upload a document to start</h1><p>No document ID needed. Upload a file from the left sidebar, then chat normally.</p></div>`;
|
| 375 |
+
return;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
const convo = getConversation();
|
| 379 |
+
if (convo.length === 0) {
|
| 380 |
+
box.innerHTML = `<div class="empty"><h1>${compareDoc ? "Compare documents" : "Chat with your document"}</h1>
|
| 381 |
+
<p>${compareDoc ? `You are comparing ${escapeHtml(doc.name)} with ${escapeHtml(compareDoc.name)}.` : `Ask a question about ${escapeHtml(doc.name)}.`}</p></div>`;
|
| 382 |
+
return;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
box.innerHTML = "";
|
| 386 |
+
convo.forEach(msg => {
|
| 387 |
+
const wrapper = document.createElement("div");
|
| 388 |
+
wrapper.className = "message " + msg.role;
|
| 389 |
+
|
| 390 |
+
const bubble = document.createElement("div");
|
| 391 |
+
bubble.className = msg.type === "compare" ? "bubble" : "bubble";
|
| 392 |
+
|
| 393 |
+
if (msg.role === "assistant" && msg.html) bubble.innerHTML = msg.html;
|
| 394 |
+
else bubble.textContent = msg.content || "";
|
| 395 |
+
|
| 396 |
+
wrapper.appendChild(bubble);
|
| 397 |
+
box.appendChild(wrapper);
|
| 398 |
+
});
|
| 399 |
+
|
| 400 |
+
box.scrollTop = box.scrollHeight;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
async function uploadDocument() {
|
| 404 |
+
const file = document.getElementById("fileInput").files[0];
|
| 405 |
+
if (!file) return alert("Choose a file first.");
|
| 406 |
+
|
| 407 |
+
setStatus("Uploading...");
|
| 408 |
+
const formData = new FormData();
|
| 409 |
+
formData.append("file", file);
|
| 410 |
+
|
| 411 |
+
const result = await tryEndpoints(["/documents/upload", "/upload", "/documents", "/api/documents/upload"], () => ({ method: "POST", body: formData }));
|
| 412 |
+
if (!result.ok) {
|
| 413 |
+
setStatus("Upload failed");
|
| 414 |
+
return alert("Upload failed: " + JSON.stringify(result.error));
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
const documentId = parseDocumentId(result.data);
|
| 418 |
+
if (!documentId) {
|
| 419 |
+
setStatus("Upload returned no document ID");
|
| 420 |
+
return alert("Upload worked but document ID was not found in response.");
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
const doc = { id: documentId, name: file.name, status: "uploaded", graphStatus: "not built", uploadedAt: new Date().toLocaleString() };
|
| 424 |
+
const index = documents.findIndex(d => d.id === documentId);
|
| 425 |
+
if (index >= 0) documents[index] = doc;
|
| 426 |
+
else documents.unshift(doc);
|
| 427 |
+
|
| 428 |
+
selectedDocumentId = documentId;
|
| 429 |
+
saveDocuments();
|
| 430 |
+
renderDocuments();
|
| 431 |
+
|
| 432 |
+
setStatus("Uploaded");
|
| 433 |
+
await autoIndexDocument(documentId);
|
| 434 |
+
await buildGraph(true);
|
| 435 |
+
}
|
| 436 |
+
async function autoIndexDocument(documentId) {
|
| 437 |
+
setStatus("Indexing...");
|
| 438 |
+
const result = await tryEndpoints([`/documents/${documentId}/index`, `/documents/${documentId}/process`, `/documents/${documentId}/ingest`, `/index/${documentId}`], () => ({ method: "POST" }));
|
| 439 |
+
const doc = documents.find(d => d.id === documentId);
|
| 440 |
+
if (doc) {
|
| 441 |
+
doc.status = result.ok ? "indexed" : "uploaded";
|
| 442 |
+
saveDocuments();
|
| 443 |
+
renderDocuments();
|
| 444 |
+
}
|
| 445 |
+
setStatus(result.ok ? "Indexed" : "Uploaded");
|
| 446 |
+
}
|
| 447 |
+
async function reindexSelectedDocument() {
|
| 448 |
+
const doc = getSelectedDocument();
|
| 449 |
+
if (!doc) return alert("Select a document first.");
|
| 450 |
+
await autoIndexDocument(doc.id);
|
| 451 |
+
await buildGraph(false);
|
| 452 |
+
alert("Re-index attempt complete. Ask again now.");
|
| 453 |
+
}
|
| 454 |
+
async function buildGraph(silent = false) {
|
| 455 |
+
const doc = getSelectedDocument();
|
| 456 |
+
if (!doc) {
|
| 457 |
+
if (!silent) alert("Select a document first.");
|
| 458 |
+
return;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
setStatus("Building graph...");
|
| 462 |
+
try {
|
| 463 |
+
const response = await fetch(`/documents/${doc.id}/graph/build`, { method: "POST" });
|
| 464 |
+
const data = await response.json();
|
| 465 |
+
if (!response.ok) throw new Error(JSON.stringify(data));
|
| 466 |
+
|
| 467 |
+
doc.graphStatus = "graph built";
|
| 468 |
+
doc.graphData = { entities: data.total_entities ?? data.entity_count ?? null, relations: data.total_relations ?? data.relation_count ?? null };
|
| 469 |
+
saveDocuments();
|
| 470 |
+
renderDocuments();
|
| 471 |
+
|
| 472 |
+
document.getElementById("metricsBox").innerHTML = `<span class="metric">graph built</span>
|
| 473 |
+
<span class="metric">entities: ${doc.graphData.entities ?? "NA"}</span>
|
| 474 |
+
<span class="metric">relations: ${doc.graphData.relations ?? "NA"}</span>`;
|
| 475 |
+
|
| 476 |
+
setStatus("Graph ready");
|
| 477 |
+
} catch (error) {
|
| 478 |
+
doc.graphStatus = "graph not built";
|
| 479 |
+
saveDocuments();
|
| 480 |
+
renderDocuments();
|
| 481 |
+
setStatus("Graph skipped");
|
| 482 |
+
if (!silent) alert("Graph build failed. Re-index or re-upload if needed.");
|
| 483 |
+
}
|
| 484 |
+
}
|
| 485 |
+
function openGraphViewer() {
|
| 486 |
+
const doc = getSelectedDocument();
|
| 487 |
+
if (!doc) return alert("Select a document first.");
|
| 488 |
+
window.open(`/documents/${doc.id}/graph/view`, "_blank");
|
| 489 |
+
}
|
| 490 |
+
async function deleteSelectedDocument() {
|
| 491 |
+
const doc = getSelectedDocument();
|
| 492 |
+
if (!doc) return alert("Select a document first.");
|
| 493 |
+
if (!confirm(`Delete "${doc.name}"?`)) return;
|
| 494 |
+
|
| 495 |
+
setStatus("Deleting...");
|
| 496 |
+
await tryEndpoints([`/documents/${doc.id}/delete`, `/documents/${doc.id}`, `/api/documents/${doc.id}`], () => ({ method: "DELETE" }));
|
| 497 |
+
|
| 498 |
+
documents = documents.filter(d => d.id !== doc.id);
|
| 499 |
+
Object.keys(conversations).forEach(k => { if (k.includes(doc.id)) delete conversations[k]; });
|
| 500 |
+
selectedDocumentId = documents.length ? documents[0].id : null;
|
| 501 |
+
saveDocuments();
|
| 502 |
+
saveConversations();
|
| 503 |
+
renderDocuments();
|
| 504 |
+
renderMessages();
|
| 505 |
+
setStatus("Deleted");
|
| 506 |
+
}
|
| 507 |
+
function clearWorkspaceCache() {
|
| 508 |
+
if (!confirm("Clear browser document list and chat history? Use this after Hugging Face rebuilds.")) return;
|
| 509 |
+
["graphrag_documents", "graphrag_selected_document_id", "graphrag_conversations", "graphrag_conversations_v2", "graphrag_conversations_v3", "graphrag_final_conversations"].forEach(k => localStorage.removeItem(k));
|
| 510 |
+
documents = [];
|
| 511 |
+
selectedDocumentId = null;
|
| 512 |
+
conversations = {};
|
| 513 |
+
renderDocuments();
|
| 514 |
+
renderMessages();
|
| 515 |
+
setStatus("Cache cleared");
|
| 516 |
+
}
|
| 517 |
+
function refreshDocuments() { renderDocuments(); setStatus("Refreshed"); }
|
| 518 |
+
function handleKeyDown(event) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); sendMessage(); } }
|
| 519 |
+
|
| 520 |
+
function buildContextualQuery(currentQuestion) {
|
| 521 |
+
const clean = String(currentQuestion || "").trim();
|
| 522 |
+
if (!clean) return clean;
|
| 523 |
+
|
| 524 |
+
const wordCount = clean.split(/\\s+/).filter(Boolean).length;
|
| 525 |
+
if (wordCount <= 8) {
|
| 526 |
+
const prev = getConversation().filter(m => m.role === "user").slice(-2).map(m => m.content).join(" ");
|
| 527 |
+
return prev ? prev + " " + clean : clean;
|
| 528 |
+
}
|
| 529 |
+
return clean;
|
| 530 |
+
}
|
| 531 |
+
function askPayload(query, documentId) {
|
| 532 |
+
return {
|
| 533 |
+
query,
|
| 534 |
+
document_id: documentId,
|
| 535 |
+
top_k: 8,
|
| 536 |
+
retrieval_mode: "hybrid",
|
| 537 |
+
use_reranker: document.getElementById("useReranker").checked,
|
| 538 |
+
use_llm: document.getElementById("useLLM").checked,
|
| 539 |
+
use_graph: document.getElementById("useGraph").checked,
|
| 540 |
+
graph_entity_limit: 12,
|
| 541 |
+
use_graph_retrieval: document.getElementById("useGraphRetrieval").checked,
|
| 542 |
+
graph_retrieval_top_k: 6
|
| 543 |
+
};
|
| 544 |
+
}
|
| 545 |
+
async function callAsk(payload) {
|
| 546 |
+
const response = await fetch("/ask", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
|
| 547 |
+
const data = await response.json();
|
| 548 |
+
if (!response.ok) throw new Error(JSON.stringify(data));
|
| 549 |
+
return data;
|
| 550 |
+
}
|
| 551 |
+
function valueFrom(obj, keys, fallback = "Not available") {
|
| 552 |
+
for (const key of keys) if (obj && obj[key] !== undefined && obj[key] !== null && obj[key] !== "") return obj[key];
|
| 553 |
+
if (obj && obj.metadata) for (const key of keys) if (obj.metadata[key] !== undefined && obj.metadata[key] !== null && obj.metadata[key] !== "") return obj.metadata[key];
|
| 554 |
+
return fallback;
|
| 555 |
+
}
|
| 556 |
+
function buildSources(data, doc) {
|
| 557 |
+
const sources = [];
|
| 558 |
+
(data.citations || []).forEach(x => sources.push(x));
|
| 559 |
+
(data.retrieval_fusion?.fused_results || []).forEach(x => sources.push(x));
|
| 560 |
+
(data.sources || data.source_chunks || data.retrieved_sources || []).forEach(x => sources.push(x));
|
| 561 |
+
|
| 562 |
+
const cleaned = [];
|
| 563 |
+
const seen = new Set();
|
| 564 |
+
|
| 565 |
+
sources.forEach((src, i) => {
|
| 566 |
+
const sid = valueFrom(src, ["source_id", "id", "citation_id"], "S" + (i + 1));
|
| 567 |
+
const chunk = valueFrom(src, ["chunk_id", "source_chunk_id", "chunk"], sid);
|
| 568 |
+
const page = valueFrom(src, ["page_number", "page", "page_no"], "Not available");
|
| 569 |
+
const key = doc.id + "|" + sid + "|" + chunk + "|" + page;
|
| 570 |
+
if (seen.has(key)) return;
|
| 571 |
+
seen.add(key);
|
| 572 |
+
|
| 573 |
+
cleaned.push({
|
| 574 |
+
source_id: sid,
|
| 575 |
+
document_id: doc.id,
|
| 576 |
+
document_name: valueFrom(src, ["document_name", "source_file_name", "file_name", "filename", "document_title"], doc.name || "Selected document"),
|
| 577 |
+
page,
|
| 578 |
+
chunk_id: chunk,
|
| 579 |
+
preview: valueFrom(src, ["text_preview", "preview", "chunk_preview", "content_preview", "text", "content"], "Preview not available"),
|
| 580 |
+
raw: src
|
| 581 |
+
});
|
| 582 |
+
});
|
| 583 |
+
|
| 584 |
+
return cleaned.slice(0, 8);
|
| 585 |
+
}
|
| 586 |
+
function updateCitations(groups) {
|
| 587 |
+
const box = document.getElementById("citationsBox");
|
| 588 |
+
lastSources = [];
|
| 589 |
+
box.innerHTML = "";
|
| 590 |
+
|
| 591 |
+
groups.forEach(group => {
|
| 592 |
+
const h = document.createElement("div");
|
| 593 |
+
h.innerHTML = `<h4>${escapeHtml(group.label)}</h4>`;
|
| 594 |
+
box.appendChild(h);
|
| 595 |
+
|
| 596 |
+
group.sources.forEach(src => {
|
| 597 |
+
const idx = lastSources.length;
|
| 598 |
+
lastSources.push(src);
|
| 599 |
+
const card = document.createElement("div");
|
| 600 |
+
card.className = "citation-card";
|
| 601 |
+
card.innerHTML = `<b>Source ${idx + 1}: ${escapeHtml(src.source_id)}</b>
|
| 602 |
+
<div class="source-line"><b>Document:</b> ${escapeHtml(src.document_name)}</div>
|
| 603 |
+
<div class="source-line"><b>Page:</b> ${escapeHtml(src.page)}</div>
|
| 604 |
+
<div class="source-line"><b>Chunk:</b> ${escapeHtml(src.chunk_id)}</div>
|
| 605 |
+
<div class="preview-text">${escapeHtml(String(src.preview).slice(0, 260))}</div><br>
|
| 606 |
+
<button class="light" onclick="openSource(${idx})">Open source</button>`;
|
| 607 |
+
box.appendChild(card);
|
| 608 |
+
});
|
| 609 |
+
});
|
| 610 |
+
|
| 611 |
+
if (!lastSources.length) box.textContent = "No source details returned.";
|
| 612 |
+
}
|
| 613 |
+
function openSource(index) {
|
| 614 |
+
const src = lastSources[index];
|
| 615 |
+
if (!src) return;
|
| 616 |
+
window.open(`/documents/${src.document_id}/sources/${encodeURIComponent(src.source_id)}/view?page=${encodeURIComponent(src.page || "")}&chunk_id=${encodeURIComponent(src.chunk_id || "")}`, "_blank");
|
| 617 |
+
}
|
| 618 |
+
function updateMetrics(data, label) {
|
| 619 |
+
const fusion = data.retrieval_fusion || {};
|
| 620 |
+
const graph = data.graph_context || {};
|
| 621 |
+
document.getElementById("metricsBox").innerHTML += `<div style="margin-bottom:8px;"><b>${escapeHtml(label || "")}</b><br>
|
| 622 |
+
<span class="metric">strategy: ${escapeHtml(data.answer_strategy || "NA")}</span>
|
| 623 |
+
<span class="metric">LLM: ${data.used_llm}</span>
|
| 624 |
+
<span class="metric">graph: ${data.graph_used}</span>
|
| 625 |
+
<span class="metric">graph available: ${graph.graph_available}</span>
|
| 626 |
+
<span class="metric">fusion: ${fusion.fusion_used}</span></div>`;
|
| 627 |
+
}
|
| 628 |
+
function answerLooksWeak(answer) {
|
| 629 |
+
const words = String(answer || "").split(/\\s+/).filter(Boolean);
|
| 630 |
+
return words.length < 100;
|
| 631 |
+
}
|
| 632 |
+
function renderAnswerHtml(question, data, doc) {
|
| 633 |
+
const answer = String(data.answer || "I could not generate an answer.").trim();
|
| 634 |
+
const sources = buildSources(data, doc);
|
| 635 |
+
|
| 636 |
+
let html = `<div class="answer-card">`;
|
| 637 |
+
|
| 638 |
+
if (answer.toLowerCase().includes("i could not find relevant indexed sources")) {
|
| 639 |
+
html += `<h2>I could not find indexed evidence</h2>
|
| 640 |
+
<div class="warning">This usually means the browser remembers an old document but backend files were reset. Clear cache and upload again.</div></div>`;
|
| 641 |
+
return html;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
html += `<h2>Answer</h2>`;
|
| 645 |
+
|
| 646 |
+
const escaped = escapeHtml(answer).replaceAll("\\n", "<br>");
|
| 647 |
+
html += `<p>${escaped}</p>`;
|
| 648 |
+
|
| 649 |
+
if (sources.length) {
|
| 650 |
+
html += `<h3>Evidence used</h3>`;
|
| 651 |
+
sources.slice(0, 5).forEach((src, i) => {
|
| 652 |
+
html += `<div class="evidence-box"><b>[${escapeHtml(src.source_id)}]</b>
|
| 653 |
+
<span class="source-chip">Page: ${escapeHtml(src.page)}</span>
|
| 654 |
+
<span class="source-chip">Chunk: ${escapeHtml(src.chunk_id)}</span>
|
| 655 |
+
<div style="margin-top:6px;">${escapeHtml(String(src.preview).slice(0, 320))}</div></div>`;
|
| 656 |
+
});
|
| 657 |
+
} else {
|
| 658 |
+
html += `<div class="warning">No source metadata was returned. Re-index or re-upload if this seems wrong.</div>`;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
html += `</div>`;
|
| 662 |
+
return html;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
async function sendMessage() {
|
| 666 |
+
const doc = getSelectedDocument();
|
| 667 |
+
const compareDoc = getCompareDocument();
|
| 668 |
+
const input = document.getElementById("messageInput");
|
| 669 |
+
const userText = input.value.trim();
|
| 670 |
+
|
| 671 |
+
if (!doc) return alert("Upload or select a document first.");
|
| 672 |
+
if (!userText) return;
|
| 673 |
+
|
| 674 |
+
const convo = getConversation();
|
| 675 |
+
convo.push({ role: "user", content: userText, createdAt: new Date().toISOString() });
|
| 676 |
+
input.value = "";
|
| 677 |
+
saveConversations();
|
| 678 |
+
renderMessages();
|
| 679 |
+
|
| 680 |
+
setStatus(compareDoc ? "Comparing..." : "Thinking...");
|
| 681 |
+
document.getElementById("metricsBox").innerHTML = "";
|
| 682 |
+
|
| 683 |
+
try {
|
| 684 |
+
if (compareDoc) {
|
| 685 |
+
let data;
|
| 686 |
+
try {
|
| 687 |
+
const res = await fetch("/documents/compare", {
|
| 688 |
+
method: "POST",
|
| 689 |
+
headers: { "Content-Type": "application/json" },
|
| 690 |
+
body: JSON.stringify({
|
| 691 |
+
primary_document_id: doc.id,
|
| 692 |
+
compare_document_id: compareDoc.id,
|
| 693 |
+
query: userText,
|
| 694 |
+
retrieval_mode: "hybrid",
|
| 695 |
+
top_k: 8,
|
| 696 |
+
use_reranker: true,
|
| 697 |
+
use_llm: true,
|
| 698 |
+
use_graph: true,
|
| 699 |
+
graph_entity_limit: 12,
|
| 700 |
+
use_graph_retrieval: true,
|
| 701 |
+
graph_retrieval_top_k: 6
|
| 702 |
+
})
|
| 703 |
+
});
|
| 704 |
+
data = await res.json();
|
| 705 |
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
| 706 |
+
} catch (err) {
|
| 707 |
+
const dataA = await callAsk(askPayload(buildContextualQuery(userText), doc.id));
|
| 708 |
+
const dataB = await callAsk(askPayload(buildContextualQuery(userText), compareDoc.id));
|
| 709 |
+
data = {
|
| 710 |
+
document_a: { answer: dataA.answer, ask_response: dataA },
|
| 711 |
+
document_b: { answer: dataB.answer, ask_response: dataB }
|
| 712 |
+
};
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
const dataA = data.document_a?.ask_response || { answer: data.document_a?.answer || "No answer.", citations: data.document_a?.sources || [] };
|
| 716 |
+
const dataB = data.document_b?.ask_response || { answer: data.document_b?.answer || "No answer.", citations: data.document_b?.sources || [] };
|
| 717 |
+
|
| 718 |
+
const html = `<div class="answer-card"><h2>Comparison Answer</h2>
|
| 719 |
+
<div class="compare-grid">
|
| 720 |
+
<div class="compare-card"><h3>${escapeHtml(doc.name || "Document A")}</h3>${renderAnswerHtml(userText, dataA, doc)}</div>
|
| 721 |
+
<div class="compare-card"><h3>${escapeHtml(compareDoc.name || "Document B")}</h3>${renderAnswerHtml(userText, dataB, compareDoc)}</div>
|
| 722 |
+
</div></div>`;
|
| 723 |
+
|
| 724 |
+
convo.push({ role: "assistant", html, content: "Comparison answer", createdAt: new Date().toISOString(), rawCompare: data });
|
| 725 |
+
saveConversations();
|
| 726 |
+
renderMessages();
|
| 727 |
+
|
| 728 |
+
updateMetrics(dataA, doc.name || "Document A");
|
| 729 |
+
updateMetrics(dataB, compareDoc.name || "Document B");
|
| 730 |
+
updateCitations([
|
| 731 |
+
{ label: doc.name || "Document A", sources: buildSources(dataA, doc) },
|
| 732 |
+
{ label: compareDoc.name || "Document B", sources: buildSources(dataB, compareDoc) }
|
| 733 |
+
]);
|
| 734 |
+
setStatus("Comparison ready");
|
| 735 |
+
} else {
|
| 736 |
+
const data = await callAsk(askPayload(buildContextualQuery(userText), doc.id));
|
| 737 |
+
const html = renderAnswerHtml(userText, data, doc);
|
| 738 |
+
convo.push({ role: "assistant", content: data.answer || "Answer", html, createdAt: new Date().toISOString(), raw: data });
|
| 739 |
+
saveConversations();
|
| 740 |
+
renderMessages();
|
| 741 |
+
|
| 742 |
+
updateMetrics(data, doc.name || "Selected document");
|
| 743 |
+
updateCitations([{ label: doc.name || "Selected document", sources: buildSources(data, doc) }]);
|
| 744 |
+
setStatus("Ready");
|
| 745 |
+
}
|
| 746 |
+
} catch (error) {
|
| 747 |
+
convo.push({ role: "assistant", content: "Error: " + error.message, createdAt: new Date().toISOString() });
|
| 748 |
+
saveConversations();
|
| 749 |
+
renderMessages();
|
| 750 |
+
setStatus("Error");
|
| 751 |
+
}
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
renderDocuments();
|
| 755 |
+
</script>
|
| 756 |
+
</body>
|
| 757 |
+
</html>
|
| 758 |
+
"""
|
scripts/fix_phase38_literal_newline_error.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
path = Path("app/deployment/hf_status.py")
|
| 4 |
+
text = path.read_text(encoding="utf-8-sig")
|
| 5 |
+
text = text.replace("\ufeff", "")
|
| 6 |
+
|
| 7 |
+
fixed_lines = []
|
| 8 |
+
|
| 9 |
+
for line in text.splitlines():
|
| 10 |
+
stripped = line.strip()
|
| 11 |
+
|
| 12 |
+
# Remove broken literal backslash-n lines accidentally written as Python code
|
| 13 |
+
if stripped in {r"\n", r"\n\n", r"\n\n\n"}:
|
| 14 |
+
continue
|
| 15 |
+
|
| 16 |
+
# Fix if the broken literal got attached before a comment/import
|
| 17 |
+
if stripped.startswith(r"\n\n#"):
|
| 18 |
+
line = line.replace(r"\n\n", "", 1)
|
| 19 |
+
|
| 20 |
+
if stripped.startswith(r"\n\nfrom "):
|
| 21 |
+
line = line.replace(r"\n\n", "", 1)
|
| 22 |
+
|
| 23 |
+
fixed_lines.append(line)
|
| 24 |
+
|
| 25 |
+
fixed = "\n".join(fixed_lines) + "\n"
|
| 26 |
+
|
| 27 |
+
# Extra targeted safety for Phase 38 export block
|
| 28 |
+
fixed = fixed.replace(r"\n\n# =====================================================", "\n\n# =====================================================")
|
| 29 |
+
fixed = fixed.replace(r"\n\nfrom app.product.final_product_ui", "\n\nfrom app.product.final_product_ui")
|
| 30 |
+
|
| 31 |
+
path.write_text(fixed, encoding="utf-8")
|
| 32 |
+
|
| 33 |
+
print("Fixed literal newline syntax error in hf_status.py")
|
scripts/phase38_final_ui_stabilization.py
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
# Clean BOM
|
| 4 |
+
for path in Path("app").rglob("*.py"):
|
| 5 |
+
text = path.read_text(encoding="utf-8-sig")
|
| 6 |
+
text = text.replace("\ufeff", "")
|
| 7 |
+
path.write_text(text, encoding="utf-8")
|
| 8 |
+
|
| 9 |
+
Path("app/product").mkdir(parents=True, exist_ok=True)
|
| 10 |
+
Path("app/product/__init__.py").touch()
|
| 11 |
+
|
| 12 |
+
Path("app/product/final_product_ui.py").write_text(r'''
|
| 13 |
+
def get_home_html() -> str:
|
| 14 |
+
return """
|
| 15 |
+
<!DOCTYPE html>
|
| 16 |
+
<html>
|
| 17 |
+
<head>
|
| 18 |
+
<title>GraphResearcher</title>
|
| 19 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 20 |
+
<style>
|
| 21 |
+
* { box-sizing: border-box; }
|
| 22 |
+
body { margin: 0; font-family: Inter, Arial, sans-serif; background: #f8fafc; color: #0f172a; }
|
| 23 |
+
.nav { height: 72px; display: flex; align-items: center; justify-content: space-between; padding: 0 56px; background: white; border-bottom: 1px solid #e5e7eb; }
|
| 24 |
+
.brand { font-weight: 900; font-size: 24px; letter-spacing: -0.7px; }
|
| 25 |
+
.nav a { text-decoration: none; color: #334155; margin-left: 22px; font-weight: 800; }
|
| 26 |
+
.hero { min-height: calc(100vh - 72px); display: grid; grid-template-columns: 1fr 0.9fr; gap: 52px; align-items: center; padding: 64px 72px; }
|
| 27 |
+
.badge { display: inline-block; background: #dbeafe; color: #1d4ed8; padding: 8px 12px; border-radius: 999px; font-weight: 900; font-size: 13px; margin-bottom: 18px; }
|
| 28 |
+
h1 { font-size: 58px; line-height: 1.04; margin: 0 0 20px; letter-spacing: -2px; }
|
| 29 |
+
p { font-size: 19px; line-height: 1.7; color: #475569; max-width: 720px; }
|
| 30 |
+
.actions { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 32px; }
|
| 31 |
+
.btn { display: inline-block; text-decoration: none; background: #2563eb; color: white; padding: 14px 22px; border-radius: 12px; font-weight: 900; }
|
| 32 |
+
.btn.dark { background: #0f172a; }
|
| 33 |
+
.preview { background: #0f172a; color: white; border-radius: 26px; padding: 24px; box-shadow: 0 28px 70px rgba(15,23,42,0.24); }
|
| 34 |
+
.preview-card { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.12); border-radius: 18px; padding: 16px; margin-bottom: 14px; line-height: 1.55; }
|
| 35 |
+
.preview-card b { color: #bfdbfe; }
|
| 36 |
+
.features { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; padding: 0 72px 58px; }
|
| 37 |
+
.feature { background: white; border: 1px solid #e5e7eb; border-radius: 18px; padding: 24px; }
|
| 38 |
+
.feature p { font-size: 15px; color: #64748b; }
|
| 39 |
+
@media(max-width: 950px) {
|
| 40 |
+
.nav { padding: 0 22px; }
|
| 41 |
+
.hero { grid-template-columns: 1fr; padding: 42px 24px; }
|
| 42 |
+
h1 { font-size: 42px; }
|
| 43 |
+
.features { grid-template-columns: 1fr; padding: 0 24px 44px; }
|
| 44 |
+
}
|
| 45 |
+
</style>
|
| 46 |
+
</head>
|
| 47 |
+
<body>
|
| 48 |
+
<div class="nav">
|
| 49 |
+
<div class="brand">GraphResearcher</div>
|
| 50 |
+
<div><a href="/app">Launch App</a></div>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
<section class="hero">
|
| 54 |
+
<div>
|
| 55 |
+
<div class="badge">Document Chat + GraphRAG</div>
|
| 56 |
+
<h1>Upload documents. Ask questions. Verify every answer.</h1>
|
| 57 |
+
<p>
|
| 58 |
+
GraphResearcher lets users chat with PDFs and reports using citation-grounded retrieval,
|
| 59 |
+
graph context, source verification, graph view, and document comparison.
|
| 60 |
+
</p>
|
| 61 |
+
<div class="actions">
|
| 62 |
+
<a class="btn" href="/app">Start Chatting</a>
|
| 63 |
+
<a class="btn dark" href="/login">Login</a>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
<div class="preview">
|
| 68 |
+
<div class="preview-card"><b>1. Upload</b><br>Add a document from the app sidebar. No document ID needed.</div>
|
| 69 |
+
<div class="preview-card"><b>2. Chat</b><br>Ask natural questions and get source-backed answers.</div>
|
| 70 |
+
<div class="preview-card"><b>3. Verify</b><br>Open sources, see page/chunk metadata, and inspect the graph.</div>
|
| 71 |
+
<div class="preview-card"><b>4. Compare</b><br>Select a second document and compare them side by side.</div>
|
| 72 |
+
</div>
|
| 73 |
+
</section>
|
| 74 |
+
|
| 75 |
+
<section class="features">
|
| 76 |
+
<div class="feature"><h3>Simple workspace</h3><p>Upload, select, delete, and re-index documents from one clean UI.</p></div>
|
| 77 |
+
<div class="feature"><h3>Grounded answers</h3><p>Answers show evidence boxes and source cards for verification.</p></div>
|
| 78 |
+
<div class="feature"><h3>GraphRAG inside</h3><p>Graph view and graph retrieval stay available without exposing developer tools.</p></div>
|
| 79 |
+
</section>
|
| 80 |
+
</body>
|
| 81 |
+
</html>
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def get_product_app_html() -> str:
|
| 86 |
+
return """
|
| 87 |
+
<!DOCTYPE html>
|
| 88 |
+
<html>
|
| 89 |
+
<head>
|
| 90 |
+
<title>GraphResearcher App</title>
|
| 91 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 92 |
+
|
| 93 |
+
<style>
|
| 94 |
+
* { box-sizing: border-box; }
|
| 95 |
+
body { margin: 0; font-family: Inter, Arial, sans-serif; background: #f8fafc; color: #111827; }
|
| 96 |
+
.app { display: grid; grid-template-columns: 310px 1fr 370px; height: 100vh; }
|
| 97 |
+
.sidebar { background: #0f172a; color: white; padding: 18px; overflow-y: auto; }
|
| 98 |
+
.brand { font-size: 24px; font-weight: 900; margin-bottom: 18px; letter-spacing: -0.7px; }
|
| 99 |
+
.upload-box { border: 1px dashed rgba(255,255,255,0.35); border-radius: 16px; padding: 14px; background: rgba(255,255,255,0.06); margin-bottom: 20px; }
|
| 100 |
+
.upload-box input { width: 100%; font-size: 12px; margin: 10px 0; }
|
| 101 |
+
button { border: none; border-radius: 10px; padding: 10px 13px; cursor: pointer; background: #2563eb; color: white; font-weight: 800; }
|
| 102 |
+
button:hover { background: #1d4ed8; }
|
| 103 |
+
button.secondary { background: #334155; }
|
| 104 |
+
button.green { background: #059669; }
|
| 105 |
+
button.red { background: #dc2626; }
|
| 106 |
+
button.light { background: #f1f5f9; color: #0f172a; border: 1px solid #cbd5e1; }
|
| 107 |
+
.full { width: 100%; margin-bottom: 9px; }
|
| 108 |
+
.small { font-size: 12px; color: #94a3b8; line-height: 1.5; }
|
| 109 |
+
.doc-card { padding: 12px; border-radius: 13px; background: rgba(255,255,255,0.07); margin-bottom: 9px; cursor: pointer; border: 1px solid transparent; }
|
| 110 |
+
.doc-card.active { background: #2563eb; border-color: #93c5fd; }
|
| 111 |
+
.doc-title { font-size: 14px; font-weight: 800; word-break: break-word; }
|
| 112 |
+
.doc-meta { font-size: 11px; color: #cbd5e1; margin-top: 4px; }
|
| 113 |
+
.main { display: flex; flex-direction: column; height: 100vh; }
|
| 114 |
+
.topbar { padding: 14px 20px; border-bottom: 1px solid #e5e7eb; background: white; display: flex; justify-content: space-between; gap: 14px; align-items: center; }
|
| 115 |
+
.selected-doc { font-weight: 900; font-size: 17px; }
|
| 116 |
+
.selected-doc span { display: block; color: #64748b; font-weight: 500; font-size: 12px; margin-top: 2px; }
|
| 117 |
+
.status-pill { font-size: 12px; padding: 7px 10px; border-radius: 999px; background: #e0f2fe; color: #075985; white-space: nowrap; font-weight: 800; }
|
| 118 |
+
.messages { flex: 1; overflow-y: auto; padding: 26px; }
|
| 119 |
+
.message { max-width: 940px; margin-bottom: 18px; display: flex; }
|
| 120 |
+
.message.user { margin-left: auto; justify-content: flex-end; }
|
| 121 |
+
.bubble { padding: 15px 17px; border-radius: 17px; line-height: 1.65; white-space: pre-wrap; font-size: 15.5px; }
|
| 122 |
+
.user .bubble { background: #2563eb; color: white; border-bottom-right-radius: 4px; }
|
| 123 |
+
.assistant .bubble { background: white; border: 1px solid #e5e7eb; color: #111827; border-bottom-left-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
| 124 |
+
.empty { max-width: 760px; margin: 70px auto; text-align: center; color: #475569; }
|
| 125 |
+
.empty h1 { color: #0f172a; }
|
| 126 |
+
.composer { padding: 16px 20px; background: white; border-top: 1px solid #e5e7eb; }
|
| 127 |
+
.composer-box { display: flex; gap: 10px; align-items: flex-end; }
|
| 128 |
+
textarea { flex: 1; resize: none; min-height: 54px; max-height: 160px; padding: 13px; border: 1px solid #cbd5e1; border-radius: 12px; font-size: 15px; }
|
| 129 |
+
select { width: 100%; padding: 9px; border-radius: 9px; border: 1px solid #cbd5e1; background: white; }
|
| 130 |
+
.right-panel { background: white; border-left: 1px solid #e5e7eb; padding: 16px; overflow-y: auto; }
|
| 131 |
+
.panel-section { margin-bottom: 20px; }
|
| 132 |
+
.panel-section h3 { margin-bottom: 8px; font-size: 16px; }
|
| 133 |
+
.toggle-row { display: flex; flex-direction: column; gap: 8px; font-size: 14px; }
|
| 134 |
+
.metric { display: inline-block; background: #eef2ff; color: #3730a3; padding: 5px 8px; border-radius: 999px; font-size: 12px; margin: 3px; }
|
| 135 |
+
.citation-card { border: 1px solid #e5e7eb; background: #f8fafc; border-radius: 12px; padding: 12px; margin-bottom: 11px; font-size: 13px; }
|
| 136 |
+
.source-line { margin-top: 5px; color: #475569; }
|
| 137 |
+
.preview-text { margin-top: 8px; color: #64748b; font-size: 12px; line-height: 1.5; }
|
| 138 |
+
.answer-card { line-height: 1.72; }
|
| 139 |
+
.answer-card h2 { margin: 0 0 10px; font-size: 18px; color: #0f172a; }
|
| 140 |
+
.answer-card h3 { margin: 18px 0 8px; font-size: 15px; color: #1d4ed8; }
|
| 141 |
+
.answer-card p { margin: 8px 0; }
|
| 142 |
+
.evidence-box { background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 12px; padding: 11px; margin-top: 9px; font-size: 13px; color: #475569; }
|
| 143 |
+
.source-chip { display: inline-block; background: #eef2ff; color: #3730a3; padding: 3px 7px; border-radius: 999px; font-size: 12px; margin: 2px; font-weight: 700; }
|
| 144 |
+
.warning { background: #fff7ed; border: 1px solid #fed7aa; color: #9a3412; padding: 10px; border-radius: 12px; margin: 10px 0; }
|
| 145 |
+
.compare-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 12px; }
|
| 146 |
+
.compare-card { border: 1px solid #e5e7eb; background: #f8fafc; border-radius: 14px; padding: 14px; line-height: 1.6; }
|
| 147 |
+
.danger-zone { border-top: 1px solid #e5e7eb; padding-top: 14px; }
|
| 148 |
+
@media (max-width: 1150px) { .app { grid-template-columns: 290px 1fr; } .right-panel { display: none; } }
|
| 149 |
+
@media (max-width: 850px) { .compare-grid { grid-template-columns: 1fr; } }
|
| 150 |
+
@media (max-width: 760px) { .app { grid-template-columns: 1fr; } .sidebar { display: none; } }
|
| 151 |
+
</style>
|
| 152 |
+
</head>
|
| 153 |
+
|
| 154 |
+
<body>
|
| 155 |
+
<div class="app">
|
| 156 |
+
<aside class="sidebar">
|
| 157 |
+
<div class="brand">GraphResearcher</div>
|
| 158 |
+
|
| 159 |
+
<div class="upload-box">
|
| 160 |
+
<b>Upload document</b>
|
| 161 |
+
<p class="small">Upload, select, and chat. If Hugging Face rebuilt recently, clear cache and re-upload.</p>
|
| 162 |
+
<input id="fileInput" type="file">
|
| 163 |
+
<button class="full green" onclick="uploadDocument()">Upload & Select</button>
|
| 164 |
+
<button class="full secondary" onclick="refreshDocuments()">Refresh Documents</button>
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
+
<div class="small" style="margin-bottom:8px;">My Documents</div>
|
| 168 |
+
<div id="documentList"></div>
|
| 169 |
+
|
| 170 |
+
<hr style="border-color:rgba(255,255,255,0.15); margin:18px 0;">
|
| 171 |
+
|
| 172 |
+
<button class="full secondary" onclick="newChat()">+ New Chat</button>
|
| 173 |
+
<button class="full secondary" onclick="reindexSelectedDocument()">Re-index Selected</button>
|
| 174 |
+
<button class="full secondary" onclick="buildGraph()">Build / Rebuild Graph</button>
|
| 175 |
+
<button class="full secondary" onclick="openGraphViewer()">View Graph</button>
|
| 176 |
+
<button class="full secondary" onclick="clearWorkspaceCache()">Clear Workspace Cache</button>
|
| 177 |
+
<button class="full secondary" onclick="window.location.href='/'">Home</button>
|
| 178 |
+
</aside>
|
| 179 |
+
|
| 180 |
+
<main class="main">
|
| 181 |
+
<div class="topbar">
|
| 182 |
+
<div class="selected-doc" id="selectedDocTitle">
|
| 183 |
+
No document selected
|
| 184 |
+
<span>Upload or select a document from the left sidebar.</span>
|
| 185 |
+
</div>
|
| 186 |
+
<span id="appStatus" class="status-pill">Ready</span>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<div id="messages" class="messages"></div>
|
| 190 |
+
|
| 191 |
+
<div class="composer">
|
| 192 |
+
<div class="composer-box">
|
| 193 |
+
<textarea id="messageInput" placeholder="Ask about the selected document..." onkeydown="handleKeyDown(event)"></textarea>
|
| 194 |
+
<button onclick="sendMessage()">Send</button>
|
| 195 |
+
</div>
|
| 196 |
+
<div class="small" style="margin-top:8px;color:#64748b;">
|
| 197 |
+
Tip: choose a second document in the right panel to compare two documents.
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
</main>
|
| 201 |
+
|
| 202 |
+
<aside class="right-panel">
|
| 203 |
+
<div class="panel-section">
|
| 204 |
+
<h3>Selected Document</h3>
|
| 205 |
+
<div id="docDetails" class="small" style="color:#64748b;">No document selected.</div>
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
<div class="panel-section">
|
| 209 |
+
<h3>Compare With</h3>
|
| 210 |
+
<select id="compareDocumentSelect" onchange="renderSelectedDocument()">
|
| 211 |
+
<option value="">No comparison</option>
|
| 212 |
+
</select>
|
| 213 |
+
<p class="small" style="color:#64748b;">Choose another document for side-by-side comparison.</p>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<div class="panel-section">
|
| 217 |
+
<h3>Graph</h3>
|
| 218 |
+
<button class="green" onclick="buildGraph()">Build / Rebuild Graph</button>
|
| 219 |
+
<button class="secondary" onclick="openGraphViewer()">View Graph</button>
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<div class="panel-section">
|
| 223 |
+
<h3>Answer Style</h3>
|
| 224 |
+
<select id="answerStyle">
|
| 225 |
+
<option value="detailed" selected>Detailed</option>
|
| 226 |
+
<option value="step_by_step">Step-by-step</option>
|
| 227 |
+
<option value="concise">Concise</option>
|
| 228 |
+
<option value="research">Research style</option>
|
| 229 |
+
<option value="comparison">Comparison focused</option>
|
| 230 |
+
</select>
|
| 231 |
+
</div>
|
| 232 |
+
|
| 233 |
+
<div class="panel-section">
|
| 234 |
+
<h3>Advanced Settings</h3>
|
| 235 |
+
<div class="toggle-row">
|
| 236 |
+
<label><input type="checkbox" id="useLLM" checked> Use LLM</label>
|
| 237 |
+
<label><input type="checkbox" id="useReranker" checked> Use reranker</label>
|
| 238 |
+
<label><input type="checkbox" id="useGraph" checked> Use graph context</label>
|
| 239 |
+
<label><input type="checkbox" id="useGraphRetrieval" checked> Use graph retrieval fusion</label>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
|
| 243 |
+
<div class="panel-section">
|
| 244 |
+
<h3>Last Answer Metrics</h3>
|
| 245 |
+
<div id="metricsBox"><span class="metric">No answer yet</span></div>
|
| 246 |
+
</div>
|
| 247 |
+
|
| 248 |
+
<div class="panel-section">
|
| 249 |
+
<h3>Sources</h3>
|
| 250 |
+
<div id="citationsBox" class="small" style="color:#64748b;">Sources will appear here after an answer.</div>
|
| 251 |
+
</div>
|
| 252 |
+
|
| 253 |
+
<div class="panel-section danger-zone">
|
| 254 |
+
<h3>Danger Zone</h3>
|
| 255 |
+
<button class="red" onclick="deleteSelectedDocument()">Delete Selected Document</button>
|
| 256 |
+
</div>
|
| 257 |
+
</aside>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
<script>
|
| 261 |
+
let documents = JSON.parse(localStorage.getItem("graphrag_documents") || "[]");
|
| 262 |
+
let selectedDocumentId = localStorage.getItem("graphrag_selected_document_id") || null;
|
| 263 |
+
let conversations = JSON.parse(localStorage.getItem("graphrag_final_conversations") || "{}");
|
| 264 |
+
let lastSources = [];
|
| 265 |
+
|
| 266 |
+
function saveDocuments() {
|
| 267 |
+
localStorage.setItem("graphrag_documents", JSON.stringify(documents));
|
| 268 |
+
if (selectedDocumentId) localStorage.setItem("graphrag_selected_document_id", selectedDocumentId);
|
| 269 |
+
}
|
| 270 |
+
function saveConversations() { localStorage.setItem("graphrag_final_conversations", JSON.stringify(conversations)); }
|
| 271 |
+
function setStatus(text) { document.getElementById("appStatus").textContent = text; }
|
| 272 |
+
function getSelectedDocument() { return documents.find(d => d.id === selectedDocumentId) || null; }
|
| 273 |
+
function getCompareDocument() {
|
| 274 |
+
const id = document.getElementById("compareDocumentSelect")?.value || "";
|
| 275 |
+
if (!id) return null;
|
| 276 |
+
return documents.find(d => d.id === id) || null;
|
| 277 |
+
}
|
| 278 |
+
function escapeHtml(value) {
|
| 279 |
+
return String(value || "").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""");
|
| 280 |
+
}
|
| 281 |
+
function parseDocumentId(data) {
|
| 282 |
+
return data.document_id || data.id || data.doc_id || data.documentId ||
|
| 283 |
+
data.document?.document_id || data.document?.id ||
|
| 284 |
+
data.file?.document_id || data.result?.document_id || data.data?.document_id;
|
| 285 |
+
}
|
| 286 |
+
async function tryEndpoints(candidates, optionsFactory) {
|
| 287 |
+
let lastError = null;
|
| 288 |
+
for (const candidate of candidates) {
|
| 289 |
+
try {
|
| 290 |
+
const response = await fetch(candidate, optionsFactory(candidate));
|
| 291 |
+
const contentType = response.headers.get("content-type") || "";
|
| 292 |
+
const data = contentType.includes("application/json") ? await response.json() : await response.text();
|
| 293 |
+
if (response.ok) return { ok: true, endpoint: candidate, data };
|
| 294 |
+
lastError = { endpoint: candidate, status: response.status, data };
|
| 295 |
+
} catch (error) {
|
| 296 |
+
lastError = { endpoint: candidate, error: error.message };
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
return { ok: false, error: lastError };
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
function renderDocuments() {
|
| 303 |
+
const list = document.getElementById("documentList");
|
| 304 |
+
list.innerHTML = "";
|
| 305 |
+
|
| 306 |
+
if (documents.length === 0) list.innerHTML = `<div class="small">No documents yet. Upload one above.</div>`;
|
| 307 |
+
|
| 308 |
+
documents.forEach(doc => {
|
| 309 |
+
const div = document.createElement("div");
|
| 310 |
+
div.className = "doc-card" + (doc.id === selectedDocumentId ? " active" : "");
|
| 311 |
+
div.onclick = () => selectDocument(doc.id);
|
| 312 |
+
div.innerHTML = `<div class="doc-title">${escapeHtml(doc.name || "Untitled document")}</div>
|
| 313 |
+
<div class="doc-meta">${escapeHtml(doc.status || "uploaded")} • ${escapeHtml(doc.graphStatus || "graph not built")}</div>`;
|
| 314 |
+
list.appendChild(div);
|
| 315 |
+
});
|
| 316 |
+
|
| 317 |
+
renderCompareDropdown();
|
| 318 |
+
renderSelectedDocument();
|
| 319 |
+
}
|
| 320 |
+
function renderCompareDropdown() {
|
| 321 |
+
const select = document.getElementById("compareDocumentSelect");
|
| 322 |
+
if (!select) return;
|
| 323 |
+
|
| 324 |
+
const oldValue = select.value;
|
| 325 |
+
select.innerHTML = `<option value="">No comparison</option>`;
|
| 326 |
+
|
| 327 |
+
documents.filter(d => d.id !== selectedDocumentId).forEach(doc => {
|
| 328 |
+
const option = document.createElement("option");
|
| 329 |
+
option.value = doc.id;
|
| 330 |
+
option.textContent = doc.name || "Untitled document";
|
| 331 |
+
select.appendChild(option);
|
| 332 |
+
});
|
| 333 |
+
|
| 334 |
+
if ([...select.options].some(o => o.value === oldValue)) select.value = oldValue;
|
| 335 |
+
}
|
| 336 |
+
function renderSelectedDocument() {
|
| 337 |
+
const doc = getSelectedDocument();
|
| 338 |
+
const compareDoc = getCompareDocument();
|
| 339 |
+
const title = document.getElementById("selectedDocTitle");
|
| 340 |
+
const details = document.getElementById("docDetails");
|
| 341 |
+
|
| 342 |
+
if (!doc) {
|
| 343 |
+
title.innerHTML = `No document selected <span>Upload or select a document from the left sidebar.</span>`;
|
| 344 |
+
details.textContent = "No document selected.";
|
| 345 |
+
renderMessages();
|
| 346 |
+
return;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
const subtitle = compareDoc ? `Compare mode active with ${compareDoc.name || "second document"}.` : "Ready for document chat.";
|
| 350 |
+
title.innerHTML = `${escapeHtml(doc.name || "Untitled document")} <span>${escapeHtml(subtitle)}</span>`;
|
| 351 |
+
details.innerHTML = `<b>Name:</b> ${escapeHtml(doc.name || "Untitled")}<br>
|
| 352 |
+
<b>Status:</b> ${escapeHtml(doc.status || "uploaded")}<br>
|
| 353 |
+
<b>Graph:</b> ${escapeHtml(doc.graphStatus || "not built")}<br>
|
| 354 |
+
<b>Uploaded:</b> ${escapeHtml(doc.uploadedAt || "unknown")}
|
| 355 |
+
${compareDoc ? `<br><br><b>Comparing with:</b> ${escapeHtml(compareDoc.name || "Untitled")}` : ""}`;
|
| 356 |
+
renderMessages();
|
| 357 |
+
}
|
| 358 |
+
function selectDocument(id) {
|
| 359 |
+
selectedDocumentId = id;
|
| 360 |
+
saveDocuments();
|
| 361 |
+
renderDocuments();
|
| 362 |
+
}
|
| 363 |
+
function getConversationKey() {
|
| 364 |
+
const compareDoc = getCompareDocument();
|
| 365 |
+
if (compareDoc) return `${selectedDocumentId}__compare__${compareDoc.id}`;
|
| 366 |
+
return selectedDocumentId || "default";
|
| 367 |
+
}
|
| 368 |
+
function getConversation() {
|
| 369 |
+
const key = getConversationKey();
|
| 370 |
+
if (!conversations[key]) conversations[key] = [];
|
| 371 |
+
return conversations[key];
|
| 372 |
+
}
|
| 373 |
+
function newChat() {
|
| 374 |
+
if (!selectedDocumentId) return alert("Select a document first.");
|
| 375 |
+
conversations[getConversationKey()] = [];
|
| 376 |
+
saveConversations();
|
| 377 |
+
renderMessages();
|
| 378 |
+
}
|
| 379 |
+
function renderMessages() {
|
| 380 |
+
const box = document.getElementById("messages");
|
| 381 |
+
const doc = getSelectedDocument();
|
| 382 |
+
const compareDoc = getCompareDocument();
|
| 383 |
+
|
| 384 |
+
if (!doc) {
|
| 385 |
+
box.innerHTML = `<div class="empty"><h1>Upload a document to start</h1><p>No document ID needed. Upload a file from the left sidebar, then chat normally.</p></div>`;
|
| 386 |
+
return;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
const convo = getConversation();
|
| 390 |
+
if (convo.length === 0) {
|
| 391 |
+
box.innerHTML = `<div class="empty"><h1>${compareDoc ? "Compare documents" : "Chat with your document"}</h1>
|
| 392 |
+
<p>${compareDoc ? `You are comparing ${escapeHtml(doc.name)} with ${escapeHtml(compareDoc.name)}.` : `Ask a question about ${escapeHtml(doc.name)}.`}</p></div>`;
|
| 393 |
+
return;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
box.innerHTML = "";
|
| 397 |
+
convo.forEach(msg => {
|
| 398 |
+
const wrapper = document.createElement("div");
|
| 399 |
+
wrapper.className = "message " + msg.role;
|
| 400 |
+
|
| 401 |
+
const bubble = document.createElement("div");
|
| 402 |
+
bubble.className = msg.type === "compare" ? "bubble" : "bubble";
|
| 403 |
+
|
| 404 |
+
if (msg.role === "assistant" && msg.html) bubble.innerHTML = msg.html;
|
| 405 |
+
else bubble.textContent = msg.content || "";
|
| 406 |
+
|
| 407 |
+
wrapper.appendChild(bubble);
|
| 408 |
+
box.appendChild(wrapper);
|
| 409 |
+
});
|
| 410 |
+
|
| 411 |
+
box.scrollTop = box.scrollHeight;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
async function uploadDocument() {
|
| 415 |
+
const file = document.getElementById("fileInput").files[0];
|
| 416 |
+
if (!file) return alert("Choose a file first.");
|
| 417 |
+
|
| 418 |
+
setStatus("Uploading...");
|
| 419 |
+
const formData = new FormData();
|
| 420 |
+
formData.append("file", file);
|
| 421 |
+
|
| 422 |
+
const result = await tryEndpoints(["/documents/upload", "/upload", "/documents", "/api/documents/upload"], () => ({ method: "POST", body: formData }));
|
| 423 |
+
if (!result.ok) {
|
| 424 |
+
setStatus("Upload failed");
|
| 425 |
+
return alert("Upload failed: " + JSON.stringify(result.error));
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
const documentId = parseDocumentId(result.data);
|
| 429 |
+
if (!documentId) {
|
| 430 |
+
setStatus("Upload returned no document ID");
|
| 431 |
+
return alert("Upload worked but document ID was not found in response.");
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
const doc = { id: documentId, name: file.name, status: "uploaded", graphStatus: "not built", uploadedAt: new Date().toLocaleString() };
|
| 435 |
+
const index = documents.findIndex(d => d.id === documentId);
|
| 436 |
+
if (index >= 0) documents[index] = doc;
|
| 437 |
+
else documents.unshift(doc);
|
| 438 |
+
|
| 439 |
+
selectedDocumentId = documentId;
|
| 440 |
+
saveDocuments();
|
| 441 |
+
renderDocuments();
|
| 442 |
+
|
| 443 |
+
setStatus("Uploaded");
|
| 444 |
+
await autoIndexDocument(documentId);
|
| 445 |
+
await buildGraph(true);
|
| 446 |
+
}
|
| 447 |
+
async function autoIndexDocument(documentId) {
|
| 448 |
+
setStatus("Indexing...");
|
| 449 |
+
const result = await tryEndpoints([`/documents/${documentId}/index`, `/documents/${documentId}/process`, `/documents/${documentId}/ingest`, `/index/${documentId}`], () => ({ method: "POST" }));
|
| 450 |
+
const doc = documents.find(d => d.id === documentId);
|
| 451 |
+
if (doc) {
|
| 452 |
+
doc.status = result.ok ? "indexed" : "uploaded";
|
| 453 |
+
saveDocuments();
|
| 454 |
+
renderDocuments();
|
| 455 |
+
}
|
| 456 |
+
setStatus(result.ok ? "Indexed" : "Uploaded");
|
| 457 |
+
}
|
| 458 |
+
async function reindexSelectedDocument() {
|
| 459 |
+
const doc = getSelectedDocument();
|
| 460 |
+
if (!doc) return alert("Select a document first.");
|
| 461 |
+
await autoIndexDocument(doc.id);
|
| 462 |
+
await buildGraph(false);
|
| 463 |
+
alert("Re-index attempt complete. Ask again now.");
|
| 464 |
+
}
|
| 465 |
+
async function buildGraph(silent = false) {
|
| 466 |
+
const doc = getSelectedDocument();
|
| 467 |
+
if (!doc) {
|
| 468 |
+
if (!silent) alert("Select a document first.");
|
| 469 |
+
return;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
setStatus("Building graph...");
|
| 473 |
+
try {
|
| 474 |
+
const response = await fetch(`/documents/${doc.id}/graph/build`, { method: "POST" });
|
| 475 |
+
const data = await response.json();
|
| 476 |
+
if (!response.ok) throw new Error(JSON.stringify(data));
|
| 477 |
+
|
| 478 |
+
doc.graphStatus = "graph built";
|
| 479 |
+
doc.graphData = { entities: data.total_entities ?? data.entity_count ?? null, relations: data.total_relations ?? data.relation_count ?? null };
|
| 480 |
+
saveDocuments();
|
| 481 |
+
renderDocuments();
|
| 482 |
+
|
| 483 |
+
document.getElementById("metricsBox").innerHTML = `<span class="metric">graph built</span>
|
| 484 |
+
<span class="metric">entities: ${doc.graphData.entities ?? "NA"}</span>
|
| 485 |
+
<span class="metric">relations: ${doc.graphData.relations ?? "NA"}</span>`;
|
| 486 |
+
|
| 487 |
+
setStatus("Graph ready");
|
| 488 |
+
} catch (error) {
|
| 489 |
+
doc.graphStatus = "graph not built";
|
| 490 |
+
saveDocuments();
|
| 491 |
+
renderDocuments();
|
| 492 |
+
setStatus("Graph skipped");
|
| 493 |
+
if (!silent) alert("Graph build failed. Re-index or re-upload if needed.");
|
| 494 |
+
}
|
| 495 |
+
}
|
| 496 |
+
function openGraphViewer() {
|
| 497 |
+
const doc = getSelectedDocument();
|
| 498 |
+
if (!doc) return alert("Select a document first.");
|
| 499 |
+
window.open(`/documents/${doc.id}/graph/view`, "_blank");
|
| 500 |
+
}
|
| 501 |
+
async function deleteSelectedDocument() {
|
| 502 |
+
const doc = getSelectedDocument();
|
| 503 |
+
if (!doc) return alert("Select a document first.");
|
| 504 |
+
if (!confirm(`Delete "${doc.name}"?`)) return;
|
| 505 |
+
|
| 506 |
+
setStatus("Deleting...");
|
| 507 |
+
await tryEndpoints([`/documents/${doc.id}/delete`, `/documents/${doc.id}`, `/api/documents/${doc.id}`], () => ({ method: "DELETE" }));
|
| 508 |
+
|
| 509 |
+
documents = documents.filter(d => d.id !== doc.id);
|
| 510 |
+
Object.keys(conversations).forEach(k => { if (k.includes(doc.id)) delete conversations[k]; });
|
| 511 |
+
selectedDocumentId = documents.length ? documents[0].id : null;
|
| 512 |
+
saveDocuments();
|
| 513 |
+
saveConversations();
|
| 514 |
+
renderDocuments();
|
| 515 |
+
renderMessages();
|
| 516 |
+
setStatus("Deleted");
|
| 517 |
+
}
|
| 518 |
+
function clearWorkspaceCache() {
|
| 519 |
+
if (!confirm("Clear browser document list and chat history? Use this after Hugging Face rebuilds.")) return;
|
| 520 |
+
["graphrag_documents", "graphrag_selected_document_id", "graphrag_conversations", "graphrag_conversations_v2", "graphrag_conversations_v3", "graphrag_final_conversations"].forEach(k => localStorage.removeItem(k));
|
| 521 |
+
documents = [];
|
| 522 |
+
selectedDocumentId = null;
|
| 523 |
+
conversations = {};
|
| 524 |
+
renderDocuments();
|
| 525 |
+
renderMessages();
|
| 526 |
+
setStatus("Cache cleared");
|
| 527 |
+
}
|
| 528 |
+
function refreshDocuments() { renderDocuments(); setStatus("Refreshed"); }
|
| 529 |
+
function handleKeyDown(event) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); sendMessage(); } }
|
| 530 |
+
|
| 531 |
+
function buildContextualQuery(currentQuestion) {
|
| 532 |
+
const clean = String(currentQuestion || "").trim();
|
| 533 |
+
if (!clean) return clean;
|
| 534 |
+
|
| 535 |
+
const wordCount = clean.split(/\\s+/).filter(Boolean).length;
|
| 536 |
+
if (wordCount <= 8) {
|
| 537 |
+
const prev = getConversation().filter(m => m.role === "user").slice(-2).map(m => m.content).join(" ");
|
| 538 |
+
return prev ? prev + " " + clean : clean;
|
| 539 |
+
}
|
| 540 |
+
return clean;
|
| 541 |
+
}
|
| 542 |
+
function askPayload(query, documentId) {
|
| 543 |
+
return {
|
| 544 |
+
query,
|
| 545 |
+
document_id: documentId,
|
| 546 |
+
top_k: 8,
|
| 547 |
+
retrieval_mode: "hybrid",
|
| 548 |
+
use_reranker: document.getElementById("useReranker").checked,
|
| 549 |
+
use_llm: document.getElementById("useLLM").checked,
|
| 550 |
+
use_graph: document.getElementById("useGraph").checked,
|
| 551 |
+
graph_entity_limit: 12,
|
| 552 |
+
use_graph_retrieval: document.getElementById("useGraphRetrieval").checked,
|
| 553 |
+
graph_retrieval_top_k: 6
|
| 554 |
+
};
|
| 555 |
+
}
|
| 556 |
+
async function callAsk(payload) {
|
| 557 |
+
const response = await fetch("/ask", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
|
| 558 |
+
const data = await response.json();
|
| 559 |
+
if (!response.ok) throw new Error(JSON.stringify(data));
|
| 560 |
+
return data;
|
| 561 |
+
}
|
| 562 |
+
function valueFrom(obj, keys, fallback = "Not available") {
|
| 563 |
+
for (const key of keys) if (obj && obj[key] !== undefined && obj[key] !== null && obj[key] !== "") return obj[key];
|
| 564 |
+
if (obj && obj.metadata) for (const key of keys) if (obj.metadata[key] !== undefined && obj.metadata[key] !== null && obj.metadata[key] !== "") return obj.metadata[key];
|
| 565 |
+
return fallback;
|
| 566 |
+
}
|
| 567 |
+
function buildSources(data, doc) {
|
| 568 |
+
const sources = [];
|
| 569 |
+
(data.citations || []).forEach(x => sources.push(x));
|
| 570 |
+
(data.retrieval_fusion?.fused_results || []).forEach(x => sources.push(x));
|
| 571 |
+
(data.sources || data.source_chunks || data.retrieved_sources || []).forEach(x => sources.push(x));
|
| 572 |
+
|
| 573 |
+
const cleaned = [];
|
| 574 |
+
const seen = new Set();
|
| 575 |
+
|
| 576 |
+
sources.forEach((src, i) => {
|
| 577 |
+
const sid = valueFrom(src, ["source_id", "id", "citation_id"], "S" + (i + 1));
|
| 578 |
+
const chunk = valueFrom(src, ["chunk_id", "source_chunk_id", "chunk"], sid);
|
| 579 |
+
const page = valueFrom(src, ["page_number", "page", "page_no"], "Not available");
|
| 580 |
+
const key = doc.id + "|" + sid + "|" + chunk + "|" + page;
|
| 581 |
+
if (seen.has(key)) return;
|
| 582 |
+
seen.add(key);
|
| 583 |
+
|
| 584 |
+
cleaned.push({
|
| 585 |
+
source_id: sid,
|
| 586 |
+
document_id: doc.id,
|
| 587 |
+
document_name: valueFrom(src, ["document_name", "source_file_name", "file_name", "filename", "document_title"], doc.name || "Selected document"),
|
| 588 |
+
page,
|
| 589 |
+
chunk_id: chunk,
|
| 590 |
+
preview: valueFrom(src, ["text_preview", "preview", "chunk_preview", "content_preview", "text", "content"], "Preview not available"),
|
| 591 |
+
raw: src
|
| 592 |
+
});
|
| 593 |
+
});
|
| 594 |
+
|
| 595 |
+
return cleaned.slice(0, 8);
|
| 596 |
+
}
|
| 597 |
+
function updateCitations(groups) {
|
| 598 |
+
const box = document.getElementById("citationsBox");
|
| 599 |
+
lastSources = [];
|
| 600 |
+
box.innerHTML = "";
|
| 601 |
+
|
| 602 |
+
groups.forEach(group => {
|
| 603 |
+
const h = document.createElement("div");
|
| 604 |
+
h.innerHTML = `<h4>${escapeHtml(group.label)}</h4>`;
|
| 605 |
+
box.appendChild(h);
|
| 606 |
+
|
| 607 |
+
group.sources.forEach(src => {
|
| 608 |
+
const idx = lastSources.length;
|
| 609 |
+
lastSources.push(src);
|
| 610 |
+
const card = document.createElement("div");
|
| 611 |
+
card.className = "citation-card";
|
| 612 |
+
card.innerHTML = `<b>Source ${idx + 1}: ${escapeHtml(src.source_id)}</b>
|
| 613 |
+
<div class="source-line"><b>Document:</b> ${escapeHtml(src.document_name)}</div>
|
| 614 |
+
<div class="source-line"><b>Page:</b> ${escapeHtml(src.page)}</div>
|
| 615 |
+
<div class="source-line"><b>Chunk:</b> ${escapeHtml(src.chunk_id)}</div>
|
| 616 |
+
<div class="preview-text">${escapeHtml(String(src.preview).slice(0, 260))}</div><br>
|
| 617 |
+
<button class="light" onclick="openSource(${idx})">Open source</button>`;
|
| 618 |
+
box.appendChild(card);
|
| 619 |
+
});
|
| 620 |
+
});
|
| 621 |
+
|
| 622 |
+
if (!lastSources.length) box.textContent = "No source details returned.";
|
| 623 |
+
}
|
| 624 |
+
function openSource(index) {
|
| 625 |
+
const src = lastSources[index];
|
| 626 |
+
if (!src) return;
|
| 627 |
+
window.open(`/documents/${src.document_id}/sources/${encodeURIComponent(src.source_id)}/view?page=${encodeURIComponent(src.page || "")}&chunk_id=${encodeURIComponent(src.chunk_id || "")}`, "_blank");
|
| 628 |
+
}
|
| 629 |
+
function updateMetrics(data, label) {
|
| 630 |
+
const fusion = data.retrieval_fusion || {};
|
| 631 |
+
const graph = data.graph_context || {};
|
| 632 |
+
document.getElementById("metricsBox").innerHTML += `<div style="margin-bottom:8px;"><b>${escapeHtml(label || "")}</b><br>
|
| 633 |
+
<span class="metric">strategy: ${escapeHtml(data.answer_strategy || "NA")}</span>
|
| 634 |
+
<span class="metric">LLM: ${data.used_llm}</span>
|
| 635 |
+
<span class="metric">graph: ${data.graph_used}</span>
|
| 636 |
+
<span class="metric">graph available: ${graph.graph_available}</span>
|
| 637 |
+
<span class="metric">fusion: ${fusion.fusion_used}</span></div>`;
|
| 638 |
+
}
|
| 639 |
+
function answerLooksWeak(answer) {
|
| 640 |
+
const words = String(answer || "").split(/\\s+/).filter(Boolean);
|
| 641 |
+
return words.length < 100;
|
| 642 |
+
}
|
| 643 |
+
function renderAnswerHtml(question, data, doc) {
|
| 644 |
+
const answer = String(data.answer || "I could not generate an answer.").trim();
|
| 645 |
+
const sources = buildSources(data, doc);
|
| 646 |
+
|
| 647 |
+
let html = `<div class="answer-card">`;
|
| 648 |
+
|
| 649 |
+
if (answer.toLowerCase().includes("i could not find relevant indexed sources")) {
|
| 650 |
+
html += `<h2>I could not find indexed evidence</h2>
|
| 651 |
+
<div class="warning">This usually means the browser remembers an old document but backend files were reset. Clear cache and upload again.</div></div>`;
|
| 652 |
+
return html;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
html += `<h2>Answer</h2>`;
|
| 656 |
+
|
| 657 |
+
const escaped = escapeHtml(answer).replaceAll("\\n", "<br>");
|
| 658 |
+
html += `<p>${escaped}</p>`;
|
| 659 |
+
|
| 660 |
+
if (sources.length) {
|
| 661 |
+
html += `<h3>Evidence used</h3>`;
|
| 662 |
+
sources.slice(0, 5).forEach((src, i) => {
|
| 663 |
+
html += `<div class="evidence-box"><b>[${escapeHtml(src.source_id)}]</b>
|
| 664 |
+
<span class="source-chip">Page: ${escapeHtml(src.page)}</span>
|
| 665 |
+
<span class="source-chip">Chunk: ${escapeHtml(src.chunk_id)}</span>
|
| 666 |
+
<div style="margin-top:6px;">${escapeHtml(String(src.preview).slice(0, 320))}</div></div>`;
|
| 667 |
+
});
|
| 668 |
+
} else {
|
| 669 |
+
html += `<div class="warning">No source metadata was returned. Re-index or re-upload if this seems wrong.</div>`;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
html += `</div>`;
|
| 673 |
+
return html;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
async function sendMessage() {
|
| 677 |
+
const doc = getSelectedDocument();
|
| 678 |
+
const compareDoc = getCompareDocument();
|
| 679 |
+
const input = document.getElementById("messageInput");
|
| 680 |
+
const userText = input.value.trim();
|
| 681 |
+
|
| 682 |
+
if (!doc) return alert("Upload or select a document first.");
|
| 683 |
+
if (!userText) return;
|
| 684 |
+
|
| 685 |
+
const convo = getConversation();
|
| 686 |
+
convo.push({ role: "user", content: userText, createdAt: new Date().toISOString() });
|
| 687 |
+
input.value = "";
|
| 688 |
+
saveConversations();
|
| 689 |
+
renderMessages();
|
| 690 |
+
|
| 691 |
+
setStatus(compareDoc ? "Comparing..." : "Thinking...");
|
| 692 |
+
document.getElementById("metricsBox").innerHTML = "";
|
| 693 |
+
|
| 694 |
+
try {
|
| 695 |
+
if (compareDoc) {
|
| 696 |
+
let data;
|
| 697 |
+
try {
|
| 698 |
+
const res = await fetch("/documents/compare", {
|
| 699 |
+
method: "POST",
|
| 700 |
+
headers: { "Content-Type": "application/json" },
|
| 701 |
+
body: JSON.stringify({
|
| 702 |
+
primary_document_id: doc.id,
|
| 703 |
+
compare_document_id: compareDoc.id,
|
| 704 |
+
query: userText,
|
| 705 |
+
retrieval_mode: "hybrid",
|
| 706 |
+
top_k: 8,
|
| 707 |
+
use_reranker: true,
|
| 708 |
+
use_llm: true,
|
| 709 |
+
use_graph: true,
|
| 710 |
+
graph_entity_limit: 12,
|
| 711 |
+
use_graph_retrieval: true,
|
| 712 |
+
graph_retrieval_top_k: 6
|
| 713 |
+
})
|
| 714 |
+
});
|
| 715 |
+
data = await res.json();
|
| 716 |
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
| 717 |
+
} catch (err) {
|
| 718 |
+
const dataA = await callAsk(askPayload(buildContextualQuery(userText), doc.id));
|
| 719 |
+
const dataB = await callAsk(askPayload(buildContextualQuery(userText), compareDoc.id));
|
| 720 |
+
data = {
|
| 721 |
+
document_a: { answer: dataA.answer, ask_response: dataA },
|
| 722 |
+
document_b: { answer: dataB.answer, ask_response: dataB }
|
| 723 |
+
};
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
const dataA = data.document_a?.ask_response || { answer: data.document_a?.answer || "No answer.", citations: data.document_a?.sources || [] };
|
| 727 |
+
const dataB = data.document_b?.ask_response || { answer: data.document_b?.answer || "No answer.", citations: data.document_b?.sources || [] };
|
| 728 |
+
|
| 729 |
+
const html = `<div class="answer-card"><h2>Comparison Answer</h2>
|
| 730 |
+
<div class="compare-grid">
|
| 731 |
+
<div class="compare-card"><h3>${escapeHtml(doc.name || "Document A")}</h3>${renderAnswerHtml(userText, dataA, doc)}</div>
|
| 732 |
+
<div class="compare-card"><h3>${escapeHtml(compareDoc.name || "Document B")}</h3>${renderAnswerHtml(userText, dataB, compareDoc)}</div>
|
| 733 |
+
</div></div>`;
|
| 734 |
+
|
| 735 |
+
convo.push({ role: "assistant", html, content: "Comparison answer", createdAt: new Date().toISOString(), rawCompare: data });
|
| 736 |
+
saveConversations();
|
| 737 |
+
renderMessages();
|
| 738 |
+
|
| 739 |
+
updateMetrics(dataA, doc.name || "Document A");
|
| 740 |
+
updateMetrics(dataB, compareDoc.name || "Document B");
|
| 741 |
+
updateCitations([
|
| 742 |
+
{ label: doc.name || "Document A", sources: buildSources(dataA, doc) },
|
| 743 |
+
{ label: compareDoc.name || "Document B", sources: buildSources(dataB, compareDoc) }
|
| 744 |
+
]);
|
| 745 |
+
setStatus("Comparison ready");
|
| 746 |
+
} else {
|
| 747 |
+
const data = await callAsk(askPayload(buildContextualQuery(userText), doc.id));
|
| 748 |
+
const html = renderAnswerHtml(userText, data, doc);
|
| 749 |
+
convo.push({ role: "assistant", content: data.answer || "Answer", html, createdAt: new Date().toISOString(), raw: data });
|
| 750 |
+
saveConversations();
|
| 751 |
+
renderMessages();
|
| 752 |
+
|
| 753 |
+
updateMetrics(data, doc.name || "Selected document");
|
| 754 |
+
updateCitations([{ label: doc.name || "Selected document", sources: buildSources(data, doc) }]);
|
| 755 |
+
setStatus("Ready");
|
| 756 |
+
}
|
| 757 |
+
} catch (error) {
|
| 758 |
+
convo.push({ role: "assistant", content: "Error: " + error.message, createdAt: new Date().toISOString() });
|
| 759 |
+
saveConversations();
|
| 760 |
+
renderMessages();
|
| 761 |
+
setStatus("Error");
|
| 762 |
+
}
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
renderDocuments();
|
| 766 |
+
</script>
|
| 767 |
+
</body>
|
| 768 |
+
</html>
|
| 769 |
+
"""
|
| 770 |
+
''', encoding="utf-8")
|
| 771 |
+
|
| 772 |
+
hf_path = Path("app/deployment/hf_status.py")
|
| 773 |
+
text = hf_path.read_text(encoding="utf-8-sig")
|
| 774 |
+
text = text.replace("\ufeff", "")
|
| 775 |
+
|
| 776 |
+
alias_block = """
|
| 777 |
+
# =====================================================
|
| 778 |
+
# Phase 38 final stable UI export
|
| 779 |
+
# This keeps old helper functions but forces / and /app to use the clean final UI.
|
| 780 |
+
# =====================================================
|
| 781 |
+
from app.product.final_product_ui import get_home_html, get_product_app_html
|
| 782 |
+
"""
|
| 783 |
+
|
| 784 |
+
if "Phase 38 final stable UI export" not in text:
|
| 785 |
+
text += "\\n\\n" + alias_block
|
| 786 |
+
print("Added Phase 38 final UI export alias.")
|
| 787 |
+
else:
|
| 788 |
+
print("Phase 38 final UI export already exists.")
|
| 789 |
+
|
| 790 |
+
hf_path.write_text(text, encoding="utf-8")
|
| 791 |
+
|
| 792 |
+
print("Phase 38 final UI stabilization complete.")
|