Merge branch 'main' into fix-264

This commit is contained in:
0xSage 2023-10-08 16:53:55 +08:00 committed by GitHub
commit 8cf71567c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
181 changed files with 37439 additions and 2846 deletions

View File

@ -37,17 +37,22 @@ jobs:
env:
VERSION_TAG: ${{ steps.tag.outputs.tag }}
- name: Install yarn dependencies
run: |
yarn install
yarn build:plugins
- name: Get Cer for code signing
run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
shell: bash
env:
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
- uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
p12-password: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
- name: Install yarn dependencies
run: |
yarn install
yarn build:plugins-darwin
- name: Build and publish app
run: |
yarn build:publish-darwin
@ -56,6 +61,8 @@ jobs:
CSC_LINK: "/tmp/codesign.p12"
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
CSC_IDENTITY_AUTO_DISCOVERY: "true"
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
build-windows-x64:
runs-on: windows-latest

88
.github/workflows/linter-and-test.yml vendored Normal file
View File

@ -0,0 +1,88 @@
name: Linter & Test
on:
push:
branches:
- main
paths:
- 'electron/**'
- .github/workflows/linter-and-test.yml
- 'web/**'
- 'package.json'
- 'node_modules/**'
- 'yarn.lock'
pull_request:
branches:
- main
paths:
- 'electron/**'
- .github/workflows/linter-and-test.yml
- 'web/**'
- 'package.json'
- 'node_modules/**'
- 'yarn.lock'
jobs:
test-on-macos:
runs-on: [self-hosted, macOS, macos-desktop]
steps:
- name: Getting the repo
uses: actions/checkout@v3
- name: Installing node
uses: actions/setup-node@v1
with:
node-version: 20
- name: Linter and test
run: |
yarn config set network-timeout 300000
yarn install
yarn lint
yarn build:plugins
yarn build
yarn test
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
test-on-windows:
runs-on: [self-hosted, Windows, windows-desktop]
steps:
- name: Getting the repo
uses: actions/checkout@v3
- name: Installing node
uses: actions/setup-node@v1
with:
node-version: 20
- name: Linter and test
run: |
yarn config set network-timeout 300000
yarn install
yarn lint
yarn build:plugins
yarn build:win32
yarn test
test-on-ubuntu:
runs-on: [self-hosted, Linux, ubuntu-desktop]
steps:
- name: Getting the repo
uses: actions/checkout@v3
- name: Installing node
uses: actions/setup-node@v1
with:
node-version: 20
- name: Linter and test
run: |
yarn config set network-timeout 300000
yarn install
yarn lint
yarn build:plugins
yarn build:linux
yarn test
env:
DISPLAY: ":0"

1
.gitignore vendored
View File

@ -5,7 +5,6 @@
models/**
error.log
node_modules
package-lock.json
*.tgz
yarn.lock
dist

View File

@ -34,5 +34,11 @@ module.exports = {
{ name: "Link", linkAttribute: "to" },
],
},
ignorePatterns: ["renderer/*", "node_modules/*", "core/plugins"],
ignorePatterns: [
"build",
"renderer",
"node_modules",
"core/plugins",
"core/**/*.test.js",
],
};

5
electron/auto-sign.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
DEVELOPER_ID="Developer ID Application: Eigenvector Pte Ltd"
find electron -type f -perm +111 -exec codesign -s "Developer ID Application: Eigenvector Pte Ltd (YT49P7GXG4)" --options=runtime {} \;

View File

