Commit caca3b43b683ab2960478bbf50405d60d05d2459

Authored by yibo
1 parent 939c7714

Add multi-viewer chat with nickname identity, moderation, and queue attribution

- Cookie-based viewer identity: first chat attempt prompts for a nickname
  (inline modal, not a blocking native prompt()), generates a client-side
  uid, both persisted in cookies for 1 year
- Right panel is now a live chat feed (username + message + status) with
  an input row below it, replacing the old bare prompt textarea
- New POST /api/chat: runs a moderation check (banned-word blocklist —
  ponytail: starter list, not a real content policy, swap in the real one)
  before a message is allowed into the existing prompt-matching/generation
  pipeline; rejected messages still show in the chat log, just flagged
- New GET /api/chat: recent chat history (capped at 300), polled every 3s
- Left queue simplified to prompt + submitter name only — dropped the
  resolution/duration/date metadata that was there before
- schedule/promptQueue now carry `requestedBy`/`nickname` through to
  /api/live/state so the left queue and "now playing" bar can show who
  asked for what
- /api/prompt kept working (now a thin wrapper over the same
  submitPromptToQueue() the chat endpoint uses) for direct API testing

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
library.json
1 1 [
2 2 {
  3 + "id": "af0d86637410",
  4 + "source": "h3console",
  5 + "prompt": "叫几个小猫咪和绿巨人一起跳舞",
  6 + "fullPrompt": "叫几个小猫咪和绿巨人一起跳舞",
  7 + "edited": false,
  8 + "width": 832,
  9 + "height": 480,
  10 + "duration": 5,
  11 + "partition": "text",
  12 + "likes": 0,
  13 + "createdAt": 1788239123020
  14 + },
  15 + {
3 16 "id": "aa1e9123bd35",
4 17 "source": "h3console",
5 18 "prompt": "道士打量古宅,一脸怀疑",
... ... @@ -11,7 +24,8 @@
11 24 "partition": "ref2va",
12 25 "likes": 0,
13 26 "createdAt": 1788192916199,
14   - "promptEn": "taoist eyes the old house suspiciously"
  27 + "promptEn": "taoist eyes the old house suspiciously",
  28 + "lastPlayedAt": 1788238919277
15 29 },
16 30 {
17 31 "id": "cb5baad5d724",
... ... @@ -26,7 +40,7 @@
26 40 "likes": 0,
27 41 "createdAt": 1788192915845,
28 42 "promptEn": "scholar watches the room warily",
29   - "lastPlayedAt": 1788231313021
  43 + "lastPlayedAt": 1788239169402
30 44 },
31 45 {
32 46 "id": "e8cbb73560ef",
... ... @@ -41,7 +55,7 @@
41 55 "likes": 0,
42 56 "createdAt": 1788192915549,
43 57 "promptEn": "taoist steels himself to exorcise something",
44   - "lastPlayedAt": 1788232627825
  58 + "lastPlayedAt": 1788239125383
45 59 },
46 60 {
47 61 "id": "5b6dcb357792",
... ... @@ -70,7 +84,7 @@
70 84 "likes": 0,
71 85 "createdAt": 1788192914949,
72 86 "promptEn": "taoist acts tough but is nervous",
73   - "lastPlayedAt": 1788232638595
  87 + "lastPlayedAt": 1788239063344
74 88 },
75 89 {
76 90 "id": "167ce1bfa285",
... ... @@ -85,7 +99,7 @@
85 99 "likes": 0,
86 100 "createdAt": 1788192914047,
87 101 "promptEn": "scholar looks scared, wants to flee",
88   - "lastPlayedAt": 1788233032856
  102 + "lastPlayedAt": 1788239189657
89 103 },
90 104 {
91 105 "id": "053bc957b4f8",
... ... @@ -99,7 +113,8 @@
99 113 "partition": "ref2va",
100 114 "likes": 0,
101 115 "createdAt": 1788192913886,
102   - "promptEn": "taoist panics, freaked out"
  116 + "promptEn": "taoist panics, freaked out",
  117 + "lastPlayedAt": 1788237354074
103 118 },
104 119 {
105 120 "id": "45c1136d1e05",
... ... @@ -113,7 +128,8 @@
113 128 "partition": "ref2va",
114 129 "likes": 0,
115 130 "createdAt": 1788192913551,
116   - "promptEn": "taoist looking panicked and scared"
  131 + "promptEn": "taoist looking panicked and scared",
  132 + "lastPlayedAt": 1788238667095
117 133 },
118 134 {
119 135 "id": "d169d42a67de",
... ... @@ -127,7 +143,8 @@
127 143 "partition": "ref2va",
128 144 "likes": 0,
129 145 "createdAt": 1788192913492,
130   - "promptEn": "scholar defiant out of desperation"
  146 + "promptEn": "scholar defiant out of desperation",
  147 + "lastPlayedAt": 1788237894431
131 148 },
132 149 {
133 150 "id": "2abd2793fae9",
... ... @@ -142,7 +159,7 @@
142 159 "likes": 0,
143 160 "createdAt": 1788192913244,
144 161 "promptEn": "scholar cornered but still defiant",
145   - "lastPlayedAt": 1788233292965
  162 + "lastPlayedAt": 1788239435546
146 163 },
147 164 {
148 165 "id": "bbf7efb467e0",
... ... @@ -157,7 +174,7 @@
157 174 "likes": 0,
158 175 "createdAt": 1788192912912,
159 176 "promptEn": "taoist charmed, hesitating",
160   - "lastPlayedAt": 1788233054864
  177 + "lastPlayedAt": 1788239405537
161 178 },
162 179 {
163 180 "id": "f8da9934c823",
... ... @@ -172,7 +189,7 @@
172 189 "likes": 0,
173 190 "createdAt": 1788192912819,
174 191 "promptEn": "scholar dazed, charmed",
175   - "lastPlayedAt": 1788231297013
  192 + "lastPlayedAt": 1788239529590
176 193 },
177 194 {
178 195 "id": "feb96d5be50f",
... ... @@ -187,7 +204,7 @@
187 204 "likes": 0,
188 205 "createdAt": 1788191432903,
189 206 "promptEn": "woman in white startled, a bit shy",
190   - "lastPlayedAt": 1788232706663
  207 + "lastPlayedAt": 1788235312284
191 208 },
192 209 {
193 210 "id": "14f482bdae7f",
... ... @@ -202,7 +219,7 @@
202 219 "likes": 0,
203 220 "createdAt": 1788191432362,
204 221 "promptEn": "woman in white being intimate and alluring",
205   - "lastPlayedAt": 1788233385001
  222 + "lastPlayedAt": 1788239853614
206 223 },
207 224 {
208 225 "id": "362b15ab1811",
... ... @@ -216,7 +233,8 @@
216 233 "partition": "ref2va",
217 234 "likes": 0,
218 235 "createdAt": 1788191431874,
219   - "promptEn": "woman in white gently comforting someone"
  236 + "promptEn": "woman in white gently comforting someone",
  237 + "lastPlayedAt": 1788239698615
220 238 },
221 239 {
222 240 "id": "a2c515055269",
... ... @@ -231,7 +249,7 @@
231 249 "likes": 0,
232 250 "createdAt": 1788191431505,
233 251 "promptEn": "woman in white scared, dodging the question",
234   - "lastPlayedAt": 1788233203825
  252 + "lastPlayedAt": 1788237426143
235 253 },
236 254 {
237 255 "id": "667978d52305",
... ... @@ -246,7 +264,7 @@
246 264 "likes": 0,
247 265 "createdAt": 1788191431016,
248 266 "promptEn": "woman in white coldly threatening",
249   - "lastPlayedAt": 1788232575821
  267 + "lastPlayedAt": 1788237922460
250 268 },
251 269 {
252 270 "id": "7f5213748d87",
... ... @@ -261,7 +279,7 @@
261 279 "likes": 0,
262 280 "createdAt": 1788188423583,
263 281 "promptEn": "woman in white scared, dodging",
264   - "lastPlayedAt": 1788232370410
  282 + "lastPlayedAt": 1788239361511
265 283 },
266 284 {
267 285 "id": "1597db4be561",
... ... @@ -275,7 +293,7 @@
275 293 "partition": "ref2va",
276 294 "likes": 0,
277 295 "createdAt": 1788187365789,
278   - "lastPlayedAt": 1788233018847,
  296 + "lastPlayedAt": 1788236567379,
279 297 "promptEn": "woman in white being gentle and comforting"
280 298 },
281 299 {
... ... @@ -290,7 +308,7 @@
290 308 "partition": "ref2va",
291 309 "likes": 0,
292 310 "createdAt": 1788186218619,
293   - "lastPlayedAt": 1788232390420,
  311 + "lastPlayedAt": 1788239105368,
294 312 "promptEn": "woman in white evading, scared"
295 313 },
296 314 {
... ... @@ -306,7 +324,7 @@
306 324 "likes": 0,
307 325 "createdAt": 1788185090875,
308 326 "promptEn": "woman in white cold and threatening",
309   - "lastPlayedAt": 1788232355821
  327 + "lastPlayedAt": 1788236497319
310 328 },
311 329 {
312 330 "id": "a46f1daa9ee0",
... ... @@ -321,7 +339,7 @@
321 339 "likes": 0,
322 340 "createdAt": 1788181569491,
323 341 "promptEn": "a massive warship glides through the stars, the bridge suddenly shakes hard",
324   - "lastPlayedAt": 1788233312967
  342 + "lastPlayedAt": 1788239135388
325 343 },
326 344 {
327 345 "id": "44bf5b969e94",
... ... @@ -336,7 +354,7 @@
336 354 "likes": 0,
337 355 "createdAt": 1788162094002,
338 356 "promptEn": "woman in old chinese robe talking, gentle but wary, in an old room",
339   - "lastPlayedAt": 1788231002876
  357 + "lastPlayedAt": 1788238847245
340 358 },
341 359 {
342 360 "id": "e17cfe57f56a",
... ... @@ -351,7 +369,7 @@
351 369 "likes": 0,
352 370 "createdAt": 1788160917846,
353 371 "promptEn": "woman in old chinese robe talking, gentle but wary, in an old room",
354   - "lastPlayedAt": 1788232031703
  372 + "lastPlayedAt": 1788234265405
355 373 },
356 374 {
357 375 "id": "24e49e5ae069",
... ... @@ -381,7 +399,7 @@
381 399 "likes": 0,
382 400 "createdAt": 1788157322611,
383 401 "promptEn": "constable talking, looking scared",
384   - "lastPlayedAt": 1788230591198
  402 + "lastPlayedAt": 1788239195414
385 403 },
386 404 {
387 405 "id": "7977eeeabe8b",
... ... @@ -396,7 +414,7 @@
396 414 "likes": 0,
397 415 "createdAt": 1788157177505,
398 416 "promptEn": "constable bursts in and blocks the door, talks to a pale woman behind the screen",
399   - "lastPlayedAt": 1788232081727
  417 + "lastPlayedAt": 1788236561376
400 418 },
401 419 {
402 420 "id": "a9c4ba5e453b",
... ... @@ -411,7 +429,7 @@
411 429 "likes": 0,
412 430 "createdAt": 1788156878022,
413 431 "promptEn": "constable bursts in and blocks the door, talks to a pale woman behind the screen",
414   - "lastPlayedAt": 1788233751168
  432 + "lastPlayedAt": 1788238326802
415 433 },
416 434 {
417 435 "id": "a0b752e7ac0d",
... ... @@ -426,7 +444,7 @@
426 444 "likes": 0,
427 445 "createdAt": 1788154942596,
428 446 "promptEn": "constable talking, looking scared",
429   - "lastPlayedAt": 1788231156952
  447 + "lastPlayedAt": 1788239059343
430 448 },
431 449 {
432 450 "id": "b6608e2eac60",
... ... @@ -441,7 +459,7 @@
441 459 "likes": 0,
442 460 "createdAt": 1788153988474,
443 461 "promptEn": "astronaut at a martian canyon, visor reflecting alien ruins, then aerial view of the city",
444   - "lastPlayedAt": 1788232531824
  462 + "lastPlayedAt": 1788238420888
445 463 },
446 464 {
447 465 "id": "194fc2b56cb5",
... ... @@ -456,7 +474,7 @@
456 474 "likes": 0,
457 475 "createdAt": 1788138492364,
458 476 "promptEn": "astronaut on a Martian canyon, helmet reflecting an alien ruined city",
459   - "lastPlayedAt": 1788232051710
  477 + "lastPlayedAt": 1788239857611
460 478 },
461 479 {
462 480 "id": "f84b8f044ec5",
... ... @@ -486,7 +504,7 @@
486 504 "likes": 0,
487 505 "createdAt": 1788120171608,
488 506 "promptEn": "constable bursts in and blocks the door, talks to a pale woman behind the screen",
489   - "lastPlayedAt": 1788232690632
  507 + "lastPlayedAt": 1788239624540
490 508 },
491 509 {
492 510 "id": "b8c762a3672e",
... ... @@ -501,7 +519,7 @@
501 519 "likes": 0,
502 520 "createdAt": 1788106707982,
503 521 "promptEn": "constable bursts in and blocks the door, talks to a pale woman behind the screen",
504   - "lastPlayedAt": 1788230489157
  522 + "lastPlayedAt": 1788235032035
505 523 },
506 524 {
507 525 "id": "1fa40bca47f5",
... ... @@ -516,7 +534,7 @@
516 534 "likes": 0,
517 535 "createdAt": 1788085282201,
518 536 "promptEn": "constable stands still, quietly says he won't make a sound, woman stays silent behind the screen",
519   - "lastPlayedAt": 1788230988868
  537 + "lastPlayedAt": 1788238925278
520 538 },
521 539 {
522 540 "id": "683ab2fd4681",
... ... @@ -531,7 +549,7 @@
531 549 "likes": 0,
532 550 "createdAt": 1788071067702,
533 551 "promptEn": "constable stands still, quietly says he won't make a sound, woman stays silent behind the screen",
534   - "lastPlayedAt": 1788231799507
  552 + "lastPlayedAt": 1788239269456
535 553 },
536 554 {
537 555 "id": "2e09fcd3d42c",
... ... @@ -546,7 +564,7 @@
546 564 "likes": 0,
547 565 "createdAt": 1788062322252,
548 566 "promptEn": "constable stands still, quietly says he won't make a sound, woman stays silent behind the screen",
549   - "lastPlayedAt": 1788233302967
  567 + "lastPlayedAt": 1788237998513
550 568 },
551 569 {
552 570 "id": "12ea9ce00f36",
... ... @@ -561,7 +579,7 @@
561 579 "likes": 0,
562 580 "createdAt": 1788039783070,
563 581 "promptEn": "silver-haired idol backstage, red light pulsing, offering a blood-red contract",
564   - "lastPlayedAt": 1788229466221
  582 + "lastPlayedAt": 1788236086962
565 583 },
566 584 {
567 585 "id": "75bdbacd3012",
... ... @@ -576,7 +594,7 @@
576 594 "likes": 0,
577 595 "createdAt": 1788039782992,
578 596 "promptEn": "silver-haired idol backstage, red light pulsing, offering a blood-red contract",
579   - "lastPlayedAt": 1788230543177
  597 + "lastPlayedAt": 1788238901269
580 598 },
581 599 {
582 600 "id": "69e75a6b3997",
... ... @@ -591,7 +609,7 @@
591 609 "likes": 0,
592 610 "createdAt": 1788027833836,
593 611 "promptEn": "wounded hunter at a ruined subway dungeon entrance, lightning flickers, pointing into the tunnel",
594   - "lastPlayedAt": 1788232085728
  612 + "lastPlayedAt": 1788238448910
595 613 },
596 614 {
597 615 "id": "cde399c27dc4",
... ... @@ -606,7 +624,7 @@
606 624 "likes": 0,
607 625 "createdAt": 1788026877547,
608 626 "promptEn": "a deity in gold crown and red veil, quietly judging you in a moonlit bridal chamber",
609   - "lastPlayedAt": 1788229302095
  627 + "lastPlayedAt": 1788239662572
610 628 },
611 629 {
612 630 "id": "d5f811737e95",
... ... @@ -621,7 +639,7 @@
621 639 "likes": 0,
622 640 "createdAt": 1788026876886,
623 641 "promptEn": "apartment manager in an eerie corridor, red elevator light flickering, holding out a key",
624   - "lastPlayedAt": 1788232386408
  642 + "lastPlayedAt": 1788239031335
625 643 },
626 644 {
627 645 "id": "cdd03cca8461",
... ... @@ -636,7 +654,7 @@
636 654 "likes": 0,
637 655 "createdAt": 1788026569422,
638 656 "promptEn": "stitched-face long-haired curse on a subway platform, flickering lights, cruel smile",
639   - "lastPlayedAt": 1788233199827
  657 + "lastPlayedAt": 1788239131385
640 658 },
641 659 {
642 660 "id": "b8dd0f62f648",
... ... @@ -651,7 +669,7 @@
651 669 "likes": 0,
652 670 "createdAt": 1788026569123,
653 671 "promptEn": "pale dream demon in a suit, reaching out a hand inside a dream train car",
654   - "lastPlayedAt": 1788232663823
  672 + "lastPlayedAt": 1788237442156
655 673 },
656 674 {
657 675 "id": "bd6e2b264243",
... ... @@ -666,7 +684,7 @@
666 684 "likes": 0,
667 685 "createdAt": 1788026568815,
668 686 "promptEn": "blond soldier in combat gear, bracing at the entrance of a ruined basement battlefield",
669   - "lastPlayedAt": 1788233058865
  687 + "lastPlayedAt": 1788237670284
670 688 },
671 689 {
672 690 "id": "b0d6af33eace",
... ... @@ -681,7 +699,7 @@
681 699 "likes": 0,
682 700 "createdAt": 1788026568516,
683 701 "promptEn": "orange-haired villain in a black cloak with red clouds, raising a hand in a ruined village",
684   - "lastPlayedAt": 1788233126898
  702 + "lastPlayedAt": 1788239243436
685 703 },
686 704 {
687 705 "id": "b077fd090415",
... ... @@ -696,7 +714,7 @@
696 714 "likes": 0,
697 715 "createdAt": 1788026568242,
698 716 "promptEn": "magma commander in a white coat, raising a lava fist on the battlefield",
699   - "lastPlayedAt": 1788232916802
  717 + "lastPlayedAt": 1788239786023
700 718 },
701 719 {
702 720 "id": "1442013e94f0",
... ... @@ -711,7 +729,7 @@
711 729 "likes": 0,
712 730 "createdAt": 1788026068497,
713 731 "promptEn": "black-haired prince in a red cloak, pressing down an execution order in the trial hall",
714   - "lastPlayedAt": 1788232404430
  732 + "lastPlayedAt": 1788235548506
715 733 },
716 734 {
717 735 "id": "0a5e37bb328c",
... ... @@ -726,7 +744,7 @@
726 744 "likes": 0,
727 745 "createdAt": 1788025642732,
728 746 "promptEn": "Vecna reaching out a claw in his lair in the Upside Down, red lightning",
729   - "lastPlayedAt": 1788232907826
  747 + "lastPlayedAt": 1788236459278
730 748 },
731 749 {
732 750 "id": "00862f966c3f",
... ... @@ -741,7 +759,7 @@
741 759 "likes": 0,
742 760 "createdAt": 1788025640972,
743 761 "promptEn": "Voldemort in a graveyard, wand raised, stepping closer",
744   - "lastPlayedAt": 1788232551825
  762 + "lastPlayedAt": 1788233989281
745 763 },
746 764 {
747 765 "id": "c3875fbd6cc4",
... ... @@ -756,7 +774,7 @@
756 774 "likes": 0,
757 775 "createdAt": 1788025632028,
758 776 "promptEn": "a giant mouth embedded in the wall at the end of an endless hotel corridor, slowly opening",
759   - "lastPlayedAt": 1788231351046
  777 + "lastPlayedAt": 1788239439551
760 778 },
761 779 {
762 780 "id": "1c6e2df243f4",
... ... @@ -771,7 +789,7 @@
771 789 "likes": 0,
772 790 "createdAt": 1788025630249,
773 791 "promptEn": "white-haired villain in a ruined street at night, cracked smile, reaching out a hand",
774   - "lastPlayedAt": 1788229732479
  792 + "lastPlayedAt": 1788237604251
775 793 },
776 794 {
777 795 "id": "d735f1fc010f",
... ... @@ -786,7 +804,7 @@
786 804 "likes": 0,
787 805 "createdAt": 1788025624377,
788 806 "promptEn": "cat-eared white-haired guard crouching in a ruined palace, purple aura around them",
789   - "lastPlayedAt": 1788232950817
  807 + "lastPlayedAt": 1788239069350
790 808 },
791 809 {
792 810 "id": "b16b64e52f83",
... ... @@ -801,7 +819,7 @@
801 819 "likes": 0,
802 820 "createdAt": 1788025623833,
803 821 "promptEn": "cat-eared white-haired guard crouching in a ruined palace, purple aura around them",
804   - "lastPlayedAt": 1788231919621
  822 + "lastPlayedAt": 1788237726312
805 823 },
806 824 {
807 825 "id": "0d721acbc298",
... ... @@ -816,7 +834,7 @@
816 834 "likes": 0,
817 835 "createdAt": 1788025622363,
818 836 "promptEn": "detective in a surveillance room squinting at the monitors",
819   - "lastPlayedAt": 1788228717592
  837 + "lastPlayedAt": 1788237418135
820 838 },
821 839 {
822 840 "id": "639069dea7c5",
... ... @@ -831,7 +849,7 @@
831 849 "likes": 0,
832 850 "createdAt": 1788025620717,
833 851 "promptEn": "alien tyrant holding a glowing energy sphere, sneering on a rocky green battlefield",
834   - "lastPlayedAt": 1788230286981
  852 + "lastPlayedAt": 1788238961298
835 853 },
836 854 {
837 855 "id": "9db41d807330",
... ... @@ -846,7 +864,7 @@
846 864 "likes": 0,
847 865 "createdAt": 1788025220954,
848 866 "promptEn": "pirate captain plants a cutlass on the table, making a dangerous bargain",
849   - "lastPlayedAt": 1788232503823
  867 + "lastPlayedAt": 1788239620537
850 868 },
851 869 {
852 870 "id": "3e03975ff40a",
... ... @@ -861,7 +879,7 @@
861 879 "likes": 0,
862 880 "createdAt": 1788025003469,
863 881 "promptEn": "vampire idol with red eyes, offering a blood-red contract backstage",
864   - "lastPlayedAt": 1788230557183
  882 + "lastPlayedAt": 1788238038549
865 883 },
866 884 {
867 885 "id": "2857aacf11bb",
... ... @@ -876,7 +894,7 @@
876 894 "likes": 0,
877 895 "createdAt": 1788024900966,
878 896 "promptEn": "player 101 blocking the path on a high glass bridge, threatening you",
879   - "lastPlayedAt": 1788229306100
  897 + "lastPlayedAt": 1788238777192
880 898 },
881 899 {
882 900 "id": "d6aab3fce63d",
... ... @@ -891,7 +909,7 @@
891 909 "likes": 0,
892 910 "createdAt": 1788024747209,
893 911 "promptEn": "wounded hunter leaning on a subway turnstile, ordering you into the tunnel",
894   - "lastPlayedAt": 1788229298090
  912 + "lastPlayedAt": 1788239325490
895 913 },
896 914 {
897 915 "id": "fdefca4e4980",
... ... @@ -906,7 +924,7 @@
906 924 "likes": 0,
907 925 "createdAt": 1788024491625,
908 926 "promptEn": "prince signs an execution order in the trial hall, points at the guillotine",
909   - "lastPlayedAt": 1788230830800
  927 + "lastPlayedAt": 1788239261453
910 928 },
911 929 {
912 930 "id": "8a933feb52c4",
... ... @@ -921,7 +939,7 @@
921 939 "likes": 0,
922 940 "createdAt": 1788019483999,
923 941 "promptEn": "constable rushes in, glances back at the door, then talks to the woman behind the screen",
924   - "lastPlayedAt": 1788233350990
  942 + "lastPlayedAt": 1788238909272
925 943 },
926 944 {
927 945 "id": "998e3e6f9a2f",
... ... @@ -936,7 +954,7 @@
936 954 "likes": 0,
937 955 "createdAt": 1788016668421,
938 956 "promptEn": "pirate negotiation, catching Barbossa's flaw and turning the tables",
939   - "lastPlayedAt": 1788233635107
  957 + "lastPlayedAt": 1788238619060
940 958 },
941 959 {
942 960 "id": "e3b18b5dfd8e",
... ... @@ -951,7 +969,7 @@
951 969 "likes": 0,
952 970 "createdAt": 1788016434846,
953 971 "promptEn": "pirate negotiation, catching Barbossa's flaw and turning the tables",
954   - "lastPlayedAt": 1788230098805
  972 + "lastPlayedAt": 1788237756332
955 973 },
956 974 {
957 975 "id": "03e32ff5f0ef",
... ... @@ -966,7 +984,7 @@
966 984 "likes": 0,
967 985 "createdAt": 1788016203093,
968 986 "promptEn": "pirate negotiation, held the goal but paid a price",
969   - "lastPlayedAt": 1788230908828
  987 + "lastPlayedAt": 1788236333170
970 988 },
971 989 {
972 990 "id": "18aa882cbd0b",
... ... @@ -981,7 +999,7 @@
981 999 "likes": 0,
982 1000 "createdAt": 1788015968838,
983 1001 "promptEn": "pirate negotiation, held the goal but paid a price",
984   - "lastPlayedAt": 1788233671127
  1002 + "lastPlayedAt": 1788238100608
985 1003 },
986 1004 {
987 1005 "id": "b328649000d1",
... ... @@ -996,7 +1014,7 @@
996 1014 "likes": 0,
997 1015 "createdAt": 1788015737873,
998 1016 "promptEn": "pirate negotiation, went in head-on and Barbossa saw it coming",
999   - "lastPlayedAt": 1788231967667
  1017 + "lastPlayedAt": 1788237117870
1000 1018 },
1001 1019 {
1002 1020 "id": "2a5ee87b8f13",
... ... @@ -1011,7 +1029,7 @@
1011 1029 "likes": 0,
1012 1030 "createdAt": 1788015493445,
1013 1031 "promptEn": "pirate negotiation, went in head-on and Barbossa saw it coming",
1014   - "lastPlayedAt": 1788233308972
  1032 + "lastPlayedAt": 1788237736321
1015 1033 },
1016 1034 {
1017 1035 "id": "3f30f5097438",
... ... @@ -1026,7 +1044,7 @@
1026 1044 "likes": 0,
1027 1045 "createdAt": 1788015035234,
1028 1046 "promptEn": "pirate negotiation, stayed passive and got cornered",
1029   - "lastPlayedAt": 1788229614350
  1047 + "lastPlayedAt": 1788238905268
1030 1048 },
1031 1049 {
1032 1050 "id": "6a0d14cabe17",
... ... @@ -1041,7 +1059,7 @@
1041 1059 "likes": 0,
1042 1060 "createdAt": 1788014791733,
1043 1061 "promptEn": "pirate negotiation, stayed passive and got cornered",
1044   - "lastPlayedAt": 1788229432201
  1062 + "lastPlayedAt": 1788239570515
1045 1063 },
1046 1064 {
1047 1065 "id": "08120928cc77",
... ... @@ -1056,7 +1074,7 @@
1056 1074 "likes": 0,
1057 1075 "createdAt": 1788014790969,
1058 1076 "promptEn": "pirate negotiation, stayed passive and got cornered",
1059   - "lastPlayedAt": 1788232946815
  1077 + "lastPlayedAt": 1788239443550
1060 1078 },
1061 1079 {
1062 1080 "id": "e14fe7c3220e",
... ... @@ -1071,7 +1089,7 @@
1071 1089 "likes": 0,
1072 1090 "createdAt": 1788014563344,
1073 1091 "promptEn": "captain's cabin on the Black Pearl, surrounded by pirates, negotiation could turn into an execution",
1074   - "lastPlayedAt": 1788231511189
  1092 + "lastPlayedAt": 1788238589036
1075 1093 },
1076 1094 {
1077 1095 "id": "c6e8560f62a2",
... ... @@ -1086,7 +1104,7 @@
1086 1104 "likes": 0,
1087 1105 "createdAt": 1788014336515,
1088 1106 "promptEn": "captain's cabin on the Black Pearl, surrounded by pirates, negotiation could turn into an execution",
1089   - "lastPlayedAt": 1788232214325
  1107 + "lastPlayedAt": 1788235820732
1090 1108 },
1091 1109 {
1092 1110 "id": "eae105ee1204",
... ... @@ -1101,7 +1119,7 @@
1101 1119 "likes": 0,
1102 1120 "createdAt": 1788014099497,
1103 1121 "promptEn": "glass bridge game, exploiting a flaw to push back player 101",
1104   - "lastPlayedAt": 1788230116823
  1122 + "lastPlayedAt": 1788237704301
1105 1123 },
1106 1124 {
1107 1125 "id": "24576c264a25",
... ... @@ -1116,7 +1134,7 @@
1116 1134 "likes": 0,
1117 1135 "createdAt": 1788013870849,
1118 1136 "promptEn": "glass bridge game, exploiting a flaw to push back player 101",
1119   - "lastPlayedAt": 1788229452214
  1137 + "lastPlayedAt": 1788237432150
1120 1138 },
1121 1139 {
1122 1140 "id": "dd12bbb55060",
... ... @@ -1131,7 +1149,7 @@
1131 1149 "likes": 0,
1132 1150 "createdAt": 1788013634333,
1133 1151 "promptEn": "glass bridge game, held the goal but paid a price",
1134   - "lastPlayedAt": 1788232940812
  1152 + "lastPlayedAt": 1788238096601
1135 1153 },
1136 1154 {
1137 1155 "id": "5b8ef21616d4",
... ... @@ -1146,7 +1164,7 @@
1146 1164 "likes": 0,
1147 1165 "createdAt": 1788013632065,
1148 1166 "promptEn": "glass bridge game, held the goal but paid a price",
1149   - "lastPlayedAt": 1788232107744
  1167 + "lastPlayedAt": 1788238787200
1150 1168 },
1151 1169 {
1152 1170 "id": "892b923eed29",
... ... @@ -1161,7 +1179,7 @@
1161 1179 "likes": 0,
1162 1180 "createdAt": 1788013393463,
1163 1181 "promptEn": "guessed wrong in a survival game, got countered",
1164   - "lastPlayedAt": 1788233653115
  1182 + "lastPlayedAt": 1788239548495
1165 1183 },
1166 1184 {
1167 1185 "id": "7880a91928c6",
... ... @@ -1176,7 +1194,7 @@
1176 1194 "likes": 0,
1177 1195 "createdAt": 1788013155592,
1178 1196 "promptEn": "glass bridge game, stayed passive and got cornered",
1179   - "lastPlayedAt": 1788230447128
  1197 + "lastPlayedAt": 1788237956491
1180 1198 },
1181 1199 {
1182 1200 "id": "f80c8ec1a8c8",
... ... @@ -1191,7 +1209,7 @@
1191 1209 "likes": 0,
1192 1210 "createdAt": 1788012917294,
1193 1211 "promptEn": "standing on the glass bridge, facing off with player 101",
1194   - "lastPlayedAt": 1788233130898
  1212 + "lastPlayedAt": 1788237284002
1195 1213 },
1196 1214 {
1197 1215 "id": "8f23de61904f",
... ... @@ -1206,7 +1224,7 @@
1206 1224 "likes": 0,
1207 1225 "createdAt": 1788012917185,
1208 1226 "promptEn": "standing on the glass bridge, facing off with player 101",
1209   - "lastPlayedAt": 1788233296966
  1227 + "lastPlayedAt": 1788239642565
1210 1228 },
1211 1229 {
1212 1230 "id": "bb4a8351d822",
... ... @@ -1221,7 +1239,7 @@
1221 1239 "likes": 0,
1222 1240 "createdAt": 1788012679740,
1223 1241 "promptEn": "in the mental lair, exploiting a flaw to counter Vecna",
1224   - "lastPlayedAt": 1788229618353
  1242 + "lastPlayedAt": 1788236895678
1225 1243 },
1226 1244 {
1227 1245 "id": "3913752bbe3a",
... ... @@ -1236,7 +1254,7 @@
1236 1254 "likes": 0,
1237 1255 "createdAt": 1788012441458,
1238 1256 "promptEn": "facing Vecna, held the goal but paid a price",
1239   - "lastPlayedAt": 1788232483825
  1257 + "lastPlayedAt": 1788236173036
1240 1258 },
1241 1259 {
1242 1260 "id": "691c2f679759",
... ... @@ -1251,7 +1269,7 @@
1251 1269 "likes": 0,
1252 1270 "createdAt": 1788012202802,
1253 1271 "promptEn": "facing Vecna, went in head-on and got seen through",
1254   - "lastPlayedAt": 1788232320378
  1272 + "lastPlayedAt": 1788235248233
1255 1273 },
1256 1274 {
1257 1275 "id": "07f957783a09",
... ... @@ -1266,7 +1284,7 @@
1266 1284 "likes": 0,
1267 1285 "createdAt": 1788012202708,
1268 1286 "promptEn": "facing Vecna, went in head-on and got seen through",
1269   - "lastPlayedAt": 1788232140287
  1287 + "lastPlayedAt": 1788239507583
1270 1288 },
1271 1289 {
1272 1290 "id": "40ccb04f76e9",
... ... @@ -1281,7 +1299,7 @@
1281 1299 "likes": 0,
1282 1300 "createdAt": 1788011964828,
1283 1301 "promptEn": "facing Vecna, stayed passive and got cornered",
1284   - "lastPlayedAt": 1788232971826
  1302 + "lastPlayedAt": 1788235900804
1285 1303 },
1286 1304 {
1287 1305 "id": "7161aecafa32",
... ... @@ -1296,7 +1314,7 @@
1296 1314 "likes": 0,
1297 1315 "createdAt": 1788011727444,
1298 1316 "promptEn": "entering Vecna's mental lair, red lightning, twisted space",
1299   - "lastPlayedAt": 1788232338391
  1317 + "lastPlayedAt": 1788237488190
1300 1318 },
1301 1319 {
1302 1320 "id": "6a9af7e7368d",
... ... @@ -1311,7 +1329,7 @@
1311 1329 "likes": 0,
1312 1330 "createdAt": 1788011488775,
1313 1331 "promptEn": "graveyard standoff, exploiting a flaw to counter Voldemort",
1314   - "lastPlayedAt": 1788233272950
  1332 + "lastPlayedAt": 1788237678291
1315 1333 },
1316 1334 {
1317 1335 "id": "24845be71ea0",
... ... @@ -1326,7 +1344,7 @@
1326 1344 "likes": 0,
1327 1345 "createdAt": 1788011488667,
1328 1346 "promptEn": "graveyard standoff, exploiting a flaw to counter Voldemort",
1329   - "lastPlayedAt": 1788231671384
  1347 + "lastPlayedAt": 1788239425542
1330 1348 },
1331 1349 {
1332 1350 "id": "f1012a3d7aa7",
... ... @@ -1341,7 +1359,7 @@
1341 1359 "likes": 0,
1342 1360 "createdAt": 1788011251158,
1343 1361 "promptEn": "facing Voldemort, held the goal but paid a price",
1344   - "lastPlayedAt": 1788231773483
  1362 + "lastPlayedAt": 1788239329493
1345 1363 },
1346 1364 {
1347 1365 "id": "58f68b111429",
... ... @@ -1356,7 +1374,7 @@
1356 1374 "likes": 0,
1357 1375 "createdAt": 1788011013309,
1358 1376 "promptEn": "facing Voldemort, went in head-on and got seen through",
1359   - "lastPlayedAt": 1788231341043
  1377 + "lastPlayedAt": 1788234335440
1360 1378 },
1361 1379 {
1362 1380 "id": "682cbc166479",
... ... @@ -1371,7 +1389,7 @@
1371 1389 "likes": 0,
1372 1390 "createdAt": 1788010775613,
1373 1391 "promptEn": "facing Voldemort, stayed passive and got cornered",
1374   - "lastPlayedAt": 1788233344985
  1392 + "lastPlayedAt": 1788239736647
1375 1393 },
1376 1394 {
1377 1395 "id": "795842efa4a1",
... ... @@ -1386,7 +1404,7 @@
1386 1404 "likes": 0,
1387 1405 "createdAt": 1788010537514,
1388 1406 "promptEn": "the triwizard cup teleports you to the graveyard, Voldemort's revival just finished",
1389   - "lastPlayedAt": 1788233095826
  1407 + "lastPlayedAt": 1788238659090
1390 1408 },
1391 1409 {
1392 1410 "id": "6ed9eaf4fa1d",
... ... @@ -1401,7 +1419,7 @@
1401 1419 "likes": 0,
1402 1420 "createdAt": 1788010299688,
1403 1421 "promptEn": "facing Thanos, exploiting a flaw to counter him",
1404   - "lastPlayedAt": 1788232151824
  1422 + "lastPlayedAt": 1788237710304
1405 1423 },
1406 1424 {
1407 1425 "id": "469294a4ed09",
... ... @@ -1416,7 +1434,7 @@
1416 1434 "likes": 0,
1417 1435 "createdAt": 1788010062535,
1418 1436 "promptEn": "facing Thanos, held the goal but paid a price",
1419   - "lastPlayedAt": 1788233166915
  1437 + "lastPlayedAt": 1788235376337
1420 1438 },
1421 1439 {
1422 1440 "id": "324b84ebc607",
... ... @@ -1431,7 +1449,7 @@
1431 1449 "likes": 0,
1432 1450 "createdAt": 1788009825082,
1433 1451 "promptEn": "facing Thanos, went in head-on and got seen through",
1434   - "lastPlayedAt": 1788233391003
  1452 + "lastPlayedAt": 1788238058572
1435 1453 },
1436 1454 {
1437 1455 "id": "a6ead2cbe03f",
... ... @@ -1446,7 +1464,7 @@
1446 1464 "likes": 0,
1447 1465 "createdAt": 1788009586293,
1448 1466 "promptEn": "facing Thanos, stayed passive and got cornered",
1449   - "lastPlayedAt": 1788233681128
  1467 + "lastPlayedAt": 1788239503570
1450 1468 },
1451 1469 {
1452 1470 "id": "438c98cf4973",
... ... @@ -1461,7 +1479,7 @@
1461 1479 "likes": 0,
1462 1480 "createdAt": 1788009346020,
1463 1481 "promptEn": "battlefield on Titan, the last chance to stop Thanos",
1464   - "lastPlayedAt": 1788229456216
  1482 + "lastPlayedAt": 1788239381524
1465 1483 },
1466 1484 {
1467 1485 "id": "66a96c6bc7cb",
... ... @@ -1491,7 +1509,7 @@
1491 1509 "likes": 0,
1492 1510 "createdAt": 1788008870118,
1493 1511 "promptEn": "facing the Eternity Devil, held the goal but paid a price",
1494   - "lastPlayedAt": 1788232976830
  1512 + "lastPlayedAt": 1788235496464
1495 1513 },
1496 1514 {
1497 1515 "id": "35833c2cabb9",
... ... @@ -1506,7 +1524,7 @@
1506 1524 "likes": 0,
1507 1525 "createdAt": 1788008632603,
1508 1526 "promptEn": "facing the Eternity Devil, went in head-on and got seen through",
1509   - "lastPlayedAt": 1788233795940
  1527 + "lastPlayedAt": 1788239149394
1510 1528 },
1511 1529 {
1512 1530 "id": "361c80537652",
... ... @@ -1521,7 +1539,7 @@
1521 1539 "likes": 0,
1522 1540 "createdAt": 1788008394344,
1523 1541 "promptEn": "facing the Eternity Devil, stayed passive and got cornered",
1524   - "lastPlayedAt": 1788228541431
  1542 + "lastPlayedAt": 1788236225074
1525 1543 },
1526 1544 {
1527 1545 "id": "1a57de01bad0",
... ... @@ -1536,7 +1554,7 @@
1536 1554 "likes": 0,
1537 1555 "createdAt": 1788008156307,
1538 1556 "promptEn": "hotel floor sealed by the Eternity Devil, an endless looping corridor",
1539   - "lastPlayedAt": 1788229258057
  1557 + "lastPlayedAt": 1788238969299
1540 1558 },
1541 1559 {
1542 1560 "id": "cd3b6dda90ad",
... ... @@ -1551,7 +1569,7 @@
1551 1569 "likes": 0,
1552 1570 "createdAt": 1788007915610,
1553 1571 "promptEn": "in the ruined city, exploiting a flaw to counter Tomura",
1554   - "lastPlayedAt": 1788233841207
  1572 + "lastPlayedAt": 1788236513335
1555 1573 },
1556 1574 {
1557 1575 "id": "f67685cd6862",
... ... @@ -1581,7 +1599,7 @@
1581 1599 "likes": 0,
1582 1600 "createdAt": 1788007441156,
1583 1601 "promptEn": "battlefield in Jaku city, the city in ruins, heroes in crisis",
1584   - "lastPlayedAt": 1788232272360
  1602 + "lastPlayedAt": 1788237350069
1585 1603 },
1586 1604 {
1587 1605 "id": "68219d6b68c8",
... ... @@ -1596,7 +1614,7 @@
1596 1614 "likes": 0,
1597 1615 "createdAt": 1788007201908,
1598 1616 "promptEn": "breaking into the palace, Neferpitou stalling to protect the king",
1599   - "lastPlayedAt": 1788230437117
  1617 + "lastPlayedAt": 1788239281462
1600 1618 },
1601 1619 {
1602 1620 "id": "628026375795",
... ... @@ -1611,7 +1629,7 @@
1611 1629 "likes": 0,
1612 1630 "createdAt": 1788006964122,
1613 1631 "promptEn": "grabbing an evidence flaw to counter L",
1614   - "lastPlayedAt": 1788232774691
  1632 + "lastPlayedAt": 1788236128993
1615 1633 },
1616 1634 {
1617 1635 "id": "0c6306b07f79",
... ... @@ -1626,7 +1644,7 @@
1626 1644 "likes": 0,
1627 1645 "createdAt": 1788006725887,
1628 1646 "promptEn": "facing off with L, went in head-on and got seen through",
1629   - "lastPlayedAt": 1788233326977
  1647 + "lastPlayedAt": 1788238599043
1630 1648 },
1631 1649 {
1632 1650 "id": "ee82188227fa",
... ... @@ -1641,7 +1659,7 @@
1641 1659 "likes": 0,
1642 1660 "createdAt": 1788006252918,
1643 1661 "promptEn": "facing L, stayed passive and the evidence pressure maxed out",
1644   - "lastPlayedAt": 1788233354985
  1662 + "lastPlayedAt": 1788237536217
1645 1663 },
1646 1664 {
1647 1665 "id": "0e28a7fecf03",
... ... @@ -1656,7 +1674,7 @@
1656 1674 "likes": 0,
1657 1675 "createdAt": 1788006252824,
1658 1676 "promptEn": "facing L, stayed passive and the evidence pressure maxed out",
1659   - "lastPlayedAt": 1788230964858
  1677 + "lastPlayedAt": 1788238803216
1660 1678 },
1661 1679 {
1662 1680 "id": "fb0738ad3e3b",
... ... @@ -1671,7 +1689,7 @@
1671 1689 "likes": 0,
1672 1690 "createdAt": 1788006015175,
1673 1691 "promptEn": "on the ruined planet, exploiting a flaw to counter Frieza",
1674   - "lastPlayedAt": 1788233190924
  1692 + "lastPlayedAt": 1788239487574
1675 1693 },
1676 1694 {
1677 1695 "id": "e6fda5c2237d",
... ... @@ -1686,7 +1704,7 @@
1686 1704 "likes": 0,
1687 1705 "createdAt": 1788005778588,
1688 1706 "promptEn": "facing Frieza, held the goal but paid a price",
1689   - "lastPlayedAt": 1788233701138
  1707 + "lastPlayedAt": 1788239429548
1690 1708 },
1691 1709 {
1692 1710 "id": "b96cc9762cf6",
... ... @@ -1716,7 +1734,7 @@
1716 1734 "likes": 0,
1717 1735 "createdAt": 1788005305091,
1718 1736 "promptEn": "facing Frieza, stayed passive and got cornered",
1719   - "lastPlayedAt": 1788232546520
  1737 + "lastPlayedAt": 1788239513586
1720 1738 },
1721 1739 {
1722 1740 "id": "d680e111ade5",
... ... @@ -1731,7 +1749,7 @@
1731 1749 "likes": 0,
1732 1750 "createdAt": 1788005066484,
1733 1751 "promptEn": "Namek is about to be destroyed, the dragon ball wish still up for grabs",
1734   - "lastPlayedAt": 1788230088799
  1752 + "lastPlayedAt": 1788236841630
1735 1753 },
1736 1754 {
1737 1755 "id": "565059763888",
... ... @@ -1746,7 +1764,7 @@
1746 1764 "likes": 0,
1747 1765 "createdAt": 1788004828526,
1748 1766 "promptEn": "in the subway, exploiting a flaw to counter Mahito",
1749   - "lastPlayedAt": 1788226535712
  1767 + "lastPlayedAt": 1788237532215
1750 1768 },
1751 1769 {
1752 1770 "id": "323737948733",
... ... @@ -1761,7 +1779,7 @@
1761 1779 "likes": 0,
1762 1780 "createdAt": 1788004590937,
1763 1781 "promptEn": "facing Mahito, held the goal but paid a price",
1764   - "lastPlayedAt": 1788229662416
  1782 + "lastPlayedAt": 1788239395530
1765 1783 },
1766 1784 {
1767 1785 "id": "9996a8ac8326",
... ... @@ -1776,7 +1794,7 @@
1776 1794 "likes": 0,
1777 1795 "createdAt": 1788004353928,
1778 1796 "promptEn": "facing Mahito, went in head-on and got seen through",
1779   - "lastPlayedAt": 1788232310379
  1797 + "lastPlayedAt": 1788237410131
1780 1798 },
1781 1799 {
1782 1800 "id": "971d80ec087c",
... ... @@ -1791,7 +1809,7 @@
1791 1809 "likes": 0,
1792 1810 "createdAt": 1788004115725,
1793 1811 "promptEn": "facing Mahito, stayed passive and got cornered",
1794   - "lastPlayedAt": 1788230311003
  1812 + "lastPlayedAt": 1788239389526
1795 1813 },
1796 1814 {
1797 1815 "id": "5dd52c568f66",
... ... @@ -1806,7 +1824,7 @@
1806 1824 "likes": 0,
1807 1825 "createdAt": 1788004115612,
1808 1826 "promptEn": "facing Mahito, stayed passive and got cornered",
1809   - "lastPlayedAt": 1788232315826
  1827 + "lastPlayedAt": 1788239333489
1810 1828 },
1811 1829 {
1812 1830 "id": "f4a2870386c6",
... ... @@ -1821,7 +1839,7 @@
1821 1839 "likes": 0,
1822 1840 "createdAt": 1788003877425,
1823 1841 "promptEn": "in the dream, exploiting a flaw to counter Enmu",
1824   - "lastPlayedAt": 1788232025702
  1842 + "lastPlayedAt": 1788239399534
1825 1843 },
1826 1844 {
1827 1845 "id": "7c4f9460e6b7",
... ... @@ -1836,7 +1854,7 @@
1836 1854 "likes": 0,
1837 1855 "createdAt": 1788003639415,
1838 1856 "promptEn": "facing Enmu, held the goal but paid a price",
1839   - "lastPlayedAt": 1788229416189
  1857 + "lastPlayedAt": 1788238813220
1840 1858 },
1841 1859 {
1842 1860 "id": "6881dbe839a0",
... ... @@ -1851,7 +1869,7 @@
1851 1869 "likes": 0,
1852 1870 "createdAt": 1788003402559,
1853 1871 "promptEn": "facing Enmu, went in head-on and got seen through",
1854   - "lastPlayedAt": 1788233214933
  1872 + "lastPlayedAt": 1788239481564
1855 1873 },
1856 1874 {
1857 1875 "id": "5c0f1663f4ae",
... ... @@ -1866,7 +1884,7 @@
1866 1884 "likes": 0,
1867 1885 "createdAt": 1788003166117,
1868 1886 "promptEn": "facing Enmu, stayed passive and got cornered",
1869   - "lastPlayedAt": 1788232239825
  1887 + "lastPlayedAt": 1788238218715
1870 1888 },
1871 1889 {
1872 1890 "id": "a3f39da74c1f",
... ... @@ -1881,7 +1899,7 @@
1881 1899 "likes": 0,
1882 1900 "createdAt": 1788002929455,
1883 1901 "promptEn": "facing Reiner, exploiting a flaw to counter him",
1884   - "lastPlayedAt": 1788233194926
  1902 + "lastPlayedAt": 1788239265453
1885 1903 },
1886 1904 {
1887 1905 "id": "ea8b38014674",
... ... @@ -1896,7 +1914,7 @@
1896 1914 "likes": 0,
1897 1915 "createdAt": 1788002691822,
1898 1916 "promptEn": "facing Reiner, held the goal but paid a price",
1899   - "lastPlayedAt": 1788232045709
  1917 + "lastPlayedAt": 1788238975304
1900 1918 },
1901 1919 {
1902 1920 "id": "f65b15d67966",
... ... @@ -1911,7 +1929,7 @@
1911 1929 "likes": 0,
1912 1930 "createdAt": 1788002455066,
1913 1931 "promptEn": "facing Reiner, went in head-on and got seen through",
1914   - "lastPlayedAt": 1788227954924
  1932 + "lastPlayedAt": 1788237878416
1915 1933 },
1916 1934 {
1917 1935 "id": "5846d645e870",
... ... @@ -1926,7 +1944,7 @@
1926 1944 "likes": 0,
1927 1945 "createdAt": 1788002217928,
1928 1946 "promptEn": "facing Reiner, stayed passive and got cornered",
1929   - "lastPlayedAt": 1788229744490
  1947 + "lastPlayedAt": 1788237580239
1930 1948 },
1931 1949 {
1932 1950 "id": "3fedf5531999",
... ... @@ -1941,7 +1959,7 @@
1941 1959 "likes": 0,
1942 1960 "createdAt": 1788001981272,
1943 1961 "promptEn": "basement in Shiganshina, the titan secret weighing down on you",
1944   - "lastPlayedAt": 1788233150908
  1962 + "lastPlayedAt": 1788239223427
1945 1963 },
1946 1964 {
1947 1965 "id": "7c2351a27a96",
... ... @@ -1971,7 +1989,7 @@
1971 1989 "likes": 0,
1972 1990 "createdAt": 1788001428705,
1973 1991 "promptEn": "in the ruins of Konoha, exploiting a flaw to counter Pain",
1974   - "lastPlayedAt": 1788229818554
  1992 + "lastPlayedAt": 1788238687110
1975 1993 },
1976 1994 {
1977 1995 "id": "e486fab4c24d",
... ... @@ -1986,7 +2004,7 @@
1986 2004 "likes": 0,
1987 2005 "createdAt": 1788001285796,
1988 2006 "promptEn": "in the ruins of Konoha, exploiting a flaw to counter Pain",
1989   - "lastPlayedAt": 1788229776519
  2007 + "lastPlayedAt": 1788236845633
1990 2008 },
1991 2009 {
1992 2010 "id": "808e89ca06a4",
... ... @@ -2001,7 +2019,7 @@
2001 2019 "likes": 0,
2002 2020 "createdAt": 1788001049233,
2003 2021 "promptEn": "facing Pain, held the goal but paid a price",
2004   - "lastPlayedAt": 1788229928658
  2022 + "lastPlayedAt": 1788238809210
2005 2023 },
2006 2024 {
2007 2025 "id": "7ecd480a3c31",
... ... @@ -2016,7 +2034,7 @@
2016 2034 "likes": 0,
2017 2035 "createdAt": 1788000812932,
2018 2036 "promptEn": "facing Pain, went in head-on and got seen through",
2019   - "lastPlayedAt": 1788231423103
  2037 + "lastPlayedAt": 1788237904442
2020 2038 },
2021 2039 {
2022 2040 "id": "f691ac021df0",
... ... @@ -2031,7 +2049,7 @@
2031 2049 "likes": 0,
2032 2050 "createdAt": 1788000577474,
2033 2051 "promptEn": "facing Pain, stayed passive and got cornered",
2034   - "lastPlayedAt": 1788230162864
  2052 + "lastPlayedAt": 1788239588520
2035 2053 },
2036 2054 {
2037 2055 "id": "5ef9489bdc5b",
... ... @@ -2046,7 +2064,7 @@
2046 2064 "likes": 0,
2047 2065 "createdAt": 1788000341927,
2048 2066 "promptEn": "Konoha destroyed by Shinra Tensei, Pain declaring order in the ruins",
2049   - "lastPlayedAt": 1788233757171
  2067 + "lastPlayedAt": 1788237584239
2050 2068 },
2051 2069 {
2052 2070 "id": "e245dbcc6d36",
... ... @@ -2061,7 +2079,7 @@
2061 2079 "likes": 0,
2062 2080 "createdAt": 1788000105889,
2063 2081 "promptEn": "facing Sakazuki, exploiting a flaw to counter him",
2064   - "lastPlayedAt": 1788232794712
  2082 + "lastPlayedAt": 1788235920823
2065 2083 },
2066 2084 {
2067 2085 "id": "c9be7c07b793",
... ... @@ -2076,7 +2094,7 @@
2076 2094 "likes": 0,
2077 2095 "createdAt": 1787999869694,
2078 2096 "promptEn": "facing Sakazuki, held the goal but paid a price",
2079   - "lastPlayedAt": 1788228957793
  2097 + "lastPlayedAt": 1788238915275
2080 2098 },
2081 2099 {
2082 2100 "id": "677941e683c4",
... ... @@ -2091,7 +2109,7 @@
2091 2109 "likes": 0,
2092 2110 "createdAt": 1787999633745,
2093 2111 "promptEn": "facing Sakazuki, went in head-on and got seen through",
2094   - "lastPlayedAt": 1788232798715
  2112 + "lastPlayedAt": 1788237468177
2095 2113 },
2096 2114 {
2097 2115 "id": "932d15f7f8bd",
... ... @@ -2106,7 +2124,7 @@
2106 2124 "likes": 0,
2107 2125 "createdAt": 1787999633670,
2108 2126 "promptEn": "facing Sakazuki, went in head-on and got seen through",
2109   - "lastPlayedAt": 1788232700636
  2127 + "lastPlayedAt": 1788238018530
2110 2128 },
2111 2129 {
2112 2130 "id": "a8b8b4430ee3",
... ... @@ -2121,7 +2139,7 @@
2121 2139 "likes": 0,
2122 2140 "createdAt": 1787999397097,
2123 2141 "promptEn": "facing Sakazuki, stayed passive and got cornered",
2124   - "lastPlayedAt": 1788229874611
  2142 + "lastPlayedAt": 1788234753781
2125 2143 },
2126 2144 {
2127 2145 "id": "88923aaf57f3",
... ... @@ -2136,7 +2154,7 @@
2136 2154 "likes": 0,
2137 2155 "createdAt": 1787999161013,
2138 2156 "promptEn": "execution platform at Marineford, Ace about to be executed, navy vs pirates",
2139   - "lastPlayedAt": 1788233208928
  2157 + "lastPlayedAt": 1788237021780
2140 2158 },
2141 2159 {
2142 2160 "id": "00476b62bf75",
... ... @@ -2151,7 +2169,7 @@
2151 2169 "likes": 0,
2152 2170 "createdAt": 1787903583872,
2153 2171 "promptEn": "constable asks if she's always been alone here, she doesn't answer",
2154   - "lastPlayedAt": 1788232243824
  2172 + "lastPlayedAt": 1788238082590
2155 2173 },
2156 2174 {
2157 2175 "id": "2ab2094e5679",
... ... @@ -2166,7 +2184,7 @@
2166 2184 "likes": 0,
2167 2185 "createdAt": 1787900324972,
2168 2186 "promptEn": "constable declines a drink, asks what this place is, she watches him through the screen",
2169   - "lastPlayedAt": 1788229530286
  2187 + "lastPlayedAt": 1788234247395
2170 2188 },
2171 2189 {
2172 2190 "id": "43ee5c728f1b",
... ... @@ -2181,7 +2199,7 @@
2181 2199 "likes": 0,
2182 2200 "createdAt": 1787850191899,
2183 2201 "promptEn": "constable reaches out to touch her hand through the screen gap, no response",
2184   - "lastPlayedAt": 1788229017844
  2202 + "lastPlayedAt": 1788236525342
2185 2203 },
2186 2204 {
2187 2205 "id": "9bd8c9fbf1f6",
... ... @@ -2196,7 +2214,7 @@
2196 2214 "likes": 0,
2197 2215 "createdAt": 1787847447661,
2198 2216 "promptEn": "constable tells the woman behind the screen he'll trust her tonight",
2199   - "lastPlayedAt": 1788232414433
  2217 + "lastPlayedAt": 1788239648569
2200 2218 },
2201 2219 {
2202 2220 "id": "8cb493338cb3",
... ... @@ -2211,7 +2229,7 @@
2211 2229 "likes": 0,
2212 2230 "createdAt": 1787831362083,
2213 2231 "promptEn": "constable promises to get her out safely if she trusts him",
2214   - "lastPlayedAt": 1788233483044
  2232 + "lastPlayedAt": 1788237063824
2215 2233 },
2216 2234 {
2217 2235 "id": "5ede61c01b22",
... ... @@ -2226,7 +2244,7 @@
2226 2244 "likes": 0,
2227 2245 "createdAt": 1787762342686,
2228 2246 "promptEn": "woman tells him from behind the screen to go check outside, he steps out warily",
2229   - "lastPlayedAt": 1788233421018
  2247 + "lastPlayedAt": 1788239339500
2230 2248 },
2231 2249 {
2232 2250 "id": "47a6de809c61",
... ... @@ -2241,7 +2259,7 @@
2241 2259 "likes": 0,
2242 2260 "createdAt": 1787762332289,
2243 2261 "promptEn": "woman behind the screen stirs and softly reassures him",
2244   - "lastPlayedAt": 1788231953653
  2262 + "lastPlayedAt": 1788237304019
2245 2263 },
2246 2264 {
2247 2265 "id": "0abf6e529c92",
... ... @@ -2256,7 +2274,7 @@
2256 2274 "likes": 0,
2257 2275 "createdAt": 1787762311804,
2258 2276 "promptEn": "constable sits with a cup of water, slowly looking up at the woman behind the screen",
2259   - "lastPlayedAt": 1788232819824
  2277 + "lastPlayedAt": 1788237147890
2260 2278 },
2261 2279 {
2262 2280 "id": "850f13881576",
... ... @@ -2271,7 +2289,7 @@
2271 2289 "likes": 0,
2272 2290 "createdAt": 1787762286261,
2273 2291 "promptEn": "constable stares at her through the screen, silent, saying nothing",
2274   - "lastPlayedAt": 1788230968864
  2292 + "lastPlayedAt": 1788237231957
2275 2293 },
2276 2294 {
2277 2295 "id": "59711a102e84",
... ... @@ -2286,7 +2304,7 @@
2286 2304 "likes": 0,
2287 2305 "createdAt": 1787762224218,
2288 2306 "promptEn": "constable stands under the lamp, holding her gaze through the screen, not backing away",
2289   - "lastPlayedAt": 1788232250345
  2307 + "lastPlayedAt": 1788239794026
2290 2308 },
2291 2309 {
2292 2310 "id": "f2e70205766b",
... ... @@ -2301,7 +2319,7 @@
2301 2319 "likes": 0,
2302 2320 "createdAt": 1787738044140,
2303 2321 "promptEn": "constable carefully checks the door and window latches, she watches from behind the screen",
2304   - "lastPlayedAt": 1788233721152
  2322 + "lastPlayedAt": 1788239592524
2305 2323 },
2306 2324 {
2307 2325 "id": "cfbef6e37ae4",
... ... @@ -2316,7 +2334,7 @@
2316 2334 "likes": 0,
2317 2335 "createdAt": 1787730413689,
2318 2336 "promptEn": "constable grabs the oil lamp and hurls it at the portrait, the scroll catches fire",
2319   - "lastPlayedAt": 1788233781184
  2337 + "lastPlayedAt": 1788236819612
2320 2338 },
2321 2339 {
2322 2340 "id": "e6c23c6adc42",
... ... @@ -2331,7 +2349,7 @@
2331 2349 "likes": 0,
2332 2350 "createdAt": 1787730331754,
2333 2351 "promptEn": "constable steps in from the rainy night, spots her, introduces himself with a cover story",
2334   - "lastPlayedAt": 1788232642592
  2352 + "lastPlayedAt": 1788239748660
2335 2353 },
2336 2354 {
2337 2355 "id": "d720d52c783f",
... ... @@ -2346,7 +2364,7 @@
2346 2364 "likes": 0,
2347 2365 "createdAt": 1787730331663,
2348 2366 "promptEn": "constable steps in from the rainy night, spots her, introduces himself with a cover story",
2349   - "lastPlayedAt": 1788232982830
  2367 + "lastPlayedAt": 1788237267984
2350 2368 },
2351 2369 {
2352 2370 "id": "84e8337de242",
... ... @@ -2361,7 +2379,7 @@
2361 2379 "likes": 0,
2362 2380 "createdAt": 1787724313620,
2363 2381 "promptEn": "first-person view bursting into the room, glancing back in fear, the woman startled",
2364   - "lastPlayedAt": 1788232846754
  2382 + "lastPlayedAt": 1788239682009
2365 2383 },
2366 2384 {
2367 2385 "id": "81b6c3789111",
... ... @@ -2391,7 +2409,7 @@
2391 2409 "likes": 0,
2392 2410 "createdAt": 1787708701547,
2393 2411 "promptEn": "constable gently tells her he'll stay and keep her company",
2394   - "lastPlayedAt": 1788232778701
  2412 + "lastPlayedAt": 1788237740325
2395 2413 },
2396 2414 {
2397 2415 "id": "8d9c1dad041f",
... ... @@ -2406,7 +2424,7 @@
2406 2424 "likes": 0,
2407 2425 "createdAt": 1787708686397,
2408 2426 "promptEn": "constable accepts the invitation, moves to sit on a stool closer to the screen",
2409   - "lastPlayedAt": 1788231192963
  2427 + "lastPlayedAt": 1788236026908
2410 2428 },
2411 2429 {
2412 2430 "id": "ad611bcbd06b",
... ... @@ -2436,7 +2454,7 @@
2436 2454 "likes": 0,
2437 2455 "createdAt": 1787708674508,
2438 2456 "promptEn": "constable sees through her painted disguise and rips her face off, revealing a demonic visage underneath",
2439   - "lastPlayedAt": 1788231877578
  2457 + "lastPlayedAt": 1788234739771
2440 2458 },
2441 2459 {
2442 2460 "id": "6d433dcc104c",
... ... @@ -2451,7 +2469,7 @@
2451 2469 "likes": 0,
2452 2470 "createdAt": 1787708663418,
2453 2471 "promptEn": "constable catches his breath, thanks her, and rests on the couch while she watches",
2454   - "lastPlayedAt": 1788229822555
  2472 + "lastPlayedAt": 1788239175402
2455 2473 },
2456 2474 {
2457 2475 "id": "cd22c2b491fc",
... ... @@ -2466,7 +2484,7 @@
2466 2484 "likes": 0,
2467 2485 "createdAt": 1787708653181,
2468 2486 "promptEn": "constable holds her gaze, quietly says \"go on\", then waits in silence",
2469   - "lastPlayedAt": 1788231861563
  2487 + "lastPlayedAt": 1788235216202
2470 2488 },
2471 2489 {
2472 2490 "id": "55f98ceec9e4",
... ... @@ -2481,7 +2499,7 @@
2481 2499 "likes": 0,
2482 2500 "createdAt": 1787708648409,
2483 2501 "promptEn": "she reaches a hand through the screen gap beckoning him, he steps back and firmly refuses",
2484   - "lastPlayedAt": 1788232290370
  2502 + "lastPlayedAt": 1788239079356
2485 2503 },
2486 2504 {
2487 2505 "id": "3f0c28f5373a",
... ... @@ -2496,7 +2514,7 @@
2496 2514 "likes": 0,
2497 2515 "createdAt": 1787708646888,
2498 2516 "promptEn": "constable studies her through the screen growing suspicious, hand drifting to his sword",
2499   - "lastPlayedAt": 1788231483166
  2517 + "lastPlayedAt": 1788239409555
2500 2518 },
2501 2519 {
2502 2520 "id": "1c0c203cfb3b",
... ... @@ -2511,7 +2529,7 @@
2511 2529 "likes": 0,
2512 2530 "createdAt": 1787708641782,
2513 2531 "promptEn": "constable sits down at the table, studying her expression through the screen",
2514   - "lastPlayedAt": 1788229894631
  2532 + "lastPlayedAt": 1788238627059
2515 2533 },
2516 2534 {
2517 2535 "id": "89975be9def4",
... ... @@ -2526,7 +2544,7 @@
2526 2544 "likes": 0,
2527 2545 "createdAt": 1787708626228,
2528 2546 "promptEn": "constable drinks some water first, thanks her, watching the screen warily as he drinks",
2529   - "lastPlayedAt": 1788232223822
  2547 + "lastPlayedAt": 1788237762334
2530 2548 },
2531 2549 {
2532 2550 "id": "e08f7f7e54dd",
... ... @@ -2541,7 +2559,7 @@
2541 2559 "likes": 0,
2542 2560 "createdAt": 1787708622306,
2543 2561 "promptEn": "constable gives a clasped-hand salute and thanks her, she gives a subtle nod",
2544   - "lastPlayedAt": 1788229043869
  2562 + "lastPlayedAt": 1788236275114
2545 2563 },
2546 2564 {
2547 2565 "id": "538dc7e5746f",
... ... @@ -2556,7 +2574,7 @@
2556 2574 "likes": 0,
2557 2575 "createdAt": 1787708622178,
2558 2576 "promptEn": "constable gives a clasped-hand salute and thanks her, she gives a subtle nod",
2559   - "lastPlayedAt": 1788231903603
  2577 + "lastPlayedAt": 1788236299134
2560 2578 },
2561 2579 {
2562 2580 "id": "1721c75dbc3b",
... ... @@ -2571,7 +2589,7 @@
2571 2589 "likes": 0,
2572 2590 "createdAt": 1787708615415,
2573 2591 "promptEn": "she slides her hand through the screen gap asking him to warm it, he hesitates without touching",
2574   - "lastPlayedAt": 1788230768774
  2592 + "lastPlayedAt": 1788238224719
2575 2593 },
2576 2594 {
2577 2595 "id": "c2b145b111e2",
... ... @@ -2586,7 +2604,7 @@
2586 2604 "likes": 0,
2587 2605 "createdAt": 1787708613364,
2588 2606 "promptEn": "constable steps in, notices her behind the screen, greets her, she just watches silently",
2589   - "lastPlayedAt": 1788232155826
  2607 + "lastPlayedAt": 1788238833238
2590 2608 },
2591 2609 {
2592 2610 "id": "a5d5d353bbf8",
... ... @@ -2601,7 +2619,7 @@
2601 2619 "likes": 0,
2602 2620 "createdAt": 1787674768144,
2603 2621 "promptEn": "constable lifts a bronze mirror, reflecting candlelight onto the portrait on the wall",
2604   - "lastPlayedAt": 1788231997701
  2622 + "lastPlayedAt": 1788237542221
2605 2623 },
2606 2624 {
2607 2625 "id": "3be1f275fde2",
... ... @@ -2616,7 +2634,7 @@
2616 2634 "likes": 0,
2617 2635 "createdAt": 1787674743188,
2618 2636 "promptEn": "constable draws his sword and breaks the sealed talisman on the screen, revealing her behind it",
2619   - "lastPlayedAt": 1788233395001
  2637 + "lastPlayedAt": 1788238747163
2620 2638 },
2621 2639 {
2622 2640 "id": "693e0b9ca66a",
... ... @@ -2631,7 +2649,7 @@
2631 2649 "likes": 0,
2632 2650 "createdAt": 1787674737743,
2633 2651 "promptEn": "constable slowly looks around the room, discovers her watching him motionlessly through the screen",
2634   - "lastPlayedAt": 1788230912836
  2652 + "lastPlayedAt": 1788238877259
2635 2653 },
2636 2654 {
2637 2655 "id": "c74118d0413f",
... ... @@ -2646,7 +2664,7 @@
2646 2664 "likes": 0,
2647 2665 "createdAt": 1787674716075,
2648 2666 "promptEn": "constable watches the oil lamp flame flicker, her silhouette motionless behind the screen",
2649   - "lastPlayedAt": 1788232920805
  2667 + "lastPlayedAt": 1788239041338
2650 2668 },
2651 2669 {
2652 2670 "id": "23d1ae1982e6",
... ... @@ -2661,7 +2679,7 @@
2661 2679 "likes": 0,
2662 2680 "createdAt": 1787674713724,
2663 2681 "promptEn": "constable enters and cautiously looks around, his gaze settling on her silhouette behind the screen",
2664   - "lastPlayedAt": 1788233370997
  2682 + "lastPlayedAt": 1788239716626
2665 2683 },
2666 2684 {
2667 2685 "id": "b69389094416",
... ... @@ -2676,7 +2694,7 @@
2676 2694 "likes": 0,
2677 2695 "createdAt": 1787674695703,
2678 2696 "promptEn": "constable stops, faces the screen, quietly promises to keep his voice down",
2679   - "lastPlayedAt": 1788231753466
  2697 + "lastPlayedAt": 1788238863250
2680 2698 },
2681 2699 {
2682 2700 "id": "94dc455e26bf",
... ... @@ -2691,7 +2709,7 @@
2691 2709 "likes": 0,
2692 2710 "createdAt": 1787674688717,
2693 2711 "promptEn": "constable agrees to stay, lets his guard down, she gives a barely-there nod",
2694   - "lastPlayedAt": 1788233705137
  2712 + "lastPlayedAt": 1788238939281
2695 2713 },
2696 2714 {
2697 2715 "id": "4874fe3fdbc9",
... ... @@ -2706,7 +2724,7 @@
2706 2724 "likes": 0,
2707 2725 "createdAt": 1787674660698,
2708 2726 "promptEn": "constable smashes the bronze mirror on the table, it shatters, she watches from behind the screen",
2709   - "lastPlayedAt": 1788228043003
  2727 + "lastPlayedAt": 1788238705131
2710 2728 },
2711 2729 {
2712 2730 "id": "ed77b83851a2",
... ... @@ -2721,7 +2739,7 @@
2721 2739 "likes": 0,
2722 2740 "createdAt": 1787674642139,
2723 2741 "promptEn": "constable forms hand seals and summons a giant toad out of smoke",
2724   - "lastPlayedAt": 1788228853703
  2742 + "lastPlayedAt": 1788237684292
2725 2743 },
2726 2744 {
2727 2745 "id": "e9a6320e18a9",
... ... @@ -2736,7 +2754,7 @@
2736 2754 "likes": 0,
2737 2755 "createdAt": 1787674642026,
2738 2756 "promptEn": "constable enters from the side and sits down on a stool by the lamp to rest",
2739   - "lastPlayedAt": 1788233801194
  2757 + "lastPlayedAt": 1788236343178
2740 2758 },
2741 2759 {
2742 2760 "id": "fe7ffbe65653",
... ... @@ -2766,7 +2784,7 @@
2766 2784 "likes": 0,
2767 2785 "createdAt": 1787674623678,
2768 2786 "promptEn": "constable bows to the screen, thanking her for sheltering him",
2769   - "lastPlayedAt": 1788230802785
  2787 + "lastPlayedAt": 1788239287469
2770 2788 },
2771 2789 {
2772 2790 "id": "d94991a1e21b",
... ... @@ -2781,7 +2799,7 @@
2781 2799 "likes": 0,
2782 2800 "createdAt": 1787674609636,
2783 2801 "promptEn": "constable silently watches her through the screen gaps, camera slowly zooming in on her face",
2784   - "lastPlayedAt": 1788233276957
  2802 + "lastPlayedAt": 1788239155387
2785 2803 },
2786 2804 {
2787 2805 "id": "5e53f7f5fe9b",
... ... @@ -2796,7 +2814,7 @@
2796 2814 "likes": 0,
2797 2815 "createdAt": 1787674446841,
2798 2816 "promptEn": "player panics into the room, glances back, out of breath, says something's chasing him outside",
2799   - "lastPlayedAt": 1788232872779
  2817 + "lastPlayedAt": 1788238603043
2800 2818 },
2801 2819 {
2802 2820 "id": "c25d53a5e086",
... ... @@ -2811,7 +2829,7 @@
2811 2829 "likes": 0,
2812 2830 "createdAt": 1787674395006,
2813 2831 "promptEn": "constable and the woman hold a tense silent stare through the screen",
2814   - "lastPlayedAt": 1788229934661
  2832 + "lastPlayedAt": 1788238004523
2815 2833 },
2816 2834 {
2817 2835 "id": "f264bce22903",
... ... @@ -2826,7 +2844,7 @@
2826 2844 "likes": 0,
2827 2845 "createdAt": 1787674361187,
2828 2846 "promptEn": "constable presses his hand to his chest, vowing his heart to her, she gives a knowing smile",
2829   - "lastPlayedAt": 1788233038859
  2847 + "lastPlayedAt": 1788238671098
2830 2848 },
2831 2849 {
2832 2850 "id": "8aba0a6bf05b",
... ... @@ -2841,7 +2859,7 @@
2841 2859 "likes": 0,
2842 2860 "createdAt": 1787674351536,
2843 2861 "promptEn": "constable searches an old house at night, candle flickering, woman hiding behind the screen",
2844   - "lastPlayedAt": 1788232892795
  2862 + "lastPlayedAt": 1788237512206
2845 2863 },
2846 2864 {
2847 2865 "id": "dc13e4f165dc",
... ... @@ -2856,7 +2874,7 @@
2856 2874 "likes": 0,
2857 2875 "createdAt": 1787672849330,
2858 2876 "promptEn": "constable cautiously follows her silent gesture, hand near his sword, tracking her movement",
2859   - "lastPlayedAt": 1788232675826
  2877 + "lastPlayedAt": 1788236149011
2860 2878 },
2861 2879 {
2862 2880 "id": "feb4df69e1a2",
... ... @@ -2870,7 +2888,8 @@
2870 2888 "partition": "ref2va",
2871 2889 "likes": 0,
2872 2890 "createdAt": 1787665100787,
2873   - "promptEn": "constable politely asks the woman behind the screen for tea"
  2891 + "promptEn": "constable politely asks the woman behind the screen for tea",
  2892 + "lastPlayedAt": 1788237217948
2874 2893 },
2875 2894 {
2876 2895 "id": "d2c77b87b84b",
... ... @@ -2884,7 +2903,7 @@
2884 2903 "partition": "ref2va",
2885 2904 "likes": 0,
2886 2905 "createdAt": 1787652321725,
2887   - "lastPlayedAt": 1788232862770,
  2906 + "lastPlayedAt": 1788239762009,
2888 2907 "promptEn": "constable talking to the woman through the screen"
2889 2908 },
2890 2909 {
... ... @@ -2900,7 +2919,7 @@
2900 2919 "likes": 0,
2901 2920 "createdAt": 1787642579766,
2902 2921 "promptEn": "a summoner chants and calls a giant toad, which swallows the white-robed ghost whole",
2903   - "lastPlayedAt": 1788231783491
  2922 + "lastPlayedAt": 1788238761173
2904 2923 },
2905 2924 {
2906 2925 "id": "22a666455198",
... ... @@ -2914,7 +2933,8 @@
2914 2933 "partition": "ref2va",
2915 2934 "likes": 0,
2916 2935 "createdAt": 1787640567001,
2917   - "promptEn": "summoned toad Wentai, beat the painted-skin female ghost"
  2936 + "promptEn": "summoned toad Wentai, beat the painted-skin female ghost",
  2937 + "lastPlayedAt": 1788238370847
2918 2938 },
2919 2939 {
2920 2940 "id": "1cc42a8669a1",
... ... @@ -2928,7 +2948,7 @@
2928 2948 "partition": "ref2va",
2929 2949 "likes": 0,
2930 2950 "createdAt": 1787639957338,
2931   - "lastPlayedAt": 1788232935826,
  2951 + "lastPlayedAt": 1788239301475,
2932 2952 "promptEn": "constable talking to the woman through the screen"
2933 2953 },
2934 2954 {
... ... @@ -2944,7 +2964,7 @@
2944 2964 "likes": 0,
2945 2965 "createdAt": 1787637292741,
2946 2966 "promptEn": "constable bursts in panicked, spots the woman behind the screen",
2947   - "lastPlayedAt": 1788233080873
  2967 + "lastPlayedAt": 1788236779575
2948 2968 },
2949 2969 {
2950 2970 "id": "93c2b84a2a10",
... ... @@ -2958,7 +2978,8 @@
2958 2978 "partition": "ref2va",
2959 2979 "likes": 0,
2960 2980 "createdAt": 1787635935502,
2961   - "promptEn": "constable bursts in panicked, spots the woman behind the screen"
  2981 + "promptEn": "constable bursts in panicked, spots the woman behind the screen",
  2982 + "lastPlayedAt": 1788239035337
2962 2983 },
2963 2984 {
2964 2985 "id": "b32769bf80db",
... ... @@ -2973,7 +2994,7 @@
2973 2994 "likes": 0,
2974 2995 "createdAt": 1787632587351,
2975 2996 "promptEn": "constable bursts in panicked, spots the woman behind the screen",
2976   - "lastPlayedAt": 1788232814731
  2997 + "lastPlayedAt": 1788236799594
2977 2998 },
2978 2999 {
2979 3000 "id": "282281777d1b",
... ... @@ -2987,7 +3008,8 @@
2987 3008 "partition": "text",
2988 3009 "likes": 0,
2989 3010 "createdAt": 1787615243753,
2990   - "promptEn": "robot arm picks up a red block and puts it down"
  3011 + "promptEn": "robot arm picks up a red block and puts it down",
  3012 + "lastPlayedAt": 1788238791204
2991 3013 },
2992 3014 {
2993 3015 "id": "d040eec16a88",
... ... @@ -3002,7 +3024,7 @@
3002 3024 "likes": 0,
3003 3025 "createdAt": 1787615242602,
3004 3026 "promptEn": "otome game male lead showcase PV",
3005   - "lastPlayedAt": 1788233659118
  3027 + "lastPlayedAt": 1788239449554
3006 3028 },
3007 3029 {
3008 3030 "id": "423a20afba5a",
... ... @@ -3016,7 +3038,8 @@
3016 3038 "partition": "ref2va",
3017 3039 "likes": 0,
3018 3040 "createdAt": 1787615241814,
3019   - "promptEn": "xianxia character turning, only back and side profile shown"
  3041 + "promptEn": "xianxia character turning, only back and side profile shown",
  3042 + "lastPlayedAt": 1788239247441
3020 3043 },
3021 3044 {
3022 3045 "id": "1a398591663e",
... ... @@ -3031,7 +3054,7 @@
3031 3054 "likes": 0,
3032 3055 "createdAt": 1787615240988,
3033 3056 "promptEn": "clay fox sprints off a cliff, slow-mo leap over the canyon",
3034   - "lastPlayedAt": 1788232611825
  3057 + "lastPlayedAt": 1788239093359
3035 3058 },
3036 3059 {
3037 3060 "id": "db125dc6a831",
... ... @@ -3045,7 +3068,7 @@
3045 3068 "partition": "text",
3046 3069 "likes": 0,
3047 3070 "createdAt": 1787615240199,
3048   - "lastPlayedAt": 1788230818793,
  3071 + "lastPlayedAt": 1788239558507,
3049 3072 "promptEn": "website UI title slides in, car light fades to red"
3050 3073 },
3051 3074 {
... ... @@ -3061,7 +3084,7 @@
3061 3084 "likes": 0,
3062 3085 "createdAt": 1787588547172,
3063 3086 "promptEn": "product website scrolling down with flashy effects",
3064   - "lastPlayedAt": 1788232013692
  3087 + "lastPlayedAt": 1788236609419
3065 3088 },
3066 3089 {
3067 3090 "id": "2e18bf09b46a",
... ... @@ -3075,7 +3098,8 @@
3075 3098 "partition": "ref2va",
3076 3099 "likes": 0,
3077 3100 "createdAt": 1787588545801,
3078   - "promptEn": "game menu UI, character swaps out a mechanical arm"
  3101 + "promptEn": "game menu UI, character swaps out a mechanical arm",
  3102 + "lastPlayedAt": 1788236467285
3079 3103 },
3080 3104 {
3081 3105 "id": "fe749aa533a0",
... ... @@ -3090,7 +3114,7 @@
3090 3114 "likes": 0,
3091 3115 "createdAt": 1787588545243,
3092 3116 "promptEn": "ergonomic office chair 360 showcase ad",
3093   - "lastPlayedAt": 1788231939637
  3117 + "lastPlayedAt": 1788236183042
3094 3118 },
3095 3119 {
3096 3120 "id": "b0d258352c3c",
... ... @@ -3105,7 +3129,7 @@
3105 3129 "likes": 0,
3106 3130 "createdAt": 1787588544667,
3107 3131 "promptEn": "white studio shoot, two models wearing futuristic glasses",
3108   - "lastPlayedAt": 1788232326384
  3132 + "lastPlayedAt": 1788239826056
3109 3133 },
3110 3134 {
3111 3135 "id": "71a4d093aebc",
... ... @@ -3119,7 +3143,8 @@
3119 3143 "partition": "ref2va",
3120 3144 "likes": 0,
3121 3145 "createdAt": 1787588544109,
3122   - "promptEn": "vampire romance drama trailer, girl wanders into the castle"
  3146 + "promptEn": "vampire romance drama trailer, girl wanders into the castle",
  3147 + "lastPlayedAt": 1788234595640
3123 3148 },
3124 3149 {
3125 3150 "id": "7ded7db676d3",
... ... @@ -3134,7 +3159,7 @@
3134 3159 "likes": 0,
3135 3160 "createdAt": 1787588543551,
3136 3161 "promptEn": "MiniMax H3 tech brand film, blue waveform logo",
3137   - "lastPlayedAt": 1788232834747
  3162 + "lastPlayedAt": 1788237714306
3138 3163 },
3139 3164 {
3140 3165 "id": "81711242178d",
... ... @@ -3149,7 +3174,7 @@
3149 3174 "likes": 0,
3150 3175 "createdAt": 1787588542968,
3151 3176 "promptEn": "mystery crime title sequence, collage split-screen credits",
3152   - "lastPlayedAt": 1788232113749
  3177 + "lastPlayedAt": 1788239768008
3153 3178 },
3154 3179 {
3155 3180 "id": "67ababbea78f",
... ... @@ -3164,7 +3189,7 @@
3164 3189 "likes": 0,
3165 3190 "createdAt": 1787588542426,
3166 3191 "promptEn": "fashion guy in sunglasses and coat by a fire",
3167   - "lastPlayedAt": 1788233623102
  3192 + "lastPlayedAt": 1788239534502
3168 3193 },
3169 3194 {
3170 3195 "id": "c1237b599bf4",
... ... @@ -3209,7 +3234,7 @@
3209 3234 "likes": 0,
3210 3235 "createdAt": 1787588540642,
3211 3236 "promptEn": "telescope view searching for a MiniMax device, fashionable passerby",
3212   - "lastPlayedAt": 1788232740670
  3237 + "lastPlayedAt": 1788238735151
3213 3238 },
3214 3239 {
3215 3240 "id": "a59cae01ec39",
... ... @@ -3224,7 +3249,7 @@
3224 3249 "likes": 0,
3225 3250 "createdAt": 1787588540567,
3226 3251 "promptEn": "telescope view searching for a MiniMax device",
3227   - "lastPlayedAt": 1788231329035
  3252 + "lastPlayedAt": 1788238332809
3228 3253 },
3229 3254 {
3230 3255 "id": "ba8c0758e5b4",
... ... @@ -3239,7 +3264,7 @@
3239 3264 "likes": 0,
3240 3265 "createdAt": 1787588114381,
3241 3266 "promptEn": "underground rap duo freestyling at the camera",
3242   - "lastPlayedAt": 1788232182310
  3267 + "lastPlayedAt": 1788238348824
3243 3268 },
3244 3269 {
3245 3270 "id": "58a439a93c02",
... ... @@ -3254,7 +3279,7 @@
3254 3279 "likes": 0,
3255 3280 "createdAt": 1787577878370,
3256 3281 "promptEn": "man barges into the old house apologizing, woman in white behind the screen",
3257   - "lastPlayedAt": 1788232887827
  3282 + "lastPlayedAt": 1788237932471
3258 3283 },
3259 3284 {
3260 3285 "id": "0e75ef71c3d0",
... ... @@ -3269,7 +3294,7 @@
3269 3294 "likes": 0,
3270 3295 "createdAt": 1787577088842,
3271 3296 "promptEn": "man thanks the woman for taking him in, behind the screen",
3272   - "lastPlayedAt": 1788232866775
  3297 + "lastPlayedAt": 1788235894800
3273 3298 },
3274 3299 {
3275 3300 "id": "e656777b6eef",
... ... @@ -3283,7 +3308,8 @@
3283 3308 "partition": "text",
3284 3309 "likes": 0,
3285 3310 "createdAt": 1787577088778,
3286   - "promptEn": "man thanks the woman for taking him in"
  3311 + "promptEn": "man thanks the woman for taking him in",
  3312 + "lastPlayedAt": 1788239552505
3287 3313 },
3288 3314 {
3289 3315 "id": "8d0270466c35",
... ... @@ -3298,7 +3324,7 @@
3298 3324 "likes": 0,
3299 3325 "createdAt": 1787576943673,
3300 3326 "promptEn": "man lowers his voice like she said, checks the door, window and lamp",
3301   - "lastPlayedAt": 1788232408430
  3327 + "lastPlayedAt": 1788239307486
3302 3328 },
3303 3329 {
3304 3330 "id": "396de15c1439",
... ... @@ -3313,7 +3339,7 @@
3313 3339 "likes": 0,
3314 3340 "createdAt": 1787576943608,
3315 3341 "promptEn": "man lowers his voice like she said, checks the door, window and lamp",
3316   - "lastPlayedAt": 1788233675127
  3342 + "lastPlayedAt": 1788237526211
3317 3343 },
3318 3344 {
3319 3345 "id": "cde82791f726",
... ... @@ -3328,7 +3354,7 @@
3328 3354 "likes": 0,
3329 3355 "createdAt": 1787576798050,
3330 3356 "promptEn": "man says he's willing to give her his heart",
3331   - "lastPlayedAt": 1788231829543
  3357 + "lastPlayedAt": 1788238452913
3332 3358 },
3333 3359 {
3334 3360 "id": "57f2c6fa351d",
... ... @@ -3342,7 +3368,8 @@
3342 3368 "partition": "text",
3343 3369 "likes": 0,
3344 3370 "createdAt": 1787576651668,
3345   - "promptEn": "man silently looks at her, doesn't respond"
  3371 + "promptEn": "man silently looks at her, doesn't respond",
  3372 + "lastPlayedAt": 1788238781189
3346 3373 },
3347 3374 {
3348 3375 "id": "5958b86ab8f4",
... ... @@ -3356,7 +3383,7 @@
3356 3383 "partition": "text",
3357 3384 "likes": 0,
3358 3385 "createdAt": 1787576506447,
3359   - "lastPlayedAt": 1788232658608,
  3386 + "lastPlayedAt": 1788239732650,
3360 3387 "promptEn": "man does as she says but stays on guard"
3361 3388 },
3362 3389 {
... ... @@ -3371,7 +3398,7 @@
3371 3398 "partition": "text",
3372 3399 "likes": 0,
3373 3400 "createdAt": 1787576358982,
3374   - "lastPlayedAt": 1788230984868,
  3401 + "lastPlayedAt": 1788238965299,
3375 3402 "promptEn": "man apologizes, says something outside was chasing him"
3376 3403 },
3377 3404 {
... ... @@ -3387,7 +3414,7 @@
3387 3414 "likes": 0,
3388 3415 "createdAt": 1787576013870,
3389 3416 "promptEn": "man apologizes, says something outside was chasing him",
3390   - "lastPlayedAt": 1788232670613
  3417 + "lastPlayedAt": 1788238583032
3391 3418 },
3392 3419 {
3393 3420 "id": "7d425d2d35fb",
... ... @@ -3402,7 +3429,7 @@
3402 3429 "likes": 0,
3403 3430 "createdAt": 1787575790848,
3404 3431 "promptEn": "man apologizes, says something outside was chasing him",
3405   - "lastPlayedAt": 1788232526503
  3432 + "lastPlayedAt": 1788238955291
3406 3433 },
3407 3434 {
3408 3435 "id": "5e6dcf2c8cfd",
... ... @@ -3417,7 +3444,7 @@
3417 3444 "likes": 0,
3418 3445 "createdAt": 1787575164928,
3419 3446 "promptEn": "man apologizes, says something outside was chasing him",
3420   - "lastPlayedAt": 1788233417016
  3447 + "lastPlayedAt": 1788237376095
3421 3448 },
3422 3449 {
3423 3450 "id": "250fad90393c",
... ... @@ -3432,7 +3459,7 @@
3432 3459 "likes": 0,
3433 3460 "createdAt": 1787575164766,
3434 3461 "promptEn": "man apologizes, says something outside was chasing him",
3435   - "lastPlayedAt": 1788233171823
  3462 + "lastPlayedAt": 1788239027329
3436 3463 },
3437 3464 {
3438 3465 "id": "fb3000701384",
... ... @@ -3447,7 +3474,7 @@
3447 3474 "likes": 0,
3448 3475 "createdAt": 1787573698485,
3449 3476 "promptEn": "underground rap duo freestyling at the camera",
3450   - "lastPlayedAt": 1788231445130
  3477 + "lastPlayedAt": 1788235852763
3451 3478 },
3452 3479 {
3453 3480 "id": "e5246eae814e",
... ... @@ -3462,7 +3489,7 @@
3462 3489 "likes": 0,
3463 3490 "createdAt": 1787569314178,
3464 3491 "promptEn": "man apologizes, says something outside was chasing him",
3465   - "lastPlayedAt": 1788232479825
  3492 + "lastPlayedAt": 1788238593040
3466 3493 },
3467 3494 {
3468 3495 "id": "a0d7a106b8d6",
... ... @@ -3476,7 +3503,7 @@
3476 3503 "partition": "text",
3477 3504 "likes": 0,
3478 3505 "createdAt": 1787568512602,
3479   - "lastPlayedAt": 1788233062862,
  3506 + "lastPlayedAt": 1788238088593,
3480 3507 "promptEn": "man apologizes, says something outside was chasing him"
3481 3508 },
3482 3509 {
... ... @@ -3491,7 +3518,7 @@
3491 3518 "partition": "ref2va",
3492 3519 "likes": 0,
3493 3520 "createdAt": 1787559156410,
3494   - "lastPlayedAt": 1788231162964,
  3521 + "lastPlayedAt": 1788237187923,
3495 3522 "promptEn": "two characters sitting, game menu UI swaps in a mechanical arm"
3496 3523 },
3497 3524 {
... ... @@ -3506,7 +3533,8 @@
3506 3533 "partition": "ref2va",
3507 3534 "likes": 0,
3508 3535 "createdAt": 1787542770522,
3509   - "promptEn": "clip cut to match a reference video's editing rhythm"
  3536 + "promptEn": "clip cut to match a reference video's editing rhythm",
  3537 + "lastPlayedAt": 1788239790028
3510 3538 },
3511 3539 {
3512 3540 "id": "f9286d694bad",
... ... @@ -3521,7 +3549,7 @@
3521 3549 "likes": 0,
3522 3550 "createdAt": 1787497396594,
3523 3551 "promptEn": "otome game male lead showcase PV",
3524   - "lastPlayedAt": 1788232490471
  3552 + "lastPlayedAt": 1788237590243
3525 3553 },
3526 3554 {
3527 3555 "id": "36ac12f2f29e",
... ... @@ -3535,7 +3563,8 @@
3535 3563 "partition": "ref2va",
3536 3564 "likes": 0,
3537 3565 "createdAt": 1787493339415,
3538   - "promptEn": "game menu UI, character swaps out a mechanical arm"
  3566 + "promptEn": "game menu UI, character swaps out a mechanical arm",
  3567 + "lastPlayedAt": 1788239837605
3539 3568 },
3540 3569 {
3541 3570 "id": "656c897b04fc",
... ... @@ -3549,7 +3578,8 @@
3549 3578 "partition": "ref2va",
3550 3579 "likes": 0,
3551 3580 "createdAt": 1787493303602,
3552   - "promptEn": "game menu UI, character swaps out a mechanical arm"
  3581 + "promptEn": "game menu UI, character swaps out a mechanical arm",
  3582 + "lastPlayedAt": 1788238364841
3553 3583 },
3554 3584 {
3555 3585 "id": "e1fd99e4ab97",
... ... @@ -3564,7 +3594,7 @@
3564 3594 "likes": 0,
3565 3595 "createdAt": 1787470101960,
3566 3596 "promptEn": "cute hand-drawn beagle dog making faces, game screen",
3567   - "lastPlayedAt": 1788232202318
  3597 + "lastPlayedAt": 1788235590540
3568 3598 },
3569 3599 {
3570 3600 "id": "4067993ff824",
... ... @@ -3579,7 +3609,7 @@
3579 3609 "likes": 0,
3580 3610 "createdAt": 1787470101693,
3581 3611 "promptEn": "black-background product-launch style shot of a roast duck",
3582   - "lastPlayedAt": 1788233499050
  3612 + "lastPlayedAt": 1788239608539
3583 3613 },
3584 3614 {
3585 3615 "id": "99e771bbb50d",
... ... @@ -3608,7 +3638,7 @@
3608 3638 "likes": 0,
3609 3639 "createdAt": 1787470101060,
3610 3640 "promptEn": "snowy bamboo forest at night, wuxia mystery standoff",
3611   - "lastPlayedAt": 1788231499181
  3641 + "lastPlayedAt": 1788238691117
3612 3642 },
3613 3643 {
3614 3644 "id": "125751cac1c8",
... ... @@ -3623,7 +3653,7 @@
3623 3653 "likes": 0,
3624 3654 "createdAt": 1787470100377,
3625 3655 "promptEn": "all-black background, experimental Chinese kinetic typography",
3626   - "lastPlayedAt": 1788233176921
  3656 + "lastPlayedAt": 1788239704619
3627 3657 },
3628 3658 {
3629 3659 "id": "c24c15736daa",
... ... @@ -3638,7 +3668,7 @@
3638 3668 "likes": 0,
3639 3669 "createdAt": 1787470100107,
3640 3670 "promptEn": "MiniMax H3 brand motion graphics, a dot growing into a network",
3641   - "lastPlayedAt": 1788233154911
  3671 + "lastPlayedAt": 1788235878787
3642 3672 },
3643 3673 {
3644 3674 "id": "194e9bd77da6",
... ... @@ -3653,7 +3683,7 @@
3653 3683 "likes": 0,
3654 3684 "createdAt": 1787470100022,
3655 3685 "promptEn": "MiniMax H3 brand motion graphics, a dot growing into a network",
3656   - "lastPlayedAt": 1788233112891
  3686 + "lastPlayedAt": 1788239517585
3657 3687 },
3658 3688 {
3659 3689 "id": "eaf2a1433c41",
... ... @@ -3668,7 +3698,7 @@
3668 3698 "likes": 0,
3669 3699 "createdAt": 1787470099635,
3670 3700 "promptEn": "pointing at things at a beach market, they turn into hand-drawn animation",
3671   - "lastPlayedAt": 1788233100885
  3701 + "lastPlayedAt": 1788239313488
3672 3702 },
3673 3703 {
3674 3704 "id": "0fe0f2101c44",
... ... @@ -3682,7 +3712,8 @@
3682 3712 "partition": "text",
3683 3713 "likes": 0,
3684 3714 "createdAt": 1787470099306,
3685   - "promptEn": "late-night laundromat footage mixed with glowing hand-drawn animation"
  3715 + "promptEn": "late-night laundromat footage mixed with glowing hand-drawn animation",
  3716 + "lastPlayedAt": 1788237966499
3686 3717 },
3687 3718 {
3688 3719 "id": "c0e23beb4132",
... ... @@ -3697,7 +3728,7 @@
3697 3728 "likes": 0,
3698 3729 "createdAt": 1787470098914,
3699 3730 "promptEn": "evening kitchen footage mixed with a glowing hand-drawn creature",
3700   - "lastPlayedAt": 1788232278363
  3731 + "lastPlayedAt": 1788236951715
3701 3732 },
3702 3733 {
3703 3734 "id": "6639b07acc7b",
... ... @@ -3727,7 +3758,7 @@
3727 3758 "likes": 0,
3728 3759 "createdAt": 1787461994394,
3729 3760 "promptEn": "a quiet mountain village at dawn (second attempt)",
3730   - "lastPlayedAt": 1788232130282
  3761 + "lastPlayedAt": 1788238893265
3731 3762 },
3732 3763 {
3733 3764 "id": "8e16b6faafd9",
... ... @@ -3742,7 +3773,7 @@
3742 3773 "likes": 0,
3743 3774 "createdAt": 1787455895150,
3744 3775 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3745   - "lastPlayedAt": 1788231515190
  3776 + "lastPlayedAt": 1788238458919
3746 3777 },
3747 3778 {
3748 3779 "id": "2a4e45820135",
... ... @@ -3757,7 +3788,7 @@
3757 3788 "likes": 0,
3758 3789 "createdAt": 1787453697093,
3759 3790 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3760   - "lastPlayedAt": 1788233511073
  3791 + "lastPlayedAt": 1788238106611
3761 3792 },
3762 3793 {
3763 3794 "id": "bfe13d1cc4a2",
... ... @@ -3771,7 +3802,8 @@
3771 3802 "partition": "text",
3772 3803 "likes": 0,
3773 3804 "createdAt": 1787452521817,
3774   - "promptEn": "protagonist leaps across rooftops fleeing pursuers"
  3805 + "promptEn": "protagonist leaps across rooftops fleeing pursuers",
  3806 + "lastPlayedAt": 1788238250741
3775 3807 },
3776 3808 {
3777 3809 "id": "f36cdd73ff1b",
... ... @@ -3786,7 +3818,7 @@
3786 3818 "likes": 0,
3787 3819 "createdAt": 1787450754171,
3788 3820 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3789   - "lastPlayedAt": 1788232430442
  3821 + "lastPlayedAt": 1788237818368
3790 3822 },
3791 3823 {
3792 3824 "id": "fd0183c54763",
... ... @@ -3800,7 +3832,8 @@
3800 3832 "partition": "text",
3801 3833 "likes": 0,
3802 3834 "createdAt": 1787447175021,
3803   - "promptEn": "protagonist leaps across rooftops fleeing pursuers"
  3835 + "promptEn": "protagonist leaps across rooftops fleeing pursuers",
  3836 + "lastPlayedAt": 1788238641079
3804 3837 },
3805 3838 {
3806 3839 "id": "4580ca66725a",
... ... @@ -3815,7 +3848,7 @@
3815 3848 "likes": 0,
3816 3849 "createdAt": 1787444313690,
3817 3850 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3818   - "lastPlayedAt": 1788232555823
  3851 + "lastPlayedAt": 1788239810044
3819 3852 },
3820 3853 {
3821 3854 "id": "e56962bd770f",
... ... @@ -3830,7 +3863,7 @@
3830 3863 "likes": 0,
3831 3864 "createdAt": 1787442174982,
3832 3865 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3833   - "lastPlayedAt": 1788233641108
  3866 + "lastPlayedAt": 1788237380102
3834 3867 },
3835 3868 {
3836 3869 "id": "18105bc2900b",
... ... @@ -3845,7 +3878,7 @@
3845 3878 "likes": 0,
3846 3879 "createdAt": 1787437988124,
3847 3880 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3848   - "lastPlayedAt": 1788233437022
  3881 + "lastPlayedAt": 1788238979307
3849 3882 },
3850 3883 {
3851 3884 "id": "e497093c79ce",
... ... @@ -3860,7 +3893,7 @@
3860 3893 "likes": 0,
3861 3894 "createdAt": 1787436029475,
3862 3895 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3863   - "lastPlayedAt": 1788233236940
  3896 + "lastPlayedAt": 1788234989999
3864 3897 },
3865 3898 {
3866 3899 "id": "896859e59dae",
... ... @@ -3875,7 +3908,7 @@
3875 3908 "likes": 0,
3876 3909 "createdAt": 1787432295531,
3877 3910 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3878   - "lastPlayedAt": 1788232582552
  3911 + "lastPlayedAt": 1788236229080
3879 3912 },
3880 3913 {
3881 3914 "id": "79368c9cf499",
... ... @@ -3890,7 +3923,7 @@
3890 3923 "likes": 0,
3891 3924 "createdAt": 1787428860940,
3892 3925 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3893   - "lastPlayedAt": 1788232711826
  3926 + "lastPlayedAt": 1788236855644
3894 3927 },
3895 3928 {
3896 3929 "id": "dbc7c27b72ba",
... ... @@ -3905,7 +3938,7 @@
3905 3938 "likes": 0,
3906 3939 "createdAt": 1787426955312,
3907 3940 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3908   - "lastPlayedAt": 1788232996834
  3941 + "lastPlayedAt": 1788236901679
3909 3942 },
3910 3943 {
3911 3944 "id": "734d28c52670",
... ... @@ -3920,7 +3953,7 @@
3920 3953 "likes": 0,
3921 3954 "createdAt": 1787426484939,
3922 3955 "promptEn": "small boat sailing through a misty forest river",
3923   - "lastPlayedAt": 1788232264353
  3956 + "lastPlayedAt": 1788236929703
3924 3957 },
3925 3958 {
3926 3959 "id": "9fbcf0d87021",
... ... @@ -3935,7 +3968,7 @@
3935 3968 "likes": 0,
3936 3969 "createdAt": 1787425932119,
3937 3970 "promptEn": "lighthouse on a cliff at sunset, seagulls flying",
3938   - "lastPlayedAt": 1788231891592
  3971 + "lastPlayedAt": 1788239634566
3939 3972 },
3940 3973 {
3941 3974 "id": "cf2015093ec4",
... ... @@ -3950,7 +3983,7 @@
3950 3983 "likes": 0,
3951 3984 "createdAt": 1787415067615,
3952 3985 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3953   - "lastPlayedAt": 1788233819198
  3986 + "lastPlayedAt": 1788239461561
3954 3987 },
3955 3988 {
3956 3989 "id": "9bcff387cddf",
... ... @@ -3965,7 +3998,7 @@
3965 3998 "likes": 0,
3966 3999 "createdAt": 1787413999418,
3967 4000 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3968   - "lastPlayedAt": 1788232955824
  4001 + "lastPlayedAt": 1788239201420
3969 4002 },
3970 4003 {
3971 4004 "id": "ade0318a7a4a",
... ... @@ -3980,7 +4013,7 @@
3980 4013 "likes": 0,
3981 4014 "createdAt": 1787413511959,
3982 4015 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
3983   - "lastPlayedAt": 1788233218933
  4016 + "lastPlayedAt": 1788237392113
3984 4017 },
3985 4018 {
3986 4019 "id": "5d4f40bf0889",
... ... @@ -3994,7 +4027,8 @@
3994 4027 "partition": "text",
3995 4028 "likes": 0,
3996 4029 "createdAt": 1787412952086,
3997   - "promptEn": "protagonist leaps across rooftops fleeing pursuers"
  4030 + "promptEn": "protagonist leaps across rooftops fleeing pursuers",
  4031 + "lastPlayedAt": 1788238436902
3998 4032 },
3999 4033 {
4000 4034 "id": "59fd7a087f5b",
... ... @@ -4008,7 +4042,8 @@
4008 4042 "partition": "text",
4009 4043 "likes": 0,
4010 4044 "createdAt": 1787412606465,
4011   - "promptEn": "protagonist leaps across rooftops fleeing pursuers"
  4045 + "promptEn": "protagonist leaps across rooftops fleeing pursuers",
  4046 + "lastPlayedAt": 1788238819228
4012 4047 },
4013 4048 {
4014 4049 "id": "29a99dc412b2",
... ... @@ -4022,7 +4057,8 @@
4022 4057 "partition": "text",
4023 4058 "likes": 0,
4024 4059 "createdAt": 1787412150062,
4025   - "promptEn": "protagonist leaps across rooftops fleeing pursuers"
  4060 + "promptEn": "protagonist leaps across rooftops fleeing pursuers",
  4061 + "lastPlayedAt": 1788239387526
4026 4062 },
4027 4063 {
4028 4064 "id": "9d2486d6f969",
... ... @@ -4036,7 +4072,8 @@
4036 4072 "partition": "text",
4037 4073 "likes": 0,
4038 4074 "createdAt": 1787397450973,
4039   - "promptEn": "a cat sitting on a windowsill"
  4075 + "promptEn": "a cat sitting on a windowsill",
  4076 + "lastPlayedAt": 1788235350315
4040 4077 },
4041 4078 {
4042 4079 "id": "580d1d722adc",
... ... @@ -4051,7 +4088,7 @@
4051 4088 "likes": 0,
4052 4089 "createdAt": 1787388090646,
4053 4090 "promptEn": "a cat sitting on a windowsill",
4054   - "lastPlayedAt": 1788233511058
  4091 + "lastPlayedAt": 1788235956854
4055 4092 },
4056 4093 {
4057 4094 "id": "c551457bc55e",
... ... @@ -4065,7 +4102,7 @@
4065 4102 "partition": "text",
4066 4103 "likes": 0,
4067 4104 "createdAt": 1787386316175,
4068   - "lastPlayedAt": 1788232954819,
  4105 + "lastPlayedAt": 1788239634555,
4069 4106 "promptEn": "a cat sitting on a windowsill"
4070 4107 },
4071 4108 {
... ... @@ -4081,7 +4118,7 @@
4081 4118 "likes": 0,
4082 4119 "createdAt": 1787384983124,
4083 4120 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
4084   - "lastPlayedAt": 1788233687131
  4121 + "lastPlayedAt": 1788239576512
4085 4122 },
4086 4123 {
4087 4124 "id": "7753a046ba79",
... ... @@ -4096,7 +4133,7 @@
4096 4133 "likes": 0,
4097 4134 "createdAt": 1787384417941,
4098 4135 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
4099   - "lastPlayedAt": 1788231933631
  4136 + "lastPlayedAt": 1788239696610
4100 4137 },
4101 4138 {
4102 4139 "id": "2cf5c8e67510",
... ... @@ -4111,7 +4148,7 @@
4111 4148 "likes": 0,
4112 4149 "createdAt": 1787383911831,
4113 4150 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
4114   - "lastPlayedAt": 1788232706647
  4151 + "lastPlayedAt": 1788239227431
4115 4152 },
4116 4153 {
4117 4154 "id": "9cba879e263d",
... ... @@ -4126,7 +4163,7 @@
4126 4163 "likes": 0,
4127 4164 "createdAt": 1787375413935,
4128 4165 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
4129   - "lastPlayedAt": 1788233360994
  4166 + "lastPlayedAt": 1788239578514
4130 4167 },
4131 4168 {
4132 4169 "id": "10112fb3c1fd",
... ... @@ -4140,7 +4177,8 @@
4140 4177 "partition": "fl2va",
4141 4178 "likes": 0,
4142 4179 "createdAt": 1787375413623,
4143   - "promptEn": "golden portal opens, man in green jacket throws a glowing spear"
  4180 + "promptEn": "golden portal opens, man in green jacket throws a glowing spear",
  4181 + "lastPlayedAt": 1788238094600
4144 4182 },
4145 4183 {
4146 4184 "id": "78e698c924b1",
... ... @@ -4154,7 +4192,8 @@
4154 4192 "partition": "fl2va",
4155 4193 "likes": 0,
4156 4194 "createdAt": 1787375413203,
4157   - "promptEn": "golden portal opens, man in green jacket throws a glowing spear"
  4195 + "promptEn": "golden portal opens, man in green jacket throws a glowing spear",
  4196 + "lastPlayedAt": 1788239359509
4158 4197 },
4159 4198 {
4160 4199 "id": "219ed88c3ea7",
... ... @@ -4169,7 +4208,7 @@
4169 4208 "likes": 0,
4170 4209 "createdAt": 1787375412946,
4171 4210 "promptEn": "golden portal opens, man in green jacket throws a glowing spear",
4172   - "lastPlayedAt": 1788233815949
  4211 + "lastPlayedAt": 1788238897263
4173 4212 },
4174 4213 {
4175 4214 "id": "a72236902363",
... ... @@ -4184,7 +4223,7 @@
4184 4223 "likes": 0,
4185 4224 "createdAt": 1787367454249,
4186 4225 "promptEn": "golden portal opens, man in green jacket throws a glowing spear",
4187   - "lastPlayedAt": 1788232523825
  4226 + "lastPlayedAt": 1788235826739
4188 4227 },
4189 4228 {
4190 4229 "id": "c48af2f34e62",
... ... @@ -4198,7 +4237,8 @@
4198 4237 "partition": "fl2va",
4199 4238 "likes": 0,
4200 4239 "createdAt": 1787367454065,
4201   - "promptEn": "golden portal opens, man in green jacket throws a glowing spear"
  4240 + "promptEn": "golden portal opens, man in green jacket throws a glowing spear",
  4241 + "lastPlayedAt": 1788238623063
4202 4242 },
4203 4243 {
4204 4244 "id": "e6c241be377f",
... ... @@ -4213,7 +4253,7 @@
4213 4253 "likes": 0,
4214 4254 "createdAt": 1787367453839,
4215 4255 "promptEn": "golden portal opens, man in green jacket throws a glowing spear",
4216   - "lastPlayedAt": 1788232667826
  4256 + "lastPlayedAt": 1788239055341
4217 4257 },
4218 4258 {
4219 4259 "id": "8402adae59dc",
... ... @@ -4228,7 +4268,7 @@
4228 4268 "likes": 0,
4229 4269 "createdAt": 1787363956909,
4230 4270 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
4231   - "lastPlayedAt": 1788232803824
  4271 + "lastPlayedAt": 1788238048564
4232 4272 },
4233 4273 {
4234 4274 "id": "5692965402f5",
... ... @@ -4243,7 +4283,7 @@
4243 4283 "likes": 0,
4244 4284 "createdAt": 1787332996051,
4245 4285 "promptEn": "golden portal opens, man in green jacket throws a glowing spear",
4246   - "lastPlayedAt": 1788232624582
  4286 + "lastPlayedAt": 1788238937288
4247 4287 },
4248 4288 {
4249 4289 "id": "bf7d973d02a3",
... ... @@ -4258,7 +4298,7 @@
4258 4298 "likes": 0,
4259 4299 "createdAt": 1787332047061,
4260 4300 "promptEn": "golden portal opens, man in green jacket throws a glowing spear",
4261   - "lastPlayedAt": 1788231825536
  4301 + "lastPlayedAt": 1788238663091
4262 4302 },
4263 4303 {
4264 4304 "id": "a5688d8f5db3",
... ... @@ -4272,7 +4312,8 @@
4272 4312 "partition": "fl2va",
4273 4313 "likes": 0,
4274 4314 "createdAt": 1787330101793,
4275   - "promptEn": "a red apple on a wooden table, cinematic lighting"
  4315 + "promptEn": "a red apple on a wooden table, cinematic lighting",
  4316 + "lastPlayedAt": 1788239409539
4276 4317 },
4277 4318 {
4278 4319 "id": "0024d3c2d4cb",
... ... @@ -4302,7 +4343,7 @@
4302 4343 "likes": 0,
4303 4344 "createdAt": 1787326019290,
4304 4345 "promptEn": "misty Jiangnan old town at dawn, woman walking with an oil-paper umbrella",
4305   - "lastPlayedAt": 1788232768693
  4346 + "lastPlayedAt": 1788239121373
4306 4347 },
4307 4348 {
4308 4349 "id": "fd8534ce588c",
... ... @@ -4317,7 +4358,7 @@
4317 4358 "likes": 0,
4318 4359 "createdAt": 1787309864037,
4319 4360 "promptEn": "transparent gaming mouse product film, blue and orange lighting",
4320   - "lastPlayedAt": 1788233134901
  4361 + "lastPlayedAt": 1788237984509
4321 4362 },
4322 4363 {
4323 4364 "id": "073f5df6427a",
... ... @@ -4331,7 +4372,8 @@
4331 4372 "partition": "text",
4332 4373 "likes": 0,
4333 4374 "createdAt": 1787305544815,
4334   - "promptEn": "transparent gaming mouse product film, blue and orange lighting"
  4375 + "promptEn": "transparent gaming mouse product film, blue and orange lighting",
  4376 + "lastPlayedAt": 1788239497567
4335 4377 },
4336 4378 {
4337 4379 "id": "e23b04ac2c78",
... ... @@ -4346,7 +4388,7 @@
4346 4388 "likes": 0,
4347 4389 "createdAt": 1787304875681,
4348 4390 "promptEn": "protagonist leaps across rooftops fleeing pursuers",
4349   - "lastPlayedAt": 1788232911824
  4391 + "lastPlayedAt": 1788239217427
4350 4392 },
4351 4393 {
4352 4394 "id": "c84bc1c7bfa0",
... ... @@ -4360,7 +4402,8 @@
4360 4402 "partition": "ref2va",
4361 4403 "likes": 0,
4362 4404 "createdAt": 1787304031214,
4363   - "promptEn": "constable asks how she's sure it's not human, she says a person would make a sound"
  4405 + "promptEn": "constable asks how she's sure it's not human, she says a person would make a sound",
  4406 + "lastPlayedAt": 1788239229433
4364 4407 },
4365 4408 {
4366 4409 "id": "23d2fd81eb4d",
... ... @@ -4375,7 +4418,7 @@
4375 4418 "likes": 0,
4376 4419 "createdAt": 1787304031103,
4377 4420 "promptEn": "constable asks how she's sure it's not human, she says a person would make a sound",
4378   - "lastPlayedAt": 1788232754686
  4421 + "lastPlayedAt": 1788239666580
4379 4422 },
4380 4423 {
4381 4424 "id": "807e042dce0d",
... ... @@ -4389,7 +4432,8 @@
4389 4432 "partition": "text",
4390 4433 "likes": 0,
4391 4434 "createdAt": 1787290119529,
4392   - "promptEn": "misty Jiangnan old town at dawn, woman walking with an oil-paper umbrella"
  4435 + "promptEn": "misty Jiangnan old town at dawn, woman walking with an oil-paper umbrella",
  4436 + "lastPlayedAt": 1788237290006
4393 4437 },
4394 4438 {
4395 4439 "id": "06bb43196c46",
... ... @@ -4404,7 +4448,7 @@
4404 4448 "likes": 0,
4405 4449 "createdAt": 1787290119114,
4406 4450 "promptEn": "misty Jiangnan old town at dawn, woman walking with an oil-paper umbrella",
4407   - "lastPlayedAt": 1788232126281
  4451 + "lastPlayedAt": 1788238827234
4408 4452 },
4409 4453 {
4410 4454 "id": "0f22388f7847",
... ... @@ -4418,7 +4462,8 @@
4418 4462 "partition": "text",
4419 4463 "likes": 0,
4420 4464 "createdAt": 1787290118806,
4421   - "promptEn": "misty Jiangnan old town at dawn, woman walking with an oil-paper umbrella"
  4465 + "promptEn": "misty Jiangnan old town at dawn, woman walking with an oil-paper umbrella",
  4466 + "lastPlayedAt": 1788235388347
4422 4467 },
4423 4468 {
4424 4469 "id": "0c67d73b06d4",
... ... @@ -4433,7 +4478,7 @@
4433 4478 "likes": 0,
4434 4479 "createdAt": 1787231456079,
4435 4480 "promptEn": "constable burns the beauty portrait with the lamp, the door opens on its own",
4436   - "lastPlayedAt": 1788233411014
  4481 + "lastPlayedAt": 1788237107861
4437 4482 },
4438 4483 {
4439 4484 "id": "66158f543a20",
... ... @@ -4448,7 +4493,7 @@
4448 4493 "likes": 0,
4449 4494 "createdAt": 1787231440149,
4450 4495 "promptEn": "constable burns the beauty portrait with the lamp, the door opens on its own",
4451   - "lastPlayedAt": 1788233266956
  4496 + "lastPlayedAt": 1788239377519
4452 4497 },
4453 4498 {
4454 4499 "id": "63c91f4b6bc7",
... ... @@ -4463,7 +4508,7 @@
4463 4508 "likes": 0,
4464 4509 "createdAt": 1787225897650,
4465 4510 "promptEn": "constable burns the beauty portrait with the lamp, the door opens on its own",
4466   - "lastPlayedAt": 1788232632587
  4511 + "lastPlayedAt": 1788236947717
4467 4512 },
4468 4513 {
4469 4514 "id": "ed3663dcda7f",
... ... @@ -4477,7 +4522,7 @@
4477 4522 "partition": "fl2va",
4478 4523 "likes": 0,
4479 4524 "createdAt": 1787224701504,
4480   - "lastPlayedAt": 1788232507824,
  4525 + "lastPlayedAt": 1788238719143,
4481 4526 "promptEn": "constable burns the beauty portrait with the lamp, the door opens on its own"
4482 4527 },
4483 4528 {
... ... @@ -4492,7 +4537,8 @@
4492 4537 "partition": "fl2va",
4493 4538 "likes": 0,
4494 4539 "createdAt": 1787224519456,
4495   - "promptEn": "constable burns the beauty portrait with the lamp, the door opens on its own"
  4540 + "promptEn": "constable burns the beauty portrait with the lamp, the door opens on its own",
  4541 + "lastPlayedAt": 1788239742011
4496 4542 },
4497 4543 {
4498 4544 "id": "a1cc621cc6b2",
... ... @@ -4506,6 +4552,7 @@
4506 4552 "partition": "ref2va",
4507 4553 "likes": 0,
4508 4554 "createdAt": 1787220579189,
4509   - "promptEn": "gothic castle at night, vampire noble carries a princess, rose petals falling"
  4555 + "promptEn": "gothic castle at night, vampire noble carries a princess, rose petals falling",
  4556 + "lastPlayedAt": 1788239073351
4510 4557 }
4511 4558 ]
4512 4559 \ No newline at end of file
... ...
live.css
... ... @@ -176,32 +176,84 @@ body {
176 176 color: var(--muted);
177 177 padding: 12px 14px 6px;
178 178 }
179   -.say-panel { padding: 0 14px 14px; display: flex; flex-direction: column; gap: 8px; flex: 1; }
180   -.say-panel textarea {
  179 +.chat-list {
  180 + flex: 1;
  181 + overflow-y: auto;
  182 + padding: 0 14px;
  183 + display: flex;
  184 + flex-direction: column;
  185 + gap: 8px;
  186 +}
  187 +.chat-msg { font-size: 12.5px; line-height: 1.5; word-break: break-word; }
  188 +.chat-nick { color: var(--accent); font-weight: 700; margin-right: 6px; }
  189 +.chat-text { color: #e4e4e8; }
  190 +.chat-status { display: block; font-size: 10.5px; color: var(--muted); font-weight: 700; margin-top: 1px; }
  191 +
  192 +.chat-input-row { display: flex; gap: 6px; padding: 10px 14px 4px; }
  193 +.chat-input-row input {
  194 + flex: 1;
  195 + min-width: 0;
181 196 background: #fff;
182 197 border: none;
183   - border-radius: 12px;
184   - padding: 10px 12px;
  198 + border-radius: 999px;
  199 + padding: 9px 14px;
185 200 color: var(--accent-text);
186 201 font-weight: 700;
187 202 font-size: 13px;
188   - resize: none;
189   - min-height: 90px;
190 203 font-family: inherit;
191 204 }
192   -.say-panel textarea::placeholder { color: #8a8a8a; font-weight: 400; }
193   -.say-panel button {
  205 +.chat-input-row input::placeholder { color: #8a8a8a; font-weight: 400; }
  206 +.chat-input-row button {
  207 + background: var(--accent);
  208 + color: var(--accent-text);
  209 + border: none;
  210 + border-radius: 999px;
  211 + padding: 0 16px;
  212 + font-weight: 700;
  213 + font-size: 13px;
  214 + cursor: pointer;
  215 +}
  216 +.chat-input-row button:disabled { opacity: .5; cursor: not-allowed; }
  217 +.say-hint { padding: 0 14px 10px; font-size: 11.5px; font-weight: 700; color: var(--muted); min-height: 1.4em; }
  218 +
  219 +/* ---- nickname modal ---- */
  220 +.nickname-modal {
  221 + position: fixed;
  222 + inset: 0;
  223 + z-index: 10;
  224 + display: flex;
  225 + align-items: center;
  226 + justify-content: center;
  227 + background: rgba(0,0,0,.7);
  228 +}
  229 +.nickname-box {
  230 + background: #fff;
  231 + color: var(--accent-text);
  232 + border-radius: 16px;
  233 + padding: 20px;
  234 + width: 260px;
  235 + display: flex;
  236 + flex-direction: column;
  237 + gap: 10px;
  238 +}
  239 +.nickname-title { font-weight: 700; font-size: 14px; }
  240 +.nickname-box input {
  241 + border: 1px solid #ddd;
  242 + border-radius: 10px;
  243 + padding: 8px 12px;
  244 + font-size: 13px;
  245 + font-family: inherit;
  246 +}
  247 +.nickname-box button {
194 248 background: var(--accent);
195 249 color: var(--accent-text);
196 250 border: none;
197 251 border-radius: 999px;
198   - padding: 10px 0;
  252 + padding: 9px 0;
199 253 font-weight: 700;
200 254 font-size: 13px;
201 255 cursor: pointer;
202 256 }
203   -.say-panel button:disabled { opacity: .5; cursor: not-allowed; }
204   -.say-hint { font-size: 11.5px; font-weight: 700; color: var(--muted); min-height: 1.4em; }
205 257  
206 258 /* ---- mobile sub-tabs (live mode only) ---- */
207 259 .mobile-subtabs {
... ...
live.html
... ... @@ -40,29 +40,39 @@
40 40 </div>
41 41  
42 42 <aside class="right-col" id="rightCol" hidden>
43   - <div class="section-title">接下来想看什么</div>
44   - <div class="say-panel">
45   - <textarea id="sayInput" placeholder="接下来想看什么…" maxlength="300"></textarea>
46   - <button id="sayBtn">发送</button>
47   - <div class="say-hint" id="sayHint"></div>
  43 + <div class="section-title">聊天</div>
  44 + <div class="chat-list" id="chatList"></div>
  45 + <div class="chat-input-row">
  46 + <input type="text" id="chatInput" placeholder="说点什么,或者想看什么…" maxlength="300" />
  47 + <button id="chatSendBtn">发送</button>
48 48 </div>
  49 + <div class="say-hint" id="chatHint"></div>
49 50 </aside>
50 51  
51 52 <!-- flex item of #body (not .channel) so `order` in the mobile media
52 53 query actually places it between the player and left/right-col. -->
53 54 <div class="mobile-subtabs" id="mobileSubtabs" hidden>
54 55 <button data-sub="queue" class="active">排队队列</button>
55   - <button data-sub="input">输入</button>
  56 + <button data-sub="input">聊天</button>
56 57 </div>
57 58 </div>
58 59 </div>
59 60  
  61 +<div class="nickname-modal" id="nicknameModal" hidden>
  62 + <div class="nickname-box">
  63 + <div class="nickname-title">先起个昵称才能发言</div>
  64 + <input type="text" id="nicknameInput" placeholder="你的昵称" maxlength="20" />
  65 + <button id="nicknameConfirmBtn">确定</button>
  66 + </div>
  67 +</div>
  68 +
60 69 <script>
61 70 let mode = "live";
62 71 let clips = []; // browse mode: full list
63 72 let currentIndex = -1;
64 73 let currentClipId = null;
65 74 let livePollTimer = null;
  75 +let chatPollTimer = null;
66 76 let advanceTimer = null; // precise, server-endAt-driven switch to the next clip — the 4s poll is just a resync/fallback
67 77 let upNextCache = [];
68 78 let mobileSub = "queue";
... ... @@ -75,11 +85,15 @@ const liveDot = document.getElementById(&quot;liveDot&quot;);
75 85 const leftCol = document.getElementById("leftCol");
76 86 const rightCol = document.getElementById("rightCol");
77 87 const mobileSubtabs = document.getElementById("mobileSubtabs");
78   -const sayInput = document.getElementById("sayInput");
79   -const sayBtn = document.getElementById("sayBtn");
80   -const sayHint = document.getElementById("sayHint");
  88 +const chatList = document.getElementById("chatList");
  89 +const chatInput = document.getElementById("chatInput");
  90 +const chatSendBtn = document.getElementById("chatSendBtn");
  91 +const chatHint = document.getElementById("chatHint");
81 92 const watchingCount = document.getElementById("watchingCount");
82 93 const muteBtn = document.getElementById("muteBtn");
  94 +const nicknameModal = document.getElementById("nicknameModal");
  95 +const nicknameInput = document.getElementById("nicknameInput");
  96 +const nicknameConfirmBtn = document.getElementById("nicknameConfirmBtn");
83 97 const clientId = Math.random().toString(36).slice(2);
84 98  
85 99 muteBtn.addEventListener("click", () => {
... ... @@ -96,6 +110,40 @@ function escapeHtml(s) {
96 110 return div.innerHTML;
97 111 }
98 112  
  113 +// ---- viewer identity: nickname + uid, stored in cookies ----
  114 +function getCookie(name) {
  115 + const m = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
  116 + return m ? decodeURIComponent(m[1]) : null;
  117 +}
  118 +function setCookie(name, value, days) {
  119 + document.cookie = `${name}=${encodeURIComponent(value)}; max-age=${days * 86400}; path=/; samesite=lax`;
  120 +}
  121 +function getIdentity() {
  122 + const uid = getCookie("viewer_uid");
  123 + const nickname = getCookie("viewer_nickname");
  124 + return uid && nickname ? { uid, nickname } : null;
  125 +}
  126 +function ensureIdentity() {
  127 + const existing = getIdentity();
  128 + if (existing) return Promise.resolve(existing);
  129 + return new Promise((resolve) => {
  130 + nicknameModal.hidden = false;
  131 + nicknameInput.value = "";
  132 + nicknameInput.focus();
  133 + const onConfirm = () => {
  134 + const nickname = nicknameInput.value.trim().slice(0, 20);
  135 + if (!nickname) return;
  136 + const uid = (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`);
  137 + setCookie("viewer_uid", uid, 365);
  138 + setCookie("viewer_nickname", nickname, 365);
  139 + nicknameModal.hidden = true;
  140 + nicknameConfirmBtn.removeEventListener("click", onConfirm);
  141 + resolve({ uid, nickname });
  142 + };
  143 + nicknameConfirmBtn.addEventListener("click", onConfirm);
  144 + });
  145 +}
  146 +
99 147 function renderBrowseList() {
100 148 if (clips.length === 0) {
101 149 clipList.innerHTML = `<div class="empty-hint">片段库为空</div>`;
... ... @@ -148,16 +196,29 @@ async function loadBrowse(sort) {
148 196 if (clips.length) playBrowse(0);
149 197 }
150 198  
  199 +// Left queue: prompt + who asked for it only — no resolution/duration/date.
151 200 function renderLiveList(state) {
152 201 listTitle.textContent = "排队队列";
153 202 const upcoming = state.upNext || [];
154 203 const queue = state.queue || [];
155 204 const statusLabel = { generating: "生成中…", matched: "即将播放", failed: "❌ 生成失败" };
156 205 const queueHtml = queue
157   - .map((q) => `<div class="clip-row"><div class="prompt">💬 ${escapeHtml(q.prompt)}</div><div class="meta">${statusLabel[q.status] || q.status}</div></div>`)
  206 + .map(
  207 + (q) => `
  208 + <div class="clip-row">
  209 + <div class="prompt">💬 ${escapeHtml(q.prompt)}</div>
  210 + <div class="meta">${q.nickname ? `${escapeHtml(q.nickname)} · ` : ""}${statusLabel[q.status] || q.status}</div>
  211 + </div>`
  212 + )
158 213 .join("");
159 214 const upcomingHtml = upcoming
160   - .map((c) => `<div class="clip-row"><div class="prompt">${escapeHtml(c.prompt)}</div><div class="meta">${fmtMeta(c)}</div></div>`)
  215 + .map(
  216 + (c) => `
  217 + <div class="clip-row">
  218 + <div class="prompt">${escapeHtml(c.prompt)}</div>
  219 + ${c.requestedBy ? `<div class="meta">by ${escapeHtml(c.requestedBy)}</div>` : ""}
  220 + </div>`
  221 + )
161 222 .join("");
162 223 clipList.innerHTML = queueHtml + upcomingHtml || `<div class="empty-hint">暂无排队</div>`;
163 224 }
... ... @@ -207,33 +268,54 @@ async function pollLive() {
207 268 }
208 269 }
209 270  
210   -async function submitPrompt() {
211   - const prompt = sayInput.value.trim();
212   - if (!prompt) return;
213   - sayBtn.disabled = true;
  271 +const chatStatusLabel = { rejected: "❌ 未通过审核", generating: "生成中…", matched: "即将播放", scheduled: "即将播放", ready: "已排播放", failed: "❌ 生成失败" };
  272 +function renderChat(messages) {
  273 + const atBottom = chatList.scrollHeight - chatList.scrollTop - chatList.clientHeight < 40;
  274 + chatList.innerHTML = messages
  275 + .map((m) => {
  276 + const status = m.status && chatStatusLabel[m.status] ? `<span class="chat-status">${chatStatusLabel[m.status]}</span>` : "";
  277 + return `<div class="chat-msg"><span class="chat-nick">${escapeHtml(m.nickname)}</span><span class="chat-text">${escapeHtml(m.text)}</span>${status}</div>`;
  278 + })
  279 + .join("");
  280 + if (atBottom) chatList.scrollTop = chatList.scrollHeight;
  281 +}
  282 +
  283 +async function pollChat() {
214 284 try {
215   - const res = await fetch("/api/prompt", {
  285 + const res = await fetch(`/api/chat?cid=${clientId}`);
  286 + renderChat(await res.json());
  287 + } catch {
  288 + // transient — next poll retries
  289 + }
  290 +}
  291 +
  292 +async function sendChat() {
  293 + const text = chatInput.value.trim();
  294 + if (!text) return;
  295 + const identity = await ensureIdentity();
  296 + chatSendBtn.disabled = true;
  297 + try {
  298 + const res = await fetch("/api/chat", {
216 299 method: "POST",
217 300 headers: { "Content-Type": "application/json" },
218   - body: JSON.stringify({ prompt }),
  301 + body: JSON.stringify({ uid: identity.uid, nickname: identity.nickname, text }),
219 302 });
220 303 const data = await res.json();
221 304 if (res.status === 429) {
222   - sayHint.textContent = data.error;
  305 + chatHint.textContent = data.error;
223 306 return;
224 307 }
225   - sayInput.value = "";
226   - sayHint.textContent = data.status === "matched" ? "已匹配片段,即将播放" : data.status === "generating" ? "库里没有,已提交生成,请稍候…" : "";
  308 + chatInput.value = "";
  309 + chatHint.textContent =
  310 + data.status === "rejected" ? `未通过审核:${data.reason}` : data.status === "matched" ? "已匹配片段,即将播放" : data.status === "generating" ? "库里没有,已提交生成,请稍候…" : "";
  311 + pollChat();
227 312 } finally {
228   - sayBtn.disabled = false;
  313 + chatSendBtn.disabled = false;
229 314 }
230 315 }
231   -sayBtn.addEventListener("click", submitPrompt);
232   -sayInput.addEventListener("keydown", (e) => {
233   - if (e.key === "Enter" && !e.shiftKey) {
234   - e.preventDefault();
235   - submitPrompt();
236   - }
  316 +chatSendBtn.addEventListener("click", sendChat);
  317 +chatInput.addEventListener("keydown", (e) => {
  318 + if (e.key === "Enter") sendChat();
237 319 });
238 320  
239 321 function applyMobileSub() {
... ... @@ -252,6 +334,7 @@ async function setMode(newMode) {
252 334 mode = newMode;
253 335 document.querySelectorAll("#tabs button").forEach((b) => b.classList.toggle("active", b.dataset.mode === newMode));
254 336 clearInterval(livePollTimer);
  337 + clearInterval(chatPollTimer);
255 338 clearTimeout(advanceTimer);
256 339 currentClipId = null;
257 340 if (newMode === "live") {
... ... @@ -259,8 +342,9 @@ async function setMode(newMode) {
259 342 rightCol.hidden = false;
260 343 mobileSubtabs.hidden = false;
261 344 applyMobileSub();
262   - await pollLive();
  345 + await Promise.all([pollLive(), pollChat()]);
263 346 livePollTimer = setInterval(pollLive, 4000);
  347 + chatPollTimer = setInterval(pollChat, 3000);
264 348 } else {
265 349 liveDot.hidden = true;
266 350 rightCol.hidden = true;
... ...
server.js
... ... @@ -39,13 +39,13 @@ const LIVE_STATE_PATH = path.join(__dirname, &quot;live-state.json&quot;);
39 39 function loadLiveState() {
40 40 try {
41 41 const s = JSON.parse(fs.readFileSync(LIVE_STATE_PATH, "utf8"));
42   - return { schedule: s.schedule || [], promptQueue: s.promptQueue || [] };
  42 + return { schedule: s.schedule || [], promptQueue: s.promptQueue || [], chatMessages: s.chatMessages || [] };
43 43 } catch {
44   - return { schedule: [], promptQueue: [] };
  44 + return { schedule: [], promptQueue: [], chatMessages: [] };
45 45 }
46 46 }
47 47 function saveLiveState() {
48   - fs.writeFileSync(LIVE_STATE_PATH, JSON.stringify({ schedule, promptQueue }, null, 2));
  48 + fs.writeFileSync(LIVE_STATE_PATH, JSON.stringify({ schedule, promptQueue, chatMessages }, null, 2));
49 49 }
50 50  
51 51 function readJsonBody(req) {
... ... @@ -101,8 +101,52 @@ const H3_GEN_PARAMS = { width: 832, height: 480, frames: 120, steps: 20, turbo:
101 101 const FPS = 24;
102 102  
103 103 const initialLiveState = loadLiveState();
104   -let schedule = initialLiveState.schedule; // [{clip, startAt, endAt}]
105   -let promptQueue = initialLiveState.promptQueue; // [{id, prompt, status, clip?, error?, submittedAt, h3JobId?}]
  104 +let schedule = initialLiveState.schedule; // [{clip, startAt, endAt, requestedBy?}]
  105 +let promptQueue = initialLiveState.promptQueue; // [{id, prompt, nickname?, status, clip?, error?, submittedAt, h3JobId?}]
  106 +let chatMessages = initialLiveState.chatMessages; // [{id, uid, nickname, text, createdAt, status}]
  107 +
  108 +// ---- chat + moderation ----
  109 +// ponytail: a starter blocklist, not a real content-policy list — swap in
  110 +// whatever the actual moderation requirements are. Substring match on the
  111 +// lowercased text; cheap and deterministic. Anything this doesn't catch
  112 +// (content that's fine as *text* but unsafe to *generate*) falls through to
  113 +// the H3 console's own generation-time safety checker — a rejected/failed
  114 +// job there just surfaces as a normal "failed" queue entry.
  115 +const BANNED_WORDS = ["色情", "porn", "自杀", "血腥暴力", "毒品"];
  116 +function moderateText(text) {
  117 + const lower = text.toLowerCase();
  118 + const hit = BANNED_WORDS.find((w) => lower.includes(w));
  119 + if (hit) return { ok: false, reason: `包含违禁词` };
  120 + return { ok: true };
  121 +}
  122 +
  123 +const CHAT_HISTORY_LIMIT = 300;
  124 +function pushChatMessage(msg) {
  125 + chatMessages.push(msg);
  126 + if (chatMessages.length > CHAT_HISTORY_LIMIT) chatMessages = chatMessages.slice(-CHAT_HISTORY_LIMIT);
  127 +}
  128 +function updateChatStatus(id, status) {
  129 + const msg = chatMessages.find((m) => m.id === id);
  130 + if (msg) msg.status = status;
  131 +}
  132 +
  133 +// Shared by /api/chat (after moderation) and /api/prompt (direct API use,
  134 +// e.g. testing) — looks up a library match or kicks off generation, exactly
  135 +// like before chat existed. `nickname` is optional (empty for /api/prompt).
  136 +function submitPromptToQueue(id, prompt, nickname) {
  137 + const entry = { id, prompt, nickname: nickname || null, submittedAt: Date.now() };
  138 + const match = findLibraryMatch(normalizePrompt(prompt));
  139 + if (match) {
  140 + entry.status = "matched";
  141 + entry.clip = match;
  142 + promptQueue.push(entry);
  143 + return { status: "matched", clip: match };
  144 + }
  145 + entry.status = "generating";
  146 + promptQueue.push(entry);
  147 + generateForPrompt(entry); // fire and forget — polled via /api/live/state
  148 + return { status: "generating", id: entry.id };
  149 +}
106 150  
107 151 function normalizePrompt(s) {
108 152 return s.trim().toLowerCase().replace(/\s+/g, " ");
... ... @@ -138,11 +182,19 @@ function markPlayed(id) {
138 182 // status:"scheduled" — once a clip is in `schedule` it shows up via
139 183 // current/upNext, so keeping a second, never-cleared copy in promptQueue
140 184 // just grows forever and duplicates the same info.
  185 +// Returns {clip, requestedBy, chatId} — chatId lets the caller flip the
  186 +// originating chat message to "scheduled" once it's actually about to air.
141 187 function pickNextClip() {
142 188 const readyIdx = promptQueue.findIndex((q) => q.status === "ready");
143   - if (readyIdx !== -1) return promptQueue.splice(readyIdx, 1)[0].clip;
  189 + if (readyIdx !== -1) {
  190 + const q = promptQueue.splice(readyIdx, 1)[0];
  191 + return { clip: q.clip, requestedBy: q.nickname, chatId: q.id };
  192 + }
144 193 const matchedIdx = promptQueue.findIndex((q) => q.status === "matched");
145   - if (matchedIdx !== -1) return promptQueue.splice(matchedIdx, 1)[0].clip;
  194 + if (matchedIdx !== -1) {
  195 + const q = promptQueue.splice(matchedIdx, 1)[0];
  196 + return { clip: q.clip, requestedBy: q.nickname, chatId: q.id };
  197 + }
146 198  
147 199 const lib = getLibrary();
148 200 if (!lib.length) return null;
... ... @@ -153,7 +205,7 @@ function pickNextClip() {
153 205 // random among everything that isn't on cooldown, no bias toward "fresh".
154 206 const offCooldown = notPrev.filter((c) => !c.lastPlayedAt || now - c.lastPlayedAt > REPEAT_COOLDOWN_MS);
155 207 const pool = offCooldown.length ? offCooldown : notPrev;
156   - return pool[Math.floor(Math.random() * pool.length)];
  208 + return { clip: pool[Math.floor(Math.random() * pool.length)], requestedBy: null, chatId: null };
157 209 }
158 210  
159 211 const FAILED_ENTRY_TTL_MS = 20000; // how long a "generation failed" entry stays visible before cleanup
... ... @@ -162,11 +214,13 @@ function ensureSchedule() {
162 214 const now = Date.now();
163 215 while (schedule.length > 1 && schedule[1].endAt <= now) schedule.shift();
164 216 while (schedule.length === 0 || schedule[schedule.length - 1].endAt < now + FILLER_DURATION_MS) {
165   - const clip = pickNextClip();
166   - if (!clip) break;
  217 + const picked = pickNextClip();
  218 + if (!picked || !picked.clip) break;
  219 + const { clip, requestedBy, chatId } = picked;
167 220 markPlayed(clip.id);
  221 + if (chatId) updateChatStatus(chatId, "scheduled");
168 222 const startAt = schedule.length ? schedule[schedule.length - 1].endAt : now;
169   - schedule.push({ clip, startAt, endAt: startAt + clip.duration * 1000 });
  223 + schedule.push({ clip, startAt, endAt: startAt + clip.duration * 1000, requestedBy });
170 224 }
171 225 promptQueue = promptQueue.filter((q) => q.status !== "failed" || now - q.failedAt < FAILED_ENTRY_TTL_MS);
172 226  
... ... @@ -229,6 +283,7 @@ async function generateForPrompt(entry) {
229 283 saveLibrary(lib);
230 284 entry.status = "ready";
231 285 entry.clip = clip;
  286 + updateChatStatus(entry.id, "ready");
232 287 saveLiveState();
233 288 return;
234 289 }
... ... @@ -239,6 +294,7 @@ async function generateForPrompt(entry) {
239 294 entry.status = "failed";
240 295 entry.error = err.message;
241 296 entry.failedAt = Date.now();
  297 + updateChatStatus(entry.id, "failed");
242 298 saveLiveState();
243 299 }
244 300 }
... ... @@ -310,11 +366,11 @@ http
310 366 JSON.stringify({
311 367 now,
312 368 watching: viewers.size,
313   - current: cur ? { clip: cur.clip, startAt: cur.startAt, endAt: cur.endAt } : null,
314   - upNext: schedule.slice(idx + 1, idx + 4).map((s) => s.clip),
  369 + current: cur ? { clip: cur.clip, startAt: cur.startAt, endAt: cur.endAt, requestedBy: cur.requestedBy || null } : null,
  370 + upNext: schedule.slice(idx + 1, idx + 4).map((s) => ({ ...s.clip, requestedBy: s.requestedBy || null })),
315 371 queue: promptQueue
316 372 .filter((q) => ["matched", "generating", "failed"].includes(q.status))
317   - .map((q) => ({ prompt: q.prompt, status: q.status, error: q.error })),
  373 + .map((q) => ({ prompt: q.prompt, nickname: q.nickname, status: q.status, error: q.error })),
318 374 })
319 375 );
320 376 return;
... ... @@ -334,23 +390,63 @@ http
334 390 res.end(JSON.stringify({ error: "prompt required, max 300 chars" }));
335 391 return;
336 392 }
337   - const entry = { id: Date.now().toString(36), prompt, submittedAt: Date.now() };
338   - const match = findLibraryMatch(normalizePrompt(prompt));
339   - if (match) {
340   - entry.status = "matched";
341   - entry.clip = match;
342   - promptQueue.push(entry);
343   - saveLiveState();
344   - res.writeHead(200, { "Content-Type": "application/json" });
345   - res.end(JSON.stringify({ status: "matched", clip: match }));
346   - } else {
347   - entry.status = "generating";
348   - promptQueue.push(entry);
  393 + const result = submitPromptToQueue(Date.now().toString(36), prompt, null);
  394 + saveLiveState();
  395 + res.writeHead(200, { "Content-Type": "application/json" });
  396 + res.end(JSON.stringify(result));
  397 + })
  398 + .catch(() => {
  399 + res.writeHead(400, { "Content-Type": "application/json" });
  400 + res.end(JSON.stringify({ error: "invalid JSON body" }));
  401 + });
  402 + return;
  403 + }
  404 +
  405 + if (pathname === "/api/chat" && req.method === "GET") {
  406 + const cid = new URL(req.url, "http://x").searchParams.get("cid");
  407 + if (cid) viewers.set(cid, Date.now());
  408 + res.writeHead(200, { "Content-Type": "application/json" });
  409 + res.end(JSON.stringify(chatMessages.slice(-100)));
  410 + return;
  411 + }
  412 +
  413 + if (pathname === "/api/chat" && req.method === "POST") {
  414 + if (isRateLimited(req.socket.remoteAddress)) {
  415 + res.writeHead(429, { "Content-Type": "application/json" });
  416 + res.end(JSON.stringify({ error: `最多每 ${RATE_LIMIT_WINDOW_MS / 60000} 分钟发送 ${RATE_LIMIT_MAX} 条,请稍后再试` }));
  417 + return;
  418 + }
  419 + readJsonBody(req)
  420 + .then((body) => {
  421 + const uid = (body.uid || "").trim();
  422 + const nickname = (body.nickname || "").trim().slice(0, 20);
  423 + const text = (body.text || "").trim();
  424 + if (!uid || !nickname || !text || text.length > 300) {
  425 + res.writeHead(400, { "Content-Type": "application/json" });
  426 + res.end(JSON.stringify({ error: "uid, nickname, text required, text max 300 chars" }));
  427 + return;
  428 + }
  429 + const id = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
  430 + const msg = { id, uid, nickname, text, createdAt: Date.now() };
  431 +
  432 + const mod = moderateText(text);
  433 + if (!mod.ok) {
  434 + msg.status = "rejected";
  435 + msg.rejectReason = mod.reason;
  436 + pushChatMessage(msg);
349 437 saveLiveState();
350   - generateForPrompt(entry); // fire and forget — polled via /api/live/state
351 438 res.writeHead(200, { "Content-Type": "application/json" });
352   - res.end(JSON.stringify({ status: "generating", id: entry.id }));
  439 + res.end(JSON.stringify({ status: "rejected", reason: mod.reason }));
  440 + return;
353 441 }
  442 +
  443 + msg.status = "approved";
  444 + pushChatMessage(msg);
  445 + const result = submitPromptToQueue(id, text, nickname);
  446 + msg.status = result.status; // "matched" | "generating"
  447 + saveLiveState();
  448 + res.writeHead(200, { "Content-Type": "application/json" });
  449 + res.end(JSON.stringify(result));
354 450 })
355 451 .catch(() => {
356 452 res.writeHead(400, { "Content-Type": "application/json" });
... ...