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