Update files
#2
by
Xenova
HF Staff
- opened
- .gitattributes +2 -0
- README.md +3 -1
- assets/index-Cy-lD5Cm.css +0 -1
- assets/index-FQf4wfd9.js +0 -0
- assets/worker-BMn4lUIh.js +0 -0
- eslint.config.js +38 -0
- index.html +1 -2
- package.json +34 -0
- postcss.config.js +6 -0
- public/banner.png +3 -0
- public/logo.png +3 -0
- src/App.jsx +512 -0
- src/components/Chat.css +109 -0
- src/components/Chat.jsx +78 -0
- src/components/ImagePreview.jsx +24 -0
- src/components/Progress.jsx +22 -0
- src/components/icons/ArrowRightIcon.jsx +19 -0
- src/components/icons/BotIcon.jsx +23 -0
- src/components/icons/CrossIcon.jsx +18 -0
- src/components/icons/ImageIcon.jsx +18 -0
- src/components/icons/StopIcon.jsx +22 -0
- src/components/icons/UserIcon.jsx +19 -0
- src/index.css +32 -0
- src/main.jsx +10 -0
- src/worker.js +271 -0
- tailwind.config.js +8 -0
- vite.config.js +7 -0
.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
|
public/logo.png
ADDED
![]() |
Git LFS Details
|
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 |
+
🤗 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 (
|
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">).</span>}
|
371 |
+
</>
|
372 |
+
) : (
|
373 |
+
imageProgress && (
|
374 |
+
<>
|
375 |
+
{isRunning ? (
|
376 |
+
<>
|
377 |
+
<span>Generating image...</span> (
|
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">)</span>
|
382 |
+
</>
|
383 |
+
) : (
|
384 |
+
<span>
|
385 |
+
Generated image in{" "}
|
386 |
+
{(imageGenerationTime / 1000).toFixed(2)}{" "}
|
387 |
+
seconds.
|
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 :(
|
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 |
+
});
|