feat(trip-cluster): master/detail review routes + templates + clusterReview JS
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
{% from "macros.html" import lightbox %}
|
||||
<div data-cluster-id="{{ c.id }}">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-3">
|
||||
<input id="cluster-name" class="input input-bordered input-sm"
|
||||
value="{{ c.decided_name or c.suggested_name }}">
|
||||
<button class="btn btn-sm btn-success" @click="approve()">Approve (A)</button>
|
||||
<button class="btn btn-sm" @click="nonTrip()">Non-trip (N)</button>
|
||||
<!-- F7: Split disabled until a boundary photo is focused -->
|
||||
<button class="btn btn-sm" :disabled="!focused" @click="split()">Split (S)</button>
|
||||
<button class="btn btn-sm" @click="skip()">Skip (X)</button>
|
||||
<button class="btn btn-sm btn-ghost" @click="apply()">Apply</button>
|
||||
{% if prev_id %}<button class="btn btn-xs" @click="merge({{ prev_id }})">⤺ merge prev</button>{% endif %}
|
||||
{% if next_id %}<button class="btn btn-xs" @click="merge({{ next_id }})">merge next ⤻</button>{% endif %}
|
||||
</div>
|
||||
<!-- F7: visible instruction for Split -->
|
||||
<div class="text-xs opacity-60 mb-2" x-show="!focused">
|
||||
Focus the first photo of the second trip (click it or use ← →), then Split.
|
||||
</div>
|
||||
<div class="text-sm opacity-60 mb-2">
|
||||
{{ c.start_at[:10] }} → {{ c.end_at[:10] }} · {{ members | length }} assets · status {{ c.status }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2">
|
||||
{% for a, m in members %}
|
||||
<div class="photo-card relative cursor-pointer rounded-lg overflow-hidden border-2
|
||||
{% if m.flagged_coverage %}border-info{% elif m.is_outlier %}border-warning{% else %}border-transparent{% endif %}"
|
||||
data-asset-id="{{ a.immich_id }}" data-included="{{ '1' if m.included else '' }}"
|
||||
tabindex="0" @click="openLightbox($el)" @focus="select($el)">
|
||||
<img src="/thumb/{{ a.immich_id }}" class="w-full aspect-square object-cover" loading="lazy" alt="">
|
||||
{% if m.flagged_coverage %}
|
||||
<button class="absolute bottom-1 left-1 badge badge-xs badge-info"
|
||||
@click.stop="setMember('{{ a.immich_id }}', true)">+ include</button>
|
||||
{% elif m.is_outlier %}
|
||||
<button class="absolute bottom-1 left-1 badge badge-xs badge-warning"
|
||||
@click.stop="setMember('{{ a.immich_id }}', false)">– exclude</button>
|
||||
{% endif %}
|
||||
{% if not m.included %}<div class="absolute inset-0 bg-black/50 pointer-events-none"></div>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ lightbox() }}
|
||||
</div>
|
||||
@@ -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