mnoorchenar commited on
Commit
5fafc19
Β·
1 Parent(s): c0a4e5a

Update 2026-03-22 16:53:56

Browse files
.claude/settings.local.json CHANGED
@@ -2,7 +2,8 @@
2
  "permissions": {
3
  "allow": [
4
  "Bash(wc -l /e/HuggingFace/docmind/*.py /e/HuggingFace/docmind/**/*.py)",
5
- "Bash(python3:*)"
 
6
  ]
7
  }
8
  }
 
2
  "permissions": {
3
  "allow": [
4
  "Bash(wc -l /e/HuggingFace/docmind/*.py /e/HuggingFace/docmind/**/*.py)",
5
+ "Bash(python3:*)",
6
+ "Bash(python write_html.py)"
7
  ]
8
  }
9
  }
agents/planner.py CHANGED
@@ -1,7 +1,7 @@
1
  from agents.llm_factory import call_llm
2
 
3
  _TEMPLATE = """You are a research planning agent. Given the user's question, produce a brief research plan.
4
- Decide: should the answer be grounded in uploaded documents, web search, or both?
5
  Output your plan in 2-3 concise sentences. Start with "PLAN:".
6
 
7
  Question: {question}
 
1
  from agents.llm_factory import call_llm
2
 
3
  _TEMPLATE = """You are a research planning agent. Given the user's question, produce a brief research plan.
4
+ Describe which aspects of the uploaded document are most relevant to answer the question.
5
  Output your plan in 2-3 concise sentences. Start with "PLAN:".
6
 
7
  Question: {question}
templates/index.html CHANGED
@@ -1,246 +1,520 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
- <meta charset="UTF-8"/>
5
- <meta name="viewport" content="width=device-width,initial-scale=1"/>
6
- <title>DocMind</title>
7
- <style>
8
- :root{--bg:#0f111a;--card:#181b24;--border:#2a2d3a;--text:#e4e6ef;--muted:#8892a4;--accent:#4f8ef7;--green:#22c55e;--red:#ef4444;--teal:#06b6d4;--gold:#f59e0b}
9
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
10
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
11
- header{background:var(--card);border-bottom:1px solid var(--border);padding:13px 24px;display:flex;align-items:center;gap:12px}
12
- .logo{font-size:1.1rem;font-weight:800;color:var(--accent)}
13
- .logo span{color:var(--text)}
14
- .model-badge{font-size:.68rem;background:rgba(79,142,247,.1);border:1px solid rgba(79,142,247,.2);color:var(--accent);padding:3px 9px;border-radius:20px}
15
- #source-pill{margin-left:auto;font-size:.72rem;padding:4px 10px;border-radius:20px;background:rgba(136,146,164,.08);border:1px solid var(--border);color:var(--muted)}
16
- #source-pill.loaded{background:rgba(34,197,94,.08);border-color:rgba(34,197,94,.25);color:var(--green)}
17
- main{display:grid;grid-template-columns:340px 1fr;min-height:calc(100vh - 50px)}
18
- .panel{padding:22px 20px;overflow-y:auto}
19
- .panel-left{border-right:1px solid var(--border)}
20
- .slabel{font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:12px}
21
- .tabs{display:flex;gap:3px;background:rgba(255,255,255,.04);border-radius:8px;padding:3px;margin-bottom:16px}
22
- .tab-btn{flex:1;background:none;border:none;color:var(--muted);font-size:.8rem;font-weight:600;padding:7px;border-radius:6px;cursor:pointer;transition:all .15s}
23
- .tab-btn.active{background:var(--card);color:var(--text);box-shadow:0 1px 4px rgba(0,0,0,.3)}
24
- .dropzone{border:2px dashed var(--border);border-radius:9px;padding:30px 16px;text-align:center;cursor:pointer;transition:all .2s}
25
- .dropzone:hover,.dropzone.drag-over{border-color:var(--accent);background:rgba(79,142,247,.05)}
26
- .dz-icon{font-size:2rem;margin-bottom:8px}
27
- .dz-text{font-size:.82rem;color:var(--muted);line-height:1.5}
28
- .dz-text strong{color:var(--accent)}
29
- input[type=url],textarea{width:100%;background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:7px;padding:9px 12px;color:var(--text);font-size:.86rem;font-family:inherit;outline:none;transition:border-color .2s}
30
- input[type=url]:focus,textarea:focus{border-color:var(--accent)}
31
- textarea{resize:vertical;min-height:80px}
32
- .btn{display:inline-flex;align-items:center;gap:6px;padding:9px 18px;border-radius:7px;border:none;font-size:.84rem;font-weight:600;cursor:pointer;transition:all .15s;font-family:inherit}
33
- .btn-primary{background:var(--accent);color:#fff}
34
- .btn-primary:hover{background:#3a7ae8}
35
- .btn-primary:disabled{opacity:.45;cursor:default}
36
- .btn-sm{padding:7px 14px;font-size:.78rem}
37
- .msg{border-radius:8px;padding:10px 13px;font-size:.8rem;margin-top:10px;line-height:1.55}
38
- .msg-ok{background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.2);color:var(--green)}
39
- .msg-err{background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.2);color:var(--red)}
40
- .msg-info{color:var(--muted);font-size:.78rem;margin-top:8px}
41
- #source-card{display:none;background:rgba(34,197,94,.06);border:1px solid rgba(34,197,94,.18);border-radius:9px;padding:12px 14px;margin-top:14px;font-size:.8rem}
42
- #source-card strong{color:var(--text);display:block;margin-bottom:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
43
- #source-card span{color:var(--teal);font-size:.73rem}
44
- .q-wrap{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:14px}
45
- .q-row{display:flex;align-items:center;gap:10px;margin-top:10px}
46
- #q-err{font-size:.78rem;color:var(--red)}
47
- #trace-wrap{display:none;background:rgba(0,0,0,.25);border:1px solid var(--border);border-radius:9px;padding:10px 12px;max-height:240px;overflow-y:auto;margin-bottom:14px}
48
- .t-step{display:flex;align-items:flex-start;gap:8px;font-size:.74rem;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.04)}
49
- .t-step:last-child{border-bottom:none}
50
- .t-badge{font-size:.6rem;font-weight:800;text-transform:uppercase;padding:2px 6px;border-radius:4px;flex-shrink:0;margin-top:1px}
51
- .b-planner{background:rgba(79,142,247,.15);color:var(--accent)}
52
- .b-retriever{background:rgba(6,182,212,.15);color:var(--teal)}
53
- .b-grader{background:rgba(245,158,11,.15);color:var(--gold)}
54
- .b-generator{background:rgba(34,197,94,.15);color:var(--green)}
55
- .b-critic{background:rgba(239,68,68,.1);color:var(--red)}
56
- .t-msg{flex:1;color:var(--muted);line-height:1.45}
57
- .t-lat{color:rgba(136,146,164,.45);font-size:.65rem;white-space:nowrap}
58
- #answer-wrap{display:none;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:18px}
59
- .ans-label{font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);margin-bottom:10px}
60
- #answer-text{font-size:.88rem;line-height:1.72;white-space:pre-wrap;word-break:break-word}
61
- #verdict{margin-top:12px;font-size:.72rem;font-weight:700;padding:4px 10px;border-radius:5px;display:inline-block}
62
- .v-ok{background:rgba(34,197,94,.12);color:var(--green)}
63
- .v-warn{background:rgba(245,158,11,.12);color:var(--gold)}
64
- @media(max-width:768px){main{grid-template-columns:1fr}.panel-left{border-right:none;border-bottom:1px solid var(--border)}}
65
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </head>
67
  <body>
 
 
68
  <header>
69
- <div class="logo">Doc<span>Mind</span></div>
70
- <span class="model-badge">Qwen 2.5 7B</span>
71
- <span id="source-pill">No source loaded</span>
 
 
 
 
 
 
72
  </header>
 
73
  <main>
74
- <!-- LEFT: LOAD SOURCE -->
75
- <div class="panel panel-left">
76
- <div class="slabel">Load Knowledge Source</div>
77
- <div class="tabs">
78
- <button class="tab-btn active" onclick="switchTab(this,'pdf')">&#128196; Upload PDF</button>
79
- <button class="tab-btn" onclick="switchTab(this,'url')">&#127760; Paste URL</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </div>
81
- <div id="tab-pdf">
82
- <div class="dropzone" id="dz"
83
- onclick="document.getElementById('fi').click()"
84
- ondragover="dg(event,true)" ondragleave="dg(event,false)" ondrop="dp(event)">
85
- <div class="dz-icon">&#128194;</div>
86
- <div class="dz-text"><strong>Click to browse</strong> or drag &amp; drop<br>PDF &middot; max 10 MB</div>
 
 
 
 
 
87
  </div>
88
- <input type="file" id="fi" accept=".pdf" style="display:none" onchange="fc(event)"/>
89
- <div id="pdf-msg"></div>
90
  </div>
91
- <div id="tab-url" style="display:none">
92
- <input type="url" id="url-inp" placeholder="https://en.wikipedia.org/wiki/..."
93
- style="margin-bottom:10px" onkeydown="if(event.key==='Enter')fetchURL()"/>
94
- <button class="btn btn-primary btn-sm" id="url-btn" onclick="fetchURL()">Fetch &amp; Index</button>
95
- <div id="url-msg"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  </div>
97
- <div id="source-card">
98
- <strong id="source-name"></strong>
99
- <span id="source-chunks"></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  </div>
 
101
  </div>
102
- <!-- RIGHT: RESEARCH -->
103
- <div class="panel">
104
- <div class="slabel">Research Question</div>
105
- <div class="q-wrap">
106
- <textarea id="q-inp" rows="3"
107
- placeholder="Ask anything about the loaded source... (Enter to submit)"
108
- onkeydown="qk(event)"></textarea>
109
- <div class="q-row">
110
- <button class="btn btn-primary" id="ask-btn" onclick="ask()">&#9889; Ask</button>
111
- <span id="q-err"></span>
 
