|
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> | void) => await fn(); |
|
|
|
|
|
|
|
|
|
|
|
export function create_components(initial_layout: ComponentMeta | undefined): { |
|
layout: Writable<ComponentMeta>; |
|
targets: Writable<TargetMap>; |
|
update_value: (updates: UpdateTransaction[]) => void; |
|
get_data: (id: number) => any | Promise<any>; |
|
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<typeof create_loading_status_store>; |
|
scheduled_updates: Writable<boolean>; |
|
create_layout: (args: { |
|
app: client_return; |
|
components: ComponentMeta[]; |
|
layout: LayoutNode; |
|
dependencies: Dependency[]; |
|
root: string; |
|
options: { |
|
fill_height: boolean; |
|
}; |
|
}) => Promise<void>; |
|
rerender_layout: (args: { |
|
render_id: number; |
|
components: ComponentMeta[]; |
|
layout: LayoutNode; |
|
root: string; |
|
dependencies: Dependency[]; |
|
}) => void; |
|
} { |
|
let _component_map: Map<number, ComponentMeta>; |
|
|
|
let target_map: Writable<TargetMap> = writable({}); |
|
let _target_map: TargetMap = {}; |
|
let inputs: Set<number>; |
|
let outputs: Set<number>; |
|
let constructor_map: Map<ComponentMeta["type"], LoadingComponent>; |
|
let instance_map: { [id: number]: ComponentMeta }; |
|
let loading_status: ReturnType<typeof create_loading_status_store> = |
|
create_loading_status_store(); |
|
const layout_store: Writable<ComponentMeta> = writable(initial_layout); |
|
let _components: ComponentMeta[] = []; |
|
let app: client_return; |
|
let keyed_component_values: Record<string | number, any> = {}; |
|
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<void> { |
|
|
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
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<ComponentMeta> { |
|
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<any> { |
|
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<any>; |
|
} |
|
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 |
|
}; |
|
} |
|
|
|
|
|
export const AsyncFunction: new ( |
|
...args: string[] |
|
) => (...args: any[]) => Promise<any> = Object.getPrototypeOf( |
|
async function () {} |
|
).constructor; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function process_frontend_fn( |
|
source: string | null | undefined | false, |
|
backend_fn: boolean, |
|
input_length: number, |
|
output_length: number |
|
): ((...args: unknown[]) => Promise<unknown[]>) | 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; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function get_inputs_outputs( |
|
dep: Dependency, |
|
inputs: Set<number>, |
|
outputs: Set<number> |
|
): [Set<number>, Set<number>] { |
|
dep.inputs.forEach((input) => inputs.add(input)); |
|
dep.outputs.forEach((output) => outputs.add(output)); |
|
return [inputs, outputs]; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function has_no_default_value(value: any): boolean { |
|
return ( |
|
(Array.isArray(value) && value.length === 0) || |
|
value === "" || |
|
value === 0 || |
|
!value |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function determine_interactivity( |
|
id: number, |
|
interactive_prop: boolean | undefined, |
|
value: any, |
|
inputs: Set<number>, |
|
outputs: Set<number> |
|
): 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<string, (...args: any[]) => Promise<any>>; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function get_component( |
|
type: string, |
|
class_id: string, |
|
root: string, |
|
components: ComponentMeta[], |
|
example_components?: string[] |
|
): { |
|
component: LoadingComponent; |
|
name: ComponentMeta["type"]; |
|
example_components?: Map<ComponentMeta["type"], LoadingComponent>; |
|
} { |
|
let example_component_map: Map<ComponentMeta["type"], LoadingComponent> = |
|
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 |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function preload_all_components( |
|
components: ComponentMeta[], |
|
root: string |
|
): Map<ComponentMeta["type"], LoadingComponent> { |
|
let constructor_map: Map<ComponentMeta["type"], LoadingComponent> = 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; |
|
} |
|
|