adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
545 lines
12 KiB
Svelte
545 lines
12 KiB
Svelte
<script lang="ts">
|
|
import { MODALITIES, DATASET_MODALITY } from "./workflow-modalities";
|
|
import type { ModalityConfig } from "./workflow-modalities";
|
|
import ImageIcon from "./icons/ImageIcon.svelte";
|
|
import AudioIcon from "./icons/AudioIcon.svelte";
|
|
import VideoIcon from "./icons/VideoIcon.svelte";
|
|
import Model3DIcon from "./icons/Model3DIcon.svelte";
|
|
import TextIcon from "./icons/TextIcon.svelte";
|
|
import DatasetIcon from "./icons/DatasetIcon.svelte";
|
|
import PlusIcon from "./icons/PlusIcon.svelte";
|
|
import FunctionIcon from "./icons/FunctionIcon.svelte";
|
|
import StopIcon from "./icons/StopIcon.svelte";
|
|
import PlayIcon from "./icons/PlayIcon.svelte";
|
|
|
|
export interface BoundFnTemplate {
|
|
fn: string;
|
|
label: string;
|
|
inputs: { id: string; label: string; type: string }[];
|
|
outputs: { id: string; label: string; type: string }[];
|
|
}
|
|
|
|
interface Props {
|
|
running: boolean;
|
|
hasTransforms: boolean;
|
|
boundFns: BoundFnTemplate[];
|
|
activeModalityKey?: string | null;
|
|
server?: Record<string, any>;
|
|
readOnly?: boolean;
|
|
onopenpicker: (modality: ModalityConfig) => void;
|
|
onaddinput: (portType: string) => void;
|
|
onaddfn: (template: BoundFnTemplate) => void;
|
|
onrun: () => void;
|
|
onstop: () => void;
|
|
}
|
|
|
|
let {
|
|
running,
|
|
hasTransforms,
|
|
boundFns,
|
|
activeModalityKey = null,
|
|
server = {},
|
|
readOnly = false,
|
|
onopenpicker,
|
|
onaddinput,
|
|
onaddfn,
|
|
onrun,
|
|
onstop
|
|
}: Props = $props();
|
|
|
|
let available_modalities = $state<Set<string> | null>(null);
|
|
|
|
$effect(() => {
|
|
if (!server?.curated_modalities) {
|
|
available_modalities = new Set(MODALITIES.map((m) => m.key));
|
|
return;
|
|
}
|
|
void server.curated_modalities([]).then((raw: any) => {
|
|
try {
|
|
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
available_modalities = new Set(
|
|
Array.isArray(parsed) && parsed.length > 0
|
|
? parsed
|
|
: MODALITIES.map((m) => m.key)
|
|
);
|
|
} catch {
|
|
available_modalities = new Set(MODALITIES.map((m) => m.key));
|
|
}
|
|
});
|
|
});
|
|
|
|
const visible_modalities = $derived.by(() => {
|
|
const mods = available_modalities ?? new Set(MODALITIES.map((m) => m.key));
|
|
return MODALITIES.filter((m) => mods.has(m.key));
|
|
});
|
|
|
|
const INPUT_TYPES = [
|
|
{ key: "image", label: "Image" },
|
|
{ key: "text", label: "Text" },
|
|
{ key: "audio", label: "Audio" },
|
|
{ key: "video", label: "Video" },
|
|
{ key: "number", label: "Number" },
|
|
{ key: "file", label: "File" }
|
|
];
|
|
|
|
let showInputMenu = $state(false);
|
|
let showFnMenu = $state(false);
|
|
|
|
function closeMenus(): void {
|
|
showInputMenu = false;
|
|
showFnMenu = false;
|
|
}
|
|
|
|
function toggleInputMenu(e: MouseEvent) {
|
|
e.stopPropagation();
|
|
showFnMenu = false;
|
|
showInputMenu = !showInputMenu;
|
|
}
|
|
|
|
function toggleFnMenu(e: MouseEvent) {
|
|
e.stopPropagation();
|
|
showInputMenu = false;
|
|
showFnMenu = !showFnMenu;
|
|
}
|
|
|
|
function handleInputType(type: string, e: MouseEvent) {
|
|
e.stopPropagation();
|
|
showInputMenu = false;
|
|
onaddinput(type);
|
|
}
|
|
|
|
function handleFnClick(tmpl: BoundFnTemplate, e: MouseEvent) {
|
|
e.stopPropagation();
|
|
showFnMenu = false;
|
|
onaddfn(tmpl);
|
|
}
|
|
</script>
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="bottom-bar"
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
closeMenus();
|
|
}}
|
|
>
|
|
{#if !readOnly}
|
|
{#if visible_modalities.length > 0}
|
|
<div class="bb-modality-group">
|
|
<span class="bb-group-label">Add a node:</span>
|
|
<div class="bb-group">
|
|
{#each visible_modalities as m}
|
|
<div class="bb-modality-wrap">
|
|
<button
|
|
class="bb-btn bb-modality-btn"
|
|
class:bb-modality-active={activeModalityKey === m.key}
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
closeMenus();
|
|
onopenpicker(m);
|
|
}}
|
|
title="Add {m.label} node"
|
|
>
|
|
<span class="bb-icon">
|
|
{#if m.key === "image"}
|
|
<ImageIcon />
|
|
{:else if m.key === "audio"}
|
|
<AudioIcon />
|
|
{:else if m.key === "video"}
|
|
<VideoIcon />
|
|
{:else if m.key === "3d"}
|
|
<Model3DIcon />
|
|
{:else if m.key === "text"}
|
|
<TextIcon />
|
|
{/if}
|
|
</span>
|
|
<span class="bb-label">{m.label}</span>
|
|
{#if activeModalityKey === m.key}
|
|
<span class="bb-active-caret" aria-hidden="true"></span>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<button
|
|
class="bb-btn"
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
onopenpicker(DATASET_MODALITY);
|
|
}}
|
|
title="Add dataset node"
|
|
>
|
|
<span class="bb-icon">
|
|
<DatasetIcon />
|
|
</span>
|
|
<span class="bb-label">Data</span>
|
|
</button>
|
|
|
|
<div class="bb-divider"></div>
|
|
|
|
<div class="bb-input-wrap">
|
|
<button
|
|
class="bb-input-btn"
|
|
onclick={toggleInputMenu}
|
|
title="Add input node"
|
|
>
|
|
<PlusIcon />
|
|
Input
|
|
</button>
|
|
{#if showInputMenu}
|
|
<div class="input-type-menu">
|
|
{#each INPUT_TYPES as t}
|
|
<button
|
|
class="input-type-opt"
|
|
onclick={(e) => handleInputType(t.key, e)}>{t.label}</button
|
|
>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if boundFns.length > 0}
|
|
<div class="bb-input-wrap">
|
|
<button
|
|
class="bb-input-btn"
|
|
onclick={toggleFnMenu}
|
|
title="Add Python function node"
|
|
>
|
|
<FunctionIcon />
|
|
Function
|
|
</button>
|
|
{#if showFnMenu}
|
|
<div class="input-type-menu fn-menu">
|
|
{#each boundFns as t}
|
|
<button
|
|
class="input-type-opt"
|
|
onclick={(e) => handleFnClick(t, e)}
|
|
title={t.fn}
|
|
>
|
|
{t.label}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="bb-divider"></div>
|
|
{/if}
|
|
|
|
{#if running}
|
|
<button class="bb-run-btn stop" onclick={onstop}>
|
|
<StopIcon />
|
|
Stop
|
|
</button>
|
|
{:else}
|
|
<button class="bb-run-btn" onclick={onrun} disabled={!hasTransforms}>
|
|
<PlayIcon />
|
|
Run
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.bottom-bar {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 5px 8px;
|
|
background: rgba(16, 17, 24, 0.92);
|
|
border: 1px solid rgba(255, 255, 255, 0.07);
|
|
border-radius: 40px;
|
|
backdrop-filter: blur(16px);
|
|
-webkit-backdrop-filter: blur(16px);
|
|
box-shadow:
|
|
0 4px 24px rgba(0, 0, 0, 0.5),
|
|
0 0 0 1px rgba(255, 255, 255, 0.03);
|
|
z-index: 20;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.bb-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
}
|
|
|
|
.bb-modality-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 2px 8px 2px 10px;
|
|
border-radius: 32px;
|
|
background: rgba(255, 255, 255, 0.025);
|
|
}
|
|
|
|
.bb-group-label {
|
|
font-family: "Manrope", sans-serif;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: #4a4d57;
|
|
flex-shrink: 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
:global(body:not(.dark)) .bb-modality-group {
|
|
background: rgba(0, 0, 0, 0.03);
|
|
}
|
|
|
|
:global(body:not(.dark)) .bb-group-label {
|
|
color: #8b8d98;
|
|
}
|
|
|
|
.bb-modality-active {
|
|
background: rgba(249, 115, 22, 0.14);
|
|
color: #f97316;
|
|
box-shadow: inset 0 0 0 1.5px rgba(249, 115, 22, 0.5);
|
|
}
|
|
|
|
.bb-modality-active:hover {
|
|
background: rgba(249, 115, 22, 0.18);
|
|
color: #f97316;
|
|
}
|
|
|
|
:global(body:not(.dark)) .bb-modality-active {
|
|
background: rgba(249, 115, 22, 0.12);
|
|
color: #d65500;
|
|
box-shadow: inset 0 0 0 1.5px rgba(249, 115, 22, 0.45);
|
|
}
|
|
|
|
:global(body:not(.dark)) .bb-modality-active:hover {
|
|
background: rgba(249, 115, 22, 0.18);
|
|
color: #d65500;
|
|
}
|
|
|
|
.bb-active-caret {
|
|
position: absolute;
|
|
top: -6px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 0;
|
|
height: 0;
|
|
border-left: 5px solid transparent;
|
|
border-right: 5px solid transparent;
|
|
border-bottom: 6px solid #f97316;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.bb-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 12px;
|
|
border: none;
|
|
border-radius: 28px;
|
|
background: transparent;
|
|
color: #8b8d98;
|
|
font-family: "Manrope", sans-serif;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition:
|
|
background 0.15s,
|
|
color 0.15s;
|
|
}
|
|
|
|
.bb-btn:hover {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
color: #d5d6de;
|
|
}
|
|
|
|
.bb-modality-wrap {
|
|
position: relative;
|
|
}
|
|
|
|
.bb-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.bb-label {
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.bb-divider {
|
|
width: 1px;
|
|
height: 20px;
|
|
background: rgba(255, 255, 255, 0.07);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.bb-input-wrap {
|
|
position: relative;
|
|
}
|
|
|
|
.bb-input-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 6px 12px;
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 28px;
|
|
background: transparent;
|
|
color: #6b6e78;
|
|
font-family: "Manrope", sans-serif;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition:
|
|
background 0.15s,
|
|
color 0.15s,
|
|
border-color 0.15s;
|
|
}
|
|
|
|
.bb-input-btn:hover {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
color: #a0a2ae;
|
|
border-color: rgba(255, 255, 255, 0.14);
|
|
}
|
|
|
|
.input-type-menu {
|
|
position: absolute;
|
|
bottom: calc(100% + 16px);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: #16171f;
|
|
border: 1px solid #2a2b36;
|
|
border-radius: 10px;
|
|
padding: 4px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1px;
|
|
/* Negative Y offset casts the shadow UPWARD, since the menu opens
|
|
* above the bottom bar. The previous downward shadow smudged onto
|
|
* the button below. Pair with a subtle inset ring for crisper
|
|
* edge definition on dark bg. */
|
|
box-shadow:
|
|
0 -6px 20px rgba(0, 0, 0, 0.35),
|
|
0 0 0 1px rgba(255, 255, 255, 0.03);
|
|
z-index: 30;
|
|
min-width: 90px;
|
|
}
|
|
|
|
.input-type-opt {
|
|
padding: 7px 12px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
background: transparent;
|
|
color: #8b8d98;
|
|
font-family: "Manrope", sans-serif;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
transition:
|
|
background 0.1s,
|
|
color 0.1s;
|
|
}
|
|
|
|
.input-type-opt:hover {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
color: #d5d6de;
|
|
}
|
|
|
|
.bb-run-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 7px 16px;
|
|
border: none;
|
|
border-radius: 28px;
|
|
background: linear-gradient(135deg, #f97316, #ea580c);
|
|
color: #fff;
|
|
font-family: "Manrope", sans-serif;
|
|
font-size: 12.5px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
transition:
|
|
transform 0.1s,
|
|
box-shadow 0.15s;
|
|
box-shadow: 0 2px 10px rgba(249, 115, 22, 0.3);
|
|
}
|
|
|
|
.bb-run-btn:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 18px rgba(249, 115, 22, 0.4);
|
|
}
|
|
|
|
.bb-run-btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.bb-run-btn:disabled {
|
|
opacity: 0.35;
|
|
cursor: default;
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.bb-run-btn.stop {
|
|
background: linear-gradient(135deg, #ef4444, #dc2626);
|
|
box-shadow: 0 2px 10px rgba(239, 68, 68, 0.3);
|
|
}
|
|
|
|
.bb-run-btn.stop:hover {
|
|
box-shadow: 0 4px 18px rgba(239, 68, 68, 0.4);
|
|
}
|
|
|
|
/* Light mode */
|
|
:global(body:not(.dark)) .bottom-bar {
|
|
background: rgba(255, 255, 255, 0.92);
|
|
border-color: rgba(0, 0, 0, 0.08);
|
|
box-shadow:
|
|
0 4px 24px rgba(0, 0, 0, 0.12),
|
|
0 0 0 1px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
:global(body:not(.dark)) .bb-btn {
|
|
color: #6b6e78;
|
|
}
|
|
|
|
:global(body:not(.dark)) .bb-btn:hover {
|
|
background: rgba(0, 0, 0, 0.04);
|
|
color: #1a1b25;
|
|
}
|
|
|
|
:global(body:not(.dark)) .bb-divider {
|
|
background: rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
:global(body:not(.dark)) .bb-input-btn {
|
|
border-color: rgba(0, 0, 0, 0.1);
|
|
color: #8b8d98;
|
|
}
|
|
|
|
:global(body:not(.dark)) .bb-input-btn:hover {
|
|
background: rgba(0, 0, 0, 0.04);
|
|
color: #3e4050;
|
|
}
|
|
|
|
:global(body:not(.dark)) .input-type-menu {
|
|
background: #ffffff;
|
|
border-color: #e2e4ea;
|
|
}
|
|
|
|
:global(body:not(.dark)) .input-type-opt {
|
|
color: #6b6e78;
|
|
}
|
|
|
|
:global(body:not(.dark)) .input-type-opt:hover {
|
|
background: #f0f1f5;
|
|
color: #1a1b25;
|
|
}
|
|
</style>
|