Merge branch 'main' into fix-264
This commit is contained in:
commit
8cf71567c2
17
.github/workflows/build-app.yml
vendored
17
.github/workflows/build-app.yml
vendored
@ -37,17 +37,22 @@ jobs:
|
||||
env:
|
||||
VERSION_TAG: ${{ steps.tag.outputs.tag }}
|
||||
|
||||
- name: Install yarn dependencies
|
||||
run: |
|
||||
yarn install
|
||||
yarn build:plugins
|
||||
|
||||
- name: Get Cer for code signing
|
||||
run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
|
||||
shell: bash
|
||||
env:
|
||||
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@v2
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
||||
p12-password: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
|
||||
|
||||
- name: Install yarn dependencies
|
||||
run: |
|
||||
yarn install
|
||||
yarn build:plugins-darwin
|
||||
|
||||
- name: Build and publish app
|
||||
run: |
|
||||
yarn build:publish-darwin
|
||||
@ -56,6 +61,8 @@ jobs:
|
||||
CSC_LINK: "/tmp/codesign.p12"
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "true"
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
|
||||
build-windows-x64:
|
||||
runs-on: windows-latest
|
||||
|
||||
88
.github/workflows/linter-and-test.yml
vendored
Normal file
88
.github/workflows/linter-and-test.yml
vendored
Normal 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
1
.gitignore
vendored
@ -5,7 +5,6 @@
|
||||
models/**
|
||||
error.log
|
||||
node_modules
|
||||
package-lock.json
|
||||
*.tgz
|
||||
yarn.lock
|
||||
dist
|
||||
|
||||
@ -34,5 +34,11 @@ module.exports = {
|
||||
{ name: "Link", linkAttribute: "to" },
|
||||
],
|
||||
},
|
||||
ignorePatterns: ["renderer/*", "node_modules/*", "core/plugins"],
|
||||
ignorePatterns: [
|
||||
"build",
|
||||
"renderer",
|
||||
"node_modules",
|
||||
"core/plugins",
|
||||
"core/**/*.test.js",
|
||||
],
|
||||
};
|
||||
|
||||
5
electron/auto-sign.sh
Executable file
5
electron/auto-sign.sh
Executable 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 {} \;
|
||||
@ -25,6 +25,7 @@ export async function install(plugins) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.install(plugins);
|
||||
if (plgList.cancelled) return false;
|
||||
return plgList.map((plg) => {
|
||||
@ -50,6 +51,7 @@ export function uninstall(plugins, reload = true) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
return window.pluggableElectronIpc.uninstall(plugins, reload);
|
||||
}
|
||||
|
||||
@ -62,6 +64,7 @@ export async function getActive() {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.getActive();
|
||||
return plgList.map(
|
||||
(plugin) =>
|
||||
@ -86,6 +89,7 @@ export async function registerActive() {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.getActive();
|
||||
plgList.forEach((plugin) =>
|
||||
register(
|
||||
@ -110,6 +114,7 @@ export async function update(plugins, reload = true) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.update(plugins, reload);
|
||||
return plgList.map(
|
||||
(plugin) =>
|
||||
@ -132,6 +137,7 @@ export function updatesAvailable(plugin) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
return window.pluggableElectronIpc.updatesAvailable(plugin);
|
||||
}
|
||||
|
||||
@ -146,6 +152,7 @@ export async function toggleActive(plugin, active) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plg = await window.pluggableElectronIpc.toggleActive(plugin, active);
|
||||
return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ export * as activationPoints from "./activation-manager.js";
|
||||
export * as plugins from "./facade.js";
|
||||
export { default as ExtensionPoint } from "./ExtensionPoint.js";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
if (typeof window !== "undefined" && !window.pluggableElectronIpc)
|
||||
console.warn(
|
||||
"Facade is not registered in preload. Facade functions will throw an error if used."
|
||||
|
||||
5606
electron/core/plugins/data-plugin/package-lock.json
generated
Normal file
5606
electron/core/plugins/data-plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,8 +11,8 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"build:package": "rimraf ./data-plugin*.tgz && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=arm64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=glibc --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=musl --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_libc=unknown --target_arch=x64 && npm run build && npm pack",
|
||||
"build:publish": "npm run build:package && cpx *.tgz ../../pre-install"
|
||||
"postinstall": "rimraf ./data-plugin*.tgz && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=arm64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=glibc --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=musl --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_libc=unknown --target_arch=x64 && npm run build",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
|
||||
@ -17,11 +17,16 @@ const dispose = async () =>
|
||||
.then((res) => resolve(res));
|
||||
}
|
||||
});
|
||||
const inferenceUrl = () => "http://localhost:8080/llama/chat_completion";
|
||||
const inferenceUrl = () => "http://localhost:3928/llama/chat_completion";
|
||||
|
||||
const stopModel = () => {
|
||||
window.electronAPI.invokePluginFunc(MODULE_PATH, "killSubprocess");
|
||||
};
|
||||
|
||||
// Register all the above functions and objects with the relevant extension points
|
||||
export function init({ register }) {
|
||||
register("initModel", "initModel", initModel);
|
||||
register("inferenceUrl", "inferenceUrl", inferenceUrl);
|
||||
register("dispose", "dispose", dispose);
|
||||
register("stopModel", "stopModel", stopModel);
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ async function initModel(product) {
|
||||
console.error(
|
||||
"A subprocess is already running. Attempt to kill then reinit."
|
||||
);
|
||||
killSubprocess();
|
||||
dispose();
|
||||
}
|
||||
|
||||
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
|
||||
@ -51,12 +51,12 @@ async function initModel(product) {
|
||||
let binaryName;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
binaryName = "nitro.exe";
|
||||
binaryName = "nitro_windows_amd64.exe";
|
||||
} else if (process.platform === "darwin") { // Mac OS platform
|
||||
binaryName = process.arch === "arm64" ? "nitro" : "nitro_mac_intel";
|
||||
binaryName = process.arch === "arm64" ? "nitro_mac_arm64" : "nitro_mac_amd64";
|
||||
} else {
|
||||
// Linux
|
||||
binaryName = "nitro_linux"; // For other platforms
|
||||
binaryName = "nitro_linux_amd64_cuda"; // For other platforms
|
||||
}
|
||||
|
||||
const binaryPath = path.join(binaryFolder, binaryName);
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,12 +24,59 @@ typedef struct {
|
||||
int8_t qs[QK8_0]; // quants
|
||||
} block_q8_0;
|
||||
|
||||
// general-purpose kernel for addition of two tensors
|
||||
// pros: works for non-contiguous tensors, supports broadcast across dims 1, 2 and 3
|
||||
// cons: not very efficient
|
||||
kernel void kernel_add(
|
||||
device const float4 * src0,
|
||||
device const float4 * src1,
|
||||
device float4 * dst,
|
||||
uint tpig[[thread_position_in_grid]]) {
|
||||
dst[tpig] = src0[tpig] + src1[tpig];
|
||||
device const char * src0,
|
||||
device const char * src1,
|
||||
device char * dst,
|
||||
constant int64_t & ne00,
|
||||
constant int64_t & ne01,
|
||||
constant int64_t & ne02,
|
||||
constant int64_t & ne03,
|
||||
constant int64_t & nb00,
|
||||
constant int64_t & nb01,
|
||||
constant int64_t & nb02,
|
||||
constant int64_t & nb03,
|
||||
constant int64_t & ne10,
|
||||
constant int64_t & ne11,
|
||||
constant int64_t & ne12,
|
||||
constant int64_t & ne13,
|
||||
constant int64_t & nb10,
|
||||
constant int64_t & nb11,
|
||||
constant int64_t & nb12,
|
||||
constant int64_t & nb13,
|
||||
constant int64_t & ne0,
|
||||
constant int64_t & ne1,
|
||||
constant int64_t & ne2,
|
||||
constant int64_t & ne3,
|
||||
constant int64_t & nb0,
|
||||
constant int64_t & nb1,
|
||||
constant int64_t & nb2,
|
||||
constant int64_t & nb3,
|
||||
uint3 tgpig[[threadgroup_position_in_grid]],
|
||||
uint3 tpitg[[thread_position_in_threadgroup]],
|
||||
uint3 ntg[[threads_per_threadgroup]]) {
|
||||
const int64_t i03 = tgpig.z;
|
||||
const int64_t i02 = tgpig.y;
|
||||
const int64_t i01 = tgpig.x;
|
||||
|
||||
const int64_t i13 = i03 % ne13;
|
||||
const int64_t i12 = i02 % ne12;
|
||||
const int64_t i11 = i01 % ne11;
|
||||
|
||||
device const char * src0_ptr = src0 + i03*nb03 + i02*nb02 + i01*nb01 + tpitg.x*nb00;
|
||||
device const char * src1_ptr = src1 + i13*nb13 + i12*nb12 + i11*nb11 + tpitg.x*nb10;
|
||||
device char * dst_ptr = dst + i03*nb3 + i02*nb2 + i01*nb1 + tpitg.x*nb0;
|
||||
|
||||
for (int i0 = tpitg.x; i0 < ne0; i0 += ntg.x) {
|
||||
((device float *)dst_ptr)[0] = ((device float *)src0_ptr)[0] + ((device float *)src1_ptr)[0];
|
||||
|
||||
src0_ptr += ntg.x*nb00;
|
||||
src1_ptr += ntg.x*nb10;
|
||||
dst_ptr += ntg.x*nb0;
|
||||
}
|
||||
}
|
||||
|
||||
// assumption: src1 is a row
|
||||
@ -38,7 +85,7 @@ kernel void kernel_add_row(
|
||||
device const float4 * src0,
|
||||
device const float4 * src1,
|
||||
device float4 * dst,
|
||||
constant int64_t & nb,
|
||||
constant int64_t & nb [[buffer(27)]],
|
||||
uint tpig[[thread_position_in_grid]]) {
|
||||
dst[tpig] = src0[tpig] + src1[tpig % nb];
|
||||
}
|
||||
@ -783,7 +830,9 @@ kernel void kernel_alibi_f32(
|
||||
constant uint64_t & nb1,
|
||||
constant uint64_t & nb2,
|
||||
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 tpitg[[thread_position_in_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);
|
||||
|
||||
device float * dst_data = (device float *) ((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||
float m_k = pow(m0, i2 + 1);
|
||||
float m_k;
|
||||
if (i2 < n_heads_log2_floor) {
|
||||
m_k = pow(m0, i2 + 1);
|
||||
} else {
|
||||
m_k = pow(m1, 2 * (i2 - n_heads_log2_floor) + 1);
|
||||
}
|
||||
for (int64_t i00 = tpitg.x; i00 < ne00; i00 += ntg.x) {
|
||||
device const float * src = (device float *)((device char *) src0 + i03*nb03 + i02*nb02 + i01*nb01 + i00*nb00);
|
||||
dst_data[i00] = src[0] + m_k * (i00 - ne00 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
typedef void (rope_t)(
|
||||
device const void * src0,
|
||||
device const int32_t * src1,
|
||||
device float * dst,
|
||||
constant int64_t & ne00,
|
||||
constant int64_t & ne01,
|
||||
constant int64_t & ne02,
|
||||
constant int64_t & ne03,
|
||||
constant uint64_t & nb00,
|
||||
constant uint64_t & nb01,
|
||||
constant uint64_t & nb02,
|
||||
constant uint64_t & nb03,
|
||||
constant int64_t & ne0,
|
||||
constant int64_t & ne1,
|
||||
constant int64_t & ne2,
|
||||
constant int64_t & ne3,
|
||||
constant uint64_t & nb0,
|
||||
constant uint64_t & nb1,
|
||||
constant uint64_t & nb2,
|
||||
constant uint64_t & nb3,
|
||||
constant int & n_past,
|
||||
constant int & n_dims,
|
||||
constant int & mode,
|
||||
constant float & freq_base,
|
||||
constant float & freq_scale,
|
||||
uint tiitg[[thread_index_in_threadgroup]],
|
||||
uint3 tptg[[threads_per_threadgroup]],
|
||||
uint3 tgpig[[threadgroup_position_in_grid]]);
|
||||
|
||||
template<typename T>
|
||||
kernel void kernel_rope(
|
||||
device const void * src0,
|
||||
device 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,
|
||||
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]]) {
|
||||
@ -839,7 +924,9 @@ kernel void kernel_rope(
|
||||
|
||||
const bool is_neox = mode & 2;
|
||||
|
||||
const int64_t p = ((mode & 1) == 0 ? n_past + i2 : i2);
|
||||
device const int32_t * pos = src1;
|
||||
|
||||
const int64_t p = pos[i2];
|
||||
|
||||
const float theta_0 = freq_scale * (float)p;
|
||||
const float inv_ndims = -1.f/n_dims;
|
||||
@ -851,11 +938,11 @@ kernel void kernel_rope(
|
||||
const float cos_theta = cos(theta);
|
||||
const float sin_theta = sin(theta);
|
||||
|
||||
device const float * const src = (device float *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
||||
device float * dst_data = (device float *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||
device const T * const src = (device T *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
||||
device T * dst_data = (device T *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||
|
||||
const float x0 = src[0];
|
||||
const float x1 = src[1];
|
||||
const T x0 = src[0];
|
||||
const T x1 = src[1];
|
||||
|
||||
dst_data[0] = x0*cos_theta - x1*sin_theta;
|
||||
dst_data[1] = x0*sin_theta + x1*cos_theta;
|
||||
@ -870,8 +957,8 @@ kernel void kernel_rope(
|
||||
|
||||
const int64_t i0 = ib*n_dims + ic/2;
|
||||
|
||||
device const float * const src = (device float *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
||||
device float * dst_data = (device float *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||
device const T * const src = (device T *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
||||
device T * dst_data = (device T *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||
|
||||
const float x0 = src[0];
|
||||
const float x1 = src[n_dims/2];
|
||||
@ -883,6 +970,9 @@ kernel void kernel_rope(
|
||||
}
|
||||
}
|
||||
|
||||
template [[host_name("kernel_rope_f32")]] kernel rope_t kernel_rope<float>;
|
||||
template [[host_name("kernel_rope_f16")]] kernel rope_t kernel_rope<half>;
|
||||
|
||||
kernel void kernel_cpy_f16_f16(
|
||||
device const half * src0,
|
||||
device half * dst,
|
||||
@ -1273,8 +1363,8 @@ kernel void kernel_mul_mat_q3_K_f32(
|
||||
|
||||
float yl[32];
|
||||
|
||||
const uint16_t kmask1 = 0x3030;
|
||||
const uint16_t kmask2 = 0x0f0f;
|
||||
//const uint16_t kmask1 = 0x3030;
|
||||
//const uint16_t kmask2 = 0x0f0f;
|
||||
|
||||
const int tid = tiisg/4;
|
||||
const int ix = tiisg%4;
|
||||
@ -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_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_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.
BIN
electron/core/plugins/inference-plugin/nitro/nitro_linux_amd64_cuda
Executable file
BIN
electron/core/plugins/inference-plugin/nitro/nitro_linux_amd64_cuda
Executable file
Binary file not shown.
BIN
electron/core/plugins/inference-plugin/nitro/nitro_mac_amd64
Executable file
BIN
electron/core/plugins/inference-plugin/nitro/nitro_mac_amd64
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3674
electron/core/plugins/inference-plugin/package-lock.json
generated
Normal file
3674
electron/core/plugins/inference-plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,8 +11,8 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\" && npm pack",
|
||||
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
||||
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
@ -25,8 +25,7 @@
|
||||
"node-llama-cpp"
|
||||
],
|
||||
"dependencies": {
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"node-llama-cpp": "^2.4.1"
|
||||
"electron-is-dev": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@ -38,10 +38,20 @@ const deleteModel = async (path) =>
|
||||
}
|
||||
});
|
||||
|
||||
const searchModels = async (params) =>
|
||||
new Promise(async (resolve) => {
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI
|
||||
.invokePluginFunc(MODULE_PATH, "searchModels", params)
|
||||
.then((res) => resolve(res));
|
||||
}
|
||||
});
|
||||
|
||||
// Register all the above functions and objects with the relevant extension points
|
||||
export function init({ register }) {
|
||||
register("getDownloadedModels", "getDownloadedModels", getDownloadedModels);
|
||||
register("getAvailableModels", "getAvailableModels", getAvailableModels);
|
||||
register("downloadModel", "downloadModel", downloadModel);
|
||||
register("deleteModel", "deleteModel", deleteModel);
|
||||
register("searchModels", "searchModels", searchModels);
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
const path = require("path");
|
||||
const { readdirSync, lstatSync } = require("fs");
|
||||
const { app } = require("electron");
|
||||
const { listModels, listFiles, fileDownloadInfo } = require("@huggingface/hub");
|
||||
|
||||
let modelsIterator = undefined;
|
||||
let currentSearchOwner = undefined;
|
||||
|
||||
const ALL_MODELS = [
|
||||
{
|
||||
@ -87,6 +91,76 @@ function getDownloadedModels() {
|
||||
return downloadedModels;
|
||||
}
|
||||
|
||||
const getNextModels = async (count) => {
|
||||
const models = [];
|
||||
let hasMore = true;
|
||||
|
||||
while (models.length < count) {
|
||||
const next = await modelsIterator.next();
|
||||
|
||||
// end if we reached the end
|
||||
if (next.done) {
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const model = next.value;
|
||||
const files = await listFilesByName(model.name);
|
||||
|
||||
models.push({
|
||||
...model,
|
||||
files,
|
||||
});
|
||||
}
|
||||
|
||||
const result = {
|
||||
data: models,
|
||||
hasMore,
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
const searchModels = async (params) => {
|
||||
if (currentSearchOwner === params.search.owner && modelsIterator != null) {
|
||||
// paginated search
|
||||
console.debug(`Paginated search owner: ${params.search.owner}`);
|
||||
const models = await getNextModels(params.limit);
|
||||
return models;
|
||||
} else {
|
||||
// new search
|
||||
console.debug(`Init new search owner: ${params.search.owner}`);
|
||||
currentSearchOwner = params.search.owner;
|
||||
modelsIterator = listModels({
|
||||
search: params.search,
|
||||
credentials: params.credentials,
|
||||
});
|
||||
|
||||
const models = await getNextModels(params.limit);
|
||||
return models;
|
||||
}
|
||||
};
|
||||
|
||||
const listFilesByName = async (modelName) => {
|
||||
const repo = { type: "model", name: modelName };
|
||||
const fileDownloadInfoMap = {};
|
||||
for await (const file of listFiles({
|
||||
repo: repo,
|
||||
})) {
|
||||
if (file.type === "file" && file.path.endsWith(".bin")) {
|
||||
const downloadInfo = await fileDownloadInfo({
|
||||
repo: repo,
|
||||
path: file.path,
|
||||
});
|
||||
fileDownloadInfoMap[file.path] = {
|
||||
...file,
|
||||
...downloadInfo,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return fileDownloadInfoMap;
|
||||
};
|
||||
|
||||
function getAvailableModels() {
|
||||
const downloadedModelIds = getDownloadedModels().map((model) => model.id);
|
||||
return ALL_MODELS.filter((model) => {
|
||||
@ -99,4 +173,5 @@ function getAvailableModels() {
|
||||
module.exports = {
|
||||
getDownloadedModels,
|
||||
getAvailableModels,
|
||||
searchModels,
|
||||
};
|
||||
|
||||
3607
electron/core/plugins/model-management-plugin/package-lock.json
generated
Normal file
3607
electron/core/plugins/model-management-plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,8 +11,8 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
|
||||
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
||||
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\"",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
@ -24,5 +24,11 @@
|
||||
"dist/*",
|
||||
"package.json",
|
||||
"README.md"
|
||||
],
|
||||
"dependencies": {
|
||||
"@huggingface/hub": "^0.8.5"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"@huggingface/hub"
|
||||
]
|
||||
}
|
||||
|
||||
1479
electron/core/plugins/monitoring-plugin/package-lock.json
generated
Normal file
1479
electron/core/plugins/monitoring-plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,8 +11,8 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
|
||||
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
||||
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\"",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^3.0.2",
|
||||
|
||||
14
electron/entitlements.mac.plist
Normal file
14
electron/entitlements.mac.plist
Normal 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>
|
||||
@ -147,6 +147,18 @@ function handleIPCs() {
|
||||
ipcMain.handle("relaunch", async (_event, url) => {
|
||||
dispose(requiredModules);
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
});
|
||||
|
||||
ipcMain.handle("reloadPlugins", async (_event, url) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const fullPath = join(userDataPath, "plugins");
|
||||
|
||||
rmdir(fullPath, { recursive: true }, function (err) {
|
||||
if (err) console.log(err);
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -27,12 +27,18 @@
|
||||
],
|
||||
"extends": null,
|
||||
"mac": {
|
||||
"type": "distribution"
|
||||
}
|
||||
"type": "distribution",
|
||||
"entitlements": "./entitlements.mac.plist",
|
||||
"entitlementsInherit": "./entitlements.mac.plist",
|
||||
"notarize": {
|
||||
"teamId": "YT49P7GXG4"
|
||||
}
|
||||
},
|
||||
"artifactName": "${name}-${os}-${arch}-${version}.${ext}"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||
"test:e2e": "playwright test --workers=2",
|
||||
"test:e2e": "playwright test --workers=1",
|
||||
"dev": "tsc -p . && electron .",
|
||||
"build": "tsc -p . && electron-builder -p never -m",
|
||||
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
|
||||
@ -45,13 +51,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@npmcli/arborist": "^7.1.0",
|
||||
"@uiball/loaders": "^1.3.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.4",
|
||||
"pacote": "^17.0.4",
|
||||
"react-intersection-observer": "^9.5.2",
|
||||
"request": "^2.88.2",
|
||||
"request-progress": "^3.0.0"
|
||||
"request-progress": "^3.0.0",
|
||||
"use-debounce": "^9.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/notarize": "^2.1.0",
|
||||
"@playwright/test": "^1.38.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
|
||||
@ -3,7 +3,8 @@ import { PlaywrightTestConfig } from "@playwright/test";
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: "./tests",
|
||||
testIgnore: "./core/**",
|
||||
retries: 0
|
||||
retries: 0,
|
||||
timeout: 120000,
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@ -13,6 +13,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
|
||||
pluginPath: () => ipcRenderer.invoke("pluginPath"),
|
||||
|
||||
reloadPlugins: () => ipcRenderer.invoke("reloadPlugins"),
|
||||
|
||||
appVersion: () => ipcRenderer.invoke("appVersion"),
|
||||
|
||||
openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url),
|
||||
|
||||
@ -19,8 +19,6 @@ test.beforeAll(async () => {
|
||||
// parse the packaged Electron app and find paths and other info
|
||||
const appInfo = parseElectronApp(latestBuild);
|
||||
expect(appInfo).toBeTruthy();
|
||||
expect(appInfo.arch).toBeTruthy();
|
||||
expect(appInfo.arch).toBe(process.arch);
|
||||
expect(appInfo.asar).toBe(true);
|
||||
expect(appInfo.executable).toBeTruthy();
|
||||
expect(appInfo.main).toBeTruthy();
|
||||
|
||||
@ -52,10 +52,6 @@ test("renders left navigation panel", async () => {
|
||||
.getByRole("button", { name: "Explore Models" })
|
||||
.first()
|
||||
.isEnabled();
|
||||
const startConversation = await page
|
||||
.getByRole("button", { name: "Start a Conversation" })
|
||||
.first()
|
||||
.isEnabled();
|
||||
const discordBtn = await page
|
||||
.getByRole("button", { name: "Discord" })
|
||||
.first()
|
||||
@ -72,7 +68,6 @@ test("renders left navigation panel", async () => {
|
||||
[
|
||||
newChatBtn,
|
||||
exploreBtn,
|
||||
startConversation,
|
||||
discordBtn,
|
||||
myModelsBtn,
|
||||
settingsBtn,
|
||||
|
||||
1614
node_modules/.yarn-integrity
generated
vendored
1614
node_modules/.yarn-integrity
generated
vendored
File diff suppressed because it is too large
Load Diff
14224
package-lock.json
generated
Normal file
14224
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -21,7 +21,8 @@
|
||||
"dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
|
||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||
"build:electron": "yarn workspace jan-electron build",
|
||||
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/model-management-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
||||
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm ci\" \"cd ./electron/core/plugins/inference-plugin && npm ci\" \"cd ./electron/core/plugins/model-management-plugin && npm ci\" \"cd ./electron/core/plugins/monitoring-plugin && npm ci\" && concurrently \"cd ./electron/core/plugins/data-plugin && npm run build:publish\" \"cd ./electron/core/plugins/inference-plugin && npm run build:publish\" \"cd ./electron/core/plugins/model-management-plugin && npm run build:publish\" \"cd ./electron/core/plugins/monitoring-plugin && npm run build:publish\"",
|
||||
"build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm ci\" \"cd ./electron/core/plugins/inference-plugin && npm ci\" \"cd ./electron/core/plugins/model-management-plugin && npm ci\" \"cd ./electron/core/plugins/monitoring-plugin && npm ci\" && chmod +x ./electron/auto-sign.sh && ./electron/auto-sign.sh && concurrently \"cd ./electron/core/plugins/data-plugin && npm run build:publish\" \"cd ./electron/core/plugins/inference-plugin && npm run build:publish\" \"cd ./electron/core/plugins/model-management-plugin && npm run build:publish\" \"cd ./electron/core/plugins/monitoring-plugin && npm run build:publish\"",
|
||||
"build": "yarn build:web && yarn build:electron",
|
||||
"build:darwin": "yarn build:web && yarn workspace jan-electron build:darwin",
|
||||
"build:win32": "yarn build:web && yarn workspace jan-electron build:win32",
|
||||
|
||||
@ -9,10 +9,10 @@ const ActiveModelTable: React.FC = () => {
|
||||
if (!activeModel) return null;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="pl-[63px] pr-[89px]">
|
||||
<h3 className="text-xl leading-[25px] mb-[13px]">Active Model(s)</h3>
|
||||
<ModelTable models={[activeModel]} />
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ const AvailableModelCard: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg border-gray-200">
|
||||
<div className="flex justify-between py-4 px-3 gap-[10px]">
|
||||
<div className="flex justify-between py-4 px-3 gap-2.5">
|
||||
<DownloadModelContent
|
||||
required={required}
|
||||
author={product.author}
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import React, { useCallback, useRef, useState, useEffect } from "react";
|
||||
import ChatItem from "../ChatItem";
|
||||
import { ChatMessage } from "@/_models/ChatMessage";
|
||||
import useChatMessages from "@/_hooks/useChatMessages";
|
||||
import { showingTyping } from "@/_helpers/JotaiWrapper";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import LoadingIndicator from "../LoadingIndicator";
|
||||
import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||
import { chatMessages } from "@/_helpers/atoms/ChatMessage.atom";
|
||||
|
||||
@ -16,12 +14,11 @@ const ChatBody: React.FC = () => {
|
||||
const messageList = useAtomValue(
|
||||
selectAtom(
|
||||
chatMessages,
|
||||
useCallback((v) => v[activeConversationId], [activeConversationId])
|
||||
)
|
||||
useCallback((v) => v[activeConversationId], [activeConversationId]),
|
||||
),
|
||||
);
|
||||
const [content, setContent] = useState<React.JSX.Element[]>([]);
|
||||
|
||||
const isTyping = useAtomValue(showingTyping);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const { loading, hasMore } = useChatMessages(offset);
|
||||
const intersectObs = useRef<any>(null);
|
||||
@ -40,10 +37,10 @@ const ChatBody: React.FC = () => {
|
||||
|
||||
if (message) intersectObs.current.observe(message);
|
||||
},
|
||||
[loading, hasMore]
|
||||
[loading, hasMore],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const list = messageList?.map((message, index) => {
|
||||
if (messageList?.length === index + 1) {
|
||||
return (
|
||||
@ -58,11 +55,6 @@ const ChatBody: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col-reverse flex-1 py-4 overflow-y-auto scroll">
|
||||
{isTyping && (
|
||||
<div className="ml-4 mb-2" key="indicator">
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
)}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -2,22 +2,17 @@ import SimpleControlNetMessage from "../SimpleControlNetMessage";
|
||||
import SimpleImageMessage from "../SimpleImageMessage";
|
||||
import SimpleTextMessage from "../SimpleTextMessage";
|
||||
import { ChatMessage, MessageType } from "@/_models/ChatMessage";
|
||||
import StreamTextMessage from "../StreamTextMessage";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { currentStreamingMessageAtom } from "@/_helpers/atoms/ChatMessage.atom";
|
||||
|
||||
export default function renderChatMessage({
|
||||
id,
|
||||
messageType,
|
||||
messageSenderType,
|
||||
senderAvatarUrl,
|
||||
senderName,
|
||||
createdAt,
|
||||
imageUrls,
|
||||
htmlText,
|
||||
text,
|
||||
}: ChatMessage): React.ReactNode {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const message = useAtomValue(currentStreamingMessageAtom);
|
||||
switch (messageType) {
|
||||
case MessageType.ImageWithText:
|
||||
return (
|
||||
@ -42,22 +37,14 @@ export default function renderChatMessage({
|
||||
/>
|
||||
);
|
||||
case MessageType.Text:
|
||||
return id !== message?.id ? (
|
||||
return (
|
||||
<SimpleTextMessage
|
||||
key={id}
|
||||
avatarUrl={senderAvatarUrl}
|
||||
senderName={senderName}
|
||||
createdAt={createdAt}
|
||||
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
|
||||
/>
|
||||
) : (
|
||||
<StreamTextMessage
|
||||
key={id}
|
||||
id={id}
|
||||
avatarUrl={senderAvatarUrl}
|
||||
senderName={senderName}
|
||||
createdAt={createdAt}
|
||||
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
|
||||
senderType={messageSenderType}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
||||
@ -63,8 +63,8 @@ const ConfirmDeleteConversationModal: React.FC = () => {
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Are you sure you want to delete this conversation? All
|
||||
of messages will be permanently removed from our servers
|
||||
forever. This action cannot be undone.
|
||||
of messages will be permanently removed. This action
|
||||
cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -32,7 +32,7 @@ const ConversationalCard: React.FC<Props> = ({ product }) => {
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex text-xs leading-5 text-gray-500 items-center gap-[2px]">
|
||||
<span className="flex text-xs leading-5 text-gray-500 items-center gap-0.5">
|
||||
<Image src={"icons/play.svg"} width={16} height={16} alt="" />
|
||||
32.2k runs
|
||||
</span>
|
||||
|
||||
@ -18,19 +18,19 @@ const DownloadModelContent: React.FC<Props> = ({
|
||||
type,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-4/5 flex flex-col gap-[10px]">
|
||||
<div className="w-4/5 flex flex-col gap-2.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<h2 className="font-medium text-xl leading-[25px] tracking-[-0.4px] text-gray-900">
|
||||
{name}
|
||||
</h2>
|
||||
<DownloadModelTitle title={type} />
|
||||
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
|
||||
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
|
||||
<span className="text-xs leading-[18px] font-semibold text-purple-800">
|
||||
{author}
|
||||
</span>
|
||||
</div>
|
||||
{required && (
|
||||
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
|
||||
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
|
||||
<span className="text-xs leading-[18px] text-[#11192899]">
|
||||
Required{" "}
|
||||
</span>
|
||||
@ -44,7 +44,7 @@ const DownloadModelContent: React.FC<Props> = ({
|
||||
<div
|
||||
className={`${
|
||||
isRecommend ? "flex" : "hidden"
|
||||
} w-fit justify-center items-center bg-green-50 rounded-full px-[10px] py-[2px] gap-2`}
|
||||
} w-fit justify-center items-center bg-green-50 rounded-full px-2.5 py-0.5 gap-2`}
|
||||
>
|
||||
<div className="w-3 h-3 rounded-full bg-green-400"></div>
|
||||
<span className="text-green-600 font-medium text-xs leading-18px">
|
||||
|
||||
@ -3,7 +3,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const DownloadModelTitle: React.FC<Props> = ({ title }) => (
|
||||
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
|
||||
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
|
||||
<span className="text-xs leading-[18px] font-medium text-purple-800">
|
||||
{title}
|
||||
</span>
|
||||
|
||||
@ -16,7 +16,7 @@ const DownloadedModelCard: React.FC<Props> = ({
|
||||
onDeleteClick,
|
||||
}) => (
|
||||
<div className="border rounded-lg border-gray-200">
|
||||
<div className="flex justify-between py-4 px-3 gap-[10px]">
|
||||
<div className="flex justify-between py-4 px-3 gap-2.5">
|
||||
<DownloadModelContent
|
||||
required={required}
|
||||
author={product.author}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { Fragment } from "react";
|
||||
import React from "react";
|
||||
import SearchBar from "../SearchBar";
|
||||
import ModelTable from "../ModelTable";
|
||||
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
||||
@ -6,14 +6,16 @@ import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
||||
const DownloadedModelTable: React.FC = () => {
|
||||
const { downloadedModels } = useGetDownloadedModels();
|
||||
|
||||
if (!downloadedModels || downloadedModels.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="pl-[63px] pr-[89px]">
|
||||
<h3 className="text-xl leading-[25px] mt-[50px]">Downloaded Models</h3>
|
||||
<div className="py-5 w-[568px]">
|
||||
<SearchBar />
|
||||
</div>
|
||||
<ModelTable models={downloadedModels} />
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
29
web/app/_components/DownloadingModelTable/index.tsx
Normal file
29
web/app/_components/DownloadingModelTable/index.tsx
Normal 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;
|
||||
@ -8,7 +8,7 @@ type Props = {
|
||||
|
||||
const ExpandableHeader: React.FC<Props> = ({ title, expanded, onClick }) => (
|
||||
<button onClick={onClick} className="flex items-center justify-between px-2">
|
||||
<h2 className="text-gray-400 font-bold text-[12px] leading-[12px] pl-1">
|
||||
<h2 className="text-gray-400 font-bold text-xs leading-[12px] pl-1">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="mr-2">
|
||||
|
||||
@ -1,55 +1,20 @@
|
||||
import useGetAvailableModels from "@/_hooks/useGetAvailableModels";
|
||||
import ExploreModelItem from "../ExploreModelItem";
|
||||
import HeaderTitle from "../HeaderTitle";
|
||||
import SearchBar from "../SearchBar";
|
||||
import SimpleCheckbox from "../SimpleCheckbox";
|
||||
import SimpleTag, { TagType } from "../SimpleTag";
|
||||
import SearchBar, { SearchType } from "../SearchBar";
|
||||
import ExploreModelList from "../ExploreModelList";
|
||||
import ExploreModelFilter from "../ExploreModelFilter";
|
||||
|
||||
const tags = [
|
||||
"Roleplay",
|
||||
"Llama",
|
||||
"Story",
|
||||
"Casual",
|
||||
"Professional",
|
||||
"CodeLlama",
|
||||
"Coding",
|
||||
];
|
||||
const checkboxs = ["GGUF", "TensorRT", "Meow", "JigglyPuff"];
|
||||
|
||||
const ExploreModelContainer: React.FC = () => {
|
||||
const { allAvailableModels } = useGetAvailableModels();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
|
||||
<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>
|
||||
const ExploreModelContainer: React.FC = () => (
|
||||
<div className="flex flex-col flex-1 px-16 pt-14 overflow-hidden">
|
||||
<HeaderTitle title="Explore Models" />
|
||||
<SearchBar
|
||||
type={SearchType.Model}
|
||||
placeholder="Owner name like TheBloke, bhlim etc.."
|
||||
/>
|
||||
<div className="flex flex-1 gap-x-10 mt-9 overflow-hidden">
|
||||
<ExploreModelFilter />
|
||||
<ExploreModelList />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ExploreModelContainer;
|
||||
|
||||
40
web/app/_components/ExploreModelFilter/index.tsx
Normal file
40
web/app/_components/ExploreModelFilter/index.tsx
Normal 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;
|
||||
@ -1,36 +1,30 @@
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
"use client";
|
||||
|
||||
import ExploreModelItemHeader from "../ExploreModelItemHeader";
|
||||
import ModelVersionList from "../ModelVersionList";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Product } from "@/_models/Product";
|
||||
import { Fragment, forwardRef, useState } from "react";
|
||||
import SimpleTag, { TagType } from "../SimpleTag";
|
||||
import { displayDate } from "@/_utils/datetime";
|
||||
import useDownloadModel from "@/_hooks/useDownloadModel";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
|
||||
import { Product } from "@/_models/Product";
|
||||
|
||||
type Props = {
|
||||
model: Product;
|
||||
};
|
||||
|
||||
const ExploreModelItem: React.FC<Props> = ({ model }) => {
|
||||
const downloadAtom = useMemo(
|
||||
() => atom((get) => get(modelDownloadStateAtom)[model.fileName ?? ""]),
|
||||
[model.fileName ?? ""]
|
||||
);
|
||||
const downloadState = useAtomValue(downloadAtom);
|
||||
const { downloadModel } = useDownloadModel();
|
||||
const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col border border-gray-200 rounded-[5px]">
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex flex-col border border-gray-200 rounded-md mb-4"
|
||||
>
|
||||
<ExploreModelItemHeader
|
||||
name={model.name}
|
||||
status={TagType.Recommended}
|
||||
total={model.totalSize}
|
||||
downloadState={downloadState}
|
||||
onDownloadClick={() => downloadModel(model)}
|
||||
versions={model.availableVersions}
|
||||
/>
|
||||
<div className="flex flex-col px-[26px] py-[22px]">
|
||||
<div className="flex justify-between">
|
||||
@ -39,7 +33,7 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
|
||||
<div className="text-sm font-medium text-gray-500">
|
||||
Model Format
|
||||
</div>
|
||||
<div className="px-[10px] py-0.5 bg-gray-100 text-xs text-gray-800 w-fit">
|
||||
<div className="px-2.5 py-0.5 bg-gray-100 text-xs text-gray-800 w-fit">
|
||||
GGUF
|
||||
</div>
|
||||
</div>
|
||||
@ -87,15 +81,24 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
|
||||
<span className="text-sm font-medium text-gray-500">Tags</span>
|
||||
</div>
|
||||
</div>
|
||||
{show && <ModelVersionList />}
|
||||
<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>
|
||||
{model.availableVersions.length > 0 && (
|
||||
<Fragment>
|
||||
{show && (
|
||||
<ModelVersionList
|
||||
model={model}
|
||||
versions={model.availableVersions}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShow(!show)}
|
||||
className="bg-[#FBFBFB] text-gray-500 text-sm text-left py-2 px-4 border-t border-gray-200"
|
||||
>
|
||||
{!show ? "+ Show Available Versions" : "- Collapse"}
|
||||
</button>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default ExploreModelItem;
|
||||
|
||||
@ -3,11 +3,13 @@ import PrimaryButton from "../PrimaryButton";
|
||||
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
|
||||
import { DownloadState } from "@/_models/DownloadState";
|
||||
import SecondaryButton from "../SecondaryButton";
|
||||
import { ModelVersion } from "@/_models/Product";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
total: number;
|
||||
status: TagType;
|
||||
versions: ModelVersion[];
|
||||
size?: number;
|
||||
downloadState?: DownloadState;
|
||||
onDownloadClick?: () => void;
|
||||
};
|
||||
@ -15,30 +17,41 @@ type Props = {
|
||||
const ExploreModelItemHeader: React.FC<Props> = ({
|
||||
name,
|
||||
status,
|
||||
total,
|
||||
size,
|
||||
versions,
|
||||
downloadState,
|
||||
onDownloadClick,
|
||||
}) => (
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{name}</span>
|
||||
<SimpleTag title={status} type={status} clickable={false} />
|
||||
</div>
|
||||
{downloadState != null ? (
|
||||
}) => {
|
||||
let downloadButton = (
|
||||
<PrimaryButton
|
||||
title={size ? `Download (${toGigabytes(size)})` : "Download"}
|
||||
onClick={() => onDownloadClick?.()}
|
||||
/>
|
||||
);
|
||||
|
||||
if (downloadState != null) {
|
||||
// downloading
|
||||
downloadButton = (
|
||||
<SecondaryButton
|
||||
disabled
|
||||
title={`Downloading (${formatDownloadPercentage(
|
||||
downloadState.percent
|
||||
)})`}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
title={total ? `Download (${toGigabytes(total)})` : "Download"}
|
||||
onClick={() => onDownloadClick?.()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
} else if (versions.length === 0) {
|
||||
downloadButton = <SecondaryButton disabled title="No files available" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{name}</span>
|
||||
<SimpleTag title={status} type={status} clickable={false} />
|
||||
</div>
|
||||
{downloadButton}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreModelItemHeader;
|
||||
|
||||
52
web/app/_components/ExploreModelList/index.tsx
Normal file
52
web/app/_components/ExploreModelList/index.tsx
Normal 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;
|
||||
@ -1,11 +1,14 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const HeaderTitle: React.FC<Props> = ({ title }) => (
|
||||
<h2 className="my-5 font-semibold text-[34px] tracking-[-0.4px] leading-[41px]">
|
||||
const HeaderTitle: React.FC<Props> = ({ title, className }) => (
|
||||
<h2
|
||||
className={`my-5 font-semibold text-[34px] tracking-[-0.4px] leading-[41px] ${className}`}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
);
|
||||
|
||||
@ -66,7 +66,7 @@ const HistoryItem: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`flex flex-row mx-1 items-center gap-[10px] rounded-lg p-2 ${backgroundColor} hover:bg-hover-light`}
|
||||
className={`flex flex-row mx-1 items-center gap-2.5 rounded-lg p-2 ${backgroundColor} hover:bg-hover-light`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Image
|
||||
@ -79,7 +79,7 @@ const HistoryItem: React.FC<Props> = ({
|
||||
<div className="flex flex-col justify-between text-sm leading-[20px] w-full">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-gray-900 text-left">{name}</span>
|
||||
<span className="text-[11px] leading-[13px] tracking-[-0.4px] text-gray-400">
|
||||
<span className="text-xs leading-[13px] tracking-[-0.4px] text-gray-400">
|
||||
{updatedAt && new Date(updatedAt).toDateString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -18,14 +18,14 @@ const HistoryList: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-grow pt-3 gap-2">
|
||||
<div className="flex flex-col flex-grow pt-3 gap-2 overflow-hidden">
|
||||
<ExpandableHeader
|
||||
title="CHAT HISTORY"
|
||||
expanded={expand}
|
||||
onClick={() => setExpand(!expand)}
|
||||
/>
|
||||
<div
|
||||
className={`flex flex-col gap-1 mt-1 ${!expand ? "hidden " : "block"}`}
|
||||
className={`flex flex-col gap-1 mt-1 overflow-y-auto scroll ${!expand ? "hidden " : "block"}`}
|
||||
>
|
||||
{conversations.length > 0 ? (
|
||||
conversations
|
||||
|
||||
@ -4,19 +4,56 @@ import BasicPromptInput from "../BasicPromptInput";
|
||||
import BasicPromptAccessories from "../BasicPromptAccessories";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
|
||||
import SecondaryButton from "../SecondaryButton";
|
||||
import { Fragment } from "react";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||
import { showingTyping } from "@/_helpers/JotaiWrapper";
|
||||
import LoadingIndicator from "../LoadingIndicator";
|
||||
|
||||
const InputToolbar: React.FC = () => {
|
||||
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom);
|
||||
const currentProduct = useAtomValue(currentProductAtom);
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
const isTyping = useAtomValue(showingTyping);
|
||||
|
||||
if (showingAdvancedPrompt) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
// TODO: implement regenerate
|
||||
// const onRegenerateClick = () => {};
|
||||
|
||||
const onNewConversationClick = () => {
|
||||
if (currentProduct) {
|
||||
requestCreateConvo(currentProduct);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
<div className="flex justify-between gap-2 mr-3 my-2">
|
||||
<div className="h-6">
|
||||
{isTyping && (
|
||||
<div className="my-2" key="indicator">
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
)}{" "}
|
||||
</div>
|
||||
|
||||
{/* <SecondaryButton title="Regenerate" onClick={onRegenerateClick} /> */}
|
||||
<SecondaryButton
|
||||
onClick={onNewConversationClick}
|
||||
title="New Conversation"
|
||||
icon={<PlusIcon width={16} height={16} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-3 mb-3 flex-none overflow-hidden shadow-sm ring-1 ring-inset ring-gray-300 rounded-lg dark:bg-gray-800">
|
||||
<BasicPromptInput />
|
||||
<BasicPromptAccessories />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ const JanLogo: React.FC = () => {
|
||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
|
||||
return (
|
||||
<button
|
||||
className="p-3 flex gap-[2px] items-center"
|
||||
className="p-3 flex gap-0.5 items-center"
|
||||
onClick={() => setActiveConvoId(undefined)}
|
||||
>
|
||||
<Image src={"icons/app_icon.svg"} width={28} height={28} alt="" />
|
||||
|
||||
@ -6,7 +6,7 @@ import HistoryList from "../HistoryList";
|
||||
import NewChatButton from "../NewChatButton";
|
||||
|
||||
const LeftContainer: React.FC = () => (
|
||||
<div className="w-[323px] flex-shrink-0 p-3 h-screen border-r border-gray-200 flex flex-col">
|
||||
<div className="w-[323px] flex-shrink-0 py-3 h-screen border-r border-gray-200 flex flex-col">
|
||||
<SidebarHeader />
|
||||
<NewChatButton />
|
||||
<HistoryList />
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
const LoadingIndicator = () => {
|
||||
let circleCommonClasses = "h-1.5 w-1.5 bg-current rounded-full";
|
||||
|
||||
return (
|
||||
// <div className="flex">
|
||||
// <div className={`${circleCommonClasses} mr-1 animate-bounce`}></div>
|
||||
// <div className={`${circleCommonClasses} mr-1 animate-bounce200`}></div>
|
||||
// <div className={`${circleCommonClasses} animate-bounce400`}></div>
|
||||
// </div>
|
||||
<div className="typingIndicatorContainer">
|
||||
<div className="typingIndicatorBubble">
|
||||
<div className="typingIndicatorBubbleDot"></div>
|
||||
|
||||
@ -12,7 +12,7 @@ const LoginButton: React.FC = () => {
|
||||
// <button
|
||||
// onClick={signInWithKeyCloak}
|
||||
// type="button"
|
||||
// className="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
// className="rounded-md bg-blue-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
// >
|
||||
// Login
|
||||
// </button>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { ReactNode } from "react";
|
||||
import Welcome from "../WelcomeContainer";
|
||||
import { Preferences } from "../Preferences";
|
||||
import MyModelContainer from "../MyModelContainer";
|
||||
@ -11,17 +10,14 @@ import {
|
||||
getMainViewStateAtom,
|
||||
} from "@/_helpers/atoms/MainView.atom";
|
||||
import EmptyChatContainer from "../EmptyChatContainer";
|
||||
import MainChat from "../MainChat";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function ChatContainer({ children }: Props) {
|
||||
const MainView: React.FC = () => {
|
||||
const viewState = useAtomValue(getMainViewStateAtom);
|
||||
|
||||
switch (viewState) {
|
||||
case MainViewState.ConversationEmptyModel:
|
||||
return <EmptyChatContainer />
|
||||
return <EmptyChatContainer />;
|
||||
case MainViewState.ExploreModel:
|
||||
return <ExploreModelContainer />;
|
||||
case MainViewState.Setting:
|
||||
@ -32,6 +28,8 @@ export default function ChatContainer({ children }: Props) {
|
||||
case MainViewState.Welcome:
|
||||
return <Welcome />;
|
||||
default:
|
||||
return <div className="flex flex-1 overflow-hidden">{children}</div>;
|
||||
return <MainChat />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default MainView;
|
||||
@ -38,7 +38,7 @@ const ModelActionButton: React.FC<Props> = ({ type, onActionClick }) => {
|
||||
|
||||
return (
|
||||
<td className="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<PrimaryButton title={styles.title} onClick={onClick} />
|
||||
<PrimaryButton title={styles.title} onClick={onClick} className={styles.backgroundColor} />
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,30 +6,37 @@ type Props = {
|
||||
onDeleteClick: () => void;
|
||||
};
|
||||
|
||||
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => {
|
||||
return (
|
||||
<Menu as="div" className="relative flex-none">
|
||||
<Menu.Button className="block text-gray-500 hover:text-gray-900">
|
||||
<span className="sr-only">Open options</span>
|
||||
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
<Menu.Item>
|
||||
<button onClick={onDeleteClick}>Delete</button>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => (
|
||||
<Menu as="div" className="relative flex-none">
|
||||
<Menu.Button className="block text-gray-500 hover:text-gray-900">
|
||||
<span className="sr-only">Open options</span>
|
||||
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${
|
||||
active ? "bg-violet-500 text-white" : "text-gray-900"
|
||||
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
|
||||
onClick={onDeleteClick}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
export default ModelActionMenu;
|
||||
|
||||
@ -11,7 +11,7 @@ const ModelDownloadingButton: React.FC<Props> = ({ total, value }) => {
|
||||
<button className="py-2 px-3 flex gap-2 border text-xs leading-[18px] border-gray-200 rounded-lg">
|
||||
Downloading...
|
||||
</button>
|
||||
<div className="py-[2px] px-[10px] bg-gray-200 rounded">
|
||||
<div className="py-0.5 px-2.5 bg-gray-200 rounded">
|
||||
<span className="text-xs font-medium text-gray-800">
|
||||
{toGigabytes(value)} / {toGigabytes(total)}
|
||||
</span>
|
||||
|
||||
36
web/app/_components/ModelDownloadingRow/index.tsx
Normal file
36
web/app/_components/ModelDownloadingRow/index.tsx
Normal 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;
|
||||
34
web/app/_components/ModelDownloadingTable/index.tsx
Normal file
34
web/app/_components/ModelDownloadingTable/index.tsx
Normal 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);
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -1,29 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
|
||||
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||
|
||||
const ModelMenu: React.FC = () => {
|
||||
const currentProduct = useAtomValue(currentProductAtom);
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
const setShowConfirmDeleteConversationModal = useSetAtom(
|
||||
showConfirmDeleteConversationModalAtom
|
||||
);
|
||||
|
||||
const onCreateConvoClick = () => {
|
||||
if (currentProduct) {
|
||||
requestCreateConvo(currentProduct);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={() => onCreateConvoClick()}>
|
||||
<PlusIcon width={24} height={24} color="#9CA3AF" />
|
||||
</button>
|
||||
<button onClick={() => setShowConfirmDeleteConversationModal(true)}>
|
||||
<TrashIcon width={24} height={24} color="#9CA3AF" />
|
||||
</button>
|
||||
|
||||
@ -14,7 +14,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const ModelRow: React.FC<Props> = ({ model }) => {
|
||||
const { startModel } = useStartStopModel();
|
||||
const { startModel, stopModel } = useStartStopModel();
|
||||
const activeModel = useAtomValue(currentProductAtom);
|
||||
const { deleteModel } = useDeleteModel();
|
||||
|
||||
@ -31,6 +31,8 @@ const ModelRow: React.FC<Props> = ({ model }) => {
|
||||
const onModelActionClick = (action: ModelActionType) => {
|
||||
if (action === ModelActionType.Start) {
|
||||
startModel(model.id);
|
||||
} else {
|
||||
stopModel(model.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import { Fragment, useEffect } from "react";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
|
||||
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
||||
import { Product } from "@/_models/Product";
|
||||
import { useAtom } from "jotai";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { selectedModelAtom } from "@/_helpers/atoms/Model.atom";
|
||||
import { downloadedModelAtom } from "@/_helpers/atoms/DownloadedModel.atom";
|
||||
|
||||
function classNames(...classes: any) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
const SelectModels: React.FC = () => {
|
||||
const { downloadedModels } = useGetDownloadedModels();
|
||||
const downloadedModels = useAtomValue(downloadedModelAtom);
|
||||
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom);
|
||||
|
||||
useEffect(() => {
|
||||
@ -36,7 +36,7 @@ const SelectModels: React.FC = () => {
|
||||
Select a Model:
|
||||
</Listbox.Label>
|
||||
<div className="relative mt-[19px]">
|
||||
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 sm:text-sm sm:leading-6">
|
||||
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-sm sm:leading-6">
|
||||
<span className="flex items-center">
|
||||
<img
|
||||
src={selectedModel.avatarUrl}
|
||||
@ -68,8 +68,8 @@ const SelectModels: React.FC = () => {
|
||||
key={model.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-600 text-white" : "text-gray-900",
|
||||
"relative cursor-default select-none py-2 pl-3 pr-9"
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900",
|
||||
"relative cursor-default select-none py-2 pl-3 pr-9",
|
||||
)
|
||||
}
|
||||
value={model}
|
||||
@ -85,7 +85,7 @@ const SelectModels: React.FC = () => {
|
||||
<span
|
||||
className={classNames(
|
||||
selected ? "font-semibold" : "font-normal",
|
||||
"ml-3 block truncate"
|
||||
"ml-3 block truncate",
|
||||
)}
|
||||
>
|
||||
{model.name}
|
||||
@ -95,8 +95,8 @@ const SelectModels: React.FC = () => {
|
||||
{selected ? (
|
||||
<span
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-indigo-600",
|
||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
active ? "text-white" : "text-blue-600",
|
||||
"absolute inset-y-0 right-0 flex items-center pr-4",
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
|
||||
@ -38,7 +38,7 @@ export const ModelStatusComponent: React.FC<Props> = ({ status }) => {
|
||||
const statusType = ModelStatusMapper[status];
|
||||
return (
|
||||
<div
|
||||
className={`rounded-[10px] py-0.5 px-[10px] w-fit text-xs font-medium ${statusType.backgroundColor}`}
|
||||
className={`rounded-[10px] py-0.5 px-2.5 w-fit text-xs font-medium ${statusType.backgroundColor}`}
|
||||
>
|
||||
{statusType.title}
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,7 @@ type Props = {
|
||||
const tableHeaders = ["MODEL", "FORMAT", "SIZE", "STATUS", "ACTIONS"];
|
||||
|
||||
const ModelTable: React.FC<Props> = ({ models }) => (
|
||||
<div className="flow-root inline-block border rounded-lg border-gray-200 min-w-full align-middle shadow-lg">
|
||||
<div className="flow-root border rounded-lg border-gray-200 min-w-full align-middle shadow-lg">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr className="rounded-t-lg">
|
||||
|
||||
@ -1,25 +1,57 @@
|
||||
import React from "react";
|
||||
import { toGigabytes } from "@/_utils/converter";
|
||||
import React, { useMemo } from "react";
|
||||
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
|
||||
import Image from "next/image";
|
||||
import { ModelVersion, Product } from "@/_models/Product";
|
||||
import useDownloadModel from "@/_hooks/useDownloadModel";
|
||||
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
totalSizeInByte: number;
|
||||
model: Product;
|
||||
modelVersion: ModelVersion;
|
||||
};
|
||||
|
||||
const ModelVersionItem: React.FC<Props> = ({ title, totalSizeInByte }) => (
|
||||
<div className="flex justify-between items-center gap-4 pl-[13px] pt-[13px] pr-[17px] pb-3 border-t border-gray-200 first:border-t-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<Image src={"/icons/app_icon.svg"} width={14} height={20} alt="" />
|
||||
<span className="font-sm text-gray-900">{title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="px-[10px] py-0.5 bg-gray-200 text-xs font-medium rounded">
|
||||
{toGigabytes(totalSizeInByte)}
|
||||
const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
|
||||
const { downloadHfModel } = useDownloadModel();
|
||||
const downloadAtom = useMemo(
|
||||
() => atom((get) => get(modelDownloadStateAtom)[modelVersion.path ?? ""]),
|
||||
[modelVersion.path ?? ""]
|
||||
);
|
||||
const downloadState = useAtomValue(downloadAtom);
|
||||
|
||||
const onDownloadClick = () => {
|
||||
downloadHfModel(model, modelVersion);
|
||||
};
|
||||
|
||||
let downloadButton = (
|
||||
<button
|
||||
className="text-indigo-600 text-sm font-medium"
|
||||
onClick={onDownloadClick}
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
);
|
||||
|
||||
if (downloadState) {
|
||||
downloadButton = (
|
||||
<div>{formatDownloadPercentage(downloadState.percent)}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center gap-4 pl-3 pt-3 pr-4 pb-3 border-t border-gray-200 first:border-t-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<Image src={"/icons/app_icon.svg"} width={14} height={20} alt="" />
|
||||
<span className="font-sm text-gray-900">{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>
|
||||
<button className="text-indigo-600 text-sm font-medium">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelVersionItem;
|
||||
|
||||
@ -1,38 +1,21 @@
|
||||
import React from "react";
|
||||
import ModelVersionItem from "../ModelVersionItem";
|
||||
import { ModelVersion, Product } from "@/_models/Product";
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: "Q4_K_M.gguf",
|
||||
total: 5600,
|
||||
},
|
||||
{
|
||||
name: "Q4_K_M.gguf",
|
||||
total: 5600,
|
||||
},
|
||||
{
|
||||
name: "Q4_K_M.gguf",
|
||||
total: 5600,
|
||||
},
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
type Props = {
|
||||
model: Product;
|
||||
versions: ModelVersion[];
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@ -2,63 +2,43 @@ import ProgressBar from "../ProgressBar";
|
||||
import SystemItem from "../SystemItem";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { appDownloadProgress } from "@/_helpers/JotaiWrapper";
|
||||
import { useEffect, useState } from "react";
|
||||
import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager";
|
||||
import { SystemMonitoringService } from "../../../shared/coreService";
|
||||
import { getSystemBarVisibilityAtom } from "@/_helpers/atoms/SystemBar.atom";
|
||||
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||
import useGetAppVersion from "@/_hooks/useGetAppVersion";
|
||||
import useGetSystemResources from "@/_hooks/useGetSystemResources";
|
||||
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
|
||||
import { DownloadState } from "@/_models/DownloadState";
|
||||
import { formatDownloadPercentage } from "@/_utils/converter";
|
||||
|
||||
const MonitorBar: React.FC = () => {
|
||||
const show = useAtomValue(getSystemBarVisibilityAtom);
|
||||
const progress = useAtomValue(appDownloadProgress);
|
||||
const activeModel = useAtomValue(currentProductAtom);
|
||||
const [ram, setRam] = useState<number>(0);
|
||||
const [gpu, setGPU] = useState<number>(0);
|
||||
const [cpu, setCPU] = useState<number>(0);
|
||||
const [version, setVersion] = useState<string>("");
|
||||
const { version } = useGetAppVersion();
|
||||
const { ram, cpu } = useGetSystemResources();
|
||||
const modelDownloadStates = useAtomValue(modelDownloadStateAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const getSystemResources = async () => {
|
||||
const resourceInfor = await executeSerial(
|
||||
SystemMonitoringService.GET_RESOURCES_INFORMATION
|
||||
);
|
||||
const currentLoadInfor = await executeSerial(
|
||||
SystemMonitoringService.GET_CURRENT_LOAD_INFORMATION
|
||||
);
|
||||
const ram =
|
||||
(resourceInfor?.mem?.used ?? 0) / (resourceInfor?.mem?.total ?? 1);
|
||||
setRam(Math.round(ram * 100));
|
||||
setCPU(Math.round(currentLoadInfor?.currentLoad ?? 0));
|
||||
};
|
||||
const getAppVersion = () => {
|
||||
window.electronAPI.appVersion().then((version: string | undefined) => {
|
||||
setVersion(version ?? "");
|
||||
});
|
||||
};
|
||||
getAppVersion();
|
||||
getSystemResources();
|
||||
// Fetch interval - every 3s
|
||||
const intervalId = setInterval(() => {
|
||||
getSystemResources();
|
||||
}, 3000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
if (!show) return null;
|
||||
const downloadStates: DownloadState[] = [];
|
||||
for (const [, value] of Object.entries(modelDownloadStates)) {
|
||||
downloadStates.push(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center justify-between border-t border-gray-200">
|
||||
{progress && progress >= 0 ? (
|
||||
<ProgressBar total={100} used={progress} />
|
||||
) : (
|
||||
<div className="w-full" />
|
||||
)}
|
||||
<div className="flex-1 flex items-center gap-8 px-2">
|
||||
) : null}
|
||||
<div className="flex-1 justify-end flex items-center gap-8 px-2">
|
||||
{downloadStates.length > 0 && (
|
||||
<SystemItem
|
||||
name="Downloading"
|
||||
value={`${downloadStates[0].fileName}: ${formatDownloadPercentage(
|
||||
downloadStates[0].percent
|
||||
)}`}
|
||||
/>
|
||||
)}
|
||||
<SystemItem name="CPU" value={`${cpu}%`} />
|
||||
<SystemItem name="Mem" value={`${ram}%`} />
|
||||
|
||||
{activeModel && (
|
||||
<SystemItem name={`Active model: ${activeModel.name}`} value={"1"} />
|
||||
<SystemItem name={`Active model: ${activeModel.name}`} value={""} />
|
||||
)}
|
||||
<span className="text-gray-900 text-sm">v{version}</span>
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import HeaderTitle from "../HeaderTitle";
|
||||
import DownloadedModelTable from "../DownloadedModelTable";
|
||||
import ActiveModelTable from "../ActiveModelTable";
|
||||
import DownloadingModelTable from "../DownloadingModelTable";
|
||||
|
||||
const MyModelContainer: React.FC = () => (
|
||||
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px]">
|
||||
<HeaderTitle title="My Models" />
|
||||
<ActiveModelTable />
|
||||
<DownloadedModelTable />
|
||||
<div className="flex flex-col flex-1 pt-[60px]">
|
||||
<HeaderTitle title="My Models" className="pl-[63px] pr-[89px]" />
|
||||
<div className="pb-6 overflow-y-auto scroll">
|
||||
<ActiveModelTable />
|
||||
<DownloadingModelTable />
|
||||
<DownloadedModelTable />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||
import useInitModel from "@/_hooks/useInitModel";
|
||||
import { Product } from "@/_models/Product";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
const NewChatButton: React.FC = () => {
|
||||
const activeModel = useAtomValue(currentProductAtom);
|
||||
@ -32,8 +33,13 @@ const NewChatButton: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SecondaryButton title={"New Chat"} onClick={onClick} className="my-5" />
|
||||
<SecondaryButton
|
||||
title={"New Chat"}
|
||||
onClick={onClick}
|
||||
className="my-5 mx-3"
|
||||
icon={<PlusIcon width={16} height={16} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewChatButton;
|
||||
export default React.memo(NewChatButton);
|
||||
|
||||
@ -172,15 +172,30 @@ export const Preferences = () => {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className={classNames(
|
||||
"rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
|
||||
fileName ? "bg-indigo-600 hover:bg-indigo-500" : "bg-gray-500"
|
||||
)}
|
||||
>
|
||||
Install Plugin
|
||||
</button>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<button
|
||||
type="submit"
|
||||
className={classNames(
|
||||
"rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
|
||||
fileName
|
||||
? "bg-blue-500 hover:bg-blue-300"
|
||||
: "bg-gray-500"
|
||||
)}
|
||||
>
|
||||
Install Plugin
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={classNames(
|
||||
"bg-blue-500 hover:bg-blue-300 rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
)}
|
||||
onClick={() => {
|
||||
window.electronAPI.reloadPlugins();
|
||||
}}
|
||||
>
|
||||
Reload Plugins
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@ -4,17 +4,19 @@ type Props = {
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
fullWidth?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const PrimaryButton: React.FC<Props> = ({
|
||||
title,
|
||||
onClick,
|
||||
fullWidth = false,
|
||||
className,
|
||||
}) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
className={`rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-50 ${
|
||||
className={`rounded-md bg-blue-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-50 line-clamp-1 flex-shrink-0 ${className} ${
|
||||
fullWidth ? "flex-1 " : ""
|
||||
}}`}
|
||||
>
|
||||
|
||||
@ -5,24 +5,22 @@ type Props = {
|
||||
used: number;
|
||||
};
|
||||
|
||||
const ProgressBar: React.FC<Props> = ({ used, total }) => {
|
||||
return (
|
||||
<div className="flex gap-[10px] items-center p-[10px]">
|
||||
<div className="text-xs leading-[18px] gap-0.5 flex items-center">
|
||||
<Image src={"icons/app_icon.svg"} width={18} height={18} alt="" />
|
||||
Updating
|
||||
</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>
|
||||
const ProgressBar: React.FC<Props> = ({ used, total }) => (
|
||||
<div className="flex gap-2.5 items-center p-[10px]">
|
||||
<div className="text-xs leading-[18px] gap-0.5 flex items-center">
|
||||
<Image src={"icons/app_icon.svg"} width={18} height={18} alt="" />
|
||||
Updating
|
||||
</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;
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import ChatContainer from "../ChatContainer";
|
||||
import MainChat from "../MainChat";
|
||||
import MainView from "../MainView";
|
||||
import MonitorBar from "../MonitorBar";
|
||||
|
||||
const RightContainer = () => (
|
||||
<div className="flex flex-col flex-1 h-screen">
|
||||
<ChatContainer>
|
||||
<MainChat />
|
||||
</ChatContainer>
|
||||
<MainView />
|
||||
<MonitorBar />
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,14 +1,25 @@
|
||||
import { searchAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { modelSearchAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
export enum SearchType {
|
||||
Model = "model",
|
||||
}
|
||||
|
||||
type Props = {
|
||||
type?: SearchType;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
const SearchBar: React.FC<Props> = ({ placeholder }) => {
|
||||
const setText = useSetAtom(searchAtom);
|
||||
const SearchBar: React.FC<Props> = ({ type, placeholder }) => {
|
||||
const setModelSearch = useSetAtom(modelSearchAtom);
|
||||
let placeholderText = placeholder ? placeholder : "Search (⌘K)";
|
||||
|
||||
const debounced = useDebouncedCallback((value) => {
|
||||
setModelSearch(value);
|
||||
}, 300);
|
||||
|
||||
return (
|
||||
<div className="relative mt-3 flex items-center">
|
||||
<div className="absolute top-0 left-2 h-full flex items-center">
|
||||
@ -24,7 +35,7 @@ const SearchBar: React.FC<Props> = ({ placeholder }) => {
|
||||
name="search"
|
||||
id="search"
|
||||
placeholder={placeholderText}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onChange={(e) => debounced(e.target.value)}
|
||||
className="block w-full rounded-md border-0 py-1.5 pl-8 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
const SecondaryButton: React.FC<Props> = ({
|
||||
@ -10,15 +13,17 @@ const SecondaryButton: React.FC<Props> = ({
|
||||
onClick,
|
||||
disabled,
|
||||
className,
|
||||
icon,
|
||||
}) => (
|
||||
<button
|
||||
disabled={disabled}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${className}`}
|
||||
className={`flex items-center justify-center gap-1 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${className} flex-shrink-0 line-clamp-1`}
|
||||
>
|
||||
{icon}
|
||||
{title}
|
||||
</button>
|
||||
);
|
||||
|
||||
export default SecondaryButton;
|
||||
export default React.memo(SecondaryButton);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||
import useSendChatMessage from "@/_hooks/useSendChatMessage";
|
||||
import { ArrowRightIcon } from "@heroicons/react/24/outline";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import Image from "next/image";
|
||||
|
||||
const SendButton: React.FC = () => {
|
||||
const [currentPrompt] = useAtom(currentPromptAtom);
|
||||
@ -25,9 +25,9 @@ const SendButton: React.FC = () => {
|
||||
onClick={sendChatMessage}
|
||||
style={disabled ? disabledStyle : enabledStyle}
|
||||
type="submit"
|
||||
className="p-2 gap-[10px] inline-flex items-center rounded-[12px] text-sm font-semibold shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
className="p-2 gap-2.5 inline-flex items-center rounded-xl text-sm font-semibold shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
<Image src={"icons/ic_arrowright.svg"} width={24} height={24} alt="" />
|
||||
<ArrowRightIcon width={16} height={16} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,7 +2,6 @@ import Image from "next/image";
|
||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||
import PrimaryButton from "../PrimaryButton";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
MainViewState,
|
||||
@ -11,6 +10,7 @@ import {
|
||||
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||
import useInitModel from "@/_hooks/useInitModel";
|
||||
import { Product } from "@/_models/Product";
|
||||
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
||||
|
||||
enum ActionButton {
|
||||
DownloadModel = "Download a Model",
|
||||
|
||||
@ -2,7 +2,7 @@ import React from "react";
|
||||
import SecondaryButton from "../SecondaryButton";
|
||||
|
||||
const SidebarFooter: React.FC = () => (
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex justify-between items-center gap-2 mx-3">
|
||||
<SecondaryButton
|
||||
title={"Discord"}
|
||||
onClick={() =>
|
||||
@ -11,7 +11,7 @@ const SidebarFooter: React.FC = () => (
|
||||
className="flex-1"
|
||||
/>
|
||||
<SecondaryButton
|
||||
title={"Discord"}
|
||||
title={"Twitter"}
|
||||
onClick={() =>
|
||||
window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai")
|
||||
}
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
const SidebarHeader: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<Image src={"icons/Jan_AppIcon.svg"} width={68} height={28} alt="" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const SidebarHeader: React.FC = () => (
|
||||
<div className="flex flex-col gap-2.5 px-3">
|
||||
<Image src={"icons/Jan_AppIcon.svg"} width={68} height={28} alt="" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SidebarHeader;
|
||||
export default React.memo(SidebarHeader);
|
||||
|
||||
@ -21,21 +21,16 @@ const menu = [
|
||||
];
|
||||
|
||||
const SidebarMenu: React.FC = () => (
|
||||
<div className="flex flex-col">
|
||||
<div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3">
|
||||
Your Configurations
|
||||
</div>
|
||||
<ul role="list" className="-mx-2 mt-2 space-y-1 mb-2">
|
||||
{menu.map((item) => (
|
||||
<SidebarMenuItem
|
||||
title={item.name}
|
||||
viewState={item.state}
|
||||
iconName={item.icon}
|
||||
key={item.name}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<ul role="list" className="mx-1 mt-2 space-y-1 mb-2">
|
||||
{menu.map((item) => (
|
||||
<SidebarMenuItem
|
||||
title={item.name}
|
||||
viewState={item.state}
|
||||
iconName={item.icon}
|
||||
key={item.name}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
export default React.memo(SidebarMenu);
|
||||
|
||||
@ -30,15 +30,15 @@ const SimpleControlNetMessage: React.FC<Props> = ({
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1 justify-start items-baseline">
|
||||
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px]">
|
||||
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px]">
|
||||
{senderName}
|
||||
</div>
|
||||
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400 ml-2">
|
||||
<div className="text-xs leading-[13.2px] font-medium text-gray-400 ml-2">
|
||||
{displayDate(createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 flex-col">
|
||||
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
|
||||
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
|
||||
{text}
|
||||
</p>
|
||||
<JanImage
|
||||
@ -49,7 +49,7 @@ const SimpleControlNetMessage: React.FC<Props> = ({
|
||||
<Link
|
||||
href={imageUrls[0] || "#"}
|
||||
target="_blank_"
|
||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
|
||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
|
||||
>
|
||||
<Image src="icons/download.svg" width={16} height={16} alt="" />
|
||||
<span className="leading-[20px] text-[14px] text-[#111928]">
|
||||
|
||||
@ -30,10 +30,10 @@ const SimpleImageMessage: React.FC<Props> = ({
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1 justify-start items-baseline">
|
||||
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px]">
|
||||
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px]">
|
||||
{senderName}
|
||||
</div>
|
||||
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400 ml-2">
|
||||
<div className="text-xs leading-[13.2px] font-medium text-gray-400 ml-2">
|
||||
{displayDate(createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
@ -46,19 +46,19 @@ const SimpleImageMessage: React.FC<Props> = ({
|
||||
<Link
|
||||
href={imageUrls[0] || "#"}
|
||||
target="_blank_"
|
||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
|
||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
|
||||
>
|
||||
<Image src="icons/download.svg" width={16} height={16} alt="" />
|
||||
<span className="leading-[20px] text-[14px] text-[#111928]">
|
||||
<span className="leading-[20px] text-sm text-[#111928]">
|
||||
Download
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
|
||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
|
||||
// onClick={() => sendChatMessage()}
|
||||
>
|
||||
<Image src="icons/refresh.svg" width={16} height={16} alt="" />
|
||||
<span className="leading-[20px] text-[14px] text-[#111928]">
|
||||
<span className="leading-[20px] text-sm text-[#111928]">
|
||||
Re-generate
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@ -69,7 +69,7 @@ const SimpleTag: React.FC<Props> = ({
|
||||
if (!clickable) {
|
||||
return (
|
||||
<div
|
||||
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
|
||||
className={`px-2.5 py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
@ -79,7 +79,7 @@ const SimpleTag: React.FC<Props> = ({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
|
||||
className={`px-2.5 py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
|
||||
>
|
||||
{title} x
|
||||
</button>
|
||||
|
||||
@ -3,54 +3,61 @@ import { displayDate } from "@/_utils/datetime";
|
||||
import { TextCode } from "../TextCode";
|
||||
import { getMessageCode } from "@/_utils/message";
|
||||
import Image from "next/image";
|
||||
import { MessageSenderType } from "@/_models/ChatMessage";
|
||||
|
||||
type Props = {
|
||||
avatarUrl: string;
|
||||
senderName: string;
|
||||
createdAt: number;
|
||||
senderType: MessageSenderType;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
const SimpleTextMessage: React.FC<Props> = ({
|
||||
senderName,
|
||||
createdAt,
|
||||
senderType,
|
||||
avatarUrl = "",
|
||||
text = "",
|
||||
}) => (
|
||||
<div className="flex items-start gap-2 ml-3">
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src={avatarUrl}
|
||||
width={32}
|
||||
height={32}
|
||||
alt=""
|
||||
/>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="flex gap-1 justify-start items-baseline">
|
||||
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
|
||||
{senderName}
|
||||
</div>
|
||||
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400">
|
||||
{displayDate(createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
{text.includes("```") ? (
|
||||
getMessageCode(text).map((item, i) => (
|
||||
<div className="flex gap-1 flex-col" key={i}>
|
||||
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
|
||||
{item.text}
|
||||
</p>
|
||||
{item.code.trim().length > 0 && <TextCode text={item.code} />}
|
||||
}) => {
|
||||
const backgroundColor =
|
||||
senderType === MessageSenderType.User ? "" : "bg-gray-100";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-start gap-2 px-[148px] ${backgroundColor} py-5`}
|
||||
>
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src={avatarUrl}
|
||||
width={32}
|
||||
height={32}
|
||||
alt=""
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1 justify-start items-baseline">
|
||||
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
|
||||
{senderName}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p
|
||||
className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]"
|
||||
dangerouslySetInnerHTML={{ __html: text }}
|
||||
/>
|
||||
)}
|
||||
<div className="text-xs leading-[13.2px] font-medium text-gray-400">
|
||||
{displayDate(createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
{text.includes("```") ? (
|
||||
getMessageCode(text).map((item, i) => (
|
||||
<div className="flex gap-1 flex-col" key={i}>
|
||||
<p className="leading-[20px] whitespace-break-spaces text-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>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SimpleTextMessage);
|
||||
|
||||
@ -34,17 +34,17 @@ const StreamTextMessage: React.FC<Props> = ({
|
||||
/>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="flex gap-1 justify-start items-baseline">
|
||||
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
|
||||
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
|
||||
{senderName}
|
||||
</div>
|
||||
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400">
|
||||
<div className="text-xs leading-[13.2px] font-medium text-gray-400">
|
||||
{displayDate(createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
{message.text.includes("```") ? (
|
||||
getMessageCode(message.text).map((item, i) => (
|
||||
<div className="flex gap-1 flex-col" key={i}>
|
||||
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
|
||||
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
|
||||
{item.text}
|
||||
</p>
|
||||
{item.code.trim().length > 0 && <TextCode text={item.code} />}
|
||||
@ -52,7 +52,7 @@ const StreamTextMessage: React.FC<Props> = ({
|
||||
))
|
||||
) : (
|
||||
<p
|
||||
className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]"
|
||||
className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]"
|
||||
dangerouslySetInnerHTML={{ __html: message.text }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -3,15 +3,13 @@ type Props = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
const SystemItem: React.FC<Props> = ({ name, value }) => {
|
||||
return (
|
||||
<div className="flex gap-2 pl-4 my-1">
|
||||
<div className="flex gap-[10px] w-max font-bold text-gray-900 text-sm">
|
||||
{name}
|
||||
</div>
|
||||
<span className="text-gray-900 text-sm">{value}</span>
|
||||
const SystemItem: React.FC<Props> = ({ name, value }) => (
|
||||
<div className="flex gap-2 pl-4 my-1">
|
||||
<div className="flex gap-2.5 w-max font-bold text-gray-900 text-sm">
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<span className="text-gray-900 text-sm">{value}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SystemItem;
|
||||
|
||||
@ -17,7 +17,7 @@ export const TabModelDetail: React.FC<Props> = ({ onTabClick, tab }) => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex gap-[2px] rounded p-1 w-full bg-gray-200">
|
||||
<div className="flex gap-0.5 rounded p-1 w-full bg-gray-200">
|
||||
{btns.map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
|
||||
@ -19,7 +19,7 @@ const UserToolbar: React.FC = () => {
|
||||
width={36}
|
||||
height={36}
|
||||
/>
|
||||
<span className="flex gap-[2px] leading-6 text-base font-semibold">
|
||||
<span className="flex gap-0.5 leading-6 text-base font-semibold">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user