app
Adom Video Post-Production
Public Made by Adomby adom
AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones, speed up dead air, narrate, mux and auto-level, validate the final cut, and publish straight to the wiki, driven end to end by your AI.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
//! Frontend /eval hot-patch channel — the AI's escape hatch for
//! driving any webapp-backed subcommand's UI without a rebuild.
//!
//! The pattern is specified in `gallia/skills/app-creator/SKILL.md` §7b.
//! AI (or any CLI caller) queues a JS snippet via `POST /eval`, the UI
//! long-polls `GET /eval/pending`, runs the snippet via
//! `new AsyncFunction('ctx', code)(window.__appCtx || {})`, and POSTs
//! the return value (or error) to `POST /eval/:id/result`. The queuer
//! blocks on a Condvar waiting for that result slot to fill.
//!
//! Because this is a full remote-code-execution primitive against
//! the app's own frontend it MUST NOT be exposed in any context
//! other than a dev-mode local webview. Our binding is `127.0.0.1`
//! only so that property holds.
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use tiny_http::{Header, Request, Response};
use uuid::Uuid;
/// A pending snippet waiting for the UI to pick it up.
pub struct EvalJob {
pub id: String,
pub code: String,
}
/// Shared state: a FIFO of pending snippets + a map of returned
/// results keyed by id. A single Condvar wakes both long-poll
/// `GET /eval/pending` waiters (when new jobs arrive) AND
/// `block_on_result` waiters (when the UI posts a result). We
/// demultiplex by checking what we were waiting for on wakeup.
pub struct EvalQueue {
pub state: Mutex<EvalState>,
pub cond: Condvar,
}
#[derive(Default)]
pub struct EvalState {
pub pending: VecDeque<EvalJob>,
pub results: HashMap<String, serde_json::Value>,
}
pub type SharedEval = Arc<EvalQueue>;
pub fn new() -> SharedEval {
Arc::new(EvalQueue {
state: Mutex::new(EvalState::default()),
cond: Condvar::new(),
})
}
/// `POST /eval` — queue a new snippet, return its id. Body shape:
/// `{"code":"return 1+1"}`. Response: `{"id":"<uuid>"}`.
pub fn handle_post(mut request: Request, eval: &SharedEval) -> Result<(), String> {
let mut body = String::new();
request
.as_reader()
.read_to_string(&mut body)
.map_err(|e| e.to_string())?;
let parsed: serde_json::Value = match serde_json::from_str(&body) {
Ok(v) => v,
Err(e) => {
let _ = request.respond(json_response(
400,
&format!(r#"{{"ok":false,"error":"invalid json: {}"}}"#, e),
));
return Ok(());
}
};
let code = match parsed.get("code").and_then(|v| v.as_str()) {
Some(c) => c.to_string(),
None => {
let _ = request.respond(json_response(
400,
r#"{"ok":false,"error":"missing 'code' field"}"#,
));
return Ok(());
}
};
let id = Uuid::new_v4().to_string();
{
let mut guard = eval.state.lock().unwrap();
guard.pending.push_back(EvalJob {
id: id.clone(),
code,
});
}
eval.cond.notify_all();
let body = format!(r#"{{"ok":true,"id":"{}"}}"#, id);
request
.respond(json_response(200, &body))
.map_err(|e| e.to_string())
}
/// `GET /eval/pending` — long-poll for the next pending snippet.
/// Blocks up to 25 seconds waiting for the queue to have something,
/// returns the oldest pending job as `{"id":"...","code":"..."}`,
/// or 204 No Content on timeout. The UI is expected to loop.
pub fn handle_get_pending(request: Request, eval: &SharedEval) -> Result<(), String> {
let deadline = Instant::now() + Duration::from_secs(25);
let mut guard = eval.state.lock().unwrap();
loop {
if let Some(job) = guard.pending.pop_front() {
let body = format!(
r#"{{"id":"{}","code":{}}}"#,
job.id,
serde_json::to_string(&job.code).unwrap()
);
return request
.respond(json_response(200, &body))
.map_err(|e| e.to_string());
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return request
.respond(empty_response(204))
.map_err(|e| e.to_string());
}
let (g, wait_result) = eval.cond.wait_timeout(guard, remaining).unwrap();
guard = g;
if wait_result.timed_out() && guard.pending.is_empty() {
return request
.respond(empty_response(204))
.map_err(|e| e.to_string());
}
}
}
/// `GET /eval/:id/result` — the CLI caller (usually
/// `adom-video-post eval`) polls this to retrieve the snippet's return
/// value. Long-polls up to 25 seconds on a Condvar. Returns 200 +
/// the stored result object on success, 204 No Content on timeout.
pub fn handle_get_result(request: Request, eval: &SharedEval, id: &str) -> Result<(), String> {
let deadline = Instant::now() + Duration::from_secs(25);
let mut guard = eval.state.lock().unwrap();
loop {
if let Some(result) = guard.results.remove(id) {
let body = result.to_string();
return request
.respond(json_response(200, &body))
.map_err(|e| e.to_string());
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return request
.respond(empty_response(204))
.map_err(|e| e.to_string());
}
let (g, wait_result) = eval.cond.wait_timeout(guard, remaining).unwrap();
guard = g;
if wait_result.timed_out() && !guard.results.contains_key(id) {
return request
.respond(empty_response(204))
.map_err(|e| e.to_string());
}
}
}
/// `POST /eval/:id/result` — the UI posts the snippet's return value
/// (or error) back. Body shape: `{"ok":true,"value":...}` or
/// `{"ok":false,"error":"...","stack":"..."}`. We store the whole
/// object in the result map and wake any callers blocked on this id.
pub fn handle_post_result(
mut request: Request,
eval: &SharedEval,
id: &str,
) -> Result<(), String> {
let mut body = String::new();
request
.as_reader()
.read_to_string(&mut body)
.map_err(|e| e.to_string())?;
let parsed: serde_json::Value = match serde_json::from_str(&body) {
Ok(v) => v,
Err(e) => {
let _ = request.respond(json_response(
400,
&format!(r#"{{"ok":false,"error":"invalid json: {}"}}"#, e),
));
return Ok(());
}
};
{
let mut guard = eval.state.lock().unwrap();
guard.results.insert(id.to_string(), parsed);
}
eval.cond.notify_all();
request
.respond(json_response(200, r#"{"ok":true}"#))
.map_err(|e| e.to_string())
}
fn json_response(status: u16, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string(body)
.with_status_code(status)
.with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
}
fn empty_response(status: u16) -> Response<std::io::Empty> {
Response::empty(status)
}