112
  </div>
113
  </div>
114
- <div id="trace-wrap"><div id="trace-log"></div></div>
115
- <div id="answer-wrap">
116
- <div class="ans-label">Answer</div>
117
  <div id="answer-text"></div>
118
- <div id="verdict"></div>
119
  </div>
120
  </div>
 
121
  </main>
 
122
  <script>
123
  let pollTimer=null,seen=0;
124
- const esc=s=>String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
125
 
 
 
 
126
  function switchTab(btn,name){
127
- document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
128
- btn.classList.add('active');
129
- document.getElementById('tab-pdf').style.display=name==='pdf'?'':'none';
130
- document.getElementById('tab-url').style.display=name==='url'?'':'none';
131
  }
132
- function dg(e,over){e.preventDefault();document.getElementById('dz').classList[over?'add':'remove']('drag-over');}
133
- function dp(e){e.preventDefault();document.getElementById('dz').classList.remove('drag-over');const f=e.dataTransfer.files[0];if(f)up(f);}
 
 
134
  function fc(e){if(e.target.files[0])up(e.target.files[0]);}
135
 
 
136
  async function up(file){
137
- if(!file.name.toLowerCase().endsWith('.pdf')){sm('pdf-msg','error','Only PDF files are supported.');return;}
138
- sm('pdf-msg','info','Uploading '+file.name+'...');
139
- const fd=new FormData();fd.append('file',file);
140
  try{
141
- const r=await fetch('/api/upload',{method:'POST',body:fd});
142
  const d=await r.json();
143
- if(d.error){sm('pdf-msg','error',d.error);return;}
144
- src(d.filename,d.chunks);sm('pdf-msg','ok','Indexed '+d.chunks+' chunks from "'+d.filename+'"');
145
- }catch(e){sm('pdf-msg','error','Upload failed: '+e.message);}
 
146
  }
147
 
 
148
  async function fetchURL(){
149
- const url=document.getElementById('url-inp').value.trim();
150
- if(!url){sm('url-msg','error','Please enter a URL.');return;}
151
- document.getElementById('url-btn').disabled=true;
152
- sm('url-msg','info','Fetching page...');
 
153
  try{
154
- const r=await fetch('/api/ingest_url',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})});
155
  const d=await r.json();
156
- if(d.error){sm('url-msg','error',d.error);return;}
157
- src(d.url,d.chunks);sm('url-msg','ok','Indexed '+d.chunks+' chunks');
158
- }catch(e){sm('url-msg','error','Failed: '+e.message);}
159
- finally{document.getElementById('url-btn').disabled=false;}
 
160
  }
161
 
162
- function src(name,chunks){
163
- document.getElementById('source-name').textContent=name;
164
- document.getElementById('source-chunks').textContent=chunks+' chunks indexed';
165
- document.getElementById('source-card').style.display='block';
166
- const p=document.getElementById('source-pill');
167
- p.textContent=name.length>32?name.slice(0,32)+'...':name;
168
- p.classList.add('loaded');
 
 
169
  }
170
 
171
- function qk(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();ask();}}
 
 
 
 
 
 
 
 
 
 
 
172
 
173
  async function ask(){
174
- const q=document.getElementById('q-inp').value.trim();
175
- document.getElementById('q-err').textContent='';
176
- if(!q){document.getElementById('q-err').textContent='Please enter a question.';return;}
177
- document.getElementById('trace-wrap').style.display='block';
178
- document.getElementById('trace-log').innerHTML='<div class="t-step"><span class="t-msg" style="color:var(--muted)">Starting agents…</span></div>';
179
- document.getElementById('answer-wrap').style.display='none';
180
- document.getElementById('ask-btn').disabled=true;
 
 
 
 
 
 
181
  seen=0;clearInterval(pollTimer);
 
182
  try{
183
- const r=await fetch('/api/research',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question:q})});
184
  const d=await r.json();
185
- if(d.error){
186
- traceErr(d.error);
187
- document.getElementById('ask-btn').disabled=false;
188
- return;
189
- }
190
  pollTimer=setInterval(()=>poll(d.query_id),1500);
191
- }catch(e){
192
- traceErr('Network error: '+e.message);
193
- document.getElementById('ask-btn').disabled=false;
194
- }
 
 
 
195
  }
196
 
 
197
  async function poll(qid){
198
  try{
199
- const r=await fetch('/api/trace/'+qid);
200
- if(!r.ok){traceErr('Server error '+r.status);clearInterval(pollTimer);document.getElementById('ask-btn').disabled=false;return;}
201
  const d=await r.json();
202
- rt(d.trace||[]);
203
- if(['complete','error'].includes(d.status)){
204
  clearInterval(pollTimer);
205
- document.getElementById('ask-btn').disabled=false;
206
- if(d.status==='complete'&&d.result) ra(d.result);
207
- else if(d.status==='error'&&d.result) traceErr(d.result.error||'An error occurred.');
208
  }
209
- }catch(e){traceErr('Poll error: '+e.message);clearInterval(pollTimer);document.getElementById('ask-btn').disabled=false;}
210
- }
211
-
212
- function traceErr(msg){
213
- const log=document.getElementById('trace-log');
214
- log.innerHTML+='<div class="t-step"><span class="t-badge" style="background:rgba(239,68,68,.15);color:var(--red)">error</span><span class="t-msg" style="color:var(--red)">'+esc(msg)+'</span></div>';
215
- log.scrollTop=log.scrollHeight;
216
  }
217
 
218
- function rt(steps){
 
219
  if(!steps.length)return;
220
- const log=document.getElementById('trace-log');
221
- if(seen===0)log.innerHTML='';
222
  for(let i=seen;i<steps.length;i++){
223
  const s=steps[i];
224
- const lat=s.latency_ms>0?'<span class="t-lat">'+s.latency_ms+'ms</span>':'';
225
- log.innerHTML+='<div class="t-step"><span class="t-badge b-'+s.agent+'">'+s.agent+'</span><span class="t-msg">'+esc(s.message)+'</span>'+lat+'</div>';
 
 
 
 
 
 
 
 
226
  }
227
- seen=steps.length;log.scrollTop=log.scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  }
229
 
230
- function ra(result){
231
- document.getElementById('answer-wrap').style.display='block';
232
- document.getElementById('answer-text').textContent=result.generation||'No answer generated.';
233
- const v=document.getElementById('verdict');
234
- if(result.verdict==='APPROVED'){v.className='v-ok';v.textContent='High confidence';}
235
- else if(result.verdict){v.className='v-warn';v.textContent='Low confidence - verify with source';}
236
- else{v.textContent='';}
 
 
237
  }
238
 
239
- function sm(id,type,msg){
 
240
  const el=document.getElementById(id);
241
- if(type==='ok')el.innerHTML='<div class="msg msg-ok">'+esc(msg)+'</div>';
242
- else if(type==='error')el.innerHTML='<div class="msg msg-err">'+esc(msg)+'</div>';
243
- else el.innerHTML='<div class="msg-info">'+esc(msg)+'</div>';
244
  }
245
  </script>
246
  </body>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
