//// // The scrollback shows your history of interacting with the shell. // input, output, etc import type { Message, CommandOutput, CommandResult } from "../shared/types" import { scrollback, cmdInput, $$ } from "./dom" import { randomId } from "../shared/utils" type InputStatus = "waiting" | "streaming" | "ok" | "error" export function initScrollback() { window.addEventListener("click", handleInputClick) } export function autoScroll() { // requestAnimationFrame(() => scrollback.scrollTop = scrollback.scrollHeight - scrollback.clientHeight) // scrollback.scrollTop = scrollback.scrollHeight - scrollback.clientHeight } export function insert(node: HTMLElement) { scrollback.append(node) } export function addInput(id: string, input: string) { const parent = $$("li.input") const status = $$("span.status.yellow", "•") const content = $$("span.content", input) parent.append(status, content) parent.dataset.id = id insert(parent) scrollback.scrollTop = scrollback.scrollHeight - scrollback.clientHeight } export function setStatus(id: string, status: InputStatus) { const statusEl = document.querySelector(`[data-id="${id}"].input .status`) if (!statusEl) return const colors = { waiting: "yellow", streaming: "purple", ok: "green", error: "red" } statusEl.classList.remove(...Object.values(colors)) statusEl.classList.add(colors[status]) } export function addOutput(id: string, output: CommandOutput) { const item = $$("li") item.classList.add("output") item.dataset.id = id || randomId() const [format, content] = processOutput(output) if (format === "html") item.innerHTML = content else item.textContent = content const input = document.querySelector(`[data-id="${id}"].input`) if (input instanceof HTMLLIElement) { input.parentNode!.insertBefore(item, input.nextSibling) } else { insert(item) } autoScroll() } export function addErrorMessage(message: string) { addOutput("", { html: `${message}` }) } export function appendOutput(id: string, output: CommandOutput) { const item = document.querySelector(`[data-id="${id}"].output`) if (!item) { console.error(`output id ${id} not found`) return } const [format, content] = processOutput(output) if (format === "html") item.innerHTML += content else item.textContent += content autoScroll() } export function replaceOutput(id: string, output: CommandOutput) { const item = document.querySelector(`[data-id="${id}"].output`) if (!item) { console.error(`output id ${id} not found`) return } const [format, content] = processOutput(output) if (format === "html") item.innerHTML = content else item.textContent = content autoScroll() } function processOutput(output: CommandOutput): ["html" | "text", string] { let content = "" let html = false if (typeof output === "string") { content = output } else if (Array.isArray(output)) { content = output.join(" ") } else if (typeof output === "object" && "html" in output) { html = true content = output.html if (output.script) eval(output.script) } else if (typeof output === "object" && "text" in output) { content = output.text if (output.script) eval(output.script) } else if (typeof output === "object" && "script" in output) { eval(output.script!) } else { content = JSON.stringify(output) } return [html ? "html" : "text", content] } function handleInputClick(e: MouseEvent) { const target = e.target if (!(target instanceof HTMLElement)) return if (target.matches(".input .content")) { cmdInput.value = target.textContent } } export function handleOutput(msg: Message) { const result = msg.data as CommandResult setStatus(msg.id!, result.status) addOutput(msg.id!, result.output) }