@ -25,6 +25,7 @@ export async function install(plugins) {
if (typeof window === "undefined") {
return;
}
// eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.install(plugins);
if (plgList.cancelled) return false;
return plgList.map((plg) => {
@ -50,6 +51,7 @@ export function uninstall(plugins, reload = true) {
if (typeof window === "undefined") {
return;
}
// eslint-disable-next-line no-undef
return window.pluggableElectronIpc.uninstall(plugins, reload);
}
@ -62,6 +64,7 @@ export async function getActive() {
if (typeof window === "undefined") {
return;
}
// eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.getActive();
return plgList.map(
(plugin) =>
@ -86,6 +89,7 @@ export async function registerActive() {
if (typeof window === "undefined") {
return;
}
// eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.getActive();
plgList.forEach((plugin) =>
register(
@ -110,6 +114,7 @@ export async function update(plugins, reload = true) {
if (typeof window === "undefined") {
return;
}
// eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.update(plugins, reload);
return plgList.map(
(plugin) =>
@ -132,6 +137,7 @@ export function updatesAvailable(plugin) {
if (typeof window === "undefined") {
return;
}
// eslint-disable-next-line no-undef
return window.pluggableElectronIpc.updatesAvailable(plugin);
}
@ -146,6 +152,7 @@ export async function toggleActive(plugin, active) {
if (typeof window === "undefined") {
return;
}
// eslint-disable-next-line no-undef
const plg = await window.pluggableElectronIpc.toggleActive(plugin, active);
return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active);
}

View File

@ -5,6 +5,7 @@ export * as activationPoints from "./activation-manager.js";
export * as plugins from "./facade.js";
export { default as ExtensionPoint } from "./ExtensionPoint.js";
// eslint-disable-next-line no-undef
if (typeof window !== "undefined" && !window.pluggableElectronIpc)
console.warn(
"Facade is not registered in preload. Facade functions will throw an error if used."

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@
],
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
"build:package": "rimraf ./data-plugin*.tgz && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=arm64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=glibc --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=musl --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_libc=unknown --target_arch=x64 && npm run build && npm pack",
"build:publish": "npm run build:package && cpx *.tgz ../../pre-install"
"postinstall": "rimraf ./data-plugin*.tgz && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=arm64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=glibc --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=musl --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_libc=unknown --target_arch=x64 && npm run build",
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
},
"exports": {
".": "./dist/index.js",

View File

@ -17,11 +17,16 @@ const dispose = async () =>
.then((res) => resolve(res));
}
});
const inferenceUrl = () => "http://localhost:8080/llama/chat_completion";
const inferenceUrl = () => "http://localhost:3928/llama/chat_completion";
const stopModel = () => {
window.electronAPI.invokePluginFunc(MODULE_PATH, "killSubprocess");
};
// Register all the above functions and objects with the relevant extension points
export function init({ register }) {
register("initModel", "initModel", initModel);
register("inferenceUrl", "inferenceUrl", inferenceUrl);
register("dispose", "dispose", dispose);
register("stopModel", "stopModel", stopModel);
}

View File

@ -23,7 +23,7 @@ async function initModel(product) {
console.error(
"A subprocess is already running. Attempt to kill then reinit."
);
killSubprocess();
dispose();
}
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
@ -51,12 +51,12 @@ async function initModel(product) {
let binaryName;
if (process.platform === "win32") {
binaryName = "nitro.exe";
binaryName = "nitro_windows_amd64.exe";
} else if (process.platform === "darwin") { // Mac OS platform
binaryName = process.arch === "arm64" ? "nitro" : "nitro_mac_intel";
binaryName = process.arch === "arm64" ? "nitro_mac_arm64" : "nitro_mac_amd64";
} else {
// Linux
binaryName = "nitro_linux"; // For other platforms
binaryName = "nitro_linux_amd64_cuda"; // For other platforms
}
const binaryPath = path.join(binaryFolder, binaryName);

View File

@ -1 +1,13 @@
{"custom_config": {"llama_model_path":"","ctx_len":2048,"ngl":100}}
{
"listeners": [
{
"address": "0.0.0.0",
"port": 3928
}
],
"custom_config": {
"llama_model_path": "",
"ctx_len": 2048,
"ngl": 100
}
}

View File

@ -24,12 +24,59 @@ typedef struct {
int8_t qs[QK8_0]; // quants
} block_q8_0;
// general-purpose kernel for addition of two tensors
// pros: works for non-contiguous tensors, supports broadcast across dims 1, 2 and 3
// cons: not very efficient
kernel void kernel_add(
device const float4 * src0,
device const float4 * src1,
device float4 * dst,
uint tpig[[thread_position_in_grid]]) {
dst[tpig] = src0[tpig] + src1[tpig];
device const char * src0,
device const char * src1,
device char * dst,
constant int64_t & ne00,
constant int64_t & ne01,
constant int64_t & ne02,
constant int64_t & ne03,
constant int64_t & nb00,
constant int64_t & nb01,
constant int64_t & nb02,
constant int64_t & nb03,
constant int64_t & ne10,
constant int64_t & ne11,
constant int64_t & ne12,
constant int64_t & ne13,
constant int64_t & nb10,
constant int64_t & nb11,
constant int64_t & nb12,
constant int64_t & nb13,
constant int64_t & ne0,
constant int64_t & ne1,
constant int64_t & ne2,
constant int64_t & ne3,
constant int64_t & nb0,
constant int64_t & nb1,
constant int64_t & nb2,
constant int64_t & nb3,
uint3 tgpig[[threadgroup_position_in_grid]],
uint3 tpitg[[thread_position_in_threadgroup]],
uint3 ntg[[threads_per_threadgroup]]) {
const int64_t i03 = tgpig.z;
const int64_t i02 = tgpig.y;
const int64_t i01 = tgpig.x;
const int64_t i13 = i03 % ne13;
const int64_t i12 = i02 % ne12;
const int64_t i11 = i01 % ne11;
device const char * src0_ptr = src0 + i03*nb03 + i02*nb02 + i01*nb01 + tpitg.x*nb00;
device const char * src1_ptr = src1 + i13*nb13 + i12*nb12 + i11*nb11 + tpitg.x*nb10;
device char * dst_ptr = dst + i03*nb3 + i02*nb2 + i01*nb1 + tpitg.x*nb0;
for (int i0 = tpitg.x; i0 < ne0; i0 += ntg.x) {
((device float *)dst_ptr)[0] = ((device float *)src0_ptr)[0] + ((device float *)src1_ptr)[0];
src0_ptr += ntg.x*nb00;
src1_ptr += ntg.x*nb10;
dst_ptr += ntg.x*nb0;
}
}
// assumption: src1 is a row
@ -38,7 +85,7 @@ kernel void kernel_add_row(
device const float4 * src0,
device const float4 * src1,
device float4 * dst,
constant int64_t & nb,
constant int64_t & nb [[buffer(27)]],
uint tpig[[thread_position_in_grid]]) {
dst[tpig] = src0[tpig] + src1[tpig % nb];
}
@ -784,6 +831,8 @@ kernel void kernel_alibi_f32(
constant uint64_t & nb2,
constant uint64_t & nb3,
constant float & m0,
constant float & m1,
constant int & n_heads_log2_floor,
uint3 tgpig[[threadgroup_position_in_grid]],
uint3 tpitg[[thread_position_in_threadgroup]],
uint3 ntg[[threads_per_threadgroup]]) {
@ -799,15 +848,51 @@ kernel void kernel_alibi_f32(
const int64_t i0 = (n - i3*ne2*ne1*ne0 - i2*ne1*ne0 - i1*ne0);
device float * dst_data = (device float *) ((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
float m_k = pow(m0, i2 + 1);
float m_k;
if (i2 < n_heads_log2_floor) {
m_k = pow(m0, i2 + 1);
} else {
m_k = pow(m1, 2 * (i2 - n_heads_log2_floor) + 1);
}
for (int64_t i00 = tpitg.x; i00 < ne00; i00 += ntg.x) {
device const float * src = (device float *)((device char *) src0 + i03*nb03 + i02*nb02 + i01*nb01 + i00*nb00);
dst_data[i00] = src[0] + m_k * (i00 - ne00 + 1);
}
}
typedef void (rope_t)(
device const void * src0,
device const int32_t * src1,
device float * dst,
constant int64_t & ne00,
constant int64_t & ne01,
constant int64_t & ne02,
constant int64_t & ne03,
constant uint64_t & nb00,
constant uint64_t & nb01,
constant uint64_t & nb02,
constant uint64_t & nb03,
constant int64_t & ne0,
constant int64_t & ne1,
constant int64_t & ne2,
constant int64_t & ne3,
constant uint64_t & nb0,
constant uint64_t & nb1,
constant uint64_t & nb2,
constant uint64_t & nb3,
constant int & n_past,
constant int & n_dims,
constant int & mode,
constant float & freq_base,
constant float & freq_scale,
uint tiitg[[thread_index_in_threadgroup]],
uint3 tptg[[threads_per_threadgroup]],
uint3 tgpig[[threadgroup_position_in_grid]]);
template<typename T>
kernel void kernel_rope(
device const void * src0,
device const int32_t * src1,
device float * dst,
constant int64_t & ne00,
constant int64_t & ne01,
@ -839,7 +924,9 @@ kernel void kernel_rope(
const bool is_neox = mode & 2;
const int64_t p = ((mode & 1) == 0 ? n_past + i2 : i2);
device const int32_t * pos = src1;
const int64_t p = pos[i2];
const float theta_0 = freq_scale * (float)p;
const float inv_ndims = -1.f/n_dims;
@ -851,11 +938,11 @@ kernel void kernel_rope(
const float cos_theta = cos(theta);
const float sin_theta = sin(theta);
device const float * const src = (device float *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
device float * dst_data = (device float *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
device const T * const src = (device T *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
device T * dst_data = (device T *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
const float x0 = src[0];
const float x1 = src[1];
const T x0 = src[0];
const T x1 = src[1];
dst_data[0] = x0*cos_theta - x1*sin_theta;
dst_data[1] = x0*sin_theta + x1*cos_theta;
@ -870,8 +957,8 @@ kernel void kernel_rope(
const int64_t i0 = ib*n_dims + ic/2;
device const float * const src = (device float *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
device float * dst_data = (device float *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
device const T * const src = (device T *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
device T * dst_data = (device T *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
const float x0 = src[0];
const float x1 = src[n_dims/2];
@ -883,6 +970,9 @@ kernel void kernel_rope(
}
}
template [[host_name("kernel_rope_f32")]] kernel rope_t kernel_rope<float>;
template [[host_name("kernel_rope_f16")]] kernel rope_t kernel_rope<half>;
kernel void kernel_cpy_f16_f16(
device const half * src0,
device half * dst,
@ -1273,8 +1363,8 @@ kernel void kernel_mul_mat_q3_K_f32(
float yl[32];
const uint16_t kmask1 = 0x3030;
const uint16_t kmask2 = 0x0f0f;
//const uint16_t kmask1 = 0x3030;
//const uint16_t kmask2 = 0x0f0f;
const int tid = tiisg/4;
const int ix = tiisg%4;

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@
],
"scripts": {
"build": "webpack --config webpack.config.js",
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\" && npm pack",
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
},
"devDependencies": {
"cpx": "^1.5.0",
@ -25,8 +25,7 @@
"node-llama-cpp"
],
"dependencies": {
"electron-is-dev": "^2.0.0",
"node-llama-cpp": "^2.4.1"
"electron-is-dev": "^2.0.0"
},
"engines": {
"node": ">=18.0.0"

View File

@ -38,10 +38,20 @@ const deleteModel = async (path) =>
}
});
const searchModels = async (params) =>
new Promise(async (resolve) => {
if (window.electronAPI) {
window.electronAPI
.invokePluginFunc(MODULE_PATH, "searchModels", params)
.then((res) => resolve(res));
}
});
// Register all the above functions and objects with the relevant extension points
export function init({ register }) {
register("getDownloadedModels", "getDownloadedModels", getDownloadedModels);
register("getAvailableModels", "getAvailableModels", getAvailableModels);
register("downloadModel", "downloadModel", downloadModel);
register("deleteModel", "deleteModel", deleteModel);
register("searchModels", "searchModels", searchModels);
}

View File

@ -1,6 +1,10 @@
const path = require("path");
const { readdirSync, lstatSync } = require("fs");
const { app } = require("electron");
const { listModels, listFiles, fileDownloadInfo } = require("@huggingface/hub");
let modelsIterator = undefined;
let currentSearchOwner = undefined;
const ALL_MODELS = [
{
@ -87,6 +91,76 @@ function getDownloadedModels() {
return downloadedModels;
}
const getNextModels = async (count) => {
const models = [];
let hasMore = true;
while (models.length < count) {
const next = await modelsIterator.next();
// end if we reached the end
if (next.done) {
hasMore = false;
break;
}
const model = next.value;
const files = await listFilesByName(model.name);
models.push({
...model,
files,
});
}
const result = {
data: models,
hasMore,
};
return result;
};
const searchModels = async (params) => {
if (currentSearchOwner === params.search.owner && modelsIterator != null) {
// paginated search
console.debug(`Paginated search owner: ${params.search.owner}`);
const models = await getNextModels(params.limit);
return models;
} else {
// new search
console.debug(`Init new search owner: ${params.search.owner}`);
currentSearchOwner = params.search.owner;
modelsIterator = listModels({
search: params.search,
credentials: params.credentials,
});
const models = await getNextModels(params.limit);
return models;
}
};
const listFilesByName = async (modelName) => {
const repo = { type: "model", name: modelName };
const fileDownloadInfoMap = {};
for await (const file of listFiles({
repo: repo,
})) {
if (file.type === "file" && file.path.endsWith(".bin")) {
const downloadInfo = await fileDownloadInfo({
repo: repo,
path: file.path,
});
fileDownloadInfoMap[file.path] = {
...file,
...downloadInfo,
};
}
}
return fileDownloadInfoMap;
};
function getAvailableModels() {
const downloadedModelIds = getDownloadedModels().map((model) => model.id);
return ALL_MODELS.filter((model) => {
@ -99,4 +173,5 @@ function getAvailableModels() {
module.exports = {
getDownloadedModels,
getAvailableModels,
searchModels,
};

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@
],
"scripts": {
"build": "webpack --config webpack.config.js",
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\"",
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
},
"devDependencies": {
"cpx": "^1.5.0",
@ -24,5 +24,11 @@
"dist/*",
"package.json",
"README.md"
],
"dependencies": {
"@huggingface/hub": "^0.8.5"
},
"bundledDependencies": [
"@huggingface/hub"
]
}

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@
],
"scripts": {
"build": "webpack --config webpack.config.js",
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\"",
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
},
"devDependencies": {
"rimraf": "^3.0.2",

View File

@ -0,0 +1,14 @@
<?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-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

View File

@ -147,6 +147,18 @@ function handleIPCs() {
ipcMain.handle("relaunch", async (_event, url) => {
dispose(requiredModules);
app.relaunch();
app.exit();
});
ipcMain.handle("reloadPlugins", async (_event, url) => {
const userDataPath = app.getPath("userData");
const fullPath = join(userDataPath, "plugins");
rmdir(fullPath, { recursive: true }, function (err) {
if (err) console.log(err);
app.relaunch();
app.exit();
});
});
/**

View File

@ -27,12 +27,18 @@
],
"extends": null,
"mac": {
"type": "distribution"
"type": "distribution",
"entitlements": "./entitlements.mac.plist",
"entitlementsInherit": "./entitlements.mac.plist",
"notarize": {
"teamId": "YT49P7GXG4"
}
},
"artifactName": "${name}-${os}-${arch}-${version}.${ext}"
},
"scripts": {
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
"test:e2e": "playwright test --workers=2",
"test:e2e": "playwright test --workers=1",
"dev": "tsc -p . && electron .",
"build": "tsc -p . && electron-builder -p never -m",
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
@ -45,13 +51,17 @@
},
"dependencies": {
"@npmcli/arborist": "^7.1.0",
"@uiball/loaders": "^1.3.0",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.4",
"pacote": "^17.0.4",
"react-intersection-observer": "^9.5.2",
"request": "^2.88.2",
"request-progress": "^3.0.0"
"request-progress": "^3.0.0",
"use-debounce": "^9.0.4"
},
"devDependencies": {
"@electron/notarize": "^2.1.0",
"@playwright/test": "^1.38.1",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",

View File

@ -3,7 +3,8 @@ import { PlaywrightTestConfig } from "@playwright/test";
const config: PlaywrightTestConfig = {
testDir: "./tests",
testIgnore: "./core/**",
retries: 0
retries: 0,
timeout: 120000,
};
export default config;

View File

@ -13,6 +13,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
pluginPath: () => ipcRenderer.invoke("pluginPath"),
reloadPlugins: () => ipcRenderer.invoke("reloadPlugins"),
appVersion: () => ipcRenderer.invoke("appVersion"),
openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url),

View File

@ -19,8 +19,6 @@ test.beforeAll(async () => {
// parse the packaged Electron app and find paths and other info
const appInfo = parseElectronApp(latestBuild);
expect(appInfo).toBeTruthy();
expect(appInfo.arch).toBeTruthy();
expect(appInfo.arch).toBe(process.arch);
expect(appInfo.asar).toBe(true);
expect(appInfo.executable).toBeTruthy();
expect(appInfo.main).toBeTruthy();

View File

@ -52,10 +52,6 @@ test("renders left navigation panel", async () => {
.getByRole("button", { name: "Explore Models" })
.first()
.isEnabled();
const startConversation = await page
.getByRole("button", { name: "Start a Conversation" })
.first()
.isEnabled();
const discordBtn = await page
.getByRole("button", { name: "Discord" })
.first()
@ -72,7 +68,6 @@ test("renders left navigation panel", async () => {
[
newChatBtn,
exploreBtn,
startConversation,
discordBtn,
myModelsBtn,
settingsBtn,

1614
node_modules/.yarn-integrity generated vendored

File diff suppressed because it is too large Load Diff

14224
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,8 @@
"dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
"build:electron": "yarn workspace jan-electron build",
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/model-management-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/monitoring-plugin && npm install && npm run build:publish\"",
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm ci\" \"cd ./electron/core/plugins/inference-plugin && npm ci\" \"cd ./electron/core/plugins/model-management-plugin && npm ci\" \"cd ./electron/core/plugins/monitoring-plugin && npm ci\" && concurrently \"cd ./electron/core/plugins/data-plugin && npm run build:publish\" \"cd ./electron/core/plugins/inference-plugin && npm run build:publish\" \"cd ./electron/core/plugins/model-management-plugin && npm run build:publish\" \"cd ./electron/core/plugins/monitoring-plugin && npm run build:publish\"",
"build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm ci\" \"cd ./electron/core/plugins/inference-plugin && npm ci\" \"cd ./electron/core/plugins/model-management-plugin && npm ci\" \"cd ./electron/core/plugins/monitoring-plugin && npm ci\" && chmod +x ./electron/auto-sign.sh && ./electron/auto-sign.sh && concurrently \"cd ./electron/core/plugins/data-plugin && npm run build:publish\" \"cd ./electron/core/plugins/inference-plugin && npm run build:publish\" \"cd ./electron/core/plugins/model-management-plugin && npm run build:publish\" \"cd ./electron/core/plugins/monitoring-plugin && npm run build:publish\"",
"build": "yarn build:web && yarn build:electron",
"build:darwin": "yarn build:web && yarn workspace jan-electron build:darwin",
"build:win32": "yarn build:web && yarn workspace jan-electron build:win32",

View File

@ -9,10 +9,10 @@ const ActiveModelTable: React.FC = () => {
if (!activeModel) return null;
return (
<Fragment>
<div className="pl-[63px] pr-[89px]">
<h3 className="text-xl leading-[25px] mb-[13px]">Active Model(s)</h3>
<ModelTable models={[activeModel]} />
</Fragment>
</div>
);
};

View File

@ -47,7 +47,7 @@ const AvailableModelCard: React.FC<Props> = ({
return (
<div className="border rounded-lg border-gray-200">
<div className="flex justify-between py-4 px-3 gap-[10px]">
<div className="flex justify-between py-4 px-3 gap-2.5">
<DownloadModelContent
required={required}
author={product.author}

View File

@ -1,13 +1,11 @@
"use client";
import React, { useCallback, useRef, useState } from "react";
import React, { useCallback, useRef, useState, useEffect } from "react";
import ChatItem from "../ChatItem";
import { ChatMessage } from "@/_models/ChatMessage";
import useChatMessages from "@/_hooks/useChatMessages";
import { showingTyping } from "@/_helpers/JotaiWrapper";
import { useAtomValue } from "jotai";
import { selectAtom } from "jotai/utils";
import LoadingIndicator from "../LoadingIndicator";
import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
import { chatMessages } from "@/_helpers/atoms/ChatMessage.atom";
@ -16,12 +14,11 @@ const ChatBody: React.FC = () => {
const messageList = useAtomValue(
selectAtom(
chatMessages,
useCallback((v) => v[activeConversationId], [activeConversationId])
)
useCallback((v) => v[activeConversationId], [activeConversationId]),
),
);
const [content, setContent] = useState<React.JSX.Element[]>([]);
const isTyping = useAtomValue(showingTyping);
const [offset, setOffset] = useState(0);
const { loading, hasMore } = useChatMessages(offset);
const intersectObs = useRef<any>(null);
@ -40,10 +37,10 @@ const ChatBody: React.FC = () => {
if (message) intersectObs.current.observe(message);
},
[loading, hasMore]
[loading, hasMore],
);
React.useEffect(() => {
useEffect(() => {
const list = messageList?.map((message, index) => {
if (messageList?.length === index + 1) {
return (
@ -58,11 +55,6 @@ const ChatBody: React.FC = () => {
return (
<div className="flex flex-col-reverse flex-1 py-4 overflow-y-auto scroll">
{isTyping && (
<div className="ml-4 mb-2" key="indicator">
<LoadingIndicator />
</div>
)}
{content}
</div>
);

View File

@ -2,22 +2,17 @@ import SimpleControlNetMessage from "../SimpleControlNetMessage";
import SimpleImageMessage from "../SimpleImageMessage";
import SimpleTextMessage from "../SimpleTextMessage";
import { ChatMessage, MessageType } from "@/_models/ChatMessage";
import StreamTextMessage from "../StreamTextMessage";
import { useAtomValue } from "jotai";
import { currentStreamingMessageAtom } from "@/_helpers/atoms/ChatMessage.atom";
export default function renderChatMessage({
id,
messageType,
messageSenderType,
senderAvatarUrl,
senderName,
createdAt,
imageUrls,
htmlText,
text,
}: ChatMessage): React.ReactNode {
// eslint-disable-next-line react-hooks/rules-of-hooks
const message = useAtomValue(currentStreamingMessageAtom);
switch (messageType) {
case MessageType.ImageWithText:
return (
@ -42,22 +37,14 @@ export default function renderChatMessage({
/>
);
case MessageType.Text:
return id !== message?.id ? (
return (
<SimpleTextMessage
key={id}
avatarUrl={senderAvatarUrl}
senderName={senderName}
createdAt={createdAt}
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
/>
) : (
<StreamTextMessage
key={id}
id={id}
avatarUrl={senderAvatarUrl}
senderName={senderName}
createdAt={createdAt}
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
senderType={messageSenderType}
text={text}
/>
);
default:

View File

@ -63,8 +63,8 @@ const ConfirmDeleteConversationModal: React.FC = () => {
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to delete this conversation? All
of messages will be permanently removed from our servers
forever. This action cannot be undone.
of messages will be permanently removed. This action
cannot be undone.
</p>
</div>
</div>

View File

@ -32,7 +32,7 @@ const ConversationalCard: React.FC<Props> = ({ product }) => {
{description}
</span>
</div>
<span className="flex text-xs leading-5 text-gray-500 items-center gap-[2px]">
<span className="flex text-xs leading-5 text-gray-500 items-center gap-0.5">
<Image src={"icons/play.svg"} width={16} height={16} alt="" />
32.2k runs
</span>

View File

@ -18,19 +18,19 @@ const DownloadModelContent: React.FC<Props> = ({
type,
}) => {
return (
<div className="w-4/5 flex flex-col gap-[10px]">
<div className="w-4/5 flex flex-col gap-2.5">
<div className="flex items-center gap-1">
<h2 className="font-medium text-xl leading-[25px] tracking-[-0.4px] text-gray-900">
{name}
</h2>
<DownloadModelTitle title={type} />
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
<span className="text-xs leading-[18px] font-semibold text-purple-800">
{author}
</span>
</div>
{required && (
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
<span className="text-xs leading-[18px] text-[#11192899]">
Required{" "}
</span>
@ -44,7 +44,7 @@ const DownloadModelContent: React.FC<Props> = ({
<div
className={`${
isRecommend ? "flex" : "hidden"
} w-fit justify-center items-center bg-green-50 rounded-full px-[10px] py-[2px] gap-2`}
} w-fit justify-center items-center bg-green-50 rounded-full px-2.5 py-0.5 gap-2`}
>
<div className="w-3 h-3 rounded-full bg-green-400"></div>
<span className="text-green-600 font-medium text-xs leading-18px">

View File

@ -3,7 +3,7 @@ type Props = {
};
export const DownloadModelTitle: React.FC<Props> = ({ title }) => (
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
<span className="text-xs leading-[18px] font-medium text-purple-800">
{title}
</span>

View File

@ -16,7 +16,7 @@ const DownloadedModelCard: React.FC<Props> = ({
onDeleteClick,
}) => (
<div className="border rounded-lg border-gray-200">
<div className="flex justify-between py-4 px-3 gap-[10px]">
<div className="flex justify-between py-4 px-3 gap-2.5">
<DownloadModelContent
required={required}
author={product.author}

View File

@ -1,4 +1,4 @@
import React, { Fragment } from "react";
import React from "react";
import SearchBar from "../SearchBar";
import ModelTable from "../ModelTable";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
@ -6,14 +6,16 @@ import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
const DownloadedModelTable: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels();
if (!downloadedModels || downloadedModels.length === 0) return null;
return (
<Fragment>
<div className="pl-[63px] pr-[89px]">
<h3 className="text-xl leading-[25px] mt-[50px]">Downloaded Models</h3>
<div className="py-5 w-[568px]">
<SearchBar />
</div>
<ModelTable models={downloadedModels} />
</Fragment>
</div>
);
};

View File

@ -0,0 +1,29 @@
import React, { Fragment } from "react";
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
import { useAtomValue } from "jotai";
import ModelDownloadingTable from "../ModelDownloadingTable";
import { DownloadState } from "@/_models/DownloadState";
const DownloadingModelTable: React.FC = () => {
const modelDownloadState = useAtomValue(modelDownloadStateAtom);
const isAnyModelDownloading = Object.values(modelDownloadState).length > 0;
if (!isAnyModelDownloading) return null;
const downloadStates: DownloadState[] = [];
for (const [, value] of Object.entries(modelDownloadState)) {
downloadStates.push(value);
}
return (
<div className="pl-[63px] pr-[89px]">
<h3 className="text-xl leading-[25px] mt-[50px] mb-4">
Downloading Models
</h3>
<ModelDownloadingTable downloadStates={downloadStates} />
</div>
);
};
export default DownloadingModelTable;

View File

@ -8,7 +8,7 @@ type Props = {
const ExpandableHeader: React.FC<Props> = ({ title, expanded, onClick }) => (
<button onClick={onClick} className="flex items-center justify-between px-2">
<h2 className="text-gray-400 font-bold text-[12px] leading-[12px] pl-1">
<h2 className="text-gray-400 font-bold text-xs leading-[12px] pl-1">
{title}
</h2>
<div className="mr-2">

View File

@ -1,55 +1,20 @@
import useGetAvailableModels from "@/_hooks/useGetAvailableModels";
import ExploreModelItem from "../ExploreModelItem";
import HeaderTitle from "../HeaderTitle";
import SearchBar from "../SearchBar";
import SimpleCheckbox from "../SimpleCheckbox";
import SimpleTag, { TagType } from "../SimpleTag";
import SearchBar, { SearchType } from "../SearchBar";
import ExploreModelList from "../ExploreModelList";
import ExploreModelFilter from "../ExploreModelFilter";
const tags = [
"Roleplay",
"Llama",
"Story",
"Casual",
"Professional",
"CodeLlama",
"Coding",
];
const checkboxs = ["GGUF", "TensorRT", "Meow", "JigglyPuff"];
const ExploreModelContainer: React.FC = () => {
const { allAvailableModels } = useGetAvailableModels();
return (
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
const ExploreModelContainer: React.FC = () => (
<div className="flex flex-col flex-1 px-16 pt-14 overflow-hidden">
<HeaderTitle title="Explore Models" />
<SearchBar placeholder="Search or HuggingFace URL" />
<div className="flex gap-x-14 mt-[38px]">
<div className="flex-1 flex-shrink-0">
<h2 className="font-semibold text-xs mb-[15px]">Tags</h2>
<SearchBar placeholder="Filter by tags" />
<div className="flex flex-wrap gap-[9px] mt-[14px]">
{tags.map((item) => (
<SimpleTag key={item} title={item} type={item as TagType} />
))}
</div>
<hr className="my-10" />
<fieldset>
{checkboxs.map((item) => (
<SimpleCheckbox key={item} name={item} />
))}
</fieldset>
</div>
<div className="flex-[3_3_0%]">
<h2 className="font-semibold text-xs mb-[18px]">Results</h2>
<div className="flex flex-col gap-[31px]">
{allAvailableModels.map((item) => (
<ExploreModelItem key={item.id} model={item} />
))}
<SearchBar
type={SearchType.Model}
placeholder="Owner name like TheBloke, bhlim etc.."
/>
<div className="flex flex-1 gap-x-10 mt-9 overflow-hidden">
<ExploreModelFilter />
<ExploreModelList />
</div>
</div>
</div>
</div>
);
};
);
export default ExploreModelContainer;

View File

@ -0,0 +1,40 @@
import React from "react";
import SearchBar from "../SearchBar";
import SimpleCheckbox from "../SimpleCheckbox";
import SimpleTag, { TagType } from "../SimpleTag";
const tags = [
"Roleplay",
"Llama",
"Story",
"Casual",
"Professional",
"CodeLlama",
"Coding",
];
const checkboxs = ["GGUF", "TensorRT", "Meow", "JigglyPuff"];
const ExploreModelFilter: React.FC = () => {
const enabled = false;
if (!enabled) return null;
return (
<div className="w-64">
<h2 className="font-semibold text-xs mb-[15px]">Tags</h2>
<SearchBar placeholder="Filter by tags" />
<div className="flex flex-wrap gap-[9px] mt-[14px]">
{tags.map((item) => (
<SimpleTag key={item} title={item} type={item as TagType} />
))}
</div>
<hr className="my-10" />
<fieldset>
{checkboxs.map((item) => (
<SimpleCheckbox key={item} name={item} />
))}
</fieldset>
</div>
);
};
export default ExploreModelFilter;

View File

@ -1,36 +1,30 @@
/* eslint-disable react/display-name */
"use client";
import ExploreModelItemHeader from "../ExploreModelItemHeader";
import ModelVersionList from "../ModelVersionList";
import { useMemo, useState } from "react";
import { Product } from "@/_models/Product";
import { Fragment, forwardRef, useState } from "react";
import SimpleTag, { TagType } from "../SimpleTag";
import { displayDate } from "@/_utils/datetime";
import useDownloadModel from "@/_hooks/useDownloadModel";
import { atom, useAtomValue } from "jotai";
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
import { Product } from "@/_models/Product";
type Props = {
model: Product;
};
const ExploreModelItem: React.FC<Props> = ({ model }) => {
const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[model.fileName ?? ""]),
[model.fileName ?? ""]
);
const downloadState = useAtomValue(downloadAtom);
const { downloadModel } = useDownloadModel();
const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
const [show, setShow] = useState(false);
return (
<div className="flex flex-col border border-gray-200 rounded-[5px]">
<div
ref={ref}
className="flex flex-col border border-gray-200 rounded-md mb-4"
>
<ExploreModelItemHeader
name={model.name}
status={TagType.Recommended}
total={model.totalSize}
downloadState={downloadState}
onDownloadClick={() => downloadModel(model)}
versions={model.availableVersions}
/>
<div className="flex flex-col px-[26px] py-[22px]">
<div className="flex justify-between">
@ -39,7 +33,7 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
<div className="text-sm font-medium text-gray-500">
Model Format
</div>
<div className="px-[10px] py-0.5 bg-gray-100 text-xs text-gray-800 w-fit">
<div className="px-2.5 py-0.5 bg-gray-100 text-xs text-gray-800 w-fit">
GGUF
</div>
</div>
@ -87,15 +81,24 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
<span className="text-sm font-medium text-gray-500">Tags</span>
</div>
</div>
{show && <ModelVersionList />}
{model.availableVersions.length > 0 && (
<Fragment>
{show && (
<ModelVersionList
model={model}
versions={model.availableVersions}
/>
)}
<button
onClick={() => setShow(!show)}
className="bg-[#FBFBFB] text-gray-500 text-sm text-left py-2 px-4 border-t border-gray-200"
>
{!show ? "+ Show Available Versions" : "- Collapse"}
</button>
</Fragment>
)}
</div>
);
};
});
export default ExploreModelItem;

View File

@ -3,11 +3,13 @@ import PrimaryButton from "../PrimaryButton";
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
import { DownloadState } from "@/_models/DownloadState";
import SecondaryButton from "../SecondaryButton";
import { ModelVersion } from "@/_models/Product";
type Props = {
name: string;
total: number;
status: TagType;
versions: ModelVersion[];
size?: number;
downloadState?: DownloadState;
onDownloadClick?: () => void;
};
@ -15,30 +17,41 @@ type Props = {
const ExploreModelItemHeader: React.FC<Props> = ({
name,
status,
total,
size,
versions,
downloadState,
onDownloadClick,
}) => (
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<div className="flex items-center gap-2">
<span>{name}</span>
<SimpleTag title={status} type={status} clickable={false} />
</div>
{downloadState != null ? (
}) => {
let downloadButton = (
<PrimaryButton
title={size ? `Download (${toGigabytes(size)})` : "Download"}
onClick={() => onDownloadClick?.()}
/>
);
if (downloadState != null) {
// downloading
downloadButton = (
<SecondaryButton
disabled
title={`Downloading (${formatDownloadPercentage(
downloadState.percent
)})`}
onClick={() => {}}
/>
) : (
<PrimaryButton
title={total ? `Download (${toGigabytes(total)})` : "Download"}
onClick={() => onDownloadClick?.()}
/>
)}
);
} else if (versions.length === 0) {
downloadButton = <SecondaryButton disabled title="No files available" />;
}
return (
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<div className="flex items-center gap-2">
<span>{name}</span>
<SimpleTag title={status} type={status} clickable={false} />
</div>
);
{downloadButton}
</div>
);
};
export default ExploreModelItemHeader;

View File

@ -0,0 +1,52 @@
import React, { useEffect } from "react";
import ExploreModelItem from "../ExploreModelItem";
import { modelSearchAtom } from "@/_helpers/JotaiWrapper";
import useGetHuggingFaceModel from "@/_hooks/useGetHuggingFaceModel";
import { useAtom, useAtomValue } from "jotai";
import { useInView } from "react-intersection-observer";
import { modelLoadMoreAtom } from "@/_helpers/atoms/ExploreModelLoading.atom";
import { Waveform } from "@uiball/loaders";
const ExploreModelList: React.FC = () => {
const [loadMoreInProgress, setLoadMoreInProress] = useAtom(modelLoadMoreAtom);
const modelSearch = useAtomValue(modelSearchAtom);
const { modelList, getHuggingFaceModel } = useGetHuggingFaceModel();
const { ref, inView } = useInView({
threshold: 0,
triggerOnce: true,
});
useEffect(() => {
if (modelList.length === 0 && modelSearch.length > 0) {
setLoadMoreInProress(true);
}
getHuggingFaceModel(modelSearch);
}, [modelSearch]);
useEffect(() => {
if (inView) {
console.debug("Load more models..");
setLoadMoreInProress(true);
getHuggingFaceModel(modelSearch);
}
}, [inView]);
return (
<div className="flex flex-col flex-1 overflow-y-auto scroll">
{modelList.map((item, index) => (
<ExploreModelItem
ref={index === modelList.length - 1 ? ref : null}
key={item.id}
model={item}
/>
))}
{loadMoreInProgress && (
<div className="mx-auto mt-2 mb-4">
<Waveform size={24} color="#9CA3AF" />
</div>
)}
</div>
);
};
export default ExploreModelList;

View File

@ -1,11 +1,14 @@
import React from 'react';
import React from "react";
type Props = {
title: string;
className?: string;
};
const HeaderTitle: React.FC<Props> = ({ title }) => (
<h2 className="my-5 font-semibold text-[34px] tracking-[-0.4px] leading-[41px]">
const HeaderTitle: React.FC<Props> = ({ title, className }) => (
<h2
className={`my-5 font-semibold text-[34px] tracking-[-0.4px] leading-[41px] ${className}`}
>
{title}
</h2>
);

View File

@ -66,7 +66,7 @@ const HistoryItem: React.FC<Props> = ({
return (
<button
className={`flex flex-row mx-1 items-center gap-[10px] rounded-lg p-2 ${backgroundColor} hover:bg-hover-light`}
className={`flex flex-row mx-1 items-center gap-2.5 rounded-lg p-2 ${backgroundColor} hover:bg-hover-light`}
onClick={onClick}
>
<Image
@ -79,7 +79,7 @@ const HistoryItem: React.FC<Props> = ({
<div className="flex flex-col justify-between text-sm leading-[20px] w-full">
<div className="flex flex-row items-center justify-between">
<span className="text-gray-900 text-left">{name}</span>
<span className="text-[11px] leading-[13px] tracking-[-0.4px] text-gray-400">
<span className="text-xs leading-[13px] tracking-[-0.4px] text-gray-400">
{updatedAt && new Date(updatedAt).toDateString()}
</span>
</div>

View File

@ -18,14 +18,14 @@ const HistoryList: React.FC = () => {
}, []);
return (
<div className="flex flex-col flex-grow pt-3 gap-2">
<div className="flex flex-col flex-grow pt-3 gap-2 overflow-hidden">
<ExpandableHeader
title="CHAT HISTORY"
expanded={expand}
onClick={() => setExpand(!expand)}
/>
<div
className={`flex flex-col gap-1 mt-1 ${!expand ? "hidden " : "block"}`}
className={`flex flex-col gap-1 mt-1 overflow-y-auto scroll ${!expand ? "hidden " : "block"}`}
>
{conversations.length > 0 ? (
conversations

View File

@ -4,19 +4,56 @@ import BasicPromptInput from "../BasicPromptInput";
import BasicPromptAccessories from "../BasicPromptAccessories";
import { useAtomValue } from "jotai";
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
import SecondaryButton from "../SecondaryButton";
import { Fragment } from "react";
import { PlusIcon } from "@heroicons/react/24/outline";
import useCreateConversation from "@/_hooks/useCreateConversation";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import { showingTyping } from "@/_helpers/JotaiWrapper";
import LoadingIndicator from "../LoadingIndicator";
const InputToolbar: React.FC = () => {
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom);
const currentProduct = useAtomValue(currentProductAtom);
const { requestCreateConvo } = useCreateConversation();
const isTyping = useAtomValue(showingTyping);
if (showingAdvancedPrompt) {
return <div />;
}
// TODO: implement regenerate
// const onRegenerateClick = () => {};
const onNewConversationClick = () => {
if (currentProduct) {
requestCreateConvo(currentProduct);
}
};
return (
<Fragment>
<div className="flex justify-between gap-2 mr-3 my-2">
<div className="h-6">
{isTyping && (
<div className="my-2" key="indicator">
<LoadingIndicator />
</div>
)}{" "}
</div>
{/* <SecondaryButton title="Regenerate" onClick={onRegenerateClick} /> */}
<SecondaryButton
onClick={onNewConversationClick}
title="New Conversation"
icon={<PlusIcon width={16} height={16} />}
/>
</div>
<div className="mx-3 mb-3 flex-none overflow-hidden shadow-sm ring-1 ring-inset ring-gray-300 rounded-lg dark:bg-gray-800">
<BasicPromptInput />
<BasicPromptAccessories />
</div>
</Fragment>
);
};

View File

@ -7,7 +7,7 @@ const JanLogo: React.FC = () => {
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
return (
<button
className="p-3 flex gap-[2px] items-center"
className="p-3 flex gap-0.5 items-center"
onClick={() => setActiveConvoId(undefined)}
>
<Image src={"icons/app_icon.svg"} width={28} height={28} alt="" />

View File

@ -6,7 +6,7 @@ import HistoryList from "../HistoryList";
import NewChatButton from "../NewChatButton";
const LeftContainer: React.FC = () => (
<div className="w-[323px] flex-shrink-0 p-3 h-screen border-r border-gray-200 flex flex-col">
<div className="w-[323px] flex-shrink-0 py-3 h-screen border-r border-gray-200 flex flex-col">
<SidebarHeader />
<NewChatButton />
<HistoryList />

View File

@ -1,12 +1,6 @@
const LoadingIndicator = () => {
let circleCommonClasses = "h-1.5 w-1.5 bg-current rounded-full";
return (
// <div className="flex">
// <div className={`${circleCommonClasses} mr-1 animate-bounce`}></div>
// <div className={`${circleCommonClasses} mr-1 animate-bounce200`}></div>
// <div className={`${circleCommonClasses} animate-bounce400`}></div>
// </div>
<div className="typingIndicatorContainer">
<div className="typingIndicatorBubble">
<div className="typingIndicatorBubbleDot"></div>

View File

@ -12,7 +12,7 @@ const LoginButton: React.FC = () => {
// <button
// onClick={signInWithKeyCloak}
// type="button"
// className="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
// className="rounded-md bg-blue-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
// >
// Login
// </button>

View File

@ -1,7 +1,6 @@
"use client";
import { useAtomValue } from "jotai";
import { ReactNode } from "react";
import Welcome from "../WelcomeContainer";
import { Preferences } from "../Preferences";
import MyModelContainer from "../MyModelContainer";
@ -11,17 +10,14 @@ import {
getMainViewStateAtom,
} from "@/_helpers/atoms/MainView.atom";
import EmptyChatContainer from "../EmptyChatContainer";
import MainChat from "../MainChat";
type Props = {
children: ReactNode;
};
export default function ChatContainer({ children }: Props) {
const MainView: React.FC = () => {
const viewState = useAtomValue(getMainViewStateAtom);
switch (viewState) {
case MainViewState.ConversationEmptyModel:
return <EmptyChatContainer />
return <EmptyChatContainer />;
case MainViewState.ExploreModel:
return <ExploreModelContainer />;
case MainViewState.Setting:
@ -32,6 +28,8 @@ export default function ChatContainer({ children }: Props) {
case MainViewState.Welcome:
return <Welcome />;
default:
return <div className="flex flex-1 overflow-hidden">{children}</div>;
return <MainChat />;
}
}
};
export default MainView;

View File

@ -38,7 +38,7 @@ const ModelActionButton: React.FC<Props> = ({ type, onActionClick }) => {
return (
<td className="whitespace-nowrap px-6 py-4 text-sm">
<PrimaryButton title={styles.title} onClick={onClick} />
<PrimaryButton title={styles.title} onClick={onClick} className={styles.backgroundColor} />
</td>
);
};

View File

@ -6,8 +6,7 @@ type Props = {
onDeleteClick: () => void;
};
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => {
return (
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => (
<Menu as="div" className="relative flex-none">
<Menu.Button className="block text-gray-500 hover:text-gray-900">
<span className="sr-only">Open options</span>
@ -22,14 +21,22 @@ const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
<Menu.Items className="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
<Menu.Item>
<button onClick={onDeleteClick}>Delete</button>
{({ active }) => (
<button
className={`${
active ? "bg-violet-500 text-white" : "text-gray-900"
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={onDeleteClick}
>
Delete
</button>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
);
};
);
export default ModelActionMenu;

View File

@ -11,7 +11,7 @@ const ModelDownloadingButton: React.FC<Props> = ({ total, value }) => {
<button className="py-2 px-3 flex gap-2 border text-xs leading-[18px] border-gray-200 rounded-lg">
Downloading...
</button>
<div className="py-[2px] px-[10px] bg-gray-200 rounded">
<div className="py-0.5 px-2.5 bg-gray-200 rounded">
<span className="text-xs font-medium text-gray-800">
{toGigabytes(value)} / {toGigabytes(total)}
</span>

View File

@ -0,0 +1,36 @@
import React from "react";
import { DownloadState } from "@/_models/DownloadState";
import {
formatDownloadPercentage,
formatDownloadSpeed,
toGigabytes,
} from "@/_utils/converter";
type Props = {
downloadState: DownloadState;
};
const ModelDownloadingRow: React.FC<Props> = ({ downloadState }) => (
<tr
className="border-b border-gray-200 last:border-b-0 last:rounded-lg"
key={downloadState.fileName}
>
<td className="flex flex-col whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900">
{downloadState.fileName}
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{toGigabytes(downloadState.size.transferred)}
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{toGigabytes(downloadState.size.total)}
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{formatDownloadPercentage(downloadState.percent)}
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{formatDownloadSpeed(downloadState.speed)}
</td>
</tr>
);
export default ModelDownloadingRow;

View File

@ -0,0 +1,34 @@
import React from "react";
import ModelTableHeader from "../ModelTableHeader";
import { DownloadState } from "@/_models/DownloadState";
import ModelDownloadingRow from "../ModelDownloadingRow";
type Props = {
downloadStates: DownloadState[];
};
const tableHeaders = ["MODEL", "TRANSFERRED", "SIZE", "PERCENTAGE", "SPEED"];
const ModelDownloadingTable: React.FC<Props> = ({ downloadStates }) => (
<div className="flow-root border rounded-lg border-gray-200 min-w-full align-middle shadow-lg">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr className="rounded-t-lg">
{tableHeaders.map((item) => (
<ModelTableHeader key={item} title={item} />
))}
<th scope="col" className="relative px-6 py-3 w-fit">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody>
{downloadStates.map((state) => (
<ModelDownloadingRow key={state.fileName} downloadState={state} />
))}
</tbody>
</table>
</div>
);
export default React.memo(ModelDownloadingTable);

View File

@ -1,86 +0,0 @@
"use client";
import { useAtomValue } from "jotai";
import { searchingModelText } from "@/_helpers/JotaiWrapper";
import { Product } from "@/_models/Product";
import DownloadedModelCard from "../DownloadedModelCard";
import AvailableModelCard from "../AvailableModelCard";
import useDeleteModel from "@/_hooks/useDeleteModel";
import useGetAvailableModels from "@/_hooks/useGetAvailableModels";
import useDownloadModel from "@/_hooks/useDownloadModel";
const ModelListContainer: React.FC = () => {
const searchText = useAtomValue(searchingModelText);
const { deleteModel } = useDeleteModel();
const { downloadModel } = useDownloadModel();
const {
availableModels,
downloadedModels,
getAvailableModelExceptDownloaded,
} = useGetAvailableModels();
const onDeleteClick = async (product: Product) => {
await deleteModel(product);
await getAvailableModelExceptDownloaded();
};
const onDownloadClick = async (model: Product) => {
await downloadModel(model);
};
return (
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
<div className="pb-5 flex flex-col gap-2">
<Title title="Downloaded models" />
{downloadedModels
?.filter(
(e) =>
searchText.toLowerCase().trim() === "" ||
e.name.toLowerCase().includes(searchText.toLowerCase())
)
.map((item) => (
<DownloadedModelCard
key={item.id}
product={item}
onDeleteClick={onDeleteClick}
isRecommend={false}
/>
))}
</div>
<div className="pb-5 flex flex-col gap-2">
<Title title="Browse available models" />
{availableModels
?.filter(
(e) =>
searchText.toLowerCase().trim() === "" ||
e.name.toLowerCase().includes(searchText.toLowerCase())
)
.map((item) => (
<AvailableModelCard
key={item.id}
product={item}
onDownloadClick={onDownloadClick}
isRecommend={false}
/>
))}
</div>
</div>
);
};
type Props = {
title: string;
};
const Title: React.FC<Props> = ({ title }) => {
return (
<div className="flex gap-[10px]">
<span className="font-semibold text-xl leading-[25px] tracking-[-0.4px]">
{title}
</span>
</div>
);
};
export default ModelListContainer;

View File

@ -1,15 +0,0 @@
import HeaderBackButton from "../HeaderBackButton";
import HeaderTitle from "../HeaderTitle";
import ModelListContainer from "../ModelListContainer";
import ModelSearchBar from "../ModelSearchBar";
export default function ModelManagement() {
return (
<main className="pt-[30px] pr-[89px] pl-[60px] pb-[70px] flex-1">
{/* <HeaderBackButton /> */}
<HeaderTitle title="Explore Models" />
<ModelSearchBar />
<ModelListContainer />
</main>
);
}

View File

@ -1,29 +1,16 @@
"use client";
import { useAtomValue, useSetAtom } from "jotai";
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
import useCreateConversation from "@/_hooks/useCreateConversation";
import { useSetAtom } from "jotai";
import { TrashIcon } from "@heroicons/react/24/outline";
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
const ModelMenu: React.FC = () => {
const currentProduct = useAtomValue(currentProductAtom);
const { requestCreateConvo } = useCreateConversation();
const setShowConfirmDeleteConversationModal = useSetAtom(
showConfirmDeleteConversationModalAtom
);
const onCreateConvoClick = () => {
if (currentProduct) {
requestCreateConvo(currentProduct);
}
};
return (
<div className="flex items-center gap-3">
<button onClick={() => onCreateConvoClick()}>
<PlusIcon width={24} height={24} color="#9CA3AF" />
</button>
<button onClick={() => setShowConfirmDeleteConversationModal(true)}>
<TrashIcon width={24} height={24} color="#9CA3AF" />
</button>

View File

@ -14,7 +14,7 @@ type Props = {
};
const ModelRow: React.FC<Props> = ({ model }) => {
const { startModel } = useStartStopModel();
const { startModel, stopModel } = useStartStopModel();
const activeModel = useAtomValue(currentProductAtom);
const { deleteModel } = useDeleteModel();
@ -31,6 +31,8 @@ const ModelRow: React.FC<Props> = ({ model }) => {
const onModelActionClick = (action: ModelActionType) => {
if (action === ModelActionType.Start) {
startModel(model.id);
} else {
stopModel(model.id);
}
};

View File

@ -1,17 +1,17 @@
import { Fragment, useEffect } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
import { Product } from "@/_models/Product";
import { useAtom } from "jotai";
import { useAtom, useAtomValue } from "jotai";
import { selectedModelAtom } from "@/_helpers/atoms/Model.atom";
import { downloadedModelAtom } from "@/_helpers/atoms/DownloadedModel.atom";
function classNames(...classes: any) {
return classes.filter(Boolean).join(" ");
}
const SelectModels: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels();
const downloadedModels = useAtomValue(downloadedModelAtom);
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom);
useEffect(() => {
@ -36,7 +36,7 @@ const SelectModels: React.FC = () => {
Select a Model:
</Listbox.Label>
<div className="relative mt-[19px]">
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 sm:text-sm sm:leading-6">
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-sm sm:leading-6">
<span className="flex items-center">
<img
src={selectedModel.avatarUrl}
@ -68,8 +68,8 @@ const SelectModels: React.FC = () => {
key={model.id}
className={({ active }) =>
classNames(
active ? "bg-indigo-600 text-white" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9"
active ? "bg-blue-600 text-white" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9",
)
}
value={model}
@ -85,7 +85,7 @@ const SelectModels: React.FC = () => {
<span
className={classNames(
selected ? "font-semibold" : "font-normal",
"ml-3 block truncate"
"ml-3 block truncate",
)}
>
{model.name}
@ -95,8 +95,8 @@ const SelectModels: React.FC = () => {
{selected ? (
<span
className={classNames(
active ? "text-white" : "text-indigo-600",
"absolute inset-y-0 right-0 flex items-center pr-4"
active ? "text-white" : "text-blue-600",
"absolute inset-y-0 right-0 flex items-center pr-4",
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />

View File

@ -38,7 +38,7 @@ export const ModelStatusComponent: React.FC<Props> = ({ status }) => {
const statusType = ModelStatusMapper[status];
return (
<div
className={`rounded-[10px] py-0.5 px-[10px] w-fit text-xs font-medium ${statusType.backgroundColor}`}
className={`rounded-[10px] py-0.5 px-2.5 w-fit text-xs font-medium ${statusType.backgroundColor}`}
>
{statusType.title}
</div>

View File

@ -10,7 +10,7 @@ type Props = {
const tableHeaders = ["MODEL", "FORMAT", "SIZE", "STATUS", "ACTIONS"];
const ModelTable: React.FC<Props> = ({ models }) => (
<div className="flow-root inline-block border rounded-lg border-gray-200 min-w-full align-middle shadow-lg">
<div className="flow-root border rounded-lg border-gray-200 min-w-full align-middle shadow-lg">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr className="rounded-t-lg">

View File

@ -1,25 +1,57 @@
import React from "react";
import { toGigabytes } from "@/_utils/converter";
import React, { useMemo } from "react";
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
import Image from "next/image";
import { ModelVersion, Product } from "@/_models/Product";
import useDownloadModel from "@/_hooks/useDownloadModel";
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
import { atom, useAtomValue } from "jotai";
type Props = {
title: string;
totalSizeInByte: number;
model: Product;
modelVersion: ModelVersion;
};
const ModelVersionItem: React.FC<Props> = ({ title, totalSizeInByte }) => (
<div className="flex justify-between items-center gap-4 pl-[13px] pt-[13px] pr-[17px] pb-3 border-t border-gray-200 first:border-t-0">
const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
const { downloadHfModel } = useDownloadModel();
const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[modelVersion.path ?? ""]),
[modelVersion.path ?? ""]
);
const downloadState = useAtomValue(downloadAtom);
const onDownloadClick = () => {
downloadHfModel(model, modelVersion);
};
let downloadButton = (
<button
className="text-indigo-600 text-sm font-medium"
onClick={onDownloadClick}
>
Download
</button>
);
if (downloadState) {
downloadButton = (
<div>{formatDownloadPercentage(downloadState.percent)}</div>
);
}
return (
<div className="flex justify-between items-center gap-4 pl-3 pt-3 pr-4 pb-3 border-t border-gray-200 first:border-t-0">
<div className="flex items-center gap-4">
<Image src={"/icons/app_icon.svg"} width={14} height={20} alt="" />
<span className="font-sm text-gray-900">{title}</span>
<span className="font-sm text-gray-900">{modelVersion.path}</span>
</div>
<div className="flex items-center gap-4">
<div className="px-[10px] py-0.5 bg-gray-200 text-xs font-medium rounded">
{toGigabytes(totalSizeInByte)}
<div className="px-2.5 py-0.5 bg-gray-200 text-xs font-medium rounded">
{toGigabytes(modelVersion.size)}
</div>
<button className="text-indigo-600 text-sm font-medium">Download</button>
{downloadButton}
</div>
</div>
);
);
};
export default ModelVersionItem;

View File

@ -1,38 +1,21 @@
import React from "react";
import ModelVersionItem from "../ModelVersionItem";
import { ModelVersion, Product } from "@/_models/Product";
const data = [
{
name: "Q4_K_M.gguf",
total: 5600,
},
{
name: "Q4_K_M.gguf",
total: 5600,
},
{
name: "Q4_K_M.gguf",
total: 5600,
},
];
type Props = {
model: Product;
versions: ModelVersion[];
};
const ModelVersionList: React.FC = () => {
return (
const ModelVersionList: React.FC<Props> = ({ model, versions }) => (
<div className="px-4 py-5 border-t border-gray-200">
<div className="text-sm font-medium text-gray-500">
Available Versions
</div>
<div className="text-sm font-medium text-gray-500">Available Versions</div>
<div className="border border-gray-200 rounded-lg overflow-hidden">
{data.map((item, index) => (
<ModelVersionItem
key={index}
title={item.name}
totalSizeInByte={item.total}
/>
{versions.map((item) => (
<ModelVersionItem key={item.path} model={model} modelVersion={item} />
))}
</div>
</div>
);
};
);
export default ModelVersionList;

View File

@ -2,63 +2,43 @@ import ProgressBar from "../ProgressBar";
import SystemItem from "../SystemItem";
import { useAtomValue } from "jotai";
import { appDownloadProgress } from "@/_helpers/JotaiWrapper";
import { useEffect, useState } from "react";
import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager";
import { SystemMonitoringService } from "../../../shared/coreService";
import { getSystemBarVisibilityAtom } from "@/_helpers/atoms/SystemBar.atom";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import useGetAppVersion from "@/_hooks/useGetAppVersion";
import useGetSystemResources from "@/_hooks/useGetSystemResources";
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
import { DownloadState } from "@/_models/DownloadState";
import { formatDownloadPercentage } from "@/_utils/converter";
const MonitorBar: React.FC = () => {
const show = useAtomValue(getSystemBarVisibilityAtom);
const progress = useAtomValue(appDownloadProgress);
const activeModel = useAtomValue(currentProductAtom);
const [ram, setRam] = useState<number>(0);
const [gpu, setGPU] = useState<number>(0);
const [cpu, setCPU] = useState<number>(0);
const [version, setVersion] = useState<string>("");
const { version } = useGetAppVersion();
const { ram, cpu } = useGetSystemResources();
const modelDownloadStates = useAtomValue(modelDownloadStateAtom);
useEffect(() => {
const getSystemResources = async () => {
const resourceInfor = await executeSerial(
SystemMonitoringService.GET_RESOURCES_INFORMATION
);
const currentLoadInfor = await executeSerial(
SystemMonitoringService.GET_CURRENT_LOAD_INFORMATION
);
const ram =
(resourceInfor?.mem?.used ?? 0) / (resourceInfor?.mem?.total ?? 1);
setRam(Math.round(ram * 100));
setCPU(Math.round(currentLoadInfor?.currentLoad ?? 0));
};
const getAppVersion = () => {
window.electronAPI.appVersion().then((version: string | undefined) => {
setVersion(version ?? "");
});
};
getAppVersion();
getSystemResources();
// Fetch interval - every 3s
const intervalId = setInterval(() => {
getSystemResources();
}, 3000);
return () => clearInterval(intervalId);
}, []);
if (!show) return null;
const downloadStates: DownloadState[] = [];
for (const [, value] of Object.entries(modelDownloadStates)) {
downloadStates.push(value);
}
return (
<div className="flex flex-row items-center justify-between border-t border-gray-200">
{progress && progress >= 0 ? (
<ProgressBar total={100} used={progress} />
) : (
<div className="w-full" />
) : null}
<div className="flex-1 justify-end flex items-center gap-8 px-2">
{downloadStates.length > 0 && (
<SystemItem
name="Downloading"
value={`${downloadStates[0].fileName}: ${formatDownloadPercentage(
downloadStates[0].percent
)}`}
/>
)}
<div className="flex-1 flex items-center gap-8 px-2">
<SystemItem name="CPU" value={`${cpu}%`} />
<SystemItem name="Mem" value={`${ram}%`} />
{activeModel && (
<SystemItem name={`Active model: ${activeModel.name}`} value={"1"} />
<SystemItem name={`Active model: ${activeModel.name}`} value={""} />
)}
<span className="text-gray-900 text-sm">v{version}</span>
</div>

View File

@ -1,13 +1,17 @@
import HeaderTitle from "../HeaderTitle";
import DownloadedModelTable from "../DownloadedModelTable";
import ActiveModelTable from "../ActiveModelTable";
import DownloadingModelTable from "../DownloadingModelTable";
const MyModelContainer: React.FC = () => (
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px]">
<HeaderTitle title="My Models" />
<div className="flex flex-col flex-1 pt-[60px]">
<HeaderTitle title="My Models" className="pl-[63px] pr-[89px]" />
<div className="pb-6 overflow-y-auto scroll">
<ActiveModelTable />
<DownloadingModelTable />
<DownloadedModelTable />
</div>
</div>
);
export default MyModelContainer;

View File

@ -11,6 +11,7 @@ import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import useCreateConversation from "@/_hooks/useCreateConversation";
import useInitModel from "@/_hooks/useInitModel";
import { Product } from "@/_models/Product";
import { PlusIcon } from "@heroicons/react/24/outline";
const NewChatButton: React.FC = () => {
const activeModel = useAtomValue(currentProductAtom);
@ -32,8 +33,13 @@ const NewChatButton: React.FC = () => {
};
return (
<SecondaryButton title={"New Chat"} onClick={onClick} className="my-5" />
<SecondaryButton
title={"New Chat"}
onClick={onClick}
className="my-5 mx-3"
icon={<PlusIcon width={16} height={16} />}
/>
);
};
export default NewChatButton;
export default React.memo(NewChatButton);

View File

@ -172,15 +172,30 @@ export const Preferences = () => {
/>
</label>
</div>
<div className="flex flex-col space-y-2">
<button
type="submit"
className={classNames(
"rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
fileName ? "bg-indigo-600 hover:bg-indigo-500" : "bg-gray-500"
fileName
? "bg-blue-500 hover:bg-blue-300"
: "bg-gray-500"
)}
>
Install Plugin
</button>
<button
className={classNames(
"bg-blue-500 hover:bg-blue-300 rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
)}
onClick={() => {
window.electronAPI.reloadPlugins();
}}
>
Reload Plugins
</button>
</div>
</div>
</form>

View File

@ -4,17 +4,19 @@ type Props = {
title: string;
onClick: () => void;
fullWidth?: boolean;
className?: string;
};
const PrimaryButton: React.FC<Props> = ({
title,
onClick,
fullWidth = false,
className,
}) => (
<button
onClick={onClick}
type="button"
className={`rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-50 ${
className={`rounded-md bg-blue-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-50 line-clamp-1 flex-shrink-0 ${className} ${
fullWidth ? "flex-1 " : ""
}}`}
>

View File

@ -5,9 +5,8 @@ type Props = {
used: number;
};
const ProgressBar: React.FC<Props> = ({ used, total }) => {
return (
<div className="flex gap-[10px] items-center p-[10px]">
const ProgressBar: React.FC<Props> = ({ used, total }) => (
<div className="flex gap-2.5 items-center p-[10px]">
<div className="text-xs leading-[18px] gap-0.5 flex items-center">
<Image src={"icons/app_icon.svg"} width={18} height={18} alt="" />
Updating
@ -22,7 +21,6 @@ const ProgressBar: React.FC<Props> = ({ used, total }) => {
{((used / total) * 100).toFixed(0)}%
</div>
</div>
);
};
);
export default ProgressBar;

View File

@ -1,12 +1,9 @@
import ChatContainer from "../ChatContainer";
import MainChat from "../MainChat";
import MainView from "../MainView";
import MonitorBar from "../MonitorBar";
const RightContainer = () => (
<div className="flex flex-col flex-1 h-screen">
<ChatContainer>
<MainChat />
</ChatContainer>
<MainView />
<MonitorBar />
</div>
);

View File

@ -1,14 +1,25 @@
import { searchAtom } from "@/_helpers/JotaiWrapper";
import { modelSearchAtom } from "@/_helpers/JotaiWrapper";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useSetAtom } from "jotai";
import { useDebouncedCallback } from "use-debounce";
export enum SearchType {
Model = "model",
}
type Props = {
type?: SearchType;
placeholder?: string;
};
const SearchBar: React.FC<Props> = ({ placeholder }) => {
const setText = useSetAtom(searchAtom);
const SearchBar: React.FC<Props> = ({ type, placeholder }) => {
const setModelSearch = useSetAtom(modelSearchAtom);
let placeholderText = placeholder ? placeholder : "Search (⌘K)";
const debounced = useDebouncedCallback((value) => {
setModelSearch(value);
}, 300);
return (
<div className="relative mt-3 flex items-center">
<div className="absolute top-0 left-2 h-full flex items-center">
@ -24,7 +35,7 @@ const SearchBar: React.FC<Props> = ({ placeholder }) => {
name="search"
id="search"
placeholder={placeholderText}
onChange={(e) => setText(e.target.value)}
onChange={(e) => debounced(e.target.value)}
className="block w-full rounded-md border-0 py-1.5 pl-8 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>

View File

@ -1,8 +1,11 @@
import React from "react";
type Props = {
title: string;
onClick: () => void;
onClick?: () => void;
disabled?: boolean;
className?: string;
icon?: React.ReactNode;
};
const SecondaryButton: React.FC<Props> = ({
@ -10,15 +13,17 @@ const SecondaryButton: React.FC<Props> = ({
onClick,
disabled,
className,
icon,
}) => (
<button
disabled={disabled}
type="button"
onClick={onClick}
className={`rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${className}`}
className={`flex items-center justify-center gap-1 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${className} flex-shrink-0 line-clamp-1`}
>
{icon}
{title}
</button>
);
export default SecondaryButton;
export default React.memo(SecondaryButton);

View File

@ -1,8 +1,8 @@
import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
import useSendChatMessage from "@/_hooks/useSendChatMessage";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
import { useAtom, useAtomValue } from "jotai";
import Image from "next/image";
const SendButton: React.FC = () => {
const [currentPrompt] = useAtom(currentPromptAtom);
@ -25,9 +25,9 @@ const SendButton: React.FC = () => {
onClick={sendChatMessage}
style={disabled ? disabledStyle : enabledStyle}
type="submit"
className="p-2 gap-[10px] inline-flex items-center rounded-[12px] text-sm font-semibold shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
className="p-2 gap-2.5 inline-flex items-center rounded-xl text-sm font-semibold shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
<Image src={"icons/ic_arrowright.svg"} width={24} height={24} alt="" />
<ArrowRightIcon width={16} height={16} />
</button>
);
};

View File

@ -2,7 +2,6 @@ import Image from "next/image";
import useCreateConversation from "@/_hooks/useCreateConversation";
import PrimaryButton from "../PrimaryButton";
import { useAtomValue, useSetAtom } from "jotai";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
import { useEffect, useState } from "react";
import {
MainViewState,
@ -11,6 +10,7 @@ import {
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import useInitModel from "@/_hooks/useInitModel";
import { Product } from "@/_models/Product";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
enum ActionButton {
DownloadModel = "Download a Model",

View File

@ -2,7 +2,7 @@ import React from "react";
import SecondaryButton from "../SecondaryButton";
const SidebarFooter: React.FC = () => (
<div className="flex justify-between items-center gap-2">
<div className="flex justify-between items-center gap-2 mx-3">
<SecondaryButton
title={"Discord"}
onClick={() =>
@ -11,7 +11,7 @@ const SidebarFooter: React.FC = () => (
className="flex-1"
/>
<SecondaryButton
title={"Discord"}
title={"Twitter"}
onClick={() =>
window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai")
}

View File

@ -1,11 +1,10 @@
import React from "react";
import Image from "next/image";
const SidebarHeader: React.FC = () => {
return (
<div className="flex flex-col gap-[10px]">
const SidebarHeader: React.FC = () => (
<div className="flex flex-col gap-2.5 px-3">
<Image src={"icons/Jan_AppIcon.svg"} width={68} height={28} alt="" />
</div>
);
};
);
export default SidebarHeader;
export default React.memo(SidebarHeader);

View File

@ -21,11 +21,7 @@ const menu = [
];
const SidebarMenu: React.FC = () => (
<div className="flex flex-col">
<div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3">
Your Configurations
</div>
<ul role="list" className="-mx-2 mt-2 space-y-1 mb-2">
<ul role="list" className="mx-1 mt-2 space-y-1 mb-2">
{menu.map((item) => (
<SidebarMenuItem
title={item.name}
@ -35,7 +31,6 @@ const SidebarMenu: React.FC = () => (
/>
))}
</ul>
</div>
);
export default React.memo(SidebarMenu);

View File

@ -30,15 +30,15 @@ const SimpleControlNetMessage: React.FC<Props> = ({
/>
<div className="flex flex-col gap-1">
<div className="flex gap-1 justify-start items-baseline">
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px]">
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px]">
{senderName}
</div>
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400 ml-2">
<div className="text-xs leading-[13.2px] font-medium text-gray-400 ml-2">
{displayDate(createdAt)}
</div>
</div>
<div className="flex gap-3 flex-col">
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
{text}
</p>
<JanImage
@ -49,7 +49,7 @@ const SimpleControlNetMessage: React.FC<Props> = ({
<Link
href={imageUrls[0] || "#"}
target="_blank_"
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
>
<Image src="icons/download.svg" width={16} height={16} alt="" />
<span className="leading-[20px] text-[14px] text-[#111928]">

View File

@ -30,10 +30,10 @@ const SimpleImageMessage: React.FC<Props> = ({
/>
<div className="flex flex-col gap-1">
<div className="flex gap-1 justify-start items-baseline">
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px]">
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px]">
{senderName}
</div>
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400 ml-2">
<div className="text-xs leading-[13.2px] font-medium text-gray-400 ml-2">
{displayDate(createdAt)}
</div>
</div>
@ -46,19 +46,19 @@ const SimpleImageMessage: React.FC<Props> = ({
<Link
href={imageUrls[0] || "#"}
target="_blank_"
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
>
<Image src="icons/download.svg" width={16} height={16} alt="" />
<span className="leading-[20px] text-[14px] text-[#111928]">
<span className="leading-[20px] text-sm text-[#111928]">
Download
</span>
</Link>
<button
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
// onClick={() => sendChatMessage()}
>
<Image src="icons/refresh.svg" width={16} height={16} alt="" />
<span className="leading-[20px] text-[14px] text-[#111928]">
<span className="leading-[20px] text-sm text-[#111928]">
Re-generate
</span>
</button>

View File

@ -69,7 +69,7 @@ const SimpleTag: React.FC<Props> = ({
if (!clickable) {
return (
<div
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
className={`px-2.5 py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
>
{title}
</div>
@ -79,7 +79,7 @@ const SimpleTag: React.FC<Props> = ({
return (
<button
onClick={onClick}
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
className={`px-2.5 py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
>
{title} x
</button>

View File

@ -3,21 +3,30 @@ import { displayDate } from "@/_utils/datetime";
import { TextCode } from "../TextCode";
import { getMessageCode } from "@/_utils/message";
import Image from "next/image";
import { MessageSenderType } from "@/_models/ChatMessage";
type Props = {
avatarUrl: string;
senderName: string;
createdAt: number;
senderType: MessageSenderType;
text?: string;
};
const SimpleTextMessage: React.FC<Props> = ({
senderName,
createdAt,
senderType,
avatarUrl = "",
text = "",
}) => (
<div className="flex items-start gap-2 ml-3">
}) => {
const backgroundColor =
senderType === MessageSenderType.User ? "" : "bg-gray-100";
return (
<div
className={`flex items-start gap-2 px-[148px] ${backgroundColor} py-5`}
>
<Image
className="rounded-full"
src={avatarUrl}
@ -25,32 +34,30 @@ const SimpleTextMessage: React.FC<Props> = ({
height={32}
alt=""
/>
<div className="flex flex-col gap-1 w-full">
<div className="flex flex-col gap-1">
<div className="flex gap-1 justify-start items-baseline">
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
{senderName}
</div>
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400">
<div className="text-xs leading-[13.2px] font-medium text-gray-400">
{displayDate(createdAt)}
</div>
</div>
{text.includes("```") ? (
getMessageCode(text).map((item, i) => (
<div className="flex gap-1 flex-col" key={i}>
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
{item.text}
</p>
{item.code.trim().length > 0 && <TextCode text={item.code} />}
</div>
))
) : (
<p
className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]"
dangerouslySetInnerHTML={{ __html: text }}
/>
<span className="text-sm leading-loose font-normal">{text}</span>
)}
</div>
</div>
);
);
};
export default React.memo(SimpleTextMessage);

View File

@ -34,17 +34,17 @@ const StreamTextMessage: React.FC<Props> = ({
/>
<div className="flex flex-col gap-1 w-full">
<div className="flex gap-1 justify-start items-baseline">
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
{senderName}
</div>
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400">
<div className="text-xs leading-[13.2px] font-medium text-gray-400">
{displayDate(createdAt)}
</div>
</div>
{message.text.includes("```") ? (
getMessageCode(message.text).map((item, i) => (
<div className="flex gap-1 flex-col" key={i}>
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
{item.text}
</p>
{item.code.trim().length > 0 && <TextCode text={item.code} />}
@ -52,7 +52,7 @@ const StreamTextMessage: React.FC<Props> = ({
))
) : (
<p
className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]"
className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]"
dangerouslySetInnerHTML={{ __html: message.text }}
/>
)}

View File

@ -3,15 +3,13 @@ type Props = {
value: string;
};
const SystemItem: React.FC<Props> = ({ name, value }) => {
return (
const SystemItem: React.FC<Props> = ({ name, value }) => (
<div className="flex gap-2 pl-4 my-1">
<div className="flex gap-[10px] w-max font-bold text-gray-900 text-sm">
<div className="flex gap-2.5 w-max font-bold text-gray-900 text-sm">
{name}
</div>
<span className="text-gray-900 text-sm">{value}</span>
</div>
);
};
);
export default SystemItem;

View File

@ -17,7 +17,7 @@ export const TabModelDetail: React.FC<Props> = ({ onTabClick, tab }) => {
];
return (
<div className="flex gap-[2px] rounded p-1 w-full bg-gray-200">
<div className="flex gap-0.5 rounded p-1 w-full bg-gray-200">
{btns.map((item, index) => (
<button
key={index}

View File

@ -19,7 +19,7 @@ const UserToolbar: React.FC = () => {
width={36}
height={36}
/>
<span className="flex gap-[2px] leading-6 text-base font-semibold">
<span className="flex gap-0.5 leading-6 text-base font-semibold">
{title}
</span>
</div>

Some files were not shown because too many files have changed in this diff Show More