Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
336 changes: 105 additions & 231 deletions README.md

Large diffs are not rendered by default.

Binary file added docs/images/banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions docs/images/how-it-works.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/screenshot-dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/screenshot-install.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions docs/images/vs-rag.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 9 additions & 2 deletions localapp/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -375,14 +375,21 @@ <h3>Model & API key</h3>
document.getElementById("q").addEventListener("keydown",e=>{ if((e.metaKey||e.ctrlKey)&&e.key==="Enter") ask(); });
async function ask(){
if(!activeDoc) return; const q=document.getElementById("q").value.trim(); if(!q) return;
if(!getSettings().apiKey){ openSettings(); document.getElementById("setStatus").innerHTML='<span style="color:var(--warn)">Set an API key to ask questions.</span>'; return; }
const out=document.getElementById("result"), btn=document.getElementById("ask"); btn.disabled=true; pdfDoc=null;
out.innerHTML=`<div class="res card" style="margin-top:16px"><div class="body"><span class="spin"></span><span class="askhint">Navigating the document…</span></div></div>`;
const t0=performance.now();
try{
const r=await fetch(E("/v1/answer/treewalk"),{method:"POST",headers:{"Content-Type":"application/json",...llmHeaders()},body:JSON.stringify({document_id:activeDoc.id,query:q})});
const d=await r.json();
if(!r.ok){ out.innerHTML=`<div class="res card"><div class="body err">Error: ${esc(d.error||JSON.stringify(d))}</div></div>`; return; }
if(!r.ok){
const msg=d.error||JSON.stringify(d);
if(/no LLM credentials|X-LLM-Api-Key/i.test(msg)){
out.innerHTML=""; openSettings();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Clearing the result area on missing-credentials errors may degrade UX by removing prior answers.

This now clears out.innerHTML on missing-API-key, removing any prior answer the user may still be reading. Previously we never entered the ask flow, so the last result stayed visible. Consider keeping the existing result card and only updating the settings/status UI, so going to settings doesn’t wipe the current content.

document.getElementById("setStatus").innerHTML='<span style="color:var(--warn)">Set your API key to ask questions.</span>';
Comment on lines +386 to +388

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Relying on server error strings for credential detection is brittle and may cause extra round-trips.

The previous client-side apiKey check avoided a request and didn’t depend on specific error text. This new flow relies on matching no LLM credentials|X-LLM-Api-Key in the server message, which is fragile if backend wording changes and always sends a request even when we already know there’s no key. Consider restoring a lightweight client-side guard when the UI knows no key is set, or have the backend return a structured/typed error that you can check instead of regexing the message text.

return;
}
out.innerHTML=`<div class="res card"><div class="body err">Error: ${esc(msg)}</div></div>`; return;
}
renderResult(d,Math.round(performance.now()-t0));
}catch(e){ out.innerHTML=`<div class="res card"><div class="body err">${esc(String(e))}</div></div>`; }
finally{ btn.disabled=false; }
Expand Down
8 changes: 6 additions & 2 deletions pkg/ingest/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,11 @@ func (p *Pipeline) parse(ctx context.Context, parsers *parser.Registry, pl Paylo
// retried with short backoff rather than failing the whole document.
// Any non-ErrNotFound error returns immediately.
func getSourceWithRetry(ctx context.Context, s storage.Storage, key string) (io.ReadCloser, storage.Metadata, error) {
const attempts = 6
// Up to ~16s of incremental backoff. A large source (multi-MB) written
// under heavy concurrent ingestion on a busy/low-disk filesystem can take
// several seconds to become visible to this worker; a too-short window
// turns that transient into a hard "object not found" failure.
const attempts = 16
var lastErr error
for i := 0; i < attempts; i++ {
rc, meta, err := s.Get(ctx, key)
Expand All @@ -570,7 +574,7 @@ func getSourceWithRetry(ctx context.Context, s storage.Storage, key string) (io.
select {
case <-ctx.Done():
return nil, storage.Metadata{}, ctx.Err()
case <-time.After(time.Duration(i+1) * 150 * time.Millisecond):
case <-time.After(time.Duration(i+1) * 125 * time.Millisecond):
}
}
return nil, storage.Metadata{}, lastErr
Expand Down
Loading