6
+ <title>DocMind β€” AI Document Research</title>
7
+ <style>
8
+ :root{
9
+ --bg:#0d0f1a;--surface:#13161f;--card:#181c27;--card2:#1e2230;
10
+ --border:#252836;--border2:#2e3244;
11
+ --text:#e8eaf2;--sub:#b0b8cc;--muted:#7880a0;
12
+ --accent:#5b8ff9;--accent2:#3a6ee8;
13
+ --green:#22d47a;--red:#f05c5c;--teal:#29c6d4;--gold:#f5a623;--purple:#a78bfa;
14
+ }
15
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
16
+ html,body{height:100%}
17
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
18
+ background:var(--bg);color:var(--text);font-size:14px;line-height:1.5}
19
+
20
+ /* ── ANIMATIONS ────────────────────────────────── */
21
+ @keyframes spin{to{transform:rotate(360deg)}}
22
+ @keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
23
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
24
+ @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
25
+
26
+ /* ── HEADER ────────────────────────────────────── */
27
+ header{
28
+ background:var(--surface);
29
+ border-bottom:1px solid var(--border);
30
+ padding:0 28px;height:56px;
31
+ display:flex;align-items:center;gap:14px;
32
+ position:sticky;top:0;z-index:100;
33
+ }
34
+ .logo{display:flex;align-items:center;gap:9px;text-decoration:none}
35
+ .logo-icon{width:30px;height:30px;background:linear-gradient(135deg,var(--accent),var(--purple));
36
+ border-radius:8px;display:flex;align-items:center;justify-content:center;
37
+ font-size:15px;flex-shrink:0}
38
+ .logo-text{font-size:1rem;font-weight:800;color:var(--text);letter-spacing:-.3px}
39
+ .logo-text span{color:var(--accent)}
40
+ .logo-sub{font-size:.68rem;color:var(--muted);margin-left:2px;font-weight:400;
41
+ display:none} /* shown on wider screens */
42
+ .hdr-badges{display:flex;gap:8px;align-items:center;margin-left:4px}
43
+ .badge{font-size:.65rem;font-weight:700;padding:3px 9px;border-radius:20px;letter-spacing:.02em}
44
+ .badge-model{background:rgba(91,143,249,.12);border:1px solid rgba(91,143,249,.25);color:var(--accent)}
45
+ .badge-src{background:rgba(113,128,160,.1);border:1px solid var(--border);color:var(--muted);
46
+ max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
47
+ .badge-src.loaded{background:rgba(34,212,122,.1);border-color:rgba(34,212,122,.3);color:var(--green)}
48
+ #hdr-src{margin-left:auto}
49
+
50
+ /* ── LAYOUT ─────────────────────────────────────── */
51
+ main{display:grid;grid-template-columns:320px 1fr;height:calc(100vh - 56px);overflow:hidden}
52
+ .panel{padding:24px 22px;overflow-y:auto;height:100%}
53
+ .panel-left{border-right:1px solid var(--border);background:var(--surface)}
54
+ .panel-right{background:var(--bg)}
55
+
56
+ /* ── SECTION HEADERS ────────────────────────────── */
57
+ .sec-head{display:flex;align-items:center;gap:8px;margin-bottom:16px}
58
+ .sec-icon{width:26px;height:26px;border-radius:7px;display:flex;align-items:center;
59
+ justify-content:center;font-size:13px;flex-shrink:0}
60
+ .sec-icon-blue{background:rgba(91,143,249,.15)}
61
+ .sec-icon-purple{background:rgba(167,139,250,.15)}
62
+ .sec-title{font-size:.75rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:var(--sub)}
63
+
64
+ /* ── TABS ────────────────────────────────────────── */
65
+ .tabs{display:flex;gap:2px;background:rgba(255,255,255,.03);border-radius:9px;
66
+ padding:3px;margin-bottom:18px;border:1px solid var(--border)}
67
+ .tab-btn{flex:1;background:none;border:none;color:var(--muted);font-size:.78rem;
68
+ font-weight:600;padding:7px 10px;border-radius:7px;cursor:pointer;
69
+ transition:all .18s;font-family:inherit;display:flex;align-items:center;
70
+ justify-content:center;gap:5px}
71
+ .tab-btn.active{background:var(--card2);color:var(--text);
72
+ box-shadow:0 1px 6px rgba(0,0,0,.35)}
73
+
74
+ /* ── DROPZONE ────────────────────────────────────── */
75
+ .dropzone{
76
+ border:2px dashed var(--border2);border-radius:12px;padding:28px 16px;
77
+ text-align:center;cursor:pointer;transition:all .22s;position:relative;overflow:hidden;
78
+ }
79
+ .dropzone::before{
80
+ content:"";position:absolute;inset:0;
81
+ background:linear-gradient(90deg,transparent 0%,rgba(91,143,249,.04) 50%,transparent 100%);
82
+ background-size:200% 100%;opacity:0;transition:opacity .3s;
83
+ }
84
+ .dropzone:hover{border-color:var(--accent);background:rgba(91,143,249,.04)}
85
+ .dropzone:hover::before{opacity:1;animation:shimmer 2s infinite}
86
+ .dropzone.drag-over{border-color:var(--accent);background:rgba(91,143,249,.08);transform:scale(1.01)}
87
+ .dz-icon{font-size:2.2rem;margin-bottom:10px;display:block}
88
+ .dz-label{font-size:.84rem;color:var(--sub);line-height:1.5}
89
+ .dz-label strong{color:var(--accent)}
90
+ .dz-hint{font-size:.72rem;color:var(--muted);margin-top:5px}
91
+
92
+ /* ── URL INPUT ────────────────────────────────────── */
93
+ .url-row{display:flex;gap:8px;margin-bottom:10px}
94
+ .url-row input{flex:1;background:var(--card);border:1px solid var(--border2);
95
+ border-radius:8px;padding:9px 12px;color:var(--text);font-size:.84rem;
96
+ font-family:inherit;outline:none;transition:border-color .18s}
97
+ .url-row input:focus{border-color:var(--accent)}
98
+
99
+ /* ── SOURCE LOADED CARD ──────────────────────────── */
100
+ #source-card{
101
+ display:none;margin-top:16px;background:var(--card);
102
+ border:1px solid rgba(34,212,122,.2);border-radius:10px;padding:13px 14px;
103
+ animation:fadeUp .25s ease;
104
+ }
105
+ .sc-row{display:flex;align-items:flex-start;gap:10px}
106
+ .sc-icon{width:34px;height:34px;background:rgba(34,212,122,.12);border-radius:8px;
107
+ flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:16px}
108
+ .sc-info{flex:1;min-width:0}
109
+ .sc-name{font-size:.82rem;font-weight:600;color:var(--text);
110
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:3px}
111
+ .sc-meta{font-size:.72rem;color:var(--teal)}
112
+ .sc-ready{display:inline-flex;align-items:center;gap:5px;margin-top:8px;
113
+ font-size:.7rem;font-weight:700;color:var(--green);
114
+ background:rgba(34,212,122,.08);border:1px solid rgba(34,212,122,.18);
115
+ border-radius:5px;padding:3px 8px}
116
+
117
+ /* ── MESSAGES ────────────────────────────────────── */
118
+ .msg{border-radius:8px;padding:10px 13px;font-size:.79rem;
119
+ margin-top:10px;line-height:1.55;animation:fadeUp .2s ease}
120
+ .msg-ok{background:rgba(34,212,122,.07);border:1px solid rgba(34,212,122,.2);color:var(--green)}
121
+ .msg-err{background:rgba(240,92,92,.07);border:1px solid rgba(240,92,92,.2);color:var(--red)}
122
+ .msg-info{color:var(--muted);font-size:.76rem;margin-top:8px}
123
+
124
+ /* ── BUTTONS ─────────────────────────────────────── */
125
+ .btn{display:inline-flex;align-items:center;gap:7px;padding:9px 20px;
126
+ border-radius:8px;border:none;font-size:.84rem;font-weight:600;
127
+ cursor:pointer;transition:all .18s;font-family:inherit}
128
+ .btn-primary{background:linear-gradient(135deg,var(--accent),var(--accent2));
129
+ color:#fff;box-shadow:0 2px 8px rgba(91,143,249,.25)}
130
+ .btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 14px rgba(91,143,249,.35)}
131
+ .btn-primary:active{transform:none}
132
+ .btn-primary:disabled{opacity:.5;cursor:default;transform:none;box-shadow:none}
133
+ .btn-sm{padding:7px 14px;font-size:.78rem}
134
+ .btn-ghost{background:rgba(255,255,255,.06);border:1px solid var(--border2);
135
+ color:var(--sub);font-size:.74rem;padding:5px 12px;border-radius:6px}
136
+ .btn-ghost:hover{background:rgba(255,255,255,.1);color:var(--text)}
137
+ .spinner{width:13px;height:13px;border:2px solid rgba(255,255,255,.3);
138
+ border-top-color:#fff;border-radius:50%;animation:spin .65s linear infinite;flex-shrink:0}
139
+
140
+ /* ── QUESTION AREA ───────────────────────────────── */
141
+ .q-card{background:var(--card);border:1px solid var(--border2);
142
+ border-radius:11px;padding:16px;margin-bottom:16px}
143
+ .q-label{font-size:.7rem;font-weight:700;color:var(--muted);text-transform:uppercase;
144
+ letter-spacing:.06em;margin-bottom:8px}
145
+ textarea{width:100%;background:transparent;border:none;color:var(--text);
146
+ font-size:.9rem;font-family:inherit;outline:none;resize:none;
147
+ line-height:1.6;min-height:72px}
148
+ textarea::placeholder{color:var(--muted)}
149
+ .q-footer{display:flex;align-items:center;gap:10px;padding-top:12px;
150
+ border-top:1px solid var(--border)}
151
+ #q-err{font-size:.77rem;color:var(--red)}
152
+
153
+ /* ── PIPELINE ────────────────────────────────────── */
154
+ #pipeline{display:none;margin-bottom:14px;animation:fadeUp .2s ease}
155
+ .pipe-row{display:flex;align-items:center;gap:0;background:var(--card);
156
+ border:1px solid var(--border2);border-radius:10px;padding:10px 14px;
157
+ overflow-x:auto}
158
+ .pipe-step{display:flex;align-items:center;gap:5px;font-size:.72rem;font-weight:600;
159
+ color:var(--muted);white-space:nowrap;padding:4px 8px;border-radius:6px;
160
+ transition:all .2s}
161
+ .pipe-step.active{color:var(--accent);background:rgba(91,143,249,.12)}
162
+ .pipe-step.done{color:var(--green)}
163
+ .pipe-step .step-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0}
164
+ .pipe-step.active .step-dot{animation:pulse .9s ease infinite}
165
+ .pipe-arrow{color:var(--border2);font-size:.8rem;padding:0 2px;flex-shrink:0}
166
+
167
+ /* ── TRACE LOG ───────────────────────────────────── */
168
+ #trace-wrap{display:none;margin-bottom:16px;animation:fadeUp .2s ease}
169
+ .trace-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px}
170
+ .trace-title{font-size:.7rem;font-weight:700;text-transform:uppercase;
171
+ letter-spacing:.07em;color:var(--muted)}
172
+ .trace-box{background:var(--card);border:1px solid var(--border2);border-radius:10px;
173
+ padding:4px 0;max-height:220px;overflow-y:auto}
174
+ .t-step{display:flex;align-items:flex-start;gap:10px;padding:8px 14px;
175
+ border-bottom:1px solid var(--border);animation:fadeUp .18s ease;transition:background .15s}
176
+ .t-step:last-child{border-bottom:none}
177
+ .t-step:hover{background:rgba(255,255,255,.02)}
178
+ .t-icon{font-size:13px;flex-shrink:0;margin-top:1px;width:18px;text-align:center}
179
+ .t-agent{font-size:.64rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;
180
+ padding:2px 7px;border-radius:4px;flex-shrink:0;white-space:nowrap}
181
+ .a-planner {background:rgba(91,143,249,.14);color:var(--accent)}
182
+ .a-retriever{background:rgba(41,198,212,.14);color:var(--teal)}
183
+ .a-grader {background:rgba(245,166,35,.14);color:var(--gold)}
184
+ .a-generator{background:rgba(34,212,122,.14);color:var(--green)}
185
+ .a-critic {background:rgba(167,139,250,.14);color:var(--purple)}
186
+ .a-error {background:rgba(240,92,92,.14);color:var(--red)}
187
+ .t-msg{flex:1;font-size:.78rem;color:var(--sub);line-height:1.5}
188
+ .t-lat{font-size:.65rem;color:rgba(128,136,160,.5);flex-shrink:0;margin-top:2px}
189
+
190
+ /* ── ANSWER CARD ─────────────────────────────────── */
191
+ #answer-wrap{display:none;background:var(--card);border:1px solid var(--border2);
192
+ border-radius:12px;overflow:hidden;animation:fadeUp .3s ease}
193
+ .ans-header{display:flex;align-items:center;justify-content:space-between;
194
+ padding:13px 18px 12px;border-bottom:1px solid var(--border);
195
+ background:rgba(255,255,255,.02)}
196
+ .ans-title{display:flex;align-items:center;gap:8px;font-size:.72rem;font-weight:800;
197
+ text-transform:uppercase;letter-spacing:.08em;color:var(--sub)}
198
+ .ans-title-icon{width:22px;height:22px;background:rgba(34,212,122,.15);border-radius:6px;
199
+ display:flex;align-items:center;justify-content:center;font-size:11px}
200
+ .ans-actions{display:flex;align-items:center;gap:8px}
201
+ #verdict-badge{font-size:.68rem;font-weight:700;padding:3px 10px;border-radius:20px}
202
+ .v-ok {background:rgba(34,212,122,.1);border:1px solid rgba(34,212,122,.25);color:var(--green)}
203
+ .v-warn{background:rgba(245,166,35,.1);border:1px solid rgba(245,166,35,.25);color:var(--gold)}
204
+ .ans-body{padding:18px}
205
+ #answer-text{font-size:.9rem;line-height:1.78;color:var(--text);
206
+ white-space:pre-wrap;word-break:break-word}
207
+
208
+ /* ── RESPONSIVE ──────────────────────────────────── */
209
+ @media(min-width:1100px){.logo-sub{display:block}}
210
+ @media(max-width:768px){
211
+ main{grid-template-columns:1fr;height:auto;overflow:visible}
212
+ .panel{height:auto}
213
+ .panel-left{border-right:none;border-bottom:1px solid var(--border)}
214
+ }
215
+ </style>
216
  </head>
