import { writable, type Writable, get } from "svelte/store"; import type { ComponentMeta, Dependency, LayoutNode, TargetMap, LoadingComponent } from "./types"; import { load_component } from "virtual:component-loader"; import type { client_return } from "@gradio/client"; import { create_loading_status_store } from "./stores"; import { _ } from "svelte-i18n"; export interface UpdateTransaction { id: number; value: any; prop: string; } let pending_updates: UpdateTransaction[][] = []; const is_browser = typeof window !== "undefined"; const raf = is_browser ? requestAnimationFrame : async (fn: () => Promise | void) => await fn(); /** * Create a store with the layout and a map of targets * @returns A store with the layout and a map of targets */ export function create_components(initial_layout: ComponentMeta | undefined): { layout: Writable; targets: Writable; update_value: (updates: UpdateTransaction[]) => void; get_data: (id: number) => any | Promise; modify_stream: (id: number, state: "open" | "waiting" | "closed") => void; get_stream_state: (id: number) => "open" | "waiting" | "closed" | "not_set"; set_time_limit: (id: number, time_limit: number | undefined) => void; loading_status: ReturnType; scheduled_updates: Writable; create_layout: (args: { app: client_return; components: ComponentMeta[]; layout: LayoutNode; dependencies: Dependency[]; root: string; options: { fill_height: boolean; }; }) => Promise; rerender_layout: (args: { render_id: number; components: ComponentMeta[]; layout: LayoutNode; root: string; dependencies: Dependency[]; }) => void; } { let _component_map: Map; let target_map: Writable = writable({}); let _target_map: TargetMap = {}; let inputs: Set; let outputs: Set; let constructor_map: Map; let instance_map: { [id: number]: ComponentMeta }; let loading_status: ReturnType = create_loading_status_store(); const layout_store: Writable = writable(initial_layout); let _components: ComponentMeta[] = []; let app: client_return; let keyed_component_values: Record = {}; let _rootNode: ComponentMeta; function set_event_specific_args(dependencies: Dependency[]): void { dependencies.forEach((dep) => { dep.targets.forEach((target) => { const instance = instance_map[target[0]]; if (instance && dep.event_specific_args?.length > 0) { dep.event_specific_args?.forEach((arg: string) => { instance.props[arg] = dep[arg as keyof Dependency]; }); } }); }); } async function create_layout({ app: _app, components, layout, dependencies, root, options }: { app: client_return; components: ComponentMeta[]; layout: LayoutNode; dependencies: Dependency[]; root: string; options: { fill_height: boolean; }; }): Promise { // make sure the state is settled before proceeding flush(); app = _app; store_keyed_values(_components); _components = components; inputs = new Set(); outputs = new Set(); pending_updates = []; constructor_map = new Map(); _component_map = new Map(); instance_map = {}; _rootNode = { id: layout.id, type: "column", props: { interactive: false, scale: options.fill_height ? 1 : null }, has_modes: false, instance: null as unknown as ComponentMeta["instance"], component: null as unknown as ComponentMeta["component"], component_class_id: "", key: null }; components.push(_rootNode); dependencies.forEach((dep) => { loading_status.register(dep.id, dep.inputs, dep.outputs); dep.frontend_fn = process_frontend_fn( dep.js, !!dep.backend_fn, dep.inputs.length, dep.outputs.length ); create_target_meta(dep.targets, dep.id, _target_map); get_inputs_outputs(dep, inputs, outputs); }); target_map.set(_target_map); constructor_map = preload_all_components(components, root); instance_map = components.reduce( (acc, c) => { acc[c.id] = c; return acc; }, {} as { [id: number]: ComponentMeta } ); await walk_layout(layout, root); layout_store.set(_rootNode); set_event_specific_args(dependencies); } /** * Rerender the layout when the config has been modified to attach new components */ function rerender_layout({ render_id, components, layout, root, dependencies }: { render_id: number; components: ComponentMeta[]; layout: LayoutNode; root: string; dependencies: Dependency[]; }): void { let _constructor_map = preload_all_components(components, root); _constructor_map.forEach((v, k) => { constructor_map.set(k, v); }); _target_map = {}; dependencies.forEach((dep) => { loading_status.register(dep.id, dep.inputs, dep.outputs); dep.frontend_fn = process_frontend_fn( dep.js, !!dep.backend_fn, dep.inputs.length, dep.outputs.length ); create_target_meta(dep.targets, dep.id, _target_map); get_inputs_outputs(dep, inputs, outputs); }); target_map.set(_target_map); let current_element = instance_map[layout.id]; let all_current_children: ComponentMeta[] = []; const add_to_current_children = (component: ComponentMeta): void => { all_current_children.push(component); if (component.children) { component.children.forEach((child) => { add_to_current_children(child); }); } }; add_to_current_children(current_element); store_keyed_values(all_current_children); Object.entries(instance_map).forEach(([id, component]) => { let _id = Number(id); if (component.rendered_in === render_id) { delete instance_map[_id]; if (_component_map.has(_id)) { _component_map.delete(_id); } } }); components.forEach((c) => { instance_map[c.id] = c; _component_map.set(c.id, c); }); if (current_element.parent) { current_element.parent.children![ current_element.parent.children!.indexOf(current_element) ] = instance_map[layout.id]; } walk_layout(layout, root, current_element.parent).then(() => { layout_store.set(_rootNode); }); set_event_specific_args(dependencies); } async function walk_layout( node: LayoutNode, root: string, parent?: ComponentMeta ): Promise { const instance = instance_map[node.id]; instance.component = (await constructor_map.get( instance.component_class_id || instance.type ))!?.default; instance.parent = parent; if (instance.type === "dataset") { instance.props.component_map = get_component( instance.type, instance.component_class_id, root, _components, instance.props.components ).example_components; } if (_target_map[instance.id]) { instance.props.attached_events = Object.keys(_target_map[instance.id]); } instance.props.interactive = determine_interactivity( instance.id, instance.props.interactive, instance.props.value, inputs, outputs ); instance.props.server = process_server_fn( instance.id, instance.props.server_fns, app ); if ( instance.key != null && keyed_component_values[instance.key] !== undefined ) { instance.props.value = keyed_component_values[instance.key]; } _component_map.set(instance.id, instance); if (node.children) { instance.children = await Promise.all( node.children.map((v) => walk_layout(v, root, instance)) ); } return instance; } let update_scheduled = false; let update_scheduled_store = writable(false); function store_keyed_values(components: ComponentMeta[]): void { components.forEach((c) => { if (c.key != null) { keyed_component_values[c.key] = c.props.value; } }); } function flush(): void { layout_store.update((layout) => { for (let i = 0; i < pending_updates.length; i++) { for (let j = 0; j < pending_updates[i].length; j++) { const update = pending_updates[i][j]; if (!update) continue; const instance = instance_map[update.id]; if (!instance) continue; let new_value; if (update.value instanceof Map) new_value = new Map(update.value); else if (update.value instanceof Set) new_value = new Set(update.value); else if (Array.isArray(update.value)) new_value = [...update.value]; else if (update.value === null) new_value = null; else if (typeof update.value === "object") new_value = { ...update.value }; else new_value = update.value; instance.props[update.prop] = new_value; } } return layout; }); pending_updates = []; update_scheduled = false; update_scheduled_store.set(false); } function update_value(updates: UpdateTransaction[] | undefined): void { if (!updates) return; pending_updates.push(updates); if (!update_scheduled) { update_scheduled = true; update_scheduled_store.set(true); raf(flush); } } function get_data(id: number): any | Promise { let comp = _component_map.get(id); if (!comp) { const layout = get(layout_store); comp = findComponentById(layout, id); } if (!comp) { return null; } if (comp.instance.get_value) { return comp.instance.get_value() as Promise; } return comp.props.value; } function findComponentById( node: ComponentMeta, id: number ): ComponentMeta | undefined { if (node.id === id) { return node; } if (node.children) { for (const child of node.children) { const result = findComponentById(child, id); if (result) { return result; } } } return undefined; } function modify_stream( id: number, state: "open" | "closed" | "waiting" ): void { const comp = _component_map.get(id); if (comp && comp.instance.modify_stream_state) { comp.instance.modify_stream_state(state); } } function get_stream_state( id: number ): "open" | "closed" | "waiting" | "not_set" { const comp = _component_map.get(id); if (comp && comp.instance.get_stream_state) return comp.instance.get_stream_state(); return "not_set"; } function set_time_limit(id: number, time_limit: number | undefined): void { const comp = _component_map.get(id); if (comp && comp.instance.set_time_limit) { comp.instance.set_time_limit(time_limit); } } return { layout: layout_store, targets: target_map, update_value, get_data, modify_stream, get_stream_state, set_time_limit, loading_status, scheduled_updates: update_scheduled_store, create_layout: create_layout, rerender_layout }; } /** An async version of 'new Function' */ export const AsyncFunction: new ( ...args: string[] ) => (...args: any[]) => Promise = Object.getPrototypeOf( async function () {} ).constructor; /** * Takes a string of source code and returns a function that can be called with arguments * @param source the source code * @param backend_fn if there is also a backend function * @param input_length the number of inputs * @param output_length the number of outputs * @returns The function, or null if the source code is invalid or missing */ export function process_frontend_fn( source: string | null | undefined | false, backend_fn: boolean, input_length: number, output_length: number ): ((...args: unknown[]) => Promise) | null { if (!source) return null; const wrap = backend_fn ? input_length === 1 : output_length === 1; try { return new AsyncFunction( "__fn_args", ` let result = await (${source})(...__fn_args); if (typeof result === "undefined") return []; return (${wrap} && !Array.isArray(result)) ? [result] : result;` ); } catch (e) { console.error("Could not parse custom js method."); console.error(e); return null; } } /** * `Dependency.targets` is an array of `[id, trigger]` pairs with the ids as the `fn_id`. * This function take a single list of `Dependency.targets` and add those to the target_map. * @param targets the targets array * @param fn_id the function index * @param target_map the target map * @returns the tagert map */ export function create_target_meta( targets: Dependency["targets"], fn_id: number, target_map: TargetMap ): TargetMap { targets.forEach(([id, trigger]) => { if (!target_map[id]) { target_map[id] = {}; } if ( target_map[id]?.[trigger] && !target_map[id]?.[trigger].includes(fn_id) ) { target_map[id][trigger].push(fn_id); } else { target_map[id][trigger] = [fn_id]; } }); return target_map; } /** * Get all component ids that are an input or output of a dependency * @param dep the dependency * @param inputs the set of inputs * @param outputs the set of outputs * @returns a tuple of the inputs and outputs */ export function get_inputs_outputs( dep: Dependency, inputs: Set, outputs: Set ): [Set, Set] { dep.inputs.forEach((input) => inputs.add(input)); dep.outputs.forEach((output) => outputs.add(output)); return [inputs, outputs]; } /** * Check if a value is not a default value * @param value the value to check * @returns default value boolean */ function has_no_default_value(value: any): boolean { return ( (Array.isArray(value) && value.length === 0) || value === "" || value === 0 || !value ); } /** * Determines if a component is interactive * @param id component id * @param interactive_prop value of the interactive prop * @param value the main value of the component * @param inputs set of ids that are inputs to a dependency * @param outputs set of ids that are outputs to a dependency * @returns if the component is interactive */ export function determine_interactivity( id: number, interactive_prop: boolean | undefined, value: any, inputs: Set, outputs: Set ): boolean { if (interactive_prop === false) { return false; } else if (interactive_prop === true) { return true; } else if ( inputs.has(id) || (!outputs.has(id) && has_no_default_value(value)) ) { return true; } return false; } type ServerFunctions = Record Promise>; /** * Process the server function names and return a dictionary of functions * @param id the component id * @param server_fns the server function names * @param app the client instance * @returns the actual server functions */ export function process_server_fn( id: number, server_fns: string[] | undefined, app: client_return ): ServerFunctions { if (!server_fns) { return {}; } return server_fns.reduce((acc, fn: string) => { acc[fn] = async (...args: any[]) => { if (args.length === 1) { args = args[0]; } const result = await app.component_server(id, fn, args); return result; }; return acc; }, {} as ServerFunctions); } /** * Get a component from the backend * @param type the type of the component * @param class_id the class id of the component * @param root the root url of the app * @param components the list of component metadata * @param example_components the list of example components * @returns the component and its name */ export function get_component( type: string, class_id: string, root: string, components: ComponentMeta[], example_components?: string[] ): { component: LoadingComponent; name: ComponentMeta["type"]; example_components?: Map; } { let example_component_map: Map = new Map(); if (type === "dataset" && example_components) { (example_components as string[]).forEach((name: string) => { if (example_component_map.has(name)) { return; } let _c; const matching_component = components.find((c) => c.type === name); if (matching_component) { _c = load_component({ api_url: root, name, id: matching_component.component_class_id, variant: "example" }); example_component_map.set(name, _c.component); } }); } const _c = load_component({ api_url: root, name: type, id: class_id, variant: "component" }); return { component: _c.component, name: _c.name, example_components: example_component_map.size > 0 ? example_component_map : undefined }; } /** * Preload all components * @param components A list of component metadata * @param root The root url of the app * @returns A map of component ids to their constructors */ export function preload_all_components( components: ComponentMeta[], root: string ): Map { let constructor_map: Map = new Map(); components.forEach((c) => { const { component, example_components } = get_component( c.type, c.component_class_id, root, components ); constructor_map.set(c.component_class_id || c.type, component); if (example_components) { for (const [name, example_component] of example_components) { constructor_map.set(name, example_component); } } }); return constructor_map; }