diff --git a/Makefile b/Makefile index 0e25c3a..57024cb 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,16 @@ build-linux: @echo ---- build for linux wails build -upx -ldflags "-s -w" -platform linux/amd64 +build-web: + @echo ---- build for web + cd frontend && npm run build + dev: wails dev +dev-web: + cd frontend && npm run dev + +preview: + cd frontend && npm run preview + diff --git a/frontend/index.html b/frontend/index.html index cbc94f0..82fe91b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,9 +1,10 @@ - - - RWKV-Runner + + + RWKV-Runner +
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3987a2f..49ef74d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@primer/octicons-react": "^19.1.0", "chart.js": "^4.3.0", "classnames": "^2.3.2", + "file-saver": "^2.0.5", "github-markdown-css": "^5.2.0", "html-midi-player": "^1.5.0", "i18next": "^22.4.15", @@ -37,6 +38,7 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@types/file-saver": "^2.0.7", "@types/react": "^18.2.6", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^18.2.4", @@ -74,12 +76,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -125,12 +128,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -159,22 +162,22 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -245,9 +248,9 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { "@babel/types": "^7.22.5" @@ -266,9 +269,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -298,13 +301,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -312,9 +315,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -365,33 +368,33 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -400,13 +403,13 @@ } }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2279,6 +2282,12 @@ "@types/ms": "*" } }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true + }, "node_modules/@types/hast": { "version": "2.3.4", "resolved": "https://registry.npmmirror.com/@types/hast/-/hast-2.3.4.tgz", @@ -2288,9 +2297,9 @@ } }, "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" @@ -2371,9 +2380,9 @@ } }, "node_modules/@types/react-redux": { - "version": "7.1.25", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz", - "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==", + "version": "7.1.29", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.29.tgz", + "integrity": "sha512-orHCOWqBBQ1LP1uD6JVdXL+ZRTEWhGGne+VOPcXef03rC+QYdzktLhxR3ozymPDyZK0CNCUuQs9tyQhfg1ku+w==", "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -2649,10 +2658,24 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001482", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001482.tgz", - "integrity": "sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ==", - "dev": true + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] }, "node_modules/ccount": { "version": "2.0.1", @@ -3232,6 +3255,11 @@ "resolved": "https://registry.npmjs.org/fft.js/-/fft.js-4.0.4.tgz", "integrity": "sha512-f9c00hphOgeQTlDyavwTtu6RiK8AIFjD6+jvXkNkpeQ7rirK3uFWVpalkoS4LAwbdX7mfZ8aoBfFVQX1Re/8aw==" }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz", @@ -4591,10 +4619,24 @@ } }, "node_modules/postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -4696,9 +4738,9 @@ "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==" }, "node_modules/protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", diff --git a/frontend/package.json b/frontend/package.json index 58cf001..cbdecc8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@primer/octicons-react": "^19.1.0", "chart.js": "^4.3.0", "classnames": "^2.3.2", + "file-saver": "^2.0.5", "github-markdown-css": "^5.2.0", "html-midi-player": "^1.5.0", "i18next": "^22.4.15", @@ -38,6 +39,7 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@types/file-saver": "^2.0.7", "@types/react": "^18.2.6", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^18.2.4", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 829546c..a02778b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -26,18 +26,22 @@ import { FluentProvider, Tab, TabList, webDarkTheme, webLightTheme } from '@fluentui/react-components'; import { FC, useEffect, useState } from 'react'; import { Route, Routes, useLocation, useNavigate } from 'react-router'; -import { pages } from './pages'; +import { pages as clientPages } from './pages'; import { useMediaQuery } from 'usehooks-ts'; import commonStore from './stores/commonStore'; import { observer } from 'mobx-react-lite'; import { useTranslation } from 'react-i18next'; import { CustomToastContainer } from './components/CustomToastContainer'; +import { LazyImportComponent } from './components/LazyImportComponent'; const App: FC = observer(() => { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const mq = useMediaQuery('(min-width: 640px)'); + const pages = commonStore.platform === 'web' ? clientPages.filter(page => + !['/configs', '/models', '/downloads', '/train', '/about'].some(path => page.path === path) + ) : clientPages; const [path, setPath] = useState(pages[0].path); @@ -82,7 +86,7 @@ const App: FC = observer(() => {
{pages.map(({ path, element }, index) => ( - + } /> ))}
diff --git a/frontend/src/_locales/ja/main.json b/frontend/src/_locales/ja/main.json index 66e1baf..c115727 100644 --- a/frontend/src/_locales/ja/main.json +++ b/frontend/src/_locales/ja/main.json @@ -262,5 +262,7 @@ "is as follows. When replying to me, consider the file content and respond accordingly:": "の内容は以下の通りです。私に返信する際は、ファイルの内容を考慮して適切に返信してください:", "What's the file name": "ファイル名は何ですか", "The file name is: ": "ファイル名は次のとおりです: ", - "Port is occupied. Change it in Configs page or close the program that occupies the port.": "ポートが占有されています。設定ページで変更するか、ポートを占有しているプログラムを終了してください。" + "Port is occupied. Change it in Configs page or close the program that occupies the port.": "ポートが占有されています。設定ページで変更するか、ポートを占有しているプログラムを終了してください。", + "Loading...": "読み込み中...", + "Hello, what can I do for you?": "こんにちは、何かお手伝いできますか?" } \ No newline at end of file diff --git a/frontend/src/_locales/zh-hans/main.json b/frontend/src/_locales/zh-hans/main.json index f3eceb1..0dac207 100644 --- a/frontend/src/_locales/zh-hans/main.json +++ b/frontend/src/_locales/zh-hans/main.json @@ -262,5 +262,7 @@ "is as follows. When replying to me, consider the file content and respond accordingly:": "内容如下。回复时考虑文件内容并做出相应回复:", "What's the file name": "文件名是什么", "The file name is: ": "文件名是:", - "Port is occupied. Change it in Configs page or close the program that occupies the port.": "端口被占用。请在配置页面更改端口,或关闭占用端口的程序" + "Port is occupied. Change it in Configs page or close the program that occupies the port.": "端口被占用。请在配置页面更改端口,或关闭占用端口的程序", + "Loading...": "加载中...", + "Hello, what can I do for you?": "你好,有什么要我帮忙的吗?" } \ No newline at end of file diff --git a/frontend/src/components/DialogButton.tsx b/frontend/src/components/DialogButton.tsx index 0886fae..db35c4a 100644 --- a/frontend/src/components/DialogButton.tsx +++ b/frontend/src/components/DialogButton.tsx @@ -1,4 +1,4 @@ -import { FC, ReactElement } from 'react'; +import React, { FC, ReactElement } from 'react'; import { Button, Dialog, @@ -11,7 +11,9 @@ import { } from '@fluentui/react-components'; import { ToolTipButton } from './ToolTipButton'; import { useTranslation } from 'react-i18next'; -import MarkdownRender from './MarkdownRender'; +import { LazyImportComponent } from './LazyImportComponent'; + +const MarkdownRender = React.lazy(() => import('./MarkdownRender')); export const DialogButton: FC<{ text?: string | null @@ -45,7 +47,9 @@ export const DialogButton: FC<{ { markdown ? - {contentText} : + + {contentText} + : contentText } diff --git a/frontend/src/components/LazyImportComponent.tsx b/frontend/src/components/LazyImportComponent.tsx new file mode 100644 index 0000000..3c154d2 --- /dev/null +++ b/frontend/src/components/LazyImportComponent.tsx @@ -0,0 +1,20 @@ +import { FC, LazyExoticComponent, ReactNode, Suspense } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface LazyImportComponentProps { + lazyChildren: LazyExoticComponent>; + lazyProps?: any; + children?: ReactNode; +} + +export const LazyImportComponent: FC = (props) => { + const { t } = useTranslation(); + + return ( + {t('Loading...')}}> + + {props.children} + + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/MarkdownRender.tsx b/frontend/src/components/MarkdownRender.tsx index 3983cf9..f765131 100644 --- a/frontend/src/components/MarkdownRender.tsx +++ b/frontend/src/components/MarkdownRender.tsx @@ -21,7 +21,7 @@ const Hyperlink: FC = ({ href, children }) => { ); }; -export const MarkdownRender: FC = (props) => { +const MarkdownRender: FC = (props) => { return (
{ const { t } = useTranslation(); const port = commonStore.getCurrentModelConfig().apiParameters.apiPort; - return ( + return commonStore.platform === 'web' ? +
:
@@ -42,5 +43,5 @@ export const WorkHeader: FC = observer(() => {
- ); + ; }); \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index c8a60d6..bbf8af8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,3 +1,4 @@ +import './webWails'; import React from 'react'; import { createRoot } from 'react-dom/client'; import './style.scss'; @@ -6,7 +7,6 @@ import App from './App'; import { HashRouter } from 'react-router-dom'; import { startup } from './startup'; import './_locales/i18n-react'; -import 'html-midi-player'; import { WindowShow } from '../wailsjs/runtime'; startup().then(() => { diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx index 70bafc5..608dab8 100644 --- a/frontend/src/pages/About.tsx +++ b/frontend/src/pages/About.tsx @@ -5,9 +5,7 @@ import MarkdownRender from '../components/MarkdownRender'; import { observer } from 'mobx-react-lite'; import commonStore from '../stores/commonStore'; -export type AboutContent = { [lang: string]: string } - -export const About: FC = observer(() => { +const About: FC = observer(() => { const { t } = useTranslation(); const lang: string = commonStore.settings.language; @@ -21,3 +19,5 @@ export const About: FC = observer(() => { } /> ); }); + +export default About; diff --git a/frontend/src/pages/Chat.tsx b/frontend/src/pages/Chat.tsx index 05d66ce..b4c654f 100644 --- a/frontend/src/pages/Chat.tsx +++ b/frontend/src/pages/Chat.tsx @@ -28,46 +28,16 @@ import { OpenFileFolder, OpenOpenFileDialog, OpenSaveFileDialog } from '../../wa import { absPathAsset, bytesToReadable, toastWithButton } from '../utils'; import { PresetsButton } from './PresetsManager/PresetsButton'; import { useMediaQuery } from 'usehooks-ts'; +import { botName, ConversationMessage, MessageType, userName, welcomeUuid } from '../types/chat'; -export const userName = 'M E'; -export const botName = 'A I'; +let chatSseControllers: { + [id: string]: AbortController +} = {}; -export const welcomeUuid = 'welcome'; - -export enum MessageType { - Normal, - Error -} - -export type Side = 'left' | 'right' - -export type Color = 'neutral' | 'brand' | 'colorful' - -export type MessageItem = { - sender: string, - type: MessageType, - color: Color, - avatarImg?: string, - time: string, - content: string, - side: Side, - done: boolean -} - -export type Conversation = { - [uuid: string]: MessageItem -} - -export type Role = 'assistant' | 'user' | 'system'; - -export type ConversationMessage = { - role: Role; - content: string; -} - -let chatSseControllers: { [id: string]: AbortController } = {}; - -const MoreUtilsButton: FC<{ uuid: string, setEditing: (editing: boolean) => void }> = observer(({ +const MoreUtilsButton: FC<{ + uuid: string, + setEditing: (editing: boolean) => void +}> = observer(({ uuid, setEditing }) => { @@ -98,7 +68,8 @@ const MoreUtilsButton: FC<{ uuid: string, setEditing: (editing: boolean) => void }); const ChatMessageItem: FC<{ - uuid: string, onSubmit: (message: string | null, answerId: string | null, + uuid: string, + onSubmit: (message: string | null, answerId: string | null, startUuid: string | null, endUuid: string | null, includeEndUuid: boolean) => void }> = observer(({ uuid, onSubmit }) => { const { t } = useTranslation(); @@ -243,7 +214,7 @@ const ChatPanel: FC = observer(() => { color: 'colorful', avatarImg: logo, time: new Date().toISOString(), - content: t('Hello! I\'m RWKV, an open-source and commercially usable large language model.'), + content: commonStore.platform === 'web' ? t('Hello, what can I do for you?') : t('Hello! I\'m RWKV, an open-source and commercially usable large language model.'), side: 'left', done: true } @@ -260,7 +231,7 @@ const ChatPanel: FC = observer(() => { e.stopPropagation(); if (e.type === 'click' || (e.keyCode === 13 && !e.shiftKey)) { e.preventDefault(); - if (commonStore.status.status === ModelStatus.Offline && !commonStore.settings.apiUrl) { + if (commonStore.status.status === ModelStatus.Offline && !commonStore.settings.apiUrl && commonStore.platform !== 'web') { toast(t('Please click the button in the top right corner to start the model'), { type: 'warning' }); return; } @@ -464,7 +435,7 @@ const ChatPanel: FC = observer(() => { : } size="small" shape="circular" appearance="secondary" onClick={() => { - if (commonStore.status.status === ModelStatus.Offline && !commonStore.settings.apiUrl) { + if (commonStore.status.status === ModelStatus.Offline && !commonStore.settings.apiUrl && commonStore.platform !== 'web') { toast(t('Please click the button in the top right corner to start the model'), { type: 'warning' }); return; } @@ -478,43 +449,62 @@ const ChatPanel: FC = observer(() => { commonStore.setAttachmentUploading(true); - // Both are slow. Communication between frontend and backend is slow. Use AssetServer Handler to read the file. - // const blob = new Blob([atob(info.content as unknown as string)]); // await fetch(`data:application/octet-stream;base64,${info.content}`).then(r => r.blob()); - const blob = await fetch(absPathAsset(filePath)).then(r => r.blob()); - const attachmentName = filePath.split(/[\\/]/).pop(); - const urlPath = `/file-to-text?file_name=${attachmentName}`; - const bodyForm = new FormData(); - bodyForm.append('file_data', blob, attachmentName); - fetch(commonStore.settings.apiUrl ? - commonStore.settings.apiUrl + urlPath : - `http://127.0.0.1:${port}${urlPath}`, { - method: 'POST', - body: bodyForm - }).then(async r => { - if (r.status === 200) { - const pages = (await r.json()).pages as any[]; - let attachmentContent: string; - if (pages.length === 1) - attachmentContent = pages[0].page_content; - else - attachmentContent = pages.map((p, i) => `Page ${i + 1}:\n${p.page_content}`).join('\n\n'); - commonStore.setCurrentTempAttachment( - { - name: attachmentName!, - size: blob.size, - content: attachmentContent - }); - } else { - toast(r.statusText + '\n' + (await r.text()), { - type: 'error' - }); - } - commonStore.setAttachmentUploading(false); - } - ).catch(e => { + let blob: Blob; + let attachmentName: string | undefined; + let attachmentContent: string | undefined; + if (commonStore.platform === 'web') { + const webReturn = filePath as any; + blob = webReturn.blob; + attachmentName = blob.name; + attachmentContent = webReturn.content; + } else { + // Both are slow. Communication between frontend and backend is slow. Use AssetServer Handler to read the file. + // const blob = new Blob([atob(info.content as unknown as string)]); // await fetch(`data:application/octet-stream;base64,${info.content}`).then(r => r.blob()); + blob = await fetch(absPathAsset(filePath)).then(r => r.blob()); + attachmentName = filePath.split(/[\\/]/).pop(); + } + if (attachmentContent) { + commonStore.setCurrentTempAttachment( + { + name: attachmentName!, + size: blob.size, + content: attachmentContent + }); commonStore.setAttachmentUploading(false); - toast(t('Error') + ' - ' + (e.message || e), { type: 'error', autoClose: 2500 }); - }); + } else { + const urlPath = `/file-to-text?file_name=${attachmentName}`; + const bodyForm = new FormData(); + bodyForm.append('file_data', blob, attachmentName); + fetch(commonStore.settings.apiUrl ? + commonStore.settings.apiUrl + urlPath : + `http://127.0.0.1:${port}${urlPath}`, { + method: 'POST', + body: bodyForm + }).then(async r => { + if (r.status === 200) { + const pages = (await r.json()).pages as any[]; + if (pages.length === 1) + attachmentContent = pages[0].page_content; + else + attachmentContent = pages.map((p, i) => `Page ${i + 1}:\n${p.page_content}`).join('\n\n'); + commonStore.setCurrentTempAttachment( + { + name: attachmentName!, + size: blob.size, + content: attachmentContent! + }); + } else { + toast(r.statusText + '\n' + (await r.text()), { + type: 'error' + }); + } + commonStore.setAttachmentUploading(false); + } + ).catch(e => { + commonStore.setAttachmentUploading(false); + toast(t('Error') + ' - ' + (e.message || e), { type: 'error', autoClose: 2500 }); + }); + } }).catch(e => { toast(t('Error') + ' - ' + (e.message || e), { type: 'error', autoClose: 2500 }); }); @@ -586,7 +576,7 @@ const ChatPanel: FC = observer(() => { ); }); -export const Chat: FC = observer(() => { +const Chat: FC = observer(() => { return (
@@ -594,3 +584,5 @@ export const Chat: FC = observer(() => {
); }); + +export default Chat; diff --git a/frontend/src/pages/Completion.tsx b/frontend/src/pages/Completion.tsx index a1133d4..fe57f35 100644 --- a/frontend/src/pages/Completion.tsx +++ b/frontend/src/pages/Completion.tsx @@ -5,7 +5,6 @@ import { Button, Dropdown, Input, Option, Textarea } from '@fluentui/react-compo import { Labeled } from '../components/Labeled'; import { ValuedSlider } from '../components/ValuedSlider'; import { useTranslation } from 'react-i18next'; -import { ApiParameters } from './Configs'; import commonStore, { ModelStatus } from '../stores/commonStore'; import { fetchEventSource } from '@microsoft/fetch-event-source'; import { toast } from 'react-toastify'; @@ -14,18 +13,7 @@ import { PresetsButton } from './PresetsManager/PresetsButton'; import { ToolTipButton } from '../components/ToolTipButton'; import { ArrowSync20Regular } from '@fluentui/react-icons'; import { defaultPresets } from './defaultConfigs'; - -export type CompletionParams = Omit & { - stop: string, - injectStart: string, - injectEnd: string -}; - -export type CompletionPreset = { - name: string, - prompt: string, - params: CompletionParams -} +import { CompletionParams, CompletionPreset } from '../types/completion'; let completionSseController: AbortController | null = null; @@ -80,7 +68,7 @@ const CompletionPanel: FC = observer(() => { const onSubmit = (prompt: string) => { commonStore.setCompletionSubmittedPrompt(prompt); - if (commonStore.status.status === ModelStatus.Offline && !commonStore.settings.apiUrl) { + if (commonStore.status.status === ModelStatus.Offline && !commonStore.settings.apiUrl && commonStore.platform !== 'web') { toast(t('Please click the button in the top right corner to start the model'), { type: 'warning' }); commonStore.setCompletionGenerating(false); return; @@ -269,7 +257,7 @@ const CompletionPanel: FC = observer(() => { } />
-
+
; }; -export const PresetCard: FC<{ +const PresetCard: FC<{ avatarImg: string, name: string, desc: string, @@ -147,7 +126,7 @@ export const PresetCard: FC<{ ; }); -export const ChatPresetEditor: FC<{ +const ChatPresetEditor: FC<{ triggerButton: ReactElement, presetIndex: number }> = observer(({ triggerButton, presetIndex }) => { @@ -291,7 +270,7 @@ export const ChatPresetEditor: FC<{ }); }} /> } /> - +
:
; }); -export const ChatPresets: FC = observer(() => { +const ChatPresets: FC = observer(() => { const { t } = useTranslation(); return
@@ -392,11 +371,6 @@ export const ChatPresets: FC = observer(() => {
; }); -type PresetsNavigationItem = { - icon: ReactElement; - element: ReactElement; -}; - const pages: { [label: string]: PresetsNavigationItem } = { Chat: { icon: , @@ -412,7 +386,7 @@ const pages: { [label: string]: PresetsNavigationItem } = { } }; -export const PresetsManager: FC<{ initTab: string }> = ({ initTab }) => { +const PresetsManager: FC<{ initTab: string }> = ({ initTab }) => { const { t } = useTranslation(); const [tab, setTab] = useState(initTab); diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 5905c50..b93e893 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -16,32 +16,170 @@ import { observer } from 'mobx-react-lite'; import { useTranslation } from 'react-i18next'; import { checkUpdate, toastWithButton } from '../utils'; import { RestartApp } from '../../wailsjs/go/backend_golang/App'; +import { Language, Languages } from '../types/settings'; -export const Languages = { - dev: 'English', // i18n default - zh: '简体中文', - ja: '日本語' -}; +export const GeneralSettings: FC = observer(() => { + const { t } = useTranslation(); -export type Language = keyof typeof Languages; + return
+ { + if (data.optionValue) { + const lang = data.optionValue as Language; + commonStore.setSettings({ + language: lang + }); + } + }}> + { + Object.entries(Languages).map(([langKey, desc]) => + ) + } + + } /> + { + commonStore.platform === 'windows' && + { + if (data.optionValue) { + commonStore.setSettings({ + dpiScaling: Number(data.optionValue) + }); + toastWithButton(t('Restart the app to apply DPI Scaling.'), t('Restart'), () => { + RestartApp(); + }, { + autoClose: 5000 + }); + } + }}> + { + Array.from({ length: 7 }, (_, i) => (i + 2) * 25).map((v, i) => + ) + } + + } /> + } + { + commonStore.setSettings({ + darkMode: data.checked + }); + }} /> + } /> +
; +}); -export type SettingsType = { - language: Language - darkMode: boolean - autoUpdatesCheck: boolean - giteeUpdatesSource: boolean - cnMirror: boolean - host: string - dpiScaling: number - customModelsPath: string - customPythonPath: string - apiUrl: string - apiKey: string - apiChatModelName: string - apiCompletionModelName: string -} +export const AdvancedGeneralSettings: FC = observer(() => { + const { t } = useTranslation(); -export const Settings: FC = observer(() => { + return
+ + { + commonStore.setSettings({ + apiUrl: data.value + }); + }} /> + { + commonStore.setSettings({ + apiUrl: data.optionValue + }); + if (data.optionText === 'OpenAI') { + if (commonStore.settings.apiChatModelName === 'rwkv') + commonStore.setSettings({ + apiChatModelName: 'gpt-3.5-turbo' + }); + if (commonStore.settings.apiCompletionModelName === 'rwkv') + commonStore.setSettings({ + apiCompletionModelName: 'text-davinci-003' + }); + } + }}> + + + +
+ } /> + { + commonStore.setSettings({ + apiKey: data.value + }); + }} /> + } /> + + { + commonStore.setSettings({ + apiChatModelName: data.value + }); + }} /> + { + if (data.optionValue) { + commonStore.setSettings({ + apiChatModelName: data.optionValue + }); + } + }}> + { + ['rwkv', 'gpt-4', 'gpt-4-0613', 'gpt-4-32k', 'gpt-4-32k-0613', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0613', 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo-16k-0613'] + .map((v, i) => + + ) + } + +
+ } /> + + { + commonStore.setSettings({ + apiCompletionModelName: data.value + }); + }} /> + { + if (data.optionValue) { + commonStore.setSettings({ + apiCompletionModelName: data.optionValue + }); + } + }}> + { + ['rwkv', 'text-davinci-003', 'text-davinci-002', 'text-curie-001', 'text-babbage-001', 'text-ada-001'] + .map((v, i) => + + ) + } + +
+ } /> +
; +}); + +const Settings: FC = observer(() => { const { t } = useTranslation(); const advancedHeaderRef = useRef(null); @@ -53,227 +191,101 @@ export const Settings: FC = observer(() => { return ( - { - if (data.optionValue) { - const lang = data.optionValue as Language; - commonStore.setSettings({ - language: lang - }); - } - }}> - { - Object.entries(Languages).map(([langKey, desc]) => - ) - } - - } /> { - commonStore.platform === 'windows' && - { - if (data.optionValue) { - commonStore.setSettings({ - dpiScaling: Number(data.optionValue) - }); - toastWithButton(t('Restart the app to apply DPI Scaling.'), t('Restart'), () => { - RestartApp(); - }, { - autoClose: 5000 - }); - } - }}> - { - Array.from({ length: 7 }, (_, i) => (i + 2) * 25).map((v, i) => - ) - } - - } /> - } - { - commonStore.setSettings({ - darkMode: data.checked - }); - }} /> - } /> - { - commonStore.setSettings({ - autoUpdatesCheck: data.checked - }); - if (data.checked) - checkUpdate(true); - }} /> - } /> - { - commonStore.settings.language === 'zh' && - { - commonStore.setSettings({ - giteeUpdatesSource: data.checked - }); - }} /> - } /> - } - { - commonStore.settings.language === 'zh' && commonStore.platform !== 'linux' && - { - commonStore.setSettings({ - cnMirror: data.checked - }); - }} /> - } /> - } - { - commonStore.setSettings({ - host: data.checked ? '0.0.0.0' : '127.0.0.1' - }); - }} /> - } /> - { - if (data.value === 'advanced') - commonStore.setAdvancedCollapsed(!commonStore.advancedCollapsed); - }}> - - {t('Advanced')} - -
- {commonStore.platform !== 'darwin' && - { - commonStore.setSettings({ - customModelsPath: data.value - }); - }} /> - } /> - } - { - commonStore.setDepComplete(false); - commonStore.setSettings({ - customPythonPath: data.value - }); - }} /> - } /> - - { - commonStore.setSettings({ - apiUrl: data.value - }); - }} /> - { - commonStore.setSettings({ - apiUrl: data.optionValue - }); - if (data.optionText === 'OpenAI') { - if (commonStore.settings.apiChatModelName === 'rwkv') - commonStore.setSettings({ - apiChatModelName: 'gpt-3.5-turbo' - }); - if (commonStore.settings.apiCompletionModelName === 'rwkv') - commonStore.setSettings({ - apiCompletionModelName: 'text-davinci-003' - }); - } - }}> - - - -
- } /> - { - commonStore.setSettings({ - apiKey: data.value - }); - }} /> - } /> - - { - commonStore.setSettings({ - apiChatModelName: data.value - }); - }} /> - { - if (data.optionValue) { - commonStore.setSettings({ - apiChatModelName: data.optionValue - }); - } - }}> - { - ['rwkv', 'gpt-4', 'gpt-4-0613', 'gpt-4-32k', 'gpt-4-32k-0613', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0613', 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo-16k-0613'] - .map((v, i) => - - ) - } - -
- } /> - - { - commonStore.setSettings({ - apiCompletionModelName: data.value - }); - }} /> - { - if (data.optionValue) { - commonStore.setSettings({ - apiCompletionModelName: data.optionValue - }); - } - }}> - { - ['rwkv', 'text-davinci-003', 'text-davinci-002', 'text-curie-001', 'text-babbage-001', 'text-ada-001'] - .map((v, i) => - - ) - } - -
- } /> + commonStore.platform === 'web' ? + ( +
+ +
- - - + ) + : + ( +
+ + { + commonStore.setSettings({ + autoUpdatesCheck: data.checked + }); + if (data.checked) + checkUpdate(true); + }} /> + } /> + { + commonStore.settings.language === 'zh' && + { + commonStore.setSettings({ + giteeUpdatesSource: data.checked + }); + }} /> + } /> + } + { + commonStore.settings.language === 'zh' && commonStore.platform !== 'linux' && + { + commonStore.setSettings({ + cnMirror: data.checked + }); + }} /> + } /> + } + { + commonStore.setSettings({ + host: data.checked ? '0.0.0.0' : '127.0.0.1' + }); + }} /> + } /> + { + if (data.value === 'advanced') + commonStore.setAdvancedCollapsed(!commonStore.advancedCollapsed); + }}> + + {t('Advanced')} + +
+ {commonStore.platform !== 'darwin' && + { + commonStore.setSettings({ + customModelsPath: data.value + }); + }} /> + } /> + } + { + commonStore.setDepComplete(false); + commonStore.setSettings({ + customPythonPath: data.value + }); + }} /> + } /> + +
+
+
+
+
+ ) + }
} /> ); }); + +export default Settings; diff --git a/frontend/src/pages/Train.tsx b/frontend/src/pages/Train.tsx index 2ab38f3..11d56cd 100644 --- a/frontend/src/pages/Train.tsx +++ b/frontend/src/pages/Train.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactElement, useEffect, useRef, useState } from 'react'; +import React, { FC, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Dropdown, Input, Option, Select, Switch, Tab, TabList } from '@fluentui/react-components'; import { @@ -24,7 +24,6 @@ import { Labeled } from '../components/Labeled'; import { ToolTipButton } from '../components/ToolTipButton'; import { DataUsageSettings20Regular, Folder20Regular } from '@fluentui/react-icons'; import { useNavigate } from 'react-router'; -import { Precision } from './Configs'; import { CategoryScale, Chart as ChartJS, @@ -40,6 +39,12 @@ import { ChartJSOrUndefined } from 'react-chartjs-2/dist/types'; import { WindowShow } from '../../wailsjs/runtime'; import { t } from 'i18next'; import { DialogButton } from '../components/DialogButton'; +import { + DataProcessParameters, + LoraFinetuneParameters, + LoraFinetunePrecision, + TrainNavigationItem +} from '../types/train'; ChartJS.register( CategoryScale, @@ -86,39 +91,6 @@ const addLossDataToChart = (epoch: number, loss: number) => { commonStore.setChartData(commonStore.chartData); }; -export type DataProcessParameters = { - dataPath: string; - vocabPath: string; -} - -export type LoraFinetunePrecision = 'bf16' | 'fp16' | 'tf32'; - -export type LoraFinetuneParameters = { - baseModel: string; - ctxLen: number; - epochSteps: number; - epochCount: number; - epochBegin: number; - epochSave: number; - microBsz: number; - accumGradBatches: number; - preFfn: boolean; - headQk: boolean; - lrInit: string; - lrFinal: string; - warmupSteps: number; - beta1: number; - beta2: number; - adamEps: string; - devices: number; - precision: LoraFinetunePrecision; - gradCp: boolean; - loraR: number; - loraAlpha: number; - loraDropout: number; - loraLoad: string -} - const loraFinetuneParametersOptions: Array<[key: keyof LoraFinetuneParameters, type: string, name: string]> = [ ['devices', 'number', 'Devices'], ['precision', 'LoraFinetunePrecision', 'Precision'], @@ -568,10 +540,6 @@ const LoraFinetune: FC = observer(() => { ); }); -type TrainNavigationItem = { - element: ReactElement; -}; - const pages: { [label: string]: TrainNavigationItem } = { 'LoRA Finetune': { element: @@ -582,7 +550,7 @@ const pages: { [label: string]: TrainNavigationItem } = { }; -export const Train: FC = () => { +const Train: FC = () => { const { t } = useTranslation(); const [tab, setTab] = useState('LoRA Finetune'); @@ -607,3 +575,5 @@ export const Train: FC = () => { ; }; + +export default Train; diff --git a/frontend/src/pages/defaultConfigs.ts b/frontend/src/pages/defaultConfigs.ts index cbf4ccf..6bd0dca 100644 --- a/frontend/src/pages/defaultConfigs.ts +++ b/frontend/src/pages/defaultConfigs.ts @@ -1,5 +1,5 @@ -import { ModelConfig } from './Configs'; -import { CompletionPreset } from './Completion'; +import { CompletionPreset } from '../types/completion'; +import { ModelConfig } from '../types/configs'; export const defaultCompositionPrompt = ''; diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 7f1d4f8..3e6110c 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,5 +1,4 @@ -import { ReactElement } from 'react'; -import { Configs } from './Configs'; +import { FC, lazy, LazyExoticComponent, ReactElement } from 'react'; import { ArrowDownload20Regular, Chat20Regular, @@ -12,21 +11,12 @@ import { Settings20Regular, Storage20Regular } from '@fluentui/react-icons'; -import { Home } from './Home'; -import { Chat } from './Chat'; -import { Models } from './Models'; -import { Train } from './Train'; -import { Settings } from './Settings'; -import { About } from './About'; -import { Downloads } from './Downloads'; -import { Completion } from './Completion'; -import { Composition } from './Composition'; type NavigationItem = { label: string; path: string; icon: ReactElement; - element: ReactElement; + element: LazyExoticComponent; top: boolean; }; @@ -35,70 +25,70 @@ export const pages: NavigationItem[] = [ label: 'Home', path: '/', icon: , - element: , + element: lazy(() => import('./Home')), top: true }, { label: 'Chat', path: '/chat', icon: , - element: , + element: lazy(() => import('./Chat')), top: true }, { label: 'Completion', path: '/completion', icon: , - element: , + element: lazy(() => import('./Completion')), top: true }, { label: 'Composition', path: '/composition', icon: , - element: , + element: lazy(() => import('./Composition')), top: true }, { label: 'Configs', path: '/configs', icon: , - element: , + element: lazy(() => import('./Configs')), top: true }, { label: 'Models', path: '/models', icon: , - element: , + element: lazy(() => import('./Models')), top: true }, { label: 'Downloads', path: '/downloads', icon: , - element: , + element: lazy(() => import('./Downloads')), top: true }, { label: 'Train', path: '/train', icon: , - element: , + element: lazy(() => import('./Train')), top: true }, { label: 'Settings', path: '/settings', icon: , - element: , + element: lazy(() => import('./Settings')), top: false }, { label: 'About', path: '/about', icon: , - element: , + element: lazy(() => import('./About')), top: false } ]; diff --git a/frontend/src/startup.ts b/frontend/src/startup.ts index f65091e..63415a6 100644 --- a/frontend/src/startup.ts +++ b/frontend/src/startup.ts @@ -5,39 +5,42 @@ import { getStatus } from './apis'; import { EventsOn, WindowSetTitle } from '../wailsjs/runtime'; import manifest from '../../manifest.json'; import { defaultModelConfigs, defaultModelConfigsMac } from './pages/defaultConfigs'; -import { Preset } from './pages/PresetsManager/PresetsButton'; -import { wslHandler } from './pages/Train'; import { t } from 'i18next'; +import { Preset } from './types/presets'; export async function startup() { - downloadProgramFiles(); - EventsOn('downloadList', (data) => { - if (data) - commonStore.setDownloadList(data); - }); - EventsOn('wsl', wslHandler); - EventsOn('wslerr', (e) => { - console.log(e); - }); - initLocalModelsNotify(); - initLoraModels(); - initPresets(); - initHardwareMonitor(); - await GetPlatform().then(p => commonStore.setPlatform(p as Platform)); + + if (commonStore.platform !== 'web') { + downloadProgramFiles(); + EventsOn('downloadList', (data) => { + if (data) + commonStore.setDownloadList(data); + }); + EventsOn('wsl', (await import('./pages/Train')).wslHandler); + EventsOn('wslerr', (e) => { + console.log(e); + }); + initLocalModelsNotify(); + initLoraModels(); + initHardwareMonitor(); + } + await initConfig(); - initCache(true).then(initRemoteText); // depends on config customModelsPath + if (commonStore.platform !== 'web') { + initCache(true).then(initRemoteText); // depends on config customModelsPath - if (commonStore.settings.autoUpdatesCheck) // depends on config settings - checkUpdate(); + if (commonStore.settings.autoUpdatesCheck) // depends on config settings + checkUpdate(); - getStatus(1000).then(status => { // depends on config api port - if (status) - commonStore.setStatus(status); - }); + getStatus(1000).then(status => { // depends on config api port + if (status) + commonStore.setStatus(status); + }); + } } async function initRemoteText() { @@ -88,7 +91,8 @@ async function initCache(initUnfinishedModels: boolean) { async function initPresets() { await ReadJson('presets.json').then((presets: Preset[]) => { - commonStore.setPresets(presets, false); + if (Array.isArray(presets)) + commonStore.setPresets(presets, false); }).catch(() => { }); } diff --git a/frontend/src/stores/commonStore.ts b/frontend/src/stores/commonStore.ts index 1d234ec..573a1d5 100644 --- a/frontend/src/stores/commonStore.ts +++ b/frontend/src/stores/commonStore.ts @@ -2,21 +2,21 @@ import { makeAutoObservable } from 'mobx'; import { getUserLanguage, isSystemLightMode, saveCache, saveConfigs, savePresets } from '../utils'; import { WindowSetDarkTheme, WindowSetLightTheme } from '../../wailsjs/runtime'; import manifest from '../../../manifest.json'; -import { ModelConfig } from '../pages/Configs'; -import { Conversation } from '../pages/Chat'; -import { ModelSourceItem } from '../pages/Models'; -import { DownloadStatus } from '../pages/Downloads'; -import { SettingsType } from '../pages/Settings'; -import { IntroductionContent } from '../pages/Home'; -import { AboutContent } from '../pages/About'; import i18n from 'i18next'; -import { CompletionPreset } from '../pages/Completion'; import { defaultCompositionPrompt, defaultModelConfigs, defaultModelConfigsMac } from '../pages/defaultConfigs'; import commonStore from './commonStore'; -import { Preset } from '../pages/PresetsManager/PresetsButton'; -import { DataProcessParameters, LoraFinetuneParameters } from '../pages/Train'; import { ChartData } from 'chart.js'; -import { CompositionParams } from '../pages/Composition'; +import { Preset } from '../types/presets'; +import { AboutContent } from '../types/about'; +import { Conversation } from '../types/chat'; +import { CompletionPreset } from '../types/completion'; +import { CompositionParams } from '../types/composition'; +import { ModelConfig } from '../types/configs'; +import { DownloadStatus } from '../types/downloads'; +import { IntroductionContent } from '../types/home'; +import { ModelSourceItem } from '../types/models'; +import { SettingsType } from '../types/settings'; +import { DataProcessParameters, LoraFinetuneParameters } from '../types/train'; export enum ModelStatus { Offline, @@ -37,7 +37,7 @@ export type Attachment = { content: string; } -export type Platform = 'windows' | 'darwin' | 'linux'; +export type Platform = 'windows' | 'darwin' | 'linux' | 'web'; class CommonStore { // global @@ -135,7 +135,7 @@ class CommonStore { customModelsPath: './models', customPythonPath: '', apiUrl: '', - apiKey: 'sk-', + apiKey: '', apiChatModelName: 'rwkv', apiCompletionModelName: 'rwkv' }; diff --git a/frontend/src/types/about.ts b/frontend/src/types/about.ts new file mode 100644 index 0000000..fc64699 --- /dev/null +++ b/frontend/src/types/about.ts @@ -0,0 +1 @@ +export type AboutContent = { [lang: string]: string } \ No newline at end of file diff --git a/frontend/src/types/chat.ts b/frontend/src/types/chat.ts new file mode 100644 index 0000000..1ac9885 --- /dev/null +++ b/frontend/src/types/chat.ts @@ -0,0 +1,29 @@ +export const userName = 'M E'; +export const botName = 'A I'; +export const welcomeUuid = 'welcome'; + +export enum MessageType { + Normal, + Error +} + +export type Side = 'left' | 'right' +export type Color = 'neutral' | 'brand' | 'colorful' +export type MessageItem = { + sender: string, + type: MessageType, + color: Color, + avatarImg?: string, + time: string, + content: string, + side: Side, + done: boolean +} +export type Conversation = { + [uuid: string]: MessageItem +} +export type Role = 'assistant' | 'user' | 'system'; +export type ConversationMessage = { + role: Role; + content: string; +} \ No newline at end of file diff --git a/frontend/src/types/completion.ts b/frontend/src/types/completion.ts new file mode 100644 index 0000000..993c941 --- /dev/null +++ b/frontend/src/types/completion.ts @@ -0,0 +1,12 @@ +import { ApiParameters } from './configs'; + +export type CompletionParams = Omit & { + stop: string, + injectStart: string, + injectEnd: string +}; +export type CompletionPreset = { + name: string, + prompt: string, + params: CompletionParams +} \ No newline at end of file diff --git a/frontend/src/types/composition.ts b/frontend/src/types/composition.ts new file mode 100644 index 0000000..dd3beca --- /dev/null +++ b/frontend/src/types/composition.ts @@ -0,0 +1,12 @@ +import { NoteSequence } from '@magenta/music/esm/protobuf'; + +export type CompositionParams = { + prompt: string, + maxResponseToken: number, + temperature: number, + topP: number, + autoPlay: boolean, + useLocalSoundFont: boolean, + midi: ArrayBuffer | null, + ns: NoteSequence | null +} \ No newline at end of file diff --git a/frontend/src/types/configs.ts b/frontend/src/types/configs.ts new file mode 100644 index 0000000..9385635 --- /dev/null +++ b/frontend/src/types/configs.ts @@ -0,0 +1,28 @@ +export type ApiParameters = { + apiPort: number + maxResponseToken: number; + temperature: number; + topP: number; + presencePenalty: number; + frequencyPenalty: number; +} +export type Device = 'CPU' | 'CUDA' | 'CUDA-Beta' | 'WebGPU' | 'MPS' | 'Custom'; +export type Precision = 'fp16' | 'int8' | 'fp32'; +export type ModelParameters = { + // different models can not have the same name + modelName: string; + device: Device; + precision: Precision; + storedLayers: number; + maxStoredLayers: number; + useCustomCuda?: boolean; + customStrategy?: string; + useCustomTokenizer?: boolean; + customTokenizer?: string; +} +export type ModelConfig = { + // different configs can have the same name + name: string; + apiParameters: ApiParameters + modelParameters: ModelParameters +} \ No newline at end of file diff --git a/frontend/src/types/downloads.ts b/frontend/src/types/downloads.ts new file mode 100644 index 0000000..5718ec2 --- /dev/null +++ b/frontend/src/types/downloads.ts @@ -0,0 +1,11 @@ +export type DownloadStatus = { + name: string; + path: string; + url: string; + transferred: number; + size: number; + speed: number; + progress: number; + downloading: boolean; + done: boolean; +} \ No newline at end of file diff --git a/frontend/src/types/home.ts b/frontend/src/types/home.ts new file mode 100644 index 0000000..2955528 --- /dev/null +++ b/frontend/src/types/home.ts @@ -0,0 +1,11 @@ +import { ReactElement } from 'react'; + +export type IntroductionContent = { + [lang: string]: string +} +export type NavCard = { + label: string; + desc: string; + path: string; + icon: ReactElement; +}; \ No newline at end of file diff --git a/frontend/src/types/models.ts b/frontend/src/types/models.ts new file mode 100644 index 0000000..7eb5b88 --- /dev/null +++ b/frontend/src/types/models.ts @@ -0,0 +1,14 @@ +export type ModelSourceItem = { + name: string; + size: number; + lastUpdated: string; + desc?: { [lang: string]: string | undefined; }; + SHA256?: string; + url?: string; + downloadUrl?: string; + isComplete?: boolean; + isLocal?: boolean; + localSize?: number; + lastUpdatedMs?: number; + hide?: boolean; +}; \ No newline at end of file diff --git a/frontend/src/types/presets.ts b/frontend/src/types/presets.ts new file mode 100644 index 0000000..d5d6f5b --- /dev/null +++ b/frontend/src/types/presets.ts @@ -0,0 +1,30 @@ +import { ReactElement } from 'react'; + +import { ConversationMessage } from './chat'; + +export type PresetType = 'chat' | 'completion' | 'chatInCompletion' +export type Preset = { + name: string, + tag: string, + // if name and sourceUrl are same, it will be overridden when importing + sourceUrl: string, + desc: string, + avatarImg: string, + type: PresetType, + // chat + welcomeMessage: string, + messages: ConversationMessage[], + displayPresetMessages: boolean, + // completion + prompt: string, + stop: string, + injectStart: string, + injectEnd: string, + presystem?: boolean, + userName?: string, + assistantName?: string +} +export type PresetsNavigationItem = { + icon: ReactElement; + element: ReactElement; +}; \ No newline at end of file diff --git a/frontend/src/types/settings.ts b/frontend/src/types/settings.ts new file mode 100644 index 0000000..2fe3299 --- /dev/null +++ b/frontend/src/types/settings.ts @@ -0,0 +1,21 @@ +export const Languages = { + dev: 'English', // i18n default + zh: '简体中文', + ja: '日本語' +}; +export type Language = keyof typeof Languages; +export type SettingsType = { + language: Language + darkMode: boolean + autoUpdatesCheck: boolean + giteeUpdatesSource: boolean + cnMirror: boolean + host: string + dpiScaling: number + customModelsPath: string + customPythonPath: string + apiUrl: string + apiKey: string + apiChatModelName: string + apiCompletionModelName: string +} \ No newline at end of file diff --git a/frontend/src/types/train.ts b/frontend/src/types/train.ts new file mode 100644 index 0000000..fd056f8 --- /dev/null +++ b/frontend/src/types/train.ts @@ -0,0 +1,35 @@ +import { ReactElement } from 'react'; + +export type DataProcessParameters = { + dataPath: string; + vocabPath: string; +} +export type LoraFinetunePrecision = 'bf16' | 'fp16' | 'tf32'; +export type LoraFinetuneParameters = { + baseModel: string; + ctxLen: number; + epochSteps: number; + epochCount: number; + epochBegin: number; + epochSave: number; + microBsz: number; + accumGradBatches: number; + preFfn: boolean; + headQk: boolean; + lrInit: string; + lrFinal: string; + warmupSteps: number; + beta1: number; + beta2: number; + adamEps: string; + devices: number; + precision: LoraFinetunePrecision; + gradCp: boolean; + loraR: number; + loraAlpha: number; + loraDropout: number; + loraLoad: string +} +export type TrainNavigationItem = { + element: ReactElement; +}; \ No newline at end of file diff --git a/frontend/src/utils/index.tsx b/frontend/src/utils/index.tsx index 6e0f247..27cd45c 100644 --- a/frontend/src/utils/index.tsx +++ b/frontend/src/utils/index.tsx @@ -15,13 +15,13 @@ import { toast } from 'react-toastify'; import { t } from 'i18next'; import { ToastOptions } from 'react-toastify/dist/types'; import { Button } from '@fluentui/react-components'; -import { Language, Languages, SettingsType } from '../pages/Settings'; -import { ModelSourceItem } from '../pages/Models'; -import { ModelConfig, ModelParameters } from '../pages/Configs'; -import { DownloadStatus } from '../pages/Downloads'; -import { DataProcessParameters, LoraFinetuneParameters } from '../pages/Train'; import { BrowserOpenURL, WindowShow } from '../../wailsjs/runtime'; import { NavigateFunction } from 'react-router'; +import { ModelConfig, ModelParameters } from '../types/configs'; +import { DownloadStatus } from '../types/downloads'; +import { ModelSourceItem } from '../types/models'; +import { Language, Languages, SettingsType } from '../types/settings'; +import { DataProcessParameters, LoraFinetuneParameters } from '../types/train'; export type Cache = { version: string @@ -290,6 +290,8 @@ export function bytesToReadable(size: number) { } export function absPathAsset(path: string) { + if (commonStore.platform === 'web') + return path; if ((path.length > 0 && path[0] === '/') || (path.length > 1 && path[1] === ':')) { return '=>' + path; diff --git a/frontend/src/webWails.js b/frontend/src/webWails.js new file mode 100644 index 0000000..04a22b1 --- /dev/null +++ b/frontend/src/webWails.js @@ -0,0 +1,157 @@ +function defineRuntime(name, func) { + window.runtime[name] = func +} + +function defineApp(name, func) { + window.go['backend_golang']['App'][name] = func +} + +if (!window.runtime) { + window.runtime = {} + document.title += ' WebUI' + + // not implemented + defineRuntime('EventsOnMultiple', () => { + }) + defineRuntime('WindowSetLightTheme', () => { + }) + defineRuntime('WindowSetDarkTheme', () => { + }) + defineRuntime('WindowShow', () => { + }) + defineRuntime('WindowHide', () => { + }) + + // implemented + defineRuntime('ClipboardGetText', async () => { + return await navigator.clipboard.readText() + }) + defineRuntime('ClipboardSetText', async (text) => { + await navigator.clipboard.writeText(text) + return true + }) + defineRuntime('WindowSetTitle', (title) => { + document.title = title + }) + defineRuntime('BrowserOpenURL', (url) => { + window.open(url, '_blank', 'noopener, noreferrer') + }) +} + +if (!window.go) { + window.go = {} + window.go['backend_golang'] = {} + window.go['backend_golang']['App'] = {} + + // not implemented + defineApp('AddToDownloadList', async () => { + }) + defineApp('ContinueDownload', async () => { + }) + defineApp('ConvertData', async () => { + }) + defineApp('ConvertModel', async () => { + }) + defineApp('ConvertSafetensors', async () => { + }) + defineApp('CopyFile', async () => { + }) + defineApp('DeleteFile', async () => { + }) + defineApp('DepCheck', async () => { + }) + defineApp('DownloadFile', async () => { + }) + defineApp('GetPyError', async () => { + }) + defineApp('InstallPyDep', async () => { + }) + defineApp('IsPortAvailable', async () => { + }) + defineApp('MergeLora', async () => { + }) + defineApp('OpenFileFolder', async () => { + }) + defineApp('PauseDownload', async () => { + }) + defineApp('ReadFileInfo', async () => { + }) + defineApp('RestartApp', async () => { + }) + defineApp('StartServer', async () => { + }) + defineApp('StartWebGPUServer', async () => { + }) + defineApp('UpdateApp', async () => { + }) + defineApp('WslCommand', async () => { + }) + defineApp('WslEnable', async () => { + }) + defineApp('WslInstallUbuntu', async () => { + }) + defineApp('WslIsEnabled', async () => { + }) + defineApp('WslStart', async () => { + }) + defineApp('WslStop', async () => { + }) + + // implemented + defineApp('FileExists', async () => { + return false + }) + defineApp('GetPlatform', async () => { + return 'web' + }) + defineApp('ListDirFiles', async () => { + return [] + }) + defineApp('OpenOpenFileDialog', async (filterPattern) => { + return new Promise((resolve, reject) => { + const input = document.createElement('input') + input.type = 'file' + input.accept = filterPattern + .replaceAll('*.txt', 'text/plain') + .replaceAll('*.', 'application/') + .replaceAll(';', ',') + + input.onchange = e => { + const file = e.target?.files[0] + if (file.type === 'text/plain') { + const reader = new FileReader() + reader.readAsText(file, 'UTF-8') + + reader.onload = readerEvent => { + const content = readerEvent.target?.result + resolve({ + blob: file, + content: content + }) + } + } else { + resolve({ + blob: file + }) + } + } + input.click() + }) + }) + defineApp('OpenSaveFileDialog', async (filterPattern, defaultFileName, savedContent) => { + const saver = await import('file-saver') + saver.saveAs(new Blob([savedContent], { type: 'text/plain;charset=utf-8' }), defaultFileName) + return '' + }) + defineApp('OpenSaveFileDialogBytes', async (filterPattern, defaultFileName, savedContent) => { + const saver = await import('file-saver') + saver.saveAs(new Blob([new Uint8Array(savedContent)], { type: 'octet/stream' }), defaultFileName) + return '' + }) + defineApp('ReadJson', async (fileName) => { + return JSON.parse(localStorage.getItem(fileName)) + }) + defineApp('SaveJson', async (fileName, data) => { + localStorage.setItem(fileName, JSON.stringify(data)) + }) +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 96a127c..e50f696 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,6 +1,37 @@ -import {defineConfig} from 'vite'; +// @ts-ignore +import { dependencies } from './package.json'; +import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; -import {visualizer} from 'rollup-plugin-visualizer'; +import { visualizer } from 'rollup-plugin-visualizer'; + +// dependencies that exist anywhere +const vendor = [ + 'react', 'react-dom', 'react-router', 'react-router-dom', + '@fluentui/react-icons', + 'mobx', 'mobx-react-lite', + 'i18next', 'react-i18next', + 'usehooks-ts', 'react-toastify', + 'classnames' +]; + +const embedded = [ + // split @fluentui/react-components by components + '@fluentui/react-components', + + // dependencies that exist in single component + 'react-beautiful-dnd', + '@magenta/music', 'html-midi-player', + 'react-markdown', 'rehype-highlight', 'rehype-raw', 'remark-breaks', 'remark-gfm' +]; + +function renderChunks(deps: Record) { + let chunks = {}; + Object.keys(deps).forEach((key) => { + if ([...vendor, ...embedded].includes(key)) return; + chunks[key] = [key]; + }); + return chunks; +} // https://vitejs.dev/config/ export default defineConfig({ @@ -9,5 +40,16 @@ export default defineConfig({ template: 'treemap', gzipSize: true, brotliSize: true - })] + })], + build: { + chunkSizeWarningLimit: 3000, + rollupOptions: { + output: { + manualChunks: { + vendor, + ...renderChunks(dependencies) + } + } + } + } });