feat(trip-cluster): master/detail review routes + templates + clusterReview JS
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros.html" import confidence_badge, status_badge %}
|
||||
{% block title %}trip-cluster{% endblock %}
|
||||
{% block navbar_title %}trip-cluster{% endblock %}
|
||||
{% block content %}
|
||||
{% if stats.clusters == 0 %}
|
||||
<div class="hero py-20"><div class="hero-content text-center"><div>
|
||||
<h1 class="text-2xl font-bold">No clusters yet</h1>
|
||||
{% if stats.assets == 0 %}
|
||||
<p class="opacity-70 mt-2">Nothing ingested. Run
|
||||
<code>categorize ingest</code> then <code>categorize cluster</code>.</p>
|
||||
{% else %}
|
||||
<p class="opacity-70 mt-2">{{ stats.assets }} assets ingested, but this scope
|
||||
produced no clusters. Try a wider <code>categorize ingest</code> scope.</p>
|
||||
{% endif %}
|
||||
</div></div></div>
|
||||
{% else %}
|
||||
<div x-data="clusterReview()" @keydown.window="onKey($event)" class="flex gap-4">
|
||||
<div class="w-72 shrink-0 max-h-[85vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="font-bold">Clusters</span>
|
||||
<button class="btn btn-xs btn-primary" @click="approveHighConfidence()">Approve high-conf</button>
|
||||
</div>
|
||||
<!-- F8: explain the attention sort -->
|
||||
<p class="text-xs opacity-60 mb-2">Needs attention first — lowest-confidence pending.</p>
|
||||
{% if stats.pending == 0 %}
|
||||
<div class="alert alert-success text-xs mb-2">All reviewed — “Apply all” or <code>categorize apply</code>.</div>
|
||||
{% endif %}
|
||||
<!-- F4: write-failed clusters surfaced here after Apply all -->
|
||||
<div x-show="failures.length" class="alert alert-error text-xs mb-2 flex-col items-start">
|
||||
<span class="font-semibold">Write failures:</span>
|
||||
<ul class="list-disc ml-4">
|
||||
<template x-for="f in failures" :key="f.id">
|
||||
<li><span x-text="f.name"></span> — <span x-text="f.failed"></span> failed</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
{% for c in clusters %}
|
||||
<div class="cluster-row block p-2 rounded cursor-pointer hover:bg-base-300"
|
||||
:class="selected == {{ c.id }} && 'bg-base-300'"
|
||||
data-cluster-id="{{ c.id }}" data-cluster-name="{{ c.decided_name or c.suggested_name }}"
|
||||
@click="selectCluster({{ c.id }})">
|
||||
<div class="flex items-center justify-between gap-1">
|
||||
<span class="truncate text-sm font-medium">{{ c.decided_name or c.suggested_name }}</span>
|
||||
{{ confidence_badge(c.confidence) }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 mt-1">
|
||||
<span class="cluster-status" data-cluster-id="{{ c.id }}">{{ status_badge(c.status) }}</span>
|
||||
<span class="badge badge-xs">{{ c.count }}</span>
|
||||
<span class="text-xs opacity-50">{{ c.start_at[:10] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button class="btn btn-xs btn-block mt-3" @click="applyAll()">Apply all approved</button>
|
||||
</div>
|
||||
<div class="flex-1 relative">
|
||||
<!-- F16: loading indicator for declarative + programmatic swaps -->
|
||||
<div id="detail-indicator" class="htmx-indicator absolute top-2 right-2 z-10">
|
||||
<span class="loading loading-spinner loading-md"></span>
|
||||
</div>
|
||||
<div id="detail" class="flex-1"
|
||||
hx-get="/cluster/{{ clusters[0].id }}" hx-trigger="load" hx-target="#detail"
|
||||
hx-indicator="#detail-indicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fixed bottom-2 right-2 text-xs opacity-50">
|
||||
[ ] cluster · ← → grid · Enter open · A approve · N non-trip · S split (focus a photo) · X skip · Esc close
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block extra_scripts %}<script src="/static/app.js"></script>{% endblock %}
|
||||
Reference in New Issue
Block a user