Skip to content

Commit 63d36fb

Browse files
hannahblairgradio-pr-botpngwn
authored
Handle gradio apps using state in the JS Client (#8439)
* send `null` for each `state` param in space api * add changeset * test * remove state value from payload from server * tweak * test * test * Revert "test" This reverts commit 182045e. * Revert "test" This reverts commit 70e074d. * fixes * add changeset * fixes * add changeset --------- Co-authored-by: gradio-pr-bot <[email protected]> Co-authored-by: pngwn <[email protected]>
1 parent 5c8915b commit 63d36fb

File tree

10 files changed

+304
-19
lines changed

10 files changed

+304
-19
lines changed

.changeset/young-poets-change.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@gradio/app": patch
3+
"@gradio/client": patch
4+
"@gradio/preview": patch
5+
"gradio": patch
6+
---
7+
8+
fix:Handle gradio apps using `state` in the JS Client

client/js/src/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
DuplicateOptions,
77
EndpointInfo,
88
JsApiData,
9+
PredictReturn,
910
SpaceStatus,
1011
Status,
1112
SubmitReturn,
@@ -114,7 +115,7 @@ export class Client {
114115
endpoint: string | number,
115116
data: unknown[] | Record<string, unknown>,
116117
event_data?: unknown
117-
) => Promise<SubmitReturn>;
118+
) => Promise<PredictReturn>;
118119
open_stream: () => Promise<void>;
119120
private resolve_config: (endpoint: string) => Promise<Config | undefined>;
120121
private resolve_cookies: () => Promise<void>;

client/js/src/helpers/data.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import type {
55
Config,
66
EndpointInfo,
77
JsApiData,
8-
DataType
8+
DataType,
9+
Dependency,
10+
ComponentMeta
911
} from "../types";
1012

1113
export function update_object(
@@ -118,3 +120,62 @@ export function post_message<Res = any>(
118120
window.parent.postMessage(message, origin, [channel.port2]);
119121
});
120122
}
123+
124+
/**
125+
* Handles the payload by filtering out state inputs and returning an array of resolved payload values.
126+
* We send null values for state inputs to the server, but we don't want to include them in the resolved payload.
127+
*
128+
* @param resolved_payload - The resolved payload values received from the client or the server
129+
* @param dependency - The dependency object.
130+
* @param components - The array of component metadata.
131+
* @param with_null_state - Optional. Specifies whether to include null values for state inputs. Default is false.
132+
* @returns An array of resolved payload values, filtered based on the dependency and component metadata.
133+
*/
134+
export function handle_payload(
135+
resolved_payload: unknown[],
136+
dependency: Dependency,
137+
components: ComponentMeta[],
138+
type: "input" | "output",
139+
with_null_state = false
140+
): unknown[] {
141+
if (type === "input" && !with_null_state) {
142+
throw new Error("Invalid code path. Cannot skip state inputs for input.");
143+
}
144+
// data comes from the server with null state values so we skip
145+
if (type === "output" && with_null_state) {
146+
return resolved_payload;
147+
}
148+
149+
let updated_payload: unknown[] = [];
150+
let payload_index = 0;
151+
for (let i = 0; i < dependency.inputs.length; i++) {
152+
const input_id = dependency.inputs[i];
153+
const component = components.find((c) => c.id === input_id);
154+
155+
if (component?.type === "state") {
156+
// input + with_null_state needs us to fill state with null values
157+
if (with_null_state) {
158+
if (resolved_payload.length === dependency.inputs.length) {
159+
const value = resolved_payload[payload_index];
160+
updated_payload.push(value);
161+
payload_index++;
162+
} else {
163+
updated_payload.push(null);
164+
}
165+
} else {
166+
// this is output & !with_null_state, we skip state inputs
167+
// the server payload always comes with null state values so we move along the payload index
168+
payload_index++;
169+
continue;
170+
}
171+
// input & !with_null_state isn't a case we care about, server needs null
172+
continue;
173+
} else {
174+
const value = resolved_payload[payload_index];
175+
updated_payload.push(value);
176+
payload_index++;
177+
}
178+
}
179+
180+
return updated_payload;
181+
}

client/js/src/test/api_info.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { initialise_server } from "./server";
1616
import { transformed_api_info } from "./test_data";
1717

