@neta-art/cohub 5.6.0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -5
- package/dist/board/codec.js +15 -0
- package/dist/board/core/palette.d.ts +3 -2
- package/dist/board/core/shape-types.d.ts +8 -2
- package/dist/board/core/shape-types.js +3 -7
- package/dist/board/core/tool-styles.d.ts +2 -1
- package/dist/board/image-key.js +3 -5
- package/dist/board/index.d.ts +10 -5
- package/dist/board/index.js +7 -3
- package/dist/board/media-playback.d.ts +22 -0
- package/dist/board/media-playback.js +67 -0
- package/dist/board/media.d.ts +7 -0
- package/dist/board/media.js +70 -0
- package/dist/board/nodes.d.ts +113 -0
- package/dist/board/nodes.js +154 -0
- package/dist/board/render/audio-waveform.d.ts +12 -0
- package/dist/board/render/audio-waveform.js +36 -0
- package/dist/board/render/index.d.ts +4 -1
- package/dist/board/render/index.js +4 -1
- package/dist/board/render/media-interaction.d.ts +19 -0
- package/dist/board/render/media-interaction.js +26 -0
- package/dist/board/render/renderers/audio-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/audio-card-renderer.js +120 -0
- package/dist/board/render/renderers/board-renderer-registry.js +4 -2
- package/dist/board/render/renderers/draw-card-renderer.js +1 -1
- package/dist/board/render/renderers/file-card-renderer.js +1 -1
- package/dist/board/render/renderers/frame-card-renderer.js +1 -1
- package/dist/board/render/renderers/geo-card-renderer.js +1 -1
- package/dist/board/render/renderers/image-card-renderer.js +1 -1
- package/dist/board/render/renderers/task-card-renderer.d.ts +2 -1
- package/dist/board/render/renderers/task-card-renderer.js +21 -53
- package/dist/board/render/renderers/text-card-renderer.js +1 -1
- package/dist/board/render/renderers/unknown-card-renderer.js +1 -1
- package/dist/board/render/renderers/video-card-renderer.js +1 -1
- package/dist/board/render/video-thumbnail.d.ts +16 -0
- package/dist/board/render/video-thumbnail.js +86 -0
- package/dist/board/task.d.ts +10 -5
- package/dist/board/task.js +159 -103
- package/dist/chunks/environment.d.ts +6 -6
- package/dist/chunks/environment.js +6 -6
- package/dist/chunks/http.d.ts +71 -5
- package/dist/chunks/http.js +1535 -167
- package/dist/chunks/transport.js +8 -1
- package/dist/chunks/websocket.d.ts +139 -3
- package/dist/http.d.ts +3 -3
- package/dist/index.d.ts +245 -4
- package/dist/index.js +438 -788
- package/dist/protocol/dist/board-document.d.ts +271 -53
- package/dist/protocol/dist/board-document.js +54 -22
- package/dist/protocol/dist/board-node.d.ts +18 -0
- package/dist/protocol/dist/board-node.js +239 -0
- package/dist/protocol/dist/board-url.d.ts +12 -0
- package/dist/protocol/dist/board-url.js +83 -0
- package/dist/protocol/dist/board.d.ts +5 -0
- package/dist/protocol/dist/index.d.ts +2 -1
- package/dist/protocol/dist/index.js +2 -1
- package/dist/protocol/dist/provenance.js +1 -0
- package/docs/work-runtime-guide.md +7 -7
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
2
|
import { unknownRealType } from "../../../protocol/dist/board-document.js";
|
|
3
3
|
import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
|
|
4
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
5
4
|
import { positionShell } from "./base-card-renderer.js";
|
|
5
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
6
6
|
import { Container, Graphics, Text } from "pixi.js";
|
|
7
7
|
//#region src/board/render/renderers/unknown-card-renderer.ts
|
|
8
8
|
const RADIUS = 10;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
2
|
import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
|
|
3
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
4
3
|
import { positionShell } from "./base-card-renderer.js";
|
|
4
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
5
5
|
import { Container, Graphics, Sprite, Text, Texture } from "pixi.js";
|
|
6
6
|
//#region src/board/render/renderers/video-card-renderer.ts
|
|
7
7
|
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Texture } from "pixi.js";
|
|
2
|
+
//#region src/board/render/video-thumbnail.d.ts
|
|
3
|
+
declare const VIDEO_THUMBNAIL_MAX_EDGE = 960;
|
|
4
|
+
type VideoNaturalSize = {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
8
|
+
declare function videoThumbnailSize(width: number, height: number, maxEdge?: number): {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
declare function videoTextureNaturalSize(texture: Texture): VideoNaturalSize | null;
|
|
13
|
+
/** Decode one static, bounded preview frame without starting playback. */
|
|
14
|
+
declare function loadVideoThumbnailTexture(url: string): Promise<Texture>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Texture } from "pixi.js";
|
|
2
|
+
//#region src/board/render/video-thumbnail.ts
|
|
3
|
+
const VIDEO_THUMBNAIL_MAX_EDGE = 960;
|
|
4
|
+
const VIDEO_LOAD_TIMEOUT_MS = 15e3;
|
|
5
|
+
const FRAME_PRESENT_TIMEOUT_MS = 250;
|
|
6
|
+
const naturalSizeByTexture = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function videoThumbnailSize(width, height, maxEdge = 960) {
|
|
8
|
+
if (width <= 0 || height <= 0 || maxEdge <= 0) throw new Error("Video has no displayable frame");
|
|
9
|
+
const scale = Math.min(1, maxEdge / Math.max(width, height));
|
|
10
|
+
return {
|
|
11
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
12
|
+
height: Math.max(1, Math.round(height * scale))
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function waitForFirstFrame(video) {
|
|
16
|
+
if (video.readyState >= 2) return Promise.resolve();
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const timeout = setTimeout(() => finish(/* @__PURE__ */ new Error("Video preview timed out")), VIDEO_LOAD_TIMEOUT_MS);
|
|
19
|
+
const onLoaded = () => finish();
|
|
20
|
+
const onError = () => finish(/* @__PURE__ */ new Error("Video preview could not be decoded"));
|
|
21
|
+
function finish(error) {
|
|
22
|
+
clearTimeout(timeout);
|
|
23
|
+
video.removeEventListener("loadeddata", onLoaded);
|
|
24
|
+
video.removeEventListener("error", onError);
|
|
25
|
+
if (error) reject(error);
|
|
26
|
+
else resolve();
|
|
27
|
+
}
|
|
28
|
+
video.addEventListener("loadeddata", onLoaded, { once: true });
|
|
29
|
+
video.addEventListener("error", onError, { once: true });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function waitForPresentedFrame(video) {
|
|
33
|
+
if (!video.requestVideoFrameCallback) return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
let settled = false;
|
|
36
|
+
let timeout;
|
|
37
|
+
const finish = () => {
|
|
38
|
+
if (settled) return;
|
|
39
|
+
settled = true;
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
resolve();
|
|
42
|
+
};
|
|
43
|
+
const handle = video.requestVideoFrameCallback(finish);
|
|
44
|
+
timeout = setTimeout(() => {
|
|
45
|
+
video.cancelVideoFrameCallback?.(handle);
|
|
46
|
+
finish();
|
|
47
|
+
}, FRAME_PRESENT_TIMEOUT_MS);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function videoTextureNaturalSize(texture) {
|
|
51
|
+
return naturalSizeByTexture.get(texture) ?? null;
|
|
52
|
+
}
|
|
53
|
+
/** Decode one static, bounded preview frame without starting playback. */
|
|
54
|
+
async function loadVideoThumbnailTexture(url) {
|
|
55
|
+
const video = document.createElement("video");
|
|
56
|
+
video.muted = true;
|
|
57
|
+
video.playsInline = true;
|
|
58
|
+
video.preload = "auto";
|
|
59
|
+
if (/^https?:/i.test(url)) video.crossOrigin = "anonymous";
|
|
60
|
+
try {
|
|
61
|
+
const ready = waitForFirstFrame(video);
|
|
62
|
+
video.src = url;
|
|
63
|
+
video.load();
|
|
64
|
+
await ready;
|
|
65
|
+
await waitForPresentedFrame(video);
|
|
66
|
+
const size = videoThumbnailSize(video.videoWidth, video.videoHeight);
|
|
67
|
+
const canvas = document.createElement("canvas");
|
|
68
|
+
canvas.width = size.width;
|
|
69
|
+
canvas.height = size.height;
|
|
70
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
71
|
+
if (!context) throw new Error("Canvas is not supported");
|
|
72
|
+
context.drawImage(video, 0, 0, size.width, size.height);
|
|
73
|
+
const texture = Texture.from(canvas);
|
|
74
|
+
naturalSizeByTexture.set(texture, {
|
|
75
|
+
width: video.videoWidth,
|
|
76
|
+
height: video.videoHeight
|
|
77
|
+
});
|
|
78
|
+
return texture;
|
|
79
|
+
} finally {
|
|
80
|
+
video.pause();
|
|
81
|
+
video.removeAttribute("src");
|
|
82
|
+
video.load();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { VIDEO_THUMBNAIL_MAX_EDGE, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize };
|
package/dist/board/task.d.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { BoardTaskSnapshot } from "../protocol/dist/board-document.js";
|
|
1
|
+
import { BoardTaskArtifact, BoardTaskSnapshot } from "../protocol/dist/board-document.js";
|
|
2
2
|
import { TaskRunRecord } from "../types.js";
|
|
3
3
|
//#region src/board/task.d.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Group provider blocks into user-facing works. A cover/poster sharing a stable
|
|
6
|
+
* provider id with playable media belongs to that work instead of becoming a
|
|
7
|
+
* competing image result.
|
|
7
8
|
*/
|
|
8
|
-
declare function
|
|
9
|
+
declare function taskArtifacts(blocks: Record<string, unknown>[]): BoardTaskArtifact[];
|
|
10
|
+
/** Highest-value artifact first, with provider order as the stable final tie. */
|
|
11
|
+
declare function rankedTaskArtifacts(artifacts: readonly BoardTaskArtifact[]): BoardTaskArtifact[];
|
|
12
|
+
declare function featuredTaskArtifact(artifacts: readonly BoardTaskArtifact[]): BoardTaskArtifact | undefined;
|
|
13
|
+
declare function taskArtifactPreviewUrl(artifact: BoardTaskArtifact | undefined): string | undefined;
|
|
9
14
|
/**
|
|
10
15
|
* Project an authoritative TaskRun into a small, replaceable Board display cache.
|
|
11
16
|
* Raw payloads, complete results and inline media are never copied into the Board.
|
|
12
17
|
*/
|
|
13
18
|
declare function taskRunToBoardTaskSnapshot(run: TaskRunRecord): BoardTaskSnapshot;
|
|
14
19
|
//#endregion
|
|
15
|
-
export {
|
|
20
|
+
export { featuredTaskArtifact, rankedTaskArtifacts, taskArtifactPreviewUrl, taskArtifacts, taskRunToBoardTaskSnapshot };
|
package/dist/board/task.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeBoardRemoteUrl } from "../protocol/dist/board-url.js";
|
|
2
|
+
import "../protocol/dist/board-document.js";
|
|
1
3
|
//#region src/board/task.ts
|
|
2
4
|
const EXCERPT_LIMIT = 240;
|
|
3
5
|
function record(value) {
|
|
@@ -9,85 +11,12 @@ function cleanExcerpt(value, limit = EXCERPT_LIMIT) {
|
|
|
9
11
|
if (!clean) return void 0;
|
|
10
12
|
return clean.length > limit ? `${clean.slice(0, limit - 3).trimEnd()}...` : clean;
|
|
11
13
|
}
|
|
12
|
-
function parseIpv4(host) {
|
|
13
|
-
const parts = host.split(".").map(Number);
|
|
14
|
-
return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) ? parts : null;
|
|
15
|
-
}
|
|
16
|
-
function isBlockedIpv4(host) {
|
|
17
|
-
const parts = parseIpv4(host);
|
|
18
|
-
if (!parts) return false;
|
|
19
|
-
const [first, second, third] = parts;
|
|
20
|
-
if (first === 0 || first === 10 || first === 127) return true;
|
|
21
|
-
if (first === 100 && second >= 64 && second <= 127) return true;
|
|
22
|
-
if (first === 169 && second === 254) return true;
|
|
23
|
-
if (first === 172 && second >= 16 && second <= 31) return true;
|
|
24
|
-
if (first === 192 && second === 168) return true;
|
|
25
|
-
if (first === 192 && second === 0 && (third === 0 || third === 2)) return true;
|
|
26
|
-
if (first === 192 && second === 88 && third === 99) return true;
|
|
27
|
-
if (first === 198 && (second === 18 || second === 19)) return true;
|
|
28
|
-
if (first === 198 && second === 51 && third === 100) return true;
|
|
29
|
-
if (first === 203 && second === 0 && third === 113) return true;
|
|
30
|
-
return first >= 224;
|
|
31
|
-
}
|
|
32
|
-
function expandIpv6(host) {
|
|
33
|
-
const [head, tail, extra] = host.toLowerCase().split("::");
|
|
34
|
-
if (extra !== void 0) return null;
|
|
35
|
-
const headParts = head ? head.split(":").filter(Boolean) : [];
|
|
36
|
-
const tailParts = tail ? tail.split(":").filter(Boolean) : [];
|
|
37
|
-
const missing = 8 - headParts.length - tailParts.length;
|
|
38
|
-
if (missing < 0 || tail === void 0 && missing !== 0) return null;
|
|
39
|
-
const parts = [
|
|
40
|
-
...headParts,
|
|
41
|
-
...Array.from({ length: missing }, () => "0"),
|
|
42
|
-
...tailParts
|
|
43
|
-
];
|
|
44
|
-
if (parts.length !== 8 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) return null;
|
|
45
|
-
return parts.map((part) => part.padStart(4, "0"));
|
|
46
|
-
}
|
|
47
|
-
function isBlockedIpv6(host) {
|
|
48
|
-
const parts = expandIpv6(host);
|
|
49
|
-
if (!parts) return true;
|
|
50
|
-
if (parts.every((part) => part === "0000")) return true;
|
|
51
|
-
if (parts.slice(0, 7).every((part) => part === "0000") && parts[7] === "0001") return true;
|
|
52
|
-
if (parts.slice(0, 5).every((part) => part === "0000") && parts[5] === "ffff") {
|
|
53
|
-
const high = Number.parseInt(parts[6] ?? "0", 16);
|
|
54
|
-
const low = Number.parseInt(parts[7] ?? "0", 16);
|
|
55
|
-
return isBlockedIpv4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
|
|
56
|
-
}
|
|
57
|
-
if (parts.slice(0, 6).every((part) => part === "0000")) return true;
|
|
58
|
-
const first = Number.parseInt(parts[0] ?? "0", 16);
|
|
59
|
-
if ((first & 65024) === 64512) return true;
|
|
60
|
-
if ((first & 65472) === 65152 || (first & 65472) === 65216) return true;
|
|
61
|
-
if ((first & 65280) === 65280) return true;
|
|
62
|
-
return parts[0] === "2001" && parts[1] === "0db8";
|
|
63
|
-
}
|
|
64
|
-
function isBlockedTaskOutputHost(hostname) {
|
|
65
|
-
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
66
|
-
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
|
|
67
|
-
if (parseIpv4(host)) return isBlockedIpv4(host);
|
|
68
|
-
return host.includes(":") && isBlockedIpv6(host);
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Normalize a persistable remote media URL and reject credentialed or visibly
|
|
72
|
-
* non-public hosts. Server-side fetchers must still validate resolved DNS addresses.
|
|
73
|
-
*/
|
|
74
|
-
function normalizeBoardTaskOutputUrl(value) {
|
|
75
|
-
if (typeof value !== "string") return void 0;
|
|
76
|
-
try {
|
|
77
|
-
const url = new URL(value.trim());
|
|
78
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
|
|
79
|
-
if (url.username || url.password || isBlockedTaskOutputHost(url.hostname)) return void 0;
|
|
80
|
-
return url.toString();
|
|
81
|
-
} catch {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
14
|
function blockText(block) {
|
|
86
15
|
return cleanExcerpt(block.text ?? block.content ?? block.value);
|
|
87
16
|
}
|
|
88
17
|
function blockUrl(block) {
|
|
89
18
|
const source = record(block.source);
|
|
90
|
-
return
|
|
19
|
+
return normalizeBoardRemoteUrl(source?.url ?? source?.src ?? block.url ?? block.src);
|
|
91
20
|
}
|
|
92
21
|
function blockMimeType(block) {
|
|
93
22
|
const source = record(block.source);
|
|
@@ -122,37 +51,163 @@ function generationPrompt(run) {
|
|
|
122
51
|
if (text) return text;
|
|
123
52
|
}
|
|
124
53
|
}
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
54
|
+
function blockMeta(block) {
|
|
55
|
+
return record(block.meta);
|
|
56
|
+
}
|
|
57
|
+
function blockIdentity(block) {
|
|
58
|
+
const meta = blockMeta(block);
|
|
59
|
+
const value = meta?.id ?? meta?.clip_id ?? meta?.clipId ?? block.id ?? block.clip_id ?? block.clipId;
|
|
60
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
61
|
+
return cleanExcerpt(String(value), 220);
|
|
62
|
+
}
|
|
63
|
+
function blockTitle(block) {
|
|
64
|
+
return cleanExcerpt(blockMeta(block)?.title ?? block.title ?? block.name, 240);
|
|
65
|
+
}
|
|
66
|
+
function blockDurationMs(block) {
|
|
67
|
+
const source = record(block.source);
|
|
68
|
+
const meta = blockMeta(block);
|
|
69
|
+
const milliseconds = positiveNumber(source?.durationMs ?? meta?.durationMs ?? block.durationMs);
|
|
70
|
+
if (milliseconds) return Math.round(milliseconds);
|
|
71
|
+
const seconds = positiveNumber(source?.duration ?? meta?.duration ?? block.duration);
|
|
72
|
+
return seconds ? Math.round(seconds * 1e3) : void 0;
|
|
73
|
+
}
|
|
74
|
+
function blockPreviewUrl(block) {
|
|
75
|
+
const source = record(block.source);
|
|
76
|
+
return normalizeBoardRemoteUrl(source?.poster ?? source?.thumbnail ?? source?.previewUrl ?? block.poster ?? block.thumbnail ?? block.previewUrl);
|
|
77
|
+
}
|
|
78
|
+
function artifactId(base, used, suffix) {
|
|
79
|
+
const stem = cleanExcerpt(base, 220) ?? "output";
|
|
80
|
+
let candidate = suffix ? `${stem}-${suffix}` : stem;
|
|
81
|
+
let sequence = 2;
|
|
82
|
+
while (used.has(candidate)) {
|
|
83
|
+
candidate = `${stem}-${suffix ? `${suffix}-` : ""}${sequence}`;
|
|
84
|
+
sequence += 1;
|
|
137
85
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
86
|
+
used.add(candidate);
|
|
87
|
+
return candidate;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Group provider blocks into user-facing works. A cover/poster sharing a stable
|
|
91
|
+
* provider id with playable media belongs to that work instead of becoming a
|
|
92
|
+
* competing image result.
|
|
93
|
+
*/
|
|
94
|
+
function taskArtifacts(blocks) {
|
|
95
|
+
const groups = /* @__PURE__ */ new Map();
|
|
96
|
+
blocks.forEach((block, index) => {
|
|
97
|
+
const key = blockIdentity(block) ?? `output-${index + 1}`;
|
|
98
|
+
const group = groups.get(key);
|
|
99
|
+
if (group) group.push(block);
|
|
100
|
+
else groups.set(key, [block]);
|
|
101
|
+
});
|
|
102
|
+
const artifacts = [];
|
|
103
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
104
|
+
for (const [groupId, blocksInGroup] of groups) {
|
|
105
|
+
const images = blocksInGroup.filter((block) => block.type === "image").map((block) => ({
|
|
106
|
+
block,
|
|
107
|
+
url: blockUrl(block)
|
|
108
|
+
})).filter((entry) => Boolean(entry.url));
|
|
109
|
+
const media = blocksInGroup.filter((block) => block.type === "video" || block.type === "audio").map((block) => ({
|
|
110
|
+
block,
|
|
111
|
+
url: blockUrl(block)
|
|
112
|
+
})).filter((entry) => Boolean(entry.url));
|
|
113
|
+
const pairedPreview = images[0];
|
|
114
|
+
media.forEach(({ block, url }, mediaIndex) => {
|
|
115
|
+
const type = block.type;
|
|
116
|
+
const mimeType = blockMimeType(block);
|
|
117
|
+
const title = blockTitle(block);
|
|
118
|
+
const durationMs = blockDurationMs(block);
|
|
119
|
+
const previewUrl = blockPreviewUrl(block) ?? pairedPreview?.url;
|
|
120
|
+
const id = artifactId(groupId, usedIds, media.length > 1 ? `${type}-${mediaIndex + 1}` : void 0);
|
|
121
|
+
if (type === "video") {
|
|
122
|
+
artifacts.push({
|
|
123
|
+
id,
|
|
124
|
+
type,
|
|
125
|
+
url,
|
|
126
|
+
...title ? { title } : {},
|
|
127
|
+
...previewUrl ? { previewUrl } : {},
|
|
128
|
+
...mimeType ? { mimeType } : {},
|
|
129
|
+
...durationMs ? { durationMs } : {},
|
|
130
|
+
...blockNaturalSize(block)
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
artifacts.push({
|
|
135
|
+
id,
|
|
136
|
+
type,
|
|
137
|
+
url,
|
|
138
|
+
...title ? { title } : {},
|
|
139
|
+
...previewUrl ? { previewUrl } : {},
|
|
140
|
+
...mimeType ? { mimeType } : {},
|
|
141
|
+
...durationMs ? { durationMs } : {}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
const firstUnpairedImage = media.length > 0 ? 1 : 0;
|
|
145
|
+
images.slice(firstUnpairedImage).forEach(({ block, url }, imageIndex) => {
|
|
141
146
|
const mimeType = blockMimeType(block);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
const title = blockTitle(block);
|
|
148
|
+
artifacts.push({
|
|
149
|
+
id: artifactId(groupId, usedIds, images.length - firstUnpairedImage > 1 ? `image-${imageIndex + 1}` : void 0),
|
|
150
|
+
type: "image",
|
|
151
|
+
url,
|
|
152
|
+
...title ? { title } : {},
|
|
153
|
+
...mimeType ? { mimeType } : {},
|
|
154
|
+
...blockNaturalSize(block)
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
blocksInGroup.filter((block) => block.type === "text").forEach((block, textIndex, texts) => {
|
|
149
158
|
const textExcerpt = blockText(block);
|
|
150
|
-
if (textExcerpt) return
|
|
159
|
+
if (!textExcerpt) return;
|
|
160
|
+
const title = blockTitle(block);
|
|
161
|
+
artifacts.push({
|
|
162
|
+
id: artifactId(groupId, usedIds, texts.length > 1 ? `text-${textIndex + 1}` : void 0),
|
|
151
163
|
type: "text",
|
|
164
|
+
...title ? { title } : {},
|
|
152
165
|
textExcerpt
|
|
153
|
-
};
|
|
154
|
-
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
155
168
|
}
|
|
169
|
+
return artifacts;
|
|
170
|
+
}
|
|
171
|
+
function artifactScore(artifact) {
|
|
172
|
+
const kind = {
|
|
173
|
+
text: 1,
|
|
174
|
+
image: 2,
|
|
175
|
+
audio: 3,
|
|
176
|
+
video: 4
|
|
177
|
+
}[artifact.type];
|
|
178
|
+
if (artifact.type === "text") return [kind, artifact.textExcerpt.length];
|
|
179
|
+
if (artifact.type === "image") return [kind, (artifact.naturalWidth ?? 0) * (artifact.naturalHeight ?? 0)];
|
|
180
|
+
return [
|
|
181
|
+
kind,
|
|
182
|
+
artifact.previewUrl ? 1 : 0,
|
|
183
|
+
artifact.durationMs ? 1 : 0,
|
|
184
|
+
artifact.durationMs ?? 0
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
function compareArtifacts(a, b) {
|
|
188
|
+
const left = artifactScore(a);
|
|
189
|
+
const right = artifactScore(b);
|
|
190
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
191
|
+
const difference = (right[index] ?? 0) - (left[index] ?? 0);
|
|
192
|
+
if (difference !== 0) return difference;
|
|
193
|
+
}
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
/** Highest-value artifact first, with provider order as the stable final tie. */
|
|
197
|
+
function rankedTaskArtifacts(artifacts) {
|
|
198
|
+
return artifacts.map((artifact, index) => ({
|
|
199
|
+
artifact,
|
|
200
|
+
index
|
|
201
|
+
})).sort((a, b) => compareArtifacts(a.artifact, b.artifact) || a.index - b.index).map(({ artifact }) => artifact);
|
|
202
|
+
}
|
|
203
|
+
function featuredTaskArtifact(artifacts) {
|
|
204
|
+
let featured;
|
|
205
|
+
for (const artifact of artifacts) if (!featured || compareArtifacts(artifact, featured) < 0) featured = artifact;
|
|
206
|
+
return featured;
|
|
207
|
+
}
|
|
208
|
+
function taskArtifactPreviewUrl(artifact) {
|
|
209
|
+
if (artifact?.type === "image") return artifact.url;
|
|
210
|
+
if (artifact?.type === "video" || artifact?.type === "audio") return artifact.previewUrl;
|
|
156
211
|
}
|
|
157
212
|
function taskTypeTitle(taskType) {
|
|
158
213
|
return taskType.replace(/[._-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
@@ -166,17 +221,18 @@ function taskRunToBoardTaskSnapshot(run) {
|
|
|
166
221
|
const blocks = run.taskType === "generation" ? generationOutput(run) : [];
|
|
167
222
|
const promptExcerpt = run.taskType === "generation" ? generationPrompt(run) : cleanExcerpt(data?.command ?? data?.prompt ?? data?.title);
|
|
168
223
|
const model = typeof data?.model === "string" ? data.model : void 0;
|
|
169
|
-
const
|
|
224
|
+
const allArtifacts = taskArtifacts(blocks);
|
|
225
|
+
const artifacts = rankedTaskArtifacts(allArtifacts).slice(0, 6);
|
|
170
226
|
return {
|
|
171
227
|
taskType: run.taskType,
|
|
172
228
|
status: run.status,
|
|
173
229
|
title: promptExcerpt ?? taskTypeTitle(run.taskType),
|
|
174
230
|
...model ? { model } : {},
|
|
175
231
|
...promptExcerpt ? { promptExcerpt } : {},
|
|
176
|
-
|
|
177
|
-
|
|
232
|
+
artifactCount: allArtifacts.length,
|
|
233
|
+
artifacts,
|
|
178
234
|
updatedAt: run.updatedAt
|
|
179
235
|
};
|
|
180
236
|
}
|
|
181
237
|
//#endregion
|
|
182
|
-
export {
|
|
238
|
+
export { featuredTaskArtifact, rankedTaskArtifacts, taskArtifactPreviewUrl, taskArtifacts, taskRunToBoardTaskSnapshot };
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
type CohubEnvironment = "prod" | "dev";
|
|
3
3
|
declare const COHUB_ENVIRONMENTS: {
|
|
4
4
|
readonly prod: {
|
|
5
|
-
readonly apiBaseUrl: "https://api.cohub.
|
|
6
|
-
readonly websocketUrl: "wss://gateway.cohub.
|
|
7
|
-
readonly voiceInputWebsocketUrl: "wss://gateway.cohub.
|
|
5
|
+
readonly apiBaseUrl: "https://api.cohub.live";
|
|
6
|
+
readonly websocketUrl: "wss://gateway.cohub.live/ws";
|
|
7
|
+
readonly voiceInputWebsocketUrl: "wss://gateway.cohub.live/asr/ws";
|
|
8
8
|
};
|
|
9
9
|
readonly dev: {
|
|
10
|
-
readonly apiBaseUrl: "https://api-dev.cohub.
|
|
11
|
-
readonly websocketUrl: "wss://gateway-dev.cohub.
|
|
12
|
-
readonly voiceInputWebsocketUrl: "wss://gateway-dev.cohub.
|
|
10
|
+
readonly apiBaseUrl: "https://api-dev.cohub.live";
|
|
11
|
+
readonly websocketUrl: "wss://gateway-dev.cohub.live/ws";
|
|
12
|
+
readonly voiceInputWebsocketUrl: "wss://gateway-dev.cohub.live/asr/ws";
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
15
|
declare const resolveCohubEnvironment: (env?: CohubEnvironment) => CohubEnvironment;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
//#region src/environment.ts
|
|
2
2
|
const COHUB_ENVIRONMENTS = {
|
|
3
3
|
prod: {
|
|
4
|
-
apiBaseUrl: "https://api.cohub.
|
|
5
|
-
websocketUrl: "wss://gateway.cohub.
|
|
6
|
-
voiceInputWebsocketUrl: "wss://gateway.cohub.
|
|
4
|
+
apiBaseUrl: "https://api.cohub.live",
|
|
5
|
+
websocketUrl: "wss://gateway.cohub.live/ws",
|
|
6
|
+
voiceInputWebsocketUrl: "wss://gateway.cohub.live/asr/ws"
|
|
7
7
|
},
|
|
8
8
|
dev: {
|
|
9
|
-
apiBaseUrl: "https://api-dev.cohub.
|
|
10
|
-
websocketUrl: "wss://gateway-dev.cohub.
|
|
11
|
-
voiceInputWebsocketUrl: "wss://gateway-dev.cohub.
|
|
9
|
+
apiBaseUrl: "https://api-dev.cohub.live",
|
|
10
|
+
websocketUrl: "wss://gateway-dev.cohub.live/ws",
|
|
11
|
+
voiceInputWebsocketUrl: "wss://gateway-dev.cohub.live/asr/ws"
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
14
|
const readRuntimeEnv = () => {
|