chore: resolve conflict
158
.github/workflows/build-app.yml
vendored
Normal 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 }}
|
||||
2
.github/workflows/deploy-jan-docs.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
run: yarn install
|
||||
working-directory: docs
|
||||
- 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
|
||||
|
||||
- name: Add Custome Domain file
|
||||
|
||||
29
.github/workflows/jan-docs-test.yml
vendored
Normal 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
@ -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
|
||||
50
.github/workflows/macos-build-app.yml
vendored
@ -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
@ -5,7 +5,6 @@
|
||||
models/**
|
||||
error.log
|
||||
node_modules
|
||||
package-lock.json
|
||||
*.tgz
|
||||
yarn.lock
|
||||
dist
|
||||
|
||||
82
README.md
@ -20,64 +20,75 @@
|
||||
|
||||
> ⚠️ **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 -->
|
||||
<!-- 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. -->
|
||||
> Download Jan at https://jan.ai/
|
||||
|
||||
## Demo
|
||||
|
||||
<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>
|
||||
|
||||
_Screenshot: Jan v0.1.3 on Mac M1 Pro, 16GB Sonoma_
|
||||
|
||||
## Quicklinks
|
||||
|
||||
- Developer documentation: https://jan.ai/docs (Work in Progress)
|
||||
- Desktop app: Download at https://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 (C++ AI Engine): https://nitro.jan.ai
|
||||
- [Developer docs](https://jan.ai/docs) (WIP)
|
||||
- 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)
|
||||
- [Nitro Github](https://nitro.jan.ai): Jan's AI engine
|
||||
|
||||
## Plugins
|
||||
|
||||
Jan supports core & 3rd party extensions:
|
||||
|
||||
- [x] **LLM chat**: Self-hosted Llama2 and LLMs
|
||||
- [x] **Model Manager**: 1-click to install, swap, and delete models
|
||||
- [x] **Storage**: Optionally store your conversation history and other data in SQLite/your storage of choice
|
||||
- [x] **Model Manager**: 1-click to install, swap, and delete models with HuggingFace integration
|
||||
- [x] **Storage**: Optionally save conversation history and other data in SQLite
|
||||
- [ ] **3rd-party AIs**: Connect to ChatGPT, Claude via API Key (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)
|
||||
- [ ] **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)
|
||||
- [x] Apple M-series (accelerated)
|
||||
- [x] Linux DEB
|
||||
- [x] Windows x64
|
||||
> See the open source Nitro codebase at https://nitro.jan.ai.
|
||||
|
||||
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
|
||||
|
||||
Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
- node >= 20.0.0
|
||||
- yarn >= 1.22.0
|
||||
|
||||
### Use as complete suite (in progress)
|
||||
### For interactive development
|
||||
### Instructions
|
||||
|
||||
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 checkout feature/hackathon-refactor-jan-into-electron-app
|
||||
git checkout DESIRED_BRANCH
|
||||
cd jan
|
||||
```
|
||||
|
||||
@ -98,28 +109,29 @@ Note: This instruction is tested on MacOS only.
|
||||
yarn build:plugins
|
||||
```
|
||||
|
||||
4. **Run development and Using Jan Desktop**
|
||||
3. **Run development and Using Jan Desktop**
|
||||
|
||||
```
|
||||
yarn dev
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### For production build
|
||||
|
||||
```bash
|
||||
# Do step 1 and 2 in previous section
|
||||
git clone https://github.com/janhq/jan
|
||||
cd jan
|
||||
yarn install
|
||||
yarn build:plugins
|
||||
```bash
|
||||
# Do step 1 and 2 in previous section
|
||||
git clone https://github.com/janhq/jan
|
||||
cd jan
|
||||
yarn install
|
||||
yarn build:plugins
|
||||
|
||||
# Build the app
|
||||
yarn build
|
||||
```
|
||||
# Build the app
|
||||
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
|
||||
|
||||
|
||||
29
adr/adr-003-jan-plugins.md
Normal 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
|
||||
|
||||

|
||||
|
||||
## Consequences
|
||||
|
||||
What becomes easier or more difficult to do because of this change?
|
||||
|
||||
## Reference
|
||||
[Plugin APIs](./adr-003-jan-plugins.md)
|
||||
37
adr/docs/adr-003-plugins.md
Normal 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
|
After Width: | Height: | Size: 335 KiB |
15166
docs/package-lock.json
generated
Normal file
@ -21,7 +21,9 @@
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"axios": "^1.5.1",
|
||||
"clsx": "^1.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"postcss": "^8.4.30",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
@ -30,7 +32,8 @@
|
||||
"tailwindcss": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^2.4.3"
|
||||
"@docusaurus/module-type-aliases": "2.4.1",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@ -1,28 +1,29 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Fragment } from "react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import axios from "axios";
|
||||
|
||||
const items = [
|
||||
const systemsTemplate = [
|
||||
{
|
||||
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,
|
||||
fileFormat: "{appname}-mac-arm64-{tag}.dmg",
|
||||
},
|
||||
{
|
||||
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,
|
||||
fileFormat: "{appname}-mac-x64-{tag}.dmg",
|
||||
},
|
||||
{
|
||||
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,
|
||||
fileFormat: "{appname}-win-x64-{tag}.exe",
|
||||
},
|
||||
{
|
||||
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,
|
||||
fileFormat: "{appname}-linux-amd64-{tag}.deb",
|
||||
},
|
||||
];
|
||||
|
||||
@ -31,22 +32,81 @@ function classNames(...classes) {
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="inline-flex align-items-stretch">
|
||||
{/* TODO dynamically detect users OS through browser */}
|
||||
<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"
|
||||
href={items[0].href}
|
||||
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={defaultSystem.href}
|
||||
>
|
||||
<img
|
||||
src={require("@site/static/img/apple-logo-white.png").default}
|
||||
alt="Logo"
|
||||
className="h-5 mr-3 -mt-1"
|
||||
/>
|
||||
Download for Mac (Silicon)
|
||||
{defaultSystem.name}
|
||||
</a>
|
||||
<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>
|
||||
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
@ -59,26 +119,26 @@ export default function Dropdown() {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
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">
|
||||
{items.map((item) => (
|
||||
<Menu.Item key={item.name}>
|
||||
{systems.map((system) => (
|
||||
<Menu.Item key={system.name}>
|
||||
{({ active }) => (
|
||||
<a
|
||||
href={item.href}
|
||||
href={system.href}
|
||||
className={classNames(
|
||||
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",
|
||||
"block px-4 py-2"
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={item.logo}
|
||||
src={system.logo}
|
||||
alt="Logo"
|
||||
className="w-3 mr-3 -mt-1"
|
||||
/>
|
||||
{item.name}
|
||||
{system.name}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
LockClosedIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
|
||||
const features = [
|
||||
const systems = [
|
||||
{
|
||||
name: "Mac",
|
||||
description:
|
||||
@ -47,20 +47,20 @@ export default function HomepageDownloads() {
|
||||
</div>
|
||||
<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">
|
||||
{features.map((feature) => (
|
||||
<div key={feature.name} className="flex flex-col">
|
||||
{systems.map((system) => (
|
||||
<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">
|
||||
<feature.icon
|
||||
<system.icon
|
||||
className="h-5 w-5 flex-none text-indigo-600 dark:text-indigo-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{feature.name}
|
||||
{system.name}
|
||||
</dt>
|
||||
<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">
|
||||
<a
|
||||
href={feature.href}
|
||||
href={system.href}
|
||||
className="text-sm font-semibold leading-6 text-indigo-600 dark:text-indigo-400"
|
||||
>
|
||||
Learn more <span aria-hidden="true">→</span>
|
||||
|
||||
@ -8,7 +8,7 @@ export default function HomepageHero() {
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
{colorMode === "dark" ? (
|
||||
<div
|
||||
@ -39,7 +39,7 @@ export default function HomepageHero() {
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
{/* Hero text and buttons */}
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
@ -60,7 +60,7 @@ export default function HomepageHero() {
|
||||
<Dropdown />
|
||||
<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={() =>
|
||||
window.open(
|
||||
"https://github.com/janhq/jan",
|
||||
@ -79,8 +79,10 @@ export default function HomepageHero() {
|
||||
src={
|
||||
colorMode === "dark"
|
||||
? // TODO replace with darkmode image
|
||||
require("@site/static/img/desktop-llm-chat.png").default
|
||||
: require("@site/static/img/desktop-llm-chat.png").default
|
||||
require("@site/static/img/desktop-llm-chat-dark.png")
|
||||
.default
|
||||
: require("@site/static/img/desktop-llm-chat-light.png")
|
||||
.default
|
||||
}
|
||||
alt="App screenshot"
|
||||
width={2432}
|
||||
|
||||
@ -58,7 +58,7 @@ export default function HomepageSectionOne() {
|
||||
/>
|
||||
{feature.name}
|
||||
</dt>{" "}
|
||||
<dd className="inline">{feature.description}</dd>
|
||||
<dt>{feature.description}</dt>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
@ -71,7 +71,7 @@ export default function HomepageSectionOne() {
|
||||
? // TODO replace with darkmode image
|
||||
require("@site/static/img/desktop-explore-models-dark.png")
|
||||
.default
|
||||
: require("@site/static/img/desktop-explore-models.png")
|
||||
: require("@site/static/img/desktop-explore-models-light.png")
|
||||
.default
|
||||
}
|
||||
alt="Product screenshot"
|
||||
|
||||
@ -58,7 +58,7 @@ export default function sectionTwo() {
|
||||
/>
|
||||
{feature.name}
|
||||
</dt>{" "}
|
||||
<dd className="inline">{feature.description}</dd>
|
||||
<dt>{feature.description}</dt>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
@ -68,8 +68,8 @@ export default function sectionTwo() {
|
||||
src={
|
||||
colorMode === "dark"
|
||||
? // TODO replace with darkmode image
|
||||
require("@site/static/img/desktop-model-settings.png").default
|
||||
: 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-light.png").default
|
||||
}
|
||||
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"
|
||||
|
||||
@ -12,26 +12,35 @@
|
||||
Full list of Infima variables: https://github.com/facebook/docusaurus/issues/3955#issuecomment-1521944593
|
||||
*/
|
||||
:root {
|
||||
--ifm-color-primary: #2e8555;
|
||||
--ifm-color-primary-dark: #29784c;
|
||||
--ifm-color-primary-darker: #277148;
|
||||
--ifm-color-primary-darkest: #205d3b;
|
||||
--ifm-color-primary-light: #33925d;
|
||||
--ifm-color-primary-lighter: #359962;
|
||||
--ifm-color-primary-lightest: #3cad6e;
|
||||
--ifm-color-primary: #2563EB; /* New Primary Blue */
|
||||
--ifm-color-primary-dark: #204FCF; /* Darker Blue */
|
||||
--ifm-color-primary-darker: #1B45B7; /* Even Darker Blue */
|
||||
--ifm-color-primary-darkest: #163C9D; /* Darkest Blue */
|
||||
--ifm-color-primary-light: #2974FF; /* Light Blue */
|
||||
--ifm-color-primary-lighter: #3280FF; /* Lighter Blue */
|
||||
--ifm-color-primary-lightest: #3A8BFF; /* Lightest Blue */
|
||||
--ifm-code-font-size: 95%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme="dark"] {
|
||||
--ifm-color-primary: #25c2a0;
|
||||
--ifm-color-primary-dark: #21af90;
|
||||
--ifm-color-primary-darker: #1fa588;
|
||||
--ifm-color-primary-darkest: #1a8870;
|
||||
--ifm-color-primary-light: #29d5b0;
|
||||
--ifm-color-primary-lighter: #32d8b4;
|
||||
--ifm-color-primary-lightest: #4fddbf;
|
||||
--ifm-navbar-background-color: rgba(15, 23, 42);
|
||||
--ifm-color-primary: #ffffff; /* New Primary Blue */
|
||||
--ifm-color-primary-dark: #204FCF; /* Darker Blue */
|
||||
--ifm-color-primary-darker: #1B45B7; /* Even Darker Blue */
|
||||
--ifm-color-primary-darkest: #163C9D; /* Darkest Blue */
|
||||
--ifm-color-primary-light: #2974FF; /* Light Blue */
|
||||
--ifm-color-primary-lighter: #3280FF; /* Lighter Blue */
|
||||
--ifm-color-primary-lightest: #3A8BFF; /* Lightest Blue */
|
||||
--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)
|
||||
}
|
||||
BIN
docs/static/img/desktop-explore-models-dark.png
vendored
|
Before Width: | Height: | Size: 389 KiB After Width: | Height: | Size: 434 KiB |
BIN
docs/static/img/desktop-explore-models-light.png
vendored
Normal file
|
After Width: | Height: | Size: 435 KiB |
BIN
docs/static/img/desktop-explore-models.png
vendored
|
Before Width: | Height: | Size: 379 KiB After Width: | Height: | Size: 129 KiB |
BIN
docs/static/img/desktop-llm-chat-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
docs/static/img/desktop-llm-chat-light.png
vendored
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
docs/static/img/desktop-model-settings-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
docs/static/img/desktop-model-settings-light.png
vendored
Normal file
|
After Width: | Height: | Size: 192 KiB |
@ -9,5 +9,7 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
require("tailwindcss-animate"),
|
||||
],
|
||||
};
|
||||
|
||||
2731
docs/yarn.lock
44
electron/.eslintrc.js
Normal 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
@ -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 {} \;
|
||||
@ -16,11 +16,23 @@ class Plugin {
|
||||
/** @type {boolean} Whether this plugin should be activated when its activation points are triggered. */
|
||||
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.url = url
|
||||
this.activationPoints = activationPoints
|
||||
this.active = active
|
||||
this.description = description
|
||||
this.version = version
|
||||
this.icon = icon
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -25,6 +25,7 @@ export async function install(plugins) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.install(plugins);
|
||||
if (plgList.cancelled) return false;
|
||||
return plgList.map((plg) => {
|
||||
@ -50,6 +51,7 @@ export function uninstall(plugins, reload = true) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
return window.pluggableElectronIpc.uninstall(plugins, reload);
|
||||
}
|
||||
|
||||
@ -62,6 +64,7 @@ export async function getActive() {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.getActive();
|
||||
return plgList.map(
|
||||
(plugin) =>
|
||||
@ -69,7 +72,10 @@ export async function getActive() {
|
||||
plugin.name,
|
||||
plugin.url,
|
||||
plugin.activationPoints,
|
||||
plugin.active
|
||||
plugin.active,
|
||||
plugin.description,
|
||||
plugin.version,
|
||||
plugin.icon
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -83,6 +89,7 @@ export async function registerActive() {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.getActive();
|
||||
plgList.forEach((plugin) =>
|
||||
register(
|
||||
@ -107,6 +114,7 @@ export async function update(plugins, reload = true) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.update(plugins, reload);
|
||||
return plgList.map(
|
||||
(plugin) =>
|
||||
@ -129,6 +137,7 @@ export function updatesAvailable(plugin) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
return window.pluggableElectronIpc.updatesAvailable(plugin);
|
||||
}
|
||||
|
||||
@ -143,6 +152,7 @@ export async function toggleActive(plugin, active) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plg = await window.pluggableElectronIpc.toggleActive(plugin, active);
|
||||
return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ export * as activationPoints from "./activation-manager.js";
|
||||
export * as plugins from "./facade.js";
|
||||
export { default as ExtensionPoint } from "./ExtensionPoint.js";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
if (typeof window !== "undefined" && !window.pluggableElectronIpc)
|
||||
console.warn(
|
||||
"Facade is not registered in preload. Facade functions will throw an error if used."
|
||||
|
||||
@ -1,30 +1,32 @@
|
||||
import { ipcRenderer, contextBridge } from "electron"
|
||||
const { ipcRenderer, contextBridge } = require("electron");
|
||||
|
||||
export default function useFacade() {
|
||||
function useFacade() {
|
||||
const interfaces = {
|
||||
install(plugins) {
|
||||
return ipcRenderer.invoke('pluggable:install', plugins)
|
||||
return ipcRenderer.invoke("pluggable:install", plugins);
|
||||
},
|
||||
uninstall(plugins, reload) {
|
||||
return ipcRenderer.invoke('pluggable:uninstall', plugins, reload)
|
||||
return ipcRenderer.invoke("pluggable:uninstall", plugins, reload);
|
||||
},
|
||||
getActive() {
|
||||
return ipcRenderer.invoke('pluggable:getActivePlugins')
|
||||
return ipcRenderer.invoke("pluggable:getActivePlugins");
|
||||
},
|
||||
update(plugins, reload) {
|
||||
return ipcRenderer.invoke('pluggable:update', plugins, reload)
|
||||
return ipcRenderer.invoke("pluggable:update", plugins, reload);
|
||||
},
|
||||
updatesAvailable(plugin) {
|
||||
return ipcRenderer.invoke('pluggable:updatesAvailable', plugin)
|
||||
return ipcRenderer.invoke("pluggable:updatesAvailable", plugin);
|
||||
},
|
||||
toggleActive(plugin, active) {
|
||||
return ipcRenderer.invoke('pluggable:togglePluginActive', plugin, active)
|
||||
return ipcRenderer.invoke("pluggable:togglePluginActive", plugin, active);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if (contextBridge) {
|
||||
contextBridge.exposeInMainWorld('pluggableElectronIpc', interfaces)
|
||||
contextBridge.exposeInMainWorld("pluggableElectronIpc", interfaces);
|
||||
}
|
||||
|
||||
return interfaces
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
module.exports = useFacade;
|
||||
|
||||
@ -18,6 +18,8 @@ class Plugin {
|
||||
* @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 {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 */
|
||||
@ -75,6 +77,8 @@ class Plugin {
|
||||
this.version = mnf.version
|
||||
this.activationPoints = mnf.activationPoints || null
|
||||
this.main = mnf.main
|
||||
this.description = mnf.description
|
||||
this.icon = mnf.icon
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`)
|
||||
|
||||
@ -61,10 +61,6 @@ function init() {
|
||||
);
|
||||
});
|
||||
|
||||
const stmt = db.prepare(
|
||||
"INSERT INTO conversations (name, model_id, image, message) VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
stmt.finalize();
|
||||
db.close();
|
||||
}
|
||||
|
||||
@ -168,7 +164,7 @@ function getFinishedDownloadModels() {
|
||||
|
||||
const query = `SELECT * FROM models WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`;
|
||||
db.all(query, (err: Error, row: any) => {
|
||||
res(row);
|
||||
res(row?.map((item: any) => parseToProduct(item)) ?? []);
|
||||
});
|
||||
db.close();
|
||||
});
|
||||
@ -184,6 +180,7 @@ function deleteDownloadModel(modelId: string) {
|
||||
const stmt = db.prepare("DELETE FROM models WHERE id = ?");
|
||||
stmt.run(modelId);
|
||||
stmt.finalize();
|
||||
res(modelId);
|
||||
});
|
||||
|
||||
db.close();
|
||||
@ -352,7 +349,7 @@ function deleteConversation(id: any) {
|
||||
);
|
||||
deleteMessages.run(id);
|
||||
deleteMessages.finalize();
|
||||
res([]);
|
||||
res(id);
|
||||
});
|
||||
|
||||
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 = {
|
||||
init,
|
||||
getConversations,
|
||||
|
||||
5606
electron/core/plugins/data-plugin/package-lock.json
generated
Normal file
@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "data-plugin",
|
||||
"version": "2.1.0",
|
||||
"description": "",
|
||||
"version": "1.0.0",
|
||||
"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",
|
||||
"author": "Jan",
|
||||
"license": "MIT",
|
||||
@ -10,8 +11,8 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"build:package": "rimraf ./data-plugin*.tgz && npm run build && npm pack",
|
||||
"build:publish": "npm run build:package && cpx *.tgz ../../pre-install"
|
||||
"postinstall": "rimraf ./data-plugin*.tgz && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=arm64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=glibc --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=musl --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_libc=unknown --target_arch=x64 && npm run build",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
@ -36,6 +37,7 @@
|
||||
"node_modules"
|
||||
],
|
||||
"dependencies": {
|
||||
"node-pre-gyp": "^0.17.0",
|
||||
"sqlite3": "^5.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,15 +13,20 @@ const dispose = async () =>
|
||||
new Promise(async (resolve) => {
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI
|
||||
.invokePluginFunc(MODULE_PATH, "killSubprocess")
|
||||
.invokePluginFunc(MODULE_PATH, "dispose")
|
||||
.then((res) => resolve(res));
|
||||
}
|
||||
});
|
||||
const inferenceUrl = () => "http://localhost:8080/llama/chat_completion";
|
||||
const inferenceUrl = () => "http://localhost:3928/llama/chat_completion";
|
||||
|
||||
const stopModel = () => {
|
||||
window.electronAPI.invokePluginFunc(MODULE_PATH, "killSubprocess");
|
||||
};
|
||||
|
||||
// Register all the above functions and objects with the relevant extension points
|
||||
export function init({ register }) {
|
||||
register("initModel", "initModel", initModel);
|
||||
register("inferenceUrl", "inferenceUrl", inferenceUrl);
|
||||
register("dispose", "dispose", dispose);
|
||||
register("stopModel", "stopModel", stopModel);
|
||||
}
|
||||
|
||||
@ -5,14 +5,6 @@ const fs = require("fs");
|
||||
|
||||
let subprocess = null;
|
||||
|
||||
process.on("exit", () => {
|
||||
// Perform cleanup tasks here
|
||||
console.log("kill subprocess on exit");
|
||||
if (subprocess) {
|
||||
subprocess.kill();
|
||||
}
|
||||
});
|
||||
|
||||
async function initModel(product) {
|
||||
// fileName fallback
|
||||
if (!product.fileName) {
|
||||
@ -31,13 +23,13 @@ async function initModel(product) {
|
||||
console.error(
|
||||
"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
|
||||
const configFilePath = `${binaryFolder}/config/config.json`;
|
||||
const configFilePath = path.join(binaryFolder, "config", "config.json");
|
||||
let config = {};
|
||||
if (fs.existsSync(configFilePath)) {
|
||||
const rawData = fs.readFileSync(configFilePath, "utf-8");
|
||||
@ -56,8 +48,22 @@ async function initModel(product) {
|
||||
// Write the updated config back to the file
|
||||
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
|
||||
subprocess = spawn(`${binaryFolder}/nitro`, [configFilePath]);
|
||||
|
||||
subprocess = spawn(binaryPath, [configFilePath], { cwd: binaryFolder });
|
||||
|
||||
// Handle subprocess output
|
||||
subprocess.stdout.on("data", (data) => {
|
||||
@ -74,6 +80,11 @@ async function initModel(product) {
|
||||
});
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
killSubprocess();
|
||||
// clean other registered resources here
|
||||
}
|
||||
|
||||
function killSubprocess() {
|
||||
if (subprocess) {
|
||||
subprocess.kill();
|
||||
@ -87,4 +98,5 @@ function killSubprocess() {
|
||||
module.exports = {
|
||||
initModel,
|
||||
killSubprocess,
|
||||
dispose,
|
||||
};
|
||||
|
||||
@ -1 +1,13 @@
|
||||
{"custom_config": {"llama_model_path":"","ctx_len":2048,"ngl":100}}
|
||||
{
|
||||
"listeners": [
|
||||
{
|
||||
"address": "0.0.0.0",
|
||||
"port": 3928
|
||||
}
|
||||
],
|
||||
"custom_config": {
|
||||
"llama_model_path": "",
|
||||
"ctx_len": 2048,
|
||||
"ngl": 100
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,12 +24,59 @@ typedef struct {
|
||||
int8_t qs[QK8_0]; // quants
|
||||
} block_q8_0;
|
||||
|
||||
// general-purpose kernel for addition of two tensors
|
||||
// pros: works for non-contiguous tensors, supports broadcast across dims 1, 2 and 3
|
||||
// cons: not very efficient
|
||||
kernel void kernel_add(
|
||||
device const float4 * src0,
|
||||
device const float4 * src1,
|
||||
device float4 * dst,
|
||||
uint tpig[[thread_position_in_grid]]) {
|
||||
dst[tpig] = src0[tpig] + src1[tpig];
|
||||
device const char * src0,
|
||||
device const char * src1,
|
||||
device char * dst,
|
||||
constant int64_t & ne00,
|
||||
constant int64_t & ne01,
|
||||
constant int64_t & ne02,
|
||||
constant int64_t & ne03,
|
||||
constant int64_t & nb00,
|
||||
constant int64_t & nb01,
|
||||
constant int64_t & nb02,
|
||||
constant int64_t & nb03,
|
||||
constant int64_t & ne10,
|
||||
constant int64_t & ne11,
|
||||
constant int64_t & ne12,
|
||||
constant int64_t & ne13,
|
||||
constant int64_t & nb10,
|
||||
constant int64_t & nb11,
|
||||
constant int64_t & nb12,
|
||||
constant int64_t & nb13,
|
||||
constant int64_t & ne0,
|
||||
constant int64_t & ne1,
|
||||
constant int64_t & ne2,
|
||||
constant int64_t & ne3,
|
||||
constant int64_t & nb0,
|
||||
constant int64_t & nb1,
|
||||
constant int64_t & nb2,
|
||||
constant int64_t & nb3,
|
||||
uint3 tgpig[[threadgroup_position_in_grid]],
|
||||
uint3 tpitg[[thread_position_in_threadgroup]],
|
||||
uint3 ntg[[threads_per_threadgroup]]) {
|
||||
const int64_t i03 = tgpig.z;
|
||||
const int64_t i02 = tgpig.y;
|
||||
const int64_t i01 = tgpig.x;
|
||||
|
||||
const int64_t i13 = i03 % ne13;
|
||||
const int64_t i12 = i02 % ne12;
|
||||
const int64_t i11 = i01 % ne11;
|
||||
|
||||
device const char * src0_ptr = src0 + i03*nb03 + i02*nb02 + i01*nb01 + tpitg.x*nb00;
|
||||
device const char * src1_ptr = src1 + i13*nb13 + i12*nb12 + i11*nb11 + tpitg.x*nb10;
|
||||
device char * dst_ptr = dst + i03*nb3 + i02*nb2 + i01*nb1 + tpitg.x*nb0;
|
||||
|
||||
for (int i0 = tpitg.x; i0 < ne0; i0 += ntg.x) {
|
||||
((device float *)dst_ptr)[0] = ((device float *)src0_ptr)[0] + ((device float *)src1_ptr)[0];
|
||||
|
||||
src0_ptr += ntg.x*nb00;
|
||||
src1_ptr += ntg.x*nb10;
|
||||
dst_ptr += ntg.x*nb0;
|
||||
}
|
||||
}
|
||||
|
||||
// assumption: src1 is a row
|
||||
@ -38,7 +85,7 @@ kernel void kernel_add_row(
|
||||
device const float4 * src0,
|
||||
device const float4 * src1,
|
||||
device float4 * dst,
|
||||
constant int64_t & nb,
|
||||
constant int64_t & nb [[buffer(27)]],
|
||||
uint tpig[[thread_position_in_grid]]) {
|
||||
dst[tpig] = src0[tpig] + src1[tpig % nb];
|
||||
}
|
||||
@ -783,7 +830,9 @@ kernel void kernel_alibi_f32(
|
||||
constant uint64_t & nb1,
|
||||
constant uint64_t & nb2,
|
||||
constant uint64_t & nb3,
|
||||
constant float & m0,
|
||||
constant float & m0,
|
||||
constant float & m1,
|
||||
constant int & n_heads_log2_floor,
|
||||
uint3 tgpig[[threadgroup_position_in_grid]],
|
||||
uint3 tpitg[[thread_position_in_threadgroup]],
|
||||
uint3 ntg[[threads_per_threadgroup]]) {
|
||||
@ -799,37 +848,73 @@ kernel void kernel_alibi_f32(
|
||||
const int64_t i0 = (n - i3*ne2*ne1*ne0 - i2*ne1*ne0 - i1*ne0);
|
||||
|
||||
device float * dst_data = (device float *) ((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||
float m_k = pow(m0, i2 + 1);
|
||||
float m_k;
|
||||
if (i2 < n_heads_log2_floor) {
|
||||
m_k = pow(m0, i2 + 1);
|
||||
} else {
|
||||
m_k = pow(m1, 2 * (i2 - n_heads_log2_floor) + 1);
|
||||
}
|
||||
for (int64_t i00 = tpitg.x; i00 < ne00; i00 += ntg.x) {
|
||||
device const float * src = (device float *)((device char *) src0 + i03*nb03 + i02*nb02 + i01*nb01 + i00*nb00);
|
||||
dst_data[i00] = src[0] + m_k * (i00 - ne00 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
typedef void (rope_t)(
|
||||
device const void * src0,
|
||||
device const int32_t * src1,
|
||||
device float * dst,
|
||||
constant int64_t & ne00,
|
||||
constant int64_t & ne01,
|
||||
constant int64_t & ne02,
|
||||
constant int64_t & ne03,
|
||||
constant uint64_t & nb00,
|
||||
constant uint64_t & nb01,
|
||||
constant uint64_t & nb02,
|
||||
constant uint64_t & nb03,
|
||||
constant int64_t & ne0,
|
||||
constant int64_t & ne1,
|
||||
constant int64_t & ne2,
|
||||
constant int64_t & ne3,
|
||||
constant uint64_t & nb0,
|
||||
constant uint64_t & nb1,
|
||||
constant uint64_t & nb2,
|
||||
constant uint64_t & nb3,
|
||||
constant int & n_past,
|
||||
constant int & n_dims,
|
||||
constant int & mode,
|
||||
constant float & freq_base,
|
||||
constant float & freq_scale,
|
||||
uint tiitg[[thread_index_in_threadgroup]],
|
||||
uint3 tptg[[threads_per_threadgroup]],
|
||||
uint3 tgpig[[threadgroup_position_in_grid]]);
|
||||
|
||||
template<typename T>
|
||||
kernel void kernel_rope(
|
||||
device const void * src0,
|
||||
device float * dst,
|
||||
constant int64_t & ne00,
|
||||
constant int64_t & ne01,
|
||||
constant int64_t & ne02,
|
||||
constant int64_t & ne03,
|
||||
constant uint64_t & nb00,
|
||||
constant uint64_t & nb01,
|
||||
constant uint64_t & nb02,
|
||||
constant uint64_t & nb03,
|
||||
constant int64_t & ne0,
|
||||
constant int64_t & ne1,
|
||||
constant int64_t & ne2,
|
||||
constant int64_t & ne3,
|
||||
constant uint64_t & nb0,
|
||||
constant uint64_t & nb1,
|
||||
constant uint64_t & nb2,
|
||||
constant uint64_t & nb3,
|
||||
constant int & n_past,
|
||||
constant int & n_dims,
|
||||
constant int & mode,
|
||||
constant float & freq_base,
|
||||
constant float & freq_scale,
|
||||
device const void * src0,
|
||||
device const int32_t * src1,
|
||||
device float * dst,
|
||||
constant int64_t & ne00,
|
||||
constant int64_t & ne01,
|
||||
constant int64_t & ne02,
|
||||
constant int64_t & ne03,
|
||||
constant uint64_t & nb00,
|
||||
constant uint64_t & nb01,
|
||||
constant uint64_t & nb02,
|
||||
constant uint64_t & nb03,
|
||||
constant int64_t & ne0,
|
||||
constant int64_t & ne1,
|
||||
constant int64_t & ne2,
|
||||
constant int64_t & ne3,
|
||||
constant uint64_t & nb0,
|
||||
constant uint64_t & nb1,
|
||||
constant uint64_t & nb2,
|
||||
constant uint64_t & nb3,
|
||||
constant int & n_past,
|
||||
constant int & n_dims,
|
||||
constant int & mode,
|
||||
constant float & freq_base,
|
||||
constant float & freq_scale,
|
||||
uint tiitg[[thread_index_in_threadgroup]],
|
||||
uint3 tptg[[threads_per_threadgroup]],
|
||||
uint3 tgpig[[threadgroup_position_in_grid]]) {
|
||||
@ -839,7 +924,9 @@ kernel void kernel_rope(
|
||||
|
||||
const bool is_neox = mode & 2;
|
||||
|
||||
const int64_t p = ((mode & 1) == 0 ? n_past + i2 : i2);
|
||||
device const int32_t * pos = src1;
|
||||
|
||||
const int64_t p = pos[i2];
|
||||
|
||||
const float theta_0 = freq_scale * (float)p;
|
||||
const float inv_ndims = -1.f/n_dims;
|
||||
@ -851,11 +938,11 @@ kernel void kernel_rope(
|
||||
const float cos_theta = cos(theta);
|
||||
const float sin_theta = sin(theta);
|
||||
|
||||
device const float * const src = (device float *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
||||
device float * dst_data = (device float *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||
device const T * const src = (device T *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
||||
device T * dst_data = (device T *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||
|
||||
const float x0 = src[0];
|
||||
const float x1 = src[1];
|
||||
const T x0 = src[0];
|
||||
const T x1 = src[1];
|
||||
|
||||
dst_data[0] = x0*cos_theta - x1*sin_theta;
|
||||
dst_data[1] = x0*sin_theta + x1*cos_theta;
|
||||
@ -870,8 +957,8 @@ kernel void kernel_rope(
|
||||
|
||||
const int64_t i0 = ib*n_dims + ic/2;
|
||||
|
||||
device const float * const src = (device float *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
||||
device float * dst_data = (device float *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||
device const T * const src = (device T *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
||||
device T * dst_data = (device T *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||
|
||||
const float x0 = src[0];
|
||||
const float x1 = src[n_dims/2];
|
||||
@ -883,6 +970,9 @@ kernel void kernel_rope(
|
||||
}
|
||||
}
|
||||
|
||||
template [[host_name("kernel_rope_f32")]] kernel rope_t kernel_rope<float>;
|
||||
template [[host_name("kernel_rope_f16")]] kernel rope_t kernel_rope<half>;
|
||||
|
||||
kernel void kernel_cpy_f16_f16(
|
||||
device const half * src0,
|
||||
device half * dst,
|
||||
@ -1273,8 +1363,8 @@ kernel void kernel_mul_mat_q3_K_f32(
|
||||
|
||||
float yl[32];
|
||||
|
||||
const uint16_t kmask1 = 0x3030;
|
||||
const uint16_t kmask2 = 0x0f0f;
|
||||
//const uint16_t kmask1 = 0x3030;
|
||||
//const uint16_t kmask2 = 0x0f0f;
|
||||
|
||||
const int tid = tiisg/4;
|
||||
const int ix = tiisg%4;
|
||||
@ -2350,4 +2440,4 @@ template [[host_name("kernel_mul_mm_q2_K_f32")]] kernel mat_mm_t kernel_mul_mm<b
|
||||
template [[host_name("kernel_mul_mm_q3_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q3_K, QK_NL, dequantize_q3_K>;
|
||||
template [[host_name("kernel_mul_mm_q4_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q4_K, QK_NL, dequantize_q4_K>;
|
||||
template [[host_name("kernel_mul_mm_q5_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q5_K, QK_NL, dequantize_q5_K>;
|
||||
template [[host_name("kernel_mul_mm_q6_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q6_K, QK_NL, dequantize_q6_K>;
|
||||
template [[host_name("kernel_mul_mm_q6_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q6_K, QK_NL, dequantize_q6_K>;
|
||||
BIN
electron/core/plugins/inference-plugin/nitro/nitro_linux_amd64_cuda
Executable file
BIN
electron/core/plugins/inference-plugin/nitro/nitro_mac_amd64
Executable file
BIN
electron/core/plugins/inference-plugin/nitro/zlib.dll
Normal file
3674
electron/core/plugins/inference-plugin/package-lock.json
generated
Normal file
@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "inference-plugin",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"version": "1.0.0",
|
||||
"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",
|
||||
"author": "James",
|
||||
"author": "Jan",
|
||||
"license": "MIT",
|
||||
"activationPoints": [
|
||||
"init"
|
||||
],
|
||||
"scripts": {
|
||||
"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",
|
||||
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
||||
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
@ -24,8 +25,7 @@
|
||||
"node-llama-cpp"
|
||||
],
|
||||
"dependencies": {
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"node-llama-cpp": "^2.4.1"
|
||||
"electron-is-dev": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@ -38,10 +38,20 @@ const deleteModel = async (path) =>
|
||||
}
|
||||
});
|
||||
|
||||
const searchModels = async (params) =>
|
||||
new Promise(async (resolve) => {
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI
|
||||
.invokePluginFunc(MODULE_PATH, "searchModels", params)
|
||||
.then((res) => resolve(res));
|
||||
}
|
||||
});
|
||||
|
||||
// Register all the above functions and objects with the relevant extension points
|
||||
export function init({ register }) {
|
||||
register("getDownloadedModels", "getDownloadedModels", getDownloadedModels);
|
||||
register("getAvailableModels", "getAvailableModels", getAvailableModels);
|
||||
register("downloadModel", "downloadModel", downloadModel);
|
||||
register("deleteModel", "deleteModel", deleteModel);
|
||||
register("searchModels", "searchModels", searchModels);
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
const path = require("path");
|
||||
const { readdirSync, lstatSync } = require("fs");
|
||||
const { app } = require("electron");
|
||||
const { listModels, listFiles, fileDownloadInfo } = require("@huggingface/hub");
|
||||
|
||||
let modelsIterator = undefined;
|
||||
let currentSearchOwner = undefined;
|
||||
|
||||
const ALL_MODELS = [
|
||||
{
|
||||
@ -87,6 +91,76 @@ function getDownloadedModels() {
|
||||
return downloadedModels;
|
||||
}
|
||||
|
||||
const getNextModels = async (count) => {
|
||||
const models = [];
|
||||
let hasMore = true;
|
||||
|
||||
while (models.length < count) {
|
||||
const next = await modelsIterator.next();
|
||||
|
||||
// end if we reached the end
|
||||
if (next.done) {
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const model = next.value;
|
||||
const files = await listFilesByName(model.name);
|
||||
|
||||
models.push({
|
||||
...model,
|
||||
files,
|
||||
});
|
||||
}
|
||||
|
||||
const result = {
|
||||
data: models,
|
||||
hasMore,
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
const searchModels = async (params) => {
|
||||
if (currentSearchOwner === params.search.owner && modelsIterator != null) {
|
||||
// paginated search
|
||||
console.debug(`Paginated search owner: ${params.search.owner}`);
|
||||
const models = await getNextModels(params.limit);
|
||||
return models;
|
||||
} else {
|
||||
// new search
|
||||
console.debug(`Init new search owner: ${params.search.owner}`);
|
||||
currentSearchOwner = params.search.owner;
|
||||
modelsIterator = listModels({
|
||||
search: params.search,
|
||||
credentials: params.credentials,
|
||||
});
|
||||
|
||||
const models = await getNextModels(params.limit);
|
||||
return models;
|
||||
}
|
||||
};
|
||||
|
||||
const listFilesByName = async (modelName) => {
|
||||
const repo = { type: "model", name: modelName };
|
||||
const fileDownloadInfoMap = {};
|
||||
for await (const file of listFiles({
|
||||
repo: repo,
|
||||
})) {
|
||||
if (file.type === "file" && file.path.endsWith(".bin")) {
|
||||
const downloadInfo = await fileDownloadInfo({
|
||||
repo: repo,
|
||||
path: file.path,
|
||||
});
|
||||
fileDownloadInfoMap[file.path] = {
|
||||
...file,
|
||||
...downloadInfo,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return fileDownloadInfoMap;
|
||||
};
|
||||
|
||||
function getAvailableModels() {
|
||||
const downloadedModelIds = getDownloadedModels().map((model) => model.id);
|
||||
return ALL_MODELS.filter((model) => {
|
||||
@ -99,4 +173,5 @@ function getAvailableModels() {
|
||||
module.exports = {
|
||||
getDownloadedModels,
|
||||
getAvailableModels,
|
||||
searchModels,
|
||||
};
|
||||
|
||||
3607
electron/core/plugins/model-management-plugin/package-lock.json
generated
Normal file
@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "model-management-plugin",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"version": "1.0.0",
|
||||
"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",
|
||||
"author": "James",
|
||||
"license": "MIT",
|
||||
@ -10,8 +11,8 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
|
||||
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
||||
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\"",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
@ -23,5 +24,11 @@
|
||||
"dist/*",
|
||||
"package.json",
|
||||
"README.md"
|
||||
],
|
||||
"dependencies": {
|
||||
"@huggingface/hub": "^0.8.5"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"@huggingface/hub"
|
||||
]
|
||||
}
|
||||
|
||||
1479
electron/core/plugins/monitoring-plugin/package-lock.json
generated
Normal file
@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "monitoring-plugin",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"version": "1.0.0",
|
||||
"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",
|
||||
"author": "Jan",
|
||||
"license": "MIT",
|
||||
@ -10,8 +11,8 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
|
||||
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
||||
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\"",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^3.0.2",
|
||||
|
||||
14
electron/entitlements.mac.plist
Normal file
@ -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>
|
||||
331
electron/main.ts
@ -1,30 +1,41 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
screen as electronScreen,
|
||||
ipcMain,
|
||||
dialog,
|
||||
shell,
|
||||
} from "electron";
|
||||
import { app, BrowserWindow, ipcMain, dialog, shell } from "electron";
|
||||
import { readdirSync } from "fs";
|
||||
import { resolve, join, extname } from "path";
|
||||
import { rmdir, unlink, createWriteStream } from "fs";
|
||||
import isDev = require("electron-is-dev");
|
||||
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 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;
|
||||
const store = new Store();
|
||||
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
app
|
||||
.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({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
@ -37,29 +48,9 @@ const createMainWindow = () => {
|
||||
},
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"invokePluginFunc",
|
||||
async (_event, modulePath, method, ...args) => {
|
||||
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")}`;
|
||||
const startURL = app.isPackaged
|
||||
? `file://${join(__dirname, "../renderer/index.html")}`
|
||||
: "http://localhost:3000";
|
||||
|
||||
mainWindow.loadURL(startURL);
|
||||
|
||||
@ -68,140 +59,162 @@ const createMainWindow = () => {
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
|
||||
if (isDev) mainWindow.webContents.openDevTools();
|
||||
};
|
||||
if (!app.isPackaged) mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(migratePlugins)
|
||||
.then(() => {
|
||||
createMainWindow();
|
||||
setupPlugins();
|
||||
autoUpdater.checkForUpdates();
|
||||
|
||||
ipcMain.handle("basePlugins", async (event) => {
|
||||
const basePluginPath = join(
|
||||
__dirname,
|
||||
"../",
|
||||
isDev ? "/core/pre-install" : "../app.asar.unpacked/core/pre-install"
|
||||
);
|
||||
return readdirSync(basePluginPath)
|
||||
.filter((file) => extname(file) === ".tgz")
|
||||
.map((file) => join(basePluginPath, file));
|
||||
function handleAppUpdates() {
|
||||
/*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();
|
||||
});
|
||||
|
||||
ipcMain.handle("pluginPath", async (event) => {
|
||||
return join(app.getPath("userData"), "plugins");
|
||||
/*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"],
|
||||
});
|
||||
ipcMain.handle("appVersion", async (event) => {
|
||||
return app.getVersion();
|
||||
});
|
||||
ipcMain.handle("openExternalUrl", async (event, url) => {
|
||||
shell.openExternal(url);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to delete a file from the user data folder
|
||||
*/
|
||||
ipcMain.handle("deleteFile", async (_event, filePath) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const fullPath = join(userDataPath, filePath);
|
||||
function handleIPCs() {
|
||||
ipcMain.handle(
|
||||
"invokePluginFunc",
|
||||
async (_event, modulePath, method, ...args) => {
|
||||
const module = require(/* webpackIgnore: true */ join(
|
||||
app.getPath("userData"),
|
||||
"plugins",
|
||||
modulePath
|
||||
));
|
||||
requiredModules[modulePath] = module;
|
||||
|
||||
let result = "NULL";
|
||||
unlink(fullPath, function (err) {
|
||||
if (err && err.code == "ENOENT") {
|
||||
result = `File not exist: ${err}`;
|
||||
} else if (err) {
|
||||
result = `File delete error: ${err}`;
|
||||
} else {
|
||||
result = "File deleted successfully";
|
||||
}
|
||||
console.log(
|
||||
`Delete file ${filePath} from ${fullPath} result: ${result}`
|
||||
);
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Used to download a file from a given url
|
||||
*/
|
||||
ipcMain.handle("downloadFile", async (_event, url, fileName) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const destination = resolve(userDataPath, fileName);
|
||||
|
||||
progress(request(url), {})
|
||||
.on("progress", function (state: any) {
|
||||
mainWindow?.webContents.send("FILE_DOWNLOAD_UPDATE", {
|
||||
...state,
|
||||
fileName,
|
||||
});
|
||||
})
|
||||
.on("error", function (err: Error) {
|
||||
mainWindow?.webContents.send("FILE_DOWNLOAD_ERROR", {
|
||||
fileName,
|
||||
err,
|
||||
});
|
||||
})
|
||||
.on("end", function () {
|
||||
mainWindow?.webContents.send("FILE_DOWNLOAD_COMPLETE", {
|
||||
fileName,
|
||||
});
|
||||
})
|
||||
.pipe(createWriteStream(destination));
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (!BrowserWindow.getAllWindows().length) {
|
||||
createMainWindow();
|
||||
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(
|
||||
__dirname,
|
||||
"../",
|
||||
app.isPackaged
|
||||
? "../app.asar.unpacked/core/pre-install"
|
||||
: "/core/pre-install"
|
||||
);
|
||||
return readdirSync(basePluginPath)
|
||||
.filter((file) => extname(file) === ".tgz")
|
||||
.map((file) => join(basePluginPath, file));
|
||||
});
|
||||
|
||||
ipcMain.handle("pluginPath", async (_event) => {
|
||||
return join(app.getPath("userData"), "plugins");
|
||||
});
|
||||
ipcMain.handle("appVersion", async (_event) => {
|
||||
return app.getVersion();
|
||||
});
|
||||
ipcMain.handle("openExternalUrl", async (_event, 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();
|
||||
});
|
||||
});
|
||||
|
||||
/*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"],
|
||||
/**
|
||||
* Used to delete a file from the user data folder
|
||||
*/
|
||||
ipcMain.handle("deleteFile", async (_event, filePath) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const fullPath = join(userDataPath, filePath);
|
||||
|
||||
let result = "NULL";
|
||||
unlink(fullPath, function (err) {
|
||||
if (err && err.code == "ENOENT") {
|
||||
result = `File not exist: ${err}`;
|
||||
} else if (err) {
|
||||
result = `File delete error: ${err}`;
|
||||
} else {
|
||||
result = "File deleted successfully";
|
||||
}
|
||||
console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`);
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
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"],
|
||||
/**
|
||||
* Used to download a file from a given url
|
||||
*/
|
||||
ipcMain.handle("downloadFile", async (_event, url, fileName) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const destination = resolve(userDataPath, fileName);
|
||||
|
||||
progress(request(url), {})
|
||||
.on("progress", function (state: any) {
|
||||
mainWindow?.webContents.send("FILE_DOWNLOAD_UPDATE", {
|
||||
...state,
|
||||
fileName,
|
||||
});
|
||||
})
|
||||
.on("error", function (err: Error) {
|
||||
mainWindow?.webContents.send("FILE_DOWNLOAD_ERROR", {
|
||||
fileName,
|
||||
err,
|
||||
});
|
||||
})
|
||||
.on("end", function () {
|
||||
mainWindow?.webContents.send("FILE_DOWNLOAD_COMPLETE", {
|
||||
fileName,
|
||||
});
|
||||
})
|
||||
.pipe(createWriteStream(destination));
|
||||
});
|
||||
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() {
|
||||
return new Promise((resolve) => {
|
||||
const store = new Store();
|
||||
if (store.get("migrated_version") !== app.getVersion()) {
|
||||
console.log("start migration:", store.get("migrated_version"));
|
||||
const userDataPath = app.getPath("userData");
|
||||
@ -217,12 +230,12 @@ function migratePlugins() {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setupPlugins() {
|
||||
init({
|
||||
// Function to check from the main process that user wants to install a plugin
|
||||
confirmInstall: async (plugins: string[]) => {
|
||||
confirmInstall: async (_plugins: string[]) => {
|
||||
return true;
|
||||
},
|
||||
// Path to install plugin to
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "jan-electron",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"main": "./build/main.js",
|
||||
"author": "Jan",
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"license": "MIT",
|
||||
"homepage": "./",
|
||||
"build": {
|
||||
@ -11,8 +11,9 @@
|
||||
"files": [
|
||||
"renderer/**/*",
|
||||
"build/*.{js,map}",
|
||||
"build/core/plugin-manager/**/*",
|
||||
"core/pre-install"
|
||||
"build/**/*.{js,map}",
|
||||
"core/pre-install",
|
||||
"core/plugin-manager/facade"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"core/pre-install"
|
||||
@ -26,29 +27,48 @@
|
||||
],
|
||||
"extends": null,
|
||||
"mac": {
|
||||
"type": "distribution"
|
||||
}
|
||||
"type": "distribution",
|
||||
"entitlements": "./entitlements.mac.plist",
|
||||
"entitlementsInherit": "./entitlements.mac.plist",
|
||||
"notarize": {
|
||||
"teamId": "YT49P7GXG4"
|
||||
}
|
||||
},
|
||||
"artifactName": "jan-${os}-${arch}-${version}.${ext}"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||
"test:e2e": "playwright test --workers=1",
|
||||
"dev": "tsc -p . && electron .",
|
||||
"build": "tsc -p . && electron-builder -p never -mw",
|
||||
"build:publish": "tsc -p . && electron-builder -p onTagOrDraft -mw",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
"build": "tsc -p . && electron-builder -p never -m",
|
||||
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
|
||||
"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": {
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"@npmcli/arborist": "^7.1.0",
|
||||
"@uiball/loaders": "^1.3.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.4",
|
||||
"node-llama-cpp": "^2.4.1",
|
||||
"pluggable-electron": "^0.6.0",
|
||||
"pacote": "^17.0.4",
|
||||
"react-intersection-observer": "^9.5.2",
|
||||
"request": "^2.88.2",
|
||||
"request-progress": "^3.0.0"
|
||||
"request-progress": "^3.0.0",
|
||||
"use-debounce": "^9.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"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-builder": "^24.6.4",
|
||||
"wait-on": "^7.0.1"
|
||||
"electron-playwright-helpers": "^1.6.0",
|
||||
"eslint-plugin-react": "^7.33.2"
|
||||
},
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
|
||||
10
electron/playwright.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { PlaywrightTestConfig } from "@playwright/test";
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: "./tests",
|
||||
testIgnore: "./core/**",
|
||||
retries: 0,
|
||||
timeout: 120000,
|
||||
};
|
||||
|
||||
export default config;
|
||||
@ -1,7 +1,6 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
// Make Pluggable Electron's facade available to the renderer on window.plugins
|
||||
//@ts-ignore
|
||||
const useFacade = require("pluggable-electron/facade");
|
||||
const useFacade = require("../core/plugin-manager/facade");
|
||||
useFacade();
|
||||
//@ts-ignore
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
@ -14,10 +13,14 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
|
||||
pluginPath: () => ipcRenderer.invoke("pluginPath"),
|
||||
|
||||
reloadPlugins: () => ipcRenderer.invoke("reloadPlugins"),
|
||||
|
||||
appVersion: () => ipcRenderer.invoke("appVersion"),
|
||||
|
||||
openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url),
|
||||
|
||||
relaunch: () => ipcRenderer.invoke("relaunch"),
|
||||
|
||||
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
|
||||
|
||||
downloadFile: (url: string, path: string) =>
|
||||
|
||||
47
electron/tests/explore.e2e.spec.ts
Normal 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...
|
||||
});
|
||||
57
electron/tests/main.e2e.spec.ts
Normal 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,let’s download your first model",
|
||||
})
|
||||
.first()
|
||||
.isDisabled();
|
||||
expect(welcomeText).toBe(false);
|
||||
});
|
||||
46
electron/tests/my-models.e2e.spec.ts
Normal 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...
|
||||
});
|
||||
76
electron/tests/navigation.e2e.spec.ts
Normal 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);
|
||||
});
|
||||
42
electron/tests/settings.e2e.spec.ts
Normal 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);
|
||||
});
|
||||
@ -2,13 +2,17 @@
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"outDir": "./build",
|
||||
"rootDir": "./",
|
||||
"noEmitOnError": true,
|
||||
"baseUrl": ".",
|
||||
"allowJs": true,
|
||||
"paths": { "*": ["node_modules/*"] },
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
},
|
||||
"exclude": ["core", "build", "node_modules"]
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["core", "build", "dist", "tests"]
|
||||
}
|
||||
|
||||
8
electron/utils/disposable.ts
Normal 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
@ -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);
|
||||
};
|
||||
21
electron/utils/versionDiff.ts
Normal 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
14224
package-lock.json
generated
Normal file
18
package.json
@ -14,19 +14,29 @@
|
||||
]
|
||||
},
|
||||
"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: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: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: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": {
|
||||
"concurrently": "^8.2.1",
|
||||
"cpx": "^1.5.0",
|
||||
"wait-on": "^7.0.1"
|
||||
"wait-on": "^7.0.1",
|
||||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"version": "0.0.0"
|
||||
}
|
||||
|
||||
19
web/app/_components/ActiveModelTable/index.tsx
Normal 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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -2,9 +2,8 @@ import { Product } from "@/_models/Product";
|
||||
import DownloadModelContent from "../DownloadModelContent";
|
||||
import ModelDownloadButton from "../ModelDownloadButton";
|
||||
import ModelDownloadingButton from "../ModelDownloadingButton";
|
||||
import ViewModelDetailButton from "../ViewModelDetailButton";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { modelDownloadStateAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
|
||||
|
||||
type Props = {
|
||||
product: Product;
|
||||
@ -36,8 +35,6 @@ const AvailableModelCard: React.FC<Props> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewDetails = () => {};
|
||||
|
||||
const downloadButton = isDownloading ? (
|
||||
<div className="w-1/5 flex items-start justify-end">
|
||||
<ModelDownloadingButton total={total} value={transferred} />
|
||||
@ -50,7 +47,7 @@ const AvailableModelCard: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg border-gray-200">
|
||||
<div className="flex justify-between py-4 px-3 gap-[10px]">
|
||||
<div className="flex justify-between py-4 px-3 gap-2.5">
|
||||
<DownloadModelContent
|
||||
required={required}
|
||||
author={product.author}
|
||||
|
||||
@ -1,20 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
currentConversationAtom,
|
||||
showingAdvancedPromptAtom,
|
||||
} from "@/_helpers/JotaiWrapper";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { useSetAtom } from "jotai";
|
||||
import SecondaryButton from "../SecondaryButton";
|
||||
import SendButton from "../SendButton";
|
||||
import { ProductType } from "@/_models/Product";
|
||||
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
|
||||
|
||||
const BasicPromptAccessories: React.FC = () => {
|
||||
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom);
|
||||
const currentConversation = useAtomValue(currentConversationAtom);
|
||||
|
||||
const shouldShowAdvancedPrompt = false;
|
||||
// currentConversation?.product.type === ProductType.ControlNet;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/24/outline";
|
||||
import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
|
||||
|
||||
const BasicPromptButton: React.FC = () => {
|
||||
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom);
|
||||
|
||||
@ -1,22 +1,45 @@
|
||||
"use client";
|
||||
|
||||
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 { useAtom } from "jotai";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { ChangeEvent } from "react";
|
||||
|
||||
const BasicPromptInput: React.FC = () => {
|
||||
const activeConversationId = useAtomValue(getActiveConvoIdAtom);
|
||||
const selectedModel = useAtomValue(selectedModelAtom);
|
||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom);
|
||||
const { sendChatMessage } = useSendChatMessage();
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
|
||||
const handleMessageChange = (event: any) => {
|
||||
const { initModel } = useInitModel();
|
||||
|
||||
const handleMessageChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCurrentPrompt(event.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
const handleKeyDown = async (
|
||||
event: React.KeyboardEvent<HTMLTextAreaElement>
|
||||
) => {
|
||||
if (event.key === "Enter") {
|
||||
if (!event.shiftKey) {
|
||||
event.preventDefault();
|
||||
sendChatMessage();
|
||||
if (activeConversationId) {
|
||||
event.preventDefault();
|
||||
sendChatMessage();
|
||||
} else {
|
||||
if (!selectedModel) {
|
||||
console.log("No model selected");
|
||||
return;
|
||||
}
|
||||
|
||||
await requestCreateConvo(selectedModel);
|
||||
await initModel(selectedModel);
|
||||
sendChatMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,29 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import React, { useCallback, useRef, useState, useEffect } from "react";
|
||||
import ChatItem from "../ChatItem";
|
||||
import { ChatMessage } from "@/_models/ChatMessage";
|
||||
import useChatMessages from "@/_hooks/useChatMessages";
|
||||
import {
|
||||
chatMessages,
|
||||
getActiveConvoIdAtom,
|
||||
showingTyping,
|
||||
} from "@/_helpers/JotaiWrapper";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import LoadingIndicator from "../LoadingIndicator";
|
||||
import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||
import { chatMessages } from "@/_helpers/atoms/ChatMessage.atom";
|
||||
|
||||
const ChatBody: React.FC = () => {
|
||||
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? "";
|
||||
const messageList = useAtomValue(
|
||||
selectAtom(
|
||||
chatMessages,
|
||||
useCallback((v) => v[activeConversationId], [activeConversationId])
|
||||
)
|
||||
useCallback((v) => v[activeConversationId], [activeConversationId]),
|
||||
),
|
||||
);
|
||||
const [content, setContent] = useState<React.JSX.Element[]>([]);
|
||||
|
||||
const isTyping = useAtomValue(showingTyping);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const { loading, hasMore } = useChatMessages(offset);
|
||||
const intersectObs = useRef<any>(null);
|
||||
@ -42,10 +37,10 @@ const ChatBody: React.FC = () => {
|
||||
|
||||
if (message) intersectObs.current.observe(message);
|
||||
},
|
||||
[loading, hasMore]
|
||||
[loading, hasMore],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const list = messageList?.map((message, index) => {
|
||||
if (messageList?.length === index + 1) {
|
||||
return (
|
||||
@ -60,11 +55,6 @@ const ChatBody: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col-reverse flex-1 py-4 overflow-y-auto scroll">
|
||||
{isTyping && (
|
||||
<div className="ml-4 mb-2" key="indicator">
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
)}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -2,22 +2,17 @@ import SimpleControlNetMessage from "../SimpleControlNetMessage";
|
||||
import SimpleImageMessage from "../SimpleImageMessage";
|
||||
import SimpleTextMessage from "../SimpleTextMessage";
|
||||
import { ChatMessage, MessageType } from "@/_models/ChatMessage";
|
||||
import StreamTextMessage from "../StreamTextMessage";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { currentStreamingMessageAtom } from "@/_helpers/JotaiWrapper";
|
||||
|
||||
export default function renderChatMessage({
|
||||
id,
|
||||
messageType,
|
||||
messageSenderType,
|
||||
senderAvatarUrl,
|
||||
senderName,
|
||||
createdAt,
|
||||
imageUrls,
|
||||
htmlText,
|
||||
text,
|
||||
}: ChatMessage): React.ReactNode {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const message = useAtomValue(currentStreamingMessageAtom);
|
||||
switch (messageType) {
|
||||
case MessageType.ImageWithText:
|
||||
return (
|
||||
@ -42,22 +37,14 @@ export default function renderChatMessage({
|
||||
/>
|
||||
);
|
||||
case MessageType.Text:
|
||||
return id !== message?.id ? (
|
||||
return (
|
||||
<SimpleTextMessage
|
||||
key={id}
|
||||
avatarUrl={senderAvatarUrl}
|
||||
senderName={senderName}
|
||||
createdAt={createdAt}
|
||||
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
|
||||
/>
|
||||
) : (
|
||||
<StreamTextMessage
|
||||
key={id}
|
||||
id={id}
|
||||
avatarUrl={senderAvatarUrl}
|
||||
senderName={senderName}
|
||||
createdAt={createdAt}
|
||||
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
|
||||
senderType={messageSenderType}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import JanImage from "../JanImage";
|
||||
import { setActiveConvoIdAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { setActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||
|
||||
const CompactLogo: React.FC = () => {
|
||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
|
||||
|
||||
@ -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;
|
||||
@ -1,4 +1,4 @@
|
||||
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
|
||||
import useDeleteConversation from "@/_hooks/useDeleteConversation";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
@ -63,8 +63,8 @@ const ConfirmDeleteConversationModal: React.FC = () => {
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Are you sure you want to delete this conversation? All
|
||||
of messages will be permanently removed from our servers
|
||||
forever. This action cannot be undone.
|
||||
of messages will be permanently removed. This action
|
||||
cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,17 +1,13 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { showConfirmDeleteModalAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { useAtom } from "jotai";
|
||||
import useSignOut from "@/_hooks/useSignOut";
|
||||
import { showConfirmDeleteModalAtom } from "@/_helpers/atoms/Modal.atom";
|
||||
|
||||
const ConfirmDeleteModelModal: React.FC = () => {
|
||||
const [show, setShow] = useAtom(showConfirmDeleteModalAtom);
|
||||
const { signOut } = useSignOut();
|
||||
|
||||
const onLogOutClick = () => {
|
||||
signOut().then(() => setShow(false));
|
||||
};
|
||||
const onConfirmDelete = () => {};
|
||||
|
||||
return (
|
||||
<Transition.Root show={show} as={Fragment}>
|
||||
@ -65,7 +61,7 @@ const ConfirmDeleteModelModal: React.FC = () => {
|
||||
<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"
|
||||
onClick={onLogOutClick}
|
||||
onClick={onConfirmDelete}
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { showConfirmSignOutModalAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { useAtom } from "jotai";
|
||||
import useSignOut from "@/_hooks/useSignOut";
|
||||
import { showConfirmSignOutModalAtom } from "@/_helpers/atoms/Modal.atom";
|
||||
|
||||
const ConfirmSignOutModal: React.FC = () => {
|
||||
const [show, setShow] = useAtom(showConfirmSignOutModalAtom);
|
||||
|
||||
@ -32,7 +32,7 @@ const ConversationalCard: React.FC<Props> = ({ product }) => {
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex text-xs leading-5 text-gray-500 items-center gap-[2px]">
|
||||
<span className="flex text-xs leading-5 text-gray-500 items-center gap-0.5">
|
||||
<Image src={"icons/play.svg"} width={16} height={16} alt="" />
|
||||
32.2k runs
|
||||
</span>
|
||||
|
||||
@ -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;
|
||||
@ -18,19 +18,19 @@ const DownloadModelContent: React.FC<Props> = ({
|
||||
type,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-4/5 flex flex-col gap-[10px]">
|
||||
<div className="w-4/5 flex flex-col gap-2.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<h2 className="font-medium text-xl leading-[25px] tracking-[-0.4px] text-gray-900">
|
||||
{name}
|
||||
</h2>
|
||||
<DownloadModelTitle title={type} />
|
||||
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
|
||||
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
|
||||
<span className="text-xs leading-[18px] font-semibold text-purple-800">
|
||||
{author}
|
||||
</span>
|
||||
</div>
|
||||
{required && (
|
||||
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
|
||||
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
|
||||
<span className="text-xs leading-[18px] text-[#11192899]">
|
||||
Required{" "}
|
||||
</span>
|
||||
@ -44,7 +44,7 @@ const DownloadModelContent: React.FC<Props> = ({
|
||||
<div
|
||||
className={`${
|
||||
isRecommend ? "flex" : "hidden"
|
||||
} w-fit justify-center items-center bg-green-50 rounded-full px-[10px] py-[2px] gap-2`}
|
||||
} w-fit justify-center items-center bg-green-50 rounded-full px-2.5 py-0.5 gap-2`}
|
||||
>
|
||||
<div className="w-3 h-3 rounded-full bg-green-400"></div>
|
||||
<span className="text-green-600 font-medium text-xs leading-18px">
|
||||
|
||||