Skip to content

Commit ac6faf3

Browse files
committed
Refactor WidgetWrapper
1 parent 41ddbb0 commit ac6faf3

File tree

34 files changed

+1228
-1190
lines changed

34 files changed

+1228
-1190
lines changed

packages/widgets/src/lib/components/Icons/IconMaximize.svelte

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<script lang="ts">
2+
import type { ExampleRunOpts, WidgetProps } from "../types.js";
3+
import type { WidgetExample, WidgetExampleAttribute } from "@huggingface/tasks";
4+
5+
type TWidgetExample = $$Generic<WidgetExample>;
6+
7+
import { onMount } from "svelte";
8+
import { slide } from "svelte/transition";
9+
10+
import IconCaretDownV2 from "../../..//Icons/IconCaretDownV2.svelte";
11+
import WidgetExamplesGroup from "./WidgetExamplesGroup.svelte";
12+
import { getQueryParamVal, getWidgetExample } from "../../..//InferenceWidget/shared/helpers.js";
13+
14+
export let isLoading = false;
15+
export let model: WidgetProps["model"];
16+
export let callApiOnMount: WidgetProps["callApiOnMount"];
17+
export let exampleQueryParams: WidgetExampleAttribute[] = [];
18+
export let applyInputSample: (sample: TWidgetExample, opts?: ExampleRunOpts) => void;
19+
export let validateExample: (sample: WidgetExample) => sample is TWidgetExample;
20+
21+
export let examplesAll: TWidgetExample[];
22+
23+
interface ExamplesGroup {
24+
group: string;
25+
examples: TWidgetExample[];
26+
}
27+
28+
$: exampleGroups = getExamplesGroups(examplesAll);
29+
$: examples = exampleGroups?.[0]?.examples ?? [];
30+
// for examples with multiple groups, a group needs to be selected first, before an example can be clicked
31+
$: clickable = exampleGroups?.length === 1;
32+
let containerEl: HTMLElement;
33+
let isOptionsVisible = false;
34+
let title = "Examples";
35+
36+
function getExamplesGroups(_examplesAll: TWidgetExample[]): ExamplesGroup[] {
37+
const examplesAll = _examplesAll.map((sample, idx) => ({
38+
example_title: `Example ${++idx}`,
39+
group: "Group 1",
40+
...sample,
41+
}));
42+
const examplesGroups: ExamplesGroup[] = [];
43+
for (const example of examplesAll) {
44+
const groupExists = examplesGroups.find(({ group }) => group === example.group);
45+
if (!groupExists) {
46+
examplesGroups.push({ group: example.group as string, examples: [] });
47+
}
48+
examplesGroups.find(({ group }) => group === example.group)?.examples.push(example);
49+
}
50+
return examplesGroups;
51+
}
52+
53+
function _applyInputSample(idx: number) {
54+
hideOptions();
55+
const sample = examples[idx];
56+
title = sample.example_title as string;
57+
applyInputSample(sample);
58+
}
59+
60+
function _previewInputSample(idx: number) {
61+
const sample = examples[idx];
62+
applyInputSample(sample, { isPreview: true });
63+
}
64+
65+
function toggleOptionsVisibility() {
66+
isOptionsVisible = !isOptionsVisible;
67+
}
68+
69+
function onClick(e: MouseEvent | TouchEvent) {
70+
let targetElement = e.target;
71+
do {
72+
if (targetElement === containerEl) {
73+
// This is a click inside. Do nothing, just return.
74+
return;
75+
}
76+
targetElement = (targetElement as HTMLElement).parentElement;
77+
} while (targetElement);
78+
// This is a click outside
79+
hideOptions();
80+
}
81+
82+
function hideOptions() {
83+
isOptionsVisible = false;
84+
}
85+
function changeGroup(e: CustomEvent<string>) {
86+
const selectedGroup = e.detail;
87+
const newGroup = exampleGroups.find(({ group }) => group === selectedGroup);
88+
if (!newGroup) {
89+
return;
90+
}
91+
examples = newGroup?.examples ?? [];
92+
title = "Examples";
93+
clickable = true;
94+
}
95+
96+
onMount(() => {
97+
// run random example onMount
98+
(async () => {
99+
const exampleFromQueryParams = {} as TWidgetExample;
100+
for (const key of exampleQueryParams) {
101+
const val = getQueryParamVal(key);
102+
if (val) {
103+
// @ts-expect-error complicated type
104+
exampleFromQueryParams[key] = val;
105+
}
106+
}
107+
if (Object.keys(exampleFromQueryParams).length) {
108+
// run widget example from query params
109+
applyInputSample(exampleFromQueryParams);
110+
} else {
111+
// run random widget example
112+
const example = getWidgetExample<TWidgetExample>(model, validateExample);
113+
if (callApiOnMount && example) {
114+
applyInputSample(example, { inferenceOpts: { isOnLoadCall: true } });
115+
}
116+
}
117+
})();
118+
});
119+
</script>
120+
121+
<svelte:window on:click={onClick} />
122+
123+
{#if examplesAll.length}
124+
<div class="ml-auto flex gap-x-1">
125+
<!-- Example Groups -->
126+
{#if exampleGroups.length > 1}
127+
<WidgetExamplesGroup
128+
on:groupSelected={changeGroup}
129+
{isLoading}
130+
groupNames={exampleGroups.map(({ group }) => group)}
131+
/>
132+
{/if}
133+
134+
<!-- Example picker -->
135+
<div
136+
class="relative mb-1.5
137+
{isLoading || !clickable ? 'pointer-events-none opacity-50' : ''}
138+
{isOptionsVisible ? 'z-10' : ''}"
139+
bind:this={containerEl}
140+
>
141+
<!-- svelte-ignore a11y-click-events-have-key-events -->
142+
<div
143+
class="inline-flex w-32 justify-between rounded-md border border-gray-100 px-4 py-1"
144+
on:click={toggleOptionsVisibility}
145+
>
146+
<div class="truncate text-sm">{title}</div>
147+
<IconCaretDownV2
148+
classNames="-mr-1 ml-2 h-5 w-5 transition ease-in-out transform {isOptionsVisible && '-rotate-180'}"
149+
/>
150+
</div>
151+
152+
{#if isOptionsVisible}
153+
<div
154+
class="absolute right-0 mt-1 w-full origin-top-right rounded-md ring-1 ring-black ring-opacity-10"
155+
transition:slide
156+
>
157+
<div class="rounded-md bg-white py-1" role="none">
158+
{#each examples as { example_title }, i}
159+
<!-- svelte-ignore a11y-click-events-have-key-events a11y-mouse-events-have-key-events -->
160+
<div
161+
class="cursor-pointer truncate px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-200"
162+
on:mouseover={() => _previewInputSample(i)}
163+
on:click={() => _applyInputSample(i)}
164+
>
165+
{example_title}
166+
</div>
167+
{/each}
168+
</div>
169+
</div>
170+
{/if}
171+
</div>
172+
</div>
173+
{/if}
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
<script lang="ts">
2+
import { createEventDispatcher } from "svelte";
23
import { slide } from "svelte/transition";
34
45
import IconCaretDownV2 from "../../..//Icons/IconCaretDownV2.svelte";
56
67
export let classNames = "";
78
export let isLoading = false;
8-
export let inputGroups: string[];
9-
export let selectedInputGroup: string;
9+
export let groupNames: string[];
10+
11+
const dispatch = createEventDispatcher<{ groupSelected: string }>();
1012
1113
let containerEl: HTMLElement;
1214
let isOptionsVisible = false;
13-
let title = "Groups";
15+
let selectedGroupName: string;
1416
1517
function chooseInputGroup(idx: number) {
1618
hideOptions();
17-
const inputGroup = inputGroups[idx];
18-
title = inputGroup;
19-
selectedInputGroup = inputGroup;
19+
const inputGroup = groupNames[idx];
20+
selectedGroupName = inputGroup;
21+
dispatch("groupSelected", selectedGroupName);
2022
}
2123
2224
function toggleOptionsVisibility() {
@@ -54,7 +56,7 @@
5456
class="inline-flex w-32 justify-between rounded-md border border-gray-100 px-4 py-1"
5557
on:click={toggleOptionsVisibility}
5658
>
57-
<div class="truncate text-sm">{title}</div>
59+
<div class="truncate text-sm">{selectedGroupName ?? "Groups"}</div>
5860
<IconCaretDownV2
5961
classNames="-mr-1 ml-2 h-5 w-5 transition ease-in-out transform {isOptionsVisible && '-rotate-180'}"
6062
/>
@@ -66,7 +68,7 @@
6668
transition:slide
6769
>
6870
<div class="rounded-md bg-white py-1" role="none">
69-
{#each inputGroups as inputGroup, i}
71+
{#each groupNames as inputGroup, i}
7072
<!-- svelte-ignore a11y-click-events-have-key-events -->
7173
<div
7274
class="truncate px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-200"

packages/widgets/src/lib/components/InferenceWidget/shared/WidgetFooter/WidgetFooter.svelte

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
<script lang="ts">
22
import IconCode from "../../..//Icons/IconCode.svelte";
3-
import IconMaximize from "../../..//Icons/IconMaximize.svelte";
43
5-
export let isMaximized = false;
64
export let outputJson: string;
75
export let isDisabled = false;
86
@@ -22,14 +20,6 @@
2220
JSON Output
2321
</button>
2422
{/if}
25-
<button class="ml-auto flex items-center" on:click|preventDefault={() => (isMaximized = !isMaximized)}>
26-
<IconMaximize classNames="mr-1" />
27-
{#if !isMaximized}
28-
Maximize
29-
{:else}
30-
Minimize
31-
{/if}
32-
</button>
3323
</div>
3424
{#if outputJson && isOutputJsonVisible}
3525
<pre

packages/widgets/src/lib/components/InferenceWidget/shared/WidgetHeader/WidgetHeader.svelte

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,46 @@
11
<script lang="ts">
2-
import { TASKS_DATA, type PipelineType } from "@huggingface/tasks";
2+
import { getContext } from "svelte";
3+
import { TASKS_DATA } from "@huggingface/tasks";
4+
import type { WidgetExample, WidgetExampleAttribute } from "@huggingface/tasks";
5+
import type { WidgetProps, ExampleRunOpts } from "../types.js";
36
import { getPipelineTask } from "../../../../utils/ViewUtils.js";
47
import IconInfo from "../../..//Icons/IconInfo.svelte";
58
import IconLightning from "../../..//Icons/IconLightning.svelte";
69
import PipelineTag from "../../../PipelineTag/PipelineTag.svelte";
10+
import WidgetExamples from "../WidgetExamples/WidgetExamples.svelte";
711
12+
type TWidgetExample = $$Generic<WidgetExample>;
13+
14+
export let model: WidgetProps["model"];
815
export let noTitle = false;
916
export let title: string | null = null;
10-
export let pipeline: PipelineType | undefined;
17+
export let isLoading = false;
1118
export let isDisabled = false;
19+
export let applyInputSample: (sample: TWidgetExample, opts?: ExampleRunOpts) => void = () => {};
20+
export let validateExample: (sample: WidgetExample) => sample is TWidgetExample = (
21+
sample
22+
): sample is TWidgetExample => true;
23+
export let callApiOnMount: WidgetProps["callApiOnMount"] = false;
24+
export let exampleQueryParams: WidgetExampleAttribute[] = [];
25+
26+
const pipeline = model?.pipeline_tag;
1227
1328
$: task = pipeline ? getPipelineTask(pipeline) : undefined;
29+
30+
$: examplesAll = getExamples(isDisabled);
31+
32+
function getExamples(isDisabled: boolean): TWidgetExample[] {
33+
const examples = ((model?.widgetData ?? []) as TWidgetExample[])
34+
.filter((sample) => validateExample(sample) && (!isDisabled || sample.output !== undefined))
35+
.sort((sample1, sample2) => (sample2.example_title ? 1 : 0) - (sample1.example_title ? 1 : 0));
36+
37+
if (isDisabled && !examples.length) {
38+
const makeWidgetUndisplayable = getContext<() => void>("makeWidgetUndisplayable");
39+
makeWidgetUndisplayable();
40+
}
41+
42+
return examples;
43+
}
1444
</script>
1545

1646
<div class="mb-2 flex items-center font-semibold">
@@ -45,5 +75,15 @@
4575
<PipelineTag classNames="mr-2 mb-1.5" {pipeline} />
4676
</a>
4777
{/if}
48-
<slot />
78+
{#if examplesAll.length}
79+
<WidgetExamples
80+
{examplesAll}
81+
{isLoading}
82+
{applyInputSample}
83+
{validateExample}
84+
{model}
85+
{callApiOnMount}
86+
{exampleQueryParams}
87+
/>
88+
{/if}
4989
</div>

packages/widgets/src/lib/components/InferenceWidget/shared/WidgetInfo/WidgetInfo.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
<script lang="ts">
22
import { InferenceDisplayability } from "@huggingface/tasks";
33
import { type WidgetProps, type ModelLoadInfo, LoadState, ComputeType } from "../types.js";
4+
import WidgetModelLoading from "../WidgetModelLoading/WidgetModelLoading.svelte";
45
import IconAzureML from "../../..//Icons/IconAzureML.svelte";
56
import IconInfo from "../../..//Icons/IconInfo.svelte";
67
78
export let model: WidgetProps["model"];
89
export let computeTime: string = "";
910
export let error: string = "";
1011
export let modelLoadInfo: ModelLoadInfo | undefined = undefined;
11-
export let modelTooBig = false;
12+
export let modelLoading = {
13+
isLoading: false,
14+
estimatedTime: 0,
15+
};
16+
17+
$: modelTooBig = modelLoadInfo?.state === "TooBig";
1218
1319
const state = {
1420
[LoadState.Loadable]: "This model can be loaded on the Inference API on-demand.",
@@ -113,4 +119,7 @@
113119
{#if error}
114120
<div class="alert alert-error mt-3">{error}</div>
115121
{/if}
122+
{#if modelLoading.isLoading}
123+
<WidgetModelLoading estimatedTime={modelLoading.estimatedTime} />
124+
{/if}
116125
</div>

0 commit comments

Comments
 (0)