217
  <body>
218
+
219
+ <!-- ── HEADER ── -->
220
  <header>
221
+ <a class="logo" href="#">
222
+ <div class="logo-icon">&#129504;</div>
223
+ <span class="logo-text">Doc<span>Mind</span></span>
224
+ </a>
225
+ <span class="logo-sub">AI Document Research</span>
226
+ <div class="hdr-badges">
227
+ <span class="badge badge-model">Qwen 2.5 7B</span>
228
+ </div>
229
+ <span class="badge badge-src" id="hdr-src">No source loaded</span>
230
  </header>
231
+
232
  <main>
233
+ <!-- ── LEFT PANEL: KNOWLEDGE BASE ── -->
234
+ <div class="panel panel-left">
235
+ <div class="sec-head">
236
+ <div class="sec-icon sec-icon-blue">&#128218;</div>
237
+ <span class="sec-title">Knowledge Base</span>
238
+ </div>
239
+
240
+ <div class="tabs">
241
+ <button class="tab-btn active" onclick="switchTab(this,'pdf')">&#128196;&ensp;Upload PDF</button>
242
+ <button class="tab-btn" onclick="switchTab(this,'url')">&#127760;&ensp;Paste URL</button>
243
+ </div>
244
+
245
+ <!-- PDF TAB -->
246
+ <div id="tab-pdf">
247
+ <div class="dropzone" id="dz"
248
+ onclick="document.getElementById('fi').click()"
249
+ ondragover="dg(event,true)" ondragleave="dg(event,false)" ondrop="dp(event)">
250
+ <span class="dz-icon">&#128196;</span>
251
+ <div class="dz-label"><strong>Click to browse</strong> or drag &amp; drop</div>
252
+ <div class="dz-hint">PDF only &middot; max 10 MB</div>
253
+ </div>
254
+ <input type="file" id="fi" accept=".pdf" style="display:none" onchange="fc(event)"/>
255
+ <div id="pdf-msg"></div>
256
+ </div>
257
+
258
+ <!-- URL TAB -->
259
+ <div id="tab-url" style="display:none">
260
+ <div class="url-row">
261
+ <input type="url" id="url-inp" placeholder="https://en.wikipedia.org/wiki/..."
262
+ onkeydown="if(event.key==='Enter')fetchURL()"/>
263
+ <button class="btn btn-primary btn-sm" id="url-btn" onclick="fetchURL()">Fetch</button>
264
  </div>
265
+ <div class="msg-info" style="margin-bottom:4px">Wikipedia, government sites, and docs work best. Some sites block automated access.</div>
266
+ <div id="url-msg"></div>
267
+ </div>
268
+
269
+ <!-- SOURCE LOADED -->
270
+ <div id="source-card">
271
+ <div class="sc-row">
272
+ <div class="sc-icon" id="sc-icon">&#128196;</div>
273
+ <div class="sc-info">
274
+ <div class="sc-name" id="source-name"></div>
275
+ <div class="sc-meta" id="source-chunks"></div>
276
  </div>
 
 
277
  </div>
278
+ <div class="sc-ready">&#10003;&ensp;Ready for questions</div>
279
+ </div>
280
+ </div>
281
+
282
+ <!-- ── RIGHT PANEL: RESEARCH ── -->
283
+ <div class="panel panel-right">
284
+ <div class="sec-head">
285
+ <div class="sec-icon sec-icon-purple">&#128269;</div>
286
+ <span class="sec-title">Research Query</span>
287
+ </div>
288
+
289
+ <div class="q-card">
290
+ <div class="q-label">Your Question</div>
291
+ <textarea id="q-inp" rows="3"
292
+ placeholder="Ask anything about the loaded document or URL&#10;Press Enter to submit, Shift+Enter for new line"
293
+ onkeydown="qk(event)"></textarea>
294
+ <div class="q-footer">
295
+ <button class="btn btn-primary" id="ask-btn" onclick="ask()">&#9889;&ensp;Ask</button>
296
+ <span id="q-err"></span>
297
  </div>
298
+ </div>
299
+
300
+ <!-- AGENT PIPELINE -->
301
+ <div id="pipeline">
302
+ <div class="pipe-row">
303
+ <div class="pipe-step" id="ps-planner"><span class="step-dot"></span>&#129518;&ensp;Planner</div>
304
+ <div class="pipe-arrow">&#8594;</div>
305
+ <div class="pipe-step" id="ps-retriever"><span class="step-dot"></span>&#128269;&ensp;Retriever</div>
306
+ <div class="pipe-arrow">&#8594;</div>
307
+ <div class="pipe-step" id="ps-grader"><span class="step-dot"></span>&#9878;&ensp;Grader</div>
308
+ <div class="pipe-arrow">&#8594;</div>
309
+ <div class="pipe-step" id="ps-generator"><span class="step-dot"></span>&#9997;&ensp;Generator</div>
310
+ <div class="pipe-arrow">&#8594;</div>
311
+ <div class="pipe-step" id="ps-critic"><span class="step-dot"></span>&#128300;&ensp;Critic</div>
312
+ </div>
313
+ </div>
314
+
315
+ <!-- TRACE LOG -->
316
+ <div id="trace-wrap">
317
+ <div class="trace-hdr">
318
+ <span class="trace-title">&#128240;&ensp;Agent Trace</span>
319
  </div>
320
+ <div class="trace-box" id="trace-log"></div>
321
  </div>
322
+
323
+ <!-- ANSWER -->
324
+ <div id="answer-wrap">
325
+ <div class="ans-header">
326
+ <div class="ans-title">
327
+ <div class="ans-title-icon">&#128161;</div>
328
+ Answer
329
+ </div>
330
+ <div class="ans-actions">
331
+ <span id="verdict-badge"></span>
332
+ <button class="btn btn-ghost" id="copy-btn" onclick="copyAns()">&#128203;&ensp;Copy</button>
333
  </div>
334
  </div>
335
+ <div class="ans-body">
 
 
336
  <div id="answer-text"></div>
 
337
  </div>
338
  </div>
339
+ </div>
340
  </main>
341
+
342
  <script>
343
  let pollTimer=null,seen=0;
344
+ const esc=s=>String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
345
 
346
+ const AGENT_ICONS={planner:"&#129518;",retriever:"&#128269;",grader:"&#9878;",generator:"&#9997;",critic:"&#128300;"};
347
+
348
+ /* ── TABS ── */
349
  function switchTab(btn,name){
350
+ document.querySelectorAll(".tab-btn").forEach(b=>b.classList.remove("active"));
351
+ btn.classList.add("active");
352
+ document.getElementById("tab-pdf").style.display=name==="pdf"?"":"none";
353
+ document.getElementById("tab-url").style.display=name==="url"?"":"none";
354
  }
355
+
356
+ /* ── DRAG & DROP ── */
357
+ function dg(e,over){e.preventDefault();document.getElementById("dz").classList[over?"add":"remove"]("drag-over");}
358
+ function dp(e){e.preventDefault();document.getElementById("dz").classList.remove("drag-over");const f=e.dataTransfer.files[0];if(f)up(f);}
359
  function fc(e){if(e.target.files[0])up(e.target.files[0]);}
360
 
