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: env:
VERSION_TAG: ${{ steps.tag.outputs.tag }} VERSION_TAG: ${{ steps.tag.outputs.tag }}
- name: Install yarn dependencies
run: |
yarn install
yarn build:plugins
- name: Get Cer for code signing - name: Get Cer for code signing
run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12 run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
shell: bash shell: bash
env: env:
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }} 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 - name: Build and publish app
run: | run: |
yarn build:publish-darwin yarn build:publish-darwin
@ -56,6 +61,8 @@ jobs:
CSC_LINK: "/tmp/codesign.p12" CSC_LINK: "/tmp/codesign.p12"
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
CSC_IDENTITY_AUTO_DISCOVERY: "true" CSC_IDENTITY_AUTO_DISCOVERY: "true"
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
build-windows-x64: build-windows-x64:
runs-on: windows-latest 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/** models/**
error.log error.log
node_modules node_modules
package-lock.json
*.tgz *.tgz
yarn.lock yarn.lock
dist dist

View File

@ -34,5 +34,11 @@ module.exports = {
{ name: "Link", linkAttribute: "to" }, { 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") { if (typeof window === "undefined") {
return; return;
} }
// eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.install(plugins); const plgList = await window.pluggableElectronIpc.install(plugins);
if (plgList.cancelled) return false; if (plgList.cancelled) return false;
return plgList.map((plg) => { return plgList.map((plg) => {
@ -50,6 +51,7 @@ export function uninstall(plugins, reload = true) {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return; return;
} }
// eslint-disable-next-line no-undef
return window.pluggableElectronIpc.uninstall(plugins, reload); return window.pluggableElectronIpc.uninstall(plugins, reload);
} }
@ -62,6 +64,7 @@ export async function getActive() {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return; return;
} }
// eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.getActive(); const plgList = await window.pluggableElectronIpc.getActive();
return plgList.map( return plgList.map(
(plugin) => (plugin) =>
@ -86,6 +89,7 @@ export async function registerActive() {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return; return;
} }
// eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.getActive(); const plgList = await window.pluggableElectronIpc.getActive();
plgList.forEach((plugin) => plgList.forEach((plugin) =>
register( register(
@ -110,6 +114,7 @@ export async function update(plugins, reload = true) {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return; return;
} }
// eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.update(plugins, reload); const plgList = await window.pluggableElectronIpc.update(plugins, reload);
return plgList.map( return plgList.map(
(plugin) => (plugin) =>
@ -132,6 +137,7 @@ export function updatesAvailable(plugin) {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return; return;
} }
// eslint-disable-next-line no-undef
return window.pluggableElectronIpc.updatesAvailable(plugin); return window.pluggableElectronIpc.updatesAvailable(plugin);
} }
@ -146,6 +152,7 @@ export async function toggleActive(plugin, active) {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return; return;
} }
// eslint-disable-next-line no-undef
const plg = await window.pluggableElectronIpc.toggleActive(plugin, active); const plg = await window.pluggableElectronIpc.toggleActive(plugin, active);
return new Plugin(plg.name, plg.url, plg.activationPoints, plg.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 * as plugins from "./facade.js";
export { default as ExtensionPoint } from "./ExtensionPoint.js"; export { default as ExtensionPoint } from "./ExtensionPoint.js";
// eslint-disable-next-line no-undef
if (typeof window !== "undefined" && !window.pluggableElectronIpc) if (typeof window !== "undefined" && !window.pluggableElectronIpc)
console.warn( console.warn(
"Facade is not registered in preload. Facade functions will throw an error if used." "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": { "scripts": {
"build": "tsc -b . && webpack --config webpack.config.js", "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", "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 run build:package && cpx *.tgz ../../pre-install" "build:publish": "npm pack && cpx *.tgz ../../pre-install"
}, },
"exports": { "exports": {
".": "./dist/index.js", ".": "./dist/index.js",

View File

@ -17,11 +17,16 @@ const dispose = async () =>
.then((res) => resolve(res)); .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 // Register all the above functions and objects with the relevant extension points
export function init({ register }) { export function init({ register }) {
register("initModel", "initModel", initModel); register("initModel", "initModel", initModel);
register("inferenceUrl", "inferenceUrl", inferenceUrl); register("inferenceUrl", "inferenceUrl", inferenceUrl);
register("dispose", "dispose", dispose); register("dispose", "dispose", dispose);
register("stopModel", "stopModel", stopModel);
} }

View File

@ -23,7 +23,7 @@ async function initModel(product) {
console.error( console.error(
"A subprocess is already running. Attempt to kill then reinit." "A subprocess is already running. Attempt to kill then reinit."
); );
killSubprocess(); dispose();
} }
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
@ -51,12 +51,12 @@ async function initModel(product) {
let binaryName; let binaryName;
if (process.platform === "win32") { if (process.platform === "win32") {
binaryName = "nitro.exe"; binaryName = "nitro_windows_amd64.exe";
} else if (process.platform === "darwin") { // Mac OS platform } 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 { } else {
// Linux // Linux
binaryName = "nitro_linux"; // For other platforms binaryName = "nitro_linux_amd64_cuda"; // For other platforms
} }
const binaryPath = path.join(binaryFolder, binaryName); 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 int8_t qs[QK8_0]; // quants
} block_q8_0; } 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( kernel void kernel_add(
device const float4 * src0, device const char * src0,
device const float4 * src1, device const char * src1,
device float4 * dst, device char * dst,
uint tpig[[thread_position_in_grid]]) { constant int64_t & ne00,
dst[tpig] = src0[tpig] + src1[tpig]; 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 // assumption: src1 is a row
@ -38,7 +85,7 @@ kernel void kernel_add_row(
device const float4 * src0, device const float4 * src0,
device const float4 * src1, device const float4 * src1,
device float4 * dst, device float4 * dst,
constant int64_t & nb, constant int64_t & nb [[buffer(27)]],
uint tpig[[thread_position_in_grid]]) { uint tpig[[thread_position_in_grid]]) {
dst[tpig] = src0[tpig] + src1[tpig % nb]; dst[tpig] = src0[tpig] + src1[tpig % nb];
} }
@ -783,7 +830,9 @@ kernel void kernel_alibi_f32(
constant uint64_t & nb1, constant uint64_t & nb1,
constant uint64_t & nb2, constant uint64_t & nb2,
constant uint64_t & nb3, constant uint64_t & nb3,
constant float & m0, constant float & m0,
constant float & m1,
constant int & n_heads_log2_floor,
uint3 tgpig[[threadgroup_position_in_grid]], uint3 tgpig[[threadgroup_position_in_grid]],
uint3 tpitg[[thread_position_in_threadgroup]], uint3 tpitg[[thread_position_in_threadgroup]],
uint3 ntg[[threads_per_threadgroup]]) { uint3 ntg[[threads_per_threadgroup]]) {
@ -799,37 +848,73 @@ kernel void kernel_alibi_f32(
const int64_t i0 = (n - i3*ne2*ne1*ne0 - i2*ne1*ne0 - i1*ne0); 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); 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) { 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); 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); 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( kernel void kernel_rope(
device const void * src0, device const void * src0,
device float * dst, device const int32_t * src1,
constant int64_t & ne00, device float * dst,
constant int64_t & ne01, constant int64_t & ne00,
constant int64_t & ne02, constant int64_t & ne01,
constant int64_t & ne03, constant int64_t & ne02,
constant uint64_t & nb00, constant int64_t & ne03,
constant uint64_t & nb01, constant uint64_t & nb00,
constant uint64_t & nb02, constant uint64_t & nb01,
constant uint64_t & nb03, constant uint64_t & nb02,
constant int64_t & ne0, constant uint64_t & nb03,
constant int64_t & ne1, constant int64_t & ne0,
constant int64_t & ne2, constant int64_t & ne1,
constant int64_t & ne3, constant int64_t & ne2,
constant uint64_t & nb0, constant int64_t & ne3,
constant uint64_t & nb1, constant uint64_t & nb0,
constant uint64_t & nb2, constant uint64_t & nb1,
constant uint64_t & nb3, constant uint64_t & nb2,
constant int & n_past, constant uint64_t & nb3,
constant int & n_dims, constant int & n_past,
constant int & mode, constant int & n_dims,
constant float & freq_base, constant int & mode,
constant float & freq_scale, constant float & freq_base,
constant float & freq_scale,
uint tiitg[[thread_index_in_threadgroup]], uint tiitg[[thread_index_in_threadgroup]],
uint3 tptg[[threads_per_threadgroup]], uint3 tptg[[threads_per_threadgroup]],
uint3 tgpig[[threadgroup_position_in_grid]]) { uint3 tgpig[[threadgroup_position_in_grid]]) {
@ -839,7 +924,9 @@ kernel void kernel_rope(
const bool is_neox = mode & 2; 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 theta_0 = freq_scale * (float)p;
const float inv_ndims = -1.f/n_dims; const float inv_ndims = -1.f/n_dims;
@ -851,11 +938,11 @@ kernel void kernel_rope(
const float cos_theta = cos(theta); const float cos_theta = cos(theta);
const float sin_theta = sin(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 const T * const src = (device T *)((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 T * dst_data = (device T *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
const float x0 = src[0]; const T x0 = src[0];
const float x1 = src[1]; const T x1 = src[1];
dst_data[0] = x0*cos_theta - x1*sin_theta; dst_data[0] = x0*cos_theta - x1*sin_theta;
dst_data[1] = x0*sin_theta + x1*cos_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; 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 const T * const src = (device T *)((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 T * dst_data = (device T *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
const float x0 = src[0]; const float x0 = src[0];
const float x1 = src[n_dims/2]; 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( kernel void kernel_cpy_f16_f16(
device const half * src0, device const half * src0,
device half * dst, device half * dst,
@ -1273,8 +1363,8 @@ kernel void kernel_mul_mat_q3_K_f32(
float yl[32]; float yl[32];
const uint16_t kmask1 = 0x3030; //const uint16_t kmask1 = 0x3030;
const uint16_t kmask2 = 0x0f0f; //const uint16_t kmask2 = 0x0f0f;
const int tid = tiisg/4; const int tid = tiisg/4;
const int ix = tiisg%4; const int ix = tiisg%4;
@ -2350,4 +2440,4 @@ template [[host_name("kernel_mul_mm_q2_K_f32")]] kernel mat_mm_t kernel_mul_mm<b
template [[host_name("kernel_mul_mm_q3_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q3_K, QK_NL, dequantize_q3_K>; template [[host_name("kernel_mul_mm_q3_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q3_K, QK_NL, dequantize_q3_K>;
template [[host_name("kernel_mul_mm_q4_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q4_K, QK_NL, dequantize_q4_K>; template [[host_name("kernel_mul_mm_q4_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q4_K, QK_NL, dequantize_q4_K>;
template [[host_name("kernel_mul_mm_q5_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q5_K, QK_NL, dequantize_q5_K>; template [[host_name("kernel_mul_mm_q5_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q5_K, QK_NL, dequantize_q5_K>;
template [[host_name("kernel_mul_mm_q6_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q6_K, QK_NL, dequantize_q6_K>; template [[host_name("kernel_mul_mm_q6_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q6_K, QK_NL, dequantize_q6_K>;

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@
], ],
"scripts": { "scripts": {
"build": "webpack --config webpack.config.js", "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", "postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install" "build:publish": "npm pack && cpx *.tgz ../../pre-install"
}, },
"devDependencies": { "devDependencies": {
"cpx": "^1.5.0", "cpx": "^1.5.0",
@ -25,8 +25,7 @@
"node-llama-cpp" "node-llama-cpp"
], ],
"dependencies": { "dependencies": {
"electron-is-dev": "^2.0.0", "electron-is-dev": "^2.0.0"
"node-llama-cpp": "^2.4.1"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "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 // Register all the above functions and objects with the relevant extension points
export function init({ register }) { export function init({ register }) {
register("getDownloadedModels", "getDownloadedModels", getDownloadedModels); register("getDownloadedModels", "getDownloadedModels", getDownloadedModels);
register("getAvailableModels", "getAvailableModels", getAvailableModels); register("getAvailableModels", "getAvailableModels", getAvailableModels);
register("downloadModel", "downloadModel", downloadModel); register("downloadModel", "downloadModel", downloadModel);
register("deleteModel", "deleteModel", deleteModel); register("deleteModel", "deleteModel", deleteModel);
register("searchModels", "searchModels", searchModels);
} }

View File

@ -1,6 +1,10 @@
const path = require("path"); const path = require("path");
const { readdirSync, lstatSync } = require("fs"); const { readdirSync, lstatSync } = require("fs");
const { app } = require("electron"); const { app } = require("electron");
const { listModels, listFiles, fileDownloadInfo } = require("@huggingface/hub");
let modelsIterator = undefined;
let currentSearchOwner = undefined;
const ALL_MODELS = [ const ALL_MODELS = [
{ {
@ -87,6 +91,76 @@ function getDownloadedModels() {
return downloadedModels; 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() { function getAvailableModels() {
const downloadedModelIds = getDownloadedModels().map((model) => model.id); const downloadedModelIds = getDownloadedModels().map((model) => model.id);
return ALL_MODELS.filter((model) => { return ALL_MODELS.filter((model) => {
@ -99,4 +173,5 @@ function getAvailableModels() {
module.exports = { module.exports = {
getDownloadedModels, getDownloadedModels,
getAvailableModels, getAvailableModels,
searchModels,
}; };

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@
], ],
"scripts": { "scripts": {
"build": "webpack --config webpack.config.js", "build": "webpack --config webpack.config.js",
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack", "postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\"",
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install" "build:publish": "npm pack && cpx *.tgz ../../pre-install"
}, },
"devDependencies": { "devDependencies": {
"cpx": "^1.5.0", "cpx": "^1.5.0",
@ -24,5 +24,11 @@
"dist/*", "dist/*",
"package.json", "package.json",
"README.md" "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": { "scripts": {
"build": "webpack --config webpack.config.js", "build": "webpack --config webpack.config.js",
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack", "postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\"",
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install" "build:publish": "npm pack && cpx *.tgz ../../pre-install"
}, },
"devDependencies": { "devDependencies": {
"rimraf": "^3.0.2", "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) => { ipcMain.handle("relaunch", async (_event, url) => {
dispose(requiredModules); dispose(requiredModules);
app.relaunch(); 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, "extends": null,
"mac": { "mac": {
"type": "distribution" "type": "distribution",
} "entitlements": "./entitlements.mac.plist",
"entitlementsInherit": "./entitlements.mac.plist",
"notarize": {
"teamId": "YT49P7GXG4"
}
},
"artifactName": "${name}-${os}-${arch}-${version}.${ext}"
}, },
"scripts": { "scripts": {
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"", "lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
"test:e2e": "playwright test --workers=2", "test:e2e": "playwright test --workers=1",
"dev": "tsc -p . && electron .", "dev": "tsc -p . && electron .",
"build": "tsc -p . && electron-builder -p never -m", "build": "tsc -p . && electron-builder -p never -m",
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64", "build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
@ -45,13 +51,17 @@
}, },
"dependencies": { "dependencies": {
"@npmcli/arborist": "^7.1.0", "@npmcli/arborist": "^7.1.0",
"@uiball/loaders": "^1.3.0",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"electron-updater": "^6.1.4", "electron-updater": "^6.1.4",
"pacote": "^17.0.4", "pacote": "^17.0.4",
"react-intersection-observer": "^9.5.2",
"request": "^2.88.2", "request": "^2.88.2",
"request-progress": "^3.0.0" "request-progress": "^3.0.0",
"use-debounce": "^9.0.4"
}, },
"devDependencies": { "devDependencies": {
"@electron/notarize": "^2.1.0",
"@playwright/test": "^1.38.1", "@playwright/test": "^1.38.1",
"@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3", "@typescript-eslint/parser": "^6.7.3",

View File

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

View File

@ -13,6 +13,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
pluginPath: () => ipcRenderer.invoke("pluginPath"), pluginPath: () => ipcRenderer.invoke("pluginPath"),
reloadPlugins: () => ipcRenderer.invoke("reloadPlugins"),
appVersion: () => ipcRenderer.invoke("appVersion"), appVersion: () => ipcRenderer.invoke("appVersion"),
openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url), 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 // parse the packaged Electron app and find paths and other info
const appInfo = parseElectronApp(latestBuild); const appInfo = parseElectronApp(latestBuild);
expect(appInfo).toBeTruthy(); expect(appInfo).toBeTruthy();
expect(appInfo.arch).toBeTruthy();
expect(appInfo.arch).toBe(process.arch);
expect(appInfo.asar).toBe(true); expect(appInfo.asar).toBe(true);
expect(appInfo.executable).toBeTruthy(); expect(appInfo.executable).toBeTruthy();
expect(appInfo.main).toBeTruthy(); expect(appInfo.main).toBeTruthy();

View File

@ -52,10 +52,6 @@ test("renders left navigation panel", async () => {
.getByRole("button", { name: "Explore Models" }) .getByRole("button", { name: "Explore Models" })
.first() .first()
.isEnabled(); .isEnabled();
const startConversation = await page
.getByRole("button", { name: "Start a Conversation" })
.first()
.isEnabled();
const discordBtn = await page const discordBtn = await page
.getByRole("button", { name: "Discord" }) .getByRole("button", { name: "Discord" })
.first() .first()
@ -72,7 +68,6 @@ test("renders left navigation panel", async () => {
[ [
newChatBtn, newChatBtn,
exploreBtn, exploreBtn,
startConversation,
discordBtn, discordBtn,
myModelsBtn, myModelsBtn,
settingsBtn, 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\"", "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:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
"build:electron": "yarn workspace jan-electron build", "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": "yarn build:web && yarn build:electron",
"build:darwin": "yarn build:web && yarn workspace jan-electron build:darwin", "build:darwin": "yarn build:web && yarn workspace jan-electron build:darwin",
"build:win32": "yarn build:web && yarn workspace jan-electron build:win32", "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; if (!activeModel) return null;
return ( return (
<Fragment> <div className="pl-[63px] pr-[89px]">
<h3 className="text-xl leading-[25px] mb-[13px]">Active Model(s)</h3> <h3 className="text-xl leading-[25px] mb-[13px]">Active Model(s)</h3>
<ModelTable models={[activeModel]} /> <ModelTable models={[activeModel]} />
</Fragment> </div>
); );
}; };

View File

@ -47,7 +47,7 @@ const AvailableModelCard: React.FC<Props> = ({
return ( return (
<div className="border rounded-lg border-gray-200"> <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 <DownloadModelContent
required={required} required={required}
author={product.author} author={product.author}

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ const ConversationalCard: React.FC<Props> = ({ product }) => {
{description} {description}
</span> </span>
</div> </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="" /> <Image src={"icons/play.svg"} width={16} height={16} alt="" />
32.2k runs 32.2k runs
</span> </span>

View File

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

View File

@ -16,7 +16,7 @@ const DownloadedModelCard: React.FC<Props> = ({
onDeleteClick, onDeleteClick,
}) => ( }) => (
<div className="border rounded-lg border-gray-200"> <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 <DownloadModelContent
required={required} required={required}
author={product.author} author={product.author}

View File

@ -1,4 +1,4 @@
import React, { Fragment } from "react"; import React from "react";
import SearchBar from "../SearchBar"; import SearchBar from "../SearchBar";
import ModelTable from "../ModelTable"; import ModelTable from "../ModelTable";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels"; import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
@ -6,14 +6,16 @@ import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
const DownloadedModelTable: React.FC = () => { const DownloadedModelTable: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels(); const { downloadedModels } = useGetDownloadedModels();
if (!downloadedModels || downloadedModels.length === 0) return null;
return ( return (
<Fragment> <div className="pl-[63px] pr-[89px]">
<h3 className="text-xl leading-[25px] mt-[50px]">Downloaded Models</h3> <h3 className="text-xl leading-[25px] mt-[50px]">Downloaded Models</h3>
<div className="py-5 w-[568px]"> <div className="py-5 w-[568px]">
<SearchBar /> <SearchBar />
</div> </div>
<ModelTable models={downloadedModels} /> <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 }) => ( const ExpandableHeader: React.FC<Props> = ({ title, expanded, onClick }) => (
<button onClick={onClick} className="flex items-center justify-between px-2"> <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} {title}
</h2> </h2>
<div className="mr-2"> <div className="mr-2">

View File

@ -1,55 +1,20 @@
import useGetAvailableModels from "@/_hooks/useGetAvailableModels";
import ExploreModelItem from "../ExploreModelItem";
import HeaderTitle from "../HeaderTitle"; import HeaderTitle from "../HeaderTitle";
import SearchBar from "../SearchBar"; import SearchBar, { SearchType } from "../SearchBar";
import SimpleCheckbox from "../SimpleCheckbox"; import ExploreModelList from "../ExploreModelList";
import SimpleTag, { TagType } from "../SimpleTag"; import ExploreModelFilter from "../ExploreModelFilter";
const tags = [ const ExploreModelContainer: React.FC = () => (
"Roleplay", <div className="flex flex-col flex-1 px-16 pt-14 overflow-hidden">
"Llama", <HeaderTitle title="Explore Models" />
"Story", <SearchBar
"Casual", type={SearchType.Model}
"Professional", placeholder="Owner name like TheBloke, bhlim etc.."
"CodeLlama", />
"Coding", <div className="flex flex-1 gap-x-10 mt-9 overflow-hidden">
]; <ExploreModelFilter />
const checkboxs = ["GGUF", "TensorRT", "Meow", "JigglyPuff"]; <ExploreModelList />
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">
<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} />
))}
</div>
</div>
</div>
</div> </div>
); </div>
}; );
export default ExploreModelContainer; 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"; "use client";
import ExploreModelItemHeader from "../ExploreModelItemHeader"; import ExploreModelItemHeader from "../ExploreModelItemHeader";
import ModelVersionList from "../ModelVersionList"; import ModelVersionList from "../ModelVersionList";
import { useMemo, useState } from "react"; import { Fragment, forwardRef, useState } from "react";
import { Product } from "@/_models/Product";
import SimpleTag, { TagType } from "../SimpleTag"; import SimpleTag, { TagType } from "../SimpleTag";
import { displayDate } from "@/_utils/datetime"; import { displayDate } from "@/_utils/datetime";
import useDownloadModel from "@/_hooks/useDownloadModel"; import { Product } from "@/_models/Product";
import { atom, useAtomValue } from "jotai";
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
type Props = { type Props = {
model: Product; model: Product;
}; };
const ExploreModelItem: React.FC<Props> = ({ model }) => { const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[model.fileName ?? ""]),
[model.fileName ?? ""]
);
const downloadState = useAtomValue(downloadAtom);
const { downloadModel } = useDownloadModel();
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
return ( 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 <ExploreModelItemHeader
name={model.name} name={model.name}
status={TagType.Recommended} status={TagType.Recommended}
total={model.totalSize} versions={model.availableVersions}
downloadState={downloadState}
onDownloadClick={() => downloadModel(model)}
/> />
<div className="flex flex-col px-[26px] py-[22px]"> <div className="flex flex-col px-[26px] py-[22px]">
<div className="flex justify-between"> <div className="flex justify-between">
@ -39,7 +33,7 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
<div className="text-sm font-medium text-gray-500"> <div className="text-sm font-medium text-gray-500">
Model Format Model Format
</div> </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 GGUF
</div> </div>
</div> </div>
@ -87,15 +81,24 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
<span className="text-sm font-medium text-gray-500">Tags</span> <span className="text-sm font-medium text-gray-500">Tags</span>
</div> </div>
</div> </div>
{show && <ModelVersionList />} {model.availableVersions.length > 0 && (
<button <Fragment>
onClick={() => setShow(!show)} {show && (
className="bg-[#FBFBFB] text-gray-500 text-sm text-left py-2 px-4 border-t border-gray-200" <ModelVersionList
> model={model}
{!show ? "+ Show Available Versions" : "- Collapse"} versions={model.availableVersions}
</button> />
)}
<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> </div>
); );
}; });
export default ExploreModelItem; export default ExploreModelItem;

View File

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

View File

@ -66,7 +66,7 @@ const HistoryItem: React.FC<Props> = ({
return ( return (
<button <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} onClick={onClick}
> >
<Image <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-col justify-between text-sm leading-[20px] w-full">
<div className="flex flex-row items-center justify-between"> <div className="flex flex-row items-center justify-between">
<span className="text-gray-900 text-left">{name}</span> <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()} {updatedAt && new Date(updatedAt).toDateString()}
</span> </span>
</div> </div>

View File

@ -18,14 +18,14 @@ const HistoryList: React.FC = () => {
}, []); }, []);
return ( 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 <ExpandableHeader
title="CHAT HISTORY" title="CHAT HISTORY"
expanded={expand} expanded={expand}
onClick={() => setExpand(!expand)} onClick={() => setExpand(!expand)}
/> />
<div <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.length > 0 ? (
conversations conversations

View File

@ -4,19 +4,56 @@ import BasicPromptInput from "../BasicPromptInput";
import BasicPromptAccessories from "../BasicPromptAccessories"; import BasicPromptAccessories from "../BasicPromptAccessories";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom"; 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 InputToolbar: React.FC = () => {
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom); const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom);
const currentProduct = useAtomValue(currentProductAtom);
const { requestCreateConvo } = useCreateConversation();
const isTyping = useAtomValue(showingTyping);
if (showingAdvancedPrompt) { if (showingAdvancedPrompt) {
return <div />; return <div />;
} }
// TODO: implement regenerate
// const onRegenerateClick = () => {};
const onNewConversationClick = () => {
if (currentProduct) {
requestCreateConvo(currentProduct);
}
};
return ( return (
<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"> <Fragment>
<BasicPromptInput /> <div className="flex justify-between gap-2 mr-3 my-2">
<BasicPromptAccessories /> <div className="h-6">
</div> {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); const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
return ( return (
<button <button
className="p-3 flex gap-[2px] items-center" className="p-3 flex gap-0.5 items-center"
onClick={() => setActiveConvoId(undefined)} onClick={() => setActiveConvoId(undefined)}
> >
<Image src={"icons/app_icon.svg"} width={28} height={28} alt="" /> <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"; import NewChatButton from "../NewChatButton";
const LeftContainer: React.FC = () => ( 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 /> <SidebarHeader />
<NewChatButton /> <NewChatButton />
<HistoryList /> <HistoryList />

View File

@ -1,12 +1,6 @@
const LoadingIndicator = () => { const LoadingIndicator = () => {
let circleCommonClasses = "h-1.5 w-1.5 bg-current rounded-full";
return ( 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="typingIndicatorContainer">
<div className="typingIndicatorBubble"> <div className="typingIndicatorBubble">
<div className="typingIndicatorBubbleDot"></div> <div className="typingIndicatorBubbleDot"></div>

View File

@ -12,7 +12,7 @@ const LoginButton: React.FC = () => {
// <button // <button
// onClick={signInWithKeyCloak} // onClick={signInWithKeyCloak}
// type="button" // 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 // Login
// </button> // </button>

View File

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

View File

@ -6,30 +6,37 @@ type Props = {
onDeleteClick: () => void; onDeleteClick: () => void;
}; };
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => { const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => (
return ( <Menu as="div" className="relative flex-none">
<Menu as="div" className="relative flex-none"> <Menu.Button className="block text-gray-500 hover:text-gray-900">
<Menu.Button className="block text-gray-500 hover:text-gray-900"> <span className="sr-only">Open options</span>
<span className="sr-only">Open options</span> <EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" /> </Menu.Button>
</Menu.Button> <Transition
<Transition as={Fragment}
as={Fragment} enter="transition ease-out duration-100"
enter="transition ease-out duration-100" enterFrom="transform opacity-0 scale-95"
enterFrom="transform opacity-0 scale-95" enterTo="transform opacity-100 scale-100"
enterTo="transform opacity-100 scale-100" leave="transition ease-in duration-75"
leave="transition ease-in duration-75" leaveFrom="transform opacity-100 scale-100"
leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95"
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 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 py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none"> <Menu.Item>
<Menu.Item> {({ active }) => (
<button onClick={onDeleteClick}>Delete</button> <button
</Menu.Item> className={`${
</Menu.Items> active ? "bg-violet-500 text-white" : "text-gray-900"
</Transition> } group flex w-full items-center rounded-md px-2 py-2 text-sm`}
</Menu> onClick={onDeleteClick}
); >
}; Delete
</button>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
);
export default ModelActionMenu; 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"> <button className="py-2 px-3 flex gap-2 border text-xs leading-[18px] border-gray-200 rounded-lg">
Downloading... Downloading...
</button> </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"> <span className="text-xs font-medium text-gray-800">
{toGigabytes(value)} / {toGigabytes(total)} {toGigabytes(value)} / {toGigabytes(total)}
</span> </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"; "use client";
import { useAtomValue, useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; import { TrashIcon } from "@heroicons/react/24/outline";
import useCreateConversation from "@/_hooks/useCreateConversation";
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom"; import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
const ModelMenu: React.FC = () => { const ModelMenu: React.FC = () => {
const currentProduct = useAtomValue(currentProductAtom);
const { requestCreateConvo } = useCreateConversation();
const setShowConfirmDeleteConversationModal = useSetAtom( const setShowConfirmDeleteConversationModal = useSetAtom(
showConfirmDeleteConversationModalAtom showConfirmDeleteConversationModalAtom
); );
const onCreateConvoClick = () => {
if (currentProduct) {
requestCreateConvo(currentProduct);
}
};
return ( return (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button onClick={() => onCreateConvoClick()}>
<PlusIcon width={24} height={24} color="#9CA3AF" />
</button>
<button onClick={() => setShowConfirmDeleteConversationModal(true)}> <button onClick={() => setShowConfirmDeleteConversationModal(true)}>
<TrashIcon width={24} height={24} color="#9CA3AF" /> <TrashIcon width={24} height={24} color="#9CA3AF" />
</button> </button>

View File

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

View File

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

View File

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

View File

@ -1,25 +1,57 @@
import React from "react"; import React, { useMemo } from "react";
import { toGigabytes } from "@/_utils/converter"; import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
import Image from "next/image"; 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 = { type Props = {
title: string; model: Product;
totalSizeInByte: number; modelVersion: ModelVersion;
}; };
const ModelVersionItem: React.FC<Props> = ({ title, totalSizeInByte }) => ( const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
<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 { downloadHfModel } = useDownloadModel();
<div className="flex items-center gap-4"> const downloadAtom = useMemo(
<Image src={"/icons/app_icon.svg"} width={14} height={20} alt="" /> () => atom((get) => get(modelDownloadStateAtom)[modelVersion.path ?? ""]),
<span className="font-sm text-gray-900">{title}</span> [modelVersion.path ?? ""]
</div> );
<div className="flex items-center gap-4"> const downloadState = useAtomValue(downloadAtom);
<div className="px-[10px] py-0.5 bg-gray-200 text-xs font-medium rounded">
{toGigabytes(totalSizeInByte)} 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">{modelVersion.path}</span>
</div>
<div className="flex items-center gap-4">
<div className="px-2.5 py-0.5 bg-gray-200 text-xs font-medium rounded">
{toGigabytes(modelVersion.size)}
</div>
{downloadButton}
</div> </div>
<button className="text-indigo-600 text-sm font-medium">Download</button>
</div> </div>
</div> );
); };
export default ModelVersionItem; export default ModelVersionItem;

View File

@ -1,38 +1,21 @@
import React from "react"; import React from "react";
import ModelVersionItem from "../ModelVersionItem"; import ModelVersionItem from "../ModelVersionItem";
import { ModelVersion, Product } from "@/_models/Product";
const data = [ type Props = {
{ model: Product;
name: "Q4_K_M.gguf", versions: ModelVersion[];
total: 5600,
},
{
name: "Q4_K_M.gguf",
total: 5600,
},
{
name: "Q4_K_M.gguf",
total: 5600,
},
];
const ModelVersionList: React.FC = () => {
return (
<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="border border-gray-200 rounded-lg overflow-hidden">
{data.map((item, index) => (
<ModelVersionItem
key={index}
title={item.name}
totalSizeInByte={item.total}
/>
))}
</div>
</div>
);
}; };
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="border border-gray-200 rounded-lg overflow-hidden">
{versions.map((item) => (
<ModelVersionItem key={item.path} model={model} modelVersion={item} />
))}
</div>
</div>
);
export default ModelVersionList; export default ModelVersionList;

View File

@ -2,63 +2,43 @@ import ProgressBar from "../ProgressBar";
import SystemItem from "../SystemItem"; import SystemItem from "../SystemItem";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { appDownloadProgress } from "@/_helpers/JotaiWrapper"; 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 { 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 MonitorBar: React.FC = () => {
const show = useAtomValue(getSystemBarVisibilityAtom);
const progress = useAtomValue(appDownloadProgress); const progress = useAtomValue(appDownloadProgress);
const activeModel = useAtomValue(currentProductAtom); const activeModel = useAtomValue(currentProductAtom);
const [ram, setRam] = useState<number>(0); const { version } = useGetAppVersion();
const [gpu, setGPU] = useState<number>(0); const { ram, cpu } = useGetSystemResources();
const [cpu, setCPU] = useState<number>(0); const modelDownloadStates = useAtomValue(modelDownloadStateAtom);
const [version, setVersion] = useState<string>("");
useEffect(() => { const downloadStates: DownloadState[] = [];
const getSystemResources = async () => { for (const [, value] of Object.entries(modelDownloadStates)) {
const resourceInfor = await executeSerial( downloadStates.push(value);
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;
return ( return (
<div className="flex flex-row items-center justify-between border-t border-gray-200"> <div className="flex flex-row items-center justify-between border-t border-gray-200">
{progress && progress >= 0 ? ( {progress && progress >= 0 ? (
<ProgressBar total={100} used={progress} /> <ProgressBar total={100} used={progress} />
) : ( ) : null}
<div className="w-full" /> <div className="flex-1 justify-end flex items-center gap-8 px-2">
)} {downloadStates.length > 0 && (
<div className="flex-1 flex items-center gap-8 px-2"> <SystemItem
name="Downloading"
value={`${downloadStates[0].fileName}: ${formatDownloadPercentage(
downloadStates[0].percent
)}`}
/>
)}
<SystemItem name="CPU" value={`${cpu}%`} /> <SystemItem name="CPU" value={`${cpu}%`} />
<SystemItem name="Mem" value={`${ram}%`} /> <SystemItem name="Mem" value={`${ram}%`} />
{activeModel && ( {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> <span className="text-gray-900 text-sm">v{version}</span>
</div> </div>

View File

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

View File

@ -11,6 +11,7 @@ import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import useCreateConversation from "@/_hooks/useCreateConversation"; import useCreateConversation from "@/_hooks/useCreateConversation";
import useInitModel from "@/_hooks/useInitModel"; import useInitModel from "@/_hooks/useInitModel";
import { Product } from "@/_models/Product"; import { Product } from "@/_models/Product";
import { PlusIcon } from "@heroicons/react/24/outline";
const NewChatButton: React.FC = () => { const NewChatButton: React.FC = () => {
const activeModel = useAtomValue(currentProductAtom); const activeModel = useAtomValue(currentProductAtom);
@ -32,8 +33,13 @@ const NewChatButton: React.FC = () => {
}; };
return ( 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> </label>
</div> </div>
<button <div className="flex flex-col space-y-2">
type="submit" <button
className={classNames( type="submit"
"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", className={classNames(
fileName ? "bg-indigo-600 hover:bg-indigo-500" : "bg-gray-500" "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-blue-500 hover:bg-blue-300"
Install Plugin : "bg-gray-500"
</button> )}
>
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> </div>
</form> </form>

View File

@ -4,17 +4,19 @@ type Props = {
title: string; title: string;
onClick: () => void; onClick: () => void;
fullWidth?: boolean; fullWidth?: boolean;
className?: string;
}; };
const PrimaryButton: React.FC<Props> = ({ const PrimaryButton: React.FC<Props> = ({
title, title,
onClick, onClick,
fullWidth = false, fullWidth = false,
className,
}) => ( }) => (
<button <button
onClick={onClick} onClick={onClick}
type="button" 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 " : "" fullWidth ? "flex-1 " : ""
}}`} }}`}
> >

View File

@ -5,24 +5,22 @@ type Props = {
used: number; used: number;
}; };
const ProgressBar: React.FC<Props> = ({ used, total }) => { const ProgressBar: React.FC<Props> = ({ used, total }) => (
return ( <div className="flex gap-2.5 items-center p-[10px]">
<div className="flex gap-[10px] items-center p-[10px]"> <div className="text-xs leading-[18px] gap-0.5 flex items-center">
<div className="text-xs leading-[18px] gap-0.5 flex items-center"> <Image src={"icons/app_icon.svg"} width={18} height={18} alt="" />
<Image src={"icons/app_icon.svg"} width={18} height={18} alt="" /> Updating
Updating
</div>
<div className="w-[150px] relative bg-blue-200 h-1 rounded-md flex">
<div
className="absolute top-0 left-0 h-full rounded-md bg-blue-600"
style={{ width: `${((used / total) * 100).toFixed(2)}%` }}
></div>
</div>
<div className="text-xs leading-[18px]">
{((used / total) * 100).toFixed(0)}%
</div>
</div> </div>
); <div className="w-[150px] relative bg-blue-200 h-1 rounded-md flex">
}; <div
className="absolute top-0 left-0 h-full rounded-md bg-blue-600"
style={{ width: `${((used / total) * 100).toFixed(2)}%` }}
></div>
</div>
<div className="text-xs leading-[18px]">
{((used / total) * 100).toFixed(0)}%
</div>
</div>
);
export default ProgressBar; export default ProgressBar;

View File

@ -1,12 +1,9 @@
import ChatContainer from "../ChatContainer"; import MainView from "../MainView";
import MainChat from "../MainChat";
import MonitorBar from "../MonitorBar"; import MonitorBar from "../MonitorBar";
const RightContainer = () => ( const RightContainer = () => (
<div className="flex flex-col flex-1 h-screen"> <div className="flex flex-col flex-1 h-screen">
<ChatContainer> <MainView />
<MainChat />
</ChatContainer>
<MonitorBar /> <MonitorBar />
</div> </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 { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { useDebouncedCallback } from "use-debounce";
export enum SearchType {
Model = "model",
}
type Props = { type Props = {
type?: SearchType;
placeholder?: string; placeholder?: string;
}; };
const SearchBar: React.FC<Props> = ({ placeholder }) => { const SearchBar: React.FC<Props> = ({ type, placeholder }) => {
const setText = useSetAtom(searchAtom); const setModelSearch = useSetAtom(modelSearchAtom);
let placeholderText = placeholder ? placeholder : "Search (⌘K)"; let placeholderText = placeholder ? placeholder : "Search (⌘K)";
const debounced = useDebouncedCallback((value) => {
setModelSearch(value);
}, 300);
return ( return (
<div className="relative mt-3 flex items-center"> <div className="relative mt-3 flex items-center">
<div className="absolute top-0 left-2 h-full 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" name="search"
id="search" id="search"
placeholder={placeholderText} 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" 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> </div>

View File

@ -1,8 +1,11 @@
import React from "react";
type Props = { type Props = {
title: string; title: string;
onClick: () => void; onClick?: () => void;
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
icon?: React.ReactNode;
}; };
const SecondaryButton: React.FC<Props> = ({ const SecondaryButton: React.FC<Props> = ({
@ -10,15 +13,17 @@ const SecondaryButton: React.FC<Props> = ({
onClick, onClick,
disabled, disabled,
className, className,
icon,
}) => ( }) => (
<button <button
disabled={disabled} disabled={disabled}
type="button" type="button"
onClick={onClick} 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} {title}
</button> </button>
); );
export default SecondaryButton; export default React.memo(SecondaryButton);

View File

@ -1,8 +1,8 @@
import { currentPromptAtom } from "@/_helpers/JotaiWrapper"; import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom"; import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
import useSendChatMessage from "@/_hooks/useSendChatMessage"; import useSendChatMessage from "@/_hooks/useSendChatMessage";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import Image from "next/image";
const SendButton: React.FC = () => { const SendButton: React.FC = () => {
const [currentPrompt] = useAtom(currentPromptAtom); const [currentPrompt] = useAtom(currentPromptAtom);
@ -25,9 +25,9 @@ const SendButton: React.FC = () => {
onClick={sendChatMessage} onClick={sendChatMessage}
style={disabled ? disabledStyle : enabledStyle} style={disabled ? disabledStyle : enabledStyle}
type="submit" 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> </button>
); );
}; };

View File

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

View File

@ -2,7 +2,7 @@ import React from "react";
import SecondaryButton from "../SecondaryButton"; import SecondaryButton from "../SecondaryButton";
const SidebarFooter: React.FC = () => ( 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 <SecondaryButton
title={"Discord"} title={"Discord"}
onClick={() => onClick={() =>
@ -11,7 +11,7 @@ const SidebarFooter: React.FC = () => (
className="flex-1" className="flex-1"
/> />
<SecondaryButton <SecondaryButton
title={"Discord"} title={"Twitter"}
onClick={() => onClick={() =>
window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai") window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai")
} }

View File

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

View File

@ -21,21 +21,16 @@ const menu = [
]; ];
const SidebarMenu: React.FC = () => ( const SidebarMenu: React.FC = () => (
<div className="flex flex-col"> <ul role="list" className="mx-1 mt-2 space-y-1 mb-2">
<div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3"> {menu.map((item) => (
Your Configurations <SidebarMenuItem
</div> title={item.name}
<ul role="list" className="-mx-2 mt-2 space-y-1 mb-2"> viewState={item.state}
{menu.map((item) => ( iconName={item.icon}
<SidebarMenuItem key={item.name}
title={item.name} />
viewState={item.state} ))}
iconName={item.icon} </ul>
key={item.name}
/>
))}
</ul>
</div>
); );
export default React.memo(SidebarMenu); 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 flex-col gap-1">
<div className="flex gap-1 justify-start items-baseline"> <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} {senderName}
</div> </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)} {displayDate(createdAt)}
</div> </div>
</div> </div>
<div className="flex gap-3 flex-col"> <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} {text}
</p> </p>
<JanImage <JanImage
@ -49,7 +49,7 @@ const SimpleControlNetMessage: React.FC<Props> = ({
<Link <Link
href={imageUrls[0] || "#"} href={imageUrls[0] || "#"}
target="_blank_" 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="" /> <Image src="icons/download.svg" width={16} height={16} alt="" />
<span className="leading-[20px] text-[14px] text-[#111928]"> <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 flex-col gap-1">
<div className="flex gap-1 justify-start items-baseline"> <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} {senderName}
</div> </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)} {displayDate(createdAt)}
</div> </div>
</div> </div>
@ -46,19 +46,19 @@ const SimpleImageMessage: React.FC<Props> = ({
<Link <Link
href={imageUrls[0] || "#"} href={imageUrls[0] || "#"}
target="_blank_" 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="" /> <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 Download
</span> </span>
</Link> </Link>
<button <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()} // onClick={() => sendChatMessage()}
> >
<Image src="icons/refresh.svg" width={16} height={16} alt="" /> <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 Re-generate
</span> </span>
</button> </button>

View File

@ -69,7 +69,7 @@ const SimpleTag: React.FC<Props> = ({
if (!clickable) { if (!clickable) {
return ( return (
<div <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} {title}
</div> </div>
@ -79,7 +79,7 @@ const SimpleTag: React.FC<Props> = ({
return ( return (
<button <button
onClick={onClick} 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 {title} x
</button> </button>

View File

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

View File

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

View File

@ -17,7 +17,7 @@ export const TabModelDetail: React.FC<Props> = ({ onTabClick, tab }) => {
]; ];
return ( 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) => ( {btns.map((item, index) => (
<button <button
key={index} key={index}

View File

@ -19,7 +19,7 @@ const UserToolbar: React.FC = () => {
width={36} width={36}
height={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} {title}
</span> </span>
</div> </div>

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