2
0
mirror of https://github.com/gradio-app/gradio.git synced 2025-04-18 12:50:30 +08:00

delegate gradio events via a custom event dispatcher ()

* delegate gradio events via a custom event dispatcher

* improve md perf + share code

* fix df markdown

* prevent model3d from rerending too frequently

* tweaks

* fix more event bugs with video

* add changeset

* optimise handle mount

* does this do anything

* fix

* remove old dispatches

* fix dropdown position

* oops

* fixes

* fix tests

* fix types

* format

* fix markdown code

* add changeset

* fix typecheck

* fix typecheck

* fix demos

* notebooks

* fix tests

* changer

* maybe this

* fixes

* add changeset

* fix chatbot alignment mobile

* fix chantbot

* add changeset

* changeset

* changeset

* storybook

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
pngwn 2023-08-23 21:48:10 +01:00 committed by GitHub
parent cf167cd1dd
commit fe057300f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
117 changed files with 3216 additions and 2461 deletions
.changeset
demo
event_trigger
latex
markdown_example
js
_website/src/lib/assets
annotatedimage/static
app
atoms/src
audio
button
chatbot
checkbox
checkboxgroup
code
colorpicker
dataframe
dropdown
file
gallery/static
highlightedtext
html
image
json
label
markdown
model3D
number
plot/static
radio
slider
tabitem/static
tabs/static
textbox
theme/src
timeseries/interactive
tootils/src
upload/src

@ -0,0 +1,56 @@
---
"@gradio/annotatedimage": patch
"@gradio/app": patch
"@gradio/atoms": patch
"@gradio/audio": patch
"@gradio/button": patch
"@gradio/chatbot": patch
"@gradio/checkbox": patch
"@gradio/checkboxgroup": patch
"@gradio/code": patch
"@gradio/colorpicker": patch
"@gradio/dataframe": patch
"@gradio/dropdown": patch
"@gradio/file": patch
"@gradio/gallery": patch
"@gradio/highlightedtext": patch
"@gradio/html": patch
"@gradio/image": patch
"@gradio/json": patch
"@gradio/label": patch
"@gradio/markdown": patch
"@gradio/model3d": patch
"@gradio/number": patch
"@gradio/plot": patch
"@gradio/radio": patch
"@gradio/slider": patch
"@gradio/tabitem": patch
"@gradio/tabs": patch
"@gradio/textbox": patch
"@gradio/theme": patch
"@gradio/timeseries": patch
"@gradio/tootils": patch
"@gradio/upload": patch
"@gradio/uploadbutton": patch
"@gradio/utils": patch
"@gradio/video": patch
"gradio": patch
"website": patch
---
highlight:
#### Improve startup performance and markdown support
##### Improved markdown support
We now have better support for markdown in `gr.Markdown` and `gr.Dataframe`. Including syntax highlighting and Github Flavoured Markdown. We also have more consistent markdown behaviour and styling.
##### Various performance improvements
These improvements will be particularly beneficial to large applications.
- Rather than attaching events manually, they are now delegated, leading to a significant performance improvement and addressing a performance regression introduced in a recent version of Gradio. App startup for large applications is now around twice as fast.
- Optimised the mounting of individual components, leading to a modest performance improvement during startup (~30%).
- Corrected an issue that was causing markdown to re-render infinitely.
- Ensured that the `gr.3DModel` does re-render prematurely.

File diff suppressed because one or more lines are too long

@ -69,9 +69,13 @@ with gr.Blocks() as demo:
def video_stop():
print("video_stop")
def video_end():
print("video_end")
video1.play(fn=video_play)
video1.pause(fn=video_pause)
video1.stop(fn=video_stop)
video1.end(fn=video_end)
radio1.change(fn=change_video, inputs=radio1, outputs=video1)
video1.change(fn=alert_change, inputs=[gr.State("Video"), video1])

@ -1 +1 @@
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: latex"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", " gr.Markdown(\n", " r\"\"\"\n", " # Hello World! $\\frac{\\sqrt{x + y}}{4}$ is today's lesson\n", "\n", " ## the $\\sqrt{x + y}$ is first\n", "\n", " Start with $\\frac{\\frac{x+1}{x+2}}{x+3}$ then we get $ 2+x $ and $3$.\n", " \n", " There are three formulas to know:\n", " \n", " the first is $\\gamma^2 + \\theta^2 = \\omega^2$\n", " \n", " $\\sqrt{x^2+1}\n", " $ is next\n", " \n", " Integral $\\int_{a}^{b} x^2 \\,dx$ is last\n", "\n", " Start typing below to see the output.\n", "\n", " I spent $5 at the grocery store. Then I bought a $2.50 ice cream cone.\n", " \"\"\")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: latex"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", " gr.Markdown(\n", " r\"\"\"\n", " # Hello World! $\\frac{\\sqrt{x + y}}{4}$ is today's lesson\n", "\n", " ## the $\\sqrt{x + y}$ is first\n", "\n", " Start with $\\frac{\\frac{x+1}{x+2}}{x+3}$ then we get $ 2+x $ and $3$.\n", " \n", " There are three formulas to know:\n", " \n", " the first is $\\gamma^2 + \\theta^2 = \\omega^2$\n", " \n", " $\\sqrt{x^2+1}$ is next\n", " \n", " Integral $\\int_{a}^{b} x^2 \\,dx$ is last\n", "\n", " Start typing below to see the output.\n", "\n", " I spent $5 at the grocery store. Then I bought a $2.50 ice cream cone.\n", " \"\"\")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

@ -13,8 +13,7 @@ with gr.Blocks() as demo:
the first is $\gamma^2 + \theta^2 = \omega^2$
$\sqrt{x^2+1}
$ is next
$\sqrt{x^2+1}$ is next
Integral $\int_{a}^{b} x^2 \,dx$ is last

File diff suppressed because one or more lines are too long

@ -168,6 +168,23 @@ your preferred browser.
127.0.0.1:8000
```
```python
import gradio as gr
gr.Blocks() as demo:
gr.Markdown(value=md)
demo.launch()
```
```js
function fancyAlert(arg) {
if(arg) {
$.facebox({div:'#foo'})
}
}
```
## License
MIT

@ -9,7 +9,6 @@ https://prismjs.com/download.html#themes=prism&languages=python */
code[class*="language-"],
pre[class*="language-"] {
word-wrap: normal;
background: none;
color: black;
font-size: 1em;
line-height: 1.5;

@ -1,11 +1,11 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Gradio, SelectData } from "@gradio/utils";
import { Block, BlockLabel, Empty } from "@gradio/atoms";
import { Image } from "@gradio/icons";
import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker";
import { type FileData, normalise_file } from "@gradio/upload";
import type { SelectData } from "@gradio/utils";
import { _ } from "svelte-i18n";
export let elem_id = "";
export let elem_classes: string[] = [];
@ -26,24 +26,23 @@
export let root_url: string;
let active: string | null = null;
export let loading_status: LoadingStatus;
const dispatch = createEventDispatcher<{
export let gradio: Gradio<{
change: undefined;
select: SelectData;
}>();
}>;
$: {
if (value !== old_value) {
old_value = value;
dispatch("change");
gradio.dispatch("change");
}
if (value) {
_value = [
normalise_file(value[0], root, root_url) as FileData,
value[1].map(([file, _label]) => [
normalise_file(file, root, root_url) as FileData,
_label,
]),
_label
])
];
} else {
_value = null;
@ -55,6 +54,13 @@
function handle_mouseout(): void {
active = null;
}
function handle_click(i: number): void {
gradio.dispatch("select", {
value: label,
index: i
});
}
</script>
<Block
@ -115,7 +121,7 @@
on:focus={() => handle_mouseover(label)}
on:mouseout={() => handle_mouseout()}
on:blur={() => handle_mouseout()}
on:click={() => dispatch("select", { index: i, value: label })}
on:click={() => handle_click(i)}
>
{label}
</div>

@ -67,7 +67,6 @@
"@gradio/utils": "workspace:^",
"@gradio/video": "workspace:^",
"@gradio/wasm": "workspace:^",
"@playwright/test": "^1.35.1",
"d3-dsv": "^3.0.1",
"mime-types": "^2.1.34",
"postcss": "^8.4.21",

@ -1,5 +1,5 @@
<script lang="ts">
import { tick } from "svelte";
import { onMount, tick } from "svelte";
import { _ } from "svelte-i18n";
import type { client } from "@gradio/client";
@ -10,16 +10,15 @@
import type {
ComponentMeta,
Dependency,
LayoutNode,
LayoutNode
} from "./components/types";
import { setupi18n } from "./i18n";
import Render from "./Render.svelte";
import { ApiDocs } from "./api_docs/";
import type { ThemeMode } from "./components/types";
import { Toast } from "@gradio/statustracker";
import type { ToastMessage } from "@gradio/statustracker";
import type { ShareData } from "@gradio/utils";
import { dequal } from "dequal";
import MountComponents from "./MountComponents.svelte";
import logo from "./images/logo.svg";
import api_logo from "./api_docs/img/api-logo.svg";
@ -42,12 +41,10 @@
export let theme_mode: ThemeMode;
export let app: Awaited<ReturnType<typeof client>>;
export let space_id: string | null;
export let version: string;
let loading_status = create_loading_status_store();
const walked_node_ids = new Set();
const mounted_node_ids = new Set();
$: app_state.update((s) => ({ ...s, autoscroll }));
let rootNode: ComponentMeta = {
@ -56,7 +53,7 @@
props: { mode: "static" },
has_modes: false,
instance: {} as ComponentMeta["instance"],
component: {} as ComponentMeta["component"],
component: {} as ComponentMeta["component"]
};
components.push(rootNode);
@ -128,10 +125,13 @@
);
}
let instance_map = components.reduce((acc, next) => {
acc[next.id] = next;
return acc;
}, {} as { [id: number]: ComponentMeta });
let instance_map = components.reduce(
(acc, next) => {
acc[next.id] = next;
return acc;
},
{} as { [id: number]: ComponentMeta }
);
type LoadedComponent = {
default: ComponentMeta["component"];
@ -149,7 +149,7 @@
const c = await component_map[name][mode]();
return {
name,
component: c as LoadedComponent,
component: c as LoadedComponent
};
} catch (e) {
if (mode === "interactive") {
@ -157,7 +157,7 @@
const c = await component_map[name]["static"]();
return {
name,
component: c as LoadedComponent,
component: c as LoadedComponent
};
} catch (e) {
console.error(`failed to load: ${name}`);
@ -184,7 +184,6 @@
async function walk_layout(node: LayoutNode): Promise<void> {
let instance = instance_map[node.id];
walked_node_ids.add(node.id);
const _component = (await _component_map.get(
`${instance.type}_${_type_for_id.get(node.id) || "static"}`
@ -308,7 +307,7 @@
message,
fn_index,
type,
id: ++_error_id,
id: ++_error_id
};
}
@ -359,7 +358,7 @@
let payload = {
fn_index: dep_index,
data: dep.inputs.map((id) => instance_map[id].props.value),
event_data: dep.collects_event_data ? event_data : null,
event_data: dep.collects_event_data ? event_data : null
};
if (dep.frontend_fn) {
@ -395,7 +394,7 @@
...status,
status: status.stage,
progress: status.progress_data,
fn_index,
fn_index
});
if (
!showed_duplicate_message &&
@ -408,7 +407,7 @@
showed_duplicate_message = true;
messages = [
new_message(DUPLICATE_MESSAGE, fn_index, "warning"),
...messages,
...messages
];
}
if (
@ -420,7 +419,7 @@
showed_mobile_warning = true;
messages = [
new_message(MOBILE_QUEUE_WARNING, fn_index, "warning"),
...messages,
...messages
];
}
@ -437,7 +436,7 @@
window.setTimeout(() => {
messages = [
new_message(MOBILE_RECONNECT_MESSAGE, fn_index, "error"),
...messages,
...messages
];
}, 0);
trigger_api_call(dep_index, event_data);
@ -450,7 +449,7 @@
);
messages = [
new_message(_message, fn_index, "error"),
...messages,
...messages
];
}
dependencies.map(async (dep, i) => {
@ -495,11 +494,7 @@
const is_external_url = (link: string | null): boolean =>
!!(link && new URL(link, location.href).origin !== location.origin);
let attached_error_listeners: number[] = [];
let shareable_components: number[] = [];
async function handle_mount({ detail }: { detail: number }): Promise<void> {
mounted_node_ids.add(detail);
async function handle_mount(): Promise<void> {
await tick();
var a = target.getElementsByTagName("a");
@ -513,68 +508,14 @@
a[i].setAttribute("target", "_blank");
}
// handle load triggers
dependencies.forEach((dep, i) => {
let { targets, trigger, inputs, outputs } = dep;
const target_instances: [number, ComponentMeta][] = targets.map((t) => [
t,
instance_map[t],
]);
// page events
if (
targets.length === 0 &&
!handled_dependencies[i]?.includes(-1) &&
trigger === "load" &&
// check all input + output elements are on the page
outputs.every((v) => instance_map?.[v].instance) &&
inputs.every((v) => instance_map?.[v].instance)
) {
if (dep.targets.length === 0 && dep.trigger === "load") {
trigger_api_call(i);
handled_dependencies[i] = [-1];
}
// component events
target_instances
.filter((v) => !!v && !!v[1])
.forEach(([id, { instance }]: [number, ComponentMeta]) => {
if (handled_dependencies[i]?.includes(id) || !instance) return;
instance?.$on(trigger, (event_data: any) => {
trigger_api_call(i, event_data.detail);
});
if (!handled_dependencies[i]) handled_dependencies[i] = [];
handled_dependencies[i].push(id);
});
});
// share events
components.forEach((c) => {
if (
c.props.show_share_button &&
!shareable_components.includes(c.id) // only one share listener per component
) {
shareable_components.push(c.id);
c.instance.$on("share", (event_data) => {
const { title, description } = event_data.detail as ShareData;
trigger_share(title, description);
});
}
});
components.forEach((c) => {
if (!attached_error_listeners.includes(c.id)) {
if (c.instance) {
attached_error_listeners.push(c.id);
c.instance.$on("error", (event_data: any) => {
messages = [
new_message(event_data.detail, -1, "error"),
...messages,
];
});
}
}
});
checkRenderCompletion();
render_complete = true;
}
function handle_destroy(id: number): void {
@ -604,11 +545,50 @@
}
}
function checkRenderCompletion(): void {
if (dequal(walked_node_ids, mounted_node_ids)) {
render_complete = true;
}
const target_map: Record<number, Record<string, number[]>> = {};
function isCustomEvent(event: Event): event is CustomEvent {
return "detail" in event;
}
onMount(() => {
dependencies.forEach((dep, i) => {
let { targets, trigger, inputs, outputs } = dep;
const target_instances: [number, ComponentMeta][] = targets.map((t) => [
t,
instance_map[t]
]);
target_instances.forEach(([id]) => {
if (!target_map[id]) {
target_map[id] = {};
}
if (target_map[id]?.[trigger]) {
target_map[id][trigger].push(i);
} else {
target_map[id][trigger] = [i];
}
});
});
target.addEventListener("gradio", (e: Event) => {
if (!isCustomEvent(e)) throw new Error("not a custom event");
const { id, event, data } = e.detail;
if (event === "share") {
const { title, description } = data as ShareData;
trigger_share(title, description);
} else if (event === "error") {
messages = [new_message(data, -1, "error"), ...messages];
}
const deps = target_map[id]?.[event];
deps?.forEach((dep_id) => {
trigger_api_call(dep_id, data);
});
});
});
</script>
<svelte:head>
@ -635,11 +615,8 @@
<div class="wrap" style:min-height={app_mode ? "100%" : "auto"}>
<div class="contain" style:flex-grow={app_mode ? "1" : "auto"}>
{#if ready}
<Render
component={rootNode.component}
id={rootNode.id}
props={rootNode.props}
children={rootNode.children}
<MountComponents
{rootNode}
{dynamic_ids}
{instance_map}
{root}
@ -647,6 +624,7 @@
{theme_mode}
on:mount={handle_mount}
on:destroy={({ detail }) => handle_destroy(detail)}
{version}
/>
{/if}
</div>

@ -5,7 +5,7 @@
import type {
ComponentMeta,
Dependency,
LayoutNode,
LayoutNode
} from "./components/types";
declare let BUILD_MODE: string;
@ -188,7 +188,7 @@
message: "",
load_status: "pending",
status: "sleeping",
detail: "SLEEPING",
detail: "SLEEPING"
};
let app: Awaited<ReturnType<typeof client>>;
@ -208,7 +208,7 @@
app = await client(api_url, {
status_callback: handle_status,
normalise_files: false,
normalise_files: false
});
config = app.config;
window.__gradio_space__ = config.space_id;
@ -217,7 +217,7 @@
message: "",
load_status: "complete",
status: "running",
detail: "RUNNING",
detail: "RUNNING"
};
await mount_custom_css(wrapper, config.css);
@ -269,7 +269,7 @@
CONFIG_ERROR: $_("errors.config_error"),
BUILD_ERROR: $_("errors.build_error"),
RUNTIME_ERROR: $_("errors.runtime_error"),
PAUSED: $_("errors.space_paused"),
PAUSED: $_("errors.space_paused")
} as const,
title(error: error_types): string {
return encodeURIComponent($_("errors.space_not_working"));
@ -280,7 +280,7 @@
this.readable_error[error] || "an error"
}.\n\nIt would be great if you could take a look at this because this space is being embedded on ${site}.\n\nThanks!`
);
},
}
};
onMount(async () => {
@ -292,7 +292,7 @@
new CustomEvent("render", {
bubbles: true,
cancelable: false,
composed: true,
composed: true
})
);
}
@ -359,6 +359,7 @@
bind:render_complete
show_footer={!is_embed}
{app_mode}
{version}
/>
{/if}
</Embed>

