chore: resolve conflict

This commit is contained in:
0xSage 2023-10-09 13:41:16 +08:00
commit 1551f05769
180 changed files with 37460 additions and 2902 deletions

View File

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

29
.github/workflows/jan-docs-test.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Jan Docs Test Build
on:
pull_request:
branches:
- main
paths:
- 'docs/**'
- '.github/workflows/deploy-jan-docs.yml'
- '.github/workflows/jan-docs-test.yml'
jobs:
deploy:
name: Test Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
cache-dependency-path: './docs/yarn.lock'
- name: Install dependencies
run: yarn install
working-directory: docs
- name: Test Build Command
run: yarn build
working-directory: docs

View File

@ -3,10 +3,24 @@ on:
push: push:
branches: branches:
- main - main
- fix/eslint-ignore-patterns paths:
- 'electron/**'
- .github/workflows/linter-and-test.yml
- 'web/**'
- 'package.json'
- 'node_modules/**'
- 'yarn.lock'
pull_request: pull_request:
branches: branches:
- main - main
paths:
- 'electron/**'
- .github/workflows/linter-and-test.yml
- 'web/**'
- 'package.json'
- 'node_modules/**'
- 'yarn.lock'
jobs: jobs:
test-on-macos: test-on-macos:
@ -64,11 +78,12 @@ jobs:
- name: Linter and test - name: Linter and test
run: | run: |
w_output=$(w)
export DISPLAY=$(echo "$w_output" | awk '$3 ~ /:[0-9]+/ {print $3}')
echo -e "Display ID: $DISPLAY"
yarn config set network-timeout 300000 yarn config set network-timeout 300000
yarn install yarn install
yarn lint yarn lint
yarn build:plugins yarn build:plugins
yarn build:linux yarn build:linux
yarn test yarn test
env:
DISPLAY: ":0"

1
.gitignore vendored
View File

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

View File

