mirror of
https://github.com/transformerlab/transformerlab-app.git
synced 2025-04-14 07:48:20 +03:00
first checkin
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
7
.erb/configs/.eslintrc
Normal file
7
.erb/configs/.eslintrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"no-console": "off",
|
||||||
|
"global-require": "off",
|
||||||
|
"import/no-dynamic-require": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
59
.erb/configs/webpack.config.base.ts
Normal file
59
.erb/configs/webpack.config.base.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Base webpack config used across other specific configs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import { dependencies as externals } from '../../release/app/package.json';
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
externals: [...Object.keys(externals || {})],
|
||||||
|
|
||||||
|
stats: 'errors-only',
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.[jt]sx?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
// Remove this line to enable type checking in webpack builds
|
||||||
|
transpileOnly: true,
|
||||||
|
compilerOptions: {
|
||||||
|
module: 'esnext',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.srcPath,
|
||||||
|
// https://github.com/webpack/webpack/issues/1114
|
||||||
|
library: {
|
||||||
|
type: 'commonjs2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the array of extensions that should be used to resolve modules.
|
||||||
|
*/
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
|
||||||
|
modules: [webpackPaths.srcPath, 'node_modules'],
|
||||||
|
// There is no need to add aliases here, the paths in tsconfig get mirrored
|
||||||
|
plugins: [new TsconfigPathsPlugins()],
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default configuration;
|
||||||
3
.erb/configs/webpack.config.eslint.ts
Normal file
3
.erb/configs/webpack.config.eslint.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/* eslint import/no-unresolved: off, import/no-self-import: off */
|
||||||
|
|
||||||
|
module.exports = require('./webpack.config.renderer.dev').default;
|
||||||
83
.erb/configs/webpack.config.main.prod.ts
Normal file
83
.erb/configs/webpack.config.main.prod.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Webpack config for production electron main process
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import { merge } from 'webpack-merge';
|
||||||
|
import TerserPlugin from 'terser-webpack-plugin';
|
||||||
|
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||||
|
import baseConfig from './webpack.config.base';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import checkNodeEnv from '../scripts/check-node-env';
|
||||||
|
import deleteSourceMaps from '../scripts/delete-source-maps';
|
||||||
|
|
||||||
|
checkNodeEnv('production');
|
||||||
|
deleteSourceMaps();
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
devtool: 'source-map',
|
||||||
|
|
||||||
|
mode: 'production',
|
||||||
|
|
||||||
|
target: 'electron-main',
|
||||||
|
|
||||||
|
entry: {
|
||||||
|
main: path.join(webpackPaths.srcMainPath, 'main.ts'),
|
||||||
|
preload: path.join(webpackPaths.srcMainPath, 'preload.ts'),
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.distMainPath,
|
||||||
|
filename: '[name].js',
|
||||||
|
library: {
|
||||||
|
type: 'umd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
parallel: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
|
||||||
|
analyzerPort: 8888,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create global constants which can be configured at compile time.
|
||||||
|
*
|
||||||
|
* Useful for allowing different behaviour between development builds and
|
||||||
|
* release builds
|
||||||
|
*
|
||||||
|
* NODE_ENV should be production so that modules do not perform certain
|
||||||
|
* development checks
|
||||||
|
*/
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
DEBUG_PROD: false,
|
||||||
|
START_MINIMIZED: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.type': '"browser"',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables webpack processing of __dirname and __filename.
|
||||||
|
* If you run the bundle in node.js it falls back to these values of node.js.
|
||||||
|
* https://github.com/webpack/webpack/issues/2010
|
||||||
|
*/
|
||||||
|
node: {
|
||||||
|
__dirname: false,
|
||||||
|
__filename: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default merge(baseConfig, configuration);
|
||||||
71
.erb/configs/webpack.config.preload.dev.ts
Normal file
71
.erb/configs/webpack.config.preload.dev.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import { merge } from 'webpack-merge';
|
||||||
|
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||||
|
import baseConfig from './webpack.config.base';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import checkNodeEnv from '../scripts/check-node-env';
|
||||||
|
|
||||||
|
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
|
||||||
|
// at the dev webpack config is not accidentally run in a production environment
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
checkNodeEnv('development');
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
|
||||||
|
mode: 'development',
|
||||||
|
|
||||||
|
target: 'electron-preload',
|
||||||
|
|
||||||
|
entry: path.join(webpackPaths.srcMainPath, 'preload.ts'),
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.dllPath,
|
||||||
|
filename: 'preload.js',
|
||||||
|
library: {
|
||||||
|
type: 'umd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create global constants which can be configured at compile time.
|
||||||
|
*
|
||||||
|
* Useful for allowing different behaviour between development builds and
|
||||||
|
* release builds
|
||||||
|
*
|
||||||
|
* NODE_ENV should be production so that modules do not perform certain
|
||||||
|
* development checks
|
||||||
|
*
|
||||||
|
* By default, use 'development' as NODE_ENV. This can be overriden with
|
||||||
|
* 'staging', for example, by changing the ENV variables in the npm scripts
|
||||||
|
*/
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.LoaderOptionsPlugin({
|
||||||
|
debug: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables webpack processing of __dirname and __filename.
|
||||||
|
* If you run the bundle in node.js it falls back to these values of node.js.
|
||||||
|
* https://github.com/webpack/webpack/issues/2010
|
||||||
|
*/
|
||||||
|
node: {
|
||||||
|
__dirname: false,
|
||||||
|
__filename: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default merge(baseConfig, configuration);
|
||||||
77
.erb/configs/webpack.config.renderer.dev.dll.ts
Normal file
77
.erb/configs/webpack.config.renderer.dev.dll.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Builds the DLL for development electron renderer process
|
||||||
|
*/
|
||||||
|
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import path from 'path';
|
||||||
|
import { merge } from 'webpack-merge';
|
||||||
|
import baseConfig from './webpack.config.base';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import { dependencies } from '../../package.json';
|
||||||
|
import checkNodeEnv from '../scripts/check-node-env';
|
||||||
|
|
||||||
|
checkNodeEnv('development');
|
||||||
|
|
||||||
|
const dist = webpackPaths.dllPath;
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
context: webpackPaths.rootPath,
|
||||||
|
|
||||||
|
devtool: 'eval',
|
||||||
|
|
||||||
|
mode: 'development',
|
||||||
|
|
||||||
|
target: 'electron-renderer',
|
||||||
|
|
||||||
|
externals: ['fsevents', 'crypto-browserify'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use `module` from `webpack.config.renderer.dev.js`
|
||||||
|
*/
|
||||||
|
module: require('./webpack.config.renderer.dev').default.module,
|
||||||
|
|
||||||
|
entry: {
|
||||||
|
renderer: Object.keys(dependencies || {}),
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: dist,
|
||||||
|
filename: '[name].dev.dll.js',
|
||||||
|
library: {
|
||||||
|
name: 'renderer',
|
||||||
|
type: 'var',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new webpack.DllPlugin({
|
||||||
|
path: path.join(dist, '[name].json'),
|
||||||
|
name: '[name]',
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create global constants which can be configured at compile time.
|
||||||
|
*
|
||||||
|
* Useful for allowing different behaviour between development builds and
|
||||||
|
* release builds
|
||||||
|
*
|
||||||
|
* NODE_ENV should be production so that modules do not perform certain
|
||||||
|
* development checks
|
||||||
|
*/
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.LoaderOptionsPlugin({
|
||||||
|
debug: true,
|
||||||
|
options: {
|
||||||
|
context: webpackPaths.srcPath,
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.dllPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default merge(baseConfig, configuration);
|
||||||
213
.erb/configs/webpack.config.renderer.dev.ts
Normal file
213
.erb/configs/webpack.config.renderer.dev.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import 'webpack-dev-server';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { merge } from 'webpack-merge';
|
||||||
|
import { execSync, spawn } from 'child_process';
|
||||||
|
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
|
||||||
|
import baseConfig from './webpack.config.base';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import checkNodeEnv from '../scripts/check-node-env';
|
||||||
|
|
||||||
|
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
|
||||||
|
// at the dev webpack config is not accidentally run in a production environment
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
checkNodeEnv('development');
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = process.env.PORT || 1212;
|
||||||
|
const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json');
|
||||||
|
const skipDLLs =
|
||||||
|
module.parent?.filename.includes('webpack.config.renderer.dev.dll') ||
|
||||||
|
module.parent?.filename.includes('webpack.config.eslint');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warn if the DLL is not built
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
!skipDLLs &&
|
||||||
|
!(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest))
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
chalk.black.bgYellow.bold(
|
||||||
|
'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
execSync('npm run postinstall');
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
|
||||||
|
mode: 'development',
|
||||||
|
|
||||||
|
target: ['web', 'electron-renderer'],
|
||||||
|
|
||||||
|
entry: [
|
||||||
|
`webpack-dev-server/client?http://localhost:${port}/dist`,
|
||||||
|
'webpack/hot/only-dev-server',
|
||||||
|
path.join(webpackPaths.srcRendererPath, 'index.tsx'),
|
||||||
|
],
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.distRendererPath,
|
||||||
|
publicPath: '/',
|
||||||
|
filename: 'renderer.dev.js',
|
||||||
|
library: {
|
||||||
|
type: 'umd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.s?(c|a)ss$/,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
modules: true,
|
||||||
|
sourceMap: true,
|
||||||
|
importLoaders: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'sass-loader',
|
||||||
|
],
|
||||||
|
include: /\.module\.s?(c|a)ss$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.s?css$/,
|
||||||
|
use: ['style-loader', 'css-loader', 'sass-loader'],
|
||||||
|
exclude: /\.module\.s?(c|a)ss$/,
|
||||||
|
},
|
||||||
|
// Fonts
|
||||||
|
{
|
||||||
|
test: /\.(woff|woff2|eot|ttf|otf)$/i,
|
||||||
|
type: 'asset/resource',
|
||||||
|
},
|
||||||
|
// Images
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|jpeg|gif)$/i,
|
||||||
|
type: 'asset/resource',
|
||||||
|
},
|
||||||
|
// SVG
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: '@svgr/webpack',
|
||||||
|
options: {
|
||||||
|
prettier: false,
|
||||||
|
svgo: false,
|
||||||
|
svgoConfig: {
|
||||||
|
plugins: [{ removeViewBox: false }],
|
||||||
|
},
|
||||||
|
titleProp: true,
|
||||||
|
ref: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'file-loader',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
...(skipDLLs
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
new webpack.DllReferencePlugin({
|
||||||
|
context: webpackPaths.dllPath,
|
||||||
|
manifest: require(manifest),
|
||||||
|
sourceType: 'var',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
|
||||||
|
new webpack.NoEmitOnErrorsPlugin(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create global constants which can be configured at compile time.
|
||||||
|
*
|
||||||
|
* Useful for allowing different behaviour between development builds and
|
||||||
|
* release builds
|
||||||
|
*
|
||||||
|
* NODE_ENV should be production so that modules do not perform certain
|
||||||
|
* development checks
|
||||||
|
*
|
||||||
|
* By default, use 'development' as NODE_ENV. This can be overriden with
|
||||||
|
* 'staging', for example, by changing the ENV variables in the npm scripts
|
||||||
|
*/
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.LoaderOptionsPlugin({
|
||||||
|
debug: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
new ReactRefreshWebpackPlugin(),
|
||||||
|
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: path.join('index.html'),
|
||||||
|
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
|
||||||
|
minify: {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeAttributeQuotes: true,
|
||||||
|
removeComments: true,
|
||||||
|
},
|
||||||
|
isBrowser: false,
|
||||||
|
env: process.env.NODE_ENV,
|
||||||
|
isDevelopment: process.env.NODE_ENV !== 'production',
|
||||||
|
nodeModules: webpackPaths.appNodeModulesPath,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
node: {
|
||||||
|
__dirname: false,
|
||||||
|
__filename: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
devServer: {
|
||||||
|
port,
|
||||||
|
compress: true,
|
||||||
|
hot: true,
|
||||||
|
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||||
|
static: {
|
||||||
|
publicPath: '/',
|
||||||
|
},
|
||||||
|
historyApiFallback: {
|
||||||
|
verbose: true,
|
||||||
|
},
|
||||||
|
setupMiddlewares(middlewares) {
|
||||||
|
console.log('Starting preload.js builder...');
|
||||||
|
const preloadProcess = spawn('npm', ['run', 'start:preload'], {
|
||||||
|
shell: true,
|
||||||
|
stdio: 'inherit',
|
||||||
|
})
|
||||||
|
.on('close', (code: number) => process.exit(code!))
|
||||||
|
.on('error', (spawnError) => console.error(spawnError));
|
||||||
|
|
||||||
|
console.log('Starting Main Process...');
|
||||||
|
let args = ['run', 'start:main'];
|
||||||
|
if (process.env.MAIN_ARGS) {
|
||||||
|
args = args.concat(
|
||||||
|
['--', ...process.env.MAIN_ARGS.matchAll(/"[^"]+"|[^\s"]+/g)].flat()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
spawn('npm', args, {
|
||||||
|
shell: true,
|
||||||
|
stdio: 'inherit',
|
||||||
|
})
|
||||||
|
.on('close', (code: number) => {
|
||||||
|
preloadProcess.kill();
|
||||||
|
process.exit(code!);
|
||||||
|
})
|
||||||
|
.on('error', (spawnError) => console.error(spawnError));
|
||||||
|
return middlewares;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default merge(baseConfig, configuration);
|
||||||
141
.erb/configs/webpack.config.renderer.prod.ts
Normal file
141
.erb/configs/webpack.config.renderer.prod.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Build config for electron renderer process
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import webpack from 'webpack';
|
||||||
|
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||||
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||||
|
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||||
|
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
|
||||||
|
import { merge } from 'webpack-merge';
|
||||||
|
import TerserPlugin from 'terser-webpack-plugin';
|
||||||
|
import baseConfig from './webpack.config.base';
|
||||||
|
import webpackPaths from './webpack.paths';
|
||||||
|
import checkNodeEnv from '../scripts/check-node-env';
|
||||||
|
import deleteSourceMaps from '../scripts/delete-source-maps';
|
||||||
|
|
||||||
|
checkNodeEnv('production');
|
||||||
|
deleteSourceMaps();
|
||||||
|
|
||||||
|
const configuration: webpack.Configuration = {
|
||||||
|
devtool: 'source-map',
|
||||||
|
|
||||||
|
mode: 'production',
|
||||||
|
|
||||||
|
target: ['web', 'electron-renderer'],
|
||||||
|
|
||||||
|
entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')],
|
||||||
|
|
||||||
|
output: {
|
||||||
|
path: webpackPaths.distRendererPath,
|
||||||
|
publicPath: './',
|
||||||
|
filename: 'renderer.js',
|
||||||
|
library: {
|
||||||
|
type: 'umd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.s?(a|c)ss$/,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
modules: true,
|
||||||
|
sourceMap: true,
|
||||||
|
importLoaders: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'sass-loader',
|
||||||
|
],
|
||||||
|
include: /\.module\.s?(c|a)ss$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.s?(a|c)ss$/,
|
||||||
|
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
|
||||||
|
exclude: /\.module\.s?(c|a)ss$/,
|
||||||
|
},
|
||||||
|
// Fonts
|
||||||
|
{
|
||||||
|
test: /\.(woff|woff2|eot|ttf|otf)$/i,
|
||||||
|
type: 'asset/resource',
|
||||||
|
},
|
||||||
|
// Images
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|jpeg|gif)$/i,
|
||||||
|
type: 'asset/resource',
|
||||||
|
},
|
||||||
|
// SVG
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: '@svgr/webpack',
|
||||||
|
options: {
|
||||||
|
prettier: false,
|
||||||
|
svgo: false,
|
||||||
|
svgoConfig: {
|
||||||
|
plugins: [{ removeViewBox: false }],
|
||||||
|
},
|
||||||
|
titleProp: true,
|
||||||
|
ref: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'file-loader',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
/**
|
||||||
|
* Create global constants which can be configured at compile time.
|
||||||
|
*
|
||||||
|
* Useful for allowing different behaviour between development builds and
|
||||||
|
* release builds
|
||||||
|
*
|
||||||
|
* NODE_ENV should be production so that modules do not perform certain
|
||||||
|
* development checks
|
||||||
|
*/
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
DEBUG_PROD: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: 'style.css',
|
||||||
|
}),
|
||||||
|
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
|
||||||
|
analyzerPort: 8889,
|
||||||
|
}),
|
||||||
|
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: 'index.html',
|
||||||
|
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
|
||||||
|
minify: {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeAttributeQuotes: true,
|
||||||
|
removeComments: true,
|
||||||
|
},
|
||||||
|
isBrowser: false,
|
||||||
|
isDevelopment: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.type': '"renderer"',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default merge(baseConfig, configuration);
|
||||||
38
.erb/configs/webpack.paths.ts
Normal file
38
.erb/configs/webpack.paths.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const rootPath = path.join(__dirname, '../..');
|
||||||
|
|
||||||
|
const dllPath = path.join(__dirname, '../dll');
|
||||||
|
|
||||||
|
const srcPath = path.join(rootPath, 'src');
|
||||||
|
const srcMainPath = path.join(srcPath, 'main');
|
||||||
|
const srcRendererPath = path.join(srcPath, 'renderer');
|
||||||
|
|
||||||
|
const releasePath = path.join(rootPath, 'release');
|
||||||
|
const appPath = path.join(releasePath, 'app');
|
||||||
|
const appPackagePath = path.join(appPath, 'package.json');
|
||||||
|
const appNodeModulesPath = path.join(appPath, 'node_modules');
|
||||||
|
const srcNodeModulesPath = path.join(srcPath, 'node_modules');
|
||||||
|
|
||||||
|
const distPath = path.join(appPath, 'dist');
|
||||||
|
const distMainPath = path.join(distPath, 'main');
|
||||||
|
const distRendererPath = path.join(distPath, 'renderer');
|
||||||
|
|
||||||
|
const buildPath = path.join(releasePath, 'build');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
rootPath,
|
||||||
|
dllPath,
|
||||||
|
srcPath,
|
||||||
|
srcMainPath,
|
||||||
|
srcRendererPath,
|
||||||
|
releasePath,
|
||||||
|
appPath,
|
||||||
|
appPackagePath,
|
||||||
|
appNodeModulesPath,
|
||||||
|
srcNodeModulesPath,
|
||||||
|
distPath,
|
||||||
|
distMainPath,
|
||||||
|
distRendererPath,
|
||||||
|
buildPath,
|
||||||
|
};
|
||||||
32
.erb/img/erb-banner.svg
Normal file
32
.erb/img/erb-banner.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 24 KiB |
BIN
.erb/img/erb-logo.png
Normal file
BIN
.erb/img/erb-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
6
.erb/img/palette-sponsor-banner.svg
Normal file
6
.erb/img/palette-sponsor-banner.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 33 KiB |
1
.erb/mocks/fileMock.js
Normal file
1
.erb/mocks/fileMock.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default 'test-file-stub';
|
||||||
8
.erb/scripts/.eslintrc
Normal file
8
.erb/scripts/.eslintrc
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"no-console": "off",
|
||||||
|
"global-require": "off",
|
||||||
|
"import/no-dynamic-require": "off",
|
||||||
|
"import/no-extraneous-dependencies": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
.erb/scripts/check-build-exists.ts
Normal file
24
.erb/scripts/check-build-exists.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Check if the renderer and main bundles are built
|
||||||
|
import path from 'path';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import fs from 'fs';
|
||||||
|
import webpackPaths from '../configs/webpack.paths';
|
||||||
|
|
||||||
|
const mainPath = path.join(webpackPaths.distMainPath, 'main.js');
|
||||||
|
const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js');
|
||||||
|
|
||||||
|
if (!fs.existsSync(mainPath)) {
|
||||||
|
throw new Error(
|
||||||
|
chalk.whiteBright.bgRed.bold(
|
||||||
|
'The main process is not built yet. Build it by running "npm run build:main"'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(rendererPath)) {
|
||||||
|
throw new Error(
|
||||||
|
chalk.whiteBright.bgRed.bold(
|
||||||
|
'The renderer process is not built yet. Build it by running "npm run build:renderer"'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
54
.erb/scripts/check-native-dep.js
Normal file
54
.erb/scripts/check-native-dep.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { dependencies } from '../../package.json';
|
||||||
|
|
||||||
|
if (dependencies) {
|
||||||
|
const dependenciesKeys = Object.keys(dependencies);
|
||||||
|
const nativeDeps = fs
|
||||||
|
.readdirSync('node_modules')
|
||||||
|
.filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`));
|
||||||
|
if (nativeDeps.length === 0) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Find the reason for why the dependency is installed. If it is installed
|
||||||
|
// because of a devDependency then that is okay. Warn when it is installed
|
||||||
|
// because of a dependency
|
||||||
|
const { dependencies: dependenciesObject } = JSON.parse(
|
||||||
|
execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString()
|
||||||
|
);
|
||||||
|
const rootDependencies = Object.keys(dependenciesObject);
|
||||||
|
const filteredRootDependencies = rootDependencies.filter((rootDependency) =>
|
||||||
|
dependenciesKeys.includes(rootDependency)
|
||||||
|
);
|
||||||
|
if (filteredRootDependencies.length > 0) {
|
||||||
|
const plural = filteredRootDependencies.length > 1;
|
||||||
|
console.log(`
|
||||||
|
${chalk.whiteBright.bgYellow.bold(
|
||||||
|
'Webpack does not work with native dependencies.'
|
||||||
|
)}
|
||||||
|
${chalk.bold(filteredRootDependencies.join(', '))} ${
|
||||||
|
plural ? 'are native dependencies' : 'is a native dependency'
|
||||||
|
} and should be installed inside of the "./release/app" folder.
|
||||||
|
First, uninstall the packages from "./package.json":
|
||||||
|
${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')}
|
||||||
|
${chalk.bold(
|
||||||
|
'Then, instead of installing the package to the root "./package.json":'
|
||||||
|
)}
|
||||||
|
${chalk.whiteBright.bgRed.bold('npm install your-package')}
|
||||||
|
${chalk.bold('Install the package to "./release/app/package.json"')}
|
||||||
|
${chalk.whiteBright.bgGreen.bold(
|
||||||
|
'cd ./release/app && npm install your-package'
|
||||||
|
)}
|
||||||
|
Read more about native dependencies at:
|
||||||
|
${chalk.bold(
|
||||||
|
'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure'
|
||||||
|
)}
|
||||||
|
`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Native dependencies could not be checked');
|
||||||
|
}
|
||||||
|
}
|
||||||
16
.erb/scripts/check-node-env.js
Normal file
16
.erb/scripts/check-node-env.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
export default function checkNodeEnv(expectedEnv) {
|
||||||
|
if (!expectedEnv) {
|
||||||
|
throw new Error('"expectedEnv" not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== expectedEnv) {
|
||||||
|
console.log(
|
||||||
|
chalk.whiteBright.bgRed.bold(
|
||||||
|
`"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
.erb/scripts/check-port-in-use.js
Normal file
16
.erb/scripts/check-port-in-use.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
import detectPort from 'detect-port';
|
||||||
|
|
||||||
|
const port = process.env.PORT || '1212';
|
||||||
|
|
||||||
|
detectPort(port, (err, availablePort) => {
|
||||||
|
if (port !== String(availablePort)) {
|
||||||
|
throw new Error(
|
||||||
|
chalk.whiteBright.bgRed.bold(
|
||||||
|
`Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
13
.erb/scripts/clean.js
Normal file
13
.erb/scripts/clean.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { rimrafSync } from 'rimraf';
|
||||||
|
import fs from 'fs';
|
||||||
|
import webpackPaths from '../configs/webpack.paths';
|
||||||
|
|
||||||
|
const foldersToRemove = [
|
||||||
|
webpackPaths.distPath,
|
||||||
|
webpackPaths.buildPath,
|
||||||
|
webpackPaths.dllPath,
|
||||||
|
];
|
||||||
|
|
||||||
|
foldersToRemove.forEach((folder) => {
|
||||||
|
if (fs.existsSync(folder)) rimrafSync(folder);
|
||||||
|
});
|
||||||
15
.erb/scripts/delete-source-maps.js
Normal file
15
.erb/scripts/delete-source-maps.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { rimrafSync } from 'rimraf';
|
||||||
|
import webpackPaths from '../configs/webpack.paths';
|
||||||
|
|
||||||
|
export default function deleteSourceMaps() {
|
||||||
|
if (fs.existsSync(webpackPaths.distMainPath))
|
||||||
|
rimrafSync(path.join(webpackPaths.distMainPath, '*.js.map'), {
|
||||||
|
glob: true,
|
||||||
|
});
|
||||||
|
if (fs.existsSync(webpackPaths.distRendererPath))
|
||||||
|
rimrafSync(path.join(webpackPaths.distRendererPath, '*.js.map'), {
|
||||||
|
glob: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
20
.erb/scripts/electron-rebuild.js
Normal file
20
.erb/scripts/electron-rebuild.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { execSync } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { dependencies } from '../../release/app/package.json';
|
||||||
|
import webpackPaths from '../configs/webpack.paths';
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(dependencies || {}).length > 0 &&
|
||||||
|
fs.existsSync(webpackPaths.appNodeModulesPath)
|
||||||
|
) {
|
||||||
|
const electronRebuildCmd =
|
||||||
|
'../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .';
|
||||||
|
const cmd =
|
||||||
|
process.platform === 'win32'
|
||||||
|
? electronRebuildCmd.replace(/\//g, '\\')
|
||||||
|
: electronRebuildCmd;
|
||||||
|
execSync(cmd, {
|
||||||
|
cwd: webpackPaths.appPath,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
}
|
||||||
9
.erb/scripts/link-modules.ts
Normal file
9
.erb/scripts/link-modules.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import webpackPaths from '../configs/webpack.paths';
|
||||||
|
|
||||||
|
const { srcNodeModulesPath } = webpackPaths;
|
||||||
|
const { appNodeModulesPath } = webpackPaths;
|
||||||
|
|
||||||
|
if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) {
|
||||||
|
fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction');
|
||||||
|
}
|
||||||
30
.erb/scripts/notarize.js
Normal file
30
.erb/scripts/notarize.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const { notarize } = require('@electron/notarize');
|
||||||
|
const { build } = require('../../package.json');
|
||||||
|
|
||||||
|
exports.default = async function notarizeMacos(context) {
|
||||||
|
const { electronPlatformName, appOutDir } = context;
|
||||||
|
if (electronPlatformName !== 'darwin') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.CI !== 'true') {
|
||||||
|
console.warn('Skipping notarizing step. Packaging is not running in CI');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) {
|
||||||
|
console.warn(
|
||||||
|
'Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appName = context.packager.appInfo.productFilename;
|
||||||
|
|
||||||
|
await notarize({
|
||||||
|
appBundleId: build.appId,
|
||||||
|
appPath: `${appOutDir}/${appName}.app`,
|
||||||
|
appleId: process.env.APPLE_ID,
|
||||||
|
appleIdPassword: process.env.APPLE_ID_PASS,
|
||||||
|
});
|
||||||
|
};
|
||||||
33
.eslintignore
Normal file
33
.eslintignore
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
release/app/dist
|
||||||
|
release/build
|
||||||
|
.erb/dll
|
||||||
|
|
||||||
|
.idea
|
||||||
|
npm-debug.log.*
|
||||||
|
*.css.d.ts
|
||||||
|
*.sass.d.ts
|
||||||
|
*.scss.d.ts
|
||||||
|
|
||||||
|
# eslint ignores hidden directories by default:
|
||||||
|
# https://github.com/eslint/eslint/issues/8429
|
||||||
|
!.erb
|
||||||
43
.eslintrc.js
Normal file
43
.eslintrc.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: 'erb',
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
rules: {
|
||||||
|
// A temporary hack related to IDE not resolving correct package.json
|
||||||
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/jsx-filename-extension': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'import/no-import-module-exports': 'off',
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': 'error',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'no-restricted-exports': 'off',
|
||||||
|
|
||||||
|
// TODO: Fix prop types!
|
||||||
|
'react/function-component-definition': 'off',
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
project: './tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
createDefaultProgram: true,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
// See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
|
||||||
|
node: {},
|
||||||
|
webpack: {
|
||||||
|
config: require.resolve('./.erb/configs/webpack.config.eslint.ts'),
|
||||||
|
},
|
||||||
|
typescript: {},
|
||||||
|
},
|
||||||
|
'import/parsers': {
|
||||||
|
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
6
.github/config.yml
vendored
Normal file
6
.github/config.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
requiredHeaders:
|
||||||
|
- Prerequisites
|
||||||
|
- Expected Behavior
|
||||||
|
- Current Behavior
|
||||||
|
- Possible Solution
|
||||||
|
- Your Environment
|
||||||
17
.github/stale.yml
vendored
Normal file
17
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Number of days of inactivity before an issue becomes stale
|
||||||
|
daysUntilStale: 60
|
||||||
|
# Number of days of inactivity before a stale issue is closed
|
||||||
|
daysUntilClose: 7
|
||||||
|
# Issues with these labels will never be considered stale
|
||||||
|
exemptLabels:
|
||||||
|
- discussion
|
||||||
|
- security
|
||||||
|
# Label to use when marking an issue as stale
|
||||||
|
staleLabel: wontfix
|
||||||
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
|
for your contributions.
|
||||||
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
|
closeComment: false
|
||||||
72
.github/workflows/codeql-analysis.yml
vendored
Normal file
72
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ "main" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '44 16 * * 4'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
|
||||||
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
|
|
||||||
|
# - run: |
|
||||||
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
13
.github/workflows/lint.yml
vendored
Normal file
13
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install modules
|
||||||
|
run: npm install
|
||||||
|
- name: Run ESLint
|
||||||
|
run: npm run lint
|
||||||
44
.github/workflows/publish.yml
vendored
Normal file
44
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout git repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Node and NPM
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install and build
|
||||||
|
run: |
|
||||||
|
npm install
|
||||||
|
npm run postinstall
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Publish releases
|
||||||
|
env:
|
||||||
|
# TODO: Ali will need to set these values
|
||||||
|
# These values are used for auto updates signing
|
||||||
|
# APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
# APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASS }}
|
||||||
|
# CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
# CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
# This is used for uploading release assets to github
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
npm exec electron-builder -- --publish always --win --mac --linux
|
||||||
34
.github/workflows/test.yml
vendored
Normal file
34
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Node.js and NPM
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: npm install
|
||||||
|
run: |
|
||||||
|
npm install
|
||||||
|
|
||||||
|
- name: npm test
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
npm run package
|
||||||
|
npm run lint
|
||||||
|
npm exec tsc
|
||||||
|
npm test
|
||||||
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
release/app/dist
|
||||||
|
release/build
|
||||||
|
.erb/dll
|
||||||
|
|
||||||
|
.idea
|
||||||
|
npm-debug.log.*
|
||||||
|
*.css.d.ts
|
||||||
|
*.sass.d.ts
|
||||||
|
*.scss.d.ts
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"]
|
||||||
|
}
|
||||||
30
.vscode/launch.json
vendored
Normal file
30
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Electron: Main",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"protocol": "inspector",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "start"],
|
||||||
|
"env": {
|
||||||
|
"MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Electron: Renderer",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "attach",
|
||||||
|
"port": 9223,
|
||||||
|
"webRoot": "${workspaceFolder}",
|
||||||
|
"timeout": 15000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Electron: All",
|
||||||
|
"configurations": ["Electron: Main", "Electron: Renderer"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
30
.vscode/settings.json
vendored
Normal file
30
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
".eslintrc": "jsonc",
|
||||||
|
".prettierrc": "jsonc",
|
||||||
|
".eslintignore": "ignore"
|
||||||
|
},
|
||||||
|
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"html",
|
||||||
|
"typescriptreact"
|
||||||
|
],
|
||||||
|
|
||||||
|
"javascript.validate.enable": false,
|
||||||
|
"javascript.format.enable": false,
|
||||||
|
"typescript.format.enable": false,
|
||||||
|
|
||||||
|
"search.exclude": {
|
||||||
|
".git": true,
|
||||||
|
".eslintcache": true,
|
||||||
|
".erb/dll": true,
|
||||||
|
"release/{build,app/dist}": true,
|
||||||
|
"node_modules": true,
|
||||||
|
"npm-debug.log.*": true,
|
||||||
|
"test/**/__snapshots__": true,
|
||||||
|
"package-lock.json": true,
|
||||||
|
"*.{css,sass,scss}.d.ts": true
|
||||||
|
}
|
||||||
|
}
|
||||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at electronreactboilerplate@gmail.com. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
||||||
112
README.md
Normal file
112
README.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<!-- PROJECT LOGO -->
|
||||||
|
<br />
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/transformerlab/transformerlab-app">
|
||||||
|
<img src="https://transformerlab.ai/img/flask.svg" alt="Logo" width="30" height="30">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<h1 align="center" style="color: rgb(68, 73, 80); letter-spacing: -1px">Transformer Lab</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Download, interact, and finetune models locally.
|
||||||
|
<br />
|
||||||
|
<a href=http://localhost:3000/docs/intro"><strong>Explore the docs »</strong></a>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a href="#">View Demo</a>
|
||||||
|
·
|
||||||
|
<a href="#">Report Bugs</a>
|
||||||
|
·
|
||||||
|
<a href="#">Suggest Features</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ABOUT THE PROJECT -->
|
||||||
|
|
||||||
|
## About The Project
|
||||||
|
|
||||||
|
[![Product Screen Shot][product-screenshot]](https://transformerlab.ai/)
|
||||||
|
|
||||||
|
Transformer Lab is an app that allows anyone to experiment with Large Language Models.
|
||||||
|
|
||||||
|
With Transformer Lab, you can:
|
||||||
|
|
||||||
|
- Download hundreds of popular models
|
||||||
|
- Interact with models through chat or completions
|
||||||
|
- Finetune models using a library of datasets, or your own data
|
||||||
|
- Evaluate models
|
||||||
|
- Access all of the functionality through a REST API
|
||||||
|
|
||||||
|
And you can do the above, all through a simple cross-platform GUI.
|
||||||
|
|
||||||
|
### Built With
|
||||||
|
|
||||||
|
- [![Electron][Electron]][Electron-url]
|
||||||
|
- [![React][React.js]][React-url]
|
||||||
|
- [![HuggingFace][HuggingFace]][HuggingFace-url]
|
||||||
|
|
||||||
|
<!-- GETTING STARTED -->
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
The best way to get started is to download the app from the Transformer Lab Homepage.
|
||||||
|
|
||||||
|
## Building from Scratch
|
||||||
|
|
||||||
|
To build the app yourself, pull this repo, and follow the steps below:
|
||||||
|
|
||||||
|
### Install packages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start the app in dev:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Packaging for Production
|
||||||
|
|
||||||
|
To package apps for the local platform:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run package
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- LICENSE -->
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Distributed under the ??? License. See `LICENSE.txt` for more information.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
If you found Transformer Lab useful in your research or applications, please cite using the following BibTeX:
|
||||||
|
|
||||||
|
```
|
||||||
|
@software{transformerlab,
|
||||||
|
author = {Asaria, Ali},
|
||||||
|
title = {Transformer Lab: Experiment with Large Language Models},
|
||||||
|
month = December,
|
||||||
|
year = 2023,
|
||||||
|
url = {https://github.com/transformerlab/transformerlab-app}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- CONTACT -->
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Ali Asaria - [@aliasaria](https://twitter.com/aliasaria)
|
||||||
|
|
||||||
|
<!-- MARKDOWN LINKS & IMAGES -->
|
||||||
|
|
||||||
|
[product-screenshot]: https://transformerlab.ai/assets/images/screenshot01-53ecb8c52338db3c9246cf2ebbbdc40d.png
|
||||||
|
[React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
|
||||||
|
[React-url]: https://reactjs.org/
|
||||||
|
[Electron]: https://img.shields.io/badge/Electron-20232A?style=for-the-badge&logo=electron&logoColor=61DAFB
|
||||||
|
[Electron-url]: https://www.electronjs.org/
|
||||||
|
[HuggingFace]: https://img.shields.io/badge/🤗_HuggingFace-20232A?style=for-the-badge
|
||||||
|
[HuggingFace-url]: https://huggingface.co/
|
||||||
35
assets/assets.d.ts
vendored
Normal file
35
assets/assets.d.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
type Styles = Record<string, string>;
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
import React = require('react');
|
||||||
|
|
||||||
|
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||||
|
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.png' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.scss' {
|
||||||
|
const content: Styles;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.sass' {
|
||||||
|
const content: Styles;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.css' {
|
||||||
|
const content: Styles;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
10
assets/entitlements.mac.plist
Normal file
10
assets/entitlements.mac.plist
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
assets/icon.png
Normal file
BIN
assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
74
assets/icon.svg
Normal file
74
assets/icon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
21688
package-lock.json
generated
Normal file
21688
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
278
package.json
Normal file
278
package.json
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
{
|
||||||
|
"description": "A tool to play with and train large language models",
|
||||||
|
"keywords": [
|
||||||
|
"LLM",
|
||||||
|
"LocalLLaMA"
|
||||||
|
],
|
||||||
|
"homepage": "https://transformerlab.ai",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://transformerlab.ai"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/transformerlab/transformerlab-app.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "Ali Asaria",
|
||||||
|
"email": "ali",
|
||||||
|
"url": "https://transformerlab.ai"
|
||||||
|
},
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Ghost Person",
|
||||||
|
"email": "ghostpersonk@gmail.com",
|
||||||
|
"url": "https://github.com/ghost"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": "0.0.1",
|
||||||
|
"main": "./src/main/main.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
||||||
|
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
||||||
|
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
|
||||||
|
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
|
||||||
|
"lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
|
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
|
||||||
|
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
|
||||||
|
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
|
||||||
|
"start:main": "cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only .",
|
||||||
|
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
|
||||||
|
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"browserslist": [],
|
||||||
|
"prettier": {
|
||||||
|
"singleQuote": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
".prettierrc",
|
||||||
|
".eslintrc"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"parser": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleDirectories": [
|
||||||
|
"node_modules",
|
||||||
|
"release/app/node_modules",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"jsx",
|
||||||
|
"ts",
|
||||||
|
"tsx",
|
||||||
|
"json"
|
||||||
|
],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/.erb/mocks/fileMock.js",
|
||||||
|
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
||||||
|
},
|
||||||
|
"setupFiles": [
|
||||||
|
"./.erb/scripts/check-build-exists.ts"
|
||||||
|
],
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
|
"testEnvironmentOptions": {
|
||||||
|
"url": "http://localhost/"
|
||||||
|
},
|
||||||
|
"testPathIgnorePatterns": [
|
||||||
|
"release/app/dist",
|
||||||
|
".erb/dll"
|
||||||
|
],
|
||||||
|
"transform": {
|
||||||
|
"\\.(ts|tsx|js|jsx)$": "ts-jest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.1",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@mui/joy": "^5.0.0-beta.8",
|
||||||
|
"@uppy/dashboard": "^3.5.1",
|
||||||
|
"@uppy/drag-drop": "^3.0.3",
|
||||||
|
"@uppy/file-input": "^3.0.3",
|
||||||
|
"@uppy/progress-bar": "^3.0.3",
|
||||||
|
"@uppy/react": "^3.1.3",
|
||||||
|
"@uppy/status-bar": "^3.2.4",
|
||||||
|
"@uppy/xhr-upload": "^3.3.2",
|
||||||
|
"basic-auth": "~2.0.1",
|
||||||
|
"cidr-matcher": "^2.1.1",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"easy-peasy": "^6.0.2",
|
||||||
|
"electron-debug": "^3.2.0",
|
||||||
|
"electron-download-manager": "^2.1.2",
|
||||||
|
"electron-log": "^4.4.8",
|
||||||
|
"electron-ssh2": "^0.1.2",
|
||||||
|
"electron-store": "^8.1.0",
|
||||||
|
"electron-updater": "^5.3.0",
|
||||||
|
"express": "^4.18.1",
|
||||||
|
"express-session": "^1.17.3",
|
||||||
|
"lucide-react": "^0.287.0",
|
||||||
|
"monaco-themes": "^0.4.4",
|
||||||
|
"morgan": "~1.10.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-markdown": "^8.0.7",
|
||||||
|
"react-router-dom": "^6.11.2",
|
||||||
|
"react-sparklines": "^1.7.0",
|
||||||
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"read-config-ng": "^3.0.5",
|
||||||
|
"remark-gfm": "^3.0.1",
|
||||||
|
"serve-favicon": "^2.5.0",
|
||||||
|
"socket.io": "^4.5.1",
|
||||||
|
"swr": "^2.2.0",
|
||||||
|
"use-debounce": "^9.0.4",
|
||||||
|
"validator": "^13.7.0",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
|
"xterm-addon-fit": "^0.8.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@electron/notarize": "^1.2.3",
|
||||||
|
"@electron/rebuild": "^3.3.0",
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||||
|
"@svgr/webpack": "^8.0.1",
|
||||||
|
"@teamsupercell/typings-for-css-modules-loader": "^2.5.2",
|
||||||
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^14.0.0",
|
||||||
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/node": "20.2.5",
|
||||||
|
"@types/react": "^18.2.8",
|
||||||
|
"@types/react-dom": "^18.2.4",
|
||||||
|
"@types/react-sparklines": "^1.7.2",
|
||||||
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
|
"@types/terser-webpack-plugin": "^5.0.4",
|
||||||
|
"@types/webpack-bundle-analyzer": "^4.6.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||||
|
"@typescript-eslint/parser": "^5.59.8",
|
||||||
|
"browserslist-config-erb": "^0.0.3",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"concurrently": "^8.1.0",
|
||||||
|
"core-js": "^3.30.2",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"css-loader": "^6.8.1",
|
||||||
|
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||||
|
"detect-port": "^1.5.1",
|
||||||
|
"electron": "^25.0.1",
|
||||||
|
"electron-builder": "^24.2.1",
|
||||||
|
"electron-devtools-installer": "^3.2.0",
|
||||||
|
"electronmon": "^2.0.2",
|
||||||
|
"eslint": "^8.42.0",
|
||||||
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
"eslint-config-erb": "^4.0.6",
|
||||||
|
"eslint-import-resolver-typescript": "^3.5.5",
|
||||||
|
"eslint-import-resolver-webpack": "^0.13.2",
|
||||||
|
"eslint-plugin-compat": "^4.1.4",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
|
"eslint-plugin-jest": "^27.2.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"html-webpack-plugin": "^5.5.1",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
|
"mini-css-extract-plugin": "^2.7.6",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
|
"react-refresh": "^0.14.0",
|
||||||
|
"react-test-renderer": "^18.2.0",
|
||||||
|
"rimraf": "^5.0.1",
|
||||||
|
"sass": "^1.62.1",
|
||||||
|
"sass-loader": "^13.3.1",
|
||||||
|
"style-loader": "^3.3.3",
|
||||||
|
"terser-webpack-plugin": "^5.3.9",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"ts-loader": "^9.4.3",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths-webpack-plugin": "^4.0.1",
|
||||||
|
"typescript": "^5.1.3",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack": "^5.85.0",
|
||||||
|
"webpack-bundle-analyzer": "^4.9.0",
|
||||||
|
"webpack-cli": "^5.1.1",
|
||||||
|
"webpack-dev-server": "^4.15.0",
|
||||||
|
"webpack-merge": "^5.9.0"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"productName": "Transformer Lab",
|
||||||
|
"appId": "ai.transformerlab.app",
|
||||||
|
"asar": true,
|
||||||
|
"asarUnpack": "**\\*.{node,dll}",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"extraFiles": [],
|
||||||
|
"afterSign": ".erb/scripts/notarize.js",
|
||||||
|
"mac": {
|
||||||
|
"target": {
|
||||||
|
"target": "default",
|
||||||
|
"arch": [
|
||||||
|
"arm64",
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "distribution",
|
||||||
|
"hardenedRuntime": true,
|
||||||
|
"entitlements": "assets/entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "assets/entitlements.mac.plist",
|
||||||
|
"gatekeeperAssess": false
|
||||||
|
},
|
||||||
|
"dmg": {
|
||||||
|
"contents": [
|
||||||
|
{
|
||||||
|
"x": 130,
|
||||||
|
"y": 220
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 410,
|
||||||
|
"y": 220,
|
||||||
|
"type": "link",
|
||||||
|
"path": "/Applications"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
"nsis"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": [
|
||||||
|
"AppImage"
|
||||||
|
],
|
||||||
|
"category": "Development"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"app": "release/app",
|
||||||
|
"buildResources": "assets",
|
||||||
|
"output": "release/build"
|
||||||
|
},
|
||||||
|
"extraResources": [
|
||||||
|
"./assets/**"
|
||||||
|
],
|
||||||
|
"publish": {
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "transformerlab",
|
||||||
|
"repo": "transformerlab-app"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devEngines": {
|
||||||
|
"node": ">=14.x",
|
||||||
|
"npm": ">=7.x"
|
||||||
|
},
|
||||||
|
"electronmon": {
|
||||||
|
"patterns": [
|
||||||
|
"!**/**",
|
||||||
|
"src/main/**"
|
||||||
|
],
|
||||||
|
"logLevel": "quiet"
|
||||||
|
}
|
||||||
|
}
|
||||||
150
release/app/package-lock.json
generated
Normal file
150
release/app/package-lock.json
generated
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"name": "transformerlab",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "transformerlab",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "???",
|
||||||
|
"dependencies": {
|
||||||
|
"ssh2": "^1.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/asn1": {
|
||||||
|
"version": "0.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||||
|
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": "~2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bcrypt-pbkdf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||||
|
"dependencies": {
|
||||||
|
"tweetnacl": "^0.14.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buildcheck": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cpu-features": {
|
||||||
|
"version": "0.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz",
|
||||||
|
"integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"buildcheck": "~0.0.6",
|
||||||
|
"nan": "^2.17.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nan": {
|
||||||
|
"version": "2.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
|
||||||
|
"integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
|
},
|
||||||
|
"node_modules/ssh2": {
|
||||||
|
"version": "1.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.14.0.tgz",
|
||||||
|
"integrity": "sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"asn1": "^0.2.6",
|
||||||
|
"bcrypt-pbkdf": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.16.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"cpu-features": "~0.0.8",
|
||||||
|
"nan": "^2.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tweetnacl": {
|
||||||
|
"version": "0.14.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
|
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"asn1": {
|
||||||
|
"version": "0.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||||
|
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||||
|
"requires": {
|
||||||
|
"safer-buffer": "~2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bcrypt-pbkdf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||||
|
"requires": {
|
||||||
|
"tweetnacl": "^0.14.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buildcheck": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"cpu-features": {
|
||||||
|
"version": "0.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz",
|
||||||
|
"integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"buildcheck": "~0.0.6",
|
||||||
|
"nan": "^2.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nan": {
|
||||||
|
"version": "2.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
|
||||||
|
"integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
|
},
|
||||||
|
"ssh2": {
|
||||||
|
"version": "1.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.14.0.tgz",
|
||||||
|
"integrity": "sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==",
|
||||||
|
"requires": {
|
||||||
|
"asn1": "^0.2.6",
|
||||||
|
"bcrypt-pbkdf": "^1.0.2",
|
||||||
|
"cpu-features": "~0.0.8",
|
||||||
|
"nan": "^2.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tweetnacl": {
|
||||||
|
"version": "0.14.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
|
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
release/app/package.json
Normal file
20
release/app/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "transformerlab",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "A tool to play with and train LLMs",
|
||||||
|
"license": "???",
|
||||||
|
"author": {
|
||||||
|
"name": "Ali Asaria",
|
||||||
|
"email": "ali.asaria@gmail.com",
|
||||||
|
"url": "https://github.com/aliasaria"
|
||||||
|
},
|
||||||
|
"main": "./dist/main/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
|
||||||
|
"postinstall": "npm run rebuild && npm run link-modules",
|
||||||
|
"link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ssh2": "^1.14.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/__tests__/App.test.tsx
Normal file
9
src/__tests__/App.test.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import App from '../renderer/App';
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
expect(render(<App />)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
19
src/main/get-platform.js
Normal file
19
src/main/get-platform.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { platform } from 'os';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
switch (platform()) {
|
||||||
|
case 'aix':
|
||||||
|
case 'freebsd':
|
||||||
|
case 'linux':
|
||||||
|
case 'openbsd':
|
||||||
|
case 'android':
|
||||||
|
return 'linux';
|
||||||
|
case 'darwin':
|
||||||
|
case 'sunos':
|
||||||
|
return 'mac';
|
||||||
|
case 'win32':
|
||||||
|
return 'win';
|
||||||
|
default:
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
};
|
||||||
182
src/main/main.ts
Normal file
182
src/main/main.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
/* eslint-disable prefer-template */
|
||||||
|
/* eslint global-require: off, no-console: off, promise/always-return: off */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module executes inside of electron's main process. You can start
|
||||||
|
* electron renderer process from here and communicate with the other processes
|
||||||
|
* through IPC.
|
||||||
|
*
|
||||||
|
* When running `npm run build` or `npm run build:main`, this file is compiled to
|
||||||
|
* `./src/main.js` using webpack. This gives us some performance wins.
|
||||||
|
*/
|
||||||
|
import path from 'path';
|
||||||
|
import { app, BrowserWindow, shell, ipcMain, utilityProcess } from 'electron';
|
||||||
|
import { autoUpdater } from 'electron-updater';
|
||||||
|
import log from 'electron-log';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
|
||||||
|
import MenuBuilder from './menu';
|
||||||
|
import { resolveHtmlPath } from './util';
|
||||||
|
|
||||||
|
// ////////////
|
||||||
|
// STORAGE
|
||||||
|
// ////////////
|
||||||
|
const store = new Store();
|
||||||
|
store.set('unicorn', '🦄');
|
||||||
|
|
||||||
|
ipcMain.handle('getStoreValue', (event, key) => {
|
||||||
|
return store.get(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('setStoreValue', (event, key, value) => {
|
||||||
|
console.log('setting', key, value);
|
||||||
|
return store.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('deleteStoreValue', (event, key) => {
|
||||||
|
console.log('deleting', key);
|
||||||
|
return store.delete(key);
|
||||||
|
});
|
||||||
|
// ////////////
|
||||||
|
// ////////////
|
||||||
|
|
||||||
|
class AppUpdater {
|
||||||
|
constructor() {
|
||||||
|
log.transports.file.level = 'info';
|
||||||
|
autoUpdater.logger = log;
|
||||||
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
process.on('uncaughtException', function (error) {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
const sourceMapSupport = require('source-map-support');
|
||||||
|
sourceMapSupport.install();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDebug =
|
||||||
|
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
require('electron-debug')();
|
||||||
|
}
|
||||||
|
|
||||||
|
const installExtensions = async () => {
|
||||||
|
const installer = require('electron-devtools-installer');
|
||||||
|
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
|
||||||
|
const extensions = ['REACT_DEVELOPER_TOOLS'];
|
||||||
|
|
||||||
|
return installer
|
||||||
|
.default(
|
||||||
|
extensions.map((name) => installer[name]),
|
||||||
|
forceDownload
|
||||||
|
)
|
||||||
|
.catch(console.log);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createWindow = async () => {
|
||||||
|
if (isDebug) {
|
||||||
|
await installExtensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
const RESOURCES_PATH = app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, 'assets')
|
||||||
|
: path.join(__dirname, '../../assets');
|
||||||
|
|
||||||
|
const getAssetPath = (...paths: string[]): string => {
|
||||||
|
return path.join(RESOURCES_PATH, ...paths);
|
||||||
|
};
|
||||||
|
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
show: false,
|
||||||
|
width: 1200,
|
||||||
|
height: 728,
|
||||||
|
minWidth: 900,
|
||||||
|
minHeight: 640,
|
||||||
|
icon: getAssetPath('icon.png'),
|
||||||
|
titleBarStyle: 'hiddenInset',
|
||||||
|
vibrancy: 'light',
|
||||||
|
visualEffectState: 'followWindow',
|
||||||
|
webPreferences: {
|
||||||
|
webSecurity: false,
|
||||||
|
preload: app.isPackaged
|
||||||
|
? path.join(__dirname, 'preload.js')
|
||||||
|
: path.join(__dirname, '../../.erb/dll/preload.js'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.loadURL(resolveHtmlPath('index.html'));
|
||||||
|
|
||||||
|
mainWindow.on('ready-to-show', () => {
|
||||||
|
if (!mainWindow) {
|
||||||
|
throw new Error('"mainWindow" is not defined');
|
||||||
|
}
|
||||||
|
if (process.env.START_MINIMIZED) {
|
||||||
|
mainWindow.minimize();
|
||||||
|
} else {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuBuilder = new MenuBuilder(mainWindow);
|
||||||
|
menuBuilder.buildMenu();
|
||||||
|
|
||||||
|
// Open urls in the user's browser
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
||||||
|
shell.openExternal(edata.url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
|
||||||
|
sshClient(mainWindow);
|
||||||
|
|
||||||
|
// Remove this if your app does not use auto updates
|
||||||
|
// eslint-disable-next-line
|
||||||
|
//new AppUpdater();
|
||||||
|
};
|
||||||
|
|
||||||
|
const appAboutOptions = {
|
||||||
|
applicationName: 'Transformer Lab',
|
||||||
|
applicationVersion: app.getVersion(),
|
||||||
|
credits: 'Made by @aliasaria',
|
||||||
|
authors: ['Ali Asaria'],
|
||||||
|
website: 'https://transformerlab.ai',
|
||||||
|
// iconPath: path.join(__dirname, 'assets/icon.png'),
|
||||||
|
};
|
||||||
|
app.setAboutPanelOptions(appAboutOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event listeners...
|
||||||
|
*/
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
// Respect the OSX convention of having the application in memory even
|
||||||
|
// after all windows have been closed
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// /////////////////////////////
|
||||||
|
import sshClient from './ssh-client';
|
||||||
|
// /////////////////////////////
|
||||||
|
|
||||||
|
app
|
||||||
|
.whenReady()
|
||||||
|
.then(() => {
|
||||||
|
createWindow();
|
||||||
|
app.on('activate', () => {
|
||||||
|
// On macOS it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
if (mainWindow === null) createWindow();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(console.log);
|
||||||
292
src/main/menu.ts
Normal file
292
src/main/menu.ts
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import {
|
||||||
|
app,
|
||||||
|
Menu,
|
||||||
|
shell,
|
||||||
|
BrowserWindow,
|
||||||
|
MenuItemConstructorOptions,
|
||||||
|
} from 'electron';
|
||||||
|
|
||||||
|
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
|
||||||
|
selector?: string;
|
||||||
|
submenu?: DarwinMenuItemConstructorOptions[] | Menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MenuBuilder {
|
||||||
|
mainWindow: BrowserWindow;
|
||||||
|
|
||||||
|
constructor(mainWindow: BrowserWindow) {
|
||||||
|
this.mainWindow = mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildMenu(): Menu {
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV === 'development' ||
|
||||||
|
process.env.DEBUG_PROD === 'true'
|
||||||
|
) {
|
||||||
|
this.setupDevelopmentEnvironment();
|
||||||
|
}
|
||||||
|
|
||||||
|
const template =
|
||||||
|
process.platform === 'darwin'
|
||||||
|
? this.buildDarwinTemplate()
|
||||||
|
: this.buildDefaultTemplate();
|
||||||
|
|
||||||
|
const menu = Menu.buildFromTemplate(template);
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDevelopmentEnvironment(): void {
|
||||||
|
this.mainWindow.webContents.on('context-menu', (_, props) => {
|
||||||
|
const { x, y } = props;
|
||||||
|
|
||||||
|
Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: 'Inspect element',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.webContents.inspectElement(x, y);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]).popup({ window: this.mainWindow });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDarwinTemplate(): MenuItemConstructorOptions[] {
|
||||||
|
const subMenuAbout: DarwinMenuItemConstructorOptions = {
|
||||||
|
label: 'Transformer Lab',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'About Transformer Lab',
|
||||||
|
selector: 'orderFrontStandardAboutPanel:',
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Services', submenu: [] },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Hide Transformer Lab',
|
||||||
|
accelerator: 'Command+H',
|
||||||
|
selector: 'hide:',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hide Others',
|
||||||
|
accelerator: 'Command+Shift+H',
|
||||||
|
selector: 'hideOtherApplications:',
|
||||||
|
},
|
||||||
|
{ label: 'Show All', selector: 'unhideAllApplications:' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Quit',
|
||||||
|
accelerator: 'Command+Q',
|
||||||
|
click: () => {
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const subMenuEdit: DarwinMenuItemConstructorOptions = {
|
||||||
|
label: 'Edit',
|
||||||
|
submenu: [
|
||||||
|
{ label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },
|
||||||
|
{ label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },
|
||||||
|
{ label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },
|
||||||
|
{ label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },
|
||||||
|
{
|
||||||
|
label: 'Select All',
|
||||||
|
accelerator: 'Command+A',
|
||||||
|
selector: 'selectAll:',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const subMenuViewDev: MenuItemConstructorOptions = {
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Reload',
|
||||||
|
accelerator: 'Command+R',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.webContents.reload();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Toggle Full Screen',
|
||||||
|
accelerator: 'Ctrl+Command+F',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Toggle Developer Tools',
|
||||||
|
accelerator: 'Alt+Command+I',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.webContents.toggleDevTools();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'resetZoom' },
|
||||||
|
{ role: 'zoomIn' },
|
||||||
|
{ role: 'zoomOut' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const subMenuViewProd: MenuItemConstructorOptions = {
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Toggle Full Screen',
|
||||||
|
accelerator: 'Ctrl+Command+F',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const subMenuWindow: DarwinMenuItemConstructorOptions = {
|
||||||
|
label: 'Window',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Minimize',
|
||||||
|
accelerator: 'Command+M',
|
||||||
|
selector: 'performMiniaturize:',
|
||||||
|
},
|
||||||
|
{ label: 'Close', accelerator: 'Command+W', selector: 'performClose:' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Bring All to Front', selector: 'arrangeInFront:' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const subMenuHelp: MenuItemConstructorOptions = {
|
||||||
|
label: 'Help',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Learn More',
|
||||||
|
click() {
|
||||||
|
shell.openExternal('https://transformerlab.ai');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Documentation',
|
||||||
|
click() {
|
||||||
|
shell.openExternal('https://transformerlab.ai/docs/intro');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// label: 'Community Discussions',
|
||||||
|
// click() {
|
||||||
|
// shell.openExternal('https://www.electronjs.org/community');
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'Search Issues',
|
||||||
|
// click() {
|
||||||
|
// shell.openExternal('https://github.com/electron/electron/issues');
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const subMenuView =
|
||||||
|
process.env.NODE_ENV === 'development' ||
|
||||||
|
process.env.DEBUG_PROD === 'true'
|
||||||
|
? subMenuViewDev
|
||||||
|
: subMenuViewProd;
|
||||||
|
|
||||||
|
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDefaultTemplate() {
|
||||||
|
const templateDefault = [
|
||||||
|
{
|
||||||
|
label: '&File',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: '&Open',
|
||||||
|
accelerator: 'Ctrl+O',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '&Close',
|
||||||
|
accelerator: 'Ctrl+W',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.close();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '&View',
|
||||||
|
submenu:
|
||||||
|
process.env.NODE_ENV === 'development' ||
|
||||||
|
process.env.DEBUG_PROD === 'true'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: '&Reload',
|
||||||
|
accelerator: 'Ctrl+R',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.webContents.reload();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Toggle &Full Screen',
|
||||||
|
accelerator: 'F11',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.setFullScreen(
|
||||||
|
!this.mainWindow.isFullScreen()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Toggle &Developer Tools',
|
||||||
|
accelerator: 'Alt+Ctrl+I',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.webContents.toggleDevTools();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
label: 'Toggle &Full Screen',
|
||||||
|
accelerator: 'F11',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.setFullScreen(
|
||||||
|
!this.mainWindow.isFullScreen()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Help',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Learn More',
|
||||||
|
click() {
|
||||||
|
shell.openExternal('https://electronjs.org');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Documentation',
|
||||||
|
click() {
|
||||||
|
shell.openExternal(
|
||||||
|
'https://github.com/electron/electron/tree/main/docs#readme'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Community Discussions',
|
||||||
|
click() {
|
||||||
|
shell.openExternal('https://www.electronjs.org/community');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Search Issues',
|
||||||
|
click() {
|
||||||
|
shell.openExternal('https://github.com/electron/electron/issues');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return templateDefault;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/main/preload.ts
Normal file
80
src/main/preload.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Disable no-unused-vars, broken for spread args
|
||||||
|
/* eslint no-unused-vars: off */
|
||||||
|
import {
|
||||||
|
contextBridge,
|
||||||
|
ipcRenderer,
|
||||||
|
IpcRendererEvent,
|
||||||
|
webFrame,
|
||||||
|
} from 'electron';
|
||||||
|
|
||||||
|
webFrame.setZoomFactor(0.85);
|
||||||
|
|
||||||
|
export type Channels =
|
||||||
|
| 'ipc-example'
|
||||||
|
| 'getStoreValue'
|
||||||
|
| 'setStoreValue'
|
||||||
|
| 'deleteStoreValue'
|
||||||
|
| 'spawn-start-controller'
|
||||||
|
| 'spawn-start-model-worker'
|
||||||
|
| 'spawn-quit-model-worker'
|
||||||
|
| 'spawn-start-transformerlab-api'
|
||||||
|
| 'spawn-start-localai'
|
||||||
|
| 'openURL';
|
||||||
|
|
||||||
|
const electronHandler = {
|
||||||
|
ipcRenderer: {
|
||||||
|
sendMessage(channel: Channels, ...args: unknown[]) {
|
||||||
|
ipcRenderer.send(channel, ...args);
|
||||||
|
},
|
||||||
|
on(channel: Channels, func: (...args: unknown[]) => void) {
|
||||||
|
const subscription = (_event: IpcRendererEvent, ...args: unknown[]) =>
|
||||||
|
func(...args);
|
||||||
|
ipcRenderer.on(channel, subscription);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener(channel, subscription);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
once(channel: Channels, func: (...args: unknown[]) => void) {
|
||||||
|
ipcRenderer.once(channel, (_event, ...args) => func(...args));
|
||||||
|
},
|
||||||
|
removeAllListeners: (channel: string) =>
|
||||||
|
ipcRenderer.removeAllListeners(channel),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ElectronHandler = typeof electronHandler;
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electron', electronHandler);
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('versions', {
|
||||||
|
node: () => process.versions.node,
|
||||||
|
chrome: () => process.versions.chrome,
|
||||||
|
electron: () => process.versions.electron,
|
||||||
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('storage', {
|
||||||
|
get: (key: string) => {
|
||||||
|
return ipcRenderer.invoke('getStoreValue', key);
|
||||||
|
},
|
||||||
|
set: (key: string, value: string) => {
|
||||||
|
return ipcRenderer.invoke('setStoreValue', key, value);
|
||||||
|
},
|
||||||
|
delete: (key: string) => {
|
||||||
|
console.log('inv delete', key);
|
||||||
|
return ipcRenderer.invoke('deleteStoreValue', key);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('sshClient', {
|
||||||
|
connect: (data) => ipcRenderer.invoke('ssh:connect', data),
|
||||||
|
data: (data) => ipcRenderer.send('ssh:data', data),
|
||||||
|
|
||||||
|
onData: (data) => ipcRenderer.on('ssh:data', data),
|
||||||
|
onSSHConnected: (callback) => ipcRenderer.on('ssh:connected', callback),
|
||||||
|
|
||||||
|
removeAllListeners: () => {
|
||||||
|
ipcRenderer.removeAllListeners('ssh:data');
|
||||||
|
ipcRenderer.removeAllListeners('ssh:connected');
|
||||||
|
},
|
||||||
|
});
|
||||||
68
src/main/shell_commands/shellCommands.js
Normal file
68
src/main/shell_commands/shellCommands.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
export const updateAndInstallCommand = `
|
||||||
|
TFL_FILENAME="transformerlab_api_v0.1.0.zip"
|
||||||
|
TFL_URL="https://transformerlab-binaries.s3.amazonaws.com/\${TFL_FILENAME}"
|
||||||
|
TFL_DEST_DIR="\${HOME}/.transformerlab/src/"
|
||||||
|
echo "Check if \${TFL_DEST_DIR} exists"
|
||||||
|
mkdir -p "\${TFL_DEST_DIR}"
|
||||||
|
echo "Downloading \${TFL_URL} to \${TFL_DEST_DIR}"
|
||||||
|
curl "\${TFL_URL}" --output "\${TFL_DEST_DIR}\${TFL_FILENAME}"
|
||||||
|
unzip -o "\${TFL_DEST_DIR}\${TFL_FILENAME}" -d "$TFL_DEST_DIR"
|
||||||
|
echo "Starting API server"
|
||||||
|
cd "\${TFL_DEST_DIR}" || exit
|
||||||
|
|
||||||
|
if [ -f .DEPENDENCIES_INSTALLED ]; then
|
||||||
|
echo "Dependencies already installed. Skipping."
|
||||||
|
echo "To reinstall dependencies, delete the .DEPENDENCIES_INSTALLED file and run this script again."
|
||||||
|
else
|
||||||
|
./init.sh
|
||||||
|
touch .DEPENDENCIES_INSTALLED
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const installOnlyIfNotInstalledCommand = `
|
||||||
|
TFL_FILENAME="transformerlab_api_v0.1.0.zip"
|
||||||
|
TFL_URL="https://transformerlab-binaries.s3.amazonaws.com/\${TFL_FILENAME}"
|
||||||
|
TFL_DEST_DIR="\${HOME}/.transformerlab/src/"
|
||||||
|
echo "Check if \${TFL_DEST_DIR} exists"
|
||||||
|
TFL_DEST_DIR="\${HOME}/.transformerlab/src/"
|
||||||
|
# Check if the Install directory exists, if so, do nothing
|
||||||
|
if [[ ! -d "\${TFL_DEST_DIR}" ]]
|
||||||
|
then
|
||||||
|
mkdir -p "\${TFL_DEST_DIR}"
|
||||||
|
echo "Downloading \${TFL_URL} to \${TFL_DEST_DIR}"
|
||||||
|
curl "\${TFL_URL}" --output "\${TFL_DEST_DIR}\${TFL_FILENAME}"
|
||||||
|
unzip -o "\${TFL_DEST_DIR}\${TFL_FILENAME}" -d "$TFL_DEST_DIR"
|
||||||
|
echo "Starting API server"
|
||||||
|
cd "\${TFL_DEST_DIR}" || exit
|
||||||
|
|
||||||
|
if [ -f .DEPENDENCIES_INSTALLED ]; then
|
||||||
|
echo "Dependencies already installed. Skipping."
|
||||||
|
echo "To reinstall dependencies, delete the .DEPENDENCIES_INSTALLED file and run this script again."
|
||||||
|
else
|
||||||
|
./init.sh
|
||||||
|
touch .DEPENDENCIES_INSTALLED
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Install directory already exists. Skipping."
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const runCommand = `
|
||||||
|
conda activate transformerlab
|
||||||
|
TFL_DEST_DIR="\${HOME}/.transformerlab/src/"
|
||||||
|
cd "\${TFL_DEST_DIR}" || exit
|
||||||
|
./run.sh
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const runCommandInBackground = `
|
||||||
|
conda activate transformerlab
|
||||||
|
TFL_DEST_DIR="\${HOME}/.transformerlab/src/"
|
||||||
|
cd "\${TFL_DEST_DIR}" || exit
|
||||||
|
if [ -f pid.nohup ]; then
|
||||||
|
echo "PID file exists, killing process"
|
||||||
|
kill $(cat pid.nohup)
|
||||||
|
fi
|
||||||
|
nohup ./run.sh > /dev/null 2>&1 &
|
||||||
|
# Write the PID to a file
|
||||||
|
echo $! > pid.nohup
|
||||||
|
`;
|
||||||
123
src/main/ssh-client.js
Normal file
123
src/main/ssh-client.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
var Client = require('electron-ssh2').Client;
|
||||||
|
import { app, ipcMain } from 'electron';
|
||||||
|
|
||||||
|
import * as shellCommands from './shell_commands/shellCommands';
|
||||||
|
|
||||||
|
const HOME_DIR = app.getPath('home');
|
||||||
|
|
||||||
|
const default_private_key = require('fs').readFileSync(
|
||||||
|
HOME_DIR + '/.ssh/id_rsa',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
var mainWindow = null;
|
||||||
|
|
||||||
|
function sendToRenderer(channel, data) {
|
||||||
|
if (mainWindow === null) {
|
||||||
|
console.log('mainWindow is null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mainWindow.webContents.send(channel, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('ssh:connect', (event, key) => {
|
||||||
|
console.log('ssh:connect');
|
||||||
|
|
||||||
|
// Core SSH connection parameters
|
||||||
|
const host = key.host;
|
||||||
|
const username = key.username;
|
||||||
|
const password = key.password;
|
||||||
|
const sshkeylocation = key?.sshkeylocation;
|
||||||
|
|
||||||
|
// Extra options:
|
||||||
|
const update_and_install = key?.update_and_install;
|
||||||
|
const create_reverse_tunnel = key?.create_reverse_tunnel;
|
||||||
|
const run_permanent = key?.run_permanent;
|
||||||
|
|
||||||
|
const tryKeyboard = key?.tryKeyboard;
|
||||||
|
console.log('tryKeyboard', tryKeyboard);
|
||||||
|
|
||||||
|
if (sshkeylocation) {
|
||||||
|
var private_key = require('fs').readFileSync(sshkeylocation, 'utf8');
|
||||||
|
} else {
|
||||||
|
var private_key = default_private_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = '';
|
||||||
|
|
||||||
|
ipcMain.removeAllListeners('ssh:data');
|
||||||
|
ipcMain.removeAllListeners('ssh:resize');
|
||||||
|
var conn = new Client();
|
||||||
|
|
||||||
|
conn
|
||||||
|
.on('ready', function () {
|
||||||
|
sendToRenderer('ssh:connected', true);
|
||||||
|
console.log('Client :: ready');
|
||||||
|
conn.shell(function (err, stream) {
|
||||||
|
if (err) {
|
||||||
|
console.log('error', err);
|
||||||
|
sendToRenderer('ssh:connected', false);
|
||||||
|
return conn.end();
|
||||||
|
}
|
||||||
|
stream
|
||||||
|
.on('close', function () {
|
||||||
|
console.log('Stream :: close');
|
||||||
|
conn.end();
|
||||||
|
ipcMain.removeAllListeners('ssh:data');
|
||||||
|
ipcMain.removeAllListeners('ssh:resize');
|
||||||
|
})
|
||||||
|
.on('data', function (data) {
|
||||||
|
sendToRenderer('ssh:data', data.toString('utf-8'));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (update_and_install) {
|
||||||
|
stream.write(shellCommands.updateAndInstallCommand);
|
||||||
|
} else {
|
||||||
|
stream.write(shellCommands.installOnlyIfNotInstalledCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (create_reverse_tunnel) {
|
||||||
|
console.log('create_reverse_tunnel is not implemented yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (run_permanent) {
|
||||||
|
stream.write(shellCommands.runCommand);
|
||||||
|
} else {
|
||||||
|
stream.write(shellCommands.runCommandInBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.on('ssh:data', (event, key) => {
|
||||||
|
stream.write(key);
|
||||||
|
});
|
||||||
|
ipcMain.on('ssh:resize', (event, key) => {
|
||||||
|
stream.setWindow(data.rows, data.cols);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.connect({
|
||||||
|
host: host,
|
||||||
|
port: 22,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
privateKey: private_key,
|
||||||
|
tryKeyboard: key?.tryKeyboard,
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.on('end', (err) => {
|
||||||
|
if (err) console.log('CONN END BY HOST', err);
|
||||||
|
});
|
||||||
|
conn.on('close', (err) => {
|
||||||
|
if (err) console.log('CONN CLOSE', err);
|
||||||
|
});
|
||||||
|
conn.on('error', (err) => {
|
||||||
|
console.log(err);
|
||||||
|
// Send the error to the user
|
||||||
|
sendToRenderer('ssh:data', err.toString('utf-8'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function setupSSHClient(browserWindow) {
|
||||||
|
console.log('setting up ssh client');
|
||||||
|
console.log(browserWindow);
|
||||||
|
mainWindow = browserWindow;
|
||||||
|
}
|
||||||
13
src/main/util.ts
Normal file
13
src/main/util.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/* eslint import/prefer-default-export: off */
|
||||||
|
import { URL } from 'url';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export function resolveHtmlPath(htmlFileName: string) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
const port = process.env.PORT || 1212;
|
||||||
|
const url = new URL(`http://localhost:${port}`);
|
||||||
|
url.pathname = htmlFileName;
|
||||||
|
return url.href;
|
||||||
|
}
|
||||||
|
return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`;
|
||||||
|
}
|
||||||
116
src/renderer/App.tsx
Normal file
116
src/renderer/App.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { CssVarsProvider } from '@mui/joy/styles';
|
||||||
|
import CssBaseline from '@mui/joy/CssBaseline';
|
||||||
|
import Box from '@mui/joy/Box';
|
||||||
|
|
||||||
|
import Sidebar from './components/Nav/Sidebar';
|
||||||
|
import MainAppPanel from './components/MainAppPanel';
|
||||||
|
import Header from './components/Header';
|
||||||
|
|
||||||
|
import customTheme from './lib/theme';
|
||||||
|
|
||||||
|
import './styles.css';
|
||||||
|
import LoginModal from './components/Connect/LoginModal';
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import XtermJSDrawer from './components/Connect/XtermJS';
|
||||||
|
import OutputTerminal from './components/OutputTerminal';
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [experimentId, setExperimentId] = useState('');
|
||||||
|
|
||||||
|
const [connection, setConnection] = useState('');
|
||||||
|
|
||||||
|
const [sshConnection, setSSHConnection] = useState(null);
|
||||||
|
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!window.TransformerLab) {
|
||||||
|
window.TransformerLab = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TransformerLab.API_URL = connection;
|
||||||
|
}, [connection]);
|
||||||
|
|
||||||
|
// Fetch the experiment info, if the experimentId changes
|
||||||
|
const {
|
||||||
|
data: experimentInfo,
|
||||||
|
error: experimentInfoError,
|
||||||
|
isLoading: experimentInfoIsLoading,
|
||||||
|
mutate: experimentInfoMutate,
|
||||||
|
} = useSWR(chatAPI.GET_EXPERIMENT_URL(experimentId), fetcher);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CssVarsProvider disableTransitionOnChange theme={customTheme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Box
|
||||||
|
component="main"
|
||||||
|
className="MainContent"
|
||||||
|
sx={() => ({
|
||||||
|
display: 'grid',
|
||||||
|
height: '100dvh',
|
||||||
|
width: '100dvw',
|
||||||
|
overflow: 'hidden',
|
||||||
|
gridTemplateColumns: '220px 1fr',
|
||||||
|
gridTemplateRows: '60px 5fr 0fr',
|
||||||
|
gridTemplateAreas: `
|
||||||
|
"sidebar header"
|
||||||
|
"sidebar main"
|
||||||
|
"sidebar footer"
|
||||||
|
`,
|
||||||
|
|
||||||
|
// backgroundColor: (theme) => theme.vars.palette.background.surface,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Header
|
||||||
|
connection={connection}
|
||||||
|
setConnection={setConnection}
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
/>
|
||||||
|
{/* <FirstSidebar setDrawerOpen={setDrawerOpen} /> */}
|
||||||
|
|
||||||
|
<Sidebar
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
setExperimentId={setExperimentId}
|
||||||
|
setDrawerOpen={setDrawerOpen}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
px: {
|
||||||
|
xs: 1,
|
||||||
|
md: 6,
|
||||||
|
},
|
||||||
|
paddingTop: '32px',
|
||||||
|
height: '100%',
|
||||||
|
gridArea: 'main',
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: 'var(--joy-palette-background-surface)',
|
||||||
|
}}
|
||||||
|
id="main-app-panel"
|
||||||
|
>
|
||||||
|
<MainAppPanel
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
setExperimentId={setExperimentId}
|
||||||
|
experimentInfoMutate={experimentInfoMutate}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{/* <OutputTerminal /> */}
|
||||||
|
<LoginModal
|
||||||
|
setServer={setConnection}
|
||||||
|
connection={connection}
|
||||||
|
setTerminalDrawerOpen={setDrawerOpen}
|
||||||
|
setSSHConnection={setSSHConnection}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<XtermJSDrawer
|
||||||
|
sshConnection={sshConnection}
|
||||||
|
drawerOpen={drawerOpen}
|
||||||
|
setDrawerOpen={setDrawerOpen}
|
||||||
|
/>
|
||||||
|
</CssVarsProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
163
src/renderer/components/Computer.tsx
Normal file
163
src/renderer/components/Computer.tsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
|
||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
|
||||||
|
import { Card, CardContent, Chip, Grid, Typography } from '@mui/joy';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CalculatorIcon,
|
||||||
|
Code2Icon,
|
||||||
|
DatabaseIcon,
|
||||||
|
FlameIcon,
|
||||||
|
GridIcon,
|
||||||
|
LayoutIcon,
|
||||||
|
MemoryStickIcon,
|
||||||
|
RouterIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import { formatBytes } from 'renderer/lib/utils';
|
||||||
|
|
||||||
|
import { useServerStats } from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
function getSystemProperties() {
|
||||||
|
const information = document.getElementById('info');
|
||||||
|
information.innerText = `This app is using Chrome (v${window.versions.chrome()}), Node.js (v${window.versions.node()}), and Electron (v${window.versions.electron()})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComputerCard({ children, title, description = '', chip = '', icon }) {
|
||||||
|
return (
|
||||||
|
<Card variant="outlined">
|
||||||
|
<CardContent>
|
||||||
|
<Typography
|
||||||
|
level="h2"
|
||||||
|
fontSize="lg"
|
||||||
|
id="card-description"
|
||||||
|
mb={0.5}
|
||||||
|
startDecorator={icon}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography fontSize="sm" aria-describedby="card-description" mb={1}>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
{children}
|
||||||
|
{chip !== '' && (
|
||||||
|
<Chip
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
sx={{ pointerEvents: 'none' }}
|
||||||
|
>
|
||||||
|
{chip}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Computer() {
|
||||||
|
const { server, isLoading, isError } = useServerStats();
|
||||||
|
|
||||||
|
if (server) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* {JSON.stringify(server)} */}
|
||||||
|
<Typography level="h2" paddingBottom={3}>
|
||||||
|
Server Information:
|
||||||
|
</Typography>
|
||||||
|
<Sheet className="OrderTableContainer">
|
||||||
|
<Grid container spacing={2} sx={{}}>
|
||||||
|
<Grid xs={4}>
|
||||||
|
<ComputerCard
|
||||||
|
icon={<RouterIcon />}
|
||||||
|
title="Machine"
|
||||||
|
description={`${server.os} - ${server.name}`}
|
||||||
|
>
|
||||||
|
CPU: {server.cpu_percent}%<br />
|
||||||
|
{server.cpu_count} Cores
|
||||||
|
</ComputerCard>
|
||||||
|
</Grid>{' '}
|
||||||
|
<Grid xs={2}>
|
||||||
|
<ComputerCard
|
||||||
|
icon={<GridIcon />}
|
||||||
|
title="GPU"
|
||||||
|
description={server.gpu?.length === 0 ? '❌ No GPU' : '✅ GPU'}
|
||||||
|
/>
|
||||||
|
</Grid>{' '}
|
||||||
|
<Grid xs={4}>
|
||||||
|
<ComputerCard
|
||||||
|
icon={<CalculatorIcon />}
|
||||||
|
title="GPU Specs"
|
||||||
|
image={undefined}
|
||||||
|
>
|
||||||
|
{server.gpu?.map((g) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
🔥 {g.name}
|
||||||
|
<br />
|
||||||
|
{formatBytes(Math.round(g?.used_memory))} Used
|
||||||
|
<br />
|
||||||
|
{formatBytes(g.total_memory)} Total
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<br />
|
||||||
|
Used Memory:{' '}
|
||||||
|
{Math.round(
|
||||||
|
server.gpu[0]?.used_memory / server.gpu[0]?.total_memory
|
||||||
|
)}
|
||||||
|
%
|
||||||
|
</ComputerCard>
|
||||||
|
</Grid>{' '}
|
||||||
|
<Grid xs={2}>
|
||||||
|
<ComputerCard icon={<FlameIcon />} title="CUDA">
|
||||||
|
{server?.device === 'cuda' ? '✅ Yes' : '❌ No'}
|
||||||
|
<br />
|
||||||
|
Version: {server?.cuda_version}
|
||||||
|
</ComputerCard>
|
||||||
|
</Grid>{' '}
|
||||||
|
<Grid xs={3}>
|
||||||
|
<ComputerCard icon={<LayoutIcon />} title="Operating System">
|
||||||
|
{server?.platform}
|
||||||
|
</ComputerCard>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={3}>
|
||||||
|
<ComputerCard icon={<MemoryStickIcon />} title="Memory">
|
||||||
|
<>
|
||||||
|
<Typography>
|
||||||
|
Total Memory: {formatBytes(server.memory?.total)}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
Available: {formatBytes(server.memory?.available)}
|
||||||
|
</Typography>
|
||||||
|
<Typography>Percent: {server.memory?.percent}%</Typography>
|
||||||
|
</>
|
||||||
|
</ComputerCard>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={3}>
|
||||||
|
<ComputerCard title="Disk" icon={<DatabaseIcon />}>
|
||||||
|
Total: {formatBytes(server.disk?.total)} - Used:{' '}
|
||||||
|
{formatBytes(server.disk?.free)} - Free: {server.disk?.percent}%
|
||||||
|
</ComputerCard>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={3}>
|
||||||
|
<ComputerCard icon={<Code2Icon />} title="Python Version">
|
||||||
|
{server.python_version}
|
||||||
|
</ComputerCard>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* <h3>System Properties in Electron</h3>
|
||||||
|
|
||||||
|
<Button onClick={getSystemProperties}>
|
||||||
|
Print all System Properties
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<pre id="info" style={{ whiteSpace: 'pre-wrap' }} /> */}
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
427
src/renderer/components/Connect/LoginModal.tsx
Normal file
427
src/renderer/components/Connect/LoginModal.tsx
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import * as React from 'react';
|
||||||
|
import Button from '@mui/joy/Button';
|
||||||
|
import FormControl from '@mui/joy/FormControl';
|
||||||
|
import FormLabel from '@mui/joy/FormLabel';
|
||||||
|
import Input from '@mui/joy/Input';
|
||||||
|
import Modal from '@mui/joy/Modal';
|
||||||
|
import ModalDialog from '@mui/joy/ModalDialog';
|
||||||
|
import Stack from '@mui/joy/Stack';
|
||||||
|
import Typography from '@mui/joy/Typography';
|
||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
CircularProgress,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Divider,
|
||||||
|
FormHelperText,
|
||||||
|
Link,
|
||||||
|
ModalClose,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
import { apiHealthz } from '../../lib/transformerlab-api-sdk';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { connect } from 'http2';
|
||||||
|
|
||||||
|
export default function LoginModal({
|
||||||
|
setServer,
|
||||||
|
connection,
|
||||||
|
setTerminalDrawerOpen,
|
||||||
|
setSSHConnection,
|
||||||
|
}) {
|
||||||
|
const [checking, setChecking] = React.useState<boolean>(false);
|
||||||
|
const [failed, setFailed] = React.useState<boolean>(false);
|
||||||
|
const [recentConnections, setRecentConnections] = React.useState<string[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [recentSSHConnections, setRecentSSHConnections] = React.useState<
|
||||||
|
string[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const [host, setHost] = useState('');
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.storage
|
||||||
|
.get('recentConnections')
|
||||||
|
.then((result) => {
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
setRecentConnections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
window.storage
|
||||||
|
.get('recentSSHConnections')
|
||||||
|
.then((result) => {
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
setRecentSSHConnections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [connection]);
|
||||||
|
|
||||||
|
async function checkServer() {
|
||||||
|
setChecking(true);
|
||||||
|
const response = await apiHealthz();
|
||||||
|
const apiStatus = response !== null ? 1 : 0;
|
||||||
|
setChecking(false);
|
||||||
|
if (apiStatus === 1) {
|
||||||
|
if (!recentConnections.includes(window.TransformerLab.API_URL)) {
|
||||||
|
if (recentConnections.length > 4) {
|
||||||
|
recentConnections.pop();
|
||||||
|
}
|
||||||
|
window.storage.set('recentConnections', [
|
||||||
|
window.TransformerLab.API_URL,
|
||||||
|
...recentConnections,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
setServer(window.TransformerLab.API_URL);
|
||||||
|
} else {
|
||||||
|
setFailed(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={connection == ''} keepMounted>
|
||||||
|
<ModalDialog
|
||||||
|
aria-labelledby="basic-modal-dialog-title"
|
||||||
|
aria-describedby="basic-modal-dialog-description"
|
||||||
|
sx={{
|
||||||
|
top: '10vh', // Sit 20% from the top of the screen
|
||||||
|
margin: 'auto',
|
||||||
|
transform: 'translateX(-50%)', // This undoes the default translateY that centers vertically
|
||||||
|
width: '55vw',
|
||||||
|
maxWidth: '700px',
|
||||||
|
maxHeight: '70vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
aria-label="Basic tabs"
|
||||||
|
defaultValue={0}
|
||||||
|
sx={{ overflow: 'hidden' }}
|
||||||
|
onChange={(_event, newValue) => {}}
|
||||||
|
>
|
||||||
|
<TabList tabFlex={1}>
|
||||||
|
<Tab>Remote Connection</Tab>
|
||||||
|
<Tab>Local Connection</Tab>
|
||||||
|
<Tab value="SSH">Connect via SSH</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={0} sx={{ p: 2 }}>
|
||||||
|
{/* <Typography id="basic-modal-dialog-title" component="h2">
|
||||||
|
Connect to Server
|
||||||
|
</Typography> */}
|
||||||
|
{/* <Typography
|
||||||
|
id="basic-modal-dialog-description"
|
||||||
|
textColor="text.tertiary"
|
||||||
|
>
|
||||||
|
Provide connection information:
|
||||||
|
</Typography> */}
|
||||||
|
<form
|
||||||
|
onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const server = event.currentTarget.elements[0].value;
|
||||||
|
const port = event.currentTarget.elements[1].value;
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-template
|
||||||
|
const fullServer = 'http://' + server + ':' + port + '/';
|
||||||
|
|
||||||
|
window.TransformerLab = {};
|
||||||
|
window.TransformerLab.API_URL = fullServer;
|
||||||
|
|
||||||
|
checkServer();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Server URL</FormLabel>
|
||||||
|
<Input autoFocus required placeholder="192.168.1.100" />
|
||||||
|
<FormHelperText>
|
||||||
|
Do not include http:// in the URL or a / at the end
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Server Port</FormLabel>
|
||||||
|
<Input required defaultValue="8000" placeholder="8000" />
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
startDecorator={
|
||||||
|
checking && (
|
||||||
|
<CircularProgress
|
||||||
|
variant="solid"
|
||||||
|
thickness={2}
|
||||||
|
sx={{
|
||||||
|
'--CircularProgress-size': '16px',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sx={{ p: 1 }}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
{failed && (
|
||||||
|
<div style={{ color: 'red' }}>
|
||||||
|
Couldn't connect to server. Please try a different URL.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Divider />
|
||||||
|
<div>
|
||||||
|
<Typography>
|
||||||
|
<b>Recent Connections:</b>{' '}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="plain"
|
||||||
|
color="neutral"
|
||||||
|
sx={{ fontWeight: 'normal' }}
|
||||||
|
onClick={() => {
|
||||||
|
window.storage.set('recentConnections', []);
|
||||||
|
setRecentConnections([]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
clear
|
||||||
|
</Button>
|
||||||
|
</Typography>
|
||||||
|
{recentConnections.length > 0 &&
|
||||||
|
recentConnections.map((connection, index) => (
|
||||||
|
<Typography
|
||||||
|
sx={{ color: 'neutral.400', mt: '0px!' }}
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
onClick={() => {
|
||||||
|
window.TransformerLab = {};
|
||||||
|
window.TransformerLab.API_URL = connection;
|
||||||
|
checkServer();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{connection}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={1} sx={{ p: 2 }}>
|
||||||
|
Not yet implemented.
|
||||||
|
<br /> Please run the API on your local machine and use the{' '}
|
||||||
|
<b>Remote Connection</b> to connect to
|
||||||
|
<br />
|
||||||
|
localhost or 127.0.0.1
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value="SSH" sx={{ height: '100%', overflow: 'auto' }}>
|
||||||
|
<form
|
||||||
|
id="ssh-form"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
|
||||||
|
const host = formData.get('host')?.toString();
|
||||||
|
const username = formData.get('username')?.toString();
|
||||||
|
const password = formData.get('userpassword')?.toString();
|
||||||
|
const sshkeylocation = formData
|
||||||
|
.get('sshkeylocation')
|
||||||
|
?.toString();
|
||||||
|
const update_and_install =
|
||||||
|
window.document.getElementsByName('update_and_install')[0]
|
||||||
|
?.checked;
|
||||||
|
const create_reverse_tunnel = window.document.getElementsByName(
|
||||||
|
'create_reverse_tunnel'
|
||||||
|
)[0]?.checked;
|
||||||
|
const run_permanent =
|
||||||
|
window.document.getElementsByName('run_permanent')[0]
|
||||||
|
?.checked;
|
||||||
|
|
||||||
|
setSSHConnection({
|
||||||
|
host: host,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
sshkeylocation: sshkeylocation,
|
||||||
|
update_and_install: update_and_install,
|
||||||
|
create_reverse_tunnel: create_reverse_tunnel,
|
||||||
|
run_permanent: run_permanent,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTerminalDrawerOpen(true);
|
||||||
|
|
||||||
|
const fullServer = 'http://' + host + ':' + '8000' + '/';
|
||||||
|
|
||||||
|
window.TransformerLab = {};
|
||||||
|
window.TransformerLab.API_URL = fullServer;
|
||||||
|
|
||||||
|
setServer(fullServer);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack sx={{}} spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>SSH Host:</FormLabel>
|
||||||
|
<Input
|
||||||
|
name="host"
|
||||||
|
autoFocus
|
||||||
|
required
|
||||||
|
placeholder="192.168.1.100"
|
||||||
|
value={host}
|
||||||
|
onChange={(e) => setHost(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Username:</FormLabel>
|
||||||
|
<Input name="username" required placeholder="username" />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Password:</FormLabel>
|
||||||
|
<Input
|
||||||
|
name="userpassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="password"
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
Leave blank to use SSH key auth
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>SSH Key:</FormLabel>
|
||||||
|
<Input
|
||||||
|
name="sshkeylocation"
|
||||||
|
placeholder="/Users/name/.ssh/id_rsa"
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
Enter a full path (no ~), or leave blank to use the default
|
||||||
|
which is HOME_DIR/.ssh/id_rsa
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
name="update_and_install"
|
||||||
|
label="Try to update"
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
If unchecked, launches the API server, but avoids fetching
|
||||||
|
and installing, if any version exists.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
name="create_reverse_tunnel"
|
||||||
|
label="Create reverse tunnel on port 8000"
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
This will create a reverse tunnel connecting localhost:8000
|
||||||
|
→ remote_host:8000, creating a secure connection to the API
|
||||||
|
without opening additional ports.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
name="run_permanent"
|
||||||
|
label="Keep running in background"
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
Keep the API server running even if the SSH connection is
|
||||||
|
lost. Uses the <b>nohup</b> command
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<Button type="submit">Connect</Button>
|
||||||
|
<Divider />
|
||||||
|
<div>
|
||||||
|
<Typography>
|
||||||
|
<b>Recent SSH Connections:</b>{' '}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="plain"
|
||||||
|
color="neutral"
|
||||||
|
sx={{ fontWeight: 'normal' }}
|
||||||
|
onClick={() => {
|
||||||
|
window.storage.set('recentSSHConnections', []);
|
||||||
|
setRecentSSHConnections([]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
clear
|
||||||
|
</Button>
|
||||||
|
</Typography>
|
||||||
|
{recentSSHConnections.length > 0 &&
|
||||||
|
recentSSHConnections.map((connection, index) => (
|
||||||
|
<Typography
|
||||||
|
sx={{ color: 'neutral.400', mt: '0px!' }}
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
onClick={() => {
|
||||||
|
setSSHConnection({
|
||||||
|
host: connection.host,
|
||||||
|
username: connection.username,
|
||||||
|
password: null,
|
||||||
|
sshkeylocation: connection.sshkeylocation,
|
||||||
|
update_and_install: connection.update_and_install,
|
||||||
|
create_reverse_tunnel:
|
||||||
|
connection.create_reverse_tunnel,
|
||||||
|
run_permanent: connection.run_permanent,
|
||||||
|
tryKeyboard: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTerminalDrawerOpen(true);
|
||||||
|
|
||||||
|
const fullServer =
|
||||||
|
'http://' + connection.host + ':' + '8000' + '/';
|
||||||
|
|
||||||
|
window.TransformerLab = {};
|
||||||
|
window.TransformerLab.API_URL = fullServer;
|
||||||
|
|
||||||
|
setServer(fullServer);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{connection.username}@{connection.host} [
|
||||||
|
{connection.sshkeylocation
|
||||||
|
? 'key: ' + connection.sshkeylocation
|
||||||
|
: 'password'}
|
||||||
|
{connection.update_and_install ? (
|
||||||
|
' - ✔️ update'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
- <s>update</s>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{connection.create_reverse_tunnel ? (
|
||||||
|
' - ✔️ reverse tunnel'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
- <s>reverse tunnel</s>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{connection.run_permanent ? (
|
||||||
|
' - ✔️ nohup'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
- <s>nohup</s>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
]
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
186
src/renderer/components/Connect/XtermJS.tsx
Normal file
186
src/renderer/components/Connect/XtermJS.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import { Button, DialogTitle, Drawer, Sheet } from '@mui/joy';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
|
|
||||||
|
import 'xterm/css/xterm.css';
|
||||||
|
|
||||||
|
export default function XtermJSDrawer({
|
||||||
|
sshConnection,
|
||||||
|
drawerOpen,
|
||||||
|
setDrawerOpen,
|
||||||
|
}) {
|
||||||
|
const terminalContainerRef = useRef(null);
|
||||||
|
const termRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sshConnection === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const term = new Terminal({
|
||||||
|
theme: {
|
||||||
|
background: '#334155',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
termRef.current = term;
|
||||||
|
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
|
||||||
|
window.sshClient.removeAllListeners();
|
||||||
|
|
||||||
|
// SSH LISTENERS:
|
||||||
|
window.sshClient.onSSHConnected((_event, value) => {
|
||||||
|
console.log('ssh connected', value);
|
||||||
|
|
||||||
|
// 🔐 Do not store SSH passwords in plaintext :
|
||||||
|
let sshConnectionNoPassword = { ...sshConnection, password: '' };
|
||||||
|
|
||||||
|
if (value === true) {
|
||||||
|
// If we are successfully connected, store this connection in the recentSSHConnections array
|
||||||
|
// That is part of electron storage
|
||||||
|
window.storage.get('recentSSHConnections').then((result) => {
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
const recentSSHConnections = result;
|
||||||
|
const index = recentSSHConnections.findIndex(
|
||||||
|
(item) =>
|
||||||
|
item.host === sshConnection.host &&
|
||||||
|
item.username === sshConnection.username &&
|
||||||
|
item.sshkeylocation === sshConnection.sshkeylocation
|
||||||
|
);
|
||||||
|
if (index > -1) {
|
||||||
|
recentSSHConnections.splice(index, 1);
|
||||||
|
}
|
||||||
|
recentSSHConnections.unshift(sshConnectionNoPassword);
|
||||||
|
window.storage
|
||||||
|
.set('recentSSHConnections', recentSSHConnections)
|
||||||
|
.then(() => {
|
||||||
|
console.log('recentSSHConnections saved');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.storage
|
||||||
|
.set('recentSSHConnections', [sshConnectionNoPassword])
|
||||||
|
.then(() => {
|
||||||
|
console.log('recentSSHConnections saved');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.sshClient.onData((_event, data) => {
|
||||||
|
term.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const username = sshConnection.username;
|
||||||
|
const password = sshConnection.password;
|
||||||
|
const host = sshConnection.host;
|
||||||
|
const sshkeylocation = sshConnection.sshkeylocation;
|
||||||
|
const update_and_install = sshConnection.update_and_install;
|
||||||
|
const create_reverse_tunnel = sshConnection.create_reverse_tunnel;
|
||||||
|
const run_permanent = sshConnection.run_permanent;
|
||||||
|
const tryKeyboard = sshConnection.tryKeyboard;
|
||||||
|
|
||||||
|
window.sshClient.connect({
|
||||||
|
host: host,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
sshkeylocation: sshkeylocation,
|
||||||
|
update_and_install: update_and_install,
|
||||||
|
create_reverse_tunnel: create_reverse_tunnel,
|
||||||
|
run_permanent: run_permanent,
|
||||||
|
tryKeyboard: tryKeyboard,
|
||||||
|
});
|
||||||
|
|
||||||
|
// const terminalContainer =
|
||||||
|
// window.document.getElementById('terminal-container');
|
||||||
|
|
||||||
|
const terminalContainer = terminalContainerRef.current;
|
||||||
|
term.open(terminalContainer);
|
||||||
|
term.clear();
|
||||||
|
term.loadAddon(fitAddon);
|
||||||
|
term.focus();
|
||||||
|
fitAddon.fit();
|
||||||
|
|
||||||
|
function resizeScreen() {
|
||||||
|
fitAddon.fit();
|
||||||
|
console.log(
|
||||||
|
`Resize Terminal: ${JSON.stringify({
|
||||||
|
cols: term.cols,
|
||||||
|
rows: term.rows,
|
||||||
|
})}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
terminalContainerRef.current.addEventListener(
|
||||||
|
'resize',
|
||||||
|
resizeScreen,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
term.onData((data) => {
|
||||||
|
// console.log('data:', data);
|
||||||
|
window.sshClient.data(data);
|
||||||
|
});
|
||||||
|
}, [sshConnection]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
open={drawerOpen}
|
||||||
|
onClose={() => setDrawerOpen(false)}
|
||||||
|
anchor="bottom"
|
||||||
|
color="primary"
|
||||||
|
variant="solid"
|
||||||
|
id="terminal-drawer"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
onTransitionEnd={() => {
|
||||||
|
if (drawerOpen) {
|
||||||
|
// resizeScreen();
|
||||||
|
termRef.current?.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>Terminal</DialogTitle>
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
height: 'calc(100% - 50px)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* {JSON.stringify(sshConnection)} */}
|
||||||
|
<div
|
||||||
|
id="terminal-container"
|
||||||
|
className="terminal"
|
||||||
|
ref={terminalContainerRef}
|
||||||
|
style={{
|
||||||
|
padding: '2px',
|
||||||
|
backgroundColor: '#0f172a',
|
||||||
|
overflow: 'auto',
|
||||||
|
display: 'block',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
termRef.current?.focus();
|
||||||
|
console.log('click');
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
termRef.current?.clear();
|
||||||
|
termRef.current?.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</Sheet>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/renderer/components/Data/Data.tsx
Normal file
40
src/renderer/components/Data/Data.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
import { Tab, TabList, TabPanel, Tabs } from '@mui/joy';
|
||||||
|
import { StoreIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import DataStore from './DataStore';
|
||||||
|
import LocalDatasets from './LocalDatasets';
|
||||||
|
|
||||||
|
export default function Data() {
|
||||||
|
return (
|
||||||
|
<Sheet sx={{ display: 'flex', height: '100%' }}>
|
||||||
|
<Tabs
|
||||||
|
aria-label="Dataset Tabs"
|
||||||
|
defaultValue={0}
|
||||||
|
size="sm"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 'lg',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabList tabFlex={1}>
|
||||||
|
<Tab>Local Datasets</Tab>
|
||||||
|
<Tab>
|
||||||
|
<StoreIcon color="grey" />
|
||||||
|
Dataset Store
|
||||||
|
</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={0} sx={{ p: 2 }}>
|
||||||
|
<LocalDatasets />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={1} sx={{ p: 2, height: '100%', overflow: 'hidden' }}>
|
||||||
|
<DataStore />
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
src/renderer/components/Data/DataStore.tsx
Normal file
50
src/renderer/components/Data/DataStore.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import { Grid, LinearProgress, Sheet } from '@mui/joy';
|
||||||
|
import DatasetCard from './DatasetCard';
|
||||||
|
|
||||||
|
import * as chatAPI from '../../lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function DataStore() {
|
||||||
|
const { data, error, isLoading } = useSWR(
|
||||||
|
chatAPI.Endpoints.Dataset.Gallery(),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) return 'An error has occurred.';
|
||||||
|
if (isLoading) return <LinearProgress />;
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
className="OrderTableContainer"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 'md',
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
minHeight: 0,
|
||||||
|
padding: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid container spacing={2} sx={{ flexGrow: 1 }}>
|
||||||
|
{data.map((row) => (
|
||||||
|
<Grid xs={4}>
|
||||||
|
<DatasetCard
|
||||||
|
name={row.name}
|
||||||
|
size={row.size}
|
||||||
|
key={row.id}
|
||||||
|
description={row.description}
|
||||||
|
repo={row.huggingfacerepo}
|
||||||
|
download
|
||||||
|
location={undefined}
|
||||||
|
parentMutate={undefined}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
122
src/renderer/components/Data/DatasetCard.tsx
Normal file
122
src/renderer/components/Data/DatasetCard.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Button from '@mui/joy/Button';
|
||||||
|
import Card from '@mui/joy/Card';
|
||||||
|
import CardContent from '@mui/joy/CardContent';
|
||||||
|
import Typography from '@mui/joy/Typography';
|
||||||
|
import { DownloadIcon, FileTextIcon, Trash2Icon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { formatBytes } from 'renderer/lib/utils';
|
||||||
|
import * as chatAPI from '../../lib/transformerlab-api-sdk';
|
||||||
|
import PreviewDatasetModal from './PreviewDatasetModal';
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function DatasetCard({
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
description,
|
||||||
|
repo,
|
||||||
|
download = false,
|
||||||
|
location,
|
||||||
|
parentMutate,
|
||||||
|
}) {
|
||||||
|
const [previewDatasetModalOpen, setPreviewDatasetModalOpen] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{previewDatasetModalOpen && (
|
||||||
|
<PreviewDatasetModal
|
||||||
|
open={previewDatasetModalOpen}
|
||||||
|
setOpen={setPreviewDatasetModalOpen}
|
||||||
|
dataset_id={name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Card variant="outlined" sx={{}}>
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
level="h4"
|
||||||
|
sx={{ mb: 0.5, height: '40px', overflow: 'clip' }}
|
||||||
|
startDecorator={<FileTextIcon />}
|
||||||
|
>
|
||||||
|
<b>{name}</b>
|
||||||
|
{location === 'huggingfacehub' && ' 🤗'}
|
||||||
|
{location === 'local' && ' (local)'}
|
||||||
|
</Typography>
|
||||||
|
<div style={{ height: '100px', overflow: 'clip' }}>
|
||||||
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
sx={{
|
||||||
|
overflow: 'auto',
|
||||||
|
mt: 2,
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardContent orientation="horizontal">
|
||||||
|
<div>
|
||||||
|
<Typography level="body3">Total size:</Typography>
|
||||||
|
<Typography fontSize="lg" fontWeight="lg">
|
||||||
|
{size === -1 ? 'Unknown' : formatBytes(size)}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardContent orientation="horizontal">
|
||||||
|
{!download && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
color="neutral"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={async () => {
|
||||||
|
await fetch(chatAPI.Endpoints.Dataset.Delete(name));
|
||||||
|
parentMutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2Icon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
sx={{ ml: 'auto' }}
|
||||||
|
onClick={() => setPreviewDatasetModalOpen(true)}
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
onClick={async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
chatAPI.Endpoints.Dataset.Info(name)
|
||||||
|
);
|
||||||
|
const info = await response.json();
|
||||||
|
|
||||||
|
alert(JSON.stringify(info));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Info
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{download && (
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
aria-label="Download"
|
||||||
|
sx={{ ml: 'auto' }}
|
||||||
|
onClick={() => {
|
||||||
|
chatAPI.downloadData(repo);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
<DownloadIcon size={16} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
src/renderer/components/Data/LocalDatasets.tsx
Normal file
121
src/renderer/components/Data/LocalDatasets.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Grid,
|
||||||
|
Input,
|
||||||
|
LinearProgress,
|
||||||
|
Sheet,
|
||||||
|
} from '@mui/joy';
|
||||||
|
import { PlusIcon } from 'lucide-react';
|
||||||
|
import DatasetCard from './DatasetCard';
|
||||||
|
|
||||||
|
import * as chatAPI from '../../lib/transformerlab-api-sdk';
|
||||||
|
import NewDatasetModal from './NewDatasetModal';
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function LocalDatasets() {
|
||||||
|
const [newDatasetModalOpen, setNewDatasetModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
chatAPI.Endpoints.Dataset.LocalList(),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) return 'An error has occurred.';
|
||||||
|
if (isLoading) return <LinearProgress />;
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NewDatasetModal
|
||||||
|
open={newDatasetModalOpen}
|
||||||
|
setOpen={setNewDatasetModalOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Sheet
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 'md',
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
minHeight: 0,
|
||||||
|
padding: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid container spacing={2} sx={{ flexGrow: 1 }}>
|
||||||
|
{data.map((row) => (
|
||||||
|
<Grid xs={4}>
|
||||||
|
{/* {<pre>{JSON.stringify(row, null, 2)}</pre>} */}
|
||||||
|
<DatasetCard
|
||||||
|
name={row?.dataset_id}
|
||||||
|
size={row?.size}
|
||||||
|
key={row.id}
|
||||||
|
description={row?.description}
|
||||||
|
repo={row.huggingfacerepo}
|
||||||
|
location={row?.location}
|
||||||
|
parentMutate={mutate}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Sheet>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
paddingTop: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<FormControl>
|
||||||
|
{/* <FormLabel>Load 🤗 Hugging Face Model</FormLabel> */}
|
||||||
|
<Input
|
||||||
|
placeholder="Open-Orca/OpenOrca"
|
||||||
|
endDecorator={<Button>Download 🤗 Dataset</Button>}
|
||||||
|
sx={{ width: '500px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <FormHelperText>
|
||||||
|
Enter full URL of model, for example:
|
||||||
|
"decapoda-research/llama-30b-hf"
|
||||||
|
</FormHelperText> */}
|
||||||
|
</FormControl>
|
||||||
|
<>
|
||||||
|
{/* <Button
|
||||||
|
size="sm"
|
||||||
|
sx={{ height: '30px' }}
|
||||||
|
endDecorator={<FolderOpenIcon />}
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
Open in Filesystem
|
||||||
|
</Button> */}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
sx={{ height: '30px' }}
|
||||||
|
endDecorator={<PlusIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
setNewDatasetModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
</Box>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
115
src/renderer/components/Data/NewDatasetModal.tsx
Normal file
115
src/renderer/components/Data/NewDatasetModal.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import Uppy from '@uppy/core';
|
||||||
|
import {
|
||||||
|
Dashboard,
|
||||||
|
DashboardModal,
|
||||||
|
DragDrop,
|
||||||
|
ProgressBar,
|
||||||
|
StatusBar,
|
||||||
|
} from '@uppy/react';
|
||||||
|
import XHR from '@uppy/xhr-upload';
|
||||||
|
|
||||||
|
import '@uppy/core/dist/style.min.css';
|
||||||
|
import '@uppy/dashboard/dist/style.min.css';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalClose,
|
||||||
|
ModalDialog,
|
||||||
|
Sheet,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
import * as chatAPI from '../../lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
const uppy = new Uppy().use(XHR, {
|
||||||
|
endpoint: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// uppy.on('complete', () => {
|
||||||
|
// console.log('Modal is open');
|
||||||
|
// });
|
||||||
|
|
||||||
|
export default function DatasetDetailsModal({ open, setOpen }) {
|
||||||
|
const [newDatasetName, setNewDatasetName] = useState('');
|
||||||
|
const [showUploadDialog, setShowUploadDialog] = useState(false);
|
||||||
|
|
||||||
|
// Reset newDatasetName when the modal is open/closed
|
||||||
|
// useEffect(() => {
|
||||||
|
// setNewDatasetName('');
|
||||||
|
// }, [open]);
|
||||||
|
|
||||||
|
uppy?.getPlugin('XHRUpload')?.setOptions({
|
||||||
|
endpoint: chatAPI.Endpoints.Dataset.FileUpload(newDatasetName),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalDialog>
|
||||||
|
<ModalClose />
|
||||||
|
<Typography level="h5">{newDatasetName || 'New'} Dataset</Typography>
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
<Sheet sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
{newDatasetName === '' && (
|
||||||
|
<form
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '10px',
|
||||||
|
}}
|
||||||
|
onSubmit={async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const name =
|
||||||
|
event.currentTarget.elements['dataset-name']?.value;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
chatAPI.Endpoints.Dataset.Create(name)
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Use the returned dataset_id because it has been sanitized
|
||||||
|
setNewDatasetName(data.dataset_id);
|
||||||
|
setShowUploadDialog(true);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input placeholder="Dataset Name" name="dataset-name" />
|
||||||
|
<Button type="submit">Create</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Sheet>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
<DashboardModal
|
||||||
|
uppy={uppy}
|
||||||
|
open={showUploadDialog}
|
||||||
|
onRequestClose={() => {
|
||||||
|
uppy.cancelAll();
|
||||||
|
setNewDatasetName('');
|
||||||
|
setShowUploadDialog(false);
|
||||||
|
}}
|
||||||
|
locale={{
|
||||||
|
strings: {
|
||||||
|
dropPasteFiles: 'Drop datset files here or %{browseFiles}',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
closeAfterFinish
|
||||||
|
// doneButtonHandler={() => {
|
||||||
|
// uppy.cancelAll();
|
||||||
|
// setShowUploadDialog(false);
|
||||||
|
// }}
|
||||||
|
proudlyDisplayPoweredByUppy={false}
|
||||||
|
note="Name one file '<something>_train.jsonl' and the second one '<something>_eval.jsonl' Files should be in JSONL format, with one JSON object per line."
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
79
src/renderer/components/Data/PreviewDatasetModal.tsx
Normal file
79
src/renderer/components/Data/PreviewDatasetModal.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Table,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalClose,
|
||||||
|
ModalDialog,
|
||||||
|
Sheet,
|
||||||
|
Typography,
|
||||||
|
CircularProgress,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
import * as chatAPI from '../../lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function PreviewDatasetModal({ dataset_id, open, setOpen }) {
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
chatAPI.Endpoints.Dataset.Preview(dataset_id),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalDialog>
|
||||||
|
<ModalClose />
|
||||||
|
<Typography level="h4">
|
||||||
|
Preview <b>{dataset_id}</b>
|
||||||
|
</Typography>
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
overflowY: 'scroll',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading && <CircularProgress />}
|
||||||
|
<Table sx={{ tableLayout: 'auto' }}>
|
||||||
|
<thead>
|
||||||
|
<tr>{data && Object.keys(data[0]).map((k) => <th>{k}</th>)}</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data &&
|
||||||
|
data.map((row) => {
|
||||||
|
const values = Object.values(row);
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
{values.map((v) => (
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
whiteSpace: 'pre-line',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof v === 'string' ? v : JSON.stringify(v)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Sheet>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
src/renderer/components/Experiment/Api.tsx
Normal file
45
src/renderer/components/Experiment/Api.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
|
||||||
|
import { Button, Typography } from '@mui/joy';
|
||||||
|
import { ExternalLinkIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function Api() {
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
paddingBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Typography level="h1">API Documentation</Typography>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
window.open(`${window.TransformerLab.API_URL}docs`);
|
||||||
|
}}
|
||||||
|
endDecorator={<ExternalLinkIcon />}
|
||||||
|
variant="plain"
|
||||||
|
>
|
||||||
|
Open in Browser
|
||||||
|
</Button>
|
||||||
|
<br />
|
||||||
|
<iframe
|
||||||
|
src={`${window.TransformerLab.API_URL}docs`}
|
||||||
|
title="api docs"
|
||||||
|
style={{
|
||||||
|
border: '1px solid black',
|
||||||
|
display: 'flex',
|
||||||
|
flex: 99,
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
src/renderer/components/Experiment/Embeddings.tsx
Normal file
82
src/renderer/components/Experiment/Embeddings.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import { useRef, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
import { PencilIcon, PlayIcon } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function Embeddings({ model_name }) {
|
||||||
|
async function getEmbeddings() {
|
||||||
|
const text = document.getElementsByName('inputText')[0].value;
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
let embeddings = await chatAPI.getEmbeddings(model_name, lines);
|
||||||
|
embeddings = embeddings.data;
|
||||||
|
|
||||||
|
//expand embeddings subproperty embedding array to string:
|
||||||
|
embeddings = embeddings.map((item) => {
|
||||||
|
return item.embedding;
|
||||||
|
});
|
||||||
|
|
||||||
|
embeddings = embeddings.join('\n\n\n');
|
||||||
|
|
||||||
|
document.getElementsByName('outputText')[0].value = embeddings;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sheet>
|
||||||
|
<Typography level="h1" mb={3}>
|
||||||
|
Generate Embeddings
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Input Text</FormLabel>
|
||||||
|
<Textarea
|
||||||
|
minRows={8}
|
||||||
|
size="lg"
|
||||||
|
defaultValue="This is a line
|
||||||
|
This is a second line."
|
||||||
|
name="inputText"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormHelperText>
|
||||||
|
Enter text to convert, one input per line.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
sx={{ mt: 4, mb: 4 }}
|
||||||
|
startDecorator={<PlayIcon />}
|
||||||
|
onClick={async () => await getEmbeddings()}
|
||||||
|
>
|
||||||
|
Process Embeddings
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Output Vectors</FormLabel>
|
||||||
|
<Textarea
|
||||||
|
minRows={8}
|
||||||
|
maxRows={20}
|
||||||
|
size="lg"
|
||||||
|
name="outputText"
|
||||||
|
sx={{ whiteSpace: 'nowrap' }}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
351
src/renderer/components/Experiment/Eval/Eval.tsx
Normal file
351
src/renderer/components/Experiment/Eval/Eval.tsx
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Link,
|
||||||
|
Table,
|
||||||
|
Typography,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
ModalClose,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
Stack,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Option,
|
||||||
|
Box,
|
||||||
|
FormHelperText,
|
||||||
|
IconButton,
|
||||||
|
} from '@mui/joy';
|
||||||
|
import {
|
||||||
|
FileTextIcon,
|
||||||
|
PlayIcon,
|
||||||
|
PlusCircleIcon,
|
||||||
|
XSquareIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import DownloadButton from '../Train/DownloadButton';
|
||||||
|
import { Editor } from '@monaco-editor/react';
|
||||||
|
import fairyflossTheme from '../../Shared/fairyfloss.tmTheme.js';
|
||||||
|
import ResultsModal from './ResultsModal';
|
||||||
|
const parseTmTheme = require('monaco-themes').parseTmTheme;
|
||||||
|
|
||||||
|
function listEvals(evalString) {
|
||||||
|
let result = [];
|
||||||
|
if (evalString) {
|
||||||
|
result = JSON.parse(evalString);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTheme(editor: any, monaco: any) {
|
||||||
|
const themeData = parseTmTheme(fairyflossTheme);
|
||||||
|
|
||||||
|
monaco.editor.defineTheme('my-theme', themeData);
|
||||||
|
monaco.editor.setTheme('my-theme');
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluationRun(experimentId: string, evaluator: string) {
|
||||||
|
fetch(chatAPI.Endpoints.Experiment.RunEvaluation(experimentId, evaluator));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTemplateParametersForPlugin(pluginName, plugins) {
|
||||||
|
if (!pluginName || !plugins) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin = plugins.find((row) => row.name === pluginName);
|
||||||
|
if (plugin) {
|
||||||
|
return plugin?.info?.template_parameters[0]?.options.map((row) => (
|
||||||
|
<Option value={row} key={row}>
|
||||||
|
{row}
|
||||||
|
</Option>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function Eval({
|
||||||
|
experimentInfo,
|
||||||
|
addEvaluation,
|
||||||
|
experimentInfoMutate,
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
|
const [resultsModalOpen, setResultsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const [selectedPlugin, setSelectedPlugin] = useState('');
|
||||||
|
|
||||||
|
const [currentEvaluator, setCurrentEvaluator] = useState('');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: plugins,
|
||||||
|
error: pluginsError,
|
||||||
|
isLoading: pluginsIsLoading,
|
||||||
|
} = useSWR(chatAPI.Endpoints.Evals.List(), fetcher);
|
||||||
|
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
|
async function handleEditorDidMount(editor, monaco) {
|
||||||
|
if (editor) {
|
||||||
|
editorRef.current = editor;
|
||||||
|
const response = await fetch(
|
||||||
|
chatAPI.Endpoints.Experiment.GetPlugin(
|
||||||
|
experimentInfo.id,
|
||||||
|
currentEvaluator
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const text = await response.json();
|
||||||
|
editor.setValue(text);
|
||||||
|
}
|
||||||
|
setTheme(editor, monaco);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFile() {
|
||||||
|
const value = editorRef?.current?.getValue();
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
// Use fetch to post the value to the server
|
||||||
|
await fetch(
|
||||||
|
chatAPI.Endpoints.Experiment.SavePlugin(project, evalName, 'main.py'),
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: value,
|
||||||
|
}
|
||||||
|
).then(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sheet>
|
||||||
|
<ResultsModal
|
||||||
|
open={resultsModalOpen}
|
||||||
|
setOpen={setResultsModalOpen}
|
||||||
|
experimentId={experimentInfo.id}
|
||||||
|
evaluator={currentEvaluator}
|
||||||
|
></ResultsModal>
|
||||||
|
<Modal open={editModalOpen} onClose={() => setEditModalOpen(false)}>
|
||||||
|
<ModalDialog>
|
||||||
|
<ModalClose onClick={() => setEditModalOpen(false)} />
|
||||||
|
<DialogTitle>
|
||||||
|
Edit Evaluator Script - {currentEvaluator}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Sheet
|
||||||
|
color="neutral"
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
backgroundColor: '#ddd',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
height="600px"
|
||||||
|
width="60vw"
|
||||||
|
defaultLanguage="python"
|
||||||
|
theme="my-theme"
|
||||||
|
options={{
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
fontSize: 18,
|
||||||
|
cursorStyle: 'block',
|
||||||
|
wordWrap: 'on',
|
||||||
|
}}
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
/>
|
||||||
|
</Sheet>
|
||||||
|
</DialogContent>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
display: 'flex',
|
||||||
|
gap: 1,
|
||||||
|
flexDirection: { xs: 'column', sm: 'row-reverse' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button sx={{ width: '120px' }}>Save</Button>
|
||||||
|
<Button
|
||||||
|
sx={{ width: '120px' }}
|
||||||
|
color="danger"
|
||||||
|
variant="soft"
|
||||||
|
onClick={() => setEditModalOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalDialog>
|
||||||
|
<ModalClose onClick={() => setOpen(false)} />
|
||||||
|
{/* <DialogTitle>Add Evalation</DialogTitle> */}
|
||||||
|
{/* <DialogContent>
|
||||||
|
Select an evaluation to add to this experiment.
|
||||||
|
</DialogContent> */}
|
||||||
|
<form
|
||||||
|
onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const plugin = event.target[1].value;
|
||||||
|
const task = event.target[3].value;
|
||||||
|
const localName = event.target[4].value;
|
||||||
|
|
||||||
|
addEvaluation(plugin, localName, { task: task });
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* {JSON.stringify(plugins)} */}
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Evaluation Plugin Template:</FormLabel>
|
||||||
|
<Select
|
||||||
|
placeholder={
|
||||||
|
pluginsIsLoading
|
||||||
|
? 'Loading...'
|
||||||
|
: 'Evaluation Plugin Template'
|
||||||
|
}
|
||||||
|
variant="soft"
|
||||||
|
size="lg"
|
||||||
|
name="evaluator_plugin"
|
||||||
|
value={selectedPlugin}
|
||||||
|
onChange={(e, newValue) => setSelectedPlugin(newValue)}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{plugins?.map((row) => (
|
||||||
|
<Option value={row.name} key={row.id}>
|
||||||
|
{row.name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>{' '}
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Task:</FormLabel>
|
||||||
|
<Select
|
||||||
|
placeholder="select a field"
|
||||||
|
onChange={(e, newValue) => {
|
||||||
|
document.getElementsByName('evaluator_name')[0].value =
|
||||||
|
newValue;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getTemplateParametersForPlugin(selectedPlugin, plugins)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Short name:</FormLabel>
|
||||||
|
<Input
|
||||||
|
placeholder="Rouge Eval for Sample"
|
||||||
|
name="evaluator_name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
A name to remember this evaluation by.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
<Typography level="h1">Evaluate</Typography>
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
startDecorator={<PlusCircleIcon />}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
Add Evaluation
|
||||||
|
</Button>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<Table aria-label="basic table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Evaluator</th>
|
||||||
|
<th> </th>
|
||||||
|
<th>Tasks</th>
|
||||||
|
<th>Template</th>
|
||||||
|
<th>Number of Records</th>
|
||||||
|
<th style={{ textAlign: 'right' }}> </th>
|
||||||
|
<th style={{ textAlign: 'right' }}> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{listEvals(experimentInfo?.config?.evaluations) &&
|
||||||
|
listEvals(experimentInfo?.config?.evaluations)?.map(
|
||||||
|
(evaluations) => (
|
||||||
|
<tr key={evaluations.name}>
|
||||||
|
<td>{evaluations.name}</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentEvaluator(evaluations.name);
|
||||||
|
setEditModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{evaluations?.script_parameters?.task}
|
||||||
|
<FileTextIcon size={14} />
|
||||||
|
</td>
|
||||||
|
<td>{evaluations.plugin}</td>
|
||||||
|
<td>30,252</td>
|
||||||
|
<td style={{ textAlign: 'right' }}>
|
||||||
|
{' '}
|
||||||
|
<Button
|
||||||
|
startDecorator={<PlayIcon />}
|
||||||
|
variant="soft"
|
||||||
|
onClick={() =>
|
||||||
|
evaluationRun(experimentInfo.id, evaluations.name)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Evaluate
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td style={{ textAlign: 'right' }}>
|
||||||
|
<Stack direction="row">
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentEvaluator(evaluations.name);
|
||||||
|
setResultsModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Results
|
||||||
|
</Button>
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => {
|
||||||
|
await fetch(
|
||||||
|
chatAPI.Endpoints.Experiment.DeleteEval(
|
||||||
|
experimentInfo.id,
|
||||||
|
evaluations.name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
experimentInfoMutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XSquareIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/renderer/components/Experiment/Eval/ResultsModal.tsx
Normal file
42
src/renderer/components/Experiment/Eval/ResultsModal.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { DialogContent, DialogTitle, Modal, ModalDialog } from '@mui/joy';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
export default function ResultsModal({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
experimentId,
|
||||||
|
evaluator,
|
||||||
|
}) {
|
||||||
|
const [resultText, setResultText] = useState('');
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && experimentId && evaluator) {
|
||||||
|
const output_file = 'scripts/evals/' + evaluator + '/output.txt';
|
||||||
|
fetch(
|
||||||
|
chatAPI.Endpoints.Experiment.GetFile(experimentId, output_file)
|
||||||
|
).then((res) => {
|
||||||
|
res.json().then((text) => {
|
||||||
|
setResultText(text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalDialog
|
||||||
|
sx={{
|
||||||
|
width: '70vw',
|
||||||
|
maxHeight: '80vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>Results from: {evaluator}</DialogTitle>
|
||||||
|
<DialogContent
|
||||||
|
sx={{ backgroundColor: '#222', color: '#ddd', padding: 2 }}
|
||||||
|
>
|
||||||
|
<pre>{resultText}</pre>
|
||||||
|
</DialogContent>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
186
src/renderer/components/Experiment/ExperimentNotes.tsx
Normal file
186
src/renderer/components/Experiment/ExperimentNotes.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import { useRef, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
|
||||||
|
import { Editor } from '@monaco-editor/react';
|
||||||
|
|
||||||
|
import Markdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
|
const parseTmTheme = require('monaco-themes').parseTmTheme;
|
||||||
|
// import monakai from 'monaco-themes/themes/Monokai Bright.json';
|
||||||
|
import fairyflossTheme from '../Shared/fairyfloss.tmTheme.js';
|
||||||
|
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
import { PencilIcon } from 'lucide-react';
|
||||||
|
import { Button, Typography } from '@mui/joy';
|
||||||
|
|
||||||
|
function setTheme(editor: any, monaco: any) {
|
||||||
|
const themeData = parseTmTheme(fairyflossTheme);
|
||||||
|
|
||||||
|
monaco.editor.defineTheme('my-theme', themeData);
|
||||||
|
monaco.editor.setTheme('my-theme');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function ExperimentNotes({ experimentInfo }) {
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
// Fetch the experiment markdown
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
chatAPI.Endpoints.Experiment.GetFile(experimentId(), 'readme.md'),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
if (editorRef?.current && typeof data === 'string') {
|
||||||
|
editorRef?.current?.setValue(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
function handleEditorDidMount(editor, monaco) {
|
||||||
|
editorRef.current = editor;
|
||||||
|
if (editorRef?.current && typeof data === 'string') {
|
||||||
|
editorRef?.current?.setValue(data);
|
||||||
|
}
|
||||||
|
setTheme(editor, monaco);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveValue() {
|
||||||
|
let value = editorRef?.current?.getValue();
|
||||||
|
|
||||||
|
// A blank string will cause the save to fail, so we replace it with a space
|
||||||
|
if (value == '') {
|
||||||
|
value = ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fetch to post the value to the server
|
||||||
|
fetch(
|
||||||
|
chatAPI.Endpoints.Experiment.SaveFile(experimentInfo.id, 'readme.md'),
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: value,
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
mutate(value);
|
||||||
|
setIsEditing(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function experimentId() {
|
||||||
|
if (experimentInfo) {
|
||||||
|
return experimentInfo.id;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!experimentInfo || experimentInfo.id == '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sheet>
|
||||||
|
<Typography level="h1">Experiment Notes</Typography>
|
||||||
|
{!isEditing && (
|
||||||
|
<>
|
||||||
|
<Sheet
|
||||||
|
color="neutral"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
mt: 3,
|
||||||
|
minHeight: '300px',
|
||||||
|
minWidth: '600px',
|
||||||
|
p: 3,
|
||||||
|
boxShadow:
|
||||||
|
'rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.08) 0px 0px 0px 1px',
|
||||||
|
}}
|
||||||
|
className="editableSheet"
|
||||||
|
>
|
||||||
|
{!data && 'Write experiment notes here...'}
|
||||||
|
<Markdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
className="editableSheetContent"
|
||||||
|
>
|
||||||
|
{data}
|
||||||
|
</Markdown>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditing(true);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
ml: 'auto',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '30%',
|
||||||
|
left: '20%',
|
||||||
|
}}
|
||||||
|
variant="solid"
|
||||||
|
className="hoverEditButton"
|
||||||
|
startDecorator={<PencilIcon size="18px" />}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<>
|
||||||
|
<Typography mt={3}>
|
||||||
|
Use{' '}
|
||||||
|
<a href="https://github.github.com/gfm/" target="_blank">
|
||||||
|
GitHub Flavored Markdown
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
<Sheet
|
||||||
|
color="neutral"
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
backgroundColor: '#ddd',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
height="600px"
|
||||||
|
defaultLanguage="markdown"
|
||||||
|
theme="my-theme"
|
||||||
|
options={{
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
fontSize: 18,
|
||||||
|
cursorStyle: 'block',
|
||||||
|
wordWrap: 'on',
|
||||||
|
}}
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
/>
|
||||||
|
</Sheet>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
saveValue();
|
||||||
|
}}
|
||||||
|
sx={{ mt: 1, ml: 'auto' }}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
sx={{ ml: '10px' }}
|
||||||
|
onClick={() => setIsEditing(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
|
||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
|
||||||
|
import { Button, IconButton, Stack, Typography } from '@mui/joy';
|
||||||
|
import { BabyIcon, DotIcon, Trash2Icon, XCircleIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import * as chatAPI from '../../../lib/transformerlab-api-sdk';
|
||||||
|
import ModelDetails from './ModelDetails';
|
||||||
|
|
||||||
|
const fetchWithPost = ({ url, post }) =>
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: post,
|
||||||
|
}).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function CurrentFoundationInfo({
|
||||||
|
experimentInfo,
|
||||||
|
setFoundation,
|
||||||
|
adaptor,
|
||||||
|
setAdaptor,
|
||||||
|
}) {
|
||||||
|
const { data: peftData } = useSWR(
|
||||||
|
{
|
||||||
|
url: chatAPI.Endpoints.Models.GetPeftsForModel(),
|
||||||
|
post: experimentInfo?.config?.foundation,
|
||||||
|
},
|
||||||
|
fetchWithPost
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
paddingBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModelDetails
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
adaptor={adaptor}
|
||||||
|
setAdaptor={setAdaptor}
|
||||||
|
setFoundation={setFoundation}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography level="title-lg" marginTop={4} marginBottom={1}>
|
||||||
|
<BabyIcon size="1rem" />
|
||||||
|
Available Adaptors:
|
||||||
|
</Typography>
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
spacing={1}
|
||||||
|
style={{ overflow: 'auto', height: '100%' }}
|
||||||
|
>
|
||||||
|
{peftData &&
|
||||||
|
peftData.length === 0 &&
|
||||||
|
'No Adaptors available for this model. Train one!'}
|
||||||
|
{peftData &&
|
||||||
|
peftData.map((peft) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'left',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
key={peft}
|
||||||
|
>
|
||||||
|
<Typography level="title-md" paddingRight={3}>
|
||||||
|
{peft}
|
||||||
|
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
onClick={() => {
|
||||||
|
setAdaptor(peft);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => {
|
||||||
|
fetch(
|
||||||
|
chatAPI.Endpoints.Models.DeletePeft(
|
||||||
|
experimentInfo?.config?.foundation,
|
||||||
|
peft
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2Icon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
197
src/renderer/components/Experiment/Foundation/ModelDetails.tsx
Normal file
197
src/renderer/components/Experiment/Foundation/ModelDetails.tsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
Link,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy';
|
||||||
|
import {
|
||||||
|
DeleteIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
XSquareIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import RunModelButton from 'renderer/components/Nav/RunModelButton';
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
import {
|
||||||
|
killWorker,
|
||||||
|
useModelStatus,
|
||||||
|
} from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
const hf_config_translation = {
|
||||||
|
architectures: 'Architecture',
|
||||||
|
attention_dropout: 'Attention Dropout',
|
||||||
|
bos_token_id: 'BOS Token ID',
|
||||||
|
bos_token: 'BOS Token',
|
||||||
|
classifier_dropout: 'Classifier Dropout',
|
||||||
|
decoder_start_token_id: 'Decoder Start Token ID',
|
||||||
|
decoder_start_token: 'Decoder Start Token',
|
||||||
|
dropout: 'Dropout',
|
||||||
|
d_ff: 'Feed Forward Dimension',
|
||||||
|
d_kv: 'Key/Value Dimension',
|
||||||
|
d_model: 'Model Dimensions',
|
||||||
|
num_heads: 'Number of Heads',
|
||||||
|
num_layers: 'Number of Layers',
|
||||||
|
vocab_size: 'Vocabulary Size',
|
||||||
|
};
|
||||||
|
|
||||||
|
function hf_translate(key) {
|
||||||
|
return hf_config_translation[key] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ModelDetails({
|
||||||
|
experimentInfo,
|
||||||
|
adaptor,
|
||||||
|
setFoundation,
|
||||||
|
setAdaptor,
|
||||||
|
}) {
|
||||||
|
const [huggingfaceData, setHugggingfaceData] = useState({});
|
||||||
|
const [modelDetailsData, setModelDetailsData] = useState({});
|
||||||
|
const { models, isError, isLoading, mutate } = useModelStatus();
|
||||||
|
|
||||||
|
const huggingfaceId = experimentInfo?.config?.foundation;
|
||||||
|
|
||||||
|
useMemo(() => {
|
||||||
|
if (huggingfaceId) {
|
||||||
|
fetch(`https://huggingface.co/${huggingfaceId}/resolve/main/config.json`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => setHugggingfaceData(data));
|
||||||
|
|
||||||
|
fetch(chatAPI.Endpoints.Models.ModelDetailsFromGallery(huggingfaceId))
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
setModelDetailsData(data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setHugggingfaceData({});
|
||||||
|
setModelDetailsData({});
|
||||||
|
}
|
||||||
|
}, [huggingfaceId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction="row" sx={{ minHeight: '300px' }}>
|
||||||
|
<img
|
||||||
|
src={modelDetailsData?.logo}
|
||||||
|
alt=""
|
||||||
|
style={{
|
||||||
|
float: 'left',
|
||||||
|
margin: '0px 40px 0px 0px',
|
||||||
|
width: '300px',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: '20px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography level="h1">
|
||||||
|
{experimentInfo?.config?.foundation}
|
||||||
|
</Typography>
|
||||||
|
<Typography level="h3">
|
||||||
|
<b>Adaptor:</b>
|
||||||
|
{experimentInfo?.config?.foundation ? (
|
||||||
|
<>
|
||||||
|
{adaptor}
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
sx={{
|
||||||
|
color: 'neutral.300',
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setAdaptor('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon size="18px" />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'None'
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" gap={8} marginTop={1}>
|
||||||
|
<Link
|
||||||
|
href={modelDetailsData?.resources?.canonicalUrl}
|
||||||
|
target="_blank"
|
||||||
|
endDecorator={<ExternalLinkIcon size="16px" />}
|
||||||
|
>
|
||||||
|
<Typography level="title-md">
|
||||||
|
{modelDetailsData?.author?.name}
|
||||||
|
</Typography>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={modelDetailsData?.resources?.downloadUrl}
|
||||||
|
target="_blank"
|
||||||
|
endDecorator={<ExternalLinkIcon size="16px" />}
|
||||||
|
>
|
||||||
|
<Typography level="title-md">Model Details</Typography>
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
paddingTop={2}
|
||||||
|
sx={{
|
||||||
|
maxHeight: '120px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{modelDetailsData?.description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="flex-start"
|
||||||
|
spacing={2}
|
||||||
|
>
|
||||||
|
<RunModelButton
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
killWorker={killWorker}
|
||||||
|
models={models}
|
||||||
|
mutate={mutate}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
startDecorator={<XSquareIcon />}
|
||||||
|
onClick={() => setFoundation(null)}
|
||||||
|
color="danger"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
Eject Model
|
||||||
|
</Button>
|
||||||
|
{/* <Button startDecorator={<SquareIcon />}>Stop</Button> */}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<Divider sx={{ marginTop: '30px' }} />
|
||||||
|
<div>
|
||||||
|
<Table id="huggingface-model-config-info">
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(huggingfaceData).map(
|
||||||
|
(row) =>
|
||||||
|
hf_translate(row[0]) !== null && (
|
||||||
|
<tr key={row[0]}>
|
||||||
|
<td>{hf_translate(row[0])}</td>
|
||||||
|
<td>{JSON.stringify(row[1])}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
{/* <pre>{data && JSON.stringify(data, null, 2)}</pre> */}
|
||||||
|
{/* {JSON.stringify(modelDetailsData, null, 2)} */}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
319
src/renderer/components/Experiment/Foundation/SelectAModel.tsx
Normal file
319
src/renderer/components/Experiment/Foundation/SelectAModel.tsx
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Sheet,
|
||||||
|
Table,
|
||||||
|
Typography,
|
||||||
|
Option,
|
||||||
|
Chip,
|
||||||
|
Link,
|
||||||
|
Box,
|
||||||
|
Stack,
|
||||||
|
LinearProgress,
|
||||||
|
Modal,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
import { Link as ReactRouterLink, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { ColorPaletteProp } from '@mui/joy/styles';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowDownIcon,
|
||||||
|
BoxesIcon,
|
||||||
|
CheckIcon,
|
||||||
|
CreativeCommonsIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
GraduationCapIcon,
|
||||||
|
InfoIcon,
|
||||||
|
PlusIcon,
|
||||||
|
SearchIcon,
|
||||||
|
StoreIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import SelectButton from '../SelectButton';
|
||||||
|
import CurrentFoundationInfo from './CurrentFoundationInfo';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import * as chatAPI from '../../../lib/transformerlab-api-sdk';
|
||||||
|
import Welcome from '../../Welcome';
|
||||||
|
|
||||||
|
type Order = 'asc' | 'desc';
|
||||||
|
|
||||||
|
function convertModelObjectToArray(modelObject) {
|
||||||
|
// The model object in the storage is big object,
|
||||||
|
// Here we turn that into an array of objects
|
||||||
|
|
||||||
|
const arr = [{}];
|
||||||
|
const keys = Object.keys(modelObject);
|
||||||
|
|
||||||
|
for (let i = 0, n = keys.length; i < n; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
arr[i] = modelObject[key];
|
||||||
|
arr[i].name = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModelFolderInFilesystem() {
|
||||||
|
//window.filesys.openModelFolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function SelectAModel({
|
||||||
|
experimentInfo,
|
||||||
|
setFoundation = (model) => {},
|
||||||
|
setAdaptor = (name: string) => {},
|
||||||
|
}) {
|
||||||
|
const [order, setOrder] = useState<Order>('desc');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
chatAPI.Endpoints.Models.LocalList(),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
function foundationSetter(model) {
|
||||||
|
setOpen(true);
|
||||||
|
|
||||||
|
setFoundation(model);
|
||||||
|
|
||||||
|
setAdaptor('');
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderFilters = () => (
|
||||||
|
<>
|
||||||
|
<FormControl size="sm">
|
||||||
|
<FormLabel>License</FormLabel>
|
||||||
|
<Select
|
||||||
|
placeholder="Filter by license"
|
||||||
|
slotProps={{ button: { sx: { whiteSpace: 'nowrap' } } }}
|
||||||
|
>
|
||||||
|
<Option value="MIT">MIT</Option>
|
||||||
|
<Option value="pending">CC BY-SA-4.0</Option>
|
||||||
|
<Option value="refunded">Refunded</Option>
|
||||||
|
<Option value="Cancelled">Apache 2.0</Option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl size="sm">
|
||||||
|
<FormLabel>Category</FormLabel>
|
||||||
|
<Select placeholder="All">
|
||||||
|
<Option value="all">All</Option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (experimentInfo?.config?.foundation) {
|
||||||
|
return (
|
||||||
|
<CurrentFoundationInfo
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
foundation={experimentInfo?.config?.adaptor}
|
||||||
|
setFoundation={setFoundation}
|
||||||
|
adaptor={experimentInfo?.config?.adaptor}
|
||||||
|
setAdaptor={setAdaptor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!experimentInfo && location?.pathname !== '/zoo') {
|
||||||
|
return 'Select an Experiment';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography level="h1" mb={2}>
|
||||||
|
Local Models
|
||||||
|
</Typography>
|
||||||
|
<Modal
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
aria-describedby="modal-desc"
|
||||||
|
open={open}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sheet
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
maxWidth: 500,
|
||||||
|
borderRadius: 'md',
|
||||||
|
p: 3,
|
||||||
|
boxShadow: 'lg',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
component="h2"
|
||||||
|
id="modal-title"
|
||||||
|
level="h4"
|
||||||
|
textColor="inherit"
|
||||||
|
fontWeight="lg"
|
||||||
|
mb={1}
|
||||||
|
>
|
||||||
|
Preparing Model
|
||||||
|
</Typography>
|
||||||
|
<Typography id="modal-desc" textColor="text.tertiary">
|
||||||
|
<Stack spacing={2} sx={{ flex: 1 }}>
|
||||||
|
Quantizing Parameters:
|
||||||
|
<LinearProgress />
|
||||||
|
</Stack>
|
||||||
|
</Typography>
|
||||||
|
</Sheet>
|
||||||
|
</Modal>
|
||||||
|
<Box
|
||||||
|
className="SearchAndFilters-tabletUp"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 'sm',
|
||||||
|
py: 2,
|
||||||
|
display: {
|
||||||
|
xs: 'flex',
|
||||||
|
sm: 'flex',
|
||||||
|
},
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 1.5,
|
||||||
|
'& > *': {
|
||||||
|
minWidth: {
|
||||||
|
xs: '120px',
|
||||||
|
md: '160px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl sx={{ flex: 1 }} size="sm">
|
||||||
|
<FormLabel> </FormLabel>
|
||||||
|
<Input placeholder="Search" startDecorator={<SearchIcon />} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{renderFilters()}
|
||||||
|
</Box>
|
||||||
|
<Sheet
|
||||||
|
className="OrderTableContainer"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 'md',
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
minHeight: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
aria-labelledby="tableTitle"
|
||||||
|
stickyHeader
|
||||||
|
hoverRow
|
||||||
|
sx={{
|
||||||
|
'--TableCell-headBackground': (theme) =>
|
||||||
|
theme.vars.palette.background.level1,
|
||||||
|
'--Table-headerUnderlineThickness': '1px',
|
||||||
|
'--TableRow-hoverBackground': (theme) =>
|
||||||
|
theme.vars.palette.background.level1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: 140, padding: 12 }}>
|
||||||
|
<Link
|
||||||
|
underline="none"
|
||||||
|
color="primary"
|
||||||
|
component="button"
|
||||||
|
onClick={() => setOrder(order === 'asc' ? 'desc' : 'asc')}
|
||||||
|
fontWeight="lg"
|
||||||
|
endDecorator={<ArrowDownIcon />}
|
||||||
|
sx={{
|
||||||
|
'& svg': {
|
||||||
|
transition: '0.2s',
|
||||||
|
transform:
|
||||||
|
order === 'desc' ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
<th style={{ width: 120, padding: 12 }}>Params</th>
|
||||||
|
<th style={{ width: 120, padding: 12 }}>License</th>
|
||||||
|
{/* <th style={{ width: 220, padding: 12 }}>Type</th> */}
|
||||||
|
<th style={{ width: 120, padding: 12 }}> </th>
|
||||||
|
<th style={{ width: 160, padding: 12 }}> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data &&
|
||||||
|
data.map((row) => (
|
||||||
|
<tr key={row.rowid}>
|
||||||
|
<td>
|
||||||
|
<Typography ml={2} fontWeight="lg">
|
||||||
|
{row.name}
|
||||||
|
</Typography>
|
||||||
|
</td>
|
||||||
|
<td>{row?.json_data?.parameters}</td>
|
||||||
|
<td>
|
||||||
|
<Chip
|
||||||
|
variant="soft"
|
||||||
|
size="sm"
|
||||||
|
startDecorator={
|
||||||
|
{
|
||||||
|
MIT: <CheckIcon />,
|
||||||
|
Apache: <GraduationCapIcon />,
|
||||||
|
CC: <CreativeCommonsIcon />,
|
||||||
|
}[row.status]
|
||||||
|
}
|
||||||
|
color={
|
||||||
|
{
|
||||||
|
MIT: 'success',
|
||||||
|
Apache: 'neutral',
|
||||||
|
CC: 'success',
|
||||||
|
}[row.status] as ColorPaletteProp
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row?.json_data?.license}
|
||||||
|
</Chip>
|
||||||
|
</td>
|
||||||
|
<td>{row.model_id}</td>
|
||||||
|
<td style={{ textAlign: 'right' }}>
|
||||||
|
<SelectButton
|
||||||
|
setFoundation={foundationSetter}
|
||||||
|
model={row}
|
||||||
|
setAdaptor={setAdaptor}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{data?.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5}>
|
||||||
|
<Typography
|
||||||
|
level="body-lg"
|
||||||
|
justifyContent="center"
|
||||||
|
margin={5}
|
||||||
|
>
|
||||||
|
You do not have any models on your local machine. You can
|
||||||
|
download a model by going to the{' '}
|
||||||
|
<ReactRouterLink to="/zoo">
|
||||||
|
<StoreIcon />
|
||||||
|
Model Store
|
||||||
|
</ReactRouterLink>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/renderer/components/Experiment/Foundation/index.tsx
Normal file
16
src/renderer/components/Experiment/Foundation/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import SelectAModel from './SelectAModel';
|
||||||
|
|
||||||
|
export default function FoundationHome({
|
||||||
|
pickAModelMode = false,
|
||||||
|
experimentInfo,
|
||||||
|
setFoundation = (name: string) => {},
|
||||||
|
setAdaptor = (name: string) => {},
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectAModel
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
setFoundation={setFoundation}
|
||||||
|
setAdaptor={setAdaptor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
201
src/renderer/components/Experiment/Interact/ChatBubble.tsx
Normal file
201
src/renderer/components/Experiment/Interact/ChatBubble.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import { Avatar, LinearProgress } from '@mui/joy';
|
||||||
|
import {
|
||||||
|
BotIcon,
|
||||||
|
ClipboardCopyIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
UserCircleIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import Markdown from 'react-markdown';
|
||||||
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
|
import { oneDark as oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||||
|
|
||||||
|
function convertNewLines(text) {
|
||||||
|
if (typeof text !== 'string') return text;
|
||||||
|
if (text === null) return '';
|
||||||
|
return text.split('\n').map((str) => {
|
||||||
|
return (
|
||||||
|
<p style={{ margin: 0, padding: 0, marginTop: 10, marginBottom: 10 }}>
|
||||||
|
{str}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatBubble({
|
||||||
|
t,
|
||||||
|
chatId,
|
||||||
|
pos,
|
||||||
|
isThinking = false,
|
||||||
|
hide = false,
|
||||||
|
deleteChat = (key) => {},
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: hide ? 'none' : 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: 'fit-content',
|
||||||
|
|
||||||
|
padding: '10px',
|
||||||
|
paddingLeft: '22px',
|
||||||
|
paddingRight: '18px',
|
||||||
|
paddingBottom: '8px',
|
||||||
|
|
||||||
|
backgroundColor:
|
||||||
|
pos === 'bot'
|
||||||
|
? 'var(--joy-palette-neutral-100)'
|
||||||
|
: 'var(--joy-palette-primary-400)',
|
||||||
|
marginLeft: pos === 'human' ? 'auto' : '0',
|
||||||
|
borderRadius: '20px',
|
||||||
|
}}
|
||||||
|
className="chatBubble"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
|
||||||
|
color: pos === 'bot' ? 'black' : 'white',
|
||||||
|
|
||||||
|
justifyContent: pos === 'bot' ? 'left' : 'right',
|
||||||
|
textAlign: pos === 'bot' ? 'left' : 'right',
|
||||||
|
}}
|
||||||
|
className="chatBubbleContent"
|
||||||
|
>
|
||||||
|
{pos === 'human' && !isThinking && (
|
||||||
|
<div>
|
||||||
|
<Markdown
|
||||||
|
children={t}
|
||||||
|
components={{
|
||||||
|
code(props) {
|
||||||
|
const { children, className, node, ...rest } = props;
|
||||||
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
|
return match ? (
|
||||||
|
<SyntaxHighlighter
|
||||||
|
{...rest}
|
||||||
|
PreTag="div"
|
||||||
|
children={String(children).replace(/\n$/, '')}
|
||||||
|
language={match[1]}
|
||||||
|
style={oneDark}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<code {...rest} className={className}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: pos === 'bot' ? '15px' : '0',
|
||||||
|
marginLeft: pos === 'human' ? '15px' : '0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
float: 'left',
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{pos === 'bot' ? <BotIcon /> : <UserCircleIcon />}
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
{pos === 'bot' && !isThinking && (
|
||||||
|
<div style={{ maxWidth: '40vw', overflow: 'hidden' }}>
|
||||||
|
<Markdown
|
||||||
|
children={t}
|
||||||
|
components={{
|
||||||
|
code(props) {
|
||||||
|
const { children, className, node, ...rest } = props;
|
||||||
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
|
return match ? (
|
||||||
|
<SyntaxHighlighter
|
||||||
|
{...rest}
|
||||||
|
PreTag="div"
|
||||||
|
children={String(children).replace(/\n$/, '')}
|
||||||
|
language={match[1]}
|
||||||
|
style={oneDark}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<code {...rest} className={className}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isThinking && (
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
marginTop: 10,
|
||||||
|
marginBottom: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* This is a placeholder for the bot's response.
|
||||||
|
sendMessageToLLM automatically find this box and adds streaming
|
||||||
|
response to it */}
|
||||||
|
<span id="resultText" />
|
||||||
|
</p>
|
||||||
|
<LinearProgress
|
||||||
|
variant="plain"
|
||||||
|
color="neutral"
|
||||||
|
sx={{ color: '#ddd', width: '60px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: isThinking ? 'none' : 'block',
|
||||||
|
position: 'relative',
|
||||||
|
bottom: '20px',
|
||||||
|
margin: 'auto',
|
||||||
|
height: '0px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<ClipboardCopyIcon
|
||||||
|
color={
|
||||||
|
pos === 'bot'
|
||||||
|
? 'var(--joy-palette-neutral-600)'
|
||||||
|
: 'var(--joy-palette-neutral-100)'
|
||||||
|
}
|
||||||
|
size="22px"
|
||||||
|
className="hoverIcon showOnChatBubbleHover"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(t);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<Trash2Icon
|
||||||
|
color={
|
||||||
|
pos === 'bot'
|
||||||
|
? 'var(--joy-palette-neutral-800)'
|
||||||
|
: 'var(--joy-palette-neutral-100)'
|
||||||
|
}
|
||||||
|
size="22px"
|
||||||
|
className="hoverIcon showOnChatBubbleHover"
|
||||||
|
onClick={() => deleteChat(chatId)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
155
src/renderer/components/Experiment/Interact/ChatPage.tsx
Normal file
155
src/renderer/components/Experiment/Interact/ChatPage.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Sheet,
|
||||||
|
Stack,
|
||||||
|
Textarea,
|
||||||
|
} from '@mui/joy';
|
||||||
|
import ChatBubble from './ChatBubble';
|
||||||
|
import ChatSubmit from './ChatSubmit';
|
||||||
|
import * as chatAPI from '../../../lib/transformerlab-api-sdk';
|
||||||
|
import { CheckIcon, PencilIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function ChatPage({
|
||||||
|
chats,
|
||||||
|
setChats,
|
||||||
|
templateTextIsEditable,
|
||||||
|
experimentInfo,
|
||||||
|
isThinking,
|
||||||
|
sendNewMessageToLLM,
|
||||||
|
experimentInfoMutate,
|
||||||
|
setTemplateTextIsEditable,
|
||||||
|
tokenCount,
|
||||||
|
text,
|
||||||
|
debouncedText,
|
||||||
|
}) {
|
||||||
|
// Delete a chat from state array with key provided:
|
||||||
|
const deleteChat = (key) => {
|
||||||
|
setChats((c) => c.filter((chat) => chat.key !== key));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearHistory = () => {
|
||||||
|
setChats([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
id="chat-window"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 'md',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 1,
|
||||||
|
flex: 'auto',
|
||||||
|
justifyContent: 'space-evenly',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sheet
|
||||||
|
variant="outlined"
|
||||||
|
id="system-message-box"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
// borderRadius: "md",
|
||||||
|
flex: '0 0 130px',
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel sx={{ justifyContent: 'space-between', width: '100%' }}>
|
||||||
|
<span>System message</span>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
{templateTextIsEditable ? (
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
startDecorator={<CheckIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
const experimentId = experimentInfo?.id;
|
||||||
|
const newSystemPrompt =
|
||||||
|
document.getElementsByName('system-message')[0]?.value;
|
||||||
|
|
||||||
|
var newPrompt = {
|
||||||
|
...experimentInfo?.config?.prompt_template,
|
||||||
|
};
|
||||||
|
newPrompt.system_message = newSystemPrompt;
|
||||||
|
|
||||||
|
fetch(chatAPI.SAVE_EXPERIMENT_PROMPT_URL(experimentId), {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(newPrompt),
|
||||||
|
}).then((response) => {
|
||||||
|
experimentInfoMutate();
|
||||||
|
});
|
||||||
|
|
||||||
|
setTemplateTextIsEditable(!templateTextIsEditable);
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<PencilIcon
|
||||||
|
size={18}
|
||||||
|
onClick={() =>
|
||||||
|
setTemplateTextIsEditable(!templateTextIsEditable)
|
||||||
|
}
|
||||||
|
color={templateTextIsEditable ? '#aaa' : '#000'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Textarea
|
||||||
|
placeholder="You are a helpful chatbot"
|
||||||
|
variant="plain"
|
||||||
|
name="system-message"
|
||||||
|
disabled={!templateTextIsEditable}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
|
<Sheet
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
// borderRadius: "md",
|
||||||
|
flex: '99',
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* {JSON.stringify(chats)} */}
|
||||||
|
<Stack spacing={2} sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{chats.map((chat) => (
|
||||||
|
<ChatBubble
|
||||||
|
t={chat.t}
|
||||||
|
chatId={chat.key}
|
||||||
|
pos={chat.user}
|
||||||
|
key={chat.key}
|
||||||
|
deleteChat={deleteChat}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
{/* This is a placeholder for the bot's response. sendMessageToLLM writes directly to this chat bubble */}
|
||||||
|
<ChatBubble
|
||||||
|
isThinking
|
||||||
|
chatId="thinking"
|
||||||
|
hide={!isThinking}
|
||||||
|
t="Thinking..."
|
||||||
|
pos="bot"
|
||||||
|
key={'thinking'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div id="endofchat" />
|
||||||
|
</Sheet>
|
||||||
|
<ChatSubmit
|
||||||
|
addMessage={sendNewMessageToLLM}
|
||||||
|
spinner={isThinking}
|
||||||
|
clearHistory={clearHistory}
|
||||||
|
tokenCount={tokenCount}
|
||||||
|
text={text}
|
||||||
|
debouncedText={debouncedText}
|
||||||
|
/>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
144
src/renderer/components/Experiment/Interact/ChatSubmit.tsx
Normal file
144
src/renderer/components/Experiment/Interact/ChatSubmit.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import Box from '@mui/joy/Box';
|
||||||
|
import Button from '@mui/joy/Button';
|
||||||
|
import FormControl from '@mui/joy/FormControl';
|
||||||
|
import Textarea from '@mui/joy/Textarea';
|
||||||
|
import {
|
||||||
|
DeleteIcon,
|
||||||
|
InfoIcon,
|
||||||
|
SaveIcon,
|
||||||
|
SendIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
CircularProgress,
|
||||||
|
Select,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
Option,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
function scrollChatToBottom() {
|
||||||
|
document.getElementById('endofchat').scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatSubmit({
|
||||||
|
addMessage,
|
||||||
|
spinner,
|
||||||
|
clearHistory,
|
||||||
|
tokenCount,
|
||||||
|
text,
|
||||||
|
debouncedText,
|
||||||
|
}) {
|
||||||
|
const [italic] = useState(false);
|
||||||
|
const [fontWeight] = useState('normal');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl sx={{ width: '100%', margin: 'auto', flex: 1 }}>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Type something here..."
|
||||||
|
minRows={3}
|
||||||
|
slotProps={{
|
||||||
|
textarea: {
|
||||||
|
id: 'chat-input',
|
||||||
|
name: 'chat-input',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
endDecorator={
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 'var(--Textarea-paddingBlock)',
|
||||||
|
pt: 'var(--Textarea-paddingBlock)',
|
||||||
|
borderTop: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
flex: 'auto',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="neutral"
|
||||||
|
variant="plain"
|
||||||
|
sx={{ color: 'text.tertiary' }}
|
||||||
|
startDecorator={<XCircleIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
clearHistory();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear Chat History
|
||||||
|
</Button>
|
||||||
|
<Typography
|
||||||
|
level="body-xs"
|
||||||
|
sx={{ ml: 'auto' }}
|
||||||
|
color={
|
||||||
|
tokenCount?.tokenCount > tokenCount?.contextLength
|
||||||
|
? 'danger'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{text !== debouncedText ? (
|
||||||
|
<CircularProgress
|
||||||
|
color="neutral"
|
||||||
|
sx={{
|
||||||
|
'--CircularProgress-size': '16px',
|
||||||
|
'--CircularProgress-trackThickness': '4px',
|
||||||
|
'--CircularProgress-progressThickness': '3px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
tokenCount?.tokenCount
|
||||||
|
)}{' '}
|
||||||
|
of {tokenCount?.contextLength} tokens
|
||||||
|
<Tooltip title="Approximation only" followCursor>
|
||||||
|
<InfoIcon size="12px" />
|
||||||
|
</Tooltip>
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
sx={{ ml: 'auto' }}
|
||||||
|
color="neutral"
|
||||||
|
endDecorator={
|
||||||
|
spinner ? (
|
||||||
|
<CircularProgress
|
||||||
|
thickness={2}
|
||||||
|
size="sm"
|
||||||
|
color="neutral"
|
||||||
|
sx={{
|
||||||
|
'--CircularProgress-size': '13px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SendIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={spinner}
|
||||||
|
id="chat-submit-button"
|
||||||
|
onClick={() => {
|
||||||
|
scrollChatToBottom();
|
||||||
|
const msg = document.getElementById('chat-input').value;
|
||||||
|
document.getElementById('chat-input').value = '';
|
||||||
|
document.getElementById('chat-input').focus();
|
||||||
|
addMessage(msg);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{spinner ? 'Generating' : 'Submit'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
minWidth: 300,
|
||||||
|
fontWeight,
|
||||||
|
fontStyle: italic ? 'italic' : 'initial',
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
// Support Submit on Enter, but ignore if
|
||||||
|
// User types shift-enter
|
||||||
|
if (event.shiftKey) return;
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
event.preventDefault();
|
||||||
|
document.getElementById('chat-submit-button').click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
src/renderer/components/Experiment/Interact/CompletionsPage.tsx
Normal file
102
src/renderer/components/Experiment/Interact/CompletionsPage.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Sheet,
|
||||||
|
Textarea,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy';
|
||||||
|
import { SendIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function CompletionsPage({
|
||||||
|
text,
|
||||||
|
setText,
|
||||||
|
debouncedText,
|
||||||
|
tokenCount,
|
||||||
|
isThinking,
|
||||||
|
sendCompletionToLLM,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '10px',
|
||||||
|
paddingBottom: '10px',
|
||||||
|
height: '100%',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sheet
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: 2,
|
||||||
|
margin: 'auto',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
placeholder="When I was young, I would"
|
||||||
|
variant="plain"
|
||||||
|
name="completion-text"
|
||||||
|
minRows={20}
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
height: '100%',
|
||||||
|
'--Textarea-focusedHighlight': 'rgba(13,110,253,0)',
|
||||||
|
}}
|
||||||
|
endDecorator={
|
||||||
|
<Typography level="body-xs" sx={{ ml: 'auto' }}>
|
||||||
|
{text !== debouncedText ? (
|
||||||
|
<CircularProgress
|
||||||
|
color="neutral"
|
||||||
|
sx={{
|
||||||
|
'--CircularProgress-size': '16px',
|
||||||
|
'--CircularProgress-trackThickness': '4px',
|
||||||
|
'--CircularProgress-progressThickness': '3px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
tokenCount?.tokenCount
|
||||||
|
)}{' '}
|
||||||
|
of {tokenCount?.contextLength} tokens
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
setText(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Sheet>
|
||||||
|
<Button
|
||||||
|
sx={{ ml: 'auto' }}
|
||||||
|
color="neutral"
|
||||||
|
endDecorator={
|
||||||
|
isThinking ? (
|
||||||
|
<CircularProgress
|
||||||
|
thickness={2}
|
||||||
|
size="sm"
|
||||||
|
color="neutral"
|
||||||
|
sx={{
|
||||||
|
'--CircularProgress-size': '13px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SendIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={isThinking}
|
||||||
|
id="chat-submit-button"
|
||||||
|
onClick={() =>
|
||||||
|
sendCompletionToLLM(
|
||||||
|
document.getElementsByName('completion-text')?.[0]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isThinking ? 'Generating' : 'Generate'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
599
src/renderer/components/Experiment/Interact/Interact.tsx
Normal file
599
src/renderer/components/Experiment/Interact/Interact.tsx
Normal file
@@ -0,0 +1,599 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import * as React from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Button,
|
||||||
|
Slider,
|
||||||
|
Typography,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Box,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListDivider,
|
||||||
|
ListItemDecorator,
|
||||||
|
ListItemContent,
|
||||||
|
ListItemButton,
|
||||||
|
IconButton,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
import ChatPage from './ChatPage';
|
||||||
|
|
||||||
|
import * as chatAPI from '../../../lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
import { useDebounce } from 'use-debounce';
|
||||||
|
import CompletionsPage from './CompletionsPage';
|
||||||
|
import { MessagesSquareIcon, XIcon } from 'lucide-react';
|
||||||
|
import e from 'express';
|
||||||
|
|
||||||
|
function scrollChatToBottom() {
|
||||||
|
// We animate it twice, the second time to accomodate the scale up transition
|
||||||
|
// I find this looks better than one later scroll
|
||||||
|
setTimeout(() => document.getElementById('endofchat')?.scrollIntoView(), 100);
|
||||||
|
setTimeout(() => document.getElementById('endofchat')?.scrollIntoView(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortenArray(arr, maxLen) {
|
||||||
|
if (!arr) return [];
|
||||||
|
if (arr.length <= maxLen) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
return arr.slice(0, maxLen - 1).concat('...');
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncate(str, n) {
|
||||||
|
if (!str) return '';
|
||||||
|
|
||||||
|
return str.length > n ? <>{str.slice(0, n - 1)} …</> : <>{str}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function Chat({ experimentInfo, experimentInfoMutate }) {
|
||||||
|
const { models, isError, isLoading } = chatAPI.useModelStatus();
|
||||||
|
const [mode, setMode] = React.useState('chat');
|
||||||
|
const [conversationId, setConversationId] = React.useState(null);
|
||||||
|
const [conversationList, setConversationList] = React.useState([]);
|
||||||
|
const [chats, setChats] = React.useState([]);
|
||||||
|
const [isThinking, setIsThinking] = React.useState(false);
|
||||||
|
const [temperature, setTemperature] = React.useState(0.9);
|
||||||
|
const [maxTokens, setMaxTokens] = React.useState(256);
|
||||||
|
const [topP, setTopP] = React.useState(1);
|
||||||
|
const [frequencyPenalty, setFrequencyPenalty] = React.useState(0);
|
||||||
|
|
||||||
|
const [templateTextIsEditable, setTemplateTextIsEditable] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
|
const [text, setText] = React.useState('');
|
||||||
|
|
||||||
|
var textToDebounce = '';
|
||||||
|
|
||||||
|
// The following code, when in chat mode, will try to create a fake string
|
||||||
|
// that roughly represents the chat as a long prompt. But this is a hack:
|
||||||
|
// If we really want count tokens accurately, we need to pass the system
|
||||||
|
// message and messages to the server and let it format the prompt as per
|
||||||
|
// the moel's template. More info here: https://huggingface.co/docs/transformers/main/en/chat_templating
|
||||||
|
// For now this is helpful a rough indicator of the number of tokens used.
|
||||||
|
// But we should improve this later
|
||||||
|
if (mode === 'chat') {
|
||||||
|
textToDebounce += experimentInfo?.config?.prompt_template?.system_message;
|
||||||
|
textToDebounce += '\n';
|
||||||
|
chats.forEach((c) => {
|
||||||
|
textToDebounce += c.t;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
textToDebounce = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [debouncedText] = useDebounce(textToDebounce, 1000);
|
||||||
|
|
||||||
|
const [tokenCount, setTokenCount] = React.useState({});
|
||||||
|
|
||||||
|
const currentModel = experimentInfo?.config?.foundation;
|
||||||
|
const adaptor = experimentInfo?.config?.adaptor;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (debouncedText) {
|
||||||
|
if (mode === 'chat') {
|
||||||
|
countChatTokens();
|
||||||
|
} else {
|
||||||
|
countTokens();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [debouncedText]);
|
||||||
|
|
||||||
|
// If the model changes, check the location of the inference service
|
||||||
|
// And reset the global pointer to the inference server
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!window.TransformerLab) {
|
||||||
|
window.TransformerLab = {};
|
||||||
|
}
|
||||||
|
if (models?.[0]?.location) {
|
||||||
|
window.TransformerLab.inferenceServerURL = models?.[0]?.location;
|
||||||
|
} else {
|
||||||
|
window.TransformerLab.inferenceServerURL = null;
|
||||||
|
}
|
||||||
|
}, [models]);
|
||||||
|
|
||||||
|
React.useMemo(() => {
|
||||||
|
const asyncTasks = async () => {
|
||||||
|
const result = await chatAPI.getTemplateForModel(currentModel);
|
||||||
|
const t = result?.system_message;
|
||||||
|
|
||||||
|
const parsedPromptData =
|
||||||
|
experimentInfo?.config?.prompt_template?.system_message;
|
||||||
|
|
||||||
|
if (parsedPromptData && document.getElementsByName('system-message')[0]) {
|
||||||
|
document.getElementsByName('system-message')[0].value =
|
||||||
|
parsedPromptData;
|
||||||
|
} else if (t) {
|
||||||
|
if (document.getElementsByName('system-message')[0])
|
||||||
|
document.getElementsByName('system-message')[0].value = t;
|
||||||
|
} else {
|
||||||
|
if (document.getElementsByName('system-message')[0]) {
|
||||||
|
document.getElementsByName('system-message')[0].value =
|
||||||
|
'You are a helpful chatbot';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startingChats = [];
|
||||||
|
|
||||||
|
result?.messages.forEach((m) => {
|
||||||
|
if (m[0] === 'Human') {
|
||||||
|
startingChats.push({ t: m[1], user: 'human', key: Math.random() });
|
||||||
|
} else {
|
||||||
|
startingChats.push({ t: m[1], user: 'bot', key: Math.random() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// We will ignore the FastChat starting chats for now. If you uncomment
|
||||||
|
// the following line, you will see a starting conversation.
|
||||||
|
//setChats(startingChats);
|
||||||
|
|
||||||
|
scrollChatToBottom();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!currentModel) return;
|
||||||
|
|
||||||
|
asyncTasks();
|
||||||
|
}, [currentModel, adaptor, experimentInfo?.config?.prompt_template]);
|
||||||
|
|
||||||
|
const sendNewMessageToLLM = async (text: String) => {
|
||||||
|
const r = Math.floor(Math.random() * 1000000);
|
||||||
|
|
||||||
|
// Create a new chat for the user's message
|
||||||
|
var newChats = [...chats, { t: text, user: 'human', key: r }];
|
||||||
|
|
||||||
|
// Add Message to Chat Array:
|
||||||
|
setChats((c) => [...c, { t: text, user: 'human', key: r }]);
|
||||||
|
scrollChatToBottom();
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setIsThinking(true);
|
||||||
|
|
||||||
|
scrollChatToBottom();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
const systemMessage =
|
||||||
|
document.getElementsByName('system-message')[0]?.value;
|
||||||
|
|
||||||
|
// Get a list of all the existing chats so we can send them to the LLM
|
||||||
|
let texts = chats.map((c) => {
|
||||||
|
return {
|
||||||
|
role: c.user === 'bot' ? 'user' : 'assistant',
|
||||||
|
content: c.t ? c.t : '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the user's message
|
||||||
|
texts.push({ role: 'user', content: text });
|
||||||
|
|
||||||
|
// Send them over
|
||||||
|
const result = await chatAPI.sendAndReceiveStreaming(
|
||||||
|
currentModel,
|
||||||
|
adaptor,
|
||||||
|
texts,
|
||||||
|
temperature,
|
||||||
|
maxTokens,
|
||||||
|
topP,
|
||||||
|
frequencyPenalty,
|
||||||
|
systemMessage
|
||||||
|
);
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
setIsThinking(false);
|
||||||
|
// Add Response to Chat Array:
|
||||||
|
|
||||||
|
newChats = [...newChats, { t: result?.text, user: 'bot', key: result?.id }];
|
||||||
|
|
||||||
|
setChats((c) => [...c, { t: result?.text, user: 'bot', key: result?.id }]);
|
||||||
|
|
||||||
|
var cid = conversationId;
|
||||||
|
const experimentId = experimentInfo?.id;
|
||||||
|
|
||||||
|
if (cid == null) {
|
||||||
|
cid = Math.random().toString(36).substring(7);
|
||||||
|
setConversationId(cid);
|
||||||
|
}
|
||||||
|
|
||||||
|
//save the conversation to the server
|
||||||
|
fetch(chatAPI.Endpoints.Experiment.SaveConversation(experimentId), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
conversation_id: cid,
|
||||||
|
conversation: JSON.stringify(newChats),
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
conversationsMutate();
|
||||||
|
});
|
||||||
|
|
||||||
|
scrollChatToBottom();
|
||||||
|
|
||||||
|
return result?.text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all conversations for this experiment
|
||||||
|
const {
|
||||||
|
data: conversations,
|
||||||
|
error: conversationsError,
|
||||||
|
isLoading: conversationsIsLoading,
|
||||||
|
mutate: conversationsMutate,
|
||||||
|
} = useSWR(
|
||||||
|
chatAPI.Endpoints.Experiment.GetConversations(experimentInfo?.id),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendCompletionToLLM = async (element) => {
|
||||||
|
const text = element.value;
|
||||||
|
|
||||||
|
setIsThinking(true);
|
||||||
|
|
||||||
|
var inferenceParams = '';
|
||||||
|
|
||||||
|
if (experimentInfo?.config?.inferenceParams) {
|
||||||
|
inferenceParams = experimentInfo?.config?.inferenceParams;
|
||||||
|
inferenceParams = JSON.parse(inferenceParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(inferenceParams);
|
||||||
|
|
||||||
|
const isVLLMInferenceEngine =
|
||||||
|
inferenceParams?.inferenceEngine === 'vllm_server';
|
||||||
|
|
||||||
|
console.log('WE ARE USING VLLM SERVER: ', isVLLMInferenceEngine);
|
||||||
|
|
||||||
|
const result = await chatAPI.sendCompletion(
|
||||||
|
currentModel,
|
||||||
|
adaptor,
|
||||||
|
text,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
isVLLMInferenceEngine !== null
|
||||||
|
);
|
||||||
|
setIsThinking(false);
|
||||||
|
|
||||||
|
if (result?.text) element.value += result.text;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function countTokens() {
|
||||||
|
var count = await chatAPI.countTokens(currentModel, [debouncedText]);
|
||||||
|
setTokenCount(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function countChatTokens() {
|
||||||
|
const systemMessage =
|
||||||
|
document.getElementsByName('system-message')[0]?.value;
|
||||||
|
|
||||||
|
let texts = chats.map((c) => {
|
||||||
|
return {
|
||||||
|
role: c.user === 'human' ? 'user' : 'assistant',
|
||||||
|
content: c.t ? c.t : '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
texts.push({ role: 'user', content: debouncedText });
|
||||||
|
|
||||||
|
var count = await chatAPI.countChatTokens(currentModel, texts);
|
||||||
|
|
||||||
|
setTokenCount(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!experimentInfo) return 'Select an Experiment';
|
||||||
|
if (!models?.[0]?.id) return 'No Model is Running';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sheet
|
||||||
|
id="interact-page"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
height: '100%',
|
||||||
|
paddingBottom: 4,
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <pre>{JSON.stringify(chats, null, 2)}</pre> */}
|
||||||
|
{mode === 'chat' && (
|
||||||
|
<ChatPage
|
||||||
|
key={conversationId}
|
||||||
|
chats={chats}
|
||||||
|
setChats={setChats}
|
||||||
|
templateTextIsEditable={templateTextIsEditable}
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
isThinking={isThinking}
|
||||||
|
sendNewMessageToLLM={sendNewMessageToLLM}
|
||||||
|
experimentInfoMutate={experimentInfoMutate}
|
||||||
|
setTemplateTextIsEditable={setTemplateTextIsEditable}
|
||||||
|
tokenCount={tokenCount}
|
||||||
|
text={textToDebounce}
|
||||||
|
debouncedText={debouncedText}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{mode === 'completions' && (
|
||||||
|
<CompletionsPage
|
||||||
|
text={text}
|
||||||
|
setText={setText}
|
||||||
|
debouncedText={debouncedText}
|
||||||
|
tokenCount={tokenCount}
|
||||||
|
isThinking={isThinking}
|
||||||
|
sendCompletionToLLM={sendCompletionToLLM}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
id="right-hand-panel-of-chat-page"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 1,
|
||||||
|
flex: '0 0 300px',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sheet
|
||||||
|
id="chat-settings-on-right"
|
||||||
|
variant="plain"
|
||||||
|
sx={{
|
||||||
|
// borderRadius: "md",
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flex: '1 1 50%',
|
||||||
|
xpadding: 2,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
overflow: 'hidden',
|
||||||
|
// border: '4px solid green',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography level="h2" fontSize="lg" id="card-description" mb={3}>
|
||||||
|
{currentModel} - {adaptor}
|
||||||
|
</Typography>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Mode:</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
orientation="horizontal"
|
||||||
|
aria-labelledby="segmented-controls-example"
|
||||||
|
name="mode"
|
||||||
|
value={mode}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setMode(event.target.value)
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
minHeight: 48,
|
||||||
|
padding: '4px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
bgcolor: 'neutral.softBg',
|
||||||
|
'--RadioGroup-gap': '4px',
|
||||||
|
'--Radio-actionRadius': '8px',
|
||||||
|
justifyContent: 'space-evenly',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{['chat', 'completions'].map((item) => (
|
||||||
|
<Radio
|
||||||
|
key={item}
|
||||||
|
color="neutral"
|
||||||
|
value={item}
|
||||||
|
disableIcon
|
||||||
|
label={item}
|
||||||
|
variant="plain"
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexGrow: 1,
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
label: { style: { textAlign: 'center' } },
|
||||||
|
action: ({ checked }) => ({
|
||||||
|
sx: {
|
||||||
|
...(checked && {
|
||||||
|
bgcolor: 'background.surface',
|
||||||
|
boxShadow: 'sm',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'background.surface',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<Box sx={{ overflow: 'auto', width: '100%', padding: 3 }}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>
|
||||||
|
Temperature
|
||||||
|
<span style={{ color: '#aaa' }}>{temperature}</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Slider
|
||||||
|
sx={{ margin: 'auto', width: '90%' }}
|
||||||
|
value={temperature}
|
||||||
|
onChange={(event: Event, newValue: number | number[]) => {
|
||||||
|
setTemperature(newValue as number);
|
||||||
|
}}
|
||||||
|
max={2}
|
||||||
|
min={0}
|
||||||
|
step={0.01}
|
||||||
|
valueLabelDisplay="auto"
|
||||||
|
/>
|
||||||
|
{/* <FormHelperText>This is a helper text.</FormHelperText> */}
|
||||||
|
<FormLabel>
|
||||||
|
Maximum Length
|
||||||
|
<span style={{ color: '#aaa' }}>{maxTokens}</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Slider
|
||||||
|
sx={{ margin: 'auto', width: '90%' }}
|
||||||
|
defaultValue={64}
|
||||||
|
min={0}
|
||||||
|
max={256 * 4}
|
||||||
|
value={maxTokens}
|
||||||
|
onChange={(event: Event, newValue: number | number[]) => {
|
||||||
|
setMaxTokens(newValue as number);
|
||||||
|
}}
|
||||||
|
valueLabelDisplay="auto"
|
||||||
|
/>
|
||||||
|
<FormLabel>
|
||||||
|
Top P
|
||||||
|
<span style={{ color: '#aaa' }}>{topP}</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Slider
|
||||||
|
sx={{ margin: 'auto', width: '90%' }}
|
||||||
|
value={topP}
|
||||||
|
onChange={(event: Event, newValue: number | number[]) => {
|
||||||
|
setTopP(newValue as number);
|
||||||
|
}}
|
||||||
|
defaultValue={1.0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
valueLabelDisplay="auto"
|
||||||
|
/>
|
||||||
|
<FormLabel>
|
||||||
|
Frequency Penalty
|
||||||
|
<span style={{ color: '#aaa' }}>{frequencyPenalty}</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Slider
|
||||||
|
sx={{ margin: 'auto', width: '90%' }}
|
||||||
|
value={frequencyPenalty}
|
||||||
|
onChange={(event: Event, newValue: number | number[]) => {
|
||||||
|
setFrequencyPenalty(newValue as number);
|
||||||
|
}}
|
||||||
|
defaultValue={0}
|
||||||
|
max={2}
|
||||||
|
min={-2}
|
||||||
|
step={0.2}
|
||||||
|
valueLabelDisplay="auto"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<Button variant="outlined" disabled>
|
||||||
|
Other Settings
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</Sheet>
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flex: '2',
|
||||||
|
// border: '4px solid red',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'auto',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<List>
|
||||||
|
{conversationsIsLoading && <div>Loading...</div>}
|
||||||
|
{conversations &&
|
||||||
|
conversations?.map((c) => {
|
||||||
|
return (
|
||||||
|
<div key={c?.id}>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemButton
|
||||||
|
onClick={() => {
|
||||||
|
setChats(c?.contents);
|
||||||
|
setConversationId(c?.id);
|
||||||
|
}}
|
||||||
|
selected={conversationId === c?.id}
|
||||||
|
>
|
||||||
|
<ListItemDecorator>
|
||||||
|
<MessagesSquareIcon />
|
||||||
|
</ListItemDecorator>
|
||||||
|
<ListItemContent>
|
||||||
|
<Typography level="title-md">{c?.id}</Typography>
|
||||||
|
<Typography level="body-sm">
|
||||||
|
{c?.contents?.length > 0 &&
|
||||||
|
shortenArray(c?.contents, 3).map((m) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{m?.user == 'human' ? 'User' : 'Bot'}:
|
||||||
|
|
||||||
|
{truncate(m?.t, 20)}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</ListItemContent>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
fetch(
|
||||||
|
chatAPI.Endpoints.Experiment.DeleteConversation(
|
||||||
|
experimentInfo?.id,
|
||||||
|
c?.id
|
||||||
|
),
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
).then((response) => {
|
||||||
|
conversationsMutate();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
<ListDivider />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Sheet>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
onClick={() => {
|
||||||
|
setChats([]);
|
||||||
|
setConversationId(null);
|
||||||
|
conversationsMutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
New Conversation
|
||||||
|
</Button>
|
||||||
|
</Sheet>
|
||||||
|
</Box>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
src/renderer/components/Experiment/Interact/styles.css
Normal file
33
src/renderer/components/Experiment/Interact/styles.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
.chatBubble {
|
||||||
|
width: 90%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
transform-origin: 0 100%;
|
||||||
|
padding-top: 0;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
animation: message 0.15s ease-out 0s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatBubbleL {
|
||||||
|
flex-direction: row;
|
||||||
|
text-align: right;
|
||||||
|
align-self: flex-end;
|
||||||
|
transform-origin: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes message {
|
||||||
|
0% {
|
||||||
|
max-height: 100vmax;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
max-height: 100vmax;
|
||||||
|
overflow: visible;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
272
src/renderer/components/Experiment/Prompt.tsx
Normal file
272
src/renderer/components/Experiment/Prompt.tsx
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionGroup,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
LinearProgress,
|
||||||
|
Sheet,
|
||||||
|
Textarea,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
import { ListRestartIcon, PencilIcon, SaveIcon } from 'lucide-react';
|
||||||
|
import * as chatAPI from '../../lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
function resetValuesToDefaults(model) {
|
||||||
|
fetch(chatAPI.TEMPLATE_FOR_MODEL_URL(model))
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((data) => {
|
||||||
|
console.log(data);
|
||||||
|
data = JSON.parse(data);
|
||||||
|
document.getElementsByName('system_message')[0].value =
|
||||||
|
data?.system_message;
|
||||||
|
document.getElementsByName('system_template')[0].value =
|
||||||
|
data?.system_template;
|
||||||
|
document.getElementsByName('human')[0].value = data?.roles[0];
|
||||||
|
document.getElementsByName('bot')[0].value = data?.roles[1];
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((err) => console.log(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Prompt({
|
||||||
|
experimentId,
|
||||||
|
experimentInfo,
|
||||||
|
experimentInfoMutate,
|
||||||
|
}) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useSWR(
|
||||||
|
chatAPI.TEMPLATE_FOR_MODEL_URL(experimentInfo?.config?.foundation),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsedPromptData = experimentInfo?.config?.prompt_template;
|
||||||
|
const model = experimentInfo?.config?.foundation;
|
||||||
|
|
||||||
|
if (experimentId === '') {
|
||||||
|
return <div>Select an Experiment</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a hack: it reloads and resets the form when the component is loaded
|
||||||
|
document.getElementById('prompt-form')?.reset();
|
||||||
|
|
||||||
|
if (isLoading) return <LinearProgress />;
|
||||||
|
if (error) return <div>Failed to load prompt data from API</div>;
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
overflowY: 'auto',
|
||||||
|
xoverflowX: 'hidden',
|
||||||
|
padding: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
id="prompt-form"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
const formJson = Object.fromEntries((formData as any).entries());
|
||||||
|
// alert(JSON.stringify(formJson));
|
||||||
|
fetch(chatAPI.SAVE_EXPERIMENT_PROMPT_URL(experimentId), {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(formJson),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
experimentInfoMutate();
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then((data) => console.log(data))
|
||||||
|
.catch((err) => console.log(err));
|
||||||
|
|
||||||
|
setIsEditing(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography level="h1" paddingTop={0} paddingBottom={1}>
|
||||||
|
Prompt
|
||||||
|
</Typography>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: '10px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!isEditing ? (
|
||||||
|
<Button
|
||||||
|
startDecorator={<PencilIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditing(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startDecorator={<ListRestartIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
resetValuesToDefaults(model);
|
||||||
|
}}
|
||||||
|
color="danger"
|
||||||
|
>
|
||||||
|
Reset to Defaults {model && 'for'} {model}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{isEditing && (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
startDecorator={<SaveIcon />}
|
||||||
|
color="success"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>System Message:</FormLabel>
|
||||||
|
<Textarea
|
||||||
|
defaultValue={parsedPromptData?.system_message}
|
||||||
|
disabled={!isEditing}
|
||||||
|
variant="outlined"
|
||||||
|
name="system_message"
|
||||||
|
minRows={4}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
This text is prepended to the start of a conversation and serves
|
||||||
|
as a hidden intruction to the model.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<br />
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Template:</FormLabel>
|
||||||
|
<Textarea
|
||||||
|
placeholder="{system_message}"
|
||||||
|
variant="outlined"
|
||||||
|
defaultValue={parsedPromptData?.system_template}
|
||||||
|
name="system_template"
|
||||||
|
minRows={5}
|
||||||
|
disabled={!isEditing}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
Use this template to format how to send all data to the model. Use
|
||||||
|
curly braces to refer to available fields.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<AccordionGroup variant="outlined" size="lg" sx={{ marginTop: 3 }}>
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary>Conversation Format</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Human:</FormLabel>
|
||||||
|
<Input name="human" defaultValue={data?.roles[0]} />
|
||||||
|
<FormHelperText>
|
||||||
|
Within a chat template, refer to the human by this name.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<br />
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Agent:</FormLabel>
|
||||||
|
<Input name="bot" defaultValue={data?.roles[1]} />
|
||||||
|
<FormHelperText>
|
||||||
|
Within a chat template, refer to the Agent by this name.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<br />
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel> Chat Format:</FormLabel>
|
||||||
|
<Textarea
|
||||||
|
placeholder={JSON.stringify(data?.messages)}
|
||||||
|
variant="outlined"
|
||||||
|
name="messages"
|
||||||
|
minRows={8}
|
||||||
|
endDecorator={
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 'var(--Textarea-paddingBlock)',
|
||||||
|
pt: 'var(--Textarea-paddingBlock)',
|
||||||
|
borderTop: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
flex: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button sx={{ ml: 'auto' }} color="neutral">
|
||||||
|
Save{' '}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>{' '}
|
||||||
|
<FormHelperText>
|
||||||
|
Within a chat template, refer to the human by this name.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary>Other</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Offset:</FormLabel>
|
||||||
|
<Input value={data?.offset} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Separation Style:</FormLabel>
|
||||||
|
<Input value={data?.sep_style} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Separator Style:</FormLabel>
|
||||||
|
<Input value={data?.sep_style} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Separator 1:</FormLabel>
|
||||||
|
<Input value={JSON.stringify(data?.sep)} />
|
||||||
|
<FormHelperText>
|
||||||
|
Surround with double quotations marks (they will not be
|
||||||
|
included)
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Separator 2:</FormLabel>
|
||||||
|
<Input value={JSON.stringify(data?.sep2)} />
|
||||||
|
<FormHelperText>
|
||||||
|
Surround with double quotations marks (they will not be
|
||||||
|
included)
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Stop String:</FormLabel>
|
||||||
|
<Input value={JSON.stringify(data?.stop_str)} />
|
||||||
|
</FormControl>
|
||||||
|
<FormHelperText>
|
||||||
|
Surround with double quotations marks (they will not be
|
||||||
|
included)
|
||||||
|
</FormHelperText>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Stop Token IDs:</FormLabel>
|
||||||
|
<Input value={data?.stop_token_ids} />
|
||||||
|
</FormControl>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/renderer/components/Experiment/SelectButton.tsx
Normal file
36
src/renderer/components/Experiment/SelectButton.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { Button, CircularProgress } from '@mui/joy';
|
||||||
|
|
||||||
|
export default function SelectButton({ setFoundation, model, setAdaptor }) {
|
||||||
|
const [selected, setSelected] = React.useState(false);
|
||||||
|
|
||||||
|
const name = model.id;
|
||||||
|
|
||||||
|
return selected ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="soft"
|
||||||
|
onClick={() => {
|
||||||
|
setSelected(false);
|
||||||
|
}}
|
||||||
|
startDecorator={<CircularProgress thickness={2} />}
|
||||||
|
>
|
||||||
|
Loading Model
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="soft"
|
||||||
|
color="success"
|
||||||
|
onClick={() => {
|
||||||
|
setSelected(true);
|
||||||
|
setFoundation(model);
|
||||||
|
setAdaptor('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
203
src/renderer/components/Experiment/SelectExperimentMenu.tsx
Normal file
203
src/renderer/components/Experiment/SelectExperimentMenu.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import Button from '@mui/joy/Button';
|
||||||
|
import Menu from '@mui/joy/Menu';
|
||||||
|
import MenuItem from '@mui/joy/MenuItem';
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
PlusCircleIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
XSquareIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
ListItemDecorator,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
Divider,
|
||||||
|
Dropdown,
|
||||||
|
MenuButton,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mui/joy';
|
||||||
|
import { useState, useEffect, MouseEvent, FormEvent } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function SelectExperimentMenu({
|
||||||
|
experimentInfo,
|
||||||
|
setExperimentId,
|
||||||
|
models,
|
||||||
|
}) {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// This gets all the available experiments
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
chatAPI.GET_EXPERIMENTS_URL(),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mutate();
|
||||||
|
}, [experimentInfo]);
|
||||||
|
|
||||||
|
const createHandleClose = (id: string) => () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
setExperimentId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel
|
||||||
|
sx={{
|
||||||
|
paddingLeft: '6px',
|
||||||
|
color: 'var(--joy-palette-neutral-plainColor)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Experiment:
|
||||||
|
</FormLabel>
|
||||||
|
<Dropdown>
|
||||||
|
{models?.length > 0 ? (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Experiment is locked while LLM is running.
|
||||||
|
<br />
|
||||||
|
Press stop <StopCircleIcon size="16px" /> first.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
variant="soft"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'transparent !important',
|
||||||
|
fontSize: '22px',
|
||||||
|
color: '#999',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{experimentInfo?.name || 'Select'}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: 'right',
|
||||||
|
display: 'inline-flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<MenuButton
|
||||||
|
variant="plain"
|
||||||
|
sx={{
|
||||||
|
fontSize: '22px',
|
||||||
|
backgroundColor: 'transparent !important',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{experimentInfo?.name || 'Select'}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: 'right',
|
||||||
|
display: 'inline-flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</span>
|
||||||
|
</MenuButton>
|
||||||
|
)}
|
||||||
|
<Menu className="select-experiment-menu">
|
||||||
|
{data &&
|
||||||
|
data.map((experiment: any) => {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
selected={experimentInfo?.name === experiment.name}
|
||||||
|
variant={
|
||||||
|
experimentInfo?.name === experiment.name
|
||||||
|
? 'soft'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onClick={createHandleClose(experiment.id)}
|
||||||
|
key={experiment.id}
|
||||||
|
sx={{ display: 'flex', width: '170px' }}
|
||||||
|
>
|
||||||
|
{experiment.name}
|
||||||
|
|
||||||
|
{/* <Typography level="body2" textColor="neutral.300" ml="auto">
|
||||||
|
<XSquareIcon size="20px" onClick={() => alert('del')} />
|
||||||
|
</Typography> */}
|
||||||
|
{experimentInfo?.name === experiment.name && (
|
||||||
|
<CheckIcon style={{ marginLeft: 'auto' }} />
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Divider />
|
||||||
|
<MenuItem onClick={() => setModalOpen(true)}>
|
||||||
|
<ListItemDecorator>
|
||||||
|
<PlusCircleIcon />
|
||||||
|
</ListItemDecorator>
|
||||||
|
New
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</FormControl>
|
||||||
|
<Modal open={modalOpen} onClose={() => setModalOpen(false)}>
|
||||||
|
<ModalDialog
|
||||||
|
aria-labelledby="basic-modal-dialog-title"
|
||||||
|
aria-describedby="basic-modal-dialog-description"
|
||||||
|
sx={{ maxWidth: 500 }}
|
||||||
|
>
|
||||||
|
<Typography id="basic-modal-dialog-title" component="h2">
|
||||||
|
Create new experiment
|
||||||
|
</Typography>
|
||||||
|
{/* <Typography
|
||||||
|
id="basic-modal-dialog-description"
|
||||||
|
textColor="text.tertiary"
|
||||||
|
>
|
||||||
|
Please supply a friendly name for your project
|
||||||
|
</Typography> */}
|
||||||
|
<form
|
||||||
|
onSubmit={async (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = new FormData(event.target);
|
||||||
|
// const formJson = Object.fromEntries((formData as any).entries());
|
||||||
|
// alert(JSON.stringify(formJson));
|
||||||
|
const name = form.get('name');
|
||||||
|
const response = await fetch(chatAPI.CREATE_EXPERIMENT_URL(name));
|
||||||
|
const newId = await response.json();
|
||||||
|
setExperimentId(newId);
|
||||||
|
createHandleClose(newId);
|
||||||
|
mutate();
|
||||||
|
setModalOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Experiment Name</FormLabel>
|
||||||
|
<Input name="name" autoFocus required />
|
||||||
|
</FormControl>
|
||||||
|
{/* <FormControl>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<Input required />
|
||||||
|
</FormControl> */}
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
<Button variant="soft" onClick={() => setModalOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
193
src/renderer/components/Experiment/Settings.tsx
Normal file
193
src/renderer/components/Experiment/Settings.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Divider,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
Typography,
|
||||||
|
Option,
|
||||||
|
ChipDelete,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
import { PlusCircleIcon, XCircleIcon, XIcon } from 'lucide-react';
|
||||||
|
import { useState, FormEvent } from 'react';
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
function AddPluginToExperimentModal({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
experimentInfo,
|
||||||
|
experimentInfoMutate,
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
data: pluginsData,
|
||||||
|
error: pluginsIsError,
|
||||||
|
isLoading: pluginsIsLoading,
|
||||||
|
} = useSWR(
|
||||||
|
chatAPI.Endpoints.Experiment.ListScripts(experimentInfo?.id),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!experimentInfo?.id) {
|
||||||
|
return 'No experiment selected.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalDialog sx={{ width: '30vw' }}>
|
||||||
|
<DialogTitle>Add Plugin to {experimentInfo?.name}</DialogTitle>
|
||||||
|
<form
|
||||||
|
onSubmit={async (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
const pluginName = formData.get('plugin_name');
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
chatAPI.Endpoints.Experiment.InstallPlugin(
|
||||||
|
experimentInfo?.id,
|
||||||
|
pluginName
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data?.error) {
|
||||||
|
alert(data?.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
experimentInfoMutate();
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<Select
|
||||||
|
required
|
||||||
|
placeholder="Select Script"
|
||||||
|
variant="outlined"
|
||||||
|
size="lg"
|
||||||
|
name="plugin_name"
|
||||||
|
>
|
||||||
|
{pluginsData?.map((row) => (
|
||||||
|
<Option value={row?.uniqueId} key={row.uniqueId}>
|
||||||
|
{row.name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button type="submit">Add</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExperimentSettings({
|
||||||
|
experimentInfo,
|
||||||
|
setExperimentId,
|
||||||
|
experimentInfoMutate,
|
||||||
|
}) {
|
||||||
|
const [showJSON, setShowJSON] = useState(false);
|
||||||
|
const [showPluginsModal, setShowPluginsModal] = useState(false);
|
||||||
|
|
||||||
|
let plugins = experimentInfo?.config?.plugins;
|
||||||
|
|
||||||
|
if (!experimentInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography level="h1">Experiment Settings</Typography>
|
||||||
|
<Sheet>
|
||||||
|
<Divider sx={{ mt: 2, mb: 2 }} />
|
||||||
|
Show Experiment Details (JSON):
|
||||||
|
<Switch checked={showJSON} onChange={() => setShowJSON(!showJSON)} />
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
display: showJSON ? 'block' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{JSON.stringify(experimentInfo, null, 2)}
|
||||||
|
</pre>
|
||||||
|
<Divider sx={{ mt: 2, mb: 2 }} />
|
||||||
|
<Typography level="h2" mb={2}>
|
||||||
|
Scripts
|
||||||
|
<Button
|
||||||
|
sx={{ justifySelf: 'center' }}
|
||||||
|
variant="soft"
|
||||||
|
startDecorator={<PlusCircleIcon />}
|
||||||
|
onClick={() => setShowPluginsModal(true)}
|
||||||
|
>
|
||||||
|
Add Script
|
||||||
|
</Button>
|
||||||
|
</Typography>
|
||||||
|
{plugins &&
|
||||||
|
plugins.map((plugin) => (
|
||||||
|
<>
|
||||||
|
<Chip
|
||||||
|
color="success"
|
||||||
|
endDecorator={
|
||||||
|
<ChipDelete
|
||||||
|
onDelete={async () => {
|
||||||
|
await fetch(
|
||||||
|
chatAPI.Endpoints.Experiment.DeletePlugin(
|
||||||
|
experimentInfo?.id,
|
||||||
|
plugin
|
||||||
|
)
|
||||||
|
);
|
||||||
|
experimentInfoMutate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{plugin}
|
||||||
|
</Chip>
|
||||||
|
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
<AddPluginToExperimentModal
|
||||||
|
open={showPluginsModal}
|
||||||
|
setOpen={setShowPluginsModal}
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
experimentInfoMutate={experimentInfoMutate}
|
||||||
|
/>
|
||||||
|
<Divider sx={{ mt: 2, mb: 2 }} />
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
'Are you sure you want to delete this project? If you click on "OK" There is no way to recover it.'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
fetch(chatAPI.DELETE_EXPERIMENT_URL(experimentInfo?.id));
|
||||||
|
setExperimentId(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete Project {experimentInfo?.name}
|
||||||
|
</Button>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/renderer/components/Experiment/Train/DownloadButton.tsx
Normal file
66
src/renderer/components/Experiment/Train/DownloadButton.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Button, LinearProgress, Stack } from '@mui/joy';
|
||||||
|
import { CheckIcon, DownloadIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function DownloadButton({
|
||||||
|
initialMessage = 'Download',
|
||||||
|
completeMessage = 'Downloaded',
|
||||||
|
icon = <DownloadIcon size="15px" />,
|
||||||
|
variant = 'outlined',
|
||||||
|
action = () => {},
|
||||||
|
}) {
|
||||||
|
const [selected, setSelected] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
function incrementProgress(currentProgress: number) {
|
||||||
|
if (currentProgress >= 100) {
|
||||||
|
// setSelected(false);
|
||||||
|
} else {
|
||||||
|
setProgress(currentProgress + 10);
|
||||||
|
setTimeout(() => {
|
||||||
|
incrementProgress(currentProgress + 10);
|
||||||
|
}, 700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setSelected(true);
|
||||||
|
|
||||||
|
incrementProgress(0);
|
||||||
|
|
||||||
|
action();
|
||||||
|
}}
|
||||||
|
endDecorator={<CheckIcon />}
|
||||||
|
>
|
||||||
|
{completeMessage}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return selected ? (
|
||||||
|
<Stack spacing={2} sx={{ flex: 1 }}>
|
||||||
|
<LinearProgress determinate value={progress} />
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={variant}
|
||||||
|
color="neutral"
|
||||||
|
onClick={() => {
|
||||||
|
setSelected(true);
|
||||||
|
|
||||||
|
incrementProgress(0);
|
||||||
|
}}
|
||||||
|
endDecorator={icon}
|
||||||
|
>
|
||||||
|
{initialMessage}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Button, LinearProgress, Stack } from '@mui/joy';
|
||||||
|
|
||||||
|
import * as chatAPI from '../../../lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
export default function LoRATrainingRunButton({
|
||||||
|
initialMessage,
|
||||||
|
action = () => {},
|
||||||
|
trainingTemplateId,
|
||||||
|
jobsMutate,
|
||||||
|
experimentId,
|
||||||
|
}) {
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
// Use fetch API to call endpoint
|
||||||
|
await fetch(
|
||||||
|
chatAPI.CREATE_TRAINING_JOB_URL(trainingTemplateId, experimentId)
|
||||||
|
)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => console.log(data))
|
||||||
|
.catch((error) => console.log(error));
|
||||||
|
jobsMutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{initialMessage}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Modal, ModalDialog, ModalClose, CircularProgress } from '@mui/joy';
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function TensorboardModal({ open, setOpen }) {
|
||||||
|
const [iframeReady, setIframeReady] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
console.log('starting tensorboard');
|
||||||
|
fetcher(chatAPI.API_URL() + 'train/tensorboard/start').then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait three secondes (to give tensorboard time to start) before showing the iframe
|
||||||
|
setIframeReady(false);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIframeReady(true);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
console.log('stopping tensorboard');
|
||||||
|
fetcher(chatAPI.API_URL() + 'train/tensorboard/stop').then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalDialog
|
||||||
|
sx={{
|
||||||
|
height: '80vh',
|
||||||
|
width: '80vw',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalClose />
|
||||||
|
{iframeReady ? (
|
||||||
|
<iframe
|
||||||
|
id="tensorboard"
|
||||||
|
src={`http://pop-os:6006/`}
|
||||||
|
title="api docs"
|
||||||
|
style={{
|
||||||
|
border: '1px solid black',
|
||||||
|
display: 'flex',
|
||||||
|
flex: 99,
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CircularProgress />
|
||||||
|
Waiting for tensorboard to start...
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/renderer/components/Experiment/Train/Train.tsx
Normal file
23
src/renderer/components/Experiment/Train/Train.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
|
||||||
|
import { Button, Table, Typography } from '@mui/joy';
|
||||||
|
import { CheckIcon, FileTextIcon, PlayIcon } from 'lucide-react';
|
||||||
|
import DownloadButton from './DownloadButton';
|
||||||
|
|
||||||
|
export default function Train() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* <Typography level="h3">Chat with current Model:</Typography> */}
|
||||||
|
<Sheet>
|
||||||
|
<Typography level="h1">Train / Finetune</Typography>
|
||||||
|
|
||||||
|
<Typography color="neutral" level="body-md" mt={4}>
|
||||||
|
Full training will be implemented soon. Please use LoRA Finetuning.
|
||||||
|
</Typography>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
314
src/renderer/components/Experiment/Train/TrainLoRA.tsx
Normal file
314
src/renderer/components/Experiment/Train/TrainLoRA.tsx
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
/* eslint-disable prefer-template */
|
||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import { ReactElement, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Chip,
|
||||||
|
IconButton,
|
||||||
|
LinearProgress,
|
||||||
|
Table,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ClockIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
GraduationCapIcon,
|
||||||
|
LineChartIcon,
|
||||||
|
PlayIcon,
|
||||||
|
PlusCircleIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import TrainingModalLoRA from './TrainingModalLoRA';
|
||||||
|
import * as chatAPI from '../../../lib/transformerlab-api-sdk';
|
||||||
|
import LoRATrainingRunButton from './LoRATrainingRunButton';
|
||||||
|
import TensorboardModal from './TensorboardModal';
|
||||||
|
import ViewOutputModal from './ViewOutputModal';
|
||||||
|
|
||||||
|
function formatTemplateConfig(config): ReactElement {
|
||||||
|
const c = JSON.parse(config);
|
||||||
|
|
||||||
|
const r = (
|
||||||
|
<>
|
||||||
|
<b>Model:</b> {c.model_name}
|
||||||
|
<br />
|
||||||
|
<b>Plugin:</b> {c.plugin_name} <br />
|
||||||
|
<b>Dataset:</b> {c.dataset_name} <br />
|
||||||
|
<b>Adaptor:</b> {c.adaptor_name} <br />
|
||||||
|
{/* {JSON.stringify(c)} */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jobChipColor(status: string): string {
|
||||||
|
if (status === 'COMPLETE') return 'success';
|
||||||
|
if (status === 'QUEUED') return 'warning';
|
||||||
|
if (status === 'FAILED') return 'danger';
|
||||||
|
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatJobConfig(c): ReactElement {
|
||||||
|
const r = (
|
||||||
|
<>
|
||||||
|
{/* {JSON.stringify(c)} */}
|
||||||
|
<b>Template ID:</b> {c.template_name}
|
||||||
|
<br />
|
||||||
|
<b>Model Name:</b> {c.model_name}
|
||||||
|
<br />
|
||||||
|
<b>Dataset Name:</b> {c.dataset_name}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function TrainLoRA({ experimentInfo }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [openTensorboardModal, setOpenTensorboardModal] = useState(false);
|
||||||
|
const [viewOutputFromJob, setViewOutputFromJob] = useState(-1);
|
||||||
|
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
chatAPI.GET_TRAINING_TEMPLATE_URL(),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: jobs,
|
||||||
|
error: jobsError,
|
||||||
|
isLoading: jobsIsLoading,
|
||||||
|
mutate: jobsMutate,
|
||||||
|
} = useSWR(chatAPI.API_URL() + 'train/jobs', fetcher, {
|
||||||
|
refreshInterval: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!experimentInfo) {
|
||||||
|
return 'No experiment selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TrainingModalLoRA
|
||||||
|
open={open}
|
||||||
|
onClose={() => {
|
||||||
|
setOpen(false);
|
||||||
|
mutate();
|
||||||
|
}}
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
/>
|
||||||
|
<TensorboardModal
|
||||||
|
open={openTensorboardModal}
|
||||||
|
setOpen={setOpenTensorboardModal}
|
||||||
|
/>
|
||||||
|
<ViewOutputModal
|
||||||
|
jobId={viewOutputFromJob}
|
||||||
|
setJobId={setViewOutputFromJob}
|
||||||
|
/>
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography level="h1">Train</Typography>
|
||||||
|
<Typography level="h2" mb={2} startDecorator={<GraduationCapIcon />}>
|
||||||
|
Training Templates
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
startDecorator={<PlusCircleIcon />}
|
||||||
|
sx={{ width: 'fit-content' }}
|
||||||
|
>
|
||||||
|
New Training Template
|
||||||
|
</Button>
|
||||||
|
<Sheet
|
||||||
|
color="neutral"
|
||||||
|
variant="soft"
|
||||||
|
sx={{ p: 4, mt: 1, mb: 2, flex: 1, height: '100%', overflow: 'auto' }}
|
||||||
|
>
|
||||||
|
<Table>
|
||||||
|
<thead>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Dataset</th>
|
||||||
|
<th>Data</th>
|
||||||
|
<th style={{ textAlign: 'right' }}> </th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{isLoading && (
|
||||||
|
<tr>
|
||||||
|
<td>loading...</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<tr>
|
||||||
|
<td>error...</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{data &&
|
||||||
|
data?.map((row) => {
|
||||||
|
return (
|
||||||
|
<tr key={row[0]}>
|
||||||
|
<td>
|
||||||
|
<Typography level="title-sm">{row[1]}</Typography>
|
||||||
|
</td>
|
||||||
|
<td>{row[2]}</td>
|
||||||
|
<td>
|
||||||
|
{row[4]} <FileTextIcon size={14} />
|
||||||
|
</td>
|
||||||
|
<td style={{ overflow: 'clip' }}>
|
||||||
|
{formatTemplateConfig(row[5])}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 2,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LoRATrainingRunButton
|
||||||
|
initialMessage="Queue"
|
||||||
|
trainingTemplateId={row[0]}
|
||||||
|
jobsMutate={jobsMutate}
|
||||||
|
experimentId={experimentInfo?.id}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => {
|
||||||
|
await fetch(
|
||||||
|
chatAPI.API_URL() +
|
||||||
|
'train/template/' +
|
||||||
|
row[0] +
|
||||||
|
'/delete'
|
||||||
|
);
|
||||||
|
mutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2Icon />
|
||||||
|
</IconButton>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Sheet>
|
||||||
|
<Typography level="h2" startDecorator={<ClockIcon />} mb={2}>
|
||||||
|
Queued Training Jobs
|
||||||
|
</Typography>
|
||||||
|
{/* <pre>{JSON.stringify(jobs, '\n', 2)}</pre> */}
|
||||||
|
{/* <Typography level="body2">
|
||||||
|
Current Foundation: {experimentInfo?.config?.foundation}
|
||||||
|
</Typography> */}
|
||||||
|
<ButtonGroup variant="soft">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
fetch(chatAPI.API_URL() + 'train/job/start_next');
|
||||||
|
}}
|
||||||
|
startDecorator={<PlayIcon />}
|
||||||
|
>
|
||||||
|
Start next Job
|
||||||
|
</Button>
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
startDecorator={<Trash2Icon />}
|
||||||
|
onClick={() => {
|
||||||
|
fetch(chatAPI.API_URL() + 'train/job/delete_all');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete all Jobs
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<Sheet
|
||||||
|
color="warning"
|
||||||
|
variant="soft"
|
||||||
|
sx={{ p: 4, mt: 1, mb: 4, flex: 1, overflow: 'auto' }}
|
||||||
|
>
|
||||||
|
{/* <pre>{JSON.stringify(jobs, '\n', 2)}</pre> */}
|
||||||
|
<Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Details</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody style={{ overflow: 'auto', height: '100%' }}>
|
||||||
|
{jobs?.map((job) => {
|
||||||
|
return (
|
||||||
|
<tr key={job.id}>
|
||||||
|
<td>
|
||||||
|
{/* {JSON.stringify(job)} */}
|
||||||
|
<b>{job.id}-</b> {job.type}
|
||||||
|
</td>
|
||||||
|
<td>{formatJobConfig(job.config)}</td>
|
||||||
|
<td>
|
||||||
|
<Chip color={jobChipColor(job.status)}>
|
||||||
|
{job.status}
|
||||||
|
{job.progress == '-1' ? '' : ' - ' + job.progress + '%'}
|
||||||
|
</Chip>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<LinearProgress determinate value={job.progress} />
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 2,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setOpenTensorboardModal(true);
|
||||||
|
}}
|
||||||
|
startDecorator={<LineChartIcon />}
|
||||||
|
>
|
||||||
|
Tensorboard
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setViewOutputFromJob(job?.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Output
|
||||||
|
</Button>
|
||||||
|
<IconButton variant="soft">
|
||||||
|
<Trash2Icon
|
||||||
|
onClick={async () => {
|
||||||
|
await fetch(
|
||||||
|
chatAPI.API_URL() + 'train/job/delete/' + job.id
|
||||||
|
);
|
||||||
|
jobsMutate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Sheet>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
462
src/renderer/components/Experiment/Train/TrainingModalLoRA.tsx
Normal file
462
src/renderer/components/Experiment/Train/TrainingModalLoRA.tsx
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
import { useState, FormEvent } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
Select,
|
||||||
|
Option,
|
||||||
|
Slider,
|
||||||
|
Stack,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs,
|
||||||
|
Textarea,
|
||||||
|
Typography,
|
||||||
|
Chip,
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
Sheet,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
const DefaultLoraConfig = {
|
||||||
|
model_max_length: 2048,
|
||||||
|
num_train_epochs: 3,
|
||||||
|
learning_rate: 1e-3,
|
||||||
|
lora_r: 8,
|
||||||
|
lora_alpha: 16,
|
||||||
|
lora_dropout: 0.05,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function TrainingModalLoRA({ open, onClose, experimentInfo }) {
|
||||||
|
// Store the current selected Dataset in this modal
|
||||||
|
const [selectedDataset, setSelectedDataset] = useState(null);
|
||||||
|
const [config, setConfig] = useState(DefaultLoraConfig);
|
||||||
|
|
||||||
|
// Fetch available datasets from the API
|
||||||
|
const {
|
||||||
|
data: datasets,
|
||||||
|
error: datasetsError,
|
||||||
|
isLoading: datasetsIsLoading,
|
||||||
|
} = useSWR(chatAPI.Endpoints.Dataset.LocalList(), fetcher);
|
||||||
|
|
||||||
|
//Fetch available training plugins
|
||||||
|
const {
|
||||||
|
data: pluginsData,
|
||||||
|
error: pluginsIsError,
|
||||||
|
isLoading: pluginsIsLoading,
|
||||||
|
} = useSWR(
|
||||||
|
chatAPI.Endpoints.Experiment.ListScriptsOfType(
|
||||||
|
experimentInfo?.id,
|
||||||
|
'training', // type
|
||||||
|
'model_architectures:' +
|
||||||
|
experimentInfo?.config?.foundation_model_architecture //filter
|
||||||
|
),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
// Once you have a dataset selected, we use SWR's dependency mode to fetch the
|
||||||
|
// Dataset's info. Note how useSWR is declared as a function -- this is is how
|
||||||
|
// the dependency works. If selectedDataset errors, the fetcher knows to not run.
|
||||||
|
const { data: currentDatasetInfo, isLoading: currentDatasetInfoIsLoading } =
|
||||||
|
useSWR(() => {
|
||||||
|
if (selectedDataset === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return chatAPI.Endpoints.Dataset.Info(selectedDataset);
|
||||||
|
}, fetcher);
|
||||||
|
|
||||||
|
const currentModelName = experimentInfo?.config?.foundation;
|
||||||
|
|
||||||
|
function injectIntoTemplate(key) {
|
||||||
|
// Add the key to the textbox with id "template"
|
||||||
|
const template =
|
||||||
|
document.getElementById('training-form')?.elements['template'];
|
||||||
|
|
||||||
|
if (template === undefined) return;
|
||||||
|
|
||||||
|
const cursorPosition = template.selectionStart;
|
||||||
|
const templateText = template.value;
|
||||||
|
const newText =
|
||||||
|
templateText.slice(0, cursorPosition) +
|
||||||
|
`{{${key}}}` +
|
||||||
|
templateText.slice(cursorPosition);
|
||||||
|
template.value = newText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!experimentInfo?.id) {
|
||||||
|
return 'Select an Experiment';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open}>
|
||||||
|
<ModalDialog
|
||||||
|
sx={{
|
||||||
|
width: '70vw',
|
||||||
|
transform: 'translateX(-50%)', // This undoes the default translateY that centers vertically
|
||||||
|
top: '10vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
maxHeight: '80vh',
|
||||||
|
minHeight: '70vh',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
id="training-form"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
onSubmit={(event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
const formJson = Object.fromEntries((formData as any).entries());
|
||||||
|
//alert(JSON.stringify(formJson));
|
||||||
|
chatAPI.saveTrainingTemplate(
|
||||||
|
event.currentTarget.elements['template_name'].value,
|
||||||
|
'Description',
|
||||||
|
'LoRA',
|
||||||
|
JSON.stringify(formJson)
|
||||||
|
);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
aria-label="Training Template Tabs"
|
||||||
|
defaultValue={0}
|
||||||
|
sx={{ borderRadius: 'lg' }}
|
||||||
|
>
|
||||||
|
<TabList>
|
||||||
|
<Tab>Training Data</Tab>
|
||||||
|
{/* <Tab>Training Settings</Tab> */}
|
||||||
|
<Tab>LoRA Settings</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={0} sx={{ p: 2 }} keepMounted>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Training Template Name</FormLabel>
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
placeholder="Alpaca Training Job"
|
||||||
|
name="template_name"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
Give this training recipe a unique name
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Plugin Script</FormLabel>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
placeholder={
|
||||||
|
pluginsIsLoading ? 'Loading...' : 'Select Plugin'
|
||||||
|
}
|
||||||
|
variant="soft"
|
||||||
|
size="lg"
|
||||||
|
name="plugin_name"
|
||||||
|
onChange={(e, newValue) => setSelectedDataset(newValue)}
|
||||||
|
>
|
||||||
|
{pluginsData?.map((row) => (
|
||||||
|
<Option value={row?.uniqueId} key={row.uniqueId}>
|
||||||
|
{row.name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Stack direction="row" justifyContent="space-evenly" gap={2}>
|
||||||
|
<FormControl sx={{ flex: 1 }}>
|
||||||
|
<FormLabel>Model:</FormLabel>
|
||||||
|
<Typography variant="soft">{currentModelName}</Typography>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl sx={{ flex: 1 }}>
|
||||||
|
<FormLabel>Architecture:</FormLabel>
|
||||||
|
<Typography variant="soft">
|
||||||
|
{experimentInfo?.config?.foundation_model_architecture}
|
||||||
|
</Typography>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
value={currentModelName}
|
||||||
|
name="model_name"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
value={
|
||||||
|
experimentInfo?.config?.foundation_model_architecture
|
||||||
|
}
|
||||||
|
name="model_architecture"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Dataset</FormLabel>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
placeholder={
|
||||||
|
datasetsIsLoading ? 'Loading...' : 'Select Dataset'
|
||||||
|
}
|
||||||
|
variant="soft"
|
||||||
|
size="lg"
|
||||||
|
name="dataset_name"
|
||||||
|
value={selectedDataset}
|
||||||
|
onChange={(e, newValue) => setSelectedDataset(newValue)}
|
||||||
|
>
|
||||||
|
{datasets?.map((row) => (
|
||||||
|
<Option value={row?.dataset_id} key={row.id}>
|
||||||
|
{row.dataset_id}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
{selectedDataset && (
|
||||||
|
<>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Available Fields</FormLabel>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}
|
||||||
|
>
|
||||||
|
{currentDatasetInfoIsLoading && <CircularProgress />}
|
||||||
|
{/* // For each key in the currentDatasetInfo.features object,
|
||||||
|
display it: */}
|
||||||
|
{currentDatasetInfo?.features &&
|
||||||
|
Object.keys(currentDatasetInfo?.features).map(
|
||||||
|
(key) => (
|
||||||
|
<>
|
||||||
|
<Chip
|
||||||
|
onClick={() => {
|
||||||
|
injectIntoTemplate(key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
</Chip>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{/* {selectedDataset && (
|
||||||
|
<FormHelperText>
|
||||||
|
Use the field names above, maintaining capitalization, in
|
||||||
|
the template below
|
||||||
|
</FormHelperText>
|
||||||
|
)} */}
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Template</FormLabel>
|
||||||
|
{/* I want the following to be a Textarea, not a textarea
|
||||||
|
but when we do, it gives resizeobserver error when you
|
||||||
|
switch tabs back and forth */}
|
||||||
|
<textarea
|
||||||
|
required
|
||||||
|
name="formatting_template"
|
||||||
|
id="formatting_template"
|
||||||
|
defaultValue="Instruction: $instruction \n###\n Prompt: $prompt\n###\n Generation: $generation"
|
||||||
|
rows={5}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
This describes how the data is formatted when passed to
|
||||||
|
the trainer. Use Python Standard String Templating
|
||||||
|
format. For example <br />
|
||||||
|
"Instruction: $instruction \n###\n Prompt: $prompt
|
||||||
|
\n###\n Generation: $generation"
|
||||||
|
<br />
|
||||||
|
Using the field names from above with the same
|
||||||
|
capitalization.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Adaptor Name</FormLabel>
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
placeholder="alpha-beta-gamma"
|
||||||
|
name="adaptor_name"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={1} sx={{ p: 2 }} keepMounted>
|
||||||
|
<Sheet
|
||||||
|
sx={{ maxHeight: '60vh', overflow: 'auto', display: 'flex' }}
|
||||||
|
>
|
||||||
|
<Stack gap="20px" sx={{ flex: 1, height: 'fit-content' }}>
|
||||||
|
<Typography level="h4">Training Settings</Typography>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>
|
||||||
|
Maximum Sequence Length
|
||||||
|
<span style={{ color: '#aaa' }}>
|
||||||
|
{config.model_max_length}
|
||||||
|
</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Slider
|
||||||
|
sx={{ margin: 'auto', width: '90%' }}
|
||||||
|
value={config.model_max_length}
|
||||||
|
min={0}
|
||||||
|
max={2048 * 2}
|
||||||
|
step={32}
|
||||||
|
valueLabelDisplay="off"
|
||||||
|
name="model_max_length"
|
||||||
|
onChange={(e, newValue) => {
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
model_max_length: newValue,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
Input longer than this length will be truncated. Keep
|
||||||
|
lower to save memory.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel>
|
||||||
|
Epochs
|
||||||
|
<span style={{ color: '#aaa' }}>
|
||||||
|
{config.num_train_epochs}
|
||||||
|
</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Slider
|
||||||
|
sx={{ margin: 'auto', width: '90%' }}
|
||||||
|
value={config.num_train_epochs}
|
||||||
|
min={0}
|
||||||
|
max={24}
|
||||||
|
valueLabelDisplay="off"
|
||||||
|
name="num_train_epochs"
|
||||||
|
onChange={(e, newValue) =>
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
num_train_epochs: newValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormLabel>
|
||||||
|
Learning Rate
|
||||||
|
<span style={{ color: '#aaa' }}>
|
||||||
|
{config.learning_rate.toExponential(0)}
|
||||||
|
{/* in GUI we store only
|
||||||
|
the exponent so make sure you convert when actually sending to the API */}
|
||||||
|
</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Slider
|
||||||
|
sx={{ margin: 'auto', width: '90%' }}
|
||||||
|
defaultValue={1}
|
||||||
|
min={-6}
|
||||||
|
max={6}
|
||||||
|
step={1 / 2}
|
||||||
|
name="learning_rate"
|
||||||
|
onChange={(e, newValue) => {
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
learning_rate: 10 ** newValue,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
valueLabelDisplay="off"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack gap="20px" sx={{ flex: 1, height: 'fit-content' }}>
|
||||||
|
<Typography level="h4">LoRA Settings</Typography>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>
|
||||||
|
LoRA R
|
||||||
|
<span style={{ color: '#aaa' }}>{config.lora_r}</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Slider
|
||||||
|
sx={{ margin: 'auto', width: '90%' }}
|
||||||
|
defaultValue={8}
|
||||||
|
min={4}
|
||||||
|
max={64}
|
||||||
|
step={4}
|
||||||
|
name="lora_r"
|
||||||
|
valueLabelDisplay="off"
|
||||||
|
onChange={(e, newValue) => {
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
lora_r: newValue,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
Rank of the update matrices, expressed in int. Lower rank
|
||||||
|
results in smaller update matrices with fewer trainable
|
||||||
|
parameters.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel>
|
||||||
|
LoRA Alpha
|
||||||
|
<span style={{ color: '#aaa' }}>{config.lora_alpha}</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Slider
|
||||||
|
sx={{ margin: 'auto', width: '90%' }}
|
||||||
|
defaultValue={16}
|
||||||
|
min={4}
|
||||||
|
max={64 * 2}
|
||||||
|
step={4}
|
||||||
|
name="lora_alpha"
|
||||||
|
valueLabelDisplay="off"
|
||||||
|
onChange={(e, newValue) => {
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
lora_alpha: newValue,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
LoRA scaling factor. Make it a multiple of LoRA R.
|
||||||
|
</FormHelperText>
|
||||||
|
<FormLabel>
|
||||||
|
LoRA Dropout
|
||||||
|
<span style={{ color: '#aaa' }}>{config.lora_dropout}</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Slider
|
||||||
|
sx={{ margin: 'auto', width: '90%' }}
|
||||||
|
defaultValue={0.1}
|
||||||
|
min={0.1}
|
||||||
|
max={0.9}
|
||||||
|
step={0.1}
|
||||||
|
name="lora_dropout"
|
||||||
|
valueLabelDisplay="off"
|
||||||
|
onChange={(e, newValue) => {
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
lora_dropout: newValue,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
Dropout probability of the LoRA layers
|
||||||
|
</FormHelperText>
|
||||||
|
</Stack>
|
||||||
|
</Sheet>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
<Stack spacing={2} direction="row" justifyContent="flex-end">
|
||||||
|
<Button color="danger" variant="soft" onClick={() => onClose()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="soft" type="submit">
|
||||||
|
Save Training Template
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
{/* {JSON.stringify(config, null, 2)} */}
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/renderer/components/Experiment/Train/ViewOutputModal.tsx
Normal file
48
src/renderer/components/Experiment/Train/ViewOutputModal.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import { Button, Modal, ModalClose, ModalDialog, Typography } from '@mui/joy';
|
||||||
|
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
import { Editor } from '@monaco-editor/react';
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function ViewOutputModal({ jobId, setJobId }) {
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
jobId == -1 ? null : chatAPI.Endpoints.Experiment.GetOutputFromJob(jobId),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={jobId != -1} onClose={() => setJobId(-1)}>
|
||||||
|
<ModalDialog>
|
||||||
|
<ModalClose />
|
||||||
|
<Typography>Output from {jobId}</Typography>
|
||||||
|
<Typography>
|
||||||
|
<Editor
|
||||||
|
height="80vh"
|
||||||
|
width="80vw"
|
||||||
|
defaultLanguage="text"
|
||||||
|
options={{
|
||||||
|
theme: 'vs-dark',
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
fontSize: 18,
|
||||||
|
cursorStyle: 'block',
|
||||||
|
wordWrap: 'on',
|
||||||
|
}}
|
||||||
|
value={data}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
mutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
275
src/renderer/components/Header.tsx
Normal file
275
src/renderer/components/Header.tsx
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
import { Sparklines, SparklinesLine } from 'react-sparklines';
|
||||||
|
import { Box, Button, Stack, Tooltip, Typography } from '@mui/joy';
|
||||||
|
import { useServerStats } from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Link2Icon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { formatBytes } from 'renderer/lib/utils';
|
||||||
|
import ModelCurrentlyPlayingBar from './ModelCurrentlyPlayingBar';
|
||||||
|
|
||||||
|
export default function Header({ connection, setConnection, experimentInfo }) {
|
||||||
|
const [cs, setCS] = useState({ cpu: [0], gpu: [0], mem: [0] });
|
||||||
|
const { server, isLoading, isError } = useServerStats();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (connection === '') return;
|
||||||
|
|
||||||
|
const newConnectionStats = { ...cs };
|
||||||
|
|
||||||
|
// CPU Percent:
|
||||||
|
if (server?.cpu_percent == null || Number.isNaN(server?.cpu_percent)) {
|
||||||
|
newConnectionStats.cpu.push(0);
|
||||||
|
} else {
|
||||||
|
newConnectionStats.cpu.push(server?.cpu_percent);
|
||||||
|
}
|
||||||
|
if (newConnectionStats.cpu.length > 10) {
|
||||||
|
newConnectionStats.cpu.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPU Percent:
|
||||||
|
const gpuPercent =
|
||||||
|
// eslint-disable-next-line no-unsafe-optional-chaining
|
||||||
|
(server?.gpu?.[0]?.used_memory / server?.gpu?.[0]?.total_memory) * 100;
|
||||||
|
|
||||||
|
if (Number.isNaN(gpuPercent)) {
|
||||||
|
newConnectionStats.gpu.push(0);
|
||||||
|
} else {
|
||||||
|
newConnectionStats.gpu.push(gpuPercent);
|
||||||
|
}
|
||||||
|
if (newConnectionStats.gpu.length > 10) {
|
||||||
|
newConnectionStats.gpu.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory:
|
||||||
|
if (
|
||||||
|
server?.memory?.percent == null ||
|
||||||
|
Number.isNaN(server?.memory?.percent)
|
||||||
|
) {
|
||||||
|
newConnectionStats.mem.push(0);
|
||||||
|
} else {
|
||||||
|
newConnectionStats.mem.push(server?.memory?.percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newConnectionStats.mem.length > 10) {
|
||||||
|
newConnectionStats.mem.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
setCS(newConnectionStats);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [connection, server]);
|
||||||
|
|
||||||
|
// The following effect checks if the server is returning "error"
|
||||||
|
// and if so, it resets the connection in order to force the user to
|
||||||
|
// re-connect
|
||||||
|
useEffect(() => {
|
||||||
|
if (isError) {
|
||||||
|
setConnection('');
|
||||||
|
}
|
||||||
|
}, [isError]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
gridArea: 'header',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0,
|
||||||
|
top: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 1000,
|
||||||
|
p: 0,
|
||||||
|
color: '#888',
|
||||||
|
userSelect: 'none',
|
||||||
|
backgroundColor: 'var(--joy-palette-background-level1)',
|
||||||
|
}}
|
||||||
|
className="header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="currently-playing"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--joy-palette-background-level1)',
|
||||||
|
// border: '1px solid red',
|
||||||
|
height: '100%',
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
flex: '1',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
'-webkit-app-region': 'drag',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModelCurrentlyPlayingBar experimentInfo={experimentInfo} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isError ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
height: '40px',
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
opacity: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'right',
|
||||||
|
paddingRight: 20,
|
||||||
|
paddingTop: 0,
|
||||||
|
fontSize: 15,
|
||||||
|
backgroundColor: 'var(--joy-palette-background-level1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link2Icon
|
||||||
|
size={16}
|
||||||
|
color="var(--joy-palette-danger-400)"
|
||||||
|
style={{ marginBottom: '-3px' }}
|
||||||
|
/>
|
||||||
|
Not Connected
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
height: '40px',
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
opacity: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'right',
|
||||||
|
paddingRight: 20,
|
||||||
|
paddingTop: 0,
|
||||||
|
fontSize: 15,
|
||||||
|
backgroundColor: 'var(--joy-palette-background-level1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
placement="top-end"
|
||||||
|
variant="outlined"
|
||||||
|
arrow
|
||||||
|
title={
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
maxWidth: 320,
|
||||||
|
justifyContent: 'center',
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, width: '100%', mt: 1 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
textColor="text.secondary"
|
||||||
|
fontSize="sm"
|
||||||
|
sx={{ mb: 1 }}
|
||||||
|
>
|
||||||
|
{/* {JSON.stringify(server)} */}
|
||||||
|
<Stack>
|
||||||
|
<Typography fontSize="sm">{connection}</Typography>
|
||||||
|
<Typography>
|
||||||
|
<b>OS: </b>
|
||||||
|
{server?.os_alias[0]}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
<b>CPU: </b>
|
||||||
|
{server?.cpu}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
<b>GPU: </b>
|
||||||
|
{server?.gpu[0].name}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
<b>GPU Memory: </b>
|
||||||
|
{formatBytes(server?.gpu[0].total_memory)}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color="danger"
|
||||||
|
size="small"
|
||||||
|
sx={{ m: 0, p: 1 }}
|
||||||
|
onClick={() => {
|
||||||
|
setConnection('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Diconnect
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{/* <TinyCircle size={6} /> */}
|
||||||
|
<Link2Icon
|
||||||
|
size={16}
|
||||||
|
color="var(--joy-palette-success-400)"
|
||||||
|
style={{ marginBottom: '-3px' }}
|
||||||
|
/>
|
||||||
|
Connected -
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
CPU:
|
||||||
|
<div style={{ width: '60px', textAlign: 'center' }}>
|
||||||
|
<div style={{ width: '60px', position: 'absolute', opacity: 0.6 }}>
|
||||||
|
<Sparklines height={20} width={60} data={cs.cpu}>
|
||||||
|
<SparklinesLine color="green" />
|
||||||
|
</Sparklines>
|
||||||
|
</div>
|
||||||
|
{cs.cpu[cs.cpu.length - 1]}%
|
||||||
|
</div>{' '}
|
||||||
|
RAM:{' '}
|
||||||
|
<div style={{ width: '60px', textAlign: 'center' }}>
|
||||||
|
<div style={{ width: '60px', position: 'absolute', opacity: 0.6 }}>
|
||||||
|
<Sparklines height={20} width={60} data={cs.mem} max={100}>
|
||||||
|
<SparklinesLine color="#1c8cdc" />
|
||||||
|
</Sparklines>
|
||||||
|
</div>
|
||||||
|
{Math.round(cs.mem[cs.mem.length - 1])}%
|
||||||
|
</div>
|
||||||
|
VRAM:
|
||||||
|
<div style={{ width: '60px', textAlign: 'center' }}>
|
||||||
|
<div style={{ width: '60px', position: 'absolute', opacity: 0.6 }}>
|
||||||
|
<Sparklines height={20} width={60} data={cs.gpu}>
|
||||||
|
<SparklinesLine color="red" />
|
||||||
|
</Sparklines>
|
||||||
|
</div>
|
||||||
|
{Math.round(cs.gpu[cs.gpu.length - 1])}%
|
||||||
|
</div>{' '}
|
||||||
|
<div style={{ minWidth: '80px' }}>
|
||||||
|
GPU:
|
||||||
|
{/* <div style={{ width: '60px', textAlign: 'center' }}>
|
||||||
|
<div
|
||||||
|
style={{ width: '60px', position: 'absolute', opacity: 0.6 }}
|
||||||
|
>
|
||||||
|
<Sparklines height={20} width={60} data={cs.gpu}>
|
||||||
|
<SparklinesLine color="red" />
|
||||||
|
</Sparklines>
|
||||||
|
</div>
|
||||||
|
{Math.round(cs.gpu[cs.gpu.length - 1])} %
|
||||||
|
</div>{' '} */}
|
||||||
|
{server?.gpu?.[0]?.utilization > 40 ? (
|
||||||
|
<span
|
||||||
|
style={{ backgroundColor: 'var(--joy-palette-danger-100)' }}
|
||||||
|
>
|
||||||
|
{server?.gpu?.[0]?.utilization} %
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgb(0,128,0,0.1)',
|
||||||
|
paddingRight: '3px',
|
||||||
|
paddingLeft: '3px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{server?.gpu?.[0]?.utilization} %
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/renderer/components/Logs.tsx
Normal file
8
src/renderer/components/Logs.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { Sheet } from '@mui/joy';
|
||||||
|
|
||||||
|
export default function Logs({}) {
|
||||||
|
return 'Not yet implemented';
|
||||||
|
}
|
||||||
201
src/renderer/components/MainAppPanel.tsx
Normal file
201
src/renderer/components/MainAppPanel.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
|
||||||
|
import { Routes, Route, useNavigate, redirect } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Data from './Data/Data';
|
||||||
|
import Interact from './Experiment/Interact/Interact';
|
||||||
|
import Embeddings from './Experiment/Embeddings';
|
||||||
|
import Welcome from './Welcome';
|
||||||
|
import ModelZoo from './ModelZoo/ModelZoo';
|
||||||
|
import Plugins from './Plugins/Plugins';
|
||||||
|
import PluginDetails from './Plugins/PluginDetails';
|
||||||
|
|
||||||
|
import Computer from './Computer';
|
||||||
|
import Train from './Experiment/Train/Train';
|
||||||
|
import Eval from './Experiment/Eval/Eval';
|
||||||
|
import Api from './Experiment/Api';
|
||||||
|
import Settings from './Experiment/Settings';
|
||||||
|
import ModelHome from './Experiment/ExperimentNotes';
|
||||||
|
import LocalModels from './ModelZoo/LocalModels';
|
||||||
|
import TrainLoRA from './Experiment/Train/TrainLoRA';
|
||||||
|
import Prompt from './Experiment/Prompt';
|
||||||
|
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
import ExperimentNotes from './Experiment/ExperimentNotes';
|
||||||
|
import TransformerLabSettings from './TransformerLabSettings';
|
||||||
|
import Logs from './Logs';
|
||||||
|
import FoundationHome from './Experiment/Foundation';
|
||||||
|
import LocalPlugins from './Plugins/LocalPlugins';
|
||||||
|
|
||||||
|
// This component renders the main content of the app that is shown
|
||||||
|
// On the rightmost side, regardless of what menu items are selected
|
||||||
|
// On the leftmost panel.
|
||||||
|
export default function MainAppPanel({
|
||||||
|
experimentInfo,
|
||||||
|
setExperimentId,
|
||||||
|
experimentInfoMutate,
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function setFoundation(model) {
|
||||||
|
let model_name = '';
|
||||||
|
|
||||||
|
if (model) {
|
||||||
|
model_name = model.model_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
chatAPI.GET_EXPERIMENT_UPDATE_CONFIG_URL(
|
||||||
|
experimentInfo?.id,
|
||||||
|
'foundation',
|
||||||
|
model_name
|
||||||
|
)
|
||||||
|
).then((res) => {
|
||||||
|
experimentInfoMutate();
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
chatAPI.GET_EXPERIMENT_UPDATE_CONFIG_URL(
|
||||||
|
experimentInfo?.id,
|
||||||
|
'foundation_model_architecture',
|
||||||
|
model?.json_data?.architecture
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
chatAPI.GET_EXPERIMENT_UPDATE_CONFIG_URL(
|
||||||
|
experimentInfo?.id,
|
||||||
|
'foundation_filename',
|
||||||
|
model?.json_data?.huggingface_filename
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAdaptor(name) {
|
||||||
|
fetch(
|
||||||
|
chatAPI.GET_EXPERIMENT_UPDATE_CONFIG_URL(
|
||||||
|
experimentInfo?.id,
|
||||||
|
'adaptor',
|
||||||
|
name
|
||||||
|
)
|
||||||
|
).then((res) => {
|
||||||
|
experimentInfoMutate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function experimentAddEvaluation(
|
||||||
|
pluginName: string,
|
||||||
|
localName: string,
|
||||||
|
script_template_parameters: any = {}
|
||||||
|
) {
|
||||||
|
let currentPlugins = experimentInfo?.config?.plugins;
|
||||||
|
if (!currentPlugins) {
|
||||||
|
currentPlugins = [];
|
||||||
|
} else {
|
||||||
|
currentPlugins = JSON.parse(currentPlugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
await chatAPI.EXPERIMENT_ADD_EVALUATION(
|
||||||
|
experimentInfo?.id,
|
||||||
|
localName,
|
||||||
|
pluginName,
|
||||||
|
script_template_parameters
|
||||||
|
);
|
||||||
|
experimentInfoMutate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!experimentInfo) {
|
||||||
|
redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Welcome />} />
|
||||||
|
<Route
|
||||||
|
path="/projects/notes"
|
||||||
|
element={<ExperimentNotes experimentInfo={experimentInfo} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/projects/model"
|
||||||
|
element={
|
||||||
|
<FoundationHome
|
||||||
|
pickAModelMode
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
setExperimentId={setExperimentId}
|
||||||
|
setFoundation={setFoundation}
|
||||||
|
setAdaptor={setAdaptor}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/projects/prompt"
|
||||||
|
element={
|
||||||
|
<Prompt
|
||||||
|
experimentId={experimentInfo?.id}
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
experimentInfoMutate={experimentInfoMutate}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/projects/chat"
|
||||||
|
element={
|
||||||
|
<Interact
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
experimentInfoMutate={experimentInfoMutate}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/projects/embeddings"
|
||||||
|
element={<Embeddings model_name={experimentInfo?.config?.foundation} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/projects/training"
|
||||||
|
element={<TrainLoRA experimentInfo={experimentInfo} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/projects/eval"
|
||||||
|
element={
|
||||||
|
<Eval
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
addEvaluation={experimentAddEvaluation}
|
||||||
|
experimentInfoMutate={experimentInfoMutate}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/projects/plugins"
|
||||||
|
element={<Plugins experimentInfo={experimentInfo} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/projects/plugins/:pluginName"
|
||||||
|
element={<PluginDetails experimentInfo={experimentInfo} />}
|
||||||
|
/>
|
||||||
|
<Route path="/projects/api" element={<Api />} />
|
||||||
|
<Route
|
||||||
|
path="/projects/settings"
|
||||||
|
element={
|
||||||
|
<Settings
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
setExperimentId={setExperimentId}
|
||||||
|
experimentInfoMutate={experimentInfoMutate}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/zoo"
|
||||||
|
element={<ModelZoo experimentInfo={experimentInfo} />}
|
||||||
|
/>
|
||||||
|
<Route path="/data" element={<Data />} />
|
||||||
|
<Route
|
||||||
|
path="/model-home"
|
||||||
|
element={<ModelHome experimentInfo={experimentInfo} />}
|
||||||
|
/>
|
||||||
|
<Route path="/computer" element={<Computer />} />
|
||||||
|
<Route path="/settings" element={<TransformerLabSettings />} />
|
||||||
|
<Route path="/logs" element={<Logs />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
115
src/renderer/components/ModelCurrentlyPlayingBar.tsx
Normal file
115
src/renderer/components/ModelCurrentlyPlayingBar.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { StopCircleIcon } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
killWorker,
|
||||||
|
useModelStatus,
|
||||||
|
} from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
import { Box, Button, CircularProgress, Typography } from '@mui/joy';
|
||||||
|
import TinyCircle from './Shared/TinyCircle';
|
||||||
|
|
||||||
|
export default function ModelCurrentlyPlaying({ experimentInfo }) {
|
||||||
|
const { models, isError, isLoading } = useModelStatus();
|
||||||
|
|
||||||
|
const inferenceParams = experimentInfo?.config?.inferenceParams
|
||||||
|
? JSON.parse(experimentInfo?.config?.inferenceParams)
|
||||||
|
: null;
|
||||||
|
const eightBit = inferenceParams?.['8-bit'];
|
||||||
|
const cpuOffload = inferenceParams?.['cpu-offload'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: '6px',
|
||||||
|
paddingRight: '8px',
|
||||||
|
backgroundColor: 'var(--joy-palette-background-level2)',
|
||||||
|
boxShadow: 'inset 0px 0px 4px 0px rgba(0,0,0,0.05)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <RunModelButton
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
killWorker={killWorker}
|
||||||
|
models={models}
|
||||||
|
/> */}
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
disabled
|
||||||
|
sx={{ display: models?.length > 0 ? 'none' : 'block' }}
|
||||||
|
>
|
||||||
|
<StopCircleIcon style={{ color: 'transparent' }} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
await killWorker();
|
||||||
|
}}
|
||||||
|
color="neutral"
|
||||||
|
startDecorator={null}
|
||||||
|
variant="plain"
|
||||||
|
sx={{ display: models?.length > 0 ? 'block' : 'none' }}
|
||||||
|
>
|
||||||
|
{models?.length == 0 ? (
|
||||||
|
<CircularProgress color="warning" />
|
||||||
|
) : (
|
||||||
|
<StopCircleIcon />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{models?.length > 0 ? <TinyCircle size={6} color="#51BC51" /> : ''}
|
||||||
|
{models === null ? (
|
||||||
|
<TinyCircle size={6} color="var(--joy-palette-danger-400)" />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
{isLoading ? (
|
||||||
|
<TinyCircle size={6} color="var(--joy-palette-neutral-300)" />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
sx={{
|
||||||
|
m: 0,
|
||||||
|
p: '0 8px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
className="xspin-border"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: '160px',
|
||||||
|
textWrap: 'nowrap',
|
||||||
|
margin: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{experimentInfo?.config == null &&
|
||||||
|
models?.length == 0 &&
|
||||||
|
'Select an Experiment'}
|
||||||
|
{models?.[0]?.id
|
||||||
|
? models?.[0]?.id
|
||||||
|
: experimentInfo?.config?.foundation
|
||||||
|
? experimentInfo?.config?.foundation
|
||||||
|
: 'Select Foundation'}
|
||||||
|
</span>
|
||||||
|
{models?.[0]?.id && experimentInfo?.config?.inferenceParams ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
fontSize: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{eightBit && '8-bit'}</span>
|
||||||
|
<span>{cpuOffload && 'cpu-offload'}</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
386
src/renderer/components/ModelZoo/LocalModels.tsx
Normal file
386
src/renderer/components/ModelZoo/LocalModels.tsx
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Sheet,
|
||||||
|
Table,
|
||||||
|
Typography,
|
||||||
|
Option,
|
||||||
|
Chip,
|
||||||
|
Link,
|
||||||
|
Box,
|
||||||
|
Stack,
|
||||||
|
LinearProgress,
|
||||||
|
Modal,
|
||||||
|
} from '@mui/joy';
|
||||||
|
|
||||||
|
import { Link as ReactRouterLink, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { ColorPaletteProp } from '@mui/joy/styles';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowDownIcon,
|
||||||
|
BoxesIcon,
|
||||||
|
CheckIcon,
|
||||||
|
CreativeCommonsIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
GraduationCapIcon,
|
||||||
|
InfoIcon,
|
||||||
|
PlusIcon,
|
||||||
|
SearchIcon,
|
||||||
|
StoreIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import SelectButton from '../Experiment/SelectButton';
|
||||||
|
import CurrentFoundationInfo from '../Experiment/Foundation/CurrentFoundationInfo';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import * as chatAPI from '../../lib/transformerlab-api-sdk';
|
||||||
|
import Welcome from '../Welcome';
|
||||||
|
|
||||||
|
type Order = 'asc' | 'desc';
|
||||||
|
|
||||||
|
function convertModelObjectToArray(modelObject) {
|
||||||
|
// The model object in the storage is big object,
|
||||||
|
// Here we turn that into an array of objects
|
||||||
|
|
||||||
|
const arr = [{}];
|
||||||
|
const keys = Object.keys(modelObject);
|
||||||
|
|
||||||
|
for (let i = 0, n = keys.length; i < n; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
arr[i] = modelObject[key];
|
||||||
|
arr[i].name = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModelFolderInFilesystem() {
|
||||||
|
//window.filesys.openModelFolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function LocalModels({
|
||||||
|
pickAModelMode = false,
|
||||||
|
experimentInfo,
|
||||||
|
setFoundation = (name: string) => {},
|
||||||
|
setAdaptor = (name: string) => {},
|
||||||
|
}) {
|
||||||
|
const [order, setOrder] = useState<Order>('desc');
|
||||||
|
const [selected, setSelected] = useState<readonly string[]>([]);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const [localModels, setLocalModels] = useState([]);
|
||||||
|
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
chatAPI.Endpoints.Models.LocalList(),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const foundationSetter = useCallback(async (name) => {
|
||||||
|
setOpen(true);
|
||||||
|
|
||||||
|
setFoundation(name);
|
||||||
|
const escapedModelName = name.replaceAll('.', '\\.');
|
||||||
|
|
||||||
|
setAdaptor('');
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderFilters = () => (
|
||||||
|
<>
|
||||||
|
<FormControl size="sm">
|
||||||
|
<FormLabel>License</FormLabel>
|
||||||
|
<Select
|
||||||
|
placeholder="Filter by license"
|
||||||
|
slotProps={{ button: { sx: { whiteSpace: 'nowrap' } } }}
|
||||||
|
>
|
||||||
|
<Option value="MIT">MIT</Option>
|
||||||
|
<Option value="pending">CC BY-SA-4.0</Option>
|
||||||
|
<Option value="refunded">Refunded</Option>
|
||||||
|
<Option value="Cancelled">Apache 2.0</Option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl size="sm">
|
||||||
|
<FormLabel>Category</FormLabel>
|
||||||
|
<Select placeholder="All">
|
||||||
|
<Option value="all">All</Option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pickAModelMode && experimentInfo?.config?.foundation) {
|
||||||
|
return (
|
||||||
|
<CurrentFoundationInfo
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
foundation={experimentInfo?.config?.adaptor}
|
||||||
|
setFoundation={setFoundation}
|
||||||
|
adaptor={experimentInfo?.config?.adaptor}
|
||||||
|
setAdaptor={setAdaptor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!experimentInfo && location?.pathname !== '/zoo') {
|
||||||
|
return <Welcome />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography level="h1" mb={2}>
|
||||||
|
Local Models
|
||||||
|
</Typography>
|
||||||
|
<Modal
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
aria-describedby="modal-desc"
|
||||||
|
open={open}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sheet
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
maxWidth: 500,
|
||||||
|
borderRadius: 'md',
|
||||||
|
p: 3,
|
||||||
|
boxShadow: 'lg',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
component="h2"
|
||||||
|
id="modal-title"
|
||||||
|
level="h4"
|
||||||
|
textColor="inherit"
|
||||||
|
fontWeight="lg"
|
||||||
|
mb={1}
|
||||||
|
>
|
||||||
|
Preparing Model
|
||||||
|
</Typography>
|
||||||
|
<Typography id="modal-desc" textColor="text.tertiary">
|
||||||
|
<Stack spacing={2} sx={{ flex: 1 }}>
|
||||||
|
Quantizing Parameters:
|
||||||
|
<LinearProgress />
|
||||||
|
</Stack>
|
||||||
|
</Typography>
|
||||||
|
</Sheet>
|
||||||
|
</Modal>
|
||||||
|
<Box
|
||||||
|
className="SearchAndFilters-tabletUp"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 'sm',
|
||||||
|
py: 2,
|
||||||
|
display: {
|
||||||
|
xs: 'flex',
|
||||||
|
sm: 'flex',
|
||||||
|
},
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 1.5,
|
||||||
|
'& > *': {
|
||||||
|
minWidth: {
|
||||||
|
xs: '120px',
|
||||||
|
md: '160px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl sx={{ flex: 1 }} size="sm">
|
||||||
|
<FormLabel> </FormLabel>
|
||||||
|
<Input placeholder="Search" startDecorator={<SearchIcon />} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{renderFilters()}
|
||||||
|
</Box>
|
||||||
|
<Sheet
|
||||||
|
className="OrderTableContainer"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 'md',
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
minHeight: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
aria-labelledby="tableTitle"
|
||||||
|
stickyHeader
|
||||||
|
hoverRow
|
||||||
|
sx={{
|
||||||
|
'--TableCell-headBackground': (theme) =>
|
||||||
|
theme.vars.palette.background.level1,
|
||||||
|
'--Table-headerUnderlineThickness': '1px',
|
||||||
|
'--TableRow-hoverBackground': (theme) =>
|
||||||
|
theme.vars.palette.background.level1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: 140, padding: 12 }}>
|
||||||
|
<Link
|
||||||
|
underline="none"
|
||||||
|
color="primary"
|
||||||
|
component="button"
|
||||||
|
onClick={() => setOrder(order === 'asc' ? 'desc' : 'asc')}
|
||||||
|
fontWeight="lg"
|
||||||
|
endDecorator={<ArrowDownIcon />}
|
||||||
|
sx={{
|
||||||
|
'& svg': {
|
||||||
|
transition: '0.2s',
|
||||||
|
transform:
|
||||||
|
order === 'desc' ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
<th style={{ width: 120, padding: 12 }}>Params</th>
|
||||||
|
<th style={{ width: 120, padding: 12 }}>Released</th>
|
||||||
|
{/* <th style={{ width: 220, padding: 12 }}>Type</th> */}
|
||||||
|
<th style={{ width: 120, padding: 12 }}> </th>
|
||||||
|
<th style={{ width: 160, padding: 12 }}> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data &&
|
||||||
|
data.map((row) => (
|
||||||
|
<tr key={row.rowid}>
|
||||||
|
<td>
|
||||||
|
<Typography ml={2} fontWeight="lg">
|
||||||
|
{row.name}
|
||||||
|
</Typography>
|
||||||
|
</td>
|
||||||
|
<td>{row?.json_data?.parameters}</td>
|
||||||
|
<td>
|
||||||
|
<Chip
|
||||||
|
variant="soft"
|
||||||
|
size="sm"
|
||||||
|
startDecorator={
|
||||||
|
{
|
||||||
|
MIT: <CheckIcon />,
|
||||||
|
Apache: <GraduationCapIcon />,
|
||||||
|
CC: <CreativeCommonsIcon />,
|
||||||
|
}[row.status]
|
||||||
|
}
|
||||||
|
color={
|
||||||
|
{
|
||||||
|
MIT: 'success',
|
||||||
|
Apache: 'neutral',
|
||||||
|
CC: 'success',
|
||||||
|
}[row.status] as ColorPaletteProp
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row?.json_data?.license}
|
||||||
|
</Chip>
|
||||||
|
</td>
|
||||||
|
{/* <td>
|
||||||
|
<Box
|
||||||
|
sx={{ display: "flex", gap: 2, alignItems: "center" }}
|
||||||
|
></Box>
|
||||||
|
</td> */}
|
||||||
|
<td>{row.model_id}</td>
|
||||||
|
<td style={{ textAlign: 'right' }}>
|
||||||
|
{/* <Link fontWeight="lg" component="button" color="neutral">
|
||||||
|
Archive
|
||||||
|
</Link> */}
|
||||||
|
{pickAModelMode === true ? (
|
||||||
|
<SelectButton
|
||||||
|
setFoundation={foundationSetter}
|
||||||
|
name={row.name}
|
||||||
|
setAdaptor={setAdaptor}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<InfoIcon
|
||||||
|
onClick={() => {
|
||||||
|
alert(JSON.stringify(row?.json_data));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Trash2Icon
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
mutate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{data?.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5}>
|
||||||
|
<Typography
|
||||||
|
level="body-lg"
|
||||||
|
justifyContent="center"
|
||||||
|
margin={5}
|
||||||
|
>
|
||||||
|
You do not have any models on your local machine. You can
|
||||||
|
download a model by going to the{' '}
|
||||||
|
<ReactRouterLink to="/zoo">
|
||||||
|
<StoreIcon />
|
||||||
|
Model Store
|
||||||
|
</ReactRouterLink>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Sheet>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
paddingTop: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pickAModelMode === true ? (
|
||||||
|
''
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="decapoda-research/llama-30b-hf"
|
||||||
|
endDecorator={
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
alert('Not yet implemented.');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download 🤗 Model
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
sx={{ width: '500px' }}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
sx={{ height: '30px' }}
|
||||||
|
endDecorator={<PlusIcon />}
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
311
src/renderer/components/ModelZoo/ModelStore.tsx
Normal file
311
src/renderer/components/ModelZoo/ModelStore.tsx
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Sheet,
|
||||||
|
Table,
|
||||||
|
Typography,
|
||||||
|
Option,
|
||||||
|
Chip,
|
||||||
|
Link,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
} from '@mui/joy';
|
||||||
|
import {
|
||||||
|
ArrowDownIcon,
|
||||||
|
CheckIcon,
|
||||||
|
CreativeCommonsIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
GraduationCapIcon,
|
||||||
|
SearchIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { downloadModelFromGallery } from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import * as chatAPI from '../../lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
|
||||||
|
if (b[orderBy] < a[orderBy]) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b[orderBy] > a[orderBy]) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Order = 'asc' | 'desc';
|
||||||
|
|
||||||
|
function getComparator<Key extends keyof any>(
|
||||||
|
order: Order,
|
||||||
|
orderBy: Key
|
||||||
|
): (
|
||||||
|
a: { [key in Key]: number | string },
|
||||||
|
b: { [key in Key]: number | string }
|
||||||
|
) => number {
|
||||||
|
return order === 'desc'
|
||||||
|
? (a, b) => descendingComparator(a, b, orderBy)
|
||||||
|
: (a, b) => -descendingComparator(a, b, orderBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since 2020 all major browsers ensure sort stability with Array.prototype.sort().
|
||||||
|
// stableSort() brings sort stability to non-modern browsers (notably IE11). If you
|
||||||
|
// only support modern browsers you can replace stableSort(exampleArray, exampleComparator)
|
||||||
|
// with exampleArray.slice().sort(exampleComparator)
|
||||||
|
function stableSort<T>(
|
||||||
|
array: readonly T[],
|
||||||
|
comparator: (a: T, b: T) => number
|
||||||
|
) {
|
||||||
|
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
|
||||||
|
stabilizedThis.sort((a, b) => {
|
||||||
|
const order = comparator(a[0], b[0]);
|
||||||
|
if (order !== 0) {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
return a[1] - b[1];
|
||||||
|
});
|
||||||
|
return stabilizedThis.map((el) => el[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
export default function ModelStore() {
|
||||||
|
const [order, setOrder] = useState<Order>('desc');
|
||||||
|
const [jobId, setJobId] = useState(null);
|
||||||
|
const [currentlyDownloading, setCurrentlyDownloading] = useState('');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: modelGalleryData,
|
||||||
|
error: modelGalleryError,
|
||||||
|
isLoading: modelGalleryIsLoading,
|
||||||
|
mutate: modelGalleryMutate,
|
||||||
|
} = useSWR(chatAPI.Endpoints.Models.Gallery(), fetcher);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: localModelsData,
|
||||||
|
error: localModelsError,
|
||||||
|
isLoading: localModelsIsLoading,
|
||||||
|
mutate: localModelsMutate,
|
||||||
|
} = useSWR(chatAPI.Endpoints.Models.LocalList(), fetcher);
|
||||||
|
|
||||||
|
const renderFilters = () => (
|
||||||
|
<>
|
||||||
|
<FormControl size="sm">
|
||||||
|
<FormLabel>License</FormLabel>
|
||||||
|
<Select
|
||||||
|
placeholder="Filter by license"
|
||||||
|
slotProps={{ button: { sx: { whiteSpace: 'nowrap' } } }}
|
||||||
|
>
|
||||||
|
<Option value="MIT">MIT</Option>
|
||||||
|
<Option value="pending">CC BY-SA-4.0</Option>
|
||||||
|
<Option value="refunded">Refunded</Option>
|
||||||
|
<Option value="Cancelled">Apache 2.0</Option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl size="sm">
|
||||||
|
<FormLabel>Category</FormLabel>
|
||||||
|
<Select placeholder="All">
|
||||||
|
<Option value="all">All</Option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
className="SearchAndFilters-tabletUp"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 'sm',
|
||||||
|
py: 2,
|
||||||
|
display: {
|
||||||
|
xs: 'flex',
|
||||||
|
sm: 'flex',
|
||||||
|
},
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 1.5,
|
||||||
|
'& > *': {
|
||||||
|
minWidth: {
|
||||||
|
xs: '120px',
|
||||||
|
md: '160px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl sx={{ flex: 1 }} size="sm">
|
||||||
|
<FormLabel> </FormLabel>
|
||||||
|
<Input placeholder="Search" startDecorator={<SearchIcon />} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{renderFilters()}
|
||||||
|
</Box>
|
||||||
|
<Sheet
|
||||||
|
className="OrderTableContainer"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 'md',
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
minHeight: 0,
|
||||||
|
height: '90%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
aria-labelledby="tableTitle"
|
||||||
|
stickyHeader
|
||||||
|
hoverRow
|
||||||
|
sx={{
|
||||||
|
'--TableCell-headBackground': (theme) =>
|
||||||
|
theme.vars.palette.background.level1,
|
||||||
|
'--Table-headerUnderlineThickness': '1px',
|
||||||
|
'--TableRow-hoverBackground': (theme) =>
|
||||||
|
theme.vars.palette.background.level1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: 100, padding: 12 }}>
|
||||||
|
<Link
|
||||||
|
underline="none"
|
||||||
|
color="primary"
|
||||||
|
component="button"
|
||||||
|
onClick={() => setOrder(order === 'asc' ? 'desc' : 'asc')}
|
||||||
|
fontWeight="lg"
|
||||||
|
endDecorator={<ArrowDownIcon />}
|
||||||
|
sx={{
|
||||||
|
'& svg': {
|
||||||
|
transition: '0.2s',
|
||||||
|
transform:
|
||||||
|
order === 'desc' ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th style={{ width: 50, padding: 12 }}>Params</th>
|
||||||
|
<th style={{ width: 80, padding: 12 }}>License</th>
|
||||||
|
<th style={{ width: 50, padding: 12 }}>Engine</th>
|
||||||
|
<th style={{ width: 200, padding: 12 }}>Description</th>
|
||||||
|
|
||||||
|
<th style={{ width: 80, padding: 12 }}> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{modelGalleryData &&
|
||||||
|
stableSort(modelGalleryData, getComparator(order, 'id')).map(
|
||||||
|
(row) => (
|
||||||
|
<tr key={row.uniqueID}>
|
||||||
|
<td>
|
||||||
|
<Typography level="title-md" marginLeft={2}>
|
||||||
|
{row.name}
|
||||||
|
<a href={row?.resources?.canonicalUrl} target="_blank">
|
||||||
|
<ExternalLinkIcon size="14px" />
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
</td>
|
||||||
|
<td>{row.parameters}</td>
|
||||||
|
<td>
|
||||||
|
<Chip
|
||||||
|
variant="soft"
|
||||||
|
size="sm"
|
||||||
|
startDecorator={
|
||||||
|
{
|
||||||
|
GPL: <CheckIcon />,
|
||||||
|
'Apache 2.0': <GraduationCapIcon />,
|
||||||
|
CC: <CreativeCommonsIcon />,
|
||||||
|
}[row.license]
|
||||||
|
}
|
||||||
|
color={
|
||||||
|
{
|
||||||
|
GPL: 'success',
|
||||||
|
'Apache 2.0': 'neutral',
|
||||||
|
CC: 'success',
|
||||||
|
}[row.license]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.license}
|
||||||
|
</Chip>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
color:
|
||||||
|
row.architecture == 'GGUF'
|
||||||
|
? 'var(--joy-palette-success-800)'
|
||||||
|
: '',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.architecture}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ maxHeight: '60px', overflow: 'hidden' }}>
|
||||||
|
{/* {JSON.stringify(row)} */}
|
||||||
|
{row.description}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td style={{ textAlign: 'right' }}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
disabled={row.downloaded || currentlyDownloading !== ''}
|
||||||
|
onClick={async () => {
|
||||||
|
setJobId(-1);
|
||||||
|
setCurrentlyDownloading(row.name);
|
||||||
|
try {
|
||||||
|
const response = await downloadModelFromGallery(
|
||||||
|
row.uniqueID
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
response?.message == 'Failed to download model'
|
||||||
|
) {
|
||||||
|
setCurrentlyDownloading('');
|
||||||
|
setJobId(null);
|
||||||
|
return alert(
|
||||||
|
'Failed to download: this model may require a huggingface access token (in settings).'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const job_id = response?.job_id;
|
||||||
|
setCurrentlyDownloading('');
|
||||||
|
modelGalleryMutate();
|
||||||
|
setJobId(job_id);
|
||||||
|
} catch (e) {
|
||||||
|
setCurrentlyDownloading('');
|
||||||
|
setJobId(null);
|
||||||
|
return alert('Failed to download');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
startDecorator={
|
||||||
|
jobId && currentlyDownloading == row.name ? (
|
||||||
|
<CircularProgress />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
endDecorator={
|
||||||
|
row.downloaded ? (
|
||||||
|
<CheckIcon size="18px" />
|
||||||
|
) : (
|
||||||
|
<DownloadIcon size="18px" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Download{row.downloaded ? 'ed' : ''}
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/renderer/components/ModelZoo/ModelZoo.tsx
Normal file
53
src/renderer/components/ModelZoo/ModelZoo.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import Sheet from '@mui/joy/Sheet';
|
||||||
|
import { StoreIcon } from 'lucide-react';
|
||||||
|
import { Tab, TabList, TabPanel, Tabs } from '@mui/joy';
|
||||||
|
import ModelStore from './ModelStore';
|
||||||
|
import LocalModels from './LocalModels';
|
||||||
|
|
||||||
|
export default function ModelZoo({ experimentInfo }) {
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
height: '96%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
aria-label="Basic tabs"
|
||||||
|
defaultValue={0}
|
||||||
|
size="sm"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 'lg',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'unset',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabList tabFlex={1}>
|
||||||
|
<Tab>Local Models</Tab>
|
||||||
|
<Tab>
|
||||||
|
<StoreIcon color="grey" />
|
||||||
|
Model Store
|
||||||
|
</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={0} sx={{ p: 2 }}>
|
||||||
|
<LocalModels pickAModelMode={false} experimentInfo={experimentInfo} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={1} sx={{ p: 2, height: '100%', overflow: 'hidden' }}>
|
||||||
|
<Sheet
|
||||||
|
sx={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
id="model-store-tab-panel"
|
||||||
|
>
|
||||||
|
<ModelStore />
|
||||||
|
</Sheet>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/renderer/components/Nav/ColorSchemeToggle.tsx
Normal file
18
src/renderer/components/Nav/ColorSchemeToggle.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useColorScheme } from '@mui/joy/styles';
|
||||||
|
import IconButton from '@mui/joy/IconButton';
|
||||||
|
import { MoonIcon, SunIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function ColorSchemeToggle() {
|
||||||
|
const { mode, setMode } = useColorScheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => {
|
||||||
|
setMode(mode === 'light' ? 'dark' : 'light');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mode === 'light' ? <SunIcon /> : <MoonIcon />}
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/renderer/components/Nav/NavItem.tsx
Normal file
35
src/renderer/components/Nav/NavItem.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { cloneElement, ReactElement } from 'react';
|
||||||
|
import { useNavigate, useMatch } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { IconButton, Tooltip } from '@mui/joy';
|
||||||
|
|
||||||
|
const NavItem = ({
|
||||||
|
title,
|
||||||
|
path,
|
||||||
|
icon,
|
||||||
|
disabled = false,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
path: string;
|
||||||
|
icon: ReactElement;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const match = useMatch(path);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={title} placement="right">
|
||||||
|
<IconButton
|
||||||
|
variant={match ? 'soft' : 'plain'}
|
||||||
|
onClick={() => navigate(path)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{cloneElement(icon, {
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
})}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavItem;
|
||||||
246
src/renderer/components/Nav/RunModelButton.tsx
Normal file
246
src/renderer/components/Nav/RunModelButton.tsx
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
DialogTitle,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Modal,
|
||||||
|
ModalClose,
|
||||||
|
ModalDialog,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
Option,
|
||||||
|
} from '@mui/joy';
|
||||||
|
import { CogIcon, PlayCircleIcon, StopCircleIcon } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { activateWorker } from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
|
||||||
|
import * as chatAPI from 'renderer/lib/transformerlab-api-sdk';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
function EngineSelect({
|
||||||
|
experimentInfo,
|
||||||
|
inferenceSettings,
|
||||||
|
setInferenceSettings,
|
||||||
|
}) {
|
||||||
|
//@TODO: you should filter by type later because we only want to show
|
||||||
|
//gguf loaders to gguf models, etc but I am testing right now
|
||||||
|
const { data, error, isLoading } = useSWR(
|
||||||
|
chatAPI.Endpoints.Experiment.ListScriptsOfType(
|
||||||
|
experimentInfo?.id,
|
||||||
|
'loader', // type
|
||||||
|
'model_architectures:' +
|
||||||
|
experimentInfo?.config?.foundation_model_architecture //filter
|
||||||
|
),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
placeholder={isLoading ? 'Loading...' : 'Select Engine'}
|
||||||
|
variant="soft"
|
||||||
|
size="lg"
|
||||||
|
name="inferenceEngine"
|
||||||
|
defaultValue={inferenceSettings?.inferenceEngine}
|
||||||
|
onChange={(e, newValue) => {
|
||||||
|
setInferenceSettings({
|
||||||
|
...inferenceSettings,
|
||||||
|
inferenceEngine: newValue,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Option value={null}>Default</Option>
|
||||||
|
{data?.map((row) => (
|
||||||
|
<Option value={row.uniqueId} key={row.uniqueId}>
|
||||||
|
{row.name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RunModelButton({
|
||||||
|
experimentInfo,
|
||||||
|
killWorker,
|
||||||
|
models,
|
||||||
|
mutate = () => {},
|
||||||
|
}) {
|
||||||
|
const [jobId, setJobId] = useState(null);
|
||||||
|
const [showRunSettings, setShowRunSettings] = useState(false);
|
||||||
|
const [inferenceSettings, setInferenceSettings] = useState({
|
||||||
|
'8-bit': false,
|
||||||
|
'cpu-offload': false,
|
||||||
|
inferenceEngine: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
function isPossibleToRunAModel() {
|
||||||
|
return experimentInfo != null && experimentInfo?.config?.foundation !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (experimentInfo?.config?.inferenceParams) {
|
||||||
|
setInferenceSettings(JSON.parse(experimentInfo?.config?.inferenceParams));
|
||||||
|
}
|
||||||
|
}, [experimentInfo]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: '0px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* {JSON.stringify(models)} */}
|
||||||
|
{/* {jobId} */}
|
||||||
|
{/* {JSON.stringify(experimentInfo)} */}
|
||||||
|
{models === null ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
startDecorator={
|
||||||
|
jobId === -1 ? (
|
||||||
|
<CircularProgress size="sm" thickness={2} />
|
||||||
|
) : (
|
||||||
|
<PlayCircleIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
color="success"
|
||||||
|
size="lg"
|
||||||
|
sx={{ fontSize: '1.1rem', marginRight: 1, minWidth: '200px' }}
|
||||||
|
onClick={async () => {
|
||||||
|
setJobId(-1);
|
||||||
|
|
||||||
|
const eightBit = inferenceSettings?.['8-bit'];
|
||||||
|
const cpuOffload = inferenceSettings?.['cpu-offload'];
|
||||||
|
const inferenceEngine = inferenceSettings?.inferenceEngine;
|
||||||
|
|
||||||
|
const response = await activateWorker(
|
||||||
|
experimentInfo?.config?.foundation,
|
||||||
|
experimentInfo?.config?.foundation_filename,
|
||||||
|
experimentInfo?.config?.adaptor,
|
||||||
|
eightBit,
|
||||||
|
cpuOffload,
|
||||||
|
inferenceEngine,
|
||||||
|
experimentInfo?.id
|
||||||
|
);
|
||||||
|
const job_id = response?.job_id;
|
||||||
|
setJobId(job_id);
|
||||||
|
mutate();
|
||||||
|
}}
|
||||||
|
disabled={!isPossibleToRunAModel()}
|
||||||
|
>
|
||||||
|
Run
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
await killWorker();
|
||||||
|
setJobId(null);
|
||||||
|
}}
|
||||||
|
startDecorator={
|
||||||
|
models?.length == 0 ? (
|
||||||
|
<CircularProgress size="sm" thickness={2} />
|
||||||
|
) : (
|
||||||
|
<StopCircleIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
color="success"
|
||||||
|
size="lg"
|
||||||
|
sx={{ fontSize: '1.1rem', marginRight: 1, minWidth: '200px' }}
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
color="neutral"
|
||||||
|
size="md"
|
||||||
|
disabled={models?.length > 0 || jobId == -1}
|
||||||
|
onClick={() => setShowRunSettings(!showRunSettings)}
|
||||||
|
>
|
||||||
|
<CogIcon color="var(--joy-palette-neutral-500)" />
|
||||||
|
</IconButton>
|
||||||
|
<Modal open={showRunSettings} onClose={() => setShowRunSettings(false)}>
|
||||||
|
<ModalDialog>
|
||||||
|
<DialogTitle>Inference Engine Settings</DialogTitle>
|
||||||
|
<ModalClose variant="plain" sx={{ m: 1 }} />
|
||||||
|
|
||||||
|
{/* <DialogContent>Fill in the information of the project.</DialogContent> */}
|
||||||
|
<form
|
||||||
|
onSubmit={async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const eightBit = document.getElementById('eightBit')?.checked;
|
||||||
|
const cpuOffload = document.getElementById('cpuOffload')?.checked;
|
||||||
|
const experimentId = experimentInfo?.id;
|
||||||
|
|
||||||
|
setInferenceSettings({
|
||||||
|
...inferenceSettings,
|
||||||
|
'8-bit': eightBit,
|
||||||
|
'cpu-offload': cpuOffload,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetch(
|
||||||
|
chatAPI.Endpoints.Experiment.UpdateConfig(
|
||||||
|
experimentId,
|
||||||
|
'inferenceParams',
|
||||||
|
JSON.stringify({
|
||||||
|
...inferenceSettings,
|
||||||
|
'8-bit': eightBit,
|
||||||
|
'cpu-offload': cpuOffload,
|
||||||
|
inferenceEngine: inferenceSettings?.inferenceEngine,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setShowRunSettings(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{/* {JSON.stringify(inferenceSettings)} */}
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Engine</FormLabel>
|
||||||
|
<EngineSelect
|
||||||
|
experimentInfo={experimentInfo}
|
||||||
|
inferenceSettings={inferenceSettings}
|
||||||
|
setInferenceSettings={setInferenceSettings}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
orientation="horizontal"
|
||||||
|
sx={{ width: 300, justifyContent: 'space-between' }}
|
||||||
|
>
|
||||||
|
<FormLabel>8-bit</FormLabel>
|
||||||
|
<Switch
|
||||||
|
id="eightBit"
|
||||||
|
defaultChecked={inferenceSettings?.['8-bit']}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
orientation="horizontal"
|
||||||
|
sx={{ width: 300, justifyContent: 'space-between' }}
|
||||||
|
>
|
||||||
|
<FormLabel>CPU Offload</FormLabel>
|
||||||
|
<Switch
|
||||||
|
id="cpuOffload"
|
||||||
|
defaultChecked={inferenceSettings?.['cpu-offload']}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
<Stack
|
||||||
|
sx={{ fontSize: '12px', minWidth: '80px' }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
{inferenceSettings?.inferenceEngine}
|
||||||
|
{inferenceSettings?.['8-bit'] && <div>8-bit Mode</div>}
|
||||||
|
{inferenceSettings?.['cpu-offload'] && <div>CPU-Offload</div>}
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user