| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Rabbook</title> |
| <link rel="stylesheet" href="/static/style.css"> |
| </head> |
| <body> |
| <header class="topbar"> |
| <div class="topbar__brand"> |
| <h1 class="topbar__title">Rabbook</h1> |
| <span class="topbar__badge">FastAPI + RAG</span> |
| </div> |
| </header> |
|
|
| {% if message %} |
| <div class="toast toast--success">{{ message }}</div> |
| {% endif %} |
| {% if error %} |
| <div class="toast toast--error">{{ error }}</div> |
| {% endif %} |
|
|
| <div class="layout"> |
| |
| <aside class="sidebar"> |
| <div class="sidebar__section"> |
| <h2 class="sidebar__heading">Sources</h2> |
| <p class="sidebar__desc">Upload documents or import URLs to build your knowledge base.</p> |
|
|
| <div class="source-upload-group"> |
| <form action="/documents" method="post" enctype="multipart/form-data" class="upload-form"> |
| <label for="document" class="upload-dropzone"> |
| <span class="upload-dropzone__icon">+</span> |
| <span class="upload-dropzone__text">Upload PDF or TXT</span> |
| <input id="document" name="document" type="file" accept=".pdf,.txt" hidden> |
| </label> |
| <div class="upload-form__filename" id="filename-display"></div> |
| <button type="submit" class="btn btn--sm btn--full">Upload</button> |
| </form> |
|
|
| <div class="divider-or"><span>or</span></div> |
|
|
| <form action="/urls" method="post" class="url-form"> |
| <input id="url" name="url" type="url" placeholder="https://example.com/article" class="input--compact"> |
| <button type="submit" class="btn btn--sm btn--full">Import URL</button> |
| </form> |
| </div> |
| </div> |
|
|
| <div class="sidebar__section"> |
| <h2 class="sidebar__heading">Document Library |
| {% if library_documents %}<span class="count-badge">{{ library_documents|length }}</span>{% endif %} |
| </h2> |
| {% if library_documents %} |
| <div class="doc-list"> |
| {% for document in library_documents %} |
| <div class="doc-item"> |
| <div class="doc-item__icon"> |
| {% if document.file_type == 'pdf' %} |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/></svg> |
| {% elif document.file_type == 'txt' %} |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg> |
| {% else %} |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg> |
| {% endif %} |
| </div> |
| <div class="doc-item__info"> |
| <p class="doc-item__name">{{ document.file_name }}</p> |
| <p class="doc-item__meta">{{ document.file_type|upper }} · {{ document.chunk_count }} chunks{% if document.page_count is not none %} · {{ document.page_count }} pg{% endif %}</p> |
| </div> |
| <form action="/documents/{{ document.document_id }}/delete" method="post" class="doc-item__delete" onsubmit="return confirm('Delete this document from the library and vector store?');"> |
| <button type="submit" class="btn-icon btn-icon--danger" title="Delete document"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3,6 5,6 21,6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg> |
| </button> |
| </form> |
| </div> |
| {% endfor %} |
| </div> |
| {% else %} |
| <p class="empty-state">No sources added yet. Upload a file or import a URL to get started.</p> |
| {% endif %} |
| </div> |
|
|
| <div class="sidebar__section"> |
| <h2 class="sidebar__heading">Maintenance</h2> |
| <div class="action-stack"> |
| <form action="/maintenance/refresh" method="post"> |
| <button type="submit" class="btn btn--sm btn--full">Refresh Runtime State</button> |
| </form> |
| <form action="/maintenance/registry/rebuild" method="post"> |
| <button type="submit" class="btn btn--sm btn--full">Rebuild Chunk Registry</button> |
| </form> |
| <form action="/maintenance/uploads/reingest" method="post" onsubmit="return confirm('Re-ingest all uploaded files and rebuild the vector store?');"> |
| <button type="submit" class="btn btn--sm btn--full">Re-ingest Uploads</button> |
| </form> |
| <form action="/maintenance/history/clear" method="post" onsubmit="return confirm('Clear the entire chat history?');"> |
| <button type="submit" class="btn btn--danger btn--sm btn--full">Clear Chat History</button> |
| </form> |
| <form action="/maintenance/notes/clear" method="post" onsubmit="return confirm('Clear all saved notes?');"> |
| <button type="submit" class="btn btn--danger btn--sm btn--full">Clear Saved Notes</button> |
| </form> |
| </div> |
| </div> |
| </aside> |
|
|
| |
| <main class="main"> |
| <div class="main__inner"> |
| |
| <section class="panel"> |
| <h2 class="panel__heading">Ask a Question</h2> |
| <form action="/ask" method="post" class="ask-form"> |
| <div class="ask-form__input-row"> |
| <textarea id="query" name="query" rows="3" placeholder="What is this roadmap mainly about?" class="ask-textarea">{{ query }}</textarea> |
| <button type="submit" class="btn btn--submit" title="Get Answer"> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22,2 15,22 11,13 2,9"/></svg> |
| </button> |
| </div> |
| <details class="filter-details"> |
| <summary class="filter-summary">Filters & Options</summary> |
| <div class="filter-body"> |
| <div class="filter-row"> |
| <div class="filter-field"> |
| <label for="selected_file">Document</label> |
| <select id="selected_file" name="selected_file"> |
| <option value="">All documents</option> |
| {% for file_name in available_files %} |
| <option value="{{ file_name }}" {% if selected_file == file_name %}selected{% endif %}>{{ file_name }}</option> |
| {% endfor %} |
| </select> |
| </div> |
| <div class="filter-field"> |
| <label for="selected_file_type">File Type</label> |
| <select id="selected_file_type" name="selected_file_type"> |
| <option value="">All types</option> |
| {% for file_type in available_file_types %} |
| <option value="{{ file_type }}" {% if selected_file_type == file_type %}selected{% endif %}>{{ file_type|upper }}</option> |
| {% endfor %} |
| </select> |
| </div> |
| <div class="filter-field"> |
| <label for="page_start">Start Page</label> |
| <input id="page_start" name="page_start" type="number" min="1" placeholder="Any" value="{{ page_start }}"> |
| </div> |
| <div class="filter-field"> |
| <label for="page_end">End Page</label> |
| <input id="page_end" name="page_end" type="number" min="1" placeholder="Any" value="{{ page_end }}"> |
| </div> |
| </div> |
| <label class="checkbox-row" for="debug_mode"> |
| <input id="debug_mode" name="debug_mode" type="checkbox" {% if debug_mode %}checked{% endif %}> |
| <span>Show retrieval debug info</span> |
| </label> |
| </div> |
| </details> |
| </form> |
| </section> |
|
|
| |
| {% if answer %} |
| <section class="panel answer-panel"> |
| <h2 class="panel__heading">Answer</h2> |
| <div class="answer-body">{{ answer }}</div> |
| <div class="answer-actions"> |
| <form action="/notes" method="post"> |
| <input type="hidden" name="query" value="{{ query }}"> |
| <input type="hidden" name="answer" value="{{ answer }}"> |
| <input type="hidden" name="citations_json" value="{{ citations|tojson|forceescape }}"> |
| <button type="submit" class="btn btn--outline btn--sm">Save Note</button> |
| </form> |
| <form action="/export/answer.md" method="post"> |
| <input type="hidden" name="query" value="{{ query }}"> |
| <input type="hidden" name="answer" value="{{ answer }}"> |
| <input type="hidden" name="citations_json" value="{{ citations|tojson|forceescape }}"> |
| <button type="submit" class="btn btn--outline btn--sm">Export Markdown</button> |
| </form> |
| <form action="/export/answer.json" method="post"> |
| <input type="hidden" name="query" value="{{ query }}"> |
| <input type="hidden" name="answer" value="{{ answer }}"> |
| <input type="hidden" name="citations_json" value="{{ citations|tojson|forceescape }}"> |
| <button type="submit" class="btn btn--outline btn--sm">Export JSON</button> |
| </form> |
| </div> |
| </section> |
| {% endif %} |
|
|
| |
| {% if citations %} |
| <section class="panel"> |
| <h2 class="panel__heading">Cited Sources</h2> |
| <div class="sources-grid"> |
| {% for item in citations %} |
| <article class="source-card"> |
| <div class="source-card__header"> |
| <span class="source-card__num">[{{ item.number }}]</span> |
| <span class="source-card__file">{{ item.source }}</span> |
| {% if item.page is not none %}<span class="source-card__page">p.{{ item.page }}</span>{% endif %} |
| </div> |
| <div class="score-row"> |
| <span class="score-pill">Retrieval: {{ item.retrieval_score }}</span> |
| <span class="score-pill">Rerank: {{ item.rerank_score }}</span> |
| </div> |
| <pre class="chunk-content">{{ item.content }}</pre> |
| </article> |
| {% endfor %} |
| </div> |
| </section> |
| {% endif %} |
|
|
| |
| {% if sources %} |
| <section class="panel"> |
| <h2 class="panel__heading">Retrieved Chunks</h2> |
| <div class="sources-grid"> |
| {% for item in sources %} |
| <article class="source-card"> |
| <div class="source-card__header"> |
| <span class="source-card__file">{{ item.source }}</span> |
| {% if item.page is not none %}<span class="source-card__page">p.{{ item.page }}</span>{% endif %} |
| </div> |
| <div class="score-row"> |
| <span class="score-pill">Retrieval: {{ item.retrieval_score }}</span> |
| <span class="score-pill">Rerank: {{ item.rerank_score }}</span> |
| </div> |
| <pre class="chunk-content">{{ item.content }}</pre> |
| </article> |
| {% endfor %} |
| </div> |
| </section> |
| {% endif %} |
|
|
| |
| <section class="panel"> |
| <h2 class="panel__heading">Chat History |
| {% if history_items %}<span class="count-badge">{{ history_items|length }}</span>{% endif %} |
| </h2> |
| <div class="action-row section-actions"> |
| <a class="btn btn--outline btn--sm" href="/export/history.md">Export Markdown</a> |
| <a class="btn btn--outline btn--sm" href="/export/history.json">Export JSON</a> |
| </div> |
| {% if history_items %} |
| <div class="history-list"> |
| {% for item in history_items %} |
| <article class="history-item"> |
| <div class="history-item__q"> |
| <span class="history-item__time">{{ item.created_at }}</span> |
| <p class="history-item__query">{{ item.query }}</p> |
| {% if item.selected_file or item.selected_file_type or item.page_start or item.page_end %} |
| <p class="history-item__filters"> |
| file={{ item.selected_file if item.selected_file else 'all' }}, |
| type={{ item.selected_file_type if item.selected_file_type else 'all' }}, |
| pages={{ item.page_start if item.page_start else 'start' }}-{{ item.page_end if item.page_end else 'end' }} |
| </p> |
| {% endif %} |
| </div> |
| <div class="history-item__a">{{ item.answer }}</div> |
| {% if item.citations %} |
| <div class="score-row"> |
| {% for citation in item.citations %} |
| <span class="score-pill">[{{ citation.number }}] {{ citation.source }}{% if citation.page is not none %}, p.{{ citation.page }}{% endif %}</span> |
| {% endfor %} |
| </div> |
| {% endif %} |
| <div class="action-row"> |
| <form action="/history/{{ item.history_id }}/notes" method="post"> |
| <button type="submit" class="btn btn--outline btn--sm">Save To Notes</button> |
| </form> |
| <form action="/history/{{ item.history_id }}/delete" method="post" onsubmit="return confirm('Delete this history item?');"> |
| <button type="submit" class="btn btn--danger btn--sm">Delete</button> |
| </form> |
| </div> |
| </article> |
| {% endfor %} |
| </div> |
| {% else %} |
| <p class="empty-state">No chat history yet. Ask a question to get started.</p> |
| {% endif %} |
| </section> |
|
|
| |
| <section class="panel"> |
| <h2 class="panel__heading">Saved Notes |
| {% if saved_notes %}<span class="count-badge">{{ saved_notes|length }}</span>{% endif %} |
| </h2> |
| <div class="action-row section-actions"> |
| <a class="btn btn--outline btn--sm" href="/export/notes.md">Export Markdown</a> |
| <a class="btn btn--outline btn--sm" href="/export/notes.json">Export JSON</a> |
| </div> |
| {% if saved_notes %} |
| <div class="history-list"> |
| {% for note in saved_notes %} |
| <article class="history-item"> |
| <div class="history-item__q"> |
| <span class="history-item__time">{{ note.saved_at }}</span> |
| <p class="history-item__query">{{ note.query }}</p> |
| </div> |
| <div class="history-item__a">{{ note.answer }}</div> |
| {% if note.citations %} |
| <div class="score-row"> |
| {% for item in note.citations %} |
| <span class="score-pill">[{{ item.number }}] {{ item.source }}{% if item.page is not none %}, p.{{ item.page }}{% endif %}</span> |
| {% endfor %} |
| </div> |
| {% endif %} |
| <div class="action-row"> |
| <form action="/notes/{{ note.note_id }}/delete" method="post" onsubmit="return confirm('Delete this saved note?');"> |
| <button type="submit" class="btn btn--danger btn--sm">Delete</button> |
| </form> |
| </div> |
| </article> |
| {% endfor %} |
| </div> |
| {% else %} |
| <p class="empty-state">No saved notes yet.</p> |
| {% endif %} |
| </section> |
|
|
| |
| {% if debug_data %} |
| <section class="panel"> |
| <h2 class="panel__heading">Debug Retrieval Flow</h2> |
|
|
| <div class="debug-summary"> |
| <div class="debug-pill">Queries: {{ debug_data.stage_counts.search_queries }}</div> |
| <div class="debug-pill">Dense Hits: {{ debug_data.dense_total_hits }}</div> |
| <div class="debug-pill">BM25 Hits: {{ debug_data.bm25_total_hits }}</div> |
| <div class="debug-pill">Fused: {{ debug_data.stage_counts.fused_candidates }}</div> |
| <div class="debug-pill">Top Hits: {{ debug_data.stage_counts.final_hits }}</div> |
| <div class="debug-pill">Expanded Context: {{ debug_data.stage_counts.expanded_context }}</div> |
| </div> |
|
|
| {% if debug_data.grounding %} |
| <div class="debug-block"> |
| <h3>Grounding Status</h3> |
| <ul class="debug-list"> |
| {% if debug_data.pipeline_mode %} |
| <li><strong>Pipeline Mode:</strong> {{ debug_data.pipeline_mode }}</li> |
| {% endif %} |
| {% if debug_data.next_action %} |
| <li><strong>Next Action:</strong> {{ debug_data.next_action }}</li> |
| {% endif %} |
| {% if debug_data.decision_reason %} |
| <li><strong>Decision Reason:</strong> {{ debug_data.decision_reason }}</li> |
| {% endif %} |
| {% if debug_data.retry_count %} |
| <li><strong>Retry Count:</strong> {{ debug_data.retry_count }} / 2</li> |
| {% endif %} |
| {% if debug_data.refined_query %} |
| <li><strong>Refined Query:</strong> <em>{{ debug_data.refined_query }}</em></li> |
| {% endif %} |
| {% if debug_data.web_research_performed %} |
| <li><strong>Web Research:</strong> <span class="badge badge--success">Performed</span></li> |
| {% endif %} |
| {% if debug_data.web_docs_ingested is not none %} |
| <li><strong>Web Docs Ingested:</strong> {{ debug_data.web_docs_ingested }}</li> |
| {% endif %} |
| <li><strong>Metadata Filter:</strong> |
| {% if debug_data.metadata_filter %} |
| file={{ debug_data.metadata_filter.file_name if debug_data.metadata_filter.file_name else 'all' }}, |
| type={{ debug_data.metadata_filter.file_type if debug_data.metadata_filter.file_type else 'all' }}, |
| pages= |
| {% if debug_data.metadata_filter.page_range %} |
| {{ debug_data.metadata_filter.page_range.start if debug_data.metadata_filter.page_range.start is not none else 'start' }}-{{ debug_data.metadata_filter.page_range.end if debug_data.metadata_filter.page_range.end is not none else 'end' }} |
| {% else %}all{% endif %} |
| {% else %}All documents{% endif %} |
| </li> |
| <li><strong>Result:</strong> {{ 'PASS' if debug_data.grounding.passed else 'FAIL' }}</li> |
| <li><strong>Failed At:</strong> {{ debug_data.grounding.stage }}</li> |
| <li><strong>Reason:</strong> {{ debug_data.grounding.reason }}</li> |
| <li><strong>Top Rerank Score:</strong> {{ debug_data.grounding.top_rerank_score if debug_data.grounding.top_rerank_score is not none else 'n/a' }}</li> |
| <li><strong>Retrieved Chunks:</strong> {{ debug_data.grounding.retrieved_count if debug_data.grounding.retrieved_count is not none else 'n/a' }}</li> |
| <li><strong>Expanded Chunks:</strong> {{ debug_data.grounding.expanded_count if debug_data.grounding.expanded_count is not none else 'n/a' }}</li> |
| </ul> |
| </div> |
| {% endif %} |
|
|
| <div class="debug-block"> |
| <h3>Search Queries</h3> |
| <ul class="debug-list"> |
| {% for query_item in debug_data.search_queries %} |
| <li>{{ query_item }}</li> |
| {% endfor %} |
| </ul> |
| </div> |
|
|
| <div class="debug-block"> |
| <h3>Dense Hits</h3> |
| {% for query_item, hits in debug_data.dense_hits.items() %} |
| <h4>{{ query_item }}</h4> |
| <ul class="debug-list"> |
| {% for hit in hits %} |
| <li><strong>{{ hit.chunk_id }}</strong> | {{ hit.source }} | page {{ hit.page if hit.page is not none else 'n/a' }} | {{ hit.score }}<br>{{ hit.preview }}</li> |
| {% endfor %} |
| </ul> |
| {% endfor %} |
| </div> |
|
|
| <div class="debug-block"> |
| <h3>BM25 Hits</h3> |
| {% for query_item, hits in debug_data.bm25_hits.items() %} |
| <h4>{{ query_item }}</h4> |
| <ul class="debug-list"> |
| {% for hit in hits %} |
| <li><strong>{{ hit.chunk_id }}</strong> | {{ hit.source }} | page {{ hit.page if hit.page is not none else 'n/a' }} | {{ hit.score }}<br>{{ hit.preview }}</li> |
| {% endfor %} |
| </ul> |
| {% endfor %} |
| </div> |
|
|
| <div class="debug-block"> |
| <h3>Fused Candidates</h3> |
| <ul class="debug-list"> |
| {% for hit in debug_data.fused_hits %} |
| <li><strong>{{ hit.chunk_id }}</strong> | {{ hit.source }} | page {{ hit.page if hit.page is not none else 'n/a' }} | {{ hit.score }}<br>{{ hit.preview }}</li> |
| {% endfor %} |
| </ul> |
| </div> |
|
|
| <div class="debug-block"> |
| <h3>Final Reranked Hits</h3> |
| <ul class="debug-list"> |
| {% for hit in debug_data.reranked_hits %} |
| <li><strong>{{ hit.chunk_id }}</strong> | {{ hit.source }} | page {{ hit.page if hit.page is not none else 'n/a' }} | {{ hit.score }}<br>{{ hit.preview }}</li> |
| {% endfor %} |
| </ul> |
| </div> |
|
|
| <div class="debug-block"> |
| <h3>Expanded Context Sent To LLM</h3> |
| <ul class="debug-list"> |
| {% for hit in debug_data.expanded_hits %} |
| <li><strong>{{ hit.chunk_id }}</strong> | {{ hit.source }} | page {{ hit.page if hit.page is not none else 'n/a' }} | {{ hit.score }}<br>{{ hit.preview }}</li> |
| {% endfor %} |
| </ul> |
| </div> |
| </section> |
| {% endif %} |
| </div> |
| </main> |
| </div> |
|
|
| <script> |
| |
| document.getElementById('document').addEventListener('change', function() { |
| var display = document.getElementById('filename-display'); |
| display.textContent = this.files.length ? this.files[0].name : ''; |
| }); |
| |
| |
| document.querySelectorAll('.toast').forEach(function(toast) { |
| setTimeout(function() { toast.classList.add('toast--hide'); }, 4000); |
| }); |
| </script> |
| </body> |
| </html> |
|
|