File size: 4,207 Bytes
fb57ab3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { ArrowSquareOutIcon, FileIcon, HourglassIcon } from "@phosphor-icons/react";
import type { DataType, FileRef, OutputValue } from "../lib/types";

const isRef = (v: unknown): v is FileRef => !!v && typeof v === "object" && typeof (v as FileRef).url === "string";
const isImageUrl = (u: string) => /\.(png|jpe?g|webp|gif|avif|bmp|svg)(\?|$)/i.test(u);
const isAudioUrl = (u: string) => /\.(mp3|wav|ogg|flac|m4a|aac|opus)(\?|$)/i.test(u);
const isVideoUrl = (u: string) => /\.(mp4|webm|mov|mkv)(\?|$)/i.test(u);

const kindOf = (ref: FileRef, hint: DataType): "image" | "audio" | "video" | "file" => {
  const m = ref.mime ?? "";
  if (m.startsWith("image/") || isImageUrl(ref.url)) return "image";
  if (m.startsWith("audio/") || isAudioUrl(ref.url)) return "audio";
  if (m.startsWith("video/") || isVideoUrl(ref.url)) return "video";
  if (hint === "image" || hint === "audio" || hint === "video") return hint;
  return "file";
};

function FileView({ file, hint }: { file: FileRef; hint: DataType }) {
  const kind = kindOf(file, hint);
  const open = (
    <a className="node-output__open nodrag" href={file.url} target="_blank" rel="noreferrer" title="Open in a new tab" onClick={(e) => e.stopPropagation()}>
      <ArrowSquareOutIcon size={12} weight="bold" />
    </a>
  );
  switch (kind) {
    case "image":
      return (
        <>
          <img src={file.url} alt={file.name ?? ""} draggable={false} />
          {open}
        </>
      );
    case "audio":
      return (
        <>
          <audio src={file.url} controls className="nodrag" preload="metadata" />
          {open}
        </>
      );
    case "video":
      return (
        <>
          <video src={file.url} controls className="nodrag" preload="metadata" />
          {open}
        </>
      );
    default:
      return (
        <a className="node-output__file nodrag" href={file.url} target="_blank" rel="noreferrer">
          <FileIcon size={13} />
          <span className="truncate">{file.name ?? file.url.split("/").pop()}</span>
        </a>
      );
  }
}

/** Renders one output port's value. */
export function OutputView({ value, type, pending }: { value: OutputValue | undefined; type: DataType; pending?: boolean }) {
  if (value === undefined || value === null || value === "") {
    return (
      <div className="node-output">
        <div className="node-output__empty">
          {pending ? <HourglassIcon size={12} /> : null}
          <span>{pending ? "Waiting…" : "No output yet"}</span>
        </div>
      </div>
    );
  }
  if (isRef(value)) {
    return (
      <div className="node-output">
        <FileView file={value} hint={type} />
      </div>
    );
  }
  if (Array.isArray(value) && value.length && value.every(isRef)) {
    const refs = value as FileRef[];
    if (refs.length === 1 || (type === "image" && refs.every((r) => kindOf(r, type) === "image")))
      return (
        <div className="node-output">
          <FileView file={refs[refs.length - 1]} hint={type} />
        </div>
      );
    return (
      <div className="node-output">
        <div className="node-output__grid">
          {refs.slice(0, 9).map((r, i) =>
            kindOf(r, "image") === "image" ? (
              <a key={i} href={r.url} target="_blank" rel="noreferrer" className="nodrag">
                <img src={r.url} alt="" draggable={false} />
              </a>
            ) : (
              <FileView key={i} file={r} hint="file" />
            ),
          )}
        </div>
      </div>
    );
  }
  const text = typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" ? String(value) : JSON.stringify(value, null, 1);
  return (
    <div className="node-output">
      <pre className="node-output__text nodrag nowheel">{text.length > 2000 ? `${text.slice(0, 2000)}…` : text}</pre>
    </div>
  );
}

/** First image URL in a value, for thumbnails. */
export const thumbOf = (value: OutputValue | undefined): string | null => {
  if (isRef(value)) return kindOf(value, "image") === "image" ? value.url : null;
  if (Array.isArray(value)) for (const v of value) if (isRef(v) && kindOf(v, "image") === "image") return v.url;
  return null;
};