chore: resolve conflict

This commit is contained in:
0xSage 2023-10-10 12:31:05 +08:00
commit 6be342c51c
278 changed files with 49753 additions and 7058 deletions

158
.github/workflows/build-app.yml vendored Normal file
View File

@ -0,0 +1,158 @@
name: Jan Build MacOS App
on:
push:
tags: ['v*.*.*']
jobs:
build-macos:
runs-on: macos-latest
environment: production
permissions:
contents: write
steps:
- name: Getting the repo
uses: actions/checkout@v3
- name: Installing node
uses: actions/setup-node@v1
with:
node-version: 20
- name: Install jq
uses: dcarbone/install-jq-action@v2.0.1
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- name: Update app version base on tag
run: |
if [[ ! "${VERSION_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Tag is not valid!"
exit 1
fi
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
env:
VERSION_TAG: ${{ steps.tag.outputs.tag }}
- name: Get Cer for code signing
run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
shell: bash
env:
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
- uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
p12-password: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
- name: Install yarn dependencies
run: |
yarn install
yarn build:plugins-darwin
- name: Build and publish app
run: |
yarn build:publish-darwin
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: "/tmp/codesign.p12"
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
CSC_IDENTITY_AUTO_DISCOVERY: "true"
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
build-windows-x64:
runs-on: windows-latest
permissions:
contents: write
steps:
- name: Getting the repo
uses: actions/checkout@v3
- name: Installing node
uses: actions/setup-node@v1
with:
node-version: 20
- name: Install jq
uses: dcarbone/install-jq-action@v2.0.1
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- name: Update app version base on tag
shell: bash
run: |
if [[ ! "${VERSION_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Tag is not valid!"
exit 1
fi
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
env:
VERSION_TAG: ${{ steps.tag.outputs.tag }}
- name: Install yarn dependencies
run: |
yarn config set network-timeout 300000
yarn install
yarn build:plugins
- name: Build and publish app
run: |
yarn build:publish-win32
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-linux-x64:
runs-on: ubuntu-latest
environment: production
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
permissions:
contents: write
steps:
- name: Getting the repo
uses: actions/checkout@v3
- name: Installing node
uses: actions/setup-node@v1
with:
node-version: 20
- name: Install jq
uses: dcarbone/install-jq-action@v2.0.1
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v2
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- name: Update app version base on tag
run: |
if [[ ! "${VERSION_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Tag is not valid!"
exit 1
fi
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
env:
VERSION_TAG: ${{ steps.tag.outputs.tag }}
- name: Install yarn dependencies
run: |
yarn config set network-timeout 300000
yarn install
yarn build:plugins
- name: Build and publish app
run: |
yarn build:publish-linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -26,7 +26,7 @@ jobs:
run: yarn install run: yarn install
working-directory: docs working-directory: docs
- name: Build website - name: Build website
run: yarn build run: sed -i '/process.env.DEBUG = namespaces;/c\// process.env.DEBUG = namespaces;' ./node_modules/debug/src/node.js && yarn build
working-directory: docs working-directory: docs
- name: Add Custome Domain file - name: Add Custome Domain file

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: sed -i '/process.env.DEBUG = namespaces;/c\// process.env.DEBUG = namespaces;' ./node_modules/debug/src/node.js && yarn build
working-directory: docs

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

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

View File

@ -1,50 +0,0 @@
name: Jan Build MacOS App
on:
push:
tags: ['v*.*.*']
jobs:
build-macos-app:
runs-on: macos-latest
permissions:
contents: write
steps:
- name: Getting the repo
uses: actions/checkout@v3
- name: Installing node
uses: actions/setup-node@v1
with:
node-version: 20
- name: Install jq
uses: dcarbone/install-jq-action@v2.0.1
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- name: Update app version base on tag
run: |
if [[ ! "${VERSION_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Tag is not valid!"
exit 1
fi
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
env:
VERSION_TAG: ${{ steps.tag.outputs.tag }}
- name: Install yarn dependencies
run: |
yarn install
yarn build:plugins
- name: Build and publish app
run: |
yarn build:publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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,64 +20,75 @@
> ⚠️ **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 ## Troubleshooting
As Jan is development mode, you might get stuck on a broken build.
> See [developer docs](https://docs.jan.ai/docs/) for detailed installation instructions. To reset your installation:
1. Delete Jan Application from /Applications
1. Clear cache:
`rm -rf /Users/$(whoami)/Library/Application\ Support/jan-electron`
OR
`rm -rf /Users/$(whoami)/Library/Application\ Support/jan`
---
## 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) ### Instructions
### For interactive development
Note: This instruction is tested on MacOS only. Note: This instruction is tested on MacOS only.
@ -85,7 +96,7 @@ Note: This instruction is tested on MacOS only.
``` ```
git clone https://github.com/janhq/jan git clone https://github.com/janhq/jan
git checkout feature/hackathon-refactor-jan-into-electron-app git checkout DESIRED_BRANCH
cd jan cd jan
``` ```
@ -98,28 +109,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

@ -0,0 +1,29 @@
# ADR 003: JAN PLUGINS
## Changelog
- Oct 5th 2023: Initial draft
## Status
Accepted
## Context
Modular Architecture w/ Plugins:
- Jan will have an architecture similar to VSCode or k8Lens
- "Desktop Application" whose functionality can be extended thru plugins
- Jan's architecture will need to accomodate plugins for (a) Persistence(b) IAM(c) Teams and RBAC(d) Policy engines(e) "Apps" (i.e. higher-order business logic)(f) Themes (UI)
- Nitro's architecture will need to accomodate plugins for different "model backends"(a) llama.cpp(b) rkwk (and others)(c) 3rd-party AIs
## Decision
![Architecture](./images/adr-003-01.png)
## Consequences
What becomes easier or more difficult to do because of this change?
## Reference
[Plugin APIs](./adr-003-jan-plugins.md)

View File

@ -0,0 +1,37 @@
## JAN service & plugin APIs
Jan frontend components will communicate with plugin functions via Service Interfaces:
All of the available APIs are listed in [CoreService](../../web/shared/coreService.ts)
- Data Service:
- GET_CONVERSATIONS: retrieve all of the conversations
- CREATE_CONVERSATION: start a new conversation
- DELETE_CONVERSATION: delete an existing conversation
- GET_CONVERSATION_MESSAGES: retrieve a certain conversation messages
- CREATE_MESSAGE: store a new message (both sent & received)
- UPDATE_MESSAGE: update an existing message (streaming)
- STORE_MODEL: store new model information (when clicking download)
- UPDATE_FINISHED_DOWNLOAD: mark a model as downloaded
- GET_UNFINISHED_DOWNLOAD_MODELS: retrieve all unfinished downloading model (TBD)
- GET_FINISHED_DOWNLOAD_MODELS: retrieve all finished downloading model (TBD)
- DELETE_DOWNLOAD_MODEL: delete a model (TBD)
- GET_MODEL_BY_ID: retrieve model information by its ID
- Inference Service:
- INFERENCE_URL: retrieve inference endpoint served by plugin
- INIT_MODEL: runs a model
- STOP_MODEL: stop a running model
- Model Management Service: (TBD)
- GET_AVAILABLE_MODELS: retrieve available models (deprecate soon)
- GET_DOWNLOADED_MODELS: (deprecated)
- DELETE_MODEL: (deprecated)
- DOWNLOAD_MODEL: start to download a model
- SEARCH_MODELS: explore models with search query on HuggingFace (TBD)
- Monitoring service:
- GET_RESOURCES_INFORMATION: retrieve total & used memory information
- GET_CURRENT_LOAD_INFORMATION: retrieve CPU load information

BIN
adr/images/adr-003-01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

15166
docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,9 @@
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^1.6.22",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"axios": "^1.5.1",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"js-yaml": "^4.1.0",
"postcss": "^8.4.30", "postcss": "^8.4.30",
"prism-react-renderer": "^1.3.5", "prism-react-renderer": "^1.3.5",
"react": "^17.0.2", "react": "^17.0.2",
@ -30,7 +32,8 @@
"tailwindcss": "^3.3.3" "tailwindcss": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "^2.4.3" "@docusaurus/module-type-aliases": "2.4.1",
"tailwindcss-animate": "^1.0.7"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View File

@ -1,28 +1,29 @@
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";
import axios from "axios";
const items = [ const systemsTemplate = [
{ {
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",
logo: require("@site/static/img/apple-logo-white.png").default, logo: require("@site/static/img/apple-logo-white.png").default,
fileFormat: "{appname}-mac-arm64-{tag}.dmg",
}, },
{ {
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",
logo: require("@site/static/img/apple-logo-white.png").default, logo: require("@site/static/img/apple-logo-white.png").default,
fileFormat: "{appname}-mac-x64-{tag}.dmg",
}, },
{ {
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",
logo: require("@site/static/img/windows-logo-white.png").default, logo: require("@site/static/img/windows-logo-white.png").default,
fileFormat: "{appname}-win-x64-{tag}.exe",
}, },
{ {
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",
logo: require("@site/static/img/linux-logo-white.png").default, logo: require("@site/static/img/linux-logo-white.png").default,
fileFormat: "{appname}-linux-amd64-{tag}.deb",
}, },
]; ];
@ -31,22 +32,81 @@ function classNames(...classes) {
} }
export default function Dropdown() { export default function Dropdown() {
const [systems, setSystems] = useState(systemsTemplate);
const [defaultSystem, setDefaultSystem] = useState(systems[0]);
const getLatestReleaseInfo = async (repoOwner, repoName) => {
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`;
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
console.error(error);
return null;
}
};
const extractAppName = (fileName) => {
// Extract appname using a regex that matches the provided file formats
const regex = /^(.*?)-(?:mac|win|linux)-(?:arm64|x64|amd64)-.*$/;
const match = fileName.match(regex);
return match ? match[1] : null;
};
useEffect(() => {
const updateDownloadLinks = async () => {
try {
const releaseInfo = await getLatestReleaseInfo("janhq", "jan");
// Extract appname from the first asset name
const firstAssetName = releaseInfo.assets[0].name;
const appname = extractAppName(firstAssetName);
if (!appname) {
console.error("Failed to extract appname from file name:", firstAssetName);
return;
}
// Remove 'v' at the start of the tag_name
const tag = releaseInfo.tag_name.startsWith("v")
? releaseInfo.tag_name.substring(1)
: releaseInfo.tag_name;
const updatedSystems = systems.map((system) => {
const downloadUrl = system.fileFormat
.replace("{appname}", appname)
.replace("{tag}", tag);
return {
...system,
href: `https://github.com/janhq/jan/releases/download/${releaseInfo.tag_name}/${downloadUrl}`,
};
});
setSystems(updatedSystems);
setDefaultSystem(updatedSystems[0]);
} catch (error) {
console.error("Failed to update download links:", error);
}
};
updateDownloadLinks();
}, []);
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-indigo-600 dark:bg-indigo-500 hover:bg-indigo-500 dark:hover:bg-indigo-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-indigo-600 dark:bg-indigo-500 hover:bg-indigo-500 dark:hover:bg-indigo-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">
<span className="sr-only">Open OS options</span> <span className="sr-only">Open OS options</span>
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" /> <ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button> </Menu.Button>
@ -59,26 +119,26 @@ export default function Dropdown() {
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-10 mt-2 w-72 text-left origin-top-right rounded-md bg-indigo-600 dark:bg-indigo-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-indigo-500 dark:hover:bg-indigo-400 hover:text-white" ? "bg-blue-500 dark:hover:bg-blue-400 hover:text-white"
: "text-white", : "text-white",
"block px-4 py-2" "block px-4 py-2"
)} )}
> >
<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"> <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">
@ -60,7 +60,7 @@ export default function HomepageHero() {
<Dropdown /> <Dropdown />
<button <button
type="button" type="button"
className="cursor-pointer relative inline-flex items-center rounded px-3.5 py-2 dark:py-2.5 text-base font-semibold text-indigo-600 bg-white border-indigo-600 dark:border-0 hover:bg-indigo-600 dark:hover:bg-indigo-500 hover:text-white" className="cursor-pointer relative inline-flex items-center rounded px-3.5 py-2 dark:py-2.5 text-base font-semibold text-blue-600 bg-white border-blue-600 dark:border-0 hover:bg-blue-600 dark:hover:bg-blue-500 hover:text-white"
onClick={() => onClick={() =>
window.open( window.open(
"https://github.com/janhq/jan", "https://github.com/janhq/jan",
@ -79,8 +79,10 @@ 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.png").default require("@site/static/img/desktop-llm-chat-dark.png")
: require("@site/static/img/desktop-llm-chat.png").default .default
: require("@site/static/img/desktop-llm-chat-light.png")
.default
} }
alt="App screenshot" alt="App screenshot"
width={2432} width={2432}

View File

@ -58,7 +58,7 @@ export default function HomepageSectionOne() {
/> />
{feature.name} {feature.name}
</dt>{" "} </dt>{" "}
<dd className="inline">{feature.description}</dd> <dt>{feature.description}</dt>
</div> </div>
))} ))}
</dl> </dl>
@ -71,7 +71,7 @@ export default function HomepageSectionOne() {
? // TODO replace with darkmode image ? // TODO replace with darkmode image
require("@site/static/img/desktop-explore-models-dark.png") require("@site/static/img/desktop-explore-models-dark.png")
.default .default
: require("@site/static/img/desktop-explore-models.png") : require("@site/static/img/desktop-explore-models-light.png")
.default .default
} }
alt="Product screenshot" alt="Product screenshot"