1818
const server = initialise_server();
19-
const IS_NODE = process.env.TEST_MODE === "node";
2019

2120
beforeAll(() => server.listen());
2221
afterEach(() => server.resetHandlers());

client/js/src/test/data.test.ts

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
update_object,
44
walk_and_store_blobs,
55
skip_queue,
6-
post_message
6+
post_message,
7+
handle_payload
78
} from "../helpers/data";
89
import { NodeBlob } from "../client";
910
import { config_response, endpoint_info } from "./test_data";
@@ -276,3 +277,135 @@ describe("post_message", () => {
276277
]);
277278
});
278279
});
280+
281+
describe("handle_payload", () => {
282+
it("should return an input payload with null in place of `state` when with_null_state is true", () => {
283+
const resolved_payload = [2];
284+
const dependency = {
285+
inputs: [1, 2]
286+
};
287+
const components = [
288+
{ id: 1, type: "number" },
289+
{ id: 2, type: "state" }
290+
];
291+
const with_null_state = true;
292+
const result = handle_payload(
293+
resolved_payload,
294+
// @ts-ignore
295+
dependency,
296+
components,
297+
"input",
298+
with_null_state
299+
);
300+
expect(result).toEqual([2, null]);
301+
});
302+
it("should return an input payload with null in place of two `state` components when with_null_state is true", () => {
303+
const resolved_payload = ["hello", "goodbye"];
304+
const dependency = {
305+
inputs: [1, 2, 3, 4]
306+
};
307+
const components = [
308+
{ id: 1, type: "textbox" },
309+
{ id: 2, type: "state" },
310+
{ id: 3, type: "textbox" },
311+
{ id: 4, type: "state" }
312+
];
313+
const with_null_state = true;
314+
const result = handle_payload(
315+
resolved_payload,
316+
// @ts-ignore
317+
dependency,
318+
components,
319+
"input",
320+
with_null_state
321+
);
322+
expect(result).toEqual(["hello", null, "goodbye", null]);
323+
});
324+
325+
it("should return an output payload without the state component value when with_null_state is false", () => {
326+
const resolved_payload = ["hello", null];
327+
const dependency = {
328+
inputs: [2, 3]
329+
};
330+
const components = [
331+
{ id: 2, type: "textbox" },
332+
{ id: 3, type: "state" }
333+
];
334+
const with_null_state = false;
335+
const result = handle_payload(
336+
resolved_payload,
337+
// @ts-ignore
338+
dependency,
339+
components,
340+
"output",
341+
with_null_state
342+
);
343+
expect(result).toEqual(["hello"]);
344+
});
345+
346+
it("should return an ouput payload without the two state component values when with_null_state is false", () => {
347+
const resolved_payload = ["hello", null, "world", null];
348+
const dependency = {
349+
inputs: [2, 3, 4, 5]
350+
};
351+
const components = [
352+
{ id: 2, type: "textbox" },
353+
{ id: 3, type: "state" },
354+
{ id: 4, type: "textbox" },
355+
{ id: 5, type: "state" }
356+
];
357+
const with_null_state = false;
358+
const result = handle_payload(
359+
resolved_payload,
360+
// @ts-ignore
361+
dependency,
362+
components,
363+
"output",
364+
with_null_state
365+
);
366+
expect(result).toEqual(["hello", "world"]);
367+
});
368+
369+
it("should return an ouput payload with the two state component values when with_null_state is true", () => {
370+
const resolved_payload = ["hello", null, "world", null];
371+
const dependency = {
372+
inputs: [2, 3, 4, 5]
373+
};
374+
const components = [
375+
{ id: 2, type: "textbox" },
376+
{ id: 3, type: "state" },
377+
{ id: 4, type: "textbox" },
378+
{ id: 5, type: "state" }
379+
];
380+
const with_null_state = true;
381+
const result = handle_payload(
382+
resolved_payload,
383+
// @ts-ignore
384+
dependency,
385+
components,
386+
"output",
387+
with_null_state
388+
);
389+
expect(result).toEqual(["hello", null, "world", null]);
390+
});
391+
392+
it("should return the same payload where no state components are defined", () => {
393+
const resolved_payload = ["hello", "world"];
394+
const dependency = {
395+
inputs: [2, 3]
396+
};
397+
const components = [
398+
{ id: 2, type: "textbox" },
399+
{ id: 3, type: "textbox" }
400+
];
401+
const with_null_state = true;
402+
const result = handle_payload(
403+
resolved_payload,
404+
// @ts-ignore
405+
dependency,
406+
components,
407+
with_null_state
408+
);
409+
expect(result).toEqual(["hello", "world"]);
410+
});
411+
});

