import type { FileContent } from "@opencode-ai/sdk/v2" import { createEffect, createMemo, createResource, Match, on, Show, Switch, type JSX } from "solid-js" import { useI18n } from "../context/i18n" import { dataUrlFromMediaValue, hasMediaValue, isBinaryContent, mediaKindFromPath, normalizeMimeType, svgTextFromValue, } from "../pierre/media" export type FileMediaOptions = { mode?: "auto" | "off" path?: string current?: unknown before?: unknown after?: unknown deleted?: boolean readFile?: (path: string) => Promise onLoad?: () => void onError?: (ctx: { kind: "image" | "audio" | "svg" }) => void } function mediaValue(cfg: FileMediaOptions, mode: "image" | "audio") { if (cfg.current !== undefined) return cfg.current if (mode === "image") return cfg.after ?? cfg.before return cfg.after ?? cfg.before } export function FileMedia(props: { media?: FileMediaOptions; fallback: () => JSX.Element }) { const i18n = useI18n() const cfg = () => props.media const kind = createMemo(() => { const media = cfg() if (!media || media.mode === "off") return return mediaKindFromPath(media.path) }) const isBinary = createMemo(() => { const media = cfg() if (!media || media.mode === "off") return false if (kind()) return false return isBinaryContent(media.current as any) }) const onLoad = () => props.media?.onLoad?.() const deleted = createMemo(() => { const media = cfg() const k = kind() if (!media || !k) return false if (media.deleted) return true if (k === "svg") return false if (media.current !== undefined) return false return !hasMediaValue(media.after as any) && hasMediaValue(media.before as any) }) const direct = createMemo(() => { const media = cfg() const k = kind() if (!media || (k !== "image" && k !== "audio")) return return dataUrlFromMediaValue(mediaValue(media, k), k) }) const request = createMemo(() => { const media = cfg() const k = kind() if (!media || (k !== "image" && k !== "audio")) return if (media.current !== undefined) return if (deleted()) return if (direct()) return if (!media.path || !media.readFile) return return { key: `${k}:${media.path}`, kind: k, path: media.path, readFile: media.readFile, onError: media.onError, } }) const [loaded] = createResource(request, async (input) => { return input.readFile(input.path).then( (result) => { const src = dataUrlFromMediaValue(result as any, input.kind) if (!src) { input.onError?.({ kind: input.kind }) return { key: input.key, error: true as const } } return { key: input.key, src, mime: input.kind === "audio" ? normalizeMimeType(result?.mimeType) : undefined, } }, () => { input.onError?.({ kind: input.kind }) return { key: input.key, error: true as const } }, ) }) const remote = createMemo(() => { const input = request() const value = loaded() if (!input || !value || value.key !== input.key) return return value }) const src = createMemo(() => { const value = remote() return direct() ?? (value && "src" in value ? value.src : undefined) }) const status = createMemo(() => { if (direct()) return "ready" as const if (!request()) return "idle" as const if (loaded.loading) return "loading" as const if (remote()?.error) return "error" as const if (src()) return "ready" as const return "idle" as const }) const audioMime = createMemo(() => { const value = remote() return value && "mime" in value ? value.mime : undefined }) const svgSource = createMemo(() => { const media = cfg() if (!media || kind() !== "svg") return return svgTextFromValue(media.current as any) }) const svgSrc = createMemo(() => { const media = cfg() if (!media || kind() !== "svg") return return dataUrlFromMediaValue(media.current as any, "svg") }) const svgInvalid = createMemo(() => { const media = cfg() if (!media || kind() !== "svg") return if (svgSource() !== undefined) return if (!hasMediaValue(media.current as any)) return return [media.path, media.current] as const }) createEffect( on( svgInvalid, (value) => { if (!value) return cfg()?.onError?.({ kind: "svg" }) }, { defer: true }, ), ) const kindLabel = (value: "image" | "audio") => i18n.t(value === "image" ? "ui.fileMedia.kind.image" : "ui.fileMedia.kind.audio") return ( { const media = cfg() const k = kind() if (!media || (k !== "image" && k !== "audio")) return props.fallback() const label = kindLabel(k) if (deleted()) { return (
{i18n.t("ui.fileMedia.state.removed", { kind: label })}
) } if (status() === "loading") { return (
{i18n.t("ui.fileMedia.state.loading", { kind: label })}
) } if (status() === "error") { return (
{i18n.t("ui.fileMedia.state.error", { kind: label })}
) } return (
{i18n.t("ui.fileMedia.state.unavailable", { kind: label })}
) })()} > {(value) => { const k = kind() if (k !== "image" && k !== "audio") return props.fallback() if (k === "image") { return (
{cfg()?.path}
) } return (
) }}
{(() => { if (svgSource() === undefined && svgSrc() == null) return props.fallback() return (
{props.fallback()} {(value) => (
{cfg()?.path}
)}
) })()}
{cfg()?.path?.split("/").pop() ?? i18n.t("ui.fileMedia.binary.title")}
{(() => { const path = cfg()?.path if (!path) return i18n.t("ui.fileMedia.binary.description.default") return i18n.t("ui.fileMedia.binary.description.path", { path }) })()}
{props.fallback()}
) }