View File

@ -58,7 +58,7 @@ export default function sectionTwo() {
/> />
{feature.name} {feature.name}
</dt>{" "} </dt>{" "}
<dd className="inline">{feature.description}</dd> <dt>{feature.description}</dt>
</div> </div>
))} ))}
</dl> </dl>
@ -68,8 +68,8 @@ export default function sectionTwo() {
src={ src={
colorMode === "dark" colorMode === "dark"
? // TODO replace with darkmode image ? // TODO replace with darkmode image
require("@site/static/img/desktop-model-settings.png").default require("@site/static/img/desktop-model-settings-dark.png").default
: require("@site/static/img/desktop-model-settings.png").default : require("@site/static/img/desktop-model-settings-light.png").default
} }
alt="Product screenshot" alt="Product screenshot"
className="w-[48rem] max-w-none rounded-xl shadow-xl ring-1 ring-gray-400/10 sm:w-[57rem] md:-ml-4 lg:-ml-0" className="w-[48rem] max-w-none rounded-xl shadow-xl ring-1 ring-gray-400/10 sm:w-[57rem] md:-ml-4 lg:-ml-0"

View File

@ -12,26 +12,35 @@
Full list of Infima variables: https://github.com/facebook/docusaurus/issues/3955#issuecomment-1521944593 Full list of Infima variables: https://github.com/facebook/docusaurus/issues/3955#issuecomment-1521944593
*/ */
:root { :root {
--ifm-color-primary: #2e8555; --ifm-color-primary: #2563EB; /* New Primary Blue */
--ifm-color-primary-dark: #29784c; --ifm-color-primary-dark: #204FCF; /* Darker Blue */
--ifm-color-primary-darker: #277148; --ifm-color-primary-darker: #1B45B7; /* Even Darker Blue */
--ifm-color-primary-darkest: #205d3b; --ifm-color-primary-darkest: #163C9D; /* Darkest Blue */
--ifm-color-primary-light: #33925d; --ifm-color-primary-light: #2974FF; /* Light Blue */
--ifm-color-primary-lighter: #359962; --ifm-color-primary-lighter: #3280FF; /* Lighter Blue */
--ifm-color-primary-lightest: #3cad6e; --ifm-color-primary-lightest: #3A8BFF; /* Lightest Blue */
--ifm-code-font-size: 95%; --ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
} }
/* For readability concerns, you should choose a lighter palette in dark mode. */ /* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme="dark"] { [data-theme="dark"] {
--ifm-color-primary: #25c2a0; --ifm-color-primary: #ffffff; /* New Primary Blue */
--ifm-color-primary-dark: #21af90; --ifm-color-primary-dark: #204FCF; /* Darker Blue */
--ifm-color-primary-darker: #1fa588; --ifm-color-primary-darker: #1B45B7; /* Even Darker Blue */
--ifm-color-primary-darkest: #1a8870; --ifm-color-primary-darkest: #163C9D; /* Darkest Blue */
--ifm-color-primary-light: #29d5b0; --ifm-color-primary-light: #2974FF; /* Light Blue */
--ifm-color-primary-lighter: #32d8b4; --ifm-color-primary-lighter: #3280FF; /* Lighter Blue */
--ifm-color-primary-lightest: #4fddbf; --ifm-color-primary-lightest: #3A8BFF; /* Lightest Blue */
--ifm-navbar-background-color: rgba(15, 23, 42);
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
} }
.footer.footer--dark {
--ifm-footer-background-color: #1a212f;
--ifm-footer-color: var(--ifm-footer-link-color);
--ifm-footer-link-color: var(--ifm-color-secondary);
--ifm-footer-title-color: var(--ifm-color-white);
background-color: var(--ifm-footer-background-color);
color: var(--ifm-footer-color)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -9,5 +9,7 @@ module.exports = {
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [], plugins: [
require("tailwindcss-animate"),
],
}; };

File diff suppressed because it is too large Load Diff

44
electron/.eslintrc.js Normal file
View File