client/js/src/types.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// API Data Types
22

33
import { hardware_types } from "./helpers/spaces";
4+
import type { SvelteComponent } from "svelte";
5+
import type { ComponentType } from "svelte";
46

57
export interface ApiData {
68
label: string;
@@ -62,7 +64,7 @@ export type PredictFunction = (
6264
endpoint: string | number,
6365
data: unknown[] | Record<string, unknown>,
6466
event_data?: unknown
65-
) => Promise<SubmitReturn>;
67+
) => Promise<PredictReturn>;
6668

6769
// Event and Submission Types
6870

@@ -90,6 +92,14 @@ export type SubmitReturn = {
9092
destroy: () => void;
9193
};
9294

95+
export type PredictReturn = {
96+
type: EventType;
97+
time: Date;
98+
data: unknown;
99+
endpoint: string;
100+
fn_index: number;
101+
};
102+
93103
// Space Status Types
94104

95105
export type SpaceStatus = SpaceStatusNormal | SpaceStatusError;
@@ -128,7 +138,7 @@ export interface Config {
128138
analytics_enabled: boolean;
129139
connect_heartbeat: boolean;
130140
auth_message: string;
131-
components: any[];
141+
components: ComponentMeta[];
132142
css: string | null;
133143
js: string | null;
134144
head: string | null;
@@ -153,6 +163,45 @@ export interface Config {
153163
max_file_size?: number;
154164
}
155165

166+
// todo: DRY up types
167+
export interface ComponentMeta {
168+
type: string;
169+
id: number;
170+
has_modes: boolean;
171+
props: SharedProps;
172+
instance: SvelteComponent;
173+
component: ComponentType<SvelteComponent>;
174+
documentation?: Documentation;
175+
children?: ComponentMeta[];
176+
parent?: ComponentMeta;
177+
value?: any;
178+
component_class_id: string;
179+
key: string | number | null;
180+
rendered_in?: number;
181+
}
182+
183+
interface SharedProps {
184+
elem_id?: string;
185+
elem_classes?: string[];
186+
components?: string[];
187+
server_fns?: string[];
188+
interactive: boolean;
189+
[key: string]: unknown;
190+
root_url?: string;
191+
}
192+
193+
export interface Documentation {
194+
type?: TypeDescription;
195+
description?: TypeDescription;
196+
example_data?: string;
197+
}
198+
199+
interface TypeDescription {
200+
input_payload?: string;
201+
response_object?: string;
202+
payload?: string;
203+
}
204+
156205
export interface Dependency {
157206
id: number;
158207
targets: [number, string][];
@@ -218,6 +267,7 @@ export interface ClientOptions {
218267
hf_token?: `hf_${string}`;
219268
status_callback?: SpaceStatusCallback | null;
220269
auth?: [string, string] | null;
270+
with_null_state?: boolean;
221271
}
222272

223273
export interface FileData {

client/js/src/utils/predict.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Client } from "../client";
2-
import type { Dependency, SubmitReturn } from "../types";
2+
import type { Dependency, PredictReturn } from "../types";
33

44
export async function predict(
55
this: Client,
66
endpoint: string | number,
77
data: unknown[] | Record<string, unknown>
8-
): Promise<SubmitReturn> {
8+
): Promise<PredictReturn> {
99
let data_returned = false;
1010
let status_complete = false;
1111
let dependency: Dependency;
@@ -38,7 +38,7 @@ export async function predict(
3838
// if complete message comes before data, resolve here
3939
if (status_complete) {
4040
app.destroy();
41-
resolve(d as SubmitReturn);
41+
resolve(d as PredictReturn);
4242
}
4343
data_returned = true;
4444
result = d;
@@ -50,7 +50,7 @@ export async function predict(
5050
// if complete message comes after data, resolve here
5151
if (data_returned) {
5252
app.destroy();
53-
resolve(result as SubmitReturn);
53+
resolve(result as PredictReturn);
5454
}
5555
}
5656
});

0 commit comments

Comments
 (0)