Spaces:
Running
Bring back info-dense cards, drop catcher, add semantic-flow rule
Browse filesUser feedback after PR #5: hiding all detail behind an expand button
made cards harder to scan (had to click every card to know what it
was), and the catcher at top was hijacking drops into the assembly
(its sticky z-index made it intercept the cursor on any drag heading
upward). Result: cards "snap back" after every drag attempt.
Card redesign (info-dense again, Linear tokens kept):
- Row 1: role pill + (assembly only) index badge + duration
- Row 2: name (semibold, up to 2 lines)
- Row 3: SRT text preview (up to 2 lines, italic gray)
- Row 4: cues + SRT time window (monospace, tiny)
No more expand button — everything's visible, just typographically
ranked so the eye lands on name/preview first and skips the meta.
Drag fixes:
- Removed the catcher entirely. The auto-route was nice in theory but
its sticky position + z-index intercepted drops into the assembly,
which is the most common direction. Without it, drags go where the
cursor goes.
- Switched off forceFallback. Native HTML5 drag is more reliable inside
the Streamlit iframe — forceFallback's first-event handling is what
was causing the intermittent snap-back.
- Category columns now vertical-stack cards (was horizontal-flow); pairs
well with horizontal scroll of the row.
Prompt change:
- SYS_BLOCKS now requires each segment to be self-contained (start at
sentence beginning, end at sentence ending) so segments can be cut
together in any order without sounding choppy.
- Adds an explicit "semantic flow is critical" rule for
recommended_assembly — read the picks as one paragraph, check for
mid-sentence cuts, dangling pronouns, jarring topic jumps. Prefer
fewer-but-coherent over more-but-choppy.
- app.py +23 -1
- frontend/index.html +91 -196
|
@@ -263,7 +263,14 @@ B. Drop the parts that are mic checks, false starts, asides, fumbles,
|
|
| 263 |
Q&A interruptions, irrelevant tangents, or content that doesn't fit
|
| 264 |
the chosen theme.
|
| 265 |
C. The remaining keep-worthy cues form your partition — split them into
|
| 266 |
-
12-20 SEGMENTS at natural
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
D. For each segment, assign ONE narrative_role.
|
| 268 |
|
| 269 |
narrative_role enum:
|
|
@@ -281,6 +288,21 @@ that forms a complete narrative arc:
|
|
| 281 |
The recommended_assembly may pick a SUBSET of segments (segments not on the
|
| 282 |
list still appear in the user's pool — they can pick differently).
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
**IMPORTANT: Respond in Traditional Chinese (zh-TW)** for `name`,
|
| 285 |
`selling_point`. `hook_quote` is verbatim from SRT.
|
| 286 |
|
|
|
|
| 263 |
Q&A interruptions, irrelevant tangents, or content that doesn't fit
|
| 264 |
the chosen theme.
|
| 265 |
C. The remaining keep-worthy cues form your partition — split them into
|
| 266 |
+
12-20 SEGMENTS at natural SENTENCE / TOPIC boundaries. Each segment
|
| 267 |
+
must:
|
| 268 |
+
- Start at a sentence beginning (not mid-clause)
|
| 269 |
+
- End at a sentence ending (period, full pause)
|
| 270 |
+
- Be SELF-CONTAINED — the segment makes sense on its own without
|
| 271 |
+
needing the previous or next segment to resolve references
|
| 272 |
+
This is what makes adjacent segments cuttable in any order without
|
| 273 |
+
sounding choppy.
|
| 274 |
D. For each segment, assign ONE narrative_role.
|
| 275 |
|
| 276 |
narrative_role enum:
|
|
|
|
| 288 |
The recommended_assembly may pick a SUBSET of segments (segments not on the
|
| 289 |
list still appear in the user's pool — they can pick differently).
|
| 290 |
|
| 291 |
+
⚠️ SEMANTIC FLOW IS CRITICAL — the recommended_assembly will be cut
|
| 292 |
+
together back-to-back with no transitions. Read the chosen segments AS A
|
| 293 |
+
SINGLE PARAGRAPH and make sure:
|
| 294 |
+
- Adjacent segments connect smoothly: no mid-sentence cuts, no pronouns
|
| 295 |
+
referring to things only in earlier dropped segments, no jarring
|
| 296 |
+
topic jumps.
|
| 297 |
+
- Each segment ends at a natural sentence boundary so the next segment's
|
| 298 |
+
opening sentence stands on its own.
|
| 299 |
+
- The overall arc reads like the speaker said it in one breath, not
|
| 300 |
+
like a clip show.
|
| 301 |
+
- Prefer slightly longer segments over shorter ones if it preserves
|
| 302 |
+
semantic continuity.
|
| 303 |
+
If the only segments that fit the theme don't connect smoothly, it's
|
| 304 |
+
better to return fewer segments than to assemble a choppy clip.
|
| 305 |
+
|
| 306 |
**IMPORTANT: Respond in Traditional Chinese (zh-TW)** for `name`,
|
| 307 |
`selling_point`. `hook_quote` is verbatim from SRT.
|
| 308 |
|
|
@@ -4,7 +4,6 @@
|
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<title>block-sorter</title>
|
| 6 |
<style>
|
| 7 |
-
/* ---- Design tokens (Linear-inspired, monochrome with one accent) ---- */
|
| 8 |
:root {
|
| 9 |
--canvas: #f9f9fb;
|
| 10 |
--surface-1: #ffffff;
|
|
@@ -20,10 +19,7 @@
|
|
| 20 |
|
| 21 |
--accent: #5e6ad2;
|
| 22 |
--accent-hover: #828fff;
|
| 23 |
-
--accent-pressed:#5e69d1;
|
| 24 |
--accent-soft: #eef0fb;
|
| 25 |
-
--warn: #b3580d;
|
| 26 |
-
--warn-soft: #fef3e8;
|
| 27 |
|
| 28 |
--r-sm: 6px;
|
| 29 |
--r-md: 8px;
|
|
@@ -35,7 +31,6 @@
|
|
| 35 |
--s-3: 12px;
|
| 36 |
--s-4: 16px;
|
| 37 |
--s-5: 24px;
|
| 38 |
-
--s-6: 32px;
|
| 39 |
|
| 40 |
--shadow-1: 0 1px 2px rgba(15, 16, 17, 0.04), 0 0 0 1px rgba(15, 16, 17, 0.02);
|
| 41 |
--shadow-2: 0 4px 14px rgba(15, 16, 17, 0.10), 0 0 0 1px rgba(15, 16, 17, 0.04);
|
|
@@ -56,29 +51,6 @@
|
|
| 56 |
letter-spacing: -0.005em;
|
| 57 |
}
|
| 58 |
|
| 59 |
-
/* ---- Catcher (sticky drop zone for auto-route) ---- */
|
| 60 |
-
#catcher {
|
| 61 |
-
height: 36px;
|
| 62 |
-
display: flex; align-items: center; justify-content: center;
|
| 63 |
-
border: 1px dashed var(--hairline-strong);
|
| 64 |
-
border-radius: var(--r-md);
|
| 65 |
-
background: var(--surface-2);
|
| 66 |
-
color: var(--ink-subtle);
|
| 67 |
-
font-size: 12px;
|
| 68 |
-
letter-spacing: 0.01em;
|
| 69 |
-
margin: 0 0 var(--s-4);
|
| 70 |
-
position: sticky;
|
| 71 |
-
top: 0;
|
| 72 |
-
z-index: 10;
|
| 73 |
-
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
| 74 |
-
}
|
| 75 |
-
#catcher.hot {
|
| 76 |
-
background: var(--accent-soft);
|
| 77 |
-
border-color: var(--accent);
|
| 78 |
-
color: var(--accent);
|
| 79 |
-
}
|
| 80 |
-
#catcher .item { display: none; }
|
| 81 |
-
|
| 82 |
/* ---- Containers ---- */
|
| 83 |
.container {
|
| 84 |
background: var(--surface-1);
|
|
@@ -89,19 +61,21 @@
|
|
| 89 |
}
|
| 90 |
.container.assembly {
|
| 91 |
border: 1px solid var(--accent);
|
| 92 |
-
background:
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
.header {
|
| 96 |
display: flex; align-items: center; justify-content: space-between;
|
| 97 |
-
margin-bottom: var(--s-
|
| 98 |
padding: 0 var(--s-1);
|
| 99 |
-
color: var(--ink);
|
| 100 |
}
|
| 101 |
.header .title {
|
| 102 |
font-size: 13px;
|
| 103 |
font-weight: 600;
|
| 104 |
letter-spacing: -0.01em;
|
|
|
|
| 105 |
}
|
| 106 |
.header .meta {
|
| 107 |
font-size: 11px;
|
|
@@ -110,20 +84,16 @@
|
|
| 110 |
font-variant-numeric: tabular-nums;
|
| 111 |
}
|
| 112 |
|
| 113 |
-
/* ---- Items area ---- */
|
| 114 |
.items {
|
| 115 |
-
min-height:
|
| 116 |
border-radius: var(--r-md);
|
| 117 |
padding: var(--s-1);
|
| 118 |
-
transition: background 0.1s;
|
| 119 |
}
|
| 120 |
-
.items.empty {
|
| 121 |
-
|
| 122 |
-
}
|
| 123 |
-
.items.horizontal { display: flex; flex-direction: column; gap: var(--s-2); }
|
| 124 |
-
.items.vertical { display: flex; flex-direction: column; gap: var(--s-2); }
|
| 125 |
|
| 126 |
-
/*
|
| 127 |
.row {
|
| 128 |
display: flex;
|
| 129 |
flex-direction: row;
|
|
@@ -142,141 +112,116 @@
|
|
| 142 |
}
|
| 143 |
.row .container {
|
| 144 |
margin-bottom: 0;
|
| 145 |
-
flex: 0 0
|
| 146 |
-
min-width:
|
| 147 |
-
max-width:
|
| 148 |
}
|
| 149 |
|
| 150 |
-
/* ----
|
| 151 |
.item {
|
| 152 |
position: relative;
|
| 153 |
background: var(--surface-1);
|
| 154 |
border: 1px solid var(--hairline);
|
| 155 |
border-radius: var(--r-md);
|
| 156 |
-
padding: var(--s-2) var(--s-3);
|
| 157 |
cursor: grab;
|
| 158 |
user-select: none;
|
| 159 |
transition: border-color 0.1s, box-shadow 0.1s, transform 0.1s;
|
| 160 |
box-shadow: var(--shadow-1);
|
| 161 |
}
|
| 162 |
-
.container.assembly .item {
|
| 163 |
-
background: var(--surface-1);
|
| 164 |
-
}
|
| 165 |
.item:hover {
|
| 166 |
border-color: var(--hairline-strong);
|
| 167 |
box-shadow: var(--shadow-2);
|
| 168 |
}
|
| 169 |
.item:active { cursor: grabbing; }
|
| 170 |
-
.item.ghost {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
.item.drag {
|
| 172 |
cursor: grabbing;
|
| 173 |
-
box-shadow: 0
|
| 174 |
-
transform: rotate(0.
|
| 175 |
}
|
| 176 |
|
| 177 |
-
/*
|
| 178 |
-
.item .
|
| 179 |
display: flex;
|
| 180 |
align-items: center;
|
| 181 |
gap: var(--s-2);
|
| 182 |
-
|
| 183 |
}
|
| 184 |
-
.item .role
|
| 185 |
-
|
| 186 |
-
|
|
|
|
| 187 |
background: var(--surface-2);
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
flex: 0 0
|
| 193 |
}
|
| 194 |
.item .idx {
|
| 195 |
display: none;
|
| 196 |
font-size: 10px;
|
| 197 |
-
font-weight:
|
| 198 |
-
color:
|
| 199 |
-
background: var(--accent
|
| 200 |
-
padding: 2px
|
| 201 |
border-radius: var(--r-pill);
|
| 202 |
font-variant-numeric: tabular-nums;
|
| 203 |
flex: 0 0 auto;
|
| 204 |
}
|
| 205 |
.container.assembly .item .idx { display: inline-flex; }
|
| 206 |
-
.item .
|
| 207 |
-
font-size: 13px;
|
| 208 |
-
font-weight: 500;
|
| 209 |
-
color: var(--ink);
|
| 210 |
-
letter-spacing: -0.01em;
|
| 211 |
-
overflow: hidden;
|
| 212 |
-
text-overflow: ellipsis;
|
| 213 |
-
white-space: nowrap;
|
| 214 |
-
flex: 1 1 auto;
|
| 215 |
-
min-width: 0;
|
| 216 |
-
}
|
| 217 |
.item .secs {
|
| 218 |
font-size: 11px;
|
| 219 |
color: var(--ink-subtle);
|
| 220 |
font-variant-numeric: tabular-nums;
|
| 221 |
flex: 0 0 auto;
|
| 222 |
}
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
padding: 2px 4px;
|
| 229 |
-
margin: -2px -4px;
|
| 230 |
-
border-radius: var(--r-sm);
|
| 231 |
-
font-size: 10px;
|
| 232 |
-
line-height: 1;
|
| 233 |
-
transition: color 0.1s, background 0.1s;
|
| 234 |
-
flex: 0 0 auto;
|
| 235 |
-
}
|
| 236 |
-
.item .expand-btn:hover {
|
| 237 |
color: var(--ink);
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
transform: rotate(90deg);
|
| 247 |
}
|
| 248 |
|
| 249 |
-
/*
|
| 250 |
-
.item .
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
padding-top: var(--s-2);
|
| 254 |
-
border-top: 1px solid var(--hairline);
|
| 255 |
-
font-size: 12px;
|
| 256 |
line-height: 1.5;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
}
|
| 258 |
-
.item
|
| 259 |
-
.item .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
display: flex;
|
| 261 |
-
gap: var(--s-
|
| 262 |
flex-wrap: wrap;
|
| 263 |
-
color: var(--ink-subtle);
|
| 264 |
-
font-variant-numeric: tabular-nums;
|
| 265 |
-
margin-bottom: var(--s-2);
|
| 266 |
-
}
|
| 267 |
-
.item .detail .meta-row code {
|
| 268 |
-
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
| 269 |
-
font-size: 11px;
|
| 270 |
-
background: var(--surface-2);
|
| 271 |
-
padding: 1px 6px;
|
| 272 |
-
border-radius: var(--r-sm);
|
| 273 |
-
color: var(--ink-muted);
|
| 274 |
-
}
|
| 275 |
-
.item .detail .quote {
|
| 276 |
-
color: var(--ink-muted);
|
| 277 |
-
font-style: italic;
|
| 278 |
-
border-left: 2px solid var(--hairline);
|
| 279 |
-
padding-left: var(--s-2);
|
| 280 |
}
|
| 281 |
|
| 282 |
.empty-msg {
|
|
@@ -294,7 +239,6 @@
|
|
| 294 |
<script>
|
| 295 |
"use strict";
|
| 296 |
|
| 297 |
-
// ====== Streamlit bridge ======
|
| 298 |
const RENDER_EVT = "streamlit:render";
|
| 299 |
const READY_EVT = "streamlit:componentReady";
|
| 300 |
const SET_VALUE_EVT = "streamlit:setComponentValue";
|
|
@@ -321,7 +265,6 @@ function escapeHtml(s) {
|
|
| 321 |
let sortableInstances = [];
|
| 322 |
let lastEmittedFingerprint = null;
|
| 323 |
let lastRenderedFingerprint = null;
|
| 324 |
-
const expandedIds = new Set(); // persisted across rerenders
|
| 325 |
|
| 326 |
function readDomState() {
|
| 327 |
const lists = document.querySelectorAll(".items");
|
|
@@ -342,53 +285,37 @@ function emitState() {
|
|
| 342 |
}
|
| 343 |
}
|
| 344 |
|
| 345 |
-
// ====== Card rendering ======
|
| 346 |
function makeItem(item, layout) {
|
| 347 |
const el = document.createElement("div");
|
| 348 |
el.className = "item " + layout;
|
| 349 |
el.dataset.id = item.id;
|
| 350 |
if (item.role_key) el.dataset.role = item.role_key;
|
| 351 |
-
if (expandedIds.has(item.id)) el.classList.add("expanded");
|
| 352 |
|
| 353 |
-
const
|
| 354 |
const name = escapeHtml(item.name || "?");
|
| 355 |
const secs = item.secs != null ? `~${escapeHtml(item.secs)}s` : "";
|
| 356 |
const cueRange = escapeHtml(item.cue_range || "");
|
| 357 |
const srtRange = escapeHtml(item.srt_range || "");
|
| 358 |
const quoteRaw = item.quote || "";
|
| 359 |
-
const quote = escapeHtml(quoteRaw.slice(0,
|
| 360 |
-
+ (quoteRaw.length >
|
| 361 |
|
| 362 |
el.innerHTML = `
|
| 363 |
-
<div class="
|
| 364 |
-
<span class="role-dot" title="${escapeHtml(item.role || "")}">${escapeHtml(roleEmoji)}</span>
|
| 365 |
<span class="idx">#?</span>
|
| 366 |
-
<span class="
|
| 367 |
-
<span class="
|
| 368 |
-
<
|
| 369 |
</div>
|
| 370 |
-
<div class="
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
|
|
|
|
|
|
| 374 |
</div>
|
| 375 |
-
|
| 376 |
-
</div>
|
| 377 |
`;
|
| 378 |
-
|
| 379 |
-
const btn = el.querySelector(".expand-btn");
|
| 380 |
-
btn.addEventListener("click", (e) => {
|
| 381 |
-
e.stopPropagation();
|
| 382 |
-
e.preventDefault();
|
| 383 |
-
el.classList.toggle("expanded");
|
| 384 |
-
if (el.classList.contains("expanded")) expandedIds.add(item.id);
|
| 385 |
-
else expandedIds.delete(item.id);
|
| 386 |
-
setHeight();
|
| 387 |
-
});
|
| 388 |
-
// Stop drag from starting on the button.
|
| 389 |
-
btn.addEventListener("pointerdown", (e) => e.stopPropagation());
|
| 390 |
-
btn.addEventListener("mousedown", (e) => e.stopPropagation());
|
| 391 |
-
|
| 392 |
return el;
|
| 393 |
}
|
| 394 |
|
|
@@ -472,10 +399,10 @@ function setupSortables() {
|
|
| 472 |
ghostClass: "ghost",
|
| 473 |
dragClass: "drag",
|
| 474 |
draggable: ".item",
|
| 475 |
-
filter: ".empty-msg
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
onEnd: (evt) => {
|
| 480 |
if (evt.from) fixEmptyState(evt.from);
|
| 481 |
if (evt.to && evt.to !== evt.from) fixEmptyState(evt.to);
|
|
@@ -484,33 +411,6 @@ function setupSortables() {
|
|
| 484 |
},
|
| 485 |
}));
|
| 486 |
});
|
| 487 |
-
|
| 488 |
-
const catcher = document.getElementById("catcher");
|
| 489 |
-
if (catcher) {
|
| 490 |
-
sortableInstances.push(new Sortable(catcher, {
|
| 491 |
-
group: { name: "blocks", pull: false, put: true },
|
| 492 |
-
animation: 160,
|
| 493 |
-
ghostClass: "ghost",
|
| 494 |
-
draggable: ".item",
|
| 495 |
-
forceFallback: true,
|
| 496 |
-
fallbackOnBody: true,
|
| 497 |
-
onAdd: (evt) => {
|
| 498 |
-
const item = evt.item;
|
| 499 |
-
const role = item.dataset.role;
|
| 500 |
-
let target = role
|
| 501 |
-
? document.querySelector(`.items[data-role="${role}"]`)
|
| 502 |
-
: null;
|
| 503 |
-
if (!target) target = document.querySelector(".row .items");
|
| 504 |
-
if (target) {
|
| 505 |
-
target.appendChild(item);
|
| 506 |
-
fixEmptyState(target);
|
| 507 |
-
}
|
| 508 |
-
catcher.querySelectorAll(".item").forEach((el) => el.remove());
|
| 509 |
-
updateAssemblyIndices();
|
| 510 |
-
emitState();
|
| 511 |
-
},
|
| 512 |
-
}));
|
| 513 |
-
}
|
| 514 |
}
|
| 515 |
|
| 516 |
function renderUI(args) {
|
|
@@ -518,15 +418,10 @@ function renderUI(args) {
|
|
| 518 |
const root = document.getElementById("root");
|
| 519 |
root.innerHTML = "";
|
| 520 |
|
| 521 |
-
const catcher = document.createElement("div");
|
| 522 |
-
catcher.id = "catcher";
|
| 523 |
-
catcher.textContent = "拖到這裡 → 自動歸位到對應分類";
|
| 524 |
-
root.appendChild(catcher);
|
| 525 |
-
|
| 526 |
let rowDiv = null;
|
| 527 |
containers.forEach((c, idx) => {
|
| 528 |
const isAssembly = idx === 0;
|
| 529 |
-
const layout = "
|
| 530 |
const { wrapper } = makeContainer(c, layout, isAssembly);
|
| 531 |
if (isAssembly) {
|
| 532 |
root.appendChild(wrapper);
|
|
|
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<title>block-sorter</title>
|
| 6 |
<style>
|
|
|
|
| 7 |
:root {
|
| 8 |
--canvas: #f9f9fb;
|
| 9 |
--surface-1: #ffffff;
|
|
|
|
| 19 |
|
| 20 |
--accent: #5e6ad2;
|
| 21 |
--accent-hover: #828fff;
|
|
|
|
| 22 |
--accent-soft: #eef0fb;
|
|
|
|
|
|
|
| 23 |
|
| 24 |
--r-sm: 6px;
|
| 25 |
--r-md: 8px;
|
|
|
|
| 31 |
--s-3: 12px;
|
| 32 |
--s-4: 16px;
|
| 33 |
--s-5: 24px;
|
|
|
|
| 34 |
|
| 35 |
--shadow-1: 0 1px 2px rgba(15, 16, 17, 0.04), 0 0 0 1px rgba(15, 16, 17, 0.02);
|
| 36 |
--shadow-2: 0 4px 14px rgba(15, 16, 17, 0.10), 0 0 0 1px rgba(15, 16, 17, 0.04);
|
|
|
|
| 51 |
letter-spacing: -0.005em;
|
| 52 |
}
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
/* ---- Containers ---- */
|
| 55 |
.container {
|
| 56 |
background: var(--surface-1);
|
|
|
|
| 61 |
}
|
| 62 |
.container.assembly {
|
| 63 |
border: 1px solid var(--accent);
|
| 64 |
+
background:
|
| 65 |
+
linear-gradient(180deg, rgba(94,106,210,0.05), transparent 60px),
|
| 66 |
+
var(--surface-1);
|
| 67 |
}
|
| 68 |
|
| 69 |
.header {
|
| 70 |
display: flex; align-items: center; justify-content: space-between;
|
| 71 |
+
margin-bottom: var(--s-3);
|
| 72 |
padding: 0 var(--s-1);
|
|
|
|
| 73 |
}
|
| 74 |
.header .title {
|
| 75 |
font-size: 13px;
|
| 76 |
font-weight: 600;
|
| 77 |
letter-spacing: -0.01em;
|
| 78 |
+
color: var(--ink);
|
| 79 |
}
|
| 80 |
.header .meta {
|
| 81 |
font-size: 11px;
|
|
|
|
| 84 |
font-variant-numeric: tabular-nums;
|
| 85 |
}
|
| 86 |
|
|
|
|
| 87 |
.items {
|
| 88 |
+
min-height: 48px;
|
| 89 |
border-radius: var(--r-md);
|
| 90 |
padding: var(--s-1);
|
|
|
|
| 91 |
}
|
| 92 |
+
.items.empty { background: var(--surface-2); }
|
| 93 |
+
.items.horizontal,
|
| 94 |
+
.items.vertical { display: flex; flex-direction: column; gap: var(--s-2); }
|
|
|
|
|
|
|
| 95 |
|
| 96 |
+
/* Category row — horizontal scroll, fixed-width columns */
|
| 97 |
.row {
|
| 98 |
display: flex;
|
| 99 |
flex-direction: row;
|
|
|
|
| 112 |
}
|
| 113 |
.row .container {
|
| 114 |
margin-bottom: 0;
|
| 115 |
+
flex: 0 0 240px;
|
| 116 |
+
min-width: 240px;
|
| 117 |
+
max-width: 240px;
|
| 118 |
}
|
| 119 |
|
| 120 |
+
/* ---- Card ---- */
|
| 121 |
.item {
|
| 122 |
position: relative;
|
| 123 |
background: var(--surface-1);
|
| 124 |
border: 1px solid var(--hairline);
|
| 125 |
border-radius: var(--r-md);
|
| 126 |
+
padding: var(--s-2) var(--s-3) var(--s-3);
|
| 127 |
cursor: grab;
|
| 128 |
user-select: none;
|
| 129 |
transition: border-color 0.1s, box-shadow 0.1s, transform 0.1s;
|
| 130 |
box-shadow: var(--shadow-1);
|
| 131 |
}
|
|
|
|
|
|
|
|
|
|
| 132 |
.item:hover {
|
| 133 |
border-color: var(--hairline-strong);
|
| 134 |
box-shadow: var(--shadow-2);
|
| 135 |
}
|
| 136 |
.item:active { cursor: grabbing; }
|
| 137 |
+
.item.ghost {
|
| 138 |
+
opacity: 0.35;
|
| 139 |
+
background: var(--accent-soft);
|
| 140 |
+
border-style: dashed;
|
| 141 |
+
}
|
| 142 |
.item.drag {
|
| 143 |
cursor: grabbing;
|
| 144 |
+
box-shadow: 0 14px 36px rgba(15, 16, 17, 0.22);
|
| 145 |
+
transform: rotate(0.6deg);
|
| 146 |
}
|
| 147 |
|
| 148 |
+
/* Row 1: role pill + index badge + duration */
|
| 149 |
+
.item .row1 {
|
| 150 |
display: flex;
|
| 151 |
align-items: center;
|
| 152 |
gap: var(--s-2);
|
| 153 |
+
margin-bottom: 6px;
|
| 154 |
}
|
| 155 |
+
.item .role {
|
| 156 |
+
font-size: 10px;
|
| 157 |
+
font-weight: 500;
|
| 158 |
+
color: var(--ink-muted);
|
| 159 |
background: var(--surface-2);
|
| 160 |
+
padding: 2px 8px;
|
| 161 |
+
border-radius: var(--r-pill);
|
| 162 |
+
letter-spacing: 0.02em;
|
| 163 |
+
white-space: nowrap;
|
| 164 |
+
flex: 0 0 auto;
|
| 165 |
}
|
| 166 |
.item .idx {
|
| 167 |
display: none;
|
| 168 |
font-size: 10px;
|
| 169 |
+
font-weight: 700;
|
| 170 |
+
color: #fff;
|
| 171 |
+
background: var(--accent);
|
| 172 |
+
padding: 2px 7px;
|
| 173 |
border-radius: var(--r-pill);
|
| 174 |
font-variant-numeric: tabular-nums;
|
| 175 |
flex: 0 0 auto;
|
| 176 |
}
|
| 177 |
.container.assembly .item .idx { display: inline-flex; }
|
| 178 |
+
.item .spacer { flex: 1 1 auto; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
.item .secs {
|
| 180 |
font-size: 11px;
|
| 181 |
color: var(--ink-subtle);
|
| 182 |
font-variant-numeric: tabular-nums;
|
| 183 |
flex: 0 0 auto;
|
| 184 |
}
|
| 185 |
+
|
| 186 |
+
/* Row 2: name (semibold, can wrap to 2 lines) */
|
| 187 |
+
.item .name {
|
| 188 |
+
font-size: 13px;
|
| 189 |
+
font-weight: 600;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
color: var(--ink);
|
| 191 |
+
letter-spacing: -0.01em;
|
| 192 |
+
line-height: 1.35;
|
| 193 |
+
margin-bottom: 4px;
|
| 194 |
+
display: -webkit-box;
|
| 195 |
+
-webkit-line-clamp: 2;
|
| 196 |
+
-webkit-box-orient: vertical;
|
| 197 |
+
overflow: hidden;
|
| 198 |
+
word-break: break-word;
|
|
|
|
| 199 |
}
|
| 200 |
|
| 201 |
+
/* Row 3: SRT preview text — 2 lines max */
|
| 202 |
+
.item .quote {
|
| 203 |
+
font-size: 11.5px;
|
| 204 |
+
color: var(--ink-muted);
|
|
|
|
|
|
|
|
|
|
| 205 |
line-height: 1.5;
|
| 206 |
+
display: -webkit-box;
|
| 207 |
+
-webkit-line-clamp: 2;
|
| 208 |
+
-webkit-box-orient: vertical;
|
| 209 |
+
overflow: hidden;
|
| 210 |
+
word-break: break-word;
|
| 211 |
}
|
| 212 |
+
.item .quote::before { content: "「"; color: var(--ink-tertiary); }
|
| 213 |
+
.item .quote::after { content: "」"; color: var(--ink-tertiary); }
|
| 214 |
+
|
| 215 |
+
/* Row 4: meta (cue range + SRT time) — small, monospace */
|
| 216 |
+
.item .meta-line {
|
| 217 |
+
margin-top: 6px;
|
| 218 |
+
font-size: 10.5px;
|
| 219 |
+
color: var(--ink-tertiary);
|
| 220 |
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
| 221 |
+
font-variant-numeric: tabular-nums;
|
| 222 |
display: flex;
|
| 223 |
+
gap: var(--s-2);
|
| 224 |
flex-wrap: wrap;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
}
|
| 226 |
|
| 227 |
.empty-msg {
|
|
|
|
| 239 |
<script>
|
| 240 |
"use strict";
|
| 241 |
|
|
|
|
| 242 |
const RENDER_EVT = "streamlit:render";
|
| 243 |
const READY_EVT = "streamlit:componentReady";
|
| 244 |
const SET_VALUE_EVT = "streamlit:setComponentValue";
|
|
|
|
| 265 |
let sortableInstances = [];
|
| 266 |
let lastEmittedFingerprint = null;
|
| 267 |
let lastRenderedFingerprint = null;
|
|
|
|
| 268 |
|
| 269 |
function readDomState() {
|
| 270 |
const lists = document.querySelectorAll(".items");
|
|
|
|
| 285 |
}
|
| 286 |
}
|
| 287 |
|
|
|
|
| 288 |
function makeItem(item, layout) {
|
| 289 |
const el = document.createElement("div");
|
| 290 |
el.className = "item " + layout;
|
| 291 |
el.dataset.id = item.id;
|
| 292 |
if (item.role_key) el.dataset.role = item.role_key;
|
|
|
|
| 293 |
|
| 294 |
+
const roleLabel = escapeHtml((item.role || "").trim());
|
| 295 |
const name = escapeHtml(item.name || "?");
|
| 296 |
const secs = item.secs != null ? `~${escapeHtml(item.secs)}s` : "";
|
| 297 |
const cueRange = escapeHtml(item.cue_range || "");
|
| 298 |
const srtRange = escapeHtml(item.srt_range || "");
|
| 299 |
const quoteRaw = item.quote || "";
|
| 300 |
+
const quote = escapeHtml(quoteRaw.slice(0, 120))
|
| 301 |
+
+ (quoteRaw.length > 120 ? "…" : "");
|
| 302 |
|
| 303 |
el.innerHTML = `
|
| 304 |
+
<div class="row1">
|
|
|
|
| 305 |
<span class="idx">#?</span>
|
| 306 |
+
${roleLabel ? `<span class="role">${roleLabel}</span>` : ""}
|
| 307 |
+
<span class="spacer"></span>
|
| 308 |
+
${secs ? `<span class="secs">${secs}</span>` : ""}
|
| 309 |
</div>
|
| 310 |
+
<div class="name">${name}</div>
|
| 311 |
+
${quote ? `<div class="quote">${quote}</div>` : ""}
|
| 312 |
+
${(cueRange || srtRange) ? `
|
| 313 |
+
<div class="meta-line">
|
| 314 |
+
${cueRange ? `<span>cues ${cueRange}</span>` : ""}
|
| 315 |
+
${srtRange ? `<span>${srtRange}</span>` : ""}
|
| 316 |
</div>
|
| 317 |
+
` : ""}
|
|
|
|
| 318 |
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
return el;
|
| 320 |
}
|
| 321 |
|
|
|
|
| 399 |
ghostClass: "ghost",
|
| 400 |
dragClass: "drag",
|
| 401 |
draggable: ".item",
|
| 402 |
+
filter: ".empty-msg",
|
| 403 |
+
// Use native HTML5 drag — Streamlit iframe + forceFallback has
|
| 404 |
+
// proven flaky (intermittent first-drag snap-back).
|
| 405 |
+
forceFallback: false,
|
| 406 |
onEnd: (evt) => {
|
| 407 |
if (evt.from) fixEmptyState(evt.from);
|
| 408 |
if (evt.to && evt.to !== evt.from) fixEmptyState(evt.to);
|
|
|
|
| 411 |
},
|
| 412 |
}));
|
| 413 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
}
|
| 415 |
|
| 416 |
function renderUI(args) {
|
|
|
|
| 418 |
const root = document.getElementById("root");
|
| 419 |
root.innerHTML = "";
|
| 420 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
let rowDiv = null;
|
| 422 |
containers.forEach((c, idx) => {
|
| 423 |
const isAssembly = idx === 0;
|
| 424 |
+
const layout = "vertical";
|
| 425 |
const { wrapper } = makeContainer(c, layout, isAssembly);
|
| 426 |
if (isAssembly) {
|
| 427 |
root.appendChild(wrapper);
|