@ -0,0 +1,44 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
env: {
node: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
],
rules: {
"@typescript-eslint/no-non-null-assertion": "off",
"react/prop-types": "off", // In favor of strong typing - no need to dedupe
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
},
settings: {
react: {
createClass: "createReactClass", // Regex for Component Factory to use,
// default to "createReactClass"
pragma: "React", // Pragma to use, default to "React"
version: "detect", // React version. "detect" automatically picks the version you have installed.
// You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
// default to latest and warns if missing
// It will default to "detect" in the future
},
linkComponents: [
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
"Hyperlink",
{ name: "Link", linkAttribute: "to" },
],
},
ignorePatterns: [
"build",
"renderer",
"node_modules",
"core/plugins",
"core/**/*.test.js",
],
};

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

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

View File

@ -16,11 +16,23 @@ class Plugin {
/** @type {boolean} Whether this plugin should be activated when its activation points are triggered. */ /** @type {boolean} Whether this plugin should be activated when its activation points are triggered. */
active active
constructor(name, url, activationPoints, active) { /** @type {string} Plugin's description. */
description
/** @type {string} Plugin's version. */
version
/** @type {string} Plugin's logo. */
icon
constructor(name, url, activationPoints, active, description, version, icon) {
this.name = name this.name = name
this.url = url this.url = url
this.activationPoints = activationPoints this.activationPoints = activationPoints
this.active = active this.active = active
this.description = description
this.version = version
this.icon = icon
} }
/** /**

View File

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

View File

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

View File

@ -1,30 +1,32 @@
import { ipcRenderer, contextBridge } from "electron" const { ipcRenderer, contextBridge } = require("electron");
export default function useFacade() { function useFacade() {
const interfaces = { const interfaces = {
install(plugins) { install(plugins) {
return ipcRenderer.invoke('pluggable:install', plugins) return ipcRenderer.invoke("pluggable:install", plugins);
}, },
uninstall(plugins, reload) { uninstall(plugins, reload) {
return ipcRenderer.invoke('pluggable:uninstall', plugins, reload) return ipcRenderer.invoke("pluggable:uninstall", plugins, reload);
}, },
getActive() { getActive() {
return ipcRenderer.invoke('pluggable:getActivePlugins') return ipcRenderer.invoke("pluggable:getActivePlugins");
}, },
update(plugins, reload) { update(plugins, reload) {
return ipcRenderer.invoke('pluggable:update', plugins, reload) return ipcRenderer.invoke("pluggable:update", plugins, reload);
}, },
updatesAvailable(plugin) { updatesAvailable(plugin) {
return ipcRenderer.invoke('pluggable:updatesAvailable', plugin) return ipcRenderer.invoke("pluggable:updatesAvailable", plugin);
}, },
toggleActive(plugin, active) { toggleActive(plugin, active) {
return ipcRenderer.invoke('pluggable:togglePluginActive', plugin, active) return ipcRenderer.invoke("pluggable:togglePluginActive", plugin, active);
}, },
} };
if (contextBridge) { if (contextBridge) {
contextBridge.exposeInMainWorld('pluggableElectronIpc', interfaces) contextBridge.exposeInMainWorld("pluggableElectronIpc", interfaces);
} }
return interfaces return interfaces;
} }
module.exports = useFacade;

View File

@ -18,6 +18,8 @@ class Plugin {
* @property {string} version Version of the package as defined in the manifest. * @property {string} version Version of the package as defined in the manifest.
* @property {Array<string>} activationPoints List of {@link ./Execution-API#activationPoints|activation points}. * @property {Array<string>} activationPoints List of {@link ./Execution-API#activationPoints|activation points}.
* @property {string} main The entry point as defined in the main entry of the manifest. * @property {string} main The entry point as defined in the main entry of the manifest.
* @property {string} description The description of plugin as defined in the manifest.
* @property {string} icon The icon of plugin as defined in the manifest.
*/ */
/** @private */ /** @private */
@ -75,6 +77,8 @@ class Plugin {
this.version = mnf.version this.version = mnf.version
this.activationPoints = mnf.activationPoints || null this.activationPoints = mnf.activationPoints || null
this.main = mnf.main this.main = mnf.main
this.description = mnf.description
this.icon = mnf.icon
} catch (error) { } catch (error) {
throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`) throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`)

View File

@ -61,10 +61,6 @@ function init() {
); );
}); });
const stmt = db.prepare(
"INSERT INTO conversations (name, model_id, image, message) VALUES (?, ?, ?, ?)"
);
stmt.finalize();
db.close(); db.close();
} }
@ -168,7 +164,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); res(row?.map((item: any) => parseToProduct(item)) ?? []);
}); });
db.close(); db.close();
}); });
@ -184,6 +180,7 @@ function deleteDownloadModel(modelId: string) {
const stmt = db.prepare("DELETE FROM models WHERE id = ?"); const stmt = db.prepare("DELETE FROM models WHERE id = ?");
stmt.run(modelId); stmt.run(modelId);
stmt.finalize(); stmt.finalize();
res(modelId);
}); });
db.close(); db.close();
@ -352,7 +349,7 @@ function deleteConversation(id: any) {
); );
deleteMessages.run(id); deleteMessages.run(id);
deleteMessages.finalize(); deleteMessages.finalize();
res([]); res(id);
}); });
db.close(); db.close();
@ -373,6 +370,31 @@ function getConversationMessages(conversation_id: any) {
}); });
} }
function parseToProduct(row: any) {
const product = {
id: row.id,
slug: row.slug,
name: row.name,
description: row.description,
avatarUrl: row.avatar_url,
longDescription: row.long_description,
technicalDescription: row.technical_description,
author: row.author,
version: row.version,
modelUrl: row.model_url,
nsfw: row.nsfw,
greeting: row.greeting,
type: row.type,
inputs: row.inputs,
outputs: row.outputs,
createdAt: new Date(row.created_at),
updatedAt: new Date(row.updated_at),
fileName: row.file_name,
downloadUrl: row.download_url,
};
return product;
}
module.exports = { module.exports = {
init, init,
getConversations, getConversations,

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
{ {
"name": "data-plugin", "name": "data-plugin",
"version": "2.1.0", "version": "1.0.0",
"description": "", "description": "Jan Database Plugin efficiently stores conversation and model data using SQLite, providing accessible data management",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
"main": "dist/index.js", "main": "dist/index.js",
"author": "Jan", "author": "Jan",
"license": "MIT", "license": "MIT",
@ -10,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 && 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",
@ -36,6 +37,7 @@
"node_modules" "node_modules"
], ],
"dependencies": { "dependencies": {
"node-pre-gyp": "^0.17.0",
"sqlite3": "^5.1.6" "sqlite3": "^5.1.6"
} }
} }

View File

