Update files

#2
by Xenova HF Staff - opened
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ public/banner.png filter=lfs diff=lfs merge=lfs -text
37
+ public/logo.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -9,7 +9,9 @@ license: apache-2.0
9
  models:
10
  - onnx-community/Janus-1.3B-ONNX
11
  short_description: In-browser unified multimodal understanding and generation.
12
- thumbnail: https://huggingface.co/spaces/webml-community/janus-webgpu/resolve/main/banner.png
 
 
13
  ---
14
 
15
  # Janus 1.3B WebGPU
 
9
  models:
10
  - onnx-community/Janus-1.3B-ONNX
11
  short_description: In-browser unified multimodal understanding and generation.
12
+ thumbnail: https://huggingface.co/spaces/webml-community/janus-webgpu/resolve/main/public/banner.png
13
+ app_build_command: npm run build
14
+ app_file: dist/index.html
15
  ---
16
 
17
  # Janus 1.3B WebGPU
assets/index-Cy-lD5Cm.css DELETED
@@ -1 +0,0 @@
1
- @scope (.markdown){pre{margin:.5rem 0;white-space:break-spaces}code{padding:.2em .4em;border-radius:4px;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;font-size:.9em}pre,code{background-color:#f2f2f2}@media (prefers-color-scheme: dark){pre,code{background-color:#333}}pre:has(code){padding:1rem .5rem}pre>code{padding:0}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.2}h1{font-size:2em;margin:1rem 0}h2{font-size:1.5em;margin:.83rem 0}h3{font-size:1.25em;margin:.67rem 0}h4{font-size:1em;margin:.5rem 0}h5{font-size:.875em;margin:.33rem 0}h6{font-size:.75em;margin:.25rem 0}h1,h2,h3,h4,h5,h6:first-child{margin-top:0}ul{list-style-type:disc;margin-left:1.5rem}ol{list-style-type:decimal;margin-left:1.5rem}li{margin:.25rem 0}p:not(:first-child){margin-top:.75rem}p:not(:last-child){margin-bottom:.75rem}}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.pointer-events-none{pointer-events:none}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.bottom-0{bottom:0}.bottom-3{bottom:.75rem}.left-1\.5{left:.375rem}.right-0{right:0}.right-3{right:.75rem}.top-0{top:0}.z-10{z-index:10}.m-1{margin:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.my-3{margin-top:.75rem;margin-bottom:.75rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mr-1{margin-right:.25rem}.mt-2{margin-top:.5rem}.mt-auto{margin-top:auto}.block{display:block}.flex{display:flex}.hidden{display:none}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-64{max-height:16rem}.max-h-\[200px\]{max-height:200px}.min-h-20{min-height:5rem}.min-h-6{min-height:1.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-\[384px\]{width:384px}.w-\[600px\]{width:600px}.w-full{width:100%}.w-screen{width:100vw}.min-w-20{min-width:5rem}.min-w-6{min-width:1.5rem}.max-w-\[350px\]{max-width:350px}.max-w-\[452px\]{max-width:452px}.max-w-\[500px\]{max-width:500px}.max-w-\[600px\]{max-width:600px}.max-w-\[80\%\]{max-width:80%}.max-w-\[960px\]{max-width:960px}.max-w-full{max-width:100%}.flex-1{flex:1 1 0%}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.gap-1{gap:.25rem}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-none{border-style:none}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity))}.bg-blue-400{--tw-bg-opacity: 1;background-color:rgb(96 165 250 / var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-opacity-\[92\%\]{--tw-bg-opacity: 92%}.fill-gray-200{fill:#e5e7eb}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pl-11{padding-left:2.75rem}.pr-12{padding-right:3rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-5xl{font-size:3rem;line-height:1}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-50{--tw-text-opacity: 1;color:rgb(249 250 251 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.underline{text-decoration-line:underline}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.scrollbar-thin::-webkit-scrollbar{width:.5rem}.scrollbar-thin::-webkit-scrollbar-track{border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}@media (prefers-color-scheme: dark){.scrollbar-thin::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}}.scrollbar-thin::-webkit-scrollbar-thumb{border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity))}@media (prefers-color-scheme: dark){.scrollbar-thin::-webkit-scrollbar-thumb{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity))}}.scrollbar-thin::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity))}.animation-delay-200{animation-delay:.2s}.animation-delay-400{animation-delay:.4s}.overflow-wrap-anywhere{overflow-wrap:anywhere}.hover\:bg-blue-500:hover{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-blue-100:disabled{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity))}.disabled\:text-gray-400:disabled{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.disabled\:placeholder-gray-200:disabled::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(229 231 235 / var(--tw-placeholder-opacity))}.disabled\:placeholder-gray-200:disabled::placeholder{--tw-placeholder-opacity: 1;color:rgb(229 231 235 / var(--tw-placeholder-opacity))}@media (prefers-color-scheme: dark){.dark\:border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity))}.dark\:bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.dark\:bg-gray-300{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity))}.dark\:bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity))}.dark\:bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}.dark\:bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity))}.dark\:fill-gray-400{fill:#9ca3af}.dark\:text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.dark\:text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity))}.dark\:text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity))}.dark\:text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.dark\:text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.dark\:text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.dark\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.dark\:placeholder-gray-300::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(209 213 219 / var(--tw-placeholder-opacity))}.dark\:placeholder-gray-300::placeholder{--tw-placeholder-opacity: 1;color:rgb(209 213 219 / var(--tw-placeholder-opacity))}.dark\:disabled\:placeholder-gray-500:disabled::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity))}.dark\:disabled\:placeholder-gray-500:disabled::placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity))}}
 
 
assets/index-FQf4wfd9.js DELETED
The diff for this file is too large to render. See raw diff
 
assets/worker-BMn4lUIh.js DELETED
The diff for this file is too large to render. See raw diff
 
eslint.config.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import react from "eslint-plugin-react";
4
+ import reactHooks from "eslint-plugin-react-hooks";
5
+ import reactRefresh from "eslint-plugin-react-refresh";
6
+
7
+ export default [
8
+ { ignores: ["dist"] },
9
+ {
10
+ files: ["**/*.{js,jsx}"],
11
+ languageOptions: {
12
+ ecmaVersion: 2020,
13
+ globals: globals.browser,
14
+ parserOptions: {
15
+ ecmaVersion: "latest",
16
+ ecmaFeatures: { jsx: true },
17
+ sourceType: "module",
18
+ },
19
+ },
20
+ settings: { react: { version: "18.3" } },
21
+ plugins: {
22
+ react,
23
+ "react-hooks": reactHooks,
24
+ "react-refresh": reactRefresh,
25
+ },
26
+ rules: {
27
+ ...js.configs.recommended.rules,
28
+ ...react.configs.recommended.rules,
29
+ ...react.configs["jsx-runtime"].rules,
30
+ ...reactHooks.configs.recommended.rules,
31
+ "react/jsx-no-target-blank": "off",
32
+ "react-refresh/only-export-components": [
33
+ "warn",
34
+ { allowConstantExport: true },
35
+ ],
36
+ },
37
+ },
38
+ ];
index.html CHANGED
@@ -5,8 +5,6 @@
5
  <link rel="icon" type="image/png" href="/logo.png" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Janus WebGPU</title>
8
- <script type="module" crossorigin src="/assets/index-FQf4wfd9.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Cy-lD5Cm.css">
10
  </head>
11
 
12
  <body>
@@ -29,5 +27,6 @@
29
  id="MathJax-script"
30
  src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
31
  ></script>
 
32
  </body>
33
  </html>
 
5
  <link rel="icon" type="image/png" href="/logo.png" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Janus WebGPU</title>
 
 
8
  </head>
9
 
10
  <body>
 
27
  id="MathJax-script"
28
  src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
29
  ></script>
30
+ <script type="module" src="/src/main.jsx"></script>
31
  </body>
32
  </html>
package.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "smollm-webgpu",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@huggingface/transformers": "3.7.1",
14
+ "dompurify": "^3.1.2",
15
+ "marked": "^12.0.2",
16
+ "react": "^18.3.1",
17
+ "react-dom": "^18.3.1"
18
+ },
19
+ "devDependencies": {
20
+ "@eslint/js": "^9.9.0",
21
+ "@types/react": "^18.3.3",
22
+ "@types/react-dom": "^18.3.0",
23
+ "@vitejs/plugin-react": "^4.3.1",
24
+ "autoprefixer": "^10.4.20",
25
+ "eslint": "^9.9.0",
26
+ "eslint-plugin-react": "^7.35.0",
27
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
28
+ "eslint-plugin-react-refresh": "^0.4.9",
29
+ "globals": "^15.9.0",
30
+ "postcss": "^8.4.41",
31
+ "tailwindcss": "^3.4.10",
32
+ "vite": "^6.2.0"
33
+ }
34
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
public/banner.png ADDED

Git LFS Details

  • SHA256: 7bcf20f91474550f002fce67679f55b11adbcddea83be02e0a9e67d1532e1f4e
  • Pointer size: 131 Bytes
  • Size of remote file: 960 kB
public/logo.png ADDED

Git LFS Details

  • SHA256: 1c7b812a066b72e3fcdd86174a85e316f3b9b4588e30c352e38b7ea348d0f7c3
  • Pointer size: 131 Bytes
  • Size of remote file: 297 kB
src/App.jsx ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState, useRef } from "react";
2
+
3
+ import Chat from "./components/Chat";
4
+ import ArrowRightIcon from "./components/icons/ArrowRightIcon";
5
+ import StopIcon from "./components/icons/StopIcon";
6
+ import Progress from "./components/Progress";
7
+ import ImageIcon from "./components/icons/ImageIcon";
8
+ import ImagePreview from "./components/ImagePreview";
9
+
10
+ const IS_WEBGPU_AVAILABLE = !!navigator.gpu;
11
+ const STICKY_SCROLL_THRESHOLD = 120;
12
+ const EXAMPLES = [
13
+ {
14
+ display: "Generate an image of a cute baby fox.",
15
+ prompt:
16
+ "/imagine A cute and adorable baby fox with big brown eyes, autumn leaves in the background enchanting, immortal, fluffy, shiny mane, Petals, fairyism, unreal engine 5 and Octane Render, highly detailed, photorealistic, cinematic, natural colors.",
17
+ },
18
+ {
19
+ prompt: "Convert the formula into latex code.",
20
+ image:
21
+ "https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/quadratic_formula.png",
22
+ },
23
+ {
24
+ prompt: "What is the difference between AI and ML?",
25
+ },
26
+ {
27
+ prompt: "Write python code to compute the nth fibonacci number.",
28
+ },
29
+ ];
30
+
31
+ function App() {
32
+ // Create a reference to the worker object.
33
+ const worker = useRef(null);
34
+
35
+ const textareaRef = useRef(null);
36
+ const chatContainerRef = useRef(null);
37
+ const imageUploadRef = useRef(null);
38
+
39
+ // Model loading and progress
40
+ const [status, setStatus] = useState(null);
41
+ const [error, setError] = useState(null);
42
+ const [loadingMessage, setLoadingMessage] = useState("");
43
+ const [progressItems, setProgressItems] = useState([]);
44
+ const [isRunning, setIsRunning] = useState(false);
45
+
46
+ // Inputs and outputs
47
+ const [input, setInput] = useState("");
48
+ const [image, setImage] = useState(null);
49
+ const [messages, setMessages] = useState([]);
50
+ const [tps, setTps] = useState(null);
51
+ const [numTokens, setNumTokens] = useState(null);
52
+ const [imageProgress, setImageProgress] = useState(null);
53
+ const [imageGenerationTime, setImageGenerationTime] = useState(null);
54
+
55
+ function onEnter(message, img) {
56
+ setMessages((prev) => [
57
+ ...prev,
58
+ { role: "user", content: message, image: img ?? image },
59
+ ]);
60
+ setTps(null);
61
+ setIsRunning(true);
62
+ setInput("");
63
+ setImage(null);
64
+ setNumTokens(null);
65
+ setImageProgress(null);
66
+ setImageGenerationTime(null);
67
+ }
68
+
69
+ function onInterrupt() {
70
+ // NOTE: We do not set isRunning to false here because the worker
71
+ // will send a 'complete' message when it is done.
72
+ worker.current.postMessage({ type: "interrupt" });
73
+ }
74
+
75
+ function resizeInput() {
76
+ if (!textareaRef.current) return;
77
+
78
+ const target = textareaRef.current;
79
+ target.style.height = "auto";
80
+ const newHeight = Math.min(Math.max(target.scrollHeight, 24), 200);
81
+ target.style.height = `${newHeight}px`;
82
+ }
83
+
84
+ useEffect(() => {
85
+ resizeInput();
86
+ }, [input]);
87
+
88
+ // We use the `useEffect` hook to setup the worker as soon as the `App` component is mounted.
89
+ useEffect(() => {
90
+ // Create the worker if it does not yet exist.
91
+ if (!worker.current) {
92
+ worker.current = new Worker(new URL("./worker.js", import.meta.url), {
93
+ type: "module",
94
+ });
95
+ worker.current.postMessage({ type: "check" }); // Do a feature check
96
+ }
97
+
98
+ // Create a callback function for messages from the worker thread.
99
+ const onMessageReceived = (e) => {
100
+ switch (e.data.status) {
101
+ // WebGPU feature checking
102
+ case "success":
103
+ setStatus("idle");
104
+ break;
105
+ case "error":
106
+ setError(e.data.data);
107
+ break;
108
+
109
+ case "loading":
110
+ // Model file start load: add a new progress item to the list.
111
+ setStatus("loading");
112
+ setLoadingMessage(e.data.data);
113
+ break;
114
+
115
+ case "initiate":
116
+ setProgressItems((prev) => [...prev, e.data]);
117
+ break;
118
+
119
+ case "progress":
120
+ // Model file progress: update one of the progress items.
121
+ setProgressItems((prev) =>
122
+ prev.map((item) => {
123
+ if (item.file === e.data.file) {
124
+ return { ...item, ...e.data };
125
+ }
126
+ return item;
127
+ }),
128
+ );
129
+ break;
130
+
131
+ case "done":
132
+ // Model file loaded: remove the progress item from the list.
133
+ setProgressItems((prev) =>
134
+ prev.filter((item) => item.file !== e.data.file),
135
+ );
136
+ break;
137
+
138
+ case "ready":
139
+ // Pipeline ready: the worker is ready to accept messages.
140
+ setStatus("ready");
141
+ break;
142
+
143
+ case "start":
144
+ {
145
+ // Start generation
146
+ setMessages((prev) => [
147
+ ...prev,
148
+ { role: "assistant", content: "" },
149
+ ]);
150
+ }
151
+ break;
152
+
153
+ case "text-update":
154
+ // Generation update: update the output text.
155
+ // Parse messages
156
+ const { output, tps, numTokens } = e.data;
157
+ setTps(tps);
158
+ setNumTokens(numTokens);
159
+ setMessages((prev) => {
160
+ const cloned = [...prev];
161
+ const last = cloned.at(-1);
162
+ cloned[cloned.length - 1] = {
163
+ ...last,
164
+ content: last.content + output,
165
+ };
166
+ return cloned;
167
+ });
168
+ break;
169
+
170
+ case "image-update":
171
+ const { blob, progress, time } = e.data;
172
+
173
+ if (blob) {
174
+ // Add image to the last message
175
+ const url = URL.createObjectURL(blob);
176
+ setMessages((prev) => {
177
+ const cloned = [...prev];
178
+ const last = cloned.at(-1);
179
+ cloned[cloned.length - 1] = {
180
+ ...last,
181
+ image: url,
182
+ };
183
+ return cloned;
184
+ });
185
+ } else {
186
+ setImageProgress(progress);
187
+ setImageGenerationTime(time);
188
+ }
189
+ break;
190
+
191
+ case "complete":
192
+ // Generation complete: re-enable the "Generate" button
193
+ setIsRunning(false);
194
+ break;
195
+ }
196
+ };
197
+
198
+ const onErrorReceived = (e) => {
199
+ console.error("Worker error:", e);
200
+ };
201
+
202
+ // Attach the callback function as an event listener.
203
+ worker.current.addEventListener("message", onMessageReceived);
204
+ worker.current.addEventListener("error", onErrorReceived);
205
+
206
+ // Define a cleanup function for when the component is unmounted.
207
+ return () => {
208
+ worker.current.removeEventListener("message", onMessageReceived);
209
+ worker.current.removeEventListener("error", onErrorReceived);
210
+ };
211
+ }, []);
212
+
213
+ // Send the messages to the worker thread whenever the `messages` state changes.
214
+ useEffect(() => {
215
+ if (messages.filter((x) => x.role === "user").length === 0) {
216
+ // No user messages yet: do nothing.
217
+ return;
218
+ }
219
+ if (messages.at(-1).role === "assistant") {
220
+ // Do not update if the last message is from the assistant
221
+ return;
222
+ }
223
+ setTps(null);
224
+ worker.current.postMessage({ type: "generate", data: messages });
225
+ }, [messages, isRunning]);
226
+
227
+ useEffect(() => {
228
+ if (!chatContainerRef.current || !isRunning) return;
229
+ const element = chatContainerRef.current;
230
+ if (
231
+ element.scrollHeight - element.scrollTop - element.clientHeight <
232
+ STICKY_SCROLL_THRESHOLD
233
+ ) {
234
+ element.scrollTop = element.scrollHeight;
235
+ }
236
+ }, [messages, isRunning]);
237
+
238
+ return IS_WEBGPU_AVAILABLE ? (
239
+ <div className="flex flex-col h-screen mx-auto items justify-end text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-900">
240
+ {(status === null || status === "idle") && messages.length === 0 && (
241
+ <div className="h-full overflow-auto scrollbar-thin flex justify-center items-center flex-col relative">
242
+ <div className="flex flex-col items-center mb-1 max-w-[350px] text-center">
243
+ <img
244
+ src="logo.png"
245
+ width="80%"
246
+ height="auto"
247
+ className="block"
248
+ ></img>
249
+ <h1 className="text-5xl font-bold mb-1">Janus WebGPU</h1>
250
+ <h2 className="font-semibold">
251
+ A novel autoregressive framework for unified multimodal
252
+ understanding and generation.
253
+ </h2>
254
+ </div>
255
+
256
+ <div className="flex flex-col items-center px-4">
257
+ <p className="max-w-[452px] mb-4">
258
+ <br />
259
+ You are about to load{" "}
260
+ <a
261
+ href="https://huggingface.co/onnx-community/Janus-1.3B-ONNX"
262
+ target="_blank"
263
+ rel="noreferrer"
264
+ className="font-medium underline"
265
+ >
266
+ Janus-1.3B
267
+ </a>
268
+ , a multimodal vision-language model that is optimized for
269
+ inference on the web. Everything runs 100% locally in your browser
270
+ with{" "}
271
+ <a
272
+ href="https://huggingface.co/docs/transformers.js"
273
+ target="_blank"
274
+ rel="noreferrer"
275
+ className="underline"
276
+ >
277
+ 🤗&nbsp;Transformers.js
278
+ </a>{" "}
279
+ and ONNX Runtime Web, meaning no data is sent to a server. Once
280
+ the model has loaded, it can even be used offline. The source code
281
+ for the demo can be found on{" "}
282
+ <a
283
+ href="https://github.com/huggingface/transformers.js-examples/tree/main/janus-webgpu"
284
+ target="_blank"
285
+ rel="noreferrer"
286
+ className="font-medium underline"
287
+ >
288
+ GitHub
289
+ </a>
290
+ .
291
+ </p>
292
+
293
+ {error && (
294
+ <div className="text-red-500 text-center mb-2">
295
+ <p className="mb-1">
296
+ Unable to load model due to the following error:
297
+ </p>
298
+ <p className="text-sm">{error}</p>
299
+ </div>
300
+ )}
301
+
302
+ {!error && (
303
+ <button
304
+ className="border px-4 py-2 rounded-lg bg-blue-400 text-white hover:bg-blue-500 disabled:bg-blue-100 disabled:cursor-not-allowed select-none"
305
+ onClick={() => {
306
+ worker.current.postMessage({ type: "load" });
307
+ setStatus("loading");
308
+ }}
309
+ disabled={status === null || status === "loading"}
310
+ >
311
+ {status === null ? "Running feature checks..." : "Load model"}
312
+ </button>
313
+ )}
314
+ </div>
315
+ </div>
316
+ )}
317
+ {status === "loading" && (
318
+ <>
319
+ <div className="w-full max-w-[500px] text-left mx-auto p-4 bottom-0 mt-auto">
320
+ <p className="text-center mb-1">{loadingMessage}</p>
321
+ {progressItems.map(({ file, progress, total }, i) => (
322
+ <Progress
323
+ key={i}
324
+ text={file}
325
+ percentage={progress}
326
+ total={total}
327
+ />
328
+ ))}
329
+ </div>
330
+ </>
331
+ )}
332
+
333
+ {status === "ready" && (
334
+ <div
335
+ ref={chatContainerRef}
336
+ className="overflow-y-auto scrollbar-thin w-full flex flex-col items-center h-full"
337
+ >
338
+ <Chat messages={messages} />
339
+ {messages.length === 0 && !image && (
340
+ <div className="flex flex-col center">
341
+ {EXAMPLES.map(({ display, prompt, image }, i) => (
342
+ <div
343
+ key={i}
344
+ className="max-w-[600px] m-1 border dark:border-gray-600 rounded-md p-2 bg-gray-100 dark:bg-gray-700 cursor-pointer"
345
+ onClick={() => onEnter(prompt, image)}
346
+ >
347
+ {display ?? prompt}
348
+ </div>
349
+ ))}
350
+ </div>
351
+ )}
352
+
353
+ <p className="text-center text-sm min-h-6 text-gray-500 dark:text-gray-300">
354
+ {messages.length > 0 && (
355
+ <>
356
+ {tps ? (
357
+ <>
358
+ {!isRunning && (
359
+ <span>
360
+ Generated {numTokens} tokens in{" "}
361
+ {(numTokens / tps).toFixed(2)} seconds&nbsp;&#40;
362
+ </span>
363
+ )}
364
+ <span className="font-medium font-mono text-center mr-1 text-black dark:text-white">
365
+ {tps.toFixed(2)}
366
+ </span>
367
+ <span className="text-gray-500 dark:text-gray-300">
368
+ tokens/second
369
+ </span>
370
+ {!isRunning && <span className="mr-1">&#41;.</span>}
371
+ </>
372
+ ) : (
373
+ imageProgress && (
374
+ <>
375
+ {isRunning ? (
376
+ <>
377
+ <span>Generating image...</span>&nbsp;&#40;
378
+ <span className="font-medium font-mono text-center text-black dark:text-white">
379
+ {(imageProgress * 100).toFixed(2)}%
380
+ </span>
381
+ <span className="mr-1">&#41;</span>
382
+ </>
383
+ ) : (
384
+ <span>
385
+ Generated image in{" "}
386
+ {(imageGenerationTime / 1000).toFixed(2)}{" "}
387
+ seconds.&nbsp;
388
+ </span>
389
+ )}
390
+ </>
391
+ )
392
+ )}
393
+
394
+ {!isRunning && (
395
+ <span
396
+ className="underline cursor-pointer"
397
+ onClick={() => setMessages([])}
398
+ >
399
+ Reset
400
+ </span>
401
+ )}
402
+ </>
403
+ )}
404
+ </p>
405
+ </div>
406
+ )}
407
+
408
+ <div className="mt-2 border dark:bg-gray-700 rounded-lg w-[600px] max-w-[80%] max-h-[200px] mx-auto relative mb-3 flex">
409
+ <label
410
+ htmlFor="file-upload"
411
+ className={
412
+ status === "ready"
413
+ ? "cursor-pointer"
414
+ : "cursor-not-allowed pointer-events-none"
415
+ }
416
+ >
417
+ <ImageIcon
418
+ className={`h-8 w-8 p-1 rounded-md ${status === "ready" ? "text-gray-800 dark:text-gray-100" : "text-gray-400 dark:text-gray-500"} absolute bottom-3 left-1.5`}
419
+ ></ImageIcon>
420
+ <input
421
+ ref={imageUploadRef}
422
+ id="file-upload"
423
+ type="file"
424
+ accept="image/*"
425
+ className="hidden"
426
+ onInput={(e) => {
427
+ const file = e.target.files[0];
428
+ if (!file) {
429
+ return;
430
+ }
431
+
432
+ const reader = new FileReader();
433
+
434
+ // Set up a callback when the file is loaded
435
+ reader.onload = (e2) => {
436
+ setImage(e2.target.result);
437
+ e.target.value = "";
438
+ };
439
+
440
+ reader.readAsDataURL(file);
441
+ }}
442
+ ></input>
443
+ </label>
444
+ <div className="w-full flex flex-col">
445
+ {image && (
446
+ <ImagePreview
447
+ onRemove={() => {
448
+ setImage(null);
449
+ }}
450
+ src={image}
451
+ className="w-20 h-20 min-w-20 min-h-20 relative p-2"
452
+ />
453
+ )}
454
+
455
+ <textarea
456
+ ref={textareaRef}
457
+ className="scrollbar-thin w-full pl-11 pr-12 dark:bg-gray-700 py-4 rounded-lg bg-transparent border-none outline-none text-gray-800 disabled:text-gray-400 dark:text-gray-100 placeholder-gray-500 disabled:placeholder-gray-200 dark:placeholder-gray-300 dark:disabled:placeholder-gray-500 resize-none disabled:cursor-not-allowed"
458
+ placeholder="Type message or use '/imagine <prompt>' to generate an image."
459
+ type="text"
460
+ rows={1}
461
+ value={input}
462
+ disabled={status !== "ready"}
463
+ title={
464
+ status === "ready" ? "Model is ready" : "Model not loaded yet"
465
+ }
466
+ onKeyDown={(e) => {
467
+ if (
468
+ input.length > 0 &&
469
+ !isRunning &&
470
+ e.key === "Enter" &&
471
+ !e.shiftKey
472
+ ) {
473
+ e.preventDefault(); // Prevent default behavior of Enter key
474
+ onEnter(input, image);
475
+ }
476
+ }}
477
+ onInput={(e) => setInput(e.target.value)}
478
+ />
479
+ </div>
480
+ {isRunning ? (
481
+ <div className="cursor-pointer" onClick={onInterrupt}>
482
+ <StopIcon className="h-8 w-8 p-1 rounded-md text-gray-800 dark:text-gray-100 absolute right-3 bottom-3" />
483
+ </div>
484
+ ) : input.length > 0 ? (
485
+ <div className="cursor-pointer" onClick={() => onEnter(input)}>
486
+ <ArrowRightIcon
487
+ className={`h-8 w-8 p-1 bg-gray-800 dark:bg-gray-100 text-white dark:text-black rounded-md absolute right-3 bottom-3`}
488
+ />
489
+ </div>
490
+ ) : (
491
+ <div>
492
+ <ArrowRightIcon
493
+ className={`h-8 w-8 p-1 bg-gray-200 dark:bg-gray-600 text-gray-50 dark:text-gray-800 rounded-md absolute right-3 bottom-3`}
494
+ />
495
+ </div>
496
+ )}
497
+ </div>
498
+
499
+ <p className="text-xs text-gray-400 text-center mb-3">
500
+ Disclaimer: Generated content may be inaccurate or false.
501
+ </p>
502
+ </div>
503
+ ) : (
504
+ <div className="fixed w-screen h-screen bg-black z-10 bg-opacity-[92%] text-white text-2xl font-semibold flex justify-center items-center text-center">
505
+ WebGPU is not supported
506
+ <br />
507
+ by this browser :&#40;
508
+ </div>
509
+ );
510
+ }
511
+
512
+ export default App;
src/components/Chat.css ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @scope (.markdown) {
2
+ /* Code blocks */
3
+ pre {
4
+ margin: 0.5rem 0;
5
+ white-space: break-spaces;
6
+ }
7
+
8
+ code {
9
+ padding: 0.2em 0.4em;
10
+ border-radius: 4px;
11
+ font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
12
+ font-size: 0.9em;
13
+ }
14
+
15
+ pre,
16
+ code {
17
+ background-color: #f2f2f2;
18
+ }
19
+
20
+ @media (prefers-color-scheme: dark) {
21
+ pre,
22
+ code {
23
+ background-color: #333;
24
+ }
25
+ }
26
+
27
+ pre:has(code) {
28
+ padding: 1rem 0.5rem;
29
+ }
30
+
31
+ pre > code {
32
+ padding: 0;
33
+ }
34
+
35
+ /* Headings */
36
+ h1,
37
+ h2,
38
+ h3,
39
+ h4,
40
+ h5,
41
+ h6 {
42
+ font-weight: 600;
43
+ line-height: 1.2;
44
+ }
45
+
46
+ h1 {
47
+ font-size: 2em;
48
+ margin: 1rem 0;
49
+ }
50
+
51
+ h2 {
52
+ font-size: 1.5em;
53
+ margin: 0.83rem 0;
54
+ }
55
+
56
+ h3 {
57
+ font-size: 1.25em;
58
+ margin: 0.67rem 0;
59
+ }
60
+
61
+ h4 {
62
+ font-size: 1em;
63
+ margin: 0.5rem 0;
64
+ }
65
+
66
+ h5 {
67
+ font-size: 0.875em;
68
+ margin: 0.33rem 0;
69
+ }
70
+
71
+ h6 {
72
+ font-size: 0.75em;
73
+ margin: 0.25rem 0;
74
+ }
75
+
76
+ h1,
77
+ h2,
78
+ h3,
79
+ h4,
80
+ h5,
81
+ h6:first-child {
82
+ margin-top: 0;
83
+ }
84
+
85
+ /* Unordered List */
86
+ ul {
87
+ list-style-type: disc;
88
+ margin-left: 1.5rem;
89
+ }
90
+
91
+ /* Ordered List */
92
+ ol {
93
+ list-style-type: decimal;
94
+ margin-left: 1.5rem;
95
+ }
96
+
97
+ /* List Items */
98
+ li {
99
+ margin: 0.25rem 0;
100
+ }
101
+
102
+ p:not(:first-child) {
103
+ margin-top: 0.75rem;
104
+ }
105
+
106
+ p:not(:last-child) {
107
+ margin-bottom: 0.75rem;
108
+ }
109
+ }
src/components/Chat.jsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { marked } from "marked";
2
+ import DOMPurify from "dompurify";
3
+
4
+ import BotIcon from "./icons/BotIcon";
5
+ import UserIcon from "./icons/UserIcon";
6
+
7
+ import "./Chat.css";
8
+ import { useEffect } from "react";
9
+
10
+ function render(text) {
11
+ return DOMPurify.sanitize(marked.parse(text));
12
+ }
13
+
14
+ export default function Chat({ messages }) {
15
+ const empty = messages.length === 0;
16
+
17
+ useEffect(() => {
18
+ window.MathJax.typeset();
19
+ }, [messages]);
20
+
21
+ return (
22
+ <div
23
+ className={`flex-1 p-6 max-w-[960px] w-full ${empty ? "flex flex-col items-center justify-end" : "space-y-4"}`}
24
+ >
25
+ {empty ? (
26
+ <div className="text-xl">Ready!</div>
27
+ ) : (
28
+ messages.map((msg, i) => (
29
+ <div key={`message-${i}`} className="flex items-start space-x-4">
30
+ {msg.role === "assistant" ? (
31
+ <>
32
+ <BotIcon className="h-6 w-6 min-h-6 min-w-6 my-3 text-gray-500 dark:text-gray-300" />
33
+ <div className="bg-gray-200 dark:bg-gray-700 rounded-lg p-4">
34
+ <p className="min-h-6 text-gray-800 dark:text-gray-200 overflow-wrap-anywhere">
35
+ {msg.image ? (
36
+ <img
37
+ src={msg.image}
38
+ className="max-w-full w-[384px] rounded-md"
39
+ />
40
+ ) : msg.content.length > 0 ? (
41
+ <span
42
+ className="markdown"
43
+ dangerouslySetInnerHTML={{
44
+ __html: render(msg.content),
45
+ }}
46
+ />
47
+ ) : (
48
+ <span className="h-6 flex items-center gap-1">
49
+ <span className="w-2.5 h-2.5 bg-gray-600 dark:bg-gray-300 rounded-full animate-pulse"></span>
50
+ <span className="w-2.5 h-2.5 bg-gray-600 dark:bg-gray-300 rounded-full animate-pulse animation-delay-200"></span>
51
+ <span className="w-2.5 h-2.5 bg-gray-600 dark:bg-gray-300 rounded-full animate-pulse animation-delay-400"></span>
52
+ </span>
53
+ )}
54
+ </p>
55
+ </div>
56
+ </>
57
+ ) : (
58
+ <>
59
+ <UserIcon className="h-6 w-6 min-h-6 min-w-6 my-3 text-gray-500 dark:text-gray-300" />
60
+ <div className="bg-blue-500 text-white rounded-lg p-4">
61
+ {msg.image && (
62
+ <img
63
+ src={msg.image}
64
+ className="max-w-full max-h-64 rounded-md mb-3"
65
+ />
66
+ )}
67
+ <p className="min-h-6 overflow-wrap-anywhere">
68
+ {msg.content}
69
+ </p>
70
+ </div>
71
+ </>
72
+ )}
73
+ </div>
74
+ ))
75
+ )}
76
+ </div>
77
+ );
78
+ }
src/components/ImagePreview.jsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import CrossIcon from "./icons/CrossIcon";
3
+
4
+ export default function ImagePreview({ src, onRemove, ...props }) {
5
+ const [hover, setHover] = useState(false);
6
+
7
+ return (
8
+ <div
9
+ {...props}
10
+ onMouseEnter={() => setHover(true)}
11
+ onMouseLeave={() => setHover(false)}
12
+ >
13
+ <CrossIcon
14
+ onClick={onRemove}
15
+ className={`absolute top-0 right-0 cursor-pointer dark:fill-gray-400 dark:text-gray-100 fill-gray-200 text-gray-800 ${hover ? "" : "hidden"}`}
16
+ />
17
+ <img
18
+ src={src}
19
+ alt="Upload preview"
20
+ className="w-full h-full object-cover rounded-md"
21
+ />
22
+ </div>
23
+ );
24
+ }
src/components/Progress.jsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function formatBytes(size) {
2
+ const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
3
+ return (
4
+ +(size / Math.pow(1024, i)).toFixed(2) * 1 +
5
+ ["B", "kB", "MB", "GB", "TB"][i]
6
+ );
7
+ }
8
+
9
+ export default function Progress({ text, percentage, total }) {
10
+ percentage ??= 0;
11
+ return (
12
+ <div className="w-full bg-gray-100 dark:bg-gray-700 text-left rounded-lg overflow-hidden mb-0.5">
13
+ <div
14
+ className="bg-blue-400 whitespace-nowrap px-1 text-sm"
15
+ style={{ width: `${percentage}%` }}
16
+ >
17
+ {text} ({percentage.toFixed(2)}%
18
+ {isNaN(total) ? "" : ` of ${formatBytes(total)}`})
19
+ </div>
20
+ </div>
21
+ );
22
+ }
src/components/icons/ArrowRightIcon.jsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function ArrowRightIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="M5 12h14" />
16
+ <path d="m12 5 7 7-7 7" />
17
+ </svg>
18
+ );
19
+ }
src/components/icons/BotIcon.jsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function BotIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="M12 8V4H8" />
16
+ <rect width="16" height="12" x="4" y="8" rx="2" />
17
+ <path d="M2 14h2" />
18
+ <path d="M20 14h2" />
19
+ <path d="M15 13v2" />
20
+ <path d="M9 13v2" />
21
+ </svg>
22
+ );
23
+ }
src/components/icons/CrossIcon.jsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function CrossIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
16
+ </svg>
17
+ );
18
+ }
src/components/icons/ImageIcon.jsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function ImageIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
16
+ </svg>
17
+ );
18
+ }
src/components/icons/StopIcon.jsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function StopIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
16
+ <path
17
+ fill="currentColor"
18
+ d="M9 9.563C9 9.252 9.252 9 9.563 9h4.874c.311 0 .563.252.563.563v4.874c0 .311-.252.563-.563.563H9.564A.562.562 0 0 1 9 14.437V9.564Z"
19
+ />
20
+ </svg>
21
+ );
22
+ }
src/components/icons/UserIcon.jsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function UserIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
16
+ <circle cx="12" cy="7" r="4" />
17
+ </svg>
18
+ );
19
+ }
src/index.css ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer utilities {
6
+ .scrollbar-thin::-webkit-scrollbar {
7
+ @apply w-2;
8
+ }
9
+
10
+ .scrollbar-thin::-webkit-scrollbar-track {
11
+ @apply rounded-full bg-gray-100 dark:bg-gray-700;
12
+ }
13
+
14
+ .scrollbar-thin::-webkit-scrollbar-thumb {
15
+ @apply rounded-full bg-gray-300 dark:bg-gray-600;
16
+ }
17
+
18
+ .scrollbar-thin::-webkit-scrollbar-thumb:hover {
19
+ @apply bg-gray-500;
20
+ }
21
+
22
+ .animation-delay-200 {
23
+ animation-delay: 200ms;
24
+ }
25
+ .animation-delay-400 {
26
+ animation-delay: 400ms;
27
+ }
28
+
29
+ .overflow-wrap-anywhere {
30
+ overflow-wrap: anywhere;
31
+ }
32
+ }
src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import App from "./App.jsx";
4
+ import "./index.css";
5
+
6
+ ReactDOM.createRoot(document.getElementById("root")).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
src/worker.js ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ AutoProcessor,
3
+ MultiModalityCausalLM,
4
+ BaseStreamer,
5
+ TextStreamer,
6
+ InterruptableStoppingCriteria,
7
+ } from "@huggingface/transformers";
8
+
9
+ // Define constants
10
+ const IMAGE_GENERATION_COMMAND_PREFIX = "/imagine ";
11
+ const MAX_NEW_TEXT_TOKENS = 1024;
12
+
13
+ /**
14
+ * Helper function to perform WebGPU feature detection
15
+ */
16
+ let fp16_supported = false;
17
+ async function check() {
18
+ try {
19
+ const adapter = await navigator.gpu.requestAdapter();
20
+ if (!adapter) {
21
+ throw new Error("WebGPU is not supported (no adapter found)");
22
+ }
23
+ fp16_supported = adapter.features.has("shader-f16");
24
+ self.postMessage({
25
+ status: "success",
26
+ data: fp16_supported,
27
+ });
28
+ } catch (e) {
29
+ self.postMessage({
30
+ status: "error",
31
+ data: e.toString(),
32
+ });
33
+ }
34
+ }
35
+
36
+ /**
37
+ * This class uses the Singleton pattern to enable lazy-loading of the pipeline
38
+ */
39
+ class ImageGenerationPipeline {
40
+ static model_id = "onnx-community/Janus-1.3B-ONNX";
41
+
42
+ static async getInstance(progress_callback = null) {
43
+ this.processor ??= AutoProcessor.from_pretrained(this.model_id, {
44
+ progress_callback,
45
+ });
46
+
47
+ this.model ??= MultiModalityCausalLM.from_pretrained(this.model_id, {
48
+ dtype: fp16_supported
49
+ ? {
50
+ prepare_inputs_embeds: "q4",
51
+ language_model: "q4f16",
52
+ lm_head: "fp16",
53
+ gen_head: "fp16",
54
+ gen_img_embeds: "fp16",
55
+ image_decode: "fp32",
56
+ }
57
+ : {
58
+ prepare_inputs_embeds: "fp32",
59
+ language_model: "q4",
60
+ lm_head: "fp32",
61
+ gen_head: "fp32",
62
+ gen_img_embeds: "fp32",
63
+ image_decode: "fp32",
64
+ },
65
+ device: {
66
+ prepare_inputs_embeds: "wasm", // TODO use "webgpu" when bug is fixed
67
+ language_model: "webgpu",
68
+ lm_head: "webgpu",
69
+ gen_head: "webgpu",
70
+ gen_img_embeds: "webgpu",
71
+ image_decode: "webgpu",
72
+ },
73
+ progress_callback,
74
+ });
75
+
76
+ return Promise.all([this.processor, this.model]);
77
+ }
78
+ }
79
+
80
+ class ProgressStreamer extends BaseStreamer {
81
+ constructor(total, on_progress) {
82
+ super();
83
+ this.total = total;
84
+ this.on_progress = on_progress;
85
+
86
+ this.count = null;
87
+ this.start_time = null;
88
+ }
89
+
90
+ put(value) {
91
+ if (this.count === null) {
92
+ // Ignore the first batch of tokens (prompt)
93
+ this.count = 0;
94
+ this.start_time = performance.now();
95
+ return;
96
+ }
97
+
98
+ const progress = ++this.count / this.total;
99
+
100
+ this.on_progress({
101
+ count: this.count,
102
+ total: this.total,
103
+ progress,
104
+ time: performance.now() - this.start_time,
105
+ });
106
+ }
107
+
108
+ end() {
109
+ /* no nothing */
110
+ }
111
+ }
112
+
113
+ const stopping_criteria = new InterruptableStoppingCriteria();
114
+
115
+ async function generate(messages) {
116
+ // For this demo, we only respond to the last message
117
+ const message = messages.at(-1);
118
+
119
+ // Tell the main thread we are starting
120
+ self.postMessage({ status: "start" });
121
+
122
+ // Load the pipeline
123
+ const [processor, model] = await ImageGenerationPipeline.getInstance();
124
+
125
+ // Determine if the user wants to generate an image or text
126
+ if (message.content.startsWith(IMAGE_GENERATION_COMMAND_PREFIX)) {
127
+ const text = message.content.replace(IMAGE_GENERATION_COMMAND_PREFIX, "");
128
+
129
+ const conversation = [
130
+ {
131
+ role: "User", // uses title case
132
+ content: text,
133
+ },
134
+ ];
135
+ const inputs = await processor(conversation, {
136
+ chat_template: "text_to_image",
137
+ });
138
+
139
+ const callback_function = (output) => {
140
+ self.postMessage({
141
+ status: "image-update",
142
+ ...output,
143
+ });
144
+ };
145
+
146
+ const num_image_tokens = processor.num_image_tokens;
147
+ const streamer = new ProgressStreamer(num_image_tokens, callback_function);
148
+
149
+ const outputs = await model.generate_images({
150
+ ...inputs,
151
+ min_new_tokens: num_image_tokens,
152
+ max_new_tokens: num_image_tokens,
153
+ do_sample: true,
154
+ streamer,
155
+ });
156
+
157
+ const blob = await outputs[0].toBlob();
158
+
159
+ // Send the output back to the main thread
160
+ self.postMessage({
161
+ status: "image-update",
162
+ blob,
163
+ });
164
+ } else {
165
+ const inputs = await processor(
166
+ message.image
167
+ ? [
168
+ {
169
+ role: "User",
170
+ content: "<image_placeholder>\n" + message.content,
171
+ images: [message.image],
172
+ },
173
+ ]
174
+ : [
175
+ {
176
+ role: "System",
177
+ content:
178
+ "You are a helpful assistant. Answer the user's questions in a concise manner.",
179
+ },
180
+ {
181
+ role: "User",
182
+ content: message.content,
183
+ },
184
+ ],
185
+ );
186
+
187
+ let startTime;
188
+ let numTokens = 0;
189
+ let tps;
190
+ const token_callback_function = () => {
191
+ startTime ??= performance.now();
192
+
193
+ if (numTokens++ > 0) {
194
+ tps = (numTokens / (performance.now() - startTime)) * 1000;
195
+ }
196
+ };
197
+ const callback_function = (output) => {
198
+ self.postMessage({
199
+ status: "text-update",
200
+ output,
201
+ tps,
202
+ numTokens,
203
+ });
204
+ };
205
+
206
+ const streamer = new TextStreamer(processor.tokenizer, {
207
+ skip_prompt: true,
208
+ skip_special_tokens: true,
209
+ callback_function,
210
+ token_callback_function,
211
+ });
212
+
213
+ // Generate response
214
+ const outputs = await model.generate({
215
+ ...inputs,
216
+ max_new_tokens: MAX_NEW_TEXT_TOKENS,
217
+ do_sample: false,
218
+ streamer,
219
+ stopping_criteria,
220
+ });
221
+ }
222
+
223
+ // Tell the main thread we are done
224
+ self.postMessage({
225
+ status: "complete",
226
+ });
227
+ }
228
+
229
+ async function load() {
230
+ self.postMessage({
231
+ status: "loading",
232
+ data: "Loading model...",
233
+ });
234
+
235
+ // Load the pipeline and save it for future use.
236
+ const [processor, model] = await ImageGenerationPipeline.getInstance((x) => {
237
+ // We also add a progress callback to the pipeline so that we can
238
+ // track model loading.
239
+ self.postMessage(x);
240
+ });
241
+
242
+ self.postMessage({ status: "ready" });
243
+ }
244
+
245
+ // Listen for messages from the main thread
246
+ self.addEventListener("message", async (e) => {
247
+ const { type, data } = e.data;
248
+
249
+ switch (type) {
250
+ case "check":
251
+ check();
252
+ break;
253
+
254
+ case "load":
255
+ load();
256
+ break;
257
+
258
+ case "generate":
259
+ stopping_criteria.reset();
260
+ generate(data);
261
+ break;
262
+
263
+ case "interrupt":
264
+ stopping_criteria.interrupt();
265
+ break;
266
+
267
+ case "reset":
268
+ stopping_criteria.reset();
269
+ break;
270
+ }
271
+ });
tailwind.config.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4
+ theme: {
5
+ extend: {},
6
+ },
7
+ plugins: [],
8
+ };
vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ });