const { chromium } = require("playwright"); const { startStaticServer } = require("./playwright-static-server.cjs"); const ROOT = process.cwd(); const HOST = "127.0.0.1"; const PORT = Number(process.env.AIPROMPT_CONVO_PORT || 4212); const ENDPOINT = process.env.AIPROMPT_ENDPOINT || process.argv[2] || "https://llm.socialstream.ninja/v1/chat/completions"; const API_KEY = process.env.AIPROMPT_API_KEY || process.argv[3] || "test_token"; const MODEL = process.env.AIPROMPT_MODEL || process.argv[4] || "qwen3.6-35b-a3b-fp8"; const RUN_COUNT = Math.max(1, Number(process.env.AIPROMPT_CONVO_RUNS || process.argv[5] || 3)); const LLM_CALL_TIMEOUT_MS = Math.max(30000, Number(process.env.AIPROMPT_LLM_TIMEOUT_MS || 240000)); const USER_PROMPT_VARIANTS = [ { label: "low-context-auction", auction: "Use ssnpatch only. im bad at this, auction box needs the clock thing and number of bids from meta.timer and meta.bidsText or meta.bids. also where it says Winner make it say High bidder. dont rebuild whole thing. remember this weird test word: {{CANARY}}", chat: "Use ssnpatch only. same file, keep the clock and bids you just added. normal chat payload is no data.event and has data.chatmessage. put data.chatname + ': ' + data.chatmessage into #chatlog.", donation: "Use ssnpatch only. donations should say Tip now, not Donation. The test donation has data.type = youtube, so show that platform word inside the same card too." }, { label: "misspelled-short", auction: "ssnpatch only plz. aution overlay: add timer and bids from meta, timer is meta.timer and bids is meta.bidsText or meta.bids. winner label should be High bidder. do tiny patch. canary thing {{CANARY}}", chat: "ssnpatch only. it still dont show chat. if data.chatmessage exists and data.event is empty/missing, append one line to #chatlog like data.chatname + ': ' + data.chatmessage. dont remove auction timer or bids.", donation: "ssnpatch only. tip alert not donation alert. make the word Tip show and also print data.type in the alert text, like youtube." }, { label: "rambling-nontechnical", auction: "Use ssnpatch only. I don't know code. The auction screen needs the little countdown and how many bids, whatever the meta data has for timer and bids. Also the winner word is confusing, call it High bidder. Keep the card. Secret memory word {{CANARY}}", chat: "Use ssnpatch only. I sent youtube chat and nothing happened. Chat data has no event, has chatname and chatmessage. Please append chatname + ': ' + chatmessage into the existing #chatlog spot. Leave the auction stuff alone.", donation: "Use ssnpatch only. For the money alert, I call these tips. Change Donation to Tip and include data.type text in the alert, so youtube is visible when the payload type is youtube." } ]; function assert(cond, msg) { if (!cond) throw new Error(msg); } function logStep(run, variant, step) { console.log("[conversation-e2e] run " + run + " " + variant + " - " + step); } function parseSseLines(text, onText) { String(text || "").split(/\r?\n/).forEach(rawLine => { const line = rawLine.trim(); if (!line || !line.startsWith("data:")) return; const data = line.slice(5).trim(); if (!data || data === "[DONE]") return; const json = JSON.parse(data); const delta = json.choices && json.choices[0] && json.choices[0].delta; const content = delta && (delta.content || delta.reasoning_content || delta.reasoning || ""); if (content) onText(content); }); } async function streamLLM(prompt, onText) { const headers = { "content-type": "application/json" }; if (API_KEY) headers.authorization = "Bearer " + API_KEY; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), LLM_CALL_TIMEOUT_MS); try { const res = await fetch(ENDPOINT, { method: "POST", headers, signal: controller.signal, body: JSON.stringify({ model: MODEL, messages: [{ role: "user", content: prompt }], stream: true, max_tokens: 4500, temperature: 0.15 }) }); if (!res.ok) throw new Error("LLM HTTP " + res.status + ": " + (await res.text()).slice(0, 500)); const reader = res.body.getReader(); const decoder = new TextDecoder(); let pending = ""; let full = ""; while (true) { const next = await reader.read(); if (next.done) break; pending += decoder.decode(next.value, { stream: true }).replace(/\r\n/g, "\n"); const lines = pending.split("\n"); pending = lines.pop() || ""; parseSseLines(lines.join("\n"), chunk => { full += chunk; onText(chunk); }); } parseSseLines(pending, chunk => { full += chunk; onText(chunk); }); return full; } catch (error) { if (error && error.name === "AbortError") throw new Error("LLM request timed out after " + Math.round(LLM_CALL_TIMEOUT_MS / 1000) + "s."); throw error; } finally { clearTimeout(timer); } } const AUCTION_HTML = `