@ -13,15 +13,20 @@ const dispose = async () =>
new Promise(async (resolve) => { new Promise(async (resolve) => {
if (window.electronAPI) { if (window.electronAPI) {
window.electronAPI window.electronAPI
.invokePluginFunc(MODULE_PATH, "killSubprocess") .invokePluginFunc(MODULE_PATH, "dispose")
.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

@ -5,14 +5,6 @@ const fs = require("fs");
let subprocess = null; let subprocess = null;
process.on("exit", () => {
// Perform cleanup tasks here
console.log("kill subprocess on exit");
if (subprocess) {
subprocess.kill();
}
});
async function initModel(product) { async function initModel(product) {
// fileName fallback // fileName fallback
if (!product.fileName) { if (!product.fileName) {
@ -31,13 +23,13 @@ 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 = `${__dirname}/nitro`; // Current directory by default let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
// Read the existing config // Read the existing config
const configFilePath = `${binaryFolder}/config/config.json`; const configFilePath = path.join(binaryFolder, "config", "config.json");
let config = {}; let config = {};
if (fs.existsSync(configFilePath)) { if (fs.existsSync(configFilePath)) {
const rawData = fs.readFileSync(configFilePath, "utf-8"); const rawData = fs.readFileSync(configFilePath, "utf-8");
@ -56,8 +48,22 @@ async function initModel(product) {
// Write the updated config back to the file // Write the updated config back to the file
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 4)); fs.writeFileSync(configFilePath, JSON.stringify(config, null, 4));
let binaryName;
if (process.platform === "win32") {
binaryName = "nitro_windows_amd64.exe";
} else if (process.platform === "darwin") { // Mac OS platform
binaryName = process.arch === "arm64" ? "nitro_mac_arm64" : "nitro_mac_amd64";
} else {
// Linux
binaryName = "nitro_linux_amd64_cuda"; // For other platforms
}
const binaryPath = path.join(binaryFolder, binaryName);
// Execute the binary // Execute the binary
subprocess = spawn(`${binaryFolder}/nitro`, [configFilePath]);
subprocess = spawn(binaryPath, [configFilePath], { cwd: binaryFolder });
// Handle subprocess output // Handle subprocess output
subprocess.stdout.on("data", (data) => { subprocess.stdout.on("data", (data) => {
@ -74,6 +80,11 @@ async function initModel(product) {
}); });
} }
function dispose() {
killSubprocess();
// clean other registered resources here
}
function killSubprocess() { function killSubprocess() {
if (subprocess) { if (subprocess) {
subprocess.kill(); subprocess.kill();
@ -87,4 +98,5 @@ function killSubprocess() {
module.exports = { module.exports = {
initModel, initModel,
killSubprocess, killSubprocess,
dispose,
}; };

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];
} }
@ -784,6 +831,8 @@ kernel void kernel_alibi_f32(
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,15 +848,51 @@ 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 const int32_t * src1,
device float * dst, device float * dst,
constant int64_t & ne00, constant int64_t & ne00,
constant int64_t & ne01, constant int64_t & ne01,
@ -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.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,18 @@
{ {
"name": "inference-plugin", "name": "inference-plugin",
"version": "0.0.1", "version": "1.0.0",
"description": "", "description": "Inference Plugin, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/command-line.svg",
"main": "dist/index.js", "main": "dist/index.js",
"author": "James", "author": "Jan",
"license": "MIT", "license": "MIT",
"activationPoints": [ "activationPoints": [
"init" "init"
], ],
"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\" && rm -rf dist/nitro && cp -r 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",
@ -24,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

@ -1,7 +1,8 @@
{ {
"name": "model-management-plugin", "name": "model-management-plugin",
"version": "0.0.1", "version": "1.0.0",
"description": "", "description": "Model Management Plugin leverages the HuggingFace API for model exploration and seamless downloads",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/queue-list.svg",
"main": "dist/index.js", "main": "dist/index.js",
"author": "James", "author": "James",
"license": "MIT", "license": "MIT",
@ -10,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",
@ -23,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

@ -1,7 +1,8 @@
{ {
"name": "monitoring-plugin", "name": "monitoring-plugin",
"version": "0.0.1", "version": "1.0.0",
"description": "", "description": "Utilizing systeminformation, it provides essential System and OS information retrieval",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/cpu-chip.svg",
"main": "dist/bundle.js", "main": "dist/bundle.js",
"author": "Jan", "author": "Jan",
"license": "MIT", "license": "MIT",
@ -10,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

@ -1,30 +1,41 @@
import { import { app, BrowserWindow, ipcMain, dialog, shell } from "electron";
app,
BrowserWindow,
screen as electronScreen,
ipcMain,
dialog,
shell,
} from "electron";
import { readdirSync } from "fs"; import { readdirSync } from "fs";
import { resolve, join, extname } from "path"; import { resolve, join, extname } from "path";
import { rmdir, unlink, createWriteStream } from "fs"; import { rmdir, unlink, createWriteStream } from "fs";
import isDev = require("electron-is-dev");
import { init } from "./core/plugin-manager/pluginMgr"; import { init } from "./core/plugin-manager/pluginMgr";
import { setupMenu } from "./utils/menu";
import { dispose } from "./utils/disposable";
const request = require("request");
const progress = require("request-progress");
const { autoUpdater } = require("electron-updater"); const { autoUpdater } = require("electron-updater");
const Store = require("electron-store"); const Store = require("electron-store");
// @ts-ignore
import request = require("request");
// @ts-ignore
import progress = require("request-progress");
const requiredModules: Record<string, any> = {};
let mainWindow: BrowserWindow | undefined = undefined; let mainWindow: BrowserWindow | undefined = undefined;
const store = new Store();
autoUpdater.autoDownload = false; app
autoUpdater.autoInstallOnAppQuit = true; .whenReady()
.then(migratePlugins)
.then(setupPlugins)
.then(setupMenu)
.then(handleIPCs)
.then(handleAppUpdates)
.then(createMainWindow)
.then(() => {
app.on("activate", () => {
if (!BrowserWindow.getAllWindows().length) {
createMainWindow();
}
});
});
const createMainWindow = () => { app.on("window-all-closed", () => {
dispose(requiredModules);
app.quit();
});
function createMainWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1200, width: 1200,
height: 800, height: 800,
@ -37,29 +48,9 @@ const createMainWindow = () => {
}, },
}); });
ipcMain.handle( const startURL = app.isPackaged
"invokePluginFunc", ? `file://${join(__dirname, "../renderer/index.html")}`
async (_event, modulePath, method, ...args) => { : "http://localhost:3000";
const module = join(app.getPath("userData"), "plugins", modulePath);
return await import(/* webpackIgnore: true */ module)
.then((plugin) => {
if (typeof plugin[method] === "function") {
return plugin[method](...args);
} else {
console.log(plugin[method]);
console.error(`Function "${method}" does not exist in the module.`);
}
})
.then((res) => {
return res;
})
.catch((err) => console.log(err));
}
);
const startURL = isDev
? "http://localhost:3000"
: `file://${join(__dirname, "../renderer/index.html")}`;
mainWindow.loadURL(startURL); mainWindow.loadURL(startURL);
@ -68,37 +59,107 @@ const createMainWindow = () => {
if (process.platform !== "darwin") app.quit(); if (process.platform !== "darwin") app.quit();
}); });
if (isDev) mainWindow.webContents.openDevTools(); if (!app.isPackaged) mainWindow.webContents.openDevTools();
}; }
app function handleAppUpdates() {
.whenReady() /*New Update Available*/
.then(migratePlugins) autoUpdater.on("update-available", async (_info: any) => {
.then(() => { const action = await dialog.showMessageBox({
createMainWindow(); message: `Update available. Do you want to download the latest update?`,
setupPlugins(); buttons: ["Download", "Later"],
});
if (action.response === 0) await autoUpdater.downloadUpdate();
});
/*App Update Completion Message*/
autoUpdater.on("update-downloaded", async (_info: any) => {
mainWindow?.webContents.send("APP_UPDATE_COMPLETE", {});
const action = await dialog.showMessageBox({
message: `Update downloaded. Please restart the application to apply the updates.`,
buttons: ["Restart", "Later"],
});
if (action.response === 0) {
autoUpdater.quitAndInstall();
}
});
/*App Update Error */
autoUpdater.on("error", (info: any) => {
dialog.showMessageBox({ message: info.message });
mainWindow?.webContents.send("APP_UPDATE_ERROR", {});
});
/*App Update Progress */
autoUpdater.on("download-progress", (progress: any) => {
console.log("app update progress: ", progress.percent);
mainWindow?.webContents.send("APP_UPDATE_PROGRESS", {
percent: progress.percent,
});
});
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.checkForUpdates(); autoUpdater.checkForUpdates();
}
ipcMain.handle("basePlugins", async (event) => { function handleIPCs() {
ipcMain.handle(
"invokePluginFunc",
async (_event, modulePath, method, ...args) => {
const module = require(/* webpackIgnore: true */ join(
app.getPath("userData"),
"plugins",
modulePath
));
requiredModules[modulePath] = module;
if (typeof module[method] === "function") {
return module[method](...args);
} else {
console.log(module[method]);
console.error(`Function "${method}" does not exist in the module.`);
}
}
);
ipcMain.handle("basePlugins", async (_event) => {
const basePluginPath = join( const basePluginPath = join(
__dirname, __dirname,
"../", "../",
isDev ? "/core/pre-install" : "../app.asar.unpacked/core/pre-install" app.isPackaged
? "../app.asar.unpacked/core/pre-install"
: "/core/pre-install"
); );
return readdirSync(basePluginPath) return readdirSync(basePluginPath)
.filter((file) => extname(file) === ".tgz") .filter((file) => extname(file) === ".tgz")
.map((file) => join(basePluginPath, file)); .map((file) => join(basePluginPath, file));
}); });
ipcMain.handle("pluginPath", async (event) => { ipcMain.handle("pluginPath", async (_event) => {
return join(app.getPath("userData"), "plugins"); return join(app.getPath("userData"), "plugins");
}); });
ipcMain.handle("appVersion", async (event) => { ipcMain.handle("appVersion", async (_event) => {
return app.getVersion(); return app.getVersion();
}); });
ipcMain.handle("openExternalUrl", async (event, url) => { ipcMain.handle("openExternalUrl", async (_event, url) => {
shell.openExternal(url); shell.openExternal(url);
}); });
ipcMain.handle("relaunch", async (_event, url) => {
dispose(requiredModules);
app.relaunch();
app.exit();
});
ipcMain.handle("reloadPlugins", async (_event, url) => {
const userDataPath = app.getPath("userData");
const fullPath = join(userDataPath, "plugins");
rmdir(fullPath, { recursive: true }, function (err) {
if (err) console.log(err);
app.relaunch();
app.exit();
});
});
/** /**
* Used to delete a file from the user data folder * Used to delete a file from the user data folder
@ -116,9 +177,7 @@ app
} else { } else {
result = "File deleted successfully"; result = "File deleted successfully";
} }
console.log( console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`);
`Delete file ${filePath} from ${fullPath} result: ${result}`
);
}); });
return result; return result;
@ -151,57 +210,11 @@ app
}) })
.pipe(createWriteStream(destination)); .pipe(createWriteStream(destination));
}); });
}
app.on("activate", () => {
if (!BrowserWindow.getAllWindows().length) {
createMainWindow();
}
});
});
/*New Update Available*/
autoUpdater.on("update-available", async (info: any) => {
const action = await dialog.showMessageBox({
message: `Update available. Do you want to download the latest update?`,
buttons: ["Download", "Later"],
});
if (action.response === 0) await autoUpdater.downloadUpdate();
});
/*App Update Completion Message*/
autoUpdater.on("update-downloaded", async (info: any) => {
mainWindow?.webContents.send("APP_UPDATE_COMPLETE", {});
const action = await dialog.showMessageBox({
message: `Update downloaded. Please restart the application to apply the updates.`,
buttons: ["Restart", "Later"],
});
if (action.response === 0) {
autoUpdater.quitAndInstall();
}
});
/*App Update Error */
autoUpdater.on("error", (info: any) => {
dialog.showMessageBox({ message: info.message });
mainWindow?.webContents.send("APP_UPDATE_ERROR", {});
});
/*App Update Progress */
autoUpdater.on("download-progress", (progress: any) => {
console.log("app update progress: ", progress.percent);
mainWindow?.webContents.send("APP_UPDATE_PROGRESS", {
percent: progress.percent,
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
function migratePlugins() { function migratePlugins() {
return new Promise((resolve) => { return new Promise((resolve) => {
const store = new Store();
if (store.get("migrated_version") !== app.getVersion()) { if (store.get("migrated_version") !== app.getVersion()) {
console.log("start migration:", store.get("migrated_version")); console.log("start migration:", store.get("migrated_version"));
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
@ -217,12 +230,12 @@ function migratePlugins() {
resolve(undefined); resolve(undefined);
} }
}); });
}; }
function setupPlugins() { function setupPlugins() {
init({ init({
// Function to check from the main process that user wants to install a plugin // Function to check from the main process that user wants to install a plugin
confirmInstall: async (plugins: string[]) => { confirmInstall: async (_plugins: string[]) => {
return true; return true;
}, },
// Path to install plugin to // Path to install plugin to

View File

@ -1,8 +1,8 @@
{ {
"name": "jan-electron", "name": "jan-electron",
"version": "0.1.1", "version": "0.1.3",
"main": "./build/main.js", "main": "./build/main.js",
"author": "Jan", "author": "Jan <service@jan.ai>",
"license": "MIT", "license": "MIT",
"homepage": "./", "homepage": "./",
"build": { "build": {
@ -11,8 +11,9 @@
"files": [ "files": [
"renderer/**/*", "renderer/**/*",
"build/*.{js,map}", "build/*.{js,map}",
"build/core/plugin-manager/**/*", "build/**/*.{js,map}",
"core/pre-install" "core/pre-install",
"core/plugin-manager/facade"
], ],
"asarUnpack": [ "asarUnpack": [
"core/pre-install" "core/pre-install"
@ -26,29 +27,48 @@
], ],
"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\"",
"test:e2e": "playwright test --workers=1",
"dev": "tsc -p . && electron .", "dev": "tsc -p . && electron .",
"build": "tsc -p . && electron-builder -p never -mw", "build": "tsc -p . && electron-builder -p never -m",
"build:publish": "tsc -p . && electron-builder -p onTagOrDraft -mw", "build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
"postinstall": "electron-builder install-app-deps" "build:win32": "tsc -p . && electron-builder -p never -w",
"build:linux": "tsc -p . && electron-builder -p never --linux deb",
"build:publish": "tsc -p . && electron-builder -p onTagOrDraft -m",
"build:publish-darwin": "tsc -p . && electron-builder -p onTagOrDraft -m --x64 --arm64",
"build:publish-win32": "tsc -p . && electron-builder -p onTagOrDraft -w",
"build:publish-linux": "tsc -p . && electron-builder -p onTagOrDraft --linux deb "
}, },
"dependencies": { "dependencies": {
"electron-is-dev": "^2.0.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",
"node-llama-cpp": "^2.4.1", "pacote": "^17.0.4",
"pluggable-electron": "^0.6.0", "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": {
"concurrently": "^8.2.1", "@electron/notarize": "^2.1.0",
"@playwright/test": "^1.38.1",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",
"electron": "26.2.1", "electron": "26.2.1",
"electron-builder": "^24.6.4", "electron-builder": "^24.6.4",
"wait-on": "^7.0.1" "electron-playwright-helpers": "^1.6.0",
"eslint-plugin-react": "^7.33.2"
}, },
"installConfig": { "installConfig": {
"hoistingLimits": "workspaces" "hoistingLimits": "workspaces"

View File

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

View File

@ -1,7 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */
// Make Pluggable Electron's facade available to the renderer on window.plugins // Make Pluggable Electron's facade available to the renderer on window.plugins
//@ts-ignore //@ts-ignore
const useFacade = require("pluggable-electron/facade"); const useFacade = require("../core/plugin-manager/facade");
useFacade(); useFacade();
//@ts-ignore //@ts-ignore
const { contextBridge, ipcRenderer } = require("electron"); const { contextBridge, ipcRenderer } = require("electron");
@ -14,10 +13,14 @@ 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),
relaunch: () => ipcRenderer.invoke("relaunch"),
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath), deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
downloadFile: (url: string, path: string) => downloadFile: (url: string, path: string) =>

View File

@ -0,0 +1,47 @@
import { _electron as electron } from "playwright";
import { ElectronApplication, Page, expect, test } from "@playwright/test";
import {
findLatestBuild,
parseElectronApp,
stubDialog,
} from "electron-playwright-helpers";
let electronApp: ElectronApplication;
let page: Page;
test.beforeAll(async () => {
process.env.CI = "e2e";
const latestBuild = findLatestBuild("dist");
expect(latestBuild).toBeTruthy();
// parse the packaged Electron app and find paths and other info
const appInfo = parseElectronApp(latestBuild);
expect(appInfo).toBeTruthy();
electronApp = await electron.launch({
args: [appInfo.main], // main file from package.json
executablePath: appInfo.executable, // path to the Electron executable
});
await stubDialog(electronApp, "showMessageBox", { response: 1 });
page = await electronApp.firstWindow();
});
test.afterAll(async () => {
await electronApp.close();
await page.close();
});
test("explores models", async () => {
await page.getByRole("button", { name: "Explore Models" }).first().click();
const header = await page
.getByRole("heading")
.filter({ hasText: "Explore Models" })
.first()
.isDisabled();
expect(header).toBe(false);
// More test cases here...
});

View File

@ -0,0 +1,57 @@
import { _electron as electron } from "playwright";
import { ElectronApplication, Page, expect, test } from "@playwright/test";
import {
findLatestBuild,
parseElectronApp,
stubDialog,
} from "electron-playwright-helpers";
let electronApp: ElectronApplication;
let page: Page;
test.beforeAll(async () => {
process.env.CI = "e2e";
const latestBuild = findLatestBuild("dist");
expect(latestBuild).toBeTruthy();
// parse the packaged Electron app and find paths and other info
const appInfo = parseElectronApp(latestBuild);
expect(appInfo).toBeTruthy();
expect(appInfo.asar).toBe(true);
expect(appInfo.executable).toBeTruthy();
expect(appInfo.main).toBeTruthy();
expect(appInfo.name).toBe("jan-electron");
expect(appInfo.packageJson).toBeTruthy();
expect(appInfo.packageJson.name).toBe("jan-electron");
expect(appInfo.platform).toBeTruthy();
expect(appInfo.platform).toBe(process.platform);
expect(appInfo.resourcesDir).toBeTruthy();
electronApp = await electron.launch({
args: [appInfo.main], // main file from package.json
executablePath: appInfo.executable, // path to the Electron executable
});
await stubDialog(electronApp, "showMessageBox", { response: 1 });
page = await electronApp.firstWindow();
});
test.afterAll(async () => {
await electronApp.close();
await page.close();
});
test("renders the home page", async () => {
expect(page).toBeDefined();
// Welcome text is available
const welcomeText = await page
.locator(".text-5xl", {
hasText: "Welcome,lets download your first model",
})
.first()
.isDisabled();
expect(welcomeText).toBe(false);
});

View File

@ -0,0 +1,46 @@
import { _electron as electron } from "playwright";
import { ElectronApplication, Page, expect, test } from "@playwright/test";
import {
findLatestBuild,
parseElectronApp,
stubDialog,
} from "electron-playwright-helpers";
let electronApp: ElectronApplication;
let page: Page;
test.beforeAll(async () => {
process.env.CI = "e2e";
const latestBuild = findLatestBuild("dist");
expect(latestBuild).toBeTruthy();
// parse the packaged Electron app and find paths and other info
const appInfo = parseElectronApp(latestBuild);
expect(appInfo).toBeTruthy();
electronApp = await electron.launch({
args: [appInfo.main], // main file from package.json
executablePath: appInfo.executable, // path to the Electron executable
});
await stubDialog(electronApp, "showMessageBox", { response: 1 });
page = await electronApp.firstWindow();
});
test.afterAll(async () => {
await electronApp.close();
await page.close();
});
test("shows my models", async () => {
await page.getByRole("button", { name: "My Models" }).first().click();
const header = await page
.getByRole("heading")
.filter({ hasText: "My Models" })
.first()
.isDisabled();
expect(header).toBe(false);
// More test cases here...
});

View File

@ -0,0 +1,76 @@
import { _electron as electron } from "playwright";
import { ElectronApplication, Page, expect, test } from "@playwright/test";
import {
findLatestBuild,
parseElectronApp,
stubDialog,
} from "electron-playwright-helpers";
let electronApp: ElectronApplication;
let page: Page;
test.beforeAll(async () => {
process.env.CI = "e2e";
const latestBuild = findLatestBuild("dist");
expect(latestBuild).toBeTruthy();
// parse the packaged Electron app and find paths and other info
const appInfo = parseElectronApp(latestBuild);
expect(appInfo).toBeTruthy();
electronApp = await electron.launch({
args: [appInfo.main], // main file from package.json
executablePath: appInfo.executable, // path to the Electron executable
});
await stubDialog(electronApp, "showMessageBox", { response: 1 });
page = await electronApp.firstWindow();
});
test.afterAll(async () => {
await electronApp.close();
await page.close();
});
test("renders left navigation panel", async () => {
// Chat History section is available
const chatSection = await page
.getByRole("heading")
.filter({ hasText: "CHAT HISTORY" })
.first()
.isDisabled();
expect(chatSection).toBe(false);
// Home actions
const newChatBtn = await page
.getByRole("button", { name: "New Chat" })
.first()
.isEnabled();
const exploreBtn = await page
.getByRole("button", { name: "Explore Models" })
.first()
.isEnabled();
const discordBtn = await page
.getByRole("button", { name: "Discord" })
.first()
.isEnabled();
const myModelsBtn = await page
.getByRole("button", { name: "My Models" })
.first()
.isEnabled();
const settingsBtn = await page
.getByRole("button", { name: "Settings" })
.first()
.isEnabled();
expect(
[
newChatBtn,
exploreBtn,
discordBtn,
myModelsBtn,
settingsBtn,
].filter((e) => !e).length
).toBe(0);
});

View File

@ -0,0 +1,42 @@
import { _electron as electron } from "playwright";
import { ElectronApplication, Page, expect, test } from "@playwright/test";
import {
findLatestBuild,
parseElectronApp,
stubDialog,
} from "electron-playwright-helpers";
let electronApp: ElectronApplication;
let page: Page;
test.beforeAll(async () => {
process.env.CI = "e2e";
const latestBuild = findLatestBuild("dist");
expect(latestBuild).toBeTruthy();
// parse the packaged Electron app and find paths and other info
const appInfo = parseElectronApp(latestBuild);
expect(appInfo).toBeTruthy();
electronApp = await electron.launch({
args: [appInfo.main], // main file from package.json
executablePath: appInfo.executable, // path to the Electron executable
});
await stubDialog(electronApp, "showMessageBox", { response: 1 });
page = await electronApp.firstWindow();
});
test.afterAll(async () => {
await electronApp.close();
await page.close();
});
test("shows settings", async () => {
await page.getByRole("button", { name: "Settings" }).first().click();
const pluginList = await page.getByTestId("plugin-item").count();
expect(pluginList).toBe(4);
});

View File

@ -2,13 +2,17 @@
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"module": "commonjs", "module": "commonjs",
"noImplicitAny": true,
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"outDir": "./build", "outDir": "./build",
"rootDir": "./", "rootDir": "./",
"noEmitOnError": true, "noEmitOnError": true,
"baseUrl": ".",
"allowJs": true, "allowJs": true,
"paths": { "*": ["node_modules/*"] },
"typeRoots": ["node_modules/@types"] "typeRoots": ["node_modules/@types"]
}, },
"exclude": ["core", "build", "node_modules"] "include": ["./**/*.ts"],
"exclude": ["core", "build", "dist", "tests"]
} }

View File

@ -0,0 +1,8 @@
export function dispose(requiredModules: Record<string, any>) {
for (const key in requiredModules) {
const module = requiredModules[key];
if (typeof module["dispose"] === "function") {
module["dispose"]();
}
}
}

111
electron/utils/menu.ts Normal file
View File

@ -0,0 +1,111 @@
// @ts-nocheck
const { app, Menu, dialog } = require("electron");
const isMac = process.platform === "darwin";
const { autoUpdater } = require("electron-updater");
import { compareSemanticVersions } from "./versionDiff";
const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
...(isMac
? [
{
label: app.name,
submenu: [
{ role: "about" },
{
label: "Check for Updates...",
click: () =>
autoUpdater.checkForUpdatesAndNotify().then((e) => {
if (
!e ||
compareSemanticVersions(
app.getVersion(),
e.updateInfo.version
) >= 0
)
dialog.showMessageBox({
message: `There are currently no updates available.`,
});
}),
},
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "unhide" },
{ type: "separator" },
{ role: "quit" },
],
},
]
: []),
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
...(isMac
? [
{ role: "pasteAndMatchStyle" },
{ role: "delete" },
{ role: "selectAll" },
{ type: "separator" },
{
label: "Speech",
submenu: [{ role: "startSpeaking" }, { role: "stopSpeaking" }],
},
]
: [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]),
],
},
{
label: "View",
submenu: [
{ role: "reload" },
{ role: "forceReload" },
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ type: "separator" },
{ role: "togglefullscreen" },
],
},
{
label: "Window",
submenu: [
{ role: "minimize" },
{ role: "zoom" },
...(isMac
? [
{ type: "separator" },
{ role: "front" },
{ type: "separator" },
{ role: "window" },
]
: [{ role: "close" }]),
],
},
{
role: "help",
submenu: [
{
label: "Learn More",
click: async () => {
const { shell } = require("electron");
await shell.openExternal("https://jan.ai/");
},
},
],
},
];
export const setupMenu = () => {
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
};

View File

@ -0,0 +1,21 @@
export const compareSemanticVersions = (a: string, b: string) => {
// 1. Split the strings into their parts.
const a1 = a.split('.');
const b1 = b.split('.');
// 2. Contingency in case there's a 4th or 5th version
const len = Math.min(a1.length, b1.length);
// 3. Look through each version number and compare.
for (let i = 0; i < len; i++) {
const a2 = +a1[ i ] || 0;
const b2 = +b1[ i ] || 0;
if (a2 !== b2) {
return a2 > b2 ? 1 : -1;
}
}
// 4. We hit this if the all checked versions so far are equal
//
return b1.length - a1.length;
};

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

@ -14,19 +14,29 @@
] ]
}, },
"scripts": { "scripts": {
"lint": "yarn workspace jan-electron lint && yarn workspace jan-web lint",
"test": "yarn workspace jan-electron test:e2e",
"dev:electron": "yarn workspace jan-electron dev", "dev:electron": "yarn workspace jan-electron dev",
"dev:web": "yarn workspace jan-web dev", "dev:web": "yarn workspace jan-web dev",
"dev": "concurrently --kill-others-on-fail \"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": "rm -f ./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:publish": "yarn build:web && yarn workspace jan-electron build:publish" "build:darwin": "yarn build:web && yarn workspace jan-electron build:darwin",
"build:win32": "yarn build:web && yarn workspace jan-electron build:win32",
"build:linux": "yarn build:web && yarn workspace jan-electron build:linux",
"build:publish": "yarn build:web && yarn workspace jan-electron build:publish",
"build:publish-darwin": "yarn build:web && yarn workspace jan-electron build:publish-darwin",
"build:publish-win32": "yarn build:web && yarn workspace jan-electron build:publish-win32",
"build:publish-linux": "yarn build:web && yarn workspace jan-electron build:publish-linux"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^8.2.1", "concurrently": "^8.2.1",
"cpx": "^1.5.0", "cpx": "^1.5.0",
"wait-on": "^7.0.1" "wait-on": "^7.0.1",
"rimraf": "^3.0.2"
}, },
"version": "0.0.0" "version": "0.0.0"
} }

View File

@ -0,0 +1,19 @@
import { useAtomValue } from "jotai";
import React, { Fragment } from "react";
import ModelTable from "../ModelTable";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
const ActiveModelTable: React.FC = () => {
const activeModel = useAtomValue(currentProductAtom);
if (!activeModel) return null;
return (
<div className="pl-[63px] pr-[89px]">
<h3 className="text-xl leading-[25px] mb-[13px]">Active Model(s)</h3>
<ModelTable models={[activeModel]} />
</div>
);
};
export default ActiveModelTable;

View File

@ -1,29 +0,0 @@
import { MenuAdvancedPrompt } from "../MenuAdvancedPrompt";
import { useForm } from "react-hook-form";
import BasicPromptButton from "../BasicPromptButton";
import PrimaryButton from "../PrimaryButton";
const AdvancedPrompt: React.FC = () => {
const { register, handleSubmit } = useForm();
const onSubmit = (data: any) => {};
return (
<form
className="w-[288px] h-screen flex flex-col border-r border-gray-200"
onSubmit={handleSubmit(onSubmit)}
>
<BasicPromptButton />
<MenuAdvancedPrompt register={register} />
<div className="py-3 px-2 flex flex-none gap-3 items-center justify-between border-t border-gray-200">
<PrimaryButton
fullWidth={true}
title="Generate"
onClick={() => handleSubmit(onSubmit)}
/>
</div>
</form>
);
};
export default AdvancedPrompt;

View File

@ -1,18 +0,0 @@
import React, { useState } from "react";
import TogglableHeader from "../TogglableHeader";
const AdvancedPromptGenerationParams = () => {
const [expand, setExpand] = useState(true);
return (
<>
<TogglableHeader
icon={"icons/unicorn_layers-alt.svg"}
title={"Generation Parameters"}
expand={expand}
onTitleClick={() => setExpand(!expand)}
/>
</>
);
};
export default AdvancedPromptGenerationParams;

View File

@ -1,31 +0,0 @@
import React, { useState } from "react";
import { DropdownsList } from "../DropdownList";
import TogglableHeader from "../TogglableHeader";
import { UploadFileImage } from "../UploadFileImage";
import { FieldValues, UseFormRegister } from "react-hook-form";
type Props = {
register: UseFormRegister<FieldValues>;
};
const AdvancedPromptImageUpload: React.FC<Props> = ({ register }) => {
const [expand, setExpand] = useState(true);
const data = ["test1", "test2", "test3", "test4"];
return (
<>
<TogglableHeader
icon={"icons/ic_image.svg"}
title={"Image"}
expand={expand}
onTitleClick={() => setExpand(!expand)}
/>
<div className={`${expand ? "flex" : "hidden"} flex-col gap-[5px]`}>
<UploadFileImage register={register} />
<DropdownsList title="Control image with ControlNet:" data={data} />
</div>
</>
);
};
export default AdvancedPromptImageUpload;

View File

@ -1,29 +0,0 @@
import React, { useState } from "react";
import { DropdownsList } from "../DropdownList";
import TogglableHeader from "../TogglableHeader";
const AdvancedPromptResolution = () => {
const [expand, setExpand] = useState(true);
const data = ["512", "524", "536"];
const ratioData = ["1:1", "2:2", "3:3"];
return (
<>
<TogglableHeader
icon={"icons/unicorn_layers-alt.svg"}
title={"Resolution"}
expand={expand}
onTitleClick={() => setExpand(!expand)}
/>
<div className={`${expand ? "flex" : "hidden"} flex-col gap-[5px]`}>
<div className="flex gap-3 py-3">
<DropdownsList data={data} title="Width" />
<DropdownsList data={data} title="Height" />
</div>
<DropdownsList title="Select ratio" data={ratioData} />
</div>
</>
);
};
export default AdvancedPromptResolution;

View File

@ -1,41 +0,0 @@
import React, { useState } from "react";
import TogglableHeader from "../TogglableHeader";
import { AdvancedTextArea } from "../AdvancedTextArea";
import { FieldValues, UseFormRegister } from "react-hook-form";
type Props = {
register: UseFormRegister<FieldValues>;
};
const AdvancedPromptText: React.FC<Props> = ({ register }) => {
const [expand, setExpand] = useState(true);
return (
<>
<TogglableHeader
icon={"icons/messicon.svg"}
title={"Prompt"}
expand={expand}
onTitleClick={() => setExpand(!expand)}
/>
<div className={`${expand ? "flex" : "hidden"} flex-col gap-[5px]`}>
<AdvancedTextArea
formId="prompt"
height={80}
placeholder="Prompt"
title="Prompt"
register={register}
/>
<AdvancedTextArea
formId="negativePrompt"
height={80}
placeholder="Describe what you don't want in your image"
title="Negative Prompt"
register={register}
/>
</div>
</>
);
};
export default AdvancedPromptText;

View File

@ -1,27 +0,0 @@
import { FieldValues, UseFormRegister } from "react-hook-form";
type Props = {
formId?: string;
height: number;
title: string;
placeholder: string;
register: UseFormRegister<FieldValues>;
};
export const AdvancedTextArea: React.FC<Props> = ({
formId = "",
height,
placeholder,
title,
register,
}) => (
<div className="w-full flex flex-col pt-3 gap-1">
<label className="text-sm leading-5 text-gray-800">{title}</label>
<textarea
style={{ height: `${height}px` }}
className="rounded-lg py-[13px] px-5 border outline-none resize-none border-gray-300 bg-gray-50 placeholder:gray-400 text-sm font-normal"
placeholder={placeholder}
{...register(formId, { required: formId === "prompt" ? true : false })}
/>
</div>
);

View File

@ -1,20 +0,0 @@
import Image from "next/image";
const Search: React.FC = () => {
return (
<div className="flex bg-gray-200 w-[343px] h-[36px] items-center px-2 gap-[6px] rounded-md">
<Image
src={"icons/magnifyingglass.svg"}
width={15.63}
height={15.78}
alt=""
/>
<input
className="bg-inherit outline-0 w-full border-0 p-0 focus:ring-0"
placeholder="Search"
/>
</div>
);
};
export default Search;

View File

@ -1,20 +0,0 @@
import Image from "next/image";
import Link from "next/link";
type Props = {
name: string;
imageUrl: string;
};
const AiTypeCard: React.FC<Props> = ({ imageUrl, name }) => {
return (
<Link href={`/ai/${name}`} className='flex-1'>
<div className="flex-1 h-full bg-[#F3F4F6] flex items-center justify-center gap-[10px] py-[13px] rounded-[8px] px-4 active:opacity-50 hover:opacity-20">
<Image src={imageUrl} width={82} height={82} alt="" />
<span className="font-bold">{name}</span>
</div>
</Link>
);
};
export default AiTypeCard;

View File

@ -1,15 +0,0 @@
type Props = {
title: string;
description: string;
};
export const ApiStep: React.FC<Props> = ({ description, title }) => {
return (
<div className="gap-2 flex flex-col">
<span className="text-[#8A8A8A]">{title}</span>
<div className="flex flex-col gap-[10px] p-[18px] bg-[#F9F9F9] overflow-y-hidden">
<pre className="text-sm leading-5 text-black">{description}</pre>
</div>
</div>
);
};

View File

@ -2,9 +2,8 @@ import { Product } from "@/_models/Product";
import DownloadModelContent from "../DownloadModelContent"; import DownloadModelContent from "../DownloadModelContent";
import ModelDownloadButton from "../ModelDownloadButton"; import ModelDownloadButton from "../ModelDownloadButton";
import ModelDownloadingButton from "../ModelDownloadingButton"; import ModelDownloadingButton from "../ModelDownloadingButton";
import ViewModelDetailButton from "../ViewModelDetailButton";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { modelDownloadStateAtom } from "@/_helpers/JotaiWrapper"; import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
type Props = { type Props = {
product: Product; product: Product;
@ -36,8 +35,6 @@ const AvailableModelCard: React.FC<Props> = ({
} }
} }
const handleViewDetails = () => {};
const downloadButton = isDownloading ? ( const downloadButton = isDownloading ? (
<div className="w-1/5 flex items-start justify-end"> <div className="w-1/5 flex items-start justify-end">
<ModelDownloadingButton total={total} value={transferred} /> <ModelDownloadingButton total={total} value={transferred} />
@ -50,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,20 +1,14 @@
"use client"; "use client";
import { import { useSetAtom } from "jotai";
currentConversationAtom,
showingAdvancedPromptAtom,
} from "@/_helpers/JotaiWrapper";
import { useAtomValue, useSetAtom } from "jotai";
import SecondaryButton from "../SecondaryButton"; import SecondaryButton from "../SecondaryButton";
import SendButton from "../SendButton"; import SendButton from "../SendButton";
import { ProductType } from "@/_models/Product"; import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
const BasicPromptAccessories: React.FC = () => { const BasicPromptAccessories: React.FC = () => {
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom); const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom);
const currentConversation = useAtomValue(currentConversationAtom);
const shouldShowAdvancedPrompt = false; const shouldShowAdvancedPrompt = false;
// currentConversation?.product.type === ProductType.ControlNet;
return ( return (
<div <div

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { ChevronLeftIcon } from "@heroicons/react/24/outline"; import { ChevronLeftIcon } from "@heroicons/react/24/outline";
import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper"; import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
const BasicPromptButton: React.FC = () => { const BasicPromptButton: React.FC = () => {
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom); const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom);

View File

@ -1,22 +1,45 @@
"use client"; "use client";
import { currentPromptAtom } from "@/_helpers/JotaiWrapper"; import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
import { selectedModelAtom } from "@/_helpers/atoms/Model.atom";
import useCreateConversation from "@/_hooks/useCreateConversation";
import useInitModel from "@/_hooks/useInitModel";
import useSendChatMessage from "@/_hooks/useSendChatMessage"; import useSendChatMessage from "@/_hooks/useSendChatMessage";
import { useAtom } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { ChangeEvent } from "react";
const BasicPromptInput: React.FC = () => { const BasicPromptInput: React.FC = () => {
const activeConversationId = useAtomValue(getActiveConvoIdAtom);
const selectedModel = useAtomValue(selectedModelAtom);
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom); const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom);
const { sendChatMessage } = useSendChatMessage(); const { sendChatMessage } = useSendChatMessage();
const { requestCreateConvo } = useCreateConversation();
const handleMessageChange = (event: any) => { const { initModel } = useInitModel();
const handleMessageChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setCurrentPrompt(event.target.value); setCurrentPrompt(event.target.value);
}; };
const handleKeyDown = (event: any) => { const handleKeyDown = async (
event: React.KeyboardEvent<HTMLTextAreaElement>
) => {
if (event.key === "Enter") { if (event.key === "Enter") {
if (!event.shiftKey) { if (!event.shiftKey) {
if (activeConversationId) {
event.preventDefault(); event.preventDefault();
sendChatMessage(); sendChatMessage();
} else {
if (!selectedModel) {
console.log("No model selected");
return;
}
await requestCreateConvo(selectedModel);
await initModel(selectedModel);
sendChatMessage();
}
} }
} }
}; };

View File

@ -1,39 +0,0 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import React, { PropsWithChildren } from "react";
type PropType = PropsWithChildren<
React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>
>;
export const PrevButton: React.FC<PropType> = (props) => {
const { children, ...restProps } = props;
return (
<button
className="embla__button embla__button--prev"
type="button"
{...restProps}
>
<ChevronLeftIcon width={20} height={20} />
{children}
</button>
);
};
export const NextButton: React.FC<PropType> = (props) => {
const { children, ...restProps } = props;
return (
<button
className="embla__button embla__button--next"
type="button"
{...restProps}
>
<ChevronRightIcon width={20} height={20} />
{children}
</button>
);
};

View File

@ -1,25 +0,0 @@
import { useTheme } from "next-themes";
import { SunIcon, MoonIcon } from "@heroicons/react/24/outline";
export const ThemeChanger: React.FC = () => {
const { theme, setTheme, systemTheme } = useTheme();
const currentTheme = theme === "system" ? systemTheme : theme;
if (currentTheme === "dark") {
return (
<SunIcon
className="h-6 w-6"
aria-hidden="true"
onClick={() => setTheme("light")}
/>
);
}
return (
<MoonIcon
className="h-6 w-6"
aria-hidden="true"
onClick={() => setTheme("dark")}
/>
);
};

View File

@ -1,29 +1,24 @@
"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 {
chatMessages,
getActiveConvoIdAtom,
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 { chatMessages } from "@/_helpers/atoms/ChatMessage.atom";
const ChatBody: React.FC = () => { const ChatBody: React.FC = () => {
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? ""; const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? "";
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);
@ -42,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 (
@ -60,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/JotaiWrapper";
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

@ -1,29 +0,0 @@
"use client";
import { useAtomValue } from "jotai";
import { MainViewState, getMainViewStateAtom } from "@/_helpers/JotaiWrapper";
import { ReactNode } from "react";
import ModelManagement from "../ModelManagement";
import Welcome from "../WelcomeContainer";
import { Preferences } from "../Preferences";
type Props = {
children: ReactNode;
};
export default function ChatContainer({ children }: Props) {
const viewState = useAtomValue(getMainViewStateAtom);
switch (viewState) {
case MainViewState.ExploreModel:
return <ModelManagement />;
case MainViewState.Setting:
return <Preferences />;
case MainViewState.ResourceMonitor:
case MainViewState.MyModel:
case MainViewState.Welcome:
return <Welcome />;
default:
return <div className="flex flex-1 overflow-hidden">{children}</div>;
}
}

View File

@ -1,37 +0,0 @@
import {
getActiveConvoIdAtom,
setActiveConvoIdAtom,
} from "@/_helpers/JotaiWrapper";
import { useAtomValue, useSetAtom } from "jotai";
import Image from "next/image";
type Props = {
imageUrl: string;
conversationId: string;
};
const CompactHistoryItem: React.FC<Props> = ({ imageUrl, conversationId }) => {
const activeConvoId = useAtomValue(getActiveConvoIdAtom);
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
const isSelected = activeConvoId === conversationId;
return (
<button
onClick={() => setActiveConvoId(conversationId)}
className={`${
isSelected ? "bg-gray-100" : "bg-transparent"
} w-14 h-14 rounded-lg`}
>
<Image
className="rounded-full mx-auto"
src={imageUrl}
width={36}
height={36}
alt=""
/>
</button>
);
};
export default CompactHistoryItem;

View File

@ -1,21 +0,0 @@
import { useAtomValue } from "jotai";
import CompactHistoryItem from "../CompactHistoryItem";
import { userConversationsAtom } from "@/_helpers/JotaiWrapper";
const CompactHistoryList: React.FC = () => {
const conversations = useAtomValue(userConversationsAtom);
return (
<div className="flex flex-col flex-1 gap-1 mt-3">
{conversations.map(({ id, image }) => (
<CompactHistoryItem
key={id}
conversationId={id ?? ""}
imageUrl={image ?? ""}
/>
))}
</div>
);
};
export default CompactHistoryList;

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import JanImage from "../JanImage"; import JanImage from "../JanImage";
import { setActiveConvoIdAtom } from "@/_helpers/JotaiWrapper";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { setActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
const CompactLogo: React.FC = () => { const CompactLogo: React.FC = () => {
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom); const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);

View File

@ -1,11 +0,0 @@
import CompactHistoryList from "../CompactHistoryList";
import CompactLogo from "../CompactLogo";
const CompactSideBar: React.FC = () => (
<div className="h-screen w-16 border-r border-gray-300 flex flex-col items-center pt-3 gap-3">
<CompactLogo />
<CompactHistoryList />
</div>
);
export default CompactSideBar;

View File

@ -1,4 +1,4 @@
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/JotaiWrapper"; import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
import useDeleteConversation from "@/_hooks/useDeleteConversation"; import useDeleteConversation from "@/_hooks/useDeleteConversation";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
@ -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

@ -1,17 +1,13 @@
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
import { showConfirmDeleteModalAtom } from "@/_helpers/JotaiWrapper";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import useSignOut from "@/_hooks/useSignOut"; import { showConfirmDeleteModalAtom } from "@/_helpers/atoms/Modal.atom";
const ConfirmDeleteModelModal: React.FC = () => { const ConfirmDeleteModelModal: React.FC = () => {
const [show, setShow] = useAtom(showConfirmDeleteModalAtom); const [show, setShow] = useAtom(showConfirmDeleteModalAtom);
const { signOut } = useSignOut();
const onLogOutClick = () => { const onConfirmDelete = () => {};
signOut().then(() => setShow(false));
};
return ( return (
<Transition.Root show={show} as={Fragment}> <Transition.Root show={show} as={Fragment}>
@ -65,7 +61,7 @@ const ConfirmDeleteModelModal: React.FC = () => {
<button <button
type="button" type="button"
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto" className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
onClick={onLogOutClick} onClick={onConfirmDelete}
> >
Log out Log out
</button> </button>

View File

@ -1,9 +1,9 @@
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
import { showConfirmSignOutModalAtom } from "@/_helpers/JotaiWrapper";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import useSignOut from "@/_hooks/useSignOut"; import useSignOut from "@/_hooks/useSignOut";
import { showConfirmSignOutModalAtom } from "@/_helpers/atoms/Modal.atom";
const ConfirmSignOutModal: React.FC = () => { const ConfirmSignOutModal: React.FC = () => {
const [show, setShow] = useAtom(showConfirmSignOutModalAtom); const [show, setShow] = useAtom(showConfirmSignOutModalAtom);

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

@ -1,33 +0,0 @@
import { ApiStep } from "../ApiStep";
const DescriptionPane: React.FC = () => {
const data = [
{
title: "Install the Node.js client:",
description: "npm install replicate",
},
{
title:
"Next, copy your API token and authenticate by setting it as an environment variable:",
description:
"export REPLICATE_API_TOKEN=r8_*************************************",
},
{
title: "lorem ipsum dolor asimet",
description: "come codes here",
},
];
return (
<div className="flex flex-col gap-4 w-[full]">
<h2 className="text-[20px] tracking-[-0.4px] leading-[25px]">
Run the model
</h2>
{data.map((item, index) => (
<ApiStep key={index} {...item} />
))}
</div>
);
};
export default DescriptionPane;

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">

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