Code sync, security fixes
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
{{define "content"}}
|
||||
<h1>Add text</h1>
|
||||
<p class="muted">Paste a note, an email, a transcript, Markdown or a JSON document. The original is stored unchanged.</p>
|
||||
|
||||
<form method="post" action="/add" class="stack" enctype="application/x-www-form-urlencoded">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
|
||||
<div class="field">
|
||||
<label for="title">Title</label>
|
||||
<input id="title" name="title" type="text" maxlength="300" value="{{.Form.Title}}"
|
||||
aria-describedby="title-hint">
|
||||
<p id="title-hint" class="hint">Optional. A title is generated if you leave this blank.</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="content">Content</label>
|
||||
<textarea id="content" name="content" rows="18" required aria-describedby="content-hint">{{.Form.Content}}</textarea>
|
||||
<p id="content-hint" class="hint">Relata detects the format from the content itself.</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="field">
|
||||
<label for="source_type">Source type</label>
|
||||
<select id="source_type" name="source_type">
|
||||
{{range sourceTypes}}<option value="{{.}}" {{if eq $.Form.SourceType (printf "%s" .)}}selected{{end}}>{{.}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="created_at">Date of the information</label>
|
||||
<input id="created_at" name="created_at" type="date" value="{{.Form.CreatedAt}}"
|
||||
aria-describedby="created-hint">
|
||||
<p id="created-hint" class="hint">When the information itself dates from, if not today.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset class="field">
|
||||
<legend>Metadata (optional)</legend>
|
||||
<div class="row">
|
||||
<div class="field"><label for="mk1">Key</label><input id="mk1" name="metadata_key" type="text" maxlength="64"></div>
|
||||
<div class="field"><label for="mv1">Value</label><input id="mv1" name="metadata_value" type="text" maxlength="2000"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="field"><label for="mk2">Key</label><input id="mk2" name="metadata_key" type="text" maxlength="64"></div>
|
||||
<div class="field"><label for="mv2">Value</label><input id="mv2" name="metadata_value" type="text" maxlength="2000"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="field field-check">
|
||||
<input id="allow_duplicate" name="allow_duplicate" type="checkbox" value="true">
|
||||
<label for="allow_duplicate">Store even if identical content already exists</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Store and process</button>
|
||||
</form>
|
||||
{{end}}
|
||||
@@ -0,0 +1,87 @@
|
||||
{{define "content"}}
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<section aria-labelledby="stats-heading">
|
||||
<h2 id="stats-heading" class="visually-hidden">Workspace statistics</h2>
|
||||
<ul class="stat-grid">
|
||||
<li class="stat"><span class="stat-value">{{.Stats.Items}}</span><span class="stat-label">Items</span></li>
|
||||
<li class="stat"><span class="stat-value">{{.Stats.Chunks}}</span><span class="stat-label">Chunks</span></li>
|
||||
<li class="stat"><span class="stat-value">{{.Stats.Entities}}</span><span class="stat-label">Entities</span></li>
|
||||
<li class="stat"><span class="stat-value">{{.Stats.Relationships}}</span><span class="stat-label">Relationships</span></li>
|
||||
<li class="stat"><span class="stat-value">{{.Stats.PendingJobs}}</span><span class="stat-label">Pending jobs</span></li>
|
||||
<li class="stat {{if gt .Stats.FailedJobs 0}}stat-warn{{end}}">
|
||||
<span class="stat-value">{{.Stats.FailedJobs}}</span><span class="stat-label">Failed jobs</span>
|
||||
</li>
|
||||
<li class="stat {{if gt .Stats.AwaitingReview 0}}stat-attention{{end}}">
|
||||
<span class="stat-value">{{.Stats.AwaitingReview}}</span>
|
||||
<span class="stat-label">Awaiting relationship review</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="two-col">
|
||||
<section aria-labelledby="recent-heading">
|
||||
<h2 id="recent-heading">Recent items</h2>
|
||||
{{if .RecentItems}}
|
||||
<ul class="item-list">
|
||||
{{range .RecentItems}}
|
||||
<li class="item-row">
|
||||
<a class="item-title" href="/items/{{.ID}}">{{.Title}}</a>
|
||||
<p class="item-meta">
|
||||
<span class="pill pill-{{.Status}}">{{statusLabel .Status}}</span>
|
||||
<span>{{.SourceType}}</span>
|
||||
<span>{{ago .IngestedAt}}</span>
|
||||
{{if .ChunkCount}}<span>{{.ChunkCount}} sections</span>{{end}}
|
||||
</p>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<p><a href="/items">All items</a></p>
|
||||
{{else}}
|
||||
<div class="empty">
|
||||
<h3>Nothing stored yet</h3>
|
||||
<p>Relata has no items in this workspace.</p>
|
||||
<p><a class="btn btn-primary" href="/add">Add a note</a> <a class="btn" href="/upload">Upload a document</a></p>
|
||||
<p class="muted">You can also load the demonstration dataset with <code>relata seed</code>.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="review-heading">
|
||||
<h2 id="review-heading">Awaiting review</h2>
|
||||
{{if .PendingRelationships}}
|
||||
<p class="muted">
|
||||
These relationships were proposed by analysis. They are <strong>suggestions</strong>,
|
||||
not established facts, until someone confirms them.
|
||||
</p>
|
||||
<ul class="item-list">
|
||||
{{range .PendingRelationships}}
|
||||
<li class="item-row">
|
||||
<a href="/review#rel-{{.Rel.ID}}">{{.SourceTitle}} → {{.TargetTitle}}</a>
|
||||
<p class="item-meta">
|
||||
<span class="pill">{{relTypeLabel .Rel.Type}}</span>
|
||||
<span title="Heuristic strength, not a probability">strength {{score2 .Rel.Score}} ({{strengthLabel .Rel.Score}})</span>
|
||||
</p>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<p><a href="/review">Review relationships</a></p>
|
||||
{{else}}
|
||||
<div class="empty">
|
||||
<h3>Nothing to review</h3>
|
||||
<p>No proposed relationships are waiting for a decision.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{{if .ProviderWarnings}}
|
||||
<section aria-labelledby="providers-heading" class="callout callout-warn">
|
||||
<h2 id="providers-heading">Model availability</h2>
|
||||
<ul>
|
||||
{{range .ProviderWarnings}}<li>{{.}}</li>{{end}}
|
||||
</ul>
|
||||
<p class="muted">Ingestion, keyword search and browsing continue to work without a language model.</p>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,53 @@
|
||||
{{define "content"}}
|
||||
<h1>Entities</h1>
|
||||
<p class="muted">
|
||||
People, organisations, places, products, events, dates and concepts extracted from stored items.
|
||||
Entities are matched only on an exact normalised name — similar names are never merged automatically.
|
||||
</p>
|
||||
|
||||
<form class="filter-bar" method="get" action="/entities">
|
||||
<div class="field">
|
||||
<label for="q">Name contains</label>
|
||||
<input id="q" name="q" type="search" value="{{.Filters.Query}}" autocomplete="off">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="type">Type</label>
|
||||
<select id="type" name="type">
|
||||
<option value="">Any</option>
|
||||
{{range entityTypes}}<option value="{{.}}" {{if eq $.Filters.Type (printf "%s" .)}}selected{{end}}>{{.}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn">Filter</button>
|
||||
<a class="btn btn-quiet" href="/entities">Clear</a>
|
||||
</form>
|
||||
|
||||
{{if .Entities}}
|
||||
<table class="table">
|
||||
<caption class="visually-hidden">Extracted entities</caption>
|
||||
<thead>
|
||||
<tr><th scope="col">Name</th><th scope="col">Type</th><th scope="col">Mentions</th>
|
||||
<th scope="col">Items</th><th scope="col">Method</th><th scope="col">Evidence</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Entities}}
|
||||
<tr>
|
||||
<td><a href="/entities/{{.ID}}">{{.CanonicalName}}</a></td>
|
||||
<td>{{.Type}}</td>
|
||||
<td class="num">{{.Mentions}}</td>
|
||||
<td class="num">{{len .ItemIDs}}</td>
|
||||
<td>{{.Method}}</td>
|
||||
<td class="num" title="How well attested this entity is. Not a probability.">{{score2 .EvidenceScore}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<nav class="pagination" aria-label="Entity pages">
|
||||
{{if .Pagination.PrevURL}}<a class="btn" href="{{.Pagination.PrevURL}}">Previous</a>{{end}}
|
||||
<span class="muted">Showing {{.Pagination.From}}–{{.Pagination.To}} of {{.Pagination.Total}}</span>
|
||||
{{if .Pagination.NextURL}}<a class="btn" href="{{.Pagination.NextURL}}">Next</a>{{end}}
|
||||
</nav>
|
||||
{{else}}
|
||||
<div class="empty"><h2>No entities</h2><p>No entities match this filter.</p></div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,93 @@
|
||||
{{define "content"}}
|
||||
<h1>{{.Entity.CanonicalName}}</h1>
|
||||
<p class="item-meta">
|
||||
<span class="pill">{{.Entity.Type}}</span>
|
||||
<span>{{.Entity.Mentions}} mention{{if ne .Entity.Mentions 1}}s{{end}}</span>
|
||||
<span>{{len .Entity.ItemIDs}} item{{if ne (len .Entity.ItemIDs) 1}}s{{end}}</span>
|
||||
<span>extracted by {{.Entity.Method}}</span>
|
||||
{{if .Entity.ModelVersion}}<span class="muted">{{.Entity.ModelVersion}}</span>{{end}}
|
||||
</p>
|
||||
|
||||
<section aria-labelledby="items-heading">
|
||||
<h2 id="items-heading">Items referring to this entity</h2>
|
||||
{{if .Items}}
|
||||
<ul class="item-list">
|
||||
{{range .Items}}
|
||||
<li class="item-row">
|
||||
<a class="item-title" href="/items/{{.ID}}">{{.Title}}</a>
|
||||
<p class="item-meta"><span>{{.SourceType}}</span><span>{{date .CreatedAt}}</span></p>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<p><a href="/search?entity_id={{.Entity.ID}}">Search items associated with {{.Entity.CanonicalName}}</a></p>
|
||||
{{else}}
|
||||
<p class="muted">No items currently reference this entity.</p>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="aliases-heading">
|
||||
<h2 id="aliases-heading">Candidate aliases</h2>
|
||||
<p class="muted">
|
||||
Names that look similar to this one. Relata has <strong>not</strong> merged them:
|
||||
similar names often belong to genuinely different things.
|
||||
</p>
|
||||
{{if .Aliases}}
|
||||
<table class="table">
|
||||
<caption class="visually-hidden">Candidate aliases</caption>
|
||||
<thead><tr><th scope="col">Alias</th><th scope="col">Similarity</th><th scope="col">Status</th>
|
||||
<th scope="col">Reason</th><th scope="col">Decision</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Aliases}}
|
||||
<tr>
|
||||
<td>{{.Alias}}</td>
|
||||
<td class="num">{{score2 .Similarity}}</td>
|
||||
<td><span class="pill pill-review-{{.Status}}">{{.Status}}</span></td>
|
||||
<td class="muted">{{.Reason}}</td>
|
||||
<td>
|
||||
<div class="rel-actions">
|
||||
<form method="post" action="/aliases/{{.ID}}/confirm" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-small btn-ok"
|
||||
{{if eq .Status "confirmed"}}disabled{{end}}>Same thing</button>
|
||||
</form>
|
||||
<form method="post" action="/aliases/{{.ID}}/reject" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-small btn-danger"
|
||||
{{if eq .Status "rejected"}}disabled{{end}}>Different</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="hint">
|
||||
Confirming records that these names refer to the same thing. The two entities remain
|
||||
<strong>separate records</strong>; the confirmation becomes evidence when relationships
|
||||
are discovered, weighted below an exact name match.
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="muted">No alias candidates have been proposed.</p>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="refs-heading">
|
||||
<h2 id="refs-heading">Where it appears</h2>
|
||||
{{if .Entity.SourceRefs}}
|
||||
<ul class="claim-list">
|
||||
{{range .Entity.SourceRefs}}
|
||||
<li class="claim">
|
||||
<blockquote class="claim-quote">{{.Quote}}</blockquote>
|
||||
<p class="claim-meta muted">
|
||||
<a href="/items/{{.ItemID}}">source item</a>
|
||||
<span>bytes {{.Start}}–{{.End}}</span>
|
||||
{{if .Page}}<span>page {{.Page}}</span>{{end}}
|
||||
</p>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p class="muted">No stored source references.</p>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,8 @@
|
||||
{{define "content"}}
|
||||
<div class="error-page">
|
||||
<h1>{{if .ErrorTitle}}{{.ErrorTitle}}{{else}}Something went wrong{{end}}</h1>
|
||||
<p>{{if .ErrorMessage}}{{.ErrorMessage}}{{else}}The request could not be completed.{{end}}</p>
|
||||
{{if .RequestID}}<p class="muted">Reference: <code>{{.RequestID}}</code></p>{{end}}
|
||||
<p><a class="btn" href="/">Back to the dashboard</a></p>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,236 @@
|
||||
{{define "content"}}
|
||||
<article class="item-detail">
|
||||
<header class="detail-head">
|
||||
<h1>{{.Item.Title}}</h1>
|
||||
<p class="item-meta">
|
||||
<span class="pill pill-{{.Item.Status}}">{{statusLabel .Item.Status}}</span>
|
||||
<span>{{.Item.SourceType}}</span>
|
||||
<span>{{.Item.MIMEType}}</span>
|
||||
<span>{{bytes .Item.SizeBytes}}</span>
|
||||
<span>Created {{date .Item.CreatedAt}}</span>
|
||||
<span>Ingested {{datetime .Item.IngestedAt}}</span>
|
||||
</p>
|
||||
|
||||
<div class="detail-actions">
|
||||
<a class="btn" href="/items/{{.Item.ID}}/neighbourhood">Neighbourhood</a>
|
||||
<form method="post" action="/items/{{.Item.ID}}/reprocess" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn">Reprocess</button>
|
||||
</form>
|
||||
<form method="post" action="/items/{{.Item.ID}}/delete" class="inline-form"
|
||||
data-confirm="Delete this item and its derived records? The original content will be removed.">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{if .Item.Error}}
|
||||
<p class="flash flash-error" role="alert">{{.Item.Error}}</p>
|
||||
{{end}}
|
||||
|
||||
{{if .Item.Warnings}}
|
||||
<section class="callout callout-warn" aria-labelledby="warn-heading">
|
||||
<h2 id="warn-heading">Processing warnings</h2>
|
||||
<ul>
|
||||
{{range .Item.Warnings}}
|
||||
<li><strong>{{.Message}}</strong>{{if .Detail}} <span class="muted">({{.Detail}})</span>{{end}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<p class="muted">The original content is preserved unchanged and can be reprocessed.</p>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
{{if .Item.DuplicateOf}}
|
||||
<p class="flash flash-warn">
|
||||
This item has identical content to
|
||||
<a href="/items/{{.Item.DuplicateOf}}">another stored item</a>. It was kept deliberately.
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
<nav class="tabs" aria-label="Item sections">
|
||||
<a href="#content">Content</a>
|
||||
<a href="#metadata">Metadata</a>
|
||||
<a href="#chunks">Sections ({{len .Chunks}})</a>
|
||||
<a href="#entities">Entities ({{len .Entities}})</a>
|
||||
<a href="#claims">Claims ({{len .Claims}})</a>
|
||||
<a href="#relationships">Relationships ({{.RelationshipCount}})</a>
|
||||
<a href="#history">Processing history</a>
|
||||
</nav>
|
||||
|
||||
<section id="content" aria-labelledby="content-heading">
|
||||
<h2 id="content-heading">Original content</h2>
|
||||
{{if .Item.OriginalText}}
|
||||
<pre class="content-block">{{.Item.OriginalText}}</pre>
|
||||
{{else if .Item.OriginalFilename}}
|
||||
<p>
|
||||
Original file: <strong>{{.Item.OriginalFilename}}</strong> ({{bytes .Item.SizeBytes}}).
|
||||
The uploaded file is stored unchanged and is never executed or rendered as markup.
|
||||
</p>
|
||||
<p><a class="btn" href="/items/{{.Item.ID}}/download">Download original</a></p>
|
||||
{{else}}
|
||||
<p class="muted">No original content is retained for this item.</p>
|
||||
{{end}}
|
||||
|
||||
{{if .Item.ExtractedText}}
|
||||
<h3>Extracted text</h3>
|
||||
<p class="muted">
|
||||
Produced by {{.Item.ParserVersion}}. This is what was chunked, embedded and searched.
|
||||
</p>
|
||||
<pre class="content-block">{{truncate 20000 .Item.ExtractedText}}</pre>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section id="metadata" aria-labelledby="metadata-heading">
|
||||
<h2 id="metadata-heading">Metadata</h2>
|
||||
<dl class="meta-list">
|
||||
<dt>Item ID</dt><dd><code>{{.Item.ID}}</code></dd>
|
||||
<dt>Content hash</dt><dd><code>{{.Item.ContentHash}}</code></dd>
|
||||
<dt>Parser</dt><dd>{{if .Item.ParserVersion}}{{.Item.ParserVersion}}{{else}}—{{end}}</dd>
|
||||
<dt>Embedding model</dt><dd>{{if .Item.EmbeddingModel}}{{.Item.EmbeddingModel}}{{else}}—{{end}}</dd>
|
||||
<dt>Analysis model</dt><dd>{{if .Item.AnalysisModel}}{{.Item.AnalysisModel}}{{else}}not used{{end}}</dd>
|
||||
<dt>Scoring version</dt><dd>{{if .Item.ScoreVersion}}{{.Item.ScoreVersion}}{{else}}—{{end}}</dd>
|
||||
{{range $k, $v := .Item.Metadata}}
|
||||
<dt>{{$k}}</dt><dd>{{$v}}</dd>
|
||||
{{end}}
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section id="chunks" aria-labelledby="chunks-heading">
|
||||
<h2 id="chunks-heading">Sections</h2>
|
||||
{{if .Chunks}}
|
||||
<ol class="chunk-list">
|
||||
{{range .Chunks}}
|
||||
<li class="chunk">
|
||||
<p class="chunk-head">
|
||||
Section {{add .Sequence 1}}
|
||||
{{if .Page}}<span class="muted">page {{.Page}}</span>{{end}}
|
||||
{{if .Section}}<span class="muted">{{.Section}}</span>{{end}}
|
||||
<span class="muted">bytes {{.StartOffset}}–{{.EndOffset}}</span>
|
||||
{{if .Embedded}}<span class="pill pill-ok">embedded</span>{{else}}<span class="pill">not embedded</span>{{end}}
|
||||
</p>
|
||||
<blockquote>{{truncate 800 .Text}}</blockquote>
|
||||
</li>
|
||||
{{end}}
|
||||
</ol>
|
||||
{{else}}
|
||||
<div class="empty"><p>This item has no sections. It may still be processing, or no text could be extracted.</p></div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section id="entities" aria-labelledby="entities-heading">
|
||||
<h2 id="entities-heading">Entities</h2>
|
||||
{{if .Entities}}
|
||||
<ul class="entity-list">
|
||||
{{range .Entities}}
|
||||
<li>
|
||||
<a href="/entities/{{.ID}}">{{.CanonicalName}}</a>
|
||||
<span class="pill">{{.Type}}</span>
|
||||
<span class="muted">{{.Mentions}} mention{{if ne .Mentions 1}}s{{end}}</span>
|
||||
<span class="muted">{{.Method}}</span>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="empty"><p>No entities were extracted from this item.</p></div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section id="claims" aria-labelledby="claims-heading">
|
||||
<h2 id="claims-heading">Claims</h2>
|
||||
<p class="muted">
|
||||
A claim records that this source <em>says</em> something. It is not treated as fact.
|
||||
Each one keeps the exact wording it came from.
|
||||
</p>
|
||||
{{if .Claims}}
|
||||
<ul class="claim-list">
|
||||
{{range .Claims}}
|
||||
<li class="claim">
|
||||
<p class="claim-statement">{{.Statement}}</p>
|
||||
<blockquote class="claim-quote">{{.SourceRef.Quote}}</blockquote>
|
||||
<p class="claim-meta muted">
|
||||
<span>{{.Nature}}</span>
|
||||
<span>{{.Method}}</span>
|
||||
{{if .Attribution}}<span>attributed to {{.Attribution}}</span>{{end}}
|
||||
{{if .Subject}}<span>subject: {{.Subject}}</span>{{end}}
|
||||
<span>bytes {{.SourceRef.Start}}–{{.SourceRef.End}}</span>
|
||||
</p>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="empty"><p>No claims were extracted from this item.</p></div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section id="relationships" aria-labelledby="rel-heading">
|
||||
<h2 id="rel-heading">Relationships</h2>
|
||||
<p class="muted">
|
||||
Relationship strength is a heuristic score built from the signals listed under each link.
|
||||
It is not a calibrated probability.
|
||||
</p>
|
||||
|
||||
<h3>Outgoing</h3>
|
||||
{{if .Outgoing}}
|
||||
{{range .Outgoing}}{{template "partial_relationship.html" (dict "R" . "CSRFToken" $.CSRFToken "Direction" "outgoing")}}{{end}}
|
||||
{{else}}<p class="muted">None.</p>{{end}}
|
||||
|
||||
<h3>Incoming</h3>
|
||||
{{if .Incoming}}
|
||||
{{range .Incoming}}{{template "partial_relationship.html" (dict "R" . "CSRFToken" $.CSRFToken "Direction" "incoming")}}{{end}}
|
||||
{{else}}<p class="muted">None.</p>{{end}}
|
||||
|
||||
<h3>Add a relationship</h3>
|
||||
<form method="post" action="/relationships/create" class="inline-grid">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<input type="hidden" name="source_id" value="{{.Item.ID}}">
|
||||
<div class="field">
|
||||
<label for="target_id">Related item</label>
|
||||
<select id="target_id" name="target_id" required>
|
||||
<option value="">Choose an item…</option>
|
||||
{{range .OtherItems}}<option value="{{.ID}}">{{truncate 80 .Title}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="rel_type">Relationship</label>
|
||||
<select id="rel_type" name="type" required>
|
||||
{{range relTypes}}<option value="{{.}}">{{relTypeLabel .}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="rel_note">Note (optional)</label>
|
||||
<input id="rel_note" name="note" type="text" maxlength="500">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Add relationship</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="history" aria-labelledby="history-heading">
|
||||
<h2 id="history-heading">Processing history</h2>
|
||||
{{if .Runs}}
|
||||
<table class="table">
|
||||
<caption class="visually-hidden">Processing runs for this item</caption>
|
||||
<thead>
|
||||
<tr><th scope="col">Started</th><th scope="col">Reason</th><th scope="col">Outcome</th>
|
||||
<th scope="col">Parser</th><th scope="col">Embedding model</th><th scope="col">Scoring</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Runs}}
|
||||
<tr>
|
||||
<td>{{datetime .StartedAt}}</td>
|
||||
<td>{{.Reason}}</td>
|
||||
<td>{{.Outcome}}{{if .Error}} <span class="muted">{{truncate 120 .Error}}</span>{{end}}</td>
|
||||
<td>{{.ParserVersion}}</td>
|
||||
<td>{{.EmbeddingModel}}</td>
|
||||
<td>{{.ScoreVersion}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="muted">Previous runs are retained for audit; reprocessing never erases them.</p>
|
||||
{{else}}
|
||||
<p class="muted">No processing runs recorded.</p>
|
||||
{{end}}
|
||||
</section>
|
||||
</article>
|
||||
{{end}}
|
||||
@@ -0,0 +1,67 @@
|
||||
{{define "content"}}
|
||||
<h1>Items</h1>
|
||||
|
||||
<form class="filter-bar" method="get" action="/items">
|
||||
<div class="field">
|
||||
<label for="title">Title contains</label>
|
||||
<input id="title" name="title" type="search" value="{{.Filters.Title}}" autocomplete="off">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="source_type">Source type</label>
|
||||
<select id="source_type" name="source_type">
|
||||
<option value="">Any</option>
|
||||
{{range sourceTypes}}<option value="{{.}}" {{if eq $.Filters.SourceType (printf "%s" .)}}selected{{end}}>{{.}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" name="status">
|
||||
<option value="">Any</option>
|
||||
<option value="ready" {{if eq .Filters.Status "ready"}}selected{{end}}>Ready</option>
|
||||
<option value="partial" {{if eq .Filters.Status "partial"}}selected{{end}}>Ready with warnings</option>
|
||||
<option value="failed" {{if eq .Filters.Status "failed"}}selected{{end}}>Failed</option>
|
||||
<option value="pending" {{if eq .Filters.Status "pending"}}selected{{end}}>Waiting</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn">Filter</button>
|
||||
<a class="btn btn-quiet" href="/items">Clear</a>
|
||||
</form>
|
||||
|
||||
{{if .Items}}
|
||||
<table class="table">
|
||||
<caption class="visually-hidden">Stored items</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Title</th><th scope="col">Type</th><th scope="col">Status</th>
|
||||
<th scope="col">Sections</th><th scope="col">Entities</th>
|
||||
<th scope="col">Created</th><th scope="col">Ingested</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Items}}
|
||||
<tr>
|
||||
<td><a href="/items/{{.ID}}">{{truncate 90 .Title}}</a></td>
|
||||
<td>{{.SourceType}}</td>
|
||||
<td><span class="pill pill-{{.Status}}">{{statusLabel .Status}}</span></td>
|
||||
<td class="num">{{.ChunkCount}}</td>
|
||||
<td class="num">{{.EntityCount}}</td>
|
||||
<td>{{date .CreatedAt}}</td>
|
||||
<td>{{ago .IngestedAt}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<nav class="pagination" aria-label="Item pages">
|
||||
{{if .Pagination.PrevURL}}<a class="btn" href="{{.Pagination.PrevURL}}">Previous</a>{{end}}
|
||||
<span class="muted">Showing {{.Pagination.From}}–{{.Pagination.To}} of {{.Pagination.Total}}</span>
|
||||
{{if .Pagination.NextURL}}<a class="btn" href="{{.Pagination.NextURL}}">Next</a>{{end}}
|
||||
</nav>
|
||||
{{else}}
|
||||
<div class="empty">
|
||||
<h2>No items match</h2>
|
||||
<p>Nothing here yet, or the filters exclude everything.</p>
|
||||
<p><a class="btn btn-primary" href="/add">Add a note</a> <a class="btn" href="/upload">Upload a document</a></p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,11 @@
|
||||
{{define "content"}}
|
||||
<h1>Processing queue</h1>
|
||||
<p class="muted">
|
||||
Every stage of processing is a background job. Jobs are leased, so a worker that stops
|
||||
unexpectedly returns its work to the queue rather than holding it.
|
||||
</p>
|
||||
|
||||
<div id="jobs-table" hx-get="/jobs/table" hx-trigger="every 5s" hx-swap="innerHTML">
|
||||
{{template "partial_jobs_table.html" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,56 @@
|
||||
{{define "layout.html"}}<!DOCTYPE html>
|
||||
<html lang="en-GB">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{if .Title}}{{.Title}} — Relata{{else}}Relata{{end}}</title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main">Skip to main content</a>
|
||||
|
||||
{{if .Principal}}
|
||||
<header class="masthead">
|
||||
<div class="masthead-inner">
|
||||
<a class="brand" href="/">Relata</a>
|
||||
<nav aria-label="Main">
|
||||
<ul class="nav-list">
|
||||
<li><a href="/" {{if eq .Nav "dashboard"}}aria-current="page"{{end}}>Dashboard</a></li>
|
||||
<li><a href="/search" {{if eq .Nav "search"}}aria-current="page"{{end}}>Search</a></li>
|
||||
<li><a href="/items" {{if eq .Nav "items"}}aria-current="page"{{end}}>Items</a></li>
|
||||
<li><a href="/add" {{if eq .Nav "add"}}aria-current="page"{{end}}>Add text</a></li>
|
||||
<li><a href="/upload" {{if eq .Nav "upload"}}aria-current="page"{{end}}>Upload</a></li>
|
||||
<li><a href="/review" {{if eq .Nav "review"}}aria-current="page"{{end}}>Review{{if .PendingReview}} <span class="badge" aria-label="{{.PendingReview}} awaiting review">{{.PendingReview}}</span>{{end}}</a></li>
|
||||
<li><a href="/entities" {{if eq .Nav "entities"}}aria-current="page"{{end}}>Entities</a></li>
|
||||
<li><a href="/jobs" {{if eq .Nav "jobs"}}aria-current="page"{{end}}>Queue</a></li>
|
||||
<li><a href="/settings" {{if eq .Nav "settings"}}aria-current="page"{{end}}>Settings</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<form class="signout" method="post" action="/logout">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<span class="whoami">{{.Principal.Username}}</span>
|
||||
<button type="submit" class="btn btn-quiet">Sign out</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
{{end}}
|
||||
|
||||
<main id="main" class="page">
|
||||
{{if .Flash}}<div class="flash flash-{{.Flash.Kind}}" role="status">{{.Flash.Message}}</div>{{end}}
|
||||
{{if .Error}}<div class="flash flash-error" role="alert">{{.Error}}</div>{{end}}
|
||||
{{template "content" .}}
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>
|
||||
Relata stores what you give it and proposes relationships between items.
|
||||
Relationship strengths are <strong>heuristic scores</strong> computed from listed signals —
|
||||
they are not calibrated probabilities, and an inferred relationship is a suggestion, not a fact.
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<script src="/static/vendor/htmx.min.js" defer></script>
|
||||
<script src="/static/app.js" defer></script>
|
||||
</body>
|
||||
</html>{{end}}
|
||||
@@ -0,0 +1,69 @@
|
||||
{{define "content"}}
|
||||
<div class="auth-panel">
|
||||
<h1>Sign in to Relata</h1>
|
||||
|
||||
{{if .OAuthEnabled}}
|
||||
<p class="muted">Sign in with your organisation account to reach your workspace.</p>
|
||||
<p>
|
||||
<a class="btn btn-primary btn-large btn-block" href="/auth/oauth/start{{if .Next}}?next={{.Next}}{{end}}">
|
||||
Sign in with {{.ProviderName}}
|
||||
</a>
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
{{if and .OAuthEnabled .LocalEnabled}}
|
||||
<p class="or-divider"><span>or</span></p>
|
||||
{{end}}
|
||||
|
||||
{{if .LocalEnabled}}
|
||||
<form method="post" action="/login" class="stack" novalidate>
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
|
||||
<div class="field">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" type="text" autocomplete="username"
|
||||
required {{if not .OAuthEnabled}}autofocus{{end}} autocapitalize="none" spellcheck="false"
|
||||
aria-describedby="username-hint">
|
||||
<p id="username-hint" class="hint">The username created with <code>relata createadmin</code>.</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Sign in</button>
|
||||
</form>
|
||||
|
||||
{{if .OAuthEnabled}}
|
||||
<p class="hint">
|
||||
Local accounts exist to recover from a problem with {{.ProviderName}}. If you have an
|
||||
organisation account, use the button above — a local account has its own separate workspace.
|
||||
</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if not (or .OAuthEnabled .LocalEnabled)}}
|
||||
<div class="callout">
|
||||
<h2>No way to sign in is configured</h2>
|
||||
<p>
|
||||
Neither federated nor local sign-in is enabled. Set <code>RELATA_OAUTH_ENABLED=true</code>
|
||||
or <code>RELATA_LOCAL_LOGIN_ENABLED=true</code> and restart.
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if and .FirstRun .LocalEnabled}}
|
||||
<div class="callout">
|
||||
<h2>No accounts exist yet</h2>
|
||||
<p>Create the first administrator from the command line:</p>
|
||||
<pre><code>relata createadmin --username admin</code></pre>
|
||||
<p class="muted">You will be prompted for a password. It is never shown or logged.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<p class="muted auth-footer">
|
||||
New here? <a href="/">Read what Relata does</a>.
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,59 @@
|
||||
{{define "content"}}
|
||||
<h1>Neighbourhood of “{{truncate 80 .Item.Title}}”</h1>
|
||||
<p class="muted">
|
||||
The items directly connected to this one. Column position shows direction, not importance.
|
||||
Strength is a heuristic score, not a probability.
|
||||
</p>
|
||||
<p><a href="/items/{{.Item.ID}}">← Back to the item</a></p>
|
||||
|
||||
<div class="neighbourhood">
|
||||
<section class="neighbour-col" aria-labelledby="in-heading">
|
||||
<h2 id="in-heading">Points at this item ({{len .Incoming}})</h2>
|
||||
{{if .Incoming}}
|
||||
<ul class="neighbour-list">
|
||||
{{range .Incoming}}
|
||||
<li>
|
||||
<a href="/items/{{.OtherID}}">{{if .OtherTitle}}{{truncate 70 .OtherTitle}}{{else}}(item){{end}}</a>
|
||||
<p class="neighbour-meta">
|
||||
<span class="pill">{{relTypeLabel .Rel.Type}}</span>
|
||||
<span>strength {{score2 .Rel.Score}} ({{strengthLabel .Rel.Score}})</span>
|
||||
<span class="pill pill-prov-{{.Rel.Provenance}}">{{provenanceLabel .Rel.Provenance}}</span>
|
||||
</p>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}<p class="muted">Nothing points at this item.</p>{{end}}
|
||||
</section>
|
||||
|
||||
<section class="neighbour-col neighbour-centre" aria-labelledby="centre-heading">
|
||||
<h2 id="centre-heading">This item</h2>
|
||||
<div class="neighbour-focus">
|
||||
<p class="item-title">{{.Item.Title}}</p>
|
||||
<p class="item-meta">
|
||||
<span class="pill pill-{{.Item.Status}}">{{statusLabel .Item.Status}}</span>
|
||||
<span>{{.Item.SourceType}}</span>
|
||||
<span>{{date .Item.CreatedAt}}</span>
|
||||
</p>
|
||||
<p class="excerpt">{{truncate 300 .Item.ExtractedText}}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="neighbour-col" aria-labelledby="out-heading">
|
||||
<h2 id="out-heading">This item points at ({{len .Outgoing}})</h2>
|
||||
{{if .Outgoing}}
|
||||
<ul class="neighbour-list">
|
||||
{{range .Outgoing}}
|
||||
<li>
|
||||
<a href="/items/{{.OtherID}}">{{if .OtherTitle}}{{truncate 70 .OtherTitle}}{{else}}(item){{end}}</a>
|
||||
<p class="neighbour-meta">
|
||||
<span class="pill">{{relTypeLabel .Rel.Type}}</span>
|
||||
<span>strength {{score2 .Rel.Score}} ({{strengthLabel .Rel.Score}})</span>
|
||||
<span class="pill pill-prov-{{.Rel.Provenance}}">{{provenanceLabel .Rel.Provenance}}</span>
|
||||
</p>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}<p class="muted">This item points at nothing.</p>{{end}}
|
||||
</section>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,68 @@
|
||||
{{define "partial_answer.html"}}
|
||||
<section class="answer" aria-labelledby="answer-heading">
|
||||
<h2 id="answer-heading">Answer from stored knowledge</h2>
|
||||
|
||||
<p class="answer-caveat">
|
||||
This answer was generated from the stored items listed below and nothing else.
|
||||
It is a summary of what those sources say — not a verified statement of fact.
|
||||
</p>
|
||||
|
||||
{{if not .Answer.Sufficient}}
|
||||
<p class="flash flash-warn" role="status">
|
||||
The stored material does not fully answer this question. Treat what follows as partial.
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
<div class="answer-body">
|
||||
{{range .AnswerParagraphs}}<p>{{.}}</p>{{end}}
|
||||
</div>
|
||||
|
||||
<h3>Sources used</h3>
|
||||
{{if .Answer.Sources}}
|
||||
<ol class="source-list">
|
||||
{{range .Answer.Sources}}
|
||||
<li class="source {{if not .Cited}}source-uncited{{end}}">
|
||||
<p class="source-head">
|
||||
<span class="ref">{{.Ref}}</span>
|
||||
<a href="/items/{{.ItemID}}">{{.Title}}</a>
|
||||
{{if .Date}}<span class="muted">{{.Date}}</span>{{end}}
|
||||
{{if not .Cited}}<span class="pill pill-quiet">retrieved but not cited</span>{{end}}
|
||||
</p>
|
||||
<blockquote>{{truncate 500 .Excerpt}}</blockquote>
|
||||
</li>
|
||||
{{end}}
|
||||
</ol>
|
||||
{{else}}
|
||||
<p class="muted">No stored sources were used.</p>
|
||||
{{end}}
|
||||
|
||||
{{if .Answer.Uncertainty}}
|
||||
<h3>Uncertainty and conflicts</h3>
|
||||
<ul class="conflict-list">
|
||||
{{range .Answer.Uncertainty}}<li>{{.}}</li>{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{if .Answer.InferredRelationshipsUsed}}
|
||||
<h3>Inferred relationships used in retrieval</h3>
|
||||
<p class="muted">
|
||||
These links were proposed by analysis and have not been confirmed by anyone.
|
||||
They influenced which sources were retrieved; they are not evidence in themselves.
|
||||
</p>
|
||||
<ul>
|
||||
{{range .Answer.InferredRelationshipsUsed}}
|
||||
<li>
|
||||
<a href="/items/{{.ItemID}}">{{if .Title}}{{.Title}}{{else}}(item){{end}}</a>
|
||||
— {{relTypeLabel .Type}}, strength {{score2 .Strength}} ({{strengthLabel .Strength}})
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
<p class="answer-meta muted">
|
||||
Generated by {{if .Answer.Model}}{{.Answer.Model}}{{else}}the configured model{{end}}
|
||||
from {{.Answer.Retrieved}} retrieved item{{if ne .Answer.Retrieved 1}}s{{end}}
|
||||
in {{.Answer.TookMS}} ms.
|
||||
</p>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,42 @@
|
||||
{{define "partial_jobs_table.html"}}
|
||||
<ul class="stat-grid stat-grid-compact">
|
||||
<li class="stat"><span class="stat-value">{{.Counts.Pending}}</span><span class="stat-label">Pending</span></li>
|
||||
<li class="stat"><span class="stat-value">{{.Counts.Running}}</span><span class="stat-label">Running</span></li>
|
||||
<li class="stat"><span class="stat-value">{{.Counts.Succeeded}}</span><span class="stat-label">Succeeded</span></li>
|
||||
<li class="stat {{if gt .Counts.Failed 0}}stat-warn{{end}}"><span class="stat-value">{{.Counts.Failed}}</span><span class="stat-label">Failed</span></li>
|
||||
</ul>
|
||||
|
||||
{{if .Jobs}}
|
||||
<table class="table">
|
||||
<caption class="visually-hidden">Background jobs</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Job</th><th scope="col">Item</th><th scope="col">Status</th>
|
||||
<th scope="col">Attempts</th><th scope="col">Updated</th><th scope="col">Last error</th><th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Jobs}}
|
||||
<tr>
|
||||
<td>{{.Type}}</td>
|
||||
<td>{{if .ItemID}}<a href="/items/{{.ItemID}}">item</a>{{else}}—{{end}}</td>
|
||||
<td><span class="pill pill-job-{{.Status}}">{{.Status}}</span></td>
|
||||
<td class="num">{{.Attempts}} / {{.MaxAttempts}}</td>
|
||||
<td>{{ago .UpdatedAt}}</td>
|
||||
<td class="error-cell">{{if .LastError}}{{truncate 160 .LastError}}{{else}}—{{end}}</td>
|
||||
<td>
|
||||
{{if eq .Status "failed"}}
|
||||
<form method="post" action="/jobs/{{.ID}}/retry" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-small">Retry</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="empty"><h2>The queue is empty</h2><p>No background jobs are recorded for this workspace.</p></div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,124 @@
|
||||
{{define "partial_relationship.html"}}
|
||||
{{$r := .R}}
|
||||
<article class="relationship" id="rel-{{$r.Rel.ID}}">
|
||||
<header class="rel-head">
|
||||
<p class="rel-title">
|
||||
<a href="/items/{{$r.SourceID}}">{{if $r.SourceTitle}}{{$r.SourceTitle}}{{else}}(item){{end}}</a>
|
||||
<span class="rel-arrow" aria-label="{{relTypeLabel $r.Rel.Type}}">{{relTypeLabel $r.Rel.Type}}</span>
|
||||
<a href="/items/{{$r.TargetID}}">{{if $r.TargetTitle}}{{$r.TargetTitle}}{{else}}(item){{end}}</a>
|
||||
</p>
|
||||
<p class="rel-meta">
|
||||
<span class="metric" title="Heuristic strength computed from the signals below. Not a probability.">
|
||||
Strength <strong>{{score2 $r.Rel.Score}}</strong> ({{strengthLabel $r.Rel.Score}})
|
||||
</span>
|
||||
<span class="pill pill-prov-{{$r.Rel.Provenance}}">{{provenanceLabel $r.Rel.Provenance}}</span>
|
||||
<span class="pill pill-review-{{$r.Rel.ReviewStatus}}">{{$r.Rel.ReviewStatus}}</span>
|
||||
<span class="muted">{{$r.Rel.ScoreVersion}}</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{{if $r.Rel.Inferred}}
|
||||
<p class="inference-note">
|
||||
Proposed by analysis. This is a <strong>suggestion, not an established fact</strong>,
|
||||
until someone confirms it.
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
{{if $r.Rel.Evidence.SourceExcerpt}}
|
||||
<div class="rel-excerpts">
|
||||
<blockquote><cite>From the first item</cite>{{$r.Rel.Evidence.SourceExcerpt}}</blockquote>
|
||||
{{if $r.Rel.Evidence.TargetExcerpt}}
|
||||
<blockquote><cite>From the second item</cite>{{$r.Rel.Evidence.TargetExcerpt}}</blockquote>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<details class="evidence">
|
||||
<summary>Evidence — how this strength was calculated</summary>
|
||||
|
||||
<table class="table table-compact">
|
||||
<caption class="visually-hidden">Signals contributing to the relationship strength</caption>
|
||||
<thead>
|
||||
<tr><th scope="col">Signal</th><th scope="col">Evidence</th>
|
||||
<th scope="col">Strength</th><th scope="col">Weight</th><th scope="col">Contribution</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $r.Rel.Evidence.Signals}}
|
||||
<tr>
|
||||
<td>{{.Type}}</td>
|
||||
<td>{{.Value}}{{if .Detail}}<br><span class="muted">{{.Detail}}</span>{{end}}</td>
|
||||
<td class="num">{{score2 .Strength}}</td>
|
||||
<td class="num">{{score2 .Weight}}</td>
|
||||
<td class="num">{{score2 .Contribution}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th scope="row" colspan="4">Total (capped at 1.00)</th>
|
||||
<td class="num"><strong>{{score2 $r.Rel.Score}}</strong></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
{{if $r.Rel.Evidence.SharedEntities}}
|
||||
<p>Shared entities: {{range $r.Rel.Evidence.SharedEntities}}<span class="tag">{{.}}</span>{{end}}</p>
|
||||
{{end}}
|
||||
|
||||
{{if $r.Rel.Evidence.ModelRationale}}
|
||||
<div class="model-opinion">
|
||||
<h4>Model opinion</h4>
|
||||
<p class="muted">
|
||||
A language model{{if $r.Rel.Evidence.ModelVersion}} ({{$r.Rel.Evidence.ModelVersion}}){{end}}
|
||||
described the connection as follows. This is one signal among those above, not the score itself,
|
||||
and it is not a verified fact.
|
||||
</p>
|
||||
<blockquote>{{$r.Rel.Evidence.ModelRationale}}</blockquote>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if $r.Rel.ReviewHistory}}
|
||||
<h4>Decision history</h4>
|
||||
<ul class="history-list">
|
||||
{{range $r.Rel.ReviewHistory}}
|
||||
<li>
|
||||
<strong>{{.Action}}</strong> by {{if .ActorName}}{{.ActorName}}{{else}}unknown{{end}}
|
||||
on {{datetime .At}}
|
||||
{{if .FromValue}}<span class="muted">({{.FromValue}} → {{.ToValue}})</span>{{end}}
|
||||
{{if .Note}}<br><span class="note">“{{.Note}}”</span>{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<p class="muted">
|
||||
History is append-only. Rejecting a relationship never deletes the original proposal or its evidence.
|
||||
</p>
|
||||
{{end}}
|
||||
</details>
|
||||
|
||||
<div class="rel-actions">
|
||||
<form method="post" action="/relationships/{{$r.Rel.ID}}/confirm" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<input type="text" name="note" placeholder="Note (optional)" maxlength="500" aria-label="Note explaining the decision">
|
||||
<button type="submit" class="btn btn-ok"
|
||||
{{if eq $r.Rel.ReviewStatus "confirmed"}}disabled{{end}}>Confirm</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/relationships/{{$r.Rel.ID}}/reject" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-danger"
|
||||
{{if eq $r.Rel.ReviewStatus "rejected"}}disabled{{end}}>Reject</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/relationships/{{$r.Rel.ID}}/type" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<label class="visually-hidden" for="type-{{$r.Rel.ID}}">Change relationship type</label>
|
||||
<select id="type-{{$r.Rel.ID}}" name="type">
|
||||
{{range relTypes}}
|
||||
<option value="{{.}}" {{if eq . $r.Rel.Type}}selected{{end}}>{{relTypeLabel .}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<button type="submit" class="btn">Change type</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
{{end}}
|
||||
@@ -0,0 +1,117 @@
|
||||
{{define "partial_search_results.html"}}
|
||||
<section aria-labelledby="results-heading">
|
||||
<h2 id="results-heading">
|
||||
{{if .Results.Results}}{{.Results.Total}} result{{if ne .Results.Total 1}}s{{end}}{{else}}No results{{end}}
|
||||
<span class="muted">({{.Results.TookMS}} ms)</span>
|
||||
</h2>
|
||||
|
||||
{{range .Results.Notes}}<p class="flash flash-warn" role="status">{{.}}</p>{{end}}
|
||||
|
||||
{{if .Results.Results}}
|
||||
<p class="score-key">
|
||||
<strong>Query relevance</strong> is how well an item answers this query.
|
||||
<strong>Similarity</strong> is how close its wording is to yours.
|
||||
<strong>Relationship strength</strong> is how well evidenced a link between two items is.
|
||||
These are three different measurements; none of them is a probability.
|
||||
</p>
|
||||
|
||||
<ol class="result-list">
|
||||
{{range .Results.Results}}
|
||||
<li class="result">
|
||||
<h3><a href="/items/{{.Item.ID}}">{{.Item.Title}}</a></h3>
|
||||
|
||||
<p class="result-scores">
|
||||
<span class="metric" title="How well this item answers your query, relative to the best result">
|
||||
Query relevance <strong>{{score2 .QueryRelevance}}</strong>
|
||||
</span>
|
||||
{{if .SemanticSimilarity}}
|
||||
<span class="metric" title="Cosine similarity of the closest matching section. Similar wording is not proof of a factual relationship.">
|
||||
Similarity <strong>{{score2 .SemanticSimilarity}}</strong>
|
||||
</span>
|
||||
{{end}}
|
||||
<span class="pill pill-{{.Item.Status}}">{{statusLabel .Item.Status}}</span>
|
||||
<span class="muted">{{.Item.SourceType}} · {{date .Item.CreatedAt}}</span>
|
||||
</p>
|
||||
|
||||
{{if .ViaRelationship}}
|
||||
<p class="via-note">
|
||||
Not a direct match. Reached because this item is
|
||||
<strong>{{relTypeLabel .ViaRelationship.Type}}</strong>
|
||||
a matching item — strength {{score2 .ViaRelationship.Strength}}
|
||||
({{provenanceLabel .ViaRelationship.Provenance}}).
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
<p class="excerpt">{{.Excerpt}}</p>
|
||||
|
||||
<details class="why">
|
||||
<summary>Why this matched</summary>
|
||||
<ul class="why-list">
|
||||
{{range .WhyMatched}}<li>{{.}}</li>{{end}}
|
||||
</ul>
|
||||
|
||||
{{if .MatchedEntities}}
|
||||
<p class="matched-entities">
|
||||
Matching entities:
|
||||
{{range .MatchedEntities}}<a class="tag" href="/entities/{{.ID}}">{{.Name}}</a>{{end}}
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
{{if .MatchedChunks}}
|
||||
<div class="matched-chunks">
|
||||
<h4>Matching sections</h4>
|
||||
<ul>
|
||||
{{range .MatchedChunks}}
|
||||
<li>
|
||||
<span class="chunk-loc">
|
||||
Section {{add .Sequence 1}}{{if .Page}}, page {{.Page}}{{end}}{{if .Section}} — {{.Section}}{{end}}
|
||||
{{if .Similarity}}<span class="muted">(similarity {{score2 .Similarity}})</span>{{end}}
|
||||
</span>
|
||||
<blockquote>{{truncate 400 .Text}}</blockquote>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Related}}
|
||||
<div class="related">
|
||||
<h4>Related items</h4>
|
||||
<ul>
|
||||
{{range .Related}}
|
||||
<li>
|
||||
<a href="/items/{{.ItemID}}">{{if .Title}}{{.Title}}{{else}}(untitled){{end}}</a>
|
||||
— {{relTypeLabel .Type}},
|
||||
strength {{score2 .Strength}} ({{strengthLabel .Strength}}),
|
||||
{{provenanceLabel .Provenance}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
</details>
|
||||
</li>
|
||||
{{end}}
|
||||
</ol>
|
||||
|
||||
{{if .Pagination.HasMore}}
|
||||
<nav class="pagination" aria-label="Results pages">
|
||||
{{if .Pagination.PrevURL}}<a class="btn" href="{{.Pagination.PrevURL}}">Previous</a>{{end}}
|
||||
<span class="muted">Showing {{.Pagination.From}}–{{.Pagination.To}} of {{.Results.Total}}</span>
|
||||
{{if .Pagination.NextURL}}<a class="btn" href="{{.Pagination.NextURL}}">Next</a>{{end}}
|
||||
</nav>
|
||||
{{end}}
|
||||
|
||||
{{else}}
|
||||
<div class="empty">
|
||||
<h3>Nothing matched</h3>
|
||||
<p>No stored item matched “{{.Query.Text}}”.</p>
|
||||
<ul>
|
||||
<li>Check that items have finished processing on the <a href="/jobs">queue</a> page.</li>
|
||||
<li>Try fewer or broader words, or clear the filters.</li>
|
||||
<li>Meaning-based search needs an embedding model; check <a href="/settings">Settings</a>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,103 @@
|
||||
{{define "content"}}
|
||||
<h1>The Relata API</h1>
|
||||
<p class="lede">
|
||||
Everything the interface does, <code>/api/v1</code> does too. Authenticate with a bearer
|
||||
token you create for yourself.
|
||||
</p>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Getting a token</h2>
|
||||
<p>
|
||||
Sign in, then go to <strong>Settings → API tokens</strong>. Choose a name, the scopes it
|
||||
needs, and how long it should live.
|
||||
</p>
|
||||
<p class="callout">
|
||||
The token is shown <strong>once</strong>. Relata stores only a SHA-256 hash of it, so a
|
||||
database read — or a backup — yields nothing that can authenticate. If you lose it, revoke
|
||||
it and make another.
|
||||
</p>
|
||||
|
||||
<h3>Scopes</h3>
|
||||
<table class="weights">
|
||||
<thead><tr><th>Scope</th><th>Permits</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>query</code></td><td>Search, generate answers, and read stored items, entities and relationships.</td></tr>
|
||||
<tr><td><code>ingest</code></td><td>Create items, upload files, reprocess and delete.</td></tr>
|
||||
<tr><td><code>review</code></td><td>Confirm, reject and retype relationships; confirm entity aliases.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="muted">
|
||||
A token never carries more than the role of the person who created it. The check is the
|
||||
intersection of the two, so demoting someone immediately narrows every token they hold
|
||||
without any of those tokens being touched.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Using it</h2>
|
||||
<pre><code>export RELATA_TOKEN='rlt_v1_...'
|
||||
|
||||
# Search
|
||||
curl -s -H "Authorization: Bearer $RELATA_TOKEN" \
|
||||
'https://relata.example/api/v1/search?q=delivery+schedule&limit=5'
|
||||
|
||||
# Ingest a note
|
||||
curl -s -X POST -H "Authorization: Bearer $RELATA_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
https://relata.example/api/v1/items \
|
||||
-d '{"title":"Meeting note","content":"The revised cost is 51,500.","source_type":"note"}'
|
||||
|
||||
# Ask a question of what is stored
|
||||
curl -s -X POST -H "Authorization: Bearer $RELATA_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
https://relata.example/api/v1/search/answer \
|
||||
-d '{"question":"What is the project cost?"}'</code></pre>
|
||||
|
||||
<p>
|
||||
Bearer requests do not need a CSRF token: CSRF exists to stop a third-party page spending
|
||||
<em>ambient</em> credentials, and an <code>Authorization</code> header is not ambient — a
|
||||
browser never attaches one on its own.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>What a token cannot do</h2>
|
||||
<ul class="wont">
|
||||
<li><strong>Reach the web interface.</strong> Tokens authenticate <code>/api/v1</code> only.</li>
|
||||
<li><strong>Manage tokens.</strong> Creating, listing and revoking tokens requires a
|
||||
signed-in session, so a leaked token cannot mint its own replacements or extend its life.</li>
|
||||
<li><strong>Outlive its expiry or revocation.</strong> Both are checked on every request
|
||||
against the stored record. There is no cached grant to wait out.</li>
|
||||
<li><strong>Exceed your role.</strong> Scopes narrow permission; they never widen it.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Errors</h2>
|
||||
<p>Every error is a stable code with a safe message and a request ID:</p>
|
||||
<pre><code>{
|
||||
"error": {
|
||||
"code": "insufficient_scope",
|
||||
"message": "This API token does not carry the scope this request needs.",
|
||||
"detail": { "required_scope": "ingest" },
|
||||
"request_id": "b3f1c0d2-…"
|
||||
}
|
||||
}</code></pre>
|
||||
<p class="muted">
|
||||
Rate limiting is per token. A token making too many requests gets <code>429</code> with
|
||||
<code>rate_limited</code>; the limit resets on a fixed window.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-quiet">
|
||||
<h2>For operators</h2>
|
||||
<p>
|
||||
The redirect URI to register with your identity provider is:
|
||||
</p>
|
||||
<pre><code>{{.CallbackURL}}</code></pre>
|
||||
<p class="muted">
|
||||
The path is fixed at <code>/auth/oauth/callback</code>; only the host varies. The full
|
||||
OpenAPI specification is in the repository at <code>docs/openapi.yaml</code>.
|
||||
</p>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,142 @@
|
||||
{{define "content"}}
|
||||
<h1>How Relata works</h1>
|
||||
<p class="lede">
|
||||
Ingest, derive, connect, retrieve — with the reasoning kept and shown at every step.
|
||||
</p>
|
||||
|
||||
<section class="panel">
|
||||
<h2>The pipeline</h2>
|
||||
<ol class="pipeline">
|
||||
<li><strong>Ingest.</strong> The original bytes are stored verbatim and never rewritten.
|
||||
An exact duplicate is detected and reported rather than silently stored twice.</li>
|
||||
<li><strong>Parse.</strong> Text, Markdown, JSON, PDF and DOCX become extracted text plus a
|
||||
structure map. The parser version is recorded on the item.</li>
|
||||
<li><strong>Chunk.</strong> Structure-aware splitting, so a chunk is a passage rather than
|
||||
an arbitrary window. Chunk IDs are deterministic and double as vector IDs.</li>
|
||||
<li><strong>Embed.</strong> Each chunk is embedded locally; the model that produced each
|
||||
vector is recorded, so a model change is visible rather than silent.</li>
|
||||
<li><strong>Extract.</strong> Entities and claims, deterministically where the text allows
|
||||
and with model assistance where it does not — each labelled with which produced it.</li>
|
||||
<li><strong>Discover.</strong> A bounded candidate set is scored against explicit signals.
|
||||
There is no all-pairs comparison and no unexplained number anywhere in it.</li>
|
||||
</ol>
|
||||
<p class="muted">
|
||||
Every stage is a queued job with a lease rather than a lock, so a worker that dies has its
|
||||
work returned to the queue rather than holding it forever.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>How a relationship gets its score</h2>
|
||||
<p>
|
||||
The score is the weighted sum of the signals that fired, capped at 1.00, under a versioned
|
||||
scoring model. Each signal is stored with its own value, strength and weight, so the total
|
||||
is arithmetic you can check.
|
||||
</p>
|
||||
<table class="weights">
|
||||
<thead><tr><th>Signal</th><th>Weight</th><th>What it means</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Weights}}
|
||||
<tr>
|
||||
<td><code>{{.Signal}}</code></td>
|
||||
<td class="num">{{printf "%.2f" .Weight}}</td>
|
||||
<td>{{.Means}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Three deliberate judgements</h3>
|
||||
<ul>
|
||||
<li><strong>Similarity alone cannot look like evidence.</strong> Unrelated documents from
|
||||
one corpus routinely sit at 0.5–0.7 cosine, so raw similarity carries almost no
|
||||
information at the low end. It is rebased against a floor of 0.55 before it counts.</li>
|
||||
<li><strong>A model cannot manufacture a relationship.</strong> Its weight is below the
|
||||
storage threshold, so a maximally confident classification still cannot create a link.
|
||||
It can shape a relationship other evidence already supports.</li>
|
||||
<li><strong>Human decisions dominate.</strong> Confirmation saturates the score; rejection
|
||||
drives it to zero. Neither deletes anything — the original proposal, its provenance and
|
||||
its signals are all retained.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Diminishing returns</h3>
|
||||
<p>
|
||||
Repeated signals of one type contribute 1.0, then 0.5, then 0.25, then nothing. Breadth of
|
||||
evidence beats repetition: a document mentioning one company twelve times should not
|
||||
outrank one that relates on several independent grounds.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Why each search result was returned</h2>
|
||||
<p>
|
||||
A query runs through several independent paths — keyword, vector, entity, title, and
|
||||
expansion through relationships — each producing its own ordering. They are fused by
|
||||
<em>reciprocal rank</em> rather than by adding scores, because a keyword score and a cosine
|
||||
distance are not on comparable scales and whichever had the larger range would dominate.
|
||||
</p>
|
||||
<p>Every result carries its reasons in plain words:</p>
|
||||
<ul class="why">
|
||||
<li>A section of this item is close in meaning to your query (similarity 0.89).</li>
|
||||
<li>The item's text contains words from your query.</li>
|
||||
<li>This item refers to Example Ltd (organisation), which your query names.</li>
|
||||
<li>Not a direct match. Reached because this item <strong>updates</strong> a matching item —
|
||||
strength 0.68 (suggested by analysis, not yet reviewed).</li>
|
||||
</ul>
|
||||
<p class="muted">
|
||||
Relevance is normalised against the best result in the same response, so it is comparable
|
||||
within one result set and not across queries. The interface says so rather than leaving you
|
||||
to assume otherwise.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Answers from stored knowledge</h2>
|
||||
<p>These hold regardless of what the model returns:</p>
|
||||
<ol>
|
||||
<li>The model sees only retrieved passages, inside delimited untrusted blocks.</li>
|
||||
<li>Passages are labelled <code>S1</code>, <code>S2</code>… — item IDs are never shown to
|
||||
the model, so a fabricated citation cannot resolve to a real record.</li>
|
||||
<li>An answer claiming sufficiency while citing nothing is downgraded to insufficient.</li>
|
||||
<li>Contradictions Relata recorded between the retrieved items are surfaced
|
||||
<strong>independently of the model's own account</strong>, so a model that quietly picked
|
||||
a side cannot hide the disagreement.</li>
|
||||
<li>Inferred relationships that influenced retrieval are listed as proposals, not facts.</li>
|
||||
<li>If a passage tried to instruct the model, that is reported rather than hidden.</li>
|
||||
<li>A model that returns nothing usable costs you the prose, not the retrieval: the sources
|
||||
and conflicts still come back, with the reason the answer was rejected.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Your workspace is your own</h2>
|
||||
<p>
|
||||
Each person who signs in gets a private workspace. No document, entity or relationship is
|
||||
shared with anyone else, and the boundary is enforced in every storage query rather than in
|
||||
a filter that could be forgotten.
|
||||
</p>
|
||||
<p class="muted">
|
||||
The cost is worth stating plainly: <strong>relationship discovery never spans users.</strong>
|
||||
Two people who upload documents about the same contract get no link between them, because
|
||||
as far as Relata is concerned they are separate corpora.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-quiet">
|
||||
<h2>Known limitations</h2>
|
||||
<ul class="wont">
|
||||
<li>Scores are heuristic strengths, not calibrated probabilities. The weights were chosen by
|
||||
judgement, not fitted to labelled data.</li>
|
||||
<li>Deterministic entity extraction is narrow: organisations with a legal suffix, people
|
||||
with an honorific, email addresses and dates. Broader coverage comes from the model stage
|
||||
and is labelled as such.</li>
|
||||
<li>Contradiction detection compares figures and dates about the same coarse subject. It
|
||||
will not catch a contradiction expressed entirely in prose.</li>
|
||||
<li>No OCR. A scanned PDF is preserved and flagged as having little extractable text.</li>
|
||||
<li>Answer generation on a CPU-only machine is slow. A small model produces answers that
|
||||
are often not worth reading, though the safeguards still hold.</li>
|
||||
<li>Prompt-injection defences bound the damage; they are not a proof. Treat generated
|
||||
answers as summaries of sources, not as authority.</li>
|
||||
</ul>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,133 @@
|
||||
{{define "content"}}
|
||||
<section class="hero">
|
||||
<h1>Store what you know. Find out how it connects.</h1>
|
||||
<p class="lede">
|
||||
Relata stores arbitrary information, discovers <strong>evidence-backed</strong> relationships
|
||||
between items, and retrieves related knowledge with an explanation of <em>why</em> each
|
||||
result was returned.
|
||||
</p>
|
||||
<p class="hero-actions">
|
||||
{{if .SignedIn}}
|
||||
<a class="btn btn-primary btn-large" href="/">Open your workspace</a>
|
||||
{{else if .OAuthEnabled}}
|
||||
<a class="btn btn-primary btn-large" href="/auth/oauth/start">Sign in with {{.ProviderName}}</a>
|
||||
{{else}}
|
||||
<a class="btn btn-primary btn-large" href="/login">Sign in</a>
|
||||
{{end}}
|
||||
<a class="btn btn-large" href="/how-it-works">How it works</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Three numbers, never mixed up</h2>
|
||||
<p>
|
||||
Most systems collapse "these documents read alike", "these documents are connected" and
|
||||
"this document answers your question" into a single relevance figure. They are three
|
||||
different claims with three different meanings, and Relata keeps them apart in storage, in
|
||||
the API and on screen.
|
||||
</p>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<h3>Semantic similarity</h3>
|
||||
<p class="card-q">How alike are these two pieces of text?</p>
|
||||
<p>Cosine distance between embeddings. Nothing more. Two incident reports written by the
|
||||
same team in the same house style will score highly and describe entirely different
|
||||
events.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Relationship strength</h3>
|
||||
<p class="card-q">How well evidenced is a link between two items?</p>
|
||||
<p>A weighted sum of named signals, each stored with its own value and weight, so the
|
||||
arithmetic is always inspectable. Not a probability.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Query relevance</h3>
|
||||
<p class="card-q">How well does this item answer <em>this</em> question?</p>
|
||||
<p>Several retrieval paths fused by rank. Comparable within one result set, not across
|
||||
queries — and it says so.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Similarity is not a relationship</h2>
|
||||
<p>
|
||||
This is the distinction the whole design exists to preserve. Two documents that share
|
||||
wording are not thereby connected; two documents that mention the same company are not
|
||||
thereby about the same thing. Relata records what it actually found:
|
||||
</p>
|
||||
<div class="evidence-example">
|
||||
<div class="evidence-head">
|
||||
<span class="rel-type">contradicts</span>
|
||||
<span class="rel-score">strength 0.72</span>
|
||||
<span class="rel-status">suggested by analysis — not yet reviewed</span>
|
||||
</div>
|
||||
<table class="signals">
|
||||
<thead><tr><th>Signal</th><th>Value</th><th>Strength</th><th>Weight</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>claim_contradiction</td><td>cost: 42,000 and 51,500</td><td>1.00</td><td>0.22</td></tr>
|
||||
<tr><td>shared_entity</td><td>Example Ltd</td><td>1.00</td><td>0.25</td></tr>
|
||||
<tr><td>semantic_similarity</td><td>0.89 cosine</td><td>0.76</td><td>0.30</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="evidence-note">
|
||||
Both figures are named. Relata does not decide which is correct.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>What a language model is allowed to do</h2>
|
||||
<p>
|
||||
A model may classify a relationship's type or describe a connection. Its opinion is
|
||||
<strong>one signal, weighted 0.15</strong> — below the threshold at which a relationship is
|
||||
stored at all. A model cannot create a connection on its own, however confident it is. That
|
||||
is a property of the arithmetic, not a promise, and there is a test asserting it.
|
||||
</p>
|
||||
<table class="weights">
|
||||
<caption>Signal weights in <code>relationship-score-v1</code></caption>
|
||||
<thead><tr><th>Signal</th><th>Weight</th><th>What it means</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Weights}}
|
||||
<tr>
|
||||
<td><code>{{.Signal}}</code></td>
|
||||
<td class="num">{{printf "%.2f" .Weight}}</td>
|
||||
<td>{{.Means}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>What it will not do</h2>
|
||||
<ul class="wont">
|
||||
<li><strong>It will not merge two entities because their names look alike.</strong>
|
||||
"Example Ltd" and "Example Holdings Ltd" are different companies far more often than they
|
||||
are typos of each other. Similar names become a candidate alias awaiting a person's
|
||||
confirmation, and a confirmed alias still does not merge the records.</li>
|
||||
<li><strong>It will not infer causation from sequence.</strong> Two things happening in
|
||||
order yields a chronological statement and nothing more.</li>
|
||||
<li><strong>It will not hide conflicting evidence.</strong> Contradictions between retrieved
|
||||
documents are surfaced independently of what any model said about them.</li>
|
||||
<li><strong>It will not present an inference as a fact.</strong> Every discovered
|
||||
relationship arrives as a proposal, labelled as one, and nothing is auto-confirmed.</li>
|
||||
<li><strong>It will not rewrite your originals.</strong> The bytes you supplied are stored
|
||||
verbatim; everything else is derived and reproducible.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-quiet">
|
||||
<h2>Honest about the limits</h2>
|
||||
<p>
|
||||
Relationship scores are heuristic strengths, not calibrated probabilities — the weights were
|
||||
chosen by judgement, not fitted to labelled data. A 0.7 does not mean "70% likely to be
|
||||
real". Deterministic extraction is deliberately narrow. There is no OCR. Prompt-injection
|
||||
defences make injection much harder and bound its effect; they are not a proof.
|
||||
</p>
|
||||
<p>
|
||||
<a href="/how-it-works">The full account is on the how-it-works page</a>, including what is
|
||||
verified and what is merely intended.
|
||||
</p>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,49 @@
|
||||
{{define "public_layout.html"}}<!DOCTYPE html>
|
||||
<html lang="en-GB">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{if .Title}}{{.Title}}{{else}}Relata{{end}}</title>
|
||||
<meta name="description" content="Relata stores arbitrary information, discovers evidence-backed relationships between items, and retrieves related knowledge with an explanation of why each result was returned.">
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
<link rel="stylesheet" href="/static/public.css">
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
|
||||
</head>
|
||||
<body class="public">
|
||||
<a class="skip-link" href="#main">Skip to main content</a>
|
||||
|
||||
<header class="public-masthead">
|
||||
<div class="public-inner">
|
||||
<a class="brand" href="/">Relata</a>
|
||||
<nav aria-label="Main">
|
||||
<ul class="public-nav">
|
||||
<li><a href="/how-it-works">How it works</a></li>
|
||||
<li><a href="/api">API</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="public-actions">
|
||||
{{if .SignedIn}}
|
||||
<a class="btn btn-primary" href="/">Open Relata</a>
|
||||
{{else if .OAuthEnabled}}
|
||||
<a class="btn btn-primary" href="/auth/oauth/start">Sign in with {{.ProviderName}}</a>
|
||||
{{else}}
|
||||
<a class="btn btn-primary" href="/login">Sign in</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main" class="public-page">
|
||||
{{template "content" .}}
|
||||
</main>
|
||||
|
||||
<footer class="public-footer">
|
||||
<div class="public-inner">
|
||||
<p>Relata {{.Version}} — a self-organising semantic document database.</p>
|
||||
<p class="muted">
|
||||
Nothing on this page is read from stored data. Sign in to reach your own workspace.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>{{end}}
|
||||
@@ -0,0 +1,48 @@
|
||||
{{define "content"}}
|
||||
<h1>Relationship review</h1>
|
||||
<p class="muted">
|
||||
These relationships were proposed by analysis from the evidence shown under each one.
|
||||
Nothing here is treated as fact until a person confirms it. Rejecting a relationship keeps
|
||||
the proposal and its evidence on record — it is never deleted.
|
||||
</p>
|
||||
|
||||
<form class="filter-bar" method="get" action="/review">
|
||||
<div class="field">
|
||||
<label for="review_status">Show</label>
|
||||
<select id="review_status" name="review_status">
|
||||
<option value="pending" {{if eq .Filters.Status "pending"}}selected{{end}}>Awaiting review</option>
|
||||
<option value="confirmed" {{if eq .Filters.Status "confirmed"}}selected{{end}}>Confirmed</option>
|
||||
<option value="rejected" {{if eq .Filters.Status "rejected"}}selected{{end}}>Rejected</option>
|
||||
<option value="" {{if eq .Filters.Status ""}}selected{{end}}>All</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="type">Relationship type</label>
|
||||
<select id="type" name="type">
|
||||
<option value="">Any</option>
|
||||
{{range relTypes}}<option value="{{.}}" {{if eq $.Filters.Type (printf "%s" .)}}selected{{end}}>{{relTypeLabel .}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="min_score">Minimum strength</label>
|
||||
<input id="min_score" name="min_score" type="number" min="0" max="1" step="0.05" value="{{.Filters.MinScore}}">
|
||||
</div>
|
||||
<button type="submit" class="btn">Filter</button>
|
||||
</form>
|
||||
|
||||
{{if .Relationships}}
|
||||
{{range .Relationships}}{{template "partial_relationship.html" (dict "R" . "CSRFToken" $.CSRFToken "Direction" "outgoing")}}{{end}}
|
||||
|
||||
<nav class="pagination" aria-label="Relationship pages">
|
||||
{{if .Pagination.PrevURL}}<a class="btn" href="{{.Pagination.PrevURL}}">Previous</a>{{end}}
|
||||
<span class="muted">Showing {{.Pagination.From}}–{{.Pagination.To}} of {{.Pagination.Total}}</span>
|
||||
{{if .Pagination.NextURL}}<a class="btn" href="{{.Pagination.NextURL}}">Next</a>{{end}}
|
||||
</nav>
|
||||
{{else}}
|
||||
<div class="empty">
|
||||
<h2>Nothing to review</h2>
|
||||
<p>No relationships match this filter.</p>
|
||||
<p class="muted">Relationships are proposed automatically once items finish processing.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,104 @@
|
||||
{{define "content"}}
|
||||
<h1>Search</h1>
|
||||
|
||||
<form class="search-form" method="get" action="/search" role="search">
|
||||
<div class="field field-wide">
|
||||
<label for="q">Search stored knowledge</label>
|
||||
<input id="q" name="q" type="search" value="{{.Query.Text}}"
|
||||
placeholder="e.g. What information relates to the revised delivery date?"
|
||||
autocomplete="off" aria-describedby="q-hint">
|
||||
<p id="q-hint" class="hint">
|
||||
Natural language works. Relata combines keyword matching, meaning-based similarity,
|
||||
entity matching and relationships between items.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<details class="filters" {{if .HasFilters}}open{{end}}>
|
||||
<summary>Filters</summary>
|
||||
<div class="filter-grid">
|
||||
<div class="field">
|
||||
<label for="created_from">Created from</label>
|
||||
<input id="created_from" name="created_from" type="date" value="{{.Filters.CreatedFrom}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="created_to">Created to</label>
|
||||
<input id="created_to" name="created_to" type="date" value="{{.Filters.CreatedTo}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="source_type">Source type</label>
|
||||
<select id="source_type" name="source_type">
|
||||
<option value="">Any</option>
|
||||
{{range sourceTypes}}
|
||||
<option value="{{.}}" {{if eq $.Filters.SourceType (printf "%s" .)}}selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="mime_type">File type</label>
|
||||
<select id="mime_type" name="mime_type">
|
||||
<option value="">Any</option>
|
||||
<option value="text/plain" {{if eq .Filters.MIMEType "text/plain"}}selected{{end}}>Plain text</option>
|
||||
<option value="text/markdown" {{if eq .Filters.MIMEType "text/markdown"}}selected{{end}}>Markdown</option>
|
||||
<option value="application/json" {{if eq .Filters.MIMEType "application/json"}}selected{{end}}>JSON</option>
|
||||
<option value="application/pdf" {{if eq .Filters.MIMEType "application/pdf"}}selected{{end}}>PDF</option>
|
||||
<option value="application/vnd.openxmlformats-officedocument.wordprocessingml.document" {{if eq .Filters.MIMEType "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}}selected{{end}}>Word document</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="entity_id">Entity</label>
|
||||
<select id="entity_id" name="entity_id">
|
||||
<option value="">Any</option>
|
||||
{{range .AllEntities}}
|
||||
<option value="{{.ID}}" {{if eq $.Filters.EntityID .ID}}selected{{end}}>{{.CanonicalName}} ({{.Type}})</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="relationship_type">Relationship type</label>
|
||||
<select id="relationship_type" name="relationship_type">
|
||||
<option value="">Any</option>
|
||||
{{range relTypes}}
|
||||
<option value="{{.}}" {{if eq $.Filters.RelType (printf "%s" .)}}selected{{end}}>{{relTypeLabel .}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="min_relationship_strength">Minimum relationship strength</label>
|
||||
<input id="min_relationship_strength" name="min_relationship_strength" type="number"
|
||||
min="0" max="1" step="0.05" value="{{.Filters.MinStrength}}">
|
||||
<p class="hint">A heuristic strength between 0 and 1, not a probability.</p>
|
||||
</div>
|
||||
<div class="field field-check">
|
||||
<input id="confirmed_only" name="confirmed_only" type="checkbox" value="true"
|
||||
{{if .Filters.ConfirmedOnly}}checked{{end}}>
|
||||
<label for="confirmed_only">Only follow relationships a person has confirmed</label>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="search-actions">
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
{{if .AnswerEnabled}}
|
||||
<button type="submit" name="answer" value="true" class="btn">Answer from stored knowledge</button>
|
||||
{{end}}
|
||||
<a class="btn btn-quiet" href="/search">Clear</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{if .Answer}}{{template "partial_answer.html" .}}{{end}}
|
||||
|
||||
{{if .Searched}}
|
||||
{{template "partial_search_results.html" .}}
|
||||
{{else}}
|
||||
<div class="empty">
|
||||
<h2>Try a question</h2>
|
||||
<ul class="example-list">
|
||||
<li><a href="/search?q=What+information+relates+to+the+revised+delivery+date%3F">What information relates to the revised delivery date?</a></li>
|
||||
<li><a href="/search?q=Which+sources+disagree+about+the+project+cost%3F">Which sources disagree about the project cost?</a></li>
|
||||
<li><a href="/search?q=Show+information+associated+with+Example+Ltd">Show information associated with Example Ltd</a></li>
|
||||
<li><a href="/search?q=What+replaced+the+original+policy%3F">What replaced the original policy?</a></li>
|
||||
<li><a href="/search?q=Find+items+similar+in+meaning+but+not+actually+about+the+same+event">Find items similar in meaning but not actually about the same event</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,148 @@
|
||||
{{define "content"}}
|
||||
<h1>System settings</h1>
|
||||
<p class="muted">
|
||||
Configuration is read from environment variables at startup. This page shows the values in
|
||||
effect; secrets are never displayed. Change them in <code>.env</code> and restart Relata.
|
||||
</p>
|
||||
|
||||
<section aria-labelledby="models-heading">
|
||||
<h2 id="models-heading">Model status</h2>
|
||||
<table class="table">
|
||||
<caption class="visually-hidden">AI provider status</caption>
|
||||
<thead><tr><th scope="col">Provider</th><th scope="col">Model</th><th scope="col">Endpoint</th><th scope="col">Status</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Providers}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{.Model}}</td>
|
||||
<td><code>{{.Endpoint}}</code></td>
|
||||
<td>
|
||||
{{if .Available}}<span class="pill pill-ok">available</span>
|
||||
{{else}}<span class="pill pill-warn">unavailable</span>{{end}}
|
||||
{{if .Detail}}<br><span class="muted">{{.Detail}}</span>{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="muted">
|
||||
Relata works without a language model: ingestion, chunking, embedding, deterministic entity and
|
||||
claim extraction, relationship scoring, keyword search and browsing all continue.
|
||||
Only model-assisted extraction, relationship classification and generated answers require one.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="versions-heading">
|
||||
<h2 id="versions-heading">Versions in use</h2>
|
||||
<dl class="meta-list">
|
||||
<dt>Relata</dt><dd>{{.Version}}</dd>
|
||||
<dt>Parser</dt><dd>{{.ParserVersion}}</dd>
|
||||
<dt>Relationship scoring</dt><dd>{{.ScoreVersion}}</dd>
|
||||
<dt>Search ranking</dt><dd>{{.RankVersion}}</dd>
|
||||
<dt>Embedding dimensions</dt><dd>{{.EmbeddingDims}}</dd>
|
||||
</dl>
|
||||
|
||||
{{if .ModelVersions}}
|
||||
<h3>Models that produced stored data</h3>
|
||||
<table class="table">
|
||||
<caption class="visually-hidden">Model versions observed</caption>
|
||||
<thead><tr><th scope="col">Kind</th><th scope="col">Name</th><th scope="col">Dimensions</th><th scope="col">First seen</th><th scope="col">Last seen</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .ModelVersions}}
|
||||
<tr>
|
||||
<td>{{.Kind}}</td><td>{{.Name}}</td>
|
||||
<td class="num">{{if .Dimensions}}{{.Dimensions}}{{else}}—{{end}}</td>
|
||||
<td>{{date .FirstSeenAt}}</td><td>{{date .LastSeenAt}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="muted">
|
||||
If the configured embedding model differs from the one that produced stored vectors,
|
||||
re-embed the workspace so that similarity comparisons remain meaningful.
|
||||
</p>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="limits-heading">
|
||||
<h2 id="limits-heading">Limits in effect</h2>
|
||||
<dl class="meta-list">
|
||||
<dt>Maximum upload size</dt><dd>{{.Limits.MaxUpload}}</dd>
|
||||
<dt>Maximum extracted text</dt><dd>{{.Limits.MaxText}}</dd>
|
||||
<dt>Maximum JSON nesting depth</dt><dd>{{.Limits.MaxJSONDepth}}</dd>
|
||||
<dt>Maximum sections per item</dt><dd>{{.Limits.MaxChunks}}</dd>
|
||||
<dt>Section size ceiling</dt><dd>{{.Limits.ChunkMax}} characters</dd>
|
||||
<dt>Background workers</dt><dd>{{.Limits.Workers}}</dd>
|
||||
<dt>Concurrent AI calls</dt><dd>{{.Limits.AIConcurrency}}</dd>
|
||||
<dt>Concurrent ingestion</dt><dd>{{.Limits.ConcurrentIngest}}</dd>
|
||||
<dt>Relationship candidates per item</dt><dd>{{.Limits.RelCandidates}}</dd>
|
||||
<dt>Relationship storage threshold</dt><dd>{{.Limits.RelStoreScore}}</dd>
|
||||
<dt>Near-duplicate similarity</dt><dd>{{.Limits.NearDuplicate}}</dd>
|
||||
<dt>Session idle timeout</dt><dd>{{.Limits.SessionIdle}}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="storage-heading">
|
||||
<h2 id="storage-heading">Storage</h2>
|
||||
<dl class="meta-list">
|
||||
<dt>MongoDB database</dt><dd><code>{{.Storage.MongoDatabase}}</code></dd>
|
||||
<dt>Qdrant collection</dt><dd><code>{{.Storage.QdrantCollection}}</code></dd>
|
||||
<dt>Vectors stored</dt><dd>{{.Storage.VectorCount}}</dd>
|
||||
<dt>Chunks in MongoDB</dt><dd>{{.Storage.ChunkCount}}</dd>
|
||||
<dt>Upload directory</dt><dd><code>{{.Storage.UploadDir}}</code></dd>
|
||||
</dl>
|
||||
{{if .Storage.Mismatch}}
|
||||
<p class="flash flash-warn">
|
||||
The number of stored vectors does not match the number of chunks. Re-embedding the
|
||||
workspace will reconcile them.
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
{{if .IsAdmin}}
|
||||
<form method="post" action="/settings/reembed" class="inline-form"
|
||||
data-confirm="Re-embed every chunk in this workspace? This will queue background work.">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn">Re-embed workspace</button>
|
||||
</form>
|
||||
<p class="hint">Use this after changing the embedding model.</p>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="tokens-heading">
|
||||
<h2 id="tokens-heading">API access</h2>
|
||||
<p>
|
||||
Programs authenticate to <code>/api/v1</code> with bearer tokens you create for yourself.
|
||||
Each token carries explicit scopes and never more authority than your role.
|
||||
</p>
|
||||
<p>
|
||||
<a class="btn" href="/settings/tokens">Manage API tokens</a>
|
||||
<a class="btn btn-quiet" href="/api">How the API works</a>
|
||||
</p>
|
||||
{{if .OAuthEnabled}}
|
||||
<p class="hint">
|
||||
You signed in through {{.ProviderName}}. Your workspace is private to you: no item, entity
|
||||
or relationship is shared with anyone else, which also means discovery never spans users.
|
||||
</p>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="scoring-heading">
|
||||
<h2 id="scoring-heading">How relationship strength is calculated</h2>
|
||||
<p>
|
||||
Relata does not ask a language model for a percentage. Each relationship's strength is the sum
|
||||
of weighted contributions from explicit signals, capped at 1.00. A model's opinion is
|
||||
<strong>one signal</strong> and cannot on its own push a relationship past the storage threshold.
|
||||
</p>
|
||||
<table class="table">
|
||||
<caption class="visually-hidden">Signal weights under {{.ScoreVersion}}</caption>
|
||||
<thead><tr><th scope="col">Signal</th><th scope="col">Weight</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Weights}}<tr><td>{{.Name}}</td><td class="num">{{score2 .Weight}}</td></tr>{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="muted">
|
||||
The resulting number is a <strong>heuristic relationship strength</strong> under
|
||||
<code>{{.ScoreVersion}}</code>. It is not a calibrated probability and should not be read as one.
|
||||
</p>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,52 @@
|
||||
{{define "content"}}
|
||||
<h1>Upload a document</h1>
|
||||
<p class="muted">
|
||||
Accepted: plain text, Markdown, JSON, PDF and Word (.docx).
|
||||
Archives and executables are refused. Uploaded files are stored unchanged and are never executed.
|
||||
</p>
|
||||
|
||||
<form method="post" action="/upload" class="stack" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
|
||||
<div class="field">
|
||||
<label for="file">File</label>
|
||||
<input id="file" name="file" type="file" required
|
||||
accept=".txt,.md,.markdown,.json,.pdf,.docx,text/plain,text/markdown,application/json,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
aria-describedby="file-hint">
|
||||
<p id="file-hint" class="hint">Maximum {{.MaxUploadMB}} MB. The file type is verified from its contents, not its name.</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="title">Title</label>
|
||||
<input id="title" name="title" type="text" maxlength="300">
|
||||
<p class="hint">Optional. The filename is used if you leave this blank.</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="field">
|
||||
<label for="source_type">Source type</label>
|
||||
<select id="source_type" name="source_type">
|
||||
{{range sourceTypes}}<option value="{{.}}">{{.}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="created_at">Date of the information</label>
|
||||
<input id="created_at" name="created_at" type="date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field field-check">
|
||||
<input id="allow_duplicate" name="allow_duplicate" type="checkbox" value="true">
|
||||
<label for="allow_duplicate">Store even if identical content already exists</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Upload and process</button>
|
||||
</form>
|
||||
|
||||
{{if .Warning}}
|
||||
<div class="callout callout-warn">
|
||||
<h2>Scanned documents</h2>
|
||||
<p>Relata does not perform OCR in this version. A scanned PDF with no text layer is stored and preserved, but its contents cannot be searched. Relata will tell you when it detects this.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user