chore: resolve conflict

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

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

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

View File

@ -26,7 +26,7 @@ jobs:
run: yarn install
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
View File

@ -0,0 +1,29 @@
name: Jan Docs Test Build
on:
pull_request:
branches:
- main
paths:
- 'docs/**'
- '.github/workflows/deploy-jan-docs.yml'
- '.github/workflows/jan-docs-test.yml'
jobs:
deploy:
name: Test Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
cache-dependency-path: './docs/yarn.lock'
- name: Install dependencies
run: yarn install
working-directory: docs
- name: Test Build Command
run: sed -i '/process.env.DEBUG = namespaces;/c\// process.env.DEBUG = namespaces;' ./node_modules/debug/src/node.js && yarn build
working-directory: docs

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

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

View File

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

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

15166
docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,9 @@
"@heroicons/react": "^2.0.18",
"@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": [

View File

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

View File

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

View File

@ -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}

View File

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

View File

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

View File

@ -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)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

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

File diff suppressed because it is too large Load Diff

44
electron/.eslintrc.js Normal file
View File

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

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

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

View File

@ -16,11 +16,23 @@ class Plugin {
/** @type {boolean} Whether this plugin should be activated when its activation points are triggered. */
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
}
/**

View File

@ -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);
}

View File

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

View File

@ -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;

View File

@ -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}`)

View File

@ -61,10 +61,6 @@ function init() {
);
});
const stmt = db.prepare(
"INSERT INTO conversations (name, model_id, image, message) VALUES (?, ?, ?, ?)"
);
stmt.finalize();
db.close();
}
@ -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,

File diff suppressed because it is too large Load Diff

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

View File

@ -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);
}

View File

@ -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,
};

View File

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

View File

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

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

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

View File

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

View File

@ -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,
};

File diff suppressed because it is too large Load Diff

View 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"
]
}

File diff suppressed because it is too large Load Diff

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

View File

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

View File

@ -1,30 +1,41 @@
import {
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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */
// Make Pluggable Electron's facade available to the renderer on window.plugins
//@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) =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,17 @@
"compilerOptions": {
"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"]
}

View File

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

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

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

View File

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

1614
node_modules/.yarn-integrity generated vendored

File diff suppressed because it is too large Load Diff

14224
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,19 +14,29 @@
]
},
"scripts": {
"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"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,8 @@ import { Product } from "@/_models/Product";
import DownloadModelContent from "../DownloadModelContent";
import 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}

View File

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

View File

@ -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);

View File

@ -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();
}
}
}
};

View File

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

View File

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

View File

@ -1,29 +1,24 @@
"use client";
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>
);

View File

@ -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:

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React from "react";
import 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);

View File

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

View File

@ -1,4 +1,4 @@
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/JotaiWrapper";
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
import useDeleteConversation from "@/_hooks/useDeleteConversation";
import { 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>

View File

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

View File

@ -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);

View File

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

View File

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

View File

@ -18,19 +18,19 @@ const DownloadModelContent: React.FC<Props> = ({
type,
}) => {
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">

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