@officexapp/vidfarm-devcli 0.21.10
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/.agents/skills/dollarplatoon-skill/SKILL.md +1227 -0
- package/.agents/skills/editor-capabilities/SKILL.md +182 -0
- package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
- package/.agents/skills/embedded-captions/CATALOG.md +93 -0
- package/.agents/skills/embedded-captions/SKILL.md +286 -0
- package/.agents/skills/embedded-captions/assets/brand/CDPR-fankit-terms.txt +35 -0
- package/.agents/skills/embedded-captions/assets/brand/cyberpunk-widths.json +115 -0
- package/.agents/skills/embedded-captions/assets/fonts/char-widths.json +2211 -0
- package/.agents/skills/embedded-captions/assets/strokefonts/HersheyScript1.svg +242 -0
- package/.agents/skills/embedded-captions/assets/strokefonts/HersheyScriptMed.svg +242 -0
- package/.agents/skills/embedded-captions/dna/README.md +148 -0
- package/.agents/skills/embedded-captions/dna/chrome.json +68 -0
- package/.agents/skills/embedded-captions/dna/cream.json +69 -0
- package/.agents/skills/embedded-captions/dna/documentary.json +62 -0
- package/.agents/skills/embedded-captions/dna/editorial.json +70 -0
- package/.agents/skills/embedded-captions/dna/glitch.json +82 -0
- package/.agents/skills/embedded-captions/dna/ink.json +65 -0
- package/.agents/skills/embedded-captions/dna/keynote.json +62 -0
- package/.agents/skills/embedded-captions/dna/loud.json +75 -0
- package/.agents/skills/embedded-captions/dna/neon.json +58 -0
- package/.agents/skills/embedded-captions/dna/velocity.json +89 -0
- package/.agents/skills/embedded-captions/modes/cinematic/README.md +48 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/champion/spec.md +98 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/champion/template.html +188 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/memory-wall/spec.md +134 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/memory-wall/template.html +179 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/portrait-header/spec.md +81 -0
- package/.agents/skills/embedded-captions/modes/cinematic/_archive/portrait-header/template.html +170 -0
- package/.agents/skills/embedded-captions/modes/cinematic/cinematic-cream/spec.md +16 -0
- package/.agents/skills/embedded-captions/modes/cinematic/cinematic-cream/template.html +226 -0
- package/.agents/skills/embedded-captions/modes/cinematic/engine.html +355 -0
- package/.agents/skills/embedded-captions/modes/standard/_anatomy.md +233 -0
- package/.agents/skills/embedded-captions/modes/standard/_motion.md +192 -0
- package/.agents/skills/embedded-captions/modes/standard/fonts/build-fonts-css.cjs +97 -0
- package/.agents/skills/embedded-captions/references/aesthetic-principles.md +168 -0
- package/.agents/skills/embedded-captions/references/anti-patterns.md +188 -0
- package/.agents/skills/embedded-captions/references/bespoke-vs-presets.md +178 -0
- package/.agents/skills/embedded-captions/references/caption-grouping.md +130 -0
- package/.agents/skills/embedded-captions/references/composition-craft.md +522 -0
- package/.agents/skills/embedded-captions/references/direction-catalog.md +138 -0
- package/.agents/skills/embedded-captions/references/example-renders/champion.html +371 -0
- package/.agents/skills/embedded-captions/references/example-renders/memory-wall.html +334 -0
- package/.agents/skills/embedded-captions/references/failure-modes.md +145 -0
- package/.agents/skills/embedded-captions/references/layout-heuristics.md +238 -0
- package/.agents/skills/embedded-captions/references/motion-vocabulary.md +148 -0
- package/.agents/skills/embedded-captions/references/rail.md +68 -0
- package/.agents/skills/embedded-captions/references/reference-bar.md +47 -0
- package/.agents/skills/embedded-captions/references/scene-types.md +94 -0
- package/.agents/skills/embedded-captions/references/test-set.md +66 -0
- package/.agents/skills/embedded-captions/references/typographic-moves.md +266 -0
- package/.agents/skills/embedded-captions/references/typography-presets.md +63 -0
- package/.agents/skills/embedded-captions/scripts/audio-envelope.cjs +95 -0
- package/.agents/skills/embedded-captions/scripts/check-occlusion.cjs +250 -0
- package/.agents/skills/embedded-captions/scripts/check-overflow.cjs +194 -0
- package/.agents/skills/embedded-captions/scripts/check-rail-climax.cjs +233 -0
- package/.agents/skills/embedded-captions/scripts/check-timing.cjs +173 -0
- package/.agents/skills/embedded-captions/scripts/fill-timings.cjs +124 -0
- package/.agents/skills/embedded-captions/scripts/fit-fonts.cjs +158 -0
- package/.agents/skills/embedded-captions/scripts/gen-stroke-path.py +46 -0
- package/.agents/skills/embedded-captions/scripts/inject-fonts.cjs +169 -0
- package/.agents/skills/embedded-captions/scripts/lib-dna.cjs +201 -0
- package/.agents/skills/embedded-captions/scripts/make-cinematic.cjs +1205 -0
- package/.agents/skills/embedded-captions/scripts/make-composition.cjs +404 -0
- package/.agents/skills/embedded-captions/scripts/make-theme.cjs +8781 -0
- package/.agents/skills/embedded-captions/scripts/matte.cjs +294 -0
- package/.agents/skills/embedded-captions/scripts/measure-layout.cjs +293 -0
- package/.agents/skills/embedded-captions/scripts/prepare.sh +39 -0
- package/.agents/skills/embedded-captions/scripts/preview-frames.cjs +268 -0
- package/.agents/skills/embedded-captions/scripts/render-and-composite.sh +509 -0
- package/.agents/skills/embedded-captions/scripts/render-theme.sh +34 -0
- package/.agents/skills/embedded-captions/scripts/safe-zones.cjs +813 -0
- package/.agents/skills/embedded-captions/scripts/transcribe.cjs +349 -0
- package/.agents/skills/embedded-captions/themes/PORTING.md +49 -0
- package/.agents/skills/embedded-captions/themes/README.md +244 -0
- package/.agents/skills/embedded-captions/themes/anchor.json +27 -0
- package/.agents/skills/embedded-captions/themes/arcade.json +51 -0
- package/.agents/skills/embedded-captions/themes/aurora.json +41 -0
- package/.agents/skills/embedded-captions/themes/biolume.json +35 -0
- package/.agents/skills/embedded-captions/themes/brush.json +74 -0
- package/.agents/skills/embedded-captions/themes/chalkboard.json +52 -0
- package/.agents/skills/embedded-captions/themes/dossier.json +56 -0
- package/.agents/skills/embedded-captions/themes/graffiti.json +62 -0
- package/.agents/skills/embedded-captions/themes/hologram.json +41 -0
- package/.agents/skills/embedded-captions/themes/inkwater.json +43 -0
- package/.agents/skills/embedded-captions/themes/laser.json +46 -0
- package/.agents/skills/embedded-captions/themes/lastpage.json +33 -0
- package/.agents/skills/embedded-captions/themes/neonsign.json +53 -0
- package/.agents/skills/embedded-captions/themes/nightcity.json +56 -0
- package/.agents/skills/embedded-captions/themes/ordnance.json +54 -0
- package/.agents/skills/embedded-captions/themes/papercut.json +37 -0
- package/.agents/skills/embedded-captions/themes/popup.json +46 -0
- package/.agents/skills/embedded-captions/themes/ransom.json +62 -0
- package/.agents/skills/embedded-captions/themes/scoreboard.json +54 -0
- package/.agents/skills/embedded-captions/themes/spectrum.json +46 -0
- package/.agents/skills/embedded-captions/themes/stardust.json +29 -0
- package/.agents/skills/embedded-captions/themes/stomp.json +43 -0
- package/.agents/skills/embedded-captions/themes/terminal.json +49 -0
- package/.agents/skills/embedded-captions/themes/thunder.json +44 -0
- package/.agents/skills/embedded-captions/themes/transit.json +44 -0
- package/.agents/skills/embedded-captions/themes/vhs.json +47 -0
- package/.agents/skills/faceless-explainer/SKILL.md +209 -0
- package/.agents/skills/faceless-explainer/references/cut-catalog.md +215 -0
- package/.agents/skills/faceless-explainer/references/motion-language.md +156 -0
- package/.agents/skills/faceless-explainer/references/story-design.md +248 -0
- package/.agents/skills/faceless-explainer/references/visual-design.md +146 -0
- package/.agents/skills/faceless-explainer/scripts/assemble-index.mjs +570 -0
- package/.agents/skills/faceless-explainer/scripts/audio.mjs +260 -0
- package/.agents/skills/faceless-explainer/scripts/build-frame.mjs +536 -0
- package/.agents/skills/faceless-explainer/scripts/captions.mjs +508 -0
- package/.agents/skills/faceless-explainer/scripts/lib/assets.mjs +55 -0
- package/.agents/skills/faceless-explainer/scripts/lib/dimensions.mjs +45 -0
- package/.agents/skills/faceless-explainer/scripts/lib/storyboard.mjs +249 -0
- package/.agents/skills/faceless-explainer/scripts/lib/tokens.mjs +204 -0
- package/.agents/skills/faceless-explainer/scripts/lib/transition-registry.mjs +38 -0
- package/.agents/skills/faceless-explainer/scripts/lib/transitions.json +71 -0
- package/.agents/skills/faceless-explainer/scripts/transitions.mjs +320 -0
- package/.agents/skills/faceless-explainer/sub-agents/frame-worker.md +75 -0
- package/.agents/skills/farmville-saas-ux/SKILL.md +156 -0
- package/.agents/skills/farmville-saas-ux/assets/starter.html +294 -0
- package/.agents/skills/farmville-saas-ux/references/components.md +340 -0
- package/.agents/skills/farmville-saas-ux/references/porting-guide.md +121 -0
- package/.agents/skills/farmville-saas-ux/references/tokens.md +271 -0
- package/.agents/skills/flockposter/LICENSE +17 -0
- package/.agents/skills/flockposter/README.md +72 -0
- package/.agents/skills/flockposter/SKILL.md +562 -0
- package/.agents/skills/flockposter/examples/cli-instagram-reel.sh +21 -0
- package/.agents/skills/flockposter/examples/cli-multi-platform.json +69 -0
- package/.agents/skills/flockposter/examples/cli-tiktok-upload-only.sh +22 -0
- package/.agents/skills/flockposter/examples/cli-x-thread.sh +17 -0
- package/.agents/skills/flockposter/examples/cli-youtube-short.sh +21 -0
- package/.agents/skills/flockposter/examples/public-api-instagram-reel.json +28 -0
- package/.agents/skills/general-video/SKILL.md +143 -0
- package/.agents/skills/hyperframes/SKILL.md +160 -0
- package/.agents/skills/hyperframes-animation/SKILL.md +84 -0
- package/.agents/skills/hyperframes-animation/adapters/animate-text.md +64 -0
- package/.agents/skills/hyperframes-animation/adapters/animejs.md +114 -0
- package/.agents/skills/hyperframes-animation/adapters/css-animations.md +143 -0
- package/.agents/skills/hyperframes-animation/adapters/gsap-easing-and-stagger.md +118 -0
- package/.agents/skills/hyperframes-animation/adapters/gsap-timeline-and-labels.md +96 -0
- package/.agents/skills/hyperframes-animation/adapters/gsap-transforms-and-perf.md +96 -0
- package/.agents/skills/hyperframes-animation/adapters/gsap.md +105 -0
- package/.agents/skills/hyperframes-animation/adapters/html-in-canvas-patterns.md +507 -0
- package/.agents/skills/hyperframes-animation/adapters/lottie.md +117 -0
- package/.agents/skills/hyperframes-animation/adapters/three.md +131 -0
- package/.agents/skills/hyperframes-animation/adapters/typegpu.md +178 -0
- package/.agents/skills/hyperframes-animation/adapters/waapi.md +101 -0
- package/.agents/skills/hyperframes-animation/blueprints/comparison-split.md +27 -0
- package/.agents/skills/hyperframes-animation/blueprints/constellation-hub.md +44 -0
- package/.agents/skills/hyperframes-animation/blueprints/cta-morph-press.md +28 -0
- package/.agents/skills/hyperframes-animation/blueprints/cursor-ui-demo.md +57 -0
- package/.agents/skills/hyperframes-animation/blueprints/dataviz-countup.md +46 -0
- package/.agents/skills/hyperframes-animation/blueprints/device-surface-showcase.md +53 -0
- package/.agents/skills/hyperframes-animation/blueprints/grid-card-assemble.md +66 -0
- package/.agents/skills/hyperframes-animation/blueprints/kinetic-type-beats.md +78 -0
- package/.agents/skills/hyperframes-animation/blueprints/logo-assemble-lockup.md +69 -0
- package/.agents/skills/hyperframes-animation/blueprints/overwhelm-surround.md +28 -0
- package/.agents/skills/hyperframes-animation/blueprints/spatial-pan-stations.md +37 -0
- package/.agents/skills/hyperframes-animation/blueprints/ticker-takeover.md +29 -0
- package/.agents/skills/hyperframes-animation/blueprints/titlecard-reveal.md +42 -0
- package/.agents/skills/hyperframes-animation/blueprints/typewriter-reveal.md +51 -0
- package/.agents/skills/hyperframes-animation/blueprints/video-text-pivot.md +30 -0
- package/.agents/skills/hyperframes-animation/blueprints-index.md +146 -0
- package/.agents/skills/hyperframes-animation/examples/assets/avatars/02.avif +0 -0
- package/.agents/skills/hyperframes-animation/examples/assets/brands/github.avif +0 -0
- package/.agents/skills/hyperframes-animation/examples/assets/brands/nvidia.avif +0 -0
- package/.agents/skills/hyperframes-animation/examples/assets/brands/visa.avif +0 -0
- package/.agents/skills/hyperframes-animation/examples/assets/brands/zoominfo.avif +0 -0
- package/.agents/skills/hyperframes-animation/examples/brand-reveal-assemble-zoom.html +382 -0
- package/.agents/skills/hyperframes-animation/examples/comparison-split-cards.html +649 -0
- package/.agents/skills/hyperframes-animation/examples/concept-demo-decode-pan.html +520 -0
- package/.agents/skills/hyperframes-animation/examples/cta-morph-press.html +468 -0
- package/.agents/skills/hyperframes-animation/examples/cta-orbit-collapse.html +1298 -0
- package/.agents/skills/hyperframes-animation/examples/demo-page-scroll-spotlight.html +759 -0
- package/.agents/skills/hyperframes-animation/examples/hook-counter-burst.html +729 -0
- package/.agents/skills/hyperframes-animation/examples/messaging-multi-phrase.html +352 -0
- package/.agents/skills/hyperframes-animation/examples/metric-video-text-pivot.html +779 -0
- package/.agents/skills/hyperframes-animation/examples/problem-mockup-overwhelm.html +1374 -0
- package/.agents/skills/hyperframes-animation/examples/proof-logo-chain.html +861 -0
- package/.agents/skills/hyperframes-animation/examples/takeover-ticker-displace.html +347 -0
- package/.agents/skills/hyperframes-animation/examples/workflow-approve-press.html +606 -0
- package/.agents/skills/hyperframes-animation/rules/3d-page-scroll.md +227 -0
- package/.agents/skills/hyperframes-animation/rules/3d-text-depth-layers.md +297 -0
- package/.agents/skills/hyperframes-animation/rules/ai-tracking-box.md +382 -0
- package/.agents/skills/hyperframes-animation/rules/ambient-glow-bloom.md +305 -0
- package/.agents/skills/hyperframes-animation/rules/asr-keyword-glow.md +286 -0
- package/.agents/skills/hyperframes-animation/rules/avatar-cloud-network.md +371 -0
- package/.agents/skills/hyperframes-animation/rules/camera-cursor-tracking.md +246 -0
- package/.agents/skills/hyperframes-animation/rules/card-morph-anchor.md +267 -0
- package/.agents/skills/hyperframes-animation/rules/center-outward-expansion.md +227 -0
- package/.agents/skills/hyperframes-animation/rules/context-sensitive-cursor.md +257 -0
- package/.agents/skills/hyperframes-animation/rules/coordinate-target-zoom.md +332 -0
- package/.agents/skills/hyperframes-animation/rules/counting-dynamic-scale.md +283 -0
- package/.agents/skills/hyperframes-animation/rules/css-marker-patterns.md +373 -0
- package/.agents/skills/hyperframes-animation/rules/cursor-click-ripple.md +262 -0
- package/.agents/skills/hyperframes-animation/rules/depth-of-field-blur.md +313 -0
- package/.agents/skills/hyperframes-animation/rules/depth-scatter-assemble.md +303 -0
- package/.agents/skills/hyperframes-animation/rules/discrete-text-sequence.md +273 -0
- package/.agents/skills/hyperframes-animation/rules/dynamic-content-sequencing.md +307 -0
- package/.agents/skills/hyperframes-animation/rules/gsap-effects.md +299 -0
- package/.agents/skills/hyperframes-animation/rules/hacker-flip-3d.md +223 -0
- package/.agents/skills/hyperframes-animation/rules/kinetic-beat-slam.md +183 -0
- package/.agents/skills/hyperframes-animation/rules/motion-blur-streak.md +328 -0
- package/.agents/skills/hyperframes-animation/rules/multi-phase-camera.md +273 -0
- package/.agents/skills/hyperframes-animation/rules/orbit-3d-entry.md +301 -0
- package/.agents/skills/hyperframes-animation/rules/physics-press-reaction.md +350 -0
- package/.agents/skills/hyperframes-animation/rules/press-release-spring.md +296 -0
- package/.agents/skills/hyperframes-animation/rules/reactive-displacement.md +277 -0
- package/.agents/skills/hyperframes-animation/rules/scale-swap-transition.md +298 -0
- package/.agents/skills/hyperframes-animation/rules/sine-wave-loop.md +278 -0
- package/.agents/skills/hyperframes-animation/rules/split-tilt-cards.md +277 -0
- package/.agents/skills/hyperframes-animation/rules/spring-pop-entrance.md +273 -0
- package/.agents/skills/hyperframes-animation/rules/stat-bars-and-fills.md +156 -0
- package/.agents/skills/hyperframes-animation/rules/svg-icon-enrichment.md +329 -0
- package/.agents/skills/hyperframes-animation/rules/svg-path-draw.md +274 -0
- package/.agents/skills/hyperframes-animation/rules/vertical-spring-ticker.md +239 -0
- package/.agents/skills/hyperframes-animation/rules/viewport-change.md +349 -0
- package/.agents/skills/hyperframes-animation/rules-index.md +86 -0
- package/.agents/skills/hyperframes-animation/scripts/animation-map.mjs +606 -0
- package/.agents/skills/hyperframes-animation/scripts/package-loader.mjs +288 -0
- package/.agents/skills/hyperframes-animation/scripts/package-loader.test.mjs +62 -0
- package/.agents/skills/hyperframes-animation/techniques.md +507 -0
- package/.agents/skills/hyperframes-animation/transitions/TRANSITION-REGISTRY.md +167 -0
- package/.agents/skills/hyperframes-animation/transitions/catalog.md +127 -0
- package/.agents/skills/hyperframes-animation/transitions/css-3d.md +12 -0
- package/.agents/skills/hyperframes-animation/transitions/css-blur.md +51 -0
- package/.agents/skills/hyperframes-animation/transitions/css-cover.md +43 -0
- package/.agents/skills/hyperframes-animation/transitions/css-destruction.md +95 -0
- package/.agents/skills/hyperframes-animation/transitions/css-dissolve.md +66 -0
- package/.agents/skills/hyperframes-animation/transitions/css-distortion.md +45 -0
- package/.agents/skills/hyperframes-animation/transitions/css-grid.md +10 -0
- package/.agents/skills/hyperframes-animation/transitions/css-light.md +49 -0
- package/.agents/skills/hyperframes-animation/transitions/css-mechanical.md +30 -0
- package/.agents/skills/hyperframes-animation/transitions/css-other.md +25 -0
- package/.agents/skills/hyperframes-animation/transitions/css-push.md +41 -0
- package/.agents/skills/hyperframes-animation/transitions/css-radial.md +37 -0
- package/.agents/skills/hyperframes-animation/transitions/css-scale.md +24 -0
- package/.agents/skills/hyperframes-animation/transitions/overview.md +153 -0
- package/.agents/skills/hyperframes-cli/SKILL.md +113 -0
- package/.agents/skills/hyperframes-cli/references/doctor-browser.md +45 -0
- package/.agents/skills/hyperframes-cli/references/init-and-scaffold.md +55 -0
- package/.agents/skills/hyperframes-cli/references/lambda.md +132 -0
- package/.agents/skills/hyperframes-cli/references/lint-validate-inspect.md +121 -0
- package/.agents/skills/hyperframes-cli/references/preview-render.md +149 -0
- package/.agents/skills/hyperframes-cli/references/upgrade-info-misc.md +75 -0
- package/.agents/skills/hyperframes-core/SKILL.md +78 -0
- package/.agents/skills/hyperframes-core/references/composition-patterns.md +263 -0
- package/.agents/skills/hyperframes-core/references/data-attributes.md +70 -0
- package/.agents/skills/hyperframes-core/references/determinism-rules.md +68 -0
- package/.agents/skills/hyperframes-core/references/full-screen-motion.md +62 -0
- package/.agents/skills/hyperframes-core/references/minimal-composition.md +66 -0
- package/.agents/skills/hyperframes-core/references/script-format.md +49 -0
- package/.agents/skills/hyperframes-core/references/storyboard-format.md +95 -0
- package/.agents/skills/hyperframes-core/references/sub-compositions.md +237 -0
- package/.agents/skills/hyperframes-core/references/subagent-dispatch.md +41 -0
- package/.agents/skills/hyperframes-core/references/tailwind.md +125 -0
- package/.agents/skills/hyperframes-core/references/tracks-and-clips.md +76 -0
- package/.agents/skills/hyperframes-core/references/variables-and-media.md +90 -0
- package/.agents/skills/hyperframes-creative/SKILL.md +70 -0
- package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/FRAME.md +289 -0
- package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/caption-skin.html +221 -0
- package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/frame-showcase.html +1214 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blockframe/FRAME.md +275 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blockframe/caption-skin.html +224 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blockframe/frame-showcase.html +1347 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/FRAME.md +307 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/caption-skin.html +223 -0
- package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/frame-showcase.html +1372 -0
- package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/FRAME.md +275 -0
- package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/caption-skin.html +230 -0
- package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/frame-showcase.html +1140 -0
- package/.agents/skills/hyperframes-creative/frame-presets/broadside/FRAME.md +284 -0
- package/.agents/skills/hyperframes-creative/frame-presets/broadside/caption-skin.html +225 -0
- package/.agents/skills/hyperframes-creative/frame-presets/broadside/frame-showcase.html +1166 -0
- package/.agents/skills/hyperframes-creative/frame-presets/capsule/FRAME.md +284 -0
- package/.agents/skills/hyperframes-creative/frame-presets/capsule/caption-skin.html +224 -0
- package/.agents/skills/hyperframes-creative/frame-presets/capsule/frame-showcase.html +1465 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cartesian/FRAME.md +278 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cartesian/caption-skin.html +222 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cartesian/frame-showcase.html +1263 -0
- package/.agents/skills/hyperframes-creative/frame-presets/claude/FRAME.md +277 -0
- package/.agents/skills/hyperframes-creative/frame-presets/claude/caption-skin.html +224 -0
- package/.agents/skills/hyperframes-creative/frame-presets/claude/frame-showcase.html +1439 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/FRAME.md +265 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/caption-skin.html +238 -0
- package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/frame-showcase.html +1468 -0
- package/.agents/skills/hyperframes-creative/frame-presets/coral/FRAME.md +314 -0
- package/.agents/skills/hyperframes-creative/frame-presets/coral/caption-skin.html +226 -0
- package/.agents/skills/hyperframes-creative/frame-presets/coral/frame-showcase.html +1268 -0
- package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/FRAME.md +333 -0
- package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/caption-skin.html +227 -0
- package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/frame-showcase.html +1330 -0
- package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/FRAME.md +274 -0
- package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/caption-skin.html +230 -0
- package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/frame-showcase.html +1444 -0
- package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/FRAME.md +265 -0
- package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/caption-skin.html +223 -0
- package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/frame-showcase.html +1273 -0
- package/.agents/skills/hyperframes-creative/palettes/bold-energetic.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/clean-corporate.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/dark-premium.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/jewel-rich.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/monochrome.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/nature-earth.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/neon-electric.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/pastel-soft.md +14 -0
- package/.agents/skills/hyperframes-creative/palettes/warm-editorial.md +14 -0
- package/.agents/skills/hyperframes-creative/references/audio-reactive.md +76 -0
- package/.agents/skills/hyperframes-creative/references/beat-direction.md +164 -0
- package/.agents/skills/hyperframes-creative/references/composition-patterns.md +199 -0
- package/.agents/skills/hyperframes-creative/references/data-in-motion.md +19 -0
- package/.agents/skills/hyperframes-creative/references/design-adherence.md +19 -0
- package/.agents/skills/hyperframes-creative/references/design-picker.md +123 -0
- package/.agents/skills/hyperframes-creative/references/design-spec.md +55 -0
- package/.agents/skills/hyperframes-creative/references/house-style.md +73 -0
- package/.agents/skills/hyperframes-creative/references/motion-principles.md +150 -0
- package/.agents/skills/hyperframes-creative/references/narration.md +92 -0
- package/.agents/skills/hyperframes-creative/references/prompt-expansion.md +68 -0
- package/.agents/skills/hyperframes-creative/references/typography.md +218 -0
- package/.agents/skills/hyperframes-creative/references/video-composition.md +63 -0
- package/.agents/skills/hyperframes-creative/references/visual-styles.md +457 -0
- package/.agents/skills/hyperframes-creative/scripts/contrast-report.mjs +353 -0
- package/.agents/skills/hyperframes-creative/scripts/extract-audio-data.py +188 -0
- package/.agents/skills/hyperframes-creative/scripts/package-loader.mjs +288 -0
- package/.agents/skills/hyperframes-creative/scripts/package-loader.test.mjs +62 -0
- package/.agents/skills/hyperframes-creative/templates/design-picker.html +1432 -0
- package/.agents/skills/hyperframes-keyframes/SKILL.md +237 -0
- package/.agents/skills/hyperframes-keyframes/agents/openai.yaml +4 -0
- package/.agents/skills/hyperframes-keyframes/references/keyframe-patterns.md +106 -0
- package/.agents/skills/motion-graphics/SKILL.md +172 -0
- package/.agents/skills/motion-graphics/agents/builder.md +40 -0
- package/.agents/skills/motion-graphics/agents/director.md +53 -0
- package/.agents/skills/motion-graphics/agents/finalize.md +17 -0
- package/.agents/skills/motion-graphics/catalog-map.md +40 -0
- package/.agents/skills/motion-graphics/categories/asset-fusion/module.md +37 -0
- package/.agents/skills/motion-graphics/categories/charts/module.md +21 -0
- package/.agents/skills/motion-graphics/categories/kinetic-type/module.md +19 -0
- package/.agents/skills/motion-graphics/categories/logo-reveal/module.md +17 -0
- package/.agents/skills/motion-graphics/categories/lower-thirds/module.md +16 -0
- package/.agents/skills/motion-graphics/categories/maps/bake-basemap.mjs +273 -0
- package/.agents/skills/motion-graphics/categories/maps/module.md +61 -0
- package/.agents/skills/motion-graphics/categories/news/module.md +56 -0
- package/.agents/skills/motion-graphics/categories/stat/module.md +21 -0
- package/.agents/skills/motion-graphics/categories/tweet/module.md +16 -0
- package/.agents/skills/motion-graphics/categories/webpage/module.md +21 -0
- package/.agents/skills/motion-graphics/grounding/PROTOCOL.md +64 -0
- package/.agents/skills/motion-graphics/grounding/locate.mjs +227 -0
- package/.agents/skills/motion-graphics/phases/source/guide.md +21 -0
- package/.agents/skills/motion-graphics/references/builder-contract.md +38 -0
- package/.agents/skills/motion-graphics/references/motion-vocabulary.md +43 -0
- package/.agents/skills/motion-graphics/references/shot-plan-ir.md +54 -0
- package/.agents/skills/motion-graphics/samples/asset-fusion/_ref-circle-highlight.html +306 -0
- package/.agents/skills/music/SKILL.md +416 -0
- package/.agents/skills/music/references/api_reference.md +519 -0
- package/.agents/skills/music/references/installation.md +65 -0
- package/.agents/skills/product-launch-video/SKILL.md +208 -0
- package/.agents/skills/product-launch-video/references/cut-catalog.md +220 -0
- package/.agents/skills/product-launch-video/references/motion-language.md +156 -0
- package/.agents/skills/product-launch-video/references/story-design.md +373 -0
- package/.agents/skills/product-launch-video/references/visual-design.md +126 -0
- package/.agents/skills/product-launch-video/scripts/assemble-index.mjs +570 -0
- package/.agents/skills/product-launch-video/scripts/audio.mjs +260 -0
- package/.agents/skills/product-launch-video/scripts/build-frame.mjs +536 -0
- package/.agents/skills/product-launch-video/scripts/captions.mjs +508 -0
- package/.agents/skills/product-launch-video/scripts/lib/assets.mjs +55 -0
- package/.agents/skills/product-launch-video/scripts/lib/dimensions.mjs +45 -0
- package/.agents/skills/product-launch-video/scripts/lib/pad-frame-duration.mjs +36 -0
- package/.agents/skills/product-launch-video/scripts/lib/pad-frame-duration.test.mjs +76 -0
- package/.agents/skills/product-launch-video/scripts/lib/storyboard.mjs +249 -0
- package/.agents/skills/product-launch-video/scripts/lib/tokens.mjs +204 -0
- package/.agents/skills/product-launch-video/scripts/lib/transition-registry.mjs +38 -0
- package/.agents/skills/product-launch-video/scripts/lib/transitions.json +71 -0
- package/.agents/skills/product-launch-video/scripts/stage-assets.mjs +39 -0
- package/.agents/skills/product-launch-video/scripts/transitions.mjs +327 -0
- package/.agents/skills/product-launch-video/sub-agents/frame-worker.md +75 -0
- package/.agents/skills/slideshow/SKILL.md +550 -0
- package/.agents/skills/slideshow/references/standalone-harness.md +1020 -0
- package/.agents/skills/talking-head-recut/NOTICE.md +39 -0
- package/.agents/skills/talking-head-recut/SKILL.md +1195 -0
- package/.agents/skills/talking-head-recut/assets/vendor/gsap.min.js +10 -0
- package/.agents/skills/talking-head-recut/references/DESIGN_INDEX.md +145 -0
- package/.agents/skills/talking-head-recut/references/frames/clean.html +92 -0
- package/.agents/skills/talking-head-recut/references/frames/hairline.html +171 -0
- package/.agents/skills/talking-head-recut/references/frames/polaroid.html +131 -0
- package/.agents/skills/talking-head-recut/references/layouts/overlay.html +164 -0
- package/.agents/skills/talking-head-recut/references/layouts/pip.html +143 -0
- package/.agents/skills/talking-head-recut/references/layouts/split.html +121 -0
- package/.agents/skills/talking-head-recut/references/layouts/stack.html +109 -0
- package/.agents/skills/talking-head-recut/references/styles/academic.html +172 -0
- package/.agents/skills/talking-head-recut/references/styles/audit.html +170 -0
- package/.agents/skills/talking-head-recut/references/styles/editorial.html +189 -0
- package/.agents/skills/talking-head-recut/references/styles/geom.html +181 -0
- package/.agents/skills/talking-head-recut/references/styles/minimal.html +130 -0
- package/.agents/skills/talking-head-recut/references/styles/spotlight.html +142 -0
- package/.agents/skills/talking-head-recut/references/styles/swiss.html +188 -0
- package/.agents/skills/talking-head-recut/references/styles/terminal.html +196 -0
- package/.agents/skills/talking-head-recut/references/styles/whiteboard.html +170 -0
- package/.agents/skills/talking-head-recut/references/styles/xhs.html +206 -0
- package/.agents/skills/text-to-speech/SKILL.md +226 -0
- package/.agents/skills/text-to-speech/references/installation.md +90 -0
- package/.agents/skills/text-to-speech/references/streaming.md +307 -0
- package/.agents/skills/text-to-speech/references/voice-settings.md +115 -0
- package/.agents/skills/vidfarm-director/SKILL.md +179 -0
- package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
- package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
- package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +117 -0
- package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +276 -0
- package/.agents/skills/vidfarm-director/references/core-workflows.md +301 -0
- package/.agents/skills/vidfarm-director/references/editor-workflows.md +367 -0
- package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
- package/.agents/skills/vidfarm-director/references/primitives.md +307 -0
- package/.agents/skills/vidfarm-director/references/rest-api.md +84 -0
- package/.agents/skills/vidfarm-media/SKILL.md +158 -0
- package/.agents/skills/vidfarm-media/references/tts.md +111 -0
- package/.agents/skills/vidfarm-media/scripts/audio.mjs +628 -0
- package/.agents/skills/vidfarm-media/scripts/wait-bgm.mjs +31 -0
- package/.agents/skills/website-to-video/SKILL.md +145 -0
- package/.agents/skills/website-to-video/assets/sfx/CREDITS.md +35 -0
- package/.agents/skills/website-to-video/assets/sfx/manifest.json +97 -0
- package/.agents/skills/website-to-video/references/beat-builder-guide.md +263 -0
- package/.agents/skills/website-to-video/references/capabilities.md +713 -0
- package/.agents/skills/website-to-video/references/step-0-capture.md +55 -0
- package/.agents/skills/website-to-video/references/step-1-design.md +333 -0
- package/.agents/skills/website-to-video/references/step-2-brief.md +178 -0
- package/.agents/skills/website-to-video/references/step-3-storyboard.md +572 -0
- package/.agents/skills/website-to-video/references/step-4-vo.md +225 -0
- package/.agents/skills/website-to-video/references/step-5-build.md +483 -0
- package/.agents/skills/website-to-video/references/step-6-validate.md +341 -0
- package/.agents/skills/website-to-video/scripts/w2h-verify.mjs +759 -0
- package/README.md +97 -0
- package/SKILL.director.md +1644 -0
- package/SKILL.md +83 -0
- package/demo/README.md +28 -0
- package/demo/dist/app.css +1 -0
- package/demo/dist/app.js +968 -0
- package/demo/dist/chunks/chunk-3OT2D42H.js +1 -0
- package/demo/dist/chunks/chunk-DXB73IDG.js +1 -0
- package/demo/dist/chunks/domEditingLayers-AT7G6F4L-ZTHJHB7R.js +1 -0
- package/demo/dist/chunks/hyperframes-player-7HKH6NSO.js +459 -0
- package/demo/dist/chunks/lib-XAQ37YOE.js +1 -0
- package/demo/dist/chunks/src-TJ2QYA4U.js +207 -0
- package/demo/dist/favicon.ico +0 -0
- package/demo/dist/icons/timeline/audio.svg +7 -0
- package/demo/dist/icons/timeline/captions.svg +5 -0
- package/demo/dist/icons/timeline/composition.svg +12 -0
- package/demo/dist/icons/timeline/image.svg +18 -0
- package/demo/dist/icons/timeline/music.svg +10 -0
- package/demo/dist/icons/timeline/text.svg +3 -0
- package/demo/dist/index.html +16 -0
- package/dist/src/cli.js +7258 -0
- package/dist/src/devcli/auth-store.js +67 -0
- package/dist/src/devcli/captions.js +310 -0
- package/dist/src/devcli/clip-store.js +368 -0
- package/dist/src/devcli/clips.js +1138 -0
- package/dist/src/devcli/composition-edit.js +1292 -0
- package/dist/src/devcli/cost-mode.js +149 -0
- package/dist/src/devcli/doctor.js +192 -0
- package/dist/src/devcli/hyperframes-cli.js +118 -0
- package/dist/src/devcli/local-backend.js +167 -0
- package/dist/src/devcli/local-frontend-server.js +681 -0
- package/dist/src/devcli/local-render.js +136 -0
- package/dist/src/devcli/skills.js +476 -0
- package/dist/src/devcli/speech.js +178 -0
- package/dist/src/devcli/stills.js +249 -0
- package/dist/src/devcli/telemetry.js +236 -0
- package/dist/src/devcli/timeline-edit.js +490 -0
- package/dist/src/devcli/transitions.js +205 -0
- package/dist/src/hyperframes/composition.js +780 -0
- package/dist/src/lib/crypto.js +45 -0
- package/dist/src/lib/dev-log.js +54 -0
- package/dist/src/lib/display-name.js +11 -0
- package/dist/src/lib/ids.js +24 -0
- package/dist/src/lib/images.js +19 -0
- package/dist/src/lib/json.js +15 -0
- package/dist/src/lib/package-root.js +47 -0
- package/dist/src/lib/render-media-prep.js +293 -0
- package/dist/src/lib/template-paths.js +28 -0
- package/dist/src/lib/time.js +7 -0
- package/dist/src/lib/url-clean.js +85 -0
- package/dist/src/services/captions.js +123 -0
- package/dist/src/services/clip-curation/cost.js +116 -0
- package/dist/src/services/clip-curation/ffmpeg.js +352 -0
- package/dist/src/services/clip-curation/gemini.js +418 -0
- package/dist/src/services/clip-curation/hunt.js +470 -0
- package/dist/src/services/clip-curation/index.js +19 -0
- package/dist/src/services/clip-curation/local-agent.js +248 -0
- package/dist/src/services/clip-curation/media-select.js +152 -0
- package/dist/src/services/clip-curation/presets.js +20 -0
- package/dist/src/services/clip-curation/presets.v1.json +59 -0
- package/dist/src/services/clip-curation/query.js +167 -0
- package/dist/src/services/clip-curation/refine.js +165 -0
- package/dist/src/services/clip-curation/scan.js +189 -0
- package/dist/src/services/clip-curation/source-naming.js +131 -0
- package/dist/src/services/clip-curation/taxonomy.js +73 -0
- package/dist/src/services/clip-curation/taxonomy.v1.json +102 -0
- package/dist/src/services/clip-curation/types.js +7 -0
- package/dist/src/services/composition-lint.js +199 -0
- package/dist/src/services/provider-errors.js +128 -0
- package/dist/src/services/speech.js +629 -0
- package/package.json +154 -0
- package/public/assets/discover-client-app.js +1 -0
- package/public/assets/favicon.ico +0 -0
- package/public/assets/file-directory-app.js +89 -0
- package/public/assets/homepage-app.js +54 -0
- package/public/assets/homepage-client-app.js +80 -0
- package/public/assets/landing-page-client-app.js +87 -0
- package/public/assets/logo-vidfarm.png +0 -0
- package/public/assets/page-runtime-client-app.js +94 -0
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/public/serve-shells/editor.html +3146 -0
- package/public/serve-shells/library-files.html +3450 -0
- package/public/serve-shells/library-raws.html +4404 -0
- package/public/serve-shells/tools-clipper.html +3991 -0
- package/public/serve-shells/tools-image.html +5584 -0
- package/public/serve-shells/tools-video.html +4404 -0
|
@@ -0,0 +1,4404 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<link rel="icon" href="/assets/favicon.ico" sizes="any">
|
|
7
|
+
<link rel="apple-touch-icon" href="/assets/logo-vidfarm.png">
|
|
8
|
+
<title>Video Tools · VidFarm</title>
|
|
9
|
+
<meta name="description" content="Use VidFarm video tools for editing, enhancement, and asset preparation.">
|
|
10
|
+
<meta name="theme-color" content="#f8f4eb">
|
|
11
|
+
<meta name="robots" content="index, follow">
|
|
12
|
+
<meta name="application-name" content="VidFarm">
|
|
13
|
+
<link rel="canonical" href="https://vidfarm.cc/tools/video">
|
|
14
|
+
<meta property="og:type" content="website">
|
|
15
|
+
<meta property="og:site_name" content="VidFarm">
|
|
16
|
+
<meta property="og:title" content="Video Tools · VidFarm">
|
|
17
|
+
<meta property="og:description" content="Use VidFarm video tools for editing, enhancement, and asset preparation.">
|
|
18
|
+
<meta property="og:url" content="https://vidfarm.cc/tools/video">
|
|
19
|
+
<meta property="og:image" content="https://vidfarmprodstack-vidfarmbucket335ee12f-0vsvtd5earqy.s3.us-east-1.amazonaws.com/primitives/marketing/meta-previews/tools-video-46f8e999af.jpg">
|
|
20
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
21
|
+
<meta name="twitter:title" content="Video Tools · VidFarm">
|
|
22
|
+
<meta name="twitter:description" content="Use VidFarm video tools for editing, enhancement, and asset preparation.">
|
|
23
|
+
<meta name="twitter:image" content="https://vidfarmprodstack-vidfarmbucket335ee12f-0vsvtd5earqy.s3.us-east-1.amazonaws.com/primitives/marketing/meta-previews/tools-video-46f8e999af.jpg">
|
|
24
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
25
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
26
|
+
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
27
|
+
<style>
|
|
28
|
+
:root{
|
|
29
|
+
--rk-font-display:"Hanken Grotesk","blMelody",ui-sans-serif,system-ui,sans-serif;
|
|
30
|
+
--rk-font-body:"Inter",ui-sans-serif,system-ui,-apple-system,sans-serif;
|
|
31
|
+
--rk-font-mono:ui-monospace,"SFMono-Regular",Menlo,Monaco,Consolas,monospace;
|
|
32
|
+
/* honey gold — the one joy accent */
|
|
33
|
+
--rk-gold-500:#ffc738;--rk-gold-600:#f6c744;--rk-gold-700:#fcb900;--rk-gold-tint:#fff5e4;--rk-gold-tint2:#ffe9cc;
|
|
34
|
+
/* ink + neutral scale */
|
|
35
|
+
--rk-ink:#171717;--rk-n-800:#262626;--rk-n-700:#404040;--rk-n-600:#525252;--rk-n-500:#737373;--rk-n-400:#a3a3a3;
|
|
36
|
+
--rk-n-300:#d4d4d4;--rk-n-200:#ededed;--rk-n-100:#f5f5f5;--rk-n-50:#fbfbfb;--rk-white:#fff;
|
|
37
|
+
/* support pastels */
|
|
38
|
+
--rk-sky:#3f9fe0;--rk-sky-tint:#e2effd;--rk-violet:#9f5bde;--rk-violet-tint:#e4ddfd;
|
|
39
|
+
--rk-green:#22c55e;--rk-green-tint:#dcfce7;--rk-coral:#f97316;--rk-coral-tint:#ffe9cc;
|
|
40
|
+
--rk-red:#ef4444;--rk-red-tint:#fee2e2;
|
|
41
|
+
/* semantic */
|
|
42
|
+
--rk-bg:var(--rk-n-50);--rk-bg-section:var(--rk-n-100);--rk-surface:var(--rk-white);
|
|
43
|
+
--rk-border:var(--rk-n-200);--rk-border-strong:var(--rk-n-300);
|
|
44
|
+
--rk-text:var(--rk-n-700);--rk-text-muted:var(--rk-n-500);--rk-text-faint:var(--rk-n-400);
|
|
45
|
+
/* radius */
|
|
46
|
+
--rk-r-sm:6px;--rk-r-md:8px;--rk-r-lg:10px;--rk-r-xl:12px;--rk-r-2xl:16px;--rk-r-3xl:24px;--rk-r-full:9999px;
|
|
47
|
+
/* shadow — diffuse soft sunlight, never harsh */
|
|
48
|
+
--rk-shadow-xs:0 1px 2px 0 #0000000d;
|
|
49
|
+
--rk-shadow-sm:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;
|
|
50
|
+
--rk-shadow-md:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;
|
|
51
|
+
--rk-shadow-lg:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;
|
|
52
|
+
--rk-shadow-xl:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;
|
|
53
|
+
--rk-shadow-card:0 1px 2px #0000000d,0 8px 24px -12px #00000014;
|
|
54
|
+
--rk-shadow-nav:0 4px 24px -8px #00000026,0 1px 2px #0000000d;
|
|
55
|
+
--rk-ring-gold:0 0 0 3px var(--rk-gold-600),0 0 14px rgba(246,199,68,.4);
|
|
56
|
+
/* type scale */
|
|
57
|
+
--rk-text-xs:.75rem;--rk-text-sm:.875rem;--rk-text-base:1rem;--rk-text-lg:1.125rem;
|
|
58
|
+
--rk-text-xl:1.25rem;--rk-text-2xl:1.5rem;--rk-text-3xl:1.875rem;--rk-text-4xl:2.25rem;--rk-text-5xl:3rem;
|
|
59
|
+
/* space + motion */
|
|
60
|
+
--rk-container:75rem;--rk-gutter:clamp(1rem,4vw,2rem);--rk-section-y:clamp(3rem,6vw,5rem);
|
|
61
|
+
--rk-glass-blur:16px;--rk-ease:cubic-bezier(.22,1,.36,1);--rk-dur:200ms;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
*,*::before,*::after{box-sizing:border-box}
|
|
65
|
+
.rk-root{margin:0;font-family:var(--rk-font-body);color:var(--rk-text);background:var(--rk-bg);
|
|
66
|
+
-webkit-font-smoothing:antialiased;line-height:1.5;min-height:100vh}
|
|
67
|
+
.rk-root a{text-decoration:none;color:inherit}
|
|
68
|
+
/* buttons define their own text color; don't let the inherit rule above override it */
|
|
69
|
+
.rk-root a.rk-btn-ink{color:#fff}
|
|
70
|
+
.rk-root a.rk-btn-gold{color:var(--rk-ink)}
|
|
71
|
+
.rk-root a.rk-btn-ghost{color:var(--rk-ink)}
|
|
72
|
+
.rk-root a.rk-btn-danger{color:var(--rk-red)}
|
|
73
|
+
.rk-root h1,.rk-root h2,.rk-root h3,.rk-root h4{font-family:var(--rk-font-display);font-weight:800;
|
|
74
|
+
letter-spacing:-.025em;line-height:1.1;color:var(--rk-ink);margin:0}
|
|
75
|
+
.rk-root p{margin:0}
|
|
76
|
+
.rk-root code{font-family:var(--rk-font-mono);font-size:.9em;background:var(--rk-n-100);
|
|
77
|
+
padding:2px 6px;border-radius:6px;color:var(--rk-n-700)}
|
|
78
|
+
|
|
79
|
+
/* layout */
|
|
80
|
+
.rk-container{width:min(var(--rk-container),100%);margin:0 auto;padding:0 var(--rk-gutter)}
|
|
81
|
+
.rk-container-wide{width:min(85rem,100%);margin:0 auto;padding:0 var(--rk-gutter)}
|
|
82
|
+
/* vertical-only so it never clobbers the horizontal gutter that .rk-container
|
|
83
|
+
/ .rk-container-wide set via padding-left/right (a full padding shorthand
|
|
84
|
+
here would zero the side gutter, leaving content flush to the viewport
|
|
85
|
+
edges, most visible on mobile) */
|
|
86
|
+
.rk-page{padding-top:24px;padding-bottom:80px}
|
|
87
|
+
.rk-page-head{display:grid;gap:4px;margin-bottom:18px}
|
|
88
|
+
.rk-h1{font-size:var(--rk-text-3xl);letter-spacing:-.03em}
|
|
89
|
+
.rk-sub{font-size:var(--rk-text-base);color:var(--rk-text-muted)}
|
|
90
|
+
.rk-stack{display:grid;gap:16px}
|
|
91
|
+
.rk-row{display:flex;align-items:center;gap:12px}
|
|
92
|
+
.rk-spread{display:flex;align-items:center;justify-content:space-between;gap:16px}
|
|
93
|
+
.rk-wrap{flex-wrap:wrap}
|
|
94
|
+
.rk-grid{display:grid;gap:20px}
|
|
95
|
+
.rk-grid-2{grid-template-columns:repeat(2,1fr)}
|
|
96
|
+
.rk-grid-3{grid-template-columns:repeat(3,1fr)}
|
|
97
|
+
.rk-grid-4{grid-template-columns:repeat(4,1fr)}
|
|
98
|
+
@media(max-width:900px){.rk-grid-3,.rk-grid-4{grid-template-columns:repeat(2,1fr)}}
|
|
99
|
+
@media(max-width:620px){.rk-grid-2,.rk-grid-3,.rk-grid-4{grid-template-columns:1fr}}
|
|
100
|
+
.rk-muted{color:var(--rk-text-muted)}.rk-faint{color:var(--rk-text-faint)}
|
|
101
|
+
.rk-mono{font-family:var(--rk-font-mono)}
|
|
102
|
+
|
|
103
|
+
/* app chrome — fixed LEFT sidebar nav + content column */
|
|
104
|
+
.rk-app{min-height:100vh}
|
|
105
|
+
.rk-sidebar{position:fixed;left:0;top:0;bottom:0;width:248px;z-index:50;display:flex;flex-direction:column;gap:4px;
|
|
106
|
+
padding:20px 14px 16px;background:#fff;border-right:1px solid var(--rk-border);overflow-y:auto}
|
|
107
|
+
.rk-content{margin-left:248px;min-height:100vh;background:var(--rk-bg)}
|
|
108
|
+
.rk-sidebar-brand{display:flex;align-items:center;gap:10px;padding:6px 10px 18px;font-family:var(--rk-font-display);
|
|
109
|
+
font-weight:800;font-size:20px;color:var(--rk-ink);letter-spacing:-.02em}
|
|
110
|
+
.rk-sidebar-brand .rk-brand-mark{width:36px;height:36px;border-radius:10px}
|
|
111
|
+
.rk-sidebar-nav{display:flex;flex-direction:column;gap:3px;flex:1;min-height:0}
|
|
112
|
+
.rk-sidebar-sep{height:1px;background:var(--rk-border);margin:8px}
|
|
113
|
+
.rk-sidebar-item{display:flex;align-items:center;padding:9px 13px;border-radius:var(--rk-r-lg);
|
|
114
|
+
font-size:14px;font-weight:600;color:var(--rk-n-600);white-space:nowrap;
|
|
115
|
+
transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
116
|
+
.rk-sidebar-item:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
117
|
+
.rk-sidebar-item.is-active{background:var(--rk-gold-tint);color:var(--rk-ink)}
|
|
118
|
+
/* signed-in identity — subtle, small, pinned to the very bottom of the sidebar
|
|
119
|
+
* so you can tell which account you're in at a glance. The chat FAB hovers
|
|
120
|
+
* ABOVE this block (see .rk-signed-in .rk-fab) rather than sharing the spot. */
|
|
121
|
+
.rk-sidebar-account{margin-top:8px;padding:8px 13px 2px;border-top:1px solid var(--rk-border);
|
|
122
|
+
color:var(--rk-n-500);line-height:1.3;overflow:hidden}
|
|
123
|
+
.rk-sidebar-account-name{font-size:12px;font-weight:600;color:var(--rk-n-600);
|
|
124
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
125
|
+
.rk-sidebar-account-email{font-size:11px;font-weight:500;color:var(--rk-n-400);
|
|
126
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
127
|
+
/* subtle "cloud status" line under the nav (above the account footer) — a quiet
|
|
128
|
+
always-on indicator that opens the Cloud Status drawer on click. Indented past
|
|
129
|
+
the chat FAB's default resting spot (x 16–72px) so the label is never hidden
|
|
130
|
+
behind it. */
|
|
131
|
+
.rk-sidebar-cloud{margin:2px 8px 4px 70px;display:flex;align-items:center;gap:7px;padding:6px 8px;
|
|
132
|
+
border:0;border-radius:var(--rk-r-lg);background:transparent;cursor:pointer;color:var(--rk-n-400);
|
|
133
|
+
font:inherit;font-size:11.5px;font-weight:600;text-align:left;width:calc(100% - 78px);
|
|
134
|
+
transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
135
|
+
.rk-sidebar-cloud[hidden]{display:none}
|
|
136
|
+
.rk-sidebar-cloud:hover{background:var(--rk-n-100);color:var(--rk-n-600)}
|
|
137
|
+
.rk-sidebar-cloud svg{width:14px;height:14px;flex:none;opacity:.85}
|
|
138
|
+
.rk-sidebar-cloud-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
139
|
+
.rk-sidebar-cloud.is-running{color:var(--rk-gold-700)}
|
|
140
|
+
.rk-sidebar-cloud-spin{flex:none;width:12px;height:12px;border-radius:999px;
|
|
141
|
+
border:2px solid var(--rk-gold-tint2);border-top-color:var(--rk-gold-600);
|
|
142
|
+
animation:rk-sidebar-cloud-spin .7s linear infinite}
|
|
143
|
+
@keyframes rk-sidebar-cloud-spin{to{transform:rotate(360deg)}}
|
|
144
|
+
/* AI chat dock — draggable FAB (defaults to the old sidebar-foot spot) + popup panel.
|
|
145
|
+
* The panel is 2x the sidebar width and, when it opens on the left, visually
|
|
146
|
+
* REPLACES the nav (sidebar goes visibility:hidden via body.rk-chat-left so the
|
|
147
|
+
* layout never jumps). If the FAB was dragged to the right half of the viewport
|
|
148
|
+
* the panel anchors right instead, so it is never cut off. */
|
|
149
|
+
.rk-fab{position:fixed;left:16px;bottom:calc(16px + env(safe-area-inset-bottom));z-index:90;width:56px;height:56px;border-radius:var(--rk-r-full);
|
|
150
|
+
border:0;cursor:grab;background:var(--rk-gold-500);color:var(--rk-ink);display:grid;place-items:center;
|
|
151
|
+
box-shadow:var(--rk-shadow-lg);touch-action:none;user-select:none;-webkit-user-select:none;
|
|
152
|
+
transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease),opacity var(--rk-dur) var(--rk-ease)}
|
|
153
|
+
/* signed in: the name/email block sits at the sidebar foot, so lift the FAB's
|
|
154
|
+
* default resting spot to hover just above it (never overlapping the identity).
|
|
155
|
+
* Only applies to the CSS default — a dragged FAB uses its saved inline top. */
|
|
156
|
+
.rk-signed-in .rk-fab{bottom:calc(84px + env(safe-area-inset-bottom))}
|
|
157
|
+
.rk-fab:hover{transform:translateY(-2px);box-shadow:0 6px 18px -6px rgba(252,185,0,.6),var(--rk-shadow-lg)}
|
|
158
|
+
.rk-fab.is-dragging{cursor:grabbing;transition:none;transform:scale(1.08)}
|
|
159
|
+
.rk-fab.is-open{opacity:0;pointer-events:none;transform:scale(.6)}
|
|
160
|
+
.rk-fab svg{width:26px;height:26px}
|
|
161
|
+
.rk-aichat{position:fixed;z-index:95;top:12px;bottom:12px;left:12px;width:min(440px,calc(100vw - 24px));
|
|
162
|
+
display:flex;flex-direction:row;background:var(--rk-surface);border:1px solid var(--rk-border);
|
|
163
|
+
border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-xl);overflow:hidden;
|
|
164
|
+
animation:rk-aichat-in .22s var(--rk-ease)}
|
|
165
|
+
.rk-aichat.has-files,.rk-aichat.has-history,.rk-aichat.has-cloud{width:min(772px,calc(100vw - 24px))}
|
|
166
|
+
.rk-aichat.is-right{left:auto;right:12px}
|
|
167
|
+
.rk-aichat[hidden]{display:none}
|
|
168
|
+
@keyframes rk-aichat-in{from{opacity:0;transform:translateY(12px) scale(.98)}to{opacity:1;transform:none}}
|
|
169
|
+
.rk-chat-left .rk-sidebar{visibility:hidden}
|
|
170
|
+
|
|
171
|
+
/* left Files pane — baked into the panel; My Files + Temp as root folders of
|
|
172
|
+
one virtual root + directory nav. Also reused inline by /reskin/settings. */
|
|
173
|
+
.rk-aichat-files{flex:none;width:266px;display:none;flex-direction:column;min-height:0;
|
|
174
|
+
border-right:1px solid var(--rk-border);background:var(--rk-n-50)}
|
|
175
|
+
.rk-aichat.has-files .rk-aichat-files{display:flex}
|
|
176
|
+
.rk-aichat-files .rk-aichat-crumbs{padding-top:2px}
|
|
177
|
+
|
|
178
|
+
/* files-only mode — opened from the /chat composer's attach button. Shows ONLY
|
|
179
|
+
the file explorer (no chat column / history), as a slim right-hand drawer. */
|
|
180
|
+
/* Narrow panels can't hold a left drawer AND a usable chat column side-by-side
|
|
181
|
+
(266px files + a crushed ~150px chat — the classic "can't get to the chat"
|
|
182
|
+
trap). Below this width the OPEN drawer takes the full panel and the chat
|
|
183
|
+
column steps aside; each drawer's own close/back button (Files/History/Cloud)
|
|
184
|
+
restores the chat. Above it they sit side-by-side as usual. */
|
|
185
|
+
@media (max-width:640px){
|
|
186
|
+
.rk-aichat.has-files .rk-aichat-main,
|
|
187
|
+
.rk-aichat.has-history .rk-aichat-main,
|
|
188
|
+
.rk-aichat.has-cloud .rk-aichat-main{display:none}
|
|
189
|
+
.rk-aichat.has-files .rk-aichat-files,
|
|
190
|
+
.rk-aichat.has-history .rk-aichat-history,
|
|
191
|
+
.rk-aichat.has-cloud .rk-aichat-cloud{flex:1 1 auto;width:auto;border-right:0}
|
|
192
|
+
}
|
|
193
|
+
.rk-aichat.is-files-only{width:min(384px,calc(100vw - 24px))}
|
|
194
|
+
.rk-aichat.is-files-only .rk-aichat-main,
|
|
195
|
+
.rk-aichat.is-files-only .rk-aichat-history,
|
|
196
|
+
.rk-aichat.is-files-only .rk-aichat-cloud{display:none}
|
|
197
|
+
.rk-aichat.is-files-only .rk-aichat-files{display:flex;flex:1;border-right:0}
|
|
198
|
+
.rk-aichat.is-files-only .rk-aichat-files .rk-aichat-crumbs{padding-top:2px}
|
|
199
|
+
.rk-aichat-files-head{display:flex;align-items:center;gap:8px;
|
|
200
|
+
padding:14px 14px 10px;flex:none}
|
|
201
|
+
.rk-aichat-files-title{flex:1;min-width:0;font-family:var(--rk-font-display);font-weight:800;
|
|
202
|
+
font-size:15px;color:var(--rk-ink);letter-spacing:-.01em}
|
|
203
|
+
|
|
204
|
+
/* left Chat-history pane — shares the same left slot as Files (only one shows
|
|
205
|
+
at a time); reuses the rk-aichat-frow row visuals for each past chat. */
|
|
206
|
+
.rk-aichat-history{flex:none;width:266px;display:none;flex-direction:column;min-height:0;
|
|
207
|
+
border-right:1px solid var(--rk-border);background:var(--rk-n-50)}
|
|
208
|
+
.rk-aichat.has-history .rk-aichat-history{display:flex}
|
|
209
|
+
.rk-aichat-hist-head{display:flex;align-items:center;gap:8px;padding:18px 12px 8px;flex:none}
|
|
210
|
+
.rk-aichat-hist-title{flex:1;min-width:0;font-family:var(--rk-font-display);font-weight:800;
|
|
211
|
+
font-size:13px;color:var(--rk-ink);letter-spacing:-.01em}
|
|
212
|
+
.rk-aichat-hist-body{flex:1;min-height:0;overflow-y:auto;padding:2px 8px 12px;display:grid;gap:2px;align-content:start}
|
|
213
|
+
.rk-aichat-hist-body .rk-aichat-frow.is-active{background:#fff;box-shadow:var(--rk-shadow-xs);
|
|
214
|
+
outline:1.5px solid var(--rk-gold-600);outline-offset:-1.5px}
|
|
215
|
+
.rk-aichat-hist-body .rk-aichat-frow.is-active .rk-aichat-fic{background:var(--rk-gold-tint2);color:var(--rk-gold-700)}
|
|
216
|
+
|
|
217
|
+
/* left Cloud-status pane — shares the same left slot as Files/History. Lists the
|
|
218
|
+
customer's long-lived async work (imports, forks, renders, jobs) with All /
|
|
219
|
+
Running / Done / Error filters, live status, and open-in-new-tab links. */
|
|
220
|
+
.rk-aichat-cloud{flex:none;width:300px;display:none;flex-direction:column;min-height:0;
|
|
221
|
+
border-right:1px solid var(--rk-border);background:var(--rk-n-50)}
|
|
222
|
+
.rk-aichat.has-cloud .rk-aichat-cloud{display:flex}
|
|
223
|
+
.rk-aichat-cloud-head{display:flex;align-items:center;gap:8px;padding:18px 12px 8px;flex:none}
|
|
224
|
+
.rk-aichat-cloud-title{flex:1;min-width:0;font-family:var(--rk-font-display);font-weight:800;
|
|
225
|
+
font-size:13px;color:var(--rk-ink);letter-spacing:-.01em}
|
|
226
|
+
.rk-aichat-cloud-refresh{width:26px;height:26px}
|
|
227
|
+
.rk-aichat-cloud-refresh svg{width:14px;height:14px}
|
|
228
|
+
.rk-aichat-cloud-filters{display:flex;gap:4px;padding:0 12px 8px;flex:none;flex-wrap:wrap}
|
|
229
|
+
.rk-cloud-fbtn{padding:3px 9px;border-radius:999px;border:1px solid var(--rk-border);background:#fff;
|
|
230
|
+
cursor:pointer;color:var(--rk-n-600);font:inherit;font-size:11.5px;font-weight:600;line-height:1.4;
|
|
231
|
+
transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
232
|
+
.rk-cloud-fbtn:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
233
|
+
.rk-cloud-fbtn.is-on{background:var(--rk-gold-tint);border-color:var(--rk-gold-600);color:var(--rk-ink)}
|
|
234
|
+
.rk-aichat-cloud-body{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:2px 8px 12px;
|
|
235
|
+
display:grid;grid-template-columns:minmax(0,1fr);gap:4px;align-content:start}
|
|
236
|
+
.rk-cloud-row{display:flex;align-items:center;gap:9px;min-width:0;max-width:100%;padding:8px 9px;border-radius:var(--rk-r-lg);
|
|
237
|
+
background:#fff;border:1px solid var(--rk-border);text-align:left}
|
|
238
|
+
.rk-cloud-row.is-highlight{outline:2px solid var(--rk-gold-600);outline-offset:-1px;
|
|
239
|
+
background:var(--rk-gold-tint);animation:rk-cloud-flash 1.1s var(--rk-ease)}
|
|
240
|
+
@keyframes rk-cloud-flash{0%{box-shadow:0 0 0 0 rgba(252,185,0,.55)}100%{box-shadow:0 0 0 8px rgba(252,185,0,0)}}
|
|
241
|
+
.rk-cloud-ic{flex:none;width:20px;height:20px;border-radius:999px;display:grid;place-items:center;
|
|
242
|
+
font-size:11px;line-height:1;background:var(--rk-n-100);color:var(--rk-n-600)}
|
|
243
|
+
.rk-cloud-ic.is-running{background:var(--rk-gold-tint2);color:var(--rk-gold-700);animation:rk-cloud-pulse 1.5s ease-in-out infinite}
|
|
244
|
+
.rk-cloud-ic.is-queued{background:var(--rk-n-100);color:var(--rk-n-500)}
|
|
245
|
+
.rk-cloud-ic.is-done{background:#e7f6ec;color:#1a7f43}
|
|
246
|
+
.rk-cloud-ic.is-error{background:#fdeaea;color:#c0362c}
|
|
247
|
+
@keyframes rk-cloud-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
248
|
+
.rk-cloud-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
|
|
249
|
+
.rk-cloud-name{font-size:12.5px;font-weight:700;color:var(--rk-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
250
|
+
.rk-cloud-sub{font-size:11px;color:var(--rk-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
251
|
+
.rk-cloud-right{flex:none;display:flex;align-items:center;gap:6px}
|
|
252
|
+
.rk-cloud-pill{font-size:10.5px;font-weight:700;padding:2px 7px;border-radius:999px;background:var(--rk-n-100);color:var(--rk-n-600)}
|
|
253
|
+
.rk-cloud-pill.is-running{background:var(--rk-gold-tint);color:var(--rk-gold-700)}
|
|
254
|
+
.rk-cloud-pill.is-done{background:#e7f6ec;color:#1a7f43}
|
|
255
|
+
.rk-cloud-pill.is-error{background:#fdeaea;color:#c0362c}
|
|
256
|
+
.rk-cloud-open{flex:none;width:24px;height:24px;border-radius:var(--rk-r-md);display:grid;place-items:center;
|
|
257
|
+
color:var(--rk-n-500);border:1px solid transparent}
|
|
258
|
+
.rk-cloud-open:hover{background:var(--rk-n-100);color:var(--rk-ink);border-color:var(--rk-border)}
|
|
259
|
+
/* the toggle-button running dot */
|
|
260
|
+
.rk-cloud-dot{position:absolute;top:3px;right:3px;width:7px;height:7px;border-radius:999px;
|
|
261
|
+
background:var(--rk-gold-600);box-shadow:0 0 0 2px #fff;animation:rk-cloud-pulse 1.5s ease-in-out infinite}
|
|
262
|
+
#rkCloudToggle{position:relative}
|
|
263
|
+
.rk-aichat-crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:1px;padding:0 12px 8px;flex:none}
|
|
264
|
+
.rk-aichat-crumb{padding:2px 6px;border-radius:var(--rk-r-sm);cursor:pointer;background:transparent;border:0;
|
|
265
|
+
color:var(--rk-text-muted);font-size:12px;font-weight:600;font-family:inherit;max-width:120px;overflow:hidden;
|
|
266
|
+
text-overflow:ellipsis;white-space:nowrap}
|
|
267
|
+
.rk-aichat-crumb:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
268
|
+
.rk-aichat-crumb.is-current{color:var(--rk-ink)}
|
|
269
|
+
.rk-aichat-crumb-sep{color:var(--rk-n-300);font-size:12px}
|
|
270
|
+
.rk-aichat-files-body{flex:1;min-height:0;overflow-y:auto;padding:2px 8px 12px;display:grid;gap:2px;align-content:start}
|
|
271
|
+
.rk-aichat-frow{display:flex;align-items:center;gap:9px;width:100%;padding:8px 9px;border-radius:var(--rk-r-lg);
|
|
272
|
+
border:0;background:transparent;cursor:pointer;text-align:left;font-family:inherit;min-width:0;
|
|
273
|
+
transition:background var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
274
|
+
.rk-aichat-frow:hover{background:#fff;box-shadow:var(--rk-shadow-xs)}
|
|
275
|
+
/* preview trigger fills the row; attach button sits to its right */
|
|
276
|
+
.rk-aichat-fmainbtn{flex:1;min-width:0;display:flex;align-items:center;gap:9px;border:0;background:transparent;
|
|
277
|
+
padding:0;cursor:pointer;text-align:left;font-family:inherit}
|
|
278
|
+
.rk-aichat-fic{width:30px;height:30px;border-radius:var(--rk-r-md);flex:none;display:grid;place-items:center;
|
|
279
|
+
font-size:11px;font-weight:700;overflow:hidden}
|
|
280
|
+
.rk-aichat-fic-folder{background:var(--rk-gold-tint2);color:var(--rk-gold-700)}
|
|
281
|
+
.rk-aichat-fic-file{background:var(--rk-n-100);color:var(--rk-n-600)}
|
|
282
|
+
.rk-aichat-fic img,.rk-aichat-fic video{width:100%;height:100%;object-fit:cover}
|
|
283
|
+
.rk-aichat-fmain{flex:1;min-width:0;display:grid;gap:1px}
|
|
284
|
+
.rk-aichat-fname{font-size:12.5px;font-weight:600;color:var(--rk-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
285
|
+
.rk-aichat-fmeta{font-size:11px;color:var(--rk-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
286
|
+
.rk-aichat-fadd{flex:none;font-size:11px;font-weight:700;color:var(--rk-gold-700);opacity:0;cursor:pointer;
|
|
287
|
+
border:1px solid transparent;border-radius:var(--rk-r-full);background:transparent;padding:3px 9px;font-family:inherit;
|
|
288
|
+
transition:opacity var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
289
|
+
.rk-aichat-frow:hover .rk-aichat-fadd{opacity:1}
|
|
290
|
+
.rk-aichat-fadd:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
291
|
+
.rk-aichat-fadd:focus-visible{opacity:1}
|
|
292
|
+
.rk-aichat-fstate{padding:22px 14px;text-align:center;font-size:12.5px;color:var(--rk-text-muted);line-height:1.5;display:grid;gap:8px;justify-items:center}
|
|
293
|
+
.rk-aichat-fstate a{color:var(--rk-gold-700);font-weight:600}
|
|
294
|
+
.rk-aichat-fstate button{font-family:inherit;font-size:12px;font-weight:600;color:var(--rk-ink);cursor:pointer;
|
|
295
|
+
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:5px 12px}
|
|
296
|
+
.rk-aichat-fstate button:hover{background:var(--rk-n-100)}
|
|
297
|
+
/* upload dropzone footer (shown when inside My Files / Temp) */
|
|
298
|
+
.rk-aichat-drop{flex:none;margin:2px 10px 12px;padding:13px 12px;border:1.5px dashed var(--rk-border-strong);
|
|
299
|
+
border-radius:var(--rk-r-xl);background:#fff;display:grid;gap:9px;justify-items:center;text-align:center;
|
|
300
|
+
transition:border-color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
301
|
+
.rk-aichat-drop[hidden]{display:none}
|
|
302
|
+
.rk-aichat-drop.is-drag{border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
|
|
303
|
+
.rk-aichat-drop-text{font-size:11.5px;color:var(--rk-text-muted);line-height:1.4;overflow-wrap:anywhere}
|
|
304
|
+
.rk-aichat-drop-btn{font-family:inherit;font-size:12px;font-weight:700;color:var(--rk-ink);cursor:pointer;
|
|
305
|
+
background:var(--rk-gold-tint);border:1px solid var(--rk-gold-600);border-radius:var(--rk-r-full);padding:6px 15px;
|
|
306
|
+
transition:background var(--rk-dur) var(--rk-ease)}
|
|
307
|
+
.rk-aichat-drop-btn:hover{background:var(--rk-gold-tint2)}
|
|
308
|
+
.rk-aichat-drop-btn:disabled{opacity:.6;cursor:default}
|
|
309
|
+
|
|
310
|
+
/* ── reusable directory explorer (createDirectoryExplorer) ──
|
|
311
|
+
Shares the .rk-aichat-frow/-fic/-fmain row look; adds search, multi-select,
|
|
312
|
+
a folder Open button, a per-row "…" kebab menu, and a copy-path toast.
|
|
313
|
+
Reused verbatim by the chat dock (attach mode) and /library/raws (standalone). */
|
|
314
|
+
.rk-dir{display:flex;flex-direction:column;min-height:0;flex:1}
|
|
315
|
+
.rk-dir-space{flex:none;display:inline-flex;gap:2px;margin:2px 12px 8px;padding:2px;
|
|
316
|
+
background:var(--rk-n-100,#f1ede6);border:1px solid var(--rk-border);border-radius:var(--rk-r-full);align-self:flex-start}
|
|
317
|
+
.rk-dir-space-btn{font-family:inherit;font-size:12px;font-weight:600;color:var(--rk-n-500,#7a7266);
|
|
318
|
+
background:transparent;border:0;border-radius:var(--rk-r-full);padding:4px 14px;cursor:pointer;
|
|
319
|
+
transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
320
|
+
.rk-dir-space-btn.is-on{background:var(--rk-surface,#fff);color:var(--rk-ink);box-shadow:var(--rk-shadow-sm,0 1px 2px rgba(0,0,0,.06))}
|
|
321
|
+
.rk-dir-search{flex:none;padding:0 12px 8px}
|
|
322
|
+
.rk-dir-search-input{width:100%;font-family:inherit;font-size:12.5px;color:var(--rk-ink);
|
|
323
|
+
background:var(--rk-surface,#fff);border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:7px 12px;
|
|
324
|
+
transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
325
|
+
.rk-dir-search-input:focus{outline:none;border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
|
|
326
|
+
.rk-dir-search-input::placeholder{color:var(--rk-n-400)}
|
|
327
|
+
.rk-dir-body{flex:1 1 auto;min-height:0}
|
|
328
|
+
.rk-dir-crumbs{align-items:center}
|
|
329
|
+
.rk-dir-newfolder{margin-left:auto;flex:none;font-family:inherit;font-size:11.5px;font-weight:700;cursor:pointer;
|
|
330
|
+
color:var(--rk-gold-700);background:transparent;border:1px solid transparent;border-radius:var(--rk-r-full);padding:3px 10px;
|
|
331
|
+
transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
332
|
+
.rk-dir-newfolder:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
333
|
+
.rk-dir-uploadbtn{flex:none;font-family:inherit;font-size:11.5px;font-weight:700;cursor:pointer;
|
|
334
|
+
color:var(--rk-gold-700);background:transparent;border:1px solid transparent;border-radius:var(--rk-r-full);padding:3px 10px;
|
|
335
|
+
transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
336
|
+
.rk-dir-uploadbtn:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
337
|
+
.rk-dir-uploadbtn:disabled{opacity:.6;cursor:default}
|
|
338
|
+
.rk-dir-uploadbtn.is-drag{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
339
|
+
/* selection toolbar (shown when 1+ rows are selected) */
|
|
340
|
+
.rk-dir-selbar{flex:none;display:flex;align-items:center;gap:8px;margin:0 10px 8px;padding:7px 10px;
|
|
341
|
+
background:var(--rk-gold-tint);border:1px solid var(--rk-gold-600);border-radius:var(--rk-r-xl)}
|
|
342
|
+
.rk-dir-selbar[hidden]{display:none}
|
|
343
|
+
.rk-dir-selcount{flex:1;font-size:12px;font-weight:700;color:var(--rk-gold-700)}
|
|
344
|
+
.rk-dir-selbtn{flex:none;font-family:inherit;font-size:12px;font-weight:800;color:var(--rk-ink);cursor:pointer;
|
|
345
|
+
background:transparent;border:1px solid var(--rk-gold-600);border-radius:var(--rk-r-full);padding:5px 14px;
|
|
346
|
+
transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
347
|
+
.rk-dir-selbtn.is-primary{background:var(--rk-gold-500);border-color:var(--rk-gold-600);color:var(--rk-ink)}
|
|
348
|
+
.rk-dir-selbtn.is-primary:hover{background:var(--rk-gold-600)}
|
|
349
|
+
.rk-dir-selbtn.is-ghost{border-color:var(--rk-border);color:var(--rk-text-muted);background:transparent}
|
|
350
|
+
.rk-dir-selbtn.is-ghost:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600);color:var(--rk-ink)}
|
|
351
|
+
.rk-dir-selmore{padding:5px 10px;font-size:15px;line-height:1;font-weight:700}
|
|
352
|
+
/* per-row select checkbox — discoverable on hover, sticky when selected */
|
|
353
|
+
.rk-dir-check{flex:none;width:15px;height:15px;margin:0;accent-color:var(--rk-gold-600);cursor:pointer;opacity:0;
|
|
354
|
+
transition:opacity var(--rk-dur) var(--rk-ease)}
|
|
355
|
+
.rk-aichat-frow:hover .rk-dir-check,.rk-dir-row.is-selected .rk-dir-check,.rk-dir-check:focus-visible{opacity:1}
|
|
356
|
+
.rk-dir-row.is-selected{background:var(--rk-gold-tint);outline:1.5px solid var(--rk-gold-600);outline-offset:-1.5px}
|
|
357
|
+
/* folder Open button — always visible (it's the navigation affordance) */
|
|
358
|
+
.rk-dir-open{opacity:1;color:var(--rk-text-muted)}
|
|
359
|
+
.rk-aichat-frow:hover .rk-dir-open{color:var(--rk-gold-700)}
|
|
360
|
+
/* per-row "…" kebab */
|
|
361
|
+
.rk-dir-kebab{flex:none;width:26px;height:26px;border-radius:var(--rk-r-md);border:1px solid transparent;background:transparent;
|
|
362
|
+
cursor:pointer;color:var(--rk-n-500);font-size:16px;line-height:1;display:grid;place-items:center;opacity:0;
|
|
363
|
+
transition:opacity var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
364
|
+
.rk-aichat-frow:hover .rk-dir-kebab,.rk-dir-kebab:focus-visible{opacity:1}
|
|
365
|
+
.rk-dir-kebab:hover{background:var(--rk-n-100);color:var(--rk-ink);border-color:var(--rk-border)}
|
|
366
|
+
/* kebab popup + copy toast (appended to <body>, so plain colors not tokens for
|
|
367
|
+
portability onto any host page) */
|
|
368
|
+
.rk-dir-menu{position:fixed;z-index:2147483000;min-width:150px;background:#fff;border:1px solid var(--rk-border,#e6e1d8);
|
|
369
|
+
border-radius:var(--rk-r-lg,12px);box-shadow:var(--rk-shadow-md,0 10px 30px rgba(40,30,10,.16));padding:5px;display:grid;gap:1px}
|
|
370
|
+
.rk-dir-menuitem{font-family:inherit;font-size:12.5px;font-weight:600;color:var(--rk-ink,#2b2620);cursor:pointer;
|
|
371
|
+
text-align:left;background:transparent;border:0;border-radius:var(--rk-r-md,8px);padding:7px 10px}
|
|
372
|
+
.rk-dir-menuitem:hover{background:var(--rk-n-100,#f2efe9)}
|
|
373
|
+
.rk-dir-menuitem.is-danger{color:#b4341f}
|
|
374
|
+
.rk-dir-menuitem.is-danger:hover{background:#fbeae6}
|
|
375
|
+
.rk-dir-toast{position:fixed;z-index:2147483001;left:50%;bottom:26px;transform:translate(-50%,12px);
|
|
376
|
+
background:#2b2620;color:#fff;font-family:var(--rk-font-body,inherit);font-size:12.5px;font-weight:600;
|
|
377
|
+
padding:9px 15px;border-radius:999px;box-shadow:0 10px 30px rgba(0,0,0,.25);max-width:80vw;overflow:hidden;
|
|
378
|
+
text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none;
|
|
379
|
+
transition:opacity .18s ease,transform .18s ease}
|
|
380
|
+
.rk-dir-toast.is-on{opacity:1;transform:translate(-50%,0)}
|
|
381
|
+
|
|
382
|
+
/* chat column */
|
|
383
|
+
.rk-aichat-main{flex:1;min-width:0;display:flex;flex-direction:column}
|
|
384
|
+
.rk-aichat-head{display:flex;align-items:center;gap:8px;padding:14px 14px;border-bottom:1px solid var(--rk-border);flex:none}
|
|
385
|
+
.rk-aichat-head-main{flex:1;min-width:0;display:grid;gap:1px}
|
|
386
|
+
.rk-aichat-title{font-family:var(--rk-font-display);font-weight:800;font-size:15px;color:var(--rk-ink);letter-spacing:-.01em}
|
|
387
|
+
.rk-aichat-sub{font-size:11.5px;color:var(--rk-text-muted)}
|
|
388
|
+
.rk-aichat-back{flex:1;min-width:0;display:inline-flex;align-items:center;gap:8px;padding:7px 12px 7px 9px;
|
|
389
|
+
border-radius:999px;border:1px solid var(--rk-border);background:#fff;cursor:pointer;text-decoration:none;
|
|
390
|
+
font-family:var(--rk-font-display);font-weight:700;font-size:13.5px;color:var(--rk-ink);letter-spacing:-.01em;
|
|
391
|
+
transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
392
|
+
.rk-aichat-back:hover{background:var(--rk-n-100);border-color:var(--rk-n-300)}
|
|
393
|
+
.rk-aichat-back svg{width:17px;height:17px;flex:none;color:var(--rk-n-600)}
|
|
394
|
+
.rk-aichat-back span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
395
|
+
/* SKILL.md pill riding to the right of "Back to Library" in the editor dock head */
|
|
396
|
+
.rk-aichat-head:has(.rk-aichat-skill) .rk-aichat-back{flex:0 1 auto}
|
|
397
|
+
.rk-aichat-skill{flex:1;min-width:0;display:flex}
|
|
398
|
+
.rk-aichat-skill .vf-skill-install{width:100%;background:transparent;box-shadow:none;border-color:var(--rk-border);backdrop-filter:none;-webkit-backdrop-filter:none}
|
|
399
|
+
.rk-aichat-skill .vf-skill-install-main{flex:1;justify-content:flex-start}
|
|
400
|
+
.rk-aichat-skill .vf-skill-install-menu{border-left-color:var(--rk-border)}
|
|
401
|
+
.rk-aichat-skill .vf-skill-install-popover{right:0;left:auto}
|
|
402
|
+
.rk-aichat-tool{width:30px;height:30px;border-radius:var(--rk-r-lg);border:1px solid var(--rk-border);background:#fff;
|
|
403
|
+
cursor:pointer;color:var(--rk-n-600);display:grid;place-items:center;font-size:13px;flex:none;
|
|
404
|
+
transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
405
|
+
.rk-aichat-tool:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
406
|
+
.rk-aichat-tool svg{width:15px;height:15px}
|
|
407
|
+
.rk-aichat-tool.is-on{background:var(--rk-gold-tint);border-color:var(--rk-gold-600);color:var(--rk-ink)}
|
|
408
|
+
/* overflow-x:hidden + a capped single column (minmax(0,1fr), NOT the default
|
|
409
|
+
auto track which grows to content) keep the log scrolling ONLY vertically —
|
|
410
|
+
a wide child (long tool note / URL / file path) can no longer widen the grid
|
|
411
|
+
and force a horizontal scrollbar in the narrow dock column. */
|
|
412
|
+
.rk-aichat-log{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:16px;display:grid;grid-template-columns:minmax(0,1fr);gap:10px;align-content:start;background:var(--rk-n-50)}
|
|
413
|
+
.rk-aichat-msg{max-width:85%;min-width:0;padding:10px 14px;border-radius:var(--rk-r-2xl);font-size:13.5px;line-height:1.5;overflow-wrap:anywhere;word-break:break-word}
|
|
414
|
+
.rk-aichat-msg.is-ai{background:#fff;border:1px solid var(--rk-border);color:var(--rk-text);justify-self:start;
|
|
415
|
+
border-bottom-left-radius:var(--rk-r-sm);box-shadow:var(--rk-shadow-xs)}
|
|
416
|
+
.rk-aichat-msg.is-user{background:var(--rk-gold-tint2);color:var(--rk-ink);justify-self:end;border-bottom-right-radius:var(--rk-r-sm)}
|
|
417
|
+
/* rendered markdown inside assistant replies (bold / italic / links) */
|
|
418
|
+
.rk-aichat-msg.is-ai a,.rk-aichat-stream a{color:var(--rk-gold-700);font-weight:600;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}
|
|
419
|
+
.rk-aichat-msg.is-ai strong,.rk-aichat-stream strong{color:var(--rk-ink);font-weight:700}
|
|
420
|
+
.rk-aichat-msg.is-ai em,.rk-aichat-stream em{font-style:italic}
|
|
421
|
+
.rk-aichat-chips{display:flex;flex-wrap:wrap;gap:6px;padding:10px 12px 0;flex:none}
|
|
422
|
+
.rk-aichat-chips:empty{display:none}
|
|
423
|
+
.rk-aichat-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 6px 4px 10px;border-radius:var(--rk-r-full);
|
|
424
|
+
background:var(--rk-gold-tint2);color:var(--rk-gold-700);font-size:11.5px;font-weight:600;max-width:190px}
|
|
425
|
+
.rk-aichat-chip span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
426
|
+
.rk-aichat-chip button{border:0;background:transparent;color:inherit;cursor:pointer;font-size:14px;line-height:1;padding:0 2px}
|
|
427
|
+
.rk-aichat-chip-label{max-width:150px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;
|
|
428
|
+
font-size:11.5px;font-weight:600;text-align:left;text-decoration:underline;text-underline-offset:2px;text-decoration-color:transparent}
|
|
429
|
+
.rk-aichat-chip-label:hover{text-decoration-color:currentColor}
|
|
430
|
+
.rk-aichat-chip.is-pending{opacity:.75}
|
|
431
|
+
.rk-aichat-chip.is-pending .rk-aichat-chip-label{text-decoration:none;cursor:default}
|
|
432
|
+
.rk-aichat-chip-spin{width:11px;height:11px;flex:none;border-radius:50%;border:2px solid currentColor;
|
|
433
|
+
border-top-color:transparent;animation:rk-aichat-chip-spin .7s linear infinite}
|
|
434
|
+
@keyframes rk-aichat-chip-spin{to{transform:rotate(360deg)}}
|
|
435
|
+
/* composer = one bordered "text area" box: the textarea fills the top and the
|
|
436
|
+
attach + send controls sit inside it along the bottom row. */
|
|
437
|
+
.rk-aichat-composer{display:flex;flex-direction:column;gap:8px;margin:12px;padding:10px 10px 9px;flex:none;background:#fff;
|
|
438
|
+
border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-xs);
|
|
439
|
+
transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
440
|
+
.rk-aichat-composer:focus-within{border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
|
|
441
|
+
.rk-aichat-field{width:100%;min-height:70px;max-height:210px;resize:none;border:0;background:transparent;
|
|
442
|
+
color:var(--rk-ink);font-family:var(--rk-font-body);font-size:14px;line-height:1.5;padding:4px 4px 0}
|
|
443
|
+
.rk-aichat-field:focus{outline:none;box-shadow:none}
|
|
444
|
+
.rk-aichat-field::placeholder{color:var(--rk-n-400)}
|
|
445
|
+
.rk-aichat-composer-actions{display:flex;align-items:center;gap:8px}
|
|
446
|
+
.rk-aichat-composer-actions .rk-btn{margin-left:auto}
|
|
447
|
+
/* composer paperclip — opens the folder directory nav to browse + attach files
|
|
448
|
+
(tokens so it re-themes on the dark editor dock automatically) */
|
|
449
|
+
.rk-aichat-attach{flex:none;width:38px;height:38px;border-radius:var(--rk-r-full);border:1px solid var(--rk-border);
|
|
450
|
+
background:var(--rk-surface);color:var(--rk-n-500);cursor:pointer;display:grid;place-items:center;
|
|
451
|
+
transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
452
|
+
.rk-aichat-attach:hover,.rk-aichat-attach.is-on{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink)}
|
|
453
|
+
.rk-aichat-attach svg{width:18px;height:18px}
|
|
454
|
+
|
|
455
|
+
/* ── live agentic turn: status → actions (tool notes + HTTP cards) → text ── */
|
|
456
|
+
.rk-aichat-turn{display:grid;gap:8px;justify-items:start;width:100%;min-width:0}
|
|
457
|
+
.rk-aichat-stream{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;max-width:100%}
|
|
458
|
+
.rk-aichat-stream[hidden]{display:none}
|
|
459
|
+
.rk-aichat-actions{display:grid;gap:6px;width:100%}
|
|
460
|
+
.rk-aichat-actions:empty{display:none}
|
|
461
|
+
.rk-aichat-status{display:flex;align-items:center;padding:2px 2px}
|
|
462
|
+
.rk-aichat-status[hidden]{display:none}
|
|
463
|
+
.rk-aichat-typing{display:inline-flex;gap:4px;align-items:center}
|
|
464
|
+
.rk-aichat-typing span{width:6px;height:6px;border-radius:50%;background:var(--rk-n-400);
|
|
465
|
+
animation:rk-aichat-blink 1.2s var(--rk-ease) infinite}
|
|
466
|
+
.rk-aichat-typing span:nth-child(2){animation-delay:.18s}
|
|
467
|
+
.rk-aichat-typing span:nth-child(3){animation-delay:.36s}
|
|
468
|
+
@keyframes rk-aichat-blink{0%,80%,100%{opacity:.28;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}
|
|
469
|
+
.rk-aichat-toolnote{display:inline-flex;align-items:center;gap:7px;font-size:11.5px;color:var(--rk-text-muted);
|
|
470
|
+
font-family:var(--rk-font-mono);background:#fff;border:1px solid var(--rk-border);
|
|
471
|
+
border-radius:var(--rk-r-lg);padding:5px 10px;width:fit-content;max-width:100%;overflow-wrap:anywhere;word-break:break-word}
|
|
472
|
+
.rk-aichat-toolnote::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--rk-gold-500);flex:none}
|
|
473
|
+
.rk-aichat-loadmore{width:100%;margin-top:6px;padding:9px 12px;font-size:12.5px;font-weight:600;
|
|
474
|
+
color:var(--rk-text);background:var(--rk-white);border:1px solid var(--rk-border);
|
|
475
|
+
border-radius:var(--rk-r-lg);cursor:pointer;font-family:var(--rk-font-mono)}
|
|
476
|
+
.rk-aichat-loadmore:hover{background:var(--rk-n-50);border-color:var(--rk-border-strong)}
|
|
477
|
+
.rk-aichat-loadmore:disabled{opacity:.6;cursor:default}
|
|
478
|
+
.rk-aichat-toolnote.is-ok{color:#0a7c3f}
|
|
479
|
+
.rk-aichat-toolnote.is-ok::before{background:#12a150}
|
|
480
|
+
.rk-aichat-toolnote.is-err{color:#b42318}
|
|
481
|
+
.rk-aichat-toolnote.is-err::before{background:#d92d20}
|
|
482
|
+
.rk-aichat-err{font-size:12.5px;color:#b42318;background:#fff;border:1px solid var(--rk-border);
|
|
483
|
+
border-radius:var(--rk-r-lg);padding:9px 11px;white-space:pre-wrap;width:100%}
|
|
484
|
+
|
|
485
|
+
/* first-class HTTP action card (chip → modal) */
|
|
486
|
+
.rk-aichat-http{display:flex;align-items:center;gap:8px;width:100%;padding:7px 9px;cursor:pointer;text-align:left;
|
|
487
|
+
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);box-shadow:var(--rk-shadow-xs);
|
|
488
|
+
transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
489
|
+
.rk-aichat-http:hover{border-color:var(--rk-gold-600);box-shadow:var(--rk-shadow-sm)}
|
|
490
|
+
.rk-aichat-http-method{flex:none;font-family:var(--rk-font-mono);font-size:10.5px;font-weight:700;letter-spacing:.03em;
|
|
491
|
+
color:var(--rk-gold-700);background:var(--rk-gold-tint);border-radius:var(--rk-r-sm);padding:2px 7px}
|
|
492
|
+
.rk-aichat-http-url{flex:1;min-width:0;font-family:var(--rk-font-mono);font-size:11.5px;color:var(--rk-n-700);
|
|
493
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
494
|
+
.rk-aichat-http-status{flex:none;font-family:var(--rk-font-mono);font-size:11px;font-weight:700;color:var(--rk-green);
|
|
495
|
+
background:var(--rk-green-tint);border-radius:var(--rk-r-sm);padding:2px 7px}
|
|
496
|
+
.rk-aichat-http-status[data-error="true"]{color:var(--rk-red);background:var(--rk-red-tint)}
|
|
497
|
+
|
|
498
|
+
/* HTTP request/response modal (portalled to body, above the fixed panel) */
|
|
499
|
+
.rk-aichat-modal-back{position:fixed;inset:0;z-index:120;display:grid;place-items:center;padding:20px;
|
|
500
|
+
background:rgba(23,23,23,.42);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
|
|
501
|
+
animation:rk-aichat-in .16s var(--rk-ease)}
|
|
502
|
+
.rk-aichat-modal{width:min(720px,100%);max-height:min(82vh,calc(100vh - 24px));display:flex;flex-direction:column;background:var(--rk-surface);
|
|
503
|
+
border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-xl);overflow:hidden}
|
|
504
|
+
.rk-aichat-modal-head{display:flex;align-items:center;gap:9px;padding:14px 16px;border-bottom:1px solid var(--rk-border);flex:none}
|
|
505
|
+
.rk-aichat-modal-head .rk-aichat-http-url{font-size:12.5px}
|
|
506
|
+
.rk-aichat-modal-x{flex:none;margin-left:2px;width:28px;height:28px;border-radius:var(--rk-r-lg);border:1px solid var(--rk-border);
|
|
507
|
+
background:#fff;color:var(--rk-n-500);cursor:pointer;font-size:17px;line-height:1;display:grid;place-items:center}
|
|
508
|
+
.rk-aichat-modal-x:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
509
|
+
.rk-aichat-modal-body{padding:14px 16px;overflow-y:auto;display:grid;gap:14px}
|
|
510
|
+
.rk-aichat-http-note{font-size:12.5px;color:var(--rk-text-muted);line-height:1.5}
|
|
511
|
+
.rk-aichat-http-block{display:grid;gap:5px}
|
|
512
|
+
.rk-aichat-http-blabel{font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--rk-text-faint)}
|
|
513
|
+
.rk-aichat-http-pre{margin:0;font-family:var(--rk-font-mono);font-size:11.5px;line-height:1.55;color:var(--rk-n-700);
|
|
514
|
+
background:var(--rk-n-50);border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);padding:10px 12px;
|
|
515
|
+
max-height:220px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
516
|
+
|
|
517
|
+
/* collapsible JSON tree (request/response/headers inspection in the modal) */
|
|
518
|
+
.rk-aichat-json{border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:#fff;overflow:hidden}
|
|
519
|
+
.rk-aichat-json-bar{display:flex;align-items:center;gap:8px;padding:7px 10px;border-bottom:1px solid var(--rk-border);
|
|
520
|
+
background:var(--rk-n-50)}
|
|
521
|
+
.rk-aichat-json-all{font-family:var(--rk-font-body);font-size:11px;font-weight:700;color:var(--rk-n-600);cursor:pointer;
|
|
522
|
+
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:3px 10px}
|
|
523
|
+
.rk-aichat-json-all:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600);color:var(--rk-ink)}
|
|
524
|
+
.rk-aichat-json-scroll{max-height:300px;overflow:auto;padding:8px 0}
|
|
525
|
+
.rk-aichat-json-line{position:relative;display:flex;align-items:flex-start;width:100%;min-height:22px;padding-right:34px}
|
|
526
|
+
.rk-aichat-json-line:hover{background:var(--rk-gold-tint)}
|
|
527
|
+
.rk-aichat-json-line.is-toggle{padding-left:0}
|
|
528
|
+
.rk-aichat-json-caret{flex:none;width:20px;height:20px;margin-top:1px;display:grid;place-items:center;border:0;
|
|
529
|
+
background:transparent;color:var(--rk-n-500);cursor:pointer}
|
|
530
|
+
.rk-aichat-json-caret svg{transition:transform var(--rk-dur) var(--rk-ease)}
|
|
531
|
+
.rk-aichat-json-caret[data-open="true"] svg{transform:rotate(90deg)}
|
|
532
|
+
.rk-aichat-json-text{display:block;width:100%;font-family:var(--rk-font-mono);font-size:11.5px;line-height:1.5;
|
|
533
|
+
color:var(--rk-n-700);white-space:pre-wrap;word-break:break-word}
|
|
534
|
+
.rk-aichat-json-copy{position:absolute;right:8px;top:2px;width:20px;height:20px;border-radius:var(--rk-r-sm);
|
|
535
|
+
display:grid;place-items:center;border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-500);cursor:pointer;
|
|
536
|
+
opacity:0;box-shadow:var(--rk-shadow-xs);transition:opacity var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
537
|
+
.rk-aichat-json-line:hover .rk-aichat-json-copy,.rk-aichat-json-copy:focus-visible{opacity:1}
|
|
538
|
+
.rk-aichat-json-copy:hover{color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
539
|
+
|
|
540
|
+
/* ── file preview modal (images carousel, video, audio, pdf, md/txt/json) ── */
|
|
541
|
+
.rk-aichat-preview{width:min(880px,100%);max-height:min(88vh,calc(100vh - 24px))}
|
|
542
|
+
.rk-aichat-preview-name{flex:1;min-width:0;font-family:var(--rk-font-body);font-weight:700;font-size:13px;color:var(--rk-ink);
|
|
543
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
544
|
+
.rk-aichat-preview-open{flex:none;font-size:11.5px;font-weight:700;color:var(--rk-gold-700);text-decoration:none;
|
|
545
|
+
border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:4px 11px}
|
|
546
|
+
.rk-aichat-preview-open:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
547
|
+
.rk-aichat-preview-body{padding:0;display:block;min-height:120px;background:var(--rk-n-50)}
|
|
548
|
+
.rk-aichat-preview-img{display:block;max-width:100%;max-height:70vh;margin:0 auto;object-fit:contain;background:var(--rk-n-100)}
|
|
549
|
+
.rk-aichat-preview-media{display:block;width:100%;max-height:70vh;background:#000}
|
|
550
|
+
.rk-aichat-preview-audiowrap{padding:34px 20px;display:grid;place-items:center}
|
|
551
|
+
.rk-aichat-preview-audio{width:100%;max-width:460px}
|
|
552
|
+
.rk-aichat-preview-frame{display:block;width:100%;height:70vh;border:0;background:#fff}
|
|
553
|
+
.rk-aichat-preview-loading{padding:34px 16px;text-align:center;font-size:12.5px;color:var(--rk-text-muted)}
|
|
554
|
+
.rk-aichat-preview-text{margin:0;border:0;border-radius:0;max-height:70vh;background:#fff}
|
|
555
|
+
.rk-aichat-preview .rk-aichat-json{border:0;border-radius:0}
|
|
556
|
+
.rk-aichat-preview .rk-aichat-json-scroll{max-height:70vh}
|
|
557
|
+
.rk-aichat-preview-md{padding:18px 22px;max-height:70vh;overflow:auto;background:#fff;font-size:13.5px;line-height:1.6;color:var(--rk-n-700)}
|
|
558
|
+
.rk-aichat-preview-md h1,.rk-aichat-preview-md h2,.rk-aichat-preview-md h3,.rk-aichat-preview-md h4,.rk-aichat-preview-md h5,.rk-aichat-preview-md h6{
|
|
559
|
+
font-family:var(--rk-font-display);color:var(--rk-ink);font-weight:800;line-height:1.25;margin:16px 0 8px}
|
|
560
|
+
.rk-aichat-preview-md h1{font-size:20px}.rk-aichat-preview-md h2{font-size:17px}.rk-aichat-preview-md h3{font-size:15px}
|
|
561
|
+
.rk-aichat-preview-md h4,.rk-aichat-preview-md h5,.rk-aichat-preview-md h6{font-size:13.5px}
|
|
562
|
+
.rk-aichat-preview-md p{margin:0 0 10px}
|
|
563
|
+
.rk-aichat-preview-md ul,.rk-aichat-preview-md ol{margin:0 0 10px;padding-left:22px}
|
|
564
|
+
.rk-aichat-preview-md li{margin:3px 0}
|
|
565
|
+
.rk-aichat-preview-md a{color:var(--rk-gold-700);font-weight:600;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}
|
|
566
|
+
.rk-aichat-preview-md strong{color:var(--rk-ink);font-weight:700}.rk-aichat-preview-md em{font-style:italic}
|
|
567
|
+
.rk-aichat-preview-md code{font-family:var(--rk-font-mono);font-size:12px;background:var(--rk-n-100);border-radius:5px;padding:1px 5px;color:var(--rk-n-700)}
|
|
568
|
+
.rk-aichat-preview-md blockquote{margin:0 0 10px;padding:2px 0 2px 12px;border-left:3px solid var(--rk-border-strong);color:var(--rk-text-muted)}
|
|
569
|
+
.rk-aichat-preview-md hr{border:0;border-top:1px solid var(--rk-border);margin:14px 0}
|
|
570
|
+
.rk-aichat-preview-code{margin:0 0 10px;font-family:var(--rk-font-mono);font-size:11.5px;line-height:1.5;color:var(--rk-n-700);
|
|
571
|
+
background:var(--rk-n-50);border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);padding:10px 12px;overflow:auto;white-space:pre}
|
|
572
|
+
.rk-aichat-preview-code code{background:transparent;padding:0}
|
|
573
|
+
.rk-aichat-preview-empty{padding:40px 16px;display:grid;gap:12px;justify-items:center;text-align:center;font-size:12.5px;color:var(--rk-text-muted)}
|
|
574
|
+
.rk-aichat-preview-dl{font-size:12.5px;font-weight:700;color:var(--rk-ink);cursor:pointer;background:var(--rk-gold-tint);
|
|
575
|
+
border:1px solid var(--rk-gold-600);border-radius:var(--rk-r-full);padding:7px 15px;text-decoration:none}
|
|
576
|
+
.rk-aichat-preview-dl:hover{background:var(--rk-gold-tint2)}
|
|
577
|
+
.rk-aichat-preview-nav{display:flex;align-items:center;justify-content:center;gap:14px;padding:11px 16px;
|
|
578
|
+
border-top:1px solid var(--rk-border);flex:none;background:var(--rk-surface)}
|
|
579
|
+
.rk-aichat-preview-nav[hidden]{display:none}
|
|
580
|
+
.rk-aichat-preview-btn{font-family:inherit;font-size:12px;font-weight:700;color:var(--rk-ink);cursor:pointer;
|
|
581
|
+
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:6px 14px;
|
|
582
|
+
transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
583
|
+
.rk-aichat-preview-btn:hover:not(:disabled){background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
584
|
+
.rk-aichat-preview-btn:disabled{opacity:.4;cursor:default}
|
|
585
|
+
.rk-aichat-preview-count{font-size:11.5px;font-weight:600;color:var(--rk-text-muted);font-variant-numeric:tabular-nums;min-width:52px;text-align:center}
|
|
586
|
+
/* modal header action buttons (copy / download / copy link) */
|
|
587
|
+
.rk-aichat-preview-act{flex:none;font-family:var(--rk-font-body);font-size:11.5px;font-weight:700;color:var(--rk-ink);cursor:pointer;
|
|
588
|
+
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:4px 11px;text-decoration:none;line-height:1.4;
|
|
589
|
+
transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
590
|
+
.rk-aichat-preview-act:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
591
|
+
|
|
592
|
+
/* ── markdown pipe tables (assistant messages + .md previews) ── */
|
|
593
|
+
.rk-md-tablewrap{max-width:100%;overflow-x:auto;margin:8px 0;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg)}
|
|
594
|
+
.rk-md-table{border-collapse:collapse;width:100%;font-size:12.5px;line-height:1.45;background:#fff}
|
|
595
|
+
.rk-md-table th,.rk-md-table td{padding:7px 11px;text-align:left;border-bottom:1px solid var(--rk-border);border-right:1px solid var(--rk-border);vertical-align:top;white-space:normal;overflow-wrap:anywhere}
|
|
596
|
+
.rk-md-table th:last-child,.rk-md-table td:last-child{border-right:0}
|
|
597
|
+
.rk-md-table tr:last-child td{border-bottom:0}
|
|
598
|
+
.rk-md-table thead th{background:var(--rk-n-50);font-weight:700;color:var(--rk-ink);white-space:nowrap}
|
|
599
|
+
.rk-md-table tbody tr:nth-child(even){background:var(--rk-n-50)}
|
|
600
|
+
.rk-md-table code{font-family:var(--rk-font-mono);font-size:11px;background:var(--rk-n-100);border-radius:4px;padding:1px 4px;color:var(--rk-n-700)}
|
|
601
|
+
/* inline code inside dock assistant messages */
|
|
602
|
+
.rk-aichat-msg.is-ai code,.rk-aichat-stream code{font-family:var(--rk-font-mono);font-size:12px;background:var(--rk-n-100);border-radius:5px;padding:1px 5px;color:var(--rk-n-700)}
|
|
603
|
+
|
|
604
|
+
/* ── finished job / tool output as file-preview thumbnails (in-transcript) ──
|
|
605
|
+
NOTE: distinct class from the LEFT files drawer (.rk-aichat-files). They used
|
|
606
|
+
to share the name, and this rule's display:grid clobbered the drawer's
|
|
607
|
+
display:none base — so the drawer could never be minimized (permanently open,
|
|
608
|
+
crushing the chat column). Keep these namespaced apart. */
|
|
609
|
+
.rk-aichat-filepreview{margin-top:10px;display:grid;gap:7px}
|
|
610
|
+
.rk-aichat-files-label{font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--rk-text-faint)}
|
|
611
|
+
.rk-aichat-files-grid{display:flex;flex-wrap:wrap;gap:9px}
|
|
612
|
+
.rk-aichat-file{display:flex;flex-direction:column;gap:6px;width:112px;padding:0;cursor:pointer;background:transparent;border:0;text-align:left;font-family:inherit}
|
|
613
|
+
.rk-aichat-file-thumb{position:relative;width:112px;height:84px;border-radius:var(--rk-r-lg);border:1px solid var(--rk-border);background:var(--rk-n-100);
|
|
614
|
+
overflow:hidden;display:grid;place-items:center;transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
615
|
+
.rk-aichat-file:hover .rk-aichat-file-thumb{border-color:var(--rk-gold-600);box-shadow:var(--rk-shadow-sm)}
|
|
616
|
+
.rk-aichat-file-thumb img,.rk-aichat-file-thumb video{width:100%;height:100%;object-fit:cover;display:block}
|
|
617
|
+
.rk-aichat-file-ic{font-family:var(--rk-font-mono);font-size:20px;font-weight:700;color:var(--rk-n-500)}
|
|
618
|
+
.rk-aichat-file-thumb--pdf{background:var(--rk-red-tint)}.rk-aichat-file-thumb--json{background:var(--rk-gold-tint)}
|
|
619
|
+
.rk-aichat-file-thumb--markdown,.rk-aichat-file-thumb--text{background:var(--rk-n-50)}.rk-aichat-file-thumb--audio{background:var(--rk-gold-tint2)}
|
|
620
|
+
.rk-aichat-file-play{position:absolute;inset:0;display:grid;place-items:center;font-size:22px;color:#fff;text-shadow:0 1px 6px rgba(0,0,0,.55);pointer-events:none}
|
|
621
|
+
.rk-aichat-file-name{font-size:11px;font-weight:600;color:var(--rk-n-600);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:112px}
|
|
622
|
+
|
|
623
|
+
/* create_video pipeline: live status row + clickable result links */
|
|
624
|
+
.rk-aichat-flow{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--rk-text-muted);padding:2px 2px}
|
|
625
|
+
.rk-aichat-flow[hidden]{display:none}
|
|
626
|
+
.rk-aichat-flow-label{line-height:1.4}
|
|
627
|
+
.rk-aichat-msg a,.rk-aichat-stream a{color:var(--rk-gold-700);font-weight:700;text-decoration:underline;
|
|
628
|
+
text-underline-offset:2px;overflow-wrap:anywhere}
|
|
629
|
+
.rk-aichat-msg a:hover,.rk-aichat-stream a:hover{color:var(--rk-ink)}
|
|
630
|
+
|
|
631
|
+
/* narrow: stack the left drawer above the chat so neither pane is cut off.
|
|
632
|
+
Cap the drawer to a fraction of the *viewport* (not 44% of the panel) and
|
|
633
|
+
floor the chat log so the composer is never crushed on short/landscape phones. */
|
|
634
|
+
@media(max-width:700px){
|
|
635
|
+
.rk-aichat.has-files,.rk-aichat.has-history,.rk-aichat.has-cloud{flex-direction:column}
|
|
636
|
+
.rk-aichat.has-files .rk-aichat-files,
|
|
637
|
+
.rk-aichat.has-history .rk-aichat-history,
|
|
638
|
+
.rk-aichat.has-cloud .rk-aichat-cloud{width:auto;max-height:min(44%,34vh);border-right:0;border-bottom:1px solid var(--rk-border)}
|
|
639
|
+
.rk-aichat-log{min-height:60px}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* touch devices have no hover, so controls that reveal on :hover would stay
|
|
643
|
+
invisible. Un-hide them on coarse pointers: the row kebab "…" (Copy path /
|
|
644
|
+
Delete), the multi-select checkbox, the folder Open chevron, the chat +Attach
|
|
645
|
+
button, and the JSON copy button. (Hover-only decorative states stay as-is.) */
|
|
646
|
+
@media(hover:none){
|
|
647
|
+
.rk-dir-kebab,.rk-dir-check,.rk-dir-open,.rk-aichat-fadd,.rk-aichat-json-copy{opacity:1}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/* brand + glass helpers (kept — reused by the sidebar brand + the login page) */
|
|
651
|
+
.rk-glass{background:rgba(255,255,255,.72);backdrop-filter:blur(var(--rk-glass-blur));
|
|
652
|
+
-webkit-backdrop-filter:blur(var(--rk-glass-blur));border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-nav)}
|
|
653
|
+
.rk-brand{display:inline-flex;align-items:center;gap:8px;font-family:var(--rk-font-display);
|
|
654
|
+
font-weight:800;font-size:20px;color:var(--rk-ink);letter-spacing:-.02em;flex:none}
|
|
655
|
+
.rk-brand-mark{display:inline-block;vertical-align:middle;width:26px;height:26px;border-radius:8px;
|
|
656
|
+
background:center/contain no-repeat url("/assets/logo-vidfarm.png");
|
|
657
|
+
font-size:0;color:transparent;flex:none;text-indent:-9999px;overflow:hidden}
|
|
658
|
+
.rk-brand-tag{font-family:var(--rk-font-body);font-size:11px;font-weight:600;color:var(--rk-gold-700);
|
|
659
|
+
background:var(--rk-gold-tint2);padding:2px 8px;border-radius:var(--rk-r-full);letter-spacing:.02em}
|
|
660
|
+
|
|
661
|
+
/* narrow: sidebar collapses to a horizontal top strip */
|
|
662
|
+
@media(max-width:900px){
|
|
663
|
+
.rk-sidebar{position:static;width:auto;flex-direction:row;align-items:center;gap:6px;
|
|
664
|
+
padding:10px 14px;border-right:0;border-bottom:1px solid var(--rk-border);overflow-x:auto}
|
|
665
|
+
.rk-sidebar-brand{padding:6px 8px;flex:none}
|
|
666
|
+
.rk-sidebar-nav{flex-direction:row;gap:4px;flex:1}
|
|
667
|
+
.rk-sidebar-sep{display:none}
|
|
668
|
+
.rk-sidebar-item{padding:8px 12px}
|
|
669
|
+
/* horizontal strip: FAB no longer collides with the foot, drop the reserve */
|
|
670
|
+
/* horizontal strip: kill the foot border + top offset so the account block
|
|
671
|
+
sits centered in the row instead of showing a stray hairline above it */
|
|
672
|
+
.rk-sidebar-account{padding:8px 13px 2px;border-top:0;margin-top:0}
|
|
673
|
+
/* strip/burger layout: the account block is no longer at the sidebar foot,
|
|
674
|
+
so drop the FAB back to its normal bottom-left resting spot */
|
|
675
|
+
.rk-signed-in .rk-fab{bottom:calc(16px + env(safe-area-inset-bottom))}
|
|
676
|
+
.rk-content{margin-left:0}
|
|
677
|
+
.rk-chat-left .rk-sidebar{visibility:visible}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/* hamburger button — hidden except on phones (see ≤640 block below) */
|
|
681
|
+
.rk-nav-burger{display:none;margin-left:auto;flex:none;width:40px;height:40px;border-radius:var(--rk-r-lg);
|
|
682
|
+
border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-700);cursor:pointer;place-items:center;
|
|
683
|
+
transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
684
|
+
.rk-nav-burger:hover{background:var(--rk-n-100);border-color:var(--rk-border-strong)}
|
|
685
|
+
|
|
686
|
+
/* phones: swap the horizontal-scroll strip for a real hamburger dropdown so all
|
|
687
|
+
nav items + the account block are reachable (the strip pushes them off-screen). */
|
|
688
|
+
@media(max-width:640px){
|
|
689
|
+
.rk-sidebar{flex-wrap:wrap;overflow:visible;position:sticky;top:0;z-index:60;gap:8px;
|
|
690
|
+
background:#fff;border-bottom:1px solid var(--rk-border)}
|
|
691
|
+
.rk-nav-burger{display:grid}
|
|
692
|
+
.rk-sidebar-nav{order:3;flex-basis:100%;flex-direction:column;gap:3px;display:none;padding-bottom:4px}
|
|
693
|
+
.rk-nav-toggle-cb:checked~.rk-sidebar-nav{display:flex}
|
|
694
|
+
.rk-sidebar-item{width:100%;padding:11px 13px}
|
|
695
|
+
/* account block + cloud line only inside the open dropdown (just noise in the collapsed bar) */
|
|
696
|
+
.rk-sidebar-account{display:none;order:4;flex-basis:100%;border-top:1px solid var(--rk-border);
|
|
697
|
+
margin-top:2px;padding-top:8px}
|
|
698
|
+
.rk-nav-toggle-cb:checked~.rk-sidebar-account{display:block}
|
|
699
|
+
.rk-sidebar-cloud{display:none;order:4;flex-basis:100%;margin-left:8px;width:calc(100% - 16px)}
|
|
700
|
+
.rk-nav-toggle-cb:checked~.rk-sidebar-cloud{display:flex}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/* buttons */
|
|
704
|
+
.rk-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:11px 18px;
|
|
705
|
+
border-radius:var(--rk-r-full);font-family:var(--rk-font-body);font-size:14px;font-weight:600;
|
|
706
|
+
border:1px solid transparent;cursor:pointer;white-space:nowrap;text-align:center;
|
|
707
|
+
transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
708
|
+
.rk-btn .rk-arrow{transition:transform var(--rk-dur) var(--rk-ease)}
|
|
709
|
+
.rk-btn:hover .rk-arrow{transform:translateX(3px)}
|
|
710
|
+
.rk-btn-ink{background:var(--rk-ink);color:#fff;box-shadow:var(--rk-shadow-sm)}
|
|
711
|
+
.rk-btn-ink:hover{background:#000;box-shadow:var(--rk-shadow-md);transform:translateY(-1px)}
|
|
712
|
+
.rk-btn-gold{background:var(--rk-gold-500);color:var(--rk-ink);font-weight:700;box-shadow:var(--rk-shadow-sm)}
|
|
713
|
+
.rk-btn-gold:hover{background:var(--rk-gold-700);box-shadow:0 6px 18px -6px rgba(252,185,0,.6);transform:translateY(-1px)}
|
|
714
|
+
/* interruptive Stop — the composer send button while a reply is streaming.
|
|
715
|
+
Fixed charcoal (not token-based) so it reads on both the light /chat page
|
|
716
|
+
and the dark editor dock. */
|
|
717
|
+
.rk-btn.is-stop{background:#2f2f2f;color:#fff;box-shadow:var(--rk-shadow-sm)}
|
|
718
|
+
.rk-btn.is-stop:hover{background:#1a1a1a;box-shadow:var(--rk-shadow-md);transform:none}
|
|
719
|
+
.rk-btn.is-stop svg{width:14px;height:14px}
|
|
720
|
+
.rk-btn-ghost{background:#fff;color:var(--rk-ink);border-color:var(--rk-border);box-shadow:var(--rk-shadow-xs)}
|
|
721
|
+
.rk-btn-ghost:hover{border-color:var(--rk-border-strong);box-shadow:var(--rk-shadow-sm)}
|
|
722
|
+
.rk-btn-danger{background:#fff;color:var(--rk-red);border-color:var(--rk-red-tint)}
|
|
723
|
+
.rk-btn-danger:hover{background:var(--rk-red-tint)}
|
|
724
|
+
.rk-btn-sm{padding:7px 13px;font-size:13px}
|
|
725
|
+
.rk-btn-lg{padding:14px 24px;font-size:15px}
|
|
726
|
+
.rk-btn-block{width:100%}
|
|
727
|
+
/* touch: guarantee a comfortable tap size on the small pill buttons */
|
|
728
|
+
@media(hover:none){.rk-btn-sm{min-height:40px}}
|
|
729
|
+
|
|
730
|
+
/* badges + pills */
|
|
731
|
+
.rk-badge{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;border-radius:var(--rk-r-full);
|
|
732
|
+
background:#fff;border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xs);
|
|
733
|
+
font-size:13px;font-weight:500;color:var(--rk-n-700);justify-self:start;width:fit-content}
|
|
734
|
+
.rk-pill{display:inline-flex;align-items:center;gap:6px;padding:4px 11px;border-radius:var(--rk-r-full);
|
|
735
|
+
font-size:12px;font-weight:600;background:var(--rk-n-100);color:var(--rk-n-600)}
|
|
736
|
+
.rk-pill-gold{background:var(--rk-gold-tint2);color:var(--rk-gold-700)}
|
|
737
|
+
.rk-pill-green{background:var(--rk-green-tint);color:#15803d}
|
|
738
|
+
.rk-pill-sky{background:var(--rk-sky-tint);color:#1d6fb8}
|
|
739
|
+
.rk-pill-violet{background:var(--rk-violet-tint);color:#7c3aed}
|
|
740
|
+
.rk-pill-red{background:var(--rk-red-tint);color:#b91c1c}
|
|
741
|
+
|
|
742
|
+
/* cards */
|
|
743
|
+
.rk-card{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);
|
|
744
|
+
padding:28px;box-shadow:var(--rk-shadow-card)}
|
|
745
|
+
.rk-card-hover{transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
746
|
+
.rk-card-hover:hover{transform:translateY(-2px);box-shadow:var(--rk-shadow-lg)}
|
|
747
|
+
/* subtle loading spinner injected over any <video> that is still buffering (see document.ts) */
|
|
748
|
+
.rk-vspin{position:absolute;top:50%;left:50%;width:24px;height:24px;margin:-12px 0 0 -12px;border-radius:50%;border:2.5px solid rgba(255,255,255,.28);border-top-color:rgba(255,255,255,.92);box-shadow:0 1px 5px rgba(0,0,0,.28);animation:rk-vspin-rot .72s linear infinite;pointer-events:none;opacity:0;transition:opacity .2s var(--rk-ease);z-index:4}
|
|
749
|
+
.rk-vspin.is-on{opacity:1}
|
|
750
|
+
@keyframes rk-vspin-rot{to{transform:rotate(360deg)}}
|
|
751
|
+
@media(prefers-reduced-motion:reduce){.rk-vspin{animation-duration:1.5s}}
|
|
752
|
+
.rk-card-pad-sm{padding:20px}
|
|
753
|
+
.rk-card h2{font-size:var(--rk-text-2xl);letter-spacing:-.02em}
|
|
754
|
+
.rk-card h3{font-size:var(--rk-text-lg);font-weight:700;font-family:var(--rk-font-display)}
|
|
755
|
+
.rk-card-head{display:grid;gap:6px;margin-bottom:20px}
|
|
756
|
+
.rk-eyebrow{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:600;
|
|
757
|
+
color:var(--rk-gold-700);text-transform:uppercase;letter-spacing:.04em;justify-self:start;width:fit-content}
|
|
758
|
+
|
|
759
|
+
/* icon tiles */
|
|
760
|
+
.rk-tile{width:40px;height:40px;border-radius:var(--rk-r-xl);display:grid;place-items:center;font-size:18px;flex:none}
|
|
761
|
+
.rk-tile-sm{width:32px;height:32px;font-size:15px;border-radius:var(--rk-r-lg)}
|
|
762
|
+
.rk-tile-lg{width:52px;height:52px;font-size:24px}
|
|
763
|
+
.rk-tile-gold{background:var(--rk-gold-tint2);color:var(--rk-gold-700)}
|
|
764
|
+
.rk-tile-sky{background:var(--rk-sky-tint);color:var(--rk-sky)}
|
|
765
|
+
.rk-tile-violet{background:var(--rk-violet-tint);color:var(--rk-violet)}
|
|
766
|
+
.rk-tile-green{background:var(--rk-green-tint);color:#16a34a}
|
|
767
|
+
.rk-tile-amber{background:var(--rk-gold-tint2);color:var(--rk-gold-700)}
|
|
768
|
+
.rk-tile-coral{background:var(--rk-coral-tint);color:var(--rk-coral)}
|
|
769
|
+
.rk-tile-ink{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
770
|
+
|
|
771
|
+
/* forms */
|
|
772
|
+
.rk-field{display:grid;gap:7px}
|
|
773
|
+
.rk-label{font-size:13px;font-weight:600;color:var(--rk-n-700)}
|
|
774
|
+
.rk-hint{font-size:12px;color:var(--rk-text-muted)}
|
|
775
|
+
.rk-input,.rk-textarea,.rk-select{width:100%;font-family:var(--rk-font-body);font-size:14px;
|
|
776
|
+
color:var(--rk-ink);background:#fff;border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-xl);
|
|
777
|
+
padding:11px 14px;transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
778
|
+
.rk-input::placeholder,.rk-textarea::placeholder{color:var(--rk-n-400)}
|
|
779
|
+
.rk-textarea{min-height:110px;resize:vertical;line-height:1.5}
|
|
780
|
+
.rk-select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' stroke='%23737373' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center;padding-right:36px}
|
|
781
|
+
.rk-input:focus,.rk-textarea:focus,.rk-select:focus{outline:none;border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
|
|
782
|
+
.rk-input-group{display:flex;align-items:stretch;gap:8px}
|
|
783
|
+
.rk-input-group .rk-input{flex:1}
|
|
784
|
+
|
|
785
|
+
/* notice / flash */
|
|
786
|
+
.rk-notice{display:flex;align-items:center;gap:10px;padding:13px 16px;border-radius:var(--rk-r-2xl);
|
|
787
|
+
font-size:14px;font-weight:500;border:1px solid transparent}
|
|
788
|
+
.rk-notice-ok{background:var(--rk-green-tint);color:#15803d;border-color:#bbf7d0}
|
|
789
|
+
.rk-notice-err{background:var(--rk-red-tint);color:#b91c1c;border-color:#fecaca}
|
|
790
|
+
|
|
791
|
+
/* two-column shell with sticky side rail (settings, etc.) */
|
|
792
|
+
.rk-shell{display:grid;grid-template-columns:248px minmax(0,1fr);gap:32px;align-items:start}
|
|
793
|
+
@media(max-width:900px){.rk-shell{grid-template-columns:1fr}}
|
|
794
|
+
.rk-side{position:sticky;top:88px;display:grid;gap:4px;background:#fff;border:1px solid var(--rk-border);
|
|
795
|
+
border-radius:var(--rk-r-2xl);padding:12px;box-shadow:var(--rk-shadow-card)}
|
|
796
|
+
@media(max-width:900px){.rk-side{position:static;grid-auto-flow:column;overflow:auto}}
|
|
797
|
+
.rk-side-item{display:flex;align-items:center;gap:10px;padding:11px 14px;border-radius:var(--rk-r-xl);
|
|
798
|
+
font-size:14px;font-weight:600;color:var(--rk-n-600);cursor:pointer;border:0;background:transparent;
|
|
799
|
+
box-sizing:border-box;text-decoration:none;
|
|
800
|
+
width:100%;text-align:left;white-space:nowrap;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
801
|
+
.rk-side-item:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
802
|
+
.rk-side-item.is-active{background:var(--rk-gold-tint);color:var(--rk-ink)}
|
|
803
|
+
.rk-side-item.is-active .rk-side-ic{background:var(--rk-gold-500)}
|
|
804
|
+
.rk-side-ic{width:26px;height:26px;border-radius:8px;background:var(--rk-n-100);display:grid;place-items:center;font-size:14px;flex:none}
|
|
805
|
+
|
|
806
|
+
/* simple list rows */
|
|
807
|
+
.rk-list{display:grid;gap:10px;grid-template-columns:minmax(0,1fr)}
|
|
808
|
+
.rk-item{display:flex;align-items:center;gap:14px;padding:14px 16px;background:#fff;min-width:0;
|
|
809
|
+
border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-xs)}
|
|
810
|
+
.rk-item-main{flex:1;min-width:0}
|
|
811
|
+
.rk-item-title{font-weight:600;color:var(--rk-ink);font-size:14px}
|
|
812
|
+
.rk-item-sub{font-size:13px;color:var(--rk-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
813
|
+
|
|
814
|
+
/* stat tiles */
|
|
815
|
+
.rk-stat{display:grid;gap:6px}
|
|
816
|
+
.rk-stat-label{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:var(--rk-text-muted)}
|
|
817
|
+
.rk-stat-num{font-family:var(--rk-font-display);font-size:var(--rk-text-4xl);font-weight:800;letter-spacing:-.02em;color:var(--rk-ink)}
|
|
818
|
+
|
|
819
|
+
/* footer — warm-dark charcoal */
|
|
820
|
+
.rk-footer{background:var(--rk-ink);color:var(--rk-n-400);padding:56px var(--rk-gutter) 40px;margin-top:64px}
|
|
821
|
+
.rk-footer-inner{width:min(var(--rk-container),100%);margin:0 auto;display:flex;flex-wrap:wrap;gap:24px;align-items:center;justify-content:space-between}
|
|
822
|
+
.rk-footer-brand{font-family:var(--rk-font-display);font-weight:800;font-size:22px;color:var(--rk-gold-500);display:inline-flex;align-items:center;gap:8px}
|
|
823
|
+
.rk-footer a{color:var(--rk-n-300)}.rk-footer a:hover{color:#fff}
|
|
824
|
+
.rk-footer-links{display:flex;gap:8px;flex-wrap:wrap}
|
|
825
|
+
.rk-trust-pill{display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:var(--rk-r-full);background:#ffffff0d;color:var(--rk-n-400);font-size:13px}
|
|
826
|
+
|
|
827
|
+
/* hero band (sunny sky) — sits flush at the top of the content column */
|
|
828
|
+
.rk-hero-band{position:relative;overflow:hidden;margin-top:0}
|
|
829
|
+
.rk-hero-art{position:absolute;inset:0;z-index:0;background:
|
|
830
|
+
radial-gradient(58% 46% at 78% 20%,rgba(255,241,204,.9),transparent 60%),
|
|
831
|
+
linear-gradient(180deg,#3f9fe0 0%,#6cc0f5 55%,#a9dbfb 100%)}
|
|
832
|
+
.rk-hero{position:relative;z-index:1;text-align:center;padding:clamp(56px,12vh,119px) var(--rk-gutter) clamp(44px,8vh,72px)}
|
|
833
|
+
|
|
834
|
+
:where(.rk-root a,.rk-root button,.rk-root input,.rk-root textarea,.rk-root select):focus-visible{outline:none;box-shadow:var(--rk-ring-gold)}
|
|
835
|
+
.rk-reveal[hidden]{display:none}
|
|
836
|
+
|
|
837
|
+
/* ── Desktop-agents nudge banner ─────────────────────────────────────────────
|
|
838
|
+
* A full-width alert pinned to the very top of the /editor and /chat surfaces,
|
|
839
|
+
* pointing to the "Desktop AI Agents" setup guide. Injected by the shared chrome
|
|
840
|
+
* script (RESKIN_CHROME_SCRIPT) only on those two paths; body.vf-has-topbanner
|
|
841
|
+
* offsets each surface's content so the bar never covers the app controls. */
|
|
842
|
+
.vf-topbanner{position:fixed;top:0;left:0;right:0;z-index:2147483000;height:var(--vf-topbanner-h,44px);
|
|
843
|
+
display:flex;align-items:center;justify-content:center;gap:12px;padding:0 44px 0 16px;box-sizing:border-box;
|
|
844
|
+
background:linear-gradient(90deg,#1b1305 0%,#3a2708 48%,#1b1305 100%);color:#fdf3d8;
|
|
845
|
+
font-family:var(--rk-font-body,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif);
|
|
846
|
+
font-size:13.5px;line-height:1.35;border-bottom:1px solid rgba(240,193,90,.32);
|
|
847
|
+
box-shadow:0 2px 14px rgba(0,0,0,.28);text-align:center}
|
|
848
|
+
.vf-topbanner-ico{flex:none;font-size:15px;filter:drop-shadow(0 1px 1px rgba(0,0,0,.4))}
|
|
849
|
+
.vf-topbanner-msg{max-width:900px}
|
|
850
|
+
.vf-topbanner-msg strong{color:#ffd977;font-weight:800}
|
|
851
|
+
.vf-topbanner-cta{flex:none;display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:999px;
|
|
852
|
+
background:linear-gradient(180deg,#ffcf5f,#f0b429);color:#221602;font-weight:800;font-size:12.5px;line-height:1;
|
|
853
|
+
text-decoration:none;white-space:nowrap;box-shadow:0 1px 0 rgba(255,255,255,.35) inset,0 2px 8px rgba(240,180,41,.35);
|
|
854
|
+
transition:transform .16s ease,box-shadow .16s ease}
|
|
855
|
+
.vf-topbanner-cta:hover{transform:translateY(-1px);box-shadow:0 1px 0 rgba(255,255,255,.45) inset,0 4px 12px rgba(240,180,41,.5)}
|
|
856
|
+
.vf-topbanner-close{position:absolute;right:10px;top:50%;transform:translateY(-50%);width:26px;height:26px;
|
|
857
|
+
display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:8px;cursor:pointer;
|
|
858
|
+
background:transparent;color:#e7cf9a;font-size:17px;line-height:1;transition:background .16s ease,color .16s ease}
|
|
859
|
+
.vf-topbanner-close:hover{background:rgba(255,255,255,.1);color:#fff}
|
|
860
|
+
@media(max-width:720px){
|
|
861
|
+
.vf-topbanner{--vf-topbanner-h:auto;min-height:56px;flex-wrap:wrap;gap:6px 12px;padding:8px 40px;font-size:12.5px}
|
|
862
|
+
.vf-topbanner-msg{flex:1 1 100%;order:1}
|
|
863
|
+
.vf-topbanner-ico{order:0}
|
|
864
|
+
.vf-topbanner-cta{order:2}
|
|
865
|
+
}
|
|
866
|
+
/* Per-surface content offset so nothing hides beneath the fixed bar. */
|
|
867
|
+
body.vf-has-topbanner[data-rk-editor-dock] #root{margin-top:var(--vf-topbanner-h,44px);height:calc(100dvh - var(--vf-topbanner-h,44px))}
|
|
868
|
+
body.vf-has-topbanner.rk-has-sidebar .rk-sidebar{top:var(--vf-topbanner-h,44px)}
|
|
869
|
+
body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-h,44px)}
|
|
870
|
+
|
|
871
|
+
.vf-skill-install{
|
|
872
|
+
--vf-skill-surface:var(--rk-surface,var(--surface,#fff));
|
|
873
|
+
--vf-skill-surface-strong:var(--rk-n-50,var(--n-50,#fbfbfb));
|
|
874
|
+
--vf-skill-border:var(--rk-border,var(--border,#ededed));
|
|
875
|
+
--vf-skill-border-strong:var(--rk-border-strong,var(--border-strong,#d4d4d4));
|
|
876
|
+
--vf-skill-text:var(--rk-ink,var(--ink,#171717));
|
|
877
|
+
--vf-skill-muted:var(--rk-text-muted,var(--text-muted,#737373));
|
|
878
|
+
--vf-skill-accent:var(--rk-gold-500,var(--gold-500,#ffc738));
|
|
879
|
+
--vf-skill-accent-deep:var(--rk-gold-700,var(--gold-700,#fcb900));
|
|
880
|
+
--vf-skill-shadow:var(--rk-shadow-nav,var(--shadow-nav,0 4px 24px -8px #00000026,0 1px 2px #0000000d));
|
|
881
|
+
position:relative;
|
|
882
|
+
display:inline-flex;
|
|
883
|
+
align-items:center;
|
|
884
|
+
border:1px solid var(--vf-skill-border);
|
|
885
|
+
border-radius:999px;
|
|
886
|
+
background:rgba(255,255,255,.86);
|
|
887
|
+
backdrop-filter:blur(16px);
|
|
888
|
+
-webkit-backdrop-filter:blur(16px);
|
|
889
|
+
box-shadow:0 4px 14px -10px rgba(15,23,42,.35);
|
|
890
|
+
color:var(--vf-skill-text);
|
|
891
|
+
}
|
|
892
|
+
.vf-skill-install-main,
|
|
893
|
+
.vf-skill-install-menu,
|
|
894
|
+
.vf-skill-install-copy{
|
|
895
|
+
border:0;
|
|
896
|
+
background:transparent;
|
|
897
|
+
color:inherit;
|
|
898
|
+
cursor:pointer;
|
|
899
|
+
}
|
|
900
|
+
.vf-skill-install-main{
|
|
901
|
+
display:inline-flex;
|
|
902
|
+
align-items:center;
|
|
903
|
+
gap:6px;
|
|
904
|
+
min-height:32px;
|
|
905
|
+
padding:0 9px 0 11px;
|
|
906
|
+
border-radius:999px 0 0 999px;
|
|
907
|
+
font:600 13px/1 var(--rk-font-body,var(--font-body,ui-sans-serif,system-ui,sans-serif));
|
|
908
|
+
letter-spacing:-.01em;
|
|
909
|
+
transition:background 180ms ease,color 180ms ease;
|
|
910
|
+
}
|
|
911
|
+
.vf-skill-install-main:hover,
|
|
912
|
+
.vf-skill-install-main:focus-visible{
|
|
913
|
+
background:#0000000a;
|
|
914
|
+
outline:none;
|
|
915
|
+
}
|
|
916
|
+
.vf-skill-install-main-icon,
|
|
917
|
+
.vf-skill-install-copy,
|
|
918
|
+
.vf-skill-install-link-icon{
|
|
919
|
+
display:inline-flex;
|
|
920
|
+
align-items:center;
|
|
921
|
+
justify-content:center;
|
|
922
|
+
}
|
|
923
|
+
.vf-skill-install-main-icon{
|
|
924
|
+
width:15px;
|
|
925
|
+
height:15px;
|
|
926
|
+
}
|
|
927
|
+
.vf-skill-install-main-icon svg,
|
|
928
|
+
.vf-skill-install-copy svg,
|
|
929
|
+
.vf-skill-install-link-icon svg,
|
|
930
|
+
.vf-skill-install-menu svg{
|
|
931
|
+
width:15px;
|
|
932
|
+
height:15px;
|
|
933
|
+
}
|
|
934
|
+
.vf-skill-install-main-label{
|
|
935
|
+
white-space:nowrap;
|
|
936
|
+
}
|
|
937
|
+
.vf-skill-install-menu{
|
|
938
|
+
display:inline-flex;
|
|
939
|
+
align-items:center;
|
|
940
|
+
justify-content:center;
|
|
941
|
+
width:32px;
|
|
942
|
+
min-height:32px;
|
|
943
|
+
border-left:1px solid var(--vf-skill-border);
|
|
944
|
+
border-radius:0 999px 999px 0;
|
|
945
|
+
transition:background 180ms ease,color 180ms ease;
|
|
946
|
+
}
|
|
947
|
+
.vf-skill-install-menu:hover,
|
|
948
|
+
.vf-skill-install-menu:focus-visible{
|
|
949
|
+
background:#0000000a;
|
|
950
|
+
outline:none;
|
|
951
|
+
}
|
|
952
|
+
.vf-skill-install-popover{
|
|
953
|
+
position:absolute;
|
|
954
|
+
right:0;
|
|
955
|
+
top:calc(100% + 10px);
|
|
956
|
+
z-index:40;
|
|
957
|
+
width:min(320px,calc(100vw - 28px));
|
|
958
|
+
display:grid;
|
|
959
|
+
gap:10px;
|
|
960
|
+
padding:14px;
|
|
961
|
+
border:1px solid var(--vf-skill-border);
|
|
962
|
+
border-radius:var(--rk-r-2xl,var(--r-2xl,16px));
|
|
963
|
+
background:var(--vf-skill-surface);
|
|
964
|
+
box-shadow:var(--vf-skill-shadow);
|
|
965
|
+
}
|
|
966
|
+
.vf-skill-install-popover[hidden]{
|
|
967
|
+
display:none;
|
|
968
|
+
}
|
|
969
|
+
.vf-skill-install-popover-kicker{
|
|
970
|
+
font:600 12px/1.2 var(--rk-font-body,var(--font-body,ui-sans-serif,system-ui,sans-serif));
|
|
971
|
+
color:var(--vf-skill-muted);
|
|
972
|
+
letter-spacing:.02em;
|
|
973
|
+
text-transform:uppercase;
|
|
974
|
+
}
|
|
975
|
+
.vf-skill-install-command{
|
|
976
|
+
display:grid;
|
|
977
|
+
grid-template-columns:minmax(0,1fr) 44px;
|
|
978
|
+
gap:8px;
|
|
979
|
+
align-items:stretch;
|
|
980
|
+
}
|
|
981
|
+
.vf-skill-install-input{
|
|
982
|
+
width:100%;
|
|
983
|
+
min-width:0;
|
|
984
|
+
height:36px;
|
|
985
|
+
padding:0 12px;
|
|
986
|
+
border:1px solid var(--vf-skill-border);
|
|
987
|
+
border-radius:var(--rk-r-xl,var(--r-xl,12px));
|
|
988
|
+
background:var(--vf-skill-surface-strong);
|
|
989
|
+
color:var(--vf-skill-text);
|
|
990
|
+
font:500 13px/1.2 var(--rk-font-mono,var(--font-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace));
|
|
991
|
+
letter-spacing:-.02em;
|
|
992
|
+
}
|
|
993
|
+
.vf-skill-install-copy{
|
|
994
|
+
height:36px;
|
|
995
|
+
border:1px solid color-mix(in srgb,var(--vf-skill-accent) 55%, white);
|
|
996
|
+
border-radius:var(--rk-r-xl,var(--r-xl,12px));
|
|
997
|
+
background:var(--rk-gold-tint,var(--gold-tint,#fff5e4));
|
|
998
|
+
color:var(--vf-skill-accent-deep);
|
|
999
|
+
transition:transform 180ms ease,background 180ms ease,border-color 180ms ease,color 180ms ease;
|
|
1000
|
+
}
|
|
1001
|
+
.vf-skill-install-copy:hover,
|
|
1002
|
+
.vf-skill-install-copy:focus-visible{
|
|
1003
|
+
background:var(--vf-skill-accent);
|
|
1004
|
+
border-color:var(--vf-skill-accent);
|
|
1005
|
+
color:var(--vf-skill-text);
|
|
1006
|
+
outline:none;
|
|
1007
|
+
transform:translateY(-1px);
|
|
1008
|
+
}
|
|
1009
|
+
.vf-skill-install-copy[data-copied="true"]{
|
|
1010
|
+
background:color-mix(in srgb,#22c55e 14%, white);
|
|
1011
|
+
border-color:color-mix(in srgb,#22c55e 36%, white);
|
|
1012
|
+
color:#15803d;
|
|
1013
|
+
}
|
|
1014
|
+
.vf-skill-install-link{
|
|
1015
|
+
display:inline-flex;
|
|
1016
|
+
align-items:center;
|
|
1017
|
+
gap:7px;
|
|
1018
|
+
width:max-content;
|
|
1019
|
+
color:var(--vf-skill-accent-deep);
|
|
1020
|
+
font:600 13px/1.2 var(--rk-font-body,var(--font-body,ui-sans-serif,system-ui,sans-serif));
|
|
1021
|
+
}
|
|
1022
|
+
.vf-skill-install-link-icon{
|
|
1023
|
+
width:14px;
|
|
1024
|
+
height:14px;
|
|
1025
|
+
}
|
|
1026
|
+
.vf-skill-install-link-icon svg{
|
|
1027
|
+
width:14px;
|
|
1028
|
+
height:14px;
|
|
1029
|
+
}
|
|
1030
|
+
.vf-skill-install-link:hover{
|
|
1031
|
+
text-decoration:underline;
|
|
1032
|
+
}
|
|
1033
|
+
.vf-skill-install-nav{
|
|
1034
|
+
flex:none;
|
|
1035
|
+
}
|
|
1036
|
+
.vf-skill-install-panel{
|
|
1037
|
+
align-self:flex-start;
|
|
1038
|
+
}
|
|
1039
|
+
.vf-skill-install-panel .vf-skill-install-main{
|
|
1040
|
+
min-height:32px;
|
|
1041
|
+
}
|
|
1042
|
+
@media(max-width:560px){
|
|
1043
|
+
.vf-skill-install-main-label{
|
|
1044
|
+
font-size:13px;
|
|
1045
|
+
}
|
|
1046
|
+
.vf-skill-install-popover{
|
|
1047
|
+
width:min(300px,calc(100vw - 18px));
|
|
1048
|
+
right:-4px;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
.rk-sidebar-skill{margin:4px 6px 10px;display:flex}
|
|
1053
|
+
.rk-sidebar-skill .vf-skill-install{width:100%;background:transparent;box-shadow:none;border-color:var(--rk-border);backdrop-filter:none;-webkit-backdrop-filter:none}
|
|
1054
|
+
.rk-sidebar-skill .vf-skill-install-main{flex:1;justify-content:flex-start}
|
|
1055
|
+
.rk-sidebar-skill .vf-skill-install-menu{border-left-color:var(--rk-border)}
|
|
1056
|
+
/* The sidebar rail is a fixed 248px column with overflow-y:auto (which forces
|
|
1057
|
+
overflow-x to clip too), so the default right-anchored 320px popover spills off
|
|
1058
|
+
the left edge and gets cut. Anchor it left and fit it to the rail width. */
|
|
1059
|
+
.rk-sidebar-skill .vf-skill-install-popover{left:0;right:auto;width:100%;min-width:0}
|
|
1060
|
+
|
|
1061
|
+
/* Marketplace — the sidebar's PRIMARY action, pinned directly under the brand and
|
|
1062
|
+
above the SKILL.md pill. Gold "joy" accent so it reads as the headline CTA of
|
|
1063
|
+
the whole rail; the little live dot hints at an active exchange. */
|
|
1064
|
+
.rk-sidebar-market{display:flex;align-items:center;gap:9px;margin:2px 6px 8px;padding:11px 12px;border-radius:12px;
|
|
1065
|
+
background:var(--rk-gold-500);color:var(--rk-ink);font-family:var(--rk-font-display);font-weight:800;
|
|
1066
|
+
font-size:.95rem;text-decoration:none;box-shadow:var(--rk-shadow-sm);transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
1067
|
+
.rk-sidebar-market[hidden]{display:none}
|
|
1068
|
+
.rk-sidebar-market:hover{transform:translateY(-1px);box-shadow:var(--rk-shadow-md)}
|
|
1069
|
+
.rk-sidebar-market.is-active{outline:2px solid var(--rk-gold-700);outline-offset:1px}
|
|
1070
|
+
.rk-sidebar-market svg{width:17px;height:17px;flex:none}
|
|
1071
|
+
.rk-sidebar-market-dot{margin-left:auto;width:8px;height:8px;border-radius:50%;background:var(--rk-green);
|
|
1072
|
+
box-shadow:0 0 0 3px #ffffff66;animation:rk-market-pulse 1.6s ease-in-out infinite;flex:none}
|
|
1073
|
+
@keyframes rk-market-pulse{0%,100%{opacity:1}50%{opacity:.45}}
|
|
1074
|
+
@media (prefers-reduced-motion:reduce){.rk-sidebar-market-dot{animation:none}}
|
|
1075
|
+
|
|
1076
|
+
/* page-scoped */
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
.rk-ip-tabs{display:inline-flex;gap:3px;padding:3px;background:var(--rk-n-100);border:1px solid var(--rk-border);border-radius:999px;margin-bottom:12px}
|
|
1080
|
+
.rk-ip-tab{display:inline-flex;align-items:center;gap:6px;padding:6px 15px;border-radius:999px;font-size:13px;font-weight:700;color:var(--rk-text-muted);text-decoration:none;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
1081
|
+
.rk-ip-tab:hover{color:var(--rk-ink)}
|
|
1082
|
+
.rk-ip-tab.is-active{background:var(--rk-surface);color:var(--rk-ink);box-shadow:var(--rk-shadow-sm)}
|
|
1083
|
+
.rk-ip-tab svg{flex:none}
|
|
1084
|
+
|
|
1085
|
+
.rk-inpaint-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter)}
|
|
1086
|
+
.rk-inpaint-workbench{display:grid;grid-template-columns:320px minmax(0,1fr) 320px;gap:20px;align-items:start}
|
|
1087
|
+
@media(max-width:1200px){.rk-inpaint-workbench{grid-template-columns:320px minmax(0,1fr)}.rk-inpaint-output{grid-column:1 / -1;grid-row:2}}
|
|
1088
|
+
@media(max-width:820px){.rk-inpaint-workbench{grid-template-columns:1fr}.rk-inpaint-output,.rk-inpaint-stage,.rk-inpaint-panel{grid-column:auto}.rk-inpaint-stage{order:-1}}
|
|
1089
|
+
.rk-inpaint-panel,.rk-inpaint-stage,.rk-inpaint-output{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card)}
|
|
1090
|
+
.rk-inpaint-panel{padding:18px;display:grid;gap:16px;align-content:start}
|
|
1091
|
+
.rk-inpaint-block{display:grid;gap:9px}
|
|
1092
|
+
.rk-inpaint-h2{font-family:var(--rk-font-display);font-size:var(--rk-text-lg);font-weight:800;color:var(--rk-ink);letter-spacing:-.01em}
|
|
1093
|
+
.rk-inpaint-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}
|
|
1094
|
+
.rk-inpaint-head-text{min-width:0;flex:1 1 320px}
|
|
1095
|
+
.rk-inpaint-toolbar{flex:none;display:flex;align-items:center;gap:8px;padding-top:2px}
|
|
1096
|
+
.rk-inpaint-startnew{gap:6px}
|
|
1097
|
+
.rk-ip-iconbtn{display:grid;place-items:center;width:34px;height:34px;border-radius:50%;border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-700);cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
1098
|
+
.rk-ip-iconbtn:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1099
|
+
.rk-ip-toolbtns{display:flex;align-items:center;gap:6px}
|
|
1100
|
+
|
|
1101
|
+
/* start/end frames */
|
|
1102
|
+
.rk-ipv-frames{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
|
1103
|
+
.rk-ipv-frame{display:grid;gap:6px;justify-items:center}
|
|
1104
|
+
.rk-ipv-frame-thumb{position:relative;width:100%;aspect-ratio:16/10;padding:0;border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-xl);background:var(--rk-n-50);color:var(--rk-text-faint);cursor:pointer;overflow:hidden;display:grid;place-items:center}
|
|
1105
|
+
.rk-ipv-frame-thumb:hover{border-color:var(--rk-gold-600)}
|
|
1106
|
+
.rk-ipv-frame-ph{font-size:24px;line-height:1}
|
|
1107
|
+
.rk-ipv-frame-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
1108
|
+
.rk-ipv-frame-name{font-size:12px;font-weight:700;color:var(--rk-ink)}
|
|
1109
|
+
.rk-ipv-frame-actions{display:flex;gap:6px}
|
|
1110
|
+
.rk-ipv-frame-btn{padding:3px 8px;font:inherit;font-size:11px;font-weight:700;color:var(--rk-n-700);background:#fff;border:1px solid var(--rk-border);border-radius:999px;cursor:pointer}
|
|
1111
|
+
.rk-ipv-frame-btn:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1112
|
+
|
|
1113
|
+
/* prompt field + attachments */
|
|
1114
|
+
.rk-ip-promptfield{position:relative}
|
|
1115
|
+
.rk-ip-global{min-height:74px;width:100%;padding-right:44px}
|
|
1116
|
+
.rk-ip-attach{position:absolute;right:7px;bottom:7px;display:grid;place-items:center;width:30px;height:30px;border-radius:50%;cursor:pointer;border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600)}
|
|
1117
|
+
.rk-ip-attach:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1118
|
+
.rk-ip-attach-chips{display:flex;flex-wrap:wrap;gap:6px}
|
|
1119
|
+
.editor-inpaint-attachment-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 4px 3px 6px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-50);font-size:11.5px;color:var(--rk-n-700);max-width:170px}
|
|
1120
|
+
.editor-inpaint-attachment-chip img{width:22px;height:22px;border-radius:4px;object-fit:cover;flex:none}
|
|
1121
|
+
.editor-inpaint-attachment-chip span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1122
|
+
.editor-inpaint-attachment-chip button{flex:none;border:0;background:transparent;cursor:pointer;color:var(--rk-text-faint);font-size:15px;line-height:1;padding:0 2px}
|
|
1123
|
+
.rk-ip-setting-note{font-weight:600;color:var(--rk-text-faint)}
|
|
1124
|
+
|
|
1125
|
+
/* mask color picker + timed mask rows */
|
|
1126
|
+
.rk-inpaint-brushrow{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
1127
|
+
.rk-ip-colorpicker{width:42px;height:32px;padding:2px;border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);background:#fff;cursor:pointer}
|
|
1128
|
+
.rk-inpaint-range{width:100%}
|
|
1129
|
+
.rk-ipv-masks{display:grid;gap:8px}
|
|
1130
|
+
.rk-ipv-mask{position:relative;display:grid;grid-template-columns:28px minmax(0,1fr);gap:9px;padding:8px;border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);background:var(--rk-n-50)}
|
|
1131
|
+
.rk-ipv-mask[data-active="true"]{border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
|
|
1132
|
+
.rk-ipv-mask-dot{width:24px;height:24px;padding:0;border:2px solid #fff;border-radius:50%;cursor:pointer;background:var(--ipv-c,#ff2d55);box-shadow:0 0 0 1px var(--rk-border-strong)}
|
|
1133
|
+
.rk-ipv-mask[data-active="true"] .rk-ipv-mask-dot{box-shadow:0 0 0 2px var(--rk-gold-700)}
|
|
1134
|
+
.rk-ipv-mask-body{min-width:0;display:grid;gap:6px}
|
|
1135
|
+
.rk-ipv-mask-row{display:flex;align-items:center;gap:6px}
|
|
1136
|
+
.rk-ipv-mask-time{flex:1;min-width:0;display:flex;align-items:center;gap:6px;padding:4px 9px;font:inherit;font-size:12px;font-weight:800;font-variant-numeric:tabular-nums;color:var(--rk-ink);background:#fff;border:1px solid var(--rk-border);border-radius:999px;cursor:pointer}
|
|
1137
|
+
.rk-ipv-mask-time:hover{border-color:var(--rk-gold-600);color:var(--rk-gold-700)}
|
|
1138
|
+
.rk-ipv-mask-time svg{flex:none;color:var(--rk-text-muted)}
|
|
1139
|
+
.rk-ipv-mask-mini{display:grid;place-items:center;width:24px;height:24px;flex:none;border:1px solid var(--rk-border);border-radius:50%;background:#fff;color:var(--rk-n-600);cursor:pointer}
|
|
1140
|
+
.rk-ipv-mask-mini:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1141
|
+
.rk-ipv-mask-mini.is-danger:hover{color:#b42318;border-color:#e6a8a0;background:#fff}
|
|
1142
|
+
.rk-ipv-mask-prompt{width:100%;min-height:44px;resize:vertical;padding:8px 10px;font:inherit;font-size:12.5px;line-height:1.4;color:var(--rk-ink);border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);background:#fff;outline:0}
|
|
1143
|
+
.rk-ipv-mask-prompt:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
1144
|
+
.rk-ipv-masks-empty{font-size:12px;color:var(--rk-text-faint);padding:8px 2px;line-height:1.4}
|
|
1145
|
+
.rk-ip-addcolor{justify-self:start}
|
|
1146
|
+
|
|
1147
|
+
/* provider select */
|
|
1148
|
+
.rk-ip-provider{display:none;gap:6px}
|
|
1149
|
+
.rk-ip-provider[data-visible="true"]{display:grid}
|
|
1150
|
+
.rk-ip-setting{display:grid;gap:6px}
|
|
1151
|
+
.rk-ipv-audio-row{grid-template-columns:1fr auto;align-items:center}
|
|
1152
|
+
.rk-inpaint-select{width:100%}
|
|
1153
|
+
.rk-ip-input{width:100%;padding:8px 10px;font:inherit;font-size:13px;color:var(--rk-ink);border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);background:#fff;outline:0}
|
|
1154
|
+
.rk-ip-input:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
1155
|
+
|
|
1156
|
+
/* generate + status */
|
|
1157
|
+
.rk-inpaint-generate{width:100%;justify-content:center;gap:8px;padding:13px}
|
|
1158
|
+
.rk-inpaint-generate[data-loading="true"]{opacity:.7;cursor:wait}
|
|
1159
|
+
.rk-inpaint-status{font-size:12.5px;font-weight:600;line-height:1.5;min-height:18px;color:var(--rk-text-muted)}
|
|
1160
|
+
.rk-inpaint-status[data-tone="error"]{color:#b42318}
|
|
1161
|
+
.rk-inpaint-status[data-tone="success"]{color:#1f7a44}
|
|
1162
|
+
|
|
1163
|
+
/* center video stage */
|
|
1164
|
+
.rk-inpaint-stage{min-height:420px;display:grid;place-items:center;padding:20px;overflow:hidden}
|
|
1165
|
+
.rk-ipv-stage{align-content:center}
|
|
1166
|
+
.rk-inpaint-empty{display:grid;justify-items:center;gap:8px;text-align:center;color:var(--rk-text-muted);padding:30px 20px}
|
|
1167
|
+
.rk-inpaint-empty[hidden]{display:none}
|
|
1168
|
+
.rk-inpaint-empty-ic{color:var(--rk-n-400)}
|
|
1169
|
+
.rk-inpaint-empty-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);color:var(--rk-ink)}
|
|
1170
|
+
.rk-inpaint-empty-sub{font-size:13px}
|
|
1171
|
+
.rk-ipv-upload{margin-top:6px;gap:6px}
|
|
1172
|
+
.rk-ipv-player{width:100%;display:grid;gap:12px;justify-items:center}
|
|
1173
|
+
.rk-ipv-player[hidden]{display:none}
|
|
1174
|
+
.rk-ipv-canvaswrap{position:relative;display:inline-block;line-height:0;max-width:100%;border-radius:var(--rk-r-2xl);overflow:hidden;box-shadow:var(--rk-shadow-md);
|
|
1175
|
+
--inpaint-brush-cursor-size:70px;--inpaint-brush-cursor-x:0px;--inpaint-brush-cursor-y:0px;background:#000}
|
|
1176
|
+
.rk-ipv-video{display:block;max-width:100%;max-height:min(58vh,560px);width:auto;height:auto;object-fit:contain;user-select:none}
|
|
1177
|
+
.rk-ipv-canvas{position:absolute;inset:0;width:100%;height:100%;opacity:.82;cursor:crosshair;touch-action:none}
|
|
1178
|
+
.editor-inpaint-brush-cursor{position:absolute;z-index:2;left:0;top:0;width:var(--inpaint-brush-cursor-size);height:var(--inpaint-brush-cursor-size);
|
|
1179
|
+
border:2px solid rgba(255,255,255,.95);border-radius:50%;box-shadow:0 0 0 1px rgba(28,25,23,.7),0 0 18px rgba(28,25,23,.25);opacity:0;pointer-events:none;
|
|
1180
|
+
transform:translate(var(--inpaint-brush-cursor-x),var(--inpaint-brush-cursor-y)) translate(-50%,-50%)}
|
|
1181
|
+
.editor-inpaint-brush-cursor[data-visible="true"]{opacity:1}
|
|
1182
|
+
.rk-ipv-controls{width:100%;max-width:640px;display:flex;align-items:center;gap:10px}
|
|
1183
|
+
.rk-ipv-playpause{flex:none;display:grid;place-items:center;width:38px;height:38px;border-radius:50%;border:0;background:var(--rk-ink);color:#fff;cursor:pointer}
|
|
1184
|
+
.rk-ipv-playpause:hover{background:var(--rk-n-800,#333)}
|
|
1185
|
+
.rk-ipv-scrub{flex:1;min-width:0;accent-color:var(--rk-gold-600)}
|
|
1186
|
+
.rk-ipv-timecode{flex:none;display:flex;align-items:center;gap:6px}
|
|
1187
|
+
.rk-ipv-timecode-input{width:96px;padding:5px 8px;font:inherit;font-size:12.5px;font-weight:800;font-variant-numeric:tabular-nums;text-align:center;color:var(--rk-ink);border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);background:#fff;outline:0}
|
|
1188
|
+
.rk-ipv-timecode-input:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
1189
|
+
.rk-ipv-timecode-copy{width:30px;height:30px}
|
|
1190
|
+
.rk-ipv-duration{font-size:11.5px;font-weight:700;color:var(--rk-text-faint);font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
1191
|
+
|
|
1192
|
+
/* text-to-video composer (empty state) */
|
|
1193
|
+
.rk-ip-gen{width:100%;max-width:460px;margin-top:16px;display:grid;gap:10px;justify-items:stretch}
|
|
1194
|
+
.rk-ip-gen-or{position:relative;font-size:11.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--rk-text-faint)}
|
|
1195
|
+
.rk-ip-gen-or::before,.rk-ip-gen-or::after{content:"";position:absolute;top:50%;width:calc(50% - 26px);height:1px;background:var(--rk-border)}
|
|
1196
|
+
.rk-ip-gen-or::before{left:0}.rk-ip-gen-or::after{right:0}
|
|
1197
|
+
.rk-ip-gen-box{background:#fff;border:1.5px solid var(--rk-border-strong);border-radius:var(--rk-r-2xl);padding:10px 10px 8px;display:grid;gap:8px;box-shadow:var(--rk-shadow-sm);text-align:left}
|
|
1198
|
+
.rk-ip-gen-box:focus-within{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
1199
|
+
.rk-ip-gen.is-busy .rk-ip-gen-box{opacity:.75}
|
|
1200
|
+
.rk-ip-gen-input{width:100%;border:0;outline:0;resize:none;background:transparent;font:inherit;font-size:14px;line-height:1.45;color:var(--rk-ink);min-height:44px;padding:2px 4px}
|
|
1201
|
+
.rk-ip-gen-input::placeholder{color:var(--rk-text-faint)}
|
|
1202
|
+
.rk-ip-gen-refs{display:flex;flex-wrap:wrap;gap:6px}
|
|
1203
|
+
.rk-ip-gen-refs[hidden]{display:none}
|
|
1204
|
+
.rk-ip-gen-ref{display:inline-flex;align-items:center;gap:6px;padding:3px 4px 3px 6px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-50);font-size:11.5px;color:var(--rk-n-700);max-width:180px}
|
|
1205
|
+
.rk-ip-gen-ref img{width:22px;height:22px;border-radius:4px;object-fit:cover;flex:none}
|
|
1206
|
+
.rk-ip-gen-ref span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1207
|
+
.rk-ip-gen-ref button{flex:none;border:0;background:transparent;cursor:pointer;color:var(--rk-text-faint);font-size:15px;line-height:1;padding:0 2px}
|
|
1208
|
+
.rk-ip-gen-bar{display:flex;align-items:center;gap:8px}
|
|
1209
|
+
.rk-ip-gen-hint{flex:1;font-size:11.5px;color:var(--rk-text-faint);line-height:1.35}
|
|
1210
|
+
.rk-ip-gen-clip,.rk-ip-gen-send{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;cursor:pointer}
|
|
1211
|
+
.rk-ip-gen-clip{border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600)}
|
|
1212
|
+
.rk-ip-gen-clip:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1213
|
+
.rk-ip-gen-send{border:0;background:var(--rk-gold-500,var(--rk-gold-600));color:var(--rk-ink)}
|
|
1214
|
+
.rk-ip-gen-send:hover:not(:disabled){background:var(--rk-gold-600)}
|
|
1215
|
+
.rk-ip-gen-send:disabled{opacity:.5;cursor:default}
|
|
1216
|
+
.rk-ip-gen-send.is-busy{background:var(--rk-gold-tint);pointer-events:none}
|
|
1217
|
+
.rk-ip-gen-spin{width:16px;height:16px;border-radius:50%;border:2.5px solid var(--rk-gold-tint2,rgba(252,185,0,.35));border-top-color:var(--rk-gold-700);animation:rk-ip-spin .8s linear infinite}
|
|
1218
|
+
.rk-ip-gen-status{font-size:12px;font-weight:600;min-height:16px;color:var(--rk-text-muted);text-align:left;padding-left:2px}
|
|
1219
|
+
.rk-ip-gen-status[data-tone="error"]{color:#b42318}
|
|
1220
|
+
.rk-ip-gen-status[data-tone="success"]{color:#1f7a44}
|
|
1221
|
+
|
|
1222
|
+
/* file-directory pop panel (reference picker) */
|
|
1223
|
+
.rk-ip-filemodal{position:fixed;inset:0;z-index:60;background:rgba(28,25,23,.44);backdrop-filter:blur(2px);display:grid;place-items:center;padding:20px}
|
|
1224
|
+
.rk-ip-filemodal-panel{width:min(560px,100%);max-height:min(80vh,680px);display:flex;flex-direction:column;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card));overflow:hidden}
|
|
1225
|
+
.rk-ip-filemodal-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;border-bottom:1px solid var(--rk-border)}
|
|
1226
|
+
.rk-ip-filemodal-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-md,15px);color:var(--rk-ink)}
|
|
1227
|
+
.rk-ip-filemodal-close{border:0;background:transparent;font-size:22px;line-height:1;color:var(--rk-text-muted);cursor:pointer;padding:0 4px}
|
|
1228
|
+
.rk-ip-filemodal-mount{padding:12px 14px;overflow:auto;flex:1;min-height:0}
|
|
1229
|
+
|
|
1230
|
+
/* right: result + history */
|
|
1231
|
+
.rk-inpaint-output{padding:18px;display:grid;gap:18px;align-content:start}
|
|
1232
|
+
.rk-inpaint-result[hidden]{display:none}
|
|
1233
|
+
.rk-inpaint-result-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
|
1234
|
+
.rk-ipv-result-video{display:block;width:100%;border-radius:var(--rk-r-2xl);background:#000;box-shadow:var(--rk-shadow-sm)}
|
|
1235
|
+
.rk-inpaint-history-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
|
1236
|
+
.rk-ip-histtools{display:flex;align-items:center;gap:8px}
|
|
1237
|
+
.rk-inpaint-historylist{display:grid;gap:10px}
|
|
1238
|
+
.custom-inpaint-history-empty{font-size:12.5px;color:var(--rk-text-faint);padding:14px 4px;text-align:center;line-height:1.4}
|
|
1239
|
+
.custom-inpaint-history-item{position:relative;display:grid;grid-template-columns:54px minmax(0,1fr);align-items:center;gap:10px;padding:8px;border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);background:var(--rk-n-50)}
|
|
1240
|
+
.custom-inpaint-history-thumb-button{position:relative;width:54px;height:54px;display:grid;place-items:center;padding:0;border:0;border-radius:var(--rk-r-lg);background:var(--rk-n-200);cursor:zoom-in;overflow:hidden}
|
|
1241
|
+
.custom-inpaint-history-thumb{width:54px;height:54px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);object-fit:cover;background:#000}
|
|
1242
|
+
.custom-inpaint-history-thumb-badge{position:absolute;inset:0;display:grid;place-items:center;color:#fff;text-shadow:0 1px 4px rgba(0,0,0,.6);pointer-events:none}
|
|
1243
|
+
.custom-inpaint-history-generating{width:54px;height:54px;display:grid;place-items:center;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-100)}
|
|
1244
|
+
.custom-inpaint-history-generating::before{content:"";width:22px;height:22px;border-radius:50%;border:2.5px solid var(--rk-gold-tint2,rgba(252,185,0,.35));border-top-color:var(--rk-gold-700);animation:rk-ip-spin .8s linear infinite}
|
|
1245
|
+
.custom-inpaint-history-meta{min-width:0;display:grid;gap:5px}
|
|
1246
|
+
.custom-inpaint-history-title{min-width:0;color:var(--rk-ink);font-size:12.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1247
|
+
.custom-inpaint-history-subtitle{min-width:0;color:var(--rk-text-muted);font-size:11.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1248
|
+
.custom-inpaint-history-actions{display:flex;align-items:center;gap:5px}
|
|
1249
|
+
.custom-inpaint-history-icon{width:26px;height:26px;display:inline-grid;place-items:center;padding:0;border:1px solid var(--rk-border);border-radius:50%;background:#fff;color:var(--rk-n-700);cursor:pointer;text-decoration:none}
|
|
1250
|
+
.custom-inpaint-history-icon:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
1251
|
+
.custom-inpaint-history-icon svg{width:13px;height:13px}
|
|
1252
|
+
.custom-inpaint-history-check{min-height:26px;padding:0 10px;border:1px solid var(--rk-border);border-radius:999px;background:#fff;color:var(--rk-ink);font:inherit;font-size:11.5px;font-weight:800;cursor:pointer}
|
|
1253
|
+
.custom-inpaint-history-check[disabled]{cursor:wait;opacity:.6}
|
|
1254
|
+
.custom-inpaint-history-popconfirm{position:absolute;right:6px;top:6px;z-index:6;width:160px;display:grid;gap:7px;padding:9px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-surface);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card))}
|
|
1255
|
+
.custom-inpaint-history-popconfirm[hidden]{display:none}
|
|
1256
|
+
.custom-inpaint-history-popconfirm p{margin:0;color:var(--rk-ink);font-size:12px;font-weight:700;line-height:1.3}
|
|
1257
|
+
.custom-inpaint-history-popconfirm-actions{display:flex;gap:6px}
|
|
1258
|
+
.custom-inpaint-history-popconfirm-actions button{flex:1 1 0;min-height:26px;padding:0 8px;border-radius:var(--rk-r-md,8px);border:1px solid var(--rk-border);background:#fff;color:var(--rk-ink);font:inherit;font-size:11.5px;font-weight:700;cursor:pointer}
|
|
1259
|
+
.custom-inpaint-history-popconfirm-actions button[data-danger="true"]{border-color:#e6a8a0;color:#b42318}
|
|
1260
|
+
|
|
1261
|
+
/* lightbox */
|
|
1262
|
+
.rk-ip-lightbox{position:fixed;inset:0;z-index:80;display:none;place-items:center;padding:22px;background:rgba(28,25,23,.66);backdrop-filter:blur(8px)}
|
|
1263
|
+
.rk-ip-lightbox[data-open="true"]{display:grid}
|
|
1264
|
+
.rk-ip-lightbox[hidden]{display:none}
|
|
1265
|
+
.rk-ipv-lightbox-dialog{width:min(1100px,calc(100vw - 44px));max-height:94svh;display:grid;grid-template-rows:auto minmax(0,1fr);gap:12px;padding:14px;border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);background:var(--rk-surface);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card))}
|
|
1266
|
+
.custom-inpaint-lightbox-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
1267
|
+
.custom-inpaint-lightbox-title{min-width:0;margin:0;color:var(--rk-ink);font-size:14px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1268
|
+
.custom-inpaint-lightbox-actions{display:flex;align-items:center;gap:8px}
|
|
1269
|
+
.rk-ipv-lightbox-stage{min-height:0;display:grid;place-items:center;overflow:auto}
|
|
1270
|
+
.rk-ipv-lightbox-stage video{max-width:100%;max-height:calc(94svh - 92px);border-radius:var(--rk-r-xl);background:#000}
|
|
1271
|
+
|
|
1272
|
+
@keyframes rk-ip-spin{to{transform:rotate(360deg)}}
|
|
1273
|
+
|
|
1274
|
+
/* settings modal */
|
|
1275
|
+
.rk-ip-settingsmodal{position:fixed;inset:0;z-index:70;background:rgba(28,25,23,.44);backdrop-filter:blur(2px);display:grid;place-items:center;padding:20px}
|
|
1276
|
+
.rk-ip-settingsmodal[hidden]{display:none}
|
|
1277
|
+
.rk-ip-settingsmodal-panel{width:min(440px,100%);display:flex;flex-direction:column;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card));overflow:hidden}
|
|
1278
|
+
.rk-ip-settingsmodal-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;border-bottom:1px solid var(--rk-border)}
|
|
1279
|
+
.rk-ip-settingsmodal-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-md,15px);color:var(--rk-ink)}
|
|
1280
|
+
.rk-ip-settingsmodal-body{padding:16px 18px;display:grid;gap:14px}
|
|
1281
|
+
|
|
1282
|
+
/* past-sessions panel */
|
|
1283
|
+
.rk-ip-sessions{position:fixed;left:0;top:0;bottom:0;z-index:96;width:min(320px,86vw);display:flex;flex-direction:column;background:var(--rk-surface);border-right:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xl,var(--rk-shadow-card));transform:translateX(-100%);transition:transform var(--rk-dur) var(--rk-ease)}
|
|
1284
|
+
.rk-ip-sessions.is-open{transform:none}
|
|
1285
|
+
.rk-ip-sessions-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:16px 14px 10px;border-bottom:1px solid var(--rk-border)}
|
|
1286
|
+
.rk-ip-sessions-title{font-family:var(--rk-font-display);font-weight:800;font-size:14px;color:var(--rk-ink)}
|
|
1287
|
+
.rk-ip-sessions-list{flex:1;min-height:0;overflow-y:auto;padding:8px;display:grid;gap:4px;align-content:start}
|
|
1288
|
+
.rk-ip-sessions-empty{font-size:12.5px;color:var(--rk-text-faint);padding:16px 8px;text-align:center;line-height:1.4}
|
|
1289
|
+
.rk-ip-session{display:grid;grid-template-columns:44px minmax(0,1fr);align-items:center;gap:10px;padding:7px;border:1px solid transparent;border-radius:var(--rk-r-lg);background:transparent;cursor:pointer;text-align:left;font:inherit;width:100%}
|
|
1290
|
+
.rk-ip-session:hover{background:var(--rk-n-50)}
|
|
1291
|
+
.rk-ip-session.is-active{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
1292
|
+
.rk-ip-session-thumb{width:44px;height:44px;border:1px solid var(--rk-border);border-radius:var(--rk-r-md,8px);object-fit:cover;background:var(--rk-n-200)}
|
|
1293
|
+
.rk-ip-session-thumb.is-empty{display:grid;place-items:center;color:var(--rk-text-faint)}
|
|
1294
|
+
.rk-ip-session-meta{min-width:0;display:grid;gap:3px}
|
|
1295
|
+
.rk-ip-session-title{min-width:0;color:var(--rk-ink);font-size:12.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1296
|
+
.rk-ip-session-sub{min-width:0;color:var(--rk-text-muted);font-size:11px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1297
|
+
.rk-ip-sessions-backdrop{position:fixed;inset:0;z-index:95;background:rgba(28,25,23,.34)}
|
|
1298
|
+
.rk-ip-sessions-backdrop[hidden]{display:none}
|
|
1299
|
+
|
|
1300
|
+
</style>
|
|
1301
|
+
</head>
|
|
1302
|
+
<body class="rk-root rk-has-sidebar rk-signed-in">
|
|
1303
|
+
<div class="rk-app">
|
|
1304
|
+
<aside class="rk-sidebar">
|
|
1305
|
+
<a class="rk-sidebar-brand" href="/discover"><span class="rk-brand-mark">V</span>VidFarm</a>
|
|
1306
|
+
<a class="rk-sidebar-market" href="/marketplace" hidden>
|
|
1307
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9l1-5h16l1 5"/><path d="M5 9v11h14V9"/><path d="M9 20v-6h6v6"/></svg>
|
|
1308
|
+
Marketplace
|
|
1309
|
+
<span class="rk-sidebar-market-dot" aria-hidden="true"></span>
|
|
1310
|
+
</a>
|
|
1311
|
+
<script>(function(){try{var p=new URLSearchParams(location.search);if(p.get('show_marketplace')==='true')localStorage.setItem('vf_show_marketplace','1');if(p.get('show_marketplace')==='true'||localStorage.getItem('vf_show_marketplace')==='1'){var el=document.querySelector('.rk-sidebar-market');if(el)el.hidden=false;}}catch(e){}})();</script>
|
|
1312
|
+
<div class="rk-sidebar-skill"><div class="vf-skill-install vf-skill-install-nav" data-skill-install>
|
|
1313
|
+
<button
|
|
1314
|
+
class="vf-skill-install-main"
|
|
1315
|
+
type="button"
|
|
1316
|
+
data-skill-copy
|
|
1317
|
+
data-default-label="SKILL.md"
|
|
1318
|
+
data-skill-content="Read https://vidfarm.cc/skill.md to get started with Vidfarm (agentic video editing). Start by installing the vidfarm devcli (npm i -g @officexapp/vidfarm-devcli) and logging in (vidfarm login)."
|
|
1319
|
+
aria-label="Copy SKILL.md"
|
|
1320
|
+
title="Copy SKILL.md"
|
|
1321
|
+
>
|
|
1322
|
+
<span class="vf-skill-install-main-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></span>
|
|
1323
|
+
<span class="vf-skill-install-main-label">SKILL.md</span>
|
|
1324
|
+
</button>
|
|
1325
|
+
<button
|
|
1326
|
+
class="vf-skill-install-menu"
|
|
1327
|
+
type="button"
|
|
1328
|
+
data-skill-menu-toggle
|
|
1329
|
+
aria-haspopup="dialog"
|
|
1330
|
+
aria-expanded="false"
|
|
1331
|
+
aria-label="Open skill install options"
|
|
1332
|
+
title="Open skill install options"
|
|
1333
|
+
>
|
|
1334
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5.5" r="1.75"></circle><circle cx="12" cy="12" r="1.75"></circle><circle cx="12" cy="18.5" r="1.75"></circle></svg>
|
|
1335
|
+
</button>
|
|
1336
|
+
<div class="vf-skill-install-popover" data-skill-menu hidden>
|
|
1337
|
+
<div class="vf-skill-install-popover-kicker">Install Command</div>
|
|
1338
|
+
<div class="vf-skill-install-command">
|
|
1339
|
+
<input class="vf-skill-install-input" type="text" readonly value="npx skills add https://github.com/officexapp/vidfarm-director" data-skill-command-input aria-label="Skill install command">
|
|
1340
|
+
<button class="vf-skill-install-copy" type="button" data-skill-command-copy aria-label="Copy install command" title="Copy install command">
|
|
1341
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
1342
|
+
</button>
|
|
1343
|
+
</div>
|
|
1344
|
+
<a class="vf-skill-install-link" href="https://github.com/officexapp/vidfarm-director" target="_blank" rel="noopener noreferrer">
|
|
1345
|
+
<span class="vf-skill-install-link-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 4h6v6"></path><path d="M10 14 20 4"></path><path d="M20 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5"></path></svg></span>
|
|
1346
|
+
<span>View GitHub</span>
|
|
1347
|
+
</a>
|
|
1348
|
+
</div>
|
|
1349
|
+
</div></div>
|
|
1350
|
+
<input type="checkbox" id="rk-navtoggle" class="rk-nav-toggle-cb" hidden>
|
|
1351
|
+
<label for="rk-navtoggle" class="rk-nav-burger" aria-label="Toggle navigation" aria-controls="rk-navtoggle">
|
|
1352
|
+
<svg viewBox="0 0 20 20" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M3 6h14M3 10h14M3 14h14"/></svg>
|
|
1353
|
+
</label>
|
|
1354
|
+
<nav class="rk-sidebar-nav" aria-label="Reskin pages">
|
|
1355
|
+
<a class="rk-sidebar-item" href="/discover">Discover</a><a class="rk-sidebar-item" href="/chat">Create</a><a class="rk-sidebar-item" href="/library">Library</a><a class="rk-sidebar-item" href="/calendar">Calendar</a><a class="rk-sidebar-item" href="/guides">Guides</a><a class="rk-sidebar-item" href="/settings">Settings</a>
|
|
1356
|
+
</nav>
|
|
1357
|
+
<button class="rk-sidebar-cloud" id="rkSidebarCloud" type="button" title="Open cloud status" hidden>
|
|
1358
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17.5 19a4.5 4.5 0 0 0 .5-8.98A6 6 0 0 0 6.34 9.5 4 4 0 0 0 7 19h10.5z"/></svg>
|
|
1359
|
+
<span class="rk-sidebar-cloud-label">Cloud status</span>
|
|
1360
|
+
</button>
|
|
1361
|
+
<div class="rk-sidebar-account" title="Signed in as staging@vidfarm.local">
|
|
1362
|
+
<div class="rk-sidebar-account-name">Staging</div>
|
|
1363
|
+
<div class="rk-sidebar-account-email">staging@vidfarm.local</div>
|
|
1364
|
+
</div>
|
|
1365
|
+
</aside>
|
|
1366
|
+
<div class="rk-content">
|
|
1367
|
+
|
|
1368
|
+
<main class="rk-container-wide rk-page rk-inpaint-page rk-ipv-page">
|
|
1369
|
+
<div class="rk-page-head rk-inpaint-head">
|
|
1370
|
+
<div class="rk-inpaint-head-text">
|
|
1371
|
+
<div class="rk-ip-tabs" role="tablist" aria-label="Editor mode"><a class="rk-ip-tab" href="/tools/image" role="tab" aria-selected="false"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9" r="1.5"/><path d="m4 17 4.5-4.5 3 3L16 11l4 4"/></svg><span>Image</span></a><a class="rk-ip-tab is-active" href="/tools/video" role="tab" aria-selected="true"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="14" height="14" rx="2"/><path d="m17 9 4-2v10l-4-2"/></svg><span>Video</span></a><a class="rk-ip-tab" href="/tools/clipper" role="tab" aria-selected="false"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M8.1 8.1 20 18M8.1 15.9 20 6"/></svg><span>Clipper</span></a></div>
|
|
1372
|
+
<h1 class="rk-h1">Create Video</h1>
|
|
1373
|
+
<p class="rk-sub">Load a video (or set a start & end frame), scrub the timeline, and paint <strong>time-scoped</strong> mask regions — each mask carries its own moment (MM:SS:mmm) and instruction. Generate and the masks + timecodes + frames become one precise editing prompt for your AI video model.</p>
|
|
1374
|
+
</div>
|
|
1375
|
+
<div class="rk-inpaint-toolbar">
|
|
1376
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-inpaint-startnew" data-ipv-start-new><svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4v12"/><path d="M4 10h12"/></svg> Start New</button>
|
|
1377
|
+
<button type="button" class="rk-ip-iconbtn" data-ipv-settings-button aria-label="Settings" title="Settings"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M8.8 3.4 9.4 2h1.2l.6 1.4 1.2.5 1.4-.6.9.9-.6 1.4.5 1.2 1.4.6v1.2l-1.4.6-.5 1.2.6 1.4-.9.9-1.4-.6-1.2.5-.6 1.4H9.4l-.6-1.4-1.2-.5-1.4.6-.9-.9.6-1.4-.5-1.2L4 8.6V7.4l1.4-.6.5-1.2-.6-1.4.9-.9 1.4.6 1.2-.5Z"/><circle cx="10" cy="8" r="2.4"/></svg></button>
|
|
1378
|
+
<button type="button" class="rk-ip-iconbtn" data-ipv-history-toggle aria-label="Past sessions" title="Past sessions"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3.2 10a6.8 6.8 0 1 1 2 4.8"/><path d="M3.2 14.4v-3.6h3.6"/><path d="M10 6.2V10l2.6 1.6"/></svg></button>
|
|
1379
|
+
</div>
|
|
1380
|
+
</div>
|
|
1381
|
+
|
|
1382
|
+
<div class="rk-inpaint-workbench rk-ipv-workbench">
|
|
1383
|
+
<!-- LEFT: frames + prompt + brush + timed masks -->
|
|
1384
|
+
<section class="rk-inpaint-panel" aria-label="Controls">
|
|
1385
|
+
<div class="rk-inpaint-block">
|
|
1386
|
+
<label class="rk-label">Start & end frames <span class="rk-ip-setting-note">(for full regenerate)</span></label>
|
|
1387
|
+
<div class="rk-ipv-frames">
|
|
1388
|
+
|
|
1389
|
+
<div class="rk-ipv-frame" data-ipv-frame="first">
|
|
1390
|
+
<button type="button" class="rk-ipv-frame-thumb" data-ipv-frame-open="first" aria-label="Preview Start frame">
|
|
1391
|
+
<span class="rk-ipv-frame-ph" data-ipv-frame-ph="first" aria-hidden="true">◧</span>
|
|
1392
|
+
<img class="rk-ipv-frame-img" data-ipv-frame-img="first" alt="" hidden />
|
|
1393
|
+
</button>
|
|
1394
|
+
<span class="rk-ipv-frame-name">Start</span>
|
|
1395
|
+
<div class="rk-ipv-frame-actions">
|
|
1396
|
+
<button type="button" class="rk-ipv-frame-btn" data-ipv-frame-upload="first" title="Upload an image for the start frame">Replace</button>
|
|
1397
|
+
<button type="button" class="rk-ipv-frame-btn" data-ipv-frame-capture="first" title="Grab the current playhead frame">Use current</button>
|
|
1398
|
+
</div>
|
|
1399
|
+
</div>
|
|
1400
|
+
|
|
1401
|
+
<div class="rk-ipv-frame" data-ipv-frame="last">
|
|
1402
|
+
<button type="button" class="rk-ipv-frame-thumb" data-ipv-frame-open="last" aria-label="Preview End frame">
|
|
1403
|
+
<span class="rk-ipv-frame-ph" data-ipv-frame-ph="last" aria-hidden="true">◨</span>
|
|
1404
|
+
<img class="rk-ipv-frame-img" data-ipv-frame-img="last" alt="" hidden />
|
|
1405
|
+
</button>
|
|
1406
|
+
<span class="rk-ipv-frame-name">End</span>
|
|
1407
|
+
<div class="rk-ipv-frame-actions">
|
|
1408
|
+
<button type="button" class="rk-ipv-frame-btn" data-ipv-frame-upload="last" title="Upload an image for the end frame">Replace</button>
|
|
1409
|
+
<button type="button" class="rk-ipv-frame-btn" data-ipv-frame-capture="last" title="Grab the current playhead frame">Use current</button>
|
|
1410
|
+
</div>
|
|
1411
|
+
</div>
|
|
1412
|
+
</div>
|
|
1413
|
+
<input data-ipv-frame-input type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" hidden />
|
|
1414
|
+
<span class="rk-hint">Optional. Provide a start and/or end frame to steer the regenerated clip. <strong>Use current</strong> grabs the frame at the playhead.</span>
|
|
1415
|
+
</div>
|
|
1416
|
+
|
|
1417
|
+
<div class="rk-inpaint-block">
|
|
1418
|
+
<label class="rk-label">Video prompt</label>
|
|
1419
|
+
<div class="rk-ip-promptfield">
|
|
1420
|
+
<textarea class="rk-textarea rk-ip-global" data-ipv-global-prompt placeholder="Describe the overall motion, style, and outcome for the whole clip."></textarea>
|
|
1421
|
+
<button type="button" class="rk-ip-attach" data-ipv-attach aria-label="Attach reference image" title="Attach reference image"><svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m8.1 10.7 4.8-4.8a2.5 2.5 0 0 1 3.5 3.5l-6.1 6.1a4 4 0 0 1-5.7-5.7l6.3-6.3"/><path d="m10 8.8-4.1 4.1a1.6 1.6 0 0 0 2.3 2.3l5.6-5.6"/></svg></button>
|
|
1422
|
+
</div>
|
|
1423
|
+
<span class="rk-ip-attach-chips" data-ipv-attachments></span>
|
|
1424
|
+
<input data-ipv-attachment-input type="file" accept="image/jpeg,image/png,image/webp" multiple hidden />
|
|
1425
|
+
<span class="rk-hint">Guidance for the whole video and for consistency across the timed edits below.</span>
|
|
1426
|
+
</div>
|
|
1427
|
+
|
|
1428
|
+
<div class="rk-inpaint-block">
|
|
1429
|
+
<div class="rk-inpaint-brushrow">
|
|
1430
|
+
<label class="rk-label">Brush <span data-ipv-brush-label>70</span> px</label>
|
|
1431
|
+
<div class="rk-ip-toolbtns">
|
|
1432
|
+
<button type="button" class="rk-ip-iconbtn" data-ipv-undo aria-label="Undo stroke" title="Undo"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7.2 7H3.8V3.6"/><path d="M4 7a7 7 0 1 1 1.9 6.6"/></svg></button>
|
|
1433
|
+
<button type="button" class="rk-ip-iconbtn" data-ipv-redo aria-label="Redo stroke" title="Redo"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12.8 7h3.4V3.6"/><path d="M16 7a7 7 0 1 0-1.9 6.6"/></svg></button>
|
|
1434
|
+
<button type="button" class="rk-ip-iconbtn" data-ipv-clear aria-label="Clear active mask" title="Clear active mask"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10M15 5 5 15"/></svg></button>
|
|
1435
|
+
</div>
|
|
1436
|
+
</div>
|
|
1437
|
+
<input data-ipv-brush class="rk-inpaint-range" type="range" min="6" max="180" step="1" value="70" />
|
|
1438
|
+
</div>
|
|
1439
|
+
|
|
1440
|
+
<div class="rk-inpaint-block">
|
|
1441
|
+
<div class="rk-inpaint-brushrow">
|
|
1442
|
+
<label class="rk-label">Timed masks</label>
|
|
1443
|
+
<input class="rk-ip-colorpicker" data-ipv-color-picker type="color" value="#ff2d55" aria-label="Next mask color" />
|
|
1444
|
+
</div>
|
|
1445
|
+
<span class="rk-hint">Each mask marks a region <strong>and</strong> a moment. Paint on the video, set the timecode + instruction, then add another mask for a different spot or time. Click a mask to jump the video to its moment.</span>
|
|
1446
|
+
<div class="rk-ipv-masks" data-ipv-masks></div>
|
|
1447
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-ip-addcolor" data-ipv-add-mask>+ Add timed mask</button>
|
|
1448
|
+
</div>
|
|
1449
|
+
|
|
1450
|
+
<div class="rk-inpaint-block">
|
|
1451
|
+
<button type="button" class="rk-btn rk-btn-gold rk-inpaint-generate" data-ipv-submit><svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.5l1.9 5.1 5.1 1.9-5.1 1.9L12 16.5l-1.9-5.1L5 9.5l5.1-1.9z"/></svg> <span data-ipv-submit-label>Generate video</span></button>
|
|
1452
|
+
<p class="rk-inpaint-status" data-ipv-status data-tone="" aria-live="polite"></p>
|
|
1453
|
+
</div>
|
|
1454
|
+
</section>
|
|
1455
|
+
|
|
1456
|
+
<!-- CENTER: scrubbable video stage -->
|
|
1457
|
+
<section class="rk-inpaint-stage rk-ipv-stage" aria-label="Video canvas">
|
|
1458
|
+
<div class="rk-inpaint-empty" data-ipv-empty>
|
|
1459
|
+
<span class="rk-inpaint-empty-ic" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg></span>
|
|
1460
|
+
<p class="rk-inpaint-empty-title">Load a video to edit</p>
|
|
1461
|
+
<p class="rk-inpaint-empty-sub">Drop a video here, or set a start & end frame and a prompt to generate one.</p>
|
|
1462
|
+
<button type="button" class="rk-btn rk-btn-ink rk-btn-sm rk-ipv-upload" data-ipv-upload><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg> Upload video</button>
|
|
1463
|
+
<input data-ipv-file type="file" accept="video/mp4,video/webm,video/quicktime,video/*" hidden />
|
|
1464
|
+
|
|
1465
|
+
<div class="rk-ip-gen" id="rk-ipv-gen">
|
|
1466
|
+
<span class="rk-ip-gen-or">or</span>
|
|
1467
|
+
<div class="rk-ip-gen-box">
|
|
1468
|
+
<textarea id="rk-ipv-genprompt" class="rk-ip-gen-input" rows="2" placeholder="Or describe the video you want"></textarea>
|
|
1469
|
+
<div class="rk-ip-gen-refs" id="rk-ipv-genrefs" hidden></div>
|
|
1470
|
+
<div class="rk-ip-gen-bar">
|
|
1471
|
+
<button type="button" class="rk-ip-gen-clip" id="rk-ipv-genclip" aria-label="Attach reference images from your files" title="Attach reference images from your files"><svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m8.1 10.7 4.8-4.8a2.5 2.5 0 0 1 3.5 3.5l-6.1 6.1a4 4 0 0 1-5.7-5.7l6.3-6.3"/><path d="m10 8.8-4.1 4.1a1.6 1.6 0 0 0 2.3 2.3l5.6-5.6"/></svg></button>
|
|
1472
|
+
<span class="rk-ip-gen-hint">Generate a brand-new clip from a prompt (+ optional frames).</span>
|
|
1473
|
+
<button type="button" class="rk-ip-gen-send" id="rk-ipv-gensend" aria-label="Generate video"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg></button>
|
|
1474
|
+
</div>
|
|
1475
|
+
</div>
|
|
1476
|
+
<p class="rk-ip-gen-status" id="rk-ipv-genstatus" data-tone="" aria-live="polite"></p>
|
|
1477
|
+
</div>
|
|
1478
|
+
</div>
|
|
1479
|
+
|
|
1480
|
+
<div class="rk-ipv-player" data-ipv-player hidden>
|
|
1481
|
+
<div class="rk-ipv-canvaswrap" data-ipv-canvaswrap>
|
|
1482
|
+
<video data-ipv-video class="rk-ipv-video" playsinline></video>
|
|
1483
|
+
<canvas data-ipv-canvas class="rk-ipv-canvas"></canvas>
|
|
1484
|
+
<div class="editor-inpaint-brush-cursor" data-ipv-brush-cursor aria-hidden="true"></div>
|
|
1485
|
+
</div>
|
|
1486
|
+
<div class="rk-ipv-controls">
|
|
1487
|
+
<button type="button" class="rk-ipv-playpause" data-ipv-playpause aria-label="Play/pause"><svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 5.5v13a1 1 0 0 0 1.5.87l11-6.5a1 1 0 0 0 0-1.74l-11-6.5A1 1 0 0 0 7 5.5Z"/></svg></button>
|
|
1488
|
+
<input type="range" class="rk-ipv-scrub" data-ipv-scrub min="0" max="1000" step="1" value="0" aria-label="Seek" />
|
|
1489
|
+
<div class="rk-ipv-timecode">
|
|
1490
|
+
<input class="rk-ipv-timecode-input" data-ipv-timecode-input type="text" inputmode="numeric" spellcheck="false" value="00:00:000" aria-label="Current time MM:SS:mmm" />
|
|
1491
|
+
<button type="button" class="rk-ip-iconbtn rk-ipv-timecode-copy" data-ipv-timecode-copy aria-label="Copy current timecode" title="Copy timecode"><svg width="13" height="13" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.4 11.6a3.2 3.2 0 0 0 4.5 0l2.2-2.2a3.2 3.2 0 0 0-4.5-4.5L9.4 6.1"/><path d="M11.6 8.4a3.2 3.2 0 0 0-4.5 0L4.9 10.6a3.2 3.2 0 0 0 4.5 4.5l1.2-1.2"/></svg></button>
|
|
1492
|
+
<span class="rk-ipv-duration" data-ipv-duration>/ 00:00:000</span>
|
|
1493
|
+
</div>
|
|
1494
|
+
</div>
|
|
1495
|
+
</div>
|
|
1496
|
+
</section>
|
|
1497
|
+
|
|
1498
|
+
<!-- RIGHT: result + tracer-grouped history -->
|
|
1499
|
+
<section class="rk-inpaint-output" aria-label="Result and history">
|
|
1500
|
+
<div class="rk-inpaint-result" data-ipv-output hidden>
|
|
1501
|
+
<div class="rk-inpaint-result-head">
|
|
1502
|
+
<h2 class="rk-inpaint-h2">Result</h2>
|
|
1503
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-ipv-save>Save to Files</button>
|
|
1504
|
+
</div>
|
|
1505
|
+
<video class="rk-ipv-result-video" data-ipv-result controls playsinline></video>
|
|
1506
|
+
</div>
|
|
1507
|
+
|
|
1508
|
+
<div class="rk-inpaint-history">
|
|
1509
|
+
<div class="rk-inpaint-history-head">
|
|
1510
|
+
<h2 class="rk-inpaint-h2">This session</h2>
|
|
1511
|
+
<div class="rk-ip-histtools">
|
|
1512
|
+
<button type="button" class="rk-ip-iconbtn" data-ipv-history-refresh aria-label="Refresh history" title="Refresh history"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16.5 10a6.5 6.5 0 1 1-1.9-4.6"/><path d="M16.5 4.2v4.2h-4.2"/></svg></button>
|
|
1513
|
+
</div>
|
|
1514
|
+
</div>
|
|
1515
|
+
<div class="rk-inpaint-historylist" data-ipv-history-list>
|
|
1516
|
+
<div class="custom-inpaint-history-empty">Loading history…</div>
|
|
1517
|
+
</div>
|
|
1518
|
+
</div>
|
|
1519
|
+
</section>
|
|
1520
|
+
</div>
|
|
1521
|
+
|
|
1522
|
+
<!-- settings modal (provider + duration + aspect + audio + tracer) -->
|
|
1523
|
+
<div class="rk-ip-settingsmodal" data-ipv-settings hidden>
|
|
1524
|
+
<div class="rk-ip-settingsmodal-panel" role="dialog" aria-modal="true" aria-label="Video generation settings">
|
|
1525
|
+
<div class="rk-ip-settingsmodal-head">
|
|
1526
|
+
<span class="rk-ip-settingsmodal-title">Settings</span>
|
|
1527
|
+
<button type="button" class="rk-ip-iconbtn" data-ipv-settings-close aria-label="Close settings" title="Close"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10"/><path d="M15 5 5 15"/></svg></button>
|
|
1528
|
+
</div>
|
|
1529
|
+
<div class="rk-ip-settingsmodal-body">
|
|
1530
|
+
<label class="rk-ip-setting rk-ip-provider" data-ipv-provider-wrap>
|
|
1531
|
+
<span class="rk-label">AI model</span>
|
|
1532
|
+
<select data-ipv-provider class="rk-select rk-inpaint-select"></select>
|
|
1533
|
+
</label>
|
|
1534
|
+
<label class="rk-ip-setting">
|
|
1535
|
+
<span class="rk-label">Duration</span>
|
|
1536
|
+
<select data-ipv-duration class="rk-select rk-inpaint-select">
|
|
1537
|
+
<option value="">Auto</option>
|
|
1538
|
+
<option value="4">4 seconds</option>
|
|
1539
|
+
<option value="6">6 seconds</option>
|
|
1540
|
+
<option value="8">8 seconds</option>
|
|
1541
|
+
<option value="10">10 seconds</option>
|
|
1542
|
+
</select>
|
|
1543
|
+
</label>
|
|
1544
|
+
<label class="rk-ip-setting rk-ipv-audio-row">
|
|
1545
|
+
<span class="rk-label">Generate audio</span>
|
|
1546
|
+
<input type="checkbox" data-ipv-audio class="rk-ipv-audio" />
|
|
1547
|
+
</label>
|
|
1548
|
+
<label class="rk-ip-setting">
|
|
1549
|
+
<span class="rk-label">Session name <span class="rk-ip-setting-note">(tracer — groups this session's History)</span></span>
|
|
1550
|
+
<input data-ipv-tracer class="rk-ip-input" type="text" autocomplete="off" spellcheck="false" placeholder="Auto-named from your first prompt" />
|
|
1551
|
+
</label>
|
|
1552
|
+
</div>
|
|
1553
|
+
</div>
|
|
1554
|
+
</div>
|
|
1555
|
+
|
|
1556
|
+
<!-- past-sessions left slide-in panel -->
|
|
1557
|
+
<aside class="rk-ip-sessions" data-ipv-sessions aria-label="Past sessions">
|
|
1558
|
+
<div class="rk-ip-sessions-head">
|
|
1559
|
+
<span class="rk-ip-sessions-title">Past sessions</span>
|
|
1560
|
+
<button type="button" class="rk-ip-iconbtn" data-ipv-sessions-close aria-label="Close past sessions" title="Close"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10"/><path d="M15 5 5 15"/></svg></button>
|
|
1561
|
+
</div>
|
|
1562
|
+
<div class="rk-ip-sessions-list" data-ipv-sessions-list>
|
|
1563
|
+
<div class="rk-ip-sessions-empty">Loading…</div>
|
|
1564
|
+
</div>
|
|
1565
|
+
</aside>
|
|
1566
|
+
<div class="rk-ip-sessions-backdrop" data-ipv-sessions-backdrop hidden></div>
|
|
1567
|
+
|
|
1568
|
+
<!-- video preview lightbox (result + history) -->
|
|
1569
|
+
<div class="rk-ip-lightbox rk-ipv-lightbox" data-ipv-lightbox data-open="false" hidden>
|
|
1570
|
+
<div class="rk-ipv-lightbox-dialog" role="dialog" aria-modal="true" aria-label="Video output">
|
|
1571
|
+
<div class="custom-inpaint-lightbox-head">
|
|
1572
|
+
<p class="custom-inpaint-lightbox-title" data-ipv-lightbox-title>Video output</p>
|
|
1573
|
+
<div class="custom-inpaint-lightbox-actions">
|
|
1574
|
+
<button type="button" class="rk-ip-iconbtn" data-ipv-lightbox-copy aria-label="Copy video URL" title="Copy URL"><svg width="13" height="13" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.4 11.6a3.2 3.2 0 0 0 4.5 0l2.2-2.2a3.2 3.2 0 0 0-4.5-4.5L9.4 6.1"/><path d="M11.6 8.4a3.2 3.2 0 0 0-4.5 0L4.9 10.6a3.2 3.2 0 0 0 4.5 4.5l1.2-1.2"/></svg></button>
|
|
1575
|
+
<a class="rk-btn rk-btn-ink rk-btn-sm" data-ipv-lightbox-download href="#" target="_blank" rel="noopener noreferrer" download><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3v9"/><path d="m6.5 8.5 3.5 3.5 3.5-3.5"/><path d="M4 16h12"/></svg> Download</a>
|
|
1576
|
+
<button type="button" class="rk-ip-iconbtn" data-ipv-lightbox-close aria-label="Close preview" title="Close"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10"/><path d="M15 5 5 15"/></svg></button>
|
|
1577
|
+
</div>
|
|
1578
|
+
</div>
|
|
1579
|
+
<div class="rk-ipv-lightbox-stage">
|
|
1580
|
+
<video data-ipv-lightbox-video controls playsinline></video>
|
|
1581
|
+
</div>
|
|
1582
|
+
</div>
|
|
1583
|
+
</div>
|
|
1584
|
+
|
|
1585
|
+
<script type="application/json" data-rk-inpaint-video-boot>{"accountId":"{{ACCOUNT_ID}}","isLoggedIn":true}</script>
|
|
1586
|
+
</main>
|
|
1587
|
+
</div>
|
|
1588
|
+
<button class="rk-fab" id="rkFab" type="button" aria-label="Open AI chat" aria-expanded="false" aria-controls="rkAichat" title="Chat with vidfarm AI — drag to move">
|
|
1589
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
|
|
1590
|
+
</button>
|
|
1591
|
+
<aside class="rk-aichat has-files" id="rkAichat" hidden aria-label="AI chat panel">
|
|
1592
|
+
<div class="rk-aichat-files" id="rkAichatFiles" aria-label="Files">
|
|
1593
|
+
<div class="rk-aichat-files-head">
|
|
1594
|
+
<span class="rk-aichat-files-title">Your files</span>
|
|
1595
|
+
<button class="rk-aichat-tool" id="rkFilesClose" type="button" title="Minimize files" aria-label="Minimize files">
|
|
1596
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/></svg>
|
|
1597
|
+
</button>
|
|
1598
|
+
</div>
|
|
1599
|
+
<div class="rk-aichat-files-mount" id="rkAichatFilesMount" data-rk-directory data-mode="attach" data-primary-click="preview"></div>
|
|
1600
|
+
</div>
|
|
1601
|
+
<div class="rk-aichat-history" id="rkAichatHistory" aria-label="Chat history">
|
|
1602
|
+
<div class="rk-aichat-hist-head">
|
|
1603
|
+
<span class="rk-aichat-hist-title">Chat history</span>
|
|
1604
|
+
<button class="rk-aichat-tool" id="rkHistoryClose" type="button" title="Back to chat" aria-label="Back to chat">
|
|
1605
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/></svg>
|
|
1606
|
+
</button>
|
|
1607
|
+
</div>
|
|
1608
|
+
<div class="rk-aichat-hist-body" id="rkAichatHistBody"></div>
|
|
1609
|
+
</div>
|
|
1610
|
+
<div class="rk-aichat-cloud" id="rkAichatCloud" aria-label="Cloud status">
|
|
1611
|
+
<div class="rk-aichat-cloud-head">
|
|
1612
|
+
<span class="rk-aichat-cloud-title">Cloud status</span>
|
|
1613
|
+
<button class="rk-aichat-tool rk-aichat-cloud-refresh" id="rkCloudRefresh" type="button" title="Refresh" aria-label="Refresh cloud tasks">
|
|
1614
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>
|
|
1615
|
+
</button>
|
|
1616
|
+
<button class="rk-aichat-tool" id="rkCloudClose" type="button" title="Back to chat" aria-label="Back to chat">
|
|
1617
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/></svg>
|
|
1618
|
+
</button>
|
|
1619
|
+
</div>
|
|
1620
|
+
<div class="rk-aichat-cloud-filters" id="rkCloudFilters">
|
|
1621
|
+
<button type="button" class="rk-cloud-fbtn is-on" data-f="all">All</button>
|
|
1622
|
+
<button type="button" class="rk-cloud-fbtn" data-f="running">Running</button>
|
|
1623
|
+
<button type="button" class="rk-cloud-fbtn" data-f="done">Done</button>
|
|
1624
|
+
<button type="button" class="rk-cloud-fbtn" data-f="error">Error</button>
|
|
1625
|
+
</div>
|
|
1626
|
+
<div class="rk-aichat-cloud-body" id="rkCloudBody"></div>
|
|
1627
|
+
</div>
|
|
1628
|
+
<div class="rk-aichat-main">
|
|
1629
|
+
<header class="rk-aichat-head">
|
|
1630
|
+
<span class="rk-brand-mark">V</span>
|
|
1631
|
+
<div class="rk-aichat-head-main">
|
|
1632
|
+
<div class="rk-aichat-title">VidFarm AI</div>
|
|
1633
|
+
<div class="rk-aichat-sub">on your own AI keys</div>
|
|
1634
|
+
</div>
|
|
1635
|
+
<button class="rk-aichat-tool" id="rkHistoryToggle" type="button" title="Chat history" aria-label="Toggle chat history" aria-pressed="false">
|
|
1636
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l3 2"/></svg>
|
|
1637
|
+
</button>
|
|
1638
|
+
<button class="rk-aichat-tool" id="rkCloudToggle" type="button" title="Cloud status" aria-label="Toggle cloud status" aria-pressed="false">
|
|
1639
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17.5 19a4.5 4.5 0 0 0 .5-8.98A6 6 0 0 0 6.34 9.5 4 4 0 0 0 7 19h10.5z"/></svg>
|
|
1640
|
+
<span class="rk-cloud-dot" id="rkCloudDot" hidden></span>
|
|
1641
|
+
</button>
|
|
1642
|
+
<button class="rk-aichat-tool" id="rkCopyThread" type="button" title="Copy chat ID" aria-label="Copy chat ID">
|
|
1643
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
1644
|
+
</button>
|
|
1645
|
+
<button class="rk-aichat-tool" id="rkNewChat" type="button" title="New chat" aria-label="Start a new chat">
|
|
1646
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>
|
|
1647
|
+
</button>
|
|
1648
|
+
<button class="rk-aichat-tool" id="rkAichatMin" type="button" title="Minimize" aria-label="Minimize chat">—</button>
|
|
1649
|
+
</header>
|
|
1650
|
+
<div class="rk-aichat-log" id="rkAichatLog">
|
|
1651
|
+
<div class="rk-aichat-msg is-ai">Hi! I'm the vidfarm assistant. I can take real actions on your account — browse your files, hit vidfarm APIs, kick off renders. Ask me anything, or attach a file from the panel on the left.</div>
|
|
1652
|
+
</div>
|
|
1653
|
+
<div class="rk-aichat-chips" id="rkAichatChips"></div>
|
|
1654
|
+
<form class="rk-aichat-composer" id="rkAichatForm">
|
|
1655
|
+
<textarea class="rk-aichat-field" id="rkAichatInput" rows="3" placeholder="Ask the vidfarm AI…" autocomplete="off"></textarea>
|
|
1656
|
+
<div class="rk-aichat-composer-actions">
|
|
1657
|
+
<button class="rk-aichat-attach" id="rkAichatAttach" type="button" title="Attach a file" aria-label="Attach a file">
|
|
1658
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
|
1659
|
+
</button>
|
|
1660
|
+
<button class="rk-btn rk-btn-gold rk-btn-sm" type="submit">Send</button>
|
|
1661
|
+
</div>
|
|
1662
|
+
</form>
|
|
1663
|
+
</div>
|
|
1664
|
+
</aside>
|
|
1665
|
+
</div>
|
|
1666
|
+
<script>
|
|
1667
|
+
(function(){
|
|
1668
|
+
var videoEl = document.querySelector("[data-ipv-video]");
|
|
1669
|
+
var canvas = document.querySelector("[data-ipv-canvas]");
|
|
1670
|
+
var brushCursor = document.querySelector("[data-ipv-brush-cursor]");
|
|
1671
|
+
var player = document.querySelector("[data-ipv-player]");
|
|
1672
|
+
var emptyState = document.querySelector("[data-ipv-empty]");
|
|
1673
|
+
var canvasWrap = document.querySelector("[data-ipv-canvaswrap]");
|
|
1674
|
+
var uploadBtn = document.querySelector("[data-ipv-upload]");
|
|
1675
|
+
var fileInput = document.querySelector("[data-ipv-file]");
|
|
1676
|
+
var startNewBtn = document.querySelector("[data-ipv-start-new]");
|
|
1677
|
+
var statusEl = document.querySelector("[data-ipv-status]");
|
|
1678
|
+
var submitBtn = document.querySelector("[data-ipv-submit]");
|
|
1679
|
+
var submitLabel = document.querySelector("[data-ipv-submit-label]");
|
|
1680
|
+
var globalPrompt = document.querySelector("[data-ipv-global-prompt]");
|
|
1681
|
+
var attachBtn = document.querySelector("[data-ipv-attach]");
|
|
1682
|
+
var attachChips = document.querySelector("[data-ipv-attachments]");
|
|
1683
|
+
var attachInput = document.querySelector("[data-ipv-attachment-input]");
|
|
1684
|
+
var brushInput = document.querySelector("[data-ipv-brush]");
|
|
1685
|
+
var brushLabel = document.querySelector("[data-ipv-brush-label]");
|
|
1686
|
+
var colorPicker = document.querySelector("[data-ipv-color-picker]");
|
|
1687
|
+
var addMaskBtn = document.querySelector("[data-ipv-add-mask]");
|
|
1688
|
+
var masksList = document.querySelector("[data-ipv-masks]");
|
|
1689
|
+
var undoBtn = document.querySelector("[data-ipv-undo]");
|
|
1690
|
+
var redoBtn = document.querySelector("[data-ipv-redo]");
|
|
1691
|
+
var clearBtn = document.querySelector("[data-ipv-clear]");
|
|
1692
|
+
var frameInput = document.querySelector("[data-ipv-frame-input]");
|
|
1693
|
+
var playpause = document.querySelector("[data-ipv-playpause]");
|
|
1694
|
+
var scrub = document.querySelector("[data-ipv-scrub]");
|
|
1695
|
+
var timecodeInput = document.querySelector("[data-ipv-timecode-input]");
|
|
1696
|
+
var timecodeCopy = document.querySelector("[data-ipv-timecode-copy]");
|
|
1697
|
+
var durationEl = document.querySelector("[data-ipv-duration]");
|
|
1698
|
+
var outputPanel = document.querySelector("[data-ipv-output]");
|
|
1699
|
+
var resultVideo = document.querySelector("[data-ipv-result]");
|
|
1700
|
+
var saveBtn = document.querySelector("[data-ipv-save]");
|
|
1701
|
+
var historyList = document.querySelector("[data-ipv-history-list]");
|
|
1702
|
+
var historyRefresh = document.querySelector("[data-ipv-history-refresh]");
|
|
1703
|
+
var settingsBtn = document.querySelector("[data-ipv-settings-button]");
|
|
1704
|
+
var settingsModal = document.querySelector("[data-ipv-settings]");
|
|
1705
|
+
var settingsClose = document.querySelector("[data-ipv-settings-close]");
|
|
1706
|
+
var providerWrap = document.querySelector("[data-ipv-provider-wrap]");
|
|
1707
|
+
var providerSelect = document.querySelector("[data-ipv-provider]");
|
|
1708
|
+
var durationSelect = document.querySelector("[data-ipv-duration]");
|
|
1709
|
+
var audioToggle = document.querySelector("[data-ipv-audio]");
|
|
1710
|
+
var tracerInput = document.querySelector("[data-ipv-tracer]");
|
|
1711
|
+
var sessionsToggle = document.querySelector("[data-ipv-history-toggle]");
|
|
1712
|
+
var sessionsPanel = document.querySelector("[data-ipv-sessions]");
|
|
1713
|
+
var sessionsList = document.querySelector("[data-ipv-sessions-list]");
|
|
1714
|
+
var sessionsClose = document.querySelector("[data-ipv-sessions-close]");
|
|
1715
|
+
var sessionsBackdrop = document.querySelector("[data-ipv-sessions-backdrop]");
|
|
1716
|
+
var lightbox = document.querySelector("[data-ipv-lightbox]");
|
|
1717
|
+
var lightboxTitle = document.querySelector("[data-ipv-lightbox-title]");
|
|
1718
|
+
var lightboxVideo = document.querySelector("[data-ipv-lightbox-video]");
|
|
1719
|
+
var lightboxCopy = document.querySelector("[data-ipv-lightbox-copy]");
|
|
1720
|
+
var lightboxDownload = document.querySelector("[data-ipv-lightbox-download]");
|
|
1721
|
+
var lightboxClose = document.querySelector("[data-ipv-lightbox-close]");
|
|
1722
|
+
if(!videoEl || !canvas || !masksList || !submitBtn) return;
|
|
1723
|
+
|
|
1724
|
+
var PLAY_SVG = "<svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M7 5.5v13a1 1 0 0 0 1.5.87l11-6.5a1 1 0 0 0 0-1.74l-11-6.5A1 1 0 0 0 7 5.5Z\"/></svg>";
|
|
1725
|
+
var PAUSE_SVG = "<svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\"/><rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\"/></svg>";
|
|
1726
|
+
var CLOCK_SVG = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M10 6v4l2.6 1.6\"/></svg>";
|
|
1727
|
+
|
|
1728
|
+
// ===== state =====
|
|
1729
|
+
var sourceVideoUrl = ""; // playable URL (may be a blob: object URL for uploads)
|
|
1730
|
+
var sourceVideoName = "";
|
|
1731
|
+
var sourceObjectUrl = ""; // revoke on reset
|
|
1732
|
+
var masks = []; // { id, color, prompt, timeSec, strokes:[], redo:[] }
|
|
1733
|
+
var activeMaskId = "";
|
|
1734
|
+
var isPainting = false;
|
|
1735
|
+
var frames = { first: null, last: null }; // { url, name }
|
|
1736
|
+
var attachments = []; // { id, fileName, url }
|
|
1737
|
+
var maxAttachments = 8;
|
|
1738
|
+
var allowedAttachTypes = { "image/jpeg":1, "image/png":1, "image/webp":1 };
|
|
1739
|
+
var historyItems = [];
|
|
1740
|
+
var hiddenHistoryKeys = {};
|
|
1741
|
+
var sourceHistoryItems = [];
|
|
1742
|
+
var sessionTracer = "";
|
|
1743
|
+
var tracerAutoPending = true;
|
|
1744
|
+
var maskSeq = 0;
|
|
1745
|
+
var isSeekingFor = null; // pending frame-capture continuation
|
|
1746
|
+
|
|
1747
|
+
// ===== small helpers =====
|
|
1748
|
+
function pad(n, w){ n = String(Math.max(0, Math.floor(n))); while(n.length < w) n = "0" + n; return n; }
|
|
1749
|
+
function formatTimecode(sec){
|
|
1750
|
+
var s = Math.max(0, Number(sec) || 0);
|
|
1751
|
+
var mm = Math.floor(s / 60);
|
|
1752
|
+
var ss = Math.floor(s % 60);
|
|
1753
|
+
var ms = Math.round((s - Math.floor(s)) * 1000);
|
|
1754
|
+
if(ms >= 1000){ ms -= 1000; ss += 1; }
|
|
1755
|
+
if(ss >= 60){ ss -= 60; mm += 1; }
|
|
1756
|
+
return pad(mm, 2) + ":" + pad(ss, 2) + ":" + pad(ms, 3);
|
|
1757
|
+
}
|
|
1758
|
+
function parseTimecode(str){
|
|
1759
|
+
var parts = String(str || "").trim().split(":");
|
|
1760
|
+
if(!parts.length) return NaN;
|
|
1761
|
+
var mm = 0, ss = 0, ms = 0;
|
|
1762
|
+
if(parts.length >= 3){ mm = parseInt(parts[0], 10) || 0; ss = parseInt(parts[1], 10) || 0; ms = parseInt(parts[2], 10) || 0; }
|
|
1763
|
+
else if(parts.length === 2){ mm = parseInt(parts[0], 10) || 0; ss = parseInt(parts[1], 10) || 0; }
|
|
1764
|
+
else { ss = parseInt(parts[0], 10) || 0; }
|
|
1765
|
+
return mm * 60 + ss + ms / 1000;
|
|
1766
|
+
}
|
|
1767
|
+
function absoluteUrl(value){ return new URL(value, window.location.href).toString(); }
|
|
1768
|
+
function parseJsonResponse(response){ return response.json().catch(function(){ return {}; }); }
|
|
1769
|
+
function setStatus(message, tone){ if(!statusEl) return; statusEl.textContent = message || ""; statusEl.setAttribute("data-tone", tone || ""); }
|
|
1770
|
+
function nowIso(){ return new Date().toISOString(); }
|
|
1771
|
+
function uid(prefix){
|
|
1772
|
+
if(globalThis.crypto && globalThis.crypto.randomUUID) return prefix + globalThis.crypto.randomUUID().replace(/-/g, "").slice(0, 8);
|
|
1773
|
+
return prefix + Math.random().toString(16).slice(2, 10);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// ===== tracer =====
|
|
1777
|
+
function tracerSuffix(){
|
|
1778
|
+
return (globalThis.crypto && globalThis.crypto.randomUUID)
|
|
1779
|
+
? globalThis.crypto.randomUUID().replace(/-/g, "").slice(0, 6)
|
|
1780
|
+
: Math.random().toString(16).slice(2, 8);
|
|
1781
|
+
}
|
|
1782
|
+
function slugifyTracer(value){
|
|
1783
|
+
var normalized = String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
1784
|
+
return normalized || "video-edit";
|
|
1785
|
+
}
|
|
1786
|
+
function nameTracerFromSeed(seed){
|
|
1787
|
+
var value = "video-edit-" + slugifyTracer(seed) + "-" + tracerSuffix();
|
|
1788
|
+
sessionTracer = value; tracerAutoPending = false; if(tracerInput) tracerInput.value = value; return value;
|
|
1789
|
+
}
|
|
1790
|
+
function resolveTracer(){ if(sessionTracer) return sessionTracer; var v = tracerInput ? tracerInput.value.trim() : ""; return v; }
|
|
1791
|
+
function getTracerValue(){ if(sessionTracer) return sessionTracer; var v = tracerInput ? tracerInput.value.trim() : ""; if(v){ sessionTracer = v; tracerAutoPending = false; return v; } return nameTracerFromSeed("video-edit"); }
|
|
1792
|
+
|
|
1793
|
+
// ===== provider list =====
|
|
1794
|
+
function loadProviders(){
|
|
1795
|
+
fetch("/api/v1/user/me/provider-keys", { credentials: "same-origin" }).then(parseJsonResponse).then(function(payload){
|
|
1796
|
+
var supported = { openai:1, gemini:1, openrouter:1 };
|
|
1797
|
+
var providers = []; var seen = {};
|
|
1798
|
+
var keys = (payload && payload.provider_keys) || [];
|
|
1799
|
+
for(var i = 0; i < keys.length; i++){
|
|
1800
|
+
var key = keys[i]; var provider = String(key.provider || ""); var stat = String(key.status || "active");
|
|
1801
|
+
var cd = key.cooldown_until ? Date.parse(String(key.cooldown_until)) : 0;
|
|
1802
|
+
if(!supported[provider] || stat !== "active" || (cd && cd > Date.now()) || seen[provider]) continue;
|
|
1803
|
+
seen[provider] = 1; providers.push(provider);
|
|
1804
|
+
}
|
|
1805
|
+
if(providerSelect){
|
|
1806
|
+
providerSelect.innerHTML = '<option value="">Auto</option>' + providers.map(function(p){ return '<option value="' + p + '">' + p.charAt(0).toUpperCase() + p.slice(1) + '</option>'; }).join("");
|
|
1807
|
+
}
|
|
1808
|
+
if(providerWrap) providerWrap.setAttribute("data-visible", providers.length > 1 ? "true" : "false");
|
|
1809
|
+
}).catch(function(){});
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// ===== upload / save =====
|
|
1813
|
+
function uploadTemporaryBlob(blob, fileName, contentType, folderPath){
|
|
1814
|
+
var folder = folderPath || "inpaints/_inputs";
|
|
1815
|
+
return fetch("/api/v1/user/me/temporary-files/presign", {
|
|
1816
|
+
method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" },
|
|
1817
|
+
body: JSON.stringify({ file_name: fileName, content_type: contentType, size_bytes: blob.size, folder_path: folder })
|
|
1818
|
+
}).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
|
|
1819
|
+
var presign = res.p;
|
|
1820
|
+
if(!res.ok || !presign || !presign.transport) throw new Error((presign && presign.error) || "Unable to prepare upload.");
|
|
1821
|
+
if(presign.transport === "server"){
|
|
1822
|
+
var formData = new FormData();
|
|
1823
|
+
formData.append((presign.upload && presign.upload.form_field) || "file", new File([blob], fileName, { type: contentType }));
|
|
1824
|
+
formData.append("folder_path", folder);
|
|
1825
|
+
return fetch((presign.upload && presign.upload.url) || "/api/v1/user/me/temporary-files/upload", { method: (presign.upload && presign.upload.method) || "POST", credentials: "same-origin", body: formData })
|
|
1826
|
+
.then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); })
|
|
1827
|
+
.then(function(u){ if(!u.ok || !u.p.file) throw new Error(u.p.error || "Unable to upload file."); return u.p.file.viewUrl || u.p.file.view_url || u.p.file.s3Url || u.p.file.s3_url; });
|
|
1828
|
+
}
|
|
1829
|
+
return fetch(presign.upload.url, { method: presign.upload.method, headers: presign.upload.headers, body: blob }).then(function(r){
|
|
1830
|
+
if(!r.ok) throw new Error("Upload failed with status " + r.status + ".");
|
|
1831
|
+
return fetch("/api/v1/user/me/temporary-files", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" },
|
|
1832
|
+
body: JSON.stringify({ file_id: presign.file_id, file_name: presign.file_name, content_type: presign.content_type, size_bytes: blob.size, storage_key: presign.storage_key, folder_path: presign.folder_path || folder }) })
|
|
1833
|
+
.then(function(r2){ return parseJsonResponse(r2).then(function(p){ return { ok: r2.ok, p: p }; }); })
|
|
1834
|
+
.then(function(f){ if(!f.ok || !f.p.file) throw new Error(f.p.error || "Unable to save file."); return f.p.file.viewUrl || f.p.file.view_url || f.p.file.s3Url || f.p.file.s3_url; });
|
|
1835
|
+
});
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
function saveMediaToFiles(mediaUrl, defaultName){
|
|
1839
|
+
if(!mediaUrl) return;
|
|
1840
|
+
var dir = window.__vidfarmDirectory;
|
|
1841
|
+
if(!dir || !dir.pick){ setStatus("File browser isn't available right now.", "error"); return; }
|
|
1842
|
+
dir.pick({ select: "folder", roots: ["files"], initialPath: "/files", title: "Save to My Files", hint: "Open a folder in your Files, then choose it to save this video there.", confirmLabel: "Save here",
|
|
1843
|
+
onPick: function(folder){
|
|
1844
|
+
setStatus("Saving to " + (folder && folder.path ? folder.path : "My Files") + "\u2026", "");
|
|
1845
|
+
fetch("/api/v1/user/me/attachments/from-url", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" },
|
|
1846
|
+
body: JSON.stringify({ source_url: mediaUrl, folder_path: (folder && folder.folderPath) || "", file_name: defaultName || "" }) })
|
|
1847
|
+
.then(function(r){ return r.json().then(function(j){ return { ok: r.ok, j: j }; }); })
|
|
1848
|
+
.then(function(res){ if(!res.ok || !res.j || res.j.error) throw new Error((res.j && res.j.error) || "Couldn't save to My Files."); setStatus("Saved to " + (folder && folder.path ? folder.path : "My Files") + ".", "success"); })
|
|
1849
|
+
.catch(function(err){ setStatus(err && err.message ? err.message : "Couldn't save to My Files.", "error"); });
|
|
1850
|
+
} });
|
|
1851
|
+
}
|
|
1852
|
+
function copyText(value){
|
|
1853
|
+
if(navigator.clipboard && window.isSecureContext){ return navigator.clipboard.writeText(value).then(function(){ return true; }).catch(function(){ return fallbackCopy(value); }); }
|
|
1854
|
+
return Promise.resolve(fallbackCopy(value));
|
|
1855
|
+
}
|
|
1856
|
+
function fallbackCopy(value){
|
|
1857
|
+
var field = document.createElement("textarea"); field.value = value; field.setAttribute("readonly", ""); field.style.position = "fixed"; field.style.opacity = "0";
|
|
1858
|
+
document.body.appendChild(field); field.select(); var ok = false; try { ok = document.execCommand("copy"); } catch(_){} field.remove(); return ok;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// ===== masks =====
|
|
1862
|
+
function activeMask(){ for(var i = 0; i < masks.length; i++){ if(masks[i].id === activeMaskId) return masks[i]; } return null; }
|
|
1863
|
+
function normalizeHex(v){ v = String(v || "").trim(); if(/^#[0-9a-fA-F]{6}$/.test(v)) return v.toLowerCase(); if(/^#[0-9a-fA-F]{3}$/.test(v)){ return ("#" + v[1] + v[1] + v[2] + v[2] + v[3] + v[3]).toLowerCase(); } return "#ff2d55"; }
|
|
1864
|
+
var PALETTE = ["#ff2d55","#0a84ff","#34c759","#ff9f0a","#bf5af2","#ff375f","#5ac8fa","#ffd60a"];
|
|
1865
|
+
function nextColor(){ var used = {}; masks.forEach(function(m){ used[m.color] = 1; }); for(var i = 0; i < PALETTE.length; i++){ if(!used[PALETTE[i]]) return PALETTE[i]; } return PALETTE[masks.length % PALETTE.length]; }
|
|
1866
|
+
function addMask(atTime, color){
|
|
1867
|
+
var m = { id: uid("mask_"), color: normalizeHex(color || nextColor()), prompt: "", timeSec: (atTime != null ? atTime : (videoEl.currentTime || 0)), strokes: [], redo: [] };
|
|
1868
|
+
masks.push(m); activeMaskId = m.id; if(colorPicker) colorPicker.value = m.color; renderMasks(); return m;
|
|
1869
|
+
}
|
|
1870
|
+
function removeMask(id){
|
|
1871
|
+
masks = masks.filter(function(m){ return m.id !== id; });
|
|
1872
|
+
if(activeMaskId === id) activeMaskId = masks.length ? masks[masks.length - 1].id : "";
|
|
1873
|
+
renderMasks(); redraw();
|
|
1874
|
+
}
|
|
1875
|
+
function renderMasks(){
|
|
1876
|
+
masksList.innerHTML = "";
|
|
1877
|
+
if(!masks.length){
|
|
1878
|
+
var empty = document.createElement("div"); empty.className = "rk-ipv-masks-empty";
|
|
1879
|
+
empty.textContent = "No masks yet. Paint on the video, or use + Add timed mask, to mark a region at a moment.";
|
|
1880
|
+
masksList.appendChild(empty); syncUndoRedo(); return;
|
|
1881
|
+
}
|
|
1882
|
+
masks.forEach(function(m){
|
|
1883
|
+
var row = document.createElement("div"); row.className = "rk-ipv-mask"; row.dataset.maskId = m.id; row.setAttribute("data-active", m.id === activeMaskId ? "true" : "false");
|
|
1884
|
+
var dot = document.createElement("button"); dot.type = "button"; dot.className = "rk-ipv-mask-dot"; dot.style.setProperty("--ipv-c", m.color); dot.dataset.maskSelect = m.id; dot.title = "Select this mask to paint";
|
|
1885
|
+
var bodyEl = document.createElement("div"); bodyEl.className = "rk-ipv-mask-body";
|
|
1886
|
+
var rowTop = document.createElement("div"); rowTop.className = "rk-ipv-mask-row";
|
|
1887
|
+
var timeBtn = document.createElement("button"); timeBtn.type = "button"; timeBtn.className = "rk-ipv-mask-time"; timeBtn.dataset.maskSeek = m.id; timeBtn.title = "Jump the video to this moment"; timeBtn.innerHTML = CLOCK_SVG + "<span>" + formatTimecode(m.timeSec) + "</span>";
|
|
1888
|
+
var setBtn = document.createElement("button"); setBtn.type = "button"; setBtn.className = "rk-ipv-mask-mini"; setBtn.dataset.maskSettime = m.id; setBtn.title = "Set this mask to the current playhead"; setBtn.innerHTML = "<svg width='13' height='13' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M10 4v12'/><path d='m6 8 4-4 4 4'/></svg>";
|
|
1889
|
+
var delBtn = document.createElement("button"); delBtn.type = "button"; delBtn.className = "rk-ipv-mask-mini is-danger"; delBtn.dataset.maskRemove = m.id; delBtn.title = "Remove this mask"; delBtn.innerHTML = "<svg width='12' height='12' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M5 5l10 10M15 5 5 15'/></svg>";
|
|
1890
|
+
rowTop.appendChild(timeBtn); rowTop.appendChild(setBtn); rowTop.appendChild(delBtn);
|
|
1891
|
+
var ta = document.createElement("textarea"); ta.className = "rk-ipv-mask-prompt"; ta.dataset.maskPrompt = m.id; ta.placeholder = "What changes in this region at this moment?"; ta.value = m.prompt || "";
|
|
1892
|
+
bodyEl.appendChild(rowTop); bodyEl.appendChild(ta);
|
|
1893
|
+
row.appendChild(dot); row.appendChild(bodyEl); masksList.appendChild(row);
|
|
1894
|
+
});
|
|
1895
|
+
syncUndoRedo();
|
|
1896
|
+
}
|
|
1897
|
+
function selectMask(id){ activeMaskId = id; var m = activeMask(); if(m && colorPicker) colorPicker.value = m.color; renderMasks(); redraw(); }
|
|
1898
|
+
function syncUndoRedo(){ var m = activeMask(); if(undoBtn) undoBtn.disabled = !m || !m.strokes.length; if(redoBtn) redoBtn.disabled = !m || !m.redo.length; }
|
|
1899
|
+
|
|
1900
|
+
// ===== canvas / painting =====
|
|
1901
|
+
function syncCanvasSize(){
|
|
1902
|
+
var w = Math.round(videoEl.clientWidth), h = Math.round(videoEl.clientHeight);
|
|
1903
|
+
if(w > 0 && h > 0 && (canvas.width !== w || canvas.height !== h)){ canvas.width = w; canvas.height = h; }
|
|
1904
|
+
updateBrushCursorSize();
|
|
1905
|
+
}
|
|
1906
|
+
function updateBrushCursorSize(){
|
|
1907
|
+
if(!brushCursor) return; var size = Number(brushInput ? brushInput.value : 70) || 70;
|
|
1908
|
+
var rect = canvas.getBoundingClientRect(); var scale = rect.width && canvas.width ? rect.width / canvas.width : 1;
|
|
1909
|
+
canvasWrap.style.setProperty("--inpaint-brush-cursor-size", (size * scale) + "px");
|
|
1910
|
+
}
|
|
1911
|
+
function updateBrushCursorPos(event){
|
|
1912
|
+
if(!brushCursor) return; var rect = canvasWrap.getBoundingClientRect();
|
|
1913
|
+
canvasWrap.style.setProperty("--inpaint-brush-cursor-x", (event.clientX - rect.left) + "px");
|
|
1914
|
+
canvasWrap.style.setProperty("--inpaint-brush-cursor-y", (event.clientY - rect.top) + "px");
|
|
1915
|
+
brushCursor.setAttribute("data-visible", "true");
|
|
1916
|
+
}
|
|
1917
|
+
function hideBrushCursor(){ if(brushCursor) brushCursor.setAttribute("data-visible", "false"); }
|
|
1918
|
+
function getPoint(event){
|
|
1919
|
+
var rect = canvas.getBoundingClientRect();
|
|
1920
|
+
return { x: Math.max(0, Math.min(1, (event.clientX - rect.left) / (rect.width || 1))), y: Math.max(0, Math.min(1, (event.clientY - rect.top) / (rect.height || 1))) };
|
|
1921
|
+
}
|
|
1922
|
+
var VISIBILITY_WINDOW = 0.4; // seconds either side of a mask's moment
|
|
1923
|
+
function drawStrokes(strokes, color, alpha){
|
|
1924
|
+
var ctx = canvas.getContext("2d"); if(!ctx) return;
|
|
1925
|
+
ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.globalAlpha = alpha; ctx.strokeStyle = color;
|
|
1926
|
+
for(var s = 0; s < strokes.length; s++){
|
|
1927
|
+
var stroke = strokes[s]; if(!stroke.points || !stroke.points.length) continue;
|
|
1928
|
+
ctx.lineWidth = stroke.size; ctx.beginPath();
|
|
1929
|
+
ctx.moveTo(stroke.points[0].x * canvas.width, stroke.points[0].y * canvas.height);
|
|
1930
|
+
if(stroke.points.length === 1) ctx.lineTo(stroke.points[0].x * canvas.width + 0.01, stroke.points[0].y * canvas.height + 0.01);
|
|
1931
|
+
else for(var i = 1; i < stroke.points.length; i++) ctx.lineTo(stroke.points[i].x * canvas.width, stroke.points[i].y * canvas.height);
|
|
1932
|
+
ctx.stroke();
|
|
1933
|
+
}
|
|
1934
|
+
ctx.globalAlpha = 1;
|
|
1935
|
+
}
|
|
1936
|
+
function redraw(){
|
|
1937
|
+
var ctx = canvas.getContext("2d"); if(!ctx) return;
|
|
1938
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
1939
|
+
var t = videoEl.currentTime || 0;
|
|
1940
|
+
for(var i = 0; i < masks.length; i++){
|
|
1941
|
+
var m = masks[i]; if(!m.strokes.length) continue;
|
|
1942
|
+
var isActive = m.id === activeMaskId;
|
|
1943
|
+
var inWindow = Math.abs(m.timeSec - t) <= VISIBILITY_WINDOW;
|
|
1944
|
+
if(isActive || inWindow) drawStrokes(m.strokes, m.color, isActive ? 0.9 : 0.55);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
function beginStroke(event){
|
|
1948
|
+
if(!sourceVideoUrl || submitBtn.disabled) return;
|
|
1949
|
+
event.preventDefault();
|
|
1950
|
+
if(!videoEl.paused) videoEl.pause();
|
|
1951
|
+
var m = activeMask();
|
|
1952
|
+
if(!m) m = addMask(videoEl.currentTime || 0);
|
|
1953
|
+
try { canvas.setPointerCapture(event.pointerId); } catch(_){}
|
|
1954
|
+
isPainting = true;
|
|
1955
|
+
var stroke = { size: Number(brushInput ? brushInput.value : 70) || 70, points: [getPoint(event)] };
|
|
1956
|
+
m.strokes.push(stroke); m.redo = []; redraw(); syncUndoRedo();
|
|
1957
|
+
}
|
|
1958
|
+
function continueStroke(event){
|
|
1959
|
+
updateBrushCursorPos(event);
|
|
1960
|
+
if(!isPainting) return; event.preventDefault();
|
|
1961
|
+
var m = activeMask(); if(!m || !m.strokes.length) return;
|
|
1962
|
+
m.strokes[m.strokes.length - 1].points.push(getPoint(event)); redraw();
|
|
1963
|
+
}
|
|
1964
|
+
function endStroke(event){ if(!isPainting) return; isPainting = false; try { canvas.releasePointerCapture(event.pointerId); } catch(_){} }
|
|
1965
|
+
|
|
1966
|
+
// ===== source video =====
|
|
1967
|
+
function clearSourceObjectUrl(){ if(sourceObjectUrl){ try { URL.revokeObjectURL(sourceObjectUrl); } catch(_){} sourceObjectUrl = ""; } }
|
|
1968
|
+
function sourceNameFromUrl(value){ try { var p = new URL(value, window.location.href).pathname; return decodeURIComponent((p.split("/").filter(Boolean).pop()) || "") || "Source video"; } catch(_){ return "Source video"; } }
|
|
1969
|
+
var crossOriginRetried = false;
|
|
1970
|
+
function loadSourceUrl(value, name){
|
|
1971
|
+
if(!value) return;
|
|
1972
|
+
var url; try { url = absoluteUrl(value); } catch(_){ setStatus("That video URL is invalid.", "error"); return; }
|
|
1973
|
+
clearSourceObjectUrl();
|
|
1974
|
+
sourceVideoUrl = url; sourceVideoName = name || sourceNameFromUrl(url);
|
|
1975
|
+
emptyState.hidden = true; if(player) player.hidden = false; outputPanel.hidden = true;
|
|
1976
|
+
crossOriginRetried = false;
|
|
1977
|
+
// A cross-origin source is opened with crossorigin=anonymous so "Use current"
|
|
1978
|
+
// frame-capture can read pixels when the host sends CORS headers; if the load
|
|
1979
|
+
// errors (no CORS), we retry once WITHOUT it so playback still works (frame
|
|
1980
|
+
// capture then falls back to a manual upload).
|
|
1981
|
+
var sameOrigin = false; try { sameOrigin = new URL(url).origin === window.location.origin; } catch(_){}
|
|
1982
|
+
if(sameOrigin) videoEl.removeAttribute("crossorigin"); else videoEl.setAttribute("crossorigin", "anonymous");
|
|
1983
|
+
videoEl.addEventListener("loadedmetadata", function(){ syncCanvasSize(); if(durationEl) durationEl.textContent = "/ " + formatTimecode(videoEl.duration || 0); redraw(); }, { once: true });
|
|
1984
|
+
videoEl.onerror = function(){ if(!crossOriginRetried && videoEl.getAttribute("crossorigin")){ crossOriginRetried = true; videoEl.removeAttribute("crossorigin"); videoEl.src = url; videoEl.load(); } };
|
|
1985
|
+
videoEl.src = url;
|
|
1986
|
+
videoEl.load();
|
|
1987
|
+
setStatus("Video ready. Scrub the timeline and paint timed masks.", "success");
|
|
1988
|
+
}
|
|
1989
|
+
function loadSourceFile(file){
|
|
1990
|
+
if(!file) return;
|
|
1991
|
+
clearSourceObjectUrl();
|
|
1992
|
+
sourceObjectUrl = URL.createObjectURL(file);
|
|
1993
|
+
sourceVideoUrl = sourceObjectUrl; sourceVideoName = file.name || "Uploaded video";
|
|
1994
|
+
emptyState.hidden = true; if(player) player.hidden = false; outputPanel.hidden = true;
|
|
1995
|
+
videoEl.removeAttribute("crossorigin"); // local blob doesn't need CORS and avoids taint
|
|
1996
|
+
videoEl.src = sourceObjectUrl;
|
|
1997
|
+
videoEl.addEventListener("loadedmetadata", function(){ syncCanvasSize(); if(durationEl) durationEl.textContent = "/ " + formatTimecode(videoEl.duration || 0); redraw(); }, { once: true });
|
|
1998
|
+
videoEl.load();
|
|
1999
|
+
setStatus("Video loaded. Scrub the timeline and paint timed masks.", "success");
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// ===== start/end frame slots =====
|
|
2003
|
+
var pendingFrameSlot = null;
|
|
2004
|
+
function setFrame(which, url, name){
|
|
2005
|
+
frames[which] = { url: url, name: name || "" };
|
|
2006
|
+
var img = document.querySelector('[data-ipv-frame-img="' + which + '"]');
|
|
2007
|
+
var ph = document.querySelector('[data-ipv-frame-ph="' + which + '"]');
|
|
2008
|
+
if(img){ img.src = url; img.hidden = false; }
|
|
2009
|
+
if(ph) ph.style.display = "none";
|
|
2010
|
+
}
|
|
2011
|
+
function captureCurrentFrame(which){
|
|
2012
|
+
if(!sourceVideoUrl){ setStatus("Load a video first, then grab a frame.", "error"); return; }
|
|
2013
|
+
var vw = videoEl.videoWidth, vh = videoEl.videoHeight;
|
|
2014
|
+
if(!vw || !vh){ setStatus("The video isn't ready yet — try again in a moment.", "error"); return; }
|
|
2015
|
+
var off = document.createElement("canvas"); off.width = vw; off.height = vh;
|
|
2016
|
+
var ctx = off.getContext("2d"); if(!ctx) return;
|
|
2017
|
+
try { ctx.drawImage(videoEl, 0, 0, vw, vh); } catch(err){ setStatus("Couldn't read this frame (the video blocks cross-origin capture). Upload a frame instead.", "error"); return; }
|
|
2018
|
+
setStatus("Saving " + which + " frame\u2026", "");
|
|
2019
|
+
off.toBlob(function(blob){
|
|
2020
|
+
if(!blob){ setStatus("Couldn't capture that frame — upload a frame instead.", "error"); return; }
|
|
2021
|
+
uploadTemporaryBlob(blob, which + "-frame-" + Date.now() + ".png", "image/png").then(function(uploaded){
|
|
2022
|
+
setFrame(which, absoluteUrl(uploaded), which + " frame"); setStatus((which === "first" ? "Start" : "End") + " frame set from the current playhead.", "success");
|
|
2023
|
+
}).catch(function(err){ setStatus(err && err.message ? err.message : "Couldn't save the frame.", "error"); });
|
|
2024
|
+
}, "image/png");
|
|
2025
|
+
}
|
|
2026
|
+
function uploadFrameFile(which, file){
|
|
2027
|
+
if(!file) return;
|
|
2028
|
+
setStatus("Uploading " + which + " frame\u2026", "");
|
|
2029
|
+
uploadTemporaryBlob(file, file.name || (which + "-frame.png"), file.type || "image/png").then(function(uploaded){
|
|
2030
|
+
setFrame(which, absoluteUrl(uploaded), file.name || (which + " frame")); setStatus((which === "first" ? "Start" : "End") + " frame set.", "success");
|
|
2031
|
+
}).catch(function(err){ setStatus(err && err.message ? err.message : "Couldn't upload the frame.", "error"); });
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// ===== attachments (global prompt refs) =====
|
|
2035
|
+
function renderAttachChips(){
|
|
2036
|
+
if(!attachChips) return; attachChips.innerHTML = "";
|
|
2037
|
+
attachments.forEach(function(att){
|
|
2038
|
+
var chip = document.createElement("span"); chip.className = "editor-inpaint-attachment-chip";
|
|
2039
|
+
var img = document.createElement("img"); img.src = att.url; img.alt = ""; chip.appendChild(img);
|
|
2040
|
+
var label = document.createElement("span"); label.textContent = att.fileName || "reference"; chip.appendChild(label);
|
|
2041
|
+
var x = document.createElement("button"); x.type = "button"; x.textContent = "\u00d7"; x.setAttribute("aria-label", "Remove reference"); x.addEventListener("click", function(){ attachments = attachments.filter(function(a){ return a.id !== att.id; }); renderAttachChips(); });
|
|
2042
|
+
chip.appendChild(x); attachChips.appendChild(chip);
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
function addAttachmentFiles(files){
|
|
2046
|
+
var selected = Array.prototype.slice.call(files || []); if(!selected.length) return;
|
|
2047
|
+
if(attachments.length + selected.length > maxAttachments){ setStatus("Use up to " + maxAttachments + " reference images.", "error"); return; }
|
|
2048
|
+
setStatus("Uploading reference image\u2026", "");
|
|
2049
|
+
var chain = Promise.resolve();
|
|
2050
|
+
selected.forEach(function(file){
|
|
2051
|
+
chain = chain.then(function(){
|
|
2052
|
+
if(!allowedAttachTypes[file.type]) throw new Error("Reference attachments must be JPEG, PNG, or WebP.");
|
|
2053
|
+
return uploadTemporaryBlob(file, file.name || "reference.png", file.type || "image/png").then(function(uploaded){
|
|
2054
|
+
attachments.push({ id: uid("att_"), fileName: file.name || "reference.png", url: absoluteUrl(uploaded) });
|
|
2055
|
+
});
|
|
2056
|
+
});
|
|
2057
|
+
});
|
|
2058
|
+
chain.then(function(){ renderAttachChips(); setStatus("Reference image ready.", "success"); }).catch(function(err){ setStatus(err && err.message ? err.message : "Unable to upload reference image.", "error"); });
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// ===== instruction compiler =====
|
|
2062
|
+
function regionLabel(mask){
|
|
2063
|
+
if(!mask.strokes.length) return "";
|
|
2064
|
+
var minX = 1, minY = 1, maxX = 0, maxY = 0, any = false;
|
|
2065
|
+
mask.strokes.forEach(function(s){ s.points.forEach(function(p){ any = true; if(p.x < minX) minX = p.x; if(p.y < minY) minY = p.y; if(p.x > maxX) maxX = p.x; if(p.y > maxY) maxY = p.y; }); });
|
|
2066
|
+
if(!any) return "";
|
|
2067
|
+
var cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
|
|
2068
|
+
var h = cx < 0.34 ? "left" : (cx > 0.66 ? "right" : "center");
|
|
2069
|
+
var v = cy < 0.34 ? "top" : (cy > 0.66 ? "bottom" : "middle");
|
|
2070
|
+
if(v === "middle" && h === "center") return "center";
|
|
2071
|
+
if(v === "middle") return h;
|
|
2072
|
+
if(h === "center") return v;
|
|
2073
|
+
return v + "-" + h;
|
|
2074
|
+
}
|
|
2075
|
+
function buildVideoInstruction(){
|
|
2076
|
+
var whole = globalPrompt ? globalPrompt.value.trim() : "";
|
|
2077
|
+
var timed = masks.map(function(m){ return { time: m.timeSec, region: regionLabel(m), prompt: String(m.prompt || "").trim() }; }).filter(function(x){ return x.prompt; });
|
|
2078
|
+
if(!whole && !timed.length) return "";
|
|
2079
|
+
var attNotes = attachments.map(function(a, i){ return "reference image " + (i + 1) + " (" + (a.fileName || "attachment") + ")"; });
|
|
2080
|
+
var lines = [
|
|
2081
|
+
"VIDEO EDITING BRIEF:",
|
|
2082
|
+
"You are producing an edited version of an existing clip. Preserve the overall subject, framing, style, pacing, and everything not explicitly changed below; apply only the described edits.",
|
|
2083
|
+
whole ? ("Whole clip: " + whole + ".") : "",
|
|
2084
|
+
timed.length ? ("Timed region edits: " + timed.map(function(x){ return "at " + formatTimecode(x.time) + (x.region ? (" in the " + x.region + " of the frame") : "") + ", " + x.prompt; }).join("; ") + ".") : "",
|
|
2085
|
+
"Match the surrounding lighting, motion, grain, and perspective so each edit blends seamlessly across the given timecodes.",
|
|
2086
|
+
attNotes.length ? ("Reference images: " + attNotes.join("; ") + ". Use each only as visual guidance.") : ""
|
|
2087
|
+
];
|
|
2088
|
+
return lines.filter(Boolean).join(" ");
|
|
2089
|
+
}
|
|
2090
|
+
function getAspectRatio(){
|
|
2091
|
+
var w = videoEl.videoWidth || 0, h = videoEl.videoHeight || 0;
|
|
2092
|
+
if(!w || !h){
|
|
2093
|
+
// fall back to a frame image's ratio isn't trivial; default to 16:9
|
|
2094
|
+
return "16:9";
|
|
2095
|
+
}
|
|
2096
|
+
var r = w / h;
|
|
2097
|
+
if(Math.abs(r - 1) < 0.08) return "1:1";
|
|
2098
|
+
if(r > 1) return r >= 1.55 ? "16:9" : "4:3";
|
|
2099
|
+
return r <= 0.65 ? "9:16" : "3:4";
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// ===== output extraction / history =====
|
|
2103
|
+
function extractVideoUrl(payload){
|
|
2104
|
+
var result = (payload && payload.result) || {};
|
|
2105
|
+
var out = result.output || {};
|
|
2106
|
+
var candidate = (out.video && out.video.file_url) || out.primary_file_url || (result.video && result.video.file_url) || result.primary_file_url || (Array.isArray(out.files) ? out.files[0] : "") || (Array.isArray(result.files) ? result.files[0] : "");
|
|
2107
|
+
return candidate || "";
|
|
2108
|
+
}
|
|
2109
|
+
function isPendingStatus(status){ return ["queued","running","waiting_for_provider","waiting_for_render","waiting_for_media","waiting_for_child","waiting_for_human"].indexOf(String(status || "")) >= 0; }
|
|
2110
|
+
function pendingKey(jobId){ return "job:" + String(jobId || ""); }
|
|
2111
|
+
function makePending(input){
|
|
2112
|
+
var jobId = String((input && (input.jobId || input.job_id)) || ""); if(!jobId) return null;
|
|
2113
|
+
return { key: pendingKey(jobId), kind: "pending", jobId: jobId, status: String((input && input.status) || "queued"), progress: Number((input && input.progress) || 0), createdAt: String((input && (input.createdAt || input.created_at)) || nowIso()) };
|
|
2114
|
+
}
|
|
2115
|
+
function upsertHistory(item){
|
|
2116
|
+
if(!item || !item.key) return;
|
|
2117
|
+
historyItems = historyItems.filter(function(e){ return e.key !== item.key && (!item.jobId || e.jobId !== item.jobId); });
|
|
2118
|
+
historyItems.unshift(item);
|
|
2119
|
+
historyItems.sort(function(a, b){ return Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0); });
|
|
2120
|
+
renderHistory();
|
|
2121
|
+
}
|
|
2122
|
+
function recordSourceHistoryItem(url, name){
|
|
2123
|
+
if(!url) return; var abs; try { abs = absoluteUrl(url); } catch(_){ return; }
|
|
2124
|
+
var key = "source:" + abs;
|
|
2125
|
+
var item = null;
|
|
2126
|
+
for(var i = 0; i < sourceHistoryItems.length; i++){ if(sourceHistoryItems[i].key === key){ item = sourceHistoryItems[i]; break; } }
|
|
2127
|
+
if(!item){ item = { key: key, kind: "source", url: abs, name: name || sourceNameFromUrl(abs), createdAt: nowIso() }; sourceHistoryItems.push(item); }
|
|
2128
|
+
upsertHistory(item);
|
|
2129
|
+
}
|
|
2130
|
+
function reinjectSources(){ sourceHistoryItems.forEach(function(item){ if(!historyItems.some(function(e){ return e.key === item.key; })) historyItems.push(item); }); historyItems.sort(function(a, b){ return Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0); }); }
|
|
2131
|
+
function videoDownloadName(prefix, createdAt){ var d = createdAt ? new Date(createdAt) : new Date(); return prefix + "-" + d.getTime() + ".mp4"; }
|
|
2132
|
+
function setHistoryEmpty(message){ historyList.innerHTML = ""; var e = document.createElement("div"); e.className = "custom-inpaint-history-empty"; e.textContent = message; historyList.appendChild(e); }
|
|
2133
|
+
function renderHistory(){
|
|
2134
|
+
historyList.innerHTML = "";
|
|
2135
|
+
var visible = historyItems.filter(function(item){ return !hiddenHistoryKeys[item.key]; });
|
|
2136
|
+
if(!visible.length){ setHistoryEmpty("No generated videos yet."); return; }
|
|
2137
|
+
visible.forEach(function(item){
|
|
2138
|
+
var row = document.createElement("div"); row.className = "custom-inpaint-history-item"; row.dataset.kind = item.kind;
|
|
2139
|
+
var thumb;
|
|
2140
|
+
if(item.kind === "pending"){ thumb = document.createElement("div"); thumb.className = "custom-inpaint-history-generating"; thumb.setAttribute("role", "img"); thumb.setAttribute("aria-label", "Video generating"); }
|
|
2141
|
+
else {
|
|
2142
|
+
thumb = document.createElement("button"); thumb.type = "button"; thumb.className = "custom-inpaint-history-thumb-button"; thumb.dataset.historyPreview = item.key; thumb.setAttribute("aria-label", "Preview video");
|
|
2143
|
+
var v = document.createElement("video"); v.className = "custom-inpaint-history-thumb"; v.src = item.url + "#t=0.1"; v.muted = true; v.preload = "metadata"; v.playsInline = true; thumb.appendChild(v);
|
|
2144
|
+
var badge = document.createElement("span"); badge.className = "custom-inpaint-history-thumb-badge"; badge.innerHTML = PLAY_SVG; thumb.appendChild(badge);
|
|
2145
|
+
}
|
|
2146
|
+
var meta = document.createElement("div"); meta.className = "custom-inpaint-history-meta";
|
|
2147
|
+
var title = document.createElement("div"); title.className = "custom-inpaint-history-title";
|
|
2148
|
+
title.textContent = item.kind === "pending" ? "Generating video" : item.kind === "source" ? "Source video" : (item.createdAt ? new Date(item.createdAt).toLocaleString() : "Generated video");
|
|
2149
|
+
meta.appendChild(title);
|
|
2150
|
+
if(item.kind === "pending"){ var sub = document.createElement("div"); sub.className = "custom-inpaint-history-subtitle"; var pr = Number(item.progress || 0); sub.textContent = "Job " + (item.status || "queued") + (pr > 0 ? " \u00b7 " + Math.round(pr * 100) + "%" : ""); meta.appendChild(sub); }
|
|
2151
|
+
else if(item.kind === "source"){ var sub2 = document.createElement("div"); sub2.className = "custom-inpaint-history-subtitle"; sub2.textContent = "Where you started" + (item.createdAt ? " \u00b7 " + new Date(item.createdAt).toLocaleString() : ""); meta.appendChild(sub2); }
|
|
2152
|
+
var actions = document.createElement("div"); actions.className = "custom-inpaint-history-actions";
|
|
2153
|
+
if(item.kind === "pending"){ var check = document.createElement("button"); check.type = "button"; check.className = "custom-inpaint-history-check"; check.dataset.historyCheck = item.jobId || ""; check.textContent = "Check Status"; actions.appendChild(check); }
|
|
2154
|
+
else {
|
|
2155
|
+
var dl = document.createElement("a"); dl.className = "custom-inpaint-history-icon"; dl.href = item.url; dl.target = "_blank"; dl.rel = "noopener noreferrer"; dl.download = videoDownloadName("video", item.createdAt); dl.title = "Download"; dl.innerHTML = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 3v9\"/><path d=\"m6.5 8.5 3.5 3.5 3.5-3.5\"/><path d=\"M4 16h12\"/></svg>";
|
|
2156
|
+
var cp = document.createElement("button"); cp.type = "button"; cp.className = "custom-inpaint-history-icon"; cp.dataset.historyCopy = item.key; cp.title = "Copy URL"; cp.innerHTML = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8.4 11.6a3.2 3.2 0 0 0 4.5 0l2.2-2.2a3.2 3.2 0 0 0-4.5-4.5L9.4 6.1\"/><path d=\"M11.6 8.4a3.2 3.2 0 0 0-4.5 0L4.9 10.6a3.2 3.2 0 0 0 4.5 4.5l1.2-1.2\"/></svg>";
|
|
2157
|
+
var sv = document.createElement("button"); sv.type = "button"; sv.className = "custom-inpaint-history-icon"; sv.dataset.historySave = item.key; sv.title = "Save to Files"; sv.innerHTML = "<svg viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' aria-hidden='true'><path d='M5 4h8l2 2v10H5z'></path><path d='M8 4v4h4V4'></path><path d='M8 12h4'></path></svg>";
|
|
2158
|
+
actions.appendChild(dl); actions.appendChild(cp); actions.appendChild(sv);
|
|
2159
|
+
}
|
|
2160
|
+
var rm = document.createElement("button"); rm.type = "button"; rm.className = "custom-inpaint-history-icon"; rm.dataset.historyRemove = item.key; rm.title = "Remove"; rm.innerHTML = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 5l10 10\"/><path d=\"M15 5 5 15\"/></svg>"; actions.appendChild(rm);
|
|
2161
|
+
meta.appendChild(actions);
|
|
2162
|
+
var confirm = document.createElement("div"); confirm.className = "custom-inpaint-history-popconfirm"; confirm.hidden = true; confirm.dataset.historyConfirm = item.key;
|
|
2163
|
+
var ctext = document.createElement("p"); ctext.textContent = "Remove from this history strip?";
|
|
2164
|
+
var cacts = document.createElement("div"); cacts.className = "custom-inpaint-history-popconfirm-actions";
|
|
2165
|
+
var cancel = document.createElement("button"); cancel.type = "button"; cancel.dataset.historyCancel = item.key; cancel.textContent = "Cancel";
|
|
2166
|
+
var confirmRemove = document.createElement("button"); confirmRemove.type = "button"; confirmRemove.dataset.historyConfirmRemove = item.key; confirmRemove.dataset.danger = "true"; confirmRemove.textContent = "Remove";
|
|
2167
|
+
cacts.appendChild(cancel); cacts.appendChild(confirmRemove); confirm.appendChild(ctext); confirm.appendChild(cacts);
|
|
2168
|
+
row.appendChild(thumb); row.appendChild(meta); row.appendChild(confirm); historyList.appendChild(row);
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
function historyEntryUrls(entry){
|
|
2172
|
+
var urls = (entry && entry.outputMediaUrls) || [];
|
|
2173
|
+
return urls.filter(function(u){ return /\.(mp4|webm|mov|m4v)(\?|#|$)/i.test(String(u || "")); });
|
|
2174
|
+
}
|
|
2175
|
+
function isVideoEntry(entry){
|
|
2176
|
+
var tid = String((entry && entry.linkedJob && (entry.linkedJob.template_id || entry.linkedJob.templateId)) || "").toLowerCase();
|
|
2177
|
+
var path = String((entry && entry.path) || "").toLowerCase();
|
|
2178
|
+
return tid === "primitive:video_generate" || path.indexOf("/primitives/videos/generate") >= 0 || path.indexOf("/primitives/video_generate") >= 0;
|
|
2179
|
+
}
|
|
2180
|
+
function extractHistoryItems(entries){
|
|
2181
|
+
var items = []; var seen = {};
|
|
2182
|
+
(entries || []).forEach(function(entry){
|
|
2183
|
+
if(!isVideoEntry(entry)) return;
|
|
2184
|
+
var linked = entry.linkedJob || null;
|
|
2185
|
+
if(linked && isPendingStatus(linked.status)){
|
|
2186
|
+
var p = makePending({ job_id: linked.job_id, status: linked.status, progress: linked.progress, created_at: linked.created_at || entry.createdAt });
|
|
2187
|
+
if(p && !seen[p.key]){ seen[p.key] = 1; items.push(p); } return;
|
|
2188
|
+
}
|
|
2189
|
+
historyEntryUrls(entry).forEach(function(raw, index){
|
|
2190
|
+
var url; try { url = absoluteUrl(raw); } catch(_){ return; }
|
|
2191
|
+
var key = String(entry.jobId || entry.job_id || entry.id || "video") + ":" + index + ":" + url;
|
|
2192
|
+
if(seen[key]) return; seen[key] = 1;
|
|
2193
|
+
items.push({ key: key, kind: "output", jobId: String(entry.jobId || entry.job_id || (entry.linkedJob && entry.linkedJob.job_id) || ""), url: url, createdAt: String(entry.createdAt || nowIso()) });
|
|
2194
|
+
});
|
|
2195
|
+
});
|
|
2196
|
+
return items;
|
|
2197
|
+
}
|
|
2198
|
+
function loadHistory(){
|
|
2199
|
+
var tracer = resolveTracer();
|
|
2200
|
+
if(!tracer){ historyItems = []; reinjectSources(); renderHistory(); return; }
|
|
2201
|
+
if(historyRefresh) historyRefresh.disabled = true;
|
|
2202
|
+
var endpoint = new URL("/job-runs/history", window.location.origin);
|
|
2203
|
+
endpoint.searchParams.set("limit", "120"); endpoint.searchParams.set("template_id", "primitive:video_generate"); endpoint.searchParams.set("tracer", tracer);
|
|
2204
|
+
fetch(endpoint, { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
|
|
2205
|
+
if(!res.ok || (res.p && res.p.error)) throw new Error((res.p && res.p.error) || "Unable to load history.");
|
|
2206
|
+
historyItems = extractHistoryItems(res.p.entries); reinjectSources(); renderHistory();
|
|
2207
|
+
}).catch(function(err){ setHistoryEmpty(err && err.message ? err.message : "Unable to load history."); }).then(function(){ if(historyRefresh) historyRefresh.disabled = false; });
|
|
2208
|
+
}
|
|
2209
|
+
function applyCompletedJob(payload){
|
|
2210
|
+
var out = extractVideoUrl(payload); if(!out) return "";
|
|
2211
|
+
var url = absoluteUrl(out); var jobId = String((payload && (payload.job_id || payload.jobId)) || "");
|
|
2212
|
+
resultVideo.src = url; outputPanel.hidden = false;
|
|
2213
|
+
upsertHistory({ key: String(jobId || "video") + ":0:" + url, kind: "output", jobId: jobId, url: url, createdAt: String((payload && (payload.completed_at || payload.updated_at)) || nowIso()) });
|
|
2214
|
+
return url;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// ===== job status / polling =====
|
|
2218
|
+
function checkJobStatus(jobId, button){
|
|
2219
|
+
if(!jobId) return;
|
|
2220
|
+
if(button){ button.disabled = true; button.textContent = "Checking"; }
|
|
2221
|
+
fetch("/api/v1/primitives/video_generate/jobs/" + encodeURIComponent(jobId), { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
|
|
2222
|
+
var payload = res.p; if(!res.ok) throw new Error(payload.error || "Unable to check job.");
|
|
2223
|
+
if(payload.status === "failed" || payload.status === "cancelled") throw new Error((payload.error && payload.error.message) || ("Video job " + payload.status + "."));
|
|
2224
|
+
var out = extractVideoUrl(payload);
|
|
2225
|
+
if(payload.status === "succeeded" || payload.status === "complete" || out){ if(!applyCompletedJob(payload)) throw new Error("Completed without an output URL."); setStatus("Video ready.", "success"); return; }
|
|
2226
|
+
upsertHistory(makePending(payload)); setStatus("Video job " + jobId + " is " + (payload.status || "running") + ".", "");
|
|
2227
|
+
}).catch(function(err){ setStatus(err && err.message ? err.message : "Unable to check job.", "error"); }).then(function(){ if(button){ button.disabled = false; button.textContent = "Check Status"; } });
|
|
2228
|
+
}
|
|
2229
|
+
function pollJob(jobId){
|
|
2230
|
+
var attempt = 0;
|
|
2231
|
+
function step(){
|
|
2232
|
+
return fetch("/api/v1/primitives/video_generate/jobs/" + encodeURIComponent(jobId), { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
|
|
2233
|
+
var payload = res.p; if(!res.ok) throw new Error(payload.error || "Unable to poll job.");
|
|
2234
|
+
if(payload.status === "failed" || payload.status === "cancelled") throw new Error((payload.error && payload.error.message) || ("Video job " + payload.status + "."));
|
|
2235
|
+
var out = extractVideoUrl(payload);
|
|
2236
|
+
if(payload.status === "succeeded" || payload.status === "complete" || out){ if(!out) throw new Error("Completed without an output URL."); applyCompletedJob(payload); setStatus("Video ready.", "success"); return out; }
|
|
2237
|
+
upsertHistory(makePending(payload)); setStatus("Video job " + jobId + " is " + (payload.status || "running") + " (this can take a few minutes)\u2026", "");
|
|
2238
|
+
attempt++; if(attempt >= 120){ setStatus("Still running \u2014 use Check Status in History.", ""); return ""; }
|
|
2239
|
+
return new Promise(function(res2){ setTimeout(res2, attempt < 8 ? 2000 : 4000); }).then(step);
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
return step();
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
function frameImagesPayload(){
|
|
2246
|
+
var arr = [];
|
|
2247
|
+
if(frames.first && frames.first.url) arr.push({ image_url: frames.first.url, frame_type: "first_frame" });
|
|
2248
|
+
if(frames.last && frames.last.url) arr.push({ image_url: frames.last.url, frame_type: "last_frame" });
|
|
2249
|
+
return arr;
|
|
2250
|
+
}
|
|
2251
|
+
function setSubmitLoading(loading){ submitBtn.setAttribute("data-loading", loading ? "true" : "false"); submitBtn.disabled = loading; if(submitLabel) submitLabel.textContent = loading ? "Generating\u2026" : "Generate video"; }
|
|
2252
|
+
|
|
2253
|
+
function submitGenerate(){
|
|
2254
|
+
if(submitBtn.disabled) return;
|
|
2255
|
+
var instruction = buildVideoInstruction();
|
|
2256
|
+
if(!instruction){ setStatus("Add a video prompt or a mask instruction first.", "error"); return; }
|
|
2257
|
+
var frameImgs = frameImagesPayload();
|
|
2258
|
+
setSubmitLoading(true);
|
|
2259
|
+
var seed = (globalPrompt && globalPrompt.value.trim()) || (masks.find(function(m){ return String(m.prompt || "").trim(); }) || {}).prompt || "video-edit";
|
|
2260
|
+
var tracer = tracerAutoPending ? nameTracerFromSeed(seed) : getTracerValue();
|
|
2261
|
+
var payload = { prompt: instruction, output_format: "mp4", aspect_ratio: getAspectRatio() };
|
|
2262
|
+
if(frameImgs.length) payload.frame_images = frameImgs;
|
|
2263
|
+
if(providerSelect && providerSelect.value) payload.provider = providerSelect.value;
|
|
2264
|
+
if(durationSelect && durationSelect.value) payload.duration = Number(durationSelect.value);
|
|
2265
|
+
if(audioToggle && audioToggle.checked) payload.generate_audio = true;
|
|
2266
|
+
setStatus("Creating video job\u2026", "");
|
|
2267
|
+
fetch("/api/v1/primitives/videos/generate", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ tracer: tracer, payload: payload }) })
|
|
2268
|
+
.then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); })
|
|
2269
|
+
.then(function(res){ if(!res.ok || !res.p.job_id) throw new Error(res.p.error || "Unable to create video job."); setStatus("Video job " + res.p.job_id + " started \u2014 generating\u2026", "success"); upsertHistory(makePending(res.p)); return pollJob(res.p.job_id); })
|
|
2270
|
+
.then(function(){ loadHistory(); loadSessions(); })
|
|
2271
|
+
.catch(function(err){ setStatus(err && err.message ? err.message : "Unable to create video.", "error"); })
|
|
2272
|
+
.then(function(){ setSubmitLoading(false); });
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// ===== lightbox =====
|
|
2276
|
+
function openVideoPreview(url, label, downloadName){
|
|
2277
|
+
if(!lightbox || !lightboxVideo) return;
|
|
2278
|
+
lightboxVideo.src = url; if(lightboxTitle) lightboxTitle.textContent = label || "Video output";
|
|
2279
|
+
if(lightboxDownload){ lightboxDownload.href = url; lightboxDownload.setAttribute("download", downloadName || "video.mp4"); }
|
|
2280
|
+
lightbox.hidden = false; lightbox.setAttribute("data-open", "true");
|
|
2281
|
+
}
|
|
2282
|
+
function closeVideoPreview(){ if(!lightbox) return; lightbox.setAttribute("data-open", "false"); lightbox.hidden = true; if(lightboxVideo){ try { lightboxVideo.pause(); } catch(_){} lightboxVideo.removeAttribute("src"); lightboxVideo.load(); } }
|
|
2283
|
+
|
|
2284
|
+
// ===== settings / sessions =====
|
|
2285
|
+
function openSettings(){ if(settingsModal) settingsModal.hidden = false; }
|
|
2286
|
+
function closeSettings(){ if(settingsModal) settingsModal.hidden = true; }
|
|
2287
|
+
function openSessions(){ if(sessionsPanel) sessionsPanel.classList.add("is-open"); if(sessionsBackdrop) sessionsBackdrop.hidden = false; loadSessions(); }
|
|
2288
|
+
function closeSessions(){ if(sessionsPanel) sessionsPanel.classList.remove("is-open"); if(sessionsBackdrop) sessionsBackdrop.hidden = true; }
|
|
2289
|
+
function relTimeShort(value){ var then = Date.parse(value || 0); if(!then) return ""; var secs = Math.max(0, Math.floor((Date.now() - then) / 1000)); if(secs < 60) return "just now"; var m = Math.floor(secs / 60); if(m < 60) return m + "m ago"; var h = Math.floor(m / 60); if(h < 24) return h + "h ago"; var d = Math.floor(h / 24); if(d < 7) return d + "d ago"; return new Date(then).toLocaleDateString(); }
|
|
2290
|
+
var sessionsLoading = false;
|
|
2291
|
+
function loadSessions(){
|
|
2292
|
+
if(!sessionsList || sessionsLoading) return; sessionsLoading = true;
|
|
2293
|
+
var endpoint = new URL("/job-runs/history", window.location.origin); endpoint.searchParams.set("limit", "500");
|
|
2294
|
+
fetch(endpoint, { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
|
|
2295
|
+
if(!res.ok || (res.p && res.p.error)) throw new Error((res.p && res.p.error) || "Unable to load sessions.");
|
|
2296
|
+
var groups = {}; var order = [];
|
|
2297
|
+
(res.p.entries || []).forEach(function(entry){
|
|
2298
|
+
if(!isVideoEntry(entry)) return; var tracer = String(entry.tracer || "").trim(); if(!tracer) return;
|
|
2299
|
+
var urls = historyEntryUrls(entry);
|
|
2300
|
+
if(!groups[tracer]){ groups[tracer] = { tracer: tracer, thumb: "", count: 0, createdAt: entry.createdAt || "" }; order.push(tracer); }
|
|
2301
|
+
var g = groups[tracer]; if(!g.thumb && urls[0]){ try { g.thumb = absoluteUrl(urls[0]); } catch(_){} } g.count += urls.length;
|
|
2302
|
+
if(Date.parse(entry.createdAt || 0) > Date.parse(g.createdAt || 0)) g.createdAt = entry.createdAt;
|
|
2303
|
+
});
|
|
2304
|
+
var list = order.map(function(t){ return groups[t]; }).sort(function(a, b){ return Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0); });
|
|
2305
|
+
renderSessions(list);
|
|
2306
|
+
}).catch(function(err){ renderSessions([], err && err.message ? err.message : "Unable to load sessions."); }).then(function(){ sessionsLoading = false; });
|
|
2307
|
+
}
|
|
2308
|
+
function renderSessions(groups, message){
|
|
2309
|
+
if(!sessionsList) return; sessionsList.innerHTML = "";
|
|
2310
|
+
if(message){ var n = document.createElement("div"); n.className = "rk-ip-sessions-empty"; n.textContent = message; sessionsList.appendChild(n); return; }
|
|
2311
|
+
if(!groups.length){ var e = document.createElement("div"); e.className = "rk-ip-sessions-empty"; e.textContent = "No past sessions yet. Generate a video to start one."; sessionsList.appendChild(e); return; }
|
|
2312
|
+
var active = resolveTracer();
|
|
2313
|
+
groups.forEach(function(group){
|
|
2314
|
+
var row = document.createElement("button"); row.type = "button"; row.className = "rk-ip-session" + (group.tracer === active ? " is-active" : ""); row.dataset.sessionTracer = group.tracer;
|
|
2315
|
+
if(group.thumb){ var v = document.createElement("video"); v.className = "rk-ip-session-thumb"; v.src = group.thumb + "#t=0.1"; v.muted = true; v.preload = "metadata"; row.appendChild(v); }
|
|
2316
|
+
else { var ph = document.createElement("span"); ph.className = "rk-ip-session-thumb is-empty"; ph.innerHTML = PLAY_SVG; row.appendChild(ph); }
|
|
2317
|
+
var meta = document.createElement("div"); meta.className = "rk-ip-session-meta";
|
|
2318
|
+
var title = document.createElement("div"); title.className = "rk-ip-session-title"; title.textContent = group.tracer;
|
|
2319
|
+
var sub = document.createElement("div"); sub.className = "rk-ip-session-sub"; var when = relTimeShort(group.createdAt);
|
|
2320
|
+
sub.textContent = (group.count ? group.count + " video" + (group.count === 1 ? "" : "s") : "session") + (when ? " \u00b7 " + when : "");
|
|
2321
|
+
meta.appendChild(title); meta.appendChild(sub); row.appendChild(meta); sessionsList.appendChild(row);
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
function switchToTracer(tracer){
|
|
2325
|
+
var value = String(tracer || "").trim(); if(!value) return;
|
|
2326
|
+
sessionTracer = value; if(tracerInput) tracerInput.value = value; tracerAutoPending = false; closeSessions(); loadHistory(); loadSessions();
|
|
2327
|
+
setStatus("Opened session " + value + ".", "");
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// ===== reset =====
|
|
2331
|
+
function resetWorkspace(){
|
|
2332
|
+
clearSourceObjectUrl(); sourceVideoUrl = ""; sourceVideoName = "";
|
|
2333
|
+
try { videoEl.pause(); } catch(_){} videoEl.removeAttribute("src"); videoEl.load();
|
|
2334
|
+
masks = []; activeMaskId = ""; frames = { first: null, last: null };
|
|
2335
|
+
["first","last"].forEach(function(w){ var img = document.querySelector('[data-ipv-frame-img="' + w + '"]'); var ph = document.querySelector('[data-ipv-frame-ph="' + w + '"]'); if(img){ img.hidden = true; img.removeAttribute("src"); } if(ph) ph.style.display = ""; });
|
|
2336
|
+
if(globalPrompt) globalPrompt.value = ""; attachments = []; renderAttachChips();
|
|
2337
|
+
outputPanel.hidden = true; resultVideo.removeAttribute("src");
|
|
2338
|
+
emptyState.hidden = false; if(player) player.hidden = true;
|
|
2339
|
+
canvas.width = 1; canvas.height = 1; renderMasks();
|
|
2340
|
+
if(scrub) scrub.value = "0"; if(timecodeInput) timecodeInput.value = "00:00:000"; if(durationEl) durationEl.textContent = "/ 00:00:000";
|
|
2341
|
+
}
|
|
2342
|
+
function startNew(){
|
|
2343
|
+
sessionTracer = ""; tracerAutoPending = true; if(tracerInput) tracerInput.value = "";
|
|
2344
|
+
clearUrlParams(); resetWorkspace(); loadHistory(); loadSessions();
|
|
2345
|
+
setStatus("Started a new session.", "");
|
|
2346
|
+
}
|
|
2347
|
+
function clearUrlParams(){
|
|
2348
|
+
var url = new URL(window.location.href);
|
|
2349
|
+
["tracer","tracer_id","trace","source_video_url","video_url","source_url","source_video_name","video_name","file_name","source_image_url"].forEach(function(k){ url.searchParams.delete(k); });
|
|
2350
|
+
window.history.replaceState({}, "", url.toString());
|
|
2351
|
+
}
|
|
2352
|
+
function firstParam(names){ var params = new URLSearchParams(window.location.search); for(var i = 0; i < names.length; i++){ var v = params.get(names[i]); if(v && v.trim()) return v.trim(); } return ""; }
|
|
2353
|
+
function initFromUrl(){
|
|
2354
|
+
var urlTracer = firstParam(["tracer","tracer_id","trace"]);
|
|
2355
|
+
if(urlTracer){ sessionTracer = urlTracer; if(tracerInput) tracerInput.value = urlTracer; tracerAutoPending = false; }
|
|
2356
|
+
var src = firstParam(["source_video_url","video_url","source_url"]);
|
|
2357
|
+
if(src){ var nm = firstParam(["source_video_name","video_name","file_name"]); loadSourceUrl(src, nm); recordSourceHistoryItem(src, nm); }
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// ===== timeline sync =====
|
|
2361
|
+
function syncTimeline(){
|
|
2362
|
+
var d = videoEl.duration || 0; var t = videoEl.currentTime || 0;
|
|
2363
|
+
if(scrub && document.activeElement !== scrub){ scrub.value = String(d > 0 ? Math.round((t / d) * 1000) : 0); }
|
|
2364
|
+
if(timecodeInput && document.activeElement !== timecodeInput){ timecodeInput.value = formatTimecode(t); }
|
|
2365
|
+
}
|
|
2366
|
+
function tick(){ if(!videoEl.paused && !videoEl.ended){ syncTimeline(); redraw(); requestAnimationFrame(tick); } }
|
|
2367
|
+
|
|
2368
|
+
// ===== wire events =====
|
|
2369
|
+
if(brushInput){ brushInput.addEventListener("input", function(){ if(brushLabel) brushLabel.textContent = brushInput.value; updateBrushCursorSize(); }); }
|
|
2370
|
+
if(uploadBtn && fileInput){ uploadBtn.addEventListener("click", function(){ fileInput.click(); }); fileInput.addEventListener("change", function(){ if(fileInput.files && fileInput.files[0]) loadSourceFile(fileInput.files[0]); }); }
|
|
2371
|
+
if(startNewBtn) startNewBtn.addEventListener("click", startNew);
|
|
2372
|
+
if(addMaskBtn) addMaskBtn.addEventListener("click", function(){ addMask(videoEl.currentTime || 0, colorPicker ? colorPicker.value : null); });
|
|
2373
|
+
if(colorPicker) colorPicker.addEventListener("input", function(){ var m = activeMask(); if(m){ m.color = normalizeHex(colorPicker.value); renderMasks(); redraw(); } });
|
|
2374
|
+
if(undoBtn) undoBtn.addEventListener("click", function(){ var m = activeMask(); if(m && m.strokes.length){ m.redo.push(m.strokes.pop()); redraw(); syncUndoRedo(); } });
|
|
2375
|
+
if(redoBtn) redoBtn.addEventListener("click", function(){ var m = activeMask(); if(m && m.redo.length){ m.strokes.push(m.redo.pop()); redraw(); syncUndoRedo(); } });
|
|
2376
|
+
if(clearBtn) clearBtn.addEventListener("click", function(){ var m = activeMask(); if(m){ m.redo = m.strokes.concat(m.redo); m.strokes = []; redraw(); syncUndoRedo(); } });
|
|
2377
|
+
if(attachBtn && attachInput){ attachBtn.addEventListener("click", function(){ attachInput.click(); }); attachInput.addEventListener("change", function(){ addAttachmentFiles(attachInput.files); attachInput.value = ""; }); }
|
|
2378
|
+
if(submitBtn) submitBtn.addEventListener("click", submitGenerate);
|
|
2379
|
+
|
|
2380
|
+
masksList.addEventListener("click", function(event){
|
|
2381
|
+
var t = event.target; if(!(t instanceof Element)) return;
|
|
2382
|
+
var sel = t.closest("[data-mask-select]"); if(sel){ selectMask(sel.getAttribute("data-mask-select")); return; }
|
|
2383
|
+
var seek = t.closest("[data-mask-seek]"); if(seek){ var id = seek.getAttribute("data-mask-seek"); var m = null; masks.forEach(function(x){ if(x.id === id) m = x; }); if(m){ selectMask(id); if(videoEl.duration) videoEl.currentTime = Math.min(videoEl.duration, m.timeSec); syncTimeline(); redraw(); } return; }
|
|
2384
|
+
var setT = t.closest("[data-mask-settime]"); if(setT){ var id2 = setT.getAttribute("data-mask-settime"); masks.forEach(function(x){ if(x.id === id2) x.timeSec = videoEl.currentTime || 0; }); renderMasks(); redraw(); return; }
|
|
2385
|
+
var rm = t.closest("[data-mask-remove]"); if(rm){ removeMask(rm.getAttribute("data-mask-remove")); return; }
|
|
2386
|
+
});
|
|
2387
|
+
masksList.addEventListener("input", function(event){
|
|
2388
|
+
var t = event.target; if(!(t instanceof Element)) return;
|
|
2389
|
+
var ta = t.closest("[data-mask-prompt]"); if(ta){ var id = ta.getAttribute("data-mask-prompt"); masks.forEach(function(m){ if(m.id === id) m.prompt = ta.value; }); }
|
|
2390
|
+
});
|
|
2391
|
+
masksList.addEventListener("focusin", function(event){
|
|
2392
|
+
var t = event.target; if(!(t instanceof Element)) return;
|
|
2393
|
+
var ta = t.closest("[data-mask-prompt]"); if(ta){ var id = ta.getAttribute("data-mask-prompt"); if(id !== activeMaskId){ activeMaskId = id; var m = activeMask(); if(m && colorPicker) colorPicker.value = m.color; masksList.querySelectorAll(".rk-ipv-mask").forEach(function(r){ r.setAttribute("data-active", r.dataset.maskId === id ? "true" : "false"); }); redraw(); } }
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
// frame slot buttons
|
|
2397
|
+
document.querySelectorAll("[data-ipv-frame-upload]").forEach(function(btn){ btn.addEventListener("click", function(){ pendingFrameSlot = btn.getAttribute("data-ipv-frame-upload"); if(frameInput) frameInput.click(); }); });
|
|
2398
|
+
document.querySelectorAll("[data-ipv-frame-capture]").forEach(function(btn){ btn.addEventListener("click", function(){ captureCurrentFrame(btn.getAttribute("data-ipv-frame-capture")); }); });
|
|
2399
|
+
document.querySelectorAll("[data-ipv-frame-open]").forEach(function(btn){ btn.addEventListener("click", function(){ var w = btn.getAttribute("data-ipv-frame-open"); if(frames[w] && frames[w].url){ window.open(frames[w].url, "_blank", "noopener,noreferrer"); } }); });
|
|
2400
|
+
if(frameInput) frameInput.addEventListener("change", function(){ if(pendingFrameSlot && frameInput.files && frameInput.files[0]) uploadFrameFile(pendingFrameSlot, frameInput.files[0]); pendingFrameSlot = null; frameInput.value = ""; });
|
|
2401
|
+
|
|
2402
|
+
// canvas painting
|
|
2403
|
+
canvas.addEventListener("pointerdown", beginStroke);
|
|
2404
|
+
canvas.addEventListener("pointermove", continueStroke);
|
|
2405
|
+
canvas.addEventListener("pointerup", endStroke);
|
|
2406
|
+
canvas.addEventListener("pointercancel", endStroke);
|
|
2407
|
+
canvas.addEventListener("pointerleave", function(){ hideBrushCursor(); });
|
|
2408
|
+
canvas.addEventListener("pointerenter", updateBrushCursorPos);
|
|
2409
|
+
|
|
2410
|
+
// player controls
|
|
2411
|
+
if(playpause) playpause.addEventListener("click", function(){ if(videoEl.paused){ videoEl.play(); } else { videoEl.pause(); } });
|
|
2412
|
+
videoEl.addEventListener("play", function(){ if(playpause) playpause.innerHTML = PAUSE_SVG; requestAnimationFrame(tick); });
|
|
2413
|
+
videoEl.addEventListener("pause", function(){ if(playpause) playpause.innerHTML = PLAY_SVG; syncTimeline(); redraw(); });
|
|
2414
|
+
videoEl.addEventListener("ended", function(){ if(playpause) playpause.innerHTML = PLAY_SVG; });
|
|
2415
|
+
videoEl.addEventListener("timeupdate", function(){ if(videoEl.paused){ syncTimeline(); redraw(); } });
|
|
2416
|
+
videoEl.addEventListener("seeked", function(){ syncTimeline(); redraw(); });
|
|
2417
|
+
if(scrub) scrub.addEventListener("input", function(){ var d = videoEl.duration || 0; if(d > 0){ videoEl.currentTime = (Number(scrub.value) / 1000) * d; } });
|
|
2418
|
+
if(timecodeInput){
|
|
2419
|
+
timecodeInput.addEventListener("change", function(){ var t = parseTimecode(timecodeInput.value); if(!isNaN(t) && videoEl.duration){ videoEl.currentTime = Math.max(0, Math.min(videoEl.duration, t)); } });
|
|
2420
|
+
timecodeInput.addEventListener("keydown", function(e){ if(e.key === "Enter"){ e.preventDefault(); timecodeInput.blur(); } });
|
|
2421
|
+
}
|
|
2422
|
+
if(timecodeCopy) timecodeCopy.addEventListener("click", function(){ copyText(timecodeInput ? timecodeInput.value : formatTimecode(videoEl.currentTime || 0)).then(function(ok){ timecodeCopy.setAttribute("title", ok ? "Copied" : "Copy failed"); setTimeout(function(){ timecodeCopy.setAttribute("title", "Copy timecode"); }, 1200); }); });
|
|
2423
|
+
|
|
2424
|
+
// history interactions
|
|
2425
|
+
historyList.addEventListener("click", function(event){
|
|
2426
|
+
var t = event.target; if(!(t instanceof Element)) return;
|
|
2427
|
+
var prev = t.closest("[data-history-preview]"); if(prev){ var key = prev.getAttribute("data-history-preview"); var it = historyItems.find(function(e){ return e.key === key; }); if(it) openVideoPreview(it.url, it.createdAt ? new Date(it.createdAt).toLocaleString() : "Video output", videoDownloadName("video-history", it.createdAt)); return; }
|
|
2428
|
+
var cp = t.closest("[data-history-copy]"); if(cp){ var k2 = cp.getAttribute("data-history-copy"); var it2 = historyItems.find(function(e){ return e.key === k2; }); if(it2) copyText(it2.url).then(function(ok){ cp.setAttribute("title", ok ? "Copied" : "Copy failed"); setTimeout(function(){ cp.setAttribute("title", "Copy URL"); }, 1200); }); return; }
|
|
2429
|
+
var sv = t.closest("[data-history-save]"); if(sv){ var k3 = sv.getAttribute("data-history-save"); var it3 = historyItems.find(function(e){ return e.key === k3; }); if(it3 && it3.url) saveMediaToFiles(it3.url, videoDownloadName("video-output", it3.createdAt)); return; }
|
|
2430
|
+
var ck = t.closest("[data-history-check]"); if(ck){ checkJobStatus(ck.getAttribute("data-history-check"), ck); return; }
|
|
2431
|
+
var rm = t.closest("[data-history-remove]"); if(rm){ var key4 = rm.getAttribute("data-history-remove"); var confirm = historyList.querySelector('[data-history-confirm="' + (window.CSS && CSS.escape ? CSS.escape(key4) : key4) + '"]'); if(confirm){ confirm.hidden = !confirm.hidden; } return; }
|
|
2432
|
+
var cancel = t.closest("[data-history-cancel]"); if(cancel){ var c = cancel.closest("[data-history-confirm]"); if(c) c.hidden = true; return; }
|
|
2433
|
+
var conf = t.closest("[data-history-confirm-remove]"); if(conf){ var key5 = conf.getAttribute("data-history-confirm-remove"); if(key5){ hiddenHistoryKeys[key5] = 1; renderHistory(); } return; }
|
|
2434
|
+
});
|
|
2435
|
+
if(historyRefresh) historyRefresh.addEventListener("click", loadHistory);
|
|
2436
|
+
|
|
2437
|
+
// save result
|
|
2438
|
+
if(saveBtn) saveBtn.addEventListener("click", function(){ if(resultVideo.src) saveMediaToFiles(resultVideo.currentSrc || resultVideo.src, videoDownloadName("video-result")); });
|
|
2439
|
+
|
|
2440
|
+
// settings + sessions
|
|
2441
|
+
if(settingsBtn) settingsBtn.addEventListener("click", openSettings);
|
|
2442
|
+
if(settingsClose) settingsClose.addEventListener("click", closeSettings);
|
|
2443
|
+
if(settingsModal) settingsModal.addEventListener("click", function(e){ if(e.target === settingsModal) closeSettings(); });
|
|
2444
|
+
if(sessionsToggle) sessionsToggle.addEventListener("click", function(){ if(sessionsPanel && sessionsPanel.classList.contains("is-open")) closeSessions(); else openSessions(); });
|
|
2445
|
+
if(sessionsClose) sessionsClose.addEventListener("click", closeSessions);
|
|
2446
|
+
if(sessionsBackdrop) sessionsBackdrop.addEventListener("click", closeSessions);
|
|
2447
|
+
if(sessionsList) sessionsList.addEventListener("click", function(event){ var t = event.target; if(!(t instanceof Element)) return; var row = t.closest("[data-session-tracer]"); if(row) switchToTracer(row.getAttribute("data-session-tracer")); });
|
|
2448
|
+
|
|
2449
|
+
// lightbox close
|
|
2450
|
+
if(lightboxClose) lightboxClose.addEventListener("click", closeVideoPreview);
|
|
2451
|
+
if(lightbox) lightbox.addEventListener("click", function(e){ if(e.target === lightbox) closeVideoPreview(); });
|
|
2452
|
+
if(lightboxCopy) lightboxCopy.addEventListener("click", function(){ if(lightboxVideo && lightboxVideo.src) copyText(lightboxVideo.src).then(function(ok){ lightboxCopy.setAttribute("title", ok ? "Copied" : "Copy failed"); setTimeout(function(){ lightboxCopy.setAttribute("title", "Copy URL"); }, 1200); }); });
|
|
2453
|
+
|
|
2454
|
+
window.addEventListener("resize", syncCanvasSize);
|
|
2455
|
+
window.addEventListener("keydown", function(e){ if(e.key !== "Escape") return; if(lightbox && lightbox.getAttribute("data-open") === "true"){ closeVideoPreview(); return; } if(settingsModal && !settingsModal.hidden){ closeSettings(); return; } if(sessionsPanel && sessionsPanel.classList.contains("is-open")){ closeSessions(); return; } });
|
|
2456
|
+
|
|
2457
|
+
// drag-drop video onto empty state
|
|
2458
|
+
if(emptyState){
|
|
2459
|
+
["dragenter","dragover"].forEach(function(ev){ emptyState.addEventListener(ev, function(e){ e.preventDefault(); }); });
|
|
2460
|
+
emptyState.addEventListener("drop", function(e){ e.preventDefault(); var f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; if(f && /^video\//.test(f.type)) loadSourceFile(f); });
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// ===== text-to-video composer =====
|
|
2464
|
+
var genPrompt = document.getElementById("rk-ipv-genprompt");
|
|
2465
|
+
var genClip = document.getElementById("rk-ipv-genclip");
|
|
2466
|
+
var genSend = document.getElementById("rk-ipv-gensend");
|
|
2467
|
+
var genRefsEl = document.getElementById("rk-ipv-genrefs");
|
|
2468
|
+
var genStatusEl = document.getElementById("rk-ipv-genstatus");
|
|
2469
|
+
var genWrap = document.getElementById("rk-ipv-gen");
|
|
2470
|
+
var GEN_SEND_HTML = genSend ? genSend.innerHTML : "";
|
|
2471
|
+
var genRefs = []; var genBusy = false; var genFileModal = null;
|
|
2472
|
+
function setGenStatus(m, t){ if(!genStatusEl) return; genStatusEl.textContent = m || ""; genStatusEl.setAttribute("data-tone", t || ""); }
|
|
2473
|
+
function syncGenSend(){ if(genPrompt) genPrompt.disabled = genBusy; if(genClip) genClip.disabled = genBusy; if(!genSend) return; if(genBusy){ genSend.classList.add("is-busy"); genSend.innerHTML = "<span class='rk-ip-gen-spin'></span>"; } else { genSend.classList.remove("is-busy"); genSend.innerHTML = GEN_SEND_HTML; } genSend.disabled = genBusy || !(genPrompt && genPrompt.value.trim()); }
|
|
2474
|
+
function renderGenRefs(){ if(!genRefsEl) return; genRefsEl.innerHTML = ""; if(!genRefs.length){ genRefsEl.hidden = true; return; } genRefsEl.hidden = false; genRefs.forEach(function(url, idx){ var chip = document.createElement("span"); chip.className = "rk-ip-gen-ref"; var img = document.createElement("img"); img.src = url; img.alt = ""; chip.appendChild(img); var label = document.createElement("span"); label.textContent = sourceNameFromUrl(url); chip.appendChild(label); var x = document.createElement("button"); x.type = "button"; x.textContent = "\u00d7"; x.addEventListener("click", function(){ genRefs.splice(idx, 1); renderGenRefs(); }); chip.appendChild(x); genRefsEl.appendChild(chip); }); }
|
|
2475
|
+
function closeGenFilePanel(){ if(genFileModal){ try { genFileModal._handle && genFileModal._handle.destroy && genFileModal._handle.destroy(); } catch(_){} genFileModal.remove(); genFileModal = null; document.removeEventListener("keydown", onGenFileEsc, true); } }
|
|
2476
|
+
function onGenFileEsc(e){ if(e.key === "Escape") closeGenFilePanel(); }
|
|
2477
|
+
function openGenFilePanel(){
|
|
2478
|
+
var dir = window.__vidfarmDirectory; if(!dir || !dir.pick){ setGenStatus("File browser isn't available right now.", "error"); return; }
|
|
2479
|
+
dir.pick({ select: "file", roots: ["files","temp","raws"], title: "Attach reference images", hint: "Pick one or more images to guide the generation.", confirmLabel: "Attach", multiple: true,
|
|
2480
|
+
onPick: function(picked){ var arr = Array.isArray(picked) ? picked : [picked]; arr.forEach(function(f){ var url = f && (f.url || f.viewUrl); if(url){ try { genRefs.push(absoluteUrl(url)); } catch(_){} } }); renderGenRefs(); } });
|
|
2481
|
+
}
|
|
2482
|
+
function pollGenerateJob(jobId){
|
|
2483
|
+
var attempt = 0;
|
|
2484
|
+
function step(){
|
|
2485
|
+
return fetch("/api/v1/primitives/video_generate/jobs/" + encodeURIComponent(jobId), { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
|
|
2486
|
+
var payload = res.p; if(!res.ok) throw new Error(payload.error || "Unable to poll job.");
|
|
2487
|
+
if(payload.status === "failed" || payload.status === "cancelled") throw new Error((payload.error && payload.error.message) || ("Video job " + payload.status + "."));
|
|
2488
|
+
var out = extractVideoUrl(payload);
|
|
2489
|
+
if(payload.status === "succeeded" || payload.status === "complete" || out){ if(!out) throw new Error("Completed without an output URL."); return absoluteUrl(out); }
|
|
2490
|
+
setGenStatus("Generating video (" + (payload.status || "running") + ")\u2026 this can take a few minutes", "");
|
|
2491
|
+
attempt++; if(attempt >= 120){ setGenStatus("Still running \u2014 check History shortly.", ""); return ""; }
|
|
2492
|
+
return new Promise(function(res2){ setTimeout(res2, attempt < 8 ? 2000 : 4000); }).then(step);
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
return step();
|
|
2496
|
+
}
|
|
2497
|
+
function submitGenerateNew(){
|
|
2498
|
+
var p = (genPrompt && genPrompt.value || "").trim();
|
|
2499
|
+
if(!p){ setGenStatus("Describe the video to generate.", "error"); if(genPrompt) genPrompt.focus(); return; }
|
|
2500
|
+
if(genBusy) return; genBusy = true; syncGenSend(); if(genWrap) genWrap.classList.add("is-busy");
|
|
2501
|
+
setGenStatus("Creating video\u2026", "");
|
|
2502
|
+
var payload = { prompt: p, output_format: "mp4" };
|
|
2503
|
+
var frameImgs = frameImagesPayload(); if(frameImgs.length) payload.frame_images = frameImgs;
|
|
2504
|
+
if(genRefs.length) payload.input_references = genRefs.slice();
|
|
2505
|
+
if(providerSelect && providerSelect.value) payload.provider = providerSelect.value;
|
|
2506
|
+
if(durationSelect && durationSelect.value) payload.duration = Number(durationSelect.value);
|
|
2507
|
+
if(audioToggle && audioToggle.checked) payload.generate_audio = true;
|
|
2508
|
+
var genTracer = tracerAutoPending ? nameTracerFromSeed(p) : getTracerValue();
|
|
2509
|
+
fetch("/api/v1/primitives/videos/generate", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ tracer: genTracer, payload: payload }) })
|
|
2510
|
+
.then(function(r){ return parseJsonResponse(r).then(function(j){ return { ok: r.ok, j: j }; }); })
|
|
2511
|
+
.then(function(res){ if(!res.ok || !res.j.job_id) throw new Error(res.j.error || "Unable to start video generation."); upsertHistory(makePending(res.j)); return pollGenerateJob(res.j.job_id); })
|
|
2512
|
+
.then(function(out){ if(out){ loadSourceUrl(out, "Generated video"); recordSourceHistoryItem(out, "Generated video"); setGenStatus("Video generated. Scrub and paint timed masks to refine.", "success"); setStatus("Generated video loaded.", "success"); loadHistory(); } })
|
|
2513
|
+
.catch(function(err){ var msg = (err && err.message) ? err.message : "Unable to generate video."; if(/401|403|sign in|unauth/i.test(msg)) msg = "Sign in to generate videos."; setGenStatus(msg, "error"); })
|
|
2514
|
+
.then(function(){ genBusy = false; if(genWrap) genWrap.classList.remove("is-busy"); syncGenSend(); });
|
|
2515
|
+
}
|
|
2516
|
+
if(genPrompt){ genPrompt.addEventListener("input", syncGenSend); genPrompt.addEventListener("keydown", function(e){ if(e.key === "Enter" && !e.shiftKey){ e.preventDefault(); submitGenerateNew(); } }); }
|
|
2517
|
+
if(genClip) genClip.addEventListener("click", openGenFilePanel);
|
|
2518
|
+
if(genSend) genSend.addEventListener("click", submitGenerateNew);
|
|
2519
|
+
renderGenRefs(); syncGenSend();
|
|
2520
|
+
|
|
2521
|
+
// ===== boot =====
|
|
2522
|
+
renderMasks();
|
|
2523
|
+
loadProviders();
|
|
2524
|
+
initFromUrl();
|
|
2525
|
+
loadHistory();
|
|
2526
|
+
})();
|
|
2527
|
+
</script>
|
|
2528
|
+
<script>(() => {
|
|
2529
|
+
// Desktop-agents nudge banner: a full-width alert pinned to the top of the
|
|
2530
|
+
// /editor and /chat surfaces pointing to the setup guide. Both surfaces embed
|
|
2531
|
+
// this chrome script, so gate strictly by pathname. Dismissible per session.
|
|
2532
|
+
(function mountDesktopAgentsBanner() {
|
|
2533
|
+
try {
|
|
2534
|
+
var path = location.pathname;
|
|
2535
|
+
var onEditor = path === '/editor' || path.indexOf('/editor/') === 0;
|
|
2536
|
+
var onChat = path === '/chat' || path.indexOf('/chat/') === 0;
|
|
2537
|
+
if (!onEditor && !onChat) return;
|
|
2538
|
+
if (document.querySelector('.vf-topbanner')) return;
|
|
2539
|
+
if (sessionStorage.getItem('vf-topbanner-dismissed') === '1') return;
|
|
2540
|
+
var GUIDE = '/blog/desktop-ai-agents';
|
|
2541
|
+
var bar = document.createElement('div');
|
|
2542
|
+
bar.className = 'vf-topbanner';
|
|
2543
|
+
bar.setAttribute('role', 'alert');
|
|
2544
|
+
bar.innerHTML =
|
|
2545
|
+
'<span class="vf-topbanner-ico" aria-hidden="true">🖥️</span>' +
|
|
2546
|
+
'<span class="vf-topbanner-msg"><strong>VidFarm.cc is best using desktop AI Agents</strong> such as OpenAI Codex & Claude Code. Follow the guide to set up.</span>' +
|
|
2547
|
+
'<a class="vf-topbanner-cta" href="' + GUIDE + '" target="_blank" rel="noopener">Follow the guide →</a>' +
|
|
2548
|
+
'<button type="button" class="vf-topbanner-close" aria-label="Dismiss">×</button>';
|
|
2549
|
+
function sizeBanner() {
|
|
2550
|
+
document.body.style.setProperty('--vf-topbanner-h', bar.offsetHeight + 'px');
|
|
2551
|
+
}
|
|
2552
|
+
bar.querySelector('.vf-topbanner-close').addEventListener('click', function () {
|
|
2553
|
+
try { sessionStorage.setItem('vf-topbanner-dismissed', '1'); } catch (e) {}
|
|
2554
|
+
bar.remove();
|
|
2555
|
+
document.body.classList.remove('vf-has-topbanner');
|
|
2556
|
+
document.body.style.removeProperty('--vf-topbanner-h');
|
|
2557
|
+
});
|
|
2558
|
+
document.body.insertBefore(bar, document.body.firstChild);
|
|
2559
|
+
document.body.classList.add('vf-has-topbanner');
|
|
2560
|
+
sizeBanner();
|
|
2561
|
+
window.addEventListener('resize', sizeBanner);
|
|
2562
|
+
} catch (e) {}
|
|
2563
|
+
})();
|
|
2564
|
+
|
|
2565
|
+
var fab = document.getElementById('rkFab');
|
|
2566
|
+
var panel = document.getElementById('rkAichat');
|
|
2567
|
+
if (!fab || !panel) return;
|
|
2568
|
+
// Editor dock mode: the /editor shell reuses this exact pop panel + draggable
|
|
2569
|
+
// FAB, dark-themed via .rk-aichat--editor and wired to the editor chat surface
|
|
2570
|
+
// (?surface=editor boot + editor_context snapshots). Same open/close/drag UX
|
|
2571
|
+
// as every other page.
|
|
2572
|
+
var isEditorDock = !!document.querySelector('[data-rk-editor-dock]');
|
|
2573
|
+
if (isEditorDock) panel.classList.add('rk-aichat--editor');
|
|
2574
|
+
var minBtn = document.getElementById('rkAichatMin');
|
|
2575
|
+
var form = document.getElementById('rkAichatForm');
|
|
2576
|
+
var input = document.getElementById('rkAichatInput');
|
|
2577
|
+
var log = document.getElementById('rkAichatLog');
|
|
2578
|
+
var MARGIN = 10;
|
|
2579
|
+
// On the dedicated /chat page the panel's chat column is redundant (the page
|
|
2580
|
+
// IS the chat), so we hide the floating FAB and instead let the composer's
|
|
2581
|
+
// attach button open the panel in FILES-ONLY mode (just the file explorer).
|
|
2582
|
+
var isChatPage = !!document.querySelector('.rk-chat-page');
|
|
2583
|
+
if (isChatPage) fab.hidden = true;
|
|
2584
|
+
|
|
2585
|
+
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
|
2586
|
+
|
|
2587
|
+
// offsetWidth/offsetLeft (not getBoundingClientRect) so the hover/drag/open
|
|
2588
|
+
// transforms never skew the clamp math
|
|
2589
|
+
function placeFab(x, y) {
|
|
2590
|
+
x = clamp(x, MARGIN, window.innerWidth - fab.offsetWidth - MARGIN);
|
|
2591
|
+
y = clamp(y, MARGIN, window.innerHeight - fab.offsetHeight - MARGIN);
|
|
2592
|
+
fab.style.left = x + 'px';
|
|
2593
|
+
fab.style.top = y + 'px';
|
|
2594
|
+
fab.style.bottom = 'auto';
|
|
2595
|
+
return { x: x, y: y };
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
try {
|
|
2599
|
+
var saved = JSON.parse(localStorage.getItem('rk-fab-pos') || 'null');
|
|
2600
|
+
if (saved && typeof saved.x === 'number') placeFab(saved.x, saved.y);
|
|
2601
|
+
} catch (e) {}
|
|
2602
|
+
|
|
2603
|
+
function anchorPanel() {
|
|
2604
|
+
var r = fab.getBoundingClientRect();
|
|
2605
|
+
var onRight = r.left + r.width / 2 > window.innerWidth / 2;
|
|
2606
|
+
panel.classList.toggle('is-right', onRight);
|
|
2607
|
+
document.body.classList.toggle('rk-chat-left', !panel.hidden && !onRight);
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
function openPanel(filesOnly) {
|
|
2611
|
+
panel.hidden = false;
|
|
2612
|
+
panel.classList.toggle('is-files-only', !!filesOnly);
|
|
2613
|
+
fab.classList.add('is-open');
|
|
2614
|
+
fab.setAttribute('aria-expanded', 'true');
|
|
2615
|
+
if (filesOnly) {
|
|
2616
|
+
// slim left-hand drawer that replaces the sidebar (like the normal dock),
|
|
2617
|
+
// so it never covers the centered chat column's composer / Send button
|
|
2618
|
+
setLeftMode('files');
|
|
2619
|
+
panel.classList.remove('is-right');
|
|
2620
|
+
document.body.classList.add('rk-chat-left');
|
|
2621
|
+
} else {
|
|
2622
|
+
anchorPanel();
|
|
2623
|
+
// On short / narrow screens the left drawer stacks ABOVE the chat and
|
|
2624
|
+
// crushes the log + composer. Start collapsed to the chat column there;
|
|
2625
|
+
// the user can still open Files/History from the header toggles.
|
|
2626
|
+
try {
|
|
2627
|
+
if (window.matchMedia && window.matchMedia('(max-width:700px)').matches) setLeftMode('');
|
|
2628
|
+
} catch (e) {}
|
|
2629
|
+
}
|
|
2630
|
+
try { sessionStorage.setItem('rk-chat-open', filesOnly ? 'files' : '1'); } catch (e) {}
|
|
2631
|
+
if (!filesOnly) loadBoot();
|
|
2632
|
+
refreshFilesOnOpen();
|
|
2633
|
+
if (!filesOnly && input) input.focus();
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function closePanel() {
|
|
2637
|
+
panel.hidden = true;
|
|
2638
|
+
panel.classList.remove('is-files-only');
|
|
2639
|
+
fab.classList.remove('is-open');
|
|
2640
|
+
fab.setAttribute('aria-expanded', 'false');
|
|
2641
|
+
document.body.classList.remove('rk-chat-left');
|
|
2642
|
+
try { sessionStorage.removeItem('rk-chat-open'); } catch (e) {}
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
var drag = null;
|
|
2646
|
+
fab.addEventListener('pointerdown', function (e) {
|
|
2647
|
+
drag = { sx: e.clientX, sy: e.clientY, ox: fab.offsetLeft, oy: fab.offsetTop, moved: false, last: null };
|
|
2648
|
+
fab.setPointerCapture(e.pointerId);
|
|
2649
|
+
});
|
|
2650
|
+
fab.addEventListener('pointermove', function (e) {
|
|
2651
|
+
if (!drag) return;
|
|
2652
|
+
var dx = e.clientX - drag.sx;
|
|
2653
|
+
var dy = e.clientY - drag.sy;
|
|
2654
|
+
if (!drag.moved && Math.hypot(dx, dy) < 6) return;
|
|
2655
|
+
drag.moved = true;
|
|
2656
|
+
fab.classList.add('is-dragging');
|
|
2657
|
+
drag.last = placeFab(drag.ox + dx, drag.oy + dy);
|
|
2658
|
+
});
|
|
2659
|
+
fab.addEventListener('pointerup', function () {
|
|
2660
|
+
if (!drag) return;
|
|
2661
|
+
var moved = drag.moved;
|
|
2662
|
+
if (drag.last) {
|
|
2663
|
+
try { localStorage.setItem('rk-fab-pos', JSON.stringify(drag.last)); } catch (e) {}
|
|
2664
|
+
}
|
|
2665
|
+
fab.classList.remove('is-dragging');
|
|
2666
|
+
drag = null;
|
|
2667
|
+
if (!moved) openPanel();
|
|
2668
|
+
});
|
|
2669
|
+
fab.addEventListener('pointercancel', function () {
|
|
2670
|
+
fab.classList.remove('is-dragging');
|
|
2671
|
+
drag = null;
|
|
2672
|
+
});
|
|
2673
|
+
fab.addEventListener('click', function (e) {
|
|
2674
|
+
if (e.detail === 0 && panel.hidden) openPanel();
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
if (minBtn) minBtn.addEventListener('click', closePanel);
|
|
2678
|
+
document.addEventListener('keydown', function (e) {
|
|
2679
|
+
if (e.key === 'Escape' && !panel.hidden) closePanel();
|
|
2680
|
+
});
|
|
2681
|
+
window.addEventListener('resize', function () {
|
|
2682
|
+
if (fab.style.top) {
|
|
2683
|
+
var pos = placeFab(fab.offsetLeft, fab.offsetTop);
|
|
2684
|
+
try { localStorage.setItem('rk-fab-pos', JSON.stringify(pos)); } catch (e) {}
|
|
2685
|
+
}
|
|
2686
|
+
if (!panel.hidden) anchorPanel();
|
|
2687
|
+
});
|
|
2688
|
+
|
|
2689
|
+
function bubble(kind, text) {
|
|
2690
|
+
var el = document.createElement('div');
|
|
2691
|
+
el.className = 'rk-aichat-msg ' + kind;
|
|
2692
|
+
el.textContent = text;
|
|
2693
|
+
log.appendChild(el);
|
|
2694
|
+
log.scrollTop = log.scrollHeight;
|
|
2695
|
+
return el;
|
|
2696
|
+
}
|
|
2697
|
+
function logBottom() { if (log) log.scrollTop = log.scrollHeight; }
|
|
2698
|
+
|
|
2699
|
+
// Remember the opening greeting so "New chat" can restore a fresh thread.
|
|
2700
|
+
var GREETING = (function () {
|
|
2701
|
+
var first = log && log.querySelector('.rk-aichat-msg.is-ai');
|
|
2702
|
+
return first ? first.textContent : "Hi! I'm the vidfarm assistant — ask me anything about your videos, clips, and posts.";
|
|
2703
|
+
})();
|
|
2704
|
+
|
|
2705
|
+
// ─── real agentic chat: wired to the live editor-chat backend ──────────────
|
|
2706
|
+
// Boot (endpoint + api key + brainstorm template) is fetched lazily on first
|
|
2707
|
+
// open from /chat-dock/boot (cookie-authed). Tool calls surface as first-class
|
|
2708
|
+
// actions: every http_request becomes a clickable card that opens a modal with
|
|
2709
|
+
// the exact request + response, exactly like the /editor chat.
|
|
2710
|
+
var BOOT = null, BOOT_STATE = 'idle'; // idle|loading|ready|anon|error
|
|
2711
|
+
var ENDPOINT = '/api/v1/editor-chat', API_KEY = null, TEMPLATE = null, CACHED = null;
|
|
2712
|
+
var THREADS_URL = '/api/v1/editor-chat/threads', TEMPLATE_ID = 'chat-brainstorm';
|
|
2713
|
+
var convo = []; // [{role,text}] running conversation
|
|
2714
|
+
// Minted up-front so the "Copy chat ID" tool always has something to copy
|
|
2715
|
+
// (even before the first message) and so the id we copy is the SAME one the
|
|
2716
|
+
// first send persists under. Reset to a fresh id on New chat, replaced by the
|
|
2717
|
+
// real id when a saved thread is opened.
|
|
2718
|
+
var threadId = genId('thread');
|
|
2719
|
+
|
|
2720
|
+
// Cross-page hand-off: /chat (or a future AI-driven page nav) can pass
|
|
2721
|
+
// ?rk_chat=<threadId> to continue that EXACT conversation in this dock. Read it
|
|
2722
|
+
// once, strip it from the URL (so reload / back-button doesn't re-trigger), and
|
|
2723
|
+
// replay the thread as soon as boot is ready (see consumeHandoff, called from
|
|
2724
|
+
// loadBoot's ready path). Also opens the panel below so the jump is seamless.
|
|
2725
|
+
var handoffThread = null;
|
|
2726
|
+
try {
|
|
2727
|
+
var _hq = new URLSearchParams(window.location.search);
|
|
2728
|
+
handoffThread = _hq.get('rk_chat') || null;
|
|
2729
|
+
if (handoffThread) {
|
|
2730
|
+
_hq.delete('rk_chat');
|
|
2731
|
+
var _hqs = _hq.toString();
|
|
2732
|
+
window.history.replaceState(window.history.state, '', window.location.pathname + (_hqs ? '?' + _hqs : '') + window.location.hash);
|
|
2733
|
+
}
|
|
2734
|
+
} catch (e) { handoffThread = null; }
|
|
2735
|
+
// Replay a handed-off thread once the dock is authed + booted. openThread needs
|
|
2736
|
+
// API_KEY (set by loadBoot), so this only fires from the boot-ready path.
|
|
2737
|
+
function consumeHandoff() {
|
|
2738
|
+
if (!handoffThread || BOOT_STATE !== 'ready') return;
|
|
2739
|
+
var id = handoffThread; handoffThread = null;
|
|
2740
|
+
setLeftMode(''); // show the conversation, not the Files/History drawer
|
|
2741
|
+
openThread(id);
|
|
2742
|
+
}
|
|
2743
|
+
var busy = false;
|
|
2744
|
+
var pendingAbort = null; // AbortController for the in-flight reply (Stop button)
|
|
2745
|
+
|
|
2746
|
+
// On the /editor dock the chat MUST be scoped to the OPEN composition, not the
|
|
2747
|
+
// generic brainstorm boot — otherwise the agent has no fork id and can't run
|
|
2748
|
+
// video_context / editor_action (it guesses "chat-brainstorm" and gives up).
|
|
2749
|
+
// The editor page already embeds the composition identity in #hf-boot; use it
|
|
2750
|
+
// as the template context (exactly like the SPA chat's body.template) so
|
|
2751
|
+
// thread_template_id is the real template_… id and create_video is dropped in
|
|
2752
|
+
// favor of the edit tools. Set eagerly so the very first send is correct even
|
|
2753
|
+
// before /chat-dock/boot resolves (that fetch still supplies the api key).
|
|
2754
|
+
var EDITOR_TEMPLATE = null;
|
|
2755
|
+
if (isEditorDock) {
|
|
2756
|
+
try {
|
|
2757
|
+
var hfBootEl = document.getElementById('hf-boot');
|
|
2758
|
+
var hfBoot = hfBootEl ? JSON.parse(hfBootEl.textContent || '{}') : null;
|
|
2759
|
+
var editorTplId = hfBoot && (hfBoot.templateId || hfBoot.compositionId);
|
|
2760
|
+
if (editorTplId) {
|
|
2761
|
+
EDITOR_TEMPLATE = {
|
|
2762
|
+
page: 'docs', tracerId: null, tracers: [], defaultRequestTracer: null,
|
|
2763
|
+
templateId: editorTplId,
|
|
2764
|
+
templateSlug: (hfBoot && (hfBoot.slugId || hfBoot.templateId)) || editorTplId,
|
|
2765
|
+
templateTitle: (hfBoot && hfBoot.title) || 'Studio',
|
|
2766
|
+
templateDescription: '', docsRoutes: []
|
|
2767
|
+
};
|
|
2768
|
+
TEMPLATE = EDITOR_TEMPLATE;
|
|
2769
|
+
TEMPLATE_ID = editorTplId;
|
|
2770
|
+
}
|
|
2771
|
+
} catch (e) {}
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
function genId(p) { return p + '-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8); }
|
|
2775
|
+
function setBusy(v) {
|
|
2776
|
+
busy = v;
|
|
2777
|
+
// While a reply streams, the Send button becomes an interruptive Stop
|
|
2778
|
+
// (stays clickable so the user can abort); it flips back to Send when done.
|
|
2779
|
+
if (form) { var b = form.querySelector('button[type=submit]'); if (b) { b.textContent = v ? 'Stop' : 'Send'; b.classList.toggle('is-stop', v); } }
|
|
2780
|
+
if (newChatBtn) newChatBtn.disabled = v;
|
|
2781
|
+
}
|
|
2782
|
+
function clientContext() {
|
|
2783
|
+
var now = new Date(), local = null, tz = null;
|
|
2784
|
+
try { local = new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(now); } catch (e) { local = now.toString(); }
|
|
2785
|
+
try { tz = (Intl.DateTimeFormat().resolvedOptions().timeZone) || null; } catch (e) { tz = null; }
|
|
2786
|
+
return { currentDateTime: now.toISOString(), localDateTime: local, timeZone: tz };
|
|
2787
|
+
}
|
|
2788
|
+
// The live composition snapshot (fork id, layers, DNA) the SPA editor bundle
|
|
2789
|
+
// exposes on window.__vidfarmEditorAction.getSnapshot(). Injected into the
|
|
2790
|
+
// outgoing user turn — verbatim to what the SPA's own chat sends — so the agent
|
|
2791
|
+
// knows which fork to read (video_context) and mutate (editor_action). Only the
|
|
2792
|
+
// /editor dock has this bridge; elsewhere it returns ''.
|
|
2793
|
+
function editorContextBlock() {
|
|
2794
|
+
if (!isEditorDock) return '';
|
|
2795
|
+
var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
|
|
2796
|
+
if (!bridge || typeof bridge.getSnapshot !== 'function') return '';
|
|
2797
|
+
var snap; try { snap = bridge.getSnapshot(); } catch (e) { snap = null; }
|
|
2798
|
+
if (!snap) return '';
|
|
2799
|
+
try { return '\n\n<editor_context>\n' + JSON.stringify(snap, null, 2) + '\n</editor_context>'; }
|
|
2800
|
+
catch (e) { return ''; }
|
|
2801
|
+
}
|
|
2802
|
+
function loadBoot() {
|
|
2803
|
+
if (BOOT_STATE === 'ready' || BOOT_STATE === 'loading') return;
|
|
2804
|
+
BOOT_STATE = 'loading';
|
|
2805
|
+
fetch('/chat-dock/boot' + (isEditorDock ? '?surface=editor' : ''), { credentials: 'same-origin', headers: { accept: 'application/json' } })
|
|
2806
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
2807
|
+
.then(function (j) {
|
|
2808
|
+
var b = j && j.editorChat;
|
|
2809
|
+
if (!b) { BOOT_STATE = 'anon'; if (leftMode() === 'cloud') loadTasks(); return; }
|
|
2810
|
+
BOOT = b;
|
|
2811
|
+
ENDPOINT = b.apiUrl || ENDPOINT;
|
|
2812
|
+
API_KEY = b.vidfarmApiKey || null;
|
|
2813
|
+
// On the editor dock keep the composition-scoped template from #hf-boot —
|
|
2814
|
+
// the generic brainstorm boot would strand the agent without a fork id.
|
|
2815
|
+
if (EDITOR_TEMPLATE) { TEMPLATE = EDITOR_TEMPLATE; TEMPLATE_ID = EDITOR_TEMPLATE.templateId; }
|
|
2816
|
+
else { TEMPLATE = b.template || null; TEMPLATE_ID = (TEMPLATE && TEMPLATE.templateId) || TEMPLATE_ID; }
|
|
2817
|
+
CACHED = b.cachedContext || null;
|
|
2818
|
+
THREADS_URL = b.threadsUrl || THREADS_URL;
|
|
2819
|
+
BOOT_STATE = 'ready';
|
|
2820
|
+
loadThreads();
|
|
2821
|
+
if (leftMode() === 'cloud') loadTasks();
|
|
2822
|
+
consumeHandoff();
|
|
2823
|
+
})
|
|
2824
|
+
.catch(function () { BOOT_STATE = 'error'; });
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
// ── first-class HTTP action card + request/response modal ──
|
|
2828
|
+
function fmtBody(v) {
|
|
2829
|
+
if (v === null || v === undefined) return '';
|
|
2830
|
+
if (typeof v === 'string') { try { return JSON.stringify(JSON.parse(v), null, 2); } catch (e) { return v; } }
|
|
2831
|
+
try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }
|
|
2832
|
+
}
|
|
2833
|
+
function copyText(t) {
|
|
2834
|
+
try { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(t); return; } } catch (e) {}
|
|
2835
|
+
try { var ta = document.createElement('textarea'); ta.value = t; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } catch (e) {}
|
|
2836
|
+
}
|
|
2837
|
+
var COPY_SVG = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
2838
|
+
var CARET_SVG = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 4 6 6-6 6"/></svg>';
|
|
2839
|
+
|
|
2840
|
+
// Expandable/collapsible JSON tree — a recursive walk of the value with a
|
|
2841
|
+
// per-path collapsed set (re-rendered on toggle). Mirrors the /editor chat's
|
|
2842
|
+
// JsonViewer: indent depth*16+10px, quoted-key + colon, [ … ]/{ … } collapsed
|
|
2843
|
+
// previews, per-line hover copy button.
|
|
2844
|
+
function jsonViewer(value) {
|
|
2845
|
+
var collapsed = {};
|
|
2846
|
+
var wrap = document.createElement('div'); wrap.className = 'rk-aichat-json';
|
|
2847
|
+
var bar = document.createElement('div'); bar.className = 'rk-aichat-json-bar';
|
|
2848
|
+
var expand = document.createElement('button'); expand.type = 'button'; expand.className = 'rk-aichat-json-all'; expand.textContent = 'Collapse all';
|
|
2849
|
+
var copyAll = document.createElement('button'); copyAll.type = 'button'; copyAll.className = 'rk-aichat-json-all'; copyAll.textContent = 'Copy all';
|
|
2850
|
+
bar.appendChild(expand); bar.appendChild(copyAll);
|
|
2851
|
+
var scroll = document.createElement('div'); scroll.className = 'rk-aichat-json-scroll';
|
|
2852
|
+
wrap.appendChild(bar); wrap.appendChild(scroll);
|
|
2853
|
+
function fmt(v) { if (typeof v === 'string') return JSON.stringify(v); if (typeof v === 'number' || typeof v === 'boolean') return String(v); if (v === null) return 'null'; return JSON.stringify(v); }
|
|
2854
|
+
function lineRow(text, depth) {
|
|
2855
|
+
var row = document.createElement('div'); row.className = 'rk-aichat-json-line'; row.style.paddingLeft = (depth * 16 + 10) + 'px';
|
|
2856
|
+
var cp = document.createElement('button'); cp.type = 'button'; cp.className = 'rk-aichat-json-copy'; cp.setAttribute('aria-label', 'Copy line'); cp.innerHTML = COPY_SVG;
|
|
2857
|
+
cp.addEventListener('click', function () { copyText(text); });
|
|
2858
|
+
var sp = document.createElement('span'); sp.className = 'rk-aichat-json-text'; sp.textContent = text;
|
|
2859
|
+
row.appendChild(cp); row.appendChild(sp); return row;
|
|
2860
|
+
}
|
|
2861
|
+
function toggleRow(text, depth, path, open) {
|
|
2862
|
+
var row = document.createElement('div'); row.className = 'rk-aichat-json-line is-toggle'; row.style.paddingLeft = (depth * 16 + 10) + 'px';
|
|
2863
|
+
var t = document.createElement('button'); t.type = 'button'; t.className = 'rk-aichat-json-caret'; t.setAttribute('data-open', open ? 'true' : 'false'); t.innerHTML = CARET_SVG;
|
|
2864
|
+
t.addEventListener('click', function () { if (open) collapsed[path] = true; else delete collapsed[path]; render(); });
|
|
2865
|
+
var sp = document.createElement('span'); sp.className = 'rk-aichat-json-text'; sp.textContent = text;
|
|
2866
|
+
row.appendChild(t); row.appendChild(sp); return row;
|
|
2867
|
+
}
|
|
2868
|
+
function walk(v, depth, prefix, isLast, path) {
|
|
2869
|
+
var comma = isLast ? '' : ',';
|
|
2870
|
+
if (Array.isArray(v)) {
|
|
2871
|
+
if (!v.length) { scroll.appendChild(lineRow(prefix + '[]' + comma, depth)); return; }
|
|
2872
|
+
if (collapsed[path]) { scroll.appendChild(toggleRow(prefix + '[ … ]' + comma, depth, path, false)); return; }
|
|
2873
|
+
scroll.appendChild(toggleRow(prefix + '[', depth, path, true));
|
|
2874
|
+
for (var i = 0; i < v.length; i++) walk(v[i], depth + 1, '', i === v.length - 1, path + '.' + i);
|
|
2875
|
+
scroll.appendChild(lineRow(']' + comma, depth));
|
|
2876
|
+
} else if (v && typeof v === 'object') {
|
|
2877
|
+
var keys = Object.keys(v);
|
|
2878
|
+
if (!keys.length) { scroll.appendChild(lineRow(prefix + '{}' + comma, depth)); return; }
|
|
2879
|
+
if (collapsed[path]) { scroll.appendChild(toggleRow(prefix + '{ … }' + comma, depth, path, false)); return; }
|
|
2880
|
+
scroll.appendChild(toggleRow(prefix + '{', depth, path, true));
|
|
2881
|
+
for (var k = 0; k < keys.length; k++) {
|
|
2882
|
+
var key = keys[k], cp = JSON.stringify(key) + ': ', last = k === keys.length - 1, child = v[key];
|
|
2883
|
+
if (child && typeof child === 'object') walk(child, depth + 1, cp, last, path + '.' + key);
|
|
2884
|
+
else scroll.appendChild(lineRow(cp + fmt(child) + (last ? '' : ','), depth + 1));
|
|
2885
|
+
}
|
|
2886
|
+
scroll.appendChild(lineRow('}' + comma, depth));
|
|
2887
|
+
} else {
|
|
2888
|
+
scroll.appendChild(lineRow(prefix + fmt(v) + comma, depth));
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
function render() { scroll.innerHTML = ''; walk(value, 0, '', true, 'root'); }
|
|
2892
|
+
var allCollapsed = false;
|
|
2893
|
+
function markAll(v, path) {
|
|
2894
|
+
if (Array.isArray(v)) { if (v.length) { collapsed[path] = true; for (var i = 0; i < v.length; i++) markAll(v[i], path + '.' + i); } }
|
|
2895
|
+
else if (v && typeof v === 'object') { var ks = Object.keys(v); if (ks.length) { collapsed[path] = true; for (var k = 0; k < ks.length; k++) markAll(v[ks[k]], path + '.' + ks[k]); } }
|
|
2896
|
+
}
|
|
2897
|
+
expand.addEventListener('click', function () {
|
|
2898
|
+
allCollapsed = !allCollapsed;
|
|
2899
|
+
if (allCollapsed) { markAll(value, 'root'); delete collapsed['root']; expand.textContent = 'Expand all'; }
|
|
2900
|
+
else { collapsed = {}; expand.textContent = 'Collapse all'; }
|
|
2901
|
+
render();
|
|
2902
|
+
});
|
|
2903
|
+
copyAll.addEventListener('click', function () { copyText(fmtBody(value)); });
|
|
2904
|
+
render();
|
|
2905
|
+
return wrap;
|
|
2906
|
+
}
|
|
2907
|
+
function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
2908
|
+
// Render inline markdown in assistant text: bold, italic, markdown links and
|
|
2909
|
+
// bare absolute URLs → HTML (mirrors the /editor chat's renderInlineMarkdown).
|
|
2910
|
+
// Order matters: **bold** before *italic*; links before bare-URL autolinking
|
|
2911
|
+
// (an href="…" is never preceded by whitespace/'(' so it won't double-link).
|
|
2912
|
+
function linkify(text) {
|
|
2913
|
+
var BT = String.fromCharCode(96);
|
|
2914
|
+
var html = escapeHtml(text);
|
|
2915
|
+
html = html.replace(new RegExp(BT + '([^' + BT + ']+)' + BT, 'g'), '<code>$1</code>');
|
|
2916
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
2917
|
+
html = html.replace(/(^|[^\w*])\*([^*\n]+)\*/g, '$1<em>$2</em>');
|
|
2918
|
+
html = html.replace(/(^|[^\w_])_([^_\n]+)_/g, '$1<em>$2</em>');
|
|
2919
|
+
html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
2920
|
+
html = html.replace(/(^|[\s(])(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/g, '$1<a href="$2" target="_blank" rel="noopener noreferrer">$2</a>');
|
|
2921
|
+
return html;
|
|
2922
|
+
}
|
|
2923
|
+
// ── GitHub-style pipe tables ──────────────────────────────────────────────
|
|
2924
|
+
// A table = a header row containing '|', a separator row of dashes/pipes, then
|
|
2925
|
+
// 0+ body rows. Used both by assistant-message rendering and .md previews so
|
|
2926
|
+
// the model's markdown tables (job registers, comparisons) render as tables.
|
|
2927
|
+
function rkTableCells(line) { var s = String(line).trim().replace(/^\|/, '').replace(/\|$/, ''); return s.split('|').map(function (c) { return c.trim(); }); }
|
|
2928
|
+
function rkIsTableSep(line) { return /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)+\|?\s*$/.test(String(line || '')); }
|
|
2929
|
+
function rkBuildTable(header, rows, fmt) {
|
|
2930
|
+
var thead = '<thead><tr>' + header.map(function (c) { return '<th>' + fmt(c) + '</th>'; }).join('') + '</tr></thead>';
|
|
2931
|
+
var tbody = '<tbody>' + rows.map(function (r) {
|
|
2932
|
+
var cells = ''; for (var i = 0; i < header.length; i++) { cells += '<td>' + fmt(r[i] != null ? r[i] : '') + '</td>'; }
|
|
2933
|
+
return '<tr>' + cells + '</tr>';
|
|
2934
|
+
}).join('') + '</tbody>';
|
|
2935
|
+
return '<div class="rk-md-tablewrap"><table class="rk-md-table">' + thead + tbody + '</table></div>';
|
|
2936
|
+
}
|
|
2937
|
+
// Assistant message → HTML: pipe tables become real <table>s; everything else
|
|
2938
|
+
// keeps its inline (linkify) formatting under the bubble's pre-wrap whitespace.
|
|
2939
|
+
function formatAssistantMarkdown(text) {
|
|
2940
|
+
var lines = String(text).replace(/\r\n?/g, '\n').split('\n');
|
|
2941
|
+
var out = [], buf = [], i = 0;
|
|
2942
|
+
function flush() { if (buf.length) { out.push(linkify(buf.join('\n'))); buf = []; } }
|
|
2943
|
+
while (i < lines.length) {
|
|
2944
|
+
var ln = lines[i], nxt = (i + 1 < lines.length) ? lines[i + 1] : null;
|
|
2945
|
+
if (ln.indexOf('|') >= 0 && nxt != null && rkIsTableSep(nxt) && rkTableCells(ln).length >= 2) {
|
|
2946
|
+
flush();
|
|
2947
|
+
var header = rkTableCells(ln), rows = [], j = i + 2;
|
|
2948
|
+
while (j < lines.length && lines[j].indexOf('|') >= 0 && lines[j].trim() !== '') { rows.push(rkTableCells(lines[j])); j++; }
|
|
2949
|
+
out.push(rkBuildTable(header, rows, linkify)); i = j; continue;
|
|
2950
|
+
}
|
|
2951
|
+
buf.push(ln); i++;
|
|
2952
|
+
}
|
|
2953
|
+
flush();
|
|
2954
|
+
return out.join('');
|
|
2955
|
+
}
|
|
2956
|
+
function openHttpModal(ex) {
|
|
2957
|
+
var req = ex.request || {}, res = ex.response || {};
|
|
2958
|
+
var status = typeof res.status === 'number' ? res.status : 0;
|
|
2959
|
+
var isErr = status >= 400 || res.error;
|
|
2960
|
+
var back = document.createElement('div');
|
|
2961
|
+
back.className = 'rk-aichat-modal-back';
|
|
2962
|
+
var modal = document.createElement('div');
|
|
2963
|
+
modal.className = 'rk-aichat-modal';
|
|
2964
|
+
var head = document.createElement('div');
|
|
2965
|
+
head.className = 'rk-aichat-modal-head';
|
|
2966
|
+
head.innerHTML = '<span class="rk-aichat-http-method">' + (req.method || 'GET') + '</span>'
|
|
2967
|
+
+ '<span class="rk-aichat-http-url"></span>'
|
|
2968
|
+
+ '<span class="rk-aichat-http-status" data-error="' + (isErr ? 'true' : 'false') + '">' + (status || '—') + (res.statusText ? ' ' + res.statusText : '') + '</span>'
|
|
2969
|
+
+ '<button type="button" class="rk-aichat-modal-x" aria-label="Close">×</button>';
|
|
2970
|
+
head.querySelector('.rk-aichat-http-url').textContent = req.url || req.path || '';
|
|
2971
|
+
var body = document.createElement('div');
|
|
2972
|
+
body.className = 'rk-aichat-modal-body';
|
|
2973
|
+
// A raw string block (URL, error text). Always plain <pre>.
|
|
2974
|
+
function textBlock(label, text) {
|
|
2975
|
+
if (!text) return;
|
|
2976
|
+
var wrap = document.createElement('div'); wrap.className = 'rk-aichat-http-block';
|
|
2977
|
+
var l = document.createElement('div'); l.className = 'rk-aichat-http-blabel'; l.textContent = label;
|
|
2978
|
+
var pre = document.createElement('pre'); pre.className = 'rk-aichat-http-pre'; pre.textContent = text;
|
|
2979
|
+
wrap.appendChild(l); wrap.appendChild(pre); body.appendChild(wrap);
|
|
2980
|
+
}
|
|
2981
|
+
// A body/headers block: collapsible JSON tree when structured, else <pre>.
|
|
2982
|
+
function dataBlock(label, value) {
|
|
2983
|
+
if (value === null || value === undefined || value === '') return;
|
|
2984
|
+
var data = value;
|
|
2985
|
+
if (typeof value === 'string') { try { var p = JSON.parse(value); if (p && typeof p === 'object') data = p; } catch (e) {} }
|
|
2986
|
+
var wrap = document.createElement('div'); wrap.className = 'rk-aichat-http-block';
|
|
2987
|
+
var l = document.createElement('div'); l.className = 'rk-aichat-http-blabel'; l.textContent = label;
|
|
2988
|
+
wrap.appendChild(l);
|
|
2989
|
+
if (data && typeof data === 'object') { wrap.appendChild(jsonViewer(data)); }
|
|
2990
|
+
else { var pre = document.createElement('pre'); pre.className = 'rk-aichat-http-pre'; pre.textContent = (typeof value === 'string') ? value : fmtBody(value); wrap.appendChild(pre); }
|
|
2991
|
+
body.appendChild(wrap);
|
|
2992
|
+
}
|
|
2993
|
+
function hasKeys(o) { return o && typeof o === 'object' && Object.keys(o).length > 0; }
|
|
2994
|
+
if (ex.explanation) { var note = document.createElement('div'); note.className = 'rk-aichat-http-note'; note.textContent = ex.explanation; body.appendChild(note); }
|
|
2995
|
+
textBlock('URL', req.url || req.path || '');
|
|
2996
|
+
if (req.body !== undefined && req.body !== null && req.body !== '') dataBlock('Request', req.body);
|
|
2997
|
+
if (res.error) textBlock('Response', res.error);
|
|
2998
|
+
else dataBlock('Response', (res.body === undefined || res.body === null || res.body === '') ? '(empty)' : res.body);
|
|
2999
|
+
if (hasKeys(res.headers)) dataBlock('Response headers', res.headers);
|
|
3000
|
+
modal.appendChild(head); modal.appendChild(body); back.appendChild(modal);
|
|
3001
|
+
function close() { if (back.parentNode) back.parentNode.removeChild(back); document.removeEventListener('keydown', onKey); }
|
|
3002
|
+
function onKey(e) { if (e.key === 'Escape') { e.stopPropagation(); close(); } }
|
|
3003
|
+
back.addEventListener('click', function (e) { if (e.target === back) close(); });
|
|
3004
|
+
head.querySelector('.rk-aichat-modal-x').addEventListener('click', close);
|
|
3005
|
+
document.addEventListener('keydown', onKey);
|
|
3006
|
+
document.body.appendChild(back);
|
|
3007
|
+
}
|
|
3008
|
+
function httpCard(ex) {
|
|
3009
|
+
var req = ex.request || {}, res = ex.response || {};
|
|
3010
|
+
var status = typeof res.status === 'number' ? res.status : 0;
|
|
3011
|
+
var isErr = status >= 400 || res.error;
|
|
3012
|
+
var b = document.createElement('button');
|
|
3013
|
+
b.type = 'button';
|
|
3014
|
+
b.className = 'rk-aichat-http';
|
|
3015
|
+
var m = document.createElement('span'); m.className = 'rk-aichat-http-method'; m.textContent = req.method || 'GET';
|
|
3016
|
+
var u = document.createElement('span'); u.className = 'rk-aichat-http-url';
|
|
3017
|
+
var url = req.url || req.path || '';
|
|
3018
|
+
u.textContent = url.replace(/^https?:\/\/[^/]+/, ''); u.title = url;
|
|
3019
|
+
var s = document.createElement('span'); s.className = 'rk-aichat-http-status';
|
|
3020
|
+
s.setAttribute('data-error', isErr ? 'true' : 'false'); s.textContent = status || '—';
|
|
3021
|
+
b.appendChild(m); b.appendChild(u); b.appendChild(s);
|
|
3022
|
+
b.addEventListener('click', function () { openHttpModal(ex); });
|
|
3023
|
+
return b;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// ── file preview: classify a file by content-type + extension ──
|
|
3027
|
+
function fileKind(name, ct) {
|
|
3028
|
+
ct = (ct || '').toLowerCase();
|
|
3029
|
+
var ext = ((name || '').split('.').pop() || '').toLowerCase();
|
|
3030
|
+
if (ct.indexOf('image/') === 0 || /^(png|jpe?g|gif|webp|svg|avif|bmp|heic|ico)$/.test(ext)) return 'image';
|
|
3031
|
+
if (ct.indexOf('video/') === 0 || /^(mp4|mov|webm|m4v|mkv|ogv)$/.test(ext)) return 'video';
|
|
3032
|
+
if (ct.indexOf('audio/') === 0 || /^(mp3|wav|m4a|aac|ogg|oga|flac|opus)$/.test(ext)) return 'audio';
|
|
3033
|
+
if (ct.indexOf('application/pdf') === 0 || ext === 'pdf') return 'pdf';
|
|
3034
|
+
if (ct.indexOf('json') >= 0 || ext === 'json') return 'json';
|
|
3035
|
+
if (ext === 'md' || ext === 'markdown' || ct.indexOf('markdown') >= 0) return 'markdown';
|
|
3036
|
+
if (ct.indexOf('text/') === 0 || /^(txt|csv|tsv|srt|vtt|log|xml|yaml|yml|ini|conf|html?)$/.test(ext)) return 'text';
|
|
3037
|
+
return 'other';
|
|
3038
|
+
}
|
|
3039
|
+
function fileIcon(kind) { return kind === 'pdf' ? '📕' : kind === 'json' ? '{ }' : kind === 'markdown' ? '📝' : kind === 'audio' ? '🎵' : kind === 'text' ? '📄' : '📄'; }
|
|
3040
|
+
|
|
3041
|
+
// Compact markdown → HTML for .md previews (headings, lists, quotes, rules,
|
|
3042
|
+
// fenced + inline code, bold/italic/links). Escapes first, then injects tags
|
|
3043
|
+
// (same order as linkify). Backticks are built via char codes to avoid
|
|
3044
|
+
// escaping issues inside this template literal.
|
|
3045
|
+
function renderMarkdown(src) {
|
|
3046
|
+
var BT = String.fromCharCode(96), FENCE = BT + BT + BT;
|
|
3047
|
+
function inline(s) {
|
|
3048
|
+
s = escapeHtml(s);
|
|
3049
|
+
s = s.replace(new RegExp(BT + '([^' + BT + ']+)' + BT, 'g'), '<code>$1</code>');
|
|
3050
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
3051
|
+
s = s.replace(/(^|[^\w*])\*([^*\n]+)\*/g, '$1<em>$2</em>');
|
|
3052
|
+
s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
3053
|
+
s = s.replace(/(^|[\s(])(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/g, '$1<a href="$2" target="_blank" rel="noopener noreferrer">$2</a>');
|
|
3054
|
+
return s;
|
|
3055
|
+
}
|
|
3056
|
+
var lines = String(src).replace(/\r\n?/g, '\n').split('\n');
|
|
3057
|
+
var out = [], inCode = false, code = [], listType = null, listItems = [];
|
|
3058
|
+
function flushList() { if (!listType) return; out.push('<' + listType + '>' + listItems.join('') + '</' + listType + '>'); listType = null; listItems = []; }
|
|
3059
|
+
function flushCode() { out.push('<pre class="rk-aichat-preview-code"><code>' + escapeHtml(code.join('\n')) + '</code></pre>'); code = []; }
|
|
3060
|
+
for (var i = 0; i < lines.length; i++) {
|
|
3061
|
+
var ln = lines[i];
|
|
3062
|
+
if (ln.trim().indexOf(FENCE) === 0) { if (inCode) { flushCode(); inCode = false; } else { flushList(); inCode = true; } continue; }
|
|
3063
|
+
if (inCode) { code.push(ln); continue; }
|
|
3064
|
+
if (ln.indexOf('|') >= 0 && (i + 1) < lines.length && rkIsTableSep(lines[i + 1]) && rkTableCells(ln).length >= 2) {
|
|
3065
|
+
flushList();
|
|
3066
|
+
var thead = rkTableCells(ln), trows = [], jj = i + 2;
|
|
3067
|
+
while (jj < lines.length && lines[jj].indexOf('|') >= 0 && lines[jj].trim() !== '') { trows.push(rkTableCells(lines[jj])); jj++; }
|
|
3068
|
+
out.push(rkBuildTable(thead, trows, inline)); i = jj - 1; continue;
|
|
3069
|
+
}
|
|
3070
|
+
var h = ln.match(/^(#{1,6})\s+(.*)$/);
|
|
3071
|
+
if (h) { flushList(); var lvl = h[1].length; out.push('<h' + lvl + '>' + inline(h[2]) + '</h' + lvl + '>'); continue; }
|
|
3072
|
+
if (/^\s*([-*+])\s+/.test(ln)) { if (listType !== 'ul') { flushList(); listType = 'ul'; } listItems.push('<li>' + inline(ln.replace(/^\s*[-*+]\s+/, '')) + '</li>'); continue; }
|
|
3073
|
+
var om = ln.match(/^\s*\d+\.\s+(.*)$/);
|
|
3074
|
+
if (om) { if (listType !== 'ol') { flushList(); listType = 'ol'; } listItems.push('<li>' + inline(om[1]) + '</li>'); continue; }
|
|
3075
|
+
if (/^\s*>\s?/.test(ln)) { flushList(); out.push('<blockquote>' + inline(ln.replace(/^\s*>\s?/, '')) + '</blockquote>'); continue; }
|
|
3076
|
+
if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(ln)) { flushList(); out.push('<hr>'); continue; }
|
|
3077
|
+
if (!ln.trim()) { flushList(); continue; }
|
|
3078
|
+
flushList();
|
|
3079
|
+
out.push('<p>' + inline(ln) + '</p>');
|
|
3080
|
+
}
|
|
3081
|
+
if (inCode) flushCode();
|
|
3082
|
+
flushList();
|
|
3083
|
+
return out.join('');
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// ── file preview modal: images (carousel), video, audio, pdf, and text
|
|
3087
|
+
// (json → collapsible tree, md → rendered, txt/csv/srt/vtt → <pre>) ──
|
|
3088
|
+
function openFilePreview(items, index) {
|
|
3089
|
+
items = (items || []).filter(function (f) { return f && f.viewUrl; });
|
|
3090
|
+
if (!items.length) return;
|
|
3091
|
+
var idx = Math.max(0, Math.min(index || 0, items.length - 1));
|
|
3092
|
+
|
|
3093
|
+
var back = document.createElement('div'); back.className = 'rk-aichat-modal-back';
|
|
3094
|
+
var modal = document.createElement('div'); modal.className = 'rk-aichat-modal rk-aichat-preview';
|
|
3095
|
+
var head = document.createElement('div'); head.className = 'rk-aichat-modal-head';
|
|
3096
|
+
var nameEl = document.createElement('span'); nameEl.className = 'rk-aichat-http-url rk-aichat-preview-name';
|
|
3097
|
+
var copyBtn = document.createElement('button'); copyBtn.type = 'button'; copyBtn.className = 'rk-aichat-preview-act'; copyBtn.textContent = 'Copy';
|
|
3098
|
+
var dlBtn = document.createElement('a'); dlBtn.className = 'rk-aichat-preview-act'; dlBtn.target = '_blank'; dlBtn.rel = 'noopener noreferrer'; dlBtn.textContent = 'Download';
|
|
3099
|
+
var linkBtn = document.createElement('button'); linkBtn.type = 'button'; linkBtn.className = 'rk-aichat-preview-act'; linkBtn.textContent = 'Copy link';
|
|
3100
|
+
var xBtn = document.createElement('button'); xBtn.type = 'button'; xBtn.className = 'rk-aichat-modal-x'; xBtn.setAttribute('aria-label', 'Close'); xBtn.innerHTML = '×';
|
|
3101
|
+
head.appendChild(nameEl); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
|
|
3102
|
+
function flashAct(btn, label) { var o = btn.getAttribute('data-label') || btn.textContent; btn.setAttribute('data-label', o); btn.textContent = label; setTimeout(function () { btn.textContent = o; }, 1400); }
|
|
3103
|
+
// Copy = file text (json/md/txt) or its URL for binaries. Copy link = the
|
|
3104
|
+
// durable public S3 URL. Download opens in a new tab when the browser can't
|
|
3105
|
+
// force a same-origin download (cross-origin S3).
|
|
3106
|
+
copyBtn.addEventListener('click', function () {
|
|
3107
|
+
var it = items[idx], k = fileKind(it.name, it.contentType);
|
|
3108
|
+
if (k === 'json' || k === 'markdown' || k === 'text') {
|
|
3109
|
+
fetch(it.viewUrl, { credentials: 'same-origin' }).then(function (r) { return r.text(); })
|
|
3110
|
+
.then(function (t) { copyText(t); flashAct(copyBtn, 'Copied'); })
|
|
3111
|
+
.catch(function () { copyText(it.publicUrl || it.viewUrl); flashAct(copyBtn, 'Copied'); });
|
|
3112
|
+
} else { copyText(it.publicUrl || it.viewUrl); flashAct(copyBtn, 'Copied'); }
|
|
3113
|
+
});
|
|
3114
|
+
linkBtn.addEventListener('click', function () { var it = items[idx]; copyText(it.publicUrl || it.viewUrl); flashAct(linkBtn, 'Copied'); });
|
|
3115
|
+
|
|
3116
|
+
var body = document.createElement('div'); body.className = 'rk-aichat-modal-body rk-aichat-preview-body';
|
|
3117
|
+
|
|
3118
|
+
var nav = document.createElement('div'); nav.className = 'rk-aichat-preview-nav';
|
|
3119
|
+
var prev = document.createElement('button'); prev.type = 'button'; prev.className = 'rk-aichat-preview-btn'; prev.innerHTML = '‹ Prev';
|
|
3120
|
+
var counter = document.createElement('span'); counter.className = 'rk-aichat-preview-count';
|
|
3121
|
+
var next = document.createElement('button'); next.type = 'button'; next.className = 'rk-aichat-preview-btn'; next.innerHTML = 'Next ›';
|
|
3122
|
+
nav.appendChild(prev); nav.appendChild(counter); nav.appendChild(next);
|
|
3123
|
+
|
|
3124
|
+
function renderUnknown(container, it, msg) {
|
|
3125
|
+
var wrap = document.createElement('div'); wrap.className = 'rk-aichat-preview-empty';
|
|
3126
|
+
var p = document.createElement('div'); p.textContent = msg || 'No inline preview for this file type.'; wrap.appendChild(p);
|
|
3127
|
+
var a = document.createElement('a'); a.href = it.viewUrl; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.className = 'rk-aichat-preview-dl'; a.textContent = 'Open / download ↗'; wrap.appendChild(a);
|
|
3128
|
+
container.appendChild(wrap);
|
|
3129
|
+
}
|
|
3130
|
+
function renderText(container, it, kind) {
|
|
3131
|
+
var loading = document.createElement('div'); loading.className = 'rk-aichat-preview-loading'; loading.textContent = 'Loading…'; container.appendChild(loading);
|
|
3132
|
+
fetch(it.viewUrl, { credentials: 'same-origin' })
|
|
3133
|
+
.then(function (r) { if (!r.ok) throw new Error('http ' + r.status); return r.text(); })
|
|
3134
|
+
.then(function (txt) {
|
|
3135
|
+
container.innerHTML = '';
|
|
3136
|
+
if (kind === 'json') {
|
|
3137
|
+
var parsed; try { parsed = JSON.parse(txt); } catch (e) { parsed = undefined; }
|
|
3138
|
+
if (parsed && typeof parsed === 'object') { container.appendChild(jsonViewer(parsed)); return; }
|
|
3139
|
+
var pj = document.createElement('pre'); pj.className = 'rk-aichat-http-pre rk-aichat-preview-text'; pj.textContent = txt; container.appendChild(pj); return;
|
|
3140
|
+
}
|
|
3141
|
+
if (kind === 'markdown') { var md = document.createElement('div'); md.className = 'rk-aichat-preview-md'; md.innerHTML = renderMarkdown(txt); container.appendChild(md); return; }
|
|
3142
|
+
var pre = document.createElement('pre'); pre.className = 'rk-aichat-http-pre rk-aichat-preview-text'; pre.textContent = txt; container.appendChild(pre);
|
|
3143
|
+
})
|
|
3144
|
+
.catch(function () { container.innerHTML = ''; renderUnknown(container, it, 'Couldn’t load this file.'); });
|
|
3145
|
+
}
|
|
3146
|
+
function render() {
|
|
3147
|
+
var it = items[idx];
|
|
3148
|
+
nameEl.textContent = it.name || 'file'; nameEl.title = it.name || '';
|
|
3149
|
+
dlBtn.href = it.viewUrl; dlBtn.setAttribute('download', it.name || '');
|
|
3150
|
+
linkBtn.style.display = it.publicUrl ? '' : 'none';
|
|
3151
|
+
body.innerHTML = '';
|
|
3152
|
+
var kind = fileKind(it.name, it.contentType);
|
|
3153
|
+
if (kind === 'image') { var im = document.createElement('img'); im.className = 'rk-aichat-preview-img'; im.src = it.viewUrl; im.alt = it.name || ''; body.appendChild(im); }
|
|
3154
|
+
else if (kind === 'video') { var v = document.createElement('video'); v.className = 'rk-aichat-preview-media'; v.src = it.viewUrl; v.controls = true; v.playsInline = true; body.appendChild(v); }
|
|
3155
|
+
else if (kind === 'audio') { var wrap = document.createElement('div'); wrap.className = 'rk-aichat-preview-audiowrap'; var a = document.createElement('audio'); a.className = 'rk-aichat-preview-audio'; a.src = it.viewUrl; a.controls = true; wrap.appendChild(a); body.appendChild(wrap); }
|
|
3156
|
+
else if (kind === 'pdf') { var f = document.createElement('iframe'); f.className = 'rk-aichat-preview-frame'; f.src = it.viewUrl; body.appendChild(f); }
|
|
3157
|
+
else if (kind === 'json' || kind === 'markdown' || kind === 'text') { renderText(body, it, kind); }
|
|
3158
|
+
else { renderUnknown(body, it); }
|
|
3159
|
+
counter.textContent = (idx + 1) + ' / ' + items.length;
|
|
3160
|
+
prev.disabled = idx <= 0; next.disabled = idx >= items.length - 1;
|
|
3161
|
+
nav.hidden = items.length < 2;
|
|
3162
|
+
}
|
|
3163
|
+
prev.addEventListener('click', function () { if (idx > 0) { idx--; render(); } });
|
|
3164
|
+
next.addEventListener('click', function () { if (idx < items.length - 1) { idx++; render(); } });
|
|
3165
|
+
|
|
3166
|
+
modal.appendChild(head); modal.appendChild(body); modal.appendChild(nav); back.appendChild(modal);
|
|
3167
|
+
function close() { if (back.parentNode) back.parentNode.removeChild(back); document.removeEventListener('keydown', onKey); }
|
|
3168
|
+
function onKey(e) { if (e.key === 'Escape') { e.stopPropagation(); close(); } else if (e.key === 'ArrowLeft') { if (idx > 0) { idx--; render(); } } else if (e.key === 'ArrowRight') { if (idx < items.length - 1) { idx++; render(); } } }
|
|
3169
|
+
back.addEventListener('click', function (e) { if (e.target === back) close(); });
|
|
3170
|
+
xBtn.addEventListener('click', close);
|
|
3171
|
+
document.addEventListener('keydown', onKey);
|
|
3172
|
+
document.body.appendChild(back);
|
|
3173
|
+
render();
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// ── job auto-poll → render finished output as file previews in chat ────────
|
|
3177
|
+
// Jobs the agent starts (brainstorm, generation, render) are async. We watch
|
|
3178
|
+
// every job id we see in assistant text or tool results, and when it finishes
|
|
3179
|
+
// we drop its output (media + JSON artifact) into the chat as file-preview
|
|
3180
|
+
// thumbnails — WITHOUT persisting them to the transcript (keeps context lean).
|
|
3181
|
+
var rkPolledJobs = {};
|
|
3182
|
+
function rkJobHeaders() { var h = { accept: 'application/json' }; if (API_KEY) h['vidfarm-api-key'] = API_KEY; return h; }
|
|
3183
|
+
function rkExtractJobIds(text) { var ids = {}, re = /job_[0-9a-f]{16,}/g, m; while ((m = re.exec(String(text || '')))) { ids[m[0]] = true; } return Object.keys(ids); }
|
|
3184
|
+
function rkJobIdsFromExchange(ex) { try { var body = ex && ex.response && ex.response.body; if (body == null) return []; var txt = (typeof body === 'string') ? body : JSON.stringify(body); return rkExtractJobIds(txt); } catch (e) { return []; } }
|
|
3185
|
+
function rkResultMediaUrl(job) {
|
|
3186
|
+
function s(v) { return (typeof v === 'string' && v.trim()) ? v.trim() : null; }
|
|
3187
|
+
var result = (job && job.result && typeof job.result === 'object') ? job.result : {};
|
|
3188
|
+
var out = (result.output && typeof result.output === 'object') ? result.output : result;
|
|
3189
|
+
var direct = s(out.primary_file_url) || s(out.video && out.video.file_url) || s(out.image && out.image.file_url) || s(out.render && out.render.output_url);
|
|
3190
|
+
if (direct) return direct;
|
|
3191
|
+
if (out.files && out.files.length) { for (var i = 0; i < out.files.length; i++) { var f = out.files[i]; var u = s(typeof f === 'string' ? f : (f && (f.file_url || f.url))); if (u) return u; } }
|
|
3192
|
+
return null;
|
|
3193
|
+
}
|
|
3194
|
+
function rkFileFromArtifact(a) {
|
|
3195
|
+
var url = a && (a.public_url || a.publicUrl); if (!url) return null;
|
|
3196
|
+
var meta = (a && a.metadata) || {}; var name = meta.file_name || meta.filename || '';
|
|
3197
|
+
if (!name) { var seg = String(url).split('?')[0].split('/').pop(); name = seg || (a.kind ? String(a.kind) : 'artifact'); }
|
|
3198
|
+
return { name: name, contentType: meta.content_type || meta.contentType || '', viewUrl: url, publicUrl: url };
|
|
3199
|
+
}
|
|
3200
|
+
function rkJobOutputFiles(job) {
|
|
3201
|
+
var files = [], seen = {};
|
|
3202
|
+
var arts = (job && job.artifacts) || [];
|
|
3203
|
+
for (var i = 0; i < arts.length; i++) { var f = rkFileFromArtifact(arts[i]); if (f && !seen[f.viewUrl]) { seen[f.viewUrl] = true; files.push(f); } }
|
|
3204
|
+
var mu = rkResultMediaUrl(job); if (mu && !seen[mu]) { seen[mu] = true; files.push({ name: (job.tracer || 'output'), contentType: '', viewUrl: mu, publicUrl: mu }); }
|
|
3205
|
+
var res = job && job.result;
|
|
3206
|
+
if (res && typeof res === 'object' && !Array.isArray(res) && Object.keys(res).length) {
|
|
3207
|
+
try { var blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' }); files.push({ name: (job.tracer || 'result') + '.json', contentType: 'application/json', viewUrl: URL.createObjectURL(blob), publicUrl: null }); } catch (e) {}
|
|
3208
|
+
}
|
|
3209
|
+
return files;
|
|
3210
|
+
}
|
|
3211
|
+
function rkPollJob(view, jobId) {
|
|
3212
|
+
if (!jobId || rkPolledJobs[jobId]) return; rkPolledJobs[jobId] = true;
|
|
3213
|
+
var origin = window.location.origin, deadline = Date.now() + 12 * 60 * 1000;
|
|
3214
|
+
function step() {
|
|
3215
|
+
fetch(origin + '/api/v1/user/me/jobs/' + encodeURIComponent(jobId), { headers: rkJobHeaders(), credentials: 'same-origin' })
|
|
3216
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
3217
|
+
.then(function (job) {
|
|
3218
|
+
if (!job) return;
|
|
3219
|
+
var st = String(job.status || '');
|
|
3220
|
+
if (st === 'succeeded') { var files = rkJobOutputFiles(job); if (files.length) view.addFiles(files, 'Output · ' + (job.tracer || jobId)); return; }
|
|
3221
|
+
if (st === 'failed' || st === 'cancelled') return;
|
|
3222
|
+
if (Date.now() > deadline) return;
|
|
3223
|
+
setTimeout(step, 5000);
|
|
3224
|
+
})
|
|
3225
|
+
.catch(function () {});
|
|
3226
|
+
}
|
|
3227
|
+
step();
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
// ── assistant turn controller (status → actions → streamed text) ──
|
|
3231
|
+
function appendAssistant() {
|
|
3232
|
+
var turn = document.createElement('div'); turn.className = 'rk-aichat-turn';
|
|
3233
|
+
var actions = document.createElement('div'); actions.className = 'rk-aichat-actions';
|
|
3234
|
+
var status = document.createElement('div'); status.className = 'rk-aichat-status';
|
|
3235
|
+
status.innerHTML = '<span class="rk-aichat-typing"><span></span><span></span><span></span></span>';
|
|
3236
|
+
var textEl = document.createElement('div'); textEl.className = 'rk-aichat-msg is-ai rk-aichat-stream'; textEl.hidden = true;
|
|
3237
|
+
turn.appendChild(actions); turn.appendChild(status); turn.appendChild(textEl);
|
|
3238
|
+
log.appendChild(turn); logBottom();
|
|
3239
|
+
var lastText = '', flowEl = null;
|
|
3240
|
+
return {
|
|
3241
|
+
hideStatus: function () { status.hidden = true; },
|
|
3242
|
+
showText: function () { status.hidden = true; textEl.hidden = false; },
|
|
3243
|
+
setText: function (t) { lastText = t; textEl.textContent = t; },
|
|
3244
|
+
// After streaming completes, re-render the text as markdown (tables,
|
|
3245
|
+
// bold/italic/links/code) — pipe tables become real <table>s.
|
|
3246
|
+
renderMd: function () { if (lastText) textEl.innerHTML = formatAssistantMarkdown(lastText); },
|
|
3247
|
+
// Attach finished job/tool output as clickable file-preview thumbnails.
|
|
3248
|
+
// Kept OUT of the persisted transcript (convo) so context stays lean.
|
|
3249
|
+
addFiles: function (files, label) {
|
|
3250
|
+
var list = (files || []).filter(function (f) { return f && f.viewUrl; });
|
|
3251
|
+
if (!list.length) return;
|
|
3252
|
+
var wrap = document.createElement('div'); wrap.className = 'rk-aichat-filepreview';
|
|
3253
|
+
if (label) { var lb = document.createElement('div'); lb.className = 'rk-aichat-files-label'; lb.textContent = label; wrap.appendChild(lb); }
|
|
3254
|
+
var grid = document.createElement('div'); grid.className = 'rk-aichat-files-grid';
|
|
3255
|
+
list.forEach(function (f, ix) {
|
|
3256
|
+
var k = fileKind(f.name, f.contentType);
|
|
3257
|
+
var card = document.createElement('button'); card.type = 'button'; card.className = 'rk-aichat-file';
|
|
3258
|
+
var thumb = document.createElement('div'); thumb.className = 'rk-aichat-file-thumb rk-aichat-file-thumb--' + k;
|
|
3259
|
+
if (k === 'image') { var im = document.createElement('img'); im.src = f.viewUrl; im.alt = ''; im.loading = 'lazy'; thumb.appendChild(im); }
|
|
3260
|
+
else if (k === 'video') { var v = document.createElement('video'); v.src = f.viewUrl; v.muted = true; v.preload = 'metadata'; v.playsInline = true; thumb.appendChild(v); var pl = document.createElement('span'); pl.className = 'rk-aichat-file-play'; pl.textContent = '▶'; thumb.appendChild(pl); }
|
|
3261
|
+
else { var ic = document.createElement('span'); ic.className = 'rk-aichat-file-ic'; ic.textContent = fileIcon(k); thumb.appendChild(ic); }
|
|
3262
|
+
var nm = document.createElement('div'); nm.className = 'rk-aichat-file-name'; nm.textContent = f.name || 'file'; nm.title = f.name || '';
|
|
3263
|
+
card.appendChild(thumb); card.appendChild(nm);
|
|
3264
|
+
card.addEventListener('click', function () { openFilePreview(list, ix); });
|
|
3265
|
+
grid.appendChild(card);
|
|
3266
|
+
});
|
|
3267
|
+
wrap.appendChild(grid); turn.appendChild(wrap); logBottom();
|
|
3268
|
+
},
|
|
3269
|
+
addNote: function (name) {
|
|
3270
|
+
var n = document.createElement('div'); n.className = 'rk-aichat-toolnote';
|
|
3271
|
+
n.textContent = 'Running ' + name + '…'; actions.appendChild(n); logBottom();
|
|
3272
|
+
return n;
|
|
3273
|
+
},
|
|
3274
|
+
// Terminal one-line note (kind 'ok' | 'err' | undefined) — e.g. the real
|
|
3275
|
+
// browser-side outcome of an editor_action apply.
|
|
3276
|
+
note: function (text, kind) {
|
|
3277
|
+
var n = document.createElement('div');
|
|
3278
|
+
n.className = 'rk-aichat-toolnote' + (kind === 'err' ? ' is-err' : (kind === 'ok' ? ' is-ok' : ''));
|
|
3279
|
+
n.textContent = text; actions.appendChild(n); logBottom();
|
|
3280
|
+
return n;
|
|
3281
|
+
},
|
|
3282
|
+
addHttp: function (ex) { actions.appendChild(httpCard(ex)); logBottom(); },
|
|
3283
|
+
// create_video pipeline progress (persistent status row, distinct from the
|
|
3284
|
+
// thinking dots which hide once agent text streams).
|
|
3285
|
+
flowStatus: function (s) {
|
|
3286
|
+
if (!flowEl) { flowEl = document.createElement('div'); flowEl.className = 'rk-aichat-flow'; flowEl.innerHTML = '<span class="rk-aichat-typing"><span></span><span></span><span></span></span><span class="rk-aichat-flow-label"></span>'; turn.appendChild(flowEl); }
|
|
3287
|
+
flowEl.hidden = false; flowEl.querySelector('.rk-aichat-flow-label').textContent = s || ''; logBottom();
|
|
3288
|
+
},
|
|
3289
|
+
// Final pipeline message (markdown-linkified, e.g. "Open it in the editor").
|
|
3290
|
+
flowResult: function (md) {
|
|
3291
|
+
if (flowEl) flowEl.hidden = true;
|
|
3292
|
+
var b = document.createElement('div'); b.className = 'rk-aichat-msg is-ai'; b.innerHTML = linkify(md); turn.appendChild(b); logBottom();
|
|
3293
|
+
},
|
|
3294
|
+
fail: function (msg) {
|
|
3295
|
+
status.hidden = true;
|
|
3296
|
+
var er = document.createElement('div'); er.className = 'rk-aichat-err';
|
|
3297
|
+
er.textContent = msg || 'Something went wrong.'; turn.appendChild(er); logBottom();
|
|
3298
|
+
}
|
|
3299
|
+
};
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
// ── SSE reader (mirrors the live editor-chat client) ──
|
|
3303
|
+
function handleEvent(raw, h) {
|
|
3304
|
+
var lines = raw.split('\n'), dataLine = null;
|
|
3305
|
+
for (var j = 0; j < lines.length; j++) { var t = lines[j].trim(); if (t.indexOf('data:') === 0) { dataLine = t; break; } }
|
|
3306
|
+
if (!dataLine) return;
|
|
3307
|
+
var payload = dataLine.slice(5).trim();
|
|
3308
|
+
if (payload === '[DONE]' || !payload) return;
|
|
3309
|
+
var chunk; try { chunk = JSON.parse(payload); } catch (e) { return; }
|
|
3310
|
+
if (!chunk || typeof chunk !== 'object') return;
|
|
3311
|
+
if (chunk.type === 'text-delta' && typeof chunk.delta === 'string') h.onDelta(chunk.delta);
|
|
3312
|
+
else if (chunk.type === 'tool-call' && typeof chunk.toolName === 'string') h.onToolCall(chunk.toolName, chunk.toolCallId);
|
|
3313
|
+
else if (chunk.type === 'tool-result') h.onToolResult(chunk.toolName, chunk.result, chunk.toolCallId);
|
|
3314
|
+
else if (chunk.type === 'error') h.onError((typeof chunk.errorText === 'string') ? chunk.errorText : ((chunk.error && chunk.error.message) || 'Unable to read assistant response.'));
|
|
3315
|
+
}
|
|
3316
|
+
function readStream(response, h) {
|
|
3317
|
+
if (!response.body || !response.body.getReader) return response.text().then(function (t) { t.split('\n\n').forEach(function (ev) { handleEvent(ev, h); }); });
|
|
3318
|
+
var reader = response.body.getReader(), decoder = new TextDecoder(), buffer = '';
|
|
3319
|
+
function pump() {
|
|
3320
|
+
return reader.read().then(function (res) {
|
|
3321
|
+
if (res.done) { if (buffer) handleEvent(buffer, h); return; }
|
|
3322
|
+
buffer += decoder.decode(res.value, { stream: true });
|
|
3323
|
+
var events = buffer.split('\n\n');
|
|
3324
|
+
buffer = events.pop() || '';
|
|
3325
|
+
for (var i = 0; i < events.length; i++) handleEvent(events[i], h);
|
|
3326
|
+
return pump();
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
return pump();
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
// Turn this turn's attachments into a text line the model can quote back as
|
|
3333
|
+
// REST payload URLs (prompt_attachments / source_image_url / media_url), in
|
|
3334
|
+
// addition to the binary file content parts. Mirrors the /editor React dock
|
|
3335
|
+
// (buildAttachmentTransportText). Double-backslash-n = a browser-side newline
|
|
3336
|
+
// (we live inside a script template literal, so a bare newline escape breaks it).
|
|
3337
|
+
function buildAttachmentUrlText(atts) {
|
|
3338
|
+
if (!atts || !atts.length) return '';
|
|
3339
|
+
var lines = atts.map(function (a) { return '- ' + (a.fileName || 'file') + ' (' + (a.contentType || '') + '): ' + a.viewUrl; });
|
|
3340
|
+
return '\n\nAttached file URLs for REST payloads:\n' + lines.join('\n') + '\nUse these exact URLs as prompt_attachments for image generation, source_image_url for image edits, media_url for video slides, or reference attachment URLs when calling template routes.';
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
function sendMessage(text) {
|
|
3344
|
+
if (busy) return;
|
|
3345
|
+
text = (text || '').trim();
|
|
3346
|
+
var picked = (typeof selected !== 'undefined' ? selected : []).slice();
|
|
3347
|
+
// A pasted file may still be uploading to /temp — wait for it rather than
|
|
3348
|
+
// silently dropping the attachment. Flash the in-flight chips as a hint.
|
|
3349
|
+
if (picked.some(function (f) { return f && f.pending; })) {
|
|
3350
|
+
picked.forEach(function (f) { if (f.pending) flashChip(f.id); });
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
var atts = picked.filter(function (f) { return f && f.viewUrl; }).map(function (f) {
|
|
3354
|
+
return { id: String(f.id), fileName: f.name || 'file', contentType: f.contentType || 'application/octet-stream', viewUrl: f.viewUrl };
|
|
3355
|
+
});
|
|
3356
|
+
if (!text && !atts.length) return;
|
|
3357
|
+
if (BOOT_STATE === 'idle' || BOOT_STATE === 'error') loadBoot();
|
|
3358
|
+
|
|
3359
|
+
var line = text;
|
|
3360
|
+
if (picked.length) { line = (text ? text + ' ' : '') + '(attached: ' + picked.map(function (f) { return f.name; }).join(', ') + ')'; }
|
|
3361
|
+
bubble('is-user', line);
|
|
3362
|
+
convo.push({ role: 'user', text: text || line });
|
|
3363
|
+
if (input) { input.value = ''; if (typeof autosizeInput === 'function') autosizeInput(); }
|
|
3364
|
+
if (typeof clearChips === 'function') clearChips();
|
|
3365
|
+
|
|
3366
|
+
if (BOOT_STATE === 'anon') {
|
|
3367
|
+
var v = appendAssistant(); v.showText();
|
|
3368
|
+
v.setText('Sign in to chat with the vidfarm AI on your own provider keys.');
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
var view = appendAssistant();
|
|
3373
|
+
setBusy(true);
|
|
3374
|
+
if (!threadId) threadId = genId('thread');
|
|
3375
|
+
// Attach a fresh <editor_context> to the current (last) user turn only, so the
|
|
3376
|
+
// model sees the composition state without bloating persisted history.
|
|
3377
|
+
var ctxBlock = editorContextBlock();
|
|
3378
|
+
// Attachments (pasted files OR files picked from the directory explorer) must
|
|
3379
|
+
// ride in the model messages as file content parts + a URL text line — the
|
|
3380
|
+
// backend only feeds the model messages[].content, NOT user_message.attachments
|
|
3381
|
+
// (which is persistence-only). Without this the AI never saw attached files.
|
|
3382
|
+
var attUrlText = atts.length ? buildAttachmentUrlText(atts) : '';
|
|
3383
|
+
var outMessages = convo.map(function (m, i) {
|
|
3384
|
+
var isLast = i === convo.length - 1;
|
|
3385
|
+
var isLastUser = isLast && m.role === 'user';
|
|
3386
|
+
var t = m.text;
|
|
3387
|
+
if (isLastUser && attUrlText) t = t + attUrlText;
|
|
3388
|
+
if (ctxBlock && isLastUser) t = t + ctxBlock;
|
|
3389
|
+
var content = [{ type: 'text', text: t }];
|
|
3390
|
+
if (isLastUser && atts.length) {
|
|
3391
|
+
atts.forEach(function (a) { content.push({ type: 'file', data: a.viewUrl, mediaType: a.contentType || 'application/octet-stream' }); });
|
|
3392
|
+
}
|
|
3393
|
+
return { role: m.role, content: content };
|
|
3394
|
+
});
|
|
3395
|
+
var body = {
|
|
3396
|
+
messages: outMessages,
|
|
3397
|
+
thread_id: threadId,
|
|
3398
|
+
thread_template_id: TEMPLATE_ID,
|
|
3399
|
+
thread_title: (text || line).slice(0, 42),
|
|
3400
|
+
thread_tracers: (TEMPLATE && TEMPLATE.tracers) || [],
|
|
3401
|
+
client_context: clientContext(),
|
|
3402
|
+
user_message: { id: genId('user'), role: 'user', text: text || line, attachments: atts },
|
|
3403
|
+
assistant_message: { id: genId('assistant') }
|
|
3404
|
+
};
|
|
3405
|
+
if (TEMPLATE) body.template = TEMPLATE;
|
|
3406
|
+
if (CACHED) body.cached_context = CACHED;
|
|
3407
|
+
var headers = { 'content-type': 'application/json' };
|
|
3408
|
+
if (API_KEY) headers['vidfarm-api-key'] = API_KEY;
|
|
3409
|
+
|
|
3410
|
+
var acc = '', sawText = false, errored = false;
|
|
3411
|
+
var handlers = {
|
|
3412
|
+
onDelta: function (d) { if (!sawText) { sawText = true; view.showText(); } acc += d; view.setText(acc); logBottom(); },
|
|
3413
|
+
onToolCall: function (name) { if (name !== 'http_request' && name !== 'create_video') view.addNote(name); },
|
|
3414
|
+
onToolResult: function (name, result) {
|
|
3415
|
+
if (name === 'create_video') { runCreateVideoFlow(view, result); return; }
|
|
3416
|
+
// editor_action only mutates the composition browser-side. On /editor the
|
|
3417
|
+
// SPA bundle exposes window.__vidfarmEditorAction; apply the tool result
|
|
3418
|
+
// through it and surface the REAL outcome. Without this the dock ignored
|
|
3419
|
+
// editor_action entirely, so the model's edits were pure no-ops even
|
|
3420
|
+
// though its reply claimed success. (Only wired on the editor dock — the
|
|
3421
|
+
// bridge is absent on /discover, /library, etc.)
|
|
3422
|
+
if (name === 'editor_action') { applyEditorActionFromChat(view, result); return; }
|
|
3423
|
+
if (result && result.request && result.response) { view.addHttp(result); rkJobIdsFromExchange(result).forEach(function (id) { rkPollJob(view, id); }); }
|
|
3424
|
+
},
|
|
3425
|
+
onError: function (m) { errored = true; view.fail(m); }
|
|
3426
|
+
};
|
|
3427
|
+
|
|
3428
|
+
var controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
3429
|
+
pendingAbort = controller;
|
|
3430
|
+
var req;
|
|
3431
|
+
try { req = fetch(ENDPOINT, { method: 'POST', headers: headers, credentials: 'same-origin', body: JSON.stringify(body), signal: controller ? controller.signal : undefined }); }
|
|
3432
|
+
catch (err) { req = Promise.reject(err); }
|
|
3433
|
+
req.then(function (resp) {
|
|
3434
|
+
if (!resp.ok) return resp.json().catch(function () { return {}; }).then(function (j) { throw new Error((j && j.error) || ('Chat request failed (' + resp.status + ').')); });
|
|
3435
|
+
return readStream(resp, handlers);
|
|
3436
|
+
}).then(function () {
|
|
3437
|
+
pendingAbort = null;
|
|
3438
|
+
view.hideStatus();
|
|
3439
|
+
if (sawText) { convo.push({ role: 'assistant', text: acc }); view.renderMd(); rkExtractJobIds(acc).forEach(function (id) { rkPollJob(view, id); }); }
|
|
3440
|
+
else if (!errored) { view.showText(); view.setText('Done.'); }
|
|
3441
|
+
setBusy(false);
|
|
3442
|
+
if (input) input.focus();
|
|
3443
|
+
loadThreads();
|
|
3444
|
+
}).catch(function (err) {
|
|
3445
|
+
pendingAbort = null;
|
|
3446
|
+
// User hit Stop mid-reply — keep whatever streamed so far, no error.
|
|
3447
|
+
if (err && (err.name === 'AbortError' || (controller && controller.signal && controller.signal.aborted))) {
|
|
3448
|
+
view.hideStatus();
|
|
3449
|
+
if (sawText) { convo.push({ role: 'assistant', text: acc }); view.renderMd(); }
|
|
3450
|
+
else { view.showText(); view.setText('Stopped.'); }
|
|
3451
|
+
setBusy(false); if (input) input.focus();
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
var msg = (err && err.message) ? err.message : String(err);
|
|
3455
|
+
if (!API_KEY) msg = msg + '\n\nAdd an AI provider key in Settings to chat on your own keys.';
|
|
3456
|
+
view.fail(msg); setBusy(false); if (input) input.focus();
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
function resetConversation() {
|
|
3461
|
+
convo = []; threadId = genId('thread'); rkPolledJobs = {};
|
|
3462
|
+
if (log) { log.innerHTML = ''; bubble('is-ai', GREETING); }
|
|
3463
|
+
if (typeof clearChips === 'function') clearChips();
|
|
3464
|
+
}
|
|
3465
|
+
function newChat() {
|
|
3466
|
+
resetConversation();
|
|
3467
|
+
setActiveThread(null);
|
|
3468
|
+
if (input) { input.value = ''; if (typeof autosizeInput === 'function') autosizeInput(); input.focus(); }
|
|
3469
|
+
}
|
|
3470
|
+
// NOTE: the New-chat button element (rkNewChat) is resolved later (see the
|
|
3471
|
+
// var block below), so bind its click handler there — binding here would run
|
|
3472
|
+
// against an undefined newChatBtn (var hoisting) and silently never attach.
|
|
3473
|
+
if (form) form.addEventListener('submit', function (e) {
|
|
3474
|
+
e.preventDefault();
|
|
3475
|
+
if (busy) { if (pendingAbort) pendingAbort.abort(); return; }
|
|
3476
|
+
sendMessage(input ? input.value : '');
|
|
3477
|
+
});
|
|
3478
|
+
|
|
3479
|
+
// The composer is now a multi-row textarea: grow with content (capped) and let
|
|
3480
|
+
// Enter send (Shift+Enter inserts a newline, like every modern chat box).
|
|
3481
|
+
function autosizeInput() {
|
|
3482
|
+
if (!input || input.tagName !== 'TEXTAREA') return;
|
|
3483
|
+
input.style.height = 'auto';
|
|
3484
|
+
input.style.height = Math.min(input.scrollHeight, 210) + 'px';
|
|
3485
|
+
}
|
|
3486
|
+
if (input) {
|
|
3487
|
+
input.addEventListener('input', autosizeInput);
|
|
3488
|
+
input.addEventListener('keydown', function (e) {
|
|
3489
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
3490
|
+
e.preventDefault();
|
|
3491
|
+
if (form && form.requestSubmit) form.requestSubmit();
|
|
3492
|
+
else { if (busy) { if (pendingAbort) pendingAbort.abort(); } else sendMessage(input.value); }
|
|
3493
|
+
}
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// ─── editor_action: apply a composition mutation through the SPA bridge that
|
|
3498
|
+
// the editor bundle exposes on window (__vidfarmEditorAction). Only present on
|
|
3499
|
+
// the /editor dock; elsewhere there is no composition to mutate. ───
|
|
3500
|
+
function applyEditorActionFromChat(view, result) {
|
|
3501
|
+
var label = (result && result.action_type) ? String(result.action_type) : 'editor_action';
|
|
3502
|
+
var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
|
|
3503
|
+
if (!bridge || typeof bridge.apply !== 'function') {
|
|
3504
|
+
view.note('⚠ Editor not ready — reload the page, then ask again (' + label + ' was not applied).', 'err');
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
// bridge.apply may be async (Option-B editor writes through the files API);
|
|
3508
|
+
// normalize sync + promise return shapes.
|
|
3509
|
+
var settle = function (outcome) {
|
|
3510
|
+
if (outcome && outcome.ok) {
|
|
3511
|
+
view.note('✓ ' + (outcome.summary || ('Applied ' + label + '.')), 'ok');
|
|
3512
|
+
} else {
|
|
3513
|
+
view.note('⚠ Couldn’t apply ' + label + ((outcome && outcome.error) ? ': ' + outcome.error : '.'), 'err');
|
|
3514
|
+
}
|
|
3515
|
+
};
|
|
3516
|
+
try {
|
|
3517
|
+
var outcome = bridge.apply(result);
|
|
3518
|
+
if (outcome && typeof outcome.then === 'function') {
|
|
3519
|
+
outcome.then(settle, function (e) { settle({ ok: false, error: (e && e.message) ? e.message : String(e) }); });
|
|
3520
|
+
} else {
|
|
3521
|
+
settle(outcome);
|
|
3522
|
+
}
|
|
3523
|
+
} catch (e) {
|
|
3524
|
+
settle({ ok: false, error: (e && e.message) ? e.message : String(e) });
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
// ─── create_video: the browser-side pipeline behind the agent's create_video
|
|
3529
|
+
// tool. The backend tool is a passthrough (returns the intent only); the
|
|
3530
|
+
// frontend runs ingest → decompose → fork and posts a clickable "Open in the
|
|
3531
|
+
// editor" link. Ported 1:1 from the /editor chat's runCreateVideoFlow. ───
|
|
3532
|
+
function ivHeaders(json) { var h = { accept: 'application/json' }; if (json) h['content-type'] = 'application/json'; if (API_KEY) h['vidfarm-api-key'] = API_KEY; return h; }
|
|
3533
|
+
function ivSleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
|
|
3534
|
+
function ivJson(res) { return res.text().then(function (t) { try { return t ? JSON.parse(t) : null; } catch (e) { return null; } }); }
|
|
3535
|
+
async function runCreateVideoFlow(view, intentRaw) {
|
|
3536
|
+
var intent = intentRaw || {};
|
|
3537
|
+
if (!API_KEY) { view.flowResult('⚠️ You need to be logged in to create a video.'); return; }
|
|
3538
|
+
var origin = window.location.origin;
|
|
3539
|
+
|
|
3540
|
+
function pollInspirationReady(id) {
|
|
3541
|
+
var deadline = Date.now() + 5 * 60 * 1000;
|
|
3542
|
+
function step() {
|
|
3543
|
+
return fetch(origin + '/api/v1/videos/' + encodeURIComponent(id), { headers: ivHeaders(false) }).then(ivJson).then(function (j) {
|
|
3544
|
+
var st = String((j && j.status) || '');
|
|
3545
|
+
if (st === 'ready') return j;
|
|
3546
|
+
if (st === 'failed') throw new Error('source video download failed' + ((j && j.error) ? ': ' + j.error : '') + '.');
|
|
3547
|
+
if (Date.now() > deadline) throw new Error('source video download timed out.');
|
|
3548
|
+
return ivSleep(5000).then(step);
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
return step();
|
|
3552
|
+
}
|
|
3553
|
+
function jobMediaUrl(job) {
|
|
3554
|
+
function s(v) { return (typeof v === 'string' && v.trim()) ? v.trim() : null; }
|
|
3555
|
+
var result = (job && job.result && typeof job.result === 'object') ? job.result : {};
|
|
3556
|
+
var out = (result.output && typeof result.output === 'object') ? result.output : result;
|
|
3557
|
+
var direct = s(out.primary_file_url) || s(out.video && out.video.file_url) || s(out.image && out.image.file_url) || s(out.render && out.render.output_url);
|
|
3558
|
+
if (direct) return direct;
|
|
3559
|
+
if (out.files && out.files.length) { for (var i = 0; i < out.files.length; i++) { var f = out.files[i]; var u = s(typeof f === 'string' ? f : (f && (f.file_url || f.url))); if (u) return u; } }
|
|
3560
|
+
if (job && job.artifacts && job.artifacts.length) { for (var k = 0; k < job.artifacts.length; k++) { var u2 = s(job.artifacts[k] && job.artifacts[k].public_url); if (u2) return u2; } }
|
|
3561
|
+
return null;
|
|
3562
|
+
}
|
|
3563
|
+
function pollJobMedia(jobId) {
|
|
3564
|
+
var deadline = Date.now() + 8 * 60 * 1000;
|
|
3565
|
+
function step() {
|
|
3566
|
+
return ivSleep(5000).then(function () {
|
|
3567
|
+
return fetch(origin + '/api/v1/user/me/jobs/' + encodeURIComponent(jobId), { headers: ivHeaders(false) }).then(ivJson).then(function (j) {
|
|
3568
|
+
var st = String((j && j.status) || '');
|
|
3569
|
+
var url = jobMediaUrl(j);
|
|
3570
|
+
if (url) return url;
|
|
3571
|
+
if (st === 'failed' || st === 'cancelled') throw new Error('generation ' + st + '.');
|
|
3572
|
+
if (Date.now() > deadline) throw new Error('generation timed out.');
|
|
3573
|
+
return step();
|
|
3574
|
+
});
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
return step();
|
|
3578
|
+
}
|
|
3579
|
+
async function ingestGeneratedVideo(mediaUrl, title) {
|
|
3580
|
+
var blob = await (await fetch(mediaUrl)).blob();
|
|
3581
|
+
var presignRes = await fetch(origin + '/discover/templates/upload/presign', { method: 'POST', headers: ivHeaders(true), body: JSON.stringify({ file_name: 'generated.mp4', content_type: 'video/mp4', size_bytes: blob.size }) });
|
|
3582
|
+
var presign = await ivJson(presignRes);
|
|
3583
|
+
if (!presignRes.ok) throw new Error((presign && presign.error) || ('upload presign failed (' + presignRes.status + ')'));
|
|
3584
|
+
var storageKey = presign && presign.storage_key;
|
|
3585
|
+
var uploadedName = (presign && presign.file_name) || 'generated.mp4';
|
|
3586
|
+
if (presign && presign.transport === 'presigned' && presign.upload && presign.upload.url) {
|
|
3587
|
+
var put = await fetch(presign.upload.url, { method: (presign.upload.method || 'PUT'), headers: presign.upload.headers || {}, body: blob });
|
|
3588
|
+
if (!put.ok) throw new Error('upload failed (' + put.status + ')');
|
|
3589
|
+
} else {
|
|
3590
|
+
var form = new FormData(); form.append('file', blob, 'generated.mp4');
|
|
3591
|
+
var up = await fetch(origin + ((presign && presign.upload && presign.upload.url) || '/discover/templates/upload'), { method: 'POST', headers: ivHeaders(false), body: form });
|
|
3592
|
+
var upJson = await ivJson(up);
|
|
3593
|
+
if (!up.ok || !upJson || !upJson.storage_key) throw new Error((upJson && upJson.error) || ('upload failed (' + up.status + ')'));
|
|
3594
|
+
storageKey = upJson.storage_key; uploadedName = upJson.file_name || 'generated.mp4';
|
|
3595
|
+
}
|
|
3596
|
+
if (!storageKey) throw new Error('upload did not return a storage key.');
|
|
3597
|
+
var addRes = await fetch(origin + '/discover/templates', { method: 'POST', headers: ivHeaders(true), body: JSON.stringify({ upload: { storage_key: storageKey, file_name: uploadedName }, title: title, origin: 'raw' }) });
|
|
3598
|
+
var add = await ivJson(addRes);
|
|
3599
|
+
if (!addRes.ok) throw new Error((add && add.error) || ('ingest failed (' + addRes.status + ')'));
|
|
3600
|
+
return add;
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
try {
|
|
3604
|
+
var mode = intent.mode === 'replicate' ? 'replicate' : (intent.mode === 'generate' ? 'generate' : null);
|
|
3605
|
+
var inspiration, userPrompt;
|
|
3606
|
+
if (mode === 'replicate') {
|
|
3607
|
+
var sourceUrl = String(intent.source_url || '').trim();
|
|
3608
|
+
if (!/^https?:\/\//i.test(sourceUrl)) { view.flowResult('⚠️ I need a video URL (TikTok/YouTube/Instagram/X) to replicate.'); return; }
|
|
3609
|
+
userPrompt = String(intent.instructions || '').trim() || null;
|
|
3610
|
+
view.flowStatus('Fetching the source video…');
|
|
3611
|
+
var addRes = await fetch(origin + '/discover/templates', { method: 'POST', headers: ivHeaders(true), body: JSON.stringify({ source_url: sourceUrl }) });
|
|
3612
|
+
var add = await ivJson(addRes);
|
|
3613
|
+
if (!addRes.ok) throw new Error((add && add.error) || ("couldn't fetch that URL (" + addRes.status + ')'));
|
|
3614
|
+
inspiration = (add && add.status === 'ready') ? add : await pollInspirationReady(add && add.inspiration_id);
|
|
3615
|
+
} else if (mode === 'generate') {
|
|
3616
|
+
var prompt = String(intent.prompt || '').trim();
|
|
3617
|
+
if (!prompt) { view.flowResult("⚠️ Tell me what the video should be about and I'll create it."); return; }
|
|
3618
|
+
userPrompt = prompt;
|
|
3619
|
+
view.flowStatus('Generating your video… (this can take a couple of minutes)');
|
|
3620
|
+
var genRes = await fetch(origin + '/api/v1/primitives/videos/generate', { method: 'POST', headers: ivHeaders(true), body: JSON.stringify({ tracer: 'chat-create-' + Date.now().toString(36), payload: { prompt: prompt } }) });
|
|
3621
|
+
var gen = await ivJson(genRes);
|
|
3622
|
+
if (!genRes.ok) throw new Error((gen && gen.error) || ("couldn't start generation (" + genRes.status + ')'));
|
|
3623
|
+
var mediaUrl = await pollJobMedia(gen && gen.job_id);
|
|
3624
|
+
view.flowStatus('Building an editable template from the generated video…');
|
|
3625
|
+
var add2 = await ingestGeneratedVideo(mediaUrl, prompt.slice(0, 80));
|
|
3626
|
+
inspiration = (add2 && add2.status === 'ready') ? add2 : await pollInspirationReady(add2 && add2.inspiration_id);
|
|
3627
|
+
} else {
|
|
3628
|
+
view.flowResult("⚠️ I couldn't tell whether to generate a new video or replicate a URL — try rephrasing.");
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
var templateId = inspiration && inspiration.template_id;
|
|
3632
|
+
var inspirationId = inspiration && inspiration.inspiration_id;
|
|
3633
|
+
if (!templateId || !inspirationId) throw new Error("the template wasn't created from the video.");
|
|
3634
|
+
view.flowStatus(mode === 'replicate' ? 'Recreating the scenes… (about a minute)' : 'Analyzing into an editable timeline…');
|
|
3635
|
+
var decRes = await fetch(origin + '/api/v1/inspirations/' + encodeURIComponent(inspirationId) + '/decompose', { method: 'POST', headers: ivHeaders(true), body: JSON.stringify({ user_prompt: userPrompt }) });
|
|
3636
|
+
if (!decRes.ok) { var dec = await ivJson(decRes); throw new Error((dec && dec.error) || ('scene analysis failed (' + decRes.status + ')')); }
|
|
3637
|
+
view.flowStatus('Finalizing your editable copy…');
|
|
3638
|
+
var forkRes = await fetch(origin + '/api/v1/compositions', { method: 'POST', headers: ivHeaders(true), body: JSON.stringify({ template_id: templateId }) });
|
|
3639
|
+
var fork = await ivJson(forkRes);
|
|
3640
|
+
if (!forkRes.ok) throw new Error((fork && fork.error) || ("couldn't create an editable copy (" + forkRes.status + ')'));
|
|
3641
|
+
var forkId = fork && fork.fork_id;
|
|
3642
|
+
var editorUrl = origin + '/editor/' + encodeURIComponent(templateId) + (forkId ? '/fork/' + encodeURIComponent(forkId) : '');
|
|
3643
|
+
view.flowResult('✅ Your video is ready — [Open it in the editor](' + editorUrl + ').');
|
|
3644
|
+
} catch (error) {
|
|
3645
|
+
view.flowResult('⚠️ I couldn’t finish creating the video: ' + ((error && error.message) ? error.message : String(error)));
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
// ─── Files drawer ───
|
|
3650
|
+
// Directory browsing lives in the reusable component (src/frontend/file-directory.ts),
|
|
3651
|
+
// auto-mounted into #rkAichatFilesMount by /assets/file-directory-app.js. This
|
|
3652
|
+
// inline script only owns the chat-side glue: the attach chips + preview modal.
|
|
3653
|
+
// We expose those to the component as host hooks BEFORE the deferred module runs
|
|
3654
|
+
// (this classic <script> executes during parse, ahead of the module).
|
|
3655
|
+
var chipsEl = document.getElementById('rkAichatChips');
|
|
3656
|
+
var filesToggle = document.getElementById('rkFilesToggle');
|
|
3657
|
+
var historyToggle = document.getElementById('rkHistoryToggle');
|
|
3658
|
+
var newChatBtn = document.getElementById('rkNewChat');
|
|
3659
|
+
if (newChatBtn) newChatBtn.addEventListener('click', function () { if (!busy) newChat(); });
|
|
3660
|
+
// Copy the current chat thread id (handy for support / linking a conversation).
|
|
3661
|
+
// threadId is minted up-front, so this always has a real id to copy.
|
|
3662
|
+
var copyThreadBtn = document.getElementById('rkCopyThread');
|
|
3663
|
+
if (copyThreadBtn) copyThreadBtn.addEventListener('click', function () {
|
|
3664
|
+
var prevTitle = copyThreadBtn.title;
|
|
3665
|
+
copyText(String(threadId));
|
|
3666
|
+
copyThreadBtn.title = 'Copied chat ID';
|
|
3667
|
+
copyThreadBtn.classList.add('is-on');
|
|
3668
|
+
setTimeout(function () { copyThreadBtn.title = prevTitle; copyThreadBtn.classList.remove('is-on'); }, 1400);
|
|
3669
|
+
});
|
|
3670
|
+
var histBody = document.getElementById('rkAichatHistBody');
|
|
3671
|
+
var selected = [];
|
|
3672
|
+
window.__rkDirectoryHost = {
|
|
3673
|
+
onAttach: function (items) { (items || []).forEach(function (it) { attachFile(it); }); },
|
|
3674
|
+
onPreview: function (items, i) { openFilePreview(items || [], i || 0); }
|
|
3675
|
+
};
|
|
3676
|
+
function refreshFiles() { if (window.__rkDirectoryInstance) window.__rkDirectoryInstance.refresh(); }
|
|
3677
|
+
|
|
3678
|
+
// Editor dock "Back to Library": leave the editor cleanly — clear the persisted
|
|
3679
|
+
// open-state so the destination page loads with the chat panel closed and on a
|
|
3680
|
+
// fresh chat, then navigate. (Real anchor href="/library" is the fallback.)
|
|
3681
|
+
var backBtn = document.getElementById('rkAichatBack');
|
|
3682
|
+
if (backBtn) backBtn.addEventListener('click', function (e) {
|
|
3683
|
+
try { sessionStorage.removeItem('rk-chat-open'); } catch (err) {}
|
|
3684
|
+
e.preventDefault();
|
|
3685
|
+
window.location.assign('/library');
|
|
3686
|
+
});
|
|
3687
|
+
|
|
3688
|
+
// Lightweight transient toast for editor-dock timeline placement feedback.
|
|
3689
|
+
function editorPlaceToast(msg, isErr) {
|
|
3690
|
+
try {
|
|
3691
|
+
var t = document.createElement('div');
|
|
3692
|
+
t.textContent = msg;
|
|
3693
|
+
t.style.cssText = 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:120;'
|
|
3694
|
+
+ 'padding:9px 15px;border-radius:10px;font:600 13px/1.3 var(--rk-font-body,system-ui);'
|
|
3695
|
+
+ 'color:' + (isErr ? '#fff' : '#1a1500') + ';background:' + (isErr ? '#b23b3b' : 'var(--rk-gold-600,#e5a400)') + ';'
|
|
3696
|
+
+ 'box-shadow:0 6px 24px rgba(0,0,0,.28);max-width:min(420px,90vw);text-align:center;pointer-events:none;'
|
|
3697
|
+
+ 'opacity:0;transition:opacity .18s ease,transform .18s ease';
|
|
3698
|
+
document.body.appendChild(t);
|
|
3699
|
+
requestAnimationFrame(function () { t.style.opacity = '1'; t.style.transform = 'translateX(-50%) translateY(-4px)'; });
|
|
3700
|
+
setTimeout(function () { t.style.opacity = '0'; setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 220); }, 2200);
|
|
3701
|
+
} catch (e) {}
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
function attachFile(it) {
|
|
3705
|
+
// In the /editor dock the "+"/Attach means "drop this file onto the timeline
|
|
3706
|
+
// at the current playhead", NOT "attach to chat" (which is what it means
|
|
3707
|
+
// everywhere else). Route placeable media through the SPA editor bridge; only
|
|
3708
|
+
// fall through to chat-attach for folders / non-placeable files.
|
|
3709
|
+
if (isEditorDock && it && it.viewUrl) {
|
|
3710
|
+
var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
|
|
3711
|
+
if (bridge && typeof bridge.placeMediaAtPlayhead === 'function') {
|
|
3712
|
+
var res = bridge.placeMediaAtPlayhead({ url: it.viewUrl, name: it.name, contentType: it.contentType });
|
|
3713
|
+
if (res && res.ok) { editorPlaceToast('Added \u201c' + (it.name || 'media') + '\u201d to the timeline'); return; }
|
|
3714
|
+
// Non-placeable (folder / doc / not-ready): fall through to chat-attach.
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
// Files-only drawer (opened from the /chat page) has no chat composer of its
|
|
3718
|
+
// own — hand the picked file to the /chat page's composer via a DOM event.
|
|
3719
|
+
if (panel.classList.contains('is-files-only')) {
|
|
3720
|
+
try {
|
|
3721
|
+
document.dispatchEvent(new CustomEvent('rk-chat-attach-file', { detail: {
|
|
3722
|
+
id: String(it.id), name: it.name || 'file',
|
|
3723
|
+
contentType: it.contentType || 'application/octet-stream', viewUrl: it.viewUrl || ''
|
|
3724
|
+
} }));
|
|
3725
|
+
} catch (e) {}
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3728
|
+
if (selected.some(function (f) { return f.id === it.id; })) { flashChip(it.id); return; }
|
|
3729
|
+
selected.push(it);
|
|
3730
|
+
renderChips();
|
|
3731
|
+
if (input) input.focus();
|
|
3732
|
+
}
|
|
3733
|
+
function renderChips() {
|
|
3734
|
+
if (!chipsEl) return;
|
|
3735
|
+
chipsEl.innerHTML = '';
|
|
3736
|
+
selected.forEach(function (f, i) {
|
|
3737
|
+
var chip = document.createElement('span');
|
|
3738
|
+
chip.className = 'rk-aichat-chip' + (f.pending ? ' is-pending' : '');
|
|
3739
|
+
chip.setAttribute('data-id', f.id);
|
|
3740
|
+
if (f.pending) { var sp = document.createElement('span'); sp.className = 'rk-aichat-chip-spin'; sp.setAttribute('aria-hidden', 'true'); chip.appendChild(sp); }
|
|
3741
|
+
var label = document.createElement('button');
|
|
3742
|
+
label.type = 'button';
|
|
3743
|
+
label.className = 'rk-aichat-chip-label';
|
|
3744
|
+
label.title = f.pending ? 'Uploading ' + f.name + '…' : 'Preview ' + f.name;
|
|
3745
|
+
label.textContent = f.name;
|
|
3746
|
+
if (f.viewUrl && !f.pending) label.addEventListener('click', function () { openFilePreview(selected.filter(function (s) { return !s.pending; }), selected.filter(function (s) { return !s.pending; }).indexOf(f)); });
|
|
3747
|
+
var x = document.createElement('button');
|
|
3748
|
+
x.type = 'button';
|
|
3749
|
+
x.setAttribute('aria-label', 'Remove ' + f.name);
|
|
3750
|
+
x.textContent = '×';
|
|
3751
|
+
x.addEventListener('click', function () {
|
|
3752
|
+
selected = selected.filter(function (s) { return s.id !== f.id; });
|
|
3753
|
+
renderChips();
|
|
3754
|
+
});
|
|
3755
|
+
chip.appendChild(label); chip.appendChild(x);
|
|
3756
|
+
chipsEl.appendChild(chip);
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
function flashChip(id) {
|
|
3760
|
+
var chip = chipsEl && chipsEl.querySelector('[data-id="' + id + '"]');
|
|
3761
|
+
if (!chip) return;
|
|
3762
|
+
chip.style.transition = 'none';
|
|
3763
|
+
chip.style.transform = 'scale(1.08)';
|
|
3764
|
+
setTimeout(function () { chip.style.transition = 'transform .18s'; chip.style.transform = 'none'; }, 20);
|
|
3765
|
+
}
|
|
3766
|
+
function clearChips() { selected = []; renderChips(); }
|
|
3767
|
+
|
|
3768
|
+
// ─── left drawer mode: 'files' | 'history' | 'cloud' | '' (closed) ─────────
|
|
3769
|
+
// Files, Chat history, and Cloud status share the one left slot, so only one
|
|
3770
|
+
// shows at a time.
|
|
3771
|
+
function leftMode() {
|
|
3772
|
+
if (panel.classList.contains('has-history')) return 'history';
|
|
3773
|
+
if (panel.classList.contains('has-cloud')) return 'cloud';
|
|
3774
|
+
if (panel.classList.contains('has-files')) return 'files';
|
|
3775
|
+
return '';
|
|
3776
|
+
}
|
|
3777
|
+
function filesOpen() { return leftMode() === 'files'; }
|
|
3778
|
+
function setLeftMode(mode) {
|
|
3779
|
+
panel.classList.toggle('has-files', mode === 'files');
|
|
3780
|
+
panel.classList.toggle('has-history', mode === 'history');
|
|
3781
|
+
panel.classList.toggle('has-cloud', mode === 'cloud');
|
|
3782
|
+
if (filesToggle) {
|
|
3783
|
+
filesToggle.classList.toggle('is-on', mode === 'files');
|
|
3784
|
+
filesToggle.setAttribute('aria-pressed', mode === 'files' ? 'true' : 'false');
|
|
3785
|
+
}
|
|
3786
|
+
if (historyToggle) {
|
|
3787
|
+
historyToggle.classList.toggle('is-on', mode === 'history');
|
|
3788
|
+
historyToggle.setAttribute('aria-pressed', mode === 'history' ? 'true' : 'false');
|
|
3789
|
+
}
|
|
3790
|
+
if (cloudToggle) {
|
|
3791
|
+
cloudToggle.classList.toggle('is-on', mode === 'cloud');
|
|
3792
|
+
cloudToggle.setAttribute('aria-pressed', mode === 'cloud' ? 'true' : 'false');
|
|
3793
|
+
}
|
|
3794
|
+
var caBtn = document.getElementById('rkAichatAttach');
|
|
3795
|
+
if (caBtn) {
|
|
3796
|
+
caBtn.classList.toggle('is-on', mode === 'files');
|
|
3797
|
+
caBtn.setAttribute('aria-pressed', mode === 'files' ? 'true' : 'false');
|
|
3798
|
+
}
|
|
3799
|
+
try { localStorage.setItem('rk-left-mode', mode); } catch (e) {}
|
|
3800
|
+
if (mode === 'files') refreshFiles();
|
|
3801
|
+
if (mode === 'history') loadThreads();
|
|
3802
|
+
if (mode === 'cloud') loadTasks();
|
|
3803
|
+
if (!panel.hidden) anchorPanel();
|
|
3804
|
+
}
|
|
3805
|
+
function refreshFilesOnOpen() {
|
|
3806
|
+
// Re-fetch the current folder so listings are fresh when the drawer opens.
|
|
3807
|
+
if (filesOpen()) refreshFiles();
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
// ─── Chat history — REAL editor-chat threads (past conversations) ──────────
|
|
3811
|
+
var threads = [], threadsState = 'idle', loadingThread = false;
|
|
3812
|
+
function authHeaders() { var h = { accept: 'application/json' }; if (API_KEY) h['vidfarm-api-key'] = API_KEY; return h; }
|
|
3813
|
+
function relTime(iso) {
|
|
3814
|
+
if (!iso) return '';
|
|
3815
|
+
var t = Date.parse(iso); if (isNaN(t)) return '';
|
|
3816
|
+
var s = Math.max(0, (Date.now() - t) / 1000);
|
|
3817
|
+
if (s < 60) return 'just now';
|
|
3818
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
|
3819
|
+
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
|
3820
|
+
if (s < 604800) return Math.floor(s / 86400) + 'd ago';
|
|
3821
|
+
try { return new Date(t).toLocaleDateString([], { month: 'short', day: 'numeric' }); } catch (e) { return ''; }
|
|
3822
|
+
}
|
|
3823
|
+
function histNote(text, opts) {
|
|
3824
|
+
var wrap = document.createElement('div'); wrap.className = 'rk-aichat-fstate';
|
|
3825
|
+
var p = document.createElement('div'); p.textContent = text; wrap.appendChild(p);
|
|
3826
|
+
if (opts && opts.href) { var a = document.createElement('a'); a.href = opts.href; a.textContent = opts.linkText || 'Open'; wrap.appendChild(a); }
|
|
3827
|
+
if (opts && opts.retry) { var b = document.createElement('button'); b.type = 'button'; b.textContent = 'Try again'; b.addEventListener('click', function () { loadThreads(); }); wrap.appendChild(b); }
|
|
3828
|
+
return wrap;
|
|
3829
|
+
}
|
|
3830
|
+
function setActiveThread(id) {
|
|
3831
|
+
threadId = id;
|
|
3832
|
+
if (!histBody) return;
|
|
3833
|
+
var rows = histBody.querySelectorAll('.rk-aichat-frow');
|
|
3834
|
+
for (var i = 0; i < rows.length; i++) rows[i].classList.toggle('is-active', rows[i].getAttribute('data-id') === id);
|
|
3835
|
+
}
|
|
3836
|
+
function threadRow(t) {
|
|
3837
|
+
var b = document.createElement('button');
|
|
3838
|
+
b.type = 'button';
|
|
3839
|
+
b.className = 'rk-aichat-frow' + (t.id === threadId ? ' is-active' : '');
|
|
3840
|
+
b.setAttribute('data-id', t.id);
|
|
3841
|
+
var ic = document.createElement('span');
|
|
3842
|
+
ic.className = 'rk-aichat-fic rk-aichat-fic-file';
|
|
3843
|
+
ic.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>';
|
|
3844
|
+
var main = document.createElement('span'); main.className = 'rk-aichat-fmain';
|
|
3845
|
+
var name = document.createElement('span'); name.className = 'rk-aichat-fname'; name.textContent = t.title || 'Untitled chat';
|
|
3846
|
+
var count = (typeof t.messageCount === 'number') ? t.messageCount : 0;
|
|
3847
|
+
var when = relTime(t.lastMessageAt || t.updatedAt);
|
|
3848
|
+
var meta = document.createElement('span'); meta.className = 'rk-aichat-fmeta';
|
|
3849
|
+
meta.textContent = (count ? count + ' message' + (count === 1 ? '' : 's') : '') + ((count && when) ? ' · ' : '') + (when || '');
|
|
3850
|
+
main.appendChild(name); main.appendChild(meta);
|
|
3851
|
+
var del = document.createElement('button');
|
|
3852
|
+
del.type = 'button'; del.className = 'rk-aichat-fadd'; del.setAttribute('aria-label', 'Delete chat'); del.textContent = '×';
|
|
3853
|
+
del.addEventListener('click', function (ev) { ev.stopPropagation(); deleteThread(t.id, b); });
|
|
3854
|
+
b.appendChild(ic); b.appendChild(main); b.appendChild(del);
|
|
3855
|
+
b.addEventListener('click', function () { openThread(t.id); });
|
|
3856
|
+
return b;
|
|
3857
|
+
}
|
|
3858
|
+
function renderHistory() {
|
|
3859
|
+
if (!histBody) return;
|
|
3860
|
+
histBody.innerHTML = '';
|
|
3861
|
+
if (BOOT_STATE === 'anon') { histBody.appendChild(histNote('Sign in to sync your saved chats.', { href: '/login', linkText: 'Sign in' })); return; }
|
|
3862
|
+
if (threadsState === 'loading' && !threads.length) { histBody.appendChild(histNote('Loading your chats…')); return; }
|
|
3863
|
+
if (threadsState === 'auth') { histBody.appendChild(histNote('Sign in to sync your saved chats.', { href: '/login', linkText: 'Sign in' })); return; }
|
|
3864
|
+
if (threadsState === 'error') { histBody.appendChild(histNote('Couldn’t load your chats.', { retry: true })); return; }
|
|
3865
|
+
if (!threads.length) { histBody.appendChild(histNote('No conversations yet. Start one below — it saves automatically.')); return; }
|
|
3866
|
+
for (var i = 0; i < threads.length; i++) histBody.appendChild(threadRow(threads[i]));
|
|
3867
|
+
}
|
|
3868
|
+
function loadThreads() {
|
|
3869
|
+
if (!histBody) return;
|
|
3870
|
+
if (BOOT_STATE === 'anon') { renderHistory(); return; }
|
|
3871
|
+
if (BOOT_STATE !== 'ready') { loadBoot(); return; } // loadBoot() calls back into loadThreads() when ready
|
|
3872
|
+
threadsState = 'loading'; renderHistory();
|
|
3873
|
+
var url = THREADS_URL + '?template_id=' + encodeURIComponent(TEMPLATE_ID) + '&limit=30&include_messages=false';
|
|
3874
|
+
fetch(url, { headers: authHeaders(), credentials: 'same-origin' })
|
|
3875
|
+
.then(function (r) {
|
|
3876
|
+
if (r.status === 401 || r.status === 403) { threadsState = 'auth'; return null; }
|
|
3877
|
+
if (!r.ok) throw new Error('http ' + r.status);
|
|
3878
|
+
return r.json();
|
|
3879
|
+
})
|
|
3880
|
+
.then(function (j) {
|
|
3881
|
+
if (!j) { renderHistory(); return; }
|
|
3882
|
+
threads = (j && j.threads) || []; threadsState = 'ready'; renderHistory();
|
|
3883
|
+
})
|
|
3884
|
+
.catch(function () { threadsState = 'error'; renderHistory(); });
|
|
3885
|
+
}
|
|
3886
|
+
function openThread(id) {
|
|
3887
|
+
if (busy || loadingThread) return;
|
|
3888
|
+
loadingThread = true;
|
|
3889
|
+
fetch(THREADS_URL + '/' + encodeURIComponent(id), { headers: authHeaders(), credentials: 'same-origin' })
|
|
3890
|
+
.then(function (r) { if (!r.ok) throw new Error('http ' + r.status); return r.json(); })
|
|
3891
|
+
.then(function (j) {
|
|
3892
|
+
var t = j && j.thread; if (!t) throw new Error('missing thread');
|
|
3893
|
+
convo = []; rkPolledJobs = {}; if (log) log.innerHTML = '';
|
|
3894
|
+
var msgs = t.messages || [];
|
|
3895
|
+
for (var i = 0; i < msgs.length; i++) {
|
|
3896
|
+
var m = msgs[i], role = (m.role === 'assistant') ? 'assistant' : 'user', txt = m.text || '';
|
|
3897
|
+
convo.push({ role: role, text: txt });
|
|
3898
|
+
if (role === 'user') { bubble('is-user', txt); }
|
|
3899
|
+
else {
|
|
3900
|
+
var v = appendAssistant();
|
|
3901
|
+
var ex = m.httpExchanges || [];
|
|
3902
|
+
for (var k = 0; k < ex.length; k++) { if (ex[k] && ex[k].request && ex[k].response) v.addHttp(ex[k]); }
|
|
3903
|
+
v.showText(); v.setText(txt); v.hideStatus(); v.renderMd();
|
|
3904
|
+
// Re-hydrate finished job output (not stored in the transcript) by
|
|
3905
|
+
// scanning this turn's text + tool calls for job ids and polling them.
|
|
3906
|
+
var jids = rkExtractJobIds(txt);
|
|
3907
|
+
for (var e2 = 0; e2 < ex.length; e2++) { rkJobIdsFromExchange(ex[e2]).forEach(function (id) { if (jids.indexOf(id) < 0) jids.push(id); }); }
|
|
3908
|
+
(function (view) { jids.forEach(function (id) { rkPollJob(view, id); }); })(v);
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
setActiveThread(id);
|
|
3912
|
+
loadingThread = false;
|
|
3913
|
+
logBottom();
|
|
3914
|
+
if (input) input.focus();
|
|
3915
|
+
})
|
|
3916
|
+
.catch(function () { loadingThread = false; });
|
|
3917
|
+
}
|
|
3918
|
+
function deleteThread(id, row) {
|
|
3919
|
+
if (!API_KEY) return;
|
|
3920
|
+
fetch(THREADS_URL + '/' + encodeURIComponent(id), { method: 'DELETE', headers: authHeaders(), credentials: 'same-origin' })
|
|
3921
|
+
.then(function (r) {
|
|
3922
|
+
if (!r.ok && r.status !== 404) throw new Error('http ' + r.status);
|
|
3923
|
+
threads = threads.filter(function (t) { return t.id !== id; });
|
|
3924
|
+
if (id === threadId) resetConversation();
|
|
3925
|
+
renderHistory();
|
|
3926
|
+
})
|
|
3927
|
+
.catch(function () {});
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
// ─── Cloud status — the customer's long-lived async work (imports, forks,
|
|
3931
|
+
// renders, jobs). Read from /me/tasks; polled every 4s while the drawer is open
|
|
3932
|
+
// and anything is still running. Exposed as window.__vidfarmCloudStatus so any
|
|
3933
|
+
// page (e.g. a raws import) can pop it open and highlight the fresh task. ──────
|
|
3934
|
+
var cloudToggle = document.getElementById('rkCloudToggle');
|
|
3935
|
+
var cloudBody = document.getElementById('rkCloudBody');
|
|
3936
|
+
var cloudFilters = document.getElementById('rkCloudFilters');
|
|
3937
|
+
var cloudDot = document.getElementById('rkCloudDot');
|
|
3938
|
+
var cloudRefresh = document.getElementById('rkCloudRefresh');
|
|
3939
|
+
var TASKS_URL = '/api/v1/user/me/tasks';
|
|
3940
|
+
var tasks = [], tasksState = 'idle', taskFilter = 'all';
|
|
3941
|
+
var taskCounts = { all: 0, running: 0, done: 0, error: 0, queued: 0 };
|
|
3942
|
+
var highlightTaskId = null, tasksTimer = null, tasksLoadSeq = 0;
|
|
3943
|
+
var sidebarCloud = document.getElementById('rkSidebarCloud');
|
|
3944
|
+
var sidebarTimer = null, sidebarStarted = false;
|
|
3945
|
+
|
|
3946
|
+
function taskStatusMeta(status) {
|
|
3947
|
+
if (status === 'running') return { label: 'Running', cls: 'is-running', glyph: '●' };
|
|
3948
|
+
if (status === 'queued') return { label: 'Queued', cls: 'is-queued', glyph: '○' };
|
|
3949
|
+
if (status === 'done') return { label: 'Done', cls: 'is-done', glyph: '✓' };
|
|
3950
|
+
if (status === 'error') return { label: 'Error', cls: 'is-error', glyph: '⚠' };
|
|
3951
|
+
return { label: status || '', cls: '', glyph: '' };
|
|
3952
|
+
}
|
|
3953
|
+
function taskKindLabel(kind) {
|
|
3954
|
+
if (kind === 'render') return 'Render';
|
|
3955
|
+
if (kind === 'import') return 'Import';
|
|
3956
|
+
if (kind === 'fork') return 'Fork';
|
|
3957
|
+
return 'Job';
|
|
3958
|
+
}
|
|
3959
|
+
function renderCloudFilters() {
|
|
3960
|
+
if (!cloudFilters) return;
|
|
3961
|
+
var btns = cloudFilters.querySelectorAll('.rk-cloud-fbtn');
|
|
3962
|
+
for (var i = 0; i < btns.length; i++) {
|
|
3963
|
+
var f = btns[i].getAttribute('data-f');
|
|
3964
|
+
var base = f === 'all' ? 'All' : f === 'running' ? 'Running' : f === 'done' ? 'Done' : 'Error';
|
|
3965
|
+
var n = (f === 'all') ? (taskCounts.all || 0) : (taskCounts[f] || 0);
|
|
3966
|
+
btns[i].textContent = base + (n ? ' ' + n : '');
|
|
3967
|
+
btns[i].classList.toggle('is-on', f === taskFilter);
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
function cloudNote(text, opts) {
|
|
3971
|
+
var wrap = document.createElement('div'); wrap.className = 'rk-aichat-fstate';
|
|
3972
|
+
var p = document.createElement('div'); p.textContent = text; wrap.appendChild(p);
|
|
3973
|
+
if (opts && opts.href) { var a = document.createElement('a'); a.href = opts.href; a.textContent = opts.linkText || 'Open'; wrap.appendChild(a); }
|
|
3974
|
+
if (opts && opts.retry) { var b = document.createElement('button'); b.type = 'button'; b.textContent = 'Try again'; b.addEventListener('click', function () { loadTasks(); }); wrap.appendChild(b); }
|
|
3975
|
+
return wrap;
|
|
3976
|
+
}
|
|
3977
|
+
function taskRow(t) {
|
|
3978
|
+
var row = document.createElement('div');
|
|
3979
|
+
row.className = 'rk-cloud-row rk-cloud-' + (t.status || '');
|
|
3980
|
+
row.setAttribute('data-id', t.id || '');
|
|
3981
|
+
if (t.id && t.id === highlightTaskId) row.classList.add('is-highlight');
|
|
3982
|
+
var meta = taskStatusMeta(t.status);
|
|
3983
|
+
var ic = document.createElement('span');
|
|
3984
|
+
ic.className = 'rk-cloud-ic ' + meta.cls;
|
|
3985
|
+
ic.textContent = meta.glyph;
|
|
3986
|
+
var main = document.createElement('span'); main.className = 'rk-cloud-main';
|
|
3987
|
+
var name = document.createElement('span'); name.className = 'rk-cloud-name';
|
|
3988
|
+
name.textContent = t.label || taskKindLabel(t.kind);
|
|
3989
|
+
var sub = document.createElement('span'); sub.className = 'rk-cloud-sub';
|
|
3990
|
+
var when = relTime(t.updated_at || t.created_at);
|
|
3991
|
+
var bits = [taskKindLabel(t.kind)];
|
|
3992
|
+
if (t.detail) bits.push(t.detail);
|
|
3993
|
+
if (when) bits.push(when);
|
|
3994
|
+
sub.textContent = bits.join(' · ');
|
|
3995
|
+
main.appendChild(name); main.appendChild(sub);
|
|
3996
|
+
var right = document.createElement('span'); right.className = 'rk-cloud-right';
|
|
3997
|
+
var pill = document.createElement('span'); pill.className = 'rk-cloud-pill ' + meta.cls; pill.textContent = meta.label;
|
|
3998
|
+
right.appendChild(pill);
|
|
3999
|
+
if (t.id) {
|
|
4000
|
+
// Arrow now opens the Cloud Task Status page (details / error / result link)
|
|
4001
|
+
// rather than jumping straight to the raw output.
|
|
4002
|
+
var open = document.createElement('a');
|
|
4003
|
+
open.className = 'rk-cloud-open'; open.href = '/cloud/task/' + encodeURIComponent(t.id);
|
|
4004
|
+
open.title = 'View task details'; open.setAttribute('aria-label', 'View task details');
|
|
4005
|
+
open.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>';
|
|
4006
|
+
open.addEventListener('click', function (e) { e.stopPropagation(); });
|
|
4007
|
+
right.appendChild(open);
|
|
4008
|
+
}
|
|
4009
|
+
if (t.error) { row.title = t.error; }
|
|
4010
|
+
row.appendChild(ic); row.appendChild(main); row.appendChild(right);
|
|
4011
|
+
return row;
|
|
4012
|
+
}
|
|
4013
|
+
function renderTasks() {
|
|
4014
|
+
if (!cloudBody) return;
|
|
4015
|
+
renderCloudFilters();
|
|
4016
|
+
cloudBody.innerHTML = '';
|
|
4017
|
+
if (BOOT_STATE === 'anon' || tasksState === 'anon') { cloudBody.appendChild(cloudNote('Sign in to see your cloud tasks.', { href: '/login', linkText: 'Sign in' })); return; }
|
|
4018
|
+
if (tasksState === 'loading' && !tasks.length) { cloudBody.appendChild(cloudNote('Loading your cloud tasks…')); return; }
|
|
4019
|
+
if (tasksState === 'auth') { cloudBody.appendChild(cloudNote('Sign in to see your cloud tasks.', { href: '/login', linkText: 'Sign in' })); return; }
|
|
4020
|
+
if (tasksState === 'error') { cloudBody.appendChild(cloudNote('Couldn’t load your cloud tasks.', { retry: true })); return; }
|
|
4021
|
+
var list = tasks.filter(function (t) { return taskFilter === 'all' || t.status === taskFilter; });
|
|
4022
|
+
if (!list.length) {
|
|
4023
|
+
cloudBody.appendChild(cloudNote(taskFilter === 'all' ? 'No cloud tasks yet. Imports, forks, and renders show up here.' : 'No ' + taskFilter + ' tasks.'));
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
var hi = null;
|
|
4027
|
+
for (var i = 0; i < list.length; i++) { var r = taskRow(list[i]); cloudBody.appendChild(r); if (r.classList.contains('is-highlight')) hi = r; }
|
|
4028
|
+
if (hi && hi.scrollIntoView) { try { hi.scrollIntoView({ block: 'nearest' }); } catch (e) {} }
|
|
4029
|
+
}
|
|
4030
|
+
function updateCloudDot() { if (cloudDot) cloudDot.hidden = (taskCounts.running || 0) <= 0; }
|
|
4031
|
+
function scheduleTaskPoll() {
|
|
4032
|
+
if (tasksTimer) { clearTimeout(tasksTimer); tasksTimer = null; }
|
|
4033
|
+
// Keep polling only while there's live work AND the drawer is showing.
|
|
4034
|
+
if ((taskCounts.running || taskCounts.queued) && leftMode() === 'cloud' && !panel.hidden) {
|
|
4035
|
+
tasksTimer = setTimeout(function () { loadTasks({ silent: true }); }, 4000);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
function loadTasks(opts) {
|
|
4039
|
+
opts = opts || {};
|
|
4040
|
+
if (!cloudBody) return;
|
|
4041
|
+
if (BOOT_STATE === 'anon') { tasksState = 'anon'; renderTasks(); return; }
|
|
4042
|
+
if (BOOT_STATE !== 'ready') { loadBoot(); return; } // loadBoot() calls back into loadTasks() when ready
|
|
4043
|
+
if (!opts.silent) { tasksState = 'loading'; renderTasks(); }
|
|
4044
|
+
var seq = ++tasksLoadSeq;
|
|
4045
|
+
fetch(TASKS_URL + '?limit=80', { headers: authHeaders(), credentials: 'same-origin' })
|
|
4046
|
+
.then(function (r) {
|
|
4047
|
+
if (r.status === 401 || r.status === 403) { tasksState = 'auth'; return null; }
|
|
4048
|
+
if (!r.ok) throw new Error('http ' + r.status);
|
|
4049
|
+
return r.json();
|
|
4050
|
+
})
|
|
4051
|
+
.then(function (j) {
|
|
4052
|
+
if (seq !== tasksLoadSeq) return; // a newer load superseded us
|
|
4053
|
+
if (!j) { renderTasks(); return; }
|
|
4054
|
+
tasks = (j && j.tasks) || [];
|
|
4055
|
+
taskCounts = (j && j.counts) || taskCounts;
|
|
4056
|
+
tasksState = 'ready';
|
|
4057
|
+
renderTasks(); updateCloudDot(); renderSidebarCloud(); scheduleTaskPoll();
|
|
4058
|
+
})
|
|
4059
|
+
.catch(function () { if (seq === tasksLoadSeq) { tasksState = 'error'; renderTasks(); } });
|
|
4060
|
+
}
|
|
4061
|
+
if (cloudToggle) cloudToggle.addEventListener('click', function () { setLeftMode(leftMode() === 'cloud' ? '' : 'cloud'); });
|
|
4062
|
+
if (cloudRefresh) cloudRefresh.addEventListener('click', function () { loadTasks(); });
|
|
4063
|
+
if (cloudFilters) cloudFilters.addEventListener('click', function (e) {
|
|
4064
|
+
var b = (e.target && e.target.closest) ? e.target.closest('.rk-cloud-fbtn') : null;
|
|
4065
|
+
if (!b) return;
|
|
4066
|
+
taskFilter = b.getAttribute('data-f') || 'all';
|
|
4067
|
+
renderTasks();
|
|
4068
|
+
});
|
|
4069
|
+
// Global control surface so any page can pop the Cloud Status drawer open and
|
|
4070
|
+
// highlight the newest task (e.g. right after kicking off a raws import).
|
|
4071
|
+
window.__vidfarmCloudStatus = {
|
|
4072
|
+
open: function (o) {
|
|
4073
|
+
o = o || {};
|
|
4074
|
+
taskFilter = o.filter || 'running';
|
|
4075
|
+
if (typeof o.highlightId !== 'undefined') highlightTaskId = o.highlightId;
|
|
4076
|
+
if (panel.hidden) openPanel();
|
|
4077
|
+
setLeftMode('cloud');
|
|
4078
|
+
loadTasks();
|
|
4079
|
+
},
|
|
4080
|
+
refresh: function () { loadTasks(); },
|
|
4081
|
+
highlight: function (id) { highlightTaskId = id; if (leftMode() === 'cloud') renderTasks(); },
|
|
4082
|
+
isOpen: function () { return leftMode() === 'cloud'; }
|
|
4083
|
+
};
|
|
4084
|
+
|
|
4085
|
+
// ─── Sidebar status line (under Settings) — a subtle always-on indicator of
|
|
4086
|
+
// cloud work: "N tasks running…" with a spinner, or "All tasks done" with a
|
|
4087
|
+
// static cloud icon. Polls /me/tasks independently of the drawer (cookie-authed,
|
|
4088
|
+
// so it works before the chat boot loads); backs off when nothing is running.
|
|
4089
|
+
var CLOUD_ICON_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17.5 19a4.5 4.5 0 0 0 .5-8.98A6 6 0 0 0 6.34 9.5 4 4 0 0 0 7 19h10.5z"/></svg>';
|
|
4090
|
+
function renderSidebarCloud() {
|
|
4091
|
+
if (!sidebarCloud) return;
|
|
4092
|
+
var running = (taskCounts && taskCounts.running) || 0;
|
|
4093
|
+
sidebarCloud.hidden = false;
|
|
4094
|
+
if (running > 0) {
|
|
4095
|
+
sidebarCloud.classList.add('is-running');
|
|
4096
|
+
sidebarCloud.innerHTML = '<span class="rk-sidebar-cloud-spin" aria-hidden="true"></span><span class="rk-sidebar-cloud-label">' + running + ' task' + (running === 1 ? '' : 's') + ' running…</span>';
|
|
4097
|
+
} else {
|
|
4098
|
+
sidebarCloud.classList.remove('is-running');
|
|
4099
|
+
sidebarCloud.innerHTML = CLOUD_ICON_SVG + '<span class="rk-sidebar-cloud-label">All tasks done</span>';
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
function scheduleSidebarPoll() {
|
|
4103
|
+
if (sidebarTimer) { clearTimeout(sidebarTimer); sidebarTimer = null; }
|
|
4104
|
+
var running = (taskCounts && taskCounts.running) || 0;
|
|
4105
|
+
sidebarTimer = setTimeout(pollSidebar, running > 0 ? 6000 : 30000);
|
|
4106
|
+
}
|
|
4107
|
+
function pollSidebar() {
|
|
4108
|
+
if (!sidebarCloud) return;
|
|
4109
|
+
fetch(TASKS_URL + '?limit=1', { headers: authHeaders(), credentials: 'same-origin' })
|
|
4110
|
+
.then(function (r) {
|
|
4111
|
+
if (r.status === 401 || r.status === 403) { sidebarCloud.hidden = true; return null; } // signed out — hide the line
|
|
4112
|
+
if (!r.ok) throw new Error('http ' + r.status);
|
|
4113
|
+
return r.json();
|
|
4114
|
+
})
|
|
4115
|
+
.then(function (j) {
|
|
4116
|
+
if (!j) return; // auth-hidden; stop polling
|
|
4117
|
+
taskCounts = j.counts || taskCounts;
|
|
4118
|
+
renderSidebarCloud(); updateCloudDot(); scheduleSidebarPoll();
|
|
4119
|
+
})
|
|
4120
|
+
.catch(function () { scheduleSidebarPoll(); }); // transient error — try again on the normal cadence
|
|
4121
|
+
}
|
|
4122
|
+
if (sidebarCloud) {
|
|
4123
|
+
sidebarCloud.addEventListener('click', function () {
|
|
4124
|
+
if (window.__vidfarmCloudStatus) window.__vidfarmCloudStatus.open({ filter: 'all' });
|
|
4125
|
+
});
|
|
4126
|
+
sidebarStarted = true;
|
|
4127
|
+
pollSidebar();
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
if (filesToggle) {
|
|
4131
|
+
filesToggle.addEventListener('click', function () { setLeftMode(leftMode() === 'files' ? '' : 'files'); });
|
|
4132
|
+
}
|
|
4133
|
+
// Composer paperclip — opens the folder directory nav (Files drawer) so the
|
|
4134
|
+
// user can browse + attach a file, mirroring the header Files toggle. Works
|
|
4135
|
+
// in both the editor dock and the floating pop-panel.
|
|
4136
|
+
var composerAttach = document.getElementById('rkAichatAttach');
|
|
4137
|
+
if (composerAttach) {
|
|
4138
|
+
composerAttach.addEventListener('click', function () { setLeftMode(leftMode() === 'files' ? '' : 'files'); });
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
// ─── paste-to-attach: drop a copied image/file straight into the composer ───
|
|
4142
|
+
// Clipboard files (screenshot, copied image, "Copy image") get auto-uploaded
|
|
4143
|
+
// to the user's /temp folder, then attached as a chip just like a picked file.
|
|
4144
|
+
// We show a spinner chip immediately and swap it for the real file on success.
|
|
4145
|
+
function extNameFor(blob, idx) {
|
|
4146
|
+
var ct = (blob && blob.type) || '';
|
|
4147
|
+
var base = ct.indexOf('image/') === 0 ? 'pasted-image' : ct.indexOf('video/') === 0 ? 'pasted-video' : ct.indexOf('audio/') === 0 ? 'pasted-audio' : 'pasted-file';
|
|
4148
|
+
var ext = ct && ct.indexOf('/') > -1 ? ct.split('/')[1].split('+')[0].split(';')[0] : 'bin';
|
|
4149
|
+
if (ext === 'jpeg') ext = 'jpg';
|
|
4150
|
+
var stamp = Date.now().toString(36) + (idx ? '-' + idx : '');
|
|
4151
|
+
return base + '-' + stamp + '.' + ext;
|
|
4152
|
+
}
|
|
4153
|
+
function uploadPastedBlob(blob, fileName, pendingId) {
|
|
4154
|
+
var fd = new FormData();
|
|
4155
|
+
fd.append('file', blob, fileName);
|
|
4156
|
+
var headers = {}; if (API_KEY) headers['vidfarm-api-key'] = API_KEY;
|
|
4157
|
+
fetch(window.location.origin + '/api/v1/user/me/temporary-files/upload', {
|
|
4158
|
+
method: 'POST', credentials: 'same-origin', headers: headers, body: fd
|
|
4159
|
+
}).then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { if (!r.ok) throw new Error((j && j.error) || ('upload failed (' + r.status + ')')); return j; }); })
|
|
4160
|
+
.then(function (j) {
|
|
4161
|
+
var f = j && j.file;
|
|
4162
|
+
if (!f || !f.viewUrl) throw new Error('upload returned no file');
|
|
4163
|
+
var idx = selected.map(function (s) { return s.id; }).indexOf(pendingId);
|
|
4164
|
+
var item = { id: String(f.id), name: f.fileName || fileName, contentType: f.contentType || (blob && blob.type) || 'application/octet-stream', viewUrl: f.viewUrl };
|
|
4165
|
+
if (idx > -1) selected[idx] = item; else selected.push(item);
|
|
4166
|
+
renderChips();
|
|
4167
|
+
})
|
|
4168
|
+
.catch(function () {
|
|
4169
|
+
// A failed paste-upload just drops its chip — keep the chat log clean.
|
|
4170
|
+
selected = selected.filter(function (s) { return s.id !== pendingId; });
|
|
4171
|
+
renderChips();
|
|
4172
|
+
});
|
|
4173
|
+
}
|
|
4174
|
+
function handlePasteFiles(e) {
|
|
4175
|
+
// Files-only drawer (on /chat) has no composer of its own — let that page's
|
|
4176
|
+
// composer own paste; here we only handle the dock's real chat composer.
|
|
4177
|
+
if (panel.classList.contains('is-files-only')) return;
|
|
4178
|
+
var dt = e.clipboardData || (window.clipboardData);
|
|
4179
|
+
if (!dt) return;
|
|
4180
|
+
var blobs = [];
|
|
4181
|
+
var items = dt.items;
|
|
4182
|
+
if (items && items.length) {
|
|
4183
|
+
for (var i = 0; i < items.length; i++) {
|
|
4184
|
+
if (items[i] && items[i].kind === 'file') { var b = items[i].getAsFile(); if (b) blobs.push(b); }
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
if (!blobs.length && dt.files && dt.files.length) { for (var k = 0; k < dt.files.length; k++) blobs.push(dt.files[k]); }
|
|
4188
|
+
if (!blobs.length) return; // plain-text paste → let the textarea handle it
|
|
4189
|
+
e.preventDefault();
|
|
4190
|
+
blobs.forEach(function (blob, idx) {
|
|
4191
|
+
var name = (blob && blob.name) || extNameFor(blob, idx);
|
|
4192
|
+
var pendingId = 'paste-' + Date.now().toString(36) + '-' + idx + '-' + Math.random().toString(36).slice(2, 6);
|
|
4193
|
+
selected.push({ id: pendingId, name: name, contentType: (blob && blob.type) || 'application/octet-stream', pending: true });
|
|
4194
|
+
renderChips();
|
|
4195
|
+
uploadPastedBlob(blob, name, pendingId);
|
|
4196
|
+
});
|
|
4197
|
+
if (input) input.focus();
|
|
4198
|
+
}
|
|
4199
|
+
if (input) input.addEventListener('paste', handlePasteFiles);
|
|
4200
|
+
if (historyToggle) {
|
|
4201
|
+
historyToggle.addEventListener('click', function () { setLeftMode(leftMode() === 'history' ? '' : 'history'); });
|
|
4202
|
+
}
|
|
4203
|
+
// /chat composer's attach button ⇄ files-only drawer (open ↔ close toggle).
|
|
4204
|
+
var chatAttachBtn = document.getElementById('rk-chat-attach');
|
|
4205
|
+
if (chatAttachBtn) {
|
|
4206
|
+
chatAttachBtn.addEventListener('click', function () {
|
|
4207
|
+
if (!panel.hidden && panel.classList.contains('is-files-only')) closePanel();
|
|
4208
|
+
else openPanel(true);
|
|
4209
|
+
});
|
|
4210
|
+
}
|
|
4211
|
+
var filesCloseBtn = document.getElementById('rkFilesClose');
|
|
4212
|
+
if (filesCloseBtn) filesCloseBtn.addEventListener('click', function () {
|
|
4213
|
+
// In the /chat files-only drawer this is the only close affordance → shut the
|
|
4214
|
+
// panel. In the full pop-panel it just collapses the Files drawer back to chat.
|
|
4215
|
+
if (panel.classList.contains('is-files-only')) closePanel();
|
|
4216
|
+
else setLeftMode('');
|
|
4217
|
+
});
|
|
4218
|
+
// History / Cloud drawers get the same "back to chat" affordance as Files — on
|
|
4219
|
+
// narrow panels the drawer takes the whole panel (chat steps aside), so the
|
|
4220
|
+
// header toggles that would normally close it are hidden; this in-drawer button
|
|
4221
|
+
// is the reliable way back to the conversation.
|
|
4222
|
+
var historyCloseBtn = document.getElementById('rkHistoryClose');
|
|
4223
|
+
if (historyCloseBtn) historyCloseBtn.addEventListener('click', function () { setLeftMode(''); });
|
|
4224
|
+
var cloudCloseBtn = document.getElementById('rkCloudClose');
|
|
4225
|
+
if (cloudCloseBtn) cloudCloseBtn.addEventListener('click', function () { setLeftMode(''); });
|
|
4226
|
+
// Honor the saved drawer mode (default: files open).
|
|
4227
|
+
try {
|
|
4228
|
+
var savedMode = localStorage.getItem('rk-left-mode');
|
|
4229
|
+
setLeftMode(savedMode === null ? 'files' : savedMode);
|
|
4230
|
+
} catch (e) { setLeftMode('files'); }
|
|
4231
|
+
renderHistory();
|
|
4232
|
+
|
|
4233
|
+
try {
|
|
4234
|
+
var reopen = sessionStorage.getItem('rk-chat-open');
|
|
4235
|
+
if (isChatPage) { if (reopen === 'files') openPanel(true); }
|
|
4236
|
+
else if (reopen === '1') openPanel();
|
|
4237
|
+
} catch (e) {}
|
|
4238
|
+
|
|
4239
|
+
// A handed-off thread (?rk_chat=…) always opens the dock so the conversation
|
|
4240
|
+
// reappears immediately (openPanel → loadBoot → consumeHandoff replays it).
|
|
4241
|
+
if (handoffThread && !isChatPage) openPanel();
|
|
4242
|
+
})();
|
|
4243
|
+
|
|
4244
|
+
// ── subtle per-video loading indicators ──────────────────────────────────────
|
|
4245
|
+
// A small spinner appears over ANY <video> while it is actively fetching data
|
|
4246
|
+
// (initial buffer OR a mid-playback rebuffer) and fades out the moment the video
|
|
4247
|
+
// has enough data to paint/play. Self-contained so it works on every reskin page
|
|
4248
|
+
// regardless of the chat dock. It only shows while the browser is genuinely
|
|
4249
|
+
// LOADING (networkState === 2), so idle preload="none" videos never spin.
|
|
4250
|
+
(function () {
|
|
4251
|
+
function host(v) { return v.parentElement; }
|
|
4252
|
+
function spinner(v, make) {
|
|
4253
|
+
var p = host(v);
|
|
4254
|
+
if (!p) return null;
|
|
4255
|
+
var s = p.querySelector(':scope > .rk-vspin');
|
|
4256
|
+
if (!s && make) {
|
|
4257
|
+
s = document.createElement('span');
|
|
4258
|
+
s.className = 'rk-vspin';
|
|
4259
|
+
s.setAttribute('aria-hidden', 'true');
|
|
4260
|
+
p.appendChild(s);
|
|
4261
|
+
}
|
|
4262
|
+
return s || null;
|
|
4263
|
+
}
|
|
4264
|
+
// NETWORK_LOADING === 2 && readyState < HAVE_FUTURE_DATA (3)
|
|
4265
|
+
function loading(v) { return v.networkState === 2 && v.readyState < 3; }
|
|
4266
|
+
function show(v) { var s = spinner(v, true); if (s) s.classList.add('is-on'); }
|
|
4267
|
+
function hide(v) { var s = spinner(v, false); if (s) s.classList.remove('is-on'); }
|
|
4268
|
+
function sync(v) { if (loading(v)) show(v); else hide(v); }
|
|
4269
|
+
function wire(v) {
|
|
4270
|
+
if (v.__rkVload) return;
|
|
4271
|
+
v.__rkVload = true;
|
|
4272
|
+
// ready / has-data events → definitely stop spinning
|
|
4273
|
+
['loadeddata', 'canplay', 'canplaythrough', 'playing', 'error', 'abort', 'suspend', 'emptied']
|
|
4274
|
+
.forEach(function (ev) { v.addEventListener(ev, function () { hide(v); }); });
|
|
4275
|
+
// fetch / rebuffer events → spin only if actually loading
|
|
4276
|
+
['loadstart', 'progress', 'waiting', 'stalled', 'seeking']
|
|
4277
|
+
.forEach(function (ev) { v.addEventListener(ev, function () { sync(v); }); });
|
|
4278
|
+
sync(v);
|
|
4279
|
+
}
|
|
4280
|
+
function scan(root) { (root || document).querySelectorAll('video').forEach(wire); }
|
|
4281
|
+
if (document.readyState === 'loading') {
|
|
4282
|
+
document.addEventListener('DOMContentLoaded', function () { scan(); });
|
|
4283
|
+
} else { scan(); }
|
|
4284
|
+
// catch lazily-inserted (TikTok viewer) and lazily-hydrated (data-vsrc → src) videos
|
|
4285
|
+
if ('MutationObserver' in window) {
|
|
4286
|
+
new MutationObserver(function (muts) {
|
|
4287
|
+
for (var i = 0; i < muts.length; i++) {
|
|
4288
|
+
var m = muts[i];
|
|
4289
|
+
if (m.type === 'attributes') {
|
|
4290
|
+
if (m.target && m.target.tagName === 'VIDEO') { wire(m.target); sync(m.target); }
|
|
4291
|
+
continue;
|
|
4292
|
+
}
|
|
4293
|
+
if (!m.addedNodes) continue;
|
|
4294
|
+
for (var j = 0; j < m.addedNodes.length; j++) {
|
|
4295
|
+
var n = m.addedNodes[j];
|
|
4296
|
+
if (!n || n.nodeType !== 1) continue;
|
|
4297
|
+
if (n.tagName === 'VIDEO') wire(n);
|
|
4298
|
+
else if (n.querySelectorAll) scan(n);
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
}).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] });
|
|
4302
|
+
}
|
|
4303
|
+
})();</script>
|
|
4304
|
+
<script>(() => {
|
|
4305
|
+
const copiedTimers = new WeakMap();
|
|
4306
|
+
const copyText = async (value) => {
|
|
4307
|
+
if (!value) return false;
|
|
4308
|
+
try {
|
|
4309
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
4310
|
+
await navigator.clipboard.writeText(value);
|
|
4311
|
+
return true;
|
|
4312
|
+
}
|
|
4313
|
+
} catch {}
|
|
4314
|
+
try {
|
|
4315
|
+
const area = document.createElement("textarea");
|
|
4316
|
+
area.value = value;
|
|
4317
|
+
area.setAttribute("readonly", "readonly");
|
|
4318
|
+
area.style.position = "fixed";
|
|
4319
|
+
area.style.opacity = "0";
|
|
4320
|
+
document.body.appendChild(area);
|
|
4321
|
+
area.select();
|
|
4322
|
+
const ok = document.execCommand("copy");
|
|
4323
|
+
document.body.removeChild(area);
|
|
4324
|
+
return ok;
|
|
4325
|
+
} catch {
|
|
4326
|
+
return false;
|
|
4327
|
+
}
|
|
4328
|
+
};
|
|
4329
|
+
const flashButtonLabel = (button, labelNode, nextLabel, resetDelay = 1400) => {
|
|
4330
|
+
if (!button || !labelNode) return;
|
|
4331
|
+
const defaultLabel = button.getAttribute("data-default-label") || labelNode.textContent || "";
|
|
4332
|
+
labelNode.textContent = nextLabel;
|
|
4333
|
+
if (copiedTimers.has(button)) {
|
|
4334
|
+
clearTimeout(copiedTimers.get(button));
|
|
4335
|
+
}
|
|
4336
|
+
const timer = window.setTimeout(() => {
|
|
4337
|
+
labelNode.textContent = defaultLabel;
|
|
4338
|
+
copiedTimers.delete(button);
|
|
4339
|
+
}, resetDelay);
|
|
4340
|
+
copiedTimers.set(button, timer);
|
|
4341
|
+
};
|
|
4342
|
+
const closeOthers = (current) => {
|
|
4343
|
+
document.querySelectorAll("[data-skill-install]").forEach((node) => {
|
|
4344
|
+
if (node === current) return;
|
|
4345
|
+
const panel = node.querySelector("[data-skill-menu]");
|
|
4346
|
+
const toggle = node.querySelector("[data-skill-menu-toggle]");
|
|
4347
|
+
if (panel) panel.hidden = true;
|
|
4348
|
+
if (toggle) toggle.setAttribute("aria-expanded", "false");
|
|
4349
|
+
});
|
|
4350
|
+
};
|
|
4351
|
+
document.querySelectorAll("[data-skill-install]").forEach((control) => {
|
|
4352
|
+
const mainButton = control.querySelector("[data-skill-copy]");
|
|
4353
|
+
const mainLabel = control.querySelector(".vf-skill-install-main-label");
|
|
4354
|
+
const menuToggle = control.querySelector("[data-skill-menu-toggle]");
|
|
4355
|
+
const menu = control.querySelector("[data-skill-menu]");
|
|
4356
|
+
const commandInput = control.querySelector("[data-skill-command-input]");
|
|
4357
|
+
const commandCopy = control.querySelector("[data-skill-command-copy]");
|
|
4358
|
+
if (mainButton && mainLabel) {
|
|
4359
|
+
mainButton.addEventListener("click", async () => {
|
|
4360
|
+
const ok = await copyText(mainButton.getAttribute("data-skill-content") || "");
|
|
4361
|
+
flashButtonLabel(mainButton, mainLabel, ok ? "Copied" : "Copy failed");
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
if (menuToggle && menu) {
|
|
4365
|
+
menuToggle.addEventListener("click", (event) => {
|
|
4366
|
+
event.stopPropagation();
|
|
4367
|
+
const willOpen = menu.hidden;
|
|
4368
|
+
closeOthers(control);
|
|
4369
|
+
menu.hidden = !willOpen;
|
|
4370
|
+
menuToggle.setAttribute("aria-expanded", willOpen ? "true" : "false");
|
|
4371
|
+
});
|
|
4372
|
+
}
|
|
4373
|
+
if (commandCopy && commandInput) {
|
|
4374
|
+
commandCopy.addEventListener("click", async () => {
|
|
4375
|
+
const ok = await copyText(commandInput.value || "");
|
|
4376
|
+
commandCopy.setAttribute("data-copied", ok ? "true" : "false");
|
|
4377
|
+
commandCopy.setAttribute("aria-label", ok ? "Install command copied" : "Unable to copy install command");
|
|
4378
|
+
window.setTimeout(() => {
|
|
4379
|
+
commandCopy.removeAttribute("data-copied");
|
|
4380
|
+
commandCopy.setAttribute("aria-label", "Copy install command");
|
|
4381
|
+
}, 1400);
|
|
4382
|
+
});
|
|
4383
|
+
}
|
|
4384
|
+
});
|
|
4385
|
+
document.addEventListener("click", (event) => {
|
|
4386
|
+
const target = event.target;
|
|
4387
|
+
if (!(target instanceof Node)) return;
|
|
4388
|
+
document.querySelectorAll("[data-skill-install]").forEach((control) => {
|
|
4389
|
+
if (control.contains(target)) return;
|
|
4390
|
+
const panel = control.querySelector("[data-skill-menu]");
|
|
4391
|
+
const toggle = control.querySelector("[data-skill-menu-toggle]");
|
|
4392
|
+
if (panel) panel.hidden = true;
|
|
4393
|
+
if (toggle) toggle.setAttribute("aria-expanded", "false");
|
|
4394
|
+
});
|
|
4395
|
+
});
|
|
4396
|
+
document.addEventListener("keydown", (event) => {
|
|
4397
|
+
if (event.key !== "Escape") return;
|
|
4398
|
+
document.querySelectorAll("[data-skill-menu-toggle]").forEach((toggle) => toggle.setAttribute("aria-expanded", "false"));
|
|
4399
|
+
document.querySelectorAll("[data-skill-menu]").forEach((panel) => { panel.hidden = true; });
|
|
4400
|
+
});
|
|
4401
|
+
})();</script>
|
|
4402
|
+
<script type="module" src="/assets/file-directory-app.js"></script>
|
|
4403
|
+
</body>
|
|
4404
|
+
</html>
|