mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
feat: post-render and Studio feedback collection via PostHog surveys (#1101)
* feat(cli): prompt for render satisfaction after successful renders * feat: add text feedback, doctor context, and Studio render feedback UI * feat(studio): replace render feedback with session-based Studio experience bar Move the feedback prompt out of RenderQueueItem (where it triggered every 5th render) into a standalone StudioFeedbackBar mounted at the bottom of the preview area. The new bar is session-gated (shows after the 5th studio session), auto-dismisses after 20s, and respects a 30-day cooldown once dismissed or submitted. Renames telemetry to trackStudioFeedback with a "studio_experience" survey ID to reflect the broader scope. * feat(studio): attach browser doctor summary to feedback events * fix(studio): use recurring interval for feedback instead of one-time cooldown * fix(cli): skip feedback prompt when an agent runtime is detected * feat(cli): add hyperframes feedback command and agent render hint - New `hyperframes feedback --rating <1-5> --comment "..."` command for submitting anonymous render satisfaction feedback via telemetry. - When an AI agent runtime is detected after a render, print a dimmed hint to stdout so the agent can optionally call the command instead of silently skipping the readline prompt. - Export getDoctorSummary from telemetry/feedback.ts to share the system-info collector between the interactive prompt and the CLI command. - Register the command in cli.ts and help.ts under Settings. * fix(studio): align feedback interval to every 15 sessions * fix: show CLI feedback on first render, Studio every 10 sessions * feat: add env flags to disable feedback prompts * feat: env flags to configure feedback prompt frequency * fix: address review — agent hint reachability, cadence gate, session debounce, deprecated API
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import * as readline from "node:readline";
|
||||
import { readConfig, writeConfig } from "./config.js";
|
||||
import { shouldTrack } from "./client.js";
|
||||
import { trackRenderFeedback } from "./events.js";
|
||||
import { detectAgentRuntime } from "./agent_runtime.js";
|
||||
import { c } from "../ui/colors.js";
|
||||
|
||||
const DEFAULT_FEEDBACK_INTERVAL = 15;
|
||||
|
||||
function getFeedbackInterval(): number {
|
||||
const env = process.env.HYPERFRAMES_FEEDBACK_INTERVAL;
|
||||
if (env) {
|
||||
const n = parseInt(env, 10);
|
||||
if (Number.isFinite(n) && n > 0) return n;
|
||||
}
|
||||
return DEFAULT_FEEDBACK_INTERVAL;
|
||||
}
|
||||
|
||||
let promptedThisSession = false;
|
||||
|
||||
/**
|
||||
* Increment the successful render counter and maybe prompt for feedback.
|
||||
* Returns immediately if conditions aren't met.
|
||||
*/
|
||||
// fallow-ignore-next-line complexity
|
||||
export async function maybePromptRenderFeedback(opts: {
|
||||
renderDurationMs: number;
|
||||
quiet: boolean;
|
||||
}): Promise<void> {
|
||||
if (promptedThisSession) return;
|
||||
if (opts.quiet) return;
|
||||
if (!shouldTrack()) return;
|
||||
if (process.env.CI) return;
|
||||
|
||||
const config = readConfig();
|
||||
config.renderSuccessCount = (config.renderSuccessCount ?? 0) + 1;
|
||||
|
||||
const lastAt = config.lastFeedbackPromptAt ?? 0;
|
||||
const isFirstEverRender = lastAt === 0;
|
||||
const sinceLastPrompt = config.renderSuccessCount - lastAt;
|
||||
if (!isFirstEverRender && sinceLastPrompt < getFeedbackInterval()) {
|
||||
writeConfig(config);
|
||||
return;
|
||||
}
|
||||
|
||||
if (detectAgentRuntime()) {
|
||||
promptedThisSession = true;
|
||||
config.lastFeedbackPromptAt = config.renderSuccessCount;
|
||||
writeConfig(config);
|
||||
console.log(
|
||||
c.dim(" [hyperframes] ") +
|
||||
c.dim("Agent feedback: ") +
|
||||
c.accent('hyperframes feedback --rating <1-5> --comment "..."'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.stdin.isTTY) {
|
||||
writeConfig(config);
|
||||
return;
|
||||
}
|
||||
|
||||
// Time to ask
|
||||
promptedThisSession = true;
|
||||
config.lastFeedbackPromptAt = config.renderSuccessCount;
|
||||
writeConfig(config);
|
||||
|
||||
const answer = await askQuestion(
|
||||
` ${c.dim("How was this render?")} ${c.accent("[1=poor 5=great, enter to skip]")} `,
|
||||
);
|
||||
|
||||
const rating = parseInt(answer.trim(), 10);
|
||||
if (rating >= 1 && rating <= 5) {
|
||||
// Ask for optional text feedback
|
||||
const details = await askQuestion(` ${c.dim("Any details?")} ${c.accent("(enter to skip)")} `);
|
||||
const trimmedDetails = details.trim();
|
||||
|
||||
trackRenderFeedback({
|
||||
rating,
|
||||
renderDurationMs: opts.renderDurationMs,
|
||||
comment: trimmedDetails || undefined,
|
||||
doctorSummary: await getDoctorSummary(),
|
||||
});
|
||||
console.log(c.dim(" Thanks for the feedback!"));
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDoctorSummary(): Promise<string> {
|
||||
try {
|
||||
const [{ getSystemMeta }, { findFFmpeg }] = await Promise.all([
|
||||
import("../telemetry/system.js"),
|
||||
import("../browser/ffmpeg.js"),
|
||||
]);
|
||||
const sys = getSystemMeta();
|
||||
const parts = [
|
||||
`os=${process.platform}/${process.arch}`,
|
||||
`node=${process.version}`,
|
||||
`cpu=${sys.cpu_count}cores`,
|
||||
`mem=${(sys.memory_total_mb / 1024).toFixed(0)}GB`,
|
||||
`ffmpeg=${findFFmpeg() ? "yes" : "no"}`,
|
||||
];
|
||||
if (sys.is_docker) parts.push("docker");
|
||||
if (sys.is_wsl) parts.push("wsl");
|
||||
return parts.join(" ");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function askQuestion(prompt: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
rl.question(prompt, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer);
|
||||
});
|
||||
// Auto-resolve after 10 seconds so the CLI never hangs
|
||||
const timeout = setTimeout(() => {
|
||||
rl.close();
|
||||
resolve("");
|
||||
}, 10_000);
|
||||
// Don't keep the process alive just for the timeout
|
||||
if (typeof timeout === "object" && timeout !== null && "unref" in timeout) {
|
||||
(timeout as { unref: () => void }).unref();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user