361
+ /* ── UPLOAD PDF ── */
362
  async function up(file){
363
+ if(!file.name.toLowerCase().endsWith(".pdf")){showMsg("pdf-msg","error","Only PDF files are supported.");return;}
364
+ showMsg("pdf-msg","info","Uploading "+file.name+"...");
365
+ const fd=new FormData();fd.append("file",file);
366
  try{
367
+ const r=await fetch("/api/upload",{method:"POST",body:fd});
368
  const d=await r.json();
369
+ if(d.error){showMsg("pdf-msg","error",d.error);return;}
370
+ setSource(d.filename,d.chunks,"pdf");
371
+ showMsg("pdf-msg","ok","&#10003;&ensp;"+d.chunks+" chunks indexed from "+d.filename);
372
+ }catch(e){showMsg("pdf-msg","error","Upload failed: "+e.message);}
373
  }
374
 
375
+ /* ── FETCH URL ── */
376
  async function fetchURL(){
377
+ const url=document.getElementById("url-inp").value.trim();
378
+ if(!url){showMsg("url-msg","error","Please enter a URL.");return;}
379
+ const btn=document.getElementById("url-btn");
380
+ btn.disabled=true;btn.textContent="Fetching...";
381
+ showMsg("url-msg","info","Fetching and indexing page...");
382
  try{
383
+ const r=await fetch("/api/ingest_url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})});
384
  const d=await r.json();
385
+ if(d.error){showMsg("url-msg","error",d.error);return;}
386
+ setSource(d.url,d.chunks,"url");
387
+ showMsg("url-msg","ok","&#10003;&ensp;"+d.chunks+" chunks indexed");
388
+ }catch(e){showMsg("url-msg","error","Failed: "+e.message);}
389
+ finally{btn.disabled=false;btn.textContent="Fetch";}
390
  }
391
 
392
+ /* ── SOURCE LOADED ── */
393
+ function setSource(name,chunks,type){
394
+ document.getElementById("source-name").textContent=name;
395
+ document.getElementById("source-chunks").textContent=chunks+" chunks indexed";
396
+ document.getElementById("sc-icon").textContent=type==="pdf"?"&#128196;":"&#127760;";
397
+ document.getElementById("source-card").style.display="block";
398
+ const p=document.getElementById("hdr-src");
399
+ p.textContent=name.length>30?name.slice(0,30)+"...":name;
400
+ p.classList.add("loaded");
401
  }
402
 
403
+ /* ── PIPELINE ── */
404
+ const PIPE_AGENTS=["planner","retriever","grader","generator","critic"];
405
+ function resetPipeline(){PIPE_AGENTS.forEach(a=>{const el=document.getElementById("ps-"+a);if(el){el.classList.remove("active","done");}});}
406
+ function setAgent(name,done){
407
+ const el=document.getElementById("ps-"+name);
408
+ if(!el)return;
409
+ if(done){el.classList.remove("active");el.classList.add("done");}
410
+ else{el.classList.remove("done");el.classList.add("active");}
411
+ }
412
+
413
+ /* ── ASK ── */
414
+ function qk(e){if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();ask();}}
415
 
416
  async function ask(){
417
+ const q=document.getElementById("q-inp").value.trim();
418
+ document.getElementById("q-err").textContent="";
419
+ if(!q){document.getElementById("q-err").textContent="Please enter a question.";return;}
420
+
421
+ const btn=document.getElementById("ask-btn");
422
+ btn.disabled=true;
423
+ btn.innerHTML="<span class='spinner'></span>&ensp;Thinking...";
424
+
425
+ document.getElementById("pipeline").style.display="block";
426
+ document.getElementById("trace-wrap").style.display="block";
427
+ document.getElementById("trace-log").innerHTML="<div class='t-step'><span class='t-msg' style='color:var(--muted);font-style:italic'>Initialising agents...</span></div>";
428
+ document.getElementById("answer-wrap").style.display="none";
429
+ resetPipeline();
430
  seen=0;clearInterval(pollTimer);
431
+
432
  try{
433
+ const r=await fetch("/api/research",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({question:q})});
434
  const d=await r.json();
435
+ if(d.error){traceErr(d.error);resetAskBtn();return;}
 
 
 
 
436
  pollTimer=setInterval(()=>poll(d.query_id),1500);
437
+ }catch(e){traceErr("Network error: "+e.message);resetAskBtn();}
438
+ }
439
+
440
+ function resetAskBtn(){
441
+ const btn=document.getElementById("ask-btn");
442
+ btn.disabled=false;
443
+ btn.innerHTML="&#9889;&ensp;Ask";
444
  }
445
 
446
+ /* ── POLL ── */
447
  async function poll(qid){
448
  try{
449
+ const r=await fetch("/api/trace/"+qid);
450
+ if(!r.ok){traceErr("Server error "+r.status);clearInterval(pollTimer);resetAskBtn();return;}
451
  const d=await r.json();
452
+ renderTrace(d.trace||[]);
453
+ if(["complete","error"].includes(d.status)){
454
  clearInterval(pollTimer);
455
+ resetAskBtn();
456
+ if(d.status==="complete"&&d.result)renderAnswer(d.result);
457
+ else if(d.status==="error"&&d.result)traceErr(d.result.error||"An error occurred.");
458
  }
459
+ }catch(e){traceErr("Poll error: "+e.message);clearInterval(pollTimer);resetAskBtn();}
 
 
 
 
 
 
460
  }
461
 
462
+ /* ── TRACE ── */
463
+ function renderTrace(steps){
464
  if(!steps.length)return;
465
+ const log=document.getElementById("trace-log");
466
+ if(seen===0)log.innerHTML="";
467
  for(let i=seen;i<steps.length;i++){
468
  const s=steps[i];
469
+ const icon=AGENT_ICONS[s.agent]||"&#9679;";
470
+ const lat=s.latency_ms>0?"<span class='t-lat'>"+s.latency_ms+"ms</span>":"";
471
+ const cls="a-"+(["planner","retriever","grader","generator","critic"].includes(s.agent)?s.agent:"error");
472
+ log.innerHTML+="<div class='t-step'>"
473
+ +"<span class='t-icon'>"+icon+"</span>"
474
+ +"<span class='t-agent "+cls+"'>"+s.agent+"</span>"
475
+ +"<span class='t-msg'>"+esc(s.message)+"</span>"
476
+ +lat+"</div>";
477
+ if(s.status==="running")setAgent(s.agent,false);
478
+ else if(s.status==="complete")setAgent(s.agent,true);
479
  }
480
+ seen=steps.length;
481
+ log.scrollTop=log.scrollHeight;
482
+ }
483
+
484
+ function traceErr(msg){
485
+ const log=document.getElementById("trace-log");
486
+ log.innerHTML+="<div class='t-step'><span class='t-icon'>&#10060;</span><span class='t-agent a-error'>error</span><span class='t-msg' style='color:var(--red)'>"+esc(msg)+"</span></div>";
487
+ log.scrollTop=log.scrollHeight;
488
+ }
489
+
490
+ /* ── ANSWER ── */
491
+ function renderAnswer(result){
492
+ document.getElementById("answer-wrap").style.display="block";
493
+ document.getElementById("answer-text").textContent=result.generation||"No answer generated.";
494
+ const vb=document.getElementById("verdict-badge");
495
+ if(result.verdict==="APPROVED"){vb.className="v-ok";vb.textContent="&#10003; High confidence";}
496
+ else if(result.verdict){vb.className="v-warn";vb.textContent="&#9888; Verify with source";}
497
+ else{vb.textContent="";}
498
+ document.getElementById("answer-wrap").scrollIntoView({behavior:"smooth",block:"start"});
499
  }
500
 
501
+ /* ── COPY ── */
502
+ async function copyAns(){
503
+ const text=document.getElementById("answer-text").textContent;
504
+ try{
505
+ await navigator.clipboard.writeText(text);
506
+ const btn=document.getElementById("copy-btn");
507
+ btn.innerHTML="&#10003;&ensp;Copied!";
508
+ setTimeout(()=>{btn.innerHTML="&#128203;&ensp;Copy";},2000);
509
+ }catch(e){}
510
  }
511
 
512
+ /* ── MSG HELPER ── */
513
+ function showMsg(id,type,msg){
514
  const el=document.getElementById(id);
515
+ if(type==="ok")el.innerHTML="<div class='msg msg-ok'>"+msg+"</div>";
516
+ else if(type==="error")el.innerHTML="<div class='msg msg-err'>"+esc(msg)+"</div>";
517
+ else el.innerHTML="<div class='msg-info'>"+esc(msg)+"</div>";
518
  }
519
  </script>
520
  </body>
