Compare commits
116
Commits
No files matched your search
@@ -270,25 +270,27 @@ export function clipOptions(model, last = {}) {
|
||||
|
||||
/**
|
||||
* A conversation with pictures and voice notes in it, as plain messages for a text model. A photo or clip the
|
||||
* character sent is mentioned in the next thing the person says ("(You sent me a photo: …)"), so the model knows
|
||||
* about it without learning to write "[photo]" itself. Your photo is "[a photo: what it shows]", your voice note is
|
||||
* the words you said.
|
||||
* character sent is in their own turn, as a stage direction ("*sends a photo: …*"), the way characters narrate what
|
||||
* they do, so the model knows what it sent and who sent it (told in the person's turn, "you sent me a photo" read as
|
||||
* the person's, and characters got confused about whose photo it was). Your photo is "[a photo: what it shows]", your
|
||||
* voice note is the words you said.
|
||||
*/
|
||||
export function apiHistory(history) {
|
||||
const out = [];
|
||||
let notes = [];
|
||||
for (const m of history || []) {
|
||||
if (!m || m.demo) continue;
|
||||
const media = m.media || null;
|
||||
if (m.role === "assistant") {
|
||||
if (media && (media.kind === "photo" || media.kind === "clip")) {
|
||||
const shot = clip(str(media.shot).trim(), 300).replace(/[.!?…\s]+$/, "");
|
||||
notes.push(`(You sent me ${media.kind === "clip" ? "a short video" : "a photo"}${shot ? ": " + shot : ""}.)`);
|
||||
const sent = `*sends ${media.kind === "clip" ? "a short video" : "a photo"}${shot ? ": " + shot : ""}*`;
|
||||
const last = out.at(-1);
|
||||
if (last && last.role === "assistant") last.content += "\n\n" + sent; // with the words it came after
|
||||
else out.push({ role: "assistant", content: sent });
|
||||
} else if (str(m.content).trim()) out.push({ role: "assistant", content: str(m.content) });
|
||||
} else if (m.role === "user") {
|
||||
let content = str(m.content);
|
||||
if (media && media.kind === "photo-in") content = `[a photo${media.saw ? ": " + media.saw : ""}]` + (content ? " " + content : "");
|
||||
if (notes.length) { content = notes.join(" ") + (content ? "\n\n" + content : ""); notes = []; }
|
||||
out.push({ role: "user", content, ...(m.hidden ? { hidden: true } : {}) });
|
||||
}
|
||||
}
|
||||
@@ -334,10 +336,14 @@ export const MOVE_SCHEMA = {
|
||||
};
|
||||
|
||||
/** the messages for a text model to decide the character's next move (adult: their pictures can be adult, adultCheck) */
|
||||
export function moveMessages(card, history, { adult = false } = {}) {
|
||||
/** the move prompt as it stands, or, for someone set to send things every round, told to lean in rather than hold back */
|
||||
export const EAGER = " This character is set to send things every round: choose \"photo\" (or \"voice\" or \"clip\" when that fits better) " +
|
||||
"whenever a picture of them could go with what was just said, however small the reason; \"nothing\" only when a photo would make no sense at all.";
|
||||
export function moveMessages(card, history, { adult = false, eager = false } = {}) {
|
||||
const lines = transcript(card, history, 12);
|
||||
const look = card.look && card.look.description;
|
||||
return [{ role: "system", content: adult ? MOVE_SYSTEM_ADULT : MOVE_SYSTEM }, { role: "user", content: [
|
||||
const system = (adult ? MOVE_SYSTEM_ADULT : MOVE_SYSTEM) + (eager ? EAGER : "");
|
||||
return [{ role: "system", content: system }, { role: "user", content: [
|
||||
`The character: ${card.name}${card.tagline ? ". " + card.tagline : ""}`,
|
||||
look ? `How they look: ${look}` : "",
|
||||
"The conversation, latest last (their reply just now is the last line):\n" + lines.join("\n"),
|
||||
@@ -353,7 +359,7 @@ export function normalizeMove(o) {
|
||||
|
||||
// words that make a move likely: asking to see or hear them, or a reply about where they are and what they're doing
|
||||
const ASKS = /\b(pics?|picture|photo|selfie|snap|show me|see you|see it|what (?:do )?you look like|look like|wearing|outfit|send (?:me )?(?:a|one|it)|video|clip|film|hear you|your voice|say it|sing|whisper|out loud|aloud)\b/i;
|
||||
const SHOWS = /\b(look at (?:this|my|the)|wearing|outfit|dress|view from|sunset|sunrise|my (?:room|place|desk|garden|kitchen|car|boat|bed|window)|just got (?:back|a|my)|i'?m (?:at|in|on) the|picture|photo|selfie|whisper)\b/i;
|
||||
const SHOWS = /\b(sends? (?:you )?(?:a |another )?(?:photo|pic|selfie|snap)|look at (?:this|my|the)|wearing|outfit|dress|view from|sunset|sunrise|my (?:room|place|desk|garden|kitchen|car|boat|bed|window)|just got (?:back|a|my)|i'?m (?:at|in|on) the|picture|photo|selfie|whisper)\b/i;
|
||||
|
||||
/**
|
||||
* Whether to think about a move after this reply: when the person asked to see or hear them, when the reply talks
|
||||
|
||||
+230
-29
@@ -6,6 +6,7 @@ import * as M from "./media.js";
|
||||
import * as T from "./threads.js";
|
||||
import * as R from "./memory.js";
|
||||
import * as W from "./workflows.js";
|
||||
import * as B from "./backup.js";
|
||||
import { Venice, VeniceError } from "./venice.js";
|
||||
import CAST from "./cast/index.js";
|
||||
|
||||
@@ -83,11 +84,14 @@ const S = {
|
||||
// or clip being asked for, and a voice note being recorded
|
||||
lists: { inpaint: null, image: null, tts: null, asr: null, video: null }, listErr: {},
|
||||
ab: { photo: "", clip: {}, hear: false, hearModel: "", see: false, seeModel: "", own: true, allowance: 0.25, memory: true,
|
||||
adult: true, adultPhoto: "", adultClip: "" }, voiceOn: {}, jobs: [], ask: null,
|
||||
adult: true, adultPhoto: "", adultClip: "", awayMin: 360 }, voiceOn: {}, jobs: [], ask: null,
|
||||
rec: null, moving: new Set(), inflight: 0,
|
||||
};
|
||||
// what characters do on their own (photos, voice notes) comes out of a daily allowance, counted here by local day
|
||||
const ALLOWANCES = [0.1, 0.25, 1, 5];
|
||||
/** how long you've been away before a character texts first: minutes, as the setting shows them */
|
||||
const AWAYS = [[1, "a minute"], [5, "5 minutes"], [15, "15 minutes"], [30, "half an hour"], [60, "an hour"], [180, "3 hours"], [360, "6 hours"], [1440, "a day"]];
|
||||
const backAfter = () => (S.ab.awayMin || 360) * 60e3;
|
||||
const localDay = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; };
|
||||
const spentToday = () => A.spentToday(local.get(K.spent, null), localDay());
|
||||
const canAfford = (usd) => A.canSpend(local.get(K.spent, null), localDay(), S.ab.allowance, usd);
|
||||
@@ -138,6 +142,7 @@ function boot() {
|
||||
S.ab = { ...S.ab, ...(local.get(K.ab, {}) || {}) };
|
||||
S.ab.clip = S.ab.clip && typeof S.ab.clip === "object" ? S.ab.clip : {};
|
||||
if (!ALLOWANCES.includes(S.ab.allowance)) S.ab.allowance = 0.25;
|
||||
if (!AWAYS.some(([m]) => m === S.ab.awayMin)) S.ab.awayMin = 360;
|
||||
S.voiceOn = local.get(K.voiceOn, {}) || {};
|
||||
S.jobs = (local.get(K.jobs, []) || []).filter((j) => j && j.queueId && j.model && Date.now() - (j.started || 0) < 23 * 3600e3);
|
||||
pruneEmpty();
|
||||
@@ -394,7 +399,7 @@ function setPane(which) {
|
||||
const st = $("#studio");
|
||||
st.classList.toggle("pane-chat", S.pane === "chat");
|
||||
st.classList.toggle("pane-list", S.pane === "list");
|
||||
if (S.pane === "chat") scrollLog(); // drawn while hidden, it couldn't scroll: now it can
|
||||
if (S.pane === "chat") scrollLog(true); // drawn while hidden, it couldn't scroll: now it can
|
||||
}
|
||||
function renderLeft() {
|
||||
for (const [w, tab, panel] of [["chats", "#lt-chats", "#lp-chats"], ["chars", "#lt-chars", "#lp-chars"], ["harbor", "#lt-harbor", "#lp-harbor"]]) {
|
||||
@@ -652,15 +657,22 @@ function syncAdult(list) {
|
||||
function renderRoster() {
|
||||
const tb = $("#roster");
|
||||
tb.textContent = "";
|
||||
$("#harbor-empty").hidden = S.cards.length > 0;
|
||||
$("#harbor .tablewrap").hidden = !S.cards.length;
|
||||
$("#harbor-empty").hidden = true;
|
||||
$("#harbor .tablewrap").hidden = false;
|
||||
const cur = card();
|
||||
// the first row is always someone new: the way back to a blank card from anyone's
|
||||
const fresh = () => { S.fresh = true; syncNew(); $("#card").scrollIntoView({ behavior: motion(), block: "start" }); $("#nc-name").focus({ preventScroll: true }); };
|
||||
tb.append(h("tr", { class: "opens newrow", onclick: (e) => { if (!e.target.closest("button, a")) fresh(); } },
|
||||
h("th", { scope: "row" }, linkish("+ new character", fresh, { class: "linkish rowname" })),
|
||||
h("td", { text: "write one here" }), h("td", { text: "" }),
|
||||
h("td", { class: "files" }, linkish("import a card", () => $("#import").click()))));
|
||||
queueMicrotask(syncNew);
|
||||
for (const c of S.cards) {
|
||||
const on = cur && c.id === cur.id;
|
||||
const from = c.engine.venice_character ? "venice, as a link with credit" : c.lineage[0] ? C.creditLine(c.lineage[0]) : "written here";
|
||||
const go = (tab) => () => { selectCard(c.id); openCard(tab); };
|
||||
// the whole row opens their card; the links in it do what they say
|
||||
tb.append(h("tr", { "aria-current": on ? "true" : null, class: "opens", onclick: (e) => { if (!e.target.closest("button, a")) go("edit")(); } },
|
||||
tb.append(h("tr", { "data-id": c.id, class: "opens", onclick: (e) => { if (!e.target.closest("button, a")) go("edit")(); } },
|
||||
h("th", { scope: "row" }, linkish(c.name, go("edit"), { class: "linkish rowname" }), c.adult ? h("span", { class: "x18", title: "for grown-ups only", text: "18+" }) : null,
|
||||
on ? h("span", { class: "now", text: "talking" }) : null),
|
||||
h("td", { text: from }),
|
||||
@@ -694,7 +706,7 @@ function selectCard(id, { talk = false, scroll = false, open } = {}) {
|
||||
if (was !== id) renderCard();
|
||||
if (!talk) return;
|
||||
setPane("chat");
|
||||
if (scroll || narrow()) (narrow() ? $("#chat") : $("#studio")).scrollIntoView({ behavior: motion(), block: "start" });
|
||||
if (scroll || narrow()) $("#studio").scrollIntoView({ behavior: motion(), block: "start" }); // on a phone: the tabs, then the chat under them
|
||||
if (!narrow()) $("#say").focus({ preventScroll: true });
|
||||
speakFirst();
|
||||
}
|
||||
@@ -803,7 +815,7 @@ function comeBack(t) {
|
||||
if (T.byLatest(threadsOf(c.id))[0] !== t) return Promise.resolve(); // only in their latest chat with you
|
||||
const go = () => {
|
||||
const tried = local.get(K.tried, {}) || {};
|
||||
if (!S.threads[t.id] || S.busy.has(t.id) || !T.readyToTextFirst(t.msgs, { now: Date.now(), after: A.BACK_AFTER, tried: tried[t.id] })) return;
|
||||
if (!S.threads[t.id] || S.busy.has(t.id) || !T.readyToTextFirst(t.msgs, { now: Date.now(), after: backAfter(), tried: tried[t.id] })) return;
|
||||
const kept = Object.fromEntries(Object.entries(tried).filter(([id]) => S.threads[id]));
|
||||
if (!local.set(K.tried, { ...kept, [t.id]: Date.now() })) return; // can't write it down: don't risk asking twice
|
||||
return send(A.comeBack(A.since(t.msgs, Date.now()), here(t)), { hidden: true, to: t, own: true });
|
||||
@@ -818,9 +830,9 @@ function comeBack(t) {
|
||||
async function comingBack() {
|
||||
if (!S.key || !S.ab.own || !seen()) return;
|
||||
comeBack(thread());
|
||||
if (Date.now() - (Number(local.get(K.came, 0)) || 0) < A.BACK_AFTER) return;
|
||||
if (Date.now() - (Number(local.get(K.came, 0)) || 0) < backAfter()) return;
|
||||
const others = T.textFirst(Object.values(S.threads).filter((t) => t.card !== S.sel && cardOf(t)), (t) => t.msgs,
|
||||
{ now: Date.now(), after: A.BACK_AFTER, tried: local.get(K.tried, {}) || {}, max: 3 });
|
||||
{ now: Date.now(), after: backAfter(), tried: local.get(K.tried, {}) || {}, max: 3 });
|
||||
if (!others.length || !local.set(K.came, Date.now())) return;
|
||||
for (const t of others) {
|
||||
await sleep(1500 + Math.random() * 2500);
|
||||
@@ -902,9 +914,18 @@ function put(el) {
|
||||
log.append(el);
|
||||
return el;
|
||||
}
|
||||
/** to the latest message: now, after the next paint, and once more when late layout (a pane shown, a font) has settled */
|
||||
function scrollLog() {
|
||||
const l = $("#log"), go = () => { l.scrollTop = l.scrollHeight; };
|
||||
/**
|
||||
* To the latest message: now, after the next paint, and once more when late layout (a pane shown, a font) has settled.
|
||||
* Only if you were at the end already: scrolled up to read, the chat stays where you are while they talk. force: it
|
||||
* was you (you sent something, opened the chat), so to the end whatever.
|
||||
*/
|
||||
let pinned = true; // at the end of the chat, as of the last scroll
|
||||
const nearEnd = (l) => l.scrollHeight - l.scrollTop - l.clientHeight < 160;
|
||||
function scrollLog(force = false) {
|
||||
const l = $("#log");
|
||||
if (!force && !pinned) return;
|
||||
const go = () => { l.scrollTop = l.scrollHeight; };
|
||||
pinned = true;
|
||||
go(); requestAnimationFrame(go); setTimeout(go, 250);
|
||||
}
|
||||
/** a photo or clip that loads after the chat is drawn pushes the bottom down: follow it, if you were at the bottom */
|
||||
@@ -1095,7 +1116,7 @@ function renderLog() {
|
||||
if (t && S.doing.has(t.id)) put(typingLine(t, S.doing.get(t.id)));
|
||||
renderJobs();
|
||||
followMedia(log);
|
||||
scrollLog();
|
||||
scrollLog(true);
|
||||
}
|
||||
|
||||
/** a character texts in short bubbles; a plain chat answers in one piece */
|
||||
@@ -1103,7 +1124,7 @@ const bubblesOf = (c, text) => (c.plain ? [C.stripThinking(text)].filter(Boolean
|
||||
/** what the model reads: the conversation, with photos and voice notes as words (see abilities.js), and what they remember */
|
||||
function messagesFor(c, hist) {
|
||||
const api = A.apiHistory(hist);
|
||||
return c.plain ? C.alternate(api.slice(-40).map(({ role, content }) => ({ role, content }))) : C.chatMessages(c, api, { memory: memoryFor(c) });
|
||||
return c.plain ? C.alternate(api.slice(-40).map(({ role, content }) => ({ role, content }))) : C.chatMessages(c, api, { memory: memoryFor(c), can: canOf(c) });
|
||||
}
|
||||
|
||||
// ---------- what they remember ----------
|
||||
@@ -1116,6 +1137,8 @@ function saveMemory(id, m) {
|
||||
}
|
||||
/** what goes into their prompt: nothing with memory switched off; for a Venice character, beside Venice's own prompt */
|
||||
const memoryFor = (c) => (S.ab.memory && c && !c.plain ? R.lines(R.select(memoryOf(c.id).notes)) : []);
|
||||
/** what they can send in this chat, for their prompt (card.js canBlock): photos and clips need a key, "on their own", and a picture of them */
|
||||
const canOf = (c) => (c && !c.plain ? { photos: !!S.key && S.ab.own !== false, pictured: refsOf(c).length > 0 } : null);
|
||||
const remembering = new Set(), memoryMissed = new Map();
|
||||
/**
|
||||
* After every dozen messages in a chat (or a few, when you leave it), its character writes down what they remember now:
|
||||
@@ -1206,7 +1229,7 @@ async function send(text, { hidden = false, media = null, picture = "", to = nul
|
||||
saveThread(t);
|
||||
if (here(t)) {
|
||||
for (const el of $$("#log .suggest, #log .note")) el.remove();
|
||||
if (!hidden) put(media ? mediaLine(c, said) : line(text, "me"));
|
||||
if (!hidden) { put(media ? mediaLine(c, said) : line(text, "me")); scrollLog(true); }
|
||||
}
|
||||
const ctrl = new AbortController();
|
||||
S.busy.set(t.id, ctrl);
|
||||
@@ -1275,7 +1298,7 @@ async function send(text, { hidden = false, media = null, picture = "", to = nul
|
||||
}
|
||||
arrived(t, msg);
|
||||
await play(t, bubblesOf(c, reply), ctrl.signal, c, msg, drawn);
|
||||
if (!ctrl.signal.aborted && !demo && !c.plain && !hidden && !msg.voice) onTheirOwn(t);
|
||||
if (!ctrl.signal.aborted && !demo && !c.plain && !hidden) onTheirOwn(t, { voiced: !!msg.voice });
|
||||
} catch (e) {
|
||||
if (e && e.name === "AbortError") return;
|
||||
done(t);
|
||||
@@ -1484,6 +1507,11 @@ function renderCard() {
|
||||
function syncNew() {
|
||||
const c = card(), real = !!c && !c.plain, fresh = S.left === "harbor" && (S.fresh || !real);
|
||||
$("#card-new").hidden = !fresh;
|
||||
// one row marked: the one whose card is open below
|
||||
for (const tr of document.querySelectorAll("#roster tr")) {
|
||||
const on = tr.classList.contains("newrow") ? fresh : !fresh && real && tr.dataset.id === c.id;
|
||||
if (on) tr.setAttribute("aria-current", "true"); else tr.removeAttribute("aria-current");
|
||||
}
|
||||
$("#card-body").hidden = fresh || !real;
|
||||
$("#card-empty").hidden = fresh || real;
|
||||
$("#card-h").textContent = fresh ? "NEW CHARACTER" : real ? "CARD: " + c.name.toUpperCase() : "CARD";
|
||||
@@ -1773,7 +1801,7 @@ function renderAbilities() {
|
||||
allowance.addEventListener("change", () => { S.ab.allowance = Number(allowance.value); saveAb(); renderAbilities(); renderTools(); });
|
||||
tb.append(
|
||||
row("ON THEIR OWN", tick("characters text first when you come back, and send photos and voice notes when they like", S.ab.own, (on) => setOwn(on)),
|
||||
["up to ", allowance, " a day · how often: ", tempoSelect(), ` · ${A.usd(spentToday())} spent today · clips are offered with venice's price, and made only if you say yes`]),
|
||||
["up to ", allowance, " a day · how often: ", tempoSelect(), " · text first after you've been away: ", awaySelect(), ` · ${A.usd(spentToday())} spent today · clips are offered with venice's price, and made only if you say yes`]),
|
||||
row("MEMORY", tick("characters remember your chats with them", S.ab.memory, (on) => {
|
||||
S.ab.memory = on;
|
||||
saveAb();
|
||||
@@ -1825,7 +1853,7 @@ function askFor(kind) {
|
||||
if (refsOf(c).length) writeShot();
|
||||
if (narrow()) $("#ask").scrollIntoView({ behavior: motion(), block: "nearest" });
|
||||
else $("#studio").scrollIntoView({ behavior: motion(), block: "start" });
|
||||
scrollLog();
|
||||
scrollLog(true);
|
||||
}
|
||||
function closeAsk() { if (S.ask && S.ask.busy) return; S.ask = null; renderAsk(); }
|
||||
function renderAsk() {
|
||||
@@ -2172,6 +2200,7 @@ function lightbox(src, alt) {
|
||||
d.append(h("img", { src, alt: alt || "" }));
|
||||
d.showModal();
|
||||
}
|
||||
$("#log").addEventListener("scroll", () => { pinned = nearEnd($("#log")); }, { passive: true });
|
||||
document.addEventListener("click", (e) => {
|
||||
const img = e.target && e.target.closest && e.target.closest(".log .pic img");
|
||||
if (img && img.src) { e.preventDefault(); lightbox(img.src, img.alt); }
|
||||
@@ -2262,6 +2291,7 @@ function runWorkflow(id, { door = "chat", idea = "", name = "" } = {}) {
|
||||
if (id === "new") return writeFromIdea(idea, { door, name });
|
||||
if (id === "photoshoot") return photoshoot(idea, { door });
|
||||
if (id === "recall") return recall(idea);
|
||||
if (id === "whisper") return whisper(idea);
|
||||
}
|
||||
/** the chat a workflow's result lands in: the one of theirs on screen, else their latest, else a new one */
|
||||
function threadFor(c) {
|
||||
@@ -2375,6 +2405,96 @@ async function photoshoot(arg, { door = "chat" } = {}) {
|
||||
balanceSoon();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* /whisper: a short ASMR voice note. One call writes the script in their voice from the chat (and what it's about,
|
||||
* if given); a whisper preset speaks it (W.WHISPER_PRESETS: luna on Inworld, eve on Grok, picked from the
|
||||
* asmr.gandora.lol bake-off); it's mixed over soft rain made here, and lands in their chat as a voice note.
|
||||
*/
|
||||
let whispering = false;
|
||||
async function whisper(arg) {
|
||||
const c = card();
|
||||
if (!c || c.plain) { err("/whisper needs a character: pick one in the harbor first."); return; }
|
||||
if (whispering) { toast("A whisper is already on its way."); return; }
|
||||
const { preset: named, brief } = W.parseWhisper(arg);
|
||||
const key = named || (c.whisper && W.WHISPER_PRESETS[c.whisper] ? c.whisper : W.WHISPER_DEFAULT), p = W.WHISPER_PRESETS[key];
|
||||
await loadList("tts");
|
||||
const vm = (S.lists.tts || []).find((m) => m.id === p.model);
|
||||
const guess = vm ? A.voicePrice(vm, 600) : null;
|
||||
if (!confirm(`A whispered voice note from ${c.name} (${p.voice})${brief ? " about " + brief : ""}? About ${guess != null ? A.usd(guess) : "a cent"} on your Venice key, plus the words.`)) return;
|
||||
const t = threadFor(c);
|
||||
whispering = true;
|
||||
S.inflight++;
|
||||
doing(t, "is writing something to whisper…");
|
||||
try {
|
||||
const lines = t.msgs.filter((m) => !m.hidden && (m.role === "user" || m.role === "assistant")).slice(-10)
|
||||
.map((m) => `${m.role === "user" ? "Them" : c.name}: ${String(m.content || "").replace(/\s+/g, " ").trim().slice(0, 300)}`);
|
||||
const o = await askJson(await jsonModel(c), { messages: W.whisperMessages(c, { brief, lines }), schema: W.WHISPER_SCHEMA, maxTokens: 400, temperature: 0.9, what: "the whisper" });
|
||||
const script = W.cleanWhisper(o);
|
||||
if (!script) throw new Error("The model sent back nothing to whisper. Try again, or pick another model on the card.");
|
||||
doing(t, "is whispering…");
|
||||
const input = W.whisperInput(script, p);
|
||||
const voice = await new Venice(S.key).speak({ model: p.model, voice: p.voice, input, speed: p.speed, format: p.format });
|
||||
let blob = voice;
|
||||
try { blob = await overRain(voice); } catch { /* the dry take is still a whisper */ }
|
||||
const price = vm ? A.voicePrice(vm, input.length) : null;
|
||||
done(t);
|
||||
deliver(t, { role: "assistant", content: script, voice: { id: await M.put(blob), model: vm ? vm.name : p.model, voice: p.voice, price, whisper: true } });
|
||||
if (named) { const now = S.cards.find((x) => x.id === c.id); if (now && now.whisper !== named) replaceCard({ ...now, whisper: named }); }
|
||||
} catch (e) {
|
||||
done(t);
|
||||
err(e.message || String(e));
|
||||
} finally {
|
||||
whispering = false;
|
||||
S.inflight--;
|
||||
balanceSoon();
|
||||
}
|
||||
}
|
||||
/** the voice, close-mic'd (a little low end, gentle compression), over soft rain made here; a wav back */
|
||||
async function overRain(blob) {
|
||||
const AC = window.OfflineAudioContext || window.webkitOfflineAudioContext;
|
||||
const probe = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const v = await probe.decodeAudioData(await blob.arrayBuffer());
|
||||
probe.close && probe.close();
|
||||
const rate = 44100, pad = 1.5, len = Math.ceil((v.duration + pad * 2) * rate);
|
||||
const ctx = new AC(2, len, rate);
|
||||
// rain: pink-ish hiss with a slow flutter, over a low brown rumble
|
||||
const noise = ctx.createBuffer(2, len, rate);
|
||||
for (let ch = 0; ch < 2; ch++) {
|
||||
const d = noise.getChannelData(ch); let b0 = 0, b1 = 0, b2 = 0, br = 0;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const w = Math.random() * 2 - 1;
|
||||
b0 = 0.997 * b0 + 0.029 * w; b1 = 0.985 * b1 + 0.032 * w; b2 = 0.95 * b2 + 0.048 * w;
|
||||
br = (br + 0.02 * w) / 1.02;
|
||||
d[i] = (b0 + b1 + b2 + w * 0.05) * 0.6 + br * 2.5;
|
||||
}
|
||||
}
|
||||
const rain = ctx.createBufferSource(); rain.buffer = noise;
|
||||
const hp = ctx.createBiquadFilter(); hp.type = "highpass"; hp.frequency.value = 120;
|
||||
const lp = ctx.createBiquadFilter(); lp.type = "lowpass"; lp.frequency.value = 6500;
|
||||
const rg = ctx.createGain(); const end = len / rate;
|
||||
rg.gain.setValueAtTime(0, 0); rg.gain.linearRampToValueAtTime(0.09, 1.2); rg.gain.setValueAtTime(0.09, end - 1.2); rg.gain.linearRampToValueAtTime(0, end);
|
||||
rain.connect(hp); hp.connect(lp); lp.connect(rg); rg.connect(ctx.destination);
|
||||
const src = ctx.createBufferSource(); src.buffer = v;
|
||||
const lo = ctx.createBiquadFilter(); lo.type = "lowshelf"; lo.frequency.value = 180; lo.gain.value = 4;
|
||||
const cmp = ctx.createDynamicsCompressor(); cmp.threshold.value = -30; cmp.ratio.value = 4; cmp.attack.value = 0.01; cmp.release.value = 0.25;
|
||||
const vg = ctx.createGain(); vg.gain.value = 1.6;
|
||||
src.connect(lo); lo.connect(cmp); cmp.connect(vg); vg.connect(ctx.destination);
|
||||
rain.start(0); src.start(pad);
|
||||
return wavBlob(await ctx.startRendering());
|
||||
}
|
||||
/** an AudioBuffer as a 16-bit wav */
|
||||
function wavBlob(buf) {
|
||||
const n = buf.numberOfChannels, len = buf.length, out = new DataView(new ArrayBuffer(44 + len * n * 2));
|
||||
const s = (o, t) => { for (let i = 0; i < t.length; i++) out.setUint8(o + i, t.charCodeAt(i)); };
|
||||
s(0, "RIFF"); out.setUint32(4, 36 + len * n * 2, true); s(8, "WAVE"); s(12, "fmt "); out.setUint32(16, 16, true);
|
||||
out.setUint16(20, 1, true); out.setUint16(22, n, true); out.setUint32(24, buf.sampleRate, true);
|
||||
out.setUint32(28, buf.sampleRate * n * 2, true); out.setUint16(32, n * 2, true); out.setUint16(34, 16, true);
|
||||
s(36, "data"); out.setUint32(40, len * n * 2, true);
|
||||
const chans = []; for (let c = 0; c < n; c++) chans.push(buf.getChannelData(c));
|
||||
let o = 44;
|
||||
for (let i = 0; i < len; i++) for (let c = 0; c < n; c++) { const x = Math.max(-1, Math.min(1, chans[c][i])); out.setInt16(o, x < 0 ? x * 0x8000 : x * 0x7fff, true); o += 2; }
|
||||
return new Blob([out], { type: "audio/wav" });
|
||||
}
|
||||
/**
|
||||
* A photo of them from the chat, kept on their card: as their portrait (as = "portrait"; the look it was drawn from goes
|
||||
* on the card too if it had none), or as one of the pictures their photos and clips are made from (as = "").
|
||||
@@ -2531,26 +2651,41 @@ async function startClip(c, cm, shot, opts, price, where, { adult = false } = {}
|
||||
* showing, or now and then), the character's model decides: a photo or a voice note, sent out of the day's
|
||||
* allowance, or a clip, offered with Venice's price for you to take or leave.
|
||||
*/
|
||||
async function onTheirOwn(t) {
|
||||
const noPicSaid = new Set(), moveSaid = new Set();
|
||||
let allowanceSaid = "";
|
||||
async function onTheirOwn(t, { voiced = false } = {}) { // voiced: the reply came with a voice note already, so not another
|
||||
const c = cardOf(t);
|
||||
if (!c || c.plain || !S.key || !S.ab.own || S.moving.has(t.id) || !A.moveLikely(t.msgs, { every: tempoEvery() })) return;
|
||||
// the call that decides is paid for too: set aside its most, from what's left today, and settle after
|
||||
const model = (S.models || []).find((m) => m.id === (c.model || S.model)) || null;
|
||||
const messages = A.moveMessages(c, A.apiHistory(t.msgs), { adult: adultFor(c, t).ok }), most = A.callEstimate(messages, 300, model);
|
||||
if (!reserve(most)) return;
|
||||
const messages = A.moveMessages(c, A.apiHistory(t.msgs), { adult: adultFor(c, t).ok, eager: tempoEvery() === 1 }), most = A.callEstimate(messages, 300, model);
|
||||
if (!reserve(most)) { // today's allowance is spent: said once a day, not every round
|
||||
const day = localDay();
|
||||
if (allowanceSaid !== day && here(t)) { allowanceSaid = day; err(`nothing on their own today: the allowance (${A.usd(S.ab.allowance)} a day, under abilities) is spent.`); }
|
||||
return;
|
||||
}
|
||||
S.moving.add(t.id); // one move at a time in a chat
|
||||
let held = most;
|
||||
try {
|
||||
const r = await new Venice(S.key).chat({ model: c.model || S.model, messages, schema: A.MOVE_SCHEMA, maxTokens: 300, temperature: 0.7 });
|
||||
const r = await new Venice(S.key).chat({ model: await jsonModel(c), messages, schema: A.MOVE_SCHEMA, maxTokens: 300, temperature: 0.7 });
|
||||
settle(held, A.callCost(r.usage, model));
|
||||
held = 0;
|
||||
if (!S.threads[t.id] || !cardOf(t)) return; // the chat, or they, went meanwhile
|
||||
const { move, shot } = A.normalizeMove(C.parseJsonLoose(r.text));
|
||||
let { move, shot } = A.normalizeMove(C.parseJsonLoose(r.text));
|
||||
const lately = A.sentLately(t.msgs, tempoEvery() === 1 ? 1 : 4); // never two photos in a row
|
||||
if (move === "photo" && !lately) await ownPhoto(t, shot);
|
||||
else if (move === "voice") await ownVoice(t);
|
||||
if (tempoEvery() === 1 && (move === "nothing" || (move === "voice" && voiced)) && !lately && refsOf(c).length) { // every round means every round: a photo, then
|
||||
const sr = await new Venice(S.key).chat({ model: c.model || S.model, messages: A.shotMessages(c, A.apiHistory(t.msgs), "", "photo", { adult: adultFor(c, t).ok }), maxTokens: 260, temperature: 0.8 });
|
||||
shot = A.cleanText(sr.text);
|
||||
if (shot) move = "photo";
|
||||
}
|
||||
if ((move === "photo" || move === "clip") && !refsOf(c).length) { // nothing to make one from: say so, once per chat
|
||||
if (!noPicSaid.has(t.id) && here(t)) { noPicSaid.add(t.id); err(`${c.name} wanted to send a ${move}, but has no picture yet to make one from: /portraits, or add one on their card (card → look).`); }
|
||||
} else if (move === "photo" && !lately) await ownPhoto(t, shot);
|
||||
else if (move === "voice" && !voiced) await ownVoice(t);
|
||||
else if (move === "clip" && !lately) await offerClip(t, shot);
|
||||
} catch { /* then they just text */ } finally { refund(held); S.moving.delete(t.id); }
|
||||
} catch (e) { // then they just text; set to every round, the reason is shown once
|
||||
if (tempoEvery() === 1 && !moveSaid.has(t.id) && here(t)) { moveSaid.add(t.id); err(`nothing on their own this time: ${e && e.message ? e.message : e}`); }
|
||||
} finally { refund(held); S.moving.delete(t.id); }
|
||||
}
|
||||
/** something they started on their own that didn't come (a photo, a voice note): said in its chat, or in a word elsewhere */
|
||||
function missed(t, what, e) {
|
||||
@@ -2571,9 +2706,10 @@ async function ownPhoto(t, shot) {
|
||||
const adult = adultFor(c, t, shot).ok; // an adult character's can be adult (abilities.js adultCheck)
|
||||
const refs = refsOf(c), tried = [];
|
||||
let pm = adult ? adultPhotoModel() : photoModel(), why = null;
|
||||
if (!pm) why = new Error("no photo model is available (abilities → photos).");
|
||||
while (pm && refs.length && tried.length < 2) { // no picture of them yet: nothing to send
|
||||
const pics = refs.slice(0, Math.max(1, pm.maxIn)), price = A.photoPrice(pm, pics.length);
|
||||
if (!reserve(price)) break; // set aside first: nothing else can spend it meanwhile
|
||||
if (!reserve(price)) { why = new Error(`it doesn't fit in what's left of today's allowance (${A.usd(price)} for the photo; the allowance is under abilities).`); break; }
|
||||
tried.push(pm.id);
|
||||
doing(t, "is taking a photo…");
|
||||
try {
|
||||
@@ -2800,7 +2936,7 @@ async function hearVoice(blob, to) {
|
||||
if (blob.size < 600) { toast("That voice note was too short to hear."); return; }
|
||||
const id = await M.put(blob);
|
||||
const note = put(line("hearing your voice note…", "havre", "typing"));
|
||||
scrollLog();
|
||||
scrollLog(true);
|
||||
try {
|
||||
await loadList("asr");
|
||||
const hm = hearModel(), ext = /mp4/.test(blob.type) ? "m4a" : /ogg/.test(blob.type) ? "ogg" : "webm";
|
||||
@@ -2837,7 +2973,7 @@ async function showPhoto(file) {
|
||||
const sm = seeModel();
|
||||
if (!sm) { err("No Venice model can see photos right now."); return; }
|
||||
const note = put(line(`${sm.name} is looking at your photo…`, "havre", "typing"));
|
||||
scrollLog();
|
||||
scrollLog(true);
|
||||
try {
|
||||
const r = await new Venice(S.key).chat({ model: sm.id, maxTokens: 220, temperature: 0.2,
|
||||
messages: [{ role: "user", content: [{ type: "text", text: A.SEE_PROMPT }, { type: "image_url", image_url: { url: pic } }] }] });
|
||||
@@ -3008,6 +3144,12 @@ function setTempo(v) {
|
||||
if ($("#abilitydlg").open) renderAbilities();
|
||||
toast(`Photos and voice notes on their own: ${TEMPOS.find(([k]) => k === v)[1]}.`);
|
||||
}
|
||||
function awaySelect() {
|
||||
const s = h("select", { "aria-label": "How long you've been away before they text first" }, AWAYS.map(([v, t]) => h("option", { value: String(v), text: t })));
|
||||
s.value = String(S.ab.awayMin || 360);
|
||||
s.addEventListener("change", () => { S.ab.awayMin = Number(s.value); saveAb(); toast(`They text first after you've been away ${AWAYS.find(([v]) => v === S.ab.awayMin)[1]}.`); });
|
||||
return s;
|
||||
}
|
||||
function tempoSelect() {
|
||||
const s = h("select", { "aria-label": "How often they send things on their own" }, TEMPOS.map(([v, t]) => h("option", { value: v, text: t })));
|
||||
s.value = S.ab.tempo || "sometimes";
|
||||
@@ -3223,9 +3365,50 @@ function forgetKey() {
|
||||
}
|
||||
|
||||
// ---------- import ----------
|
||||
/** the pictures and clips with these ids, as data: URLs (the ones this browser still has) */
|
||||
async function mediaAsData(ids) {
|
||||
const out = {};
|
||||
for (const id of ids) {
|
||||
const blob = await M.get(id);
|
||||
if (!blob) continue;
|
||||
out[id] = await new Promise((resolve) => { const r = new FileReader(); r.onload = () => resolve(String(r.result)); r.onerror = () => resolve(""); r.readAsDataURL(blob); });
|
||||
if (!out[id]) delete out[id];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
/**
|
||||
* A bundle (one character with memory and chats, or everything) put back: cards not already here are added, notes are
|
||||
* appended to what's remembered, chats by id are added if missing, pictures are put back by id. Never replaces.
|
||||
*/
|
||||
async function restoreBundle(bundle) {
|
||||
const m = B.merge(bundle, { cards: S.cards, memory: local.get(K.memory, {}) || {}, threads: S.threads });
|
||||
for (const c of m.addCards) addCard(c, { select: false });
|
||||
for (const [id, mem] of Object.entries(m.memory)) if (S.cards.some((c) => c.id === id)) saveMemory(id, mem);
|
||||
let pics = 0;
|
||||
for (const [id, d] of Object.entries(bundle.media)) { try { await M.put(dataBlob(d), id); pics++; } catch { /* that one's gone */ } }
|
||||
for (const t of m.addThreads) { if (S.cards.some((c) => c.id === t.card) || t.card === PLAIN) { S.threads[t.id] = t; saveThread(t); } }
|
||||
if (bundle.settings) {
|
||||
if (bundle.settings.abilities && typeof bundle.settings.abilities === "object") { S.ab = { ...S.ab, ...bundle.settings.abilities, clip: { ...S.ab.clip, ...(bundle.settings.abilities.clip || {}) } }; saveAb(); }
|
||||
if (bundle.settings.voiceOn && typeof bundle.settings.voiceOn === "object") { S.voiceOn = { ...S.voiceOn, ...bundle.settings.voiceOn }; local.set(K.voiceOn, S.voiceOn); }
|
||||
}
|
||||
renderAll();
|
||||
const bits = [`${m.addCards.length} character${m.addCards.length === 1 ? "" : "s"}`, `${m.addThreads.length} chat${m.addThreads.length === 1 ? "" : "s"}`, `${pics} picture${pics === 1 ? "" : "s"}`];
|
||||
const skipped = [m.skippedCards && `${m.skippedCards} character${m.skippedCards === 1 ? "" : "s"}`, m.skippedThreads && `${m.skippedThreads} chat${m.skippedThreads === 1 ? "" : "s"}`].filter(Boolean);
|
||||
toast(`Restored ${bits.join(", ")}${skipped.length ? "; already here: " + skipped.join(", ") : ""}.`);
|
||||
}
|
||||
async function importFile(file) {
|
||||
const st = $("#impstatus");
|
||||
try {
|
||||
if (/\.json$/i.test(file.name) && file.size <= 200 * 1024 * 1024) {
|
||||
let parsed = null;
|
||||
try { parsed = JSON.parse(await file.text()); } catch { /* not a bundle, then */ }
|
||||
const bundle = B.readBundle(parsed);
|
||||
if (bundle) {
|
||||
if ($("#importdlg").open) $("#importdlg").close();
|
||||
await restoreBundle(bundle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (file.size > 12 * 1024 * 1024) throw new Error("That file is too big for a character card.");
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
const { card: c, kind } = C.fromAny(bytes);
|
||||
@@ -3536,6 +3719,17 @@ async function exportAs(kind) {
|
||||
else if (kind === "v3") download(C.fileName(c, "card.json"), JSON.stringify(C.toCharacterCardV3(abs), null, 1) + "\n", "application/json");
|
||||
else if (kind === "prompt") download(C.fileName(c, "txt"), C.systemPrompt(c) + "\n", "text/plain");
|
||||
else if (kind === "png") download(C.fileName(c, "png"), new Blob([C.toPngCard(await cardPng(c), abs)], { type: "image/png" }), "image/png");
|
||||
else if (kind === "full" || kind === "all") {
|
||||
if (c.plain && kind === "full") throw new Error("A plain chat has no card to export.");
|
||||
const threads = Object.values(S.threads).map((t) => ({ ...t }));
|
||||
const mine = kind === "all" ? threads : threads.filter((t) => t.card === c.id);
|
||||
toast(`Gathering ${kind === "all" ? "everything" : c.name + "'s pictures"}…`);
|
||||
const media = await mediaAsData(B.mediaIds(mine));
|
||||
const bundle = kind === "all"
|
||||
? B.bundleAll({ cards: S.cards.map(absolute), memory: local.get(K.memory, {}) || {}, threads, media, settings: { abilities: S.ab, model: S.model, voiceOn: S.voiceOn } })
|
||||
: B.bundleOne(abs, { memory: memoryOf(c.id), threads: mine, media });
|
||||
download(B.bundleName(kind === "all" ? null : c), JSON.stringify(bundle) + "\n", "application/json");
|
||||
}
|
||||
toast("Made in your browser. Nothing was uploaded.");
|
||||
} catch (e) { toast(e.message); }
|
||||
}
|
||||
@@ -3672,6 +3866,12 @@ function bind() {
|
||||
$("#vprev").addEventListener("click", () => loadVenice(Math.max(0, S.v.offset - PAGE), { paging: true }));
|
||||
$("#vnext").addEventListener("click", () => loadVenice(S.v.offset + PAGE, { paging: true }));
|
||||
|
||||
$("#card-fresh").addEventListener("click", () => {
|
||||
S.fresh = true;
|
||||
if (S.left !== "harbor") setLeft("harbor"); else syncNew();
|
||||
$("#card").scrollIntoView({ behavior: motion(), block: "start" });
|
||||
$("#nc-name").focus({ preventScroll: true });
|
||||
});
|
||||
$("#new").addEventListener("click", () => {
|
||||
addCard({ name: "New character", persona: "", greeting: "", voice: { short_form: true }, model: S.model, license: "remix-with-credit" });
|
||||
openCard("edit");
|
||||
@@ -3687,6 +3887,7 @@ function bind() {
|
||||
openCard("edit");
|
||||
});
|
||||
$("#nc-import").addEventListener("click", () => $("#import").click());
|
||||
$("#restore").addEventListener("click", () => $("#import").click());
|
||||
$("#import").addEventListener("click", () => { $("#impstatus").textContent = ""; $("#impstatus").classList.remove("bad"); $("#importdlg").showModal(); });
|
||||
$("#impcancel").addEventListener("click", () => $("#importdlg").close());
|
||||
$("#imp-paste").addEventListener("click", importText);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// backup.js: a character with everything this browser keeps about them, or the whole harbor, as one file, and back.
|
||||
// Pure functions, no DOM: the page imports this module, and so do the tests.
|
||||
//
|
||||
// a bundle { havre_backup: 1, at, cards: [card], memory: { <card id>: memory }, threads: [thread + msgs],
|
||||
// media: { <id>: data: URL }, settings?: { abilities, model, voiceOn } }
|
||||
//
|
||||
// A card file (.havre.json) carries the character and nothing else, by design: it's the thing you share. A bundle
|
||||
// is the thing you keep: the card, what they remember, your chats with them and the pictures in those chats. "Full"
|
||||
// is every character, every chat, every picture, and your settings (never your key). Restoring merges: a card already
|
||||
// in the harbor is left as it is, notes are appended, chats by id are added if missing, pictures by id are put back.
|
||||
|
||||
import { normalizeCard } from "./card.js";
|
||||
import { readMemory, appended } from "./memory.js";
|
||||
|
||||
export const BACKUP = 1;
|
||||
|
||||
const str = (v, max = 20000) => (typeof v === "string" ? v : v == null ? "" : String(v)).slice(0, max);
|
||||
const arr = (v) => (Array.isArray(v) ? v : []);
|
||||
const obj = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
|
||||
|
||||
/** the media ids a thread's messages point at */
|
||||
export function mediaIds(threads) {
|
||||
const ids = new Set();
|
||||
for (const t of arr(threads)) for (const m of arr(t.msgs)) if (m && m.media && typeof m.media.id === "string" && m.media.id) ids.add(m.media.id);
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
/** one character's bundle: the card, their memory, the chats with them (with messages), the pictures those chats hold */
|
||||
export function bundleOne(card, { memory = null, threads = [], media = {}, at = Date.now() } = {}) {
|
||||
const mine = arr(threads).filter((t) => t && t.card === card.id);
|
||||
const ids = new Set(mediaIds(mine));
|
||||
return { havre_backup: BACKUP, at, cards: [card], memory: memory ? { [card.id]: memory } : {}, threads: mine,
|
||||
media: Object.fromEntries(Object.entries(obj(media)).filter(([id]) => ids.has(id))) };
|
||||
}
|
||||
|
||||
/** the whole harbor: every card, memory, chat and picture, and the settings worth carrying (never the key) */
|
||||
export function bundleAll({ cards = [], memory = {}, threads = [], media = {}, settings = null, at = Date.now() } = {}) {
|
||||
return { havre_backup: BACKUP, at, cards: arr(cards), memory: obj(memory), threads: arr(threads), media: obj(media),
|
||||
...(settings ? { settings } : {}) };
|
||||
}
|
||||
|
||||
/** a bundle as read from a file, made safe; null if the text isn't one */
|
||||
export function readBundle(o) {
|
||||
if (!o || typeof o !== "object" || Number(o.havre_backup) !== BACKUP) return null;
|
||||
const cards = arr(o.cards).filter((c) => c && typeof c === "object" && c.name).map((c) => normalizeCard(c));
|
||||
const memory = {};
|
||||
for (const [id, m] of Object.entries(obj(o.memory))) { const r = readMemory(m); if (r.notes.length || Object.keys(r.upTo).length) memory[str(id, 80)] = r; }
|
||||
const threads = arr(o.threads).filter((t) => t && typeof t === "object" && typeof t.id === "string" && typeof t.card === "string")
|
||||
.map((t) => ({ ...t, id: str(t.id, 80), card: str(t.card, 80), msgs: arr(t.msgs).filter((m) => m && typeof m === "object" && (m.role === "user" || m.role === "assistant")) }));
|
||||
const media = {};
|
||||
for (const [id, d] of Object.entries(obj(o.media))) if (typeof d === "string" && /^data:(image|audio|video)\//.test(d) && d.length < 20_000_000) media[str(id, 80)] = d;
|
||||
const settings = obj(o.settings);
|
||||
return { at: Number(o.at) || 0, cards, memory, threads, media, settings: Object.keys(settings).length ? settings : null };
|
||||
}
|
||||
|
||||
/**
|
||||
* What restoring a bundle into a harbor comes to, without touching anything: cards to add (not already there, by
|
||||
* id), memories merged (notes appended to what's there), threads to add (by id, not already there). Pictures are
|
||||
* put back as they are. The counts say what happened.
|
||||
*/
|
||||
export function merge(bundle, { cards = [], memory = {}, threads = {} } = {}) {
|
||||
const have = new Set(arr(cards).map((c) => c.id));
|
||||
const addCards = bundle.cards.filter((c) => !have.has(c.id));
|
||||
const mergedMemory = {};
|
||||
for (const [id, m] of Object.entries(bundle.memory)) {
|
||||
const now = readMemory(obj(memory)[id]);
|
||||
mergedMemory[id] = { notes: appended(now.notes, m.notes) || now.notes, upTo: { ...m.upTo, ...now.upTo }, at: Math.max(now.at, m.at) };
|
||||
}
|
||||
const addThreads = bundle.threads.filter((t) => !obj(threads)[t.id]);
|
||||
return { addCards, memory: mergedMemory, addThreads, skippedCards: bundle.cards.length - addCards.length, skippedThreads: bundle.threads.length - addThreads.length };
|
||||
}
|
||||
|
||||
/** a file name for a bundle: one character's, or the harbor's, with the day */
|
||||
export function bundleName(card, at = Date.now()) {
|
||||
const day = new Date(at).toISOString().slice(0, 10);
|
||||
return card ? `${card.id}.havre-full.json` : `havre-backup-${day}.json`;
|
||||
}
|
||||
+21
-5
@@ -172,7 +172,20 @@ const quoted = (s, max = 600) => `"${str(s).replace(/\s+/g, " ").trim().slice(0,
|
||||
* The system prompt: who they are, how they talk (with their examples, which teach the voice), their flow, what they
|
||||
* remember of your chats (memory: notes from memory.js, never part of the card), their name, and the house rules.
|
||||
*/
|
||||
export function systemPrompt(card, { memory = [] } = {}) {
|
||||
/**
|
||||
* What they can send in this chat, for the prompt: photos and clips (the app makes them from their pictures when they
|
||||
* decide to), not yet (no picture on their card), or not at all (no key, or switched off). "" when nothing is known.
|
||||
*/
|
||||
export function canBlock(can) {
|
||||
if (!can || typeof can !== "object") return "";
|
||||
if (!can.photos) return "In this chat you can't send pictures, clips or voice notes: if they ask for one, say so plainly, in your own voice.";
|
||||
if (!can.pictured) return "You could send photos of yourself here, but there's no picture of you on your card yet. If they ask for one, say so: they can " +
|
||||
"make your portrait on your card (or type /portraits in this chat), and after that you can send photos.";
|
||||
return "In this chat you can send photos of yourself and short clips: when you decide to, they are made from your pictures and arrive " +
|
||||
"with your message. Decide the way a person would, and never say you can't send pictures. Never describe a photo you didn't send.";
|
||||
}
|
||||
|
||||
export function systemPrompt(card, { memory = [], can = null } = {}) {
|
||||
const parts = [`You are ${card.name}.${card.tagline ? " " + card.tagline : ""}`];
|
||||
if (card.persona) parts.push(fill(card.persona, card));
|
||||
const voice = [card.voice.notes && fill(card.voice.notes, card), card.voice.short_form && SHORT_FORM].filter(Boolean);
|
||||
@@ -189,6 +202,8 @@ export function systemPrompt(card, { memory = [] } = {}) {
|
||||
}
|
||||
const mem = memoryBlock(memory);
|
||||
if (mem) parts.push(mem);
|
||||
const able = canBlock(can);
|
||||
if (able) parts.push(able);
|
||||
if (card.adult) parts.push(ADULT_RULE);
|
||||
parts.push(identityRule(card), HOUSE_RULES);
|
||||
return parts.join("\n\n");
|
||||
@@ -206,11 +221,12 @@ export function memoryBlock(memory = []) {
|
||||
* of Havre's own (not a Venice character, who brings their own prompt), the last thing said ends with a reminder of
|
||||
* who is answering. memory: what they remember, for the system prompt.
|
||||
*/
|
||||
export function chatMessages(card, history, { memory = [] } = {}) {
|
||||
export function chatMessages(card, history, { memory = [], can = null } = {}) {
|
||||
const own = !card.engine.venice_character;
|
||||
// a Venice character brings its own prompt: beside it go only what they remember and, if they're adult, the 18+ rule
|
||||
const extra = own ? "" : [memoryBlock(memory), card.adult ? ADULT_RULE : ""].filter(Boolean).join("\n\n");
|
||||
const msgs = own ? [{ role: "system", content: systemPrompt(card, { memory }) }] : extra ? [{ role: "system", content: extra }] : [];
|
||||
// a Venice character brings its own prompt: beside it go only what they remember, what they can send here and, if
|
||||
// they're adult, the 18+ rule
|
||||
const extra = own ? "" : [memoryBlock(memory), canBlock(can), card.adult ? ADULT_RULE : ""].filter(Boolean).join("\n\n");
|
||||
const msgs = own ? [{ role: "system", content: systemPrompt(card, { memory, can }) }] : extra ? [{ role: "system", content: extra }] : [];
|
||||
// the greeting starts the conversation, so it goes once the conversation's start is out of the window
|
||||
if (card.greeting && own && history.length <= 40) msgs.push({ role: "assistant", content: fill(card.greeting, card) });
|
||||
for (const m of history.slice(-40)) if (m.role === "user" || m.role === "assistant") msgs.push({ role: m.role, content: str(m.content, 8000) });
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<link rel="alternate" type="text/markdown" href="skill.md" title="Skill">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script type="module" src="app.js"></script>
|
||||
<script src="update.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip" href="#chat">skip to the chat</a>
|
||||
@@ -30,7 +31,7 @@
|
||||
<header class="mast" id="top">
|
||||
<h1>HAVRE</h1>
|
||||
<p class="motto">a harbor for characters. <br>talk to venice characters on your own key. <br>no account. no server. nothing kept but what you keep.</p>
|
||||
<nav class="nav" aria-label="On this page"><a href="#how">how it works</a> / <a href="#agents">for agents</a> / <a href="#costs">costs</a> / <a href="#plans">early access</a> / <a href="open/">open studio (beta)</a> / <button type="button" class="linkish" id="theme">dark mode</button></nav>
|
||||
<nav class="nav" aria-label="Havre"><a href="open/">studio</a> / <button type="button" class="linkish" id="theme">dark mode</button> / <button type="button" class="linkish" id="reload" title="the latest Havre (installed as an app, there's no other way to reload)">reload</button></nav>
|
||||
</header>
|
||||
|
||||
<form class="keybar" id="keyform" autocomplete="off" aria-label="Your Venice API key">
|
||||
@@ -123,7 +124,7 @@
|
||||
<section class="block card" id="card" aria-labelledby="card-h">
|
||||
<div class="sechead">
|
||||
<h2 id="card-h">CARD</h2>
|
||||
<span class="meta">the character as a file: edit it, remix it, export it.</span>
|
||||
<span class="meta">the character as a file: edit it, remix it, export it. / <button type="button" class="linkish" id="card-fresh">new character</button></span>
|
||||
</div>
|
||||
<p id="card-empty" class="mono pad">no character yet. the card shows up here once someone is in your harbor.</p>
|
||||
<form id="card-new" class="pad newcard" autocomplete="off" hidden>
|
||||
@@ -247,8 +248,10 @@
|
||||
<li><button type="button" class="out" data-out="v3"><b>CHARACTER CARD V3</b><span>.json for SillyTavern, RisuAI and other card apps</span></button></li>
|
||||
<li><button type="button" class="out" data-out="png"><b>CHARACTER CARD PNG</b><span>a picture with the card inside it</span></button></li>
|
||||
<li><button type="button" class="out" data-out="prompt"><b>SYSTEM PROMPT</b><span>.txt for any OpenAI-compatible API</span></button></li>
|
||||
<li><button type="button" class="out" data-out="full"><b>WITH MEMORY AND CHATS</b><span>.havre-full.json: the card, what they remember, your chats with them, the pictures in them</span></button></li>
|
||||
<li><button type="button" class="out" data-out="all"><b>EVERYTHING</b><span>havre-backup.json: every character, memory, chat and picture, and your settings (never your key)</span></button></li>
|
||||
</ul>
|
||||
<p class="mono">files are made here, in your browser. nothing is uploaded.</p>
|
||||
<p class="mono">files are made here, in your browser. nothing is uploaded. a .havre-full or backup file restores through <button type="button" class="linkish" id="restore">import a card</button>: what's already here stays, the rest is added.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-outline-style:solid;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:"Inter", "InterVariable", system-ui, sans-serif;--font-mono:"JetBrains Mono", "SF Mono", monospace;--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-violet-300:oklch(81.1% .111 293.571);--color-violet-400:oklch(70.2% .183 293.541);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-pink-300:oklch(82.3% .12 346.018);--color-pink-400:oklch(71.8% .202 349.761);--color-pink-500:oklch(65.6% .241 354.308);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-3xl:48rem;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-snug:1.375;--leading-relaxed:1.625;--leading-loose:2;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg-base:#0a0a0c;--color-bg-raised:#111114;--color-text-primary:rgba(var(--fg), .92);--color-accent:#6ee7d3;--color-accent-soft:#6ee7d329}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:Line truncated
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/workflows-view-7tPx_mLe.js","assets/workflow-engine-lSJFuHzZ.js","assets/workflow-engine-BZV40eAE.css","assets/playground-view-urTIiexa.js"])))=>i.map(i=>d[i]);
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/workflows-view-C9DNDVch.js","assets/workflow-engine-BuXGtBGn.js","assets/workflow-engine-BZV40eAE.css","assets/playground-view-B6WRI_IZ.js"])))=>i.map(i=>d[i]);
|
||||
(function(){const l=document.createElement("link").relList;if(l&&l.supports&&l.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))u(s);new MutationObserver(s=>{for(const c of s)if(c.type==="childList")for(const f of c.addedNodes)f.tagName==="LINK"&&f.rel==="modulepreload"&&u(f)}).observe(document,{childList:!0,subtree:!0});function a(s){const c={};return s.integrity&&(c.integrity=s.integrity),s.referrerPolicy&&(c.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?c.credentials="include":s.crossOrigin==="anonymous"?c.credentials="omit":c.credentials="same-origin",c}function u(s){if(s.ep)return;s.ep=!0;const c=a(s);fetch(s.href,c)}})();function lf(n){return n&&n.__esModule&&Object.prototype.hasOwnProperty.call(n,"default")?n.default:n}var sc={exports:{}},Ha={};var Tp;function N1(){if(Tp)return Ha;Tp=1;var n=Symbol.for("react.transitional.element"),l=Symbol.for("react.fragment");function a(u,s,c){var f=null;if(c!==void 0&&(f=""+c),s.key!==void 0&&(f=""+s.key),"key"in s){c={};for(var d in s)d!=="key"&&(c[d]=s[d])}else c=s;return s=c.ref,{$$typeof:n,type:u,key:f,ref:s!==void 0?s:null,props:c}}return Ha.Fragment=l,Ha.jsx=a,Ha.jsxs=a,Ha}var Ap;function O1(){return Ap||(Ap=1,sc.exports=N1()),sc.exports}var p=O1(),oc={exports:{}},we={};var _p;function z1(){if(_p)return we;_p=1;var n=Symbol.for("react.transitional.element"),l=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),u=Symbol.for("react.strict_mode"),s=Symbol.for("react.profiler"),c=Symbol.for("react.consumer"),f=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),g=Symbol.for("react.suspense"),m=Symbol.for("react.memo"),x=Symbol.for("react.lazy"),y=Symbol.for("react.activity"),b=Symbol.iterator;function w(_){return _===null||typeof _!="object"?null:(_=b&&_[b]||_["@@iterator"],typeof _=="function"?_:null)}var k={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},N=Object.assign,A={};function E(_,Y,C){this.props=_,this.context=Y,this.refs=A,this.updater=C||k}E.prototype.isReactComponent={},E.prototype.setState=function(_,Y){if(typeof _!="object"&&typeof _!="function"&&_!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,_,Y,"setState")},E.prototype.forceUpdate=function(_){this.updater.enqueueForceUpdate(this,_,"forceUpdate")};function O(){}O.prototype=E.prototype;function z(_,Y,C){this.props=_,this.context=Y,this.refs=A,this.updater=C||k}var X=z.prototype=new O;X.constructor=z,N(X,E.prototype),X.isPureReactComponent=!0;var V=Array.isArray;function R(){}var ee={H:null,A:null,T:null,S:null},W=Object.prototype.hasOwnProperty;function ue(_,Y,C){var le=C.ref;return{$$typeof:n,type:_,key:Y,ref:le!==void 0?le:null,props:C}}function U(_,Y){return ue(_.type,Y,_.props)}function ne(_){return typeof _=="object"&&_!==null&&_.$$typeof===n}function ie(_){var Y={"=":"=0",":":"=2"};return"$"+_.replace(/[=:]/g,function(C){return Y[C]})}var me=/\/+/g;function re(_,Y){return typeof _=="object"&&_!==null&&_.key!=null?ie(""+_.key):Y.toString(36)}function $(_){switch(_.status){case"fulfilled":return _.value;case"rejected":throw _.reason;default:switch(typeof _.status=="string"?_.then(R,R):(_.status="pending",_.then(function(Y){_.status==="pending"&&(_.status="fulfilled",_.value=Y)},function(Y){_.status==="pending"&&(_.status="rejected",_.reason=Y)})),_.status){case"fulfilled":return _.value;case"rejected":throw _.reason}}throw _}function L(_,Y,C,le,de){var oe=typeof _;(oe==="undefined"||oe==="boolean")&&(_=null);var Se=!1;if(_===null)Se=!0;else switch(oe){case"bigint":case"string":case"number":Se=!0;break;case"object":switch(_.$$typeof){case n:case l:Se=!0;break;case x:return Se=_._init,L(Se(_._payload),Y,C,le,de)}}if(Se)return de=de(_),Se=le===""?"."+re(_,0):le,V(de)?(C="",Se!=null&&(C=Se.replace(me,"$&/")+"/"),L(de,Y,C,"",function($e){return $e})):de!=null&&(ne(de)&&(de=U(de,C+(de.key==null||_&&_.key===de.key?"":(""+de.key).replace(me,"$&/")+"/")+Se)),Y.push(de)),1;Se=0;var fe=le===""?".":le+":";if(V(_))for(var ve=0;ve<_.length;ve++)le=_[ve],oe=fe+re(le,ve),Se+=L(le,Y,C,oe,de);else if(ve=w(_),typeof ve=="function")for(_=ve.call(_),ve=0;!(le=_.next()).done;)le=le.value,oe=fe+re(le,ve++),Se+=L(le,Y,C,oe,de);else if(oe==="object"){if(typeof _.then=="function")return L($(_),Y,C,le,de);throw Y=String(_),Error("Objects are not valid as a React child (found: "+(Y==="[object Object]"?"object with keys {"+Object.keys(_).join(", ")+"}":Y)+"). If you meant to render a collection of children, use an array instead.")}return Se}function P(_,Y,C){if(_==null)return _;var le=[],de=0;return L(_,le,"","",function(oe){return Y.call(C,oe,de++)}),le}function F(_){if(_._status===-1){var Y=_._result;Y=Y(),Y.then(function(C){(_._status===0||_._status===-1)&&(_._status=1,_._result=C)},function(C){(_._status===0||_._status===-1)&&(_._status=2,Line truncated
|
||||
at`)?" (<anonymous>)":-1<i.stack.indexOf("@")?"@unknown:0:0":""}return`
|
||||
`+Ht+e+Tt}var In=!1;function be(e,t){if(!e||In)return"";In=!0;var i=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var r={DetermineComponentFrameRoot:function(){try{if(t){var J=function(){throw Error()};if(Object.defineProperty(J.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(J,[])}catch(G){var Q=G}Reflect.construct(e,[],J)}else{try{J.call()}catch(G){Q=G}e.call(J.prototype)}}else{try{throw Error()}catch(G){Q=G}(J=e())&&typeof J.catch=="function"&&J.catch(function(){})}}catch(G){if(G&&Q&&typeof G.stack=="string")return[G.stack,Q.stack]}return[null,null]}};r.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var o=Object.getOwnPropertyDescriptor(r.DetermineComponentFrameRoot,"name");o&&o.configurable&&Object.defineProperty(r.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var h=r.DetermineComponentFrameRoot(),v=h[0],S=h[1];if(v&&S){var T=v.split(`
|
||||
@@ -49,4 +49,4 @@ Error generating stack: `+r.message+`
|
||||
`))}function g(w,k,N,A){const E=N.enter("tableCell"),O=N.enter("phrasing"),z=N.containerPhrasing(w,{...A,before:c,after:c});return O(),E(),z}function m(w,k){return hC(w,{align:k,alignDelimiters:u,padding:a,stringLength:s})}function x(w,k,N){const A=w.children;let E=-1;const O=[],z=k.enter("table");for(;++E<A.length;)O[E]=y(A[E],k,N);return z(),O}function y(w,k,N){const A=w.children;let E=-1;const O=[],z=k.enter("tableRow");for(;++E<A.length;)O[E]=g(A[E],w,k,N);return z(),O}function b(w,k,N){let A=rx.inlineCode(w,k,N);return N.stack.includes("tableCell")&&(A=A.replace(/\|/g,"\\$&")),A}}function WC(){return{exit:{taskListCheckValueChecked:qg,taskListCheckValueUnchecked:qg,paragraph:tE}}}function eE(){return{unsafe:[{atBreak:!0,character:"-",after:"[:|-]"}],handlers:{listItem:nE}}}function qg(n){const l=this.stack[this.stack.length-2];l.type,l.checked=n.type==="taskListCheckValueChecked"}function tE(n){const l=this.stack[this.stack.length-2];if(l&&l.type==="listItem"&&typeof l.checked=="boolean"){const a=this.stack[this.stack.length-1];a.type;const u=a.children[0];if(u&&u.type==="text"){const s=l.children;let c=-1,f;for(;++c<s.length;){const d=s[c];if(d.type==="paragraph"){f=d;break}}f===a&&(u.value=u.value.slice(1),u.value.length===0?a.children.shift():a.position&&u.position&&typeof u.position.start.offset=="number"&&(u.position.start.column++,u.position.start.offset++,a.position.start=Object.assign({},u.position.start)))}}this.exit(n)}function nE(n,l,a,u){const s=n.children[0],c=typeof n.checked=="boolean"&&s&&s.type==="paragraph",f="["+(n.checked?"x":" ")+"] ",d=a.createTracker(u);c&&d.move(f);let g=rx.listItem(n,l,a,{...u,...d.current()});return c&&(g=g.replace(/^(?:[*+-]|\d+\.)([\r\n]| {1,3})/,m)),g;function m(x){return x+f}}function iE(){return[Rk(),nC(),rC(),IC(),WC()]}function lE(n){return{extensions:[Lk(),iC(n),uC(),$C(n),eE()]}}const aE={tokenize:fE,partial:!0},ux={tokenize:hE,partial:!0},sx={tokenize:dE,partial:!0},ox={tokenize:mE,partial:!0},rE={tokenize:pE,partial:!0},cx={name:"wwwAutolink",tokenize:oE,previous:hx},fx={name:"protocolAutolink",tokenize:cE,previous:dx},Yn={name:"emailAutolink",tokenize:sE,previous:mx},kn={};function uE(){return{text:kn}}let Vi=48;for(;Vi<123;)kn[Vi]=Yn,Vi++,Vi===58?Vi=65:Vi===91&&(Vi=97);kn[43]=Yn;kn[45]=Yn;kn[46]=Yn;kn[95]=Yn;kn[72]=[Yn,fx];kn[104]=[Yn,fx];kn[87]=[Yn,cx];kn[119]=[Yn,cx];function sE(n,l,a){const u=this;let s,c;return f;function f(y){return!nf(y)||!mx.call(u,u.previous)||Mf(u.events)?a(y):(n.enter("literalAutolink"),n.enter("literalAutolinkEmail"),d(y))}function d(y){return nf(y)?(n.consume(y),d):y===64?(n.consume(y),g):a(y)}function g(y){return y===46?n.check(rE,x,m)(y):y===45||y===95||bt(y)?(c=!0,n.consume(y),g):x(y)}function m(y){return n.consume(y),s=!0,g}function x(y){return c&&s&&jt(u.previous)?(n.exit("literalAutolinkEmail"),n.exit("literalAutolink"),l(y)):a(y)}}function oE(n,l,a){const u=this;return s;function s(f){return f!==87&&f!==119||!hx.call(u,u.previous)||Mf(u.events)?a(f):(n.enter("literalAutolink"),n.enter("literalAutolinkWww"),n.check(aE,n.attempt(ux,n.attempt(sx,c),a),a)(f))}function c(f){return n.exit("literalAutolinkWww"),n.exit("literalAutolink"),l(f)}}function cE(n,l,a){const u=this;let s="",c=!1;return f;function f(y){return(y===72||y===104)&&dx.call(u,u.previous)&&!Mf(u.events)?(n.enter("literalAutolink"),n.enter("literalAutolinkHttp"),s+=String.fromCodePoint(y),n.consume(y),d):a(y)}function d(y){if(jt(y)&&s.length<5)return s+=String.fromCodePoint(y),n.consume(y),d;if(y===58){const b=s.toLowerCase();if(b==="http"||b==="https")return n.consume(y),g}return a(y)}function g(y){return y===47?(n.consume(y),c?m:(c=!0,g)):a(y)}function m(y){return y===null||Du(y)||Xe(y)||Zi(y)||Qu(y)?a(y):n.attempt(ux,n.attempt(sx,x),a)(y)}function x(y){return n.exit("literalAutolinkHttp"),n.exit("literalAutolink"),l(y)}}function fE(n,l,a){let u=0;return s;function s(f){return(f===87||f===119)&&u<3?(u++,n.consume(f),s):f===46&&u===3?(n.consume(f),c):a(f)}function c(f){return f===null?a(f):l(f)}}function hE(n,l,a){let u,s,c;return f;function f(m){return m===46||m===95?n.check(ox,g,d)(m):m===null||Xe(m)||Zi(m)||m!==45&&Qu(m)?g(m):(c=!0,n.consume(m),f)}function d(m){return m===95?u=!0:(s=u,u=void 0),n.consume(m),f}function g(m){return s||u||!c?a(m):l(m)}}function dE(n,l){let a=0,u=0;return s;function s(f){return f===40?(a++,n.consume(f),s):f===41&&u<a?c(f):f===33||f===34||f===38||f===39||f===41||f===42||f===44||f===46||f===58||f===59||f===60||f===63||f===93||f===95||f===126?n.check(ox,l,c)(f):f===null||Xe(f)||Zi(f)?l(f):(n.consume(f),s)}function c(f){return f===41&&u++,n.consume(f),s}}function mE(n,l,a){return u;function u(d){return d===33||d===34||d===39||d===41||d===42||d===44||d===46||d===58||d===59||d===63||d===95||d===126?(n.consume(d),u):d===38?(n.consume(d),c):d===93?(n.consume(d),s):d===60||d===null||Xe(d)||Zi(d)?l(d):a(d)}function s(d){return d===null||d===40||d===91||Xe(d)||Zi(d)?l(d):u(d)}function c(d){return jt(d)?f(d):a(d)}function f(d){re
Line truncated
|
||||
`)?p.jsx("code",{className:l,...a,children:n}):p.jsxs("div",{className:"relative group/code",children:[s&&p.jsx("div",{className:"absolute top-0 left-0 px-3 py-1.5 text-[13px] text-white/15 font-mono uppercase tracking-wider select-none",children:s}),p.jsx("button",{onClick:()=>{navigator.clipboard.writeText(c),d(!0),setTimeout(()=>d(!1),1500)},className:"absolute top-1.5 right-1.5 px-2 py-1 text-[13px] font-medium text-white/15 hover:text-white/40 bg-white/[0.03] hover:bg-white/[0.06] rounded-md transition-all opacity-0 group-hover/code:opacity-100",children:f?"Copied":"Copy"}),p.jsx("code",{className:l,...a,children:n})]})}function VE(n){if(typeof n=="string")return{text:n,images:[]};let l="";const a=[];for(const u of n)u.type==="text"&&u.text&&(l+=u.text),u.type==="image_url"&&u.image_url?.url&&a.push(u.image_url.url);return{text:l,images:a}}function GE({message:n,onCopy:l,onDelete:a,onRegenerate:u}){const[s,c]=q.useState(!1),[f,d]=q.useState(!1),[g,m]=q.useState(!1),x=n.role==="user",{text:y,images:b}=VE(n.content),w=()=>{navigator.clipboard.writeText(y),d(!0),l(),setTimeout(()=>d(!1),1500)},k=p.jsxs("div",{className:`flex items-center gap-0.5 h-6 transition-opacity duration-150 ${s?"opacity-100":"opacity-0"}`,children:[p.jsx(zc,{label:f?"Copied":"Copy",onClick:w,children:f?p.jsx("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:p.jsx("path",{d:"M20 6L9 17l-5-5"})}):p.jsxs("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[p.jsx("rect",{x:"9",y:"9",width:"13",height:"13",rx:"2"}),p.jsx("path",{d:"M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"})]})}),!x&&u&&p.jsx(zc,{label:"Regenerate",onClick:u,children:p.jsxs("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[p.jsx("path",{d:"M1 4v6h6"}),p.jsx("path",{d:"M3.51 15a9 9 0 102.13-9.36L1 10"})]})}),p.jsx(zc,{label:"Delete",onClick:a,children:p.jsx("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:p.jsx("path",{d:"M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"})})})]});return x?p.jsx("div",{className:"flex justify-end",onMouseEnter:()=>c(!0),onMouseLeave:()=>c(!1),children:p.jsxs("div",{className:"flex items-end gap-1.5 max-w-[78%]",children:[k,p.jsxs("div",{className:"bg-white/[0.07] border border-white/[0.05] rounded-2xl rounded-br-md px-4 py-2.5 shadow-sm",children:[b.length>0&&p.jsx("div",{className:"flex gap-1.5 mb-2",children:b.map((N,A)=>p.jsx("img",{src:N,alt:`Attachment ${A+1}`,className:"h-24 rounded-lg border border-white/[0.06]"},A))}),p.jsx("div",{className:"text-white/95 text-[15.5px] leading-relaxed whitespace-pre-wrap break-words",children:y})]})]})}):p.jsxs("div",{className:"flex gap-3",onMouseEnter:()=>c(!0),onMouseLeave:()=>c(!1),children:[p.jsx("div",{className:"w-8 h-8 rounded-lg bg-gradient-to-br from-white/95 to-white/75 flex items-center justify-center shrink-0 mt-0.5 shadow-sm",children:p.jsx("svg",{viewBox:"0 0 32 32",width:"14",height:"14",fill:"none",children:p.jsxs("g",{fill:"#0a0a0c",children:[p.jsx("rect",{x:"6.2",y:"7.5",width:"1.6",height:"18",rx:"0.8",transform:"rotate(-42 6.2 7.5)"}),p.jsx("rect",{x:"24.2",y:"6.3",width:"1.6",height:"18",rx:"0.8",transform:"rotate(42 24.2 6.3)"}),p.jsx("polygon",{points:"7.2,8.8 3.8,7.2 4.5,5.5 8.5,7.2"}),p.jsx("polygon",{points:"24.8,8.8 28.2,7.2 27.5,5.5 23.5,7.2"}),p.jsx("rect",{x:"14.3",y:"14.3",width:"3.4",height:"3.4",rx:"0.4",transform:"rotate(45 16 16)"}),p.jsx("circle",{cx:"9.2",cy:"24.5",r:"4"}),p.jsx("circle",{cx:"9.2",cy:"24.5",r:"1.7",fill:"#fff"}),p.jsx("circle",{cx:"22.8",cy:"24.5",r:"4"}),p.jsx("circle",{cx:"22.8",cy:"24.5",r:"1.7",fill:"#fff"}),p.jsx("path",{d:"M16 5.5L12.5 8.5V12.5L16 10.5L19.5 12.5V8.5Z"})]})})}),p.jsxs("div",{className:"min-w-0 flex-1",children:[n.reasoning_content&&p.jsxs("div",{className:"mb-2",children:[p.jsxs("button",{onClick:()=>m(!g),className:"flex items-center gap-1.5 text-[14px] text-white/20 hover:text-white/35 transition-colors mb-1",children:[p.jsx("svg",{width:"10",height:"10",viewBox:"0 0 10 10",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",className:Te("transition-transform duration-150",g&&"rotate-90"),children:p.jsx("path",{d:"M3.5 2L6.5 5L3.5 8"})}),"Thinking"]}),g&&p.jsx("div",{className:"bg-white/[0.02] border border-white/[0.04] rounded-lg px-3 py-2 text-[15px] text-white/30 leading-relaxed whitespace-pre-wrap animate-fade-in max-h-60 overflow-y-auto",children:n.reasoning_content})]}),y?p.jsx("div",{className:"prose-venice text-[15.5px] leading-relaxed text-white/85",children:p.jsx(jk,{remarkPlugins:[BE],urlTransform:qE,components:{code:QE,a:({href:N,childre
Line truncated
|
||||
|
||||
${n.stack}`:""]})]}),p.jsxs("div",{className:"flex gap-2 justify-center",children:[p.jsx("button",{onClick:l,className:"px-4 py-2 text-[14px] font-medium bg-white text-black rounded-md hover:bg-white/90 transition-colors",children:"Try again"}),p.jsx("button",{onClick:()=>window.location.reload(),className:"px-4 py-2 text-[14px] font-medium border border-white/[0.1] text-white/60 hover:text-white/80 hover:border-white/[0.2] rounded-md transition-colors",children:"Reload page"})]})]})})}const E5={info:"border-white/[0.08] bg-[#111]",success:"border-green-500/30 bg-green-500/[0.06]",error:"border-red-500/30 bg-red-500/[0.06]"},j5={info:"text-white/75",success:"text-green-200/85",error:"text-red-200/85"};function T5(){const n=Ql(a=>a.toasts),l=Ql(a=>a.dismiss);return n.length===0?null:p.jsx("div",{className:"fixed bottom-4 right-4 z-[100] flex flex-col gap-2 max-w-sm pointer-events-none",role:"region","aria-live":"polite","aria-label":"Notifications",children:n.map(a=>p.jsx("div",{role:a.variant==="error"?"alert":"status",className:Te("pointer-events-auto rounded-lg border px-3.5 py-2.5 shadow-xl shadow-black/60 backdrop-blur-md animate-scale-in",E5[a.variant]),children:p.jsxs("div",{className:"flex items-start gap-3",children:[p.jsxs("div",{className:"flex-1 min-w-0",children:[p.jsx("div",{className:Te("text-[13.5px] font-medium",j5[a.variant]),children:a.title}),a.description&&p.jsx("div",{className:"text-[12.5px] text-white/45 mt-0.5 leading-relaxed break-words",children:a.description}),a.action&&p.jsx("button",{onClick:()=>{a.action?.onClick(),l(a.id)},className:"mt-1.5 text-[12.5px] font-medium text-white/70 hover:text-white underline underline-offset-2",children:a.action.label})]}),p.jsx("button",{onClick:()=>l(a.id),"aria-label":"Dismiss notification",className:"text-white/25 hover:text-white/60 transition-colors p-0.5 -m-0.5 shrink-0 rounded focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/30",children:p.jsxs("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",children:[p.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),p.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]})},a.id))})}const A5=q.lazy(()=>ay(()=>import("./workflows-view-7tPx_mLe.js"),__vite__mapDeps([0,1,2])).then(n=>({default:n.WorkflowsView})));function _5(){return p.jsx(q.Suspense,{fallback:p.jsx("div",{className:"flex items-center justify-center h-full text-[12px] text-white/30",children:"Loading workflows…"}),children:p.jsx(A5,{})})}const M5=q.lazy(()=>ay(()=>import("./playground-view-urTIiexa.js"),__vite__mapDeps([3,1,2])).then(n=>({default:n.PlaygroundView})));function N5(){return p.jsx(q.Suspense,{fallback:p.jsx("div",{className:"flex items-center justify-center h-full text-[12px] text-white/30",children:"Loading playground…"}),children:p.jsx(M5,{})})}const O5={chat:XE,image:l5,audio:c5,music:m5,video:v5,embeddings:k5,workflows:_5,playground:N5},Ig=["chat","image","audio","music","video","embeddings","workflows","playground"];function z5(){const n=gn(g=>g.hasEncrypted&&!g.apiKey),[l,a]=q.useState(n),[u,s]=q.useState(!1),c=on(g=>g.activeTab),f=on(g=>g.setActiveTab),d=O5[c];return q.useEffect(()=>{const g=m=>{if(!(m.metaKey||m.ctrlKey))return;if(m.key==="n"){m.preventDefault(),f("chat"),s(!1),xt.getState().setActiveConversation(null);return}const y=parseInt(m.key,10);if(y>=1&&y<=Ig.length){m.preventDefault(),f(Ig[y-1]),s(!1);return}};return window.addEventListener("keydown",g),()=>window.removeEventListener("keydown",g)},[f]),p.jsxs("div",{className:"flex h-[100dvh] w-screen overflow-hidden",children:[u&&p.jsx("button",{"aria-label":"Close menu",className:"md:hidden fixed inset-0 z-30 bg-black/60 backdrop-blur-sm animate-fade-in",onClick:()=>s(!1)}),p.jsx(Kv,{mobileOpen:u,onMobileClose:()=>s(!1)}),p.jsxs("div",{className:"flex flex-col flex-1 min-w-0",children:[p.jsx(fb,{onOpenApiKey:()=>a(!0),onOpenMobileSidebar:()=>s(!0)}),p.jsx("main",{className:"flex-1 min-h-0 overflow-hidden",children:p.jsx(gx,{children:p.jsx(d,{})},c)})]}),p.jsx(db,{open:l,onClose:()=>a(!1)}),p.jsx(T5,{})]})}const D5=new cv({defaultOptions:{queries:{retry:1,refetchOnWindowFocus:!1}}});window.addEventListener("unhandledrejection",n=>{console.error("[unhandledrejection]",n.reason)});q1.createRoot(document.getElementById("root")).render(p.jsx(q.StrictMode,{children:p.jsx(gx,{children:p.jsx(fv,{client:D5,children:p.jsx(z5,{})})})}));export{on as A,af as B,lf as C,U1 as D,qu as E,oy as F,nv as Q,vu as R,Gn as S,Fl as a,Kg as b,Te as c,ny as d,hv as e,pv as f,Nv as g,vv as h,gv as i,p as j,yv as k,Et as l,wv as m,ct as n,Xp as o,xv as p,Bu as q,q as r,Mu as s,Fi as t,Si as u,ry as v,ff as w,uy as x,Ji as y,gn as z};
|
||||
${n.stack}`:""]})]}),p.jsxs("div",{className:"flex gap-2 justify-center",children:[p.jsx("button",{onClick:l,className:"px-4 py-2 text-[14px] font-medium bg-white text-black rounded-md hover:bg-white/90 transition-colors",children:"Try again"}),p.jsx("button",{onClick:()=>window.location.reload(),className:"px-4 py-2 text-[14px] font-medium border border-white/[0.1] text-white/60 hover:text-white/80 hover:border-white/[0.2] rounded-md transition-colors",children:"Reload page"})]})]})})}const E5={info:"border-white/[0.08] bg-[#111]",success:"border-green-500/30 bg-green-500/[0.06]",error:"border-red-500/30 bg-red-500/[0.06]"},j5={info:"text-white/75",success:"text-green-200/85",error:"text-red-200/85"};function T5(){const n=Ql(a=>a.toasts),l=Ql(a=>a.dismiss);return n.length===0?null:p.jsx("div",{className:"fixed bottom-4 right-4 z-[100] flex flex-col gap-2 max-w-sm pointer-events-none",role:"region","aria-live":"polite","aria-label":"Notifications",children:n.map(a=>p.jsx("div",{role:a.variant==="error"?"alert":"status",className:Te("pointer-events-auto rounded-lg border px-3.5 py-2.5 shadow-xl shadow-black/60 backdrop-blur-md animate-scale-in",E5[a.variant]),children:p.jsxs("div",{className:"flex items-start gap-3",children:[p.jsxs("div",{className:"flex-1 min-w-0",children:[p.jsx("div",{className:Te("text-[13.5px] font-medium",j5[a.variant]),children:a.title}),a.description&&p.jsx("div",{className:"text-[12.5px] text-white/45 mt-0.5 leading-relaxed break-words",children:a.description}),a.action&&p.jsx("button",{onClick:()=>{a.action?.onClick(),l(a.id)},className:"mt-1.5 text-[12.5px] font-medium text-white/70 hover:text-white underline underline-offset-2",children:a.action.label})]}),p.jsx("button",{onClick:()=>l(a.id),"aria-label":"Dismiss notification",className:"text-white/25 hover:text-white/60 transition-colors p-0.5 -m-0.5 shrink-0 rounded focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/30",children:p.jsxs("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",children:[p.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),p.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]})},a.id))})}const A5=q.lazy(()=>ay(()=>import("./workflows-view-C9DNDVch.js"),__vite__mapDeps([0,1,2])).then(n=>({default:n.WorkflowsView})));function _5(){return p.jsx(q.Suspense,{fallback:p.jsx("div",{className:"flex items-center justify-center h-full text-[12px] text-white/30",children:"Loading workflows…"}),children:p.jsx(A5,{})})}const M5=q.lazy(()=>ay(()=>import("./playground-view-B6WRI_IZ.js"),__vite__mapDeps([3,1,2])).then(n=>({default:n.PlaygroundView})));function N5(){return p.jsx(q.Suspense,{fallback:p.jsx("div",{className:"flex items-center justify-center h-full text-[12px] text-white/30",children:"Loading playground…"}),children:p.jsx(M5,{})})}const O5={chat:XE,image:l5,audio:c5,music:m5,video:v5,embeddings:k5,workflows:_5,playground:N5},Ig=["chat","image","audio","music","video","embeddings","workflows","playground"];function z5(){const n=gn(g=>g.hasEncrypted&&!g.apiKey),[l,a]=q.useState(n),[u,s]=q.useState(!1),c=on(g=>g.activeTab),f=on(g=>g.setActiveTab),d=O5[c];return q.useEffect(()=>{const g=m=>{if(!(m.metaKey||m.ctrlKey))return;if(m.key==="n"){m.preventDefault(),f("chat"),s(!1),xt.getState().setActiveConversation(null);return}const y=parseInt(m.key,10);if(y>=1&&y<=Ig.length){m.preventDefault(),f(Ig[y-1]),s(!1);return}};return window.addEventListener("keydown",g),()=>window.removeEventListener("keydown",g)},[f]),p.jsxs("div",{className:"flex h-[100dvh] w-screen overflow-hidden",children:[u&&p.jsx("button",{"aria-label":"Close menu",className:"md:hidden fixed inset-0 z-30 bg-black/60 backdrop-blur-sm animate-fade-in",onClick:()=>s(!1)}),p.jsx(Kv,{mobileOpen:u,onMobileClose:()=>s(!1)}),p.jsxs("div",{className:"flex flex-col flex-1 min-w-0",children:[p.jsx(fb,{onOpenApiKey:()=>a(!0),onOpenMobileSidebar:()=>s(!0)}),p.jsx("main",{className:"flex-1 min-h-0 overflow-hidden",children:p.jsx(gx,{children:p.jsx(d,{})},c)})]}),p.jsx(db,{open:l,onClose:()=>a(!1)}),p.jsx(T5,{})]})}const D5=new cv({defaultOptions:{queries:{retry:1,refetchOnWindowFocus:!1}}});window.addEventListener("unhandledrejection",n=>{console.error("[unhandledrejection]",n.reason)});q1.createRoot(document.getElementById("root")).render(p.jsx(q.StrictMode,{children:p.jsx(gx,{children:p.jsx(fv,{client:D5,children:p.jsx(z5,{})})})}));export{on as A,af as B,lf as C,U1 as D,qu as E,oy as F,nv as Q,vu as R,Gn as S,Fl as a,Kg as b,Te as c,ny as d,hv as e,pv as f,Nv as g,vv as h,gv as i,p as j,yv as k,Et as l,wv as m,ct as n,Xp as o,xv as p,Bu as q,q as r,Mu as s,Fi as t,Si as u,ry as v,ff as w,uy as x,Ji as y,gn as z};
|
||||
@@ -1 +0,0 @@
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-outline-style:solid;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:"Inter", "InterVariable", system-ui, sans-serif;--font-mono:"JetBrains Mono", "SF Mono", monospace;--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-violet-300:oklch(81.1% .111 293.571);--color-violet-400:oklch(70.2% .183 293.541);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-pink-300:oklch(82.3% .12 346.018);--color-pink-400:oklch(71.8% .202 349.761);--color-pink-500:oklch(65.6% .241 354.308);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-3xl:48rem;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-snug:1.375;--leading-relaxed:1.625;--leading-loose:2;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg-base:#0a0a0c;--color-bg-raised:#111114;--color-text-primary:rgba(var(--fg), .92);--color-accent:#6ee7d3;--color-accent-soft:#6ee7d329}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:Line truncated
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import{a as ne,n as D,s as re,b as oe,Q as Z,d as ie,e as ae,f as le,r as b,h as ce,i as de,k as ue,l as pe,m as B,o as he,p as me,q as fe,v as ge,w as xe,x as we,y as F,u as be,g as U,z as ye,A as V,j as r,c as R,t as Q}from"./index-CmWoX_gW.js";import{g as ve,N as O,h as ke,H as J,P as z,R as je,b as Ne,c as Se,a as _e,i as Re,B as $e,e as Te,C as Ce,u as L,v as Oe,f as Ee}from"./workflow-engine-lSJFuHzZ.js";function H(e,t){const n=new Set(t);return e.filter(s=>!n.has(s))}function Pe(e,t,n){const s=e.slice(0);return s[t]=n,s}var Me=class extends ne{#n;#e;#r;#o;#t;#s;#i;#a;#l;#c=[];constructor(e,t,n){super(),this.#n=e,this.#o=n,this.#r=[],this.#t=[],this.#e=[],this.setQueries(t)}onSubscribe(){this.listeners.size===1&&this.#t.forEach(e=>{e.subscribe(t=>{this.#h(e,t)})})}onUnsubscribe(){this.listeners.size||this.destroy()}destroy(){this.listeners=new Set,this.#t.forEach(e=>{e.destroy()})}setQueries(e,t){this.#r=e,this.#o=t,D.batch(()=>{const n=this.#t,s=this.#p(this.#r);s.forEach(p=>p.observer.setOptions(p.defaultedQueryOptions));const o=s.map(p=>p.observer),i=o.map(p=>p.getCurrentResult()),a=n.length!==o.length,l=o.some((p,h)=>p!==n[h]),d=a||l,x=d?!0:i.some((p,h)=>{const w=this.#e[h];return!w||!re(p,w)});!d&&!x||(d&&(this.#c=s,this.#t=o),this.#e=i,this.hasListeners()&&(d&&(H(n,o).forEach(p=>{p.destroy()}),H(o,n).forEach(p=>{p.subscribe(h=>{this.#h(p,h)})})),this.#m()))})}getCurrentResult(){return this.#e}getQueries(){return this.#t.map(e=>e.getCurrentQuery())}getObservers(){return this.#t}getOptimisticResult(e,t){const n=this.#p(e),s=n.map(i=>i.observer.getOptimisticResult(i.defaultedQueryOptions)),o=n.map(i=>i.defaultedQueryOptions.queryHash);return[s,i=>this.#u(i??s,t,o),()=>this.#d(s,n)]}#d(e,t){return t.map((n,s)=>{const o=e[s];return n.defaultedQueryOptions.notifyOnChangeProps?o:n.observer.trackResult(o,i=>{t.forEach(a=>{a.observer.trackProp(i)})})})}#u(e,t,n){if(t){const s=this.#l,o=n!==void 0&&s!==void 0&&(s.length!==n.length||n.some((i,a)=>i!==s[a]));return(!this.#s||this.#e!==this.#a||o||t!==this.#i)&&(this.#i=t,this.#a=this.#e,n!==void 0&&(this.#l=n),this.#s=oe(this.#s,t(e))),this.#s}return e}#p(e){const t=new Map;this.#t.forEach(s=>{const o=s.options.queryHash;if(!o)return;const i=t.get(o);i?i.push(s):t.set(o,[s])});const n=[];return e.forEach(s=>{const o=this.#n.defaultQueryOptions(s),a=t.get(o.queryHash)?.shift()??new Z(this.#n,o);n.push({defaultedQueryOptions:o,observer:a})}),n}#h(e,t){const n=this.#t.indexOf(e);n!==-1&&(this.#e=Pe(this.#e,n,t),this.#m())}#m(){if(this.hasListeners()){const e=this.#s,t=this.#d(this.#e,this.#c),n=this.#u(t,this.#o?.combine);e!==n&&D.batch(()=>{this.listeners.forEach(s=>{s(this.#e)})})}}};function Ae({queries:e,...t},n){const s=ie(),o=ae(),i=le(),a=b.useMemo(()=>e.map(m=>{const u=s.defaultQueryOptions(m);return u._optimisticResults=o?"isRestoring":"optimistic",u}),[e,s,o]);a.forEach(m=>{ce(m);const u=s.getQueryCache().get(m.queryHash);de(m,i,u)}),ue(i);const[l]=b.useState(()=>new Me(s,a,t)),[d,x,p]=l.getOptimisticResult(a,t.combine),h=!o&&t.subscribed!==!1;b.useSyncExternalStore(b.useCallback(m=>h?l.subscribe(D.batchCalls(m)):pe,[l,h]),()=>l.getCurrentResult(),()=>l.getCurrentResult()),b.useEffect(()=>{l.setQueries(a,t)},[a,t,l]);const y=d.some((m,u)=>B(a[u],m))?d.flatMap((m,u)=>{const g=a[u];if(g&&B(g,m)){const N=new Z(s,g);return he(g,N,i)}return[]}):[];if(y.length>0)throw Promise.all(y);const $=d.find((m,u)=>{const g=a[u];return g&&me({result:m,errorResetBoundary:i,throwOnError:g.throwOnError,query:s.getQueryCache().get(g.queryHash),suspense:g.suspense})});if($?.error)throw $.error;return x(p())}const S=fe()(ge((e,t)=>({messages:[],draft:{nodes:[],edges:[]},linkedWorkflowId:null,isThinking:!1,runResults:{},isRunning:!1,addMessage:n=>e(s=>({messages:[...s.messages,n]})),updateMessage:(n,s)=>e(o=>({messages:o.messages.map(i=>i.id===n?{...i,...s}:i)})),setThinking:n=>e({isThinking:n}),applyAgentPatches:n=>{const{nodes:s,edges:o}=t().draft,i=ve({nodes:s,edges:o},n);return e({draft:{nodes:i.nodes,edges:i.edges},runResults:{}}),i},resetDraft:()=>e({draft:{nodes:[],edges:[]},runResults:{},linkedWorkflowId:null}),clearConversation:()=>e({messages:[],draft:{nodes:[],edges:[]},runResults:{},linkedWorkflowId:null}),setRunResults:n=>e({runResults:n}),updateRunNode:(n,s)=>e(o=>({runResults:{...o.runResults,[n]:{...o.runResults[n],...s}}})),setIsRunning:n=>e({isRunning:n}),clearResults:()=>e({runResults:{}}),loadWorkflow:(n,s,o)=>e({draft:{nodes:s,edges:o},linkedWorkflowId:n,runResults:{},messages:[]}),unlinkWorkflow:()=>e({linkedWorkflowId:null})}),{name:"venice-playground",version:1,storage:xe(()=>we()),partialize:e=>({messages:e.messages.slice(-40),draft:e.draft,linkedWorkflowId:e.linkedWorkflowId})})),We=["text","image","tts","music","video"];function q(e){return e?e.data.filter(t=>!t.model_spec?.offline).map(t=>t.id).sort():[]}function qe(){const e=Ae({queries:We.map(s=>({queryKey:["models",s],queryFn:()=>F(`/models?type=${s}`,{noAuth:!0}),staleTime:3e5}))}),t={text:qLine truncated
|
||||
import{a as ne,n as D,s as re,b as oe,Q as Z,d as ie,e as ae,f as le,r as b,h as ce,i as de,k as ue,l as pe,m as B,o as he,p as me,q as fe,v as ge,w as xe,x as we,y as F,u as be,g as U,z as ye,A as V,j as r,c as R,t as Q}from"./index-fmJ2VqHZ.js";import{g as ve,N as O,h as ke,H as J,P as z,R as je,b as Ne,c as Se,a as _e,i as Re,B as $e,e as Te,C as Ce,u as L,v as Oe,f as Ee}from"./workflow-engine-BuXGtBGn.js";function H(e,t){const n=new Set(t);return e.filter(s=>!n.has(s))}function Pe(e,t,n){const s=e.slice(0);return s[t]=n,s}var Me=class extends ne{#n;#e;#r;#o;#t;#s;#i;#a;#l;#c=[];constructor(e,t,n){super(),this.#n=e,this.#o=n,this.#r=[],this.#t=[],this.#e=[],this.setQueries(t)}onSubscribe(){this.listeners.size===1&&this.#t.forEach(e=>{e.subscribe(t=>{this.#h(e,t)})})}onUnsubscribe(){this.listeners.size||this.destroy()}destroy(){this.listeners=new Set,this.#t.forEach(e=>{e.destroy()})}setQueries(e,t){this.#r=e,this.#o=t,D.batch(()=>{const n=this.#t,s=this.#p(this.#r);s.forEach(p=>p.observer.setOptions(p.defaultedQueryOptions));const o=s.map(p=>p.observer),i=o.map(p=>p.getCurrentResult()),a=n.length!==o.length,l=o.some((p,h)=>p!==n[h]),d=a||l,x=d?!0:i.some((p,h)=>{const w=this.#e[h];return!w||!re(p,w)});!d&&!x||(d&&(this.#c=s,this.#t=o),this.#e=i,this.hasListeners()&&(d&&(H(n,o).forEach(p=>{p.destroy()}),H(o,n).forEach(p=>{p.subscribe(h=>{this.#h(p,h)})})),this.#m()))})}getCurrentResult(){return this.#e}getQueries(){return this.#t.map(e=>e.getCurrentQuery())}getObservers(){return this.#t}getOptimisticResult(e,t){const n=this.#p(e),s=n.map(i=>i.observer.getOptimisticResult(i.defaultedQueryOptions)),o=n.map(i=>i.defaultedQueryOptions.queryHash);return[s,i=>this.#u(i??s,t,o),()=>this.#d(s,n)]}#d(e,t){return t.map((n,s)=>{const o=e[s];return n.defaultedQueryOptions.notifyOnChangeProps?o:n.observer.trackResult(o,i=>{t.forEach(a=>{a.observer.trackProp(i)})})})}#u(e,t,n){if(t){const s=this.#l,o=n!==void 0&&s!==void 0&&(s.length!==n.length||n.some((i,a)=>i!==s[a]));return(!this.#s||this.#e!==this.#a||o||t!==this.#i)&&(this.#i=t,this.#a=this.#e,n!==void 0&&(this.#l=n),this.#s=oe(this.#s,t(e))),this.#s}return e}#p(e){const t=new Map;this.#t.forEach(s=>{const o=s.options.queryHash;if(!o)return;const i=t.get(o);i?i.push(s):t.set(o,[s])});const n=[];return e.forEach(s=>{const o=this.#n.defaultQueryOptions(s),a=t.get(o.queryHash)?.shift()??new Z(this.#n,o);n.push({defaultedQueryOptions:o,observer:a})}),n}#h(e,t){const n=this.#t.indexOf(e);n!==-1&&(this.#e=Pe(this.#e,n,t),this.#m())}#m(){if(this.hasListeners()){const e=this.#s,t=this.#d(this.#e,this.#c),n=this.#u(t,this.#o?.combine);e!==n&&D.batch(()=>{this.listeners.forEach(s=>{s(this.#e)})})}}};function Ae({queries:e,...t},n){const s=ie(),o=ae(),i=le(),a=b.useMemo(()=>e.map(m=>{const u=s.defaultQueryOptions(m);return u._optimisticResults=o?"isRestoring":"optimistic",u}),[e,s,o]);a.forEach(m=>{ce(m);const u=s.getQueryCache().get(m.queryHash);de(m,i,u)}),ue(i);const[l]=b.useState(()=>new Me(s,a,t)),[d,x,p]=l.getOptimisticResult(a,t.combine),h=!o&&t.subscribed!==!1;b.useSyncExternalStore(b.useCallback(m=>h?l.subscribe(D.batchCalls(m)):pe,[l,h]),()=>l.getCurrentResult(),()=>l.getCurrentResult()),b.useEffect(()=>{l.setQueries(a,t)},[a,t,l]);const y=d.some((m,u)=>B(a[u],m))?d.flatMap((m,u)=>{const g=a[u];if(g&&B(g,m)){const N=new Z(s,g);return he(g,N,i)}return[]}):[];if(y.length>0)throw Promise.all(y);const $=d.find((m,u)=>{const g=a[u];return g&&me({result:m,errorResetBoundary:i,throwOnError:g.throwOnError,query:s.getQueryCache().get(g.queryHash),suspense:g.suspense})});if($?.error)throw $.error;return x(p())}const S=fe()(ge((e,t)=>({messages:[],draft:{nodes:[],edges:[]},linkedWorkflowId:null,isThinking:!1,runResults:{},isRunning:!1,addMessage:n=>e(s=>({messages:[...s.messages,n]})),updateMessage:(n,s)=>e(o=>({messages:o.messages.map(i=>i.id===n?{...i,...s}:i)})),setThinking:n=>e({isThinking:n}),applyAgentPatches:n=>{const{nodes:s,edges:o}=t().draft,i=ve({nodes:s,edges:o},n);return e({draft:{nodes:i.nodes,edges:i.edges},runResults:{}}),i},resetDraft:()=>e({draft:{nodes:[],edges:[]},runResults:{},linkedWorkflowId:null}),clearConversation:()=>e({messages:[],draft:{nodes:[],edges:[]},runResults:{},linkedWorkflowId:null}),setRunResults:n=>e({runResults:n}),updateRunNode:(n,s)=>e(o=>({runResults:{...o.runResults,[n]:{...o.runResults[n],...s}}})),setIsRunning:n=>e({isRunning:n}),clearResults:()=>e({runResults:{}}),loadWorkflow:(n,s,o)=>e({draft:{nodes:s,edges:o},linkedWorkflowId:n,runResults:{},messages:[]}),unlinkWorkflow:()=>e({linkedWorkflowId:null})}),{name:"venice-playground",version:1,storage:xe(()=>we()),partialize:e=>({messages:e.messages.slice(-40),draft:e.draft,linkedWorkflowId:e.linkedWorkflowId})})),We=["text","image","tts","music","video"];function q(e){return e?e.data.filter(t=>!t.model_spec?.offline).map(t=>t.id).sort():[]}function qe(){const e=Ae({queries:We.map(s=>({queryKey:["models",s],queryFn:()=>F(`/models?type=${s}`,{noAuth:!0}),staleTime:3e5}))}),t={text:qLine truncated
|
||||
`);return`- ${e.type} (${e.label}) | input=${e.input} output=${e.output}
|
||||
${e.description}${t?`
|
||||
params:
|
||||
@@ -80,7 +80,7 @@ ${t}
|
||||
Edges:
|
||||
${n}`}function st(e,t,n){const s=n.catalog;return s?e==="chat"?s.text.includes(t):e==="imageGen"?s.image.includes(t):e==="tts"?s.tts.includes(t):e==="music"?s.music.includes(t):e==="video"?s.video.includes(t):!0:!0}function nt(e,t,n){const s=n.catalog;if(!s)return;if(e==="chat"){const i=n.agentModels??[];if(i.length===0)return s.text[0];const a=i.map(l=>{let d=0;const x=l.capabilities;return t==="web"&&(d+=x.supportsWebSearch?100:0),t==="reasoning"&&(d+=x.supportsReasoning?100:0),t==="uncensored"&&(d+=l.uncensored?100:0),t==="best"&&(l.traits.includes("most_intelligent")&&(d+=80),l.contextTokens&&l.contextTokens>=2e5&&(d+=20)),(t==="fast"||!t)&&(x.supportsReasoning&&(d-=30),l.traits.includes("function_calling_default")&&(d+=50),l.recommended&&(d+=30)),x.supportsResponseSchema&&(d+=10),d-=l.tier*5,{id:l.id,score:d}});return a.sort((l,d)=>d.score-l.score),a[0]?.id}return(e==="imageGen"?s.image:e==="tts"?s.tts:e==="music"?s.music:e==="video"?s.video:[])[0]}function rt(e){if(!e)return{};try{const t=JSON.parse(e);return typeof t=="object"&&t!==null?t:{}}catch{return{}}}function ot(e,t,n){try{switch(e){case"clear":return n.applyPatch({op:"clear"}),{ok:!0};case"add_node":{const s=t.node_type;if(!s||!(s in O))return{error:`Unknown node_type: ${String(s)}`};const o=typeof t.id=="string"&&t.id||U(),i=typeof t.params=="object"&&t.params!==null?t.params:{},a=i.model;if(typeof a=="string"&&a.length>0&&!st(s,a,n))return{error:`Unknown model '${a}' for ${s}. Call pick_model({node_type:'${s==="chat"?"chat":s}'}) to get a valid id, then retry add_node with that id.`};const l=O[s],d=new Set(["model","prompt",...l.params.map(h=>h.name)]),x={};for(const[h,w]of Object.entries(i))d.has(h)&&(x[h]=w);for(const h of l.params)if(h.enumValues&&x[h.name]!==void 0){const w=x[h.name];if(typeof w=="boolean"){x[h.name]=w?h.enumValues.includes("on")?"on":h.enumValues[0]:h.enumValues.includes("off")?"off":"";continue}if(typeof w!="string"||!h.enumValues.includes(w))return{error:`Invalid value for ${s}.${h.name}: ${JSON.stringify(w)}. Must be one of [${h.enumValues.filter(Boolean).join(", ")}].`}}const p=n.applyPatch({op:"add_node",nodeType:s,id:o,params:x});return"error"in p?{error:p.error}:{ok:!0,id:p.id??o}}case"connect":{const s=String(t.source??""),o=String(t.target??"");if(!s||!o)return{error:"Both source and target are required."};const i=n.applyPatch({op:"connect",source:s,target:o});return"error"in i?{error:i.error}:{ok:!0,edge_id:i.edge_id}}case"set_params":{const s=String(t.id??""),o=typeof t.params=="object"&&t.params!==null?t.params:{},i=n.applyPatch({op:"set_params",id:s,params:o});return"error"in i?{error:i.error}:{ok:!0}}case"remove_node":{const s=String(t.id??""),o=n.applyPatch({op:"remove_node",id:s});return"error"in o?{error:o.error}:{ok:!0}}case"pick_model":{const s=String(t.node_type??""),o=typeof t.prefer=="string"?t.prefer:void 0,i=nt(s,o,n);return i?{ok:!0,model:i}:{error:`No model available for ${s}`}}case"ask_user":return{ok:!0,message:String(t.question??"")};case"done":return{ok:!0,message:String(t.summary??"Done.")}}return{error:`Unknown tool: ${e}`}}catch(s){return{error:s instanceof Error?s.message:"Tool failed"}}}async function it(e){const t=[{role:"system",content:et},...e.history.map(o=>o.role==="user"?{role:"user",content:o.content}:{role:"assistant",content:o.content}),{role:"user",content:`${tt(e.draft)}
|
||||
|
||||
User request: ${e.userMessage}`}];let n=0,s=!1;for(let o=0;o<K;o++){if(e.signal?.aborted)throw new DOMException("Aborted","AbortError");const a=(await F("/chat/completions",{method:"POST",body:JSON.stringify({model:e.model,messages:t,temperature:.2,max_tokens:4096,tools:Xe,tool_choice:"auto"}),signal:e.signal})).choices[0]?.message;if(!a)return{say:"Empty response from agent.",tool_calls:n,asked_user:!1};t.push({role:"assistant",content:a.content??null,tool_calls:a.tool_calls});const l=a.tool_calls??[];if(l.length===0)return{say:(a.content||"").trim()||`Made ${n} edit${n===1?"":"s"}.`,tool_calls:n,asked_user:!1};let d=null,x=!1;for(const p of l){const h=p.function?.name??"",w=rt(p.function?.arguments),y=ot(h,w,e);n++,e.onStep?.({tool:h,args:w,result:y}),t.push({role:"tool",tool_call_id:p.id,content:JSON.stringify(y)}),h==="done"?d=y.message??"Done.":h==="ask_user"&&(d=y.message??"Need clarification.",x=!0)}if(d!==null)return s=x,{say:d,tool_calls:n,asked_user:s}}return{say:`Stopped after ${K} iterations and ${n} edits — let me know if it needs adjusting.`,tool_calls:n,asked_user:!1}}const at=["Build a workflow that makes a 9:16 reel using web search to research the topic, then writes a script and generates the video","Research a topic with web search, summarize into 5 bullets, and narrate it as audio","Turn a concept into a vivid image prompt, then generate the image","Write a short song with lyrics, then generate it as music","Make a cover image and a music track from one idea, then combine them into one video where the cover shows while the track plays"];function lt(e){const t=!("error"in e.result)||!e.result.error,n=e.args;switch(e.tool){case"clear":return{tool:e.tool,summary:"Cleared canvas",ok:t};case"add_node":{const s=e.result.id,o=String(n.node_type??"?");return{tool:e.tool,summary:t?`Added ${o}${s?` "${s}"`:""}`:`Failed to add ${o}: ${e.result.error}`,ok:t}}case"connect":{const s=String(n.source??""),o=String(n.target??"");return{tool:e.tool,summary:t?`Connected ${s} → ${o}`:`Connect failed (${s} → ${o}): ${e.result.error}`,ok:t}}case"set_params":return{tool:e.tool,summary:t?`Updated params on ${String(n.id??"")}`:`set_params failed: ${e.result.error}`,ok:t};case"remove_node":return{tool:e.tool,summary:t?`Removed ${String(n.id??"")}`:`remove failed: ${e.result.error}`,ok:t};case"pick_model":{const s=e.result.model;return{tool:e.tool,summary:t?`Picked ${s} for ${String(n.node_type??"")}`:"pick_model failed",ok:t}}case"ask_user":return{tool:e.tool,summary:"Awaiting your reply",ok:t};case"done":return{tool:e.tool,summary:"Finished",ok:t};default:return{tool:e.tool,summary:e.tool,ok:t}}}function ct(){const{messages:e,draft:t,isThinking:n,addMessage:s,updateMessage:o,setThinking:i,applyAgentPatches:a}=S(),l=ye(f=>f.apiKey!==null),d=V(f=>f.playgroundAgentModel)||te,{catalog:x}=qe(),{models:p}=ee(),h=p.find(f=>f.id===d)?.capabilities,[w,y]=b.useState(""),[$,m]=b.useState(null),u=b.useRef(null),g=b.useRef(null);b.useEffect(()=>{u.current?.scrollTo({top:u.current.scrollHeight,behavior:"smooth"})},[e,n]);const N=async f=>{const C=f.trim();if(!C||n)return;if(!l){m("Connect your Venice API key first.");return}m(null),y("");const P={id:U(),role:"user",content:C},E={id:U(),role:"assistant",content:"",pending:!0,activity:[]};s(P),s(E),i(!0),g.current?.abort();const A=new AbortController;g.current=A;const I=e.filter(T=>!T.pending&&!T.error).slice(-10).map(T=>({role:T.role,content:T.content}));try{if(h?.supportsFunctionCalling===!0){const k=[],W=await it({userMessage:C,draft:t,history:I,catalog:x,agentModels:p,model:d,capabilities:h,signal:A.signal,applyPatch:c=>{try{const j=S.getState().draft,v=ke({nodes:j.nodes,edges:j.edges},c);return S.setState({draft:{nodes:v.nodes,edges:v.edges}}),{ok:!0,id:v.addedNodeId,edge_id:v.addedEdgeId}}catch(j){return{error:j instanceof Error?j.message:"Patch failed"}}},onStep:c=>{k.push(lt(c)),o(E.id,{activity:[...k]})}});o(E.id,{content:W.say||"Done.",activity:k,pending:!1})}else{const k=await Ge({userMessage:C,draft:t,history:I,catalog:x,model:d,capabilities:h,signal:A.signal});let W;try{k.patches.length>0&&a(k.patches)}catch(v){W=v instanceof Error?v.message:"Failed to apply patches"}const c=k.invalidPatches>0?` (${k.invalidPatches} invalid patch${k.invalidPatches===1?"":"es"} ignored)`:"",j=k.patches.length===0&&!k.say?"The agent returned an unparseable response. Try a different model from the picker above, or simplify the request.":k.say||(k.patches.length>0?"Updated the workflow.":"");o(E.id,{content:j+c,patches:k.patches,error:W,pending:!1})}}catch(T){if(A.signal.aborted)o(E.id,{content:"",error:"Cancelled",pending:!1});else{const k=T instanceof Error?T.message:"Agent request failed";o(E.id,{content:"",error:k,pending:!1})}}finally{i(!1),g.current=null}},_=()=>{g.current?.abort()};return r.jsxs("div",{className:"flex flex-col h-full bg-[#0c0c10]",children:[r.jsx("div",{ref:u,className:"flex-1 overflow-y-auto px-4 py-4",children:e.length===0?r.jsxs("div",{className:"flex
Line truncated
|
||||
User request: ${e.userMessage}`}];let n=0,s=!1;for(let o=0;o<K;o++){if(e.signal?.aborted)throw new DOMException("Aborted","AbortError");const a=(await F("/chat/completions",{method:"POST",body:JSON.stringify({model:e.model,messages:t,temperature:.2,max_tokens:4096,tools:Xe,tool_choice:"auto"}),signal:e.signal})).choices[0]?.message;if(!a)return{say:"Empty response from agent.",tool_calls:n,asked_user:!1};t.push({role:"assistant",content:a.content??null,tool_calls:a.tool_calls});const l=a.tool_calls??[];if(l.length===0)return{say:(a.content||"").trim()||`Made ${n} edit${n===1?"":"s"}.`,tool_calls:n,asked_user:!1};let d=null,x=!1;for(const p of l){const h=p.function?.name??"",w=rt(p.function?.arguments),y=ot(h,w,e);n++,e.onStep?.({tool:h,args:w,result:y}),t.push({role:"tool",tool_call_id:p.id,content:JSON.stringify(y)}),h==="done"?d=y.message??"Done.":h==="ask_user"&&(d=y.message??"Need clarification.",x=!0)}if(d!==null)return s=x,{say:d,tool_calls:n,asked_user:s}}return{say:`Stopped after ${K} iterations and ${n} edits — let me know if it needs adjusting.`,tool_calls:n,asked_user:!1}}const at=["Build a workflow that makes a 9:16 reel using web search to research the topic, then writes a script and generates the video","Research a topic with web search, summarize into 5 bullets, and narrate it as audio","Turn a concept into a vivid image prompt, then generate the image","Write a short song with lyrics, then generate it as music","Make a cover image and a music track from one idea, then combine them into one video where the cover shows while the track plays"];function lt(e){const t=!("error"in e.result)||!e.result.error,n=e.args;switch(e.tool){case"clear":return{tool:e.tool,summary:"Cleared canvas",ok:t};case"add_node":{const s=e.result.id,o=String(n.node_type??"?");return{tool:e.tool,summary:t?`Added ${o}${s?` "${s}"`:""}`:`Failed to add ${o}: ${e.result.error}`,ok:t}}case"connect":{const s=String(n.source??""),o=String(n.target??"");return{tool:e.tool,summary:t?`Connected ${s} → ${o}`:`Connect failed (${s} → ${o}): ${e.result.error}`,ok:t}}case"set_params":return{tool:e.tool,summary:t?`Updated params on ${String(n.id??"")}`:`set_params failed: ${e.result.error}`,ok:t};case"remove_node":return{tool:e.tool,summary:t?`Removed ${String(n.id??"")}`:`remove failed: ${e.result.error}`,ok:t};case"pick_model":{const s=e.result.model;return{tool:e.tool,summary:t?`Picked ${s} for ${String(n.node_type??"")}`:"pick_model failed",ok:t}}case"ask_user":return{tool:e.tool,summary:"Awaiting your reply",ok:t};case"done":return{tool:e.tool,summary:"Finished",ok:t};default:return{tool:e.tool,summary:e.tool,ok:t}}}function ct(){const{messages:e,draft:t,isThinking:n,addMessage:s,updateMessage:o,setThinking:i,applyAgentPatches:a}=S(),l=ye(f=>f.apiKey!==null),d=V(f=>f.playgroundAgentModel)||te,{catalog:x}=qe(),{models:p}=ee(),h=p.find(f=>f.id===d)?.capabilities,[w,y]=b.useState(""),[$,m]=b.useState(null),u=b.useRef(null),g=b.useRef(null);b.useEffect(()=>{u.current?.scrollTo({top:u.current.scrollHeight,behavior:"smooth"})},[e,n]);const N=async f=>{const C=f.trim();if(!C||n)return;if(!l){m("Connect your Venice API key first.");return}m(null),y("");const P={id:U(),role:"user",content:C},E={id:U(),role:"assistant",content:"",pending:!0,activity:[]};s(P),s(E),i(!0),g.current?.abort();const A=new AbortController;g.current=A;const I=e.filter(T=>!T.pending&&!T.error).slice(-10).map(T=>({role:T.role,content:T.content}));try{if(h?.supportsFunctionCalling===!0){const k=[],W=await it({userMessage:C,draft:t,history:I,catalog:x,agentModels:p,model:d,capabilities:h,signal:A.signal,applyPatch:c=>{try{const j=S.getState().draft,v=ke({nodes:j.nodes,edges:j.edges},c);return S.setState({draft:{nodes:v.nodes,edges:v.edges}}),{ok:!0,id:v.addedNodeId,edge_id:v.addedEdgeId}}catch(j){return{error:j instanceof Error?j.message:"Patch failed"}}},onStep:c=>{k.push(lt(c)),o(E.id,{activity:[...k]})}});o(E.id,{content:W.say||"Done.",activity:k,pending:!1})}else{const k=await Ge({userMessage:C,draft:t,history:I,catalog:x,model:d,capabilities:h,signal:A.signal});let W;try{k.patches.length>0&&a(k.patches)}catch(v){W=v instanceof Error?v.message:"Failed to apply patches"}const c=k.invalidPatches>0?` (${k.invalidPatches} invalid patch${k.invalidPatches===1?"":"es"} ignored)`:"",j=k.patches.length===0&&!k.say?"The agent returned an unparseable response. Try a different model from the picker above, or simplify the request.":k.say||(k.patches.length>0?"Updated the workflow.":"");o(E.id,{content:j+c,patches:k.patches,error:W,pending:!1})}}catch(T){if(A.signal.aborted)o(E.id,{content:"",error:"Cancelled",pending:!1});else{const k=T instanceof Error?T.message:"Agent request failed";o(E.id,{content:"",error:k,pending:!1})}}finally{i(!1),g.current=null}},_=()=>{g.current?.abort()};return r.jsxs("div",{className:"flex flex-col h-full bg-[#0c0c10]",children:[r.jsx("div",{ref:u,className:"flex-1 overflow-y-auto px-4 py-4",children:e.length===0?r.jsxs("div",{className:"flex
Line truncated
|
||||
`)}),N.errors.length===0&&N.warnings.length>0&&r.jsx("span",{className:"w-1.5 h-1.5 rounded-full bg-yellow-400/70 shrink-0",title:N.warnings.map(c=>c.message).join(`
|
||||
`)}),m&&r.jsx("span",{className:"text-[12px] text-green-400/70 truncate",children:m})]}),r.jsxs("div",{className:"flex items-center gap-1.5 shrink-0",children:[r.jsx("button",{onClick:a,disabled:!_||t,className:"text-white/25 hover:text-white/55 transition-colors p-1.5 disabled:opacity-30 disabled:cursor-not-allowed",title:"Reset canvas",children:r.jsx("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:r.jsx("path",{d:"M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"})})}),r.jsx("button",{onClick:C,disabled:!f,className:R("flex items-center gap-1.5 text-[13px] font-medium px-2.5 py-1 rounded-md transition-colors",f?"bg-green-500/20 text-green-300 border border-green-500/30 hover:bg-green-500/30":"bg-white/[0.05] text-white/25 border border-white/[0.06] cursor-not-allowed"),title:N.errors.length>0?N.errors.map(c=>c.message).join(`
|
||||
`):"Run the workflow inline",children:t?r.jsxs(r.Fragment,{children:[r.jsx("span",{className:"w-2.5 h-2.5 border-2 border-green-300/60 border-t-transparent rounded-full animate-spin"}),r.jsx("span",{className:"hidden sm:inline",children:"Running…"})]}):r.jsxs(r.Fragment,{children:[r.jsx("svg",{width:"9",height:"9",viewBox:"0 0 24 24",fill:"currentColor",children:r.jsx("polygon",{points:"5 3 19 12 5 21 5 3"})}),"Run"]})}),r.jsx("button",{onClick:E,disabled:!_||t,className:R("text-[13px] px-2.5 py-1 rounded-md border border-white/[0.08] transition-colors",_&&!t?"text-white/60 hover:border-white/[0.15] hover:text-white/85":"text-white/20 opacity-40 cursor-not-allowed"),title:g?`Update "${g.name}"`:"Save as new workflow",children:g?"Update":"Save"}),g&&r.jsx("button",{onClick:A,disabled:!_||t,className:"text-white/35 hover:text-white/65 transition-colors p-1.5 disabled:opacity-30 disabled:cursor-not-allowed",title:"Save as a new workflow",children:r.jsxs("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[r.jsx("rect",{x:"3",y:"3",width:"14",height:"14",rx:"2"}),r.jsx("path",{d:"M7 21h12a2 2 0 002-2V9"})]})}),r.jsxs("button",{onClick:I,disabled:!_||t,className:R("flex items-center gap-1.5 text-[13px] font-medium px-2.5 py-1 rounded-md transition-colors",_&&!t?"bg-white text-black hover:bg-white/90":"bg-white/[0.05] text-white/30 cursor-not-allowed"),title:"Open in Workflows tab",children:[r.jsx("span",{className:"hidden md:inline",children:"Open in Workflows"}),r.jsx("span",{className:"md:hidden",children:"Open"}),r.jsx("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:r.jsx("path",{d:"M7 17L17 7M7 7h10v10"})})]})]})]}),r.jsx("div",{className:"flex-1 min-h-0",children:r.jsx(ft,{nodes:e.nodes,edges:e.edges})})]})]})}export{yt as PlaygroundView};
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
import{B as Ar,C as Ea,R as _a,D as Sa,r as R,j as $,g as An,q as Na,v as Ca,w as Ma,x as ka,y as Nt,E as Ia,F as Aa}from"./index-CmWoX_gW.js";function ie(e){if(typeof e=="string"||typeof e=="number")return""+e;let t="";if(Array.isArray(e))for(let n=0,o;n<e.length;n++)(o=ie(e[n]))!==""&&(t+=(t&&" ")+o);else for(let n in e)e[n]&&(t+=(t&&" ")+n);return t}var $a={value:()=>{}};function Jt(){for(var e=0,t=arguments.length,n={},o;e<t;++e){if(!(o=arguments[e]+"")||o in n||/[\s.]/.test(o))throw new Error("illegal type: "+o);n[o]=[]}return new zt(n)}function zt(e){this._=e}function Pa(e,t){return e.trim().split(/^|\s+/).map(function(n){var o="",r=n.indexOf(".");if(r>=0&&(o=n.slice(r+1),n=n.slice(0,r)),n&&!t.hasOwnProperty(n))throw new Error("unknown type: "+n);return{type:n,name:o}})}zt.prototype=Jt.prototype={constructor:zt,on:function(e,t){var n=this._,o=Pa(e+"",n),r,i=-1,s=o.length;if(arguments.length<2){for(;++i<s;)if((r=(e=o[i]).type)&&(r=Ta(n[r],e.name)))return r;return}if(t!=null&&typeof t!="function")throw new Error("invalid callback: "+t);for(;++i<s;)if(r=(e=o[i]).type)n[r]=wo(n[r],e.name,t);else if(t==null)for(r in n)n[r]=wo(n[r],e.name,null);return this},copy:function(){var e={},t=this._;for(var n in t)e[n]=t[n].slice();return new zt(e)},call:function(e,t){if((r=arguments.length-2)>0)for(var n=new Array(r),o=0,r,i;o<r;++o)n[o]=arguments[o+2];if(!this._.hasOwnProperty(e))throw new Error("unknown type: "+e);for(i=this._[e],o=0,r=i.length;o<r;++o)i[o].value.apply(t,n)},apply:function(e,t,n){if(!this._.hasOwnProperty(e))throw new Error("unknown type: "+e);for(var o=this._[e],r=0,i=o.length;r<i;++r)o[r].value.apply(t,n)}};function Ta(e,t){for(var n=0,o=e.length,r;n<o;++n)if((r=e[n]).name===t)return r.value}function wo(e,t,n){for(var o=0,r=e.length;o<r;++o)if(e[o].name===t){e[o]=$a,e=e.slice(0,o).concat(e.slice(o+1));break}return n!=null&&e.push({name:t,value:n}),e}var $n="http://www.w3.org/1999/xhtml";const vo={svg:"http://www.w3.org/2000/svg",xhtml:$n,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function en(e){var t=e+="",n=t.indexOf(":");return n>=0&&(t=e.slice(0,n))!=="xmlns"&&(e=e.slice(n+1)),vo.hasOwnProperty(t)?{space:vo[t],local:e}:e}function Da(e){return function(){var t=this.ownerDocument,n=this.namespaceURI;return n===$n&&t.documentElement.namespaceURI===$n?t.createElement(e):t.createElementNS(n,e)}}function Ra(e){return function(){return this.ownerDocument.createElementNS(e.space,e.local)}}function $r(e){var t=en(e);return(t.local?Ra:Da)(t)}function za(){}function Xn(e){return e==null?za:function(){return this.querySelector(e)}}function Ha(e){typeof e!="function"&&(e=Xn(e));for(var t=this._groups,n=t.length,o=new Array(n),r=0;r<n;++r)for(var i=t[r],s=i.length,a=o[r]=new Array(s),c,u,d=0;d<s;++d)(c=i[d])&&(u=e.call(c,c.__data__,d,i))&&("__data__"in c&&(u.__data__=c.__data__),a[d]=u);return new ue(o,this._parents)}function La(e){return e==null?[]:Array.isArray(e)?e:Array.from(e)}function Oa(){return[]}function Pr(e){return e==null?Oa:function(){return this.querySelectorAll(e)}}function Va(e){return function(){return La(e.apply(this,arguments))}}function Ba(e){typeof e=="function"?e=Va(e):e=Pr(e);for(var t=this._groups,n=t.length,o=[],r=[],i=0;i<n;++i)for(var s=t[i],a=s.length,c,u=0;u<a;++u)(c=s[u])&&(o.push(e.call(c,c.__data__,u,s)),r.push(c));return new ue(o,r)}function Tr(e){return function(){return this.matches(e)}}function Dr(e){return function(t){return t.matches(e)}}var Fa=Array.prototype.find;function Wa(e){return function(){return Fa.call(this.children,e)}}function ja(){return this.firstElementChild}function Xa(e){return this.select(e==null?ja:Wa(typeof e=="function"?e:Dr(e)))}var Ya=Array.prototype.filter;function Za(){return Array.from(this.children)}function qa(e){return function(){return Ya.call(this.children,e)}}function Ga(e){return this.selectAll(e==null?Za:qa(typeof e=="function"?e:Dr(e)))}function Ua(e){typeof e!="function"&&(e=Tr(e));for(var t=this._groups,n=t.length,o=new Array(n),r=0;r<n;++r)for(var i=t[r],s=i.length,a=o[r]=[],c,u=0;u<s;++u)(c=i[u])&&e.call(c,c.__data__,u,i)&&a.push(c);return new ue(o,this._parents)}function Rr(e){return new Array(e.length)}function Ka(){return new ue(this._enter||this._groups.map(Rr),this._parents)}function Bt(e,t){this.ownerDocument=e.ownerDocument,this.namespaceURI=e.namespaceURI,this._next=null,this._parent=e,this.__data__=t}Bt.prototype={constructor:Bt,appendChild:function(e){return this._parent.insertBefore(e,this._next)},insertBefore:function(e,t){return this._parent.insertBefore(e,t)},querySelector:function(e){return this._parent.querySelector(e)},querySelectorAll:function(e){return this._parent.querySelectorAll(e)}};function Qa(e){return function(){return e}}function Ja(e,t,n,o,r,i){for(var s=0,a,c=t.length,u=i.length;s<u;++s)(a=t[s])?(a.__data__=i[s],o[s]=a):n[s]=new Bt(e,i[s]);for(;s<c;++s)(a=t[s])&&(r[s]=a)}function ec(e,t,n,o,r,i,s){var a,c,u=new Map,d=t.lengthLine truncated
|
||||
import{B as Ar,C as Ea,R as _a,D as Sa,r as R,j as $,g as An,q as Na,v as Ca,w as Ma,x as ka,y as Nt,E as Ia,F as Aa}from"./index-fmJ2VqHZ.js";function ie(e){if(typeof e=="string"||typeof e=="number")return""+e;let t="";if(Array.isArray(e))for(let n=0,o;n<e.length;n++)(o=ie(e[n]))!==""&&(t+=(t&&" ")+o);else for(let n in e)e[n]&&(t+=(t&&" ")+n);return t}var $a={value:()=>{}};function Jt(){for(var e=0,t=arguments.length,n={},o;e<t;++e){if(!(o=arguments[e]+"")||o in n||/[\s.]/.test(o))throw new Error("illegal type: "+o);n[o]=[]}return new zt(n)}function zt(e){this._=e}function Pa(e,t){return e.trim().split(/^|\s+/).map(function(n){var o="",r=n.indexOf(".");if(r>=0&&(o=n.slice(r+1),n=n.slice(0,r)),n&&!t.hasOwnProperty(n))throw new Error("unknown type: "+n);return{type:n,name:o}})}zt.prototype=Jt.prototype={constructor:zt,on:function(e,t){var n=this._,o=Pa(e+"",n),r,i=-1,s=o.length;if(arguments.length<2){for(;++i<s;)if((r=(e=o[i]).type)&&(r=Ta(n[r],e.name)))return r;return}if(t!=null&&typeof t!="function")throw new Error("invalid callback: "+t);for(;++i<s;)if(r=(e=o[i]).type)n[r]=wo(n[r],e.name,t);else if(t==null)for(r in n)n[r]=wo(n[r],e.name,null);return this},copy:function(){var e={},t=this._;for(var n in t)e[n]=t[n].slice();return new zt(e)},call:function(e,t){if((r=arguments.length-2)>0)for(var n=new Array(r),o=0,r,i;o<r;++o)n[o]=arguments[o+2];if(!this._.hasOwnProperty(e))throw new Error("unknown type: "+e);for(i=this._[e],o=0,r=i.length;o<r;++o)i[o].value.apply(t,n)},apply:function(e,t,n){if(!this._.hasOwnProperty(e))throw new Error("unknown type: "+e);for(var o=this._[e],r=0,i=o.length;r<i;++r)o[r].value.apply(t,n)}};function Ta(e,t){for(var n=0,o=e.length,r;n<o;++n)if((r=e[n]).name===t)return r.value}function wo(e,t,n){for(var o=0,r=e.length;o<r;++o)if(e[o].name===t){e[o]=$a,e=e.slice(0,o).concat(e.slice(o+1));break}return n!=null&&e.push({name:t,value:n}),e}var $n="http://www.w3.org/1999/xhtml";const vo={svg:"http://www.w3.org/2000/svg",xhtml:$n,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function en(e){var t=e+="",n=t.indexOf(":");return n>=0&&(t=e.slice(0,n))!=="xmlns"&&(e=e.slice(n+1)),vo.hasOwnProperty(t)?{space:vo[t],local:e}:e}function Da(e){return function(){var t=this.ownerDocument,n=this.namespaceURI;return n===$n&&t.documentElement.namespaceURI===$n?t.createElement(e):t.createElementNS(n,e)}}function Ra(e){return function(){return this.ownerDocument.createElementNS(e.space,e.local)}}function $r(e){var t=en(e);return(t.local?Ra:Da)(t)}function za(){}function Xn(e){return e==null?za:function(){return this.querySelector(e)}}function Ha(e){typeof e!="function"&&(e=Xn(e));for(var t=this._groups,n=t.length,o=new Array(n),r=0;r<n;++r)for(var i=t[r],s=i.length,a=o[r]=new Array(s),c,u,d=0;d<s;++d)(c=i[d])&&(u=e.call(c,c.__data__,d,i))&&("__data__"in c&&(u.__data__=c.__data__),a[d]=u);return new ue(o,this._parents)}function La(e){return e==null?[]:Array.isArray(e)?e:Array.from(e)}function Oa(){return[]}function Pr(e){return e==null?Oa:function(){return this.querySelectorAll(e)}}function Va(e){return function(){return La(e.apply(this,arguments))}}function Ba(e){typeof e=="function"?e=Va(e):e=Pr(e);for(var t=this._groups,n=t.length,o=[],r=[],i=0;i<n;++i)for(var s=t[i],a=s.length,c,u=0;u<a;++u)(c=s[u])&&(o.push(e.call(c,c.__data__,u,s)),r.push(c));return new ue(o,r)}function Tr(e){return function(){return this.matches(e)}}function Dr(e){return function(t){return t.matches(e)}}var Fa=Array.prototype.find;function Wa(e){return function(){return Fa.call(this.children,e)}}function ja(){return this.firstElementChild}function Xa(e){return this.select(e==null?ja:Wa(typeof e=="function"?e:Dr(e)))}var Ya=Array.prototype.filter;function Za(){return Array.from(this.children)}function qa(e){return function(){return Ya.call(this.children,e)}}function Ga(e){return this.selectAll(e==null?Za:qa(typeof e=="function"?e:Dr(e)))}function Ua(e){typeof e!="function"&&(e=Tr(e));for(var t=this._groups,n=t.length,o=new Array(n),r=0;r<n;++r)for(var i=t[r],s=i.length,a=o[r]=[],c,u=0;u<s;++u)(c=i[u])&&e.call(c,c.__data__,u,i)&&a.push(c);return new ue(o,this._parents)}function Rr(e){return new Array(e.length)}function Ka(){return new ue(this._enter||this._groups.map(Rr),this._parents)}function Bt(e,t){this.ownerDocument=e.ownerDocument,this.namespaceURI=e.namespaceURI,this._next=null,this._parent=e,this.__data__=t}Bt.prototype={constructor:Bt,appendChild:function(e){return this._parent.insertBefore(e,this._next)},insertBefore:function(e,t){return this._parent.insertBefore(e,t)},querySelector:function(e){return this._parent.querySelector(e)},querySelectorAll:function(e){return this._parent.querySelectorAll(e)}};function Qa(e){return function(){return e}}function Ja(e,t,n,o,r,i){for(var s=0,a,c=t.length,u=i.length;s<u;++s)(a=t[s])?(a.__data__=i[s],o[s]=a):n[s]=new Bt(e,i[s]);for(;s<c;++s)(a=t[s])&&(r[s]=a)}function ec(e,t,n,o,r,i,s){var a,c,u=new Map,d=t.lengthLine truncated
|
||||
`).replace(`
|
||||
|
||||
`,`
|
||||
+`).split(`
|
||||
`)),d=u.reduce((l,f)=>l.concat(...f),[]);return[u,d]}return[[],[]]},[e]);return R.useEffect(()=>{const c=t?.target??ar,u=t?.actInsideInputWithModifier??!0;if(e!==null){const d=h=>{if(r.current=h.ctrlKey||h.metaKey||h.shiftKey||h.altKey,(!r.current||r.current&&!u)&&wi(h))return!1;const y=ur(h.code,a);if(i.current.add(h[y]),cr(s,i.current,!1)){const w=h.composedPath?.()?.[0]||h.target,x=w?.nodeName==="BUTTON"||w?.nodeName==="A";t.preventDefault!==!1&&(r.current||!x)&&h.preventDefault(),o(!0)}},l=h=>{const p=ur(h.code,a);cr(s,i.current,!0)?(o(!1),i.current.clear()):i.current.delete(h[p]),h.key==="Meta"&&i.current.clear(),r.current=!1},f=()=>{i.current.clear(),o(!1)};return c?.addEventListener("keydown",d),c?.addEventListener("keyup",l),window.addEventListener("blur",f),window.addEventListener("contextmenu",f),()=>{c?.removeEventListener("keydown",d),c?.removeEventListener("keyup",l),window.removeEventListener("blur",f),window.removeEventListener("contextmenu",f)}}},[e,o]),n}function cr(e,t,n){return e.filter(o=>n||o.length===t.size).some(o=>o.every(r=>t.has(r)))}function ur(e,t){return t.includes(e)?"code":"key"}const Vf=()=>{const e=re();return R.useMemo(()=>({zoomIn:t=>{const{panZoom:n}=e.getState();return n?n.scaleBy(1.2,{duration:t?.duration}):Promise.resolve(!1)},zoomOut:t=>{const{panZoom:n}=e.getState();return n?n.scaleBy(1/1.2,{duration:t?.duration}):Promise.resolve(!1)},zoomTo:(t,n)=>{const{panZoom:o}=e.getState();return o?o.scaleTo(t,{duration:n?.duration}):Promise.resolve(!1)},getZoom:()=>e.getState().transform[2],setViewport:async(t,n)=>{const{transform:[o,r,i],panZoom:s}=e.getState();return s?(await s.setViewport({x:t.x??o,y:t.y??r,zoom:t.zoom??i},n),Promise.resolve(!0)):Promise.resolve(!1)},getViewport:()=>{const[t,n,o]=e.getState().transform;return{x:t,y:n,zoom:o}},setCenter:async(t,n,o)=>e.getState().setCenter(t,n,o),fitBounds:async(t,n)=>{const{width:o,height:r,minZoom:i,maxZoom:s,panZoom:a}=e.getState(),c=eo(t,o,r,i,s,n?.padding??.1);return a?(await a.setViewport(c,{duration:n?.duration,ease:n?.ease,interpolate:n?.interpolate}),Promise.resolve(!0)):Promise.resolve(!1)},screenToFlowPosition:(t,n={})=>{const{transform:o,snapGrid:r,snapToGrid:i,domNode:s}=e.getState();if(!s)return t;const{x:a,y:c}=s.getBoundingClientRect(),u={x:t.x-a,y:t.y-c},d=n.snapGrid??r,l=n.snapToGrid??i;return _t(u,o,l,d)},flowToScreenPosition:t=>{const{transform:n,domNode:o}=e.getState();if(!o)return t;const{x:r,y:i}=o.getBoundingClientRect(),s=Ut(t,n);return{x:s.x+r,y:s.y+i}}}),[])};function Bi(e,t){const n=[],o=new Map,r=[];for(const i of e)if(i.type==="add"){r.push(i);continue}else if(i.type==="remove"||i.type==="replace")o.set(i.id,[i]);else{const s=o.get(i.id);s?s.push(i):o.set(i.id,[i])}for(const i of t){const s=o.get(i.id);if(!s){n.push(i);continue}if(s[0].type==="remove")continue;if(s[0].type==="replace"){n.push({...s[0].item});continue}const a={...i};for(const c of s)Bf(c,a);n.push(a)}return r.length&&r.forEach(i=>{i.index!==void 0?n.splice(i.index,0,{...i.item}):n.push({...i.item})}),n}function Bf(e,t){switch(e.type){case"select":{t.selected=e.selected;break}case"position":{typeof e.position<"u"&&(t.position=e.position),typeof e.dragging<"u"&&(t.dragging=e.dragging);break}case"dimensions":{typeof e.dimensions<"u"&&(t.measured={...e.dimensions},e.setAttributes&&((e.setAttributes===!0||e.setAttributes==="width")&&(t.width=e.dimensions.width),(e.setAttributes===!0||e.setAttributes==="height")&&(t.height=e.dimensions.height))),typeof e.resizing=="boolean"&&(t.resizing=e.resizing);break}}}function Fi(e,t){return Bi(e,t)}function Wi(e,t){return Bi(e,t)}function Ae(e,t){return{id:e,type:"select",selected:t}}function Be(e,t=new Set,n=!1){const o=[];for(const[r,i]of e){const s=t.has(r);!(i.selected===void 0&&!s)&&i.selected!==s&&(n&&(i.selected=s),o.push(Ae(i.id,s)))}return o}function lr({items:e=[],lookup:t}){const n=[],o=new Map(e.map(r=>[r.id,r]));for(const[r,i]of e.entries()){const s=t.get(i.id),a=s?.internals?.userNode??s;a!==void 0&&a!==i&&n.push({id:i.id,item:i,type:"replace"}),a===void 0&&n.push({item:i,type:"add",index:r})}for(const[r]of t)o.get(r)===void 0&&n.push({id:r,type:"remove"});return n}function dr(e){return{id:e.id,type:"remove"}}const fr=e=>ld(e),Ff=e=>di(e);function ji(e){return R.forwardRef(e)}const Wf=typeof window<"u"?R.useLayoutEffect:R.useEffect;function hr(e){const[t,n]=R.useState(BigInt(0)),[o]=R.useState(()=>jf(()=>n(r=>r+BigInt(1))));return Wf(()=>{const r=o.get();r.length&&(e(r),o.reset())},[t]),o}function jf(e){let t=[];return{get:()=>t,reset:()=>{t=[]},push:n=>{t.push(n),e()}}}const Xi=R.createContext(null);function Xf({children:e}){const t=re(),n=R.useCallback(a=>{const{nodes:c=[],setNodes:u,hasDefaultNodes:d,onNodesChange:l,nodeLookup:f,fitViewQueued:h,onNodesChangeMiddlewareMap:p}=t.getState();let y=c;for(const x of a)y=typeof x=="function"?x(y):x;let w=lr({items:y,lookup:f});for(const x of p.values())w=x(w);d&&u(y),w.length>0?l?.(w):h&&window.requestAnimationFrame(()=>{const{fitViewQu
Line truncated
|
||||
M${P.x},${P.y}h${P.width}v${P.height}h${-P.width}z`,fillRule:"evenodd",pointerEvents:"none"})]})})}ms.displayName="MiniMap";const sg=R.memo(ms),Ap=e=>t=>e?`${Math.max(1/t.transform[2],1)}`:void 0,$p={[Ke.Line]:"right",[Ke.Handle]:"bottom-right"};function Pp({nodeId:e,position:t,variant:n=Ke.Handle,className:o,style:r=void 0,children:i,color:s,minWidth:a=10,minHeight:c=10,maxWidth:u=Number.MAX_VALUE,maxHeight:d=Number.MAX_VALUE,keepAspectRatio:l=!1,resizeDirection:f,autoScale:h=!0,shouldResize:p,onResizeStart:y,onResize:w,onResizeEnd:x}){const b=qi(),g=typeof e=="string"?e:b,m=re(),S=R.useRef(null),E=n===Ke.Handle,N=J(R.useCallback(Ap(E&&h),[E,h]),oe),P=R.useRef(null),A=t??$p[n];R.useEffect(()=>{if(!(!S.current||!g))return P.current||(P.current=uf({domNode:S.current,nodeId:g,getStoreItems:()=>{const{nodeLookup:k,transform:D,snapGrid:z,snapToGrid:v,nodeOrigin:C,domNode:_}=m.getState();return{nodeLookup:k,transform:D,snapGrid:z,snapToGrid:v,nodeOrigin:C,paneDomNode:_}},onChange:(k,D)=>{const{triggerNodeChanges:z,nodeLookup:v,parentLookup:C,nodeOrigin:_}=m.getState(),I=[],M={x:k.x,y:k.y},T=v.get(g);if(T&&T.expandParent&&T.parentId){const H=T.origin??_,O=k.width??T.measured.width??0,L=k.height??T.measured.height??0,W={id:T.id,parentId:T.parentId,rect:{width:O,height:L,...yi({x:k.x??T.position.x,y:k.y??T.position.y},{width:O,height:L},T.parentId,v,H)}},F=so([W],v,C,_);I.push(...F),M.x=k.x?Math.max(H[0]*O,k.x):void 0,M.y=k.y?Math.max(H[1]*L,k.y):void 0}if(M.x!==void 0&&M.y!==void 0){const H={id:g,type:"position",position:{...M}};I.push(H)}if(k.width!==void 0&&k.height!==void 0){const O={id:g,type:"dimensions",resizing:!0,setAttributes:f?f==="horizontal"?"width":"height":!0,dimensions:{width:k.width,height:k.height}};I.push(O)}for(const H of D){const O={...H,type:"position"};I.push(O)}z(I)},onEnd:({width:k,height:D})=>{const z={id:g,type:"dimensions",resizing:!1,dimensions:{width:k,height:D}};m.getState().triggerNodeChanges([z])}})),P.current.update({controlPosition:A,boundaries:{minWidth:a,minHeight:c,maxWidth:u,maxHeight:d},keepAspectRatio:l,resizeDirection:f,onResizeStart:y,onResize:w,onResizeEnd:x,shouldResize:p}),()=>{P.current?.destroy()}},[A,a,c,u,d,l,y,w,x,p]);const V=A.split("-");return $.jsx("div",{className:ie(["react-flow__resize-control","nodrag",...V,n,o]),ref:S,style:{...r,scale:N,...s&&{[E?"backgroundColor":"borderColor"]:s}},children:i})}R.memo(Pp);const Tp=["off","on","auto"],Dp=["16:9","9:16","1:1","4:3","3:4"],Rp=["","5s","10s"],zp=["","720p","1080p"],Hp=["mp3","opus","aac","flac","wav"],je={textInput:{type:"textInput",label:"Input",description:"Static text provided by the user. Starting point of a workflow.",input:"none",output:"text",params:[{name:"inputText",type:"text",description:"The text value.",required:!0,default:""}]},output:{type:"output",label:"Output",description:"Displays whatever its upstream node produced. Accepts any kind.",input:"text",output:"none",params:[]},chat:{type:"chat",label:"LLM",description:"Run a Venice chat completion. Accepts upstream text as context.",input:"text",output:"text",params:[{name:"model",type:"string",description:"Venice chat model id.",required:!0,default:"llama-3.3-70b"},{name:"prompt",type:"text",description:"Instruction. Use {{input}} to position upstream text, or leave empty to append.",required:!0,default:""},{name:"temperature",type:"number",description:"Sampling temperature.",default:.7,min:0,max:2},{name:"maxTokens",type:"number",description:"Max output tokens.",default:4096,min:64,max:32768},{name:"webSearch",type:"enum",description:"Enable Venice web search.",default:"off",enumValues:Tp}]},imageGen:{type:"imageGen",label:"Image Gen",description:"Generate an image from a text prompt.",input:"text",output:"image",params:[{name:"model",type:"string",description:"Venice image model id.",required:!0,default:"z-image-turbo"},{name:"prompt",type:"text",description:"Image prompt. Use {{input}} to position upstream text.",required:!0,default:""},{name:"negativePrompt",type:"string",description:"What to avoid.",default:""},{name:"steps",type:"number",description:"Denoising steps.",default:20,min:1,max:50},{name:"width",type:"number",description:"Image width in pixels.",default:1024,min:256,max:2048},{name:"height",type:"number",description:"Image height in pixels.",default:1024,min:256,max:2048},{name:"style",type:"string",description:"Style preset name.",default:""},{name:"hideWatermark",type:"boolean",description:"Omit watermark if allowed.",default:!0}]},tts:{type:"tts",label:"Text to Speech",description:"Narrate text into speech audio.",input:"text",output:"audio",params:[{name:"model",type:"string",description:"TTS model id.",required:!0,default:"tts-kokoro"},{name:"prompt",type:"text",description:"Text to speak. Use {{input}} to position upstream text.",default:""},{name:"voice",type:"string",description:"Voice id.",default:"af_sky"},{name:"speed",type:"number",description:"Playback speed.",default:1,min:.25,max:4},{name:"responseForma
Line truncated
|
||||
M${P.x},${P.y}h${P.width}v${P.height}h${-P.width}z`,fillRule:"evenodd",pointerEvents:"none"})]})})}ms.displayName="MiniMap";const sg=R.memo(ms),Ap=e=>t=>e?`${Math.max(1/t.transform[2],1)}`:void 0,$p={[Ke.Line]:"right",[Ke.Handle]:"bottom-right"};function Pp({nodeId:e,position:t,variant:n=Ke.Handle,className:o,style:r=void 0,children:i,color:s,minWidth:a=10,minHeight:c=10,maxWidth:u=Number.MAX_VALUE,maxHeight:d=Number.MAX_VALUE,keepAspectRatio:l=!1,resizeDirection:f,autoScale:h=!0,shouldResize:p,onResizeStart:y,onResize:w,onResizeEnd:x}){const b=qi(),g=typeof e=="string"?e:b,m=re(),S=R.useRef(null),E=n===Ke.Handle,N=J(R.useCallback(Ap(E&&h),[E,h]),oe),P=R.useRef(null),A=t??$p[n];R.useEffect(()=>{if(!(!S.current||!g))return P.current||(P.current=uf({domNode:S.current,nodeId:g,getStoreItems:()=>{const{nodeLookup:k,transform:D,snapGrid:z,snapToGrid:v,nodeOrigin:C,domNode:_}=m.getState();return{nodeLookup:k,transform:D,snapGrid:z,snapToGrid:v,nodeOrigin:C,paneDomNode:_}},onChange:(k,D)=>{const{triggerNodeChanges:z,nodeLookup:v,parentLookup:C,nodeOrigin:_}=m.getState(),I=[],M={x:k.x,y:k.y},T=v.get(g);if(T&&T.expandParent&&T.parentId){const H=T.origin??_,O=k.width??T.measured.width??0,L=k.height??T.measured.height??0,W={id:T.id,parentId:T.parentId,rect:{width:O,height:L,...yi({x:k.x??T.position.x,y:k.y??T.position.y},{width:O,height:L},T.parentId,v,H)}},F=so([W],v,C,_);I.push(...F),M.x=k.x?Math.max(H[0]*O,k.x):void 0,M.y=k.y?Math.max(H[1]*L,k.y):void 0}if(M.x!==void 0&&M.y!==void 0){const H={id:g,type:"position",position:{...M}};I.push(H)}if(k.width!==void 0&&k.height!==void 0){const O={id:g,type:"dimensions",resizing:!0,setAttributes:f?f==="horizontal"?"width":"height":!0,dimensions:{width:k.width,height:k.height}};I.push(O)}for(const H of D){const O={...H,type:"position"};I.push(O)}z(I)},onEnd:({width:k,height:D})=>{const z={id:g,type:"dimensions",resizing:!1,dimensions:{width:k,height:D}};m.getState().triggerNodeChanges([z])}})),P.current.update({controlPosition:A,boundaries:{minWidth:a,minHeight:c,maxWidth:u,maxHeight:d},keepAspectRatio:l,resizeDirection:f,onResizeStart:y,onResize:w,onResizeEnd:x,shouldResize:p}),()=>{P.current?.destroy()}},[A,a,c,u,d,l,y,w,x,p]);const V=A.split("-");return $.jsx("div",{className:ie(["react-flow__resize-control","nodrag",...V,n,o]),ref:S,style:{...r,scale:N,...s&&{[E?"backgroundColor":"borderColor"]:s}},children:i})}R.memo(Pp);const Tp=["off","on","auto"],Dp=["16:9","9:16","1:1","4:3","3:4"],Rp=["","5s","10s"],zp=["","720p","1080p"],Hp=["mp3","opus","aac","flac","wav"],je={textInput:{type:"textInput",label:"Input",description:"Static text provided by the user. Starting point of a workflow.",input:"none",output:"text",params:[{name:"inputText",type:"text",description:"The text value.",required:!0,default:""}]},output:{type:"output",label:"Output",description:"Displays whatever its upstream node produced. Accepts any kind.",input:"text",output:"none",params:[]},chat:{type:"chat",label:"LLM",description:"Run a Venice chat completion. Accepts upstream text as context.",input:"text",output:"text",params:[{name:"model",type:"string",description:"Venice chat model id.",required:!0,default:"llama-3.3-70b"},{name:"prompt",type:"text",description:"Instruction. Use {{input}} to position upstream text, or leave empty to append.",required:!0,default:""},{name:"temperature",type:"number",description:"Sampling temperature.",default:.7,min:0,max:2},{name:"maxTokens",type:"number",description:"Max output tokens.",default:4096,min:64,max:32768},{name:"webSearch",type:"enum",description:"Enable Venice web search.",default:"off",enumValues:Tp}]},imageGen:{type:"imageGen",label:"Image Gen",description:"Generate an image from a text prompt.",input:"text",output:"image",params:[{name:"model",type:"string",description:"Venice image model id.",required:!0,default:"z-image-turbo"},{name:"prompt",type:"text",description:"Image prompt. Use {{input}} to position upstream text.",required:!0,default:""},{name:"negativePrompt",type:"string",description:"What to avoid.",default:""},{name:"steps",type:"number",description:"Denoising steps.",default:20,min:1,max:50},{name:"width",type:"number",description:"Image width in pixels.",default:1024,min:256,max:2048},{name:"height",type:"number",description:"Image height in pixels.",default:1024,min:256,max:2048},{name:"style",type:"string",description:"Style preset name.",default:""},{name:"hideWatermark",type:"boolean",description:"Omit watermark if allowed.",default:!0}]},tts:{type:"tts",label:"Text to Speech",description:"Narrate text into speech audio.",input:"text",output:"audio",params:[{name:"model",type:"string",description:"TTS model id.",required:!0,default:"tts-kokoro"},{name:"prompt",type:"text",description:"Text to speak. Use {{input}} to position upstream text.",default:""},{name:"voice",type:"string",description:"Voice id.",default:"af_sky"},{name:"speed",type:"number",description:"Playback speed.",default:1,min:.25,max:4},{name:"responseForma
Line truncated
|
||||
|
||||
`)}function rt(e,t){return e?e.includes("{{input}}")?e.replace(/\{\{input\}\}/g,t):t?`${e}
|
||||
|
||||
${t}`:e:t}async function Ir(e,t,n,o,r){for(let i=0;i<kr;i++){if(r?.aborted)throw new DOMException("Aborted","AbortError");await new Promise((a,c)=>{const u=setTimeout(a,Mr);r?.addEventListener("abort",()=>{clearTimeout(u),c(new DOMException("Aborted","AbortError"))},{once:!0})});const s=await Aa(e,t,n,{downloadUrl:o,signal:r});if(s.state==="done")return s.url;if(s.state==="failed")throw new Error(s.error)}throw new Error(`Generation timed out after ${kr*Mr/1e3}s`)}async function Jp(e,t,n){const o=e.data;switch(o.nodeType){case"textInput":return o.inputText??"";case"compose":{const r=/\[image:([^\]]+)\]/.exec(t)?.[1],i=/\[audio:([^\]]+)\]/.exec(t)?.[1];if(!r||!i){const a=[r&&"an image",i&&"audio"].filter(Boolean).join(" and ")||"neither";throw new Error(`Cover + Track needs one image node and one audio node connected to it (got ${a})`)}return`[video:${await Up(r,i,{maxSide:o.maxSide??1080,signal:n})}]`}case"output":return t;case"chat":{const r=rt(o.prompt,t);return(await Nt("/chat/completions",{method:"POST",body:JSON.stringify({model:o.model||"llama-3.3-70b",messages:[{role:"user",content:r}],temperature:o.temperature??.7,max_tokens:o.maxTokens??4096,venice_parameters:{enable_web_search:o.webSearch??"off"}}),signal:n})).choices[0]?.message?.content??""}case"imageGen":{const r=rt(o.prompt,t),i={model:o.model||"z-image-turbo",prompt:r,negative_prompt:o.negativePrompt||void 0,steps:o.steps??20,style_preset:o.style||void 0,width:o.width??1024,height:o.height??1024,hide_watermark:o.hideWatermark??!0};o.aspectRatio&&(i.aspect_ratio=o.aspectRatio);const a=(await Nt("/image/generate",{method:"POST",body:JSON.stringify(i),signal:n})).images[0],c=typeof a=="string"?a:a.b64_json;return`[image:data:${c.startsWith("/9j/")?"image/jpeg":c.startsWith("iVBOR")?"image/png":c.startsWith("UklGR")?"image/webp":"image/png"};base64,${c}]`}case"tts":{const r=rt(o.prompt,t),i=await Ia("/audio/speech",{model:o.model||"tts-kokoro",input:r,voice:o.voice||"af_sky",speed:o.speed??1,response_format:o.responseFormat||"mp3"},{signal:n});return`[audio:${URL.createObjectURL(i)}]`}case"music":{const r=rt(o.prompt,t),i={model:o.model||"stable-audio-25",prompt:r,duration_seconds:o.duration??30,force_instrumental:o.instrumental??!1};o.lyrics&&(i.lyrics_prompt=o.lyrics);const s=await Nt("/audio/queue",{method:"POST",body:JSON.stringify(i),signal:n});return`[audio:${await Ir("audio",s.model||String(i.model),s.queue_id,s.download_url,n)}]`}case"video":{const r=rt(o.prompt,t),i={model:o.model||"wan-3-0-text-to-video",prompt:r,aspect_ratio:o.videoAspectRatio||"16:9"};o.videoDuration&&(i.duration=o.videoDuration),o.videoResolution&&(i.resolution=o.videoResolution);const s=await Nt("/video/queue",{method:"POST",body:JSON.stringify(i),signal:n});return`[video:${await Ir("video",s.model||String(i.model),s.queue_id||s.id||"",s.download_url,n)}]`}}}async function cg(e,t,n){const o=typeof n=="function"?{onUpdate:n}:n,{signal:r,onUpdate:i}=o,s=Yp({nodes:e,edges:t});if(!s.ok){const d=s.errors[0];throw new In(d?.message??"Workflow has validation errors.",d?.nodeId)}const a=Kp(e,t);if(!a)throw new In("Workflow contains a cycle.");const c=new Map,u=new Map(e.map(d=>[d.id,d]));for(const d of a){if(r?.aborted)return;await Promise.all(d.map(async l=>{const f=u.get(l);if(f){i(l,{status:"running",output:void 0,error:void 0});try{const h=Qp(l,t,c),p=await Jp(f,h,r);c.set(l,p);const y=je[f.data.nodeType]?.output;i(l,{status:"done",output:p,outputKind:y&&y!=="none"?y:void 0})}catch(h){if(h instanceof DOMException&&h.name==="AbortError")throw i(l,{status:"error",error:"Cancelled"}),h;const p=h instanceof Error?h.message:"Unknown error";throw i(l,{status:"error",error:p}),new In(p,l)}}}))}}export{rg as B,ig as C,Kt as H,sg as M,je as N,X as P,rp as R,ao as a,ng as b,og as c,Cd as d,ke as e,cg as f,jp as g,Wp as h,tg as i,ag as u,Yp as v};
|
||||
${t}`:e:t}async function Ir(e,t,n,o,r){for(let i=0;i<kr;i++){if(r?.aborted)throw new DOMException("Aborted","AbortError");await new Promise((a,c)=>{const u=setTimeout(a,Mr);r?.addEventListener("abort",()=>{clearTimeout(u),c(new DOMException("Aborted","AbortError"))},{once:!0})});const s=await Aa(e,t,n,{downloadUrl:o,signal:r});if(s.state==="done")return s.url;if(s.state==="failed")throw new Error(s.error)}throw new Error(`Generation timed out after ${kr*Mr/1e3}s`)}async function Jp(e,t,n){const o=e.data;switch(o.nodeType){case"textInput":return o.inputText??"";case"compose":{const r=/\[image:([^\]]+)\]/.exec(t)?.[1],i=/\[audio:([^\]]+)\]/.exec(t)?.[1];if(!r||!i){const a=[r&&"an image",i&&"audio"].filter(Boolean).join(" and ")||"neither";throw new Error(`Cover + Track needs one image node and one audio node connected to it (got ${a})`)}return`[video:${await Up(r,i,{maxSide:o.maxSide??1080,signal:n})}]`}case"output":return t;case"step":return t;case"chat":{const r=rt(o.prompt,t);return(await Nt("/chat/completions",{method:"POST",body:JSON.stringify({model:o.model||"llama-3.3-70b",messages:[{role:"user",content:r}],temperature:o.temperature??.7,max_tokens:o.maxTokens??4096,venice_parameters:{enable_web_search:o.webSearch??"off"}}),signal:n})).choices[0]?.message?.content??""}case"imageGen":{const r=rt(o.prompt,t),i={model:o.model||"z-image-turbo",prompt:r,negative_prompt:o.negativePrompt||void 0,steps:o.steps??20,style_preset:o.style||void 0,width:o.width??1024,height:o.height??1024,hide_watermark:o.hideWatermark??!0};o.aspectRatio&&(i.aspect_ratio=o.aspectRatio);const a=(await Nt("/image/generate",{method:"POST",body:JSON.stringify(i),signal:n})).images[0],c=typeof a=="string"?a:a.b64_json;return`[image:data:${c.startsWith("/9j/")?"image/jpeg":c.startsWith("iVBOR")?"image/png":c.startsWith("UklGR")?"image/webp":"image/png"};base64,${c}]`}case"tts":{const r=rt(o.prompt,t),i=await Ia("/audio/speech",{model:o.model||"tts-kokoro",input:r,voice:o.voice||"af_sky",speed:o.speed??1,response_format:o.responseFormat||"mp3"},{signal:n});return`[audio:${URL.createObjectURL(i)}]`}case"music":{const r=rt(o.prompt,t),i={model:o.model||"stable-audio-25",prompt:r,duration_seconds:o.duration??30,force_instrumental:o.instrumental??!1};o.lyrics&&(i.lyrics_prompt=o.lyrics);const s=await Nt("/audio/queue",{method:"POST",body:JSON.stringify(i),signal:n});return`[audio:${await Ir("audio",s.model||String(i.model),s.queue_id,s.download_url,n)}]`}case"video":{const r=rt(o.prompt,t),i={model:o.model||"wan-3-0-text-to-video",prompt:r,aspect_ratio:o.videoAspectRatio||"16:9"};o.videoDuration&&(i.duration=o.videoDuration),o.videoResolution&&(i.resolution=o.videoResolution);const s=await Nt("/video/queue",{method:"POST",body:JSON.stringify(i),signal:n});return`[video:${await Ir("video",s.model||String(i.model),s.queue_id||s.id||"",s.download_url,n)}]`}}}async function cg(e,t,n){const o=typeof n=="function"?{onUpdate:n}:n,{signal:r,onUpdate:i}=o,s=Yp({nodes:e,edges:t});if(!s.ok){const d=s.errors[0];throw new In(d?.message??"Workflow has validation errors.",d?.nodeId)}const a=Kp(e,t);if(!a)throw new In("Workflow contains a cycle.");const c=new Map,u=new Map(e.map(d=>[d.id,d]));for(const d of a){if(r?.aborted)return;await Promise.all(d.map(async l=>{const f=u.get(l);if(f){i(l,{status:"running",output:void 0,error:void 0});try{const h=Qp(l,t,c),p=await Jp(f,h,r);c.set(l,p);const y=je[f.data.nodeType]?.output;i(l,{status:"done",output:p,outputKind:y&&y!=="none"?y:void 0})}catch(h){if(h instanceof DOMException&&h.name==="AbortError")throw i(l,{status:"error",error:"Cancelled"}),h;const p=h instanceof Error?h.message:"Unknown error";throw i(l,{status:"error",error:p}),new In(p,l)}}}))}}export{rg as B,ig as C,Kt as H,sg as M,je as N,X as P,rp as R,ao as a,ng as b,og as c,Cd as d,ke as e,cg as f,jp as g,Wp as h,tg as i,ag as u,Yp as v};
|
||||
@@ -1 +0,0 @@
|
||||
import{r as h,j as e,c as w,u as F,S as z,g as E,t as M}from"./index-CmWoX_gW.js";import{u as f,a as P,H as S,P as O,R as _,b as H,c as $,d as U,i as Y,B as K,e as q,C as J,M as Q,f as X}from"./workflow-engine-lSJFuHzZ.js";function Z(){return e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("polyline",{points:"22 12 16 12 14 15 10 9 8 12 2 12"})})}function ee(){return e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("path",{d:"M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"})})}function te(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}),e.jsx("circle",{cx:"8.5",cy:"8.5",r:"1.5"}),e.jsx("polyline",{points:"21 15 16 10 5 21"})]})}function oe(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("polygon",{points:"11 5 6 9 2 9 2 15 6 15 11 19 11 5"}),e.jsx("path",{d:"M19.07 4.93a10 10 0 010 14.14M15.54 8.46a5 5 0 010 7.07"})]})}function se(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("path",{d:"M9 18V5l12-2v13"}),e.jsx("circle",{cx:"6",cy:"18",r:"3"}),e.jsx("circle",{cx:"18",cy:"16",r:"3"})]})}function ne(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("polygon",{points:"23 7 16 12 23 17 23 7"}),e.jsx("rect",{x:"1",y:"5",width:"15",height:"14",rx:"2"})]})}function ie(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}),e.jsx("path",{d:"M3 15l5-5 4 4"}),e.jsx("path",{d:"M14 17v-5l4-1v5"}),e.jsx("circle",{cx:"12.5",cy:"17.5",r:"1.5"}),e.jsx("circle",{cx:"16.5",cy:"16.5",r:"1.5"})]})}function re(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}),e.jsx("path",{d:"M8 12h8M12 8v8"})]})}const ae={textInput:void 0,output:void 0,chat:"text",imageGen:"image",tts:"tts",music:"music",video:"video",compose:void 0},le={textInput:{label:"Input",Icon:Z,color:"border-blue-500/30",hasInput:!1,hasOutput:!0},chat:{label:"LLM",Icon:ee,color:"border-purple-500/30",hasInput:!0,hasOutput:!0},imageGen:{label:"Image Gen",Icon:te,color:"border-pink-500/30",hasInput:!0,hasOutput:!0},tts:{label:"Text to Speech",Icon:oe,color:"border-green-500/30",hasInput:!0,hasOutput:!0},music:{label:"Music Gen",Icon:se,color:"border-yellow-500/30",hasInput:!0,hasOutput:!0},video:{label:"Video Gen",Icon:ne,color:"border-orange-500/30",hasInput:!0,hasOutput:!0},compose:{label:"Cover + Track",Icon:ie,color:"border-cyan-500/30",hasInput:!0,hasOutput:!0},output:{label:"Output",Icon:re,color:"border-white/20",hasInput:!0,hasOutput:!1}},x="nodrag bg-white/[0.03] border border-white/[0.06] rounded px-1.5 py-0.5 text-[12px] text-white/40 outline-none",I="nodrag w-full bg-white/[0.03] border border-white/[0.06] rounded-md px-2 py-1 text-[13px] text-white/60 outline-none placeholder:text-white/15";function ce({nodeType:n,value:t,onChange:i}){const s=ae[n],{data:d}=F(s),p=d?.map(u=>({value:u.id,label:u.model_spec?.name||u.id}))??[];return e.jsx("div",{className:"nodrag",children:e.jsx(z,{value:t,onChange:i,options:p,searchable:!0,placeholder:"Select model...",className:"w-full [&_button]:!py-1 [&_button]:!text-[13px] [&_button]:!px-2"})})}function de({id:n,data:t}){const i=le[t.nodeType],s=f(o=>o.runResults[n]),{setNodes:d}=P(),[p,u]=h.useState(!1),r=h.useCallback(o=>{d(m=>m.map(g=>g.id===n?{...g,data:{...g.data,...o}}:g))},[n,d]),a=h.useCallback(()=>{d(o=>o.filter(m=>m.id!==n))},[n,d]);return e.jsxs("div",{className:w("rounded-xl border-2 bg-[#111] shadow-xl min-w-[300px] max-w-[340px]",i.color,s?.status==="running"&&"ring-2 ring-white/20 animate-pulse",s?.status==="done"&&"ring-2 ring-green-500/30",s?.status==="error"&&"ring-2 ring-red-500/30"),children:[i.hasInput&&e.jsx(S,{type:"target",position:O.Top,className:"!w-3 !h-3 !bg-white/30 !border-2 !border-[#111]"}),e.jsxs("div",{className:"flex items-center gap-2 px-4 py-3 border-b border-white/[0.06]",children:[e.jsx("span",{className:"text-white/30",children:e.jsx(i.Icon,{})}),e.jsx("span",{className:"text-[14px] font-medium text-white/60",children:i.label}),s?.status===Line truncated
|
||||
@@ -0,0 +1,38 @@
|
||||
import{r as w,j as e,c as v,u as Q,S as X,t as $,g as V}from"./index-fmJ2VqHZ.js";import{u as C,a as Y,H as M,P as A,N as F,R as Z,b as ee,c as te,d as oe,i as ne,B as se,e as re,C as ie,M as ae,f as le}from"./workflow-engine-BuXGtBGn.js";function ce(){return e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("polyline",{points:"22 12 16 12 14 15 10 9 8 12 2 12"})})}function de(){return e.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("path",{d:"M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"})})}function pe(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}),e.jsx("circle",{cx:"8.5",cy:"8.5",r:"1.5"}),e.jsx("polyline",{points:"21 15 16 10 5 21"})]})}function ue(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("polygon",{points:"11 5 6 9 2 9 2 15 6 15 11 19 11 5"}),e.jsx("path",{d:"M19.07 4.93a10 10 0 010 14.14M15.54 8.46a5 5 0 010 7.07"})]})}function he(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("path",{d:"M9 18V5l12-2v13"}),e.jsx("circle",{cx:"6",cy:"18",r:"3"}),e.jsx("circle",{cx:"18",cy:"16",r:"3"})]})}function xe(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("polygon",{points:"23 7 16 12 23 17 23 7"}),e.jsx("rect",{x:"1",y:"5",width:"15",height:"14",rx:"2"})]})}function me(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}),e.jsx("path",{d:"M3 15l5-5 4 4"}),e.jsx("path",{d:"M14 17v-5l4-1v5"}),e.jsx("circle",{cx:"12.5",cy:"17.5",r:"1.5"}),e.jsx("circle",{cx:"16.5",cy:"16.5",r:"1.5"})]})}function ge(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("polyline",{points:"4 17 10 11 4 5"}),e.jsx("line",{x1:"12",y1:"19",x2:"20",y2:"19"})]})}function fe(){return e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}),e.jsx("path",{d:"M8 12h8M12 8v8"})]})}const we={textInput:void 0,output:void 0,chat:"text",imageGen:"image",tts:"tts",music:"music",video:"video",compose:void 0,step:void 0},be={textInput:{label:"Input",Icon:ce,color:"border-blue-500/30",hasInput:!1,hasOutput:!0},chat:{label:"LLM",Icon:de,color:"border-purple-500/30",hasInput:!0,hasOutput:!0},imageGen:{label:"Image Gen",Icon:pe,color:"border-pink-500/30",hasInput:!0,hasOutput:!0},tts:{label:"Text to Speech",Icon:ue,color:"border-green-500/30",hasInput:!0,hasOutput:!0},music:{label:"Music Gen",Icon:he,color:"border-yellow-500/30",hasInput:!0,hasOutput:!0},video:{label:"Video Gen",Icon:xe,color:"border-orange-500/30",hasInput:!0,hasOutput:!0},compose:{label:"Cover + Track",Icon:me,color:"border-cyan-500/30",hasInput:!0,hasOutput:!0},step:{label:"Step",Icon:ge,color:"border-white/20",hasInput:!0,hasOutput:!0},output:{label:"Output",Icon:fe,color:"border-white/20",hasInput:!0,hasOutput:!1}},f="nodrag bg-white/[0.03] border border-white/[0.06] rounded px-1.5 py-0.5 text-[12px] text-white/40 outline-none",N="nodrag w-full bg-white/[0.03] border border-white/[0.06] rounded-md px-2 py-1 text-[13px] text-white/60 outline-none placeholder:text-white/15";function ve({nodeType:n,value:t,onChange:r}){const s=we[n],{data:c}=Q(s),a=c?.map(i=>({value:i.id,label:i.model_spec?.name||i.id}))??[];return e.jsx("div",{className:"nodrag",children:e.jsx(X,{value:t,onChange:r,options:a,searchable:!0,placeholder:"Select model...",className:"w-full [&_button]:!py-1 [&_button]:!text-[13px] [&_button]:!px-2"})})}function je({id:n,data:t}){const r=be[t.nodeType],s=C(o=>o.runResults[n]),{setNodes:c}=Y(),[a,i]=w.useState(!1),l=w.useCallback(o=>{c(p=>p.map(h=>h.id===n?{...h,data:{...h.data,...o}}:h))},[n,c]),d=w.useCallback(()=>{c(o=>o.filter(p=>p.id!==n))},[n,c]);return e.jsxs("div",{className:v("rounded-xl border-2 bg-[#111] shadow-xl min-w-[300px] max-w-[340px]",r.color,s?.status==="running"&&"ring-2 ring-white/20 animate-pulse",s?.status==="done"&&"ring-2 ring-green-500/30",s?.status==="error"&&"ring-2 ring-red-500/30")Line truncated
|
||||
`+t.with,t.stepEnv&&`env:
|
||||
`+t.stepEnv].filter(Boolean).join(`
|
||||
`)}),e.jsx("p",{className:"text-[12px] text-white/30 leading-snug",children:"Runs on the factory, not here: on export this is the step as written."})]}):t.nodeType==="compose"?e.jsxs("div",{className:"flex flex-col gap-1.5",children:[e.jsx("p",{className:"text-[13px] text-white/40 leading-snug",children:"Shows the connected image while the connected audio plays, recorded here in your browser into one video (takes as long as the track)."}),e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-[12px] text-white/30",children:"max side"}),e.jsx("input",{type:"number",min:256,max:2048,step:2,value:t.maxSide??1080,onChange:o=>l({maxSide:Number(o.target.value)||1080}),className:v(N,"w-24")}),e.jsx("span",{className:"text-[12px] text-white/30",children:"px"})]})]}):t.nodeType==="textInput"?e.jsx("textarea",{value:t.inputText??"",onChange:o=>l({inputText:o.target.value}),placeholder:"Enter starting text...",rows:3,className:"nodrag nowheel w-full bg-white/[0.03] border border-white/[0.06] rounded-md px-2 py-1.5 text-[14px] text-white/70 outline-none resize-none placeholder:text-white/15"}):e.jsxs(e.Fragment,{children:[e.jsx(ve,{nodeType:t.nodeType,value:t.model,onChange:o=>l({model:o})}),e.jsx("textarea",{value:t.prompt,onChange:o=>l({prompt:o.target.value}),placeholder:"Instructions for this step...",rows:2,className:"nodrag nowheel w-full bg-white/[0.03] border border-white/[0.06] rounded-md px-2 py-1.5 text-[14px] text-white/70 outline-none resize-none placeholder:text-white/15"}),t.nodeType==="chat"&&e.jsxs("div",{className:"flex flex-wrap gap-1.5",children:[e.jsxs("select",{value:t.webSearch??"off",onChange:o=>l({webSearch:o.target.value}),className:f,children:[e.jsx("option",{value:"off",children:"Search off"}),e.jsx("option",{value:"on",children:"Search on"}),e.jsx("option",{value:"auto",children:"Search auto"})]}),e.jsx("input",{type:"number",value:t.temperature??.7,onChange:o=>l({temperature:parseFloat(o.target.value)}),step:.1,min:0,max:2,className:v(f,"w-14"),title:"Temperature"}),e.jsx("input",{type:"number",value:t.maxTokens??4096,onChange:o=>l({maxTokens:parseInt(o.target.value)}),step:256,min:64,max:32768,className:v(f,"w-[72px]"),title:"Max tokens"})]}),t.nodeType==="imageGen"&&e.jsxs(e.Fragment,{children:[e.jsx("input",{value:t.negativePrompt??"",onChange:o=>l({negativePrompt:o.target.value}),placeholder:"Negative prompt...",className:N}),e.jsxs("div",{className:"flex gap-1.5",children:[e.jsxs("div",{className:"flex-1",children:[e.jsx("label",{className:"text-[11px] text-white/20 mb-0.5 block",children:"Steps"}),e.jsx("input",{type:"number",value:t.steps??20,onChange:o=>l({steps:parseInt(o.target.value)}),min:1,max:50,className:f+" w-full"})]}),e.jsxs("div",{className:"flex-1",children:[e.jsx("label",{className:"text-[11px] text-white/20 mb-0.5 block",children:"Size"}),e.jsxs("select",{value:`${t.width??1024}x${t.height??1024}`,onChange:o=>{const[p,h]=o.target.value.split("x").map(Number);l({width:p,height:h})},className:f+" w-full",children:[e.jsx("option",{value:"512x512",children:"512"}),e.jsx("option",{value:"768x768",children:"768"}),e.jsx("option",{value:"1024x1024",children:"1024"}),e.jsx("option",{value:"1280x1280",children:"1280"})]})]})]}),e.jsxs("div",{className:"flex gap-1.5",children:[e.jsx("input",{value:t.style??"",onChange:o=>l({style:o.target.value}),placeholder:"Style preset...",className:v(N,"flex-1")}),e.jsxs("label",{className:"nodrag flex items-center gap-1 text-[12px] text-white/30 cursor-pointer shrink-0",children:[e.jsx("input",{type:"checkbox",checked:t.hideWatermark??!0,onChange:o=>l({hideWatermark:o.target.checked}),className:"nodrag w-3 h-3 accent-white/50"}),"No WM"]})]})]}),t.nodeType==="tts"&&e.jsxs("div",{className:"flex flex-wrap gap-1.5",children:[e.jsxs("div",{className:"flex-1 min-w-[80px]",children:[e.jsx("label",{className:"text-[11px] text-white/20 mb-0.5 block",children:"Voice"}),e.jsx("input",{value:t.voice??"af_sky",onChange:o=>l({voice:o.target.value}),placeholder:"Voice ID",className:N})]}),e.jsxs("div",{className:"w-14",children:[e.jsx("label",{className:"text-[11px] text-white/20 mb-0.5 block",children:"Speed"}),e.jsx("input",{type:"number",value:t.speed??1,onChange:o=>l({speed:parseFloat(o.target.value)}),step:.25,min:.25,max:4,className:f+" w-full"})]}),e.jsxs("div",{className:"w-16",children:[e.jsx("label",{className:"text-[11px] text-white/20 mb-0.5 block",children:"Format"}),e.jsxs("select",{value:t.responseFormat??"mp3",onChange:o=>l({responseFormat:o.target.value}),className:f+" w-full",children:[e.jsx("option",{value:"mp3",children:"MP3"}),e.jsx("option",{value:"opus",children:"Opus"}),e.jsx("option",{value:"aac",children:"AAC"}),e.jsx("option",{value:"flac",children:"FLAC"}),e.jsx("option",{value:"wav",children:"WAV"})]})]})]}),t.nodeType==="music"&&e.jsxs(e.Fragment,{children:[e.jsx("textarea",{value:t.lyrics??"",onChange:o=>l({lyrics:o.target.value}),placeholder:"Lyrics (optional)...",rows:2,className:"n
Line truncated
|
||||
`).map(r=>r.trim()?" ".repeat(t)+r:r).join(`
|
||||
`);function _(n){if(n.stepYaml&&n.stepYaml.trim())return n.stepYaml.replace(/\s+$/,"");const t=[];n.label&&n.label!=="Step"&&t.push(`- name: ${b(n.label)}`);const r=t.length?" ":"- ";let s=!t.length;const c=()=>{const i=s?r:" ";return s=!1,i};n.uses&&t.push(`${c()}uses: ${n.uses}`);const a=(n.run??"").replace(/\s+$/,"");a&&(a.includes(`
|
||||
`)?t.push(`${c()}run: |`,...a.split(`
|
||||
`).map(i=>i?" "+i:"")):t.push(`${c()}run: ${a}`));for(const[i,l]of[["with",n.with],["env",n.stepEnv]]){const d=(l??"").replace(/\s+$/,"");d&&t.push(`${c()}${i}:`,...d.split(`
|
||||
`).map(o=>o?" "+o:""))}return t.length||t.push(`- run: ${b(n.label||"step")}`),t.join(`
|
||||
`)}function W(n){const t=n.split(`
|
||||
`),r={},s=p=>{const h=new RegExp(`^(?:- | )${p}:\\s*(.*?)\\s*$`,"m").exec(n);if(!h)return;const u=h[1];return/^".*"$/.test(u)?JSON.parse(u):/^'.*'$/.test(u)?u.slice(1,-1).replace(/''/g,"'"):u},c=p=>{const h=t.findIndex(g=>new RegExp(`^(?:- | )${p}:\\s*(\\|[-+]?|>[-+]?)?\\s*$`).test(g));if(h<0)return;const u=[];for(let g=h+1;g<t.length;g++){const j=t[g];if(j.trim()&&!/^\s{4}/.test(j))break;u.push(j.slice(4))}return u.join(`
|
||||
`).replace(/\s+$/,"")},a=s("name");a&&(r.label=a);const i=s("uses");i&&(r.uses=i);const l=c("run")??s("run");l&&(r.run=l);const d=c("with");d&&(r.with=d);const o=c("env");return o&&(r.stepEnv=o),r}function $e(n){const t=n.replace(/\r\n?/g,`
|
||||
`).split(`
|
||||
`),r=t.findIndex(u=>/^\s+steps:\s*(#.*)?$/.test(u));if(r<0)return null;let s=r+1;for(;s<t.length&&(!t[s].trim()||/^\s*#/.test(t[s]));)s++;const c=/^(\s*)- /.exec(t[s]??"");if(!c)return null;const a=c[1].length,i=[];let l=s;for(;l<t.length;l++){const u=t[l];if(u.trim()&&!/^\s*#/.test(u)){const g=/^\s*/.exec(u)[0].length;if(g<a)break;if(g===a){if(!u.startsWith(" ".repeat(a)+"- "))break;i.push([])}}i.length&&i[i.length-1].push(u.slice(a))}const d=t.slice(0,s).filter(u=>!/^\s*OV_WORKFLOW:/.test(u)).join(`
|
||||
`),o=t.slice(l).join(`
|
||||
`).replace(/\s+$/,""),p=/^name:\s*(.*?)\s*$/m.exec(d),h=p?/^".*"$/.test(p[1])?JSON.parse(p[1]):p[1].replace(/^'|'$/g,""):"";return{frame:{pre:d.replace(/^env:\s*\n(?=\S)/m,""),post:o,indent:a},items:i.map(u=>u.join(`
|
||||
`).replace(/\s+$/,"")),name:h}}function B(n){const t=["- uses: actions/setup-node@v4"," with:"," node-version: 22"];return n&&t.push("- name: ffmpeg, for Cover + Track"," run: |"," command -v ffmpeg >/dev/null || sudo apt-get install -y -qq ffmpeg >/dev/null || apt-get install -y -qq ffmpeg >/dev/null"),t.push("- name: the runner"," run: |",` curl -fsSLo run.mjs ${b(ke)}`),R.startsWith("__")||t.push(` echo "${R} run.mjs" | sha256sum -c -`),t.push(" mkdir -p out"),t}function G(n,t){const r=F[n.data.nodeType],s=[`- name: ${b(`${n.id} (${r?.label??n.data.nodeType})`)}`];return n.data.nodeType==="textInput"?s.push(" run: |",` printf '%s' "$INPUT" > out/${n.id}.txt && echo "${n.id} = $INPUT"`," env:",` INPUT: \${{ inputs.${n.id} }}`):(s.push(` run: node run.mjs --node ${b(n.id)}`),t&&s.push(" env:"," VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }}")),s}function Le(n){const t=Ie(n),r=t.some(o=>o.data.nodeType==="compose"),s=t.filter(o=>o.data.nodeType==="output"),c=`${E}: ${b(Ce(JSON.stringify(Ne(n))))}`;if(n.frame){let o=n.frame.pre.replace(/\s+$/,"");o=/^env:\s*$/m.test(o)?o.replace(/^env:\s*$/m,`env:
|
||||
${c}`):o.replace(/^(jobs:\s*)$/m,`env:
|
||||
${c}
|
||||
$1`),o.includes(E)||(o+=`
|
||||
env:
|
||||
${c}`);const p=[];let h=!1;for(const g of t){if(g.data.nodeType==="step"){p.push(_(g.data));continue}h||(p.push(B(r).join(`
|
||||
`)),h=!0),p.push(G(g,!0).join(`
|
||||
`))}s.length&&p.push(["- name: outputs"," run: |",...s.map(g=>` echo "== ${g.id} =="; cat out/${g.id}.txt; echo`)].join(`
|
||||
`));const u=p.map(g=>P(g,n.frame.indent)).join(`
|
||||
`);return o+`
|
||||
`+u+(n.frame.post?`
|
||||
`+n.frame.post:"")+`
|
||||
`}const a=t.filter(o=>o.data.nodeType==="textInput"),i=[];if(i.push(`name: ${b(n.name)}`),i.push("# An openvenice workflow, exported from https://havre.yolopreneur.lol/open/ (Workflows → Export)."),i.push(`# Import the file there to edit it on the canvas; the graph rides in env.${E} below.`),i.push("# Runs on the Venice API with the secret VENICE_API_KEY. Outputs land in out/ (and an artifact, where the runner keeps them)."),i.push("on:"),i.push(" workflow_dispatch:"),a.length){i.push(" inputs:");for(const o of a)i.push(` ${o.id}:`),i.push(` description: ${b(`Input "${o.data.label||o.id}"`)}`),i.push(" required: false"),i.push(` default: ${b(o.data.inputText??"")}`)}i.push(" # schedule: # uncomment to run on a timer, e.g. every morning at 8:"),i.push(' # - cron: "0 8 * * *"'),i.push("env:"),i.push(` ${c}`),i.push("jobs:"),i.push(" run:"),i.push(" runs-on: ubuntu-latest"),i.push(" timeout-minutes: 60"),i.push(" env:"),i.push(" VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }}"),i.push(" steps:");const l=[],d=t.some(o=>o.data.nodeType!=="step");d&&l.push(B(r).join(`
|
||||
`));for(const o of t)l.push(o.data.nodeType==="step"?_(o.data):G(o,!1).join(`
|
||||
`));return s.length&&l.push(["- name: outputs"," run: |",...s.map(o=>` echo "== ${o.id} =="; cat out/${o.id}.txt; echo`)].join(`
|
||||
`)),d&&l.push(["- uses: actions/upload-artifact@v4"," if: always()"," continue-on-error: true"," with:",` name: ${b(D(n.name))}`," path: out/"].join(`
|
||||
`)),i.push(P(l.join(`
|
||||
`),6)),i.join(`
|
||||
`)+`
|
||||
`}function Ee(n){const t=n.trim();let r=null;const s=t.startsWith("{")?null:$e(t);if(t.startsWith("{"))try{r=JSON.parse(t)}catch{return null}else{const o=new RegExp(`^\\s*${E}:\\s*(?:"([^"]+)"|'([^']+)'|(\\S+))\\s*$`,"m").exec(t);if(o)try{r=JSON.parse(Te(o[1]??o[2]??o[3]))}catch{r=null}}if(!r&&s){const o=s.items.map((h,u)=>({id:`step${u+1}`,type:"venice",position:{x:280,y:40+u*180},data:{label:"Step",nodeType:"step",model:"",prompt:"",...W(h),stepYaml:h}})),p=o.slice(1).map((h,u)=>({id:`e${u}-${o[u].id}-${h.id}`,source:o[u].id,target:h.id,animated:!0}));return o.length?{name:s.name||"Imported workflow",nodes:o,edges:p,frame:s.frame}:null}if(!r||typeof r!="object")return null;const c=r;if(!Array.isArray(c.nodes)||!Array.isArray(c.edges))return null;const a=c.nodes.filter(o=>o&&typeof o.id=="string"&&o.data&&typeof o.data=="object"&&typeof o.data.nodeType=="string"&&o.data.nodeType in F).map((o,p)=>({id:o.id,type:"venice",position:o.position&&typeof o.position=="object"?o.position:{x:280,y:40+p*200},data:{...o.data}})),i=new Set(a.map(o=>o.id)),l=c.edges.filter(o=>o&&typeof o.source=="string"&&typeof o.target=="string"&&i.has(o.source)&&i.has(o.target)).map((o,p)=>({id:typeof o.id=="string"?o.id:`e${p}-${o.source}-${o.target}`,source:o.source,target:o.target,animated:!0}));if(!a.length)return null;let d=c.frame&&typeof c.frame=="object"&&typeof c.frame.pre=="string"?c.frame:void 0;if(s&&d){d=s.frame;const o=new Map(s.items.map(p=>[W(p).label??"",p]));for(const p of a){if(p.data.nodeType!=="step")continue;const h=o.get(p.data.label)??(p.data.stepYaml?s.items.find(u=>u.split(`
|
||||
`)[0]===p.data.stepYaml.split(`
|
||||
`)[0]):void 0);h&&Object.assign(p.data,W(h),{stepYaml:h})}}return{name:typeof c.name=="string"&&c.name.trim()?c.name.trim():"Imported workflow",nodes:a,edges:l,...d?{frame:d}:{}}}function Se(n,t,r="text/yaml"){const s=document.createElement("a");s.href=URL.createObjectURL(new Blob([t],{type:r})),s.download=n,s.click(),setTimeout(()=>URL.revokeObjectURL(s.href),2e3)}const We={venice:ye};function Oe(){return e.jsx("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("polyline",{points:"22 12 16 12 14 15 10 9 8 12 2 12"})})}function Me(){return e.jsx("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("path",{d:"M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"})})}function Ae(){return e.jsxs("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}),e.jsx("circle",{cx:"8.5",cy:"8.5",r:"1.5"}),e.jsx("polyline",{points:"21 15 16 10 5 21"})]})}function Re(){return e.jsxs("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("polygon",{points:"11 5 6 9 2 9 2 15 6 15 11 19 11 5"}),e.jsx("path",{d:"M15.54 8.46a5 5 0 010 7.07"})]})}function Pe(){return e.jsxs("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("path",{d:"M9 18V5l12-2v13"}),e.jsx("circle",{cx:"6",cy:"18",r:"3"}),e.jsx("circle",{cx:"18",cy:"16",r:"3"})]})}function _e(){return e.jsxs("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("polygon",{points:"23 7 16 12 23 17 23 7"}),e.jsx("rect",{x:"1",y:"5",width:"15",height:"14",rx:"2"})]})}function Be(){return e.jsxs("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}),e.jsx("path",{d:"M3 15l5-5 4 4"}),e.jsx("path",{d:"M14 17v-5l4-1v5"}),e.jsx("circle",{cx:"12.5",cy:"17.5",r:"1.5"}),e.jsx("circle",{cx:"16.5",cy:"16.5",r:"1.5"})]})}function Ge(){return e.jsxs("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("polyline",{points:"4 17 10 11 4 5"}),e.jsx("line",{x1:"12",y1:"19",x2:"20",y2:"19"})]})}function Ve(){return e.jsxs("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}),e.jsx("path",{d:"M8 12h8M12 8v8"})]})}const Ye=[{type:"textInput",label:"Input",Icon:Oe,color:"text-blue-400/50"},{type:"chat",label:"LLM",Icon:Me,color:"text-purple-400/50"},{type:"imageGen",label:"Image Gen",Icon:Ae,color:"text-pink-400/50"},{type:"tts",label:"Text to Speech",Icon:Re,color:"text-green-400/50"},{type:"music",label:"Music Gen",Icon:Pe,color:"text-yellow-400/50"},{type:"video",label:"Video Gen",Icon:_e,color:"text-orange-400/50"},{type:"compose",label:"Cover + Track",Icon:Be,color:"text-cyan-400/50"},{type:"step",label:"Step",Icon:Ge,color:"text-white/40"},{type:"output",label:"Output",Icon:Ve,color:"text-white/40"}],Fe={textInput:"",output:"",chat:"llama-3.3-70b",imageGen:"z-image-turbo",tts:"tts-kokoro",music:"stable-audio",video:"wan-2.1",compose:"",step:""},I=n=>Array.from({length:n},()=>V()),m=(n,t)=>({id:`e-${n}-${t}`,source:n,target:t,animated:!0}),De=[{name:"Album Cover",desc:"A song concept → visual art direction → cover artwork",build:()=>{const[n,t,r,s]=I(4);return{nodes:[{id:n,type:"venice",position:{x:280,y:40},data:{label:"Input",nodeType:"textInput",model:"",prompt:"",inputText:"A melancholic indie-folk album about leaving a small coastal town"}},{id:t,type:"venice",position:{x:280,y:220},data:{label:"LLM",nodeType:"chat",model:"llama-3.3-70b",prompt:"You are an art director. Given this album concept, write a vivid, specific image-generation prompt for the cover art — mood, color palette, composition, subject, style. Output only the prompt.",temperature:.9}},{id:r,type:"venice",position:{x:280,y:440},data:{label:"Image Gen",nodeType:"imageGen",model:"z-image-turbo",prompt:"",steps:30,width:1024,height:1024}},{id:s,type:"venice",position:{x:280,y:680},data:{label:"Output",nodeType:"output",model:"",prompt:""}}],edges:[m(n,t),m(t,r),m(r,s)]}}},{name:"Podcast Episode",desc:"Topic → web research → tight 90-second script → narrated audio",build:()=>{const[n,t,r,s,c]=I(5);return{nodes:[
Line truncated
|
||||
@@ -17,8 +17,8 @@
|
||||
/>
|
||||
<title>OpenVenice — clean UI for the Venice AI API</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/open/favicon.svg" />
|
||||
<script type="module" crossorigin src="/open/assets/index-CmWoX_gW.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/open/assets/index-iJLlckSX.css">
|
||||
<script type="module" crossorigin src="/open/assets/index-fmJ2VqHZ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/open/assets/index-DaJeA1VZ.css">
|
||||
</head>
|
||||
<body class="antialiased">
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env node
|
||||
// run.mjs: an openvenice workflow, one node at a time, headless. The same graph the canvas at /open runs in a
|
||||
// browser, run on a machine: a Gitea Actions runner, your laptop, a cron. Node 20+, no dependencies; ffmpeg for a
|
||||
// Cover + Track node.
|
||||
//
|
||||
// node run.mjs --graph graph.json --node <id> [--out out] one node: reads its parents' outputs from out/,
|
||||
// writes out/<id>.txt (and a file for media)
|
||||
// node run.mjs --graph graph.json --all [--out out] every node, in dependency order
|
||||
// OV_WORKFLOW=<base64 json> node run.mjs --node <id> the graph as the exported workflow carries it
|
||||
//
|
||||
// Needs VENICE_API_KEY. A node's output is one string, exactly as in the browser engine: text, or [image:<file>],
|
||||
// [audio:<file>], [video:<file>] for media; a node with several parents gets their outputs joined with blank lines.
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const BASE = process.env.VENICE_BASE_URL || "https://api.venice.ai/api/v1";
|
||||
const KEY = process.env.VENICE_API_KEY || "";
|
||||
const POLL_MS = Number(process.env.OV_POLL_MS || 3000), POLL_MAX = Number(process.env.OV_POLL_MAX || 200);
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const flag = (name, dflt = "") => { const i = args.indexOf("--" + name); return i >= 0 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : dflt; };
|
||||
const has = (name) => args.includes("--" + name);
|
||||
const OUT = flag("out", "out");
|
||||
|
||||
function loadGraph() {
|
||||
const path = flag("graph");
|
||||
const raw = path ? readFileSync(path, "utf8") : process.env.OV_WORKFLOW ? Buffer.from(process.env.OV_WORKFLOW, "base64").toString("utf8") : "";
|
||||
if (!raw) fail("no graph: --graph graph.json, or OV_WORKFLOW in the environment");
|
||||
const g = JSON.parse(raw);
|
||||
const nodes = Array.isArray(g.nodes) ? g.nodes : [], edges = Array.isArray(g.edges) ? g.edges : [];
|
||||
if (!nodes.length) fail("the graph has no nodes");
|
||||
return { name: g.name || "workflow", nodes, edges };
|
||||
}
|
||||
|
||||
function fail(msg) { console.error("run.mjs: " + msg); process.exit(1); }
|
||||
|
||||
async function venice(path, body, { binary = false } = {}) {
|
||||
if (!KEY) fail("VENICE_API_KEY is not set");
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
const res = await fetch(BASE + path, { method: "POST", headers: { Authorization: "Bearer " + KEY, "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
||||
if ([408, 425, 429, 500, 502, 503, 504].includes(res.status) && attempt < 2) { await sleep(1500 * (attempt + 1)); continue; }
|
||||
if (!res.ok) {
|
||||
let detail = "";
|
||||
try { detail = (await res.text()).slice(0, 400); } catch { /* none */ }
|
||||
throw new Error(`Venice ${res.status} on ${path}${detail ? ": " + detail : ""}`);
|
||||
}
|
||||
const type = res.headers.get("content-type") || "";
|
||||
if (binary || !/json/i.test(type)) return { binary: Buffer.from(await res.arrayBuffer()), type };
|
||||
return { json: await res.json(), type };
|
||||
}
|
||||
}
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/** a queued music or video job, asked about until it's done; the file, however Venice sends it */
|
||||
async function retrieve(kind, model, queueId, downloadUrl) {
|
||||
for (let i = 0; i < POLL_MAX; i++) {
|
||||
await sleep(POLL_MS);
|
||||
const r = await venice(`/${kind}/retrieve`, { model, queue_id: queueId });
|
||||
if (r.binary) return { bytes: r.binary, type: r.type };
|
||||
const d = r.json || {}, status = String(d.status || "").toUpperCase(), ready = d.audio_url || d.video_url || d.url;
|
||||
if (status === "COMPLETED") {
|
||||
const url = ready || downloadUrl;
|
||||
if (!url) throw new Error("completed, but Venice sent no file");
|
||||
const f = await fetch(url);
|
||||
if (!f.ok) throw new Error(`download failed (${f.status})`);
|
||||
return { bytes: Buffer.from(await f.arrayBuffer()), type: f.headers.get("content-type") || "" };
|
||||
}
|
||||
if (/FAIL|ERROR|CANCEL|REJECT|EXPIRE/.test(status)) throw new Error(d.error || `generation ${status.toLowerCase()}`);
|
||||
}
|
||||
throw new Error(`timed out after ${(POLL_MS * POLL_MAX) / 1000}s`);
|
||||
}
|
||||
|
||||
const ext = (type, dflt) => (/mp4/.test(type) ? "mp4" : /webm/.test(type) ? "webm" : /mpeg|mp3/.test(type) ? "mp3" : /wav/.test(type) ? "wav" : /ogg|opus/.test(type) ? "ogg" : /flac/.test(type) ? "flac" : /png/.test(type) ? "png" : /jpe?g/.test(type) ? "jpg" : /webp/.test(type) ? "webp" : dflt);
|
||||
|
||||
function resolvePrompt(template, input) {
|
||||
if (!template) return input;
|
||||
if (template.includes("{{input}}")) return template.replace(/\{\{input\}\}/g, input);
|
||||
return input ? `${template}\n\n${input}` : template;
|
||||
}
|
||||
|
||||
function inputsOf(node, graph) {
|
||||
return graph.edges.filter((e) => e.target === node.id).map((e) => {
|
||||
const p = join(OUT, e.source + ".txt");
|
||||
return existsSync(p) ? readFileSync(p, "utf8") : "";
|
||||
}).filter(Boolean).join("\n\n");
|
||||
}
|
||||
|
||||
const mediaPath = (s, tag) => { const m = new RegExp(`\\[${tag}:([^\\]]+)\\]`).exec(s); return m ? m[1] : ""; };
|
||||
|
||||
async function runNode(node, graph) {
|
||||
const d = node.data || {}, input = inputsOf(node, graph), file = (e) => join(OUT, `${node.id}.${e}`);
|
||||
switch (d.nodeType) {
|
||||
case "textInput": return d.inputText ?? "";
|
||||
case "output": return input;
|
||||
case "step": return input; // a runner's own step: the workflow file runs it, not this
|
||||
case "chat": {
|
||||
const r = await venice("/chat/completions", { model: d.model || "llama-3.3-70b", messages: [{ role: "user", content: resolvePrompt(d.prompt, input) }],
|
||||
temperature: d.temperature ?? 0.7, max_tokens: d.maxTokens ?? 4096, venice_parameters: { enable_web_search: d.webSearch ?? "off" } });
|
||||
return r.json?.choices?.[0]?.message?.content ?? "";
|
||||
}
|
||||
case "imageGen": {
|
||||
const body = { model: d.model || "z-image-turbo", prompt: resolvePrompt(d.prompt, input), negative_prompt: d.negativePrompt || undefined, steps: d.steps ?? 20,
|
||||
style_preset: d.style || undefined, width: d.width ?? 1024, height: d.height ?? 1024, hide_watermark: d.hideWatermark ?? true };
|
||||
if (d.aspectRatio) body.aspect_ratio = d.aspectRatio;
|
||||
const r = await venice("/image/generate", body);
|
||||
const img = r.json?.images?.[0], b64 = typeof img === "string" ? img : img?.b64_json;
|
||||
if (!b64) throw new Error("no image came back");
|
||||
const e = b64.startsWith("/9j/") ? "jpg" : b64.startsWith("UklGR") ? "webp" : "png";
|
||||
writeFileSync(file(e), Buffer.from(b64, "base64"));
|
||||
return `[image:${file(e)}]`;
|
||||
}
|
||||
case "tts": {
|
||||
const r = await venice("/audio/speech", { model: d.model || "tts-kokoro", input: resolvePrompt(d.prompt, input), voice: d.voice || "af_sky", speed: d.speed ?? 1,
|
||||
response_format: d.responseFormat || "mp3" }, { binary: true });
|
||||
const e = d.responseFormat || ext(r.type, "mp3");
|
||||
writeFileSync(file(e), r.binary);
|
||||
return `[audio:${file(e)}]`;
|
||||
}
|
||||
case "music": {
|
||||
const body = { model: d.model || "stable-audio-25", prompt: resolvePrompt(d.prompt, input), duration_seconds: d.duration ?? 30, force_instrumental: d.instrumental ?? false };
|
||||
if (d.lyrics) body.lyrics_prompt = d.lyrics;
|
||||
const q = (await venice("/audio/queue", body)).json || {};
|
||||
const { bytes, type } = await retrieve("audio", q.model || body.model, q.queue_id, q.download_url);
|
||||
const e = ext(type, "mp3");
|
||||
writeFileSync(file(e), bytes);
|
||||
return `[audio:${file(e)}]`;
|
||||
}
|
||||
case "video": {
|
||||
const body = { model: d.model || "wan-3-0-text-to-video", prompt: resolvePrompt(d.prompt, input), aspect_ratio: d.videoAspectRatio || "16:9" };
|
||||
if (d.videoDuration) body.duration = d.videoDuration;
|
||||
if (d.videoResolution) body.resolution = d.videoResolution;
|
||||
const q = (await venice("/video/queue", body)).json || {};
|
||||
const { bytes, type } = await retrieve("video", q.model || body.model, q.queue_id || q.id, q.download_url);
|
||||
const e = ext(type, "mp4");
|
||||
writeFileSync(file(e), bytes);
|
||||
return `[video:${file(e)}]`;
|
||||
}
|
||||
case "compose": {
|
||||
const image = mediaPath(input, "image"), audio = mediaPath(input, "audio");
|
||||
if (!image || !audio) throw new Error("Cover + Track needs one image node and one audio node connected to it");
|
||||
const side = Math.max(256, Math.min(2048, Number(d.maxSide) || 1080));
|
||||
const r = spawnSync("ffmpeg", ["-y", "-loglevel", "error", "-loop", "1", "-i", image, "-i", audio, "-c:v", "libx264", "-tune", "stillimage", "-preset", "medium",
|
||||
"-vf", `scale='if(gt(iw,ih),${side},-2)':'if(gt(iw,ih),-2,${side})',pad=ceil(iw/2)*2:ceil(ih/2)*2`, "-c:a", "aac", "-b:a", "160k", "-pix_fmt", "yuv420p", "-shortest", "-movflags", "+faststart", file("mp4")], { encoding: "utf8" });
|
||||
if (r.error) throw new Error("ffmpeg is not installed (a Cover + Track node needs it)");
|
||||
if (r.status !== 0) throw new Error("ffmpeg: " + (r.stderr || "").trim().slice(-400));
|
||||
return `[video:${file("mp4")}]`;
|
||||
}
|
||||
default: throw new Error(`unknown node type ${d.nodeType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** nodes in dependency order (a cycle is an error) */
|
||||
function order(graph) {
|
||||
const indeg = new Map(graph.nodes.map((n) => [n.id, 0])), adj = new Map(graph.nodes.map((n) => [n.id, []]));
|
||||
for (const e of graph.edges) { if (!indeg.has(e.source) || !indeg.has(e.target)) continue; adj.get(e.source).push(e.target); indeg.set(e.target, indeg.get(e.target) + 1); }
|
||||
const out = [];
|
||||
let frontier = graph.nodes.filter((n) => indeg.get(n.id) === 0).map((n) => n.id);
|
||||
while (frontier.length) {
|
||||
out.push(...frontier);
|
||||
const next = [];
|
||||
for (const id of frontier) for (const c of adj.get(id)) { indeg.set(c, indeg.get(c) - 1); if (indeg.get(c) === 0) next.push(c); }
|
||||
frontier = next;
|
||||
}
|
||||
if (out.length !== graph.nodes.length) fail("the graph has a cycle");
|
||||
return out.map((id) => graph.nodes.find((n) => n.id === id));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const graph = loadGraph();
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const which = has("all") ? order(graph) : [graph.nodes.find((n) => n.id === flag("node"))].filter(Boolean);
|
||||
if (!which.length) fail(`no such node: ${flag("node")} (nodes: ${graph.nodes.map((n) => n.id).join(", ")})`);
|
||||
for (const node of which) {
|
||||
const label = `${node.id} [${node.data?.nodeType}]`;
|
||||
process.stdout.write(`${label}… `);
|
||||
const started = Date.now();
|
||||
const result = await runNode(node, graph);
|
||||
writeFileSync(join(OUT, node.id + ".txt"), result);
|
||||
const shown = result.startsWith("[") ? result : result.replace(/\s+/g, " ").slice(0, 120) + (result.length > 120 ? "…" : "");
|
||||
console.log(`${((Date.now() - started) / 1000).toFixed(1)}s ${shown}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => fail(e.message || String(e)));
|
||||
@@ -24,6 +24,15 @@ has changed, so a newer upstream shows exactly which change needs redoing.
|
||||
- **Light and dark**, with Havre's look: Times and Courier, square corners, Havre's blue. The choice is
|
||||
`havre.theme` in localStorage, shared with Havre's own page ("light", "dark", or absent to follow the system).
|
||||
- **Phones**: a labeled "New chat" button, and no ⌘ shortcut hints.
|
||||
- **Workflows and Gitea Actions are two views of one thing.** Export (the active workflow's header) writes a
|
||||
workflow file that walks the graph on a runner with `public/run.mjs` (served at /open/run.mjs); the graph rides in
|
||||
`env.OV_WORKFLOW`. Import (the list view) takes any Actions workflow: one exported from here comes back as it was;
|
||||
any other becomes a chain of **Step** nodes, one per step, and the rest of the file (`on`, permissions,
|
||||
concurrency, the job's `runs-on` and `env`) is kept as the workflow's frame, so it exports back identical, with
|
||||
any Venice nodes spliced in as runner steps. A Step node is one Actions step as it is (run, uses, with, env):
|
||||
inert in the browser, verbatim on export. A **Cover + Track** node records an image over an audio track into one
|
||||
video (in the browser; ffmpeg on a runner). The first real workflow is the factory's venture loop,
|
||||
`.github/workflows/venture-loop.yml` on teahubxyz/time: one step per stage, each `venture.py stage <card> <at>`.
|
||||
|
||||
## Changing it
|
||||
|
||||
|
||||
@@ -734,6 +734,639 @@ edit(S + "components/playground/playground-chat.tsx",
|
||||
(" 'Write a short song with lyrics, then generate it as music',",
|
||||
" 'Write a short song with lyrics, then generate it as music',\n 'Make a cover image and a music track from one idea, then combine them into one video where the cover shows while the track plays',"))
|
||||
|
||||
RUN_MJS = r"""#!/usr/bin/env node
|
||||
// run.mjs: an openvenice workflow, one node at a time, headless. The same graph the canvas at /open runs in a
|
||||
// browser, run on a machine: a Gitea Actions runner, your laptop, a cron. Node 20+, no dependencies; ffmpeg for a
|
||||
// Cover + Track node.
|
||||
//
|
||||
// node run.mjs --graph graph.json --node <id> [--out out] one node: reads its parents' outputs from out/,
|
||||
// writes out/<id>.txt (and a file for media)
|
||||
// node run.mjs --graph graph.json --all [--out out] every node, in dependency order
|
||||
// OV_WORKFLOW=<base64 json> node run.mjs --node <id> the graph as the exported workflow carries it
|
||||
//
|
||||
// Needs VENICE_API_KEY. A node's output is one string, exactly as in the browser engine: text, or [image:<file>],
|
||||
// [audio:<file>], [video:<file>] for media; a node with several parents gets their outputs joined with blank lines.
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const BASE = process.env.VENICE_BASE_URL || "https://api.venice.ai/api/v1";
|
||||
const KEY = process.env.VENICE_API_KEY || "";
|
||||
const POLL_MS = Number(process.env.OV_POLL_MS || 3000), POLL_MAX = Number(process.env.OV_POLL_MAX || 200);
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const flag = (name, dflt = "") => { const i = args.indexOf("--" + name); return i >= 0 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : dflt; };
|
||||
const has = (name) => args.includes("--" + name);
|
||||
const OUT = flag("out", "out");
|
||||
|
||||
function loadGraph() {
|
||||
const path = flag("graph");
|
||||
const raw = path ? readFileSync(path, "utf8") : process.env.OV_WORKFLOW ? Buffer.from(process.env.OV_WORKFLOW, "base64").toString("utf8") : "";
|
||||
if (!raw) fail("no graph: --graph graph.json, or OV_WORKFLOW in the environment");
|
||||
const g = JSON.parse(raw);
|
||||
const nodes = Array.isArray(g.nodes) ? g.nodes : [], edges = Array.isArray(g.edges) ? g.edges : [];
|
||||
if (!nodes.length) fail("the graph has no nodes");
|
||||
return { name: g.name || "workflow", nodes, edges };
|
||||
}
|
||||
|
||||
function fail(msg) { console.error("run.mjs: " + msg); process.exit(1); }
|
||||
|
||||
async function venice(path, body, { binary = false } = {}) {
|
||||
if (!KEY) fail("VENICE_API_KEY is not set");
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
const res = await fetch(BASE + path, { method: "POST", headers: { Authorization: "Bearer " + KEY, "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
||||
if ([408, 425, 429, 500, 502, 503, 504].includes(res.status) && attempt < 2) { await sleep(1500 * (attempt + 1)); continue; }
|
||||
if (!res.ok) {
|
||||
let detail = "";
|
||||
try { detail = (await res.text()).slice(0, 400); } catch { /* none */ }
|
||||
throw new Error(`Venice ${res.status} on ${path}${detail ? ": " + detail : ""}`);
|
||||
}
|
||||
const type = res.headers.get("content-type") || "";
|
||||
if (binary || !/json/i.test(type)) return { binary: Buffer.from(await res.arrayBuffer()), type };
|
||||
return { json: await res.json(), type };
|
||||
}
|
||||
}
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/** a queued music or video job, asked about until it's done; the file, however Venice sends it */
|
||||
async function retrieve(kind, model, queueId, downloadUrl) {
|
||||
for (let i = 0; i < POLL_MAX; i++) {
|
||||
await sleep(POLL_MS);
|
||||
const r = await venice(`/${kind}/retrieve`, { model, queue_id: queueId });
|
||||
if (r.binary) return { bytes: r.binary, type: r.type };
|
||||
const d = r.json || {}, status = String(d.status || "").toUpperCase(), ready = d.audio_url || d.video_url || d.url;
|
||||
if (status === "COMPLETED") {
|
||||
const url = ready || downloadUrl;
|
||||
if (!url) throw new Error("completed, but Venice sent no file");
|
||||
const f = await fetch(url);
|
||||
if (!f.ok) throw new Error(`download failed (${f.status})`);
|
||||
return { bytes: Buffer.from(await f.arrayBuffer()), type: f.headers.get("content-type") || "" };
|
||||
}
|
||||
if (/FAIL|ERROR|CANCEL|REJECT|EXPIRE/.test(status)) throw new Error(d.error || `generation ${status.toLowerCase()}`);
|
||||
}
|
||||
throw new Error(`timed out after ${(POLL_MS * POLL_MAX) / 1000}s`);
|
||||
}
|
||||
|
||||
const ext = (type, dflt) => (/mp4/.test(type) ? "mp4" : /webm/.test(type) ? "webm" : /mpeg|mp3/.test(type) ? "mp3" : /wav/.test(type) ? "wav" : /ogg|opus/.test(type) ? "ogg" : /flac/.test(type) ? "flac" : /png/.test(type) ? "png" : /jpe?g/.test(type) ? "jpg" : /webp/.test(type) ? "webp" : dflt);
|
||||
|
||||
function resolvePrompt(template, input) {
|
||||
if (!template) return input;
|
||||
if (template.includes("{{input}}")) return template.replace(/\{\{input\}\}/g, input);
|
||||
return input ? `${template}\n\n${input}` : template;
|
||||
}
|
||||
|
||||
function inputsOf(node, graph) {
|
||||
return graph.edges.filter((e) => e.target === node.id).map((e) => {
|
||||
const p = join(OUT, e.source + ".txt");
|
||||
return existsSync(p) ? readFileSync(p, "utf8") : "";
|
||||
}).filter(Boolean).join("\n\n");
|
||||
}
|
||||
|
||||
const mediaPath = (s, tag) => { const m = new RegExp(`\\[${tag}:([^\\]]+)\\]`).exec(s); return m ? m[1] : ""; };
|
||||
|
||||
async function runNode(node, graph) {
|
||||
const d = node.data || {}, input = inputsOf(node, graph), file = (e) => join(OUT, `${node.id}.${e}`);
|
||||
switch (d.nodeType) {
|
||||
case "textInput": return d.inputText ?? "";
|
||||
case "output": return input;
|
||||
case "step": return input; // a runner's own step: the workflow file runs it, not this
|
||||
case "chat": {
|
||||
const r = await venice("/chat/completions", { model: d.model || "llama-3.3-70b", messages: [{ role: "user", content: resolvePrompt(d.prompt, input) }],
|
||||
temperature: d.temperature ?? 0.7, max_tokens: d.maxTokens ?? 4096, venice_parameters: { enable_web_search: d.webSearch ?? "off" } });
|
||||
return r.json?.choices?.[0]?.message?.content ?? "";
|
||||
}
|
||||
case "imageGen": {
|
||||
const body = { model: d.model || "z-image-turbo", prompt: resolvePrompt(d.prompt, input), negative_prompt: d.negativePrompt || undefined, steps: d.steps ?? 20,
|
||||
style_preset: d.style || undefined, width: d.width ?? 1024, height: d.height ?? 1024, hide_watermark: d.hideWatermark ?? true };
|
||||
if (d.aspectRatio) body.aspect_ratio = d.aspectRatio;
|
||||
const r = await venice("/image/generate", body);
|
||||
const img = r.json?.images?.[0], b64 = typeof img === "string" ? img : img?.b64_json;
|
||||
if (!b64) throw new Error("no image came back");
|
||||
const e = b64.startsWith("/9j/") ? "jpg" : b64.startsWith("UklGR") ? "webp" : "png";
|
||||
writeFileSync(file(e), Buffer.from(b64, "base64"));
|
||||
return `[image:${file(e)}]`;
|
||||
}
|
||||
case "tts": {
|
||||
const r = await venice("/audio/speech", { model: d.model || "tts-kokoro", input: resolvePrompt(d.prompt, input), voice: d.voice || "af_sky", speed: d.speed ?? 1,
|
||||
response_format: d.responseFormat || "mp3" }, { binary: true });
|
||||
const e = d.responseFormat || ext(r.type, "mp3");
|
||||
writeFileSync(file(e), r.binary);
|
||||
return `[audio:${file(e)}]`;
|
||||
}
|
||||
case "music": {
|
||||
const body = { model: d.model || "stable-audio-25", prompt: resolvePrompt(d.prompt, input), duration_seconds: d.duration ?? 30, force_instrumental: d.instrumental ?? false };
|
||||
if (d.lyrics) body.lyrics_prompt = d.lyrics;
|
||||
const q = (await venice("/audio/queue", body)).json || {};
|
||||
const { bytes, type } = await retrieve("audio", q.model || body.model, q.queue_id, q.download_url);
|
||||
const e = ext(type, "mp3");
|
||||
writeFileSync(file(e), bytes);
|
||||
return `[audio:${file(e)}]`;
|
||||
}
|
||||
case "video": {
|
||||
const body = { model: d.model || "wan-3-0-text-to-video", prompt: resolvePrompt(d.prompt, input), aspect_ratio: d.videoAspectRatio || "16:9" };
|
||||
if (d.videoDuration) body.duration = d.videoDuration;
|
||||
if (d.videoResolution) body.resolution = d.videoResolution;
|
||||
const q = (await venice("/video/queue", body)).json || {};
|
||||
const { bytes, type } = await retrieve("video", q.model || body.model, q.queue_id || q.id, q.download_url);
|
||||
const e = ext(type, "mp4");
|
||||
writeFileSync(file(e), bytes);
|
||||
return `[video:${file(e)}]`;
|
||||
}
|
||||
case "compose": {
|
||||
const image = mediaPath(input, "image"), audio = mediaPath(input, "audio");
|
||||
if (!image || !audio) throw new Error("Cover + Track needs one image node and one audio node connected to it");
|
||||
const side = Math.max(256, Math.min(2048, Number(d.maxSide) || 1080));
|
||||
const r = spawnSync("ffmpeg", ["-y", "-loglevel", "error", "-loop", "1", "-i", image, "-i", audio, "-c:v", "libx264", "-tune", "stillimage", "-preset", "medium",
|
||||
"-vf", `scale='if(gt(iw,ih),${side},-2)':'if(gt(iw,ih),-2,${side})',pad=ceil(iw/2)*2:ceil(ih/2)*2`, "-c:a", "aac", "-b:a", "160k", "-pix_fmt", "yuv420p", "-shortest", "-movflags", "+faststart", file("mp4")], { encoding: "utf8" });
|
||||
if (r.error) throw new Error("ffmpeg is not installed (a Cover + Track node needs it)");
|
||||
if (r.status !== 0) throw new Error("ffmpeg: " + (r.stderr || "").trim().slice(-400));
|
||||
return `[video:${file("mp4")}]`;
|
||||
}
|
||||
default: throw new Error(`unknown node type ${d.nodeType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** nodes in dependency order (a cycle is an error) */
|
||||
function order(graph) {
|
||||
const indeg = new Map(graph.nodes.map((n) => [n.id, 0])), adj = new Map(graph.nodes.map((n) => [n.id, []]));
|
||||
for (const e of graph.edges) { if (!indeg.has(e.source) || !indeg.has(e.target)) continue; adj.get(e.source).push(e.target); indeg.set(e.target, indeg.get(e.target) + 1); }
|
||||
const out = [];
|
||||
let frontier = graph.nodes.filter((n) => indeg.get(n.id) === 0).map((n) => n.id);
|
||||
while (frontier.length) {
|
||||
out.push(...frontier);
|
||||
const next = [];
|
||||
for (const id of frontier) for (const c of adj.get(id)) { indeg.set(c, indeg.get(c) - 1); if (indeg.get(c) === 0) next.push(c); }
|
||||
frontier = next;
|
||||
}
|
||||
if (out.length !== graph.nodes.length) fail("the graph has a cycle");
|
||||
return out.map((id) => graph.nodes.find((n) => n.id === id));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const graph = loadGraph();
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const which = has("all") ? order(graph) : [graph.nodes.find((n) => n.id === flag("node"))].filter(Boolean);
|
||||
if (!which.length) fail(`no such node: ${flag("node")} (nodes: ${graph.nodes.map((n) => n.id).join(", ")})`);
|
||||
for (const node of which) {
|
||||
const label = `${node.id} [${node.data?.nodeType}]`;
|
||||
process.stdout.write(`${label}… `);
|
||||
const started = Date.now();
|
||||
const result = await runNode(node, graph);
|
||||
writeFileSync(join(OUT, node.id + ".txt"), result);
|
||||
const shown = result.startsWith("[") ? result : result.replace(/\s+/g, " ").slice(0, 120) + (result.length > 120 ? "…" : "");
|
||||
console.log(`${((Date.now() - started) / 1000).toFixed(1)}s ${shown}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => fail(e.message || String(e)));
|
||||
"""
|
||||
|
||||
ACTIONS_YAML_TS = r"""// actions-yaml.ts: a workflow as a Gitea Actions workflow file, and back. Two views of one thing:
|
||||
// - a workflow made here exports as a job that walks the graph, one node per step, in dependency order, on
|
||||
// https://havre.yolopreneur.lol/open/run.mjs (the browser engine's node calls, headless);
|
||||
// - any Actions workflow imports as a chain of Step nodes (one per step, each keeping its own YAML), with the rest of
|
||||
// the file (on, permissions, concurrency, the job's runs-on and env) kept as the workflow's frame, so it exports back
|
||||
// as it came in, with any Venice nodes spliced in as runner steps.
|
||||
// The graph also rides in the file (env.OV_WORKFLOW, base64 JSON) so positions and params come back exactly; a Step
|
||||
// node re-reads its YAML from the file on import, so an edit made in the file wins over the canvas's copy.
|
||||
import type { Workflow, VeniceNodeData } from '../stores/workflow-store'
|
||||
import { NODE_SCHEMAS } from './workflow-schema'
|
||||
|
||||
export const RUNNER_URL = 'https://havre.yolopreneur.lol/open/run.mjs'
|
||||
/** sha256 of run.mjs as built beside this page; the exported file checks it before running */
|
||||
export const RUNNER_SHA = '__RUNNER_SHA__'
|
||||
const ENV_KEY = 'OV_WORKFLOW'
|
||||
|
||||
const q = (s: string) => JSON.stringify(String(s))
|
||||
export const slug = (s: string) => (s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'workflow').slice(0, 60)
|
||||
|
||||
/** the file around the steps of an imported Actions workflow: text before the steps list, text after it, and how far in the items sit */
|
||||
export interface Frame { pre: string; post: string; indent: number }
|
||||
|
||||
/** the graph as the file carries it: name, nodes (id, data, position), edges (source, target) and the frame, if any */
|
||||
export function graphOf(wf: Workflow) {
|
||||
return {
|
||||
name: wf.name,
|
||||
nodes: wf.nodes.map((n) => ({ id: n.id, type: n.type, position: n.position, data: n.data })),
|
||||
edges: wf.edges.map((e) => ({ id: e.id, source: e.source, target: e.target })),
|
||||
...(wf.frame ? { frame: wf.frame } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
/** nodes in dependency order (a cycle leaves the rest off) */
|
||||
function ordered(wf: Workflow): Workflow['nodes'] {
|
||||
const indeg = new Map(wf.nodes.map((n) => [n.id, 0])), adj = new Map<string, string[]>(wf.nodes.map((n) => [n.id, []]))
|
||||
for (const e of wf.edges) {
|
||||
if (!indeg.has(e.source) || !indeg.has(e.target)) continue
|
||||
adj.get(e.source)!.push(e.target)
|
||||
indeg.set(e.target, (indeg.get(e.target) ?? 0) + 1)
|
||||
}
|
||||
const out: string[] = []
|
||||
let frontier = wf.nodes.filter((n) => indeg.get(n.id) === 0).map((n) => n.id)
|
||||
while (frontier.length) {
|
||||
out.push(...frontier)
|
||||
const next: string[] = []
|
||||
for (const id of frontier) for (const c of adj.get(id) ?? []) { indeg.set(c, indeg.get(c)! - 1); if (indeg.get(c) === 0) next.push(c) }
|
||||
frontier = next
|
||||
}
|
||||
return out.map((id) => wf.nodes.find((n) => n.id === id)!)
|
||||
}
|
||||
|
||||
const b64 = (s: string) => btoa(String.fromCharCode(...new TextEncoder().encode(s)))
|
||||
const unb64 = (s: string) => new TextDecoder().decode(Uint8Array.from(atob(s.replace(/\s+/g, '')), (c) => c.charCodeAt(0)))
|
||||
const indentBy = (text: string, n: number) => text.split('\n').map((l) => (l.trim() ? ' '.repeat(n) + l : l)).join('\n')
|
||||
|
||||
/** one Step node as YAML list-item lines, at indent 0 ("- name: ..."): its own YAML if it has one, else from its fields */
|
||||
export function stepYaml(d: VeniceNodeData): string {
|
||||
if (d.stepYaml && d.stepYaml.trim()) return d.stepYaml.replace(/\s+$/, '')
|
||||
const lines: string[] = []
|
||||
if (d.label && d.label !== 'Step') lines.push(`- name: ${q(d.label)}`)
|
||||
const head = lines.length ? ' ' : '- '
|
||||
let first = !lines.length
|
||||
const key = () => { const k = first ? head : ' '; first = false; return k }
|
||||
if (d.uses) lines.push(`${key()}uses: ${d.uses}`)
|
||||
const run = (d.run ?? '').replace(/\s+$/, '')
|
||||
if (run) {
|
||||
if (run.includes('\n')) lines.push(`${key()}run: |`, ...run.split('\n').map((l) => (l ? ' ' + l : '')))
|
||||
else lines.push(`${key()}run: ${run}`)
|
||||
}
|
||||
for (const [k, v] of [['with', d.with], ['env', d.stepEnv]] as const) {
|
||||
const body = (v ?? '').replace(/\s+$/, '')
|
||||
if (body) lines.push(`${key()}${k}:`, ...body.split('\n').map((l) => (l ? ' ' + l : '')))
|
||||
}
|
||||
if (!lines.length) lines.push(`- run: ${q(d.label || 'step')}`)
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/** the text of a step item -> name, uses, run (a `|` block joined), with and env (raw lines) */
|
||||
function stepFields(item: string): Partial<VeniceNodeData> {
|
||||
const lines = item.split('\n')
|
||||
const out: Partial<VeniceNodeData> = {}
|
||||
const scalar = (k: string) => {
|
||||
const m = new RegExp(`^(?:- | )${k}:\\s*(.*?)\\s*$`, 'm').exec(item)
|
||||
if (!m) return undefined
|
||||
const v = m[1]
|
||||
return /^".*"$/.test(v) ? JSON.parse(v) : /^'.*'$/.test(v) ? v.slice(1, -1).replace(/''/g, "'") : v
|
||||
}
|
||||
const block = (k: string) => {
|
||||
const i = lines.findIndex((l) => new RegExp(`^(?:- | )${k}:\\s*(\\|[-+]?|>[-+]?)?\\s*$`).test(l))
|
||||
if (i < 0) return undefined
|
||||
const body: string[] = []
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
const l = lines[j]
|
||||
if (l.trim() && !/^\s{4}/.test(l)) break
|
||||
body.push(l.slice(4))
|
||||
}
|
||||
return body.join('\n').replace(/\s+$/, '')
|
||||
}
|
||||
const name = scalar('name'); if (name) out.label = name
|
||||
const uses = scalar('uses'); if (uses) out.uses = uses
|
||||
const run = block('run') ?? scalar('run'); if (run) out.run = run
|
||||
const w = block('with'); if (w) out.with = w
|
||||
const e = block('env'); if (e) out.stepEnv = e
|
||||
return out
|
||||
}
|
||||
|
||||
/** a hand-written Actions file as a frame and its step items; null when it has no steps list */
|
||||
export function frameOf(text: string): { frame: Frame; items: string[]; name: string } | null {
|
||||
const lines = text.replace(/\r\n?/g, '\n').split('\n')
|
||||
const at = lines.findIndex((l) => /^\s+steps:\s*(#.*)?$/.test(l))
|
||||
if (at < 0) return null
|
||||
let i = at + 1
|
||||
while (i < lines.length && (!lines[i].trim() || /^\s*#/.test(lines[i]))) i++
|
||||
const m = /^(\s*)- /.exec(lines[i] ?? '')
|
||||
if (!m) return null
|
||||
const indent = m[1].length
|
||||
const items: string[][] = []
|
||||
let j = i
|
||||
for (; j < lines.length; j++) {
|
||||
const l = lines[j]
|
||||
if (l.trim() && !/^\s*#/.test(l)) {
|
||||
const ind = /^\s*/.exec(l)![0].length
|
||||
if (ind < indent) break
|
||||
if (ind === indent) { if (!l.startsWith(' '.repeat(indent) + '- ')) break; items.push([]) }
|
||||
}
|
||||
if (!items.length) continue
|
||||
items[items.length - 1].push(l.slice(indent))
|
||||
}
|
||||
const pre = lines.slice(0, i).filter((l) => !/^\s*OV_WORKFLOW:/.test(l)).join('\n')
|
||||
const post = lines.slice(j).join('\n').replace(/\s+$/, '')
|
||||
const nm = /^name:\s*(.*?)\s*$/m.exec(pre)
|
||||
const name = nm ? (/^".*"$/.test(nm[1]) ? JSON.parse(nm[1]) : nm[1].replace(/^'|'$/g, '')) : ''
|
||||
return { frame: { pre: pre.replace(/^env:\s*\n(?=\S)/m, ''), post, indent }, items: items.map((it) => it.join('\n').replace(/\s+$/, '')), name }
|
||||
}
|
||||
|
||||
/** the runner steps a framed job needs before its first Venice node */
|
||||
function runnerSteps(needsFfmpeg: boolean): string[] {
|
||||
const s = ['- uses: actions/setup-node@v4', ' with:', ' node-version: 22']
|
||||
if (needsFfmpeg) s.push('- name: ffmpeg, for Cover + Track', ' run: |', ' command -v ffmpeg >/dev/null || sudo apt-get install -y -qq ffmpeg >/dev/null || apt-get install -y -qq ffmpeg >/dev/null')
|
||||
s.push('- name: the runner', ' run: |', ` curl -fsSLo run.mjs ${q(RUNNER_URL)}`)
|
||||
if (RUNNER_SHA && !RUNNER_SHA.startsWith('__')) s.push(` echo "${RUNNER_SHA} run.mjs" | sha256sum -c -`)
|
||||
s.push(' mkdir -p out')
|
||||
return s
|
||||
}
|
||||
|
||||
/** one Venice node as runner step lines at indent 0; `keyed` adds the Venice key to the step (framed jobs have none at job level) */
|
||||
function nodeStep(n: Workflow['nodes'][number], keyed: boolean): string[] {
|
||||
const s = NODE_SCHEMAS[n.data.nodeType]
|
||||
const lines = [`- name: ${q(`${n.id} (${s?.label ?? n.data.nodeType})`)}`]
|
||||
if (n.data.nodeType === 'textInput') {
|
||||
lines.push(' run: |', ` printf '%s' "$INPUT" > out/${n.id}.txt && echo "${n.id} = $INPUT"`, ' env:', ` INPUT: \${{ inputs.${n.id} }}`)
|
||||
} else {
|
||||
lines.push(` run: node run.mjs --node ${q(n.id)}`)
|
||||
if (keyed) lines.push(' env:', ' VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }}')
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
export function toActionsYaml(wf: Workflow): string {
|
||||
const nodes = ordered(wf)
|
||||
const needsFfmpeg = nodes.some((n) => n.data.nodeType === 'compose')
|
||||
const outputs = nodes.filter((n) => n.data.nodeType === 'output')
|
||||
const graph = `${ENV_KEY}: ${q(b64(JSON.stringify(graphOf(wf))))}`
|
||||
if (wf.frame) {
|
||||
// the file as it came in, its steps replaced by the graph's: Step nodes as their own YAML, Venice nodes as runner steps
|
||||
let pre = wf.frame.pre.replace(/\s+$/, '')
|
||||
pre = /^env:\s*$/m.test(pre) ? pre.replace(/^env:\s*$/m, `env:\n ${graph}`) : pre.replace(/^(jobs:\s*)$/m, `env:\n ${graph}\n$1`)
|
||||
if (!pre.includes(ENV_KEY)) pre += `\nenv:\n ${graph}`
|
||||
const body: string[] = []
|
||||
let runnerIn = false
|
||||
for (const n of nodes) {
|
||||
if (n.data.nodeType === 'step') { body.push(stepYaml(n.data)); continue }
|
||||
if (!runnerIn) { body.push(runnerSteps(needsFfmpeg).join('\n')); runnerIn = true }
|
||||
body.push(nodeStep(n, true).join('\n'))
|
||||
}
|
||||
if (outputs.length) body.push(['- name: outputs', ' run: |', ...outputs.map((n) => ` echo "== ${n.id} =="; cat out/${n.id}.txt; echo`)].join('\n'))
|
||||
const steps = body.map((b) => indentBy(b, wf.frame!.indent)).join('\n')
|
||||
return pre + '\n' + steps + (wf.frame.post ? '\n' + wf.frame.post : '') + '\n'
|
||||
}
|
||||
const inputs = nodes.filter((n) => n.data.nodeType === 'textInput')
|
||||
const lines: string[] = []
|
||||
lines.push(`name: ${q(wf.name)}`)
|
||||
lines.push(`# An openvenice workflow, exported from https://havre.yolopreneur.lol/open/ (Workflows → Export).`)
|
||||
lines.push(`# Import the file there to edit it on the canvas; the graph rides in env.${ENV_KEY} below.`)
|
||||
lines.push(`# Runs on the Venice API with the secret VENICE_API_KEY. Outputs land in out/ (and an artifact, where the runner keeps them).`)
|
||||
lines.push(`on:`)
|
||||
lines.push(` workflow_dispatch:`)
|
||||
if (inputs.length) {
|
||||
lines.push(` inputs:`)
|
||||
for (const n of inputs) {
|
||||
lines.push(` ${n.id}:`)
|
||||
lines.push(` description: ${q(`Input "${n.data.label || n.id}"`)}`)
|
||||
lines.push(` required: false`)
|
||||
lines.push(` default: ${q(n.data.inputText ?? '')}`)
|
||||
}
|
||||
}
|
||||
lines.push(` # schedule: # uncomment to run on a timer, e.g. every morning at 8:`)
|
||||
lines.push(` # - cron: "0 8 * * *"`)
|
||||
lines.push(`env:`)
|
||||
lines.push(` ${graph}`)
|
||||
lines.push(`jobs:`)
|
||||
lines.push(` run:`)
|
||||
lines.push(` runs-on: ubuntu-latest`)
|
||||
lines.push(` timeout-minutes: 60`)
|
||||
lines.push(` env:`)
|
||||
lines.push(` VENICE_API_KEY: \${{ secrets.VENICE_API_KEY }}`)
|
||||
lines.push(` steps:`)
|
||||
const body: string[] = []
|
||||
const anyVenice = nodes.some((n) => n.data.nodeType !== 'step')
|
||||
if (anyVenice) body.push(runnerSteps(needsFfmpeg).join('\n'))
|
||||
for (const n of nodes) body.push(n.data.nodeType === 'step' ? stepYaml(n.data) : nodeStep(n, false).join('\n'))
|
||||
if (outputs.length) body.push(['- name: outputs', ' run: |', ...outputs.map((n) => ` echo "== ${n.id} =="; cat out/${n.id}.txt; echo`)].join('\n'))
|
||||
if (anyVenice) body.push(['- uses: actions/upload-artifact@v4', ' if: always()', ' continue-on-error: true', ' with:', ` name: ${q(slug(wf.name))}`, ' path: out/'].join('\n'))
|
||||
lines.push(indentBy(body.join('\n'), 6))
|
||||
return lines.join('\n') + '\n'
|
||||
}
|
||||
|
||||
export type Imported = { name: string; nodes: Workflow['nodes']; edges: Workflow['edges']; frame?: Frame }
|
||||
|
||||
/** the graph an exported file carries (also a plain graph JSON), or any Actions file as a chain of Step nodes; null if neither */
|
||||
export function fromActionsYaml(text: string): Imported | null {
|
||||
const raw = text.trim()
|
||||
let g: unknown = null
|
||||
const framed = raw.startsWith('{') ? null : frameOf(raw)
|
||||
if (raw.startsWith('{')) {
|
||||
try { g = JSON.parse(raw) } catch { return null }
|
||||
} else {
|
||||
const m = new RegExp(`^\\s*${ENV_KEY}:\\s*(?:"([^"]+)"|'([^']+)'|(\\S+))\\s*$`, 'm').exec(raw)
|
||||
if (m) { try { g = JSON.parse(unb64(m[1] ?? m[2] ?? m[3])) } catch { g = null } }
|
||||
}
|
||||
if (!g && framed) {
|
||||
// a hand-written workflow: one Step node per step, chained in the file's order
|
||||
const nodes = framed.items.map((item, i) => ({
|
||||
id: `step${i + 1}`, type: 'venice', position: { x: 280, y: 40 + i * 180 },
|
||||
data: { label: 'Step', nodeType: 'step', model: '', prompt: '', ...stepFields(item), stepYaml: item } as VeniceNodeData,
|
||||
}))
|
||||
const edges = nodes.slice(1).map((n, i) => ({ id: `e${i}-${nodes[i].id}-${n.id}`, source: nodes[i].id, target: n.id, animated: true }))
|
||||
if (!nodes.length) return null
|
||||
return { name: framed.name || 'Imported workflow', nodes, edges, frame: framed.frame }
|
||||
}
|
||||
if (!g || typeof g !== 'object') return null
|
||||
const o = g as { name?: unknown; nodes?: unknown; edges?: unknown; frame?: unknown }
|
||||
if (!Array.isArray(o.nodes) || !Array.isArray(o.edges)) return null
|
||||
const nodes = (o.nodes as Array<Record<string, unknown>>).filter((n) => n && typeof n.id === 'string' && n.data && typeof n.data === 'object' && typeof (n.data as { nodeType?: unknown }).nodeType === 'string' && (n.data as { nodeType: string }).nodeType in NODE_SCHEMAS)
|
||||
.map((n, i) => ({ id: n.id as string, type: 'venice', position: (n.position && typeof n.position === 'object' ? n.position : { x: 280, y: 40 + i * 200 }) as { x: number; y: number }, data: { ...(n.data as VeniceNodeData) } }))
|
||||
const ids = new Set(nodes.map((n) => n.id))
|
||||
const edges = (o.edges as Array<Record<string, unknown>>).filter((e) => e && typeof e.source === 'string' && typeof e.target === 'string' && ids.has(e.source) && ids.has(e.target))
|
||||
.map((e, i) => ({ id: typeof e.id === 'string' ? e.id : `e${i}-${e.source}-${e.target}`, source: e.source as string, target: e.target as string, animated: true }))
|
||||
if (!nodes.length) return null
|
||||
let frame = o.frame && typeof o.frame === 'object' && typeof (o.frame as Frame).pre === 'string' ? (o.frame as Frame) : undefined
|
||||
if (framed && frame) {
|
||||
// a framed file is the other view of the same thing: its frame and its steps' YAML win over the graph's copies
|
||||
// (a file exported from a plain canvas workflow has no frame: the graph alone is the workflow)
|
||||
frame = framed.frame
|
||||
const byName = new Map<string, string>(framed.items.map((it) => [stepFields(it).label ?? '', it]))
|
||||
for (const n of nodes) {
|
||||
if (n.data.nodeType !== 'step') continue
|
||||
const it = byName.get(n.data.label) ?? (n.data.stepYaml ? framed.items.find((x) => x.split('\n')[0] === n.data.stepYaml!.split('\n')[0]) : undefined)
|
||||
if (it) Object.assign(n.data, stepFields(it), { stepYaml: it })
|
||||
}
|
||||
}
|
||||
return { name: typeof o.name === 'string' && o.name.trim() ? o.name.trim() : 'Imported workflow', nodes, edges, ...(frame ? { frame } : {}) }
|
||||
}
|
||||
|
||||
export function downloadText(name: string, text: string, type = 'text/yaml') {
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(new Blob([text], { type }))
|
||||
a.download = name
|
||||
a.click()
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 2000)
|
||||
}
|
||||
"""
|
||||
|
||||
# ---------- workflows: export as a Gitea Actions workflow file, and import one back; the headless runner ----------
|
||||
# The graph rides in the file (env.OV_WORKFLOW), so import reads back exactly what was exported; the steps walk the
|
||||
# same graph with public/run.mjs (served at /open/run.mjs), pinned by sha256 in the exported file.
|
||||
import hashlib as _hashlib
|
||||
write("public/run.mjs", RUN_MJS)
|
||||
write(S + "lib/actions-yaml.ts", ACTIONS_YAML_TS.replace("__RUNNER_SHA__", _hashlib.sha256(RUN_MJS.encode("utf-8")).hexdigest()))
|
||||
|
||||
P = S + "components/workflows/workflows-view.tsx"
|
||||
edit(P,
|
||||
("import { toast } from '../../stores/toast-store'\n",
|
||||
"import { toast } from '../../stores/toast-store'\nimport { toActionsYaml, fromActionsYaml, downloadText, slug as wfSlug } from '../../lib/actions-yaml'\n"),
|
||||
(""" <span className="text-[14px] text-white/50 font-medium">
|
||||
{workflows.find((w) => w.id === activeWorkflowId)?.name}
|
||||
</span>
|
||||
</div>""", """ <span className="text-[14px] text-white/50 font-medium">
|
||||
{workflows.find((w) => w.id === activeWorkflowId)?.name}
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
type="button"
|
||||
title="This workflow as a Gitea Actions workflow file: run it on the factory, on a timer, or import it back here"
|
||||
onClick={() => {
|
||||
const wf = workflows.find((w) => w.id === activeWorkflowId)
|
||||
if (!wf) return
|
||||
downloadText(wfSlug(wf.name) + '.yml', toActionsYaml(wf))
|
||||
toast.success('Exported', 'Commit it under .gitea/workflows/ or .github/workflows/ in a repo with the secret VENICE_API_KEY.')
|
||||
}}
|
||||
className="text-[13px] text-white/35 hover:text-white/70 transition-colors px-2 py-1 border border-white/[0.08]"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
</div>"""),
|
||||
(""" <button
|
||||
onClick={() => handleCreate(newName)}
|
||||
className="text-[14px] font-medium px-4 py-2 rounded-lg bg-white text-black hover:bg-white/90 transition-colors"
|
||||
>
|
||||
New Workflow
|
||||
</button>
|
||||
</div>""", """ <button
|
||||
onClick={() => handleCreate(newName)}
|
||||
className="text-[14px] font-medium px-4 py-2 rounded-lg bg-white text-black hover:bg-white/90 transition-colors"
|
||||
>
|
||||
New Workflow
|
||||
</button>
|
||||
<label
|
||||
title="A workflow file exported from here (.yml), or a graph as JSON"
|
||||
className="text-[14px] font-medium px-4 py-2 rounded-lg border border-white/[0.12] text-white/60 hover:text-white/90 transition-colors cursor-pointer"
|
||||
>
|
||||
Import
|
||||
<input
|
||||
type="file"
|
||||
accept=".yml,.yaml,.json,text/yaml,application/json"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const f = e.target.files?.[0]
|
||||
e.target.value = ''
|
||||
if (!f) return
|
||||
const got = fromActionsYaml(await f.text())
|
||||
if (!got) { toast.error('No workflow in that file', 'Import a .yml exported from here, or a graph as JSON.'); return }
|
||||
const id = createWorkflow(got.name)
|
||||
useWorkflowStore.getState().updateWorkflow(id, { nodes: got.nodes, edges: got.edges })
|
||||
toast.success(`Imported ${got.name}`, `${got.nodes.length} nodes`)
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>"""))
|
||||
|
||||
# ---------- step node: a literal Actions step, so any workflow file is a chain of nodes and comes back as it went ----------
|
||||
# A Step node carries one step's YAML (run, uses, with, env). In the browser it does nothing (it passes upstream text
|
||||
# through); on export it is itself, verbatim; a workflow imported from a hand-written file keeps that file's frame
|
||||
# (on, permissions, concurrency, runs-on, job env) and exports back into it, with Venice nodes spliced in as runner steps.
|
||||
edit(S + "stores/workflow-store.ts",
|
||||
("export type VeniceNodeType = 'chat' | 'imageGen' | 'tts' | 'music' | 'video' | 'compose' | 'textInput' | 'output'",
|
||||
"export type VeniceNodeType = 'chat' | 'imageGen' | 'tts' | 'music' | 'video' | 'compose' | 'step' | 'textInput' | 'output'"),
|
||||
(" // Compose (cover + track)\n maxSide?: number\n",
|
||||
" // Compose (cover + track)\n maxSide?: number\n // Step (an Actions step as it is): its fields, and its YAML as it came from a file\n run?: string\n uses?: string\n with?: string\n stepEnv?: string\n stepYaml?: string\n"),
|
||||
(" edges: Edge[]\n createdAt: number\n}",
|
||||
" edges: Edge[]\n createdAt: number\n /** an imported Actions file's text around its steps: the workflow exports back into it */\n frame?: { pre: string; post: string; indent: number }\n}"),
|
||||
(" updateWorkflow: (id: string, updates: Partial<Pick<Workflow, 'name' | 'nodes' | 'edges'>>) => void",
|
||||
" updateWorkflow: (id: string, updates: Partial<Pick<Workflow, 'name' | 'nodes' | 'edges' | 'frame'>>) => void"))
|
||||
|
||||
edit(S + "lib/workflow-schema.ts",
|
||||
(""" compose: {
|
||||
type: 'compose',""", """ step: {
|
||||
type: 'step',
|
||||
label: 'Step',
|
||||
description: 'A Gitea Actions step as it is (run, uses, with, env). Does nothing in the browser: it passes upstream text through. On export it is this step, verbatim, in the workflow file.',
|
||||
input: 'text',
|
||||
output: 'text',
|
||||
params: [
|
||||
{ name: 'run', type: 'text', description: 'Shell to run on the runner.', default: '' },
|
||||
{ name: 'uses', type: 'string', description: 'An action to use instead of run, e.g. actions/checkout@v5.', default: '' },
|
||||
],
|
||||
},
|
||||
compose: {
|
||||
type: 'compose',"""))
|
||||
|
||||
edit(S + "lib/workflow-engine.ts",
|
||||
(""" case 'output':
|
||||
return input
|
||||
""", """ case 'output':
|
||||
return input
|
||||
|
||||
case 'step':
|
||||
return input // a runner's step: nothing to do in a browser
|
||||
"""))
|
||||
|
||||
P = S + "components/workflows/workflows-view.tsx"
|
||||
edit(P,
|
||||
(" { type: 'compose', label: 'Cover + Track', Icon: PaletteComposeIcon, color: 'text-cyan-400/50' },\n",
|
||||
" { type: 'compose', label: 'Cover + Track', Icon: PaletteComposeIcon, color: 'text-cyan-400/50' },\n { type: 'step', label: 'Step', Icon: PaletteStepIcon, color: 'text-white/40' },\n"),
|
||||
("function PaletteOutputIcon() {",
|
||||
"""function PaletteStepIcon() {
|
||||
return <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 17 10 11 4 5" /><line x1="12" y1="19" x2="20" y2="19" /></svg>
|
||||
}
|
||||
function PaletteOutputIcon() {"""),
|
||||
(" compose: '',\n}", " compose: '',\n step: '',\n}"),
|
||||
(" useWorkflowStore.getState().updateWorkflow(id, { nodes: got.nodes, edges: got.edges })",
|
||||
" useWorkflowStore.getState().updateWorkflow(id, { nodes: got.nodes, edges: got.edges, frame: got.frame })"),
|
||||
(" if (!got) { toast.error('No workflow in that file', 'Import a .yml exported from here, or a graph as JSON.'); return }",
|
||||
" if (!got) { toast.error('No workflow in that file', 'Import an Actions workflow (.yml) or a graph as JSON.'); return }"),
|
||||
(""" title="A workflow file exported from here (.yml), or a graph as JSON\"""",
|
||||
""" title="Any Gitea Actions workflow (.yml): one exported from here comes back as it was; any other becomes a chain of Step nodes. Or a graph as JSON\""""))
|
||||
|
||||
P = S + "components/workflows/workflow-node.tsx"
|
||||
edit(P,
|
||||
(" compose: undefined,\n}", " compose: undefined,\n step: undefined,\n}"),
|
||||
(" compose: { label: 'Cover + Track', Icon: ComposeIcon, color: 'border-cyan-500/30', hasInput: true, hasOutput: true },\n",
|
||||
" compose: { label: 'Cover + Track', Icon: ComposeIcon, color: 'border-cyan-500/30', hasInput: true, hasOutput: true },\n step: { label: 'Step', Icon: StepIcon, color: 'border-white/20', hasInput: true, hasOutput: true },\n"),
|
||||
("\nfunction OutputIcon() {", """
|
||||
function StepIcon() {
|
||||
return <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 17 10 11 4 5" /><line x1="12" y1="19" x2="20" y2="19" /></svg>
|
||||
}
|
||||
|
||||
function OutputIcon() {"""),
|
||||
(""" <span className="text-[14px] font-medium text-white/60">{config.label}</span>""",
|
||||
""" <span className="text-[14px] font-medium text-white/60">{data.nodeType === 'step' && data.label && data.label !== 'Step' ? data.label : config.label}</span>"""),
|
||||
(""" ) : data.nodeType === 'compose' ? (""", """ ) : data.nodeType === 'step' ? (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<input
|
||||
value={data.label === 'Step' ? '' : data.label}
|
||||
onChange={(e) => updateNode({ label: e.target.value || 'Step', stepYaml: '' })}
|
||||
placeholder="name (optional)"
|
||||
className={inputCls}
|
||||
/>
|
||||
<input
|
||||
value={data.uses ?? ''}
|
||||
onChange={(e) => updateNode({ uses: e.target.value, stepYaml: '' })}
|
||||
placeholder="uses: owner/action@v1 (optional)"
|
||||
className={inputCls}
|
||||
/>
|
||||
<textarea
|
||||
value={data.run ?? ''}
|
||||
onChange={(e) => updateNode({ run: e.target.value, stepYaml: '' })}
|
||||
placeholder="run: shell for the runner"
|
||||
rows={3}
|
||||
className="nodrag nowheel w-full bg-white/[0.03] border border-white/[0.06] rounded-md px-2 py-1.5 text-[13px] font-mono text-white/70 outline-none resize-none placeholder:text-white/15"
|
||||
/>
|
||||
{(data.with || data.stepEnv) && (
|
||||
<pre className="nodrag nowheel text-[12px] font-mono text-white/40 whitespace-pre-wrap max-h-24 overflow-auto">{[data.with && 'with:\\n' + data.with, data.stepEnv && 'env:\\n' + data.stepEnv].filter(Boolean).join('\\n')}</pre>
|
||||
)}
|
||||
<p className="text-[12px] text-white/30 leading-snug">Runs on the factory, not here: on export this is the step as written.</p>
|
||||
</div>
|
||||
) : data.nodeType === 'compose' ? ("""))
|
||||
|
||||
edit(S + "components/playground/preview-node.tsx",
|
||||
(" compose: 'border-cyan-500/30',\n", " compose: 'border-cyan-500/30',\n step: 'border-white/20',\n"))
|
||||
|
||||
edit(S + "lib/playground-agent-tools.ts",
|
||||
(" return true // textInput / output / compose don't have models",
|
||||
" return true // textInput / output / compose / step don't have models"))
|
||||
|
||||
if FAILED:
|
||||
sys.exit("havre.py: upstream no longer matches:\n " + "\n ".join(FAILED))
|
||||
print("havre.py: applied")
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 78 KiB |
@@ -34,8 +34,7 @@ body {
|
||||
color: var(--ink);
|
||||
font: 19px/1.35 var(--serif);
|
||||
}
|
||||
a { color: var(--link); }
|
||||
a:visited { color: var(--visited); }
|
||||
a, a:visited { color: var(--link); }
|
||||
a:hover, a:focus-visible { color: var(--ink); background: var(--hi); }
|
||||
:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
|
||||
p { margin: 0; }
|
||||
@@ -311,13 +310,18 @@ pre.saw {
|
||||
.line.media .words { white-space: pre-wrap; }
|
||||
.pic { margin: 0; border: 1px solid var(--ink); width: min(100%, 340px); }
|
||||
.pic img { width: 100%; height: auto; max-height: 440px; object-fit: contain; background: var(--well); cursor: zoom-in; }
|
||||
.lightbox { padding: 0; border: 1px solid var(--ink); background: var(--paper); max-width: 96vw; max-height: 96vh; }
|
||||
/* the picture, big: it takes focus when it opens, and the focus ring (link blue) read as a blue background around it */
|
||||
.lightbox { padding: 0; border: 0; background: transparent; max-width: 96vw; max-height: 96vh; outline: none; }
|
||||
.lightbox:focus, .lightbox:focus-visible { outline: none; }
|
||||
.lightbox::backdrop { background: rgba(0, 0, 0, 0.88); }
|
||||
.lightbox img { max-width: 96vw; max-height: 94vh; object-fit: contain; cursor: zoom-out; }
|
||||
.newcard label { display: block; margin: 12px 0; font: 14px var(--mono); }
|
||||
.newcard input, .newcard textarea { display: block; width: 100%; margin-top: 4px; padding: 6px 8px; font: 16px var(--serif);
|
||||
border: 1px solid var(--ink); border-radius: 0; background: var(--paper); color: var(--ink); }
|
||||
tr.opens { cursor: pointer; }
|
||||
#roster tr.newrow .rowname { font-weight: 700; }
|
||||
#roster tr.newrow[aria-current="true"] { background: var(--ink); color: var(--paper); }
|
||||
#roster tr.newrow[aria-current="true"] .linkish { color: var(--paper); }
|
||||
tr.opens:hover { background: var(--hi); }
|
||||
.rowname { font: inherit; color: inherit; text-decoration: none; }
|
||||
.rewrite { display: flex; gap: 8px; margin: 6px 0; }
|
||||
@@ -491,13 +495,16 @@ p.note { padding: 8px 10px; border: 1px solid var(--ink); background: var(--hi);
|
||||
/* ---------- narrow screens ---------- */
|
||||
@media (max-width: 1040px) {
|
||||
.chars { border-right: 0; border-bottom: 1px solid var(--ink); }
|
||||
/* one at a time, like a phone's messages: the list, or the chat with a way back to it */
|
||||
.studio.pane-list .chat, .studio.pane-chat .chars { display: none; }
|
||||
/* one at a time, like a phone's messages: the list, or the chat. In the chat the left column's tabs stay above it,
|
||||
without their panels, so CHATS / CHARACTERS / HARBOR are always one tap away */
|
||||
.studio.pane-list .chat { display: none; }
|
||||
.studio.pane-chat .chars [role=tabpanel] { display: none; }
|
||||
.studio.pane-list .chars { border-bottom: 0; }
|
||||
.chat .back { display: block; flex-basis: 100%; font: 14px var(--mono); }
|
||||
.chat .back { display: none; }
|
||||
.grid { margin-right: 0; }
|
||||
.chat { position: static; max-height: none; }
|
||||
.log, .chat.asking .log { height: max(320px, 58vh); }
|
||||
/* the conversation takes what the screen has left under the tabs and above the composer */
|
||||
.log, .chat.asking .log { height: max(360px, calc(100dvh - 300px)); }
|
||||
.ask { max-height: none; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
@@ -509,7 +516,12 @@ p.note { padding: 8px 10px; border: 1px solid var(--ink); background: var(--hi);
|
||||
.keybar { gap: 8px 10px; }
|
||||
.keybar input:not([type=checkbox]) { flex: 1 1 100%; }
|
||||
.keyfine, .keystatus { margin-left: 0; }
|
||||
.sechead h2, .lefthead [role=tab], .lefthead .sep { font-size: 28px; }
|
||||
.sechead h2 { font-size: 28px; }
|
||||
/* the three tabs on one line: smaller, a dot between them instead of a slash */
|
||||
.lefthead [role=tablist] { flex-wrap: nowrap; gap: 0 10px; }
|
||||
.lefthead [role=tab] { font-size: 21px; white-space: nowrap; }
|
||||
.lefthead .sep { font-size: 0; }
|
||||
.lefthead .sep::before { content: "\00b7"; font: 700 21px/1 var(--serif); }
|
||||
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.grid > li:nth-child(2n) { border-right: 0; }
|
||||
.line { grid-template-columns: minmax(0, 1fr); gap: 0; }
|
||||
@@ -535,8 +547,17 @@ p.note { padding: 8px 10px; border: 1px solid var(--ink); background: var(--hi);
|
||||
}
|
||||
/* a small phone (an iPhone SE is 375px wide): the left column's tabs a little smaller, so a count of what's new fits */
|
||||
@media (max-width: 400px) {
|
||||
.lefthead [role=tab], .lefthead .sep { font-size: 24px; }
|
||||
.lefthead [role=tablist] { gap: 0 8px; }
|
||||
.lefthead [role=tab] { font-size: 19px; }
|
||||
.lefthead .sep::before { font-size: 19px; }
|
||||
}
|
||||
/* a new version is out: a bar at the top to reload (update.js) */
|
||||
#upd {
|
||||
position: fixed; top: calc(10px + env(safe-area-inset-top)); left: 50%; transform: translateX(-50%); z-index: 30;
|
||||
padding: 9px 16px; border: 1px solid var(--paper); border-radius: 0; background: var(--ink); color: var(--paper);
|
||||
font: 700 13px/1 var(--mono); letter-spacing: .12em; white-space: nowrap; cursor: pointer;
|
||||
}
|
||||
#upd:hover, #upd:focus-visible { background: var(--hi); color: var(--ink); border-color: var(--ink); }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { scroll-behavior: auto !important; }
|
||||
}
|
||||
@@ -284,10 +284,11 @@ test("the conversation as the model reads it: a photo it sent is mentioned to it
|
||||
{ role: "user", content: "hello there", media: { kind: "voice-in", id: "u2" } },
|
||||
];
|
||||
const api = A.apiHistory(hist);
|
||||
assert.deepEqual(api.map((m) => m.role), ["user", "user", "user", "assistant", "user"]);
|
||||
assert.equal(api[1].content, "(You sent me a photo: On the deck.) (You sent me a short video: Waving.)\n\nlovely");
|
||||
assert.equal(api[2].content, "[a photo: A gull on a post.] what's this?");
|
||||
assert.equal(api[4].content, "hello there");
|
||||
assert.deepEqual(api.map((m) => m.role), ["user", "assistant", "user", "user", "assistant", "user"]);
|
||||
assert.equal(api[1].content, "*sends a photo: On the deck*\n\n*sends a short video: Waving*");
|
||||
assert.equal(api[2].content, "lovely");
|
||||
assert.equal(api[3].content, "[a photo: A gull on a post.] what's this?");
|
||||
assert.equal(api[5].content, "hello there");
|
||||
assert.ok(!api.some((m) => /\[photo\]/.test(m.content)), "the model never sees a photo placeholder in its own voice");
|
||||
const seen = A.withPicture([{ role: "system", content: "s" }, { role: "user", content: "[a photo] look" }], "data:image/jpeg;base64,AAAA");
|
||||
assert.deepEqual(seen[1].content[1], { type: "image_url", image_url: { url: "data:image/jpeg;base64,AAAA" } });
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import * as B from "../backup.js";
|
||||
import * as R from "../memory.js";
|
||||
import { normalizeCard } from "../card.js";
|
||||
|
||||
const ines = normalizeCard({ name: "Ines Corbel", persona: "You are Ines." });
|
||||
const wren = normalizeCard({ name: "Wren", persona: "You are Wren." });
|
||||
const threads = [
|
||||
{ id: "t1", card: "ines-corbel", msgs: [{ id: "a", role: "user", content: "hi" }, { id: "b", role: "assistant", content: "", media: { kind: "photo", id: "p1" } }] },
|
||||
{ id: "t2", card: "wren", msgs: [{ id: "c", role: "assistant", content: "", media: { kind: "photo", id: "p2" } }] },
|
||||
];
|
||||
const media = { p1: "data:image/webp;base64,AAAA", p2: "data:image/webp;base64,BBBB", p9: "data:image/webp;base64,CCCC" };
|
||||
|
||||
test("one character's bundle holds their card, memory, chats and only those chats' pictures", () => {
|
||||
const mem = { notes: [R.note("They hate fog.", { at: 5 })], upTo: { t1: "b" }, at: 5 };
|
||||
const b = B.bundleOne(ines, { memory: mem, threads, media, at: 9 });
|
||||
assert.equal(b.havre_backup, 1);
|
||||
assert.deepEqual(b.cards.map((c) => c.id), ["ines-corbel"]);
|
||||
assert.deepEqual(Object.keys(b.memory), ["ines-corbel"]);
|
||||
assert.deepEqual(b.threads.map((t) => t.id), ["t1"]);
|
||||
assert.deepEqual(Object.keys(b.media), ["p1"]);
|
||||
assert.equal(B.bundleName(ines), "ines-corbel.havre-full.json");
|
||||
assert.match(B.bundleName(null, Date.UTC(2026, 9, 20)), /^havre-backup-2026-10-20\.json$/);
|
||||
});
|
||||
|
||||
test("the whole harbor: everything, and the settings, never a key", () => {
|
||||
const b = B.bundleAll({ cards: [ines, wren], memory: {}, threads, media, settings: { abilities: { own: true } } });
|
||||
assert.equal(b.cards.length, 2);
|
||||
assert.equal(b.threads.length, 2);
|
||||
assert.deepEqual(Object.keys(b.media).sort(), ["p1", "p2", "p9"]);
|
||||
assert.deepEqual(b.settings, { abilities: { own: true } });
|
||||
assert.ok(!JSON.stringify(b).includes("key"));
|
||||
});
|
||||
|
||||
test("a bundle reads back safely, and anything else is not one", () => {
|
||||
assert.equal(B.readBundle({ cards: [] }), null);
|
||||
assert.equal(B.readBundle("x"), null);
|
||||
const b = B.readBundle(JSON.parse(JSON.stringify(B.bundleOne(ines, { memory: { notes: ["old"], upTo: {}, at: 1 }, threads, media }))));
|
||||
assert.equal(b.cards[0].name, "Ines Corbel");
|
||||
assert.deepEqual(b.memory["ines-corbel"].notes.map((n) => n.text), ["old"]);
|
||||
assert.equal(b.threads.length, 1);
|
||||
assert.deepEqual(Object.keys(b.media), ["p1"]);
|
||||
assert.equal(b.settings, null);
|
||||
const junk = B.readBundle({ havre_backup: 1, cards: [{ name: "X" }, null, 3], threads: [{ id: "t", card: "x", msgs: [{ role: "system" }, { role: "user", content: "k" }] }, {}], media: { ok: "data:image/png;base64,AA", bad: "javascript:x" } });
|
||||
assert.equal(junk.cards.length, 1);
|
||||
assert.equal(junk.threads.length, 1);
|
||||
assert.equal(junk.threads[0].msgs.length, 1);
|
||||
assert.deepEqual(Object.keys(junk.media), ["ok"]);
|
||||
});
|
||||
|
||||
test("restoring merges: cards already there stay, notes are appended, chats by id are added if missing", () => {
|
||||
const b = B.readBundle(B.bundleAll({ cards: [ines, wren], memory: { "ines-corbel": { notes: [R.note("new", { at: 3 })], upTo: { t1: "b" }, at: 3 } }, threads, media }));
|
||||
const m = B.merge(b, { cards: [ines], memory: { "ines-corbel": { notes: [R.note("old", { at: 1 })], upTo: { t5: "z" }, at: 1 } }, threads: { t1: {} } });
|
||||
assert.deepEqual(m.addCards.map((c) => c.id), ["wren"]);
|
||||
assert.equal(m.skippedCards, 1);
|
||||
assert.deepEqual(m.memory["ines-corbel"].notes.map((n) => n.text), ["old", "new"]);
|
||||
assert.deepEqual(m.memory["ines-corbel"].upTo, { t1: "b", t5: "z" });
|
||||
assert.deepEqual(m.addThreads.map((t) => t.id), ["t2"]);
|
||||
assert.equal(m.skippedThreads, 1);
|
||||
});
|
||||
@@ -171,7 +171,7 @@ async function fakeVenice(route) {
|
||||
reply = /^Them:.*\bSend me a pic/i.test(said) ? json({ move: "photo", shot: "The character from the reference picture holds up a mug\nin a half-unpacked kitchen." })
|
||||
: /^Them:.*\bFilm the harbor/i.test(said) ? json({ move: "clip", shot: "The character from the reference pictures pans across the harbor at dusk." })
|
||||
: /^Them:.*\bWhisper it/i.test(said) ? json({ move: "voice", shot: "" }) : json({ move: "nothing", shot: "" });
|
||||
} else if (/^(?:\(You sent me [^\n]*\)\n\n)?\[Time passes/.test(lastSaid)) { // after a word about the photo she sent last, if she did
|
||||
} else if (/^\[Time passes/.test(lastSaid)) {
|
||||
reply = "You're back!\n\nI kept the kettle warm.";
|
||||
} else if (system.startsWith("You write the shot for a photo")) {
|
||||
reply = "Shot: The character from the reference picture sits on a stack of moving boxes, grinning, warm lamplight.";
|
||||
@@ -325,7 +325,7 @@ async function main() {
|
||||
assert.match(await textOf(page, "#who-name"), /^chat$/i);
|
||||
await has(page, "#log .line.note .said", /paste your venice key/);
|
||||
assert.ok(await page.locator("#cast-box").isHidden(), "the harbor's own aren't listed");
|
||||
assert.equal(await page.locator("#harbor-empty").evaluate((e) => e.hidden), false, "the empty harbor says so");
|
||||
assert.equal(await page.locator("#roster tr.newrow").count(), 1, "the harbor always has a row for someone new");
|
||||
assert.equal(await page.locator("#card-empty").evaluate((e) => e.hidden), false, "so does the empty card");
|
||||
assert.ok(await page.locator("#card").isHidden(), "the card lives in the harbor tab, not under the characters");
|
||||
assert.ok(await page.locator("#say").isDisabled(), "nobody to talk to yet");
|
||||
@@ -334,13 +334,14 @@ async function main() {
|
||||
await page.click("#lt-harbor");
|
||||
assert.ok(await page.locator("#card-new").isVisible(), "an empty harbor opens on a new character to write");
|
||||
assert.match(await textOf(page, "#card-h"), /^NEW CHARACTER$/);
|
||||
assert.match(await textOf(page, "#roster tr.newrow"), /\+ new character/);
|
||||
await page.click("#lt-chars");
|
||||
await shot(page, "desktop.png");
|
||||
|
||||
// a card comes in through the import dialog, and talks from its own examples without a key
|
||||
await page.click("#import");
|
||||
await page.setInputFiles("#file", { name: "ro.json", mimeType: "application/json", buffer: Buffer.from(JSON.stringify(v2)) });
|
||||
await count(page, "#roster tr", 1);
|
||||
await count(page, "#roster tr:not(.newrow)", 1);
|
||||
await has(page, "#who-name", /^Captain Ro$/);
|
||||
await has(page, "#log .line.them .said", /We're closed\. Mostly\./);
|
||||
await has(page, "#roster td", /Ported from Captain Ro by someone/);
|
||||
@@ -416,9 +417,11 @@ async function main() {
|
||||
assert.equal(await page.locator("#log .line.me").count(), 0, "what started it isn't shown");
|
||||
const hello = seen.chats[chatsBefore];
|
||||
assert.equal(hello.venice_parameters.character_slug, "sable-penrose");
|
||||
assert.equal(hello.messages.length, 1);
|
||||
assert.equal(hello.messages[0].role, "user");
|
||||
await count(page, "#roster tr", 2);
|
||||
assert.equal(hello.messages.length, 2, "what they can send here, then what started it");
|
||||
assert.equal(hello.messages[0].role, "system");
|
||||
assert.match(hello.messages[0].content, /you can send photos of yourself/);
|
||||
assert.equal(hello.messages[1].role, "user");
|
||||
await count(page, "#roster tr:not(.newrow)", 2);
|
||||
assert.match(await page.locator('#vgrid .tile[aria-pressed="true"]').evaluate((el) => el.textContent), /Sable Penrose.*talking now/);
|
||||
// the model: their own on Venice to start with, and any other Venice model from the picker
|
||||
await has(page, "#model-btn", /^GLM 5\.2$/);
|
||||
@@ -523,7 +526,8 @@ async function main() {
|
||||
await has(page, "#log .line.media .cap", /photo · Grok Imagine · \$0\.03 · save/);
|
||||
// the photo is in the conversation the model reads next, as something it sent
|
||||
await answered(page, async () => { await page.fill("#say", "Cute."); await page.keyboard.press("Enter"); });
|
||||
assert.match(talks().at(-1).messages.at(-1).content, /^\(You sent me a photo: The character from the reference picture sits on a stack of moving boxes, grinning, warm lamplight\.\)\n\nCute\.$/);
|
||||
assert.equal(talks().at(-1).messages.at(-1).content, "Cute.");
|
||||
assert.match(talks().at(-1).messages.at(-2).content, /\*sends a photo: The character from the reference picture sits on a stack of moving boxes, grinning, warm lamplight\*$/);
|
||||
|
||||
// voice notes: one on request ("hear it"), then every reply, in the voice on her card
|
||||
const hears = page.getByRole("button", { name: "Hear this as a voice note from Sable Penrose" });
|
||||
@@ -613,8 +617,8 @@ async function main() {
|
||||
assert.match(seen.heard.at(-1), /name="model"\r\n\r\nnvidia\/parakeet-tdt-0\.6b-v3/);
|
||||
assert.match(seen.heard.at(-1), /filename="voice-note\.(webm|ogg|m4a)"/);
|
||||
await until(page, () => { const a = [...document.querySelectorAll("#log .line.me audio")].at(-1); return !!a && a.src.startsWith("blob:"); }, null, "your voice note in the chat");
|
||||
assert.equal(talks().at(-1).messages.at(-1).content, "(You sent me a short video: The character from the reference pictures waves from the doorway as the camera drifts closer.)\n\nhello from the mic",
|
||||
"what you said, after a word about the clip she sent");
|
||||
assert.equal(talks().at(-1).messages.at(-1).content, "hello from the mic", "what you said");
|
||||
assert.match(talks().at(-1).messages.at(-2).content, /\*sends a short video: The character from the reference pictures waves from the doorway as the camera drifts closer\*$/, "after the clip she sent, in her turn");
|
||||
// your photo, to a model that can see (she talks through Tiny now)
|
||||
const red = await page.evaluate(() => { const c = document.createElement("canvas"); c.width = c.height = 64; const g = c.getContext("2d"); g.fillStyle = "#c00"; g.fillRect(8, 8, 48, 48); return c.toDataURL("image/png"); });
|
||||
const redPng = Buffer.from(red.split(",")[1], "base64");
|
||||
@@ -696,9 +700,9 @@ async function main() {
|
||||
});
|
||||
await page.reload();
|
||||
await has(page, "#log .line.them .said", /I kept the kettle warm\./);
|
||||
const nudges = () => talks().filter((b) => /^(?:\(You sent me [^\n]*\)\n\n)?\[Time passes/.test(String(b.messages.at(-1).content)));
|
||||
assert.match(nudges()[0].messages.at(-1).content, /^\(You sent me a photo: The character from the reference picture holds up a mug in a half-unpacked kitchen\.\)\n\n\[Time passes: about 8 hours since you last talked\. They just came back to the chat\./,
|
||||
"the photo she sent last, then the time that's passed");
|
||||
const nudges = () => talks().filter((b) => /^\[Time passes/.test(String(b.messages.at(-1).content)));
|
||||
assert.match(nudges()[0].messages.at(-1).content, /^\[Time passes: about 8 hours since you last talked\. They just came back to the chat\./, "the time that's passed");
|
||||
assert.match(nudges()[0].messages.at(-2).content, /\*sends a photo: The character from the reference picture holds up a mug in a half-unpacked kitchen\*$/, "the photo she sent last, in her turn");
|
||||
await poll(() => nudges().length === 2, "Captain Ro texting first", 9000);
|
||||
assert.match(nudges()[1].messages.at(-1).content, /^\[Time passes: about 8 hours since you last talked\. Reach out to them first/);
|
||||
await has(page, "#chats-new", /^1$/);
|
||||
@@ -791,7 +795,7 @@ async function main() {
|
||||
await shot(page, "desktop-remix.png");
|
||||
await page.locator("#rx-draft button", { hasText: "KEEP IN HARBOR" }).click();
|
||||
await has(page, "#who-name", /^Ines Corbel$/);
|
||||
await count(page, "#roster tr", 4);
|
||||
await count(page, "#roster tr:not(.newrow)", 4);
|
||||
await has(page, "#log .line.them .said", /Where are you headed\?/);
|
||||
assert.equal(await page.locator("#studio.pane-chat").count(), 1, "a kept remix opens their chat, to say hello");
|
||||
assert.ok(await page.locator("#chat-back").isHidden(), "a wide screen shows the list and the chat side by side, so no way back is needed");
|
||||
@@ -1057,7 +1061,7 @@ async function main() {
|
||||
await page.click("#lt-chars");
|
||||
await page.click("#import");
|
||||
await page.setInputFiles("#file", png);
|
||||
await count(page, "#roster tr", 5);
|
||||
await count(page, "#roster tr:not(.newrow)", 5);
|
||||
|
||||
// the security policy: nothing but api.venice.ai (and its private-share links for clips)
|
||||
assert.equal(await page.evaluate(() => fetch("https://example.com/").then(() => "reached", () => "blocked")), "blocked");
|
||||
@@ -1128,7 +1132,7 @@ async function main() {
|
||||
taskmaster: [{ role: "user", content: "Plan my week" }, { role: "assistant", content: "Monday first." }] }));
|
||||
});
|
||||
await p2.reload();
|
||||
await count(p2, "#roster tr", 1);
|
||||
await count(p2, "#roster tr:not(.newrow)", 1);
|
||||
const left = await p2.evaluate(() => {
|
||||
const threads = JSON.parse(localStorage.getItem("havre.threads.v1") || "{}"), t = Object.values(threads)[0];
|
||||
return { cards: JSON.parse(localStorage.getItem("havre.cards.v1")), seeded: localStorage.getItem("havre.seeded.v1"),
|
||||
@@ -1176,7 +1180,7 @@ async function main() {
|
||||
await has(p5, "#cast .tile[data-cast=maren] .go", /^talk again$/);
|
||||
await p5.goto(base + "?meet=maren");
|
||||
await has(p5, "#who-name", /^Maren$/);
|
||||
await count(p5, "#roster tr", 2);
|
||||
await count(p5, "#roster tr:not(.newrow)", 2);
|
||||
assert.ok(await p5.locator("#log .line.me .said", { hasText: "What is this place?" }).isVisible(), "the same chat, not a new copy of her");
|
||||
await own.close();
|
||||
|
||||
@@ -1260,15 +1264,15 @@ async function main() {
|
||||
await p3.locator("#vgrid .tile", { hasText: "Wren" }).click();
|
||||
await has(p3, "#log .line.them .said", /Careful, the boxes bite\./);
|
||||
assert.ok(await p3.locator("#say").isVisible(), "the chat is on screen");
|
||||
assert.ok(await p3.locator("#chars").isHidden(), "and not stranded below the list: one at a time");
|
||||
assert.ok(await p3.locator("#lp-chars").isHidden() && await p3.locator("#lt-chats").isVisible(), "the list is out of the way, its tabs stay above the chat");
|
||||
assert.ok(await wide() <= W, `still no sideways scroll (scrollWidth ${await wide()})`);
|
||||
assert.deepEqual(await small(), [], "nor here");
|
||||
await p3.waitForTimeout(500);
|
||||
await shot(p3, "phone-chat.png");
|
||||
await shot(p3, "phone-full.png", { fullPage: true });
|
||||
// from a chat, back to the list it came from; a chat from your chats opens in its place, and goes back to them
|
||||
await has(p3, "#chat-back", /^← characters$/);
|
||||
await p3.click("#chat-back");
|
||||
// from a chat, the tabs above it go back to the list; a chat from your chats opens in its place
|
||||
assert.ok(await p3.locator("#chat-back").isHidden(), "no back link: the tabs are the way back");
|
||||
await p3.click("#lt-chars");
|
||||
assert.ok(await p3.locator("#chat").isHidden());
|
||||
assert.ok(await p3.locator("#lp-chars").isVisible());
|
||||
await p3.click("#lt-chats");
|
||||
@@ -1276,9 +1280,8 @@ async function main() {
|
||||
await p3.waitForTimeout(400);
|
||||
await shot(p3, "phone-chats.png");
|
||||
await p3.locator("#threads .trow").first().click();
|
||||
assert.ok(await p3.locator("#chars").isHidden() && await p3.locator("#say").isVisible());
|
||||
await has(p3, "#chat-back", /^← your chats$/);
|
||||
await p3.click("#chat-back");
|
||||
assert.ok(await p3.locator("#lp-chats").isHidden() && await p3.locator("#say").isVisible());
|
||||
await p3.click("#lt-chats");
|
||||
assert.ok(await p3.locator("#lp-chats").isVisible());
|
||||
await phone.close();
|
||||
} finally {
|
||||
|
||||
@@ -15,5 +15,8 @@
|
||||
apply();
|
||||
var b = document.getElementById("theme");
|
||||
if (b) b.addEventListener("click", function () { try { localStorage.setItem(K, dark() ? "light" : "dark"); } catch (e) { /* this time only */ } apply(); });
|
||||
// installed as an app there's no address bar and no pull to refresh: this is the way to the latest Havre
|
||||
var r = document.getElementById("reload");
|
||||
if (r) r.addEventListener("click", function () { r.textContent = "reloading…"; location.reload(); });
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,37 @@
|
||||
// update.js: an installed Havre has no address bar to refresh from, so the page notices when a new version has been
|
||||
// published and offers a reload, like gandora's pill. It fingerprints the files the app is made of when it opens,
|
||||
// checks again whenever it comes back to the foreground (and every ten minutes), and shows the pill when they differ.
|
||||
// Checking fetches with cache: "reload", which also refreshes the browser's copies, so the reload gets the new files.
|
||||
(() => {
|
||||
const FILES = ["./", "app.js", "style.css", "workflows.js", "abilities.js", "card.js", "venice.js", "memory.js",
|
||||
"threads.js", "media.js", "backup.js", "theme.js"];
|
||||
let base = null, shown = false, busy = false;
|
||||
async function print() {
|
||||
const parts = await Promise.all(FILES.map((f) => fetch(f, { cache: "reload" }).then((r) => (r.ok ? r.text() : "")).catch(() => null)));
|
||||
if (parts.some((p) => p === null)) return null; // offline: no verdict
|
||||
const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(parts.join("\u0000")));
|
||||
return Array.from(new Uint8Array(d), (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
function show() {
|
||||
if (shown) return;
|
||||
shown = true;
|
||||
const b = document.createElement("button");
|
||||
b.id = "upd"; b.type = "button"; b.textContent = "NEW VERSION · RELOAD";
|
||||
b.addEventListener("click", () => location.reload());
|
||||
document.body.append(b);
|
||||
}
|
||||
async function check() {
|
||||
if (busy || shown || !crypto.subtle) return;
|
||||
busy = true;
|
||||
try {
|
||||
const now = await print();
|
||||
if (!now) return;
|
||||
if (!base) base = now;
|
||||
else if (now !== base) show();
|
||||
} finally { busy = false; }
|
||||
}
|
||||
if (location.protocol !== "https:") return;
|
||||
setTimeout(check, 3000);
|
||||
setInterval(check, 10 * 60 * 1000);
|
||||
document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") check(); });
|
||||
})();
|
||||
@@ -141,8 +141,9 @@ export class Venice {
|
||||
}
|
||||
|
||||
/** a voice note: words read out by a voice model. mood goes to models that take a style prompt; the rest ignore it. */
|
||||
async speak({ model, voice, input, mood = "", speed = 1, signal } = {}) {
|
||||
async speak({ model, voice, input, mood = "", speed = 1, format = "", signal } = {}) {
|
||||
const body = { input: String(input || "").slice(0, 4096), model, speed };
|
||||
if (format) body.response_format = format;
|
||||
if (voice) body.voice = voice;
|
||||
if (mood) body.prompt = String(mood).slice(0, 500);
|
||||
const res = await this.raw("/audio/speech", { body, signal });
|
||||
|
||||
@@ -33,6 +33,11 @@ export const WORKFLOWS = [
|
||||
about: "a few photos of them from the pictures on their card, one shoot, each a different moment (3 unless you say)",
|
||||
usage: "/photoshoot [how many] [what the shoot is about]",
|
||||
},
|
||||
{
|
||||
id: "whisper", command: "whisper", title: "a whisper", takes: "card", gives: "voice note", doors: ["chat"],
|
||||
about: "a short ASMR voice note from them, whispered over soft rain (luna or eve; add what it's about)",
|
||||
usage: "/whisper [luna|eve] [what it's about]",
|
||||
},
|
||||
{
|
||||
id: "recall", command: "recall", title: "recall", takes: "words", gives: "memory", doors: ["chat"],
|
||||
about: "what they remember about something (their notes, then the chats themselves), brought to mind for a while",
|
||||
@@ -128,6 +133,51 @@ export function cardFromIdea(o, { model } = {}) {
|
||||
return normalizeCard({ ...base, look: { ...base.look, description: look } });
|
||||
}
|
||||
|
||||
// ---------- whisper: a short ASMR voice note, over soft rain ----------
|
||||
|
||||
// The voices that whisper well and cheaply (picked in the asmr.gandora.lol bake-off): Venice model, voice, how the
|
||||
// words are wrapped so the model whispers, and the pace (0.9 of their usual reading speed sounded best).
|
||||
export const WHISPER_PRESETS = {
|
||||
luna: { model: "tts-inworld-1-5-max", voice: "Luna", lead: "[whispering] ", tail: "", speed: 0.77, format: "wav" },
|
||||
eve: { model: "tts-xai-v1", voice: "eve", lead: "<whisper>", tail: "</whisper>", speed: 0.77, format: "mp3" },
|
||||
};
|
||||
export const WHISPER_DEFAULT = "luna";
|
||||
|
||||
/** the words after /whisper: a preset name first if there is one, the rest what it's about */
|
||||
export function parseWhisper(arg = "") {
|
||||
const m = /^\s*(\w+)(?:\s+|$)([\s\S]*)$/.exec(str(arg, 2000));
|
||||
const first = m ? m[1].toLowerCase() : "";
|
||||
if (WHISPER_PRESETS[first]) return { preset: first, brief: m[2].replace(/\s+/g, " ").trim() };
|
||||
return { preset: "", brief: str(arg, 2000).replace(/\s+/g, " ").trim() };
|
||||
}
|
||||
|
||||
export const WHISPER_SCHEMA = {
|
||||
type: "object", additionalProperties: false, required: ["script"],
|
||||
properties: { script: { type: "string" } },
|
||||
};
|
||||
|
||||
export function whisperMessages(card, { brief = "", lines = [] } = {}) {
|
||||
const system = `You are ${card.name}. Write a short ASMR voice note you whisper to the person you're talking with: ` +
|
||||
"60 to 110 words, in your own voice and manner, intimate and calming, present tense, short phrases with pauses " +
|
||||
"written as \"...\". Gentle sensory detail (breath, rain, a soft sound nearby) is welcome. No stage directions, no " +
|
||||
"emoji, no markup, nothing in brackets: only the words to be whispered. Return one JSON object: script.";
|
||||
const persona = card.persona ? clip(card.persona.replace(/\s+/g, " "), 900) : "";
|
||||
const user = [
|
||||
persona ? `Who you are: ${persona}` : "",
|
||||
lines.length ? "The conversation so far, latest last:\n" + lines.join("\n") : "You haven't talked yet.",
|
||||
"What it's about: " + (str(brief, 500).trim() || "winding down together; help them relax."),
|
||||
].filter(Boolean).join("\n\n");
|
||||
return [{ role: "system", content: system }, { role: "user", content: user }];
|
||||
}
|
||||
|
||||
/** the script, cleaned of anything the model shouldn't have added (brackets, asterisks, emoji-ish markup) */
|
||||
export function cleanWhisper(o) {
|
||||
return str(o && o.script, 1500).replace(/\[[^\]]*\]|\([^)]*\)|<[^>]*>|\*[^*]*\*/g, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
/** what the voice model is sent: the script inside the preset's whisper markup */
|
||||
export const whisperInput = (script, preset) => (preset.lead || "") + script + (preset.tail || "");
|
||||
|
||||
// ---------- photoshoot: a few photos of them, from the pictures on their card ----------
|
||||
|
||||
export const SHOOT_MIN = 1, SHOOT_MAX = 6, SHOOT_DEFAULT = 3;
|
||||
|
||||
@@ -10,7 +10,7 @@ People who make a quick throwaway deck every week — sprint demos, status updat
|
||||
|
||||
## How to use it
|
||||
|
||||
1. Open the page and type markdown-ish text in the editor on the left. It comes preloaded with a sample deck so you can see real output immediately.
|
||||
1. Open the page and type markdown-ish text in the editor on the left. It comes preloaded with a sample deck so you can see real output immediately. Already have a `.md` file? Drop it anywhere on the page, or press **Open .md** above the editor; the file is read in the tab and replaces the text.
|
||||
2. Watch the preview on the right re-typeset live. Navigate with the arrow keys (or swipe on touch screens); a progress bar tracks your place.
|
||||
3. A slide with too much text shrinks to fit, down to 70% of the normal size. Past that it's cut into two or more slides, between lines and never right after a heading, and each new slide repeats its title with "(cont.)". A note under the preview says which slides were cut; put in a `---` yourself to choose the break. Cuts are worked out for a 1280x720 screen, so the copied deck has the same slides on any screen, and on a smaller one they shrink a little more.
|
||||
4. Press **Copy deck as HTML** to copy the whole deck as one self-contained HTML file. Open it in any browser and present with the arrow keys — offline, no dependencies.
|
||||
|
||||
@@ -576,6 +576,64 @@
|
||||
if (copied) done(); else failed();
|
||||
}
|
||||
|
||||
// Files: a .md (or any text file) dropped anywhere on the page, or picked with Open .md, becomes the
|
||||
// deck. It is read in the tab with FileReader; nothing is uploaded.
|
||||
var fileInput = document.getElementById("file");
|
||||
var openBtn = document.getElementById("open");
|
||||
var dragDepth = 0;
|
||||
|
||||
function hasFile(e) {
|
||||
var t = e.dataTransfer && e.dataTransfer.types;
|
||||
return !!t && Array.prototype.indexOf.call(t, "Files") >= 0;
|
||||
}
|
||||
|
||||
function openFile(file) {
|
||||
if (!file) return;
|
||||
if (file.size > 2000000) {
|
||||
say(file.name + " is over 2 MB. A deck is plain text, so something else is in that file.", true);
|
||||
return;
|
||||
}
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
load(String(reader.result).replace(/^/, ""));
|
||||
current = 0;
|
||||
history.replaceState(null, "", location.href.split("#")[0]);
|
||||
parse();
|
||||
say("Opened " + file.name + ". Edits stay in this tab; copy a link or the HTML to keep them.");
|
||||
};
|
||||
reader.onerror = function () { say("Couldn't read " + file.name + ".", true); };
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
openBtn.addEventListener("click", function () { fileInput.click(); });
|
||||
fileInput.addEventListener("change", function () {
|
||||
openFile(fileInput.files[0]);
|
||||
fileInput.value = "";
|
||||
});
|
||||
|
||||
document.addEventListener("dragenter", function (e) {
|
||||
if (!hasFile(e)) return;
|
||||
e.preventDefault();
|
||||
dragDepth++;
|
||||
document.body.classList.add("dropping");
|
||||
});
|
||||
document.addEventListener("dragover", function (e) {
|
||||
if (!hasFile(e)) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
});
|
||||
document.addEventListener("dragleave", function (e) {
|
||||
if (!hasFile(e)) return;
|
||||
if (--dragDepth <= 0) { dragDepth = 0; document.body.classList.remove("dropping"); }
|
||||
});
|
||||
document.addEventListener("drop", function (e) {
|
||||
if (!hasFile(e)) return;
|
||||
e.preventDefault();
|
||||
dragDepth = 0;
|
||||
document.body.classList.remove("dropping");
|
||||
openFile(e.dataTransfer.files[0]);
|
||||
});
|
||||
|
||||
// A deck in the address opens in place of the sample, and so does one pasted into it later.
|
||||
var linked = /(^#|&)(d|md)=/.test(location.hash);
|
||||
load(linked ? "" : SAMPLE);
|
||||
|
||||
@@ -87,12 +87,34 @@
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.editor-pane .label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.editor-pane label {
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--ink-dim);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.editor-pane .open { padding: 4px 10px; font-size: 0.78rem; }
|
||||
#file { display: none; }
|
||||
body.dropping textarea { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(110,168,254,0.25); }
|
||||
body.dropping::after {
|
||||
content: "Drop your .md file to open it";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(15,17,21,0.75);
|
||||
color: var(--ink);
|
||||
font-size: 1.3rem;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
textarea {
|
||||
flex: 1;
|
||||
@@ -402,12 +424,16 @@
|
||||
</header>
|
||||
<main id="try">
|
||||
<section class="editor-pane">
|
||||
<label for="src">Slides</label>
|
||||
<div class="label-row">
|
||||
<label for="src">Slides</label>
|
||||
<button id="open" class="open" type="button">Open .md</button>
|
||||
<input id="file" type="file" accept=".md,.markdown,.txt,text/markdown,text/plain">
|
||||
</div>
|
||||
<textarea id="src" spellcheck="false"></textarea>
|
||||
<p class="hint">
|
||||
<code># Title</code> heading · <code>## Subtitle</code> · <code>- bullet</code> point ·
|
||||
<code>---</code> on its own line starts a new slide · inline <code>**bold**</code>, <code>*italic*</code>, <code>`code`</code>.
|
||||
Plain lines become paragraphs.
|
||||
Plain lines become paragraphs. Or drop a <code>.md</code> file anywhere on the page.
|
||||
</p>
|
||||
</section>
|
||||
<section class="stage">
|
||||
@@ -429,7 +455,7 @@
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<h3>Type your slides</h3>
|
||||
<p>Markdown-ish text in the editor: headings, bullets, paragraphs, <code>---</code> between slides. The syntax is the stuff you already type.</p>
|
||||
<p>Markdown-ish text in the editor: headings, bullets, paragraphs, <code>---</code> between slides. The syntax is the stuff you already type. Already have a <code>.md</code> file? Drop it on the page.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<h3>Watch it set live</h3>
|
||||
@@ -475,7 +501,7 @@
|
||||
<div class="price">$0</div>
|
||||
<p>For anyone who needs a quick deck right now.</p>
|
||||
<ul>
|
||||
<li>Full plain-text editor</li>
|
||||
<li>Full plain-text editor; drop in a <code>.md</code> file to start from one</li>
|
||||
<li>Live navigable preview with arrow keys and progress bar</li>
|
||||
<li>One-click copy of the deck as self-contained HTML</li>
|
||||
<li>Links with the deck inside them, nothing stored</li>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
# tools/build.sh: shots/desktop.png, the first screen at 1440x900, rendered from this folder by headless Chrome.
|
||||
# Time's refresh workflow runs this (Actions -> refresh, slug roll-credits). Needs python3 (for a local server) and
|
||||
# Chrome or Chromium: set CHROME=/path/to/chrome if it isn't found, and CHROME_FLAGS=--no-sandbox when running as root.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
CHROME=${CHROME:-}
|
||||
for c in "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" "$(command -v chromium || true)" \
|
||||
"$(command -v chromium-browser || true)" "$(command -v google-chrome || true)" "$(command -v google-chrome-stable || true)"; do
|
||||
if [ -z "$CHROME" ] && [ -n "$c" ] && [ -x "$c" ]; then CHROME=$c; fi
|
||||
done
|
||||
[ -n "$CHROME" ] || { echo "no Chrome or Chromium found; set CHROME" >&2; exit 1; }
|
||||
|
||||
PORT=${PORT:-8795}
|
||||
python3 -m http.server "$PORT" --bind 127.0.0.1 >/dev/null 2>&1 </dev/null &
|
||||
SRV=$!
|
||||
trap 'kill $SRV 2>/dev/null || true' EXIT INT TERM
|
||||
n=0
|
||||
until curl -fs -o /dev/null --max-time 2 "http://127.0.0.1:$PORT/"; do
|
||||
n=$((n + 1)); [ "$n" -lt 40 ] || { echo "the local server on port $PORT never answered" >&2; exit 1; }; sleep 0.5
|
||||
done
|
||||
|
||||
mkdir -p shots
|
||||
out=$PWD/shots/desktop.png
|
||||
prof=$(mktemp -d)
|
||||
rm -f "$out"
|
||||
# shellcheck disable=SC2086
|
||||
"$CHROME" --headless=new --hide-scrollbars --no-first-run --no-default-browser-check --disable-background-networking \
|
||||
--disable-component-update --user-data-dir="$prof" --virtual-time-budget=6000 --window-size=1440,900 \
|
||||
--screenshot="$out" ${CHROME_FLAGS:-} "http://127.0.0.1:$PORT/" >"$prof.log" 2>&1 &
|
||||
pid=$!
|
||||
n=0
|
||||
while kill -0 "$pid" 2>/dev/null && [ ! -s "$out" ]; do n=$((n + 1)); [ "$n" -lt 120 ] || break; sleep 0.5; done
|
||||
sleep 1
|
||||
kill "$pid" 2>/dev/null || true; wait "$pid" 2>/dev/null || true
|
||||
[ -s "$out" ] || { echo "Chrome made no screenshot:" >&2; tail -5 "$prof.log" >&2; exit 1; }
|
||||
rm -rf "$prof" "$prof.log"
|
||||
echo "built: shots/desktop.png"
|
||||
+16
-1
@@ -1,5 +1,19 @@
|
||||
{
|
||||
"ventures": [
|
||||
{
|
||||
"slug": "gandora",
|
||||
"name": "gandora",
|
||||
"one_liner": "Type a seed, get a station: every song is written and rendered for you as it plays, every cover is drawn from the song's own settings, and your reactions steer what comes next.",
|
||||
"url": "https://gandora.lol/",
|
||||
"pages": "https://gandora.yolopreneur.lol/",
|
||||
"launched": "2026-10-20",
|
||||
"status": "live",
|
||||
"signups": 0,
|
||||
"source": "yolopreneur",
|
||||
"repo": "https://teahub.xyz/gandora/site",
|
||||
"hosting": "pages",
|
||||
"shot": "https://gandora.yolopreneur.lol/shots/desktop.png"
|
||||
},
|
||||
{
|
||||
"slug": "slyng",
|
||||
"name": "SLYNG",
|
||||
@@ -487,6 +501,7 @@
|
||||
},
|
||||
{
|
||||
"slug": "roll-credits",
|
||||
"shot": "roll-credits/shots/desktop.png",
|
||||
"name": "Roll Credits",
|
||||
"one_liner": "Type the names of the people who made something, pick a credits style, and download a scrolling end-credits video, entirely in your browser.",
|
||||
"for_whom": "A team lead or friend-group organizer who wants to thank the people behind a launch, a trip, a bake-off, or a last day at a job, and wants it to feel like a film ending.",
|
||||
@@ -505,5 +520,5 @@
|
||||
"hosting": "pages"
|
||||
}
|
||||
],
|
||||
"refreshed": "2026-10-20"
|
||||
"refreshed": "2026-10-21"
|
||||
}
|
||||
Reference in new issue
Block a user