Svelte migration (WIP) (#448)
* first migration commit * style comment * first mvp working with calculator * ali components * carousel * more changes * changes * add examples * examples support * more changes * interpretation * interpretation * submission state * first migration commit * style comment * first mvp working with calculator * ali components * carousel * more changes * changes * add examples * examples support * more changes * interpretation * interpretation * submission state * base image cropper * add image editor * css tweaks * remove dead code * finalise sketch tools * add webcam snapshot source * tweak config * tweak config * tweak config * tweaks * reset egg files * lockfile v2 * image tweaks * record audio from mic * add audio input components * audio tweaks * editable table * more table tweaks * sort columns * add row/col to table * add output table * fix broken paths * fix svelte build destination * fix svelte build destination again * fix gitignore * fix css * add themes * add all themes * snake core classnames * actually fix themes this time * merge changes Co-authored-by: Ali Abid <aliabid94@gmail.com> Co-authored-by: Ali Abid <aliabid@Alis-MacBook-Pro.local> Co-authored-by: pngwn <hello@pngwn.io>
2
.gitignore
vendored
@ -12,6 +12,8 @@ build/
|
||||
|
||||
# JS build
|
||||
gradio/templates/frontend/static
|
||||
gradio/templates/frontend/build
|
||||
gradio/templates/frontend/global.css
|
||||
|
||||
# Secrets
|
||||
.env
|
||||
|
@ -86,9 +86,11 @@ iface = gr.Interface(
|
||||
fn,
|
||||
inputs=[
|
||||
gr.inputs.Textbox(default="Lorem ipsum", label="Textbox"),
|
||||
gr.inputs.Textbox(lines=3, placeholder="Type here..", label="Textbox 2"),
|
||||
gr.inputs.Textbox(lines=3, placeholder="Type here..",
|
||||
label="Textbox 2"),
|
||||
gr.inputs.Number(label="Number", default=42),
|
||||
gr.inputs.Slider(minimum=10, maximum=20, default=15, label="Slider: 10 - 20"),
|
||||
gr.inputs.Slider(minimum=10, maximum=20, default=15,
|
||||
label="Slider: 10 - 20"),
|
||||
gr.inputs.Slider(maximum=20, step=0.04, label="Slider: step @ 0.04"),
|
||||
gr.inputs.Checkbox(label="Checkbox"),
|
||||
gr.inputs.CheckboxGroup(
|
||||
@ -97,14 +99,17 @@ iface = gr.Interface(
|
||||
gr.inputs.Radio(label="Radio", choices=CHOICES, default=CHOICES[2]),
|
||||
gr.inputs.Dropdown(label="Dropdown", choices=CHOICES),
|
||||
gr.inputs.Image(label="Image", optional=True),
|
||||
gr.inputs.Image(label="Image w/ Cropper", tool="select", optional=True),
|
||||
gr.inputs.Image(label="Image w/ Cropper",
|
||||
tool="select", optional=True),
|
||||
gr.inputs.Image(label="Sketchpad", source="canvas", optional=True),
|
||||
gr.inputs.Image(label="Webcam", source="webcam", optional=True),
|
||||
gr.inputs.Video(label="Video", optional=True),
|
||||
gr.inputs.Audio(label="Audio", optional=True),
|
||||
gr.inputs.Audio(label="Microphone", source="microphone", optional=True),
|
||||
gr.inputs.Audio(label="Microphone",
|
||||
source="microphone", optional=True),
|
||||
gr.inputs.File(label="File", optional=True),
|
||||
gr.inputs.Dataframe(label="Dataframe", headers=["Name", "Age", "Gender"]),
|
||||
gr.inputs.Dataframe(label="Dataframe", headers=[
|
||||
"Name", "Age", "Gender"]),
|
||||
gr.inputs.Timeseries(x="time", y=["price", "value"], optional=True),
|
||||
],
|
||||
outputs=[
|
||||
@ -113,7 +118,8 @@ iface = gr.Interface(
|
||||
gr.outputs.Audio(label="Audio"),
|
||||
gr.outputs.Image(label="Image"),
|
||||
gr.outputs.Video(label="Video"),
|
||||
gr.outputs.HighlightedText(label="HighlightedText"),
|
||||
gr.outputs.HighlightedText(label="HighlightedText", color_map={
|
||||
"punc": "pink", "test 0": "blue"}),
|
||||
gr.outputs.HighlightedText(label="HighlightedText", show_legend=True),
|
||||
gr.outputs.JSON(label="JSON"),
|
||||
gr.outputs.HTML(label="HTML"),
|
||||
@ -121,7 +127,8 @@ iface = gr.Interface(
|
||||
gr.outputs.Dataframe(label="Dataframe"),
|
||||
gr.outputs.Dataframe(label="Numpy", type="numpy"),
|
||||
gr.outputs.Carousel("image", label="Carousel"),
|
||||
gr.outputs.Timeseries(x="time", y=["price", "value"], label="Timeseries"),
|
||||
gr.outputs.Timeseries(
|
||||
x="time", y=["price", "value"], label="Timeseries"),
|
||||
],
|
||||
examples=[
|
||||
[
|
||||
@ -147,7 +154,7 @@ iface = gr.Interface(
|
||||
]
|
||||
]
|
||||
* 3,
|
||||
theme="huggingface",
|
||||
theme="default",
|
||||
title="Kitchen Sink",
|
||||
description="Try out all the components!",
|
||||
article="Learn more about [Gradio](http://gradio.app)",
|
||||
|
BIN
demo/zip_two_files/tmp.zip
Normal file
@ -1,23 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
@ -1 +0,0 @@
|
||||
src/vendor
|
23
frontend/.gitignore
vendored
@ -1,23 +1,4 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
/node_modules/
|
||||
/public/build/
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
@ -1,20 +1,109 @@
|
||||
# Gradio Frontend
|
||||
*Psst — looking for a more complete solution? Check out [SvelteKit](https://kit.svelte.dev), the official framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.*
|
||||
|
||||
This directory contains a React App that renders the frontend of the gradio framework.
|
||||
*Looking for a shareable component template instead? You can [use SvelteKit for that as well](https://kit.svelte.dev/docs#packaging) or the older [sveltejs/component-template](https://github.com/sveltejs/component-template)*
|
||||
|
||||
## Development Setup
|
||||
---
|
||||
|
||||
To make changes to the gradio frontend, you will need to have the following installed
|
||||
- npm / node
|
||||
- python3
|
||||
- gradio
|
||||
# svelte app
|
||||
|
||||
Once node is installed, make sure to run `npm install` in this directory to install the node packages.
|
||||
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
|
||||
|
||||
Because this is only the frontend of the library, you must first launch a Gradio interface running on port 7860, which will be used as a backend. You can use any of the demo projects in the gradio/demo folder to serve this backend role, but make sure that the port is set to 7860. Then you launch the development frontend by running `npm run start`. Once this is run, the frontend development will launch on port 3000. It will connect with port 7860 to load the initial configuration and make API calls on submit.
|
||||
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
|
||||
|
||||
As a Create-React-App, any changes you make in the code will automatically reflect in the development frontend.
|
||||
```bash
|
||||
npx degit sveltejs/template svelte-app
|
||||
cd svelte-app
|
||||
```
|
||||
|
||||
## Committing Changes
|
||||
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
|
||||
|
||||
In production, the frontend is compiled and stored in the gradio/gradio/frontend directory. (This readme is in gradio/frontend, a different directory outside the python package). To compile, run `npm run build`. At the end of the process, you should see the message "Compiled successfully" - there may be an warning or error thrown about not being able to find bundle.css which you can ignore. To include the compiled js into your local python version of gradio, run `python3 setup.py install` and then launch a gradio interface. Your changes should be visible on port 7860.
|
||||
|
||||
## Get started
|
||||
|
||||
Install the dependencies...
|
||||
|
||||
```bash
|
||||
cd svelte-app
|
||||
npm install
|
||||
```
|
||||
|
||||
...then start [Rollup](https://rollupjs.org):
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
|
||||
|
||||
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
|
||||
|
||||
If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
|
||||
|
||||
## Building and running in production mode
|
||||
|
||||
To create an optimised version of the app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
|
||||
|
||||
|
||||
## Single-page app mode
|
||||
|
||||
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
|
||||
|
||||
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
|
||||
|
||||
```js
|
||||
"start": "sirv public --single"
|
||||
```
|
||||
|
||||
## Using TypeScript
|
||||
|
||||
This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
|
||||
|
||||
```bash
|
||||
node scripts/setupTypeScript.js
|
||||
```
|
||||
|
||||
Or remove the script via:
|
||||
|
||||
```bash
|
||||
rm scripts/setupTypeScript.js
|
||||
```
|
||||
|
||||
If you want to use `baseUrl` or `path` aliases within your `tsconfig`, you need to set up `@rollup/plugin-alias` to tell Rollup to resolve the aliases. For more info, see [this StackOverflow question](https://stackoverflow.com/questions/63427935/setup-tsconfig-path-in-svelte).
|
||||
|
||||
## Deploying to the web
|
||||
|
||||
### With [Vercel](https://vercel.com)
|
||||
|
||||
Install `vercel` if you haven't already:
|
||||
|
||||
```bash
|
||||
npm install -g vercel
|
||||
```
|
||||
|
||||
Then, from within your project folder:
|
||||
|
||||
```bash
|
||||
cd public
|
||||
vercel deploy --name my-project
|
||||
```
|
||||
|
||||
### With [surge](https://surge.sh/)
|
||||
|
||||
Install `surge` if you haven't already:
|
||||
|
||||
```bash
|
||||
npm install -g surge
|
||||
```
|
||||
|
||||
Then, from within your project folder:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
surge public my-project.surge.sh
|
||||
```
|
||||
|
@ -1,49 +0,0 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "static/bundle.css",
|
||||
chunkFilename: "static/css/[name].chunk.css"
|
||||
})
|
||||
],
|
||||
eslint: {
|
||||
enable: true /* (default value) */,
|
||||
mode: "extends" /* (default value) */ || "file"
|
||||
},
|
||||
configure: (webpackConfig, { env, paths }) => {
|
||||
webpackConfig.optimization = {
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
runtimeChunk: false
|
||||
};
|
||||
webpackConfig.entry = "./src/index";
|
||||
webpackConfig.output = {
|
||||
publicPath: "",
|
||||
path: path.resolve(__dirname, "../gradio/templates/frontend"),
|
||||
filename: "static/bundle.js",
|
||||
chunkFilename: "static/js/[name].chunk.js"
|
||||
};
|
||||
paths.appBuild = webpackConfig.output.path;
|
||||
return webpackConfig;
|
||||
}
|
||||
},
|
||||
style: {
|
||||
postcss: {
|
||||
plugins: [
|
||||
require("postcss-prefixwrap")(".gradio_wrapper"),
|
||||
require("tailwindcss"),
|
||||
require("autoprefixer")
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
41337
frontend/package-lock.json
generated
@ -1,69 +1,40 @@
|
||||
{
|
||||
"name": "gradio",
|
||||
"name": "svelte-app",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@craco/craco": "^6.2.0",
|
||||
"@tailwindcss/typography": "^0.4.1",
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@toast-ui/react-image-editor": "^3.14.2",
|
||||
"chart.js": "^3.6.0",
|
||||
"classnames": "^2.3.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"fabric": "^4.5.0",
|
||||
"html2canvas-objectfit-fix": "^1.2.0",
|
||||
"jspreadsheet-ce": "^4.7.3",
|
||||
"mime-types": "^2.1.33",
|
||||
"postcss-prefixwrap": "^1.26.0",
|
||||
"prettier-eslint": "^13.0.0",
|
||||
"prettier-eslint-cli": "^5.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-chartjs-2": "^3.3.0",
|
||||
"react-cropper": "^2.1.8",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-json-tree": "^0.15.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-webcam": "^5.2.3",
|
||||
"recorder-js": "^1.0.7",
|
||||
"sass": "^1.32.8",
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"web-vitals": "^1.1.1",
|
||||
"webpack": "^4.44.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env REACT_APP_BACKEND_URL='http://localhost:7860/' craco start",
|
||||
"format": "prettier-eslint --write '**/*.js*'",
|
||||
"build": "REACT_APP_BACKEND_URL='' REACT_APP_VERSION=$(cat ../gradio/version.txt) GENERATE_SOURCEMAP=false craco build",
|
||||
"build:win": "cross-env REACT_APP_BACKEND_URL='' GENERATE_SOURCEMAP=false craco build",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"prettier"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"start": "sirv public --no-clear --port 3000"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^9.8.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^7.32.0",
|
||||
"mini-css-extract-plugin": "^0.11.3",
|
||||
"postcss": "^7.0.36",
|
||||
"prettier": "^2.3.2",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.1.2"
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||
"mime-types": "^2.1.34",
|
||||
"postcss": "^8.4.5",
|
||||
"postcss-nested": "^5.0.6",
|
||||
"rollup": "^2.3.4",
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
"rollup-plugin-svelte": "^7.0.0",
|
||||
"rollup-plugin-terser": "^7.0.0",
|
||||
"svelte": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rollup/plugin-replace": "^3.0.1",
|
||||
"autoprefixer": "^9.8.8",
|
||||
"cropperjs": "^1.5.12",
|
||||
"lazy-brush": "^1.0.1",
|
||||
"mime-types": "^2.1.34",
|
||||
"node-sass": "^7.0.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"sirv-cli": "^1.0.0",
|
||||
"svelte-preprocess": "^4.10.1",
|
||||
"svelte-range-slider-pips": "^2.0.1",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17",
|
||||
"tui-image-editor": "^3.15.2"
|
||||
}
|
||||
}
|
||||
|
BIN
frontend/public/favicon.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
377
frontend/public/global.css
Normal file
@ -1,9 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="height: 100%; margin: 0; padding: 0;">
|
||||
<html lang="en" style="min-height: 100%; margin: 0; padding: 0;">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel='stylesheet' href='/build/bundle.css'>
|
||||
<link rel='stylesheet' href='/build/themes.css'>
|
||||
|
||||
<link rel="stylesheet" href="./global.css">
|
||||
|
||||
<title>{{ config['title'] or 'Gradio' }}</title>
|
||||
<meta property="og:url" content="https://gradio.app/" />
|
||||
@ -16,11 +20,6 @@
|
||||
<meta name="twitter:title" content="{{ config['title'] or '' }}">
|
||||
<meta name="twitter:description" content="{{ config['description'] or '' }}">
|
||||
<meta name="twitter:image" content="{{ config['thumbnail'] or '' }}">
|
||||
{%if config['favicon_path']%}
|
||||
<link rel="shortcut icon" href="{{ config['favicon_path']}}">
|
||||
{%else%}
|
||||
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
|
||||
{%endif%}
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-156449732-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
@ -40,6 +39,8 @@
|
||||
|
||||
<body style="height: 100%; margin: 0; padding: 0;">
|
||||
<div id="root" style="height: 100%"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</body>
|
||||
<script defer src='/build/bundle.js'></script>
|
||||
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
1
frontend/public/static/img/camera.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-camera"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
|
After Width: | Height: | Size: 349 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
117
frontend/rollup.config.js
Normal file
@ -0,0 +1,117 @@
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
import sveltePreprocess from "svelte-preprocess";
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import livereload from 'rollup-plugin-livereload';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import json from "@rollup/plugin-json";
|
||||
import copy from 'rollup-plugin-copy';
|
||||
import postcss from 'rollup-plugin-postcss';
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
function serve() {
|
||||
let server;
|
||||
|
||||
function toExit() {
|
||||
if (server) server.kill(0);
|
||||
}
|
||||
|
||||
return {
|
||||
writeBundle() {
|
||||
if (server) return;
|
||||
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
shell: true
|
||||
});
|
||||
|
||||
process.on('SIGTERM', toExit);
|
||||
process.on('exit', toExit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
input: 'src/main.js',
|
||||
output: [{
|
||||
sourcemap: true,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'public/build/bundle.js'
|
||||
}, {
|
||||
sourcemap: true,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: '../gradio/templates/frontend/build/bundle.js'
|
||||
}],
|
||||
plugins: [
|
||||
copy({
|
||||
targets: [
|
||||
{ src: 'public/*', dest: '../gradio/templates/frontend' },
|
||||
{ src: 'public/static', dest: '../gradio/templates/frontend' }
|
||||
]
|
||||
}),
|
||||
json(),
|
||||
replace({
|
||||
BUILD_MODE: production ? "prod" : "dev",
|
||||
BACKEND_URL: production ? "" : "http://localhost:7860/"
|
||||
}),
|
||||
postcss({
|
||||
extract: 'themes.css',
|
||||
plugins: [
|
||||
require("tailwindcss"),
|
||||
require("postcss-nested"),
|
||||
require("autoprefixer"),
|
||||
]
|
||||
}),
|
||||
svelte({
|
||||
preprocess: sveltePreprocess({
|
||||
// sourceMap: true,
|
||||
postcss: {
|
||||
plugins: [
|
||||
require("tailwindcss"),
|
||||
require("postcss-nested"),
|
||||
require("autoprefixer"),
|
||||
],
|
||||
},
|
||||
}),
|
||||
compilerOptions: {
|
||||
// enable run-time checks when not in production
|
||||
dev: !production
|
||||
}
|
||||
}),
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file - better for performance
|
||||
css({
|
||||
output: 'bundle.css'
|
||||
}),
|
||||
|
||||
// If you have external dependencies installed from
|
||||
// npm, you'll most likely need these plugins. In
|
||||
// some cases you'll need additional configuration -
|
||||
// consult the documentation for details:
|
||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe: ['svelte']
|
||||
}),
|
||||
commonjs(),
|
||||
|
||||
// In dev mode, call `npm run start` once
|
||||
// the bundle has been generated
|
||||
!production && serve(),
|
||||
|
||||
// Watch the `public` directory and refresh the
|
||||
// browser on changes when not in production
|
||||
!production && livereload('public'),
|
||||
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
production && terser()
|
||||
],
|
||||
watch: {
|
||||
clearScreen: false
|
||||
}
|
||||
};
|
121
frontend/scripts/setupTypeScript.js
Normal file
@ -0,0 +1,121 @@
|
||||
// @ts-check
|
||||
|
||||
/** This script modifies the project to support TS code in .svelte files like:
|
||||
|
||||
<script lang="ts">
|
||||
export let name: string;
|
||||
</script>
|
||||
|
||||
As well as validating the code for CI.
|
||||
*/
|
||||
|
||||
/** To work on this script:
|
||||
rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template
|
||||
*/
|
||||
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { argv } = require("process")
|
||||
|
||||
const projectRoot = argv[2] || path.join(__dirname, "..")
|
||||
|
||||
// Add deps to pkg.json
|
||||
const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
|
||||
packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
|
||||
"svelte-check": "^2.0.0",
|
||||
"svelte-preprocess": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.0.0",
|
||||
"typescript": "^4.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"@tsconfig/svelte": "^2.0.0"
|
||||
})
|
||||
|
||||
// Add script for checking
|
||||
packageJSON.scripts = Object.assign(packageJSON.scripts, {
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
})
|
||||
|
||||
// Write the package JSON
|
||||
fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " "))
|
||||
|
||||
// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too
|
||||
const beforeMainJSPath = path.join(projectRoot, "src", "main.js")
|
||||
const afterMainTSPath = path.join(projectRoot, "src", "main.ts")
|
||||
fs.renameSync(beforeMainJSPath, afterMainTSPath)
|
||||
|
||||
// Switch the app.svelte file to use TS
|
||||
const appSveltePath = path.join(projectRoot, "src", "App.svelte")
|
||||
let appFile = fs.readFileSync(appSveltePath, "utf8")
|
||||
appFile = appFile.replace("<script>", '<script lang="ts">')
|
||||
appFile = appFile.replace("export let name;", 'export let name: string;')
|
||||
fs.writeFileSync(appSveltePath, appFile)
|
||||
|
||||
// Edit rollup config
|
||||
const rollupConfigPath = path.join(projectRoot, "rollup.config.js")
|
||||
let rollupConfig = fs.readFileSync(rollupConfigPath, "utf8")
|
||||
|
||||
// Edit imports
|
||||
rollupConfig = rollupConfig.replace(`'rollup-plugin-terser';`, `'rollup-plugin-terser';
|
||||
import sveltePreprocess from 'svelte-preprocess';
|
||||
import typescript from '@rollup/plugin-typescript';`)
|
||||
|
||||
// Replace name of entry point
|
||||
rollupConfig = rollupConfig.replace(`'src/main.js'`, `'src/main.ts'`)
|
||||
|
||||
// Add preprocessor
|
||||
rollupConfig = rollupConfig.replace(
|
||||
'compilerOptions:',
|
||||
'preprocess: sveltePreprocess({ sourceMap: !production }),\n\t\t\tcompilerOptions:'
|
||||
);
|
||||
|
||||
// Add TypeScript
|
||||
rollupConfig = rollupConfig.replace(
|
||||
'commonjs(),',
|
||||
'commonjs(),\n\t\ttypescript({\n\t\t\tsourceMap: !production,\n\t\t\tinlineSources: !production\n\t\t}),'
|
||||
);
|
||||
fs.writeFileSync(rollupConfigPath, rollupConfig)
|
||||
|
||||
// Add TSConfig
|
||||
const tsconfig = `{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
||||
}`
|
||||
const tsconfigPath = path.join(projectRoot, "tsconfig.json")
|
||||
fs.writeFileSync(tsconfigPath, tsconfig)
|
||||
|
||||
// Add global.d.ts
|
||||
const dtsPath = path.join(projectRoot, "src", "global.d.ts")
|
||||
fs.writeFileSync(dtsPath, `/// <reference types="svelte" />`)
|
||||
|
||||
// Delete this script, but not during testing
|
||||
if (!argv[2]) {
|
||||
// Remove the script
|
||||
fs.unlinkSync(path.join(__filename))
|
||||
|
||||
// Check for Mac's DS_store file, and if it's the only one left remove it
|
||||
const remainingFiles = fs.readdirSync(path.join(__dirname))
|
||||
if (remainingFiles.length === 1 && remainingFiles[0] === '.DS_store') {
|
||||
fs.unlinkSync(path.join(__dirname, '.DS_store'))
|
||||
}
|
||||
|
||||
// Check if the scripts folder is empty
|
||||
if (fs.readdirSync(path.join(__dirname)).length === 0) {
|
||||
// Remove the scripts folder
|
||||
fs.rmdirSync(path.join(__dirname))
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the extension recommendation
|
||||
fs.mkdirSync(path.join(projectRoot, ".vscode"), { recursive: true })
|
||||
fs.writeFileSync(path.join(projectRoot, ".vscode", "extensions.json"), `{
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
}
|
||||
`)
|
||||
|
||||
console.log("Converted to TypeScript.")
|
||||
|
||||
if (fs.existsSync(path.join(projectRoot, "node_modules"))) {
|
||||
console.log("\nYou will need to re-run your dependency manager to get started.")
|
||||
}
|
52
frontend/src/App.svelte
Normal file
@ -0,0 +1,52 @@
|
||||
<script>
|
||||
import Interface from "./Interface.svelte";
|
||||
|
||||
export let title;
|
||||
export let description;
|
||||
export let theme;
|
||||
export let dark;
|
||||
export let input_components;
|
||||
export let output_components;
|
||||
export let examples;
|
||||
export let fn;
|
||||
export let root;
|
||||
export let allow_flagging;
|
||||
export let allow_interpretation;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="gradio-bg flex flex-col dark:bg-gray-600 {window.gradio_mode === 'app'
|
||||
? 'h-full'
|
||||
: 'h-auto'}"
|
||||
{theme}
|
||||
class:dark
|
||||
>
|
||||
<div
|
||||
class="gradio-page container mx-auto flex flex-col box-border flex-grow text-gray-700 dark:text-gray-50"
|
||||
>
|
||||
<div class="content pt-4 px-4 mb-4">
|
||||
{#if title}
|
||||
<h1 class="title text-center p-4 text-4xl">{title}</h1>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p class="description pb-4">{description}</p>
|
||||
{/if}
|
||||
<Interface
|
||||
{input_components}
|
||||
{output_components}
|
||||
{examples}
|
||||
{theme}
|
||||
{fn}
|
||||
{root}
|
||||
{allow_flagging}
|
||||
{allow_interpretation}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style global lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
</style>
|
101
frontend/src/ExampleSet.svelte
Normal file
@ -0,0 +1,101 @@
|
||||
<script>
|
||||
import { input_component_map } from "./components/directory.js";
|
||||
|
||||
export let examples,
|
||||
examples_dir,
|
||||
example_id,
|
||||
setExampleId,
|
||||
examples_per_page,
|
||||
input_components,
|
||||
theme;
|
||||
|
||||
let selected_examples = examples;
|
||||
let gallery = input_components.length === 1;
|
||||
</script>
|
||||
|
||||
<div class="examples" {theme}>
|
||||
<h4 class="text-lg font-semibold my-2">Examples</h4>
|
||||
<div
|
||||
class="examples-holder mt-4 inline-block max-w-full"
|
||||
class:gallery
|
||||
class:overflow-x-auto={!gallery}
|
||||
>
|
||||
{#if gallery}
|
||||
<div class="examples-gallery flex gap-2 flex-wrap">
|
||||
{#each selected_examples as example_row, i}
|
||||
<button
|
||||
class="example cursor-pointer p-2 rounded bg-gray-50 dark:bg-gray-700 transition"
|
||||
on:click={() => setExampleId(i)}
|
||||
>
|
||||
<svelte:component
|
||||
this={input_component_map[input_components[0].name].example}
|
||||
{theme}
|
||||
value={example_row[0]}
|
||||
{examples_dir}
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<table
|
||||
class="examples-table table-auto p-2 bg-gray-50 dark:bg-gray-600 rounded max-w-full border-collapse"
|
||||
>
|
||||
<thead class="border-b-2 dark:border-gray-600">
|
||||
<tr>
|
||||
{#each input_components as input_component, i}
|
||||
<th class="py-2 px-4" key={i}>
|
||||
{input_component.label}
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each selected_examples as example_row, i}
|
||||
<tr
|
||||
class="cursor-pointer transition"
|
||||
key={i}
|
||||
class:selected={i === example_id}
|
||||
on:click={() => setExampleId(i)}
|
||||
>
|
||||
{#each example_row as example_cell, j}
|
||||
<td class="py-2 px-4">
|
||||
<svelte:component
|
||||
this={input_component_map[input_components[j].name].example}
|
||||
{theme}
|
||||
value={example_cell}
|
||||
{examples_dir}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss" global>
|
||||
.examples[theme="default"] {
|
||||
.examples-holder:not(.gallery) {
|
||||
@apply shadow;
|
||||
.examples-table {
|
||||
@apply rounded dark:bg-gray-700;
|
||||
thead {
|
||||
@apply border-gray-300 dark:border-gray-600;
|
||||
}
|
||||
tbody tr:hover {
|
||||
@apply bg-yellow-500 dark:bg-red-700 text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.examples-holder .examples-gallery {
|
||||
.example {
|
||||
@apply shadow;
|
||||
}
|
||||
.example:hover {
|
||||
@apply bg-yellow-500 text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
319
frontend/src/Interface.svelte
Normal file
@ -0,0 +1,319 @@
|
||||
<script>
|
||||
import {
|
||||
input_component_map,
|
||||
output_component_map,
|
||||
} from "./components/directory.js";
|
||||
import { deepCopy } from "./components/utils/helpers.js";
|
||||
import ExampleSet from "./ExampleSet.svelte";
|
||||
|
||||
export let input_components;
|
||||
export let output_components;
|
||||
export let theme;
|
||||
export let fn;
|
||||
export let examples;
|
||||
export let root;
|
||||
export let allow_flagging;
|
||||
export let allow_interpretation;
|
||||
export let avg_durations;
|
||||
|
||||
let examples_dir = root + "file/";
|
||||
let interpret_mode = false;
|
||||
let submission_count = 0;
|
||||
let state = "START";
|
||||
let last_duration = null;
|
||||
|
||||
const default_inputs = input_components.map((component) =>
|
||||
"default" in component ? component.default : null
|
||||
);
|
||||
const default_outputs = new Array(output_components.length).fill(null);
|
||||
|
||||
let input_values = deepCopy(default_inputs);
|
||||
let output_values = deepCopy(default_outputs);
|
||||
let interpretation_values = [];
|
||||
let timer = null;
|
||||
let timer_start = 0;
|
||||
let timer_diff = 0;
|
||||
let avg_duration = Array.isArray(avg_durations)
|
||||
? this.props.avg_durations[0]
|
||||
: null;
|
||||
|
||||
const setValues = (index, value) => {
|
||||
input_values[index] = value;
|
||||
};
|
||||
const setExampleId = async (example_id) => {
|
||||
input_components.forEach(async (input_component, i) => {
|
||||
const process_example =
|
||||
input_component_map[input_component.name].process_example;
|
||||
if (process_example !== undefined) {
|
||||
input_values[i] = await process_example(
|
||||
examples[example_id][i],
|
||||
examples_dir
|
||||
);
|
||||
} else {
|
||||
input_values[i] = examples[example_id][i];
|
||||
}
|
||||
});
|
||||
};
|
||||
const startTimer = () => {
|
||||
timer_start = Date.now();
|
||||
timer_diff = 0;
|
||||
timer = setInterval(() => {
|
||||
timer_diff = (Date.now() - timer_start) / 1000;
|
||||
}, 100);
|
||||
};
|
||||
const stopTimer = () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
const submit = () => {
|
||||
if (state === "PENDING") {
|
||||
return;
|
||||
}
|
||||
state = "PENDING";
|
||||
submission_count += 1;
|
||||
let submission_count_at_click = submission_count;
|
||||
startTimer();
|
||||
fn("predict", { data: input_values })
|
||||
.then((output) => {
|
||||
if (
|
||||
state !== "PENDING" ||
|
||||
submission_count_at_click !== submission_count
|
||||
) {
|
||||
return;
|
||||
}
|
||||
stopTimer();
|
||||
output_values = output["data"];
|
||||
if ("durations" in output) {
|
||||
last_duration = output["durations"][0];
|
||||
}
|
||||
if ("avg_duration" in output) {
|
||||
avg_duration = output["avg_durations"][0];
|
||||
}
|
||||
state = "COMPLETE";
|
||||
})
|
||||
.catch((e) => {
|
||||
if (
|
||||
state !== "PENDING" ||
|
||||
submission_count_at_click !== submission_count
|
||||
) {
|
||||
return;
|
||||
}
|
||||
stopTimer();
|
||||
console.error(e);
|
||||
state = "ERROR";
|
||||
output_values = deepCopy(default_outputs);
|
||||
});
|
||||
};
|
||||
const clear = () => {
|
||||
input_values = deepCopy(default_inputs);
|
||||
output_values = deepCopy(default_outputs);
|
||||
interpret_mode = false;
|
||||
state = "START";
|
||||
stopTimer();
|
||||
};
|
||||
const flag = () => {
|
||||
fn("flag", {
|
||||
data: {
|
||||
input_data: input_values,
|
||||
output_data: output_values,
|
||||
},
|
||||
});
|
||||
};
|
||||
const interpret = () => {
|
||||
if (interpret_mode) {
|
||||
interpret_mode = false;
|
||||
} else {
|
||||
fn("interpret", {
|
||||
data: input_values,
|
||||
}).then((output) => {
|
||||
interpret_mode = true;
|
||||
interpretation_values = output.interpretation_scores;
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="gradio-interface" {theme}>
|
||||
<div class="panels flex flex-wrap justify-center gap-4">
|
||||
<div class="panel flex-1">
|
||||
<div
|
||||
class="component-set p-2 rounded flex flex-col flex-1 gap-2"
|
||||
style="min-height: 36px"
|
||||
>
|
||||
{#each input_components as input_component, i}
|
||||
<div class="component" key={i}>
|
||||
<div class="panel-header mb-1.5">{input_component.label}</div>
|
||||
<svelte:component
|
||||
this={input_component_map[input_component.name][
|
||||
interpret_mode ? "interpretation" : "component"
|
||||
]}
|
||||
{...input_component}
|
||||
{theme}
|
||||
value={input_values[i]}
|
||||
interpretation={interpret_mode ? interpretation_values[i] : null}
|
||||
setValue={setValues.bind(this, i)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="panel-buttons flex gap-4 my-4">
|
||||
<button
|
||||
class="panel-button bg-gray-50 dark:bg-gray-700 flex-1 p-3 rounded transition font-semibold focus:outline-none"
|
||||
on:click={clear}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
class="panel-button submit bg-gray-50 dark:bg-gray-700 flex-1 p-3 rounded transition font-semibold focus:outline-none"
|
||||
on:click={submit}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel flex-1">
|
||||
<div
|
||||
class="component-set p-2 rounded flex flex-col flex-1 gap-2 relative"
|
||||
style="min-height: 36px"
|
||||
class:opacity-50={state === "PENDING"}
|
||||
>
|
||||
{#if state !== "START"}
|
||||
<div class="state absolute right-2 flex items-center gap-2 text-xs">
|
||||
{#if state === "PENDING"}
|
||||
<div class="timer font-mono">{timer_diff.toFixed(1)}s</div>
|
||||
<img
|
||||
src="./static/img/logo.svg"
|
||||
alt="Pending"
|
||||
class="pending h-5 ml-2 inline-block"
|
||||
/>
|
||||
{:else if state === "ERROR"}
|
||||
<img
|
||||
src="./static/img/logo_error.svg"
|
||||
alt="Error"
|
||||
class="error h-5 ml-2 inline-block"
|
||||
/>
|
||||
{:else if state === "COMPLETE" && last_duration !== null}
|
||||
<div class="duration font-mono">{last_duration.toFixed(1)}s</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#each output_components as output_component, i}
|
||||
{#if output_values[i] !== null}
|
||||
<div class="component" key={i}>
|
||||
<div class="panel-header mb-1.5">{output_component.label}</div>
|
||||
<svelte:component
|
||||
this={output_component_map[output_component.name].component}
|
||||
{...output_component}
|
||||
{theme}
|
||||
value={output_values[i]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="panel-buttons flex gap-4 my-4">
|
||||
{#if allow_interpretation !== false}
|
||||
<button
|
||||
class="panel-button flag bg-gray-50 dark:bg-gray-700 flex-1 p-3 rounded transition font-semibold focus:outline-none"
|
||||
on:click={interpret}
|
||||
>
|
||||
{#if interpret_mode}
|
||||
Hide
|
||||
{:else}
|
||||
Interpret
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{#if allow_flagging !== "never"}
|
||||
<button
|
||||
class="panel-button flag bg-gray-50 dark:bg-gray-700 flex-1 p-3 rounded transition font-semibold focus:outline-none"
|
||||
on:click={flag}
|
||||
>
|
||||
Flag
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if examples}
|
||||
<ExampleSet
|
||||
{examples}
|
||||
{input_components}
|
||||
{theme}
|
||||
{examples_dir}
|
||||
{setExampleId}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss" global>
|
||||
.pending {
|
||||
@keyframes ld-breath {
|
||||
0% {
|
||||
animation-timing-function: cubic-bezier(
|
||||
0.9647,
|
||||
0.2413,
|
||||
-0.0705,
|
||||
0.7911
|
||||
);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
51% {
|
||||
animation-timing-function: cubic-bezier(
|
||||
0.9226,
|
||||
0.2631,
|
||||
-0.0308,
|
||||
0.7628
|
||||
);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
animation: ld-breath 0.75s infinite linear;
|
||||
}
|
||||
.gradio-interface[theme="default"] {
|
||||
.component-set {
|
||||
@apply bg-gray-50 dark:bg-gray-700 dark:drop-shadow-xl shadow;
|
||||
}
|
||||
.component {
|
||||
@apply mb-2;
|
||||
}
|
||||
.panel-header {
|
||||
@apply uppercase text-xs;
|
||||
}
|
||||
.panel-button {
|
||||
@apply hover:bg-gray-100 dark:hover:bg-gray-600 shadow;
|
||||
}
|
||||
.panel-button.disabled {
|
||||
@apply text-gray-400 cursor-not-allowed;
|
||||
}
|
||||
.panel-button.submit {
|
||||
@apply bg-yellow-500 hover:bg-yellow-400 dark:bg-red-700 dark:hover:bg-red-600 text-white;
|
||||
}
|
||||
.examples {
|
||||
.examples-table-holder:not(.gallery) {
|
||||
@apply shadow;
|
||||
.examples-table {
|
||||
@apply rounded dark:bg-gray-700;
|
||||
thead {
|
||||
@apply border-gray-300 dark:border-gray-600;
|
||||
}
|
||||
tbody tr:hover {
|
||||
@apply bg-yellow-500 dark:bg-red-700 text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.examples-table-holder.gallery .examples-table {
|
||||
tbody td {
|
||||
@apply shadow;
|
||||
}
|
||||
tbody td:hover {
|
||||
@apply bg-yellow-500 text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.gradio-interface[theme="huggingface"] {
|
||||
}
|
||||
</style>
|
54
frontend/src/api.js
Normal file
@ -0,0 +1,54 @@
|
||||
function delay(n) {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(resolve, n * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
let postData = async (url, body) => {
|
||||
const output = await fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
return output;
|
||||
};
|
||||
|
||||
export const fn = async (api_endpoint, action, data, queue, queue_callback) => {
|
||||
if (queue && ["predict", "interpret"].includes(action)) {
|
||||
data["action"] = action;
|
||||
const output = await postData(api_endpoint + "queue/push/", data);
|
||||
const output_json = await output.json();
|
||||
let [hash, queue_position] = [
|
||||
output_json["hash"],
|
||||
output_json["queue_position"]
|
||||
];
|
||||
queue_callback(queue_position, /*is_initial=*/ true);
|
||||
let status = "UNKNOWN";
|
||||
while (status != "COMPLETE" && status != "FAILED") {
|
||||
if (status != "UNKNOWN") {
|
||||
await delay(1);
|
||||
}
|
||||
const status_response = await postData(api_endpoint + "queue/status/", {
|
||||
hash: hash
|
||||
});
|
||||
var status_obj = await status_response.json();
|
||||
status = status_obj["status"];
|
||||
if (status === "QUEUED") {
|
||||
queue_callback(status_obj["data"]);
|
||||
} else if (status === "PENDING") {
|
||||
queue_callback(null);
|
||||
}
|
||||
}
|
||||
if (status == "FAILED") {
|
||||
throw new Error(status);
|
||||
} else {
|
||||
return status_obj["data"];
|
||||
}
|
||||
} else {
|
||||
const output = await postData(api_endpoint + action + "/", data);
|
||||
if (output.status !== 200) {
|
||||
throw new Error(output.statusText);
|
||||
}
|
||||
return await output.json();
|
||||
}
|
||||
};
|
@ -1,229 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { AudioInput, AudioInputExample } from "./components/input/audio";
|
||||
import {
|
||||
CheckboxGroupInput,
|
||||
CheckboxGroupInputExample
|
||||
} from "./components/input/checkbox_group";
|
||||
import {
|
||||
CheckboxInput,
|
||||
CheckboxInputExample
|
||||
} from "./components/input/checkbox";
|
||||
import {
|
||||
DataframeInput,
|
||||
DataframeInputExample
|
||||
} from "./components/input/dataframe";
|
||||
import {
|
||||
DropdownInput,
|
||||
DropdownInputExample
|
||||
} from "./components/input/dropdown";
|
||||
import { FileInput, FileInputExample } from "./components/input/file";
|
||||
import { ImageInput, ImageInputExample } from "./components/input/image";
|
||||
import { NumberInput, NumberInputExample } from "./components/input/number";
|
||||
import { RadioInput, RadioInputExample } from "./components/input/radio";
|
||||
import { SliderInput, SliderInputExample } from "./components/input/slider";
|
||||
import { TextboxInput, TextboxInputExample } from "./components/input/textbox";
|
||||
import {
|
||||
TimeseriesInput,
|
||||
TimeseriesInputExample
|
||||
} from "./components/input/timeseries";
|
||||
import { VideoInput, VideoInputExample } from "./components/input/video";
|
||||
|
||||
import { AudioOutput, AudioOutputExample } from "./components/output/audio";
|
||||
import {
|
||||
CarouselOutput,
|
||||
CarouselOutputExample
|
||||
} from "./components/output/carousel";
|
||||
import {
|
||||
DataframeOutput,
|
||||
DataframeOutputExample
|
||||
} from "./components/output/dataframe";
|
||||
import { FileOutput, FileOutputExample } from "./components/output/file";
|
||||
import {
|
||||
HighlightedTextOutput,
|
||||
HighlightedTextOutputExample
|
||||
} from "./components/output/highlighted_text";
|
||||
import { HTMLOutput, HTMLOutputExample } from "./components/output/html";
|
||||
import { ImageOutput, ImageOutputExample } from "./components/output/image";
|
||||
import { JSONOutput, JSONOutputExample } from "./components/output/json";
|
||||
import {
|
||||
KeyValuesOutput,
|
||||
KeyValuesOutputExample
|
||||
} from "./components/output/key_values";
|
||||
import { LabelOutput, LabelOutputExample } from "./components/output/label";
|
||||
import {
|
||||
TextboxOutput,
|
||||
TextboxOutputExample
|
||||
} from "./components/output/textbox";
|
||||
import {
|
||||
TimeseriesOutput,
|
||||
TimeseriesOutputExample
|
||||
} from "./components/output/timeseries";
|
||||
import { VideoOutput, VideoOutputExample } from "./components/output/video";
|
||||
|
||||
let input_component_set = [
|
||||
{
|
||||
name: "audio",
|
||||
component: AudioInput,
|
||||
memoized_component: null,
|
||||
example_component: AudioInputExample
|
||||
},
|
||||
{
|
||||
name: "checkboxgroup",
|
||||
component: CheckboxGroupInput,
|
||||
memoized_component: null,
|
||||
example_component: CheckboxGroupInputExample
|
||||
},
|
||||
{
|
||||
name: "checkbox",
|
||||
component: CheckboxInput,
|
||||
memoized_component: null,
|
||||
example_component: CheckboxInputExample
|
||||
},
|
||||
{
|
||||
name: "dataframe",
|
||||
component: DataframeInput,
|
||||
memoized_component: null,
|
||||
example_component: DataframeInputExample
|
||||
},
|
||||
{
|
||||
name: "dropdown",
|
||||
component: DropdownInput,
|
||||
memoized_component: null,
|
||||
example_component: DropdownInputExample
|
||||
},
|
||||
{
|
||||
name: "file",
|
||||
component: FileInput,
|
||||
memoized_component: null,
|
||||
example_component: FileInputExample
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
component: ImageInput,
|
||||
memoized_component: null,
|
||||
example_component: ImageInputExample
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
component: NumberInput,
|
||||
memoized_component: null,
|
||||
example_component: NumberInputExample
|
||||
},
|
||||
{
|
||||
name: "radio",
|
||||
component: RadioInput,
|
||||
memoized_component: null,
|
||||
example_component: RadioInputExample
|
||||
},
|
||||
{
|
||||
name: "slider",
|
||||
component: SliderInput,
|
||||
memoized_component: null,
|
||||
example_component: SliderInputExample
|
||||
},
|
||||
{
|
||||
name: "textbox",
|
||||
component: TextboxInput,
|
||||
memoized_component: null,
|
||||
example_component: TextboxInputExample
|
||||
},
|
||||
{
|
||||
name: "timeseries",
|
||||
component: TimeseriesInput,
|
||||
memoized_component: null,
|
||||
example_component: TimeseriesInputExample
|
||||
},
|
||||
{ name: "video", component: VideoInput, example_component: VideoInputExample }
|
||||
];
|
||||
let output_component_set = [
|
||||
{
|
||||
name: "audio",
|
||||
component: AudioOutput,
|
||||
memoized_component: null,
|
||||
example_component: AudioOutputExample
|
||||
},
|
||||
{
|
||||
name: "carousel",
|
||||
component: CarouselOutput,
|
||||
memoized_component: null,
|
||||
example_component: CarouselOutputExample
|
||||
},
|
||||
{
|
||||
name: "dataframe",
|
||||
component: DataframeOutput,
|
||||
memoized_component: null,
|
||||
example_component: DataframeOutputExample
|
||||
},
|
||||
{
|
||||
name: "file",
|
||||
component: FileOutput,
|
||||
memoized_component: null,
|
||||
example_component: FileOutputExample
|
||||
},
|
||||
{
|
||||
name: "highlightedtext",
|
||||
component: HighlightedTextOutput,
|
||||
memoized_component: null,
|
||||
example_component: HighlightedTextOutputExample
|
||||
},
|
||||
{
|
||||
name: "html",
|
||||
component: HTMLOutput,
|
||||
memoized_component: null,
|
||||
example_component: HTMLOutputExample
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
component: ImageOutput,
|
||||
memoized_component: null,
|
||||
example_component: ImageOutputExample
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
component: JSONOutput,
|
||||
memoized_component: null,
|
||||
example_component: JSONOutputExample
|
||||
},
|
||||
{
|
||||
name: "keyvalues",
|
||||
component: KeyValuesOutput,
|
||||
memoized_component: null,
|
||||
example_component: KeyValuesOutputExample
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
component: LabelOutput,
|
||||
memoized_component: null,
|
||||
example_component: LabelOutputExample
|
||||
},
|
||||
{
|
||||
name: "textbox",
|
||||
component: TextboxOutput,
|
||||
memoized_component: null,
|
||||
example_component: TextboxOutputExample
|
||||
},
|
||||
{
|
||||
name: "timeseries",
|
||||
component: TimeseriesOutput,
|
||||
memoized_component: null,
|
||||
example_component: TimeseriesOutputExample
|
||||
},
|
||||
{
|
||||
name: "video",
|
||||
component: VideoOutput,
|
||||
memoized_component: null,
|
||||
example_component: VideoOutputExample
|
||||
}
|
||||
];
|
||||
|
||||
for (let component_set of [input_component_set, output_component_set]) {
|
||||
for (let component_data of component_set) {
|
||||
component_data.memoized_component = React.memo(
|
||||
component_data.component,
|
||||
component_data.component.memo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { input_component_set, output_component_set };
|
9
frontend/src/components/Dummy.svelte
Normal file
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
export let value, setValue, theme;
|
||||
export let choices;
|
||||
</script>
|
||||
|
||||
<div class="dummy" {theme}>DUMMY</div>
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default class BaseComponent extends React.Component {
|
||||
static memo = (a, b) => {
|
||||
return a.value === b.value && a.interpretation === b.interpretation;
|
||||
};
|
||||
static postprocess = (y) => {
|
||||
return y;
|
||||
};
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "./base_component";
|
||||
|
||||
export default class ComponentExample extends React.Component {
|
||||
render() {
|
||||
return <div>{this.props.value}</div>;
|
||||
}
|
||||
static async preprocess(x, examples_dir, component_config) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
export class FileComponentExample extends ComponentExample {
|
||||
static async preprocess(x, examples_dir, component_config) {
|
||||
return {
|
||||
name: x,
|
||||
data: examples_dir + "/" + x,
|
||||
is_example: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class DataURLComponentExample extends ComponentExample {
|
||||
static async preprocess(x, examples_dir, component_config) {
|
||||
let file_url = examples_dir + "/" + x;
|
||||
let response = await fetch(file_url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
let blob = await response.blob();
|
||||
return new Promise((resolve, reject) => {
|
||||
var reader = new FileReader();
|
||||
reader.addEventListener(
|
||||
"load",
|
||||
function () {
|
||||
resolve(reader.result);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
reader.onerror = () => {
|
||||
return reject(this);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
}
|
57
frontend/src/components/directory.js
Normal file
@ -0,0 +1,57 @@
|
||||
import InputAudio from "./input/Audio/config.js";
|
||||
import InputCheckbox from "./input/Checkbox/config.js";
|
||||
import InputCheckboxGroup from "./input/CheckboxGroup/config.js";
|
||||
import InputDropdown from "./input/Dropdown/config.js";
|
||||
import InputFile from "./input/File/config.js";
|
||||
import InputImage from "./input/Image/config.js";
|
||||
import InputNumber from "./input/Number/config.js";
|
||||
import InputRadio from "./input/Radio/config.js";
|
||||
import InputSlider from "./input/Slider/config.js";
|
||||
import InputTextbox from "./input/Textbox/config.js";
|
||||
import InputVideo from "./input/Video/config.js";
|
||||
import InputDataFrame from "./input/DataFrame/config.js";
|
||||
|
||||
import OutputAudio from "./output/Audio/config.js";
|
||||
import OutputCarousel from "./output/Carousel/config.js";
|
||||
import OutputDataframe from "./output/Dataframe/config.js";
|
||||
import OutputFile from "./output/File/config.js";
|
||||
import OutputHighlightedText from "./output/HighlightedText/config.js";
|
||||
import OutputHtml from "./output/Html/config.js";
|
||||
import OutputImage from "./output/Image/config.js";
|
||||
import OutputJson from "./output/Json/config.js";
|
||||
import OutputLabel from "./output/Label/config.js";
|
||||
import OutputTextbox from "./output/Textbox/config.js";
|
||||
import OutputVideo from "./output/Video/config.js";
|
||||
|
||||
import Dummy from "./Dummy.svelte"
|
||||
|
||||
export const input_component_map = {
|
||||
"audio": InputAudio,
|
||||
"checkbox": InputCheckbox,
|
||||
"checkboxgroup": InputCheckboxGroup,
|
||||
"dataframe": InputDataFrame,
|
||||
"dropdown": InputDropdown,
|
||||
"file": InputFile,
|
||||
"image": InputImage,
|
||||
"number": InputNumber,
|
||||
"radio": InputRadio,
|
||||
"slider": InputSlider,
|
||||
"textbox": InputTextbox,
|
||||
"timeseries": {"component": Dummy, "example": Dummy},
|
||||
"video": InputVideo,
|
||||
}
|
||||
|
||||
export const output_component_map = {
|
||||
"audio": OutputAudio,
|
||||
"carousel": OutputCarousel,
|
||||
"dataframe": OutputDataframe,
|
||||
"file": OutputFile,
|
||||
"highlightedtext": OutputHighlightedText,
|
||||
"html": OutputHtml,
|
||||
"image": OutputImage,
|
||||
"json": OutputJson,
|
||||
"label": OutputLabel,
|
||||
"textbox": OutputTextbox,
|
||||
"timeseries": {"component": Dummy, "example": Dummy},
|
||||
"video": OutputVideo,
|
||||
}
|
162
frontend/src/components/input/Audio/Component.svelte
Normal file
@ -0,0 +1,162 @@
|
||||
<script>
|
||||
import { onDestroy } from "svelte";
|
||||
import Upload from "../../utils/Upload.svelte";
|
||||
import ModifyUpload from "../../utils/ModifyUpload.svelte";
|
||||
import Range from "svelte-range-slider-pips";
|
||||
|
||||
export let value,
|
||||
setValue,
|
||||
theme,
|
||||
name,
|
||||
is_example = false;
|
||||
export let source;
|
||||
|
||||
let recording = false;
|
||||
let recorder;
|
||||
let mode = "";
|
||||
let audio_chunks = [];
|
||||
let audio_blob;
|
||||
let player;
|
||||
let inited = false;
|
||||
let crop_values = [0, 100];
|
||||
|
||||
function blob_to_data_url(blob) {
|
||||
return new Promise((fulfill, reject) => {
|
||||
let reader = new FileReader();
|
||||
reader.onerror = reject;
|
||||
reader.onload = (e) => fulfill(reader.result);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
async function prepare_audio() {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
recorder = new MediaRecorder(stream);
|
||||
|
||||
recorder.addEventListener("dataavailable", (event) => {
|
||||
audio_chunks.push(event.data);
|
||||
});
|
||||
|
||||
recorder.addEventListener("stop", async () => {
|
||||
recording = false;
|
||||
audio_blob = new Blob(audio_chunks, { type: "audio/wav" });
|
||||
|
||||
setValue({
|
||||
data: await blob_to_data_url(audio_blob),
|
||||
name,
|
||||
is_example,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function record() {
|
||||
recording = true;
|
||||
audio_chunks = [];
|
||||
|
||||
if (!inited) await prepare_audio();
|
||||
|
||||
recorder.start();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (recorder) {
|
||||
console.log(recorder);
|
||||
recorder.stop();
|
||||
}
|
||||
});
|
||||
|
||||
const stop = () => {
|
||||
recorder.stop();
|
||||
};
|
||||
|
||||
function clear() {
|
||||
setValue(null);
|
||||
mode = "";
|
||||
}
|
||||
|
||||
function loaded(node) {
|
||||
function clamp_playback() {
|
||||
const start_time = (crop_values[0] / 100) * node.duration;
|
||||
const end_time = (crop_values[1] / 100) * node.duration;
|
||||
if (node.currentTime < start_time) {
|
||||
node.currentTime = start_time;
|
||||
}
|
||||
|
||||
if (node.currentTime > end_time) {
|
||||
node.currentTime = start_time;
|
||||
node.pause();
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener("timeupdate", clamp_playback);
|
||||
|
||||
return {
|
||||
destroy: () => node.removeEventListener("timeupdate", clamp_playback),
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="input-audio">
|
||||
{#if value === null}
|
||||
{#if source === "microphone"}
|
||||
{#if recording}
|
||||
<button
|
||||
class="p-2 rounded font-semibold bg-red-200 text-red-500 dark:bg-red-600 dark:text-red-100 shadow transition hover:shadow-md"
|
||||
on:click={stop}
|
||||
>
|
||||
Stop Recording
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="p-2 rounded font-semibold bg-white dark:bg-gray-600 shadow transition hover:shadow-md bg-white dark:bg-gray-800"
|
||||
on:click={record}
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
{/if}
|
||||
{:else if source === "upload"}
|
||||
<Upload filetype="audio/*" load={setValue} {theme}>
|
||||
Drop Audio Here
|
||||
<br />- or -<br />
|
||||
Click to Upload
|
||||
</Upload>
|
||||
{/if}
|
||||
{:else}
|
||||
<ModifyUpload
|
||||
{clear}
|
||||
edit={() => (mode = "edit")}
|
||||
absolute={false}
|
||||
{theme}
|
||||
/>
|
||||
|
||||
<audio
|
||||
use:loaded
|
||||
class="w-full"
|
||||
controls
|
||||
bind:this={player}
|
||||
preload="metadata"
|
||||
src={value.data}
|
||||
/>
|
||||
|
||||
{#if mode === "edit" && player?.duration}
|
||||
<Range
|
||||
bind:values={crop_values}
|
||||
range
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
on:change={({ detail: { values } }) =>
|
||||
setValue({
|
||||
data: value.data,
|
||||
name,
|
||||
is_example,
|
||||
crop_min: values[0],
|
||||
crop_max: values[1],
|
||||
})}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
5
frontend/src/components/input/Audio/Example.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<div class="input-audio-example">{value}</div>
|
18
frontend/src/components/input/Audio/Interpretation.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import { getSaliencyColor } from "../../utils/helpers";
|
||||
export let value, interpretation, theme;
|
||||
</script>
|
||||
|
||||
<div class="input-audio" {theme}>
|
||||
<audio class="w-full" controls>
|
||||
<source src={value.data} />
|
||||
</audio>
|
||||
<div class="interpret_range flex">
|
||||
{#each interpretation as interpret_value}
|
||||
<div class="flex-1 h-4" style={"background-color: " + getSaliencyColor(interpret_value)} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
11
frontend/src/components/input/Audio/config.js
Normal file
@ -0,0 +1,11 @@
|
||||
import Component from "./Component.svelte";
|
||||
import ExampleComponent from "./Example.svelte";
|
||||
import Interpretation from "./Interpretation.svelte";
|
||||
import { loadAsFile } from "../../utils/example_processors";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
"example": ExampleComponent,
|
||||
"interpretation": Interpretation,
|
||||
"process_example": loadAsFile
|
||||
}
|
54
frontend/src/components/input/Checkbox/Component.svelte
Normal file
@ -0,0 +1,54 @@
|
||||
<script>
|
||||
export let value, setValue, theme;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="input-checkbox inline-block"
|
||||
{theme}
|
||||
on:click={() => setValue(!value)}
|
||||
>
|
||||
<button class="checkbox-item py-2 px-3 rounded cursor-pointer" class:selected={value}>
|
||||
<div class="checkbox w-4 h-4 bg-white flex items-center justify-center">
|
||||
<svg class="check opacity-0 h-3 w-4" viewBox="-10 -10 20 20">
|
||||
<line
|
||||
x1="-7.5"
|
||||
y1="0"
|
||||
x2="-2.5"
|
||||
y2="5"
|
||||
stroke="white"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="-2.5"
|
||||
y1="5"
|
||||
x2="7.5"
|
||||
y2="-7.5"
|
||||
stroke="white"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.selected .check {
|
||||
@apply opacity-100;
|
||||
}
|
||||
.input-checkbox[theme="default"] {
|
||||
.checkbox-item {
|
||||
@apply bg-white dark:bg-gray-800 shadow transition hover:shadow-md;
|
||||
}
|
||||
.checkbox {
|
||||
@apply bg-gray-100 dark:bg-gray-400 transition;
|
||||
}
|
||||
.checkbox-item.selected {
|
||||
@apply bg-yellow-500 dark:bg-red-600 text-white;
|
||||
}
|
||||
.selected .checkbox {
|
||||
@apply bg-yellow-600 dark:bg-red-700;
|
||||
}
|
||||
}
|
||||
</style>
|
5
frontend/src/components/input/Checkbox/Example.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<div class="input-checkbox-example">{value.toLocaleString()}</div>
|
56
frontend/src/components/input/Checkbox/Interpretation.svelte
Normal file
@ -0,0 +1,56 @@
|
||||
<script>
|
||||
import { getSaliencyColor } from "../../utils/helpers";
|
||||
|
||||
export let value, interpretation, theme;
|
||||
</script>
|
||||
|
||||
<div class="input-checkbox inline-block" {theme}>
|
||||
<button
|
||||
class="checkbox-item py-2 px-3 rounded cursor-pointer flex gap-1"
|
||||
class:selected={value}
|
||||
>
|
||||
<div
|
||||
class="checkbox w-4 h-4 bg-white flex items-center justify-center border border-gray-400 box-border"
|
||||
style={"background-color: " + getSaliencyColor(interpretation[0])}
|
||||
/>
|
||||
<div
|
||||
class="checkbox w-4 h-4 bg-white flex items-center justify-center border border-gray-400 box-border"
|
||||
style={"background-color: " + getSaliencyColor(interpretation[1])}
|
||||
>
|
||||
<svg class="check h-3 w-4" viewBox="-10 -10 20 20">
|
||||
<line
|
||||
x1="-7.5"
|
||||
y1="0"
|
||||
x2="-2.5"
|
||||
y2="5"
|
||||
stroke="black"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="-2.5"
|
||||
y1="5"
|
||||
x2="7.5"
|
||||
y2="-7.5"
|
||||
stroke="black"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.selected .check {
|
||||
@apply opacity-100;
|
||||
}
|
||||
.input-checkbox[theme="default"] {
|
||||
.checkbox-item {
|
||||
@apply bg-white dark:bg-gray-800 shadow transition hover:shadow-md;
|
||||
}
|
||||
.checkbox-item.selected {
|
||||
@apply bg-yellow-500 dark:bg-red-600 text-white;
|
||||
}
|
||||
}
|
||||
</style>
|
9
frontend/src/components/input/Checkbox/config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Component from "./Component.svelte";
|
||||
import ExampleComponent from "./Example.svelte";
|
||||
import Interpretation from "./Interpretation.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
"example": ExampleComponent,
|
||||
"interpretation": Interpretation,
|
||||
}
|
68
frontend/src/components/input/CheckboxGroup/Component.svelte
Normal file
@ -0,0 +1,68 @@
|
||||
<script>
|
||||
export let value, setValue, theme;
|
||||
export let choices;
|
||||
|
||||
const toggleChoice = (choice) => {
|
||||
if (value.includes(choice)) {
|
||||
value.splice(value.indexOf(choice), 1);
|
||||
} else {
|
||||
value.push(choice);
|
||||
}
|
||||
setValue(value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="input-checkbox-group flex flex-wrap gap-2" {theme}>
|
||||
{#each choices as choice, i}
|
||||
<button
|
||||
class="checkbox-item py-2 px-3 font-semibold rounded cursor-pointer flex items-center gap-2"
|
||||
class:selected={value.includes(choice)}
|
||||
key={i}
|
||||
on:click={() => toggleChoice(choice)}
|
||||
>
|
||||
<div class="checkbox w-4 h-4 bg-white flex items-center justify-center">
|
||||
<svg class="check opacity-0 h-3 w-4" viewBox="-10 -10 20 20">
|
||||
<line
|
||||
x1="-7.5"
|
||||
y1="0"
|
||||
x2="-2.5"
|
||||
y2="5"
|
||||
stroke="white"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="-2.5"
|
||||
y1="5"
|
||||
x2="7.5"
|
||||
y2="-7.5"
|
||||
stroke="white"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{choice}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.selected .check {
|
||||
@apply opacity-100;
|
||||
}
|
||||
.input-checkbox-group[theme="default"] {
|
||||
.checkbox-item {
|
||||
@apply bg-white dark:bg-gray-800 shadow transition hover:shadow-md;
|
||||
}
|
||||
.checkbox {
|
||||
@apply bg-gray-100 dark:bg-gray-400 transition;
|
||||
}
|
||||
.checkbox-item.selected {
|
||||
@apply bg-yellow-500 dark:bg-red-600 text-white;
|
||||
}
|
||||
.selected .checkbox {
|
||||
@apply bg-yellow-600 dark:bg-red-700;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<div class="input-checkboxgroup-example">{value.join(", ")}</div>
|
@ -0,0 +1,67 @@
|
||||
<script>
|
||||
import { getSaliencyColor } from "../../utils/helpers";
|
||||
|
||||
export let value, interpretation, theme;
|
||||
export let choices;
|
||||
</script>
|
||||
|
||||
<div class="input-checkbox-group flex flex-wrap gap-2" {theme}>
|
||||
{#each choices as choice, i}
|
||||
<button
|
||||
class="checkbox-item py-2 px-3 font-semibold rounded cursor-pointer flex items-center gap-1"
|
||||
class:selected={value.includes(choice)}
|
||||
key={i}
|
||||
>
|
||||
<div
|
||||
class="checkbox w-4 h-4 bg-white flex items-center justify-center border border-gray-400 box-border"
|
||||
style={"background-color: " + getSaliencyColor(interpretation[i][0])}
|
||||
/>
|
||||
<div
|
||||
class="checkbox w-4 h-4 bg-white flex items-center justify-center border border-gray-400 box-border"
|
||||
style={"background-color: " + getSaliencyColor(interpretation[i][1])}
|
||||
>
|
||||
<svg class="check h-3 w-4" viewBox="-10 -10 20 20">
|
||||
<line
|
||||
x1="-7.5"
|
||||
y1="0"
|
||||
x2="-2.5"
|
||||
y2="5"
|
||||
stroke="black"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="-2.5"
|
||||
y1="5"
|
||||
x2="7.5"
|
||||
y2="-7.5"
|
||||
stroke="black"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{choice}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.selected .check {
|
||||
@apply opacity-100;
|
||||
}
|
||||
.input-checkbox-group[theme="default"] {
|
||||
.checkbox-item {
|
||||
@apply bg-white dark:bg-gray-800 shadow transition hover:shadow-md;
|
||||
}
|
||||
.checkbox {
|
||||
@apply bg-gray-100 dark:bg-gray-400 transition;
|
||||
}
|
||||
.checkbox-item.selected {
|
||||
@apply bg-yellow-500 dark:bg-red-600 text-white;
|
||||
}
|
||||
.selected .checkbox {
|
||||
@apply bg-yellow-600 dark:bg-red-700;
|
||||
}
|
||||
}
|
||||
</style>
|
9
frontend/src/components/input/CheckboxGroup/config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Component from "./Component.svelte";
|
||||
import ExampleComponent from "./Example.svelte";
|
||||
import Interpretation from "./Interpretation.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
"example": ExampleComponent,
|
||||
"interpretation": Interpretation,
|
||||
}
|
362
frontend/src/components/input/DataFrame/Component.svelte
Normal file
@ -0,0 +1,362 @@
|
||||
<script>
|
||||
import { tick } from "svelte";
|
||||
|
||||
export let theme = "";
|
||||
export let label = "Title";
|
||||
export let headers = [];
|
||||
export let values = [
|
||||
["Frank", 32, "Male"],
|
||||
["Beatrice", 99, "Female"],
|
||||
["Simone", 999, "Male"],
|
||||
];
|
||||
export let setValue;
|
||||
export let editable = true;
|
||||
|
||||
let id = 0;
|
||||
let editing = false;
|
||||
let selected = false;
|
||||
let els = {};
|
||||
|
||||
function make_headers(_h) {
|
||||
if (_h.length === 0) {
|
||||
return values[0].map((_, i) => {
|
||||
const _id = ++id;
|
||||
els[_id] = { cell: null, input: null };
|
||||
return { id: _id, value: i + 1 };
|
||||
});
|
||||
} else {
|
||||
return _h.map((h) => {
|
||||
const _id = ++id;
|
||||
els[_id] = { cell: null, input: null };
|
||||
return { id: _id, value: h };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let _headers = make_headers(headers);
|
||||
|
||||
let data = values.map((x) =>
|
||||
x.map((n) => {
|
||||
const _id = ++id;
|
||||
els[id] = { input: null, cell: null };
|
||||
return { value: n, id: _id };
|
||||
})
|
||||
) || [
|
||||
Array(headers.length)
|
||||
.fill(0)
|
||||
|
||||
.map((_) => {
|
||||
const _id = ++id;
|
||||
els[id] = { input: null, cell: null };
|
||||
|
||||
return { value: "", id: _id };
|
||||
}),
|
||||
];
|
||||
|
||||
$: setValue(data.map((r) => r.map(({ value }) => value)));
|
||||
|
||||
function get_sort_status(name, sort) {
|
||||
if (!sort) return "none";
|
||||
if (sort[0] === name) {
|
||||
return sort[1];
|
||||
}
|
||||
}
|
||||
|
||||
async function start_edit(id) {
|
||||
if (!editable) return;
|
||||
editing = id;
|
||||
await tick();
|
||||
const { input } = els[id];
|
||||
input.focus();
|
||||
}
|
||||
|
||||
function handle_keydown(event, i, j, id) {
|
||||
let is_data;
|
||||
switch (event.key) {
|
||||
case "ArrowRight":
|
||||
if (editing) break;
|
||||
event.preventDefault();
|
||||
is_data = data[i][j + 1];
|
||||
selected = is_data ? is_data.id : selected;
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
if (editing) break;
|
||||
event.preventDefault();
|
||||
is_data = data[i][j - 1];
|
||||
selected = is_data ? is_data.id : selected;
|
||||
break;
|
||||
case "ArrowDown":
|
||||
if (editing) break;
|
||||
event.preventDefault();
|
||||
is_data = data[i + 1];
|
||||
selected = is_data ? is_data[j].id : selected;
|
||||
break;
|
||||
case "ArrowUp":
|
||||
if (editing) break;
|
||||
event.preventDefault();
|
||||
is_data = data[i - 1];
|
||||
selected = is_data ? is_data[j].id : selected;
|
||||
break;
|
||||
case "Escape":
|
||||
if (!editable) break;
|
||||
event.preventDefault();
|
||||
editing = false;
|
||||
break;
|
||||
case "Enter":
|
||||
if (!editable) break;
|
||||
event.preventDefault();
|
||||
if (editing === id) {
|
||||
editing = false;
|
||||
} else {
|
||||
editing = id;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function handle_cell_click(id) {
|
||||
editing = false;
|
||||
selected = id;
|
||||
}
|
||||
|
||||
async function set_focus(id, type) {
|
||||
if (type === "edit" && typeof id == "number") {
|
||||
await tick();
|
||||
els[id].input.focus();
|
||||
}
|
||||
|
||||
if (type === "edit" && typeof id == "boolean") {
|
||||
let cell = els[selected]?.cell;
|
||||
await tick();
|
||||
cell?.focus();
|
||||
}
|
||||
|
||||
if (type === "select" && typeof id == "number") {
|
||||
const { cell } = els[id];
|
||||
cell.setAttribute("tabindex", 0);
|
||||
await tick();
|
||||
els[id].cell.focus();
|
||||
}
|
||||
}
|
||||
|
||||
$: set_focus(editing, "edit");
|
||||
$: set_focus(selected, "select");
|
||||
|
||||
let sort_direction;
|
||||
let sort_by;
|
||||
|
||||
function sort(col, dir) {
|
||||
if (dir === "asc") {
|
||||
data = data.sort((a, b) => (a[col].value < b[col].value ? -1 : 1));
|
||||
} else if (dir === "des") {
|
||||
data = data.sort((a, b) => (a[col].value > b[col].value ? -1 : 1));
|
||||
}
|
||||
}
|
||||
|
||||
function handle_sort(col) {
|
||||
if (typeof sort_by !== "number" || sort_by !== col) {
|
||||
sort_direction = "asc";
|
||||
sort_by = col;
|
||||
} else {
|
||||
if (sort_direction === "asc") {
|
||||
sort_direction = "des";
|
||||
} else if (sort_direction === "des") {
|
||||
sort_direction = "asc";
|
||||
}
|
||||
}
|
||||
|
||||
sort(col, sort_direction);
|
||||
}
|
||||
|
||||
let header_edit;
|
||||
|
||||
async function edit_header(_id, select) {
|
||||
if (!editable) return;
|
||||
header_edit = _id;
|
||||
await tick();
|
||||
els[_id].input.focus();
|
||||
|
||||
if (select) els[_id].input.select();
|
||||
}
|
||||
|
||||
function end_header_edit(event) {
|
||||
if (!editable) return;
|
||||
|
||||
switch (event.key) {
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
header_edit = false;
|
||||
break;
|
||||
case "Enter":
|
||||
event.preventDefault();
|
||||
header_edit = false;
|
||||
}
|
||||
}
|
||||
|
||||
function add_row() {
|
||||
data.push(
|
||||
headers.map(() => {
|
||||
const _id = ++id;
|
||||
els[_id] = { cell: null, input: null };
|
||||
return { id: _id, value: "" };
|
||||
})
|
||||
);
|
||||
data = data;
|
||||
}
|
||||
|
||||
async function add_col() {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const _id = ++id;
|
||||
els[_id] = { cell: null, input: null };
|
||||
data[i].push({ id: _id, value: "" });
|
||||
}
|
||||
|
||||
const _id = ++id;
|
||||
els[_id] = { cell: null, input: null };
|
||||
_headers.push({ id: _id, value: `Header ${_headers.length + 1}` });
|
||||
|
||||
data = data;
|
||||
_headers = _headers;
|
||||
|
||||
await tick();
|
||||
|
||||
edit_header(_id, true);
|
||||
}
|
||||
|
||||
const double_click = (node, { click, dblclick }) => {
|
||||
let timer;
|
||||
|
||||
function handler(event) {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = undefined;
|
||||
dblclick(event);
|
||||
} else {
|
||||
timer = setTimeout(() => {
|
||||
click(event);
|
||||
timer = undefined;
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener("click", handler);
|
||||
|
||||
return {
|
||||
destroy: () => node.removeEventListener("click", handler),
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<h4 id="title">{label}</h4>
|
||||
|
||||
<div class="shadow overflow-hidden border-gray-200 rounded-sm relative">
|
||||
<table
|
||||
id="grid"
|
||||
role="grid"
|
||||
aria-labelledby="title"
|
||||
class="min-w-full divide-y divide-gray-200 "
|
||||
>
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
{#each _headers as { value, id }, i (id)}
|
||||
<th
|
||||
use:double_click={{
|
||||
click: () => handle_sort(i),
|
||||
dblclick: () => edit_header(id),
|
||||
}}
|
||||
aria-sort={get_sort_status(value, sort_by)}
|
||||
class="relative after:absolute after:opacity-0 after:content-['▲'] after:ml-2 after:inset-y-0 after:h-[1.05rem] after:m-auto relative px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
class:sorted={sort_by === i}
|
||||
class:des={sort_by === i && sort_direction === "des"}
|
||||
>
|
||||
{#if header_edit === id}
|
||||
<input
|
||||
class="bg-transparent inset-y-0 left-6 w-full outline-none absolute p-0 w-3/4 text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
tabindex="-1"
|
||||
bind:value
|
||||
bind:this={els[id].input}
|
||||
on:keydown={end_header_edit}
|
||||
on:blur={({ currentTarget }) =>
|
||||
currentTarget.setAttribute("tabindex", -1)}
|
||||
/>
|
||||
{/if}
|
||||
<span
|
||||
tabindex="-1"
|
||||
role="button"
|
||||
class="min-h-full"
|
||||
class:opacity-0={header_edit === id}>{value}</span
|
||||
>
|
||||
</th>
|
||||
{/each}
|
||||
</tr></thead
|
||||
><tbody class="bg-white divide-y divide-gray-200">
|
||||
{#each data as row, i (row)}
|
||||
<tr>
|
||||
{#each row as { value, id }, j (id)}
|
||||
<td
|
||||
tabindex="-1"
|
||||
class="p-0 whitespace-nowrap display-block outline-none relative "
|
||||
on:dblclick={() => start_edit(id)}
|
||||
on:click={() => handle_cell_click(id)}
|
||||
on:keydown={(e) => handle_keydown(e, i, j, id)}
|
||||
bind:this={els[id].cell}
|
||||
on:blur={({ currentTarget }) =>
|
||||
currentTarget.setAttribute("tabindex", -1)}
|
||||
>
|
||||
<div
|
||||
class:border-gray-600={selected === id}
|
||||
class:border-transparent={selected !== id}
|
||||
class="min-h-[3.3rem] px-5 py-3 border-[0.125rem]"
|
||||
>
|
||||
{#if editing === id}
|
||||
<input
|
||||
class="w-full outline-none absolute p-0 w-3/4"
|
||||
tabindex="-1"
|
||||
bind:value
|
||||
bind:this={els[id].input}
|
||||
on:blur={({ currentTarget }) =>
|
||||
currentTarget.setAttribute("tabindex", -1)}
|
||||
/>
|
||||
{/if}
|
||||
<span
|
||||
class=" cursor-default w-full"
|
||||
class:opacity-0={editing === id}
|
||||
tabindex="-1"
|
||||
role="button"
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{#if editable}
|
||||
<div class="flex justify-end ">
|
||||
<button
|
||||
on:click={add_col}
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-600 shadow py-1 px-3 rounded transition focus:outline-none m-2 mr-0"
|
||||
>New Column</button
|
||||
>
|
||||
<button
|
||||
on:click={add_row}
|
||||
class="bg-yellow-500 hover:bg-yellow-400 dark:bg-red-700 dark:hover:bg-red-600 text-white shadow py-1 px-3 rounded transition focus:outline-none m-2 mr-0"
|
||||
>New Row</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.sorted::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.des::after {
|
||||
transform: rotate(180deg) translateY(1.5px);
|
||||
}
|
||||
</style>
|
5
frontend/src/components/input/DataFrame/config.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Component from "./Component.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
}
|
46
frontend/src/components/input/Dropdown/Component.svelte
Normal file
@ -0,0 +1,46 @@
|
||||
<script>
|
||||
export let value, setValue, theme;
|
||||
export let choices;
|
||||
</script>
|
||||
|
||||
<div class="input-dropdown group inline-block relative" {theme}>
|
||||
<button
|
||||
class="selector py-2 px-3 font-semibold rounded inline-flex items-center"
|
||||
>
|
||||
{value}
|
||||
<svg class="caret ml-2 fill-current h-4 w-4" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
class="dropdown-menu-holder absolute hidden group-hover:block pt-1 z-10 bg-none"
|
||||
>
|
||||
<ul class="dropdown-menu max-h-80 overflow-y-auto">
|
||||
{#each choices as choice, i}
|
||||
<li
|
||||
class="dropdown-item first:rounded-t transition last:rounded-b py-2 px-3 block whitespace-nowrap cursor-pointer"
|
||||
on:click={() => setValue(choice)}
|
||||
key={i}
|
||||
>
|
||||
{choice}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss" global>
|
||||
.input-dropdown[theme="default"] {
|
||||
.selector {
|
||||
@apply bg-white dark:bg-gray-800 shadow transition hover:shadow-md;
|
||||
}
|
||||
.dropdown-menu {
|
||||
@apply shadow;
|
||||
}
|
||||
.dropdown-item {
|
||||
@apply bg-white dark:bg-gray-800 hover:bg-yellow-500 dark:hover:bg-red-600 hover:text-gray-50 hover:font-semibold;
|
||||
}
|
||||
}
|
||||
</style>
|
5
frontend/src/components/input/Dropdown/Example.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<div class="input-dropdown-example">{value}</div>
|
34
frontend/src/components/input/Dropdown/Interpretation.svelte
Normal file
@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import { getSaliencyColor } from "../../utils/helpers";
|
||||
|
||||
export let value, interpretation, theme;
|
||||
export let choices;
|
||||
</script>
|
||||
|
||||
<div class="input-dropdown" {theme}>
|
||||
<ul class="dropdown-menu">
|
||||
{#each choices as choice, i}
|
||||
<li
|
||||
class="dropdown-item first:rounded-t transition last:rounded-b py-2 px-3 block whitespace-nowrap cursor-pointer"
|
||||
style={"background-color: " + getSaliencyColor(interpretation[i])}
|
||||
key={i}
|
||||
>
|
||||
{choice}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style lang="postcss" global>
|
||||
.input-dropdown[theme="default"] {
|
||||
.selector {
|
||||
@apply bg-white dark:bg-gray-800 shadow transition hover:shadow-md;
|
||||
}
|
||||
.dropdown-menu {
|
||||
@apply shadow;
|
||||
}
|
||||
.dropdown-item {
|
||||
@apply bg-white dark:bg-gray-800 hover:font-semibold;
|
||||
}
|
||||
}
|
||||
</style>
|
9
frontend/src/components/input/Dropdown/config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Component from "./Component.svelte";
|
||||
import ExampleComponent from "./Example.svelte";
|
||||
import Interpretation from "./Interpretation.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
"example": ExampleComponent,
|
||||
"interpretation": Interpretation,
|
||||
}
|
35
frontend/src/components/input/File/Component.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
<script>
|
||||
import Upload from "../../utils/Upload.svelte";
|
||||
import ModifyUpload from "../../utils/ModifyUpload.svelte";
|
||||
import { prettyBytes } from "../../utils/helpers";
|
||||
|
||||
export let value, setValue, theme;
|
||||
</script>
|
||||
|
||||
<div class="input-file" {theme}>
|
||||
{#if value === null}
|
||||
<Upload load={setValue} {theme}>
|
||||
Drop File Here
|
||||
<br />- or -<br />
|
||||
Click to Upload
|
||||
</Upload>
|
||||
{:else}
|
||||
<div
|
||||
class="file-preview w-full flex flex-col justify-center items-center relative"
|
||||
>
|
||||
<ModifyUpload clear={() => setValue(null)} {theme} />
|
||||
<div class="file-name text-4xl p-6 break-all">{value.name}</div>
|
||||
{#if value.size}
|
||||
<div class="file-size text-2xl p-2">
|
||||
{prettyBytes(value.size)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.input-file[theme="default"] .file-preview {
|
||||
@apply h-60;
|
||||
}
|
||||
</style>
|
5
frontend/src/components/input/File/Example.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<div class="input-file-example">{value}</div>
|
9
frontend/src/components/input/File/config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Component from "./Component.svelte";
|
||||
import ExampleComponent from "./Example.svelte";
|
||||
import { loadAsFile } from "../../utils/example_processors";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
"example": ExampleComponent,
|
||||
"process_example": loadAsFile
|
||||
}
|
86
frontend/src/components/input/Image/Component.svelte
Normal file
@ -0,0 +1,86 @@
|
||||
<script>
|
||||
import Cropper from "../../utils/Cropper.svelte";
|
||||
|
||||
import Upload from "../../utils/Upload.svelte";
|
||||
import ModifyUpload from "../../utils/ModifyUpload.svelte";
|
||||
import ModifySketch from "../../utils/ModifySketch.svelte";
|
||||
import ImageEditor from "../../utils/ImageEditor.svelte";
|
||||
import Sketch from "../../utils/Sketch.svelte";
|
||||
import Webcam from "../../utils/Webcam.svelte";
|
||||
export let value, setValue, theme;
|
||||
export let source = "upload";
|
||||
export let tool = "editor";
|
||||
|
||||
let mode;
|
||||
let sketch;
|
||||
|
||||
function handle_save({ detail }) {
|
||||
setValue(detail);
|
||||
mode = "view";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="input-image">
|
||||
<div
|
||||
class="image-preview w-full h-80 flex justify-center items-center dark:bg-gray-600 relative"
|
||||
class:bg-gray-200={value}
|
||||
class:h-80={source !== "webcam"}
|
||||
>
|
||||
{#if source === "canvas"}
|
||||
<ModifySketch
|
||||
on:undo={() => sketch.undo()}
|
||||
on:clear={() => sketch.clear()}
|
||||
/>
|
||||
<Sketch bind:this={sketch} on:change={({ detail }) => setValue(detail)} />
|
||||
{:else if value === null}
|
||||
{#if source === "upload"}
|
||||
<Upload
|
||||
filetype="image/x-png,image/gif,image/jpeg"
|
||||
load={setValue}
|
||||
include_file_metadata={false}
|
||||
{theme}
|
||||
>
|
||||
Drop Image Here
|
||||
<br />- or -<br />
|
||||
Click to Upload
|
||||
</Upload>
|
||||
{:else if source === "webcam"}
|
||||
<Webcam on:capture={({ detail }) => setValue(detail)} />
|
||||
{/if}
|
||||
{:else if tool === "select"}
|
||||
<Cropper image={value} on:crop={({ detail }) => setValue(detail)} />
|
||||
{:else if tool === "editor"}
|
||||
{#if mode === "edit"}
|
||||
<ImageEditor
|
||||
{value}
|
||||
on:cancel={() => (mode = "view")}
|
||||
on:save={handle_save}
|
||||
/>
|
||||
{/if}
|
||||
<ModifyUpload
|
||||
edit={() => (mode = "edit")}
|
||||
clear={() => setValue(null)}
|
||||
{theme}
|
||||
/>
|
||||
|
||||
<img class="w-full h-full object-contain" src={value} alt="" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
:global(.image_editor_buttons) {
|
||||
width: 800px;
|
||||
@apply flex justify-end gap-1;
|
||||
}
|
||||
:global(.image_editor_buttons button) {
|
||||
@apply px-2 py-1 text-xl bg-black text-white font-semibold rounded-t;
|
||||
}
|
||||
:global(.tui-image-editor-header-buttons) {
|
||||
@apply hidden;
|
||||
}
|
||||
:global(.tui-colorpicker-palette-button) {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
</style>
|
6
frontend/src/components/input/Image/Example.svelte
Normal file
@ -0,0 +1,6 @@
|
||||
<script>
|
||||
export let value, examples_dir;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<img class="input-image-example h-24 max-w-none" src={examples_dir + value} />
|
72
frontend/src/components/input/Image/Interpretation.svelte
Normal file
@ -0,0 +1,72 @@
|
||||
<script>
|
||||
import ModifyUpload from "../../utils/ModifyUpload.svelte";
|
||||
import { getObjectFitSize, getSaliencyColor } from "../../utils/helpers";
|
||||
import { afterUpdate } from "svelte";
|
||||
|
||||
export let value, setValue, interpretation, shape, theme;
|
||||
|
||||
let saliency_layer;
|
||||
let image;
|
||||
|
||||
const paintSaliency = (data, ctx, width, height) => {
|
||||
var cell_width = width / data[0].length;
|
||||
var cell_height = height / data.length;
|
||||
var r = 0;
|
||||
data.forEach(function (row) {
|
||||
var c = 0;
|
||||
row.forEach(function (cell) {
|
||||
ctx.fillStyle = getSaliencyColor(cell);
|
||||
ctx.fillRect(c * cell_width, r * cell_height, cell_width, cell_height);
|
||||
c++;
|
||||
});
|
||||
r++;
|
||||
});
|
||||
};
|
||||
|
||||
afterUpdate(() => {
|
||||
let size = getObjectFitSize(
|
||||
true,
|
||||
image.width,
|
||||
image.height,
|
||||
image.naturalWidth,
|
||||
image.naturalHeight
|
||||
);
|
||||
if (shape) {
|
||||
size = getObjectFitSize(
|
||||
true,
|
||||
size.width,
|
||||
size.height,
|
||||
shape[0],
|
||||
shape[1]
|
||||
);
|
||||
}
|
||||
let width = size.width;
|
||||
let height = size.height;
|
||||
saliency_layer.setAttribute("height", height);
|
||||
saliency_layer.setAttribute("width", width);
|
||||
paintSaliency(
|
||||
interpretation,
|
||||
saliency_layer.getContext("2d"),
|
||||
width,
|
||||
height
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="input-image">
|
||||
<div
|
||||
class="image-preview w-full h-60 flex justify-center items-center bg-gray-200 dark:bg-gray-600 relative"
|
||||
>
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<div
|
||||
class="interpretation w-full h-full absolute top-0 left-0 flex justify-center items-center opacity-90 hover:opacity-20 transition"
|
||||
>
|
||||
<canvas bind:this={saliency_layer} />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<img class="w-full h-full object-contain" bind:this={image} src={value} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
11
frontend/src/components/input/Image/config.js
Normal file
@ -0,0 +1,11 @@
|
||||
import Component from "./Component.svelte";
|
||||
import ExampleComponent from "./Example.svelte";
|
||||
import Interpretation from "./Interpretation.svelte";
|
||||
import { loadAsData } from "../../utils/example_processors";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
"example": ExampleComponent,
|
||||
"interpretation": Interpretation,
|
||||
"process_example": loadAsData
|
||||
}
|
25
frontend/src/components/input/Number/Component.svelte
Normal file
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
export let value, setValue, theme;
|
||||
</script>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
class="input-number w-full rounded box-border p-2 focus:outline-none appearance-none"
|
||||
{value}
|
||||
on:change={(e) => setValue(parseFloat(e.target.value))}
|
||||
{theme}
|
||||
/>
|
||||
|
||||
<style lang="postcss" global>
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
.input-number[theme="default"] {
|
||||
@apply shadow transition hover:shadow-md dark:bg-gray-800;
|
||||
}
|
||||
</style>
|
5
frontend/src/components/input/Number/Example.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<div class="input-number-example">{value}</div>
|
32
frontend/src/components/input/Number/Interpretation.svelte
Normal file
@ -0,0 +1,32 @@
|
||||
<script>
|
||||
import { getSaliencyColor } from "../../utils/helpers";
|
||||
|
||||
export let value, interpretation, theme;
|
||||
</script>
|
||||
|
||||
<div class="input-number">
|
||||
<div class="interpret_range flex">
|
||||
{#each interpretation as interpret_value}
|
||||
<div
|
||||
class="flex-1"
|
||||
style={"background-color: " + getSaliencyColor(interpret_value[1])}
|
||||
>
|
||||
{interpret_value[0]}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss" global>
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
.input-number[theme="default"] {
|
||||
@apply shadow transition hover:shadow-md dark:bg-gray-800;
|
||||
}
|
||||
</style>
|
9
frontend/src/components/input/Number/config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Component from "./Component.svelte";
|
||||
import ExampleComponent from "./Example.svelte";
|
||||
import Interpretation from "./Interpretation.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
"example": ExampleComponent,
|
||||
"interpretation": Interpretation,
|
||||
}
|
38
frontend/src/components/input/Radio/Component.svelte
Normal file
@ -0,0 +1,38 @@
|
||||
<script>
|
||||
export let value, setValue, theme;
|
||||
export let choices;
|
||||
</script>
|
||||
|
||||
<div class="input-radio flex flex-wrap gap-2" {theme}>
|
||||
{#each choices as choice, i}
|
||||
<button
|
||||
class="radio-item py-2 px-3 font-semibold rounded cursor-pointer flex items-center gap-2"
|
||||
class:selected={value === choice}
|
||||
key={i}
|
||||
on:click={() => setValue(choice)}
|
||||
>
|
||||
<div class="radio-circle w-4 h-4 rounded-full box-border" />
|
||||
{choice}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.input-radio[theme="default"] {
|
||||
.radio-item {
|
||||
@apply bg-white dark:bg-gray-800 shadow transition hover:shadow-md;
|
||||
}
|
||||
.radio-circle {
|
||||
@apply bg-gray-50 dark:bg-gray-400 border-4 border-gray-200 dark:border-gray-600;
|
||||
}
|
||||
.radio-item.selected {
|
||||
@apply bg-yellow-500 dark:bg-red-600 text-white shadow;
|
||||
}
|
||||
.radio-circle {
|
||||
@apply w-4 h-4 bg-white transition rounded-full box-border;
|
||||
}
|
||||
.selected .radio-circle {
|
||||
@apply border-yellow-600 dark:border-red-700;
|
||||
}
|
||||
}
|
||||
</style>
|
5
frontend/src/components/input/Radio/Example.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<div class="input-radio-example">{value}</div>
|
35
frontend/src/components/input/Radio/Interpretation.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
<script>
|
||||
import { getSaliencyColor } from "../../utils/helpers";
|
||||
|
||||
export let value, interpretation, theme;
|
||||
export let choices;
|
||||
</script>
|
||||
|
||||
<div class="input-radio flex flex-wrap gap-2" {theme}>
|
||||
{#each choices as choice, i}
|
||||
<button
|
||||
class="radio-item py-2 px-3 font-semibold rounded cursor-pointer flex items-center gap-2"
|
||||
class:selected={value === choice}
|
||||
key={i}
|
||||
>
|
||||
<div class="radio-circle w-4 h-4 rounded-full box-border"
|
||||
style={"background-color: " + getSaliencyColor(interpretation[i])}
|
||||
/>
|
||||
{choice}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.input-radio[theme="default"] {
|
||||
.radio-item {
|
||||
@apply bg-white dark:bg-gray-800 shadow transition hover:shadow-md;
|
||||
}
|
||||
.radio-circle {
|
||||
@apply w-4 h-4 rounded-full box-border;
|
||||
}
|
||||
.radio-item.selected {
|
||||
@apply bg-yellow-500 dark:bg-red-600 text-white shadow;
|
||||
}
|
||||
}
|
||||
</style>
|
9
frontend/src/components/input/Radio/config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Component from "./Component.svelte";
|
||||
import ExampleComponent from "./Example.svelte";
|
||||
import Interpretation from "./Interpretation.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
"example": ExampleComponent,
|
||||
"interpretation": Interpretation,
|
||||
}
|
42
frontend/src/components/input/Slider/Component.svelte
Normal file
@ -0,0 +1,42 @@
|
||||
<script>
|
||||
export let value, setValue, theme;
|
||||
export let minimum, maximum, step;
|
||||
</script>
|
||||
|
||||
<div class="input-slider text-center" {theme}>
|
||||
<input
|
||||
type="range"
|
||||
class="range w-full appearance-none transition rounded h-4"
|
||||
on:input={(e) => setValue(parseFloat(e.target.value))}
|
||||
{value}
|
||||
min={minimum}
|
||||
max={maximum}
|
||||
{step}
|
||||
/>
|
||||
<div class="value inline-block mx-auto mt-1 px-2 py-0.5 rounded">{value}</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.range::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
@apply appearance-none w-5 h-5 rounded cursor-pointer;
|
||||
}
|
||||
.range::-moz-range-thumb {
|
||||
@apply appearance-none w-5 h-5 rounded cursor-pointer;
|
||||
}
|
||||
|
||||
.input-slider[theme="default"] {
|
||||
.range {
|
||||
@apply bg-white dark:bg-gray-800 shadow h-3 transition hover:shadow-md;
|
||||
}
|
||||
.range::-webkit-slider-thumb {
|
||||
@apply bg-gradient-to-b from-yellow-400 to-yellow-500 dark:from-red-500 dark:to-red-600 shadow;
|
||||
}
|
||||
.range::-moz-range-thumb {
|
||||
@apply bg-gradient-to-b from-yellow-400 to-yellow-500 shadow;
|
||||
}
|
||||
.value {
|
||||
@apply bg-gray-100 dark:bg-gray-600 font-semibold;
|
||||
}
|
||||
}
|
||||
</style>
|
5
frontend/src/components/input/Slider/Example.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<div class="input-slider-example">{value}</div>
|
49
frontend/src/components/input/Slider/Interpretation.svelte
Normal file
@ -0,0 +1,49 @@
|
||||
<script>
|
||||
import { getSaliencyColor } from "../../utils/helpers";
|
||||
|
||||
export let value, interpretation, theme;
|
||||
export let minimum, maximum, step;
|
||||
</script>
|
||||
|
||||
<div class="input-slider text-center" {theme}>
|
||||
<input
|
||||
type="range"
|
||||
class="range w-full appearance-none transition rounded h-4"
|
||||
disabled
|
||||
{value}
|
||||
min={minimum}
|
||||
max={maximum}
|
||||
{step}
|
||||
/>
|
||||
<div class="interpret_range flex">
|
||||
{#each interpretation as interpret_value}
|
||||
<div class="flex-1 h-4" style={"background-color: " + getSaliencyColor(interpret_value)} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="value inline-block mx-auto mt-1 px-2 py-0.5 rounded">{value}</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.range::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
@apply appearance-none w-5 h-5 rounded cursor-pointer;
|
||||
}
|
||||
.range::-moz-range-thumb {
|
||||
@apply appearance-none w-5 h-5 rounded cursor-pointer;
|
||||
}
|
||||
|
||||
.input-slider[theme="default"] {
|
||||
.range {
|
||||
@apply bg-white dark:bg-gray-800 shadow h-3 transition hover:shadow-md;
|
||||
}
|
||||
.range::-webkit-slider-thumb {
|
||||
@apply bg-gradient-to-b from-yellow-400 to-yellow-500 dark:from-red-500 dark:to-red-600 shadow;
|
||||
}
|
||||
.range::-moz-range-thumb {
|
||||
@apply bg-gradient-to-b from-yellow-400 to-yellow-500 shadow;
|
||||
}
|
||||
.value {
|
||||
@apply bg-gray-100 dark:bg-gray-600 font-semibold;
|
||||
}
|
||||
}
|
||||
</style>
|
9
frontend/src/components/input/Slider/config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Component from "./Component.svelte";
|
||||
import ExampleComponent from "./Example.svelte";
|
||||
import Interpretation from "./Interpretation.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
"example": ExampleComponent,
|
||||
"interpretation": Interpretation,
|
||||
}
|
29
frontend/src/components/input/Textbox/Component.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script>
|
||||
export let value, setValue, theme;
|
||||
export let lines, placeholder;
|
||||
</script>
|
||||
|
||||
{#if lines > 1}
|
||||
<textarea
|
||||
class="input-text w-full rounded box-border p-2 focus:outline-none appearance-none"
|
||||
{value}
|
||||
{placeholder}
|
||||
on:change={(e) => setValue(e.target.value)}
|
||||
{theme}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
type="text"
|
||||
class="input-text w-full rounded box-border p-2 focus:outline-none appearance-none"
|
||||
{value}
|
||||
{placeholder}
|
||||
on:change={(e) => setValue(e.target.value)}
|
||||
{theme}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss" global>
|
||||
.input-text[theme="default"] {
|
||||
@apply shadow transition hover:shadow-md dark:bg-gray-800;
|
||||
}
|
||||
</style>
|
5
frontend/src/components/input/Textbox/Example.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<div class="input-text-example">{value}</div>
|
7
frontend/src/components/input/Textbox/config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Component from "./Component.svelte";
|
||||
import ExampleComponent from "./Example.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
"example": ExampleComponent,
|
||||
}
|
43
frontend/src/components/input/Video/Component.svelte
Normal file
@ -0,0 +1,43 @@
|
||||
<script>
|
||||
import Upload from "../../utils/Upload.svelte";
|
||||
import ModifyUpload from "../../utils/ModifyUpload.svelte";
|
||||
import { prettyBytes, playable } from "../../utils/helpers";
|
||||
|
||||
export let value, setValue, theme;
|
||||
export let source;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="video-preview w-full h-80 object-contain flex justify-center items-center dark:bg-gray-600 relative"
|
||||
class:bg-gray-200={value}
|
||||
>
|
||||
{#if value === null}
|
||||
{#if source === "upload"}
|
||||
<Upload filetype="video/mp4,video/x-m4v,video/*" load={setValue} {theme}>
|
||||
Drop Video Here
|
||||
<br />- or -<br />
|
||||
Click to Upload
|
||||
</Upload>
|
||||
{/if}
|
||||
{:else}
|
||||
<ModifyUpload clear={() => setValue(null)} {theme} />
|
||||
{#if playable(value.name)}
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video
|
||||
class="w-full h-full object-contain bg-black"
|
||||
controls
|
||||
playsInline
|
||||
preload
|
||||
src={value.data}
|
||||
/>
|
||||
{:else}
|
||||
<div class="file-name text-4xl p-6 break-all">{value.name}</div>
|
||||
<div class="file-size text-2xl p-2">
|
||||
{prettyBytes(value.size)}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
20
frontend/src/components/input/Video/Example.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { playable } from "../../utils/helpers";
|
||||
|
||||
export let value, examples_dir;
|
||||
let video;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
{#if playable(value)}
|
||||
<video
|
||||
bind:this={video}
|
||||
on:mouseover={video.play}
|
||||
on:mouseout={video.pause}
|
||||
class="input-video-example h-24 max-w-none"
|
||||
src={examples_dir + value}
|
||||
/>
|
||||
{:else}
|
||||
<div class="input-video-example">{value}</div>
|
||||
{/if}
|
9
frontend/src/components/input/Video/config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Component from "./Component.svelte";
|
||||
import ExampleComponent from "./Example.svelte";
|
||||
import { loadAsFile } from "../../utils/example_processors";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
"example": ExampleComponent,
|
||||
"process_example": loadAsFile
|
||||
}
|
@ -1,275 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import { FileComponentExample } from "../component_example";
|
||||
import Recorder from "recorder-js";
|
||||
import { getSaliencyColor } from "../../utils";
|
||||
import classNames from "classnames";
|
||||
import edit_icon from "../../static/img/edit.svg";
|
||||
import clear_icon from "../../static/img/clear.svg";
|
||||
import MultiRangeSlider from "./../../vendor/MultiRangeSlider/MultiRangeSlider";
|
||||
|
||||
class AudioInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
recording: false,
|
||||
editorMode: false
|
||||
};
|
||||
this.src = {};
|
||||
this.key = 0; // needed to prevent audio caching
|
||||
|
||||
this.uploader = React.createRef();
|
||||
this.audioRef = React.createRef();
|
||||
this.started = false;
|
||||
}
|
||||
static memo = (a, b) => {
|
||||
if (a.value instanceof Object && b.value instanceof Object) {
|
||||
return (
|
||||
a.value["name"] === b.value["name"] &&
|
||||
a.value["data"] === b.value["data"] &&
|
||||
a.value["crop_min"] === b.value["crop_min"] &&
|
||||
a.value["crop_max"] === b.value["crop_max"]
|
||||
);
|
||||
} else {
|
||||
return a === b;
|
||||
}
|
||||
};
|
||||
start = () => {
|
||||
if (!this.started) {
|
||||
const audioContext = new (window.AudioContext ||
|
||||
window.webkitAudioContext)();
|
||||
this.recorder = new Recorder(audioContext);
|
||||
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
|
||||
this.recorder.init(stream);
|
||||
this.recorder.start();
|
||||
});
|
||||
this.started = true;
|
||||
} else {
|
||||
this.recorder.start();
|
||||
}
|
||||
this.setState({
|
||||
recording: true
|
||||
});
|
||||
};
|
||||
stop = () => {
|
||||
this.recorder.stop().then(({ blob, buffer }) => {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
this.props.handleChange({
|
||||
name: "sample.wav",
|
||||
data: e.target.result,
|
||||
is_example: false
|
||||
});
|
||||
}.bind(this);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
this.setState({
|
||||
recording: false
|
||||
});
|
||||
};
|
||||
openFileUpload = () => {
|
||||
this.uploader.current.click();
|
||||
};
|
||||
toggleEditor = () => {
|
||||
let [crop_min, crop_max] = [0, 100];
|
||||
this.props.handleChange({
|
||||
name: this.props.value["name"],
|
||||
data: this.props.value["data"],
|
||||
is_example: this.props.value["is_example"],
|
||||
crop_min: crop_min,
|
||||
crop_max: crop_max
|
||||
});
|
||||
this.setState({ editorMode: !this.state.editorMode });
|
||||
};
|
||||
crop = (min, max, lastChange) => {
|
||||
if (this.state.duration) {
|
||||
if (lastChange === "min") {
|
||||
this.audioRef.current.currentTime = (min / 100) * this.state.duration;
|
||||
} else {
|
||||
this.audioRef.current.currentTime = (max / 100) * this.state.duration;
|
||||
}
|
||||
}
|
||||
this.props.handleChange({
|
||||
name: this.props.value["name"],
|
||||
data: this.props.value["data"],
|
||||
is_example: this.props.value["is_example"],
|
||||
crop_min: min,
|
||||
crop_max: max
|
||||
});
|
||||
};
|
||||
reset_playback_within_crop = () => {
|
||||
let position_ratio =
|
||||
this.audioRef.current.currentTime / this.state.duration;
|
||||
let min_ratio = this.props.value.crop_min / 100;
|
||||
let max_ratio = this.props.value.crop_max / 100;
|
||||
if (
|
||||
position_ratio > max_ratio - 0.00001 ||
|
||||
position_ratio < min_ratio - 0.00001
|
||||
) {
|
||||
this.audioRef.current.currentTime = this.state.duration * min_ratio;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
render() {
|
||||
if (this.props.value !== null) {
|
||||
if (
|
||||
this.props.value["name"] != this.src["name"] ||
|
||||
this.props.value["data"] !== this.src["data"]
|
||||
) {
|
||||
this.key += 1;
|
||||
this.src = {
|
||||
name: this.props.value["name"],
|
||||
data: this.props.value["data"]
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div className="input_audio">
|
||||
<div className="edit_buttons">
|
||||
<button
|
||||
className={classNames("edit_button", {
|
||||
active: this.state.editorMode
|
||||
})}
|
||||
onClick={this.toggleEditor}
|
||||
>
|
||||
<img src={edit_icon} />
|
||||
</button>
|
||||
<button
|
||||
className="clear_button"
|
||||
onClick={this.props.handleChange.bind(this, null)}
|
||||
>
|
||||
<img src={clear_icon} />
|
||||
</button>
|
||||
</div>
|
||||
<audio
|
||||
controls
|
||||
key={this.key}
|
||||
ref={this.audioRef}
|
||||
onLoadedMetadata={(e) =>
|
||||
this.setState({ duration: e.nativeEvent.target.duration })
|
||||
}
|
||||
onPlay={() => {
|
||||
this.reset_playback_within_crop();
|
||||
this.audioRef.current.play();
|
||||
}}
|
||||
onTimeUpdate={() => {
|
||||
if (this.audioRef.current.paused) {
|
||||
return;
|
||||
}
|
||||
let out_of_crop = this.reset_playback_within_crop();
|
||||
if (out_of_crop) {
|
||||
this.audioRef.current.pause();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<source src={this.props.value["data"]}></source>
|
||||
</audio>
|
||||
{this.props.interpretation === null ? (
|
||||
false
|
||||
) : (
|
||||
<div class="interpret_range">
|
||||
{this.props.interpretation.map((value) => (
|
||||
<div
|
||||
style={{ "background-color": getSaliencyColor(value) }}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{this.state.editorMode ? (
|
||||
<MultiRangeSlider
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={({ min, max, lastChange }) =>
|
||||
this.crop(min, max, lastChange)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
false
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (this.props.source === "microphone") {
|
||||
return (
|
||||
<div className="input_audio">
|
||||
{this.state.recording ? (
|
||||
<button className="stop" onClick={this.stop}>
|
||||
Recording...
|
||||
</button>
|
||||
) : (
|
||||
<button className="start" onClick={this.start}>
|
||||
Record
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.source === "upload") {
|
||||
let no_action = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="input_image"
|
||||
onDrag={no_action}
|
||||
onDragStart={no_action}
|
||||
onDragEnd={no_action}
|
||||
onDragOver={no_action}
|
||||
onDragEnter={no_action}
|
||||
onDragLeave={no_action}
|
||||
onDrop={no_action}
|
||||
>
|
||||
<div
|
||||
className="upload_zone"
|
||||
onClick={this.openFileUpload}
|
||||
onDrop={this.load_preview_from_drop}
|
||||
>
|
||||
Drop Audio Here
|
||||
<br />- or -<br />
|
||||
Click to Upload
|
||||
</div>
|
||||
<input
|
||||
className="hidden_upload"
|
||||
type="file"
|
||||
ref={this.uploader}
|
||||
onChange={this.load_preview_from_upload}
|
||||
accept="audio/*"
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
load_preview_from_drop = (evt) => {
|
||||
this.load_preview_from_files(evt.dataTransfer.files);
|
||||
};
|
||||
load_preview_from_upload = (evt) => {
|
||||
this.load_preview_from_files(evt.target.files);
|
||||
};
|
||||
load_preview_from_files = (files) => {
|
||||
if (!files.length || !window.FileReader) {
|
||||
return;
|
||||
}
|
||||
var component = this;
|
||||
var ReaderObj = new FileReader();
|
||||
let file = files[0];
|
||||
ReaderObj.readAsDataURL(file);
|
||||
ReaderObj.onloadend = function () {
|
||||
component.props.handleChange({
|
||||
name: file.name,
|
||||
data: this.result,
|
||||
is_example: false
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
class AudioInputExample extends FileComponentExample {
|
||||
render() {
|
||||
return <div className="input_audio_example">{this.props.value}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export { AudioInput, AudioInputExample };
|
@ -1,71 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import ComponentExample from "../component_example";
|
||||
import classNames from "classnames";
|
||||
import { getSaliencyColor } from "../../utils";
|
||||
|
||||
class CheckboxInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
handleChange = () => {
|
||||
this.props.handleChange(this.props.value !== true);
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<div className="input_checkbox">
|
||||
<div
|
||||
className={classNames("checkbox_item", {
|
||||
selected: this.props.value
|
||||
})}
|
||||
onClick={this.handleChange}
|
||||
>
|
||||
{this.props.interpretation === null ? (
|
||||
<div className="checkbox">
|
||||
<svg className="check" viewBox="-10 -10 20 20">
|
||||
<line x1="-7.5" y1="0" x2="-2.5" y2="5"></line>
|
||||
<line x1="-2.5" y1="5" x2="7.5" y2="-7.5"></line>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div class="interpretation">
|
||||
<div
|
||||
class="interpretation_box"
|
||||
style={{
|
||||
backgroundColor: getSaliencyColor(
|
||||
this.props.interpretation[0]
|
||||
)
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
class="interpretation_box"
|
||||
style={{
|
||||
backgroundColor: getSaliencyColor(
|
||||
this.props.interpretation[1]
|
||||
)
|
||||
}}
|
||||
>
|
||||
<svg className="interpret_check" viewBox="-10 -10 20 20">
|
||||
<line x1="-7.5" y1="0" x2="-2.5" y2="5"></line>
|
||||
<line x1="-2.5" y1="5" x2="7.5" y2="-7.5"></line>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxInputExample extends ComponentExample {
|
||||
render() {
|
||||
return (
|
||||
<div className="input_checkbox_example">
|
||||
{JSON.stringify(this.props.value)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { CheckboxInput, CheckboxInputExample };
|
@ -1,83 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import ComponentExample from "../component_example";
|
||||
import classNames from "classnames";
|
||||
import { getSaliencyColor } from "../../utils";
|
||||
|
||||
class CheckboxGroupInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
handleChange = (selected_item) => {
|
||||
let all_selected = [...this.props.value];
|
||||
if (all_selected.includes(selected_item)) {
|
||||
all_selected = all_selected.filter((item) => item !== selected_item);
|
||||
} else {
|
||||
all_selected.push(selected_item);
|
||||
}
|
||||
this.props.handleChange(all_selected);
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<div className="input_checkbox_group">
|
||||
{this.props.choices.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames("checkbox_item", {
|
||||
selected: this.props.value.includes(item)
|
||||
})}
|
||||
onClick={this.handleChange.bind(this, item)}
|
||||
key={index}
|
||||
>
|
||||
{this.props.interpretation === null ? (
|
||||
<div className="checkbox">
|
||||
<svg className="check" viewBox="-10 -10 20 20">
|
||||
<line x1="-7.5" y1="0" x2="-2.5" y2="5"></line>
|
||||
<line x1="-2.5" y1="5" x2="7.5" y2="-7.5"></line>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div class="interpretation">
|
||||
<div
|
||||
class="interpretation_box"
|
||||
style={{
|
||||
backgroundColor: getSaliencyColor(
|
||||
this.props.interpretation[index][0]
|
||||
)
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
class="interpretation_box"
|
||||
style={{
|
||||
backgroundColor: getSaliencyColor(
|
||||
this.props.interpretation[index][1]
|
||||
)
|
||||
}}
|
||||
>
|
||||
<svg className="interpret_check" viewBox="-10 -10 20 20">
|
||||
<line x1="-7.5" y1="0" x2="-2.5" y2="5"></line>
|
||||
<line x1="-2.5" y1="5" x2="7.5" y2="-7.5"></line>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxGroupInputExample extends ComponentExample {
|
||||
render() {
|
||||
return (
|
||||
<div className="input_checkbox_group_example">
|
||||
{this.props.value.join(", ")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { CheckboxGroupInput, CheckboxGroupInputExample };
|
@ -1,115 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import ComponentExample from "../component_example";
|
||||
import jspreadsheet from "jspreadsheet-ce";
|
||||
import "../../../node_modules/jspreadsheet-ce/dist/jexcel.css";
|
||||
import "../../../node_modules/jspreadsheet-ce/dist/jspreadsheet.css";
|
||||
|
||||
class DataframeInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.wrapper = React.createRef();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.el = jspreadsheet(this.wrapper.current, this.getConfig());
|
||||
}
|
||||
getConfig = () => {
|
||||
let col_count = this.props.col_count;
|
||||
let config = {};
|
||||
if (this.props.headers || this.props.datatype) {
|
||||
let column_config = [];
|
||||
for (let i = 0; i < col_count; i++) {
|
||||
let column = {};
|
||||
if (this.props.datatype) {
|
||||
let datatype =
|
||||
this.props.datatype instanceof Array
|
||||
? this.props.datatype[i]
|
||||
: this.props.datatype;
|
||||
let col_width =
|
||||
this.props.col_width instanceof Array
|
||||
? this.props.col_width[i]
|
||||
: this.props.col_width;
|
||||
let datatype_map = {
|
||||
str: "text",
|
||||
bool: "checkbox",
|
||||
number: "numeric",
|
||||
date: "calendar"
|
||||
};
|
||||
column.type = datatype_map[datatype];
|
||||
column.width = col_width;
|
||||
}
|
||||
if (this.props.headers) {
|
||||
column.title = this.props.headers[i];
|
||||
}
|
||||
column_config.push(column);
|
||||
}
|
||||
config.columns = column_config;
|
||||
}
|
||||
config.data = this.props.value;
|
||||
return config;
|
||||
};
|
||||
resetData(new_data) {
|
||||
let [new_rows, new_cols] = [new_data.length, new_data[0].length];
|
||||
let current_data = this.el.getData();
|
||||
let [cur_rows, cur_cols] = [current_data.length, current_data[0].length];
|
||||
if (cur_rows < new_rows) {
|
||||
this.el.insertRow(new_rows - cur_rows);
|
||||
} else if (cur_rows > new_rows) {
|
||||
this.el.deleteRow(0, cur_rows - new_rows);
|
||||
}
|
||||
if (cur_cols < new_cols) {
|
||||
this.el.insertColumn(new_cols - cur_cols);
|
||||
} else if (cur_cols > new_cols) {
|
||||
this.el.deleteColumn(0, cur_cols - new_cols);
|
||||
}
|
||||
this.el.setData(new_data);
|
||||
}
|
||||
render() {
|
||||
if (
|
||||
JSON.stringify(this.props.value) !== JSON.stringify(this.data) &&
|
||||
this.el
|
||||
) {
|
||||
this.resetData(this.props.value);
|
||||
this.data = this.props.value;
|
||||
}
|
||||
return (
|
||||
<div className="input_dataframe">
|
||||
<div ref={this.wrapper} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DataframeInputExample extends ComponentExample {
|
||||
render() {
|
||||
let data_copy = [];
|
||||
for (let row of this.props.value.slice(0, 3)) {
|
||||
let new_row = row.slice(0, 3);
|
||||
if (row.length > 3) {
|
||||
new_row.push("...");
|
||||
}
|
||||
data_copy.push(new_row);
|
||||
}
|
||||
if (this.props.value.length > 3) {
|
||||
let new_row = Array(data_copy[0].length).fill("...");
|
||||
data_copy.push(new_row);
|
||||
}
|
||||
return (
|
||||
<table className="input_dataframe_example">
|
||||
<tbody>
|
||||
{data_copy.map((row) => {
|
||||
return (
|
||||
<tr>
|
||||
{row.map((cell) => {
|
||||
return <td>{cell}</td>;
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { DataframeInput, DataframeInputExample };
|
@ -1,68 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import ComponentExample from "../component_example";
|
||||
import classNames from "classnames";
|
||||
import { getSaliencyColor } from "../../utils";
|
||||
|
||||
class DropdownInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
handleChange(selected_item) {
|
||||
this.props.handleChange(selected_item);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="input_dropdown">
|
||||
{this.props.interpretation === null ? (
|
||||
<div className="dropdown">
|
||||
<button className="selector">
|
||||
{this.props.value}
|
||||
<svg className="caret" viewBox="0 0 20 20">
|
||||
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />{" "}
|
||||
</svg>
|
||||
</button>
|
||||
<div class="dropdown_menu_holder">
|
||||
<ul className="dropdown_menu">
|
||||
{this.props.choices.map((item, index) => {
|
||||
return (
|
||||
<li
|
||||
className={classNames("dropdown_item", {
|
||||
selected: item === this.props.value
|
||||
})}
|
||||
onClick={this.handleChange.bind(this, item)}
|
||||
key={index}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div class="interpretation">
|
||||
{this.props.interpretation.map((value, index) => (
|
||||
<div
|
||||
class="interpretation_box"
|
||||
key={index}
|
||||
style={{ backgroundColor: getSaliencyColor(value) }}
|
||||
>
|
||||
{this.props.choices[index]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DropdownInputExample extends ComponentExample {
|
||||
render() {
|
||||
return <div className="input_dropdown_example">{this.props.value}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export { DropdownInput, DropdownInputExample };
|
@ -1,147 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import { FileComponentExample } from "../component_example";
|
||||
import { prettyBytes } from "../../utils";
|
||||
import clear_icon from "../../static/img/clear.svg";
|
||||
|
||||
class FileInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.uploader = React.createRef();
|
||||
this.openFileUpload = this.openFileUpload.bind(this);
|
||||
this.load_preview_from_files = this.load_preview_from_files.bind(this);
|
||||
this.load_preview_from_upload = this.load_preview_from_upload.bind(this);
|
||||
this.load_preview_from_drop = this.load_preview_from_drop.bind(this);
|
||||
}
|
||||
handleChange(data) {
|
||||
this.props.handleChange(data);
|
||||
}
|
||||
openFileUpload() {
|
||||
this.uploader.current.click();
|
||||
}
|
||||
render() {
|
||||
let no_action = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
};
|
||||
let file_name, file_size;
|
||||
if (this.props.value !== null) {
|
||||
if (this.props.file_count === "single") {
|
||||
let file = Array.isArray(this.props.value)
|
||||
? this.props.value[0]
|
||||
: this.props.value;
|
||||
file_name = file.name;
|
||||
file_size = file.size;
|
||||
} else {
|
||||
file_name = this.props.value.length + " files.";
|
||||
file_size = 0;
|
||||
let fileset = Array.isArray(this.props.value)
|
||||
? this.props.value
|
||||
: [this.props.value];
|
||||
for (let file of fileset) {
|
||||
if (file.size === null) {
|
||||
file_size = null;
|
||||
break;
|
||||
} else {
|
||||
file_size += file.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="input_file">
|
||||
<div className="file_preview_holder">
|
||||
<div class="edit_buttons">
|
||||
<button
|
||||
className="clear_button"
|
||||
onClick={this.handleChange.bind(this, null)}
|
||||
>
|
||||
<img src={clear_icon} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="file_name">{file_name}</div>
|
||||
<div className="file_size">
|
||||
{file_size === null || file_size === undefined
|
||||
? ""
|
||||
: prettyBytes(file_size)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className="input_file"
|
||||
onDrag={no_action}
|
||||
onDragStart={no_action}
|
||||
onDragEnd={no_action}
|
||||
onDragOver={no_action}
|
||||
onDragEnter={no_action}
|
||||
onDragLeave={no_action}
|
||||
onDrop={no_action}
|
||||
>
|
||||
<div
|
||||
className="upload_zone"
|
||||
onClick={this.openFileUpload}
|
||||
onDrop={this.load_preview_from_drop}
|
||||
>
|
||||
Drop File Here
|
||||
<br />- or -<br />
|
||||
Click to Upload
|
||||
</div>
|
||||
<input
|
||||
className="hidden_upload"
|
||||
type="file"
|
||||
multiple={this.props.file_count === "multiple"}
|
||||
webkitdirectory={this.props.file_count === "directory"}
|
||||
mozdirectory={this.props.file_count === "directory"}
|
||||
ref={this.uploader}
|
||||
onChange={this.load_preview_from_upload}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
load_preview_from_drop(evt) {
|
||||
this.load_preview_from_files(evt.dataTransfer.files);
|
||||
}
|
||||
load_preview_from_upload(evt) {
|
||||
this.load_preview_from_files(evt.target.files);
|
||||
}
|
||||
add_file_to_list(reader, file, file_count) {
|
||||
this.file_data.push({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
data: reader.result,
|
||||
is_example: false
|
||||
});
|
||||
if (this.file_data.length === file_count) {
|
||||
this.handleChange(this.file_data);
|
||||
}
|
||||
}
|
||||
load_preview_from_files(files) {
|
||||
if (!files.length || !window.FileReader) {
|
||||
return;
|
||||
}
|
||||
this.file_data = [];
|
||||
for (let file of files) {
|
||||
let ReaderObj = new FileReader();
|
||||
ReaderObj.readAsDataURL(file);
|
||||
ReaderObj.onloadend = this.add_file_to_list.bind(
|
||||
this,
|
||||
ReaderObj,
|
||||
file,
|
||||
files.length
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FileInputExample extends FileComponentExample {
|
||||
render() {
|
||||
return <div className="input_file_example">{this.props.value}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export { FileInput, FileInputExample };
|
@ -1,298 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import { DataURLComponentExample } from "../component_example";
|
||||
import Webcam from "react-webcam";
|
||||
import { SketchField, Tools } from "../../vendor/ReactSketch";
|
||||
import { getObjectFitSize, paintSaliency } from "../../utils";
|
||||
import "tui-image-editor/dist/tui-image-editor.css";
|
||||
import ImageEditor from "@toast-ui/react-image-editor";
|
||||
import Cropper from "react-cropper";
|
||||
import "cropperjs/dist/cropper.css";
|
||||
import edit_icon from "../../static/img/edit.svg";
|
||||
import clear_icon from "../../static/img/clear.svg";
|
||||
|
||||
class ImageInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.uploader = React.createRef();
|
||||
this.openFileUpload = this.openFileUpload.bind(this);
|
||||
this.onImgLoad = this.onImgLoad.bind(this);
|
||||
this.load_preview_from_files = this.load_preview_from_files.bind(this);
|
||||
this.load_preview_from_upload = this.load_preview_from_upload.bind(this);
|
||||
this.load_preview_from_drop = this.load_preview_from_drop.bind(this);
|
||||
this.saveEditor = this.saveEditor.bind(this);
|
||||
this.cancelEditor = this.cancelEditor.bind(this);
|
||||
this.snapshot = this.snapshot.bind(this);
|
||||
this.getSketch = this.getSketch.bind(this);
|
||||
this.openEditor = this.openEditor.bind(this);
|
||||
this.imgRef = React.createRef();
|
||||
this.webcamRef = React.createRef();
|
||||
this.sketchRef = React.createRef();
|
||||
this.editorRef = React.createRef();
|
||||
this.cropperRef = React.createRef();
|
||||
this.sketchKey = 0;
|
||||
this.state = { editorMode: false };
|
||||
}
|
||||
static memo = (a, b) => {
|
||||
if (a.interpretation != b.interpretation) {
|
||||
return false;
|
||||
} else if (a.value === null && b.value === null) {
|
||||
return true;
|
||||
} else if (a.value === null || b.value === null) {
|
||||
return false;
|
||||
} else {
|
||||
return a.value.src === b.value.src;
|
||||
}
|
||||
};
|
||||
handleChange(data) {
|
||||
this.props.handleChange(data);
|
||||
}
|
||||
openFileUpload() {
|
||||
this.uploader.current.click();
|
||||
}
|
||||
snapshot() {
|
||||
let imageSrc = this.webcamRef.current.getScreenshot();
|
||||
this.handleChange({ src: imageSrc, crop: null });
|
||||
}
|
||||
getSketch() {
|
||||
let imageSrc = this.sketchRef.current.toDataURL();
|
||||
this.handleChange({ src: imageSrc, crop: null });
|
||||
}
|
||||
cancelEditor() {
|
||||
this.setState({ editorMode: false });
|
||||
}
|
||||
saveEditor() {
|
||||
const editorInstance = this.editorRef.current.getInstance();
|
||||
this.handleChange({ src: editorInstance.toDataURL(), crop: null });
|
||||
this.setState({ editorMode: false });
|
||||
}
|
||||
onImgLoad({ target: img }) {
|
||||
this.setState({
|
||||
dimensions: {
|
||||
height: img.offsetHeight,
|
||||
width: img.offsetWidth
|
||||
}
|
||||
});
|
||||
}
|
||||
openEditor() {
|
||||
this.setState({ editorMode: true });
|
||||
}
|
||||
onCrop = () => {
|
||||
const crop = this.cropperRef.current.cropper.getCroppedCanvas().toDataURL();
|
||||
this.handleChange({ src: this.props.value.src, crop: crop });
|
||||
};
|
||||
render() {
|
||||
let no_action = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
};
|
||||
if (this.props.value !== null && this.props.source !== "canvas") {
|
||||
let interpretation = false;
|
||||
if (this.props.interpretation !== null) {
|
||||
let img = this.imgRef.current;
|
||||
let size = getObjectFitSize(
|
||||
true,
|
||||
img.width,
|
||||
img.height,
|
||||
img.naturalWidth,
|
||||
img.naturalHeight
|
||||
);
|
||||
if (this.props.shape) {
|
||||
size = getObjectFitSize(
|
||||
true,
|
||||
size.width,
|
||||
size.height,
|
||||
this.props.shape[0],
|
||||
this.props.shape[1]
|
||||
);
|
||||
}
|
||||
let width = size.width;
|
||||
let height = size.height;
|
||||
let canvas = document.createElement("canvas");
|
||||
canvas.setAttribute("height", height);
|
||||
canvas.setAttribute("width", width);
|
||||
paintSaliency(
|
||||
this.props.interpretation,
|
||||
canvas.getContext("2d"),
|
||||
width,
|
||||
height
|
||||
);
|
||||
interpretation = (
|
||||
<div class="interpretation">
|
||||
<img src={canvas.toDataURL()}></img>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="input_image">
|
||||
<div className="image_preview_holder">
|
||||
{this.props.tool === "editor" ? (
|
||||
this.state.editorMode ? (
|
||||
<div className="image_editor">
|
||||
<div className="image_editor_buttons">
|
||||
<button onClick={this.saveEditor}>Save</button>
|
||||
<button onClick={this.cancelEditor}>Cancel</button>
|
||||
</div>
|
||||
<ImageEditor
|
||||
ref={this.editorRef}
|
||||
includeUI={{
|
||||
loadImage: { path: this.props.value.src, name: "value" },
|
||||
uiSize: {
|
||||
width: "800px",
|
||||
height: "600px"
|
||||
},
|
||||
menuBarPosition: "left"
|
||||
}}
|
||||
cssMaxHeight={500}
|
||||
cssMaxWidth={700}
|
||||
usageStatistics={false}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="edit_buttons">
|
||||
<button className="edit_button" onClick={this.openEditor}>
|
||||
<img src={edit_icon} />
|
||||
</button>
|
||||
<button
|
||||
className="clear_button"
|
||||
onClick={this.handleChange.bind(this, null)}
|
||||
>
|
||||
<img src={clear_icon} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
false
|
||||
)}
|
||||
{this.props.tool === "select" ? (
|
||||
<Cropper
|
||||
src={this.props.value.src}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
ref={this.cropperRef}
|
||||
crop={this.onCrop}
|
||||
autoCropArea={1}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
ref={this.imgRef}
|
||||
onLoad={this.onImgLoad}
|
||||
className="image_preview"
|
||||
alt=""
|
||||
src={this.props.value.src}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{interpretation}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (this.props.source === "upload") {
|
||||
return (
|
||||
<div
|
||||
className="input_image"
|
||||
onDrag={no_action}
|
||||
onDragStart={no_action}
|
||||
onDragEnd={no_action}
|
||||
onDragOver={no_action}
|
||||
onDragEnter={no_action}
|
||||
onDragLeave={no_action}
|
||||
onDrop={no_action}
|
||||
>
|
||||
<div
|
||||
className="upload_zone"
|
||||
onClick={this.openFileUpload}
|
||||
onDrop={this.load_preview_from_drop}
|
||||
>
|
||||
Drop Image Here
|
||||
<br />- or -<br />
|
||||
Click to Upload
|
||||
</div>
|
||||
<input
|
||||
className="hidden_upload"
|
||||
type="file"
|
||||
ref={this.uploader}
|
||||
onChange={this.load_preview_from_upload}
|
||||
accept="image/x-png,image/gif,image/jpeg"
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.source === "webcam") {
|
||||
return (
|
||||
<div className="input_image">
|
||||
<div className="image_preview_holder">
|
||||
<Webcam ref={this.webcamRef} />
|
||||
<div class="snapshot">
|
||||
<button onClick={this.snapshot}>Click to Take Snapshot</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.source === "canvas") {
|
||||
if (
|
||||
this.props.value === null &&
|
||||
this.sketchRef &&
|
||||
this.sketchRef.current
|
||||
) {
|
||||
this.sketchKey += 1;
|
||||
}
|
||||
return (
|
||||
<div className="input_image">
|
||||
<div className="image_preview_holder sketch">
|
||||
<SketchField
|
||||
ref={this.sketchRef}
|
||||
key={this.sketchKey}
|
||||
width="320px"
|
||||
height="100%"
|
||||
tool={Tools.Pencil}
|
||||
lineColor="black"
|
||||
lineWidth={20}
|
||||
backgroundColor="white"
|
||||
onChange={this.getSketch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
load_preview_from_drop(evt) {
|
||||
this.load_preview_from_files(evt.dataTransfer.files);
|
||||
}
|
||||
load_preview_from_upload(evt) {
|
||||
this.load_preview_from_files(evt.target.files);
|
||||
}
|
||||
load_preview_from_files(files) {
|
||||
if (!files.length || !window.FileReader || !/^image/.test(files[0].type)) {
|
||||
return;
|
||||
}
|
||||
var component = this;
|
||||
var ReaderObj = new FileReader();
|
||||
ReaderObj.readAsDataURL(files[0]);
|
||||
ReaderObj.onloadend = function () {
|
||||
component.props.handleChange({ src: this.result, crop: null });
|
||||
};
|
||||
}
|
||||
static postprocess = (y) => {
|
||||
return y === null ? null : y.crop === null ? y.src : y.crop;
|
||||
};
|
||||
}
|
||||
|
||||
class ImageInputExample extends DataURLComponentExample {
|
||||
static async preprocess(x, examples_dir) {
|
||||
let src = await DataURLComponentExample.preprocess(x, examples_dir);
|
||||
return { src: src, crop: null };
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<img
|
||||
className="input_image_example"
|
||||
src={this.props.examples_dir + "/" + this.props.value}
|
||||
alt=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { ImageInput, ImageInputExample };
|
@ -1,51 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import ComponentExample from "../component_example";
|
||||
import { getSaliencyColor } from "../../utils";
|
||||
|
||||
class NumberInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
handleChange(evt) {
|
||||
let value = evt.target.value;
|
||||
let valid_non_numbers = ["", "-", ".", "-."];
|
||||
if (!isNaN(parseFloat(value)) || valid_non_numbers.includes(value)) {
|
||||
this.props.handleChange(evt.target.value);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="input_number">
|
||||
{this.props.interpretation === null ? (
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.handleChange}
|
||||
value={this.props.value === null ? "" : this.props.value}
|
||||
></input>
|
||||
) : (
|
||||
<div class="interpretation">
|
||||
{this.props.interpretation.map((value, index) => (
|
||||
<div
|
||||
class="interpretation_box"
|
||||
key={index}
|
||||
style={{ backgroundColor: getSaliencyColor(value[1]) }}
|
||||
>
|
||||
{value[0]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NumberInputExample extends ComponentExample {
|
||||
render() {
|
||||
return <div className="input_number_example">{this.props.value}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export { NumberInput, NumberInputExample };
|
@ -1,55 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import ComponentExample from "../component_example";
|
||||
import classNames from "classnames";
|
||||
import { getSaliencyColor } from "../../utils";
|
||||
|
||||
class RadioInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
handleChange(selected_item) {
|
||||
this.props.handleChange(selected_item);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="input_radio">
|
||||
{this.props.choices.map((item, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<div
|
||||
className={classNames("radio_item", {
|
||||
selected: item === this.props.value
|
||||
})}
|
||||
onClick={this.handleChange.bind(this, item)}
|
||||
>
|
||||
{this.props.interpretation === null ? (
|
||||
<div className="radio_circle"></div>
|
||||
) : (
|
||||
<div
|
||||
className="radio_circle"
|
||||
style={{
|
||||
backgroundColor: getSaliencyColor(
|
||||
this.props.interpretation[index]
|
||||
)
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{item}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RadioInputExample extends ComponentExample {
|
||||
render() {
|
||||
return <div className="input_radio_example">{this.props.value}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export { RadioInput, RadioInputExample };
|
@ -1,50 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import ComponentExample from "../component_example";
|
||||
import { getSaliencyColor } from "../../utils";
|
||||
|
||||
class SliderInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
handleChange(evt) {
|
||||
this.props.handleChange(parseFloat(evt.target.value));
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="input_slider">
|
||||
{this.props.interpretation === null ? (
|
||||
<>
|
||||
<input
|
||||
type="range"
|
||||
className="range"
|
||||
onChange={this.handleChange}
|
||||
value={this.props.value}
|
||||
min={this.props.minimum}
|
||||
max={this.props.maximum}
|
||||
step={this.props.step}
|
||||
></input>
|
||||
<div className="value">{this.props.value}</div>
|
||||
</>
|
||||
) : (
|
||||
<div class="interpret_range">
|
||||
{this.props.interpretation.map((value) => (
|
||||
<div
|
||||
style={{ "background-color": getSaliencyColor(value) }}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SliderInputExample extends ComponentExample {
|
||||
render() {
|
||||
return <div className="input_slider_example">{this.props.value}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export { SliderInput, SliderInputExample };
|
@ -1,63 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import ComponentExample from "../component_example";
|
||||
import { getSaliencyColor } from "../../utils";
|
||||
|
||||
class TextboxInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
handleChange(evt) {
|
||||
this.props.handleChange(evt.target.value);
|
||||
}
|
||||
render() {
|
||||
if (this.props.interpretation !== null) {
|
||||
return (
|
||||
<div className="input_text">
|
||||
<div class="interpretation">
|
||||
{this.props.interpretation.map((item, index) => (
|
||||
<div
|
||||
class="interpretation_box"
|
||||
key={index}
|
||||
style={{ backgroundColor: getSaliencyColor(item[1]) }}
|
||||
>
|
||||
{item[0]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.lines > 1) {
|
||||
return (
|
||||
<div className="input_text">
|
||||
<textarea
|
||||
value={this.props.value || ""}
|
||||
rows={this.props.lines}
|
||||
onChange={this.handleChange}
|
||||
placeholder={this.props.placeholder || ""}
|
||||
></textarea>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="input_text">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.handleChange}
|
||||
value={this.props.value || ""}
|
||||
placeholder={this.props.placeholder || ""}
|
||||
></input>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TextboxInputExample extends ComponentExample {
|
||||
render() {
|
||||
return <div className="input_textbox_example">{this.props.value}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export { TextboxInput, TextboxInputExample };
|
@ -1,163 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import FileComponentExample from "../component_example";
|
||||
import { CSVToArray } from "../../utils";
|
||||
import { Scatter } from "react-chartjs-2";
|
||||
import { getNextColor } from "../../utils";
|
||||
|
||||
class TimeseriesInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.uploader = React.createRef();
|
||||
}
|
||||
handleChange = (data) => {
|
||||
this.props.handleChange(data);
|
||||
};
|
||||
openFileUpload = () => {
|
||||
this.uploader.current.click();
|
||||
};
|
||||
render = () => {
|
||||
let no_action = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
};
|
||||
if (this.props.value !== null) {
|
||||
return (
|
||||
<div className="input_timeseries">
|
||||
<Scatter
|
||||
data={{
|
||||
datasets: this.props.y.map((header, i) => {
|
||||
return {
|
||||
label: header,
|
||||
borderColor: getNextColor(i),
|
||||
showLine: true,
|
||||
fill: true,
|
||||
backgroundColor: getNextColor(i, 0.25),
|
||||
data: this.props.value["data"].map((row) => {
|
||||
return {
|
||||
x: row[0],
|
||||
y: row[i + 1]
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className="input_timeseries"
|
||||
onDrag={no_action}
|
||||
onDragStart={no_action}
|
||||
onDragEnd={no_action}
|
||||
onDragOver={no_action}
|
||||
onDragEnter={no_action}
|
||||
onDragLeave={no_action}
|
||||
onDrop={no_action}
|
||||
>
|
||||
<div
|
||||
className="upload_zone"
|
||||
onClick={this.openFileUpload}
|
||||
onDrop={this.load_preview_from_drop}
|
||||
>
|
||||
Upload Timeseries CSV
|
||||
{this.props.x !== null ? (
|
||||
<>
|
||||
<br />X Column: {this.props.x}
|
||||
<br />Y Column: {this.props.y.join(", ")}
|
||||
</>
|
||||
) : (
|
||||
false
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
className="hidden_upload"
|
||||
type="file"
|
||||
multiple={this.props.file_count === "multiple"}
|
||||
webkitdirectory={this.props.file_count === "directory"}
|
||||
mozdirectory={this.props.file_count === "directory"}
|
||||
ref={this.uploader}
|
||||
onChange={this.load_preview_from_upload}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
load_preview_from_drop = (evt) => {
|
||||
this.load_preview_from_files(evt.dataTransfer.files);
|
||||
};
|
||||
load_preview_from_upload = (evt) => {
|
||||
this.load_preview_from_files(evt.target.files);
|
||||
};
|
||||
load_file = (reader) => {
|
||||
let lines = reader.result;
|
||||
this.handleChange(load_data(lines, this.props.x, this.props.y));
|
||||
};
|
||||
load_preview_from_files = (files) => {
|
||||
if (!files.length || !window.FileReader) {
|
||||
return;
|
||||
}
|
||||
this.file_data = [];
|
||||
for (let file of files) {
|
||||
let ReaderObj = new FileReader();
|
||||
ReaderObj.readAsBinaryString(file);
|
||||
ReaderObj.onloadend = this.load_file.bind(this, ReaderObj);
|
||||
}
|
||||
};
|
||||
static memo = (a, b) => {
|
||||
if (a.value instanceof Object && b.value instanceof Object) {
|
||||
return (
|
||||
a.value["data"] === b.value["data"] &&
|
||||
a.value["headers"] === b.value["headers"]
|
||||
);
|
||||
} else {
|
||||
return a === b;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class TimeseriesInputExample extends FileComponentExample {
|
||||
static async preprocess(x, examples_dir, component_config) {
|
||||
let file_url = examples_dir + "/" + x;
|
||||
let response = await fetch(file_url);
|
||||
response = await response.text();
|
||||
return load_data(response, component_config.x, component_config.y);
|
||||
}
|
||||
render() {
|
||||
return <div className="input_file_example">{this.props.value}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
var load_data = (lines, x, y) => {
|
||||
let headers = null;
|
||||
let data = null;
|
||||
let line_array = CSVToArray(lines);
|
||||
if (line_array.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (x === null) {
|
||||
data = line_array;
|
||||
} else {
|
||||
let x_index = line_array[0].indexOf(x);
|
||||
let y_indices = y.map((y_col) => line_array[0].indexOf(y_col));
|
||||
if (x_index === -1) {
|
||||
alert("Missing x column: " + x);
|
||||
return;
|
||||
}
|
||||
if (y_indices.includes(-1)) {
|
||||
alert("Missing y column: " + y[y_indices.indexOf(-1)]);
|
||||
return;
|
||||
}
|
||||
line_array = line_array.map((line) =>
|
||||
[line[x_index]].concat(y_indices.map((y_index) => line[y_index]))
|
||||
);
|
||||
headers = line_array[0];
|
||||
data = line_array.slice(1);
|
||||
}
|
||||
return { headers: headers, data: data, range: null };
|
||||
};
|
||||
|
||||
export { TimeseriesInput, TimeseriesInputExample };
|
@ -1,223 +0,0 @@
|
||||
import React from "react";
|
||||
import BaseComponent from "../base_component";
|
||||
import { FileComponentExample } from "../component_example";
|
||||
import { isPlayable } from "../../utils";
|
||||
import clear_icon from "../../static/img/clear.svg";
|
||||
|
||||
class VideoInput extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.uploader = React.createRef();
|
||||
this.videoRecorder = React.createRef();
|
||||
this.openFileUpload = this.openFileUpload.bind(this);
|
||||
this.load_preview_from_files = this.load_preview_from_files.bind(this);
|
||||
this.load_preview_from_upload = this.load_preview_from_upload.bind(this);
|
||||
this.load_preview_from_drop = this.load_preview_from_drop.bind(this);
|
||||
this.camera_stream = null;
|
||||
this.state = {
|
||||
recording: false
|
||||
};
|
||||
}
|
||||
handleChange(evt) {
|
||||
this.props.handleChange(evt.target.value);
|
||||
}
|
||||
openFileUpload() {
|
||||
this.uploader.current.click();
|
||||
}
|
||||
record = async () => {
|
||||
if (this.state.recording) {
|
||||
this.media_recorder.stop();
|
||||
let video_blob = new Blob(this.blobs_recorded, { type: this.mimeType });
|
||||
var ReaderObj = new FileReader();
|
||||
ReaderObj.onload = function (e) {
|
||||
let file_name = "sample." + this.mimeType.substring(6);
|
||||
this.props.handleChange({
|
||||
name: file_name,
|
||||
data: e.target.result,
|
||||
is_example: false
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
ReaderObj.readAsDataURL(video_blob);
|
||||
this.setState({ recording: false });
|
||||
} else {
|
||||
this.blobs_recorded = [];
|
||||
this.camera_stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: true
|
||||
});
|
||||
this.videoRecorder.current.srcObject = this.camera_stream;
|
||||
this.videoRecorder.current.volume = 0;
|
||||
let selectedMimeType = null;
|
||||
let validMimeTypes = ["video/webm", "video/mp4"];
|
||||
for (let mimeType of validMimeTypes) {
|
||||
if (MediaRecorder.isTypeSupported(mimeType)) {
|
||||
selectedMimeType = mimeType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selectedMimeType === null) {
|
||||
console.error("No supported MediaRecorder mimeType");
|
||||
return;
|
||||
}
|
||||
this.media_recorder = new MediaRecorder(this.camera_stream, {
|
||||
mimeType: selectedMimeType
|
||||
});
|
||||
this.mimeType = selectedMimeType;
|
||||
this.media_recorder.addEventListener(
|
||||
"dataavailable",
|
||||
function (e) {
|
||||
this.blobs_recorded.push(e.data);
|
||||
}.bind(this)
|
||||
);
|
||||
this.media_recorder.start(200);
|
||||
this.videoRecorder.current.play();
|
||||
this.setState({ recording: true });
|
||||
}
|
||||
};
|
||||
render() {
|
||||
let no_action = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
};
|
||||
if (this.props.value != null) {
|
||||
return (
|
||||
<div className="input_video">
|
||||
<div className="edit_buttons">
|
||||
<button
|
||||
className="clear_button"
|
||||
onClick={this.props.handleChange.bind(this, null)}
|
||||
>
|
||||
<img src={clear_icon} />
|
||||
</button>
|
||||
</div>
|
||||
{isPlayable("video", this.props.value["name"]) ? (
|
||||
<div className="video_preview_holder">
|
||||
<video
|
||||
className="video_preview"
|
||||
controls
|
||||
playsInline
|
||||
preload
|
||||
src={this.props.value["data"]}
|
||||
></video>
|
||||
</div>
|
||||
) : (
|
||||
<div className="video_file_holder">{this.props.value["name"]}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.source == "upload") {
|
||||
return (
|
||||
<div
|
||||
className="input_video"
|
||||
onDrag={no_action}
|
||||
onDragStart={no_action}
|
||||
onDragEnd={no_action}
|
||||
onDragOver={no_action}
|
||||
onDragEnter={no_action}
|
||||
onDragLeave={no_action}
|
||||
onDrop={no_action}
|
||||
>
|
||||
<div
|
||||
className="upload_zone"
|
||||
onClick={this.openFileUpload}
|
||||
onDrop={this.load_preview_from_drop}
|
||||
>
|
||||
Drop Video Here
|
||||
<br />- or -<br />
|
||||
Click to Upload
|
||||
</div>
|
||||
<input
|
||||
className="hidden_upload"
|
||||
type="file"
|
||||
ref={this.uploader}
|
||||
onChange={this.load_preview_from_upload}
|
||||
accept="video/mp4,video/x-m4v,video/*"
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.source == "webcam") {
|
||||
return (
|
||||
<div className="input_video">
|
||||
<video
|
||||
ref={this.videoRecorder}
|
||||
class="video_recorder"
|
||||
autoPlay
|
||||
playsInline
|
||||
muted
|
||||
></video>
|
||||
<div class="record_holder">
|
||||
<div class="record_message">
|
||||
{this.state.recording ? (
|
||||
<>Stop Recording</>
|
||||
) : (
|
||||
<>Click to Record</>
|
||||
)}
|
||||
</div>
|
||||
<button class="record" onClick={this.record}></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
load_preview_from_drop(evt) {
|
||||
this.load_preview_from_files(evt.dataTransfer.files);
|
||||
}
|
||||
load_preview_from_upload(evt) {
|
||||
this.load_preview_from_files(evt.target.files);
|
||||
}
|
||||
load_preview_from_files(files) {
|
||||
if (!files.length || !window.FileReader || !/^video/.test(files[0].type)) {
|
||||
return;
|
||||
}
|
||||
var component = this;
|
||||
var ReaderObj = new FileReader();
|
||||
let file = files[0];
|
||||
ReaderObj.readAsDataURL(file);
|
||||
ReaderObj.onloadend = function () {
|
||||
component.props.handleChange({
|
||||
name: file.name,
|
||||
data: this.result,
|
||||
is_example: false
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class VideoInputExample extends FileComponentExample {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.video = React.createRef();
|
||||
}
|
||||
render() {
|
||||
if (isPlayable("video", this.props.value)) {
|
||||
return (
|
||||
<div className="input_video_example">
|
||||
<div className="video_holder">
|
||||
<video
|
||||
ref={this.video}
|
||||
className="video_preview"
|
||||
onMouseOver={() => {
|
||||
this.video.current.play();
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
this.video.current.pause();
|
||||
}}
|
||||
preload="metadata"
|
||||
>
|
||||
<source
|
||||
src={this.props.examples_dir + "/" + this.props.value}
|
||||
></source>
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div className="input_video_example">{this.props.value}</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { VideoInput, VideoInputExample };
|
10
frontend/src/components/output/Audio/Component.svelte
Normal file
@ -0,0 +1,10 @@
|
||||
<script>
|
||||
export let value, theme;
|
||||
</script>
|
||||
|
||||
<audio {theme} controls>
|
||||
<source src={value} />
|
||||
</audio>
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
5
frontend/src/components/output/Audio/config.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Component from "./Component.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
}
|
56
frontend/src/components/output/Carousel/Component.svelte
Normal file
@ -0,0 +1,56 @@
|
||||
<script>
|
||||
import { output_component_map } from "../../directory";
|
||||
|
||||
export let value, theme;
|
||||
export let components;
|
||||
|
||||
let carousel_index = 0;
|
||||
const next = () => {
|
||||
carousel_index = (carousel_index + 1) % value.length;
|
||||
};
|
||||
const prev = () => {
|
||||
carousel_index = (carousel_index - 1 + value.length) % value.length;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="output-carousel flex flex-col gap-2" {theme}>
|
||||
{#each components as component, i}
|
||||
<div class="component" key={i}>
|
||||
{#if component.label}
|
||||
<div class="panel-header">{component.label}</div>
|
||||
{/if}
|
||||
<svelte:component
|
||||
this={output_component_map[component.name].component}
|
||||
{...component}
|
||||
{theme}
|
||||
value={value[carousel_index][i]}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="carousel-control flex gap-4 justify-center items-center my-1">
|
||||
<button on:click={prev}>
|
||||
<svg class="caret h-3 mt-0.5 fill-current" viewBox="0 0 9.1457395 15.999842">
|
||||
<path
|
||||
d="M 0.32506616,7.2360106 7.1796187,0.33129769 c 0.4360247,-0.439451 1.1455702,-0.442056 1.5845974,-0.0058 0.4390612,0.435849 0.441666,1.14535901 0.00582,1.58438501 l -6.064985,6.1096644 6.10968,6.0646309 c 0.4390618,0.436026 0.4416664,1.145465 0.00582,1.584526 -0.4358485,0.439239 -1.1453586,0.441843 -1.5845975,0.0058 L 0.33088256,8.8203249 C 0.11135166,8.6022941 0.00105996,8.3161928 7.554975e-6,8.0295489 -0.00104244,7.7427633 0.10735446,7.4556467 0.32524356,7.2361162"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="carousel_index text-xl text-center font-semibold" style="min-width: 60px">
|
||||
{carousel_index + 1} / {value.length}
|
||||
</div>
|
||||
<button on:click={next}>
|
||||
<svg
|
||||
class="caret h-3 mt-0.5 fill-current"
|
||||
viewBox="0 0 9.1457395 15.999842"
|
||||
transform="scale(-1, 1)"
|
||||
>
|
||||
<path
|
||||
d="M 0.32506616,7.2360106 7.1796187,0.33129769 c 0.4360247,-0.439451 1.1455702,-0.442056 1.5845974,-0.0058 0.4390612,0.435849 0.441666,1.14535901 0.00582,1.58438501 l -6.064985,6.1096644 6.10968,6.0646309 c 0.4390618,0.436026 0.4416664,1.145465 0.00582,1.584526 -0.4358485,0.439239 -1.1453586,0.441843 -1.5845975,0.0058 L 0.33088256,8.8203249 C 0.11135166,8.6022941 0.00105996,8.3161928 7.554975e-6,8.0295489 -0.00104244,7.7427633 0.10735446,7.4556467 0.32524356,7.2361162"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
5
frontend/src/components/output/Carousel/config.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Component from "./Component.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
}
|
18
frontend/src/components/output/Dataframe/Component.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import DataFrame from "../../input/DataFrame/Component.svelte";
|
||||
|
||||
export let headers,
|
||||
value,
|
||||
theme,
|
||||
setValue = () => {};
|
||||
|
||||
$: console.log(headers, value);
|
||||
</script>
|
||||
|
||||
<DataFrame
|
||||
headers={headers || value.headers}
|
||||
values={value.data}
|
||||
{setValue}
|
||||
editable={false}
|
||||
{theme}
|
||||
/>
|
5
frontend/src/components/output/Dataframe/config.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Component from "./Component.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
}
|
23
frontend/src/components/output/File/Component.svelte
Normal file
@ -0,0 +1,23 @@
|
||||
<script>
|
||||
import { prettyBytes } from "../../utils/helpers";
|
||||
|
||||
export let value, theme;
|
||||
</script>
|
||||
|
||||
<a
|
||||
class="output-file w-full h-full flex flex-col justify-center items-center relative"
|
||||
href={value.data}
|
||||
download={value.name}
|
||||
{theme}
|
||||
>
|
||||
<div class="file-name text-4xl p-6 break-all">{value.name}</div>
|
||||
<div class="text-2xl p-2">
|
||||
{isNaN(value.size) ? "" : prettyBytes(value.size)}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style lang="postcss">
|
||||
.output-file[theme="default"] {
|
||||
@apply h-60 hover:text-gray-500;
|
||||
}
|
||||
</style>
|
5
frontend/src/components/output/File/config.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Component from "./Component.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
}
|
105
frontend/src/components/output/HighlightedText/Component.svelte
Normal file
@ -0,0 +1,105 @@
|
||||
<script>
|
||||
import { getNextColor } from "../../utils/helpers";
|
||||
|
||||
export let value, theme;
|
||||
export let show_legend, color_map;
|
||||
color_map = color_map || {};
|
||||
let mode;
|
||||
|
||||
if (value.length > 0) {
|
||||
for (let [_, label] of value) {
|
||||
if (label !== null) {
|
||||
if (typeof label === "string") {
|
||||
mode = "categories";
|
||||
if (!(label in color_map)) {
|
||||
let color = getNextColor(Object.keys(color_map).length);
|
||||
color_map[label] = color;
|
||||
}
|
||||
} else {
|
||||
mode = "scores";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(color_map);
|
||||
</script>
|
||||
|
||||
<div class="output-highlightedtext" {theme}>
|
||||
{#if mode === "categories"}
|
||||
{#if show_legend}
|
||||
<div class="category-legend flex flex-wrap gap-1 mb-2">
|
||||
{#each color_map.entries() as [category, color], i}
|
||||
<div
|
||||
class="category-label px-2 py-1 rounded text-white font-semibold"
|
||||
style={"background-color" + color}
|
||||
key={i}
|
||||
>
|
||||
{category}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="textfield p-2 bg-white dark:bg-gray-800 rounded box-border max-w-full leading-8 break-all"
|
||||
>
|
||||
{#each value as [text, category], i}
|
||||
<span
|
||||
class="textspan p-1 mr-0.5 bg-opacity-20 dark:bg-opacity-80 rounded-sm"
|
||||
title={category}
|
||||
style={category === null
|
||||
? ""
|
||||
: `color: ${color_map[category]}; background-color: ${color_map[
|
||||
category
|
||||
].replace("1)", "var(--tw-bg-opacity))")}`}
|
||||
key={i}
|
||||
>
|
||||
<span class="text dark:text-white">{text}</span>
|
||||
{#if !show_legend && category !== null}
|
||||
<span
|
||||
class="inline-category text-xs text-white ml-0.5 px-0.5 rounded-sm"
|
||||
style={category === null
|
||||
? ""
|
||||
: `background-color: ${color_map[category]}`}
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
{#if show_legend}
|
||||
<div
|
||||
class="color_legend flex px-2 py-1 justify-between rounded mb-3 font-semibold"
|
||||
style="background: -webkit-linear-gradient(to right,#8d83d6,(255,255,255,0),#eb4d4b); background: linear-gradient(to right,#8d83d6,rgba(255,255,255,0),#eb4d4b);"
|
||||
>
|
||||
<span>-1</span>
|
||||
<span>0</span>
|
||||
<span>+1</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="textfield p-2 bg-white dark:bg-gray-800 rounded box-border max-w-full leading-8 break-all"
|
||||
>
|
||||
{#each value as [text, score], i}
|
||||
<span
|
||||
class="textspan p-1 mr-0.5 bg-opacity-20 dark:bg-opacity-80 rounded-sm"
|
||||
title={value}
|
||||
style={"background-color: rgba(" +
|
||||
(score < 0 ? "141, 131, 214," + -score : "235, 77, 75," + score) +
|
||||
")"}
|
||||
>
|
||||
<span class="text dark:text-white">{text}</span>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.output-highlightedtext[theme="default"] {
|
||||
.textfield {
|
||||
@apply shadow;
|
||||
}
|
||||
}
|
||||
</style>
|
5
frontend/src/components/output/HighlightedText/config.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Component from "./Component.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
}
|
10
frontend/src/components/output/Html/Component.svelte
Normal file
@ -0,0 +1,10 @@
|
||||
<script>
|
||||
export let value, theme;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="output-html"
|
||||
{theme}
|
||||
>
|
||||
{@html value}
|
||||
</div>
|
5
frontend/src/components/output/Html/config.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Component from "./Component.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
}
|
11
frontend/src/components/output/Image/Component.svelte
Normal file
@ -0,0 +1,11 @@
|
||||
<script>
|
||||
export let value, theme;
|
||||
</script>
|
||||
|
||||
<div class="output-image w-full h-60 flex justify-center items-center bg-gray-200 dark:bg-gray-600 relative" {theme}>
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<img class="w-full h-full object-contain" src={value} />
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
5
frontend/src/components/output/Image/config.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Component from "./Component.svelte";
|
||||
|
||||
export default {
|
||||
"component": Component,
|
||||
}
|