@ -20,63 +20,62 @@
> ⚠️ **Jan is currently in Development**: Expect breaking changes and bugs! > ⚠️ **Jan is currently in Development**: Expect breaking changes and bugs!
Jan lets you run AI on your own hardware, with helpful tools to manage models and monitor your hardware performance. **Use offline LLMs with your own data.** Run open source models like Llama2 or Falcon on your internal computers/servers.
In the background, Jan runs [Nitro](https://nitro.jan.ai), a C++ inference engine. It runs various model formats (GGUF/TensorRT) on various hardware (Mac M1/M2/Intel, Windows, Linux, and datacenter-grade Nvidia GPUs) with optional GPU acceleration. **Jan runs on any hardware.** From PCs to multi-GPU clusters, Jan supports universal architectures:
> See the Nitro codebase at https://nitro.jan.ai. - [x] Nvidia GPUs (fast)
- [x] Apple M-series (fast)
- [x] Apple Intel
- [x] Linux Debian
- [x] Windows x64
<!-- TODO: uncomment this later when we have this feature --> > Download Jan at https://jan.ai/
<!-- Jan can be run as a server or cloud-native application for enterprise. We offer enterprise plugins for LDAP integration and Audit Logs. Contact us at [hello@jan.ai](mailto:hello@jan.ai) for more details. -->
## Demo ## Demo
<p align="center"> <p align="center">
<img style='border:1px solid #000000' src="https://github.com/janhq/jan/assets/69952136/1f9bb48c-2e70-4633-9f68-7881cd925972" alt="Jan Web GIF"> <img style='border:1px solid #000000' src="https://github.com/janhq/jan/assets/69952136/1db9c3d3-79b1-4988-afb5-afd4f4afd0d9" alt="Jan Web GIF">
</p> </p>
_Screenshot: Jan v0.1.3 on Mac M1 Pro, 16GB Sonoma_
## Quicklinks ## Quicklinks
- Developer documentation: https://jan.ai/docs (Work in Progress) - [Developer docs](https://jan.ai/docs) (WIP)
- Desktop app: Download at https://jan.ai/ - Mobile App shell: [App Store](https://apps.apple.com/us/app/jan-on-device-ai-cloud-ais/id6449664703) | [Android](https://play.google.com/store/apps/details?id=com.jan.ai)
- Mobile app shell: Download via [App Store](https://apps.apple.com/us/app/jan-on-device-ai-cloud-ais/id6449664703) | [Android](https://play.google.com/store/apps/details?id=com.jan.ai) - [Nitro Github](https://nitro.jan.ai): Jan's AI engine
- Nitro (C++ AI Engine): https://nitro.jan.ai
## Plugins ## Plugins
Jan supports core & 3rd party extensions: Jan supports core & 3rd party extensions:
- [x] **LLM chat**: Self-hosted Llama2 and LLMs - [x] **LLM chat**: Self-hosted Llama2 and LLMs
- [x] **Model Manager**: 1-click to install, swap, and delete models - [x] **Model Manager**: 1-click to install, swap, and delete models with HuggingFace integration
- [x] **Storage**: Optionally store your conversation history and other data in SQLite/your storage of choice - [x] **Storage**: Optionally save conversation history and other data in SQLite
- [ ] **3rd-party AIs**: Connect to ChatGPT, Claude via API Key (in progress) - [ ] **3rd-party AIs**: Connect to ChatGPT, Claude via API Key (in progress)
- [ ] **Cross device support**: Mobile & Web support for custom shared servers (in progress) - [ ] **Cross device support**: Mobile & Web support for custom shared servers (in progress)
- [ ] **File retrieval**: User can upload private and run a vectorDB (planned) - [ ] **File retrieval**: User can chat with docs
- [ ] **Multi-user support**: Share a single server across a team/friends (planned) - [ ] **Multi-user support**: Share a single server across a team/friends (planned)
- [ ] **Compliance**: Auditing and flagging features (planned) - [ ] **Compliance**: Auditing and flagging features (planned)
## Hardware Support ## Nitro (Jan's AI engine)
Nitro provides both CPU and GPU support, via [llama.cpp](https://github.com/ggerganov/llama.cpp) and [TensorRT](https://github.com/NVIDIA/TensorRT), respectively. In the background, Jan runs [Nitro](https://nitro.jan.ai), an open source, C++ inference engine. It runs various model formats (GGUF/TensorRT) on various hardware (Mac M1/M2/Intel, Windows, Linux, and datacenter-grade Nvidia GPUs) with optional GPU acceleration.
- [x] Nvidia GPUs (accelerated) > See the open source Nitro codebase at https://nitro.jan.ai.
- [x] Apple M-series (accelerated)
- [x] Linux DEB
- [x] Windows x64
Not supported yet: Apple Intel, Linux RPM, Windows x86|ARM64, AMD ROCm
> See [developer docs](https://docs.jan.ai/docs/) for detailed installation instructions.
## Contributing ## Contributing
Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file
### Pre-requisites ### Pre-requisites
- node >= 20.0.0 - node >= 20.0.0
- yarn >= 1.22.0 - yarn >= 1.22.0
### Use as complete suite (in progress) ### Use as complete suite (in progress)
### For interactive development ### For interactive development
Note: This instruction is tested on MacOS only. Note: This instruction is tested on MacOS only.
@ -98,28 +97,29 @@ Note: This instruction is tested on MacOS only.
yarn build:plugins yarn build:plugins
``` ```
4. **Run development and Using Jan Desktop** 3. **Run development and Using Jan Desktop**
``` ```
yarn dev yarn dev
``` ```
This will start the development server and open the desktop app. This will start the development server and open the desktop app.
In this step, there are a few notification about installing base plugin, just click `OK` and `Next` to continue. In this step, there are a few notification about installing base plugin, just click `OK` and `Next` to continue.
### For production build ### For production build
```bash ```bash
# Do step 1 and 2 in previous section # Do step 1 and 2 in previous section
git clone https://github.com/janhq/jan git clone https://github.com/janhq/jan
cd jan cd jan
yarn install yarn install
yarn build:plugins yarn build:plugins
# Build the app # Build the app
yarn build yarn build
``` ```
This will build the app MacOS m1/m2 for production (with code signing already done) and put the result in `dist` folder. This will build the app MacOS m1/m2 for production (with code signing already done) and put the result in `dist` folder.
## License ## License

View File

@ -1,27 +1,27 @@
import React from "react"; import React, { useState, useEffect } from "react";
import { Fragment } from "react"; import { Fragment } from "react";
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
import { ChevronDownIcon } from "@heroicons/react/20/solid"; import { ChevronDownIcon } from "@heroicons/react/20/solid";
const items = [ const systems = [
{ {
name: "Download for Mac (M1/M2)", name: "Download for Mac (M1/M2)",
href: "https://github.com/janhq/jan/releases/download/v0.1.2/Jan-0.1.2-arm64.dmg", href: "https://github.com/janhq/jan/releases/download/v0.1.3/jan-electron-mac-arm64-0.1.3.dmg",
logo: require("@site/static/img/apple-logo-white.png").default, logo: require("@site/static/img/apple-logo-white.png").default,
}, },
{ {
name: "Download for Mac (Intel)", name: "Download for Mac (Intel)",
href: "https://github.com/janhq/jan/releases/download/v0.1.2/Jan-0.1.2-arm64.dmg", href: "https://github.com/janhq/jan/releases/download/v0.1.3/jan-electron-mac-x64-0.1.3.dmg",
logo: require("@site/static/img/apple-logo-white.png").default, logo: require("@site/static/img/apple-logo-white.png").default,
}, },
{ {
name: "Download for Windows", name: "Download for Windows",
href: "https://static.vecteezy.com/system/resources/previews/004/243/615/non_2x/creative-coming-soon-teaser-background-free-vector.jpg", href: "https://github.com/janhq/jan/releases/download/v0.1.3/jan-electron-win-x64-0.1.3.exe",
logo: require("@site/static/img/windows-logo-white.png").default, logo: require("@site/static/img/windows-logo-white.png").default,
}, },
{ {
name: "Download for Linux", name: "Download for Linux",
href: "https://static.vecteezy.com/system/resources/previews/004/243/615/non_2x/creative-coming-soon-teaser-background-free-vector.jpg", href: "https://github.com/janhq/jan/releases/download/v0.1.3/jan-electron-linux-amd64-0.1.3.deb",
logo: require("@site/static/img/linux-logo-white.png").default, logo: require("@site/static/img/linux-logo-white.png").default,
}, },
]; ];
@ -31,19 +31,35 @@ function classNames(...classes) {
} }
export default function Dropdown() { export default function Dropdown() {
const [defaultSystem, setDefaultSystem] = useState(systems[0]);
useEffect(() => {
const uAgent = window.navigator.userAgent;
if (uAgent.indexOf("Win") !== -1) {
setDefaultSystem(systems[2]);
} else if (uAgent.indexOf("Mac") !== -1) {
// Note: There's no way to detect ARM architecture from browser. Hardcoding to M1/M2 for now.
setDefaultSystem(systems[0]);
} else if (uAgent.indexOf("Linux") !== -1) {
setDefaultSystem(systems[3]);
} else {
setDefaultSystem(systems[0]);
}
}, []);
return ( return (
<div className="inline-flex align-items-stretch"> <div className="inline-flex align-items-stretch">
{/* TODO dynamically detect users OS through browser */}
<a <a
className="cursor-pointer relative inline-flex items-center rounded-l-md border-0 px-3.5 py-2.5 text-base font-semibold text-white bg-blue-600 dark:bg-blue-500 hover:bg-blue-500 dark:hover:bg-blue-400 hover:text-white" className="cursor-pointer relative inline-flex items-center rounded-l-md border-0 px-3.5 py-2.5 text-base font-semibold text-white bg-blue-600 dark:bg-blue-500 hover:bg-blue-500 dark:hover:bg-blue-400 hover:text-white"
href={items[0].href} href={defaultSystem.href}
> >
<img <img
src={require("@site/static/img/apple-logo-white.png").default} src={require("@site/static/img/apple-logo-white.png").default}
alt="Logo" alt="Logo"
className="h-5 mr-3 -mt-1" className="h-5 mr-3 -mt-1"
/> />
Download for Mac (Silicon) {defaultSystem.name}
</a> </a>
<Menu as="div" className="relative -ml-px block"> <Menu as="div" className="relative -ml-px block">
<Menu.Button className="cursor-pointer relative inline-flex items-center rounded-r-md border-0 border-l border-gray-300 active:border-l active:border-white h-full text-white bg-blue-600 dark:bg-blue-500 hover:bg-blue-500 dark:hover:bg-blue-400"> <Menu.Button className="cursor-pointer relative inline-flex items-center rounded-r-md border-0 border-l border-gray-300 active:border-l active:border-white h-full text-white bg-blue-600 dark:bg-blue-500 hover:bg-blue-500 dark:hover:bg-blue-400">
@ -61,11 +77,11 @@ export default function Dropdown() {
> >
<Menu.Items className="absolute right-0 z-10 mt-2 w-72 text-left origin-top-right rounded-md bg-blue-600 dark:bg-blue-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> <Menu.Items className="absolute right-0 z-10 mt-2 w-72 text-left origin-top-right rounded-md bg-blue-600 dark:bg-blue-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1"> <div className="py-1">
{items.map((item) => ( {systems.map((system) => (
<Menu.Item key={item.name}> <Menu.Item key={system.name}>
{({ active }) => ( {({ active }) => (
<a <a
href={item.href} href={system.href}
className={classNames( className={classNames(
active active
? "bg-blue-500 dark:hover:bg-blue-400 hover:text-white" ? "bg-blue-500 dark:hover:bg-blue-400 hover:text-white"
@ -74,11 +90,11 @@ export default function Dropdown() {
)} )}
> >
<img <img
src={item.logo} src={system.logo}
alt="Logo" alt="Logo"
className="w-3 mr-3 -mt-1" className="w-3 mr-3 -mt-1"
/> />
{item.name} {system.name}
</a> </a>
)} )}
</Menu.Item> </Menu.Item>

View File

@ -5,7 +5,7 @@ import {
LockClosedIcon, LockClosedIcon,
} from "@heroicons/react/20/solid"; } from "@heroicons/react/20/solid";
const features = [ const systems = [
{ {
name: "Mac", name: "Mac",
description: description:
@ -47,20 +47,20 @@ export default function HomepageDownloads() {
</div> </div>
<div className="mx-auto mt-16 max-w-2xl sm:mt-20 lg:mt-24 lg:max-w-none"> <div className="mx-auto mt-16 max-w-2xl sm:mt-20 lg:mt-24 lg:max-w-none">
<dl className="grid max-w-xl grid-cols-1 gap-x-8 gap-y-16 lg:max-w-none lg:grid-cols-3"> <dl className="grid max-w-xl grid-cols-1 gap-x-8 gap-y-16 lg:max-w-none lg:grid-cols-3">
{features.map((feature) => ( {systems.map((system) => (
<div key={feature.name} className="flex flex-col"> <div key={system.name} className="flex flex-col">
<dt className="flex items-center gap-x-3 text-base font-semibold leading-7 text-gray-900 dark: text-white"> <dt className="flex items-center gap-x-3 text-base font-semibold leading-7 text-gray-900 dark: text-white">
<feature.icon <system.icon
className="h-5 w-5 flex-none text-indigo-600 dark:text-indigo-400" className="h-5 w-5 flex-none text-indigo-600 dark:text-indigo-400"
aria-hidden="true" aria-hidden="true"
/> />
{feature.name} {system.name}
</dt> </dt>
<dd className="mt-4 flex flex-auto flex-col text-base leading-7 text-gray-600 dark:text-gray-300"> <dd className="mt-4 flex flex-auto flex-col text-base leading-7 text-gray-600 dark:text-gray-300">
<p className="flex-auto">{feature.description}</p> <p className="flex-auto">{system.description}</p>
<p className="mt-6"> <p className="mt-6">
<a <a
href={feature.href} href={system.href}
className="text-sm font-semibold leading-6 text-indigo-600 dark:text-indigo-400" className="text-sm font-semibold leading-6 text-indigo-600 dark:text-indigo-400"
> >
Learn more <span aria-hidden="true"></span> Learn more <span aria-hidden="true"></span>

View File

@ -8,7 +8,7 @@ export default function HomepageHero() {
return ( return (
<div className="bg-white dark:bg-gray-900"> <div className="bg-white dark:bg-gray-900">
<div className="relative isolate pt-14"> <div className="relative isolate md:pt-14 pt-0">
{/* Background top gradient styling */} {/* Background top gradient styling */}
{colorMode === "dark" ? ( {colorMode === "dark" ? (
<div <div
@ -39,7 +39,7 @@ export default function HomepageHero() {
)} )}
{/* Main hero block */} {/* Main hero block */}
<div className="py-24 sm:py-32 lg:pb-40 animate-in fade-in zoom-in-50 duration-1000 "> <div className="py-24 lg:pb-40 animate-in fade-in zoom-in-50 duration-1000 ">
<div className="mx-auto max-w-7xl px-6 lg:px-8"> <div className="mx-auto max-w-7xl px-6 lg:px-8">
{/* Hero text and buttons */} {/* Hero text and buttons */}
<div className="mx-auto max-w-2xl text-center"> <div className="mx-auto max-w-2xl text-center">
@ -47,7 +47,7 @@ export default function HomepageHero() {
Run your own AI Run your own AI
</h1> </h1>
<p className="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300"> <p className="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300">
Jan lets you run AI on your own hardware. 1-click to install the Jan lets you run AI on your own hardware. 1-click to install the
latest open-source models. Monitor and manage software-hardware latest open-source models. Monitor and manage software-hardware
performance. performance.
<br></br> <br></br>
@ -79,14 +79,15 @@ export default function HomepageHero() {
src={ src={
colorMode === "dark" colorMode === "dark"
? // TODO replace with darkmode image ? // TODO replace with darkmode image
require("@site/static/img/desktop-llm-chat-dark.png").default require("@site/static/img/desktop-llm-chat-dark.png")
: require("@site/static/img/desktop-llm-chat-light.png").default .default
: require("@site/static/img/desktop-llm-chat-light.png")
.default
} }
alt="App screenshot" alt="App screenshot"
width={2432} width={2432}
className="mt-16 rounded-lg md:rounded-2xl lg:rounded-3xl bg-white/5 shadow-2xl ring-1 ring-white/10 sm:mt-24" className="mt-16 rounded-lg md:rounded-2xl lg:rounded-3xl bg-white/5 shadow-2xl ring-1 ring-white/10 sm:mt-24"
/> />
</div> </div>
</div> </div>
{/* Background top gradient styling */} {/* Background top gradient styling */}

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

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

View File

@ -168,7 +168,7 @@ function getFinishedDownloadModels() {
const query = `SELECT * FROM models WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`; const query = `SELECT * FROM models WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`;
db.all(query, (err: Error, row: any) => { db.all(query, (err: Error, row: any) => {
res(row.map((item: any) => parseToProduct(item))); res(row?.map((item: any) => parseToProduct(item)) ?? []);
}); });
db.close(); db.close();
}); });

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

View File

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

View File

@ -27,8 +27,14 @@
], ],
"extends": null, "extends": null,
"mac": { "mac": {
"type": "distribution" "type": "distribution",
} "entitlements": "./entitlements.mac.plist",
"entitlementsInherit": "./entitlements.mac.plist",
"notarize": {
"teamId": "YT49P7GXG4"
}
},
"artifactName": "jan-${os}-${arch}-${version}.${ext}"
}, },
"scripts": { "scripts": {
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"", "lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
@ -45,13 +51,17 @@
}, },
"dependencies": { "dependencies": {
"@npmcli/arborist": "^7.1.0", "@npmcli/arborist": "^7.1.0",
"@uiball/loaders": "^1.3.0",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"electron-updater": "^6.1.4", "electron-updater": "^6.1.4",
"pacote": "^17.0.4", "pacote": "^17.0.4",
"react-intersection-observer": "^9.5.2",
"request": "^2.88.2", "request": "^2.88.2",
"request-progress": "^3.0.0" "request-progress": "^3.0.0",
"use-debounce": "^9.0.4"
}, },
"devDependencies": { "devDependencies": {
"@electron/notarize": "^2.1.0",
"@playwright/test": "^1.38.1", "@playwright/test": "^1.38.1",
"@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3", "@typescript-eslint/parser": "^6.7.3",

View File

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

1614
node_modules/.yarn-integrity generated vendored

File diff suppressed because it is too large Load Diff

14224
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ const ViewModelDetailButton: React.FC<Props> = ({ callback }) => {
<div className="px-4 pb-4"> <div className="px-4 pb-4">
<button <button
onClick={callback} onClick={callback}
className="bg-gray-100 py-1 px-[10px] w-full flex items-center justify-center gap-1 rounded-lg" className="bg-gray-100 py-1 px-2.5 w-full flex items-center justify-center gap-1 rounded-lg"
> >
<span className="text-xs leading-[18px]">View Details</span> <span className="text-xs leading-[18px]">View Details</span>
<ChevronDownIcon width={18} height={18} /> <ChevronDownIcon width={18} height={18} />

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