first checkin

This commit is contained in:
ali asaria
2023-12-24 17:19:41 -05:00
parent 2a09a24af1
commit 1e0d05280b
127 changed files with 34904 additions and 0 deletions

12
.editorconfig Normal file
View 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
View File

@@ -0,0 +1,7 @@
{
"rules": {
"no-console": "off",
"global-require": "off",
"import/no-dynamic-require": "off"
}
}

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

View File

@@ -0,0 +1,3 @@
/* eslint import/no-unresolved: off, import/no-self-import: off */
module.exports = require('./webpack.config.renderer.dev').default;

View 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);

View 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);

View 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);

View 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);

View 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);

View 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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

1
.erb/mocks/fileMock.js Normal file
View File

@@ -0,0 +1 @@
export default 'test-file-stub';

8
.erb/scripts/.eslintrc Normal file
View File

@@ -0,0 +1,8 @@
{
"rules": {
"no-console": "off",
"global-require": "off",
"import/no-dynamic-require": "off",
"import/no-extraneous-dependencies": "off"
}
}

View 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"'
)
);
}

View 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');
}
}

View 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);
}
}

View 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
View 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);
});

View 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,
});
}

View 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',
});
}

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
requiredHeaders:
- Prerequisites
- Expected Behavior
- Current Behavior
- Possible Solution
- Your Environment

17
.github/stale.yml vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"]
}

30
.vscode/launch.json vendored Normal file
View 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
View 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
View 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
View 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
View 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;
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

File diff suppressed because it is too large Load Diff

278
package.json Normal file
View 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
View 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
View 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"
}
}

View 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
View 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
View 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
View 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
View 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');
},
});

View 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
View 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
View 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
View 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>
);
}

View 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>
</>
);
}
}

View 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&apos;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>
);
}

View 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>
);
}

View 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" />
&nbsp; 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>
);
}

View 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>
);
}

View 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>&nbsp;
{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 &nbsp;
<DownloadIcon size={16} />
</Button>
)}
</CardContent>
</Card>
</>
);
}

View 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>
);
}

View 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."
/>
</>
);
}

View 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>
);
}

View 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>
);
}

View 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>
</>
);
}

View 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>&nbsp;</th>
<th>Tasks</th>
<th>Template</th>
<th>Number of Records</th>
<th style={{ textAlign: 'right' }}>&nbsp;</th>
<th style={{ textAlign: 'right' }}>&nbsp;</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}&nbsp;
<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>
</>
);
}

View 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>
);
}

View 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>
</>
);
}

View File

@@ -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" />
&nbsp;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}
&nbsp;&nbsp;
</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>
);
}

View 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>&nbsp;
{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>
</>
);
}

View 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>&nbsp;</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 }}>&nbsp;</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>
</>
);
}

View 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}
/>
);
}

View 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>
&nbsp;&nbsp;
<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>
);
}

View 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>
);
}

View 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 &nbsp;
<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>
);
}

View 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>
);
}

View 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)} &hellip;</> : <>{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 &nbsp;
<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 &nbsp;
<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 &nbsp;
<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 &nbsp;
<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'}:
&nbsp;
{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>
</>
);
}

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

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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):&nbsp;
<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&nbsp;
<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>
&nbsp;
</>
))}
<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>
</>
);
}

View 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>
);
}

View File

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

View File

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

View 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>
</>
);
}

View 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' }}>&nbsp;</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 />}
>
&nbsp;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>
</>
);
}

View 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>
&nbsp;
</>
)
)}
</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 &nbsp;
<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 &nbsp;
<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 &nbsp;
<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 &nbsp;
<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 &nbsp;
<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 &nbsp;
<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>
);
}

View 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>
);
}

View 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' }}
/>
&nbsp; 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' }}
/>
&nbsp; Connected -
</div>
</Tooltip>
&nbsp;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:&nbsp;
{/* <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>
);
}

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

View 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>
);
}

View 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>
&nbsp;
{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)" />
) : (
''
)}
&nbsp;&nbsp;
<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>
);
}

View 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>&nbsp;</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 }}>&nbsp;</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));
}}
/>
&nbsp;
<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>
</>
);
}

View 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>&nbsp;</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}&nbsp;
<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>
</>
);
}

View 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" />
&nbsp; 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>
);
}

View 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>
);
}

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

View 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