write_html.py ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pathlib
2
+
3
+ HTML = '''\
4
+ <!DOCTYPE html>
5
+ <html lang="en">
6
+ <head>
7
+ <meta charset="UTF-8"/>
8
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
9
+ <title>DocMind β€” AI Document Research</title>
10
+ <style>
11
+ :root{
12
+ --bg:#0d0f1a;--surface:#13161f;--card:#181c27;--card2:#1e2230;
13
+ --border:#252836;--border2:#2e3244;
14
+ --text:#e8eaf2;--sub:#b0b8cc;--muted:#7880a0;
15
+ --accent:#5b8ff9;--accent2:#3a6ee8;
16
+ --green:#22d47a;--red:#f05c5c;--teal:#29c6d4;--gold:#f5a623;--purple:#a78bfa;
17
+ }
18
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
19
+ html,body{height:100%}
20
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
21
+ background:var(--bg);color:var(--text);font-size:14px;line-height:1.5}
22
+
23
+ /* ── ANIMATIONS ────────────────────────────────── */
24
+ @keyframes spin{to{transform:rotate(360deg)}}
25
+ @keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
26
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
27
+ @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
28
+
29
+ /* ── HEADER ────────────────────────────────────── */
30
+ header{
31
+ background:var(--surface);
32
+ border-bottom:1px solid var(--border);
33
+ padding:0 28px;height:56px;
34
+ display:flex;align-items:center;gap:14px;
35
+ position:sticky;top:0;z-index:100;
36
+ }
37
+ .logo{display:flex;align-items:center;gap:9px;text-decoration:none}
38
+ .logo-icon{width:30px;height:30px;background:linear-gradient(135deg,var(--accent),var(--purple));
39
+ border-radius:8px;display:flex;align-items:center;justify-content:center;
40
+ font-size:15px;flex-shrink:0}
41
+ .logo-text{font-size:1rem;font-weight:800;color:var(--text);letter-spacing:-.3px}
42
+ .logo-text span{color:var(--accent)}
43
+ .logo-sub{font-size:.68rem;color:var(--muted);margin-left:2px;font-weight:400;
44
+ display:none} /* shown on wider screens */
45
+ .hdr-badges{display:flex;gap:8px;align-items:center;margin-left:4px}
46
+ .badge{font-size:.65rem;font-weight:700;padding:3px 9px;border-radius:20px;letter-spacing:.02em}
47
+ .badge-model{background:rgba(91,143,249,.12);border:1px solid rgba(91,143,249,.25);color:var(--accent)}
48
+ .badge-src{background:rgba(113,128,160,.1);border:1px solid var(--border);color:var(--muted);
49
+ max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
50
+ .badge-src.loaded{background:rgba(34,212,122,.1);border-color:rgba(34,212,122,.3);color:var(--green)}
51
+ #hdr-src{margin-left:auto}
52
+
53
+ /* ── LAYOUT ─────────────────────────────────────── */
54
+ main{display:grid;grid-template-columns:320px 1fr;height:calc(100vh - 56px);overflow:hidden}
55
+ .panel{padding:24px 22px;overflow-y:auto;height:100%}
56
+ .panel-left{border-right:1px solid var(--border);background:var(--surface)}
57
+ .panel-right{background:var(--bg)}
58
+
59
+ /* ── SECTION HEADERS ────────────────────────────── */
60
+ .sec-head{display:flex;align-items:center;gap:8px;margin-bottom:16px}
61
+ .sec-icon{width:26px;height:26px;border-radius:7px;display:flex;align-items:center;
62
+ justify-content:center;font-size:13px;flex-shrink:0}
63
+ .sec-icon-blue{background:rgba(91,143,249,.15)}
64
+ .sec-icon-purple{background:rgba(167,139,250,.15)}
65
+ .sec-title{font-size:.75rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:var(--sub)}
66
+
67
+ /* ── TABS ────────────────────────────────────────── */
68
+ .tabs{display:flex;gap:2px;background:rgba(255,255,255,.03);border-radius:9px;
69
+ padding:3px;margin-bottom:18px;border:1px solid var(--border)}
70
+ .tab-btn{flex:1;background:none;border:none;color:var(--muted);font-size:.78rem;
71
+ font-weight:600;padding:7px 10px;border-radius:7px;cursor:pointer;
72
+ transition:all .18s;font-family:inherit;display:flex;align-items:center;
73
+ justify-content:center;gap:5px}
74
+ .tab-btn.active{background:var(--card2);color:var(--text);
75
+ box-shadow:0 1px 6px rgba(0,0,0,.35)}
76
+
77
+ /* ── DROPZONE ────────────────────────────────────── */
78
+ .dropzone{
79
+ border:2px dashed var(--border2);border-radius:12px;padding:28px 16px;
80
+ text-align:center;cursor:pointer;transition:all .22s;position:relative;overflow:hidden;
81
+ }
82
+ .dropzone::before{
83
+ content:"";position:absolute;inset:0;
84
+ background:linear-gradient(90deg,transparent 0%,rgba(91,143,249,.04) 50%,transparent 100%);
85
+ background-size:200% 100%;opacity:0;transition:opacity .3s;
86
+ }
87
+ .dropzone:hover{border-color:var(--accent);background:rgba(91,143,249,.04)}
88
+ .dropzone:hover::before{opacity:1;animation:shimmer 2s infinite}
89
+ .dropzone.drag-over{border-color:var(--accent);background:rgba(91,143,249,.08);transform:scale(1.01)}
90
+ .dz-icon{font-size:2.2rem;margin-bottom:10px;display:block}
91
+ .dz-label{font-size:.84rem;color:var(--sub);line-height:1.5}
92
+ .dz-label strong{color:var(--accent)}
93
+ .dz-hint{font-size:.72rem;color:var(--muted);margin-top:5px}
94
+
95
+ /* ── URL INPUT ────────────────────────────────────── */
96
+ .url-row{display:flex;gap:8px;margin-bottom:10px}
97
+ .url-row input{flex:1;background:var(--card);border:1px solid var(--border2);
98
+ border-radius:8px;padding:9px 12px;color:var(--text);font-size:.84rem;
99
+ font-family:inherit;outline:none;transition:border-color .18s}
100
+ .url-row input:focus{border-color:var(--accent)}
101
+
102
+ /* ── SOURCE LOADED CARD ──────────────────────────── */
103
+ #source-card{
104
+ display:none;margin-top:16px;background:var(--card);
105
+ border:1px solid rgba(34,212,122,.2);border-radius:10px;padding:13px 14px;
106
+ animation:fadeUp .25s ease;
107
+ }
108
+ .sc-row{display:flex;align-items:flex-start;gap:10px}
109
+ .sc-icon{width:34px;height:34px;background:rgba(34,212,122,.12);border-radius:8px;
110
+ flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:16px}
111
+ .sc-info{flex:1;min-width:0}
112
+ .sc-name{font-size:.82rem;font-weight:600;color:var(--text);
113
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:3px}
114
+ .sc-meta{font-size:.72rem;color:var(--teal)}
115
+ .sc-ready{display:inline-flex;align-items:center;gap:5px;margin-top:8px;
116
+ font-size:.7rem;font-weight:700;color:var(--green);
117
+ background:rgba(34,212,122,.08);border:1px solid rgba(34,212,122,.18);
118
+ border-radius:5px;padding:3px 8px}
119
+
120
+ /* ── MESSAGES ────────────────────────────────────── */
121
+ .msg{border-radius:8px;padding:10px 13px;font-size:.79rem;
122
+ margin-top:10px;line-height:1.55;animation:fadeUp .2s ease}
123
+ .msg-ok{background:rgba(34,212,122,.07);border:1px solid rgba(34,212,122,.2);color:var(--green)}
124
+ .msg-err{background:rgba(240,92,92,.07);border:1px solid rgba(240,92,92,.2);color:var(--red)}
125
+ .msg-info{color:var(--muted);font-size:.76rem;margin-top:8px}
126
+
127
+ /* ── BUTTONS ─────────────────────────────────────── */
128
+ .btn{display:inline-flex;align-items:center;gap:7px;padding:9px 20px;
129
+ border-radius:8px;border:none;font-size:.84rem;font-weight:600;
130
+ cursor:pointer;transition:all .18s;font-family:inherit}
131
+ .btn-primary{background:linear-gradient(135deg,var(--accent),var(--accent2));
132
+ color:#fff;box-shadow:0 2px 8px rgba(91,143,249,.25)}
133
+ .btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 14px rgba(91,143,249,.35)}
134
+ .btn-primary:active{transform:none}
135
+ .btn-primary:disabled{opacity:.5;cursor:default;transform:none;box-shadow:none}
136
+ .btn-sm{padding:7px 14px;font-size:.78rem}
137
+ .btn-ghost{background:rgba(255,255,255,.06);border:1px solid var(--border2);
138
+ color:var(--sub);font-size:.74rem;padding:5px 12px;border-radius:6px}
139
+ .btn-ghost:hover{background:rgba(255,255,255,.1);color:var(--text)}
140
+ .spinner{width:13px;height:13px;border:2px solid rgba(255,255,255,.3);
141
+ border-top-color:#fff;border-radius:50%;animation:spin .65s linear infinite;flex-shrink:0}
142
+
143
+ /* ── QUESTION AREA ───────────────────────────────── */
144
+ .q-card{background:var(--card);border:1px solid var(--border2);
145
+ border-radius:11px;padding:16px;margin-bottom:16px}
146
+ .q-label{font-size:.7rem;font-weight:700;color:var(--muted);text-transform:uppercase;
147
+ letter-spacing:.06em;margin-bottom:8px}
148
+ textarea{width:100%;background:transparent;border:none;color:var(--text);
149
+ font-size:.9rem;font-family:inherit;outline:none;resize:none;
150
+ line-height:1.6;min-height:72px}
151
+ textarea::placeholder{color:var(--muted)}
152
+ .q-footer{display:flex;align-items:center;gap:10px;padding-top:12px;
153
+ border-top:1px solid var(--border)}
154
+ #q-err{font-size:.77rem;color:var(--red)}
155
+
156
+ /* ── PIPELINE ────────────────────────────────────── */
157
+ #pipeline{display:none;margin-bottom:14px;animation:fadeUp .2s ease}
158
+ .pipe-row{display:flex;align-items:center;gap:0;background:var(--card);
159
+ border:1px solid var(--border2);border-radius:10px;padding:10px 14px;
160
+ overflow-x:auto}
161
+ .pipe-step{display:flex;align-items:center;gap:5px;font-size:.72rem;font-weight:600;
162
+ color:var(--muted);white-space:nowrap;padding:4px 8px;border-radius:6px;
163
+ transition:all .2s}
164
+ .pipe-step.active{color:var(--accent);background:rgba(91,143,249,.12)}
165
+ .pipe-step.done{color:var(--green)}
166
+ .pipe-step .step-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0}
167
+ .pipe-step.active .step-dot{animation:pulse .9s ease infinite}
168
+ .pipe-arrow{color:var(--border2);font-size:.8rem;padding:0 2px;flex-shrink:0}
169
+
170
+ /* ── TRACE LOG ───────────────────────────────────── */
171
+ #trace-wrap{display:none;margin-bottom:16px;animation:fadeUp .2s ease}
172
+ .trace-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px}
173
+ .trace-title{font-size:.7rem;font-weight:700;text-transform:uppercase;
174
+ letter-spacing:.07em;color:var(--muted)}
175
+ .trace-box{background:var(--card);border:1px solid var(--border2);border-radius:10px;
176
+ padding:4px 0;max-height:220px;overflow-y:auto}
177
+ .t-step{display:flex;align-items:flex-start;gap:10px;padding:8px 14px;
178
+ border-bottom:1px solid var(--border);animation:fadeUp .18s ease;transition:background .15s}
179
+ .t-step:last-child{border-bottom:none}
180
+ .t-step:hover{background:rgba(255,255,255,.02)}
181
+ .t-icon{font-size:13px;flex-shrink:0;margin-top:1px;width:18px;text-align:center}
182
+ .t-agent{font-size:.64rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;
183
+ padding:2px 7px;border-radius:4px;flex-shrink:0;white-space:nowrap}
184
+ .a-planner {background:rgba(91,143,249,.14);color:var(--accent)}
185
+ .a-retriever{background:rgba(41,198,212,.14);color:var(--teal)}
186
+ .a-grader {background:rgba(245,166,35,.14);color:var(--gold)}
187
+ .a-generator{background:rgba(34,212,122,.14);color:var(--green)}
188
+ .a-critic {background:rgba(167,139,250,.14);color:var(--purple)}
189
+ .a-error {background:rgba(240,92,92,.14);color:var(--red)}
190
+ .t-msg{flex:1;font-size:.78rem;color:var(--sub);line-height:1.5}
191
+ .t-lat{font-size:.65rem;color:rgba(128,136,160,.5);flex-shrink:0;margin-top:2px}
192
+
193
+ /* ── ANSWER CARD ─────────────────────────────────── */
194
+ #answer-wrap{display:none;background:var(--card);border:1px solid var(--border2);
195
+ border-radius:12px;overflow:hidden;animation:fadeUp .3s ease}
196
+ .ans-header{display:flex;align-items:center;justify-content:space-between;
197
+ padding:13px 18px 12px;border-bottom:1px solid var(--border);
198
+ background:rgba(255,255,255,.02)}
199
+ .ans-title{display:flex;align-items:center;gap:8px;font-size:.72rem;font-weight:800;
200
+ text-transform:uppercase;letter-spacing:.08em;color:var(--sub)}
201
+ .ans-title-icon{width:22px;height:22px;background:rgba(34,212,122,.15);border-radius:6px;
202
+ display:flex;align-items:center;justify-content:center;font-size:11px}
203
+ .ans-actions{display:flex;align-items:center;gap:8px}
204
+ #verdict-badge{font-size:.68rem;font-weight:700;padding:3px 10px;border-radius:20px}
205
+ .v-ok {background:rgba(34,212,122,.1);border:1px solid rgba(34,212,122,.25);color:var(--green)}
206
+ .v-warn{background:rgba(245,166,35,.1);border:1px solid rgba(245,166,35,.25);color:var(--gold)}
207
+ .ans-body{padding:18px}
208
+ #answer-text{font-size:.9rem;line-height:1.78;color:var(--text);
209
+ white-space:pre-wrap;word-break:break-word}
210
+
211
+ /* ── RESPONSIVE ──────────────────────────────────── */
212
+ @media(min-width:1100px){.logo-sub{display:block}}
213
+ @media(max-width:768px){
214
+ main{grid-template-columns:1fr;height:auto;overflow:visible}
215
+ .panel{height:auto}
216
+ .panel-left{border-right:none;border-bottom:1px solid var(--border)}
217
+ }
218
+ </style>
219
+ </head>
220
+ <body>
221
+
222
+ <!-- ── HEADER ── -->
223
+ <header>
224
+ <a class="logo" href="#">
225
+ <div class="logo-icon">&#129504;</div>
226
+ <span class="logo-text">Doc<span>Mind</span></span>
227
+ </a>
228
+ <span class="logo-sub">AI Document Research</span>
229
+ <div class="hdr-badges">
230
+ <span class="badge badge-model">Qwen 2.5 7B</span>
231
+ </div>
232
+ <span class="badge badge-src" id="hdr-src">No source loaded</span>
233
+ </header>
234
+
235
+ <main>
236
+ <!-- ── LEFT PANEL: KNOWLEDGE BASE ── -->
237
+ <div class="panel panel-left">
238
+ <div class="sec-head">
239
+ <div class="sec-icon sec-icon-blue">&#128218;</div>
240
+ <span class="sec-title">Knowledge Base</span>
241
+ </div>
242
+
243
+ <div class="tabs">
244
+ <button class="tab-btn active" onclick="switchTab(this,'pdf')">&#128196;&ensp;Upload PDF</button>
245
+ <button class="tab-btn" onclick="switchTab(this,'url')">&#127760;&ensp;Paste URL</button>
246
+ </div>
247
+
248
+ <!-- PDF TAB -->
249
+ <div id="tab-pdf">
250
+ <div class="dropzone" id="dz"
251
+ onclick="document.getElementById('fi').click()"
252
+ ondragover="dg(event,true)" ondragleave="dg(event,false)" ondrop="dp(event)">
253
+ <span class="dz-icon">&#128196;</span>
254
+ <div class="dz-label"><strong>Click to browse</strong> or drag &amp; drop</div>
255
+ <div class="dz-hint">PDF only &middot; max 10 MB</div>
256
+ </div>
257
+ <input type="file" id="fi" accept=".pdf" style="display:none" onchange="fc(event)"/>
258
+ <div id="pdf-msg"></div>
259
+ </div>
260
+
261
+ <!-- URL TAB -->
262
+ <div id="tab-url" style="display:none">
263
+ <div class="url-row">
264
+ <input type="url" id="url-inp" placeholder="https://en.wikipedia.org/wiki/..."
265
+ onkeydown="if(event.key==='Enter')fetchURL()"/>
266
+ <button class="btn btn-primary btn-sm" id="url-btn" onclick="fetchURL()">Fetch</button>
267
+ </div>
268
+ <div class="msg-info" style="margin-bottom:4px">Wikipedia, government sites, and docs work best. Some sites block automated access.</div>
269
+ <div id="url-msg"></div>
270
+ </div>
271
+
272
+ <!-- SOURCE LOADED -->
273
+ <div id="source-card">
274
+ <div class="sc-row">
275
+ <div class="sc-icon" id="sc-icon">&#128196;</div>
276
+ <div class="sc-info">
277
+ <div class="sc-name" id="source-name"></div>
278
+ <div class="sc-meta" id="source-chunks"></div>
279
+ </div>
280
+ </div>
281
+ <div class="sc-ready">&#10003;&ensp;Ready for questions</div>
282
+ </div>
283
+ </div>
284
+
285
+ <!-- ── RIGHT PANEL: RESEARCH ── -->
286
+ <div class="panel panel-right">
287
+ <div class="sec-head">
288
+ <div class="sec-icon sec-icon-purple">&#128269;</div>
289
+ <span class="sec-title">Research Query</span>
290
+ </div>
291
+
292
+ <div class="q-card">
293
+ <div class="q-label">Your Question</div>
294
+ <textarea id="q-inp" rows="3"
295
+ placeholder="Ask anything about the loaded document or URL&#10;Press Enter to submit, Shift+Enter for new line"
296
+ onkeydown="qk(event)"></textarea>
297
+ <div class="q-footer">
298
+ <button class="btn btn-primary" id="ask-btn" onclick="ask()">&#9889;&ensp;Ask</button>
299
+ <span id="q-err"></span>
300
+ </div>
301
+ </div>
302
+
303
+ <!-- AGENT PIPELINE -->
304
+ <div id="pipeline">
305
+ <div class="pipe-row">
306
+ <div class="pipe-step" id="ps-planner"><span class="step-dot"></span>&#129518;&ensp;Planner</div>
307
+ <div class="pipe-arrow">&#8594;</div>
308
+ <div class="pipe-step" id="ps-retriever"><span class="step-dot"></span>&#128269;&ensp;Retriever</div>
309
+ <div class="pipe-arrow">&#8594;</div>
310
+ <div class="pipe-step" id="ps-grader"><span class="step-dot"></span>&#9878;&ensp;Grader</div>
311
+ <div class="pipe-arrow">&#8594;</div>
312
+ <div class="pipe-step" id="ps-generator"><span class="step-dot"></span>&#9997;&ensp;Generator</div>
313
+ <div class="pipe-arrow">&#8594;</div>
314
+ <div class="pipe-step" id="ps-critic"><span class="step-dot"></span>&#128300;&ensp;Critic</div>
315
+ </div>
316
+ </div>
317
+
318
+ <!-- TRACE LOG -->
319
+ <div id="trace-wrap">
320
+ <div class="trace-hdr">
321
+ <span class="trace-title">&#128240;&ensp;Agent Trace</span>
322
+ </div>
323
+ <div class="trace-box" id="trace-log"></div>
324
+ </div>
325
+
326
+ <!-- ANSWER -->
327
+ <div id="answer-wrap">
328
+ <div class="ans-header">
329
+ <div class="ans-title">
330
+ <div class="ans-title-icon">&#128161;</div>
331
+ Answer
332
+ </div>
333
+ <div class="ans-actions">
334
+ <span id="verdict-badge"></span>
335
+ <button class="btn btn-ghost" id="copy-btn" onclick="copyAns()">&#128203;&ensp;Copy</button>
336
+ </div>
337
+ </div>
338
+ <div class="ans-body">
339
+ <div id="answer-text"></div>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </main>
344
+
345
+ <script>
346
+ let pollTimer=null,seen=0;
347
+ const esc=s=>String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
348
+
349
+ const AGENT_ICONS={planner:"&#129518;",retriever:"&#128269;",grader:"&#9878;",generator:"&#9997;",critic:"&#128300;"};
350
+
351
+ /* ── TABS ── */
352
+ function switchTab(btn,name){
353
+ document.querySelectorAll(".tab-btn").forEach(b=>b.classList.remove("active"));
354
+ btn.classList.add("active");
355
+ document.getElementById("tab-pdf").style.display=name==="pdf"?"":"none";
356
+ document.getElementById("tab-url").style.display=name==="url"?"":"none";
357
+ }
358
+
359
+ /* ── DRAG & DROP ── */
360
+ function dg(e,over){e.preventDefault();document.getElementById("dz").classList[over?"add":"remove"]("drag-over");}
361
+ function dp(e){e.preventDefault();document.getElementById("dz").classList.remove("drag-over");const f=e.dataTransfer.files[0];if(f)up(f);}
362
+ function fc(e){if(e.target.files[0])up(e.target.files[0]);}
363
+
364
+ /* ── UPLOAD PDF ── */
365
+ async function up(file){
366
+ if(!file.name.toLowerCase().endsWith(".pdf")){showMsg("pdf-msg","error","Only PDF files are supported.");return;}
367
+ showMsg("pdf-msg","info","Uploading "+file.name+"...");
368
+ const fd=new FormData();fd.append("file",file);
369
+ try{
370
+ const r=await fetch("/api/upload",{method:"POST",body:fd});
371
+ const d=await r.json();
372
+ if(d.error){showMsg("pdf-msg","error",d.error);return;}
373
+ setSource(d.filename,d.chunks,"pdf");
374
+ showMsg("pdf-msg","ok","&#10003;&ensp;"+d.chunks+" chunks indexed from "+d.filename);
375
+ }catch(e){showMsg("pdf-msg","error","Upload failed: "+e.message);}
376
+ }
377
+
378
+ /* ── FETCH URL ── */
379
+ async function fetchURL(){
380
+ const url=document.getElementById("url-inp").value.trim();
381
+ if(!url){showMsg("url-msg","error","Please enter a URL.");return;}
382
+ const btn=document.getElementById("url-btn");
383
+ btn.disabled=true;btn.textContent="Fetching...";
384
+ showMsg("url-msg","info","Fetching and indexing page...");
385
+ try{
386
+ const r=await fetch("/api/ingest_url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})});
387
+ const d=await r.json();
388
+ if(d.error){showMsg("url-msg","error",d.error);return;}
389
+ setSource(d.url,d.chunks,"url");
390
+ showMsg("url-msg","ok","&#10003;&ensp;"+d.chunks+" chunks indexed");
391
+ }catch(e){showMsg("url-msg","error","Failed: "+e.message);}
392
+ finally{btn.disabled=false;btn.textContent="Fetch";}
393
+ }
394
+
395
+ /* ── SOURCE LOADED ── */
396
+ function setSource(name,chunks,type){
397
+ document.getElementById("source-name").textContent=name;
398
+ document.getElementById("source-chunks").textContent=chunks+" chunks indexed";
399
+ document.getElementById("sc-icon").textContent=type==="pdf"?"&#128196;":"&#127760;";
400
+ document.getElementById("source-card").style.display="block";
401
+ const p=document.getElementById("hdr-src");
402
+ p.textContent=name.length>30?name.slice(0,30)+"...":name;
403
+ p.classList.add("loaded");
404
+ }
405
+
406
+ /* ── PIPELINE ── */
407
+ const PIPE_AGENTS=["planner","retriever","grader","generator","critic"];
408
+ function resetPipeline(){PIPE_AGENTS.forEach(a=>{const el=document.getElementById("ps-"+a);if(el){el.classList.remove("active","done");}});}
409
+ function setAgent(name,done){
410
+ const el=document.getElementById("ps-"+name);
411
+ if(!el)return;
412
+ if(done){el.classList.remove("active");el.classList.add("done");}
413
+ else{el.classList.remove("done");el.classList.add("active");}
414
+ }
415
+
416
+ /* ── ASK ── */
417
+ function qk(e){if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();ask();}}
418
+
419
+ async function ask(){
420
+ const q=document.getElementById("q-inp").value.trim();
421
+ document.getElementById("q-err").textContent="";
422
+ if(!q){document.getElementById("q-err").textContent="Please enter a question.";return;}
423
+
424
+ const btn=document.getElementById("ask-btn");
425
+ btn.disabled=true;
426
+ btn.innerHTML="<span class='spinner'></span>&ensp;Thinking...";
427
+
428
+ document.getElementById("pipeline").style.display="block";
429
+ document.getElementById("trace-wrap").style.display="block";
430
+ document.getElementById("trace-log").innerHTML="<div class='t-step'><span class='t-msg' style='color:var(--muted);font-style:italic'>Initialising agents...</span></div>";
431
+ document.getElementById("answer-wrap").style.display="none";
432
+ resetPipeline();
433
+ seen=0;clearInterval(pollTimer);
434
+
435
+ try{
436
+ const r=await fetch("/api/research",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({question:q})});
437
+ const d=await r.json();
438
+ if(d.error){traceErr(d.error);resetAskBtn();return;}
439
+ pollTimer=setInterval(()=>poll(d.query_id),1500);
440
+ }catch(e){traceErr("Network error: "+e.message);resetAskBtn();}
441
+ }
442
+
443
+ function resetAskBtn(){
444
+ const btn=document.getElementById("ask-btn");
445
+ btn.disabled=false;
446
+ btn.innerHTML="&#9889;&ensp;Ask";
447
+ }
448
+
449
+ /* ── POLL ── */
450
+ async function poll(qid){
451
+ try{
452
+ const r=await fetch("/api/trace/"+qid);
453
+ if(!r.ok){traceErr("Server error "+r.status);clearInterval(pollTimer);resetAskBtn();return;}
454
+ const d=await r.json();
455
+ renderTrace(d.trace||[]);
456
+ if(["complete","error"].includes(d.status)){
457
+ clearInterval(pollTimer);
458
+ resetAskBtn();
459
+ if(d.status==="complete"&&d.result)renderAnswer(d.result);
460
+ else if(d.status==="error"&&d.result)traceErr(d.result.error||"An error occurred.");
461
+ }
462
+ }catch(e){traceErr("Poll error: "+e.message);clearInterval(pollTimer);resetAskBtn();}
463
+ }
464
+
465
+ /* ── TRACE ── */
466
+ function renderTrace(steps){
467
+ if(!steps.length)return;
468
+ const log=document.getElementById("trace-log");
469
+ if(seen===0)log.innerHTML="";
470
+ for(let i=seen;i<steps.length;i++){
471
+ const s=steps[i];
472
+ const icon=AGENT_ICONS[s.agent]||"&#9679;";
473
+ const lat=s.latency_ms>0?"<span class='t-lat'>"+s.latency_ms+"ms</span>":"";
474
+ const cls="a-"+(["planner","retriever","grader","generator","critic"].includes(s.agent)?s.agent:"error");
475
+ log.innerHTML+="<div class='t-step'>"
476
+ +"<span class='t-icon'>"+icon+"</span>"
477
+ +"<span class='t-agent "+cls+"'>"+s.agent+"</span>"
478
+ +"<span class='t-msg'>"+esc(s.message)+"</span>"
479
+ +lat+"</div>";
480
+ if(s.status==="running")setAgent(s.agent,false);
481
+ else if(s.status==="complete")setAgent(s.agent,true);
482
+ }
483
+ seen=steps.length;
484
+ log.scrollTop=log.scrollHeight;
485
+ }
486
+
487
+ function traceErr(msg){
488
+ const log=document.getElementById("trace-log");
489
+ log.innerHTML+="<div class='t-step'><span class='t-icon'>&#10060;</span><span class='t-agent a-error'>error</span><span class='t-msg' style='color:var(--red)'>"+esc(msg)+"</span></div>";
490
+ log.scrollTop=log.scrollHeight;
491
+ }
492
+
493
+ /* ── ANSWER ── */
494
+ function renderAnswer(result){
495
+ document.getElementById("answer-wrap").style.display="block";
496
+ document.getElementById("answer-text").textContent=result.generation||"No answer generated.";
497
+ const vb=document.getElementById("verdict-badge");
498
+ if(result.verdict==="APPROVED"){vb.className="v-ok";vb.textContent="&#10003; High confidence";}
499
+ else if(result.verdict){vb.className="v-warn";vb.textContent="&#9888; Verify with source";}
500
+ else{vb.textContent="";}
501
+ document.getElementById("answer-wrap").scrollIntoView({behavior:"smooth",block:"start"});
502
+ }
503
+
504
+ /* ── COPY ── */
505
+ async function copyAns(){
506
+ const text=document.getElementById("answer-text").textContent;
507
+ try{
508
+ await navigator.clipboard.writeText(text);
509
+ const btn=document.getElementById("copy-btn");
510
+ btn.innerHTML="&#10003;&ensp;Copied!";
511
+ setTimeout(()=>{btn.innerHTML="&#128203;&ensp;Copy";},2000);
512
+ }catch(e){}
513
+ }
514
+
515
+ /* ── MSG HELPER ── */
516
+ function showMsg(id,type,msg){
517
+ const el=document.getElementById(id);
518
+ if(type==="ok")el.innerHTML="<div class='msg msg-ok'>"+msg+"</div>";
519
+ else if(type==="error")el.innerHTML="<div class='msg msg-err'>"+esc(msg)+"</div>";
520
+ else el.innerHTML="<div class='msg-info'>"+esc(msg)+"</div>";
521
+ }
522
+ </script>
523
+ </body>
524
+ </html>
525
+ '''
526
+
527
+ pathlib.Path('E:/HuggingFace/docmind/templates/index.html').write_text(HTML, encoding='utf-8')
528
+ print('Done, size:', len(HTML))