Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m22s Details

This commit is contained in:
Jeff Emmett 2026-04-05 16:04:08 -04:00
commit 905481eb84
5 changed files with 45 additions and 23 deletions

View File

@ -839,13 +839,15 @@ export class FolkChoiceRank extends FolkShape {
const onMove = (me: PointerEvent) => {
me.stopPropagation();
const target = this.#rankPanel!.querySelector(
`.rank-item:not(.dragging):hover`
) as HTMLElement;
// Clear previous drag-over states
this.#rankPanel!.querySelectorAll(".drag-over").forEach((d) => d.classList.remove("drag-over"));
if (target) target.classList.add("drag-over");
const items = this.#rankPanel!.querySelectorAll(".rank-item:not(.dragging)");
for (const item of items) {
const rect = (item as HTMLElement).getBoundingClientRect();
if (me.clientY >= rect.top && me.clientY <= rect.bottom) {
item.classList.add("drag-over");
break;
}
}
};
const onUp = (ue: PointerEvent) => {

View File

@ -525,6 +525,7 @@ export class FolkChoiceSpider extends FolkShape {
#selectedOptionId = "";
#drawerOpen = false;
#settingsOpen = false;
#isSliding = false;
// DOM refs
#wrapperEl: HTMLElement | null = null;
@ -748,7 +749,7 @@ export class FolkChoiceSpider extends FolkShape {
#render() {
this.#renderOptionTabs();
this.#renderChart();
this.#renderSliders();
if (!this.#isSliding) this.#renderSliders();
this.#renderLegend();
this.#renderSummary();
if (this.#drawerOpen) this.#renderDrawer();
@ -889,7 +890,9 @@ export class FolkChoiceSpider extends FolkShape {
this.#slidersEl.querySelectorAll(".slider-input").forEach((slider) => {
const input = slider as HTMLInputElement;
input.addEventListener("click", (e) => e.stopPropagation());
input.addEventListener("pointerdown", (e) => e.stopPropagation());
input.addEventListener("pointerdown", (e) => { e.stopPropagation(); this.#isSliding = true; });
input.addEventListener("pointerup", () => { this.#isSliding = false; });
input.addEventListener("lostpointercapture", () => { this.#isSliding = false; });
input.addEventListener("input", (e) => {
e.stopPropagation();
const critId = input.dataset.crit!;

View File

@ -459,7 +459,7 @@ export class FolkVideoGen extends FolkShape {
this.#error = null;
this.#progress = 0;
if (this.#generateBtn) this.#generateBtn.disabled = true;
this.#renderLoading();
this.#renderLoading("Submitting...");
try {
const endpoint = this.#mode === "i2v" ? "/api/video-gen/i2v" : "/api/video-gen/t2v";
@ -481,23 +481,35 @@ export class FolkVideoGen extends FolkShape {
const submitData = await submitRes.json();
// Poll for job completion (up to 5 minutes)
// Poll for job completion (up to 10 minutes)
const jobId = submitData.job_id;
if (!jobId) throw new Error("No job ID returned");
const deadline = Date.now() + 300_000;
let elapsed = 0;
const deadline = Date.now() + 600_000;
let statusMsg = "Submitting to queue...";
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 3000));
elapsed += 3;
this.#progress = Math.min(90, (elapsed / 120) * 100);
this.#renderLoading();
const pollRes = await fetch(`/api/video-gen/${jobId}`);
if (!pollRes.ok) continue;
const pollData = await pollRes.json();
// Update progress from real fal.ai status
const falStatus = pollData.fal_status;
const queuePos = pollData.queue_position;
if (falStatus === "IN_QUEUE") {
this.#progress = 10;
statusMsg = queuePos != null ? `In queue (position ${queuePos})...` : "Waiting in queue...";
} else if (falStatus === "IN_PROGRESS") {
this.#progress = Math.min(85, this.#progress < 40 ? 40 : this.#progress + 2);
statusMsg = "Generating video...";
} else if (!falStatus && pollData.status === "processing") {
this.#progress = Math.min(20, this.#progress + 2);
statusMsg = "Starting generation...";
}
this.#renderLoading(statusMsg);
if (pollData.status === "complete") {
const video: GeneratedVideo = {
id: crypto.randomUUID(),
@ -530,16 +542,16 @@ export class FolkVideoGen extends FolkShape {
}
}
#renderLoading() {
#renderLoading(message = "Generating video...") {
if (!this.#videoArea) return;
this.#videoArea.innerHTML = `
<div class="loading">
<div class="spinner"></div>
<span>Generating video...</span>
<span>${this.#escapeHtml(message)}</span>
<div class="progress-bar">
<div class="progress-fill" style="width: ${this.#progress}%"></div>
</div>
<span style="font-size: 11px; color: #64748b;">This may take 30-60 seconds</span>
<span style="font-size: 11px; color: #64748b;">This may take a few minutes</span>
</div>
`;
}

View File

@ -1060,6 +1060,8 @@ interface VideoGenJob {
error?: string;
createdAt: number;
completedAt?: number;
queuePosition?: number;
falStatus?: string;
}
const videoGenJobs = new Map<string, VideoGenJob>();
@ -1103,8 +1105,8 @@ async function processVideoGenJob(job: VideoGenJob) {
const { request_id } = await submitRes.json() as { request_id: string };
// Poll for completion (up to 5 min)
const deadline = Date.now() + 300_000;
// Poll for completion (up to 10 min)
const deadline = Date.now() + 600_000;
let responseUrl = "";
let completed = false;
@ -1115,8 +1117,10 @@ async function processVideoGenJob(job: VideoGenJob) {
{ headers: falHeaders },
);
if (!statusRes.ok) continue;
const statusData = await statusRes.json() as { status: string; response_url?: string };
const statusData = await statusRes.json() as { status: string; response_url?: string; queue_position?: number };
console.log(`[video-gen] Poll ${job.id}: status=${statusData.status}`);
job.falStatus = statusData.status;
job.queuePosition = statusData.queue_position;
if (statusData.response_url) responseUrl = statusData.response_url;
if (statusData.status === "COMPLETED") { completed = true; break; }
if (statusData.status === "FAILED") {
@ -1667,6 +1671,7 @@ app.get("/api/video-gen/:jobId", async (c) => {
const response: Record<string, any> = {
job_id: job.id, status: job.status, created_at: job.createdAt,
fal_status: job.falStatus, queue_position: job.queuePosition,
};
if (job.status === "complete") {
response.url = job.resultUrl;

View File

@ -1143,7 +1143,7 @@ export async function revokeSpaceInvite(id: string, spaceSlug: string): Promise<
export interface StoredNotification {
id: string;
userDid: string;
category: 'space' | 'module' | 'system' | 'social';
category: 'space' | 'module' | 'system' | 'social' | 'payment';
eventType: string;
title: string;
body: string | null;
@ -1196,7 +1196,7 @@ async function rowToNotification(row: any): Promise<StoredNotification> {
export async function createNotification(notif: {
id: string;
userDid: string;
category: 'space' | 'module' | 'system' | 'social';
category: 'space' | 'module' | 'system' | 'social' | 'payment';
eventType: string;
title: string;
body?: string;