voxflow 1.15.0 → 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/bin/voxflow.js +27 -0
- package/dist/index.js +1 -1
- package/dist/remotion-bundle/02a2fb2eb80bc7bf.woff2 +0 -0
- package/dist/remotion-bundle/052ca5351e5e06ba.woff2 +0 -0
- package/dist/remotion-bundle/05853dd28f4019cb.woff2 +0 -0
- package/dist/remotion-bundle/072ead3737f7c0d0.woff2 +0 -0
- package/dist/remotion-bundle/07d4248613c86a2e.woff2 +0 -0
- package/dist/remotion-bundle/0884a5c2d1d2d99b.woff2 +0 -0
- package/dist/remotion-bundle/0b0e185b2752095e.woff2 +0 -0
- package/dist/remotion-bundle/0e66c11bde067d91.woff2 +0 -0
- package/dist/remotion-bundle/0f7794cfba2c5d21.woff2 +0 -0
- package/dist/remotion-bundle/0fdbae5a4365783a.woff2 +0 -0
- package/dist/remotion-bundle/112.bundle.js +11 -0
- package/dist/remotion-bundle/112.bundle.js.map +1 -0
- package/dist/remotion-bundle/113.bundle.js +11 -0
- package/dist/remotion-bundle/113.bundle.js.map +1 -0
- package/dist/remotion-bundle/119cae0c4c16f7ed.woff2 +0 -0
- package/dist/remotion-bundle/14725f649fd1e78c.woff2 +0 -0
- package/dist/remotion-bundle/14abe9e3f95f7888.woff2 +0 -0
- package/dist/remotion-bundle/163.bundle.js +14678 -0
- package/dist/remotion-bundle/163.bundle.js.map +1 -0
- package/dist/remotion-bundle/1808c54072bf6d14.woff2 +0 -0
- package/dist/remotion-bundle/18948bec3e3012fe.woff2 +0 -0
- package/dist/remotion-bundle/1a661c60d0fc84fc.woff2 +0 -0
- package/dist/remotion-bundle/1af94941e1bc7e1e.woff2 +0 -0
- package/dist/remotion-bundle/1bee0219595f606c.woff2 +0 -0
- package/dist/remotion-bundle/1bfd5da7ce9d4ec4.woff2 +0 -0
- package/dist/remotion-bundle/1c158d56f1884f3f.woff2 +0 -0
- package/dist/remotion-bundle/1cf5e88e667610eb.woff2 +0 -0
- package/dist/remotion-bundle/1d431bd10f53c481.woff2 +0 -0
- package/dist/remotion-bundle/1d701a81a7670db2.woff2 +0 -0
- package/dist/remotion-bundle/1da0fecad4240f16.woff2 +0 -0
- package/dist/remotion-bundle/1ed14d3d0c5c63fe.woff2 +0 -0
- package/dist/remotion-bundle/1edfecf40e586f53.woff2 +0 -0
- package/dist/remotion-bundle/1f479711bc34b054.woff +0 -0
- package/dist/remotion-bundle/1f86e54a0ff5fcd1.woff2 +0 -0
- package/dist/remotion-bundle/2043ea87d9aabd11.woff2 +0 -0
- package/dist/remotion-bundle/20563c39ee8a0e40.woff2 +0 -0
- package/dist/remotion-bundle/20c231590fd12c44.woff2 +0 -0
- package/dist/remotion-bundle/20ce61713f754c07.woff2 +0 -0
- package/dist/remotion-bundle/21eb9306fce24bb1.woff2 +0 -0
- package/dist/remotion-bundle/244bf71c0cc851af.woff2 +0 -0
- package/dist/remotion-bundle/274d4cfc02bffbcb.woff2 +0 -0
- package/dist/remotion-bundle/275.bundle.js +21 -0
- package/dist/remotion-bundle/275.bundle.js.map +1 -0
- package/dist/remotion-bundle/2958f540b39513dc.woff2 +0 -0
- package/dist/remotion-bundle/2a168b98fd97722e.woff2 +0 -0
- package/dist/remotion-bundle/2d1f6373937ab55f.woff2 +0 -0
- package/dist/remotion-bundle/2d213ae47ff6daa9.woff2 +0 -0
- package/dist/remotion-bundle/2e4b1f04fcd05047.woff2 +0 -0
- package/dist/remotion-bundle/304170d98f4c4563.woff2 +0 -0
- package/dist/remotion-bundle/30d02e136e7a5642.woff2 +0 -0
- package/dist/remotion-bundle/3135562b52a714cd.woff2 +0 -0
- package/dist/remotion-bundle/313713af2c8144e9.woff2 +0 -0
- package/dist/remotion-bundle/325fa4108d2285b9.woff2 +0 -0
- package/dist/remotion-bundle/338e927ed3345e0c.woff2 +0 -0
- package/dist/remotion-bundle/35fc6b190365bc17.woff2 +0 -0
- package/dist/remotion-bundle/37a51f1122d4efc5.woff2 +0 -0
- package/dist/remotion-bundle/39a4d63e02736f5e.woff2 +0 -0
- package/dist/remotion-bundle/3a00e0d62dfc4171.woff2 +0 -0
- package/dist/remotion-bundle/3a6955e6561affe1.woff2 +0 -0
- package/dist/remotion-bundle/3c573945aef49b89.woff2 +0 -0
- package/dist/remotion-bundle/3cdbfbfa23b516a5.woff2 +0 -0
- package/dist/remotion-bundle/3e42f85a9e64ca8a.woff2 +0 -0
- package/dist/remotion-bundle/3e83eaf1ec859415.woff2 +0 -0
- package/dist/remotion-bundle/3f3c8c90de1250ee.woff2 +0 -0
- package/dist/remotion-bundle/434.bundle.js +205 -0
- package/dist/remotion-bundle/434.bundle.js.map +1 -0
- package/dist/remotion-bundle/44ffc6ca4d781692.woff2 +0 -0
- package/dist/remotion-bundle/4670d9c4580b09eb.woff2 +0 -0
- package/dist/remotion-bundle/479756881b302824.woff2 +0 -0
- package/dist/remotion-bundle/481b82134bfa9c82.woff2 +0 -0
- package/dist/remotion-bundle/48d27029626f4328.woff2 +0 -0
- package/dist/remotion-bundle/49b7b2a30329c511.woff2 +0 -0
- package/dist/remotion-bundle/4c8b25a1a9337045.woff2 +0 -0
- package/dist/remotion-bundle/4cba14788ca9259b.woff2 +0 -0
- package/dist/remotion-bundle/4cd6c589c004a6a7.woff2 +0 -0
- package/dist/remotion-bundle/4cd8d79c1021608d.woff2 +0 -0
- package/dist/remotion-bundle/4d8fa99b3f00f9f0.woff2 +0 -0
- package/dist/remotion-bundle/4e7805a643f86d53.woff2 +0 -0
- package/dist/remotion-bundle/4ff91be454542e3f.woff2 +0 -0
- package/dist/remotion-bundle/504cbcba1f63591b.woff2 +0 -0
- package/dist/remotion-bundle/5202d792e5791d6c.woff2 +0 -0
- package/dist/remotion-bundle/534db5ad4770cc1d.woff2 +0 -0
- package/dist/remotion-bundle/53b9568eb85f866b.woff2 +0 -0
- package/dist/remotion-bundle/543ad386ca171de9.woff2 +0 -0
- package/dist/remotion-bundle/54798e55bbf7976e.woff2 +0 -0
- package/dist/remotion-bundle/580.bundle.js +11 -0
- package/dist/remotion-bundle/580.bundle.js.map +1 -0
- package/dist/remotion-bundle/58d174d1193af6d1.woff2 +0 -0
- package/dist/remotion-bundle/591d29ff3ff53c80.woff2 +0 -0
- package/dist/remotion-bundle/5c28c4f4824383c6.woff2 +0 -0
- package/dist/remotion-bundle/5da9740d2ce894c8.woff2 +0 -0
- package/dist/remotion-bundle/6197735364642360.woff2 +0 -0
- package/dist/remotion-bundle/6265a4335724080f.woff2 +0 -0
- package/dist/remotion-bundle/633f5e4f6394daa7.woff2 +0 -0
- package/dist/remotion-bundle/637d95ace6a69c49.woff2 +0 -0
- package/dist/remotion-bundle/648e04a04dacff8f.woff2 +0 -0
- package/dist/remotion-bundle/64a6e83045a008b2.woff2 +0 -0
- package/dist/remotion-bundle/651.bundle.js +11 -0
- package/dist/remotion-bundle/651.bundle.js.map +1 -0
- package/dist/remotion-bundle/65e2a988c070facc.woff2 +0 -0
- package/dist/remotion-bundle/66a2f6ce5cc69105.woff2 +0 -0
- package/dist/remotion-bundle/690.bundle.js +3479 -0
- package/dist/remotion-bundle/690.bundle.js.map +1 -0
- package/dist/remotion-bundle/690ff55252ca715d.woff2 +0 -0
- package/dist/remotion-bundle/6a01a1cff49314fc.woff2 +0 -0
- package/dist/remotion-bundle/6cbc32670982986c.woff2 +0 -0
- package/dist/remotion-bundle/6d3cc42ae547f454.woff2 +0 -0
- package/dist/remotion-bundle/6d8f4cfa1ddc0830.woff2 +0 -0
- package/dist/remotion-bundle/6e4d7c6ae65e2dc3.woff2 +0 -0
- package/dist/remotion-bundle/6e86418bbcefb2e8.woff2 +0 -0
- package/dist/remotion-bundle/6ee02884b29cf7fb.woff2 +0 -0
- package/dist/remotion-bundle/6f436a74c9e3252c.woff2 +0 -0
- package/dist/remotion-bundle/78c8022f1657618b.woff2 +0 -0
- package/dist/remotion-bundle/7c5444169792bca4.woff2 +0 -0
- package/dist/remotion-bundle/7c86bddd9d997212.woff2 +0 -0
- package/dist/remotion-bundle/7e1284684767f584.woff2 +0 -0
- package/dist/remotion-bundle/7e81c17522d182b2.woff2 +0 -0
- package/dist/remotion-bundle/7eb87be198f7858c.woff2 +0 -0
- package/dist/remotion-bundle/8060c928f948aab5.woff2 +0 -0
- package/dist/remotion-bundle/80bc9dfbea2b35ae.woff2 +0 -0
- package/dist/remotion-bundle/811b83f69963bb48.woff2 +0 -0
- package/dist/remotion-bundle/813.bundle.js +117511 -0
- package/dist/remotion-bundle/813.bundle.js.map +1 -0
- package/dist/remotion-bundle/84df492e349f82e9.woff2 +0 -0
- package/dist/remotion-bundle/8501bfd73eb36f2b.woff2 +0 -0
- package/dist/remotion-bundle/854236a8376093fe.woff2 +0 -0
- package/dist/remotion-bundle/8571d74529082753.woff2 +0 -0
- package/dist/remotion-bundle/860bf44f8e6f4b5d.woff2 +0 -0
- package/dist/remotion-bundle/879.bundle.js +64 -0
- package/dist/remotion-bundle/879.bundle.js.map +1 -0
- package/dist/remotion-bundle/887dd482f848d56f.woff2 +0 -0
- package/dist/remotion-bundle/89b2132e85fbbb5a.woff2 +0 -0
- package/dist/remotion-bundle/8ba60d6c306010c2.woff2 +0 -0
- package/dist/remotion-bundle/8c7c4dadea897806.woff2 +0 -0
- package/dist/remotion-bundle/8c943f9999706f61.woff2 +0 -0
- package/dist/remotion-bundle/8f2a718c90575cc9.woff2 +0 -0
- package/dist/remotion-bundle/906b6edb3e1772c9.woff2 +0 -0
- package/dist/remotion-bundle/930ff9daccdf14eb.woff2 +0 -0
- package/dist/remotion-bundle/934db2f1c403c4d0.woff2 +0 -0
- package/dist/remotion-bundle/938.bundle.js +451 -0
- package/dist/remotion-bundle/938.bundle.js.map +1 -0
- package/dist/remotion-bundle/967.bundle.js +4462 -0
- package/dist/remotion-bundle/967.bundle.js.map +1 -0
- package/dist/remotion-bundle/9684a1093d3c02ce.woff2 +0 -0
- package/dist/remotion-bundle/973dcd0faa6116cc.woff2 +0 -0
- package/dist/remotion-bundle/9745400694e76cd8.woff2 +0 -0
- package/dist/remotion-bundle/999ef957bed3bdca.woff2 +0 -0
- package/dist/remotion-bundle/99a3d67c8b0f43e3.woff2 +0 -0
- package/dist/remotion-bundle/a0586c3e03127283.woff2 +0 -0
- package/dist/remotion-bundle/a0eb654fdae46269.woff2 +0 -0
- package/dist/remotion-bundle/a20e35d3b08f7994.woff2 +0 -0
- package/dist/remotion-bundle/a2dcaced7c8c25ab.woff2 +0 -0
- package/dist/remotion-bundle/a79255a972a2681a.woff2 +0 -0
- package/dist/remotion-bundle/a804b352cb9fec1a.woff2 +0 -0
- package/dist/remotion-bundle/aae7117164e1eabc.woff2 +0 -0
- package/dist/remotion-bundle/affd121385d0442d.woff2 +0 -0
- package/dist/remotion-bundle/b19a6083987ee0d7.woff2 +0 -0
- package/dist/remotion-bundle/b1b2bd04d8637981.woff2 +0 -0
- package/dist/remotion-bundle/b2c07f341486be87.woff2 +0 -0
- package/dist/remotion-bundle/b33d8f82e575c4ce.woff2 +0 -0
- package/dist/remotion-bundle/b366c0bed35ef491.woff2 +0 -0
- package/dist/remotion-bundle/b41e857ec1b85642.woff2 +0 -0
- package/dist/remotion-bundle/b420bb34ccf23e7f.woff2 +0 -0
- package/dist/remotion-bundle/b4f7bf4efb0c0ccf.woff2 +0 -0
- package/dist/remotion-bundle/b60fe5eca03cff93.woff2 +0 -0
- package/dist/remotion-bundle/b6bd31a336e64bce.woff2 +0 -0
- package/dist/remotion-bundle/b6d2befba3dfefeb.woff2 +0 -0
- package/dist/remotion-bundle/b75f39ab06c43bf4.woff2 +0 -0
- package/dist/remotion-bundle/b77880e8c413d4fd.woff2 +0 -0
- package/dist/remotion-bundle/b7e38ec441e4a77a.woff2 +0 -0
- package/dist/remotion-bundle/b83baa383ff0bf2b.woff2 +0 -0
- package/dist/remotion-bundle/b9ad7b6c0a11450a.woff2 +0 -0
- package/dist/remotion-bundle/baf84486e8ae3aaf.woff2 +0 -0
- package/dist/remotion-bundle/bc047b1f6869cffa.woff2 +0 -0
- package/dist/remotion-bundle/bf4f3ac6e93f33aa.woff2 +0 -0
- package/dist/remotion-bundle/bf6835ffec5897a2.woff2 +0 -0
- package/dist/remotion-bundle/bf8885f581eb1724.woff2 +0 -0
- package/dist/remotion-bundle/bundle.js +83376 -0
- package/dist/remotion-bundle/bundle.js.map +1 -0
- package/dist/remotion-bundle/c03f046bccd789d0.woff2 +0 -0
- package/dist/remotion-bundle/c0bb1f8962b73bc3.woff2 +0 -0
- package/dist/remotion-bundle/c1003f9a7db6e1cf.woff2 +0 -0
- package/dist/remotion-bundle/c15d83fb1e199515.woff2 +0 -0
- package/dist/remotion-bundle/c28e7e5d310f73ef.woff2 +0 -0
- package/dist/remotion-bundle/c2b840274db78aea.woff2 +0 -0
- package/dist/remotion-bundle/c3000e3299d4e45f.woff2 +0 -0
- package/dist/remotion-bundle/c83ce886e5288510.woff2 +0 -0
- package/dist/remotion-bundle/c87a5a64d4ac0918.woff2 +0 -0
- package/dist/remotion-bundle/c8a7e0d049e965fa.woff2 +0 -0
- package/dist/remotion-bundle/c949a35d3a3b1faf.woff2 +0 -0
- package/dist/remotion-bundle/c9618c9b9ac2bc78.woff2 +0 -0
- package/dist/remotion-bundle/ca3add3b84152d5b.woff2 +0 -0
- package/dist/remotion-bundle/cad9dd036408d707.woff2 +0 -0
- package/dist/remotion-bundle/cbb24916619df439.woff2 +0 -0
- package/dist/remotion-bundle/cc054f0b5514e177.woff2 +0 -0
- package/dist/remotion-bundle/ccc248ed9312bc71.woff2 +0 -0
- package/dist/remotion-bundle/cd9d623aa07af925.woff2 +0 -0
- package/dist/remotion-bundle/ce2ba7a321bd1247.woff2 +0 -0
- package/dist/remotion-bundle/cf72455f79a29b14.woff2 +0 -0
- package/dist/remotion-bundle/d267cbfefab452ac.woff2 +0 -0
- package/dist/remotion-bundle/d435cff46a64955f.woff +0 -0
- package/dist/remotion-bundle/d494d07f67e363f6.woff2 +0 -0
- package/dist/remotion-bundle/d7aa0cc1fa47bf38.woff2 +0 -0
- package/dist/remotion-bundle/d7c5ca93d885160a.woff2 +0 -0
- package/dist/remotion-bundle/d855d3e252db74e2.woff2 +0 -0
- package/dist/remotion-bundle/d8f13d47f02f82c2.woff2 +0 -0
- package/dist/remotion-bundle/d9567cce2ee11019.woff2 +0 -0
- package/dist/remotion-bundle/db8d4456fc75dd86.woff +0 -0
- package/dist/remotion-bundle/dc274628378c47ee.woff2 +0 -0
- package/dist/remotion-bundle/dc3e06947bb69903.woff2 +0 -0
- package/dist/remotion-bundle/dd67040ac3b6d523.woff2 +0 -0
- package/dist/remotion-bundle/e0b04bd488f953f4.woff2 +0 -0
- package/dist/remotion-bundle/e2a572ff95089370.woff2 +0 -0
- package/dist/remotion-bundle/e2e18a86b1c2b0cc.woff2 +0 -0
- package/dist/remotion-bundle/e3a78ee2fc9c6931.woff2 +0 -0
- package/dist/remotion-bundle/e654c9d547605a9f.woff2 +0 -0
- package/dist/remotion-bundle/e67a3a64c129927c.woff2 +0 -0
- package/dist/remotion-bundle/e6be28b4203cd6ce.woff2 +0 -0
- package/dist/remotion-bundle/e841907ad9b0a191.woff +0 -0
- package/dist/remotion-bundle/e889d1541c69fffa.woff2 +0 -0
- package/dist/remotion-bundle/e88ef8c76373a9e2.woff2 +0 -0
- package/dist/remotion-bundle/e9c72f4bc37defef.woff2 +0 -0
- package/dist/remotion-bundle/e9e35f863403a255.woff2 +0 -0
- package/dist/remotion-bundle/eb23b37b009375da.woff2 +0 -0
- package/dist/remotion-bundle/ee1342b741625721.woff2 +0 -0
- package/dist/remotion-bundle/f07da88543a57ec9.woff2 +0 -0
- package/dist/remotion-bundle/f522982115306f8a.woff2 +0 -0
- package/dist/remotion-bundle/f8449bd864e6d8bc.woff2 +0 -0
- package/dist/remotion-bundle/f906dd5bd95ff9ab.woff2 +0 -0
- package/dist/remotion-bundle/f9e9e9413e3c38bb.woff2 +0 -0
- package/dist/remotion-bundle/fa5a5b16280994a8.woff2 +0 -0
- package/dist/remotion-bundle/favicon.ico +0 -0
- package/dist/remotion-bundle/fb19c0517725599b.woff2 +0 -0
- package/dist/remotion-bundle/fcaf24232f684b9b.woff2 +0 -0
- package/dist/remotion-bundle/fe09e084a3eea8cf.woff2 +0 -0
- package/dist/remotion-bundle/ff38d5317df7345a.woff2 +0 -0
- package/dist/remotion-bundle/ffe7ea1ea08f455a.woff2 +0 -0
- package/dist/remotion-bundle/index.html +49 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-5.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-5.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-6.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-5.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-6.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-5.mp3 +0 -0
- package/dist/remotion-bundle/source-map-helper.wasm +0 -0
- package/lib/cli.js +270 -0
- package/lib/commands/_registry.js +48 -0
- package/lib/commands/add.js +242 -0
- package/lib/commands/asr/azure-transcribe.js +336 -0
- package/lib/commands/asr/cloud-transcribe.js +384 -0
- package/lib/commands/asr/helpers.js +76 -0
- package/lib/commands/asr/index.js +236 -0
- package/lib/commands/asr/local-transcribe.js +125 -0
- package/lib/commands/asr-jobs.js +257 -0
- package/lib/commands/asr.js +11 -0
- package/lib/commands/auth-cmds.js +358 -0
- package/lib/commands/dub.js +542 -0
- package/lib/commands/explain.js +512 -0
- package/lib/commands/feedback.js +152 -0
- package/lib/commands/image.js +207 -0
- package/lib/commands/mcp-key.js +166 -0
- package/lib/commands/narrate.js +639 -0
- package/lib/commands/picstory-templates.js +276 -0
- package/lib/commands/picstory.js +547 -0
- package/lib/commands/podcast/dialogue.js +109 -0
- package/lib/commands/podcast/generate.js +127 -0
- package/lib/commands/podcast/index.js +561 -0
- package/lib/commands/podcast/synthesize.js +188 -0
- package/lib/commands/podcast.js +11 -0
- package/lib/commands/present.js +519 -0
- package/lib/commands/publish.js +415 -0
- package/lib/commands/skills.js +473 -0
- package/lib/commands/slice-render.js +282 -0
- package/lib/commands/slice-stage.js +264 -0
- package/lib/commands/slice.js +346 -0
- package/lib/commands/slides/constants.js +108 -0
- package/lib/commands/slides/html-renderer.js +338 -0
- package/lib/commands/slides/index.js +345 -0
- package/lib/commands/slides.js +11 -0
- package/lib/commands/story.js +302 -0
- package/lib/commands/summarize.js +532 -0
- package/lib/commands/synthesize.js +261 -0
- package/lib/commands/translate.js +593 -0
- package/lib/commands/upgrade.js +249 -0
- package/lib/commands/video-translate.js +577 -0
- package/lib/commands/voices.js +292 -0
- package/lib/core/agent-env.js +104 -0
- package/lib/core/args.js +107 -0
- package/lib/core/asr-client.js +448 -0
- package/lib/core/asr-jobs-client.js +126 -0
- package/lib/core/asr-jobs-store.js +105 -0
- package/lib/core/asr-r2-upload.js +181 -0
- package/lib/core/asr-upload.js +132 -0
- package/lib/core/audio-extract.js +150 -0
- package/lib/core/audio.js +219 -0
- package/lib/core/auth.js +880 -0
- package/lib/core/config.js +197 -0
- package/lib/core/feedback.js +64 -0
- package/lib/core/ffmpeg.js +476 -0
- package/lib/core/http.js +188 -0
- package/lib/core/image-client.js +55 -0
- package/lib/core/intent-params.js +11 -0
- package/lib/core/llm-client.js +76 -0
- package/lib/core/logger.js +208 -0
- package/lib/core/mic-recorder.js +182 -0
- package/lib/core/pause-markers.js +94 -0
- package/lib/core/podcast-pacing.js +118 -0
- package/lib/core/spinner.js +33 -0
- package/lib/core/srt.js +394 -0
- package/lib/core/telemetry.js +100 -0
- package/lib/core/timeline.js +92 -0
- package/lib/core/tts-synthesizer.js +70 -0
- package/lib/core/update-check.js +185 -0
- package/lib/core/url-download.js +148 -0
- package/lib/core/whisper-local.js +279 -0
- package/lib/internal/deck-validator.js +488 -0
- package/lib/internal/slice-themes.json +370 -0
- package/lib/stage-core/cloud-render.js +170 -0
- package/lib/stage-core/deck-format.js +133 -0
- package/lib/stage-core/edit-prompt.js +104 -0
- package/lib/stage-core/event-bus.js +31 -0
- package/lib/stage-core/port.js +46 -0
- package/lib/stage-core/server.js +352 -0
- package/lib/stage-core/snapshot-store.js +198 -0
- package/lib/stage-core/watcher.js +106 -0
- package/lib/stage-ui/slice/template.js +1672 -0
- package/package.json +9 -4
- package/skills/.claude-plugin/marketplace.json +22 -0
- package/skills/.claude-plugin/plugin.json +25 -0
- package/skills/LICENSE +21 -0
- package/skills/README.md +120 -0
- package/skills/hub/SKILL.md +317 -0
- package/skills/podcast/SKILL.md +146 -0
- package/skills/slice/SKILL.md +205 -0
- package/skills/slice/agents/openai.yaml +4 -0
- package/skills/slice/references/deck-schema.md +183 -0
- package/skills/slice/references/example-decks.md +108 -0
- package/skills/slice/references/themes.md +172 -0
- package/skills/transcribe/SKILL.md +473 -0
- package/skills/video/SKILL.md +261 -0
- package/skills/voxflow-slice/SKILL.md +271 -0
- package/skills/voxflow-slice/examples/article.md +13 -0
- package/skills/voxflow-slice/examples/expected-deck.json +39 -0
- package/skills/voxflow-slice/examples/validate.mjs +46 -0
|
@@ -0,0 +1,1672 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stage UI for `voxflow slice stage`.
|
|
5
|
+
*
|
|
6
|
+
* Inline HTML/JS string — embedded as a JS literal so ncc bundles it without
|
|
7
|
+
* triggering the asset-rewrite trap (see scripts/check-no-asset-rewrite.js).
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* - Subscribe to /events SSE; render the deck on the `deck` event.
|
|
11
|
+
* - Render each card as a 9:16 vertical preview with per-theme styling
|
|
12
|
+
* (best-effort approximation of the 6 Slice themes — pixel-perfect
|
|
13
|
+
* output still requires the cloud Remotion render).
|
|
14
|
+
* - Light/dark page chrome with auto / manual toggle, persisted in
|
|
15
|
+
* localStorage (key: `voxflow.stage.theme`).
|
|
16
|
+
*
|
|
17
|
+
* Card theme palettes are deliberately fixed (not viewer-overridable) — a
|
|
18
|
+
* `bold-poster` card stays bright yellow even in dark page chrome, because
|
|
19
|
+
* theme is a property of the deck content, not the viewer.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { formatCardAsText, formatDeckAsMarkdown, suggestDeckFilename } = require('../../stage-core/deck-format');
|
|
23
|
+
|
|
24
|
+
function renderSliceStageHtml({ sourcePath, port }) {
|
|
25
|
+
const safePath = String(sourcePath || '').replace(/[<>&"']/g, (c) => ({
|
|
26
|
+
'<': '<', '>': '>', '&': '&', '"': '"', "'": ''',
|
|
27
|
+
}[c]));
|
|
28
|
+
|
|
29
|
+
return `<!doctype html>
|
|
30
|
+
<html lang="en" data-theme="auto">
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="utf-8">
|
|
33
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
34
|
+
<title>VoxFlow Stage — Slice</title>
|
|
35
|
+
<style>
|
|
36
|
+
/* ─── Page chrome: light + dark via prefers-color-scheme + manual override ── */
|
|
37
|
+
:root {
|
|
38
|
+
--bg: #ffffff;
|
|
39
|
+
--panel: #f7f7f9;
|
|
40
|
+
--panel-2: #ffffff;
|
|
41
|
+
--border: #e5e5ea;
|
|
42
|
+
--text: #1a1a1f;
|
|
43
|
+
--muted: #6b6b78;
|
|
44
|
+
--accent: #5851b8;
|
|
45
|
+
--good: #16a34a;
|
|
46
|
+
--warn: #d97706;
|
|
47
|
+
--bad: #dc2626;
|
|
48
|
+
--shadow: 0 1px 2px rgba(15, 15, 25, 0.04), 0 4px 12px rgba(15, 15, 25, 0.05);
|
|
49
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
50
|
+
color-scheme: light;
|
|
51
|
+
}
|
|
52
|
+
@media (prefers-color-scheme: dark) {
|
|
53
|
+
:root[data-theme="auto"] {
|
|
54
|
+
--bg: #0e0d14;
|
|
55
|
+
--panel: #16151d;
|
|
56
|
+
--panel-2: #1c1a25;
|
|
57
|
+
--border: #2a2735;
|
|
58
|
+
--text: #e8e6f0;
|
|
59
|
+
--muted: #8a8694;
|
|
60
|
+
--accent: #8b85e0;
|
|
61
|
+
--good: #3ad29f;
|
|
62
|
+
--warn: #f5a524;
|
|
63
|
+
--bad: #f56565;
|
|
64
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.5);
|
|
65
|
+
color-scheme: dark;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
:root[data-theme="dark"] {
|
|
69
|
+
--bg: #0e0d14;
|
|
70
|
+
--panel: #16151d;
|
|
71
|
+
--panel-2: #1c1a25;
|
|
72
|
+
--border: #2a2735;
|
|
73
|
+
--text: #e8e6f0;
|
|
74
|
+
--muted: #8a8694;
|
|
75
|
+
--accent: #8b85e0;
|
|
76
|
+
--good: #3ad29f;
|
|
77
|
+
--warn: #f5a524;
|
|
78
|
+
--bad: #f56565;
|
|
79
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.5);
|
|
80
|
+
color-scheme: dark;
|
|
81
|
+
}
|
|
82
|
+
:root[data-theme="light"] {
|
|
83
|
+
color-scheme: light;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
* { box-sizing: border-box; }
|
|
87
|
+
body {
|
|
88
|
+
margin: 0;
|
|
89
|
+
background: var(--bg);
|
|
90
|
+
color: var(--text);
|
|
91
|
+
min-height: 100vh;
|
|
92
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
93
|
+
}
|
|
94
|
+
header {
|
|
95
|
+
display: flex; align-items: center; gap: 12px;
|
|
96
|
+
padding: 12px 20px;
|
|
97
|
+
border-bottom: 1px solid var(--border);
|
|
98
|
+
background: var(--panel);
|
|
99
|
+
position: sticky; top: 0; z-index: 10;
|
|
100
|
+
backdrop-filter: saturate(140%) blur(8px);
|
|
101
|
+
}
|
|
102
|
+
header .brand {
|
|
103
|
+
font-weight: 600; letter-spacing: 0.02em;
|
|
104
|
+
}
|
|
105
|
+
header .brand .accent { color: var(--accent); }
|
|
106
|
+
header .src {
|
|
107
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
108
|
+
font-size: 12px; color: var(--muted);
|
|
109
|
+
max-width: 50ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
110
|
+
}
|
|
111
|
+
header .grow { flex: 1; }
|
|
112
|
+
header .status {
|
|
113
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
114
|
+
font-size: 12px; color: var(--muted);
|
|
115
|
+
}
|
|
116
|
+
.dot {
|
|
117
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
118
|
+
background: var(--muted);
|
|
119
|
+
transition: background 0.2s ease;
|
|
120
|
+
}
|
|
121
|
+
.dot.good { background: var(--good); }
|
|
122
|
+
.dot.warn { background: var(--warn); }
|
|
123
|
+
.dot.bad { background: var(--bad); }
|
|
124
|
+
|
|
125
|
+
.theme-toggle {
|
|
126
|
+
appearance: none; border: 1px solid var(--border);
|
|
127
|
+
background: var(--panel-2); color: var(--text);
|
|
128
|
+
font-size: 12px; font-family: inherit;
|
|
129
|
+
padding: 4px 10px; border-radius: 6px; cursor: pointer;
|
|
130
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
131
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
132
|
+
}
|
|
133
|
+
.theme-toggle:hover { border-color: var(--accent); }
|
|
134
|
+
.theme-toggle .icon { font-size: 13px; }
|
|
135
|
+
|
|
136
|
+
/* Versions button reuses theme-toggle visuals so the header looks like
|
|
137
|
+
one cohesive control cluster, not a bag of buttons. */
|
|
138
|
+
.versions-btn {
|
|
139
|
+
appearance: none; border: 1px solid var(--border);
|
|
140
|
+
background: var(--panel-2); color: var(--text);
|
|
141
|
+
font-size: 12px; font-family: inherit;
|
|
142
|
+
padding: 4px 10px; border-radius: 6px; cursor: pointer;
|
|
143
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
144
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
145
|
+
}
|
|
146
|
+
.versions-btn:hover { border-color: var(--accent); }
|
|
147
|
+
.versions-btn .count-pill {
|
|
148
|
+
background: var(--accent); color: white;
|
|
149
|
+
border-radius: 9px; padding: 0 6px; font-size: 10px;
|
|
150
|
+
font-weight: 600; min-width: 16px; text-align: center;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.versions-drawer {
|
|
154
|
+
position: fixed; top: 56px; right: 16px;
|
|
155
|
+
width: 320px; max-height: calc(100vh - 80px);
|
|
156
|
+
background: var(--panel); border: 1px solid var(--border);
|
|
157
|
+
border-radius: 8px; box-shadow: 0 8px 32px rgba(0,0,0,0.18);
|
|
158
|
+
display: none; flex-direction: column;
|
|
159
|
+
z-index: 50;
|
|
160
|
+
}
|
|
161
|
+
.versions-drawer.open { display: flex; }
|
|
162
|
+
.versions-drawer header.drawer-head {
|
|
163
|
+
display: flex; align-items: center; gap: 8px;
|
|
164
|
+
padding: 10px 12px; border-bottom: 1px solid var(--border);
|
|
165
|
+
font-size: 13px; font-weight: 600;
|
|
166
|
+
}
|
|
167
|
+
.versions-drawer .drawer-head .grow { flex: 1; }
|
|
168
|
+
.versions-drawer .drawer-close {
|
|
169
|
+
appearance: none; border: 0; background: transparent;
|
|
170
|
+
color: var(--muted); font-size: 18px; line-height: 1;
|
|
171
|
+
cursor: pointer; padding: 2px 6px; border-radius: 4px;
|
|
172
|
+
}
|
|
173
|
+
.versions-drawer .drawer-close:hover { background: var(--panel-2); color: var(--text); }
|
|
174
|
+
.versions-drawer .drawer-body {
|
|
175
|
+
overflow-y: auto; padding: 6px;
|
|
176
|
+
}
|
|
177
|
+
.versions-drawer .empty-msg {
|
|
178
|
+
padding: 16px 12px; color: var(--muted); font-size: 12px; text-align: center;
|
|
179
|
+
}
|
|
180
|
+
.version-row {
|
|
181
|
+
display: grid; grid-template-columns: 1fr auto;
|
|
182
|
+
gap: 8px; align-items: center;
|
|
183
|
+
padding: 8px 10px; border-radius: 6px;
|
|
184
|
+
border: 1px solid transparent;
|
|
185
|
+
}
|
|
186
|
+
.version-row + .version-row { margin-top: 2px; }
|
|
187
|
+
.version-row:hover { background: var(--panel-2); border-color: var(--border); }
|
|
188
|
+
.version-row.current {
|
|
189
|
+
background: var(--panel-2); border-color: var(--accent);
|
|
190
|
+
}
|
|
191
|
+
.version-row .ts {
|
|
192
|
+
font-size: 12px; font-weight: 500; color: var(--text);
|
|
193
|
+
font-variant-numeric: tabular-nums;
|
|
194
|
+
}
|
|
195
|
+
.version-row .meta {
|
|
196
|
+
font-size: 10px; color: var(--muted);
|
|
197
|
+
font-family: var(--mono);
|
|
198
|
+
}
|
|
199
|
+
.version-row .restore-btn {
|
|
200
|
+
appearance: none; border: 1px solid var(--border);
|
|
201
|
+
background: var(--panel); color: var(--text);
|
|
202
|
+
font-size: 11px; padding: 3px 9px; border-radius: 5px;
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
}
|
|
205
|
+
.version-row.current .restore-btn { display: none; }
|
|
206
|
+
.version-row .restore-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
207
|
+
.version-row .restore-btn:disabled { opacity: 0.5; cursor: progress; }
|
|
208
|
+
.versions-drawer .store-path {
|
|
209
|
+
padding: 8px 12px; border-top: 1px solid var(--border);
|
|
210
|
+
font-size: 10px; color: var(--muted); font-family: var(--mono);
|
|
211
|
+
word-break: break-all;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Render button — primary action, sits between History and theme toggle.
|
|
215
|
+
Visually heavier than the other header pills since it's the "ship"
|
|
216
|
+
affordance (the one quota-charging action in the page). */
|
|
217
|
+
.render-btn {
|
|
218
|
+
appearance: none; border: 1px solid var(--accent);
|
|
219
|
+
background: var(--accent); color: white;
|
|
220
|
+
font-size: 12px; font-family: inherit; font-weight: 600;
|
|
221
|
+
padding: 4px 12px; border-radius: 6px; cursor: pointer;
|
|
222
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
223
|
+
transition: filter 0.15s ease;
|
|
224
|
+
}
|
|
225
|
+
.render-btn:hover:not(:disabled) { filter: brightness(1.08); }
|
|
226
|
+
.render-btn:disabled { opacity: 0.6; cursor: progress; }
|
|
227
|
+
.render-btn .icon { font-size: 13px; }
|
|
228
|
+
.render-btn.completed {
|
|
229
|
+
background: transparent; color: var(--accent);
|
|
230
|
+
text-decoration: none;
|
|
231
|
+
}
|
|
232
|
+
.render-btn.completed:hover { filter: none; text-decoration: underline; }
|
|
233
|
+
|
|
234
|
+
.render-modal-body { display: flex; flex-direction: column; gap: 8px; padding: 4px 0 6px; }
|
|
235
|
+
.render-modal-body .quota-line {
|
|
236
|
+
display: flex; justify-content: space-between; align-items: baseline;
|
|
237
|
+
padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px;
|
|
238
|
+
background: var(--panel-2);
|
|
239
|
+
}
|
|
240
|
+
.render-modal-body .quota-line .key { color: var(--muted); font-size: 11px; }
|
|
241
|
+
.render-modal-body .quota-line .val { font-family: var(--mono); font-size: 13px; font-weight: 600; }
|
|
242
|
+
.render-modal-body .quota-line.short .val { color: var(--bad); }
|
|
243
|
+
.render-modal-body .err-msg {
|
|
244
|
+
color: var(--bad); font-size: 12px; padding: 6px 10px;
|
|
245
|
+
border: 1px solid var(--bad); border-radius: 6px;
|
|
246
|
+
background: color-mix(in srgb, var(--bad) 8%, transparent);
|
|
247
|
+
}
|
|
248
|
+
.render-modal-body .progress-msg {
|
|
249
|
+
color: var(--muted); font-size: 12px; padding: 6px 10px;
|
|
250
|
+
}
|
|
251
|
+
.render-modal-body .progress-msg .stage-label { color: var(--text); font-weight: 600; }
|
|
252
|
+
.render-confirm-btn {
|
|
253
|
+
appearance: none; border: 1px solid var(--accent);
|
|
254
|
+
background: var(--accent); color: white;
|
|
255
|
+
font-size: 12px; font-family: inherit; font-weight: 600;
|
|
256
|
+
padding: 6px 14px; border-radius: 6px; cursor: pointer;
|
|
257
|
+
}
|
|
258
|
+
.render-confirm-btn:disabled { opacity: 0.6; cursor: progress; }
|
|
259
|
+
.render-confirm-btn:hover:not(:disabled) { filter: brightness(1.08); }
|
|
260
|
+
|
|
261
|
+
main {
|
|
262
|
+
display: grid;
|
|
263
|
+
grid-template-rows: auto auto;
|
|
264
|
+
gap: 16px;
|
|
265
|
+
padding: 20px;
|
|
266
|
+
max-width: 1400px; margin: 0 auto;
|
|
267
|
+
}
|
|
268
|
+
section {
|
|
269
|
+
background: var(--panel);
|
|
270
|
+
border: 1px solid var(--border);
|
|
271
|
+
border-radius: 12px;
|
|
272
|
+
padding: 18px;
|
|
273
|
+
box-shadow: var(--shadow);
|
|
274
|
+
}
|
|
275
|
+
section h2 {
|
|
276
|
+
margin: 0 0 14px;
|
|
277
|
+
font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em;
|
|
278
|
+
color: var(--muted); font-weight: 600;
|
|
279
|
+
display: flex; align-items: center; gap: 8px;
|
|
280
|
+
}
|
|
281
|
+
section h2 .count {
|
|
282
|
+
background: var(--panel-2); border: 1px solid var(--border);
|
|
283
|
+
color: var(--text);
|
|
284
|
+
padding: 1px 8px; border-radius: 999px;
|
|
285
|
+
font-size: 11px; font-weight: 500; letter-spacing: 0;
|
|
286
|
+
}
|
|
287
|
+
section h2 .preview-tag {
|
|
288
|
+
margin-left: auto;
|
|
289
|
+
background: transparent; color: var(--muted);
|
|
290
|
+
font-size: 10px; font-weight: 500; letter-spacing: 0.04em;
|
|
291
|
+
text-transform: none;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* ─── Cards grid (9:16 vertical previews) ─────────────────────────────── */
|
|
295
|
+
.cards-grid {
|
|
296
|
+
display: grid;
|
|
297
|
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
298
|
+
gap: 18px;
|
|
299
|
+
}
|
|
300
|
+
.stage-card {
|
|
301
|
+
aspect-ratio: 9 / 16;
|
|
302
|
+
border-radius: 10px;
|
|
303
|
+
overflow: hidden;
|
|
304
|
+
position: relative;
|
|
305
|
+
box-shadow: var(--shadow);
|
|
306
|
+
display: flex; flex-direction: column;
|
|
307
|
+
transition: transform 0.15s ease;
|
|
308
|
+
cursor: default;
|
|
309
|
+
}
|
|
310
|
+
.stage-card:hover { transform: translateY(-2px); }
|
|
311
|
+
.stage-card .frame-meta {
|
|
312
|
+
position: absolute; top: 10px; left: 10px; right: 10px;
|
|
313
|
+
display: flex; align-items: center; gap: 6px;
|
|
314
|
+
font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase;
|
|
315
|
+
opacity: 0.55; pointer-events: none;
|
|
316
|
+
z-index: 2;
|
|
317
|
+
}
|
|
318
|
+
.stage-card .frame-meta .idx {
|
|
319
|
+
background: rgba(0,0,0,0.18); color: inherit;
|
|
320
|
+
padding: 1px 6px; border-radius: 4px;
|
|
321
|
+
font-weight: 600;
|
|
322
|
+
}
|
|
323
|
+
.stage-card[data-card-bg="dark"] .frame-meta .idx {
|
|
324
|
+
background: rgba(255,255,255,0.18);
|
|
325
|
+
}
|
|
326
|
+
.stage-card .figure-chip {
|
|
327
|
+
position: absolute; top: 32px; right: 10px;
|
|
328
|
+
font-size: 10px; padding: 2px 8px; border-radius: 4px;
|
|
329
|
+
background: rgba(0,0,0,0.08);
|
|
330
|
+
opacity: 0.7;
|
|
331
|
+
z-index: 2;
|
|
332
|
+
}
|
|
333
|
+
.stage-card[data-card-bg="dark"] .figure-chip {
|
|
334
|
+
background: rgba(255,255,255,0.12);
|
|
335
|
+
}
|
|
336
|
+
.stage-card .body {
|
|
337
|
+
flex: 1;
|
|
338
|
+
display: flex; flex-direction: column;
|
|
339
|
+
justify-content: center;
|
|
340
|
+
padding: 28px 22px 22px;
|
|
341
|
+
gap: 8px;
|
|
342
|
+
overflow: hidden;
|
|
343
|
+
}
|
|
344
|
+
.stage-card.kind-title .body { justify-content: center; text-align: left; }
|
|
345
|
+
/* Body cards: extra bottom padding so narration never sits underneath
|
|
346
|
+
the [Edit with AI] button that appears on hover. */
|
|
347
|
+
.stage-card.kind-body .body { justify-content: flex-end; padding-bottom: 46px; }
|
|
348
|
+
.stage-card .title-text {
|
|
349
|
+
font-size: clamp(18px, 2.4vw, 26px);
|
|
350
|
+
line-height: 1.18;
|
|
351
|
+
font-weight: var(--card-title-weight, 700);
|
|
352
|
+
word-break: keep-all;
|
|
353
|
+
}
|
|
354
|
+
.stage-card .title-text .line {
|
|
355
|
+
display: block;
|
|
356
|
+
}
|
|
357
|
+
.stage-card .caption {
|
|
358
|
+
font-size: clamp(13px, 1.6vw, 16px);
|
|
359
|
+
line-height: 1.35;
|
|
360
|
+
font-weight: 600;
|
|
361
|
+
}
|
|
362
|
+
.stage-card .narration {
|
|
363
|
+
font-size: 11px;
|
|
364
|
+
line-height: 1.5;
|
|
365
|
+
opacity: 0.7;
|
|
366
|
+
max-height: 4.5em; overflow: hidden;
|
|
367
|
+
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
|
|
368
|
+
}
|
|
369
|
+
.stage-card .accent-bar {
|
|
370
|
+
position: absolute; left: 0; bottom: 0;
|
|
371
|
+
height: 4px; width: 32%;
|
|
372
|
+
background: var(--card-accent, currentColor);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* ─── Edit-with-AI affordance: per-card button + floating selection btn ── */
|
|
376
|
+
.edit-btn {
|
|
377
|
+
position: absolute; right: 10px; bottom: 10px;
|
|
378
|
+
appearance: none; border: 0; cursor: pointer;
|
|
379
|
+
font: inherit; font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
|
|
380
|
+
padding: 5px 10px; border-radius: 6px;
|
|
381
|
+
background: rgba(0,0,0,0.78); color: #fff;
|
|
382
|
+
opacity: 0; transform: translateY(4px);
|
|
383
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
384
|
+
z-index: 3;
|
|
385
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
|
|
386
|
+
}
|
|
387
|
+
.stage-card[data-card-bg="dark"] .edit-btn {
|
|
388
|
+
background: rgba(255,255,255,0.92); color: #0a0a0a;
|
|
389
|
+
}
|
|
390
|
+
.stage-card:hover .edit-btn,
|
|
391
|
+
.edit-btn:focus-visible {
|
|
392
|
+
opacity: 1; transform: translateY(0);
|
|
393
|
+
}
|
|
394
|
+
.edit-btn:hover { filter: brightness(1.1); }
|
|
395
|
+
.edit-btn .icon { margin-right: 4px; }
|
|
396
|
+
|
|
397
|
+
/* ─── Text-export controls (Phase 1.4) — per-card copy + deck toolbar ── */
|
|
398
|
+
.stage-card .card-actions {
|
|
399
|
+
position: absolute; right: 10px; top: 10px;
|
|
400
|
+
display: flex; gap: 6px; z-index: 3;
|
|
401
|
+
opacity: 0; transform: translateY(-4px);
|
|
402
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
403
|
+
}
|
|
404
|
+
.stage-card:hover .card-actions { opacity: 1; transform: translateY(0); }
|
|
405
|
+
.stage-card .card-actions button {
|
|
406
|
+
appearance: none; border: 0; cursor: pointer; font: inherit;
|
|
407
|
+
font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
|
|
408
|
+
padding: 4px 9px; border-radius: 6px;
|
|
409
|
+
background: rgba(0,0,0,0.78); color: #fff;
|
|
410
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
|
|
411
|
+
}
|
|
412
|
+
.stage-card[data-card-bg="dark"] .card-actions button {
|
|
413
|
+
background: rgba(255,255,255,0.92); color: #0a0a0a;
|
|
414
|
+
}
|
|
415
|
+
.stage-card .card-actions button.copied {
|
|
416
|
+
background: rgba(34,197,94,0.92); color: #fff;
|
|
417
|
+
}
|
|
418
|
+
.deck-toolbar {
|
|
419
|
+
display: flex; gap: 8px; flex-wrap: wrap;
|
|
420
|
+
margin: 0 0 14px;
|
|
421
|
+
}
|
|
422
|
+
.deck-toolbar button {
|
|
423
|
+
appearance: none; cursor: pointer; font: inherit;
|
|
424
|
+
background: transparent; color: var(--text);
|
|
425
|
+
border: 1px solid var(--border); border-radius: 8px;
|
|
426
|
+
padding: 6px 12px; font-size: 12px;
|
|
427
|
+
}
|
|
428
|
+
.deck-toolbar button:hover:not(:disabled) { border-color: var(--accent); }
|
|
429
|
+
.deck-toolbar button:disabled {
|
|
430
|
+
color: var(--muted); cursor: not-allowed; opacity: 0.65;
|
|
431
|
+
}
|
|
432
|
+
.deck-toolbar button.copied {
|
|
433
|
+
color: var(--good); border-color: var(--good);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.selection-fab {
|
|
437
|
+
position: fixed; z-index: 50;
|
|
438
|
+
appearance: none; border: 0; cursor: pointer;
|
|
439
|
+
font: inherit; font-size: 11px; font-weight: 600;
|
|
440
|
+
padding: 6px 12px; border-radius: 6px;
|
|
441
|
+
background: var(--accent); color: #fff;
|
|
442
|
+
box-shadow: 0 4px 14px rgba(0,0,0,0.3);
|
|
443
|
+
display: none;
|
|
444
|
+
}
|
|
445
|
+
.selection-fab.visible { display: inline-flex; align-items: center; gap: 6px; }
|
|
446
|
+
|
|
447
|
+
/* ─── Modal for showing the copy-pasteable AI prompt ──────────────────── */
|
|
448
|
+
.modal-backdrop {
|
|
449
|
+
position: fixed; inset: 0; z-index: 100;
|
|
450
|
+
background: rgba(8, 8, 14, 0.55);
|
|
451
|
+
backdrop-filter: blur(2px);
|
|
452
|
+
display: none; align-items: center; justify-content: center;
|
|
453
|
+
padding: 20px;
|
|
454
|
+
}
|
|
455
|
+
.modal-backdrop.open { display: flex; }
|
|
456
|
+
.modal {
|
|
457
|
+
width: 100%; max-width: 560px; max-height: 86vh;
|
|
458
|
+
background: var(--panel); color: var(--text);
|
|
459
|
+
border: 1px solid var(--border); border-radius: 12px;
|
|
460
|
+
box-shadow: var(--shadow);
|
|
461
|
+
display: flex; flex-direction: column;
|
|
462
|
+
overflow: hidden;
|
|
463
|
+
}
|
|
464
|
+
.modal-header {
|
|
465
|
+
display: flex; align-items: center; gap: 12px;
|
|
466
|
+
padding: 14px 18px;
|
|
467
|
+
border-bottom: 1px solid var(--border);
|
|
468
|
+
}
|
|
469
|
+
.modal-header h3 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
470
|
+
.modal-header .grow { flex: 1; }
|
|
471
|
+
.modal-close {
|
|
472
|
+
appearance: none; border: 0; background: transparent;
|
|
473
|
+
font-size: 18px; line-height: 1; cursor: pointer;
|
|
474
|
+
color: var(--muted); padding: 4px 8px; border-radius: 4px;
|
|
475
|
+
}
|
|
476
|
+
.modal-close:hover { background: var(--panel-2); color: var(--text); }
|
|
477
|
+
.modal-body {
|
|
478
|
+
padding: 16px 18px; overflow-y: auto;
|
|
479
|
+
display: flex; flex-direction: column; gap: 12px;
|
|
480
|
+
}
|
|
481
|
+
.modal-body label {
|
|
482
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
|
|
483
|
+
color: var(--muted); font-weight: 600;
|
|
484
|
+
}
|
|
485
|
+
.modal-body textarea.instruction {
|
|
486
|
+
width: 100%; min-height: 56px; resize: vertical;
|
|
487
|
+
padding: 10px 12px; border-radius: 6px;
|
|
488
|
+
border: 1px solid var(--border); background: var(--panel-2);
|
|
489
|
+
color: var(--text); font: inherit; font-size: 13px;
|
|
490
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
491
|
+
}
|
|
492
|
+
.modal-body textarea.instruction:focus {
|
|
493
|
+
outline: none;
|
|
494
|
+
border-color: var(--accent);
|
|
495
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
|
|
496
|
+
}
|
|
497
|
+
.modal-body pre.prompt {
|
|
498
|
+
margin: 0; max-height: 320px; overflow: auto;
|
|
499
|
+
padding: 12px; border-radius: 6px;
|
|
500
|
+
background: var(--panel-2); border: 1px solid var(--border);
|
|
501
|
+
font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
502
|
+
color: var(--text); white-space: pre-wrap; word-break: break-word;
|
|
503
|
+
}
|
|
504
|
+
.modal-footer {
|
|
505
|
+
display: flex; align-items: center; gap: 12px;
|
|
506
|
+
padding: 12px 18px;
|
|
507
|
+
border-top: 1px solid var(--border);
|
|
508
|
+
background: var(--panel-2);
|
|
509
|
+
}
|
|
510
|
+
.modal-footer .hint { color: var(--muted); font-size: 11px; flex: 1; }
|
|
511
|
+
.copy-btn {
|
|
512
|
+
appearance: none; border: 0; cursor: pointer;
|
|
513
|
+
font: inherit; font-size: 13px; font-weight: 600;
|
|
514
|
+
padding: 8px 16px; border-radius: 6px;
|
|
515
|
+
background: var(--accent); color: #fff;
|
|
516
|
+
transition: background 0.15s ease, opacity 0.15s ease;
|
|
517
|
+
}
|
|
518
|
+
.copy-btn:hover { filter: brightness(1.08); }
|
|
519
|
+
.copy-btn.copied { background: var(--good); }
|
|
520
|
+
|
|
521
|
+
/* ─── Parse-error banner: shown when deck.json is invalid JSON ────────── */
|
|
522
|
+
.parse-error-banner {
|
|
523
|
+
display: none;
|
|
524
|
+
margin: 0 0 14px;
|
|
525
|
+
padding: 10px 14px; border-radius: 8px;
|
|
526
|
+
background: color-mix(in srgb, var(--bad) 12%, transparent);
|
|
527
|
+
border: 1px solid color-mix(in srgb, var(--bad) 30%, transparent);
|
|
528
|
+
color: var(--text);
|
|
529
|
+
font-size: 12px; line-height: 1.5;
|
|
530
|
+
}
|
|
531
|
+
.parse-error-banner.visible { display: block; }
|
|
532
|
+
.parse-error-banner strong {
|
|
533
|
+
color: var(--bad); font-weight: 600;
|
|
534
|
+
display: block; margin-bottom: 2px;
|
|
535
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
|
|
536
|
+
}
|
|
537
|
+
.parse-error-banner code {
|
|
538
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
539
|
+
font-size: 12px;
|
|
540
|
+
background: var(--panel-2);
|
|
541
|
+
padding: 1px 6px; border-radius: 4px;
|
|
542
|
+
word-break: break-all;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/* ─── Just-changed flash: highlights cards that changed on hot-reload ── */
|
|
546
|
+
@keyframes vfStageJustChanged {
|
|
547
|
+
0% { box-shadow: 0 0 0 3px var(--accent), var(--shadow); }
|
|
548
|
+
60% { box-shadow: 0 0 0 3px var(--accent), var(--shadow); }
|
|
549
|
+
100% { box-shadow: 0 0 0 0 transparent, var(--shadow); }
|
|
550
|
+
}
|
|
551
|
+
.stage-card.just-changed {
|
|
552
|
+
animation: vfStageJustChanged 1500ms ease-out;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
pre {
|
|
556
|
+
margin: 0;
|
|
557
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
558
|
+
font-size: 12px; line-height: 1.55;
|
|
559
|
+
color: var(--text); background: var(--panel-2);
|
|
560
|
+
border: 1px solid var(--border); border-radius: 8px;
|
|
561
|
+
padding: 14px; overflow: auto; max-height: 60vh;
|
|
562
|
+
}
|
|
563
|
+
.empty {
|
|
564
|
+
color: var(--muted);
|
|
565
|
+
font-size: 13px;
|
|
566
|
+
padding: 48px 12px;
|
|
567
|
+
text-align: center;
|
|
568
|
+
}
|
|
569
|
+
.footer-hint {
|
|
570
|
+
padding: 10px 20px 20px;
|
|
571
|
+
font-size: 11px; color: var(--muted);
|
|
572
|
+
text-align: center;
|
|
573
|
+
}
|
|
574
|
+
code.kbd {
|
|
575
|
+
background: var(--panel-2); border: 1px solid var(--border); border-radius: 4px;
|
|
576
|
+
padding: 1px 6px; font-family: ui-monospace, monospace; font-size: 11px;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
details.json-section { margin-top: 0; }
|
|
580
|
+
details.json-section > summary {
|
|
581
|
+
cursor: pointer; list-style: none;
|
|
582
|
+
font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em;
|
|
583
|
+
color: var(--muted); font-weight: 600;
|
|
584
|
+
padding: 6px 0;
|
|
585
|
+
}
|
|
586
|
+
details.json-section > summary::-webkit-details-marker { display: none; }
|
|
587
|
+
details.json-section > summary::before {
|
|
588
|
+
content: '▸'; display: inline-block; margin-right: 6px;
|
|
589
|
+
transition: transform 0.15s ease;
|
|
590
|
+
}
|
|
591
|
+
details.json-section[open] > summary::before { transform: rotate(90deg); }
|
|
592
|
+
</style>
|
|
593
|
+
</head>
|
|
594
|
+
<body>
|
|
595
|
+
<header>
|
|
596
|
+
<div class="brand">Vox<span class="accent">Flow</span> Stage <span style="color:var(--muted)">/ slice</span></div>
|
|
597
|
+
<div class="src" id="src" title="${safePath}">${safePath}</div>
|
|
598
|
+
<div class="grow"></div>
|
|
599
|
+
<div class="status">
|
|
600
|
+
<span class="dot" id="dot"></span>
|
|
601
|
+
<span id="status-label">connecting…</span>
|
|
602
|
+
</div>
|
|
603
|
+
<button class="render-btn" id="render-btn" type="button" aria-label="Render this deck to mp4 (cloud)">
|
|
604
|
+
<span class="icon">▶</span>
|
|
605
|
+
<span id="render-btn-label">Render mp4</span>
|
|
606
|
+
</button>
|
|
607
|
+
<button class="versions-btn" id="versions-btn" type="button" aria-label="Show version history" aria-expanded="false">
|
|
608
|
+
<span class="icon">📚</span>
|
|
609
|
+
<span>History</span>
|
|
610
|
+
<span class="count-pill" id="versions-count" hidden>0</span>
|
|
611
|
+
</button>
|
|
612
|
+
<button class="theme-toggle" id="theme-toggle" type="button" aria-label="Toggle color theme">
|
|
613
|
+
<span class="icon" id="theme-icon">◐</span>
|
|
614
|
+
<span id="theme-label">auto</span>
|
|
615
|
+
</button>
|
|
616
|
+
</header>
|
|
617
|
+
|
|
618
|
+
<aside class="versions-drawer" id="versions-drawer" role="dialog" aria-modal="false" aria-labelledby="versions-drawer-title">
|
|
619
|
+
<header class="drawer-head">
|
|
620
|
+
<span id="versions-drawer-title">Version history</span>
|
|
621
|
+
<span class="grow"></span>
|
|
622
|
+
<button class="drawer-close" id="versions-close" type="button" aria-label="Close history">×</button>
|
|
623
|
+
</header>
|
|
624
|
+
<div class="drawer-body" id="versions-body">
|
|
625
|
+
<div class="empty-msg">No history yet — edits will show up here.</div>
|
|
626
|
+
</div>
|
|
627
|
+
<div class="store-path" id="versions-store-path" hidden></div>
|
|
628
|
+
</aside>
|
|
629
|
+
|
|
630
|
+
<main>
|
|
631
|
+
<section>
|
|
632
|
+
<h2>
|
|
633
|
+
Cards
|
|
634
|
+
<span class="count" id="card-count" hidden>0</span>
|
|
635
|
+
<span class="preview-tag">preview · final mp4 from voxflow.studio/apps/slice</span>
|
|
636
|
+
</h2>
|
|
637
|
+
<div class="parse-error-banner" id="parse-error-banner" role="status" aria-live="polite">
|
|
638
|
+
<strong>Deck JSON is invalid — preview shows last good version</strong>
|
|
639
|
+
<span id="parse-error-detail"></span>
|
|
640
|
+
</div>
|
|
641
|
+
<div class="deck-toolbar" id="deck-toolbar">
|
|
642
|
+
<button id="copy-json-btn" type="button" disabled title="Copy raw deck.json to clipboard">Copy JSON</button>
|
|
643
|
+
<button id="download-json-btn" type="button" disabled title="Save deck.json to disk">Download .json</button>
|
|
644
|
+
<button id="copy-md-btn" type="button" disabled title="Copy as Markdown — paste into Notion / blog / 飞书">Copy as Markdown</button>
|
|
645
|
+
</div>
|
|
646
|
+
<div id="cards-pane" class="empty">Waiting for deck…</div>
|
|
647
|
+
</section>
|
|
648
|
+
<section>
|
|
649
|
+
<details class="json-section">
|
|
650
|
+
<summary>Deck JSON</summary>
|
|
651
|
+
<pre id="json-pane">// no deck loaded yet</pre>
|
|
652
|
+
</details>
|
|
653
|
+
</section>
|
|
654
|
+
</main>
|
|
655
|
+
|
|
656
|
+
<div class="footer-hint">
|
|
657
|
+
Edit <code class="kbd">${safePath}</code> in your editor — this page hot-reloads on save.
|
|
658
|
+
· Listening on <code class="kbd">http://127.0.0.1:${Number(port) || 5180}</code>
|
|
659
|
+
</div>
|
|
660
|
+
|
|
661
|
+
<button class="selection-fab" id="selection-fab" type="button" aria-label="Edit selection with AI">
|
|
662
|
+
<span>✏</span><span>Edit selection with AI</span>
|
|
663
|
+
</button>
|
|
664
|
+
|
|
665
|
+
<div class="modal-backdrop" id="render-modal" role="dialog" aria-modal="true" aria-labelledby="render-modal-title" hidden>
|
|
666
|
+
<div class="modal">
|
|
667
|
+
<div class="modal-header">
|
|
668
|
+
<h3 id="render-modal-title">Render this deck to mp4</h3>
|
|
669
|
+
<div class="grow"></div>
|
|
670
|
+
<button class="modal-close" id="render-modal-close" type="button" aria-label="Close">×</button>
|
|
671
|
+
</div>
|
|
672
|
+
<div class="modal-body render-modal-body" id="render-modal-body">
|
|
673
|
+
<div class="quota-line">
|
|
674
|
+
<span class="key">Cost</span>
|
|
675
|
+
<span class="val">500 quota</span>
|
|
676
|
+
</div>
|
|
677
|
+
<div class="quota-line" id="render-quota-line">
|
|
678
|
+
<span class="key">Your remaining</span>
|
|
679
|
+
<span class="val" id="render-quota-val">loading…</span>
|
|
680
|
+
</div>
|
|
681
|
+
<div class="progress-msg" id="render-progress-msg" hidden>
|
|
682
|
+
<span class="stage-label" id="render-progress-stage">Submitting…</span>
|
|
683
|
+
<span id="render-progress-detail"></span>
|
|
684
|
+
</div>
|
|
685
|
+
<div class="err-msg" id="render-err" hidden></div>
|
|
686
|
+
</div>
|
|
687
|
+
<div class="modal-footer">
|
|
688
|
+
<span class="hint">
|
|
689
|
+
Cloud rendered at voxflow.studio/apps/slice. The mp4 link opens in a new tab when ready.
|
|
690
|
+
</span>
|
|
691
|
+
<button class="render-confirm-btn" id="render-confirm" type="button" disabled>Render now</button>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
<div class="modal-backdrop" id="edit-modal" role="dialog" aria-modal="true" aria-labelledby="edit-modal-title" hidden>
|
|
697
|
+
<div class="modal">
|
|
698
|
+
<div class="modal-header">
|
|
699
|
+
<h3 id="edit-modal-title">Edit with AI</h3>
|
|
700
|
+
<div class="grow"></div>
|
|
701
|
+
<button class="modal-close" id="modal-close" type="button" aria-label="Close">×</button>
|
|
702
|
+
</div>
|
|
703
|
+
<div class="modal-body">
|
|
704
|
+
<label for="edit-instruction">Tell the AI what to change</label>
|
|
705
|
+
<textarea
|
|
706
|
+
id="edit-instruction"
|
|
707
|
+
class="instruction"
|
|
708
|
+
placeholder="e.g. shorten the narration to 30 chars and make it punchier"
|
|
709
|
+
autocomplete="off"
|
|
710
|
+
spellcheck="false"
|
|
711
|
+
></textarea>
|
|
712
|
+
<label>Prompt to send (auto-generated)</label>
|
|
713
|
+
<pre class="prompt" id="edit-prompt-preview"></pre>
|
|
714
|
+
</div>
|
|
715
|
+
<div class="modal-footer">
|
|
716
|
+
<span class="hint">
|
|
717
|
+
Paste into Claude Code / Cursor / ChatGPT — the file edit will hot-reload here.
|
|
718
|
+
</span>
|
|
719
|
+
<button class="copy-btn" id="copy-prompt" type="button">Copy prompt</button>
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
|
|
724
|
+
<script>
|
|
725
|
+
(function () {
|
|
726
|
+
// ─── Theme toggle (auto → light → dark → auto) ───────────────────────
|
|
727
|
+
var THEME_KEY = 'voxflow.stage.theme';
|
|
728
|
+
var html = document.documentElement;
|
|
729
|
+
var toggleBtn = document.getElementById('theme-toggle');
|
|
730
|
+
var iconEl = document.getElementById('theme-icon');
|
|
731
|
+
var labelEl = document.getElementById('theme-label');
|
|
732
|
+
|
|
733
|
+
function applyTheme(mode) {
|
|
734
|
+
html.setAttribute('data-theme', mode);
|
|
735
|
+
if (mode === 'light') { iconEl.textContent = '☀'; labelEl.textContent = 'light'; }
|
|
736
|
+
else if (mode === 'dark') { iconEl.textContent = '☾'; labelEl.textContent = 'dark'; }
|
|
737
|
+
else { iconEl.textContent = '◐'; labelEl.textContent = 'auto'; }
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
var stored;
|
|
741
|
+
try { stored = localStorage.getItem(THEME_KEY); } catch (_) { stored = null; }
|
|
742
|
+
applyTheme(stored === 'light' || stored === 'dark' ? stored : 'auto');
|
|
743
|
+
|
|
744
|
+
toggleBtn.addEventListener('click', function () {
|
|
745
|
+
var next = ({ auto: 'light', light: 'dark', dark: 'auto' })[
|
|
746
|
+
html.getAttribute('data-theme') || 'auto'
|
|
747
|
+
];
|
|
748
|
+
try { localStorage.setItem(THEME_KEY, next); } catch (_) {}
|
|
749
|
+
applyTheme(next);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// ─── Card-theme palettes (best-effort approximation of the 6 Slice
|
|
753
|
+
// themes — final pixel-accurate render lives in the cloud). ──────
|
|
754
|
+
var CARD_THEMES = {
|
|
755
|
+
'paper-slide': {
|
|
756
|
+
bg: '#f4ecd8', fg: '#2d2417', accent: '#8b6f47',
|
|
757
|
+
font: 'Georgia, "Times New Roman", serif',
|
|
758
|
+
titleWeight: 600, surface: 'light',
|
|
759
|
+
},
|
|
760
|
+
'editorial-mag': {
|
|
761
|
+
bg: '#ffffff', fg: '#0a0a0a', accent: '#c41e3a',
|
|
762
|
+
font: 'Georgia, "Times New Roman", serif',
|
|
763
|
+
titleWeight: 700, surface: 'light',
|
|
764
|
+
},
|
|
765
|
+
'bold-poster': {
|
|
766
|
+
bg: '#fde047', fg: '#0a0a0a', accent: '#dc2626',
|
|
767
|
+
font: 'system-ui, -apple-system, sans-serif',
|
|
768
|
+
titleWeight: 900, surface: 'light',
|
|
769
|
+
},
|
|
770
|
+
'notion-card': {
|
|
771
|
+
bg: '#ffffff', fg: '#37352f', accent: '#787774',
|
|
772
|
+
font: 'system-ui, "Segoe UI", -apple-system, sans-serif',
|
|
773
|
+
titleWeight: 600, surface: 'light',
|
|
774
|
+
border: '1px solid #e9e9e7',
|
|
775
|
+
},
|
|
776
|
+
'brutalist': {
|
|
777
|
+
bg: '#ffffff', fg: '#000000', accent: '#000000',
|
|
778
|
+
font: 'ui-monospace, "Courier New", monospace',
|
|
779
|
+
titleWeight: 900, surface: 'light',
|
|
780
|
+
border: '4px solid #000',
|
|
781
|
+
},
|
|
782
|
+
'glass-dark': {
|
|
783
|
+
bg: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
|
|
784
|
+
fg: '#e8e6f0', accent: '#a78bfa',
|
|
785
|
+
font: 'system-ui, -apple-system, sans-serif',
|
|
786
|
+
titleWeight: 700, surface: 'dark',
|
|
787
|
+
border: '1px solid rgba(255,255,255,0.12)',
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
var DEFAULT_THEME = 'paper-slide';
|
|
791
|
+
|
|
792
|
+
// ─── Element refs ────────────────────────────────────────────────────
|
|
793
|
+
var dot = document.getElementById('dot');
|
|
794
|
+
var statusLabel = document.getElementById('status-label');
|
|
795
|
+
var cardsPane = document.getElementById('cards-pane');
|
|
796
|
+
var jsonPane = document.getElementById('json-pane');
|
|
797
|
+
var countBadge = document.getElementById('card-count');
|
|
798
|
+
var modal = document.getElementById('edit-modal');
|
|
799
|
+
var modalTitle = document.getElementById('edit-modal-title');
|
|
800
|
+
var modalClose = document.getElementById('modal-close');
|
|
801
|
+
var instructionInput = document.getElementById('edit-instruction');
|
|
802
|
+
var promptPreview = document.getElementById('edit-prompt-preview');
|
|
803
|
+
var copyBtn = document.getElementById('copy-prompt');
|
|
804
|
+
var selectionFab = document.getElementById('selection-fab');
|
|
805
|
+
var errorBanner = document.getElementById('parse-error-banner');
|
|
806
|
+
var errorDetail = document.getElementById('parse-error-detail');
|
|
807
|
+
|
|
808
|
+
// ─── Text-export (Phase 1.4) — refs + formatter source embed ────────
|
|
809
|
+
var copyJsonBtn = document.getElementById('copy-json-btn');
|
|
810
|
+
var downloadJsonBtn = document.getElementById('download-json-btn');
|
|
811
|
+
var copyMdBtn = document.getElementById('copy-md-btn');
|
|
812
|
+
|
|
813
|
+
// Pure formatters lifted verbatim from lib/stage-core/deck-format.js
|
|
814
|
+
// via Function.prototype.toString — single source of truth, the
|
|
815
|
+
// node:test units cover the same source that runs in the browser.
|
|
816
|
+
var formatCardAsText = ${formatCardAsText.toString()};
|
|
817
|
+
var formatDeckAsMarkdown = ${formatDeckAsMarkdown.toString()};
|
|
818
|
+
var suggestDeckFilename = ${suggestDeckFilename.toString()};
|
|
819
|
+
|
|
820
|
+
function flashCopied(btn) {
|
|
821
|
+
var orig = btn.textContent;
|
|
822
|
+
btn.textContent = 'Copied!';
|
|
823
|
+
btn.classList.add('copied');
|
|
824
|
+
setTimeout(function () {
|
|
825
|
+
btn.textContent = orig;
|
|
826
|
+
btn.classList.remove('copied');
|
|
827
|
+
}, 1200);
|
|
828
|
+
}
|
|
829
|
+
function copyTextToClipboard(text, btn) {
|
|
830
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
831
|
+
navigator.clipboard.writeText(text).then(function () { flashCopied(btn); });
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
// Fallback for non-secure contexts: hidden textarea + execCommand
|
|
835
|
+
var ta = document.createElement('textarea');
|
|
836
|
+
ta.value = text;
|
|
837
|
+
ta.style.position = 'fixed';
|
|
838
|
+
ta.style.opacity = '0';
|
|
839
|
+
document.body.appendChild(ta);
|
|
840
|
+
ta.select();
|
|
841
|
+
try { document.execCommand('copy'); flashCopied(btn); } catch (_) {}
|
|
842
|
+
document.body.removeChild(ta);
|
|
843
|
+
}
|
|
844
|
+
function downloadJson(deck, btn) {
|
|
845
|
+
var blob = new Blob([JSON.stringify(deck, null, 2)], { type: 'application/json' });
|
|
846
|
+
var url = URL.createObjectURL(blob);
|
|
847
|
+
var a = document.createElement('a');
|
|
848
|
+
a.href = url;
|
|
849
|
+
a.download = suggestDeckFilename(deck);
|
|
850
|
+
document.body.appendChild(a);
|
|
851
|
+
a.click();
|
|
852
|
+
document.body.removeChild(a);
|
|
853
|
+
URL.revokeObjectURL(url);
|
|
854
|
+
flashCopied(btn);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
copyJsonBtn.addEventListener('click', function () {
|
|
858
|
+
if (!currentDeck) return;
|
|
859
|
+
copyTextToClipboard(JSON.stringify(currentDeck, null, 2), copyJsonBtn);
|
|
860
|
+
});
|
|
861
|
+
downloadJsonBtn.addEventListener('click', function () {
|
|
862
|
+
if (!currentDeck) return;
|
|
863
|
+
downloadJson(currentDeck, downloadJsonBtn);
|
|
864
|
+
});
|
|
865
|
+
copyMdBtn.addEventListener('click', function () {
|
|
866
|
+
if (!currentDeck) return;
|
|
867
|
+
copyTextToClipboard(formatDeckAsMarkdown(currentDeck), copyMdBtn);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// Per-card "Copy text" — event delegation on cardsPane so we don't
|
|
871
|
+
// re-bind on every hot-reload re-render.
|
|
872
|
+
cardsPane.addEventListener('click', function (ev) {
|
|
873
|
+
var btn = ev.target.closest && ev.target.closest('[data-action="copy-card"]');
|
|
874
|
+
if (!btn || !currentDeck) return;
|
|
875
|
+
var idx = parseInt(btn.getAttribute('data-card-index'), 10);
|
|
876
|
+
if (!Number.isFinite(idx)) return;
|
|
877
|
+
var cards = Array.isArray(currentDeck.cards) ? currentDeck.cards : [];
|
|
878
|
+
var card = cards[idx];
|
|
879
|
+
if (!card) return;
|
|
880
|
+
copyTextToClipboard(formatCardAsText(card), btn);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// For highlighting cards that just changed on hot-reload, we keep a
|
|
884
|
+
// hash of each card's stringified JSON. On the next deck event we
|
|
885
|
+
// diff per-index and add the just-changed CSS class to whichever
|
|
886
|
+
// DOM card moved.
|
|
887
|
+
var prevCardKeys = [];
|
|
888
|
+
// Whole-deck de-dup hash: editors that save atomically (write new,
|
|
889
|
+
// rename over old) cause fs.watch to fire 2-3 events for one save,
|
|
890
|
+
// which produced duplicate 'deck' SSE messages. Without this guard
|
|
891
|
+
// each duplicate would re-write innerHTML and wipe the
|
|
892
|
+
// .just-changed animation class added by the first render.
|
|
893
|
+
var lastRenderedDeckHash = null;
|
|
894
|
+
|
|
895
|
+
// Latest deck snapshot — populated by SSE. Edit-prompt builder reads
|
|
896
|
+
// current card data from here so the prompt always reflects what's
|
|
897
|
+
// on screen, not what was on screen when the modal first opened.
|
|
898
|
+
var currentDeck = null;
|
|
899
|
+
var currentDeckPath = '';
|
|
900
|
+
|
|
901
|
+
// Set when a deck event arrives with restoredFrom — the next
|
|
902
|
+
// renderDeck call should NOT flash diff highlights, because the user
|
|
903
|
+
// intentionally rewound history (every card is "different from prev"
|
|
904
|
+
// but that's the wrong story to tell visually).
|
|
905
|
+
var suppressDiffOnce = false;
|
|
906
|
+
|
|
907
|
+
function setStatus(state, label) {
|
|
908
|
+
dot.className = 'dot ' + state;
|
|
909
|
+
statusLabel.textContent = label;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function escapeHtml(s) {
|
|
913
|
+
return String(s == null ? '' : s).replace(/[<>&"']/g, function (c) {
|
|
914
|
+
return ({ '<':'<','>':'>','&':'&','"':'"',"'":''' })[c];
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function renderCard(card, i, themeId) {
|
|
919
|
+
var t = CARD_THEMES[themeId] || CARD_THEMES[DEFAULT_THEME];
|
|
920
|
+
var kind = card.kind === 'title' ? 'title' : 'body';
|
|
921
|
+
var titleHtml = '';
|
|
922
|
+
if (kind === 'title') {
|
|
923
|
+
var titleArr;
|
|
924
|
+
if (Array.isArray(card.title)) titleArr = card.title;
|
|
925
|
+
else if (typeof card.title === 'string') titleArr = [card.title];
|
|
926
|
+
else titleArr = ['(card ' + (i + 1) + ')'];
|
|
927
|
+
titleHtml = '<div class="title-text">'
|
|
928
|
+
+ titleArr.map(function (line) {
|
|
929
|
+
return '<span class="line">' + escapeHtml(line) + '</span>';
|
|
930
|
+
}).join('')
|
|
931
|
+
+ '</div>';
|
|
932
|
+
} else {
|
|
933
|
+
var caption = typeof card.caption === 'string' ? card.caption : ('Card ' + (i + 1));
|
|
934
|
+
var narration = typeof card.narration === 'string' ? card.narration : '';
|
|
935
|
+
titleHtml = '<div class="caption">' + escapeHtml(caption) + '</div>'
|
|
936
|
+
+ (narration ? '<div class="narration">' + escapeHtml(narration) + '</div>' : '');
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
var figureChip = card.figureKeyword
|
|
940
|
+
? '<div class="figure-chip">' + escapeHtml(card.figureKeyword) + '</div>'
|
|
941
|
+
: '';
|
|
942
|
+
|
|
943
|
+
var styleParts = [
|
|
944
|
+
'background:' + t.bg,
|
|
945
|
+
'color:' + t.fg,
|
|
946
|
+
'font-family:' + t.font,
|
|
947
|
+
'--card-title-weight:' + t.titleWeight,
|
|
948
|
+
'--card-accent:' + t.accent,
|
|
949
|
+
];
|
|
950
|
+
if (t.border) styleParts.push('border:' + t.border);
|
|
951
|
+
|
|
952
|
+
return '<div class="stage-card kind-' + kind + '"'
|
|
953
|
+
+ ' data-card-bg="' + (t.surface === 'dark' ? 'dark' : 'light') + '"'
|
|
954
|
+
+ ' data-card-idx="' + i + '"'
|
|
955
|
+
+ ' style="' + styleParts.join(';') + '">'
|
|
956
|
+
+ '<div class="frame-meta">'
|
|
957
|
+
+ '<span class="idx">#' + (i + 1) + '</span>'
|
|
958
|
+
+ '<span>' + escapeHtml(themeId) + ' · ' + escapeHtml(kind) + '</span>'
|
|
959
|
+
+ '</div>'
|
|
960
|
+
+ figureChip
|
|
961
|
+
+ '<div class="body">' + titleHtml + '</div>'
|
|
962
|
+
+ '<div class="accent-bar"></div>'
|
|
963
|
+
+ '<div class="card-actions">'
|
|
964
|
+
+ '<button type="button" data-action="copy-card" data-card-index="' + i + '"'
|
|
965
|
+
+ ' aria-label="Copy card ' + (i + 1) + ' as text">Copy text</button>'
|
|
966
|
+
+ '</div>'
|
|
967
|
+
+ '<button class="edit-btn" type="button" data-edit-card-idx="' + i + '"'
|
|
968
|
+
+ ' aria-label="Edit card ' + (i + 1) + ' with AI">'
|
|
969
|
+
+ '<span class="icon">✏</span>Edit with AI'
|
|
970
|
+
+ '</button>'
|
|
971
|
+
+ '</div>';
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function renderDeck(deck) {
|
|
975
|
+
currentDeck = deck;
|
|
976
|
+
// A successful render means the file is parseable now; clear any
|
|
977
|
+
// stale error banner from a previous broken-JSON state.
|
|
978
|
+
hideParseError();
|
|
979
|
+
var hasDeck = !!(deck && Array.isArray(deck.cards) && deck.cards.length > 0);
|
|
980
|
+
copyJsonBtn.disabled = !hasDeck;
|
|
981
|
+
downloadJsonBtn.disabled = !hasDeck;
|
|
982
|
+
copyMdBtn.disabled = !hasDeck;
|
|
983
|
+
if (!deck) {
|
|
984
|
+
cardsPane.className = 'empty';
|
|
985
|
+
cardsPane.textContent = 'Deck unavailable.';
|
|
986
|
+
jsonPane.textContent = '// no deck';
|
|
987
|
+
countBadge.hidden = true;
|
|
988
|
+
prevCardKeys = [];
|
|
989
|
+
lastRenderedDeckHash = null;
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
// Skip identical-content re-renders (atomic save → duplicate SSE).
|
|
993
|
+
// This both saves work and preserves the .just-changed animation
|
|
994
|
+
// applied on the first render of the new content.
|
|
995
|
+
var deckHash;
|
|
996
|
+
try { deckHash = JSON.stringify(deck); } catch (_) { deckHash = null; }
|
|
997
|
+
if (deckHash !== null && deckHash === lastRenderedDeckHash) return;
|
|
998
|
+
lastRenderedDeckHash = deckHash;
|
|
999
|
+
var cards = Array.isArray(deck.cards) ? deck.cards : [];
|
|
1000
|
+
var themeId = (typeof deck.theme === 'string' && CARD_THEMES[deck.theme])
|
|
1001
|
+
? deck.theme : DEFAULT_THEME;
|
|
1002
|
+
|
|
1003
|
+
// Compute per-card keys + which indices changed since the last
|
|
1004
|
+
// render. We diff BEFORE replacing innerHTML so the prev hash list
|
|
1005
|
+
// reflects the cards currently on screen.
|
|
1006
|
+
var newKeys = cards.map(function (c) {
|
|
1007
|
+
try { return JSON.stringify(c); } catch (_) { return Math.random().toString(36); }
|
|
1008
|
+
});
|
|
1009
|
+
var changedIdxs = [];
|
|
1010
|
+
for (var k = 0; k < newKeys.length; k++) {
|
|
1011
|
+
if (newKeys[k] !== prevCardKeys[k]) changedIdxs.push(k);
|
|
1012
|
+
}
|
|
1013
|
+
var firstRender = prevCardKeys.length === 0;
|
|
1014
|
+
prevCardKeys = newKeys;
|
|
1015
|
+
|
|
1016
|
+
if (cards.length === 0) {
|
|
1017
|
+
cardsPane.className = 'empty';
|
|
1018
|
+
cardsPane.textContent = 'Deck has no cards yet.';
|
|
1019
|
+
countBadge.hidden = true;
|
|
1020
|
+
} else {
|
|
1021
|
+
cardsPane.className = 'cards-grid';
|
|
1022
|
+
var html = '';
|
|
1023
|
+
for (var i = 0; i < cards.length; i++) {
|
|
1024
|
+
html += renderCard(cards[i] || {}, i, themeId);
|
|
1025
|
+
}
|
|
1026
|
+
cardsPane.innerHTML = html;
|
|
1027
|
+
countBadge.textContent = cards.length + ' · ' + themeId;
|
|
1028
|
+
countBadge.hidden = false;
|
|
1029
|
+
|
|
1030
|
+
// Highlight changed cards on hot-reload — but skip the very first
|
|
1031
|
+
// render so we don't flash every card just because the page loaded.
|
|
1032
|
+
// Also skip after a restore: the user just rewound history on
|
|
1033
|
+
// purpose; flashing every card as "just changed" would be noise.
|
|
1034
|
+
var skipDiffFlash = firstRender || suppressDiffOnce;
|
|
1035
|
+
suppressDiffOnce = false;
|
|
1036
|
+
if (!skipDiffFlash && changedIdxs.length > 0 && changedIdxs.length < cards.length) {
|
|
1037
|
+
for (var c2 = 0; c2 < changedIdxs.length; c2++) {
|
|
1038
|
+
var el = cardsPane.querySelector('[data-card-idx="' + changedIdxs[c2] + '"]');
|
|
1039
|
+
if (el) el.classList.add('just-changed');
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
jsonPane.textContent = JSON.stringify(deck, null, 2);
|
|
1045
|
+
} catch (e) {
|
|
1046
|
+
jsonPane.textContent = '// could not stringify deck: ' + e.message;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function showParseError(detailMsg) {
|
|
1051
|
+
errorDetail.innerHTML = '<code>' + escapeHtml(String(detailMsg || 'unknown')) + '</code>';
|
|
1052
|
+
errorBanner.classList.add('visible');
|
|
1053
|
+
}
|
|
1054
|
+
function hideParseError() {
|
|
1055
|
+
errorBanner.classList.remove('visible');
|
|
1056
|
+
errorDetail.textContent = '';
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function connect() {
|
|
1060
|
+
setStatus('warn', 'connecting…');
|
|
1061
|
+
var es = new EventSource('/events');
|
|
1062
|
+
es.addEventListener('deck', function (e) {
|
|
1063
|
+
setStatus('good', 'live');
|
|
1064
|
+
try {
|
|
1065
|
+
var msg = JSON.parse(e.data);
|
|
1066
|
+
if (msg && typeof msg.sourcePath === 'string') currentDeckPath = msg.sourcePath;
|
|
1067
|
+
if (msg && msg.restoredFrom) suppressDiffOnce = true;
|
|
1068
|
+
renderDeck(msg.deck);
|
|
1069
|
+
// Refresh the version drawer count + (if open) the row list. We
|
|
1070
|
+
// call this on every deck event so users always see the latest
|
|
1071
|
+
// count without having to re-open the drawer.
|
|
1072
|
+
refreshVersions();
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
setStatus('bad', 'parse error');
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
// The server bus emits { type: 'error', error: 'JSON parse failed: …' }
|
|
1078
|
+
// when the deck file becomes unparseable; that arrives here as an
|
|
1079
|
+
// SSE message with name=error AND data set. The connection-level
|
|
1080
|
+
// EventSource error (lost socket, etc.) arrives WITHOUT data — we
|
|
1081
|
+
// discriminate on e.data so neither swallows the other.
|
|
1082
|
+
es.addEventListener('error', function (e) {
|
|
1083
|
+
if (e && typeof e.data === 'string' && e.data.length > 0) {
|
|
1084
|
+
try {
|
|
1085
|
+
var msg = JSON.parse(e.data);
|
|
1086
|
+
if (msg && msg.type === 'error') {
|
|
1087
|
+
showParseError(msg.error || 'invalid JSON');
|
|
1088
|
+
setStatus('warn', 'deck error');
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
} catch (_) { /* fall through */ }
|
|
1092
|
+
}
|
|
1093
|
+
if (es.readyState === EventSource.CLOSED) {
|
|
1094
|
+
setStatus('bad', 'disconnected');
|
|
1095
|
+
} else {
|
|
1096
|
+
setStatus('warn', 'reconnecting…');
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
es.addEventListener('open', function () {
|
|
1100
|
+
setStatus('good', 'live');
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
connect();
|
|
1104
|
+
|
|
1105
|
+
// ─── Version history drawer ─────────────────────────────────────────
|
|
1106
|
+
var versionsBtn = document.getElementById('versions-btn');
|
|
1107
|
+
var versionsCount = document.getElementById('versions-count');
|
|
1108
|
+
var versionsDrawer = document.getElementById('versions-drawer');
|
|
1109
|
+
var versionsBody = document.getElementById('versions-body');
|
|
1110
|
+
var versionsClose = document.getElementById('versions-close');
|
|
1111
|
+
var versionsStorePath = document.getElementById('versions-store-path');
|
|
1112
|
+
|
|
1113
|
+
// Cached version list — rendered into the drawer when it opens.
|
|
1114
|
+
// We always poll on deck events to keep the count pill fresh, even
|
|
1115
|
+
// when the drawer is closed.
|
|
1116
|
+
var lastVersions = [];
|
|
1117
|
+
var lastStorePath = '';
|
|
1118
|
+
var restoringId = null;
|
|
1119
|
+
|
|
1120
|
+
function fmtRelativeTime(ts) {
|
|
1121
|
+
var diffSec = Math.floor((Date.now() - ts) / 1000);
|
|
1122
|
+
if (diffSec < 5) return 'just now';
|
|
1123
|
+
if (diffSec < 60) return diffSec + 's ago';
|
|
1124
|
+
if (diffSec < 3600) return Math.floor(diffSec / 60) + 'm ago';
|
|
1125
|
+
if (diffSec < 86400) return Math.floor(diffSec / 3600) + 'h ago';
|
|
1126
|
+
return Math.floor(diffSec / 86400) + 'd ago';
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function refreshVersions() {
|
|
1130
|
+
fetch('/api/snapshots', { credentials: 'omit' })
|
|
1131
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
1132
|
+
.then(function (data) {
|
|
1133
|
+
if (!data || !Array.isArray(data.versions)) return;
|
|
1134
|
+
lastVersions = data.versions;
|
|
1135
|
+
lastStorePath = data.storePath || '';
|
|
1136
|
+
versionsCount.textContent = String(lastVersions.length);
|
|
1137
|
+
versionsCount.hidden = lastVersions.length === 0;
|
|
1138
|
+
if (versionsDrawer.classList.contains('open')) renderVersionsList();
|
|
1139
|
+
})
|
|
1140
|
+
.catch(function () { /* drawer feature is best-effort */ });
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function renderVersionsList() {
|
|
1144
|
+
if (lastVersions.length === 0) {
|
|
1145
|
+
versionsBody.innerHTML = '<div class="empty-msg">No history yet — edits will show up here.</div>';
|
|
1146
|
+
versionsStorePath.hidden = true;
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
// The current deck = the newest snapshot (since every parseable
|
|
1150
|
+
// change is snapshotted before publish). Tag its row.
|
|
1151
|
+
var html = '';
|
|
1152
|
+
for (var i = 0; i < lastVersions.length; i++) {
|
|
1153
|
+
var v = lastVersions[i];
|
|
1154
|
+
var isCurrent = (i === 0);
|
|
1155
|
+
var label = isCurrent ? 'current' : fmtRelativeTime(v.ts);
|
|
1156
|
+
var meta = v.cardCount + ' cards · ' + (v.sizeBytes >= 1024
|
|
1157
|
+
? (v.sizeBytes / 1024).toFixed(1) + 'KB' : v.sizeBytes + 'B');
|
|
1158
|
+
var btn = isCurrent ? '' :
|
|
1159
|
+
'<button class="restore-btn" type="button" data-restore-id="' + escapeHtml(v.id) + '"'
|
|
1160
|
+
+ (restoringId === v.id ? ' disabled' : '')
|
|
1161
|
+
+ '>' + (restoringId === v.id ? 'restoring…' : 'Restore') + '</button>';
|
|
1162
|
+
html += '<div class="version-row ' + (isCurrent ? 'current' : '') + '">'
|
|
1163
|
+
+ '<div>'
|
|
1164
|
+
+ '<div class="ts">' + escapeHtml(label) + '</div>'
|
|
1165
|
+
+ '<div class="meta">' + escapeHtml(meta) + '</div>'
|
|
1166
|
+
+ '</div>'
|
|
1167
|
+
+ btn
|
|
1168
|
+
+ '</div>';
|
|
1169
|
+
}
|
|
1170
|
+
versionsBody.innerHTML = html;
|
|
1171
|
+
if (lastStorePath) {
|
|
1172
|
+
versionsStorePath.textContent = lastStorePath;
|
|
1173
|
+
versionsStorePath.hidden = false;
|
|
1174
|
+
} else {
|
|
1175
|
+
versionsStorePath.hidden = true;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function openVersionsDrawer() {
|
|
1180
|
+
versionsDrawer.classList.add('open');
|
|
1181
|
+
versionsBtn.setAttribute('aria-expanded', 'true');
|
|
1182
|
+
renderVersionsList();
|
|
1183
|
+
}
|
|
1184
|
+
function closeVersionsDrawer() {
|
|
1185
|
+
versionsDrawer.classList.remove('open');
|
|
1186
|
+
versionsBtn.setAttribute('aria-expanded', 'false');
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
versionsBtn.addEventListener('click', function () {
|
|
1190
|
+
if (versionsDrawer.classList.contains('open')) closeVersionsDrawer();
|
|
1191
|
+
else openVersionsDrawer();
|
|
1192
|
+
});
|
|
1193
|
+
versionsClose.addEventListener('click', closeVersionsDrawer);
|
|
1194
|
+
// Esc closes the drawer (mirrors modal behavior).
|
|
1195
|
+
document.addEventListener('keydown', function (e) {
|
|
1196
|
+
if (e.key === 'Escape' && versionsDrawer.classList.contains('open')) {
|
|
1197
|
+
closeVersionsDrawer();
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
versionsBody.addEventListener('click', function (e) {
|
|
1202
|
+
var target = e.target.closest('[data-restore-id]');
|
|
1203
|
+
if (!target) return;
|
|
1204
|
+
var id = target.getAttribute('data-restore-id');
|
|
1205
|
+
if (!id || restoringId) return;
|
|
1206
|
+
restoringId = id;
|
|
1207
|
+
renderVersionsList();
|
|
1208
|
+
fetch('/api/snapshots/restore', {
|
|
1209
|
+
method: 'POST',
|
|
1210
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1211
|
+
credentials: 'omit',
|
|
1212
|
+
body: JSON.stringify({ id: id }),
|
|
1213
|
+
})
|
|
1214
|
+
.then(function (r) {
|
|
1215
|
+
if (!r.ok) throw new Error('restore failed: ' + r.status);
|
|
1216
|
+
// Watcher → SSE deck event will fire shortly with restoredFrom set;
|
|
1217
|
+
// the diff-highlight suppression and refreshVersions both run there.
|
|
1218
|
+
})
|
|
1219
|
+
.catch(function (err) {
|
|
1220
|
+
// Surface the failure inline in the drawer so the user isn't
|
|
1221
|
+
// left wondering why the page didn't update.
|
|
1222
|
+
versionsBody.innerHTML = '<div class="empty-msg" style="color:var(--bad)">'
|
|
1223
|
+
+ escapeHtml('Restore failed: ' + err.message) + '</div>';
|
|
1224
|
+
})
|
|
1225
|
+
.finally(function () {
|
|
1226
|
+
restoringId = null;
|
|
1227
|
+
// Re-render after the deck event lands, but also after a short
|
|
1228
|
+
// tick so the disabled button visually clears even if the deck
|
|
1229
|
+
// event somehow doesn't arrive.
|
|
1230
|
+
setTimeout(function () {
|
|
1231
|
+
if (versionsDrawer.classList.contains('open')) renderVersionsList();
|
|
1232
|
+
}, 250);
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
// First load (the initial deck SSE event also calls this, but we want
|
|
1237
|
+
// the count pill populated even if the deck event arrives late).
|
|
1238
|
+
refreshVersions();
|
|
1239
|
+
|
|
1240
|
+
// ─── Render to MP4 (cloud) ──────────────────────────────────────────
|
|
1241
|
+
// State machine:
|
|
1242
|
+
// idle → button "Render mp4", click opens modal
|
|
1243
|
+
// confirming → modal open, fetching quota, "Render now" enables
|
|
1244
|
+
// once quota ≥ 500 (or 401/network error shown)
|
|
1245
|
+
// submitting → POST /api/render-mp4, button disabled
|
|
1246
|
+
// polling → modal hidden, button shows "Rendering…", poll every 3s
|
|
1247
|
+
// completed → button becomes "Open mp4 ↗" link to video URL
|
|
1248
|
+
// failed → button reverts to idle, modal shows error on next click
|
|
1249
|
+
var renderBtn = document.getElementById('render-btn');
|
|
1250
|
+
var renderBtnLabel = document.getElementById('render-btn-label');
|
|
1251
|
+
var renderModal = document.getElementById('render-modal');
|
|
1252
|
+
var renderModalClose = document.getElementById('render-modal-close');
|
|
1253
|
+
var renderConfirm = document.getElementById('render-confirm');
|
|
1254
|
+
var renderQuotaLine = document.getElementById('render-quota-line');
|
|
1255
|
+
var renderQuotaVal = document.getElementById('render-quota-val');
|
|
1256
|
+
var renderProgressMsg = document.getElementById('render-progress-msg');
|
|
1257
|
+
var renderProgressStage = document.getElementById('render-progress-stage');
|
|
1258
|
+
var renderProgressDetail = document.getElementById('render-progress-detail');
|
|
1259
|
+
var renderErr = document.getElementById('render-err');
|
|
1260
|
+
|
|
1261
|
+
var RENDER_COST = 500;
|
|
1262
|
+
var renderState = 'idle';
|
|
1263
|
+
var renderJobId = null;
|
|
1264
|
+
var renderVideoUrl = null;
|
|
1265
|
+
var renderPollTimer = null;
|
|
1266
|
+
var renderLastError = null;
|
|
1267
|
+
|
|
1268
|
+
function setRenderError(msg) {
|
|
1269
|
+
renderLastError = msg;
|
|
1270
|
+
renderErr.textContent = msg;
|
|
1271
|
+
renderErr.hidden = false;
|
|
1272
|
+
}
|
|
1273
|
+
function clearRenderError() {
|
|
1274
|
+
renderLastError = null;
|
|
1275
|
+
renderErr.hidden = true;
|
|
1276
|
+
renderErr.textContent = '';
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function refreshRenderBtn() {
|
|
1280
|
+
renderBtn.classList.remove('completed');
|
|
1281
|
+
if (renderState === 'idle' || renderState === 'failed') {
|
|
1282
|
+
renderBtn.disabled = false;
|
|
1283
|
+
renderBtnLabel.textContent = 'Render mp4';
|
|
1284
|
+
renderBtn.setAttribute('aria-label', 'Render this deck to mp4 (cloud)');
|
|
1285
|
+
} else if (renderState === 'submitting') {
|
|
1286
|
+
renderBtn.disabled = true;
|
|
1287
|
+
renderBtnLabel.textContent = 'Submitting…';
|
|
1288
|
+
} else if (renderState === 'polling') {
|
|
1289
|
+
renderBtn.disabled = false; // click reopens modal to show progress
|
|
1290
|
+
renderBtnLabel.textContent = 'Rendering…';
|
|
1291
|
+
} else if (renderState === 'completed') {
|
|
1292
|
+
renderBtn.disabled = false;
|
|
1293
|
+
renderBtn.classList.add('completed');
|
|
1294
|
+
renderBtnLabel.textContent = 'Open mp4 ↗';
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function openRenderModal() {
|
|
1299
|
+
renderModal.hidden = false;
|
|
1300
|
+
renderModal.classList.add('open');
|
|
1301
|
+
clearRenderError();
|
|
1302
|
+
if (renderState === 'polling') {
|
|
1303
|
+
// Reopen during polling: show progress, hide confirm.
|
|
1304
|
+
renderProgressMsg.hidden = false;
|
|
1305
|
+
renderQuotaLine.style.display = 'none';
|
|
1306
|
+
renderConfirm.hidden = true;
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
renderProgressMsg.hidden = true;
|
|
1310
|
+
renderQuotaLine.style.display = '';
|
|
1311
|
+
renderConfirm.hidden = false;
|
|
1312
|
+
renderConfirm.disabled = true;
|
|
1313
|
+
renderConfirm.textContent = 'Render now';
|
|
1314
|
+
// Pull quota balance.
|
|
1315
|
+
renderQuotaVal.textContent = 'loading…';
|
|
1316
|
+
renderQuotaLine.classList.remove('short');
|
|
1317
|
+
fetch('/api/quota-balance', { credentials: 'omit' })
|
|
1318
|
+
.then(function (r) { return r.json().then(function (j) { return { status: r.status, json: j }; }); })
|
|
1319
|
+
.then(function (resp) {
|
|
1320
|
+
if (resp.status === 401 || resp.json.code === 'not_logged_in') {
|
|
1321
|
+
renderQuotaVal.textContent = 'not logged in';
|
|
1322
|
+
setRenderError('Run \\u0060voxflow login\\u0060 in your terminal, then refresh this page.');
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (resp.status !== 200 || resp.json.code !== 'success') {
|
|
1326
|
+
renderQuotaVal.textContent = '?';
|
|
1327
|
+
setRenderError('Could not read quota: ' + (resp.json.message || 'HTTP ' + resp.status));
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
var rem = resp.json.remaining == null ? null : Number(resp.json.remaining);
|
|
1331
|
+
renderQuotaVal.textContent = rem == null ? '?' : rem.toLocaleString();
|
|
1332
|
+
if (rem !== null && rem < RENDER_COST) {
|
|
1333
|
+
renderQuotaLine.classList.add('short');
|
|
1334
|
+
setRenderError('Insufficient quota — need ' + RENDER_COST + ', have ' + rem + '.');
|
|
1335
|
+
renderConfirm.disabled = true;
|
|
1336
|
+
} else {
|
|
1337
|
+
renderConfirm.disabled = false;
|
|
1338
|
+
}
|
|
1339
|
+
})
|
|
1340
|
+
.catch(function (err) {
|
|
1341
|
+
renderQuotaVal.textContent = '?';
|
|
1342
|
+
setRenderError('Quota lookup failed: ' + err.message);
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
function closeRenderModal() {
|
|
1346
|
+
renderModal.classList.remove('open');
|
|
1347
|
+
renderModal.hidden = true;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function startPolling() {
|
|
1351
|
+
function tick() {
|
|
1352
|
+
fetch('/api/render-status/' + encodeURIComponent(renderJobId), { credentials: 'omit' })
|
|
1353
|
+
.then(function (r) { return r.json().then(function (j) { return { status: r.status, json: j }; }); })
|
|
1354
|
+
.then(function (resp) {
|
|
1355
|
+
if (resp.status !== 200 || resp.json.code !== 'success') {
|
|
1356
|
+
// Transient errors — keep polling. The user can give up by
|
|
1357
|
+
// refreshing the page; we don't auto-cancel because the job
|
|
1358
|
+
// itself is still running on the backend.
|
|
1359
|
+
renderProgressDetail.textContent = ' · ' + (resp.json.message || 'retrying…');
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
var st = resp.json.status || 'unknown';
|
|
1363
|
+
renderProgressStage.textContent = st;
|
|
1364
|
+
renderProgressDetail.textContent = '';
|
|
1365
|
+
if (st === 'completed') {
|
|
1366
|
+
renderState = 'completed';
|
|
1367
|
+
renderVideoUrl = resp.json.videoUrl || null;
|
|
1368
|
+
clearInterval(renderPollTimer);
|
|
1369
|
+
renderPollTimer = null;
|
|
1370
|
+
refreshRenderBtn();
|
|
1371
|
+
closeRenderModal();
|
|
1372
|
+
if (renderVideoUrl) {
|
|
1373
|
+
// Auto-open the mp4 — same gesture as a "Render → Open"
|
|
1374
|
+
// single click would be elsewhere. Future click on the
|
|
1375
|
+
// button still re-opens (browser may block the auto-open
|
|
1376
|
+
// but the button is the recovery affordance).
|
|
1377
|
+
try { window.open(renderVideoUrl, '_blank', 'noopener'); } catch (_) { /* popup blocked */ }
|
|
1378
|
+
}
|
|
1379
|
+
} else if (st === 'failed' || st === 'error') {
|
|
1380
|
+
renderState = 'failed';
|
|
1381
|
+
clearInterval(renderPollTimer);
|
|
1382
|
+
renderPollTimer = null;
|
|
1383
|
+
refreshRenderBtn();
|
|
1384
|
+
setRenderError('Render failed. Check \\u0060voxflow status\\u0060 or retry.');
|
|
1385
|
+
}
|
|
1386
|
+
})
|
|
1387
|
+
.catch(function (err) {
|
|
1388
|
+
renderProgressDetail.textContent = ' · ' + err.message;
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
tick();
|
|
1392
|
+
renderPollTimer = setInterval(tick, 3000);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
renderBtn.addEventListener('click', function () {
|
|
1396
|
+
if (renderState === 'completed' && renderVideoUrl) {
|
|
1397
|
+
window.open(renderVideoUrl, '_blank', 'noopener');
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
openRenderModal();
|
|
1401
|
+
});
|
|
1402
|
+
renderModalClose.addEventListener('click', closeRenderModal);
|
|
1403
|
+
// Click outside the inner modal closes (mirrors edit-modal).
|
|
1404
|
+
renderModal.addEventListener('click', function (e) {
|
|
1405
|
+
if (e.target === renderModal) closeRenderModal();
|
|
1406
|
+
});
|
|
1407
|
+
// Esc closes (mirrors edit-modal + drawer).
|
|
1408
|
+
document.addEventListener('keydown', function (e) {
|
|
1409
|
+
if (e.key === 'Escape' && renderModal.classList.contains('open')) {
|
|
1410
|
+
closeRenderModal();
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
renderConfirm.addEventListener('click', function () {
|
|
1415
|
+
if (!currentDeck || !Array.isArray(currentDeck.cards)) {
|
|
1416
|
+
setRenderError('No deck loaded — fix the JSON parse error first.');
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
renderConfirm.disabled = true;
|
|
1420
|
+
renderConfirm.textContent = 'Submitting…';
|
|
1421
|
+
clearRenderError();
|
|
1422
|
+
renderState = 'submitting';
|
|
1423
|
+
refreshRenderBtn();
|
|
1424
|
+
fetch('/api/render-mp4', {
|
|
1425
|
+
method: 'POST',
|
|
1426
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1427
|
+
credentials: 'omit',
|
|
1428
|
+
body: JSON.stringify({ deck: currentDeck }),
|
|
1429
|
+
})
|
|
1430
|
+
.then(function (r) { return r.json().then(function (j) { return { status: r.status, json: j }; }); })
|
|
1431
|
+
.then(function (resp) {
|
|
1432
|
+
if (resp.status !== 200 || resp.json.code !== 'success') {
|
|
1433
|
+
renderState = 'failed';
|
|
1434
|
+
refreshRenderBtn();
|
|
1435
|
+
setRenderError(resp.json.message || ('Render submit failed (' + resp.status + ')'));
|
|
1436
|
+
renderConfirm.disabled = false;
|
|
1437
|
+
renderConfirm.textContent = 'Render now';
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
renderJobId = resp.json.jobId;
|
|
1441
|
+
renderState = 'polling';
|
|
1442
|
+
refreshRenderBtn();
|
|
1443
|
+
// Switch the modal into "rendering…" mode so the user sees the
|
|
1444
|
+
// job pick up. They can close the modal — polling continues in
|
|
1445
|
+
// the background and the button label tracks state.
|
|
1446
|
+
renderQuotaLine.style.display = 'none';
|
|
1447
|
+
renderConfirm.hidden = true;
|
|
1448
|
+
renderProgressMsg.hidden = false;
|
|
1449
|
+
renderProgressStage.textContent = 'pending';
|
|
1450
|
+
renderProgressDetail.textContent = ' · job ' + renderJobId;
|
|
1451
|
+
startPolling();
|
|
1452
|
+
})
|
|
1453
|
+
.catch(function (err) {
|
|
1454
|
+
renderState = 'failed';
|
|
1455
|
+
refreshRenderBtn();
|
|
1456
|
+
setRenderError('Network error: ' + err.message);
|
|
1457
|
+
renderConfirm.disabled = false;
|
|
1458
|
+
renderConfirm.textContent = 'Render now';
|
|
1459
|
+
});
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
refreshRenderBtn();
|
|
1463
|
+
|
|
1464
|
+
// ─── Edit-with-AI: prompt builder (mirrors stage-core/edit-prompt.js) ─
|
|
1465
|
+
// Kept as a small in-browser duplicate because the template is an
|
|
1466
|
+
// inline string — pulling the Node module across the boundary would
|
|
1467
|
+
// need a build step we deliberately don't have. The Node module is
|
|
1468
|
+
// unit-tested; the template version follows the same contract.
|
|
1469
|
+
function buildEditPrompt(args) {
|
|
1470
|
+
var deckPath = String(args.deckPath || '').trim();
|
|
1471
|
+
var cardIdx = (typeof args.cardIdx === 'number') ? args.cardIdx : 0;
|
|
1472
|
+
var card = (args.card && typeof args.card === 'object') ? args.card : {};
|
|
1473
|
+
var selection = (typeof args.selectedText === 'string') ? args.selectedText.trim() : '';
|
|
1474
|
+
var instruction = (typeof args.instruction === 'string') ? args.instruction.trim() : '';
|
|
1475
|
+
var kind = (typeof card.kind === 'string' && card.kind) ? card.kind : 'unknown';
|
|
1476
|
+
|
|
1477
|
+
var cardJson;
|
|
1478
|
+
try {
|
|
1479
|
+
cardJson = JSON.stringify(card, null, 2);
|
|
1480
|
+
} catch (_) {
|
|
1481
|
+
cardJson = JSON.stringify({ kind: card.kind || null, _note: 'card not stringifiable' }, null, 2);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
var instructionLine = instruction
|
|
1485
|
+
? ' ' + instruction
|
|
1486
|
+
: ' <REPLACE THIS WITH YOUR INSTRUCTION — e.g. "make it punchier"; "shorten to 12 chars">';
|
|
1487
|
+
|
|
1488
|
+
var lines = [
|
|
1489
|
+
"I'm iterating on a Slice deck (vertical 1080x1920 card video — narrated, theme-styled).",
|
|
1490
|
+
'',
|
|
1491
|
+
'File to edit:',
|
|
1492
|
+
' ' + deckPath,
|
|
1493
|
+
'',
|
|
1494
|
+
'Card to change: cards[' + cardIdx + '] (kind: ' + kind + ')',
|
|
1495
|
+
];
|
|
1496
|
+
|
|
1497
|
+
if (selection) {
|
|
1498
|
+
lines.push('');
|
|
1499
|
+
lines.push('Specifically, this exact substring needs to change:');
|
|
1500
|
+
lines.push(' ' + JSON.stringify(selection));
|
|
1501
|
+
lines.push('');
|
|
1502
|
+
lines.push('Replace only that substring; keep everything else in the card identical unless the change requires touching adjacent text.');
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
lines.push('');
|
|
1506
|
+
lines.push('Current card value:');
|
|
1507
|
+
lines.push('\`\`\`json');
|
|
1508
|
+
lines.push(cardJson);
|
|
1509
|
+
lines.push('\`\`\`');
|
|
1510
|
+
lines.push('');
|
|
1511
|
+
lines.push('Change request:');
|
|
1512
|
+
lines.push(instructionLine);
|
|
1513
|
+
lines.push('');
|
|
1514
|
+
lines.push('When you finish, save the file in place — preserve every other field and the JSON structure.');
|
|
1515
|
+
lines.push('The local stage server is watching this file and will hot-reload the browser preview within ~150ms.');
|
|
1516
|
+
|
|
1517
|
+
return lines.join('\\n');
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// ─── Edit-with-AI: modal state + handlers ────────────────────────────
|
|
1521
|
+
var modalCtx = null; // { cardIdx, selectedText }
|
|
1522
|
+
|
|
1523
|
+
function refreshPromptPreview() {
|
|
1524
|
+
if (!modalCtx) return;
|
|
1525
|
+
var card = (currentDeck && Array.isArray(currentDeck.cards))
|
|
1526
|
+
? currentDeck.cards[modalCtx.cardIdx] : null;
|
|
1527
|
+
if (!card) {
|
|
1528
|
+
promptPreview.textContent = '// card no longer exists in deck — close and pick another';
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
promptPreview.textContent = buildEditPrompt({
|
|
1532
|
+
deckPath: currentDeckPath,
|
|
1533
|
+
cardIdx: modalCtx.cardIdx,
|
|
1534
|
+
card: card,
|
|
1535
|
+
selectedText: modalCtx.selectedText,
|
|
1536
|
+
instruction: instructionInput.value,
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function openModal(cardIdx, selectedText) {
|
|
1541
|
+
modalCtx = { cardIdx: cardIdx, selectedText: selectedText || '' };
|
|
1542
|
+
modalTitle.textContent = selectedText
|
|
1543
|
+
? ('Edit selection in card #' + (cardIdx + 1))
|
|
1544
|
+
: ('Edit card #' + (cardIdx + 1) + ' with AI');
|
|
1545
|
+
instructionInput.value = '';
|
|
1546
|
+
copyBtn.classList.remove('copied');
|
|
1547
|
+
copyBtn.textContent = 'Copy prompt';
|
|
1548
|
+
refreshPromptPreview();
|
|
1549
|
+
modal.classList.add('open');
|
|
1550
|
+
modal.removeAttribute('hidden');
|
|
1551
|
+
// Focus the instruction box so the user can type immediately.
|
|
1552
|
+
setTimeout(function () { instructionInput.focus(); }, 30);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function closeModal() {
|
|
1556
|
+
modal.classList.remove('open');
|
|
1557
|
+
modal.setAttribute('hidden', '');
|
|
1558
|
+
modalCtx = null;
|
|
1559
|
+
hideSelectionFab();
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
modalClose.addEventListener('click', closeModal);
|
|
1563
|
+
modal.addEventListener('click', function (e) {
|
|
1564
|
+
// Click on backdrop (not the inner .modal) closes.
|
|
1565
|
+
if (e.target === modal) closeModal();
|
|
1566
|
+
});
|
|
1567
|
+
document.addEventListener('keydown', function (e) {
|
|
1568
|
+
if (e.key === 'Escape' && modal.classList.contains('open')) closeModal();
|
|
1569
|
+
});
|
|
1570
|
+
instructionInput.addEventListener('input', refreshPromptPreview);
|
|
1571
|
+
|
|
1572
|
+
copyBtn.addEventListener('click', function () {
|
|
1573
|
+
var text = promptPreview.textContent;
|
|
1574
|
+
var done = function () {
|
|
1575
|
+
copyBtn.classList.add('copied');
|
|
1576
|
+
copyBtn.textContent = '✓ Copied — paste in your AI';
|
|
1577
|
+
// Auto-close the modal so the user can switch straight to their
|
|
1578
|
+
// AI tool. 700ms gives them time to read the "Copied" state.
|
|
1579
|
+
setTimeout(function () {
|
|
1580
|
+
if (modal.classList.contains('open')) closeModal();
|
|
1581
|
+
copyBtn.classList.remove('copied');
|
|
1582
|
+
copyBtn.textContent = 'Copy prompt';
|
|
1583
|
+
}, 700);
|
|
1584
|
+
};
|
|
1585
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1586
|
+
navigator.clipboard.writeText(text).then(done, function () {
|
|
1587
|
+
// Fallback for non-secure contexts (file://, very old browsers)
|
|
1588
|
+
fallbackCopy(text); done();
|
|
1589
|
+
});
|
|
1590
|
+
} else {
|
|
1591
|
+
fallbackCopy(text); done();
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
function fallbackCopy(text) {
|
|
1596
|
+
var ta = document.createElement('textarea');
|
|
1597
|
+
ta.value = text;
|
|
1598
|
+
ta.style.position = 'fixed'; ta.style.opacity = '0';
|
|
1599
|
+
document.body.appendChild(ta);
|
|
1600
|
+
ta.select();
|
|
1601
|
+
try { document.execCommand('copy'); } catch (_) { /* give up */ }
|
|
1602
|
+
document.body.removeChild(ta);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// ─── Per-card "Edit with AI" button (event delegation) ───────────────
|
|
1606
|
+
cardsPane.addEventListener('click', function (e) {
|
|
1607
|
+
var btn = e.target.closest && e.target.closest('.edit-btn');
|
|
1608
|
+
if (!btn) return;
|
|
1609
|
+
var idx = parseInt(btn.getAttribute('data-edit-card-idx'), 10);
|
|
1610
|
+
if (Number.isFinite(idx)) openModal(idx, '');
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
// ─── Text-selection floating button ──────────────────────────────────
|
|
1614
|
+
function hideSelectionFab() {
|
|
1615
|
+
selectionFab.classList.remove('visible');
|
|
1616
|
+
selectionFab._cardIdx = null;
|
|
1617
|
+
selectionFab._text = '';
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function maybeShowSelectionFab() {
|
|
1621
|
+
var sel = window.getSelection();
|
|
1622
|
+
if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
|
|
1623
|
+
hideSelectionFab();
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
var text = sel.toString().trim();
|
|
1627
|
+
if (!text) { hideSelectionFab(); return; }
|
|
1628
|
+
// Anchor must be inside a stage card.
|
|
1629
|
+
var anchor = sel.anchorNode;
|
|
1630
|
+
var cardEl = (anchor && anchor.nodeType === 1)
|
|
1631
|
+
? anchor.closest && anchor.closest('.stage-card')
|
|
1632
|
+
: (anchor && anchor.parentElement && anchor.parentElement.closest('.stage-card'));
|
|
1633
|
+
if (!cardEl) { hideSelectionFab(); return; }
|
|
1634
|
+
var idxStr = cardEl.getAttribute('data-card-idx');
|
|
1635
|
+
var idx = parseInt(idxStr, 10);
|
|
1636
|
+
if (!Number.isFinite(idx)) { hideSelectionFab(); return; }
|
|
1637
|
+
|
|
1638
|
+
var rect = sel.getRangeAt(0).getBoundingClientRect();
|
|
1639
|
+
// Position just below the selection, clamped inside viewport.
|
|
1640
|
+
var x = Math.min(window.innerWidth - 220, Math.max(8, rect.left));
|
|
1641
|
+
var y = Math.min(window.innerHeight - 40, rect.bottom + 6);
|
|
1642
|
+
selectionFab.style.left = x + 'px';
|
|
1643
|
+
selectionFab.style.top = y + 'px';
|
|
1644
|
+
selectionFab._cardIdx = idx;
|
|
1645
|
+
selectionFab._text = text;
|
|
1646
|
+
selectionFab.classList.add('visible');
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
document.addEventListener('mouseup', function () {
|
|
1650
|
+
// Defer so the selection settles after click-release.
|
|
1651
|
+
setTimeout(maybeShowSelectionFab, 10);
|
|
1652
|
+
});
|
|
1653
|
+
document.addEventListener('selectionchange', function () {
|
|
1654
|
+
var sel = window.getSelection();
|
|
1655
|
+
if (!sel || sel.isCollapsed || !sel.toString().trim()) hideSelectionFab();
|
|
1656
|
+
});
|
|
1657
|
+
selectionFab.addEventListener('mousedown', function (e) {
|
|
1658
|
+
// Prevent the mousedown from clearing the selection before our click handler runs.
|
|
1659
|
+
e.preventDefault();
|
|
1660
|
+
});
|
|
1661
|
+
selectionFab.addEventListener('click', function () {
|
|
1662
|
+
if (Number.isFinite(selectionFab._cardIdx)) {
|
|
1663
|
+
openModal(selectionFab._cardIdx, selectionFab._text);
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
})();
|
|
1667
|
+
</script>
|
|
1668
|
+
</body>
|
|
1669
|
+
</html>`;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
module.exports = { renderSliceStageHtml };
|