72 lines
3.5 KiB
HTML
72 lines
3.5 KiB
HTML
{% 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 %}
|