@ -1,6 +1,6 @@
<script lang="ts">
import Form from "@gradio/form";
import Textbox from "@gradio/textbox/interactive";
import { BaseTextbox as Textbox } from "@gradio/textbox/interactive";
import { BaseButton } from "@gradio/button/static";
import Column from "@gradio/column";
import { _ } from "svelte-i18n";

@ -0,0 +1,30 @@
<script lang="ts">
import { onMount, createEventDispatcher } from "svelte";
import Render from "./Render.svelte";
export let rootNode: any;
export let dynamic_ids: any;
export let instance_map: any;
export let root: any;
export let target: any;
export let theme_mode: any;
export let version: any;
const dispatch = createEventDispatcher<{ mount: never }>();
onMount(() => {
dispatch("mount");
});
</script>
<Render
component={rootNode.component}
id={rootNode.id}
props={rootNode.props}
children={rootNode.children}
{dynamic_ids}
{instance_map}
{root}
{target}
{theme_mode}
{version}
/>

@ -1,4 +1,5 @@
<script lang="ts">
import { Gradio } from "./gradio_helper";
import { onMount, createEventDispatcher, setContext } from "svelte";
import type { ComponentMeta } from "./components/types";
import type { ThemeMode } from "./components/types";
@ -15,6 +16,7 @@
export let parent: string | null = null;
export let target: HTMLElement;
export let theme_mode: ThemeMode;
export let version: string;
const dispatch = createEventDispatcher<{ mount: number; destroy: number }>();
let filtered_children: ComponentMeta[] = [];
@ -75,6 +77,7 @@
{...props}
{theme_mode}
{root}
gradio={new Gradio(id, target, theme_mode, version, root)}
>
{#if children && children.length}
{#each children as { component, id: each_id, props, children: _children, has_modes } (each_id)}

@ -1,11 +1,9 @@
<script lang="ts">
import { Block } from "@gradio/atoms";
import { createEventDispatcher } from "svelte";
import type { SvelteComponent, ComponentType } from "svelte";
import { component_map } from "./directory";
import type { SelectData } from "@gradio/utils";
import type { Gradio, SelectData } from "@gradio/utils";
import { get_fetchable_url_or_file } from "@gradio/upload";
export let components: (keyof typeof component_map)[];
export let label = "Examples";
export let headers: string[];
@ -19,11 +17,10 @@
export let samples_per_page = 10;
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
const dispatch = createEventDispatcher<{
export let gradio: Gradio<{
click: number;
select: SelectData;
}>();
}>;
let samples_dir: string = get_fetchable_url_or_file(null, root, root_url);
let page = 0;
@ -112,8 +109,8 @@
class="gallery-item"
on:click={() => {
value = i + page * samples_per_page;
dispatch("click", value);
dispatch("select", { index: value, value: sample_row });
gradio.dispatch("click", value);
gradio.dispatch("select", { index: value, value: sample_row });
}}
on:mouseenter={() => handle_mouseenter(i)}
on:mouseleave={() => handle_mouseleave()}
@ -149,7 +146,7 @@
class="tr-body"
on:click={() => {
value = i + page * samples_per_page;
dispatch("click", value);
gradio.dispatch("click", value);
}}
on:mouseenter={() => handle_mouseenter(i)}
on:mouseleave={() => handle_mouseleave()}

@ -6,8 +6,6 @@ export function mount_css(url: string, target: HTMLElement): Promise<void> {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = url;
// @ts-ignore
target.appendChild(link);
return new Promise((res, rej) => {
link.addEventListener("load", () => res());
@ -15,5 +13,6 @@ export function mount_css(url: string, target: HTMLElement): Promise<void> {
console.error(`Unable to preload CSS for ${url}`);
res();
});
target.appendChild(link);
});
}

@ -0,0 +1,33 @@
// import { _ } from "svelte-i18n";
export class Gradio<T extends Record<string, any>> {
#id: number;
theme: string;
version: string;
// i18n: typeof _;
#el: HTMLElement;
root: string;
constructor(
id: number,
el: HTMLElement,
theme: string,
version: string,
root: string
) {
this.#id = id;
this.theme = theme;
this.version = version;
this.#el = el;
// this.i18n = _;
this.root = root;
}
dispatch<E extends keyof T>(event_name: E, data?: T[E]): void {
const e = new CustomEvent("gradio", {
bubbles: true,
detail: { data, id: this.#id, event: event_name }
});
this.#el.dispatchEvent(e);
}
}

@ -240,11 +240,9 @@ describe("all components should have the appropriate label when set via the `lab
});
describe("all components should hide their label when `show_label=false`", () => {
components
.filter(([name]) => name !== "Markdown" && name !== "HTML")
.forEach(([name, component, props]) => {
test.todo(name);
});
components.forEach(([name, component, props]) => {
test.todo(name);
});
["Button", "Code", "Image", "Plot"].forEach((name) => {
test.todo(name);
@ -252,11 +250,9 @@ describe("all components should hide their label when `show_label=false`", () =>
});
describe("all components should show their label when `show_label=true`", () => {
components
.filter(([name]) => name !== "Markdown" && name !== "HTML")
.forEach(([name, component, props]) => {
test.todo(name);
});
components.forEach(([name, component, props]) => {
test.todo(name);
});
["Button", "Code", "Image", "Plot"].forEach((name) => {
test.todo(name);
@ -264,11 +260,9 @@ describe("all components should show their label when `show_label=true`", () =>
});
describe("all components should hide their container when `container=false`", () => {
components
.filter(([name]) => name !== "Markdown" && name !== "HTML")
.forEach(([name, component, props]) => {
test.todo(name);
});
components.forEach(([name, component, props]) => {
test.todo(name);
});
["Button", "Code", "Image", "Plot"].forEach((name) => {
test.todo(name);

@ -25,7 +25,7 @@
pending = true;
const formatted = await formatter(value);
dispatch("share", {
description: formatted,
description: formatted
});
} catch (e) {
console.error(e);

@ -8,7 +8,7 @@
video: "upload_text.drop_video",
audio: "upload_text.drop_audio",
file: "upload_text.drop_file",
csv: "upload_text.drop_csv",
csv: "upload_text.drop_csv"
};
</script>

@ -53,7 +53,7 @@
function get_modules(): void {
module_promises = [
import("extendable-media-recorder"),
import("extendable-media-recorder-wav-encoder"),
import("extendable-media-recorder-wav-encoder")
];
}
@ -93,7 +93,7 @@
let _audio_blob = new Blob(blobs, { type: "audio/wav" });
value = {
data: await blob_to_data_url(_audio_blob),
name: "audio.wav",
name: "audio.wav"
};
dispatch(event, value);
};
@ -202,7 +202,7 @@
}
function handle_change({
detail: { values },
detail: { values }
}: {
detail: { values: [number, number] };
}): void {
@ -212,14 +212,14 @@
data: value.data,
name,
crop_min: values[0],
crop_max: values[1],
crop_max: values[1]
});
dispatch("edit");
}
function handle_load({
detail,
detail
}: {
detail: {
data: string;

@ -1,7 +1,7 @@
<svelte:options accessors={true} />
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Gradio } from "@gradio/utils";
import { _ } from "svelte-i18n";
import { UploadText } from "@gradio/atoms";
@ -14,12 +14,6 @@
import { normalise_file } from "@gradio/upload";
const dispatch = createEventDispatcher<{
change: typeof value;
stream: typeof value;
error: string;
}>();
export let elem_id = "";
export let elem_classes: string[] = [];
export let visible = true;
@ -38,6 +32,19 @@
export let loading_status: LoadingStatus;
export let autoplay = false;
export let show_edit_button = true;
export let gradio: Gradio<{
change: typeof value;
stream: typeof value;
error: string;
edit: never;
play: never;
pause: never;
stop: never;
end: never;
start_recording: never;
stop_recording: never;
upload: never;
}>;
let old_value: null | FileData | string = null;
@ -47,7 +54,7 @@
$: {
if (JSON.stringify(value) !== JSON.stringify(old_value)) {
old_value = value;
dispatch("change");
gradio.dispatch("change");
}
}
@ -73,7 +80,7 @@
on:change={({ detail }) => (value = detail)}
on:stream={({ detail }) => {
value = detail;
dispatch("stream", value);
gradio.dispatch("stream", value);
}}
on:drag={({ detail }) => (dragging = detail)}
{name}
@ -82,18 +89,18 @@
{streaming}
{autoplay}
{show_edit_button}
on:edit
on:play
on:pause
on:stop
on:end
on:start_recording
on:stop_recording
on:upload
on:edit={() => gradio.dispatch("edit")}
on:play={() => gradio.dispatch("play")}
on:pause={() => gradio.dispatch("pause")}
on:stop={() => gradio.dispatch("stop")}
on:end={() => gradio.dispatch("end")}
on:start_recording={() => gradio.dispatch("start_recording")}
on:stop_recording={() => gradio.dispatch("stop_recording")}
on:upload={() => gradio.dispatch("upload")}
on:error={({ detail }) => {
loading_status = loading_status || {};
loading_status.status = "error";
dispatch("error", detail);
gradio.dispatch("error", detail);
}}
>
<UploadText type="audio" />

@ -35,7 +35,7 @@
$: value &&
dispatch("change", {
name: name,
data: value?.data,
data: value?.data
});
function handle_ended(): void {

@ -1,7 +1,7 @@
<svelte:options accessors={true} />
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Gradio, ShareData } from "@gradio/utils";
import { _ } from "svelte-i18n";
import type { FileData } from "@gradio/upload";
@ -13,12 +13,6 @@
import { normalise_file } from "@gradio/upload";
const dispatch = createEventDispatcher<{
change: typeof value;
stream: typeof value;
error: string;
}>();
export let elem_id = "";
export let elem_classes: string[] = [];
export let visible = true;
@ -36,6 +30,11 @@
export let autoplay = false;
export let show_download_button = true;
export let show_share_button = false;
export let gradio: Gradio<{
change: typeof value;
share: ShareData;
error: string;
}>;
let old_value: null | FileData | string = null;
@ -45,7 +44,7 @@
$: {
if (JSON.stringify(value) !== JSON.stringify(old_value)) {
old_value = value;
dispatch("change");
gradio.dispatch("change");
}
}
@ -75,7 +74,7 @@
value={_value}
name={_value?.name || "audio_file"}
{label}
on:share
on:error
on:share={(e) => gradio.dispatch("share", e.detail)}
on:error={(e) => gradio.dispatch("error", e.detail)}
/>
</Block>

@ -11,43 +11,43 @@
control: "text",
description: "The text to display on the button",
name: "label",
value: "Gradio Button",
value: "Gradio Button"
},
variant: {
options: ["primary", "secondary", "stop"],
description: "The variant of the button",
control: { type: "select" },
defaultValue: "primary",
defaultValue: "primary"
},
size: {
options: ["sm", "lg"],
description: "The size of the button",
control: { type: "select" },
defaultValue: "lg",
defaultValue: "lg"
},
visible: {
options: [true, false],
description: "Sets the visibility of the button",
control: { type: "boolean" },
defaultValue: true,
defaultValue: true
},
interactive: {
options: [true, false],
description: "If false, the button will be in a disabled state",
control: { type: "boolean" },
defaultValue: true,
defaultValue: true
},
disabled: {
options: [true, false],
control: { type: "boolean" },
defaultValue: false,
defaultValue: false
},
scale: {
options: [null, 0.5, 1, 2],
description:
"relative width compared to adjacent Components in a Row. For example, if Component A has scale=2, and Component B has scale=1, A will be twice as wide as B. Should be an integer.",
control: { type: "select" },
},
control: { type: "select" }
}
}}
/>
@ -65,12 +65,12 @@
<Story
name="Button with external image icon"
args={{
icon: "https://huggingface.co/front/assets/huggingface_logo-noborder.svg",
icon: "https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
}}
/>
<Story
name="Button with visible equal to false"
args={{
visible: false,
visible: false
}}
/>

@ -1,4 +1,6 @@
<script lang="ts">
import type { Gradio, ShareData } from "@gradio/utils";
import Button from "./Button.svelte";
import { _ } from "svelte-i18n";
@ -13,6 +15,9 @@
export let icon: string | null = null;
export let link: string | null = null;
export let min_width: number | undefined = undefined;
export let gradio: Gradio<{
click: never;
}>;
export let root = "";
export let root_url: null | string = null;
</script>
@ -31,7 +36,7 @@
{root}
{root_url}
disabled={mode === "static"}
on:click
on:click={() => gradio.dispatch("click")}
>
{$_(value)}
</Button>

@ -10,18 +10,14 @@
"dependencies": {
"@gradio/atoms": "workspace:^",
"@gradio/icons": "workspace:^",
"@gradio/markdown": "workspace:^",
"@gradio/statustracker": "workspace:^",
"@gradio/theme": "workspace:^",
"@gradio/upload": "workspace:^",
"@gradio/utils": "workspace:^",
"@types/dompurify": "^3.0.2",
"@types/katex": "^0.16.0",
"@types/prismjs": "1.26.0",
"dompurify": "^3.0.3",
"katex": "^0.16.7",
"marked": "^7.0.0",
"marked-highlight": "^2.0.1",
"prismjs": "1.29.0"
"@types/prismjs": "1.26.0"
},
"main_changeset": true,
"exports": {

@ -1,22 +1,16 @@
<script lang="ts">
import { copy, format_chat_for_sharing } from "../utils";
import "katex/dist/katex.min.css";
import { format_chat_for_sharing } from "../utils";
import { copy } from "@gradio/utils";
import { beforeUpdate, afterUpdate, createEventDispatcher } from "svelte";
import { ShareButton } from "@gradio/atoms";
import type { SelectData } from "@gradio/utils";
import type { ThemeMode } from "js/app/src/components/types";
import type { FileData } from "@gradio/upload";
import {get_fetchable_url_or_file} from "@gradio/upload";
import Markdown from "./MarkdownCode.svelte";
import { MarkdownCode as Markdown } from "@gradio/markdown/static";
import { get_fetchable_url_or_file } from "@gradio/upload";
import Copy from "./Copy.svelte";
const code_highlight_css = {
light: (): Promise<typeof import("prismjs/themes/prism.css")> =>
import("prismjs/themes/prism.css"),
dark: (): Promise<typeof import("prismjs/themes/prism.css")> =>
import("prismjs/themes/prism-dark.css")
};
export let value:
| [string | FileData | null, string | FileData | null][]
| null;
@ -31,17 +25,11 @@
export let feedback: string[] | null = null;
export let selectable = false;
export let show_share_button = false;
export let theme_mode: ThemeMode;
export let rtl = false;
export let show_copy_button = false;
export let avatar_images: [string | null, string | null] = [null, null];
export let root: string;
export let root_url: null | string;
$: if (theme_mode == "dark") {
code_highlight_css.dark();
} else {
code_highlight_css.light();
}
let div: HTMLDivElement;
let autoscroll: boolean;
@ -107,11 +95,15 @@
{#if value !== null}
{#each value as message_pair, i}
{#each message_pair as message, j}
<div class="message-row">
<div class="message-row {j == 0 ? 'user-row' : 'bot-row'}">
{#if avatar_images[j] !== null}
<img
class="avatar-image-{j == 0 ? 'user' : 'bot'}"
src={get_fetchable_url_or_file(avatar_images[j], root, root_url)}
src={get_fetchable_url_or_file(
avatar_images[j],
root,
root_url
)}
alt="avatar"
/>
{/if}
@ -247,15 +239,7 @@
}
.bot {
border-bottom-left-radius: 0;
padding-left: calc(2 * var(--spacing-xxl));
}
@media (max-width: 480px) {
.message {
width: auto;
}
.bot {
padding-left: var(--spacing-xxl);
}
padding-left: var(--spacing-xxl);
}
/* Colors */
@ -272,6 +256,22 @@
display: flex;
flex-direction: row;
}
@media (max-width: 480px) {
.user-row {
align-self: flex-end;
}
.bot-row {
align-self: flex-start;
}
.message {
width: auto;
}
.bot {
padding-left: var(--spacing-xxl);
}
}
.avatar-image-user,
.avatar-image-bot {
align-self: flex-end;
@ -365,39 +365,6 @@
display: none;
}
/* Code blocks */
.message-wrap :global(pre[class*="language-"]),
.message-wrap :global(pre) {
position: relative;
direction: ltr;
white-space: no-wrap;
overflow-x: auto;
}
.message-wrap :global(code) {
font-size: var(--text-md);
}
.message-wrap :global(div[class*="code_wrap"]) {
position: relative;
margin-top: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
box-shadow: none;
border: none;
border-radius: var(--radius-md);
background-color: var(--chatbot-code-background-color);
padding: var(--spacing-xl) 10px;
}
/* Tables */
.message-wrap :global(table),
.message-wrap :global(tr),
.message-wrap :global(td),
.message-wrap :global(th) {
margin-top: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
padding: var(--spacing-xl);
}
.message-wrap .bot :global(table),
.message-wrap .bot :global(tr),
.message-wrap .bot :global(td),

@ -1,54 +0,0 @@
<script lang="ts">
import { afterUpdate, tick, createEventDispatcher } from "svelte";
import DOMPurify from "dompurify";
import render_math_in_element from "katex/dist/contrib/auto-render.js";
import { marked } from "../utils";
const dispatch = createEventDispatcher();
export let message: string;
let old_message = "";
export let latex_delimiters: {
left: string;
right: string;
display: boolean;
}[];
let el: HTMLSpanElement;
let mounted = false;
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
if ("target" in node) {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener noreferrer");
}
});
afterUpdate(() => {
tick().then(() => {
if (message !== old_message) {
requestAnimationFrame(() => {
el.innerHTML = DOMPurify.sanitize(marked.parse(message));
mounted = true;
old_message = message;
dispatch("load");
});
}
});
});
$: mounted &&
latex_delimiters.length > 0 &&
render_math_in_element(el, {
delimiters: latex_delimiters,
throwOnError: false
});
</script>
<span bind:this={el} />
<style>
span :global(code[class*="language-"]),
span :global(pre[class*="language-"]) {
font-size: var(--text-md);
}
</style>

@ -1,8 +1,9 @@
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import ChatBot from "./ChatBot.svelte";
import { Block, BlockLabel } from "@gradio/atoms";
import type { LoadingStatus } from "@gradio/statustracker";
import type { ThemeMode } from "js/app/src/components/types";
import { Chat } from "@gradio/icons";
import type { FileData } from "@gradio/upload";
import { normalise_file } from "@gradio/upload";
@ -19,7 +20,6 @@
export let root: string;
export let root_url: null | string;
export let selectable = false;
export let theme_mode: ThemeMode;
export let show_share_button = false;
export let rtl = false;
export let show_copy_button = false;
@ -28,6 +28,12 @@
right: string;
display: boolean;
}[];
export let gradio: Gradio<{
change: typeof value;
select: SelectData;
share: ShareData;
error: string;
}>;
export let avatar_images: [string | null, string | null] = [null, null];
let _value: [string | FileData | null, string | FileData | null][];
@ -80,19 +86,18 @@
<ChatBot
{selectable}
{show_share_button}
{theme_mode}
value={_value}
{latex_delimiters}
pending_message={loading_status?.status === "pending"}
{rtl}
{show_copy_button}
on:change={() => gradio.dispatch("change", value)}
on:select={(e) => gradio.dispatch("select", e.detail)}
on:share={(e) => gradio.dispatch("share", e.detail)}
on:error={(e) => gradio.dispatch("error", e.detail)}
{avatar_images}
{root_url}
{root}
on:change
on:select
on:share
on:error
/>
</div>
</Block>

@ -1,194 +1,5 @@
import { marked, type Renderer } from "marked";
import { markedHighlight } from "marked-highlight";
import Prism from "prismjs";
import "prismjs/components/prism-python";
import "prismjs/components/prism-latex";
import type { FileData } from "@gradio/upload";
import { uploadToHuggingFace } from "@gradio/utils";
import type { ActionReturn } from "svelte/action";
const COPY_ICON_CODE = `<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 32 32"
><path
fill="currentColor"
d="M28 10v18H10V10h18m0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2Z"
/><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z" /></svg>`;
const CHECK_ICON_CODE = `<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"><polyline points="20 6 9 17 4 12" /></svg>`;
const COPY_BUTTON_CODE = `<button title="copy" class="copy_code_button">
<span class="copy-text">${COPY_ICON_CODE}</span>
<span class="check">${CHECK_ICON_CODE}</span>
</button>`;
const escape_test = /[&<>"']/;
const escape_replace = new RegExp(escape_test.source, "g");
const escape_test_no_encode =
/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
const escape_replace_no_encode = new RegExp(escape_test_no_encode.source, "g");
const escape_replacements: Record<string, any> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;"
};
const get_escape_replacement = (ch: string): string =>
escape_replacements[ch] || "";
function escape(html: string, encode?: boolean): string {
if (encode) {
if (escape_test.test(html)) {
return html.replace(escape_replace, get_escape_replacement);
}
} else {
if (escape_test_no_encode.test(html)) {
return html.replace(escape_replace_no_encode, get_escape_replacement);
}
}
return html;
}
const renderer: Partial<Omit<Renderer, "constructor" | "options">> = {
code(
this: Renderer,
code: string,
infostring: string | undefined,
escaped: boolean
) {
const lang = (infostring ?? "").match(/\S*/)?.[0] ?? "";
if (this.options.highlight) {
const out = this.options.highlight(code, lang);
if (out != null && out !== code) {
escaped = true;
code = out;
}
}
code = code.replace(/\n$/, "") + "\n";
if (!lang) {
return (
'<div class="code_wrap">' +
COPY_BUTTON_CODE +
"<pre><code>" +
(escaped ? code : escape(code, true)) +
"</code></pre></div>\n"
);
}
return (
'<div class="code_wrap">' +
COPY_BUTTON_CODE +
'<pre><code class="' +
this.options.langPrefix +
escape(lang) +
'">' +
(escaped ? code : escape(code, true)) +
"</code></pre></div>\n"
);
}
};
marked.use(
{
gfm: true,
breaks: true,
pedantic: false,
headerIds: false,
mangle: false
},
markedHighlight({
highlight: (code: string, lang: string) => {
if (Prism.languages[lang]) {
return Prism.highlight(code, Prism.languages[lang], lang);
}
return code;
}
}),
{ renderer }
);
export function copy(node: HTMLDivElement): ActionReturn {
node.addEventListener("click", handle_copy);
async function handle_copy(event: MouseEvent): Promise<void> {
const path = event.composedPath() as HTMLButtonElement[];
const [copy_button] = path.filter(
(e) => e?.tagName === "BUTTON" && e.classList.contains("copy_code_button")
);
if (copy_button) {
event.stopImmediatePropagation();
const copy_text = copy_button.parentElement!.innerText.trim();
const copy_sucess_button = Array.from(
copy_button.children
)[1] as HTMLDivElement;
const copied = await copy_to_clipboard(copy_text);
if (copied) copy_feedback(copy_sucess_button);
function copy_feedback(_copy_sucess_button: HTMLDivElement): void {
_copy_sucess_button.style.opacity = "1";
setTimeout(() => {
_copy_sucess_button.style.opacity = "0";
}, 2000);
}
}
}
return {
destroy(): void {
node.removeEventListener("click", handle_copy);
}
};
}
async function copy_to_clipboard(value: string): Promise<boolean> {
let copied = false;
if ("clipboard" in navigator) {
await navigator.clipboard.writeText(value);
copied = true;
} else {
const textArea = document.createElement("textarea");
textArea.value = value;
textArea.style.position = "absolute";
textArea.style.left = "-999999px";
document.body.prepend(textArea);
textArea.select();
try {
document.execCommand("copy");
copied = true;
} catch (error) {
console.error(error);
copied = false;
} finally {
textArea.remove();
}
}
return copied;
}
export { marked };
export const format_chat_for_sharing = async (
chat: [string | FileData | null, string | FileData | null][]

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import Checkbox from "../shared";
import { Block, Info } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -16,6 +17,11 @@
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let gradio: Gradio<{
change: never;
select: never;
input: never;
}>;
</script>
<Block {visible} {elem_id} {elem_classes} {container} {scale} {min_width}>
@ -28,8 +34,8 @@
{label}
bind:value
bind:value_is_output
on:change
on:input
on:select
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
on:select={() => gradio.dispatch("select")}
/>
</Block>

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import Checkbox from "../shared";
import { Block, Info } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -15,6 +16,11 @@
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let gradio: Gradio<{
change: never;
select: never;
input: never;
}>;
</script>
<Block {visible} {elem_id} {elem_classes} {container} {scale} {min_width}>
@ -27,9 +33,9 @@
{label}
bind:value
bind:value_is_output
on:change
on:input
on:select
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
on:select={() => gradio.dispatch("select")}
disabled
/>
</Block>

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import CheckboxGroup from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -17,6 +18,11 @@
export let label = $_("checkbox.checkbox_group");
export let info: string | undefined = undefined;
export let show_label: boolean;
export let gradio: Gradio<{
change: never;
select: SelectData;
input: never;
}>;
export let loading_status: LoadingStatus;
</script>
@ -39,8 +45,8 @@
{label}
{info}
{show_label}
on:select
on:change
on:input
on:select={(e) => gradio.dispatch("select", e.detail)}
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
/>
</Block>

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import CheckboxGroup from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -16,6 +17,11 @@
export let label = $_("checkbox.checkbox_group");
export let info: string | undefined = undefined;
export let show_label: boolean;
export let gradio: Gradio<{
change: never;
select: SelectData;
input: never;
}>;
export let loading_status: LoadingStatus;
</script>
@ -38,9 +44,9 @@
{label}
{info}
{show_label}
on:select
on:change
on:input
on:select={(e) => gradio.dispatch("select", e.detail)}
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
disabled
/>
</Block>

@ -1,5 +1,6 @@
<script lang="ts">
import { createEventDispatcher, afterUpdate } from "svelte";
import type { Gradio } from "@gradio/utils";
import { afterUpdate } from "svelte";
import type { LoadingStatus } from "@gradio/statustracker";
import { _ } from "svelte-i18n";
@ -8,11 +9,6 @@
import { Block, BlockLabel } from "@gradio/atoms";
import { Code as CodeIcon } from "@gradio/icons";
const dispatch = createEventDispatcher<{
change: typeof value;
input: undefined;
}>();
export let value = "";
export let value_is_output = false;
export let language = "";
@ -24,13 +20,17 @@
export let label = $_("code.code");
export let show_label = true;
export let loading_status: LoadingStatus;
export let gradio: Gradio<{
change: typeof value;
input: never;
}>;
let dark_mode = target.classList.contains("dark");
function handle_change(): void {
dispatch("change", value);
gradio.dispatch("change", value);
if (!value_is_output) {
dispatch("input");
gradio.dispatch("input");
}
}
afterUpdate(() => {

@ -26,6 +26,7 @@
"@gradio/icons": "workspace:^",
"@gradio/statustracker": "workspace:^",
"@gradio/upload": "workspace:^",
"@gradio/utils": "workspace:^",
"@lezer/common": "^1.0.2",
"@lezer/highlight": "^1.1.3",
"@lezer/markdown": "^1.0.2",

@ -1,5 +1,6 @@
<script lang="ts">
import { createEventDispatcher, afterUpdate } from "svelte";
import type { Gradio } from "@gradio/utils";
import { afterUpdate } from "svelte";
import { _ } from "svelte-i18n";
import type { LoadingStatus } from "@gradio/statustracker";
@ -9,11 +10,6 @@
import { Block, BlockLabel, Empty } from "@gradio/atoms";
import { Code as CodeIcon } from "@gradio/icons";
const dispatch = createEventDispatcher<{
change: typeof value;
input: undefined;
}>();
export let value = "";
export let value_is_output = false;
export let language = "";
@ -25,13 +21,17 @@
export let label = $_("code.code");
export let show_label = true;
export let loading_status: LoadingStatus;
export let gradio: Gradio<{
change: typeof value;
input: never;
}>;
let dark_mode = target.classList.contains("dark");
function handle_change(): void {
dispatch("change", value);
gradio.dispatch("change", value);
if (!value_is_output) {
dispatch("input");
gradio.dispatch("input");
}
}
afterUpdate(() => {

@ -1,6 +1,7 @@
<svelte:options accessors={true} />
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import Colorpicker from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -20,6 +21,13 @@
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let interactive = true;
export let gradio: Gradio<{
change: never;
input: never;
submit: never;
blur: never;
focus: never;
}>;
</script>
<Block {visible} {elem_id} {elem_classes} {container} {scale} {min_width}>
@ -32,10 +40,10 @@
{info}
{show_label}
disabled={!interactive}
on:change
on:input
on:submit
on:blur
on:focus
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
on:submit={() => gradio.dispatch("submit")}
on:blur={() => gradio.dispatch("blur")}
on:focus={() => gradio.dispatch("focus")}
/>
</Block>

@ -16,6 +16,7 @@
},
"dependencies": {
"@gradio/atoms": "workspace:^",
"@gradio/statustracker": "workspace:^"
"@gradio/statustracker": "workspace:^",
"@gradio/utils": "workspace:^"
}
}

@ -1,10 +1,12 @@
<svelte:options accessors={true} />
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import Colorpicker from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker";
import exp from "constants";
export let label = "ColorPicker";
export let info: string | undefined = undefined;
@ -19,6 +21,13 @@
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let interactive = true;
export let gradio: Gradio<{
change: never;
input: never;
submit: never;
blur: never;
focus: never;
}>;
</script>
<Block {visible} {elem_id} {elem_classes} {container} {scale} {min_width}>
@ -31,10 +40,10 @@
{info}
{show_label}
disabled={!interactive}
on:change
on:input
on:submit
on:blur
on:focus
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
on:submit={() => gradio.dispatch("submit")}
on:blur={() => gradio.dispatch("blur")}
on:focus={() => gradio.dispatch("focus")}
/>
</Block>

@ -1,9 +1,10 @@
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import { Block } from "@gradio/atoms";
import Table from "../shared";
import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker";
import { createEventDispatcher, afterUpdate } from "svelte";
import { afterUpdate } from "svelte";
type Headers = string[];
type Data = (string | number)[][];
@ -33,15 +34,18 @@
export let datatype: Datatype | Datatype[];
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
const dispatch = createEventDispatcher();
export let gradio: Gradio<{
change: never;
select: SelectData;
input: never;
}>;
export let loading_status: LoadingStatus;
function handle_change(): void {
dispatch("change", value);
gradio.dispatch("change");
if (!value_is_output) {
dispatch("input");
gradio.dispatch("input");
}
}
afterUpdate(() => {
@ -75,7 +79,7 @@
on:change={({ detail }) => {
value = detail;
}}
on:select
on:select={(e) => gradio.dispatch("select", e.detail)}
editable
{wrap}
{datatype}

@ -11,6 +11,7 @@
"dependencies": {
"@gradio/atoms": "workspace:^",
"@gradio/button": "workspace:^",
"@gradio/markdown": "workspace:^",
"@gradio/statustracker": "workspace:^",
"@gradio/upload": "workspace:^",
"@gradio/utils": "workspace:^",

@ -1,13 +1,8 @@
<script lang="ts">
import { afterUpdate, tick } from "svelte";
import { marked } from "marked";
import DOMPurify from "dompurify";
import render_math_in_element from "katex/dist/contrib/auto-render.js";
import "katex/dist/katex.min.css";
import { MarkdownCode } from "@gradio/markdown";
export let edit: boolean;
export let value: string | number = "";
export let el: HTMLInputElement | null;
export let header = false;
export let datatype:
| "str"
@ -22,34 +17,15 @@
display: boolean;
}[];
let span: HTMLSpanElement;
let mounted = false;
$: mounted &&
latex_delimiters.length > 0 &&
render_math_in_element(span, {
delimiters: latex_delimiters,
throwOnError: false
});
afterUpdate(() => {
if (datatype == "markdown") {
tick().then(() => {
requestAnimationFrame(() => {
span.innerHTML = DOMPurify.sanitize(marked.parse(value.toString()));
mounted = true;
});
});
}
});
export let el: HTMLInputElement | null;
</script>
{#if edit}
<input
bind:this={el}
class:header
tabindex="-1"
{value}
bind:this={el}
on:keydown
on:blur={({ currentTarget }) => {
value = currentTarget.value;
@ -57,9 +33,16 @@
}}
/>
{/if}
<span on:dblclick tabindex="-1" role="button" class:edit bind:this={span}>
<span on:dblclick tabindex="-1" role="button" class:edit>
{#if datatype === "html"}
{@html value}
{:else if datatype === "markdown"}
<MarkdownCode
message={value.toLocaleString()}
{latex_delimiters}
chatbot={false}
/>
{:else}
{value}
{/if}

@ -2,7 +2,7 @@
import { createEventDispatcher, tick } from "svelte";
import { dsvFormat } from "d3-dsv";
import { dequal } from "dequal/lite";
import { copy } from "@gradio/utils";
import { Upload } from "@gradio/upload";
import { BaseButton } from "@gradio/button/static";
import EditableCell from "./EditableCell.svelte";
@ -534,13 +534,18 @@
on:touchstart={handle_click_outside}
/>
<div class:label={label && label.length !== 0}>
<div class:label={label && label.length !== 0} use:copy>
{#if label && label.length !== 0}
<p>
{label}
</p>
{/if}
<div class="table-wrap" class:dragging class:no-wrap={!wrap} style="max-height: {typeof height === undefined ? 'auto' : height + 'px'};">
<div
class="table-wrap"
class:dragging
class:no-wrap={!wrap}
style="max-height: {typeof height === undefined ? 'auto' : height + 'px'};"
>
<Upload
flex={false}
center={false}

@ -1,9 +1,10 @@
<script lang="ts">
import { afterUpdate } from "svelte";
import type { Gradio, SelectData } from "@gradio/utils";
import { Block } from "@gradio/atoms";
import Table from "../shared";
import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker";
import { createEventDispatcher, afterUpdate } from "svelte";
type Headers = string[];
type Data = (string | number)[][];
@ -26,6 +27,11 @@
export let datatype: Datatype | Datatype[];
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let gradio: Gradio<{
change: never;
select: SelectData;
input: never;
}>;
export let latex_delimiters: {
left: string;
right: string;
@ -33,14 +39,12 @@
}[];
export let height: number | undefined = undefined;
const dispatch = createEventDispatcher();
export let loading_status: LoadingStatus;
function handle_change(): void {
dispatch("change", value);
gradio.dispatch("change");
if (!value_is_output) {
dispatch("input");
gradio.dispatch("input");
}
}
afterUpdate(() => {
@ -74,7 +78,7 @@
on:change={({ detail }) => {
value = detail;
}}
on:select
on:select={(e) => gradio.dispatch("select", e.detail)}
{wrap}
{datatype}
{latex_delimiters}

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import Dropdown from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -21,6 +22,13 @@
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let allow_custom_value = false;
export let gradio: Gradio<{
change: never;
input: never;
select: SelectData;
blur: never;
focus: never;
}>;
if (multiselect && !value) {
value = [];
@ -51,10 +59,10 @@
{show_label}
{allow_custom_value}
{container}
on:change
on:input
on:select
on:blur
on:focus
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
on:select={(e) => gradio.dispatch("select", e.detail)}
on:blur={() => gradio.dispatch("blur")}
on:focus={() => gradio.dispatch("focus")}
/>
</Block>

@ -67,7 +67,7 @@
dispatch("select", {
index: choices.indexOf(option),
value: option,
selected: true,
selected: true
});
}
value = value;
@ -81,7 +81,7 @@
dispatch("select", {
index: choices.indexOf(option),
value: option,
selected: false,
selected: false
});
}
@ -135,7 +135,7 @@
dispatch("select", {
index: choices.indexOf(option),
value: option,
selected: true,
selected: true
});
filterInput.blur();
}
@ -151,7 +151,7 @@
dispatch("select", {
index: choices.indexOf(value),
value: value,
selected: true,
selected: true
});
}
inputValue = activeOption;

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import Dropdown from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -20,6 +21,13 @@
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let allow_custom_value = false;
export let gradio: Gradio<{
change: never;
input: never;
select: SelectData;
blur: never;
focus: never;
}>;
if (multiselect && !value) {
value = [];
@ -50,11 +58,11 @@
{show_label}
{allow_custom_value}
{container}
on:change
on:input
on:select
on:blur
on:focus
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
on:select={(e) => gradio.dispatch("select", e.detail)}
on:blur={() => gradio.dispatch("blur")}
on:focus={() => gradio.dispatch("focus")}
disabled
/>
</Block>

@ -1,7 +1,8 @@
<svelte:options accessors={true} />
<script lang="ts">
import { createEventDispatcher, getContext } from "svelte";
import type { Gradio, SelectData } from "@gradio/utils";
import { getContext } from "svelte";
import FileUpload from "./FileUpload.svelte";
import { blobToBase64 } from "@gradio/upload";
import type { FileData } from "@gradio/upload";
@ -14,6 +15,7 @@
import type { LoadingStatus } from "@gradio/statustracker";
import { _ } from "svelte-i18n";
import type { S } from "@storybook/theming/dist/create-c2b2ce6d";
export let elem_id = "";
export let elem_classes: string[] = [];
@ -34,6 +36,13 @@
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let height: number | undefined = undefined;
export let gradio: Gradio<{
change: never;
error: string;
upload: never;
clear: never;
select: SelectData;
}>;
const upload_files =
getContext<typeof default_upload_files>("upload_files") ??
@ -44,17 +53,11 @@
let dragging = false;
let pending_upload = false;
const dispatch = createEventDispatcher<{
change: undefined;
error: string;
upload: undefined;
}>();
$: {
if (JSON.stringify(_value) !== JSON.stringify(old_value)) {
old_value = _value;
if (_value === null) {
dispatch("change");
gradio.dispatch("change");
pending_upload = false;
} else if (
!(Array.isArray(_value) ? _value : [_value]).every(
@ -62,7 +65,7 @@
)
) {
pending_upload = false;
dispatch("change");
gradio.dispatch("change");
} else if (mode === "interactive") {
let files = (Array.isArray(_value) ? _value : [_value]).map(
(file_data) => file_data.blob!
@ -96,8 +99,8 @@
);
old_value = _value = normalise_file(value, root, root_url);
}
dispatch("change");
dispatch("upload");
gradio.dispatch("change");
gradio.dispatch("upload");
});
}
}
@ -134,8 +137,8 @@
{height}
on:change={({ detail }) => (value = detail)}
on:drag={({ detail }) => (dragging = detail)}
on:clear
on:select
on:clear={() => gradio.dispatch("clear")}
on:select={({ detail }) => gradio.dispatch("select", detail)}
>
<UploadText type="file" />
</FileUpload>

@ -26,7 +26,7 @@
on:click={() =>
dispatch("select", {
value: file.orig_name || file.name,
index: i,
index: i
})}
>
<td>
@ -45,7 +45,7 @@
{@html display_file_size(file)}&nbsp;&#8675;
</a>
{:else}
{$_('file.uploading')}
{$_("file.uploading")}
{/if}
</td>
</tr>

@ -1,7 +1,8 @@
<svelte:options accessors={true} />
<script lang="ts">
import { createEventDispatcher, getContext } from "svelte";
import type { Gradio, SelectData } from "@gradio/utils";
import { getContext } from "svelte";
import File from "./File.svelte";
import { blobToBase64 } from "@gradio/upload";
import type { FileData } from "@gradio/upload";
@ -33,6 +34,13 @@
export let container = true;
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let gradio: Gradio<{
change: never;
error: string;
upload: never;
clear: never;
select: SelectData;
}>;
const upload_files =
getContext<typeof default_upload_files>("upload_files") ??
@ -43,17 +51,11 @@
let dragging = false;
let pending_upload = false;
const dispatch = createEventDispatcher<{
change: undefined;
error: string;
upload: undefined;
}>();
$: {
if (JSON.stringify(_value) !== JSON.stringify(old_value)) {
old_value = _value;
if (_value === null) {
dispatch("change");
gradio.dispatch("change");
pending_upload = false;
} else if (
!(Array.isArray(_value) ? _value : [_value]).every(
@ -61,7 +63,7 @@
)
) {
pending_upload = false;
dispatch("change");
gradio.dispatch("change");
} else if (mode === "interactive") {
let files = (Array.isArray(_value) ? _value : [_value]).map(
(file_data) => file_data.blob!
@ -95,8 +97,8 @@
);
old_value = _value = normalise_file(value, root, root_url);
}
dispatch("change");
dispatch("upload");
gradio.dispatch("change");
gradio.dispatch("upload");
});
}
}
@ -121,5 +123,12 @@
: loading_status?.status || "complete"}
/>
<File on:select {selectable} value={_value} {label} {show_label} {height} />
<File
on:select={({ detail }) => gradio.dispatch("select", detail)}
{selectable}
value={_value}
{label}
{show_label}
{height}
/>
</Block>

@ -127,7 +127,7 @@
if (selected_image !== null) {
dispatch("select", {
index: selected_image,
value: _value?.[selected_image][1],
value: _value?.[selected_image][1]
});
}
}
@ -160,7 +160,7 @@
container_element?.scrollTo({
left: pos < 0 ? 0 : pos,
behavior: "smooth",
behavior: "smooth"
});
}
@ -338,7 +338,9 @@
.thumbnail-item {
--ring-color: transparent;
position: relative;
box-shadow: 0 0 0 2px var(--ring-color), var(--shadow-drop);
box-shadow:
0 0 0 2px var(--ring-color),
var(--shadow-drop);
border: 1px solid var(--border-color-primary);
border-radius: var(--button-small-radius);
background: var(--background-fill-secondary);

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio, ShareData, SelectData } from "@gradio/utils";
import { Block } from "@gradio/atoms";
import Gallery from "./Gallery.svelte";
import type { LoadingStatus } from "@gradio/statustracker";
@ -27,6 +28,11 @@
"cover";
export let show_share_button = false;
export let show_download_button = false;
export let gradio: Gradio<{
select: SelectData;
share: ShareData;
error: string;
}>;
</script>
<Block
@ -43,9 +49,9 @@
>
<StatusTracker {...loading_status} />
<Gallery
on:select
on:share
on:error
on:select={(e) => gradio.dispatch("select", e.detail)}
on:share={(e) => gradio.dispatch("share", e.detail)}
on:error={(e) => gradio.dispatch("error", e.detail)}
{label}
{value}
{show_label}

@ -1,6 +1,7 @@
<script>
import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
import HighlightedText from "./static";
import { Gradio } from "../app/src/gradio_helper";
</script>
<Meta title="Components/HighlightedText" component={HighlightedText} />
@ -12,6 +13,13 @@
["dogs", "-"],
["elephants", "+"]
]}
gradio={new Gradio(
0,
document.body,
"light",
"1.1.1",
"http://localhost:7860"
)}
{...args}
/>
</Template>

@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Gradio, SelectData } from "@gradio/utils";
import HighlightedText from "./Highlightedtext.svelte";
import { Block, BlockLabel, Empty } from "@gradio/atoms";
import { TextHighlight } from "@gradio/icons";
@ -19,6 +19,10 @@
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let selectable = false;
export let gradio: Gradio<{
select: SelectData;
change: never;
}>;
$: if (!color_map && Object.keys(color_map).length) {
color_map = color_map;
@ -26,12 +30,10 @@
export let loading_status: LoadingStatus;
const dispatch = createEventDispatcher<{ change: undefined }>();
$: {
if (value !== old_value) {
old_value = value;
dispatch("change");
gradio.dispatch("change");
}
}
</script>
@ -57,7 +59,13 @@
{/if}
{#if value}
<HighlightedText on:select {selectable} {value} {show_legend} {color_map} />
<HighlightedText
on:select={({ detail }) => gradio.dispatch("select", detail)}
{selectable}
{value}
{show_legend}
{color_map}
/>
{:else}
<Empty>
<TextHighlight />

@ -10,7 +10,8 @@
"main_changeset": true,
"dependencies": {
"@gradio/atoms": "workspace:^",
"@gradio/statustracker": "workspace:^"
"@gradio/statustracker": "workspace:^",
"@gradio/utils": "workspace:^"
},
"exports": {
".": "./static/index.ts",

@ -1,6 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
export let elem_id = "";
export let elem_classes: string[] = [];
export let value: string;

@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Gradio } from "@gradio/utils";
import HTML from "./HTML.svelte";
import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker";
@ -11,10 +11,11 @@
export let visible = true;
export let value = "";
export let loading_status: LoadingStatus;
export let gradio: Gradio<{
change: never;
}>;
const dispatch = createEventDispatcher<{ change: undefined }>();
$: label, dispatch("change");
$: label, gradio.dispatch("change");
</script>
<Block {visible} {elem_id} {elem_classes} container={false}>
@ -26,7 +27,7 @@
{elem_id}
{elem_classes}
{visible}
on:change
on:change={() => gradio.dispatch("change")}
/>
</div>
</Block>

@ -34,21 +34,25 @@ describe("Image", () => {
afterEach(() => cleanup());
test("image change event trigger fires when value is changed and only fires once", async () => {
const { component } = await render(Image, {
const { component, listen } = await render(Image, {
show_label: true,
loading_status,
mode: "dynamic",
value:
"https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
root: "foo",
root_url: null,
streaming: false,
pending: false,
source: "upload"
source: "upload",
label: "Test Label",
width: 224,
height: 224,
mirror_webcam: false,
shape: [224, 224],
brush_color: "#000000",
brush_radius: 5,
mask_opacity: 0.5
});
const mock = spy();
component.$on("change", mock);
const mock = listen("change");
component.value =
"https://github.com/gradio-app/gradio/blob/main/test/test_files/cheetah1.jpg";

@ -1,7 +1,7 @@
<svelte:options accessors={true} />
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Gradio, SelectData, ShareData } from "@gradio/utils";
import Image from "./Image.svelte";
import { Block } from "@gradio/atoms";
@ -32,13 +32,19 @@
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
const dispatch = createEventDispatcher<{
change: undefined;
export let gradio: Gradio<{
change: never;
error: string;
}>();
edit: never;
stream: never;
drag: never;
upload: never;
clear: never;
select: SelectData;
share: ShareData;
}>;
$: value, dispatch("change");
$: value, gradio.dispatch("change");
let dragging: boolean;
const FIXED_HEIGHT = 240;
@ -70,17 +76,17 @@
{tool}
{selectable}
{mask_opacity}
on:edit
on:clear
on:stream
on:edit={() => gradio.dispatch("edit")}
on:clear={() => gradio.dispatch("clear")}
on:stream={() => gradio.dispatch("stream")}
on:drag={({ detail }) => (dragging = detail)}
on:upload
on:select
on:share
on:upload={() => gradio.dispatch("upload")}
on:select={({ detail }) => gradio.dispatch("select", detail)}
on:share={({ detail }) => gradio.dispatch("share", detail)}
on:error={({ detail }) => {
loading_status = loading_status || {};
loading_status.status = "error";
dispatch("error", detail);
gradio.dispatch("error", detail);
}}
{label}
{show_label}

@ -32,7 +32,7 @@
try {
stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: include_audio,
audio: include_audio
});
video_source.srcObject = stream;
video_source.muted = true;
@ -81,7 +81,7 @@
dispatch("capture", {
data: e.target.result,
name: "sample." + mimeType.substring(6),
is_example: false,
is_example: false
});
dispatch("stop_recording");
}
@ -102,7 +102,7 @@
return;
}
media_recorder = new MediaRecorder(stream, {
mimeType: mimeType,
mimeType: mimeType
});
media_recorder.addEventListener("dataavailable", function (e) {
recorded_blobs.push(e.data);

@ -1,7 +1,7 @@
<svelte:options accessors={true} />
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Gradio, SelectData } from "@gradio/utils";
import StaticImage from "./ImagePreview.svelte";
import { Block } from "@gradio/atoms";
@ -26,13 +26,14 @@
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let show_share_button = false;
const dispatch = createEventDispatcher<{
change: undefined;
export let gradio: Gradio<{
change: never;
error: string;
}>();
select: SelectData;
share: ShareData;
}>;
$: value, dispatch("change");
$: value, gradio.dispatch("change");
let dragging: boolean;
$: value = !value ? null : value;
@ -54,9 +55,9 @@
>
<StatusTracker {...loading_status} />
<StaticImage
on:select
on:share
on:error
on:select={({ detail }) => gradio.dispatch("select", detail)}
on:share={({ detail }) => gradio.dispatch("share", detail)}
on:error={({ detail }) => gradio.dispatch("error", detail)}
{value}
{label}
{show_label}

@ -10,7 +10,8 @@
"dependencies": {
"@gradio/atoms": "workspace:^",
"@gradio/icons": "workspace:^",
"@gradio/statustracker": "workspace:^"
"@gradio/statustracker": "workspace:^",
"@gradio/utils": "workspace:^"
},
"main_changeset": true,
"exports": {

@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Gradio } from "@gradio/utils";
import JSON from "./JSON.svelte";
import { Block, BlockLabel } from "@gradio/atoms";
import { JSON as JSONIcon } from "@gradio/icons";
@ -19,13 +19,14 @@
export let container = true;
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
const dispatch = createEventDispatcher<{ change: undefined }>();
export let gradio: Gradio<{
change: never;
}>;
$: {
if (value !== old_value) {
old_value = value;
dispatch("change");
gradio.dispatch("change");
}
}
</script>

@ -30,7 +30,10 @@ test("gr.Label default value and label rendered with confidences", async ({
},
label: "My Label",
show_label: true,
loading_status: loading_status
loading_status: loading_status,
gradio: {
dispatch() {}
}
}
});
await expect(component).toContainText("My Label");
@ -52,7 +55,10 @@ test("gr.Label hides label when show_label=false", async ({ mount, page }) => {
},
label: "My Label",
show_label: false,
loading_status: loading_status
loading_status: loading_status,
gradio: {
dispatch() {}
}
}
});
await expect(component.getByTestId("block-label")).toBeHidden();
@ -68,7 +74,10 @@ test("gr.Label confidence bars not rendered without confidences", async ({
},
label: "My Label",
show_label: true,
loading_status: loading_status
loading_status: loading_status,
gradio: {
dispatch() {}
}
}
});
await expect(component).toContainText("My Label");
@ -79,7 +88,14 @@ test("gr.Label confidence bars trigger select event when clicked", async ({
mount,
page
}) => {
const select = spy();
const events = {
select: [0, null]
};
function event(name: "select", value: any) {
events[name] = [events[name][0]! + 1, value];
}
const component = await mount(Label, {
props: {
value: {
@ -91,20 +107,27 @@ test("gr.Label confidence bars trigger select event when clicked", async ({
},
label: "My Label",
show_label: true,
loading_status: loading_status
},
on: {
select: select
loading_status: loading_status,
gradio: {
dispatch: event
}
}
});
await expect(component).toContainText("My Label");
await component.getByTestId("Bad-confidence-set").click();
expect(select.callCount).toEqual(1);
expect(select.calls[0][0]).toEqual({ index: 1, value: "Bad" });
expect(events.select[0]).toEqual(1);
expect(events.select[1]).toEqual({ index: 1, value: "Bad" });
});
test("gr.Label triggers change event", async ({ mount, page }) => {
const change = spy();
const events = {
change: 0
};
function event(name: "change") {
events[name] += 1;
}
const component = await mount(Label, {
props: {
value: {
@ -116,12 +139,13 @@ test("gr.Label triggers change event", async ({ mount, page }) => {
},
label: "My Label",
show_label: true,
loading_status: loading_status
},
on: {
change: change
loading_status: loading_status,
gradio: {
dispatch: event
}
}
});
await component.update({
props: {
value: {
@ -133,5 +157,5 @@ test("gr.Label triggers change event", async ({ mount, page }) => {
}
}
});
expect(change.callCount).toEqual(1);
expect(events.change).toEqual(2);
});

@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Gradio, SelectData } from "@gradio/utils";
import Label from "./Label.svelte";
import { LineChart as LabelIcon } from "@gradio/icons";
import { Block, BlockLabel, Empty } from "@gradio/atoms";
@ -22,11 +22,13 @@
export let loading_status: LoadingStatus;
export let show_label = true;
export let selectable = false;
const dispatch = createEventDispatcher<{ change: undefined }>();
export let gradio: Gradio<{
change: never;
select: SelectData;
}>;
$: ({ confidences, label: _label } = value);
$: _label, confidences, dispatch("change");
$: _label, confidences, gradio.dispatch("change");
</script>
<Block
@ -44,7 +46,12 @@
<BlockLabel Icon={LabelIcon} {label} disable={container === false} />
{/if}
{#if _label !== undefined && _label !== null}
<Label on:select {selectable} {value} {color} />
<Label
on:select={({ detail }) => gradio.dispatch("select", detail)}
{selectable}
{value}
{color}
/>
{:else}
<Empty unpadded_box={true}><LabelIcon /></Empty>
{/if}

@ -18,10 +18,14 @@
"dependencies": {
"@gradio/atoms": "workspace:^",
"@gradio/statustracker": "workspace:^",
"@gradio/utils": "workspace:^",
"@types/dompurify": "^3.0.2",
"@types/katex": "^0.16.0",
"@types/prismjs": "1.26.0",
"dompurify": "^3.0.3",
"katex": "^0.16.7",
"marked": "^7.0.0"
"marked": "^7.0.0",
"marked-highlight": "^2.0.1",
"prismjs": "1.29.0"
}
}

@ -1,9 +1,8 @@
<script lang="ts">
import { afterUpdate, createEventDispatcher, tick } from "svelte";
import { marked } from "marked";
import DOMPurify from "dompurify";
import render_math_in_element from "katex/dist/contrib/auto-render.js";
import "katex/dist/katex.min.css";
import { createEventDispatcher } from "svelte";
import { copy } from "@gradio/utils";
import MarkdownCode from "./MarkdownCode.svelte";
export let elem_id = "";
export let elem_classes: string[] = [];
@ -12,8 +11,6 @@
export let min_height = false;
export let rtl = false;
let div: HTMLDivElement;
const dispatch = createEventDispatcher<{ change: undefined }>();
$: value, dispatch("change");
@ -23,24 +20,6 @@
right: string;
display: boolean;
}[];
let mounted = false;
$: mounted &&
latex_delimiters.length > 0 &&
render_math_in_element(div, {
delimiters: latex_delimiters,
throwOnError: false
});
afterUpdate(() => {
tick().then(() => {
requestAnimationFrame(() => {
div.innerHTML = DOMPurify.sanitize(marked.parse(value));
mounted = true;
});
});
});
</script>
<div
@ -50,8 +29,10 @@
class:hide={!visible}
data-testid="markdown"
dir={rtl ? "rtl" : "ltr"}
bind:this={div}
></div>
use:copy
>
<MarkdownCode message={value} {latex_delimiters} chatbot={false} />
</div>
<style>
div :global(.math.inline) {

@ -0,0 +1,105 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import DOMPurify from "dompurify";
import render_math_in_element from "katex/dist/contrib/auto-render.js";
import "katex/dist/katex.min.css";
import { marked } from "./utils";
const dispatch = createEventDispatcher();
import "./prism.css";
// import "./prism-dark.css";
// const code_highlight_css = {
// light: (): Promise<typeof import("prismjs/themes/prism.css")> =>
// import("prismjs/themes/prism.css"),
// dark: (): Promise<typeof import("prismjs/themes/prism.css")> =>
// import("prismjs/themes/prism-dark.css")
// };
export let chatbot = true;
export let message: string;
export let latex_delimiters: {
left: string;
right: string;
display: boolean;
}[];
let el: HTMLSpanElement;
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
if ("target" in node) {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener noreferrer");
}
});
$: el && html && render_html(message);
$: html =
message && message.trim() ? DOMPurify.sanitize(marked.parse(message)) : "";
async function render_html(value: string): Promise<void> {
if (latex_delimiters.length > 0) {
render_math_in_element(el, {
delimiters: latex_delimiters,
throwOnError: false
});
}
}
</script>
<span class:chatbot bind:this={el} class="md">
{@html html}
</span>
<style>
span :global(div[class*="code_wrap"]) {
position: relative;
}
/* KaTeX */
span :global(span.katex) {
font-size: var(--text-lg);
direction: ltr;
}
span :global(div[class*="code_wrap"] > button) {
position: absolute;
top: var(--spacing-sm);
right: var(--spacing-sm);
z-index: 1;
cursor: pointer;
border-bottom-left-radius: var(--radius-sm);
padding: 5px;
padding: var(--spacing-md);
width: 25px;
height: 25px;
}
span :global(code > button > span) {
position: absolute;
top: var(--spacing-sm);
right: var(--spacing-sm);
width: 12px;
height: 12px;
}
span :global(.check) {
position: absolute;
top: 0;
right: 0;
opacity: 0;
z-index: var(--layer-top);
transition: opacity 0.2s;
background: var(--background-fill-primary);
padding: var(--size-1);
width: 100%;
height: 100%;
color: var(--body-text-color);
}
span :global(pre) {
position: relative;
}
</style>

@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Gradio } from "@gradio/utils";
import Markdown from "./Markdown.svelte";
import { StatusTracker } from "@gradio/statustracker";
@ -13,15 +13,16 @@
export let value = "";
export let loading_status: LoadingStatus;
export let rtl = false;
export let gradio: Gradio<{
change: never;
}>;
export let latex_delimiters: {
left: string;
right: string;
display: boolean;
}[];
const dispatch = createEventDispatcher<{ change: undefined }>();
$: label, dispatch("change");
$: label, gradio.dispatch("change");
</script>
<Block {visible} {elem_id} {elem_classes} container={false}>
@ -34,8 +35,8 @@
{elem_classes}
{visible}
{rtl}
on:change={() => gradio.dispatch("change")}
{latex_delimiters}
on:change
/>
</div>
</Block>

@ -1 +1,2 @@
export { default } from "./StaticMarkdown.svelte";
export { default as MarkdownCode } from "./MarkdownCode.svelte";

@ -0,0 +1,68 @@
.dark .md .token.comment,
.dark .md .token.prolog,
.dark .md .token.doctype,
.dark .md .token.cdata {
color: hsl(30, 20%, 50%);
}
.dark .md .token.punctuation {
opacity: 0.7;
}
.dark .md .token.namespace {
opacity: 0.7;
}
.dark .md .token.property,
.dark .md .token.tag,
.dark .md .token.boolean,
.dark .md .token.number,
.dark .md .token.constant,
.dark .md .token.symbol {
color: hsl(350, 40%, 70%);
}
.dark .md .token.selector,
.dark .md .token.attr-name,
.dark .md .token.string,
.dark .md .token.char,
.dark .md .token.builtin,
.dark .md .token.inserted {
color: hsl(75, 70%, 60%);
}
.dark .md .token.operator,
.dark .md .token.entity,
.dark .md .token.url,
.dark .md .language-css .token.string,
.dark .md .style .token.string,
.dark .md .token.variable {
color: hsl(40, 90%, 60%);
}
.dark .md .token.atrule,
.dark .md .token.attr-value,
.dark .md .token.keyword {
color: hsl(350, 40%, 70%);
}
.dark .md .token.regex,
.dark .md .token.important {
color: #e90;
}
.dark .md .token.important,
.dark .md .token.bold {
font-weight: bold;
}
.dark .md .token.italic {
font-style: italic;
}
.dark .md .token.entity {
cursor: help;
}
.dark .md .token.deleted {
color: red;
}

@ -0,0 +1,210 @@
Tables */ table,
tr,
td,
th {
margin-top: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
padding: var(--spacing-xl);
}
/* .message-wrap :global(pre[class*="language-"]),
.message-wrap :global(pre) {
border: none;
background: none;
position: relative;
direction: ltr;
white-space: no-wrap;
overflow-x: auto;
}
.message-wrap :global(code) {
} */
/* .message-wrap :global(div[class*="code_wrap"]) {
} */
.md code,
.md pre {
background: none;
font-family: var(--font-mono);
font-size: var(--text-sm);
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
tab-size: 2;
hyphens: none;
}
.md pre[class*="language-"]::-moz-selection,
.md pre[class*="language-"] ::-moz-selection,
.md code[class*="language-"]::-moz-selection,
.md code[class*="language-"] ::-moz-selection {
}
.md pre[class*="language-"]::selection,
.md pre[class*="language-"] ::selection,
.md code[class*="language-"]::selection,
.md code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
/* Code blocks */
.md pre {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
position: relative;
margin-top: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
box-shadow: none;
border: none;
border-radius: var(--radius-md);
background-color: var(--chatbot-code-background-color);
padding: var(--spacing-lg) var(--spacing-xl);
font-family: var(--font-mono);
/* font-size: var(--text-sm) !important; */
display: block;
white-space: pre;
border-radius: var(--radius-sm);
text-shadow: none;
border-radius: var(--radius-sm);
/* font-size: 85%; */
white-space: nowrap;
display: block;
white-space: pre;
}
.prose code {
}
.prose pre > code {
}
/* Inline code */
.md :not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: var(--radius-xs);
white-space: normal;
}
.md .token.comment,
.md .token.prolog,
.md .token.doctype,
.md .token.cdata {
color: slategray;
}
.md .token.punctuation {
color: #999;
}
.md .token.namespace {
opacity: 0.7;
}
.md .token.property,
.md .token.tag,
.md .token.boolean,
.md .token.number,
.md .token.constant,
.md .token.symbol,
.md .token.deleted {
color: #905;
}
.md .token.selector,
.md .token.attr-name,
.md .token.string,
.md .token.char,
.md .token.builtin,
.md .token.inserted {
color: #690;
}
.md .token.atrule,
.md .token.attr-value,
.md .token.keyword {
color: #07a;
}
.md .token.function,
.md .token.class-name {
color: #dd4a68;
}
.md .token.regex,
.md .token.important,
.md .token.variable {
color: #e90;
}
.md .token.important,
.md .token.bold {
font-weight: bold;
}
.md .token.italic {
font-style: italic;
}
.md .token.entity {
cursor: help;
}
.dark .md .token.comment,
.dark .md .token.prolog,
.dark .md .token.cdata {
color: hsl(220, 10%, 40%);
}
.dark .md .token.doctype,
.dark .md .token.punctuation,
.dark .md .token.entity {
color: hsl(220, 14%, 71%);
}
.dark .md .token.attr-name,
.dark .md .token.class-name,
.dark .md .token.boolean,
.dark .md .token.constant,
.dark .md .token.number,
.dark .md .token.atrule {
color: hsl(29, 54%, 61%);
}
.dark .md .token.keyword {
color: hsl(286, 60%, 67%);
}
.dark .md .token.property,
.dark .md .token.tag,
.dark .md .token.symbol,
.dark .md .token.deleted,
.dark .md .token.important {
color: hsl(355, 65%, 65%);
}
.dark .md .token.selector,
.dark .md .token.string,
.dark .md .token.char,
.dark .md .token.builtin,
.dark .md .token.inserted,
.dark .md .token.regex,
.dark .md .token.attr-value,
.dark .md .token.attr-value > .token.punctuation {
color: hsl(95, 38%, 62%);
}
.dark .md .token.variable,
.dark .md .token.operator,
.dark .md .token.function {
color: hsl(207, 82%, 66%);
}
.dark .md .token.url {
color: hsl(187, 47%, 55%);
}

193
js/markdown/static/utils.ts Normal file

@ -0,0 +1,193 @@
import { marked, type Renderer } from "marked";
import { markedHighlight } from "marked-highlight";
import Prism from "prismjs";
import "prismjs/components/prism-python";
import "prismjs/components/prism-latex";
// import loadLanguages from "prismjs/components/";
// loadLanguages(["python", "latex"]);
import type { ActionReturn } from "svelte/action";
const COPY_ICON_CODE = `<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 32 32"
><path
fill="currentColor"
d="M28 10v18H10V10h18m0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2Z"
/><path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z" /></svg>`;
const CHECK_ICON_CODE = `<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"><polyline points="20 6 9 17 4 12" /></svg>`;
const COPY_BUTTON_CODE = `<button title="copy" class="copy_code_button">
<span class="copy-text">${COPY_ICON_CODE}</span>
<span class="check">${CHECK_ICON_CODE}</span>
</button>`;
const escape_test = /[&<>"']/;
const escape_replace = new RegExp(escape_test.source, "g");
const escape_test_no_encode =
/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
const escape_replace_no_encode = new RegExp(escape_test_no_encode.source, "g");
const escape_replacements: Record<string, any> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;"
};
const get_escape_replacement = (ch: string): string =>
escape_replacements[ch] || "";
function escape(html: string, encode?: boolean): string {
if (encode) {
if (escape_test.test(html)) {
return html.replace(escape_replace, get_escape_replacement);
}
} else {
if (escape_test_no_encode.test(html)) {
return html.replace(escape_replace_no_encode, get_escape_replacement);
}
}
return html;
}
const renderer: Partial<Omit<Renderer, "constructor" | "options">> = {
code(
this: Renderer,
code: string,
infostring: string | undefined,
escaped: boolean
) {
const lang = (infostring ?? "").match(/\S*/)?.[0] ?? "";
if (this.options.highlight) {
const out = this.options.highlight(code, lang);
if (out != null && out !== code) {
escaped = true;
code = out;
}
}
code = code.replace(/\n$/, "") + "\n";
if (!lang) {
return (
'<div class="code_wrap">' +
COPY_BUTTON_CODE +
"<pre><code>" +
(escaped ? code : escape(code, true)) +
"</code></pre></div>\n"
);
}
return (
'<div class="code_wrap">' +
COPY_BUTTON_CODE +
'<pre><code class="' +
this.options.langPrefix +
escape(lang) +
'">' +
(escaped ? code : escape(code, true)) +
"</code></pre></div>\n"
);
}
};
marked.use(
{
gfm: true,
breaks: true,
pedantic: false,
headerIds: false,
mangle: false
},
markedHighlight({
highlight: (code: string, lang: string) => {
if (Prism.languages[lang]) {
return Prism.highlight(code, Prism.languages[lang], lang);
}
return code;
}
}),
{ renderer }
);
export function copy(node: HTMLDivElement): ActionReturn {
node.addEventListener("click", handle_copy);
async function handle_copy(event: MouseEvent): Promise<void> {
const path = event.composedPath() as HTMLButtonElement[];
const [copy_button] = path.filter(
(e) => e?.tagName === "BUTTON" && e.classList.contains("copy_code_button")
);
if (copy_button) {
event.stopImmediatePropagation();
const copy_text = copy_button.parentElement!.innerText.trim();
const copy_sucess_button = Array.from(
copy_button.children
)[1] as HTMLDivElement;
const copied = await copy_to_clipboard(copy_text);
if (copied) copy_feedback(copy_sucess_button);
function copy_feedback(_copy_sucess_button: HTMLDivElement): void {
_copy_sucess_button.style.opacity = "1";
setTimeout(() => {
_copy_sucess_button.style.opacity = "0";
}, 2000);
}
}
}
return {
destroy(): void {
node.removeEventListener("click", handle_copy);
}
};
}
async function copy_to_clipboard(value: string): Promise<boolean> {
let copied = false;
if ("clipboard" in navigator) {
await navigator.clipboard.writeText(value);
copied = true;
} else {
const textArea = document.createElement("textarea");
textArea.value = value;
textArea.style.position = "absolute";
textArea.style.left = "-999999px";
document.body.prepend(textArea);
textArea.select();
try {
document.execCommand("copy");
copied = true;
} catch (error) {
console.error(error);
copied = false;
} finally {
textArea.remove();
}
}
return copied;
}
export { marked };

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import type { FileData } from "@gradio/upload";
import { normalise_file } from "@gradio/upload";
import Model3DUpload from "./Model3DUpload.svelte";
@ -21,6 +22,10 @@
export let container = true;
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let gradio: Gradio<{
change: typeof value;
clear: never;
}>;
let _value: null | FileData;
$: _value = normalise_file(value, root, root_url);
@ -48,8 +53,8 @@
value={_value}
on:change={({ detail }) => (value = detail)}
on:drag={({ detail }) => (dragging = detail)}
on:change
on:clear
on:change={({ detail }) => gradio.dispatch("change", detail)}
on:clear={() => gradio.dispatch("clear")}
>
<UploadText type="file" />
</Model3DUpload>

@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher, tick, afterUpdate, onMount } from "svelte";
import { createEventDispatcher, tick, onMount } from "svelte";
import { Upload, ModifyUpload } from "@gradio/upload";
import type { FileData } from "@gradio/upload";
import { BlockLabel } from "@gradio/atoms";
@ -10,18 +10,23 @@
export let label = "";
export let show_label: boolean;
let mounted = false;
onMount(() => {
if (value != null) {
addNewModel();
}
mounted = true;
});
afterUpdate(() => {
if (value != null && value.is_file) {
addNewModel();
}
$: ({ data, is_file, name } = value || {
data: undefined,
is_file: undefined,
name: undefined
});
$: canvas && mounted && data != null && is_file && addNewModel();
async function handle_upload({
detail
}: CustomEvent<FileData>): Promise<void> {
@ -89,6 +94,7 @@
url = URL.createObjectURL(blob);
}
BABYLON.SceneLoader.ShowLoadingScreen = false;
BABYLON.SceneLoader.Append(
url,
"",
@ -132,5 +138,6 @@
width: var(--size-full);
height: var(--size-full);
object-fit: contain;
overflow: hidden;
}
</style>

@ -12,6 +12,7 @@
"@gradio/icons": "workspace:^",
"@gradio/statustracker": "workspace:^",
"@gradio/upload": "workspace:^",
"@gradio/utils": "workspace:^",
"babylonjs": "^4.2.1",
"babylonjs-loaders": "^4.2.1"
},

@ -3,30 +3,39 @@
import { BlockLabel, IconButton } from "@gradio/atoms";
import { File, Download } from "@gradio/icons";
import { _ } from "svelte-i18n";
import { onMount } from "svelte";
import * as BABYLON from "babylonjs";
import * as BABYLON_LOADERS from "babylonjs-loaders";
export let value: FileData | null;
export let clearColor: [number, number, number, number] = [0, 0, 0, 0];
export let label = "";
export let show_label: boolean;
import { onMount, afterUpdate } from "svelte";
import * as BABYLON from "babylonjs";
import * as BABYLON_LOADERS from "babylonjs-loaders";
BABYLON_LOADERS.OBJFileLoader.IMPORT_VERTEX_COLORS = true;
let canvas: HTMLCanvasElement;
let scene: BABYLON.Scene;
let engine: BABYLON.Engine | null;
let mounted = false;
onMount(() => {
engine = new BABYLON.Engine(canvas, true);
window.addEventListener("resize", () => {
engine?.resize();
});
mounted = true;
});
afterUpdate(() => {
$: ({ data, name } = value || {
data: undefined,
name: undefined
});
$: canvas && mounted && data != null && name && dispose();
function dispose(): void {
if (scene && !scene.isDisposed) {
scene.dispose();
engine?.stopRenderLoop();
@ -38,7 +47,7 @@
});
}
addNewModel();
});
}
function addNewModel(): void {
scene = new BABYLON.Scene(engine!);
@ -62,6 +71,8 @@
url = URL.createObjectURL(blob);
}
BABYLON.SceneLoader.ShowLoadingScreen = false;
BABYLON.SceneLoader.Append(
"",
url,
@ -104,6 +115,7 @@
width: var(--size-full);
height: var(--size-full);
object-fit: contain;
overflow: hidden;
}
.download {
position: absolute;

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import Number from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -20,6 +21,13 @@
export let loading_status: LoadingStatus;
export let value_is_output = false;
export let step: number | null = null;
export let gradio: Gradio<{
change: never;
input: never;
submit: never;
blur: never;
focus: never;
}>;
</script>
<Block
@ -43,10 +51,10 @@
{maximum}
{step}
{container}
on:change
on:input
on:submit
on:blur
on:focus
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
on:submit={() => gradio.dispatch("submit")}
on:blur={() => gradio.dispatch("blur")}
on:focus={() => gradio.dispatch("focus")}
/>
</Block>

@ -16,6 +16,7 @@
},
"dependencies": {
"@gradio/atoms": "workspace:^",
"@gradio/statustracker": "workspace:^"
"@gradio/statustracker": "workspace:^",
"@gradio/utils": "workspace:^"
}
}

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import Number from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -20,6 +21,13 @@
export let loading_status: LoadingStatus;
export let value_is_output = false;
export let step: number | null = null;
export let gradio: Gradio<{
change: never;
input: never;
submit: never;
blur: never;
focus: never;
}>;
</script>
<Block
@ -43,10 +51,10 @@
{step}
{container}
disabled
on:change
on:input
on:submit
on:blur
on:focus
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
on:submit={() => gradio.dispatch("submit")}
on:blur={() => gradio.dispatch("blur")}
on:focus={() => gradio.dispatch("focus")}
/>
</Block>

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import Plot from "./Plot.svelte";
import { Block, BlockLabel } from "@gradio/atoms";
@ -23,6 +24,9 @@
export let theme_mode: ThemeMode;
export let caption: string;
export let bokeh_version: string | null;
export let gradio: Gradio<{
change: never;
}>;
</script>
<Block
@ -36,5 +40,12 @@
>
<BlockLabel {show_label} label={label || $_("plot.plot")} Icon={PlotIcon} />
<StatusTracker {...loading_status} />
<Plot {value} {target} {theme_mode} {caption} {bokeh_version} on:change />
<Plot
{value}
{target}
{theme_mode}
{caption}
{bokeh_version}
on:change={() => gradio.dispatch("change")}
/>
</Block>

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import Radio from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -18,6 +19,11 @@
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let gradio: Gradio<{
change: never;
select: SelectData;
input: never;
}>;
</script>
<Block
@ -39,8 +45,8 @@
{elem_id}
{show_label}
{choices}
on:change
on:input
on:select
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
on:select={(e) => gradio.dispatch("select", e.detail)}
/>
</Block>

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import Radio from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -18,6 +19,11 @@
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let gradio: Gradio<{
change: never;
select: SelectData;
input: never;
}>;
</script>
<Block
@ -40,8 +46,8 @@
{show_label}
{choices}
disabled
on:change
on:input
on:select
on:change={() => gradio.dispatch("change")}
on:input={() => gradio.dispatch("input")}
on:select={(e) => gradio.dispatch("select", e.detail)}
/>
</Block>

@ -75,7 +75,10 @@ test("Slider respects show_label", async ({ mount, page }) => {
show_label: false,
step: 1,
mode: "dynamic",
loading_status: loading_status
loading_status: loading_status,
gradio: {
dispatch() {}
}
}
});
await expect(component.getByTestId("block-title")).toBeHidden();
@ -91,7 +94,10 @@ test("Slider Maximum/Minimum values", async ({ mount, page }) => {
show_label: true,
step: 1,
mode: "dynamic",
loading_status: loading_status
loading_status: loading_status,
gradio: {
dispatch() {}
}
}
});
const slider = component.getByLabel("My Slider");
@ -102,8 +108,15 @@ test("Slider Maximum/Minimum values", async ({ mount, page }) => {
});
test("Slider Change event", async ({ mount, page }) => {
let change = spy();
let release = spy();
const events = {
change: 0,
release: 0
};
function event(name: "change" | "release") {
events[name] += 1;
}
const component = await mount(Slider, {
props: {
value: 3,
@ -113,11 +126,10 @@ test("Slider Change event", async ({ mount, page }) => {
show_label: true,
step: 1,
mode: "dynamic",
loading_status: loading_status
},
on: {
change: change,
release: release
loading_status: loading_status,
gradio: {
dispatch: event
}
}
});
@ -127,6 +139,6 @@ test("Slider Change event", async ({ mount, page }) => {
await expect(component.getByLabel("My Slider")).toHaveValue("7");
// More than one change event and one release event.
await expect(change.callCount).toBeGreaterThan(1);
await expect(release.callCount).toEqual(1);
await expect(events.change).toBeGreaterThanOrEqual(1);
await expect(events.release).toBeGreaterThanOrEqual(1);
});

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import Slider from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -21,6 +22,11 @@
export let loading_status: LoadingStatus;
export let value_is_output = false;
export let gradio: Gradio<{
change: never;
input: never;
release: number;
}>;
</script>
<Block {visible} {elem_id} {elem_classes} {container} {scale} {min_width}>
@ -35,8 +41,8 @@
{minimum}
{maximum}
{step}
on:input
on:change
on:release
on:input={() => gradio.dispatch("input")}
on:change={() => gradio.dispatch("change")}
on:release={(e) => gradio.dispatch("release", e.detail)}
/>
</Block>

@ -16,6 +16,7 @@
},
"dependencies": {
"@gradio/atoms": "workspace:^",
"@gradio/statustracker": "workspace:^"
"@gradio/statustracker": "workspace:^",
"@gradio/utils": "workspace:^"
}
}

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio, ShareData } from "@gradio/utils";
import Slider from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -21,6 +22,11 @@
export let loading_status: LoadingStatus;
export let value_is_output = false;
export let gradio: Gradio<{
change: never;
input: never;
release: number;
}>;
</script>
<Block {visible} {elem_id} {elem_classes} {container} {scale} {min_width}>
@ -36,8 +42,8 @@
{maximum}
{step}
disabled
on:input
on:change
on:release
on:input={() => gradio.dispatch("input")}
on:change={() => gradio.dispatch("change")}
on:release={(e) => gradio.dispatch("release", e.detail)}
/>
</Block>

@ -1,12 +1,22 @@
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import TabItem from "./TabItem.svelte";
export let elem_id = "";
export let elem_classes: string[] = [];
export let label: string;
export let id: string | number;
export let gradio: Gradio<{
select: SelectData;
}>;
</script>
<TabItem {elem_id} {elem_classes} name={label} {id} on:select>
<TabItem
{elem_id}
{elem_classes}
name={label}
{id}
on:select={({ detail }) => gradio.dispatch("select", detail)}
>
<slot />
</TabItem>

@ -1,4 +1,5 @@
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import { createEventDispatcher } from "svelte";
import Tabs from "./Tabs.svelte";
@ -8,10 +9,21 @@
export let elem_id = "";
export let elem_classes: string[] = [];
export let selected: number | string;
export let gradio: Gradio<{
change: never;
select: SelectData;
}>;
$: dispatch("prop_change", { selected });
</script>
<Tabs {visible} {elem_id} {elem_classes} bind:selected on:change on:select>
<Tabs
{visible}
{elem_id}
{elem_classes}
bind:selected
on:change={() => gradio.dispatch("change")}
on:select={(e) => gradio.dispatch("select", e.detail)}
>
<slot />
</Tabs>

@ -6,14 +6,15 @@ import event from "@testing-library/user-event";
import Textbox from "./interactive";
import type { LoadingStatus } from "@gradio/statustracker";
const loading_status = {
const loading_status: LoadingStatus = {
eta: 0,
queue_position: 1,
queue_size: 1,
status: "complete" as LoadingStatus["status"],
scroll_to_output: false,
visible: true,
fn_index: 0
fn_index: 0,
show_progress: "full"
};
describe("Textbox", () => {
@ -25,7 +26,6 @@ describe("Textbox", () => {
max_lines: 1,
loading_status,
lines: 1,
mode: "dynamic",
value: "hello world",
label: "Textbox"
});
@ -37,20 +37,18 @@ describe("Textbox", () => {
});
test("changing the text should update the value", async () => {
const { component, getByDisplayValue } = await render(Textbox, {
const { component, getByDisplayValue, listen } = await render(Textbox, {
show_label: true,
max_lines: 10,
loading_status,
lines: 1,
mode: "dynamic",
value: "hi ",
label: "Textbox"
});
const item: HTMLInputElement = getByDisplayValue("hi") as HTMLInputElement;
const mock = spy();
component.$on("change", mock);
const mock = listen("change");
item.focus();
await event.keyboard("some text");
@ -58,6 +56,6 @@ describe("Textbox", () => {
assert.equal(item.value, "hi some text");
assert.equal(component.value, "hi some text");
assert.equal(mock.callCount, 9);
assert.equal(mock.calls[8][0].detail, "hi some text");
assert.equal(mock.calls[8][0].detail.data, "hi some text");
});
});

@ -1,11 +1,21 @@
<svelte:options accessors={true} />
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import TextBox from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker";
export let gradio: Gradio<{
change: string;
submit: never;
blur: never;
select: SelectData;
input: never;
focus: never;
}>;
export let label = "Textbox";
export let info: string | undefined = undefined;
export let elem_id = "";
@ -56,11 +66,11 @@
{show_copy_button}
{autofocus}
{container}
on:change
on:input
on:submit
on:blur
on:select
on:focus
on:change={() => gradio.dispatch("change", value)}
on:input={() => gradio.dispatch("input")}
on:submit={() => gradio.dispatch("submit")}
on:blur={() => gradio.dispatch("blur")}
on:select={(e) => gradio.dispatch("select", e.detail)}
on:focus={() => gradio.dispatch("focus")}
/>
</Block>

@ -1 +1,2 @@
export { default } from "./InteractiveTextbox.svelte";
export { default as BaseTextbox } from "../shared/Textbox.svelte";

@ -1,11 +1,20 @@
<svelte:options accessors={true} />
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import TextBox from "../shared";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker";
export let gradio: Gradio<{
change: string;
submit: never;
blur: never;
select: SelectData;
input: never;
focus: never;
}>;
export let label = "Textbox";
export let info: string | undefined = undefined;
export let elem_id = "";
@ -56,12 +65,12 @@
{show_copy_button}
{autofocus}
{container}
on:change
on:input
on:submit
on:blur
on:select
on:focus
on:change={() => gradio.dispatch("change", value)}
on:input={() => gradio.dispatch("input")}
on:submit={() => gradio.dispatch("submit")}
on:blur={() => gradio.dispatch("blur")}
on:select={(e) => gradio.dispatch("select", e.detail)}
on:focus={() => gradio.dispatch("focus")}
disabled
/>
</Block>

@ -82,20 +82,6 @@
/* code
*/
.prose code {
border: 1px solid var(--border-color-primary);
border-radius: var(--radius-sm);
background: var(--background-fill-secondary);
padding: 1px 3px;
font-size: 85%;
white-space: nowrap;
}
.prose pre > code {
display: block;
padding: 0.5em 0.7em;
/* font-size: 100%; */
white-space: pre;
}
/* tables
*/
@ -172,6 +158,6 @@
padding-left: var(--size-2);
}
.prose :last-child {
.prose:last-child {
margin-bottom: 0 !important;
}

@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Gradio } from "@gradio/utils";
import { Upload, ModifyUpload } from "@gradio/upload";
import type { FileData } from "@gradio/upload";
import { Block, BlockLabel, Empty } from "@gradio/atoms";
@ -18,11 +18,6 @@
);
}
const dispatch = createEventDispatcher<{
change: undefined;
clear: undefined;
}>();
interface StaticData {
data: number[][];
headers: string[];
@ -45,6 +40,10 @@
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let gradio: Gradio<{
change: undefined;
clear: undefined;
}>;
let _value: string | null;
@ -128,13 +127,13 @@
function handle_clear({ detail }: CustomEvent<FileData | null>): void {
value = null;
dispatch("change");
dispatch("clear");
gradio.dispatch("change");
gradio.dispatch("clear");
}
$: _value = value == null ? null : _value;
$: value, dispatch("change");
$: value, gradio.dispatch("change");
</script>
<Block

@ -13,6 +13,8 @@ import type {
EventType,
FireObject
} from "@testing-library/dom";
import { spy, type Spy } from "tinyspy";
import { Gradio } from "../../app/src/gradio_helper";
const containerCache = new Map();
const componentCache = new Set();
@ -40,21 +42,36 @@ export interface RenderOptions<Q extends Queries = typeof queries> {
export async function render<
Events extends Record<string, any>,
Props extends Record<string, any>,
T extends SvelteComponent<Props, Events>
T extends SvelteComponent<Props, Events>,
X extends Record<string, any>
>(
Component: ComponentType<T, Props> | { default: ComponentType<T, Props> },
props?: Props
): Promise<RenderResult<T>> {
props?: Omit<Props, "gradio">
): Promise<
RenderResult<T> & {
listen: typeof listen;
wait_for_event: typeof wait_for_event;
}
> {
const container = document.body;
const target = container.appendChild(document.createElement("div"));
const ComponentConstructor: ComponentType<T, Props> =
const ComponentConstructor: ComponentType<
T,
Props & { gradio: typeof Gradio<X> }
> =
//@ts-ignore
Component.default || Component;
const id = Math.floor(Math.random() * 1000000);
const component = new ComponentConstructor({
target,
props
//@ts-ignore
props: {
...(props || {}),
gradio: new Gradio(id, target, "light", "2.0.0", "http://localhost:8000")
}
});
containerCache.set(container, { target, component });
@ -66,6 +83,36 @@ export async function render<
await tick();
type extractGeneric<Type> = Type extends Gradio<infer X> ? X : null;
type event_name = keyof extractGeneric<Props["gradio"]>;
function listen(event: event_name): Spy {
const mock = spy();
target.addEventListener("gradio", (e: Event) => {
if (isCustomEvent(e)) {
if (e.detail.event === event && e.detail.id === id) {
mock(e);
}
}
});
return mock;
}
async function wait_for_event(event: event_name): Promise<Spy> {
return new Promise((res) => {
const mock = spy();
target.addEventListener("gradio", (e: Event) => {
if (isCustomEvent(e)) {
if (e.detail.event === event && e.detail.id === id) {
mock(e);
res(mock);
}
}
});
});
}
return {
container,
component,
@ -74,7 +121,9 @@ export async function render<
unmount: (): void => {
if (componentCache.has(component)) component.$destroy();
},
...getQueriesForElement(container)
...getQueriesForElement(container),
listen,
wait_for_event
};
}
@ -115,3 +164,7 @@ export type FireFunction = (
) => Promise<boolean>;
export * from "@testing-library/dom";
function isCustomEvent(event: Event): event is CustomEvent {
return "detail" in event;
}

@ -1,4 +1,8 @@
export { default as Upload } from "./Upload.svelte";
export { default as ModifyUpload } from "./ModifyUpload.svelte";
export type { FileData } from "./types";
export {normalise_file, get_fetchable_url_or_file, blobToBase64} from "./utils";
export {
normalise_file,
get_fetchable_url_or_file,
blobToBase64
} from "./utils";

Some files were not shown because too many files have changed in this diff Show More