webui build

This commit is contained in:
josc146 2023-11-07 19:27:21 +08:00
parent 384e4ce4d0
commit 893be5cf43
43 changed files with 1108 additions and 691 deletions

View File

@ -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

View File

@ -1,9 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>RWKV-Runner</title>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>RWKV-Runner</title>
<link href="./src/assets/images/logo.png" rel="icon" type="image/x-icon">
</head>
<body>
<div id="root"></div>

View File

@ -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",

View File

@ -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",

View File

@ -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<string>(pages[0].path);
@ -82,7 +86,7 @@ const App: FC = observer(() => {
<div className="h-full w-full p-2 box-border overflow-y-hidden">
<Routes>
{pages.map(({ path, element }, index) => (
<Route key={`${path}-${index}`} path={path} element={element} />
<Route key={`${path}-${index}`} path={path} element={<LazyImportComponent lazyChildren={element} />} />
))}
</Routes>
</div>

View File

@ -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?": "こんにちは、何かお手伝いできますか?"
}

View File

@ -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?": "你好,有什么要我帮忙的吗?"
}

View File

@ -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<{
<DialogContent>
{
markdown ?
<MarkdownRender>{contentText}</MarkdownRender> :
<LazyImportComponent lazyChildren={MarkdownRender}>
{contentText}
</LazyImportComponent> :
contentText
}
</DialogContent>

View File

@ -0,0 +1,20 @@
import { FC, LazyExoticComponent, ReactNode, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
interface LazyImportComponentProps {
lazyChildren: LazyExoticComponent<FC<any>>;
lazyProps?: any;
children?: ReactNode;
}
export const LazyImportComponent: FC<LazyImportComponentProps> = (props) => {
const { t } = useTranslation();
return (
<Suspense fallback={<div>{t('Loading...')}</div>}>
<props.lazyChildren {...props.lazyProps}>
{props.children}
</props.lazyChildren>
</Suspense>
);
};

View File

@ -21,7 +21,7 @@ const Hyperlink: FC<any> = ({ href, children }) => {
);
};
export const MarkdownRender: FC<ReactMarkdownOptions> = (props) => {
const MarkdownRender: FC<ReactMarkdownOptions> = (props) => {
return (
<div dir="auto" className="markdown-body">
<ReactMarkdown

View File

@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next';
import { ToolTipButton } from './ToolTipButton';
import { Play16Regular, Stop16Regular } from '@fluentui/react-icons';
import { useNavigate } from 'react-router';
import { WindowShow } from '../../wailsjs/runtime/runtime';
import { WindowShow } from '../../wailsjs/runtime';
const mainButtonText = {
[ModelStatus.Offline]: 'Run',

View File

@ -25,7 +25,8 @@ export const WorkHeader: FC = observer(() => {
const { t } = useTranslation();
const port = commonStore.getCurrentModelConfig().apiParameters.apiPort;
return (
return commonStore.platform === 'web' ?
<div /> :
<div className="flex flex-col gap-1">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
@ -42,5 +43,5 @@ export const WorkHeader: FC = observer(() => {
</Text>
<Divider style={{ flexGrow: 0 }} />
</div>
);
;
});

View File

@ -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(() => {

View File

@ -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;

View File

@ -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(() => {
: <Attach16Regular />}
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 (
<div className="flex flex-col gap-1 p-2 h-full overflow-hidden">
<WorkHeader />
@ -594,3 +584,5 @@ export const Chat: FC = observer(() => {
</div>
);
});
export default Chat;

View File

@ -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<ApiParameters, 'apiPort'> & {
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(() => {
} />
</div>
<div className="grow" />
<div className="flex justify-between gap-2">
<div className="hidden justify-between gap-2 sm:flex">
<Button className="grow" onClick={() => {
const newPrompt = prompt.replace(/\n+\ /g, '\n').split('\n').map((line) => line.trim()).join('\n');
setPrompt(newPrompt);
@ -303,7 +291,7 @@ const CompletionPanel: FC = observer(() => {
);
});
export const Completion: FC = observer(() => {
const Completion: FC = observer(() => {
return (
<div className="flex flex-col gap-1 p-2 h-full overflow-hidden">
<WorkHeader />
@ -311,3 +299,5 @@ export const Completion: FC = observer(() => {
</div>
);
});
export default Completion;

View File

@ -1,3 +1,4 @@
import 'html-midi-player';
import React, { FC, useEffect, useRef } from 'react';
import { observer } from 'mobx-react-lite';
import { WorkHeader } from '../components/WorkHeader';
@ -17,17 +18,7 @@ import { NoteSequence } from '@magenta/music/esm/protobuf.js';
import { defaultCompositionPrompt } from './defaultConfigs';
import { FileExists, OpenFileFolder, OpenSaveFileDialogBytes } from '../../wailsjs/go/backend_golang/App';
import { toastWithButton } from '../utils';
export type CompositionParams = {
prompt: string,
maxResponseToken: number,
temperature: number,
topP: number,
autoPlay: boolean,
useLocalSoundFont: boolean,
midi: ArrayBuffer | null,
ns: NoteSequence | null
}
import { CompositionParams } from '../types/composition';
let compositionSseController: AbortController | null = null;
@ -137,7 +128,7 @@ const CompositionPanel: FC = observer(() => {
const onSubmit = (prompt: string) => {
commonStore.setCompositionSubmittedPrompt(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.setCompositionGenerating(false);
return;
@ -335,7 +326,7 @@ const CompositionPanel: FC = observer(() => {
);
});
export const Composition: FC = observer(() => {
const Composition: FC = observer(() => {
return (
<div className="flex flex-col gap-1 p-2 h-full overflow-hidden">
<WorkHeader />
@ -343,3 +334,5 @@ export const Composition: FC = observer(() => {
</div>
);
});
export default Composition;

View File

@ -29,45 +29,14 @@ import { updateConfig } from '../apis';
import { ConvertModel, ConvertSafetensors, FileExists, GetPyError } from '../../wailsjs/go/backend_golang/App';
import { checkDependencies, getStrategy } from '../utils';
import { useTranslation } from 'react-i18next';
import { WindowShow } from '../../wailsjs/runtime/runtime';
import { WindowShow } from '../../wailsjs/runtime';
import strategyImg from '../assets/images/strategy.jpg';
import strategyZhImg from '../assets/images/strategy_zh.jpg';
import { ResetConfigsButton } from '../components/ResetConfigsButton';
import { useMediaQuery } from 'usehooks-ts';
import { ApiParameters, Device, ModelParameters, Precision } from '../types/configs';
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
}
export const Configs: FC = observer(() => {
const Configs: FC = observer(() => {
const { t } = useTranslation();
const [selectedIndex, setSelectedIndex] = React.useState(commonStore.currentModelConfigIndex);
const [selectedConfig, setSelectedConfig] = React.useState(commonStore.modelConfigs[selectedIndex]);
@ -478,3 +447,5 @@ export const Configs: FC = observer(() => {
} />
);
});
export default Configs;

View File

@ -9,19 +9,7 @@ import { ToolTipButton } from '../components/ToolTipButton';
import { Folder20Regular, Pause20Regular, Play20Regular } from '@fluentui/react-icons';
import { AddToDownloadList, OpenFileFolder, PauseDownload } from '../../wailsjs/go/backend_golang/App';
export type DownloadStatus = {
name: string;
path: string;
url: string;
transferred: number;
size: number;
speed: number;
progress: number;
downloading: boolean;
done: boolean;
}
export const Downloads: FC = observer(() => {
const Downloads: FC = observer(() => {
const { t } = useTranslation();
const finishedModelsLen = commonStore.downloadList.filter((status) => status.done && status.name.endsWith('.pth')).length;
useEffect(() => {
@ -91,3 +79,5 @@ export const Downloads: FC = observer(() => {
} />
);
});
export default Downloads;

View File

@ -1,11 +1,13 @@
import { CompoundButton, Link, Text } from '@fluentui/react-components';
import React, { FC, ReactElement } from 'react';
import React, { FC } from 'react';
import banner from '../assets/images/banner.jpg';
import {
Chat20Regular,
ClipboardEdit20Regular,
DataUsageSettings20Regular,
DocumentSettings20Regular
DocumentSettings20Regular,
MusicNote220Regular,
Settings20Regular
} from '@fluentui/react-icons';
import { useNavigate } from 'react-router';
import { observer } from 'mobx-react-lite';
@ -14,21 +16,13 @@ import manifest from '../../../manifest.json';
import { BrowserOpenURL } from '../../wailsjs/runtime';
import { useTranslation } from 'react-i18next';
import { ConfigSelector } from '../components/ConfigSelector';
import MarkdownRender from '../components/MarkdownRender';
import commonStore from '../stores/commonStore';
import { Completion } from './Completion';
import { ResetConfigsButton } from '../components/ResetConfigsButton';
import { AdvancedGeneralSettings } from './Settings';
import { NavCard } from '../types/home';
import { LazyImportComponent } from '../components/LazyImportComponent';
export type IntroductionContent = { [lang: string]: string }
type NavCard = {
label: string;
desc: string;
path: string;
icon: ReactElement;
};
const navCards: NavCard[] = [
const clientNavCards: NavCard[] = [
{
label: 'Chat',
desc: 'Go to chat page',
@ -55,7 +49,36 @@ const navCards: NavCard[] = [
}
];
export const Home: FC = observer(() => {
const webNavCards: NavCard[] = [
{
label: 'Chat',
desc: 'Go to chat page',
path: '/chat',
icon: <Chat20Regular />
},
{
label: 'Completion',
desc: 'Writer, Translator, Role-playing',
path: '/completion',
icon: <ClipboardEdit20Regular />
},
{
label: 'Composition',
desc: '',
path: '/composition',
icon: <MusicNote220Regular />
},
{
label: 'Settings',
desc: '',
path: '/settings',
icon: <Settings20Regular />
}
];
const MarkdownRender = React.lazy(() => import('../components/MarkdownRender'));
const Home: FC = observer(() => {
const { t } = useTranslation();
const navigate = useNavigate();
const lang: string = commonStore.settings.language;
@ -64,39 +87,64 @@ export const Home: FC = observer(() => {
navigate({ pathname: path });
};
return (
<div className="flex flex-col justify-between h-full">
<img className="rounded-xl select-none hidden sm:block"
style={{ maxHeight: '40%', margin: '0 auto' }} src={banner} />
<div className="flex flex-col gap-2">
<Text size={600} weight="medium">{t('Introduction')}</Text>
<div className="h-40 overflow-y-auto overflow-x-hidden p-1">
<MarkdownRender>
{lang in commonStore.introduction ? commonStore.introduction[lang] : commonStore.introduction['en']}
</MarkdownRender>
return commonStore.platform === 'web' ?
(
<div className="flex flex-col gap-2 h-full">
<img className="rounded-xl select-none object-cover grow"
style={{ maxHeight: '40%' }} src={banner} />
<div className="grow"></div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-5">
{webNavCards.map(({ label, path, icon, desc }, index) => (
<CompoundButton icon={icon} secondaryContent={t(desc)} key={`${path}-${index}`} value={path}
size="large" onClick={() => onClickNavCard(path)}>
{t(label)}
</CompoundButton>
))}
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-5">
{navCards.map(({ label, path, icon, desc }, index) => (
<CompoundButton icon={icon} secondaryContent={t(desc)} key={`${path}-${index}`} value={path}
size="large" onClick={() => onClickNavCard(path)}>
{t(label)}
</CompoundButton>
))}
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-row-reverse sm:fixed bottom-2 right-2">
<div className="flex gap-3">
<ResetConfigsButton />
<ConfigSelector />
<RunButton />
<div className="flex flex-col gap-2">
<AdvancedGeneralSettings />
<div className="flex gap-4 items-end">
{t('Version')}: {manifest.version}
<Link onClick={() => BrowserOpenURL('https://github.com/josStorer/RWKV-Runner')}>{t('Help')}</Link>
</div>
</div>
<div className="flex gap-4 items-end">
{t('Version')}: {manifest.version}
<Link onClick={() => BrowserOpenURL('https://github.com/josStorer/RWKV-Runner')}>{t('Help')}</Link>
</div>
)
: (
<div className="flex flex-col justify-between h-full">
<img className="rounded-xl select-none object-cover hidden sm:block"
style={{ maxHeight: '40%' }} src={banner} />
<div className="flex flex-col gap-2">
<Text size={600} weight="medium">{t('Introduction')}</Text>
<div className="h-40 overflow-y-auto overflow-x-hidden p-1">
<LazyImportComponent lazyChildren={MarkdownRender}>
{lang in commonStore.introduction ? commonStore.introduction[lang] : commonStore.introduction['en']}
</LazyImportComponent>
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-5">
{clientNavCards.map(({ label, path, icon, desc }, index) => (
<CompoundButton icon={icon} secondaryContent={t(desc)} key={`${path}-${index}`} value={path}
size="large" onClick={() => onClickNavCard(path)}>
{t(label)}
</CompoundButton>
))}
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-row-reverse sm:fixed bottom-2 right-2">
<div className="flex gap-3">
<ResetConfigsButton />
<ConfigSelector />
<RunButton />
</div>
</div>
<div className="flex gap-4 items-end">
{t('Version')}: {manifest.version}
<Link onClick={() => BrowserOpenURL('https://github.com/josStorer/RWKV-Runner')}>{t('Help')}</Link>
</div>
</div>
</div>
</div>
);
);
});
export default Home;

View File

@ -22,21 +22,7 @@ import { Page } from '../components/Page';
import { bytesToGb, refreshModels, saveConfigs, toastWithButton } from '../utils';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
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;
};
import { ModelSourceItem } from '../types/models';
const columns: TableColumnDefinition<ModelSourceItem>[] = [
createTableColumn<ModelSourceItem>({
@ -165,7 +151,7 @@ const columns: TableColumnDefinition<ModelSourceItem>[] = [
})
];
export const Models: FC = observer(() => {
const Models: FC = observer(() => {
const { t } = useTranslation();
return (
@ -220,3 +206,5 @@ export const Models: FC = observer(() => {
} />
);
});
export default Models;

View File

@ -1,14 +1,14 @@
import React, { FC, useState } from 'react';
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
import commonStore from '../../stores/commonStore';
import { Preset } from './PresetsButton';
import { observer } from 'mobx-react-lite';
import { v4 as uuid } from 'uuid';
import { Button, Card, Dropdown, Option, Textarea } from '@fluentui/react-components';
import { useTranslation } from 'react-i18next';
import { ToolTipButton } from '../../components/ToolTipButton';
import { Delete20Regular, ReOrderDotsVertical20Regular } from '@fluentui/react-icons';
import { ConversationMessage, Role } from '../Chat';
import { Preset } from '../../types/presets';
import { ConversationMessage, Role } from '../../types/chat';
type Item = {
id: string;
@ -31,7 +31,7 @@ const reorder = (list: Item[], startIndex: number, endIndex: number) => {
return result;
};
export const MessagesEditor: FC = observer(() => {
const MessagesEditor: FC = observer(() => {
const { t } = useTranslation();
const editingPreset = commonStore.editingPreset!;
@ -152,3 +152,5 @@ export const MessagesEditor: FC = observer(() => {
</div>
);
});
export default MessagesEditor;

View File

@ -1,6 +1,6 @@
// TODO refactor
import React, { FC, PropsWithChildren, ReactElement, useState } from 'react';
import React, { FC, lazy, PropsWithChildren, ReactElement, useState } from 'react';
import {
Button,
Dialog,
@ -25,44 +25,21 @@ import {
} from '@fluentui/react-icons';
import { ToolTipButton } from '../../components/ToolTipButton';
import { useTranslation } from 'react-i18next';
import { botName, Conversation, ConversationMessage, MessageType, userName } from '../Chat';
import { SelectTabEventHandler } from '@fluentui/react-tabs';
import { Labeled } from '../../components/Labeled';
import commonStore from '../../stores/commonStore';
import logo from '../../assets/images/logo.png';
import { observer } from 'mobx-react-lite';
import { MessagesEditor } from './MessagesEditor';
import { ClipboardGetText, ClipboardSetText } from '../../../wailsjs/runtime';
import { toast } from 'react-toastify';
import { CustomToastContainer } from '../../components/CustomToastContainer';
import { v4 as uuid } from 'uuid';
import { absPathAsset } from '../../utils';
import { Preset, PresetsNavigationItem } from '../../types/presets';
import { botName, Conversation, MessageType, userName } from '../../types/chat';
import { LazyImportComponent } from '../../components/LazyImportComponent';
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 const defaultPreset: Preset = {
const defaultPreset: Preset = {
name: 'RWKV',
tag: 'default',
sourceUrl: '',
@ -78,6 +55,8 @@ export const defaultPreset: Preset = {
injectEnd: ''
};
const MessagesEditor = lazy(() => import('./MessagesEditor'));
const setActivePreset = (preset: Preset) => {
commonStore.setActivePreset(preset);
//TODO if (preset.displayPresetMessages) {
@ -101,7 +80,7 @@ const setActivePreset = (preset: Preset) => {
//}
};
export const PresetCardFrame: FC<PropsWithChildren & { onClick?: () => void }> = (props) => {
const PresetCardFrame: FC<PropsWithChildren & { onClick?: () => void }> = (props) => {
return <Button
className="flex flex-col gap-1 w-32 h-56 break-all"
style={{ minWidth: 0, borderRadius: '0.75rem', justifyContent: 'unset' }}
@ -111,7 +90,7 @@ export const PresetCardFrame: FC<PropsWithChildren & { onClick?: () => void }> =
</Button>;
};
export const PresetCard: FC<{
const PresetCard: FC<{
avatarImg: string,
name: string,
desc: string,
@ -147,7 +126,7 @@ export const PresetCard: FC<{
</PresetCardFrame>;
});
export const ChatPresetEditor: FC<{
const ChatPresetEditor: FC<{
triggerButton: ReactElement,
presetIndex: number
}> = observer(({ triggerButton, presetIndex }) => {
@ -291,7 +270,7 @@ export const ChatPresetEditor: FC<{
});
}} />
} />
<MessagesEditor />
<LazyImportComponent lazyChildren={MessagesEditor} />
</div> :
<div className="flex flex-col gap-1 p-2 overflow-x-hidden overflow-y-auto">
<Labeled flex breakline label={`${t('Description')} (${t('Preview Only')})`}
@ -356,7 +335,7 @@ export const ChatPresetEditor: FC<{
</Dialog>;
});
export const ChatPresets: FC = observer(() => {
const ChatPresets: FC = observer(() => {
const { t } = useTranslation();
return <div className="flex flex-wrap gap-2">
@ -392,11 +371,6 @@ export const ChatPresets: FC = observer(() => {
</div>;
});
type PresetsNavigationItem = {
icon: ReactElement;
element: ReactElement;
};
const pages: { [label: string]: PresetsNavigationItem } = {
Chat: {
icon: <Chat20Regular />,
@ -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);

View File

@ -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 <div className="flex flex-col gap-2">
<Labeled label={t('Language')} flex spaceBetween content={
<Dropdown style={{ minWidth: 0 }} listbox={{ style: { minWidth: 0 } }}
value={Languages[commonStore.settings.language]}
selectedOptions={[commonStore.settings.language]}
onOptionSelect={(_, data) => {
if (data.optionValue) {
const lang = data.optionValue as Language;
commonStore.setSettings({
language: lang
});
}
}}>
{
Object.entries(Languages).map(([langKey, desc]) =>
<Option key={langKey} value={langKey}>{desc}</Option>)
}
</Dropdown>
} />
{
commonStore.platform === 'windows' &&
<Labeled label={t('DPI Scaling')} flex spaceBetween content={
<Dropdown style={{ minWidth: 0 }} listbox={{ style: { minWidth: 0 } }}
value={commonStore.settings.dpiScaling + '%'}
selectedOptions={[commonStore.settings.dpiScaling.toString()]}
onOptionSelect={(_, data) => {
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) =>
<Option key={i} value={v.toString()}>{v + '%'}</Option>)
}
</Dropdown>
} />
}
<Labeled label={t('Dark Mode')} flex spaceBetween content={
<Switch checked={commonStore.settings.darkMode}
onChange={(e, data) => {
commonStore.setSettings({
darkMode: data.checked
});
}} />
} />
</div>;
});
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 <div className="flex flex-col gap-2">
<Labeled label={'API URL'}
content={
<div className="flex gap-2">
<Input style={{ minWidth: 0 }} className="grow" value={commonStore.settings.apiUrl}
onChange={(e, data) => {
commonStore.setSettings({
apiUrl: data.value
});
}} />
<Dropdown style={{ minWidth: '33px' }} listbox={{ style: { minWidth: 0 } }}
value="..." selectedOptions={[]} expandIcon={null}
onOptionSelect={(_, data) => {
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'
});
}
}}>
<Option value="">{t('Localhost')!}</Option>
<Option value="https://api.openai.com">OpenAI</Option>
</Dropdown>
</div>
} />
<Labeled label={'API Key'}
content={
<Input type="password" className="grow" placeholder="sk-" value={commonStore.settings.apiKey}
onChange={(e, data) => {
commonStore.setSettings({
apiKey: data.value
});
}} />
} />
<Labeled label={t('API Chat Model Name')}
content={
<div className="flex gap-2">
<Input style={{ minWidth: 0 }} className="grow" placeholder="rwkv"
value={commonStore.settings.apiChatModelName}
onChange={(e, data) => {
commonStore.setSettings({
apiChatModelName: data.value
});
}} />
<Dropdown style={{ minWidth: '33px' }} listbox={{ style: { minWidth: 0 } }}
value="..." selectedOptions={[]} expandIcon={null}
onOptionSelect={(_, data) => {
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) =>
<Option key={i} value={v}>{v}</Option>
)
}
</Dropdown>
</div>
} />
<Labeled label={t('API Completion Model Name')}
content={
<div className="flex gap-2">
<Input style={{ minWidth: 0 }} className="grow" placeholder="rwkv"
value={commonStore.settings.apiCompletionModelName}
onChange={(e, data) => {
commonStore.setSettings({
apiCompletionModelName: data.value
});
}} />
<Dropdown style={{ minWidth: '33px' }} listbox={{ style: { minWidth: 0 } }}
value="..." selectedOptions={[]} expandIcon={null}
onOptionSelect={(_, data) => {
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) =>
<Option key={i} value={v}>{v}</Option>
)
}
</Dropdown>
</div>
} />
</div>;
});
const Settings: FC = observer(() => {
const { t } = useTranslation();
const advancedHeaderRef = useRef<HTMLDivElement>(null);
@ -53,227 +191,101 @@ export const Settings: FC = observer(() => {
return (
<Page title={t('Settings')} content={
<div className="flex flex-col gap-2 overflow-y-auto overflow-x-hidden p-1">
<Labeled label={t('Language')} flex spaceBetween content={
<Dropdown style={{ minWidth: 0 }} listbox={{ style: { minWidth: 0 } }}
value={Languages[commonStore.settings.language]}
selectedOptions={[commonStore.settings.language]}
onOptionSelect={(_, data) => {
if (data.optionValue) {
const lang = data.optionValue as Language;
commonStore.setSettings({
language: lang
});
}
}}>
{
Object.entries(Languages).map(([langKey, desc]) =>
<Option key={langKey} value={langKey}>{desc}</Option>)
}
</Dropdown>
} />
{
commonStore.platform === 'windows' &&
<Labeled label={t('DPI Scaling')} flex spaceBetween content={
<Dropdown style={{ minWidth: 0 }} listbox={{ style: { minWidth: 0 } }}
value={commonStore.settings.dpiScaling + '%'}
selectedOptions={[commonStore.settings.dpiScaling.toString()]}
onOptionSelect={(_, data) => {
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) =>
<Option key={i} value={v.toString()}>{v + '%'}</Option>)
}
</Dropdown>
} />
}
<Labeled label={t('Dark Mode')} flex spaceBetween content={
<Switch checked={commonStore.settings.darkMode}
onChange={(e, data) => {
commonStore.setSettings({
darkMode: data.checked
});
}} />
} />
<Labeled label={t('Automatic Updates Check')} flex spaceBetween content={
<Switch checked={commonStore.settings.autoUpdatesCheck}
onChange={(e, data) => {
commonStore.setSettings({
autoUpdatesCheck: data.checked
});
if (data.checked)
checkUpdate(true);
}} />
} />
{
commonStore.settings.language === 'zh' &&
<Labeled label={t('Use Gitee Updates Source')} flex spaceBetween content={
<Switch checked={commonStore.settings.giteeUpdatesSource}
onChange={(e, data) => {
commonStore.setSettings({
giteeUpdatesSource: data.checked
});
}} />
} />
}
{
commonStore.settings.language === 'zh' && commonStore.platform !== 'linux' &&
<Labeled label={t('Use Tsinghua Pip Mirrors')} flex spaceBetween content={
<Switch checked={commonStore.settings.cnMirror}
onChange={(e, data) => {
commonStore.setSettings({
cnMirror: data.checked
});
}} />
} />
}
<Labeled label={t('Allow external access to the API (service must be restarted)')} flex spaceBetween content={
<Switch checked={commonStore.settings.host !== '127.0.0.1'}
onChange={(e, data) => {
commonStore.setSettings({
host: data.checked ? '0.0.0.0' : '127.0.0.1'
});
}} />
} />
<Accordion collapsible openItems={!commonStore.advancedCollapsed && 'advanced'} onToggle={(e, data) => {
if (data.value === 'advanced')
commonStore.setAdvancedCollapsed(!commonStore.advancedCollapsed);
}}>
<AccordionItem value="advanced">
<AccordionHeader ref={advancedHeaderRef} size="large">{t('Advanced')}</AccordionHeader>
<AccordionPanel>
<div className="flex flex-col gap-2 overflow-hidden">
{commonStore.platform !== 'darwin' &&
<Labeled label={t('Custom Models Path')}
content={
<Input className="grow" placeholder="./models" value={commonStore.settings.customModelsPath}
onChange={(e, data) => {
commonStore.setSettings({
customModelsPath: data.value
});
}} />
} />
}
<Labeled label={t('Custom Python Path')} // if set, will not use precompiled cuda kernel
content={
<Input className="grow" placeholder="./py310/python" value={commonStore.settings.customPythonPath}
onChange={(e, data) => {
commonStore.setDepComplete(false);
commonStore.setSettings({
customPythonPath: data.value
});
}} />
} />
<Labeled label={'API URL'}
content={
<div className="flex gap-2">
<Input style={{ minWidth: 0 }} className="grow" value={commonStore.settings.apiUrl}
onChange={(e, data) => {
commonStore.setSettings({
apiUrl: data.value
});
}} />
<Dropdown style={{ minWidth: 0 }} listbox={{ style: { minWidth: 0 } }}
value="..." selectedOptions={[]} expandIcon={null}
onOptionSelect={(_, data) => {
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'
});
}
}}>
<Option value="">{t('Localhost')!}</Option>
<Option value="https://api.openai.com">OpenAI</Option>
</Dropdown>
</div>
} />
<Labeled label={'API Key'}
content={
<Input className="grow" placeholder="sk-" value={commonStore.settings.apiKey}
onChange={(e, data) => {
commonStore.setSettings({
apiKey: data.value
});
}} />
} />
<Labeled label={t('API Chat Model Name')}
content={
<div className="flex gap-2">
<Input style={{ minWidth: 0 }} className="grow" placeholder="rwkv"
value={commonStore.settings.apiChatModelName}
onChange={(e, data) => {
commonStore.setSettings({
apiChatModelName: data.value
});
}} />
<Dropdown style={{ minWidth: 0 }} listbox={{ style: { minWidth: 0 } }}
value="..." selectedOptions={[]} expandIcon={null}
onOptionSelect={(_, data) => {
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) =>
<Option key={i} value={v}>{v}</Option>
)
}
</Dropdown>
</div>
} />
<Labeled label={t('API Completion Model Name')}
content={
<div className="flex gap-2">
<Input style={{ minWidth: 0 }} className="grow" placeholder="rwkv"
value={commonStore.settings.apiCompletionModelName}
onChange={(e, data) => {
commonStore.setSettings({
apiCompletionModelName: data.value
});
}} />
<Dropdown style={{ minWidth: 0 }} listbox={{ style: { minWidth: 0 } }}
value="..." selectedOptions={[]} expandIcon={null}
onOptionSelect={(_, data) => {
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) =>
<Option key={i} value={v}>{v}</Option>
)
}
</Dropdown>
</div>
} />
commonStore.platform === 'web' ?
(
<div className="flex flex-col gap-2">
<GeneralSettings />
<AdvancedGeneralSettings />
</div>
</AccordionPanel>
</AccordionItem>
</Accordion>
)
:
(
<div className="flex flex-col gap-2">
<GeneralSettings />
<Labeled label={t('Automatic Updates Check')} flex spaceBetween content={
<Switch checked={commonStore.settings.autoUpdatesCheck}
onChange={(e, data) => {
commonStore.setSettings({
autoUpdatesCheck: data.checked
});
if (data.checked)
checkUpdate(true);
}} />
} />
{
commonStore.settings.language === 'zh' &&
<Labeled label={t('Use Gitee Updates Source')} flex spaceBetween content={
<Switch checked={commonStore.settings.giteeUpdatesSource}
onChange={(e, data) => {
commonStore.setSettings({
giteeUpdatesSource: data.checked
});
}} />
} />
}
{
commonStore.settings.language === 'zh' && commonStore.platform !== 'linux' &&
<Labeled label={t('Use Tsinghua Pip Mirrors')} flex spaceBetween content={
<Switch checked={commonStore.settings.cnMirror}
onChange={(e, data) => {
commonStore.setSettings({
cnMirror: data.checked
});
}} />
} />
}
<Labeled label={t('Allow external access to the API (service must be restarted)')} flex spaceBetween
content={
<Switch checked={commonStore.settings.host !== '127.0.0.1'}
onChange={(e, data) => {
commonStore.setSettings({
host: data.checked ? '0.0.0.0' : '127.0.0.1'
});
}} />
} />
<Accordion collapsible openItems={!commonStore.advancedCollapsed && 'advanced'} onToggle={(e, data) => {
if (data.value === 'advanced')
commonStore.setAdvancedCollapsed(!commonStore.advancedCollapsed);
}}>
<AccordionItem value="advanced">
<AccordionHeader ref={advancedHeaderRef} size="large">{t('Advanced')}</AccordionHeader>
<AccordionPanel>
<div className="flex flex-col gap-2 overflow-hidden">
{commonStore.platform !== 'darwin' &&
<Labeled label={t('Custom Models Path')}
content={
<Input className="grow" placeholder="./models"
value={commonStore.settings.customModelsPath}
onChange={(e, data) => {
commonStore.setSettings({
customModelsPath: data.value
});
}} />
} />
}
<Labeled label={t('Custom Python Path')} // if set, will not use precompiled cuda kernel
content={
<Input className="grow" placeholder="./py310/python"
value={commonStore.settings.customPythonPath}
onChange={(e, data) => {
commonStore.setDepComplete(false);
commonStore.setSettings({
customPythonPath: data.value
});
}} />
} />
<AdvancedGeneralSettings />
</div>
</AccordionPanel>
</AccordionItem>
</Accordion>
</div>
)
}
</div>
} />
);
});
export default Settings;

View File

@ -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: <LoraFinetune />
@ -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 = () => {
</div>
</div>;
};
export default Train;

View File

@ -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 = '<pad>';

View File

@ -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<FC>;
top: boolean;
};
@ -35,70 +25,70 @@ export const pages: NavigationItem[] = [
label: 'Home',
path: '/',
icon: <Home20Regular />,
element: <Home />,
element: lazy(() => import('./Home')),
top: true
},
{
label: 'Chat',
path: '/chat',
icon: <Chat20Regular />,
element: <Chat />,
element: lazy(() => import('./Chat')),
top: true
},
{
label: 'Completion',
path: '/completion',
icon: <ClipboardEdit20Regular />,
element: <Completion />,
element: lazy(() => import('./Completion')),
top: true
},
{
label: 'Composition',
path: '/composition',
icon: <MusicNote220Regular />,
element: <Composition />,
element: lazy(() => import('./Composition')),
top: true
},
{
label: 'Configs',
path: '/configs',
icon: <DocumentSettings20Regular />,
element: <Configs />,
element: lazy(() => import('./Configs')),
top: true
},
{
label: 'Models',
path: '/models',
icon: <DataUsageSettings20Regular />,
element: <Models />,
element: lazy(() => import('./Models')),
top: true
},
{
label: 'Downloads',
path: '/downloads',
icon: <ArrowDownload20Regular />,
element: <Downloads />,
element: lazy(() => import('./Downloads')),
top: true
},
{
label: 'Train',
path: '/train',
icon: <Storage20Regular />,
element: <Train />,
element: lazy(() => import('./Train')),
top: true
},
{
label: 'Settings',
path: '/settings',
icon: <Settings20Regular />,
element: <Settings />,
element: lazy(() => import('./Settings')),
top: false
},
{
label: 'About',
path: '/about',
icon: <Info20Regular />,
element: <About />,
element: lazy(() => import('./About')),
top: false
}
];

View File

@ -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(() => {
});
}

View File

@ -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'
};

View File

@ -0,0 +1 @@
export type AboutContent = { [lang: string]: string }

View File

@ -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;
}

View File

@ -0,0 +1,12 @@
import { ApiParameters } from './configs';
export type CompletionParams = Omit<ApiParameters, 'apiPort'> & {
stop: string,
injectStart: string,
injectEnd: string
};
export type CompletionPreset = {
name: string,
prompt: string,
params: CompletionParams
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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
}

View File

@ -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;
};

View File

@ -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;

157
frontend/src/webWails.js Normal file
View File

@ -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))
})
}

View File

@ -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<string, string>) {
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)
}
}
}
}
});