Merge branch 'main' into css-drake
This commit is contained in:
commit
45b16a80c5
151
.github/workflows/build-app.yml
vendored
Normal file
151
.github/workflows/build-app.yml
vendored
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
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: Install yarn dependencies
|
||||||
|
run: |
|
||||||
|
yarn install
|
||||||
|
yarn build:plugins
|
||||||
|
|
||||||
|
- name: Get Cer for code signing
|
||||||
|
run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
|
||||||
|
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 }}
|
||||||
74
.github/workflows/linter-and-test.yml
vendored
Normal file
74
.github/workflows/linter-and-test.yml
vendored
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
name: Linter & Test
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- fix/eslint-ignore-patterns
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-on-macos:
|
||||||
|
runs-on: [self-hosted, macOS, macos-desktop]
|
||||||
|
steps:
|
||||||
|
- name: Getting the repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Installing node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Linter and test
|
||||||
|
run: |
|
||||||
|
yarn config set network-timeout 300000
|
||||||
|
yarn install
|
||||||
|
yarn lint
|
||||||
|
yarn build:plugins
|
||||||
|
yarn build
|
||||||
|
yarn test
|
||||||
|
env:
|
||||||
|
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||||
|
|
||||||
|
test-on-windows:
|
||||||
|
runs-on: [self-hosted, Windows, windows-desktop]
|
||||||
|
steps:
|
||||||
|
- name: Getting the repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Installing node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Linter and test
|
||||||
|
run: |
|
||||||
|
yarn config set network-timeout 300000
|
||||||
|
yarn install
|
||||||
|
yarn lint
|
||||||
|
yarn build:plugins
|
||||||
|
yarn build:win32
|
||||||
|
yarn test
|
||||||
|
|
||||||
|
test-on-ubuntu:
|
||||||
|
runs-on: [self-hosted, Linux, ubuntu-desktop]
|
||||||
|
steps:
|
||||||
|
- name: Getting the repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Installing node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Linter and test
|
||||||
|
run: |
|
||||||
|
yarn config set network-timeout 300000
|
||||||
|
yarn install
|
||||||
|
yarn lint
|
||||||
|
yarn build:plugins
|
||||||
|
yarn build:linux
|
||||||
|
yarn test
|
||||||
|
env:
|
||||||
|
DISPLAY: ":0"
|
||||||
50
.github/workflows/macos-build-app.yml
vendored
50
.github/workflows/macos-build-app.yml
vendored
@ -1,50 +0,0 @@
|
|||||||
name: Jan Build MacOS App
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: ['v*.*.*']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-macos-app:
|
|
||||||
runs-on: macos-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Installing node
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Install jq
|
|
||||||
uses: dcarbone/install-jq-action@v2.0.1
|
|
||||||
|
|
||||||
- name: Get tag
|
|
||||||
id: tag
|
|
||||||
uses: dawidd6/action-get-tag@v1
|
|
||||||
|
|
||||||
- name: Update app version base on tag
|
|
||||||
run: |
|
|
||||||
if [[ ! "${VERSION_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
||||||
echo "Error: Tag is not valid!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
env:
|
|
||||||
VERSION_TAG: ${{ steps.tag.outputs.tag }}
|
|
||||||
|
|
||||||
- name: Install yarn dependencies
|
|
||||||
run: |
|
|
||||||
yarn install
|
|
||||||
yarn build:plugins
|
|
||||||
|
|
||||||
- name: Build and publish app
|
|
||||||
run: |
|
|
||||||
yarn build:publish
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
29
adr/adr-003-jan-plugins.md
Normal file
29
adr/adr-003-jan-plugins.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# ADR 003: JAN PLUGINS
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- Oct 5th 2023: Initial draft
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Modular Architecture w/ Plugins:
|
||||||
|
|
||||||
|
- Jan will have an architecture similar to VSCode or k8Lens
|
||||||
|
- "Desktop Application" whose functionality can be extended thru plugins
|
||||||
|
- Jan's architecture will need to accomodate plugins for (a) Persistence(b) IAM(c) Teams and RBAC(d) Policy engines(e) "Apps" (i.e. higher-order business logic)(f) Themes (UI)
|
||||||
|
- Nitro's architecture will need to accomodate plugins for different "model backends"(a) llama.cpp(b) rkwk (and others)(c) 3rd-party AIs
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
What becomes easier or more difficult to do because of this change?
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
[Plugin APIs](./adr-003-jan-plugins.md)
|
||||||
37
adr/docs/adr-003-plugins.md
Normal file
37
adr/docs/adr-003-plugins.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
## JAN service & plugin APIs
|
||||||
|
|
||||||
|
Jan frontend components will communicate with plugin functions via Service Interfaces:
|
||||||
|
|
||||||
|
|
||||||
|
All of the available APIs are listed in [CoreService](../../web/shared/coreService.ts)
|
||||||
|
|
||||||
|
- Data Service:
|
||||||
|
- GET_CONVERSATIONS: retrieve all of the conversations
|
||||||
|
- CREATE_CONVERSATION: start a new conversation
|
||||||
|
- DELETE_CONVERSATION: delete an existing conversation
|
||||||
|
- GET_CONVERSATION_MESSAGES: retrieve a certain conversation messages
|
||||||
|
- CREATE_MESSAGE: store a new message (both sent & received)
|
||||||
|
- UPDATE_MESSAGE: update an existing message (streaming)
|
||||||
|
- STORE_MODEL: store new model information (when clicking download)
|
||||||
|
- UPDATE_FINISHED_DOWNLOAD: mark a model as downloaded
|
||||||
|
- GET_UNFINISHED_DOWNLOAD_MODELS: retrieve all unfinished downloading model (TBD)
|
||||||
|
- GET_FINISHED_DOWNLOAD_MODELS: retrieve all finished downloading model (TBD)
|
||||||
|
- DELETE_DOWNLOAD_MODEL: delete a model (TBD)
|
||||||
|
- GET_MODEL_BY_ID: retrieve model information by its ID
|
||||||
|
|
||||||
|
- Inference Service:
|
||||||
|
- INFERENCE_URL: retrieve inference endpoint served by plugin
|
||||||
|
- INIT_MODEL: runs a model
|
||||||
|
- STOP_MODEL: stop a running model
|
||||||
|
|
||||||
|
- Model Management Service: (TBD)
|
||||||
|
- GET_AVAILABLE_MODELS: retrieve available models (deprecate soon)
|
||||||
|
- GET_DOWNLOADED_MODELS: (deprecated)
|
||||||
|
- DELETE_MODEL: (deprecated)
|
||||||
|
- DOWNLOAD_MODEL: start to download a model
|
||||||
|
- SEARCH_MODELS: explore models with search query on HuggingFace (TBD)
|
||||||
|
|
||||||
|
- Monitoring service:
|
||||||
|
- GET_RESOURCES_INFORMATION: retrieve total & used memory information
|
||||||
|
- GET_CURRENT_LOAD_INFORMATION: retrieve CPU load information
|
||||||
|
|
||||||
BIN
adr/images/adr-003-01.png
Normal file
BIN
adr/images/adr-003-01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 335 KiB |
@ -34,5 +34,11 @@ module.exports = {
|
|||||||
{ name: "Link", linkAttribute: "to" },
|
{ name: "Link", linkAttribute: "to" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ignorePatterns: ["renderer/*", "node_modules/*", "core/plugins"],
|
ignorePatterns: [
|
||||||
|
"build",
|
||||||
|
"renderer",
|
||||||
|
"node_modules",
|
||||||
|
"core/plugins",
|
||||||
|
"core/**/*.test.js",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,11 +16,23 @@ class Plugin {
|
|||||||
/** @type {boolean} Whether this plugin should be activated when its activation points are triggered. */
|
/** @type {boolean} Whether this plugin should be activated when its activation points are triggered. */
|
||||||
active
|
active
|
||||||
|
|
||||||
constructor(name, url, activationPoints, active) {
|
/** @type {string} Plugin's description. */
|
||||||
|
description
|
||||||
|
|
||||||
|
/** @type {string} Plugin's version. */
|
||||||
|
version
|
||||||
|
|
||||||
|
/** @type {string} Plugin's logo. */
|
||||||
|
icon
|
||||||
|
|
||||||
|
constructor(name, url, activationPoints, active, description, version, icon) {
|
||||||
this.name = name
|
this.name = name
|
||||||
this.url = url
|
this.url = url
|
||||||
this.activationPoints = activationPoints
|
this.activationPoints = activationPoints
|
||||||
this.active = active
|
this.active = active
|
||||||
|
this.description = description
|
||||||
|
this.version = version
|
||||||
|
this.icon = icon
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export async function install(plugins) {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const plgList = await window.pluggableElectronIpc.install(plugins);
|
const plgList = await window.pluggableElectronIpc.install(plugins);
|
||||||
if (plgList.cancelled) return false;
|
if (plgList.cancelled) return false;
|
||||||
return plgList.map((plg) => {
|
return plgList.map((plg) => {
|
||||||
@ -50,6 +51,7 @@ export function uninstall(plugins, reload = true) {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
return window.pluggableElectronIpc.uninstall(plugins, reload);
|
return window.pluggableElectronIpc.uninstall(plugins, reload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +64,7 @@ export async function getActive() {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const plgList = await window.pluggableElectronIpc.getActive();
|
const plgList = await window.pluggableElectronIpc.getActive();
|
||||||
return plgList.map(
|
return plgList.map(
|
||||||
(plugin) =>
|
(plugin) =>
|
||||||
@ -69,7 +72,10 @@ export async function getActive() {
|
|||||||
plugin.name,
|
plugin.name,
|
||||||
plugin.url,
|
plugin.url,
|
||||||
plugin.activationPoints,
|
plugin.activationPoints,
|
||||||
plugin.active
|
plugin.active,
|
||||||
|
plugin.description,
|
||||||
|
plugin.version,
|
||||||
|
plugin.icon
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -83,6 +89,7 @@ export async function registerActive() {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const plgList = await window.pluggableElectronIpc.getActive();
|
const plgList = await window.pluggableElectronIpc.getActive();
|
||||||
plgList.forEach((plugin) =>
|
plgList.forEach((plugin) =>
|
||||||
register(
|
register(
|
||||||
@ -107,6 +114,7 @@ export async function update(plugins, reload = true) {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const plgList = await window.pluggableElectronIpc.update(plugins, reload);
|
const plgList = await window.pluggableElectronIpc.update(plugins, reload);
|
||||||
return plgList.map(
|
return plgList.map(
|
||||||
(plugin) =>
|
(plugin) =>
|
||||||
@ -129,6 +137,7 @@ export function updatesAvailable(plugin) {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
return window.pluggableElectronIpc.updatesAvailable(plugin);
|
return window.pluggableElectronIpc.updatesAvailable(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +152,7 @@ export async function toggleActive(plugin, active) {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const plg = await window.pluggableElectronIpc.toggleActive(plugin, active);
|
const plg = await window.pluggableElectronIpc.toggleActive(plugin, active);
|
||||||
return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active);
|
return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export * as activationPoints from "./activation-manager.js";
|
|||||||
export * as plugins from "./facade.js";
|
export * as plugins from "./facade.js";
|
||||||
export { default as ExtensionPoint } from "./ExtensionPoint.js";
|
export { default as ExtensionPoint } from "./ExtensionPoint.js";
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
if (typeof window !== "undefined" && !window.pluggableElectronIpc)
|
if (typeof window !== "undefined" && !window.pluggableElectronIpc)
|
||||||
console.warn(
|
console.warn(
|
||||||
"Facade is not registered in preload. Facade functions will throw an error if used."
|
"Facade is not registered in preload. Facade functions will throw an error if used."
|
||||||
|
|||||||
@ -1,30 +1,32 @@
|
|||||||
import { ipcRenderer, contextBridge } from "electron"
|
const { ipcRenderer, contextBridge } = require("electron");
|
||||||
|
|
||||||
export default function useFacade() {
|
function useFacade() {
|
||||||
const interfaces = {
|
const interfaces = {
|
||||||
install(plugins) {
|
install(plugins) {
|
||||||
return ipcRenderer.invoke('pluggable:install', plugins)
|
return ipcRenderer.invoke("pluggable:install", plugins);
|
||||||
},
|
},
|
||||||
uninstall(plugins, reload) {
|
uninstall(plugins, reload) {
|
||||||
return ipcRenderer.invoke('pluggable:uninstall', plugins, reload)
|
return ipcRenderer.invoke("pluggable:uninstall", plugins, reload);
|
||||||
},
|
},
|
||||||
getActive() {
|
getActive() {
|
||||||
return ipcRenderer.invoke('pluggable:getActivePlugins')
|
return ipcRenderer.invoke("pluggable:getActivePlugins");
|
||||||
},
|
},
|
||||||
update(plugins, reload) {
|
update(plugins, reload) {
|
||||||
return ipcRenderer.invoke('pluggable:update', plugins, reload)
|
return ipcRenderer.invoke("pluggable:update", plugins, reload);
|
||||||
},
|
},
|
||||||
updatesAvailable(plugin) {
|
updatesAvailable(plugin) {
|
||||||
return ipcRenderer.invoke('pluggable:updatesAvailable', plugin)
|
return ipcRenderer.invoke("pluggable:updatesAvailable", plugin);
|
||||||
},
|
},
|
||||||
toggleActive(plugin, active) {
|
toggleActive(plugin, active) {
|
||||||
return ipcRenderer.invoke('pluggable:togglePluginActive', plugin, active)
|
return ipcRenderer.invoke("pluggable:togglePluginActive", plugin, active);
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
if (contextBridge) {
|
if (contextBridge) {
|
||||||
contextBridge.exposeInMainWorld('pluggableElectronIpc', interfaces)
|
contextBridge.exposeInMainWorld("pluggableElectronIpc", interfaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
return interfaces
|
return interfaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = useFacade;
|
||||||
|
|||||||
@ -18,6 +18,8 @@ class Plugin {
|
|||||||
* @property {string} version Version of the package as defined in the manifest.
|
* @property {string} version Version of the package as defined in the manifest.
|
||||||
* @property {Array<string>} activationPoints List of {@link ./Execution-API#activationPoints|activation points}.
|
* @property {Array<string>} activationPoints List of {@link ./Execution-API#activationPoints|activation points}.
|
||||||
* @property {string} main The entry point as defined in the main entry of the manifest.
|
* @property {string} main The entry point as defined in the main entry of the manifest.
|
||||||
|
* @property {string} description The description of plugin as defined in the manifest.
|
||||||
|
* @property {string} icon The icon of plugin as defined in the manifest.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
@ -75,6 +77,8 @@ class Plugin {
|
|||||||
this.version = mnf.version
|
this.version = mnf.version
|
||||||
this.activationPoints = mnf.activationPoints || null
|
this.activationPoints = mnf.activationPoints || null
|
||||||
this.main = mnf.main
|
this.main = mnf.main
|
||||||
|
this.description = mnf.description
|
||||||
|
this.icon = mnf.icon
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`)
|
throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`)
|
||||||
|
|||||||
@ -168,7 +168,7 @@ function getFinishedDownloadModels() {
|
|||||||
|
|
||||||
const query = `SELECT * FROM models WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`;
|
const query = `SELECT * FROM models WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`;
|
||||||
db.all(query, (err: Error, row: any) => {
|
db.all(query, (err: Error, row: any) => {
|
||||||
res(row);
|
res(row.map((item: any) => parseToProduct(item)));
|
||||||
});
|
});
|
||||||
db.close();
|
db.close();
|
||||||
});
|
});
|
||||||
@ -184,6 +184,7 @@ function deleteDownloadModel(modelId: string) {
|
|||||||
const stmt = db.prepare("DELETE FROM models WHERE id = ?");
|
const stmt = db.prepare("DELETE FROM models WHERE id = ?");
|
||||||
stmt.run(modelId);
|
stmt.run(modelId);
|
||||||
stmt.finalize();
|
stmt.finalize();
|
||||||
|
res(modelId);
|
||||||
});
|
});
|
||||||
|
|
||||||
db.close();
|
db.close();
|
||||||
@ -352,7 +353,7 @@ function deleteConversation(id: any) {
|
|||||||
);
|
);
|
||||||
deleteMessages.run(id);
|
deleteMessages.run(id);
|
||||||
deleteMessages.finalize();
|
deleteMessages.finalize();
|
||||||
res([]);
|
res(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
db.close();
|
db.close();
|
||||||
@ -373,6 +374,31 @@ function getConversationMessages(conversation_id: any) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseToProduct(row: any) {
|
||||||
|
const product = {
|
||||||
|
id: row.id,
|
||||||
|
slug: row.slug,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
avatarUrl: row.avatar_url,
|
||||||
|
longDescription: row.long_description,
|
||||||
|
technicalDescription: row.technical_description,
|
||||||
|
author: row.author,
|
||||||
|
version: row.version,
|
||||||
|
modelUrl: row.model_url,
|
||||||
|
nsfw: row.nsfw,
|
||||||
|
greeting: row.greeting,
|
||||||
|
type: row.type,
|
||||||
|
inputs: row.inputs,
|
||||||
|
outputs: row.outputs,
|
||||||
|
createdAt: new Date(row.created_at),
|
||||||
|
updatedAt: new Date(row.updated_at),
|
||||||
|
fileName: row.file_name,
|
||||||
|
downloadUrl: row.download_url,
|
||||||
|
};
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
getConversations,
|
getConversations,
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "data-plugin",
|
"name": "data-plugin",
|
||||||
"version": "2.1.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "Jan Database Plugin efficiently stores conversation and model data using SQLite, providing accessible data management",
|
||||||
|
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"author": "Jan",
|
"author": "Jan",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -10,7 +11,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||||
"build:package": "rimraf ./data-plugin*.tgz && npm run build && npm pack",
|
"build:package": "rimraf ./data-plugin*.tgz && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=arm64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=glibc --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=musl --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_libc=unknown --target_arch=x64 && npm run build && npm pack",
|
||||||
"build:publish": "npm run build:package && cpx *.tgz ../../pre-install"
|
"build:publish": "npm run build:package && cpx *.tgz ../../pre-install"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
@ -36,6 +37,7 @@
|
|||||||
"node_modules"
|
"node_modules"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"node-pre-gyp": "^0.17.0",
|
||||||
"sqlite3": "^5.1.6"
|
"sqlite3": "^5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const dispose = async () =>
|
|||||||
new Promise(async (resolve) => {
|
new Promise(async (resolve) => {
|
||||||
if (window.electronAPI) {
|
if (window.electronAPI) {
|
||||||
window.electronAPI
|
window.electronAPI
|
||||||
.invokePluginFunc(MODULE_PATH, "killSubprocess")
|
.invokePluginFunc(MODULE_PATH, "dispose")
|
||||||
.then((res) => resolve(res));
|
.then((res) => resolve(res));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,18 +2,9 @@ const path = require("path");
|
|||||||
const { app, dialog } = require("electron");
|
const { app, dialog } = require("electron");
|
||||||
const { spawn } = require("child_process");
|
const { spawn } = require("child_process");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
var exec = require("child_process").exec;
|
|
||||||
|
|
||||||
let subprocess = null;
|
let subprocess = null;
|
||||||
|
|
||||||
process.on("exit", () => {
|
|
||||||
// Perform cleanup tasks here
|
|
||||||
console.log("kill subprocess on exit");
|
|
||||||
if (subprocess) {
|
|
||||||
subprocess.kill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function initModel(product) {
|
async function initModel(product) {
|
||||||
// fileName fallback
|
// fileName fallback
|
||||||
if (!product.fileName) {
|
if (!product.fileName) {
|
||||||
@ -57,13 +48,22 @@ async function initModel(product) {
|
|||||||
// Write the updated config back to the file
|
// Write the updated config back to the file
|
||||||
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 4));
|
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 4));
|
||||||
|
|
||||||
const binaryPath =
|
let binaryName;
|
||||||
process.platform === "win32"
|
|
||||||
? path.join(binaryFolder, "nitro.exe")
|
if (process.platform === "win32") {
|
||||||
: path.join(binaryFolder, "nitro");
|
binaryName = "nitro.exe";
|
||||||
|
} else if (process.platform === "darwin") { // Mac OS platform
|
||||||
|
binaryName = process.arch === "arm64" ? "nitro" : "nitro_mac_intel";
|
||||||
|
} else {
|
||||||
|
// Linux
|
||||||
|
binaryName = "nitro_linux"; // For other platforms
|
||||||
|
}
|
||||||
|
|
||||||
|
const binaryPath = path.join(binaryFolder, binaryName);
|
||||||
|
|
||||||
// Execute the binary
|
// Execute the binary
|
||||||
|
|
||||||
subprocess = spawn(binaryPath, [configFilePath], {cwd: binaryFolder});
|
subprocess = spawn(binaryPath, [configFilePath], { cwd: binaryFolder });
|
||||||
|
|
||||||
// Handle subprocess output
|
// Handle subprocess output
|
||||||
subprocess.stdout.on("data", (data) => {
|
subprocess.stdout.on("data", (data) => {
|
||||||
@ -80,6 +80,11 @@ async function initModel(product) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dispose() {
|
||||||
|
killSubprocess();
|
||||||
|
// clean other registered resources here
|
||||||
|
}
|
||||||
|
|
||||||
function killSubprocess() {
|
function killSubprocess() {
|
||||||
if (subprocess) {
|
if (subprocess) {
|
||||||
subprocess.kill();
|
subprocess.kill();
|
||||||
@ -93,4 +98,5 @@ function killSubprocess() {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
initModel,
|
initModel,
|
||||||
killSubprocess,
|
killSubprocess,
|
||||||
|
dispose,
|
||||||
};
|
};
|
||||||
|
|||||||
BIN
electron/core/plugins/inference-plugin/nitro/nitro_linux
Executable file
BIN
electron/core/plugins/inference-plugin/nitro/nitro_linux
Executable file
Binary file not shown.
BIN
electron/core/plugins/inference-plugin/nitro/nitro_mac_intel
Executable file
BIN
electron/core/plugins/inference-plugin/nitro/nitro_mac_intel
Executable file
Binary file not shown.
@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "inference-plugin",
|
"name": "inference-plugin",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "Inference Plugin, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.",
|
||||||
|
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/command-line.svg",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"author": "James",
|
"author": "Jan",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"activationPoints": [
|
"activationPoints": [
|
||||||
"init"
|
"init"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && rm -rf dist/nitro && cp -r nitro dist/nitro && npm pack",
|
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\" && npm pack",
|
||||||
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "model-management-plugin",
|
"name": "model-management-plugin",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "Model Management Plugin leverages the HuggingFace API for model exploration and seamless downloads",
|
||||||
|
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/queue-list.svg",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"author": "James",
|
"author": "James",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "monitoring-plugin",
|
"name": "monitoring-plugin",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "Utilizing systeminformation, it provides essential System and OS information retrieval",
|
||||||
|
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/cpu-chip.svg",
|
||||||
"main": "dist/bundle.js",
|
"main": "dist/bundle.js",
|
||||||
"author": "Jan",
|
"author": "Jan",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
323
electron/main.ts
323
electron/main.ts
@ -1,30 +1,41 @@
|
|||||||
import {
|
import { app, BrowserWindow, ipcMain, dialog, shell } from "electron";
|
||||||
app,
|
|
||||||
BrowserWindow,
|
|
||||||
screen as electronScreen,
|
|
||||||
ipcMain,
|
|
||||||
dialog,
|
|
||||||
shell,
|
|
||||||
} from "electron";
|
|
||||||
import { readdirSync } from "fs";
|
import { readdirSync } from "fs";
|
||||||
import { resolve, join, extname } from "path";
|
import { resolve, join, extname } from "path";
|
||||||
import { rmdir, unlink, createWriteStream } from "fs";
|
import { rmdir, unlink, createWriteStream } from "fs";
|
||||||
import isDev = require("electron-is-dev");
|
|
||||||
import { init } from "./core/plugin-manager/pluginMgr";
|
import { init } from "./core/plugin-manager/pluginMgr";
|
||||||
|
import { setupMenu } from "./utils/menu";
|
||||||
|
import { dispose } from "./utils/disposable";
|
||||||
|
|
||||||
|
const request = require("request");
|
||||||
|
const progress = require("request-progress");
|
||||||
const { autoUpdater } = require("electron-updater");
|
const { autoUpdater } = require("electron-updater");
|
||||||
const Store = require("electron-store");
|
const Store = require("electron-store");
|
||||||
// @ts-ignore
|
|
||||||
import request = require("request");
|
|
||||||
// @ts-ignore
|
|
||||||
import progress = require("request-progress");
|
|
||||||
|
|
||||||
|
const requiredModules: Record<string, any> = {};
|
||||||
let mainWindow: BrowserWindow | undefined = undefined;
|
let mainWindow: BrowserWindow | undefined = undefined;
|
||||||
const store = new Store();
|
|
||||||
|
|
||||||
autoUpdater.autoDownload = false;
|
app
|
||||||
autoUpdater.autoInstallOnAppQuit = true;
|
.whenReady()
|
||||||
|
.then(migratePlugins)
|
||||||
|
.then(setupPlugins)
|
||||||
|
.then(setupMenu)
|
||||||
|
.then(handleIPCs)
|
||||||
|
.then(handleAppUpdates)
|
||||||
|
.then(createMainWindow)
|
||||||
|
.then(() => {
|
||||||
|
app.on("activate", () => {
|
||||||
|
if (!BrowserWindow.getAllWindows().length) {
|
||||||
|
createMainWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const createMainWindow = () => {
|
app.on("window-all-closed", () => {
|
||||||
|
dispose(requiredModules);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
function createMainWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
@ -37,29 +48,9 @@ const createMainWindow = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(
|
const startURL = app.isPackaged
|
||||||
"invokePluginFunc",
|
? `file://${join(__dirname, "../renderer/index.html")}`
|
||||||
async (_event, modulePath, method, ...args) => {
|
: "http://localhost:3000";
|
||||||
const module = join(app.getPath("userData"), "plugins", modulePath);
|
|
||||||
return await import(/* webpackIgnore: true */ module)
|
|
||||||
.then((plugin) => {
|
|
||||||
if (typeof plugin[method] === "function") {
|
|
||||||
return plugin[method](...args);
|
|
||||||
} else {
|
|
||||||
console.log(plugin[method]);
|
|
||||||
console.error(`Function "${method}" does not exist in the module.`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
return res;
|
|
||||||
})
|
|
||||||
.catch((err) => console.log(err));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const startURL = isDev
|
|
||||||
? "http://localhost:3000"
|
|
||||||
: `file://${join(__dirname, "../renderer/index.html")}`;
|
|
||||||
|
|
||||||
mainWindow.loadURL(startURL);
|
mainWindow.loadURL(startURL);
|
||||||
|
|
||||||
@ -68,140 +59,150 @@ const createMainWindow = () => {
|
|||||||
if (process.platform !== "darwin") app.quit();
|
if (process.platform !== "darwin") app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDev) mainWindow.webContents.openDevTools();
|
if (!app.isPackaged) mainWindow.webContents.openDevTools();
|
||||||
};
|
}
|
||||||
|
|
||||||
app
|
function handleAppUpdates() {
|
||||||
.whenReady()
|
/*New Update Available*/
|
||||||
.then(migratePlugins)
|
autoUpdater.on("update-available", async (_info: any) => {
|
||||||
.then(() => {
|
const action = await dialog.showMessageBox({
|
||||||
createMainWindow();
|
message: `Update available. Do you want to download the latest update?`,
|
||||||
setupPlugins();
|
buttons: ["Download", "Later"],
|
||||||
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));
|
|
||||||
});
|
});
|
||||||
|
if (action.response === 0) await autoUpdater.downloadUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("pluginPath", async (event) => {
|
/*App Update Completion Message*/
|
||||||
return join(app.getPath("userData"), "plugins");
|
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) => {
|
if (action.response === 0) {
|
||||||
return app.getVersion();
|
autoUpdater.quitAndInstall();
|
||||||
});
|
}
|
||||||
ipcMain.handle("openExternalUrl", async (event, url) => {
|
});
|
||||||
shell.openExternal(url);
|
|
||||||
|
/*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();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
function handleIPCs() {
|
||||||
* Used to delete a file from the user data folder
|
ipcMain.handle(
|
||||||
*/
|
"invokePluginFunc",
|
||||||
ipcMain.handle("deleteFile", async (_event, filePath) => {
|
async (_event, modulePath, method, ...args) => {
|
||||||
const userDataPath = app.getPath("userData");
|
const module = require(/* webpackIgnore: true */ join(
|
||||||
const fullPath = join(userDataPath, filePath);
|
app.getPath("userData"),
|
||||||
|
"plugins",
|
||||||
|
modulePath
|
||||||
|
));
|
||||||
|
requiredModules[modulePath] = module;
|
||||||
|
|
||||||
let result = "NULL";
|
if (typeof module[method] === "function") {
|
||||||
unlink(fullPath, function (err) {
|
return module[method](...args);
|
||||||
if (err && err.code == "ENOENT") {
|
} else {
|
||||||
result = `File not exist: ${err}`;
|
console.log(module[method]);
|
||||||
} else if (err) {
|
console.error(`Function "${method}" does not exist in the module.`);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
});
|
});
|
||||||
|
|
||||||
/*New Update Available*/
|
/**
|
||||||
autoUpdater.on("update-available", async (info: any) => {
|
* Used to download a file from a given url
|
||||||
const action = await dialog.showMessageBox({
|
*/
|
||||||
message: `Update available. Do you want to download the latest update?`,
|
ipcMain.handle("downloadFile", async (_event, url, fileName) => {
|
||||||
buttons: ["Download", "Later"],
|
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) await autoUpdater.downloadUpdate();
|
}
|
||||||
});
|
|
||||||
|
|
||||||
/*App Update Completion Message*/
|
|
||||||
autoUpdater.on("update-downloaded", async (info: any) => {
|
|
||||||
mainWindow?.webContents.send("APP_UPDATE_COMPLETE", {});
|
|
||||||
const action = await dialog.showMessageBox({
|
|
||||||
message: `Update downloaded. Please restart the application to apply the updates.`,
|
|
||||||
buttons: ["Restart", "Later"],
|
|
||||||
});
|
|
||||||
if (action.response === 0) {
|
|
||||||
autoUpdater.quitAndInstall();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/*App Update Error */
|
|
||||||
autoUpdater.on("error", (info: any) => {
|
|
||||||
dialog.showMessageBox({ message: info.message });
|
|
||||||
mainWindow?.webContents.send("APP_UPDATE_ERROR", {});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*App Update Progress */
|
|
||||||
autoUpdater.on("download-progress", (progress: any) => {
|
|
||||||
console.log("app update progress: ", progress.percent);
|
|
||||||
mainWindow?.webContents.send("APP_UPDATE_PROGRESS", {
|
|
||||||
percent: progress.percent,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
|
||||||
if (process.platform !== "darwin") {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function migratePlugins() {
|
function migratePlugins() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
const store = new Store();
|
||||||
if (store.get("migrated_version") !== app.getVersion()) {
|
if (store.get("migrated_version") !== app.getVersion()) {
|
||||||
console.log("start migration:", store.get("migrated_version"));
|
console.log("start migration:", store.get("migrated_version"));
|
||||||
const userDataPath = app.getPath("userData");
|
const userDataPath = app.getPath("userData");
|
||||||
@ -217,12 +218,12 @@ function migratePlugins() {
|
|||||||
resolve(undefined);
|
resolve(undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
function setupPlugins() {
|
function setupPlugins() {
|
||||||
init({
|
init({
|
||||||
// Function to check from the main process that user wants to install a plugin
|
// Function to check from the main process that user wants to install a plugin
|
||||||
confirmInstall: async (plugins: string[]) => {
|
confirmInstall: async (_plugins: string[]) => {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
// Path to install plugin to
|
// Path to install plugin to
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "jan-electron",
|
"name": "jan-electron",
|
||||||
"version": "0.1.1",
|
"version": "0.1.3",
|
||||||
"main": "./build/main.js",
|
"main": "./build/main.js",
|
||||||
"author": "Jan",
|
"author": "Jan <service@jan.ai>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
"build": {
|
"build": {
|
||||||
@ -11,8 +11,9 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"renderer/**/*",
|
"renderer/**/*",
|
||||||
"build/*.{js,map}",
|
"build/*.{js,map}",
|
||||||
"build/core/plugin-manager/**/*",
|
"build/**/*.{js,map}",
|
||||||
"core/pre-install"
|
"core/pre-install",
|
||||||
|
"core/plugin-manager/facade"
|
||||||
],
|
],
|
||||||
"asarUnpack": [
|
"asarUnpack": [
|
||||||
"core/pre-install"
|
"core/pre-install"
|
||||||
@ -31,28 +32,33 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||||
|
"test:e2e": "playwright test --workers=1",
|
||||||
"dev": "tsc -p . && electron .",
|
"dev": "tsc -p . && electron .",
|
||||||
"build": "tsc -p . && electron-builder -p never -mw",
|
"build": "tsc -p . && electron-builder -p never -m",
|
||||||
"build:publish": "tsc -p . && electron-builder -p onTagOrDraft -mw",
|
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
|
||||||
"postinstall": "electron-builder install-app-deps"
|
"build:win32": "tsc -p . && electron-builder -p never -w",
|
||||||
|
"build:linux": "tsc -p . && electron-builder -p never --linux deb",
|
||||||
|
"build:publish": "tsc -p . && electron-builder -p onTagOrDraft -m",
|
||||||
|
"build:publish-darwin": "tsc -p . && electron-builder -p onTagOrDraft -m --x64 --arm64",
|
||||||
|
"build:publish-win32": "tsc -p . && electron-builder -p onTagOrDraft -w",
|
||||||
|
"build:publish-linux": "tsc -p . && electron-builder -p onTagOrDraft --linux deb "
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-is-dev": "^2.0.0",
|
"@npmcli/arborist": "^7.1.0",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^6.1.4",
|
"electron-updater": "^6.1.4",
|
||||||
"node-llama-cpp": "^2.4.1",
|
"pacote": "^17.0.4",
|
||||||
"pluggable-electron": "^0.6.0",
|
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-progress": "^3.0.0"
|
"request-progress": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.38.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||||
"@typescript-eslint/parser": "^6.7.3",
|
"@typescript-eslint/parser": "^6.7.3",
|
||||||
"concurrently": "^8.2.1",
|
|
||||||
"electron": "26.2.1",
|
"electron": "26.2.1",
|
||||||
"electron-builder": "^24.6.4",
|
"electron-builder": "^24.6.4",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"electron-playwright-helpers": "^1.6.0",
|
||||||
"wait-on": "^7.0.1"
|
"eslint-plugin-react": "^7.33.2"
|
||||||
},
|
},
|
||||||
"installConfig": {
|
"installConfig": {
|
||||||
"hoistingLimits": "workspaces"
|
"hoistingLimits": "workspaces"
|
||||||
|
|||||||
10
electron/playwright.config.ts
Normal file
10
electron/playwright.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { PlaywrightTestConfig } from "@playwright/test";
|
||||||
|
|
||||||
|
const config: PlaywrightTestConfig = {
|
||||||
|
testDir: "./tests",
|
||||||
|
testIgnore: "./core/**",
|
||||||
|
retries: 0,
|
||||||
|
timeout: 120000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
|
||||||
// Make Pluggable Electron's facade available to the renderer on window.plugins
|
// Make Pluggable Electron's facade available to the renderer on window.plugins
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const useFacade = require("pluggable-electron/facade");
|
const useFacade = require("../core/plugin-manager/facade");
|
||||||
useFacade();
|
useFacade();
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const { contextBridge, ipcRenderer } = require("electron");
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
@ -18,6 +17,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
|
|
||||||
openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url),
|
openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url),
|
||||||
|
|
||||||
|
relaunch: () => ipcRenderer.invoke("relaunch"),
|
||||||
|
|
||||||
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
|
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
|
||||||
|
|
||||||
downloadFile: (url: string, path: string) =>
|
downloadFile: (url: string, path: string) =>
|
||||||
|
|||||||
47
electron/tests/explore.e2e.spec.ts
Normal file
47
electron/tests/explore.e2e.spec.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { _electron as electron } from "playwright";
|
||||||
|
import { ElectronApplication, Page, expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findLatestBuild,
|
||||||
|
parseElectronApp,
|
||||||
|
stubDialog,
|
||||||
|
} from "electron-playwright-helpers";
|
||||||
|
|
||||||
|
let electronApp: ElectronApplication;
|
||||||
|
let page: Page;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
process.env.CI = "e2e";
|
||||||
|
|
||||||
|
const latestBuild = findLatestBuild("dist");
|
||||||
|
expect(latestBuild).toBeTruthy();
|
||||||
|
|
||||||
|
// parse the packaged Electron app and find paths and other info
|
||||||
|
const appInfo = parseElectronApp(latestBuild);
|
||||||
|
expect(appInfo).toBeTruthy();
|
||||||
|
|
||||||
|
electronApp = await electron.launch({
|
||||||
|
args: [appInfo.main], // main file from package.json
|
||||||
|
executablePath: appInfo.executable, // path to the Electron executable
|
||||||
|
});
|
||||||
|
await stubDialog(electronApp, "showMessageBox", { response: 1 });
|
||||||
|
|
||||||
|
page = await electronApp.firstWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await electronApp.close();
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("explores models", async () => {
|
||||||
|
await page.getByRole("button", { name: "Explore Models" }).first().click();
|
||||||
|
const header = await page
|
||||||
|
.getByRole("heading")
|
||||||
|
.filter({ hasText: "Explore Models" })
|
||||||
|
.first()
|
||||||
|
.isDisabled();
|
||||||
|
expect(header).toBe(false);
|
||||||
|
|
||||||
|
// More test cases here...
|
||||||
|
});
|
||||||
57
electron/tests/main.e2e.spec.ts
Normal file
57
electron/tests/main.e2e.spec.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { _electron as electron } from "playwright";
|
||||||
|
import { ElectronApplication, Page, expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findLatestBuild,
|
||||||
|
parseElectronApp,
|
||||||
|
stubDialog,
|
||||||
|
} from "electron-playwright-helpers";
|
||||||
|
|
||||||
|
let electronApp: ElectronApplication;
|
||||||
|
let page: Page;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
process.env.CI = "e2e";
|
||||||
|
|
||||||
|
const latestBuild = findLatestBuild("dist");
|
||||||
|
expect(latestBuild).toBeTruthy();
|
||||||
|
|
||||||
|
// parse the packaged Electron app and find paths and other info
|
||||||
|
const appInfo = parseElectronApp(latestBuild);
|
||||||
|
expect(appInfo).toBeTruthy();
|
||||||
|
expect(appInfo.asar).toBe(true);
|
||||||
|
expect(appInfo.executable).toBeTruthy();
|
||||||
|
expect(appInfo.main).toBeTruthy();
|
||||||
|
expect(appInfo.name).toBe("jan-electron");
|
||||||
|
expect(appInfo.packageJson).toBeTruthy();
|
||||||
|
expect(appInfo.packageJson.name).toBe("jan-electron");
|
||||||
|
expect(appInfo.platform).toBeTruthy();
|
||||||
|
expect(appInfo.platform).toBe(process.platform);
|
||||||
|
expect(appInfo.resourcesDir).toBeTruthy();
|
||||||
|
|
||||||
|
electronApp = await electron.launch({
|
||||||
|
args: [appInfo.main], // main file from package.json
|
||||||
|
executablePath: appInfo.executable, // path to the Electron executable
|
||||||
|
});
|
||||||
|
await stubDialog(electronApp, "showMessageBox", { response: 1 });
|
||||||
|
|
||||||
|
page = await electronApp.firstWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await electronApp.close();
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders the home page", async () => {
|
||||||
|
expect(page).toBeDefined();
|
||||||
|
|
||||||
|
// Welcome text is available
|
||||||
|
const welcomeText = await page
|
||||||
|
.locator(".text-5xl", {
|
||||||
|
hasText: "Welcome,let’s download your first model",
|
||||||
|
})
|
||||||
|
.first()
|
||||||
|
.isDisabled();
|
||||||
|
expect(welcomeText).toBe(false);
|
||||||
|
});
|
||||||
46
electron/tests/my-models.e2e.spec.ts
Normal file
46
electron/tests/my-models.e2e.spec.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { _electron as electron } from "playwright";
|
||||||
|
import { ElectronApplication, Page, expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findLatestBuild,
|
||||||
|
parseElectronApp,
|
||||||
|
stubDialog,
|
||||||
|
} from "electron-playwright-helpers";
|
||||||
|
|
||||||
|
let electronApp: ElectronApplication;
|
||||||
|
let page: Page;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
process.env.CI = "e2e";
|
||||||
|
|
||||||
|
const latestBuild = findLatestBuild("dist");
|
||||||
|
expect(latestBuild).toBeTruthy();
|
||||||
|
|
||||||
|
// parse the packaged Electron app and find paths and other info
|
||||||
|
const appInfo = parseElectronApp(latestBuild);
|
||||||
|
expect(appInfo).toBeTruthy();
|
||||||
|
|
||||||
|
electronApp = await electron.launch({
|
||||||
|
args: [appInfo.main], // main file from package.json
|
||||||
|
executablePath: appInfo.executable, // path to the Electron executable
|
||||||
|
});
|
||||||
|
await stubDialog(electronApp, "showMessageBox", { response: 1 });
|
||||||
|
|
||||||
|
page = await electronApp.firstWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await electronApp.close();
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows my models", async () => {
|
||||||
|
await page.getByRole("button", { name: "My Models" }).first().click();
|
||||||
|
const header = await page
|
||||||
|
.getByRole("heading")
|
||||||
|
.filter({ hasText: "My Models" })
|
||||||
|
.first()
|
||||||
|
.isDisabled();
|
||||||
|
expect(header).toBe(false);
|
||||||
|
// More test cases here...
|
||||||
|
});
|
||||||
76
electron/tests/navigation.e2e.spec.ts
Normal file
76
electron/tests/navigation.e2e.spec.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { _electron as electron } from "playwright";
|
||||||
|
import { ElectronApplication, Page, expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findLatestBuild,
|
||||||
|
parseElectronApp,
|
||||||
|
stubDialog,
|
||||||
|
} from "electron-playwright-helpers";
|
||||||
|
|
||||||
|
let electronApp: ElectronApplication;
|
||||||
|
let page: Page;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
process.env.CI = "e2e";
|
||||||
|
|
||||||
|
const latestBuild = findLatestBuild("dist");
|
||||||
|
expect(latestBuild).toBeTruthy();
|
||||||
|
|
||||||
|
// parse the packaged Electron app and find paths and other info
|
||||||
|
const appInfo = parseElectronApp(latestBuild);
|
||||||
|
expect(appInfo).toBeTruthy();
|
||||||
|
|
||||||
|
electronApp = await electron.launch({
|
||||||
|
args: [appInfo.main], // main file from package.json
|
||||||
|
executablePath: appInfo.executable, // path to the Electron executable
|
||||||
|
});
|
||||||
|
await stubDialog(electronApp, "showMessageBox", { response: 1 });
|
||||||
|
|
||||||
|
page = await electronApp.firstWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await electronApp.close();
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders left navigation panel", async () => {
|
||||||
|
// Chat History section is available
|
||||||
|
const chatSection = await page
|
||||||
|
.getByRole("heading")
|
||||||
|
.filter({ hasText: "CHAT HISTORY" })
|
||||||
|
.first()
|
||||||
|
.isDisabled();
|
||||||
|
expect(chatSection).toBe(false);
|
||||||
|
|
||||||
|
// Home actions
|
||||||
|
const newChatBtn = await page
|
||||||
|
.getByRole("button", { name: "New Chat" })
|
||||||
|
.first()
|
||||||
|
.isEnabled();
|
||||||
|
const exploreBtn = await page
|
||||||
|
.getByRole("button", { name: "Explore Models" })
|
||||||
|
.first()
|
||||||
|
.isEnabled();
|
||||||
|
const discordBtn = await page
|
||||||
|
.getByRole("button", { name: "Discord" })
|
||||||
|
.first()
|
||||||
|
.isEnabled();
|
||||||
|
const myModelsBtn = await page
|
||||||
|
.getByRole("button", { name: "My Models" })
|
||||||
|
.first()
|
||||||
|
.isEnabled();
|
||||||
|
const settingsBtn = await page
|
||||||
|
.getByRole("button", { name: "Settings" })
|
||||||
|
.first()
|
||||||
|
.isEnabled();
|
||||||
|
expect(
|
||||||
|
[
|
||||||
|
newChatBtn,
|
||||||
|
exploreBtn,
|
||||||
|
discordBtn,
|
||||||
|
myModelsBtn,
|
||||||
|
settingsBtn,
|
||||||
|
].filter((e) => !e).length
|
||||||
|
).toBe(0);
|
||||||
|
});
|
||||||
42
electron/tests/settings.e2e.spec.ts
Normal file
42
electron/tests/settings.e2e.spec.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { _electron as electron } from "playwright";
|
||||||
|
import { ElectronApplication, Page, expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findLatestBuild,
|
||||||
|
parseElectronApp,
|
||||||
|
stubDialog,
|
||||||
|
} from "electron-playwright-helpers";
|
||||||
|
|
||||||
|
let electronApp: ElectronApplication;
|
||||||
|
let page: Page;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
process.env.CI = "e2e";
|
||||||
|
|
||||||
|
const latestBuild = findLatestBuild("dist");
|
||||||
|
expect(latestBuild).toBeTruthy();
|
||||||
|
|
||||||
|
// parse the packaged Electron app and find paths and other info
|
||||||
|
const appInfo = parseElectronApp(latestBuild);
|
||||||
|
expect(appInfo).toBeTruthy();
|
||||||
|
|
||||||
|
electronApp = await electron.launch({
|
||||||
|
args: [appInfo.main], // main file from package.json
|
||||||
|
executablePath: appInfo.executable, // path to the Electron executable
|
||||||
|
});
|
||||||
|
await stubDialog(electronApp, "showMessageBox", { response: 1 });
|
||||||
|
|
||||||
|
page = await electronApp.firstWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await electronApp.close();
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows settings", async () => {
|
||||||
|
await page.getByRole("button", { name: "Settings" }).first().click();
|
||||||
|
|
||||||
|
const pluginList = await page.getByTestId("plugin-item").count();
|
||||||
|
expect(pluginList).toBe(4);
|
||||||
|
});
|
||||||
@ -2,13 +2,17 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
|
"noImplicitAny": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"outDir": "./build",
|
"outDir": "./build",
|
||||||
"rootDir": "./",
|
"rootDir": "./",
|
||||||
"noEmitOnError": true,
|
"noEmitOnError": true,
|
||||||
|
"baseUrl": ".",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
"paths": { "*": ["node_modules/*"] },
|
||||||
"typeRoots": ["node_modules/@types"]
|
"typeRoots": ["node_modules/@types"]
|
||||||
},
|
},
|
||||||
"exclude": ["core", "build", "node_modules"]
|
"include": ["./**/*.ts"],
|
||||||
|
"exclude": ["core", "build", "dist", "tests"]
|
||||||
}
|
}
|
||||||
|
|||||||
8
electron/utils/disposable.ts
Normal file
8
electron/utils/disposable.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export function dispose(requiredModules: Record<string, any>) {
|
||||||
|
for (const key in requiredModules) {
|
||||||
|
const module = requiredModules[key];
|
||||||
|
if (typeof module["dispose"] === "function") {
|
||||||
|
module["dispose"]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
electron/utils/menu.ts
Normal file
111
electron/utils/menu.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
const { app, Menu, dialog } = require("electron");
|
||||||
|
const isMac = process.platform === "darwin";
|
||||||
|
const { autoUpdater } = require("electron-updater");
|
||||||
|
import { compareSemanticVersions } from "./versionDiff";
|
||||||
|
|
||||||
|
const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
|
||||||
|
...(isMac
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: app.name,
|
||||||
|
submenu: [
|
||||||
|
{ role: "about" },
|
||||||
|
{
|
||||||
|
label: "Check for Updates...",
|
||||||
|
click: () =>
|
||||||
|
autoUpdater.checkForUpdatesAndNotify().then((e) => {
|
||||||
|
if (
|
||||||
|
!e ||
|
||||||
|
compareSemanticVersions(
|
||||||
|
app.getVersion(),
|
||||||
|
e.updateInfo.version
|
||||||
|
) >= 0
|
||||||
|
)
|
||||||
|
dialog.showMessageBox({
|
||||||
|
message: `There are currently no updates available.`,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "services" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "hide" },
|
||||||
|
{ role: "hideOthers" },
|
||||||
|
{ role: "unhide" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "quit" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
submenu: [
|
||||||
|
{ role: "undo" },
|
||||||
|
{ role: "redo" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "cut" },
|
||||||
|
{ role: "copy" },
|
||||||
|
{ role: "paste" },
|
||||||
|
...(isMac
|
||||||
|
? [
|
||||||
|
{ role: "pasteAndMatchStyle" },
|
||||||
|
{ role: "delete" },
|
||||||
|
{ role: "selectAll" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Speech",
|
||||||
|
submenu: [{ role: "startSpeaking" }, { role: "stopSpeaking" }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "View",
|
||||||
|
submenu: [
|
||||||
|
{ role: "reload" },
|
||||||
|
{ role: "forceReload" },
|
||||||
|
{ role: "toggleDevTools" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "resetZoom" },
|
||||||
|
{ role: "zoomIn" },
|
||||||
|
{ role: "zoomOut" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "togglefullscreen" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Window",
|
||||||
|
submenu: [
|
||||||
|
{ role: "minimize" },
|
||||||
|
{ role: "zoom" },
|
||||||
|
...(isMac
|
||||||
|
? [
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "front" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "window" },
|
||||||
|
]
|
||||||
|
: [{ role: "close" }]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "help",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Learn More",
|
||||||
|
click: async () => {
|
||||||
|
const { shell } = require("electron");
|
||||||
|
await shell.openExternal("https://jan.ai/");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const setupMenu = () => {
|
||||||
|
const menu = Menu.buildFromTemplate(template);
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
};
|
||||||
21
electron/utils/versionDiff.ts
Normal file
21
electron/utils/versionDiff.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export const compareSemanticVersions = (a: string, b: string) => {
|
||||||
|
|
||||||
|
// 1. Split the strings into their parts.
|
||||||
|
const a1 = a.split('.');
|
||||||
|
const b1 = b.split('.');
|
||||||
|
// 2. Contingency in case there's a 4th or 5th version
|
||||||
|
const len = Math.min(a1.length, b1.length);
|
||||||
|
// 3. Look through each version number and compare.
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const a2 = +a1[ i ] || 0;
|
||||||
|
const b2 = +b1[ i ] || 0;
|
||||||
|
|
||||||
|
if (a2 !== b2) {
|
||||||
|
return a2 > b2 ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. We hit this if the all checked versions so far are equal
|
||||||
|
//
|
||||||
|
return b1.length - a1.length;
|
||||||
|
};
|
||||||
17
package.json
17
package.json
@ -14,19 +14,28 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"lint": "yarn workspace jan-electron lint && yarn workspace jan-web lint",
|
||||||
|
"test": "yarn workspace jan-electron test:e2e",
|
||||||
"dev:electron": "yarn workspace jan-electron dev",
|
"dev:electron": "yarn workspace jan-electron dev",
|
||||||
"dev:web": "yarn workspace jan-web dev",
|
"dev:web": "yarn workspace jan-web dev",
|
||||||
"dev": "concurrently --kill-others-on-fail \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
|
"dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
|
||||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||||
"build:electron": "yarn workspace jan-electron build",
|
"build:electron": "yarn workspace jan-electron build",
|
||||||
"build:plugins": "rm -f ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/model-management-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm 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": "yarn build:web && yarn build:electron",
|
"build": "yarn build:web && yarn build:electron",
|
||||||
"build:publish": "yarn build:web && yarn workspace jan-electron build:publish"
|
"build:darwin": "yarn build:web && yarn workspace jan-electron build:darwin",
|
||||||
|
"build:win32": "yarn build:web && yarn workspace jan-electron build:win32",
|
||||||
|
"build:linux": "yarn build:web && yarn workspace jan-electron build:linux",
|
||||||
|
"build:publish": "yarn build:web && yarn workspace jan-electron build:publish",
|
||||||
|
"build:publish-darwin": "yarn build:web && yarn workspace jan-electron build:publish-darwin",
|
||||||
|
"build:publish-win32": "yarn build:web && yarn workspace jan-electron build:publish-win32",
|
||||||
|
"build:publish-linux": "yarn build:web && yarn workspace jan-electron build:publish-linux"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^8.2.1",
|
"concurrently": "^8.2.1",
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"wait-on": "^7.0.1"
|
"wait-on": "^7.0.1",
|
||||||
|
"rimraf": "^3.0.2"
|
||||||
},
|
},
|
||||||
"version": "0.0.0"
|
"version": "0.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
19
web/app/_components/ActiveModelTable/index.tsx
Normal file
19
web/app/_components/ActiveModelTable/index.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import React, { Fragment } from "react";
|
||||||
|
import ModelTable from "../ModelTable";
|
||||||
|
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
|
|
||||||
|
const ActiveModelTable: React.FC = () => {
|
||||||
|
const activeModel = useAtomValue(currentProductAtom);
|
||||||
|
|
||||||
|
if (!activeModel) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<h3 className="text-xl leading-[25px] mb-[13px]">Active Model(s)</h3>
|
||||||
|
<ModelTable models={[activeModel]} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActiveModelTable;
|
||||||
@ -2,9 +2,8 @@ import { Product } from "@/_models/Product";
|
|||||||
import DownloadModelContent from "../DownloadModelContent";
|
import DownloadModelContent from "../DownloadModelContent";
|
||||||
import ModelDownloadButton from "../ModelDownloadButton";
|
import ModelDownloadButton from "../ModelDownloadButton";
|
||||||
import ModelDownloadingButton from "../ModelDownloadingButton";
|
import ModelDownloadingButton from "../ModelDownloadingButton";
|
||||||
import ViewModelDetailButton from "../ViewModelDetailButton";
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { modelDownloadStateAtom } from "@/_helpers/JotaiWrapper";
|
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
product: Product;
|
product: Product;
|
||||||
@ -36,8 +35,6 @@ const AvailableModelCard: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleViewDetails = () => {};
|
|
||||||
|
|
||||||
const downloadButton = isDownloading ? (
|
const downloadButton = isDownloading ? (
|
||||||
<div className="w-1/5 flex items-start justify-end">
|
<div className="w-1/5 flex items-start justify-end">
|
||||||
<ModelDownloadingButton total={total} value={transferred} />
|
<ModelDownloadingButton total={total} value={transferred} />
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper";
|
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import SecondaryButton from "../SecondaryButton";
|
import SecondaryButton from "../SecondaryButton";
|
||||||
import SendButton from "../SendButton";
|
import SendButton from "../SendButton";
|
||||||
|
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
|
|
||||||
const BasicPromptAccessories: React.FC = () => {
|
const BasicPromptAccessories: React.FC = () => {
|
||||||
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom);
|
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { ChevronLeftIcon } from "@heroicons/react/24/outline";
|
import { ChevronLeftIcon } from "@heroicons/react/24/outline";
|
||||||
import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper";
|
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
|
|
||||||
const BasicPromptButton: React.FC = () => {
|
const BasicPromptButton: React.FC = () => {
|
||||||
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom);
|
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom);
|
||||||
|
|||||||
@ -1,22 +1,45 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
|
import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
|
||||||
|
import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||||
|
import { selectedModelAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
|
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||||
|
import useInitModel from "@/_hooks/useInitModel";
|
||||||
import useSendChatMessage from "@/_hooks/useSendChatMessage";
|
import useSendChatMessage from "@/_hooks/useSendChatMessage";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
|
import { ChangeEvent } from "react";
|
||||||
|
|
||||||
const BasicPromptInput: React.FC = () => {
|
const BasicPromptInput: React.FC = () => {
|
||||||
|
const activeConversationId = useAtomValue(getActiveConvoIdAtom);
|
||||||
|
const selectedModel = useAtomValue(selectedModelAtom);
|
||||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom);
|
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom);
|
||||||
const { sendChatMessage } = useSendChatMessage();
|
const { sendChatMessage } = useSendChatMessage();
|
||||||
|
const { requestCreateConvo } = useCreateConversation();
|
||||||
|
|
||||||
const handleMessageChange = (event: any) => {
|
const { initModel } = useInitModel();
|
||||||
|
|
||||||
|
const handleMessageChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setCurrentPrompt(event.target.value);
|
setCurrentPrompt(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (event: any) => {
|
const handleKeyDown = async (
|
||||||
|
event: React.KeyboardEvent<HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
if (!event.shiftKey) {
|
if (!event.shiftKey) {
|
||||||
event.preventDefault();
|
if (activeConversationId) {
|
||||||
sendChatMessage();
|
event.preventDefault();
|
||||||
|
sendChatMessage();
|
||||||
|
} else {
|
||||||
|
if (!selectedModel) {
|
||||||
|
console.log("No model selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await requestCreateConvo(selectedModel);
|
||||||
|
await initModel(selectedModel);
|
||||||
|
sendChatMessage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,14 +4,12 @@ import React, { useCallback, useRef, useState } from "react";
|
|||||||
import ChatItem from "../ChatItem";
|
import ChatItem from "../ChatItem";
|
||||||
import { ChatMessage } from "@/_models/ChatMessage";
|
import { ChatMessage } from "@/_models/ChatMessage";
|
||||||
import useChatMessages from "@/_hooks/useChatMessages";
|
import useChatMessages from "@/_hooks/useChatMessages";
|
||||||
import {
|
import { showingTyping } from "@/_helpers/JotaiWrapper";
|
||||||
chatMessages,
|
|
||||||
getActiveConvoIdAtom,
|
|
||||||
showingTyping,
|
|
||||||
} from "@/_helpers/JotaiWrapper";
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { selectAtom } from "jotai/utils";
|
import { selectAtom } from "jotai/utils";
|
||||||
import LoadingIndicator from "../LoadingIndicator";
|
import LoadingIndicator from "../LoadingIndicator";
|
||||||
|
import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||||
|
import { chatMessages } from "@/_helpers/atoms/ChatMessage.atom";
|
||||||
|
|
||||||
const ChatBody: React.FC = () => {
|
const ChatBody: React.FC = () => {
|
||||||
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? "";
|
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? "";
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import SimpleTextMessage from "../SimpleTextMessage";
|
|||||||
import { ChatMessage, MessageType } from "@/_models/ChatMessage";
|
import { ChatMessage, MessageType } from "@/_models/ChatMessage";
|
||||||
import StreamTextMessage from "../StreamTextMessage";
|
import StreamTextMessage from "../StreamTextMessage";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { currentStreamingMessageAtom } from "@/_helpers/JotaiWrapper";
|
import { currentStreamingMessageAtom } from "@/_helpers/atoms/ChatMessage.atom";
|
||||||
|
|
||||||
export default function renderChatMessage({
|
export default function renderChatMessage({
|
||||||
id,
|
id,
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { MainViewState, getMainViewStateAtom } from "@/_helpers/JotaiWrapper";
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import ModelManagement from "../ModelManagement";
|
|
||||||
import Welcome from "../WelcomeContainer";
|
import Welcome from "../WelcomeContainer";
|
||||||
import { Preferences } from "../Preferences";
|
import { Preferences } from "../Preferences";
|
||||||
|
import MyModelContainer from "../MyModelContainer";
|
||||||
|
import ExploreModelContainer from "../ExploreModelContainer";
|
||||||
|
import {
|
||||||
|
MainViewState,
|
||||||
|
getMainViewStateAtom,
|
||||||
|
} from "@/_helpers/atoms/MainView.atom";
|
||||||
|
import EmptyChatContainer from "../EmptyChatContainer";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -15,12 +20,15 @@ export default function ChatContainer({ children }: Props) {
|
|||||||
const viewState = useAtomValue(getMainViewStateAtom);
|
const viewState = useAtomValue(getMainViewStateAtom);
|
||||||
|
|
||||||
switch (viewState) {
|
switch (viewState) {
|
||||||
|
case MainViewState.ConversationEmptyModel:
|
||||||
|
return <EmptyChatContainer />
|
||||||
case MainViewState.ExploreModel:
|
case MainViewState.ExploreModel:
|
||||||
return <ModelManagement />;
|
return <ExploreModelContainer />;
|
||||||
case MainViewState.Setting:
|
case MainViewState.Setting:
|
||||||
return <Preferences />;
|
return <Preferences />;
|
||||||
case MainViewState.ResourceMonitor:
|
case MainViewState.ResourceMonitor:
|
||||||
case MainViewState.MyModel:
|
case MainViewState.MyModel:
|
||||||
|
return <MyModelContainer />;
|
||||||
case MainViewState.Welcome:
|
case MainViewState.Welcome:
|
||||||
return <Welcome />;
|
return <Welcome />;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import JanImage from "../JanImage";
|
import JanImage from "../JanImage";
|
||||||
import { setActiveConvoIdAtom } from "@/_helpers/JotaiWrapper";
|
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
|
import { setActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||||
|
|
||||||
const CompactLogo: React.FC = () => {
|
const CompactLogo: React.FC = () => {
|
||||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
|
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/JotaiWrapper";
|
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
import useDeleteConversation from "@/_hooks/useDeleteConversation";
|
import useDeleteConversation from "@/_hooks/useDeleteConversation";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
|
|||||||
@ -1,17 +1,13 @@
|
|||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
|
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
|
||||||
import { showConfirmDeleteModalAtom } from "@/_helpers/JotaiWrapper";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import useSignOut from "@/_hooks/useSignOut";
|
import { showConfirmDeleteModalAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
|
|
||||||
const ConfirmDeleteModelModal: React.FC = () => {
|
const ConfirmDeleteModelModal: React.FC = () => {
|
||||||
const [show, setShow] = useAtom(showConfirmDeleteModalAtom);
|
const [show, setShow] = useAtom(showConfirmDeleteModalAtom);
|
||||||
const { signOut } = useSignOut();
|
|
||||||
|
|
||||||
const onLogOutClick = () => {
|
const onConfirmDelete = () => {};
|
||||||
signOut().then(() => setShow(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={show} as={Fragment}>
|
<Transition.Root show={show} as={Fragment}>
|
||||||
@ -65,7 +61,7 @@ const ConfirmDeleteModelModal: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
|
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
|
||||||
onClick={onLogOutClick}
|
onClick={onConfirmDelete}
|
||||||
>
|
>
|
||||||
Log out
|
Log out
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
|
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
|
||||||
import { showConfirmSignOutModalAtom } from "@/_helpers/JotaiWrapper";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import useSignOut from "@/_hooks/useSignOut";
|
import useSignOut from "@/_hooks/useSignOut";
|
||||||
|
import { showConfirmSignOutModalAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
|
|
||||||
const ConfirmSignOutModal: React.FC = () => {
|
const ConfirmSignOutModal: React.FC = () => {
|
||||||
const [show, setShow] = useAtom(showConfirmSignOutModalAtom);
|
const [show, setShow] = useAtom(showConfirmSignOutModalAtom);
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
import { Product } from "@/_models/Product";
|
import { Product } from "@/_models/Product";
|
||||||
import DownloadModelContent from "../DownloadModelContent";
|
import DownloadModelContent from "../DownloadModelContent";
|
||||||
import ViewModelDetailButton from "../ViewModelDetailButton";
|
|
||||||
import { executeSerial } from "@/_services/pluginService";
|
|
||||||
import { InfereceService } from "../../../shared/coreService";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
product: Product;
|
product: Product;
|
||||||
@ -17,28 +14,22 @@ const DownloadedModelCard: React.FC<Props> = ({
|
|||||||
isRecommend,
|
isRecommend,
|
||||||
required,
|
required,
|
||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
}) => {
|
}) => (
|
||||||
|
<div className="border rounded-lg border-gray-200">
|
||||||
const handleViewDetails = () => {};
|
<div className="flex justify-between py-4 px-3 gap-[10px]">
|
||||||
|
<DownloadModelContent
|
||||||
return (
|
required={required}
|
||||||
<div className="border rounded-lg border-gray-200">
|
author={product.author}
|
||||||
<div className="flex justify-between py-4 px-3 gap-[10px]">
|
description={product.description}
|
||||||
<DownloadModelContent
|
isRecommend={isRecommend}
|
||||||
required={required}
|
name={product.name}
|
||||||
author={product.author}
|
type={product.type}
|
||||||
description={product.description}
|
/>
|
||||||
isRecommend={isRecommend}
|
<div className="flex flex-col justify-center">
|
||||||
name={product.name}
|
<button onClick={() => onDeleteClick?.(product)}>Delete</button>
|
||||||
type={product.type}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col justify-center">
|
|
||||||
<button onClick={() => onDeleteClick?.(product)}>Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/* <ViewModelDetailButton callback={handleViewDetails} /> */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
);
|
||||||
|
|
||||||
export default DownloadedModelCard;
|
export default DownloadedModelCard;
|
||||||
|
|||||||
20
web/app/_components/DownloadedModelTable/index.tsx
Normal file
20
web/app/_components/DownloadedModelTable/index.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React, { Fragment } from "react";
|
||||||
|
import SearchBar from "../SearchBar";
|
||||||
|
import ModelTable from "../ModelTable";
|
||||||
|
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
||||||
|
|
||||||
|
const DownloadedModelTable: React.FC = () => {
|
||||||
|
const { downloadedModels } = useGetDownloadedModels();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<h3 className="text-xl leading-[25px] mt-[50px]">Downloaded Models</h3>
|
||||||
|
<div className="py-5 w-[568px]">
|
||||||
|
<SearchBar />
|
||||||
|
</div>
|
||||||
|
<ModelTable models={downloadedModels} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DownloadedModelTable;
|
||||||
14
web/app/_components/EmptyChatContainer/index.tsx
Normal file
14
web/app/_components/EmptyChatContainer/index.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from "react";
|
||||||
|
import SelectModels from "../ModelSelector";
|
||||||
|
import InputToolbar from "../InputToolbar";
|
||||||
|
|
||||||
|
const EmptyChatContainer: React.FC = () => (
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<div className="flex flex-1 items-center justify-center">
|
||||||
|
<SelectModels />
|
||||||
|
</div>
|
||||||
|
<InputToolbar />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EmptyChatContainer;
|
||||||
55
web/app/_components/ExploreModelContainer/index.tsx
Normal file
55
web/app/_components/ExploreModelContainer/index.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import useGetAvailableModels from "@/_hooks/useGetAvailableModels";
|
||||||
|
import ExploreModelItem from "../ExploreModelItem";
|
||||||
|
import HeaderTitle from "../HeaderTitle";
|
||||||
|
import SearchBar from "../SearchBar";
|
||||||
|
import SimpleCheckbox from "../SimpleCheckbox";
|
||||||
|
import SimpleTag, { TagType } from "../SimpleTag";
|
||||||
|
|
||||||
|
const tags = [
|
||||||
|
"Roleplay",
|
||||||
|
"Llama",
|
||||||
|
"Story",
|
||||||
|
"Casual",
|
||||||
|
"Professional",
|
||||||
|
"CodeLlama",
|
||||||
|
"Coding",
|
||||||
|
];
|
||||||
|
const checkboxs = ["GGUF", "TensorRT", "Meow", "JigglyPuff"];
|
||||||
|
|
||||||
|
const ExploreModelContainer: React.FC = () => {
|
||||||
|
const { allAvailableModels } = useGetAvailableModels();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
|
||||||
|
<HeaderTitle title="Explore Models" />
|
||||||
|
<SearchBar placeholder="Search or HuggingFace URL" />
|
||||||
|
<div className="flex gap-x-14 mt-[38px]">
|
||||||
|
<div className="flex-1 flex-shrink-0">
|
||||||
|
<h2 className="font-semibold text-xs mb-[15px]">Tags</h2>
|
||||||
|
<SearchBar placeholder="Filter by tags" />
|
||||||
|
<div className="flex flex-wrap gap-[9px] mt-[14px]">
|
||||||
|
{tags.map((item) => (
|
||||||
|
<SimpleTag key={item} title={item} type={item as TagType} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<hr className="my-10" />
|
||||||
|
<fieldset>
|
||||||
|
{checkboxs.map((item) => (
|
||||||
|
<SimpleCheckbox key={item} name={item} />
|
||||||
|
))}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div className="flex-[3_3_0%]">
|
||||||
|
<h2 className="font-semibold text-xs mb-[18px]">Results</h2>
|
||||||
|
<div className="flex flex-col gap-[31px]">
|
||||||
|
{allAvailableModels.map((item) => (
|
||||||
|
<ExploreModelItem key={item.id} model={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExploreModelContainer;
|
||||||
101
web/app/_components/ExploreModelItem/index.tsx
Normal file
101
web/app/_components/ExploreModelItem/index.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import ExploreModelItemHeader from "../ExploreModelItemHeader";
|
||||||
|
import ModelVersionList from "../ModelVersionList";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Product } from "@/_models/Product";
|
||||||
|
import SimpleTag, { TagType } from "../SimpleTag";
|
||||||
|
import { displayDate } from "@/_utils/datetime";
|
||||||
|
import useDownloadModel from "@/_hooks/useDownloadModel";
|
||||||
|
import { atom, useAtomValue } from "jotai";
|
||||||
|
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
model: Product;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExploreModelItem: React.FC<Props> = ({ model }) => {
|
||||||
|
const downloadAtom = useMemo(
|
||||||
|
() => atom((get) => get(modelDownloadStateAtom)[model.fileName ?? ""]),
|
||||||
|
[model.fileName ?? ""]
|
||||||
|
);
|
||||||
|
const downloadState = useAtomValue(downloadAtom);
|
||||||
|
const { downloadModel } = useDownloadModel();
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col border border-gray-200 rounded-[5px]">
|
||||||
|
<ExploreModelItemHeader
|
||||||
|
name={model.name}
|
||||||
|
status={TagType.Recommended}
|
||||||
|
total={model.totalSize}
|
||||||
|
downloadState={downloadState}
|
||||||
|
onDownloadClick={() => downloadModel(model)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col px-[26px] py-[22px]">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex-1 flex flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="text-sm font-medium text-gray-500">
|
||||||
|
Model Format
|
||||||
|
</div>
|
||||||
|
<div className="px-[10px] py-0.5 bg-gray-100 text-xs text-gray-800 w-fit">
|
||||||
|
GGUF
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-sm font-medium text-gray-500">
|
||||||
|
Hardware Compatibility
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<SimpleTag
|
||||||
|
clickable={false}
|
||||||
|
title={TagType.Compatible}
|
||||||
|
type={TagType.Compatible}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex flex-col gap-8">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-500">
|
||||||
|
Release Date
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-normal text-gray-900">
|
||||||
|
{displayDate(model.releaseDate)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-medium text-gray-500">
|
||||||
|
Expected Performance
|
||||||
|
</div>
|
||||||
|
<SimpleTag
|
||||||
|
title={TagType.Medium}
|
||||||
|
type={TagType.Medium}
|
||||||
|
clickable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1 mt-[26px]">
|
||||||
|
<span className="text-sm font-medium text-gray-500">About</span>
|
||||||
|
<span className="text-sm font-normal text-gray-500">
|
||||||
|
{model.longDescription}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium text-gray-500">Tags</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{show && <ModelVersionList />}
|
||||||
|
<button
|
||||||
|
onClick={() => setShow(!show)}
|
||||||
|
className="bg-[#FBFBFB] text-gray-500 text-sm text-left py-2 px-4 border-t border-gray-200"
|
||||||
|
>
|
||||||
|
{!show ? "+ Show Available Versions" : "- Collapse"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExploreModelItem;
|
||||||
44
web/app/_components/ExploreModelItemHeader/index.tsx
Normal file
44
web/app/_components/ExploreModelItemHeader/index.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import SimpleTag, { TagType } from "../SimpleTag";
|
||||||
|
import PrimaryButton from "../PrimaryButton";
|
||||||
|
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
|
||||||
|
import { DownloadState } from "@/_models/DownloadState";
|
||||||
|
import SecondaryButton from "../SecondaryButton";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string;
|
||||||
|
total: number;
|
||||||
|
status: TagType;
|
||||||
|
downloadState?: DownloadState;
|
||||||
|
onDownloadClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExploreModelItemHeader: React.FC<Props> = ({
|
||||||
|
name,
|
||||||
|
status,
|
||||||
|
total,
|
||||||
|
downloadState,
|
||||||
|
onDownloadClick,
|
||||||
|
}) => (
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>{name}</span>
|
||||||
|
<SimpleTag title={status} type={status} clickable={false} />
|
||||||
|
</div>
|
||||||
|
{downloadState != null ? (
|
||||||
|
<SecondaryButton
|
||||||
|
disabled
|
||||||
|
title={`Downloading (${formatDownloadPercentage(
|
||||||
|
downloadState.percent
|
||||||
|
)})`}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PrimaryButton
|
||||||
|
title={total ? `Download (${toGigabytes(total)})` : "Download"}
|
||||||
|
onClick={() => onDownloadClick?.()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ExploreModelItemHeader;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { showingMobilePaneAtom } from "@/_helpers/JotaiWrapper";
|
import { showingMobilePaneAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
import { Bars3Icon } from "@heroicons/react/24/outline";
|
import { Bars3Icon } from "@heroicons/react/24/outline";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|||||||
@ -1,22 +1,20 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import JanImage from "../JanImage";
|
import JanImage from "../JanImage";
|
||||||
import {
|
|
||||||
MainViewState,
|
|
||||||
activeModel,
|
|
||||||
conversationStatesAtom,
|
|
||||||
currentProductAtom,
|
|
||||||
getActiveConvoIdAtom,
|
|
||||||
setActiveConvoIdAtom,
|
|
||||||
setMainViewStateAtom,
|
|
||||||
} from "@/_helpers/JotaiWrapper";
|
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Conversation } from "@/_models/Conversation";
|
import { Conversation } from "@/_models/Conversation";
|
||||||
import { DataService, InfereceService } from "../../../shared/coreService";
|
import { DataService } from "../../../shared/coreService";
|
||||||
|
import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager";
|
||||||
import {
|
import {
|
||||||
execute,
|
conversationStatesAtom,
|
||||||
executeSerial,
|
getActiveConvoIdAtom,
|
||||||
} from "../../../../electron/core/plugin-manager/execution/extension-manager";
|
setActiveConvoIdAtom,
|
||||||
|
} from "@/_helpers/atoms/Conversation.atom";
|
||||||
|
import {
|
||||||
|
setMainViewStateAtom,
|
||||||
|
MainViewState,
|
||||||
|
} from "@/_helpers/atoms/MainView.atom";
|
||||||
|
import useInitModel from "@/_hooks/useInitModel";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
conversation: Conversation;
|
conversation: Conversation;
|
||||||
@ -36,23 +34,20 @@ const HistoryItem: React.FC<Props> = ({
|
|||||||
const activeConvoId = useAtomValue(getActiveConvoIdAtom);
|
const activeConvoId = useAtomValue(getActiveConvoIdAtom);
|
||||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
|
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
|
||||||
const isSelected = activeConvoId === conversation.id;
|
const isSelected = activeConvoId === conversation.id;
|
||||||
const setActiveModel = useSetAtom(activeModel);
|
|
||||||
const setActiveProduct = useSetAtom(currentProductAtom);
|
const { initModel } = useInitModel();
|
||||||
|
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
const convoModel = await executeSerial(
|
const model = await executeSerial(
|
||||||
DataService.GET_MODEL_BY_ID,
|
DataService.GET_MODEL_BY_ID,
|
||||||
conversation.model_id
|
conversation.model_id
|
||||||
);
|
);
|
||||||
if (!convoModel) {
|
if (!model) {
|
||||||
alert(
|
alert(
|
||||||
`Model ${conversation.model_id} not found! Please re-download the model first.`
|
`Model ${conversation.model_id} not found! Please re-download the model first.`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setActiveProduct(convoModel)
|
initModel(model);
|
||||||
executeSerial(InfereceService.INIT_MODEL, convoModel)
|
|
||||||
.then(() => console.info(`Init model success`))
|
|
||||||
.catch((err) => console.log(`Init model error ${err}`));
|
|
||||||
setActiveModel(convoModel.name);
|
|
||||||
}
|
}
|
||||||
if (activeConvoId !== conversation.id) {
|
if (activeConvoId !== conversation.id) {
|
||||||
setMainViewState(MainViewState.Conversation);
|
setMainViewState(MainViewState.Conversation);
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import HistoryItem from "../HistoryItem";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ExpandableHeader from "../ExpandableHeader";
|
import ExpandableHeader from "../ExpandableHeader";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { searchAtom, userConversationsAtom } from "@/_helpers/JotaiWrapper";
|
import { searchAtom } from "@/_helpers/JotaiWrapper";
|
||||||
import useGetUserConversations from "@/_hooks/useGetUserConversations";
|
import useGetUserConversations from "@/_hooks/useGetUserConversations";
|
||||||
import SidebarEmptyHistory from "../SidebarEmptyHistory";
|
import SidebarEmptyHistory from "../SidebarEmptyHistory";
|
||||||
|
import { userConversationsAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||||
|
|
||||||
const HistoryList: React.FC = () => {
|
const HistoryList: React.FC = () => {
|
||||||
const conversations = useAtomValue(userConversationsAtom);
|
const conversations = useAtomValue(userConversationsAtom);
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import BasicPromptInput from "../BasicPromptInput";
|
import BasicPromptInput from "../BasicPromptInput";
|
||||||
import BasicPromptAccessories from "../BasicPromptAccessories";
|
import BasicPromptAccessories from "../BasicPromptAccessories";
|
||||||
import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper";
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
|
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
|
|
||||||
const InputToolbar: React.FC = () => {
|
const InputToolbar: React.FC = () => {
|
||||||
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom);
|
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { setActiveConvoIdAtom } from "@/_helpers/JotaiWrapper";
|
import { setActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|||||||
@ -3,10 +3,12 @@ import SidebarFooter from "../SidebarFooter";
|
|||||||
import SidebarHeader from "../SidebarHeader";
|
import SidebarHeader from "../SidebarHeader";
|
||||||
import SidebarMenu from "../SidebarMenu";
|
import SidebarMenu from "../SidebarMenu";
|
||||||
import HistoryList from "../HistoryList";
|
import HistoryList from "../HistoryList";
|
||||||
|
import NewChatButton from "../NewChatButton";
|
||||||
|
|
||||||
const LeftContainer: React.FC = () => (
|
const LeftContainer: React.FC = () => (
|
||||||
<div className="w-[323px] flex-shrink-0 p-3 h-screen border-r border-gray-200 flex flex-col">
|
<div className="w-[323px] flex-shrink-0 p-3 h-screen border-r border-gray-200 flex flex-col">
|
||||||
<SidebarHeader />
|
<SidebarHeader />
|
||||||
|
<NewChatButton />
|
||||||
<HistoryList />
|
<HistoryList />
|
||||||
<SidebarMenu />
|
<SidebarMenu />
|
||||||
<SidebarFooter />
|
<SidebarFooter />
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Popover, Transition } from "@headlessui/react";
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
// import useGetCurrentUser from "@/_hooks/useGetCurrentUser";
|
// import useGetCurrentUser from "@/_hooks/useGetCurrentUser";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { showConfirmSignOutModalAtom } from "@/_helpers/JotaiWrapper";
|
import { showConfirmSignOutModalAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
|
|
||||||
export const MenuHeader: React.FC = () => {
|
export const MenuHeader: React.FC = () => {
|
||||||
const setShowConfirmSignOutModal = useSetAtom(showConfirmSignOutModalAtom);
|
const setShowConfirmSignOutModal = useSetAtom(showConfirmSignOutModalAtom);
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import React, { useRef } from "react";
|
|||||||
import { Dialog } from "@headlessui/react";
|
import { Dialog } from "@headlessui/react";
|
||||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { showingMobilePaneAtom } from "@/_helpers/JotaiWrapper";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
|
import { showingMobilePaneAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
|
|
||||||
const MobileMenuPane: React.FC = () => {
|
const MobileMenuPane: React.FC = () => {
|
||||||
const [show, setShow] = useAtom(showingMobilePaneAtom);
|
const [show, setShow] = useAtom(showingMobilePaneAtom);
|
||||||
|
|||||||
46
web/app/_components/ModelActionButton/index.tsx
Normal file
46
web/app/_components/ModelActionButton/index.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PrimaryButton from "../PrimaryButton";
|
||||||
|
|
||||||
|
export enum ModelActionType {
|
||||||
|
Start = "Start",
|
||||||
|
Stop = "Stop",
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelActionStyle = {
|
||||||
|
title: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
textColor: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const modelActionMapper: Record<ModelActionType, ModelActionStyle> = {
|
||||||
|
[ModelActionType.Start]: {
|
||||||
|
title: "Start",
|
||||||
|
backgroundColor: "bg-blue-500 hover:bg-blue-600",
|
||||||
|
textColor: "text-white",
|
||||||
|
},
|
||||||
|
[ModelActionType.Stop]: {
|
||||||
|
title: "Stop",
|
||||||
|
backgroundColor: "bg-red-500 hover:bg-red-600",
|
||||||
|
textColor: "text-white",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type: ModelActionType;
|
||||||
|
onActionClick: (type: ModelActionType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModelActionButton: React.FC<Props> = ({ type, onActionClick }) => {
|
||||||
|
const styles = modelActionMapper[type];
|
||||||
|
const onClick = () => {
|
||||||
|
onActionClick(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td className="whitespace-nowrap px-6 py-4 text-sm">
|
||||||
|
<PrimaryButton title={styles.title} onClick={onClick} />
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelActionButton;
|
||||||
35
web/app/_components/ModelActionMenu/index.tsx
Normal file
35
web/app/_components/ModelActionMenu/index.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDeleteClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => {
|
||||||
|
return (
|
||||||
|
<Menu as="div" className="relative flex-none">
|
||||||
|
<Menu.Button className="block text-gray-500 hover:text-gray-900">
|
||||||
|
<span className="sr-only">Open options</span>
|
||||||
|
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</Menu.Button>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||||
|
<Menu.Item>
|
||||||
|
<button onClick={onDeleteClick}>Delete</button>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelActionMenu;
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { toGigabytes } from "@/_utils/converter";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
total: number;
|
total: number;
|
||||||
value: number;
|
value: number;
|
||||||
@ -18,16 +20,4 @@ const ModelDownloadingButton: React.FC<Props> = ({ total, value }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toGigabytes = (input: number) => {
|
|
||||||
if (input > 1024 ** 3) {
|
|
||||||
return (input / 1000 ** 3).toFixed(2) + "GB";
|
|
||||||
} else if (input > 1024 ** 2) {
|
|
||||||
return (input / 1000 ** 2).toFixed(2) + "MB";
|
|
||||||
} else if (input > 1024) {
|
|
||||||
return (input / 1000).toFixed(2) + "KB";
|
|
||||||
} else {
|
|
||||||
return input + "B";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModelDownloadingButton;
|
export default ModelDownloadingButton;
|
||||||
|
|||||||
@ -1,92 +1,36 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { execute, executeSerial } from "@/_services/pluginService";
|
|
||||||
import {
|
|
||||||
DataService,
|
|
||||||
ModelManagementService,
|
|
||||||
} from "../../../shared/coreService";
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import {
|
import { searchingModelText } from "@/_helpers/JotaiWrapper";
|
||||||
modelDownloadStateAtom,
|
|
||||||
searchingModelText,
|
|
||||||
} from "@/_helpers/JotaiWrapper";
|
|
||||||
import { Product } from "@/_models/Product";
|
import { Product } from "@/_models/Product";
|
||||||
import DownloadedModelCard from "../DownloadedModelCard";
|
import DownloadedModelCard from "../DownloadedModelCard";
|
||||||
import AvailableModelCard from "../AvailableModelCard";
|
import AvailableModelCard from "../AvailableModelCard";
|
||||||
|
import useDeleteModel from "@/_hooks/useDeleteModel";
|
||||||
|
import useGetAvailableModels from "@/_hooks/useGetAvailableModels";
|
||||||
|
import useDownloadModel from "@/_hooks/useDownloadModel";
|
||||||
|
|
||||||
const ModelListContainer: React.FC = () => {
|
const ModelListContainer: React.FC = () => {
|
||||||
const [downloadedModels, setDownloadedModels] = useState<Product[]>([]);
|
|
||||||
const [availableModels, setAvailableModels] = useState<Product[]>([]);
|
|
||||||
const downloadState = useAtomValue(modelDownloadStateAtom);
|
|
||||||
const searchText = useAtomValue(searchingModelText);
|
const searchText = useAtomValue(searchingModelText);
|
||||||
|
const { deleteModel } = useDeleteModel();
|
||||||
|
const { downloadModel } = useDownloadModel();
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
const getDownloadedModels = async () => {
|
availableModels,
|
||||||
const avails = await executeSerial(
|
downloadedModels,
|
||||||
ModelManagementService.GET_AVAILABLE_MODELS
|
getAvailableModelExceptDownloaded,
|
||||||
);
|
} = useGetAvailableModels();
|
||||||
|
|
||||||
const downloaded: Product[] = await executeSerial(
|
|
||||||
ModelManagementService.GET_DOWNLOADED_MODELS
|
|
||||||
);
|
|
||||||
|
|
||||||
const downloadedSucessfullyModels: Product[] = [];
|
|
||||||
const availableOrDownloadingModels: Product[] = avails;
|
|
||||||
|
|
||||||
downloaded.forEach((item) => {
|
|
||||||
if (item.fileName && downloadState[item.fileName] == null) {
|
|
||||||
downloadedSucessfullyModels.push(item);
|
|
||||||
} else {
|
|
||||||
availableOrDownloadingModels.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setAvailableModels(availableOrDownloadingModels);
|
|
||||||
setDownloadedModels(downloadedSucessfullyModels);
|
|
||||||
};
|
|
||||||
getDownloadedModels();
|
|
||||||
}, [downloadState]);
|
|
||||||
|
|
||||||
const onDeleteClick = async (product: Product) => {
|
const onDeleteClick = async (product: Product) => {
|
||||||
execute(DataService.DELETE_DOWNLOAD_MODEL, product.id);
|
await deleteModel(product);
|
||||||
await executeSerial(ModelManagementService.DELETE_MODEL, product.fileName);
|
await getAvailableModelExceptDownloaded();
|
||||||
const getDownloadedModels = async () => {
|
|
||||||
const avails = await executeSerial(
|
|
||||||
ModelManagementService.GET_AVAILABLE_MODELS
|
|
||||||
);
|
|
||||||
|
|
||||||
const downloaded: Product[] = await executeSerial(
|
|
||||||
ModelManagementService.GET_DOWNLOADED_MODELS
|
|
||||||
);
|
|
||||||
|
|
||||||
const downloadedSucessfullyModels: Product[] = [];
|
|
||||||
const availableOrDownloadingModels: Product[] = avails;
|
|
||||||
|
|
||||||
downloaded.forEach((item) => {
|
|
||||||
if (item.fileName && downloadState[item.fileName] == null) {
|
|
||||||
downloadedSucessfullyModels.push(item);
|
|
||||||
} else {
|
|
||||||
availableOrDownloadingModels.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setAvailableModels(availableOrDownloadingModels);
|
|
||||||
setDownloadedModels(downloadedSucessfullyModels);
|
|
||||||
};
|
|
||||||
getDownloadedModels();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDownloadClick = async (product: Product) => {
|
const onDownloadClick = async (model: Product) => {
|
||||||
await executeSerial(DataService.STORE_MODEL, product);
|
await downloadModel(model);
|
||||||
await executeSerial(ModelManagementService.DOWNLOAD_MODEL, {
|
|
||||||
downloadUrl: product.downloadUrl,
|
|
||||||
fileName: product.fileName,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
|
||||||
<div className="pb-5 flex flex-col gap-2">
|
<div className="pb-5 flex flex-col gap-2">
|
||||||
<Title title="Downloaded models" />
|
<Title title="Downloaded models" />
|
||||||
{downloadedModels
|
{downloadedModels
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import {
|
|
||||||
currentProductAtom,
|
|
||||||
showConfirmDeleteConversationModalAtom,
|
|
||||||
} from "@/_helpers/JotaiWrapper";
|
|
||||||
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||||
|
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
|
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
|
|
||||||
const ModelMenu: React.FC = () => {
|
const ModelMenu: React.FC = () => {
|
||||||
const currentProduct = useAtomValue(currentProductAtom);
|
const currentProduct = useAtomValue(currentProductAtom);
|
||||||
|
|||||||
78
web/app/_components/ModelRow/index.tsx
Normal file
78
web/app/_components/ModelRow/index.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Product } from "@/_models/Product";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { ModelStatus, ModelStatusComponent } from "../ModelStatusComponent";
|
||||||
|
import ModelActionMenu from "../ModelActionMenu";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import ModelActionButton, { ModelActionType } from "../ModelActionButton";
|
||||||
|
import useStartStopModel from "@/_hooks/useStartStopModel";
|
||||||
|
import useDeleteModel from "@/_hooks/useDeleteModel";
|
||||||
|
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
model: Product;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModelRow: React.FC<Props> = ({ model }) => {
|
||||||
|
const { startModel } = useStartStopModel();
|
||||||
|
const activeModel = useAtomValue(currentProductAtom);
|
||||||
|
const { deleteModel } = useDeleteModel();
|
||||||
|
|
||||||
|
let status = ModelStatus.Installed;
|
||||||
|
if (activeModel && activeModel.id === model.id) {
|
||||||
|
status = ModelStatus.Active;
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionButtonType = ModelActionType.Start;
|
||||||
|
if (activeModel && activeModel.id === model.id) {
|
||||||
|
actionButtonType = ModelActionType.Stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onModelActionClick = (action: ModelActionType) => {
|
||||||
|
if (action === ModelActionType.Start) {
|
||||||
|
startModel(model.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteClick = () => {
|
||||||
|
deleteModel(model);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className="border-b border-gray-200 last:border-b-0 last:rounded-lg"
|
||||||
|
key={model.id}
|
||||||
|
>
|
||||||
|
<td className="flex flex-col whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900">
|
||||||
|
{model.name}
|
||||||
|
<span className="text-gray-500 font-normal">{model.version}</span>
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||||
|
<div className="flex flex-col justify-start">
|
||||||
|
<span>{model.format}</span>
|
||||||
|
{model.accelerated && (
|
||||||
|
<span className="flex items-center text-gray-500 text-sm font-normal gap-0.5">
|
||||||
|
<Image src={"/icons/flash.svg"} width={20} height={20} alt="" />
|
||||||
|
GPU Accelerated
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||||
|
{model.totalSize}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||||
|
<ModelStatusComponent status={status} />
|
||||||
|
</td>
|
||||||
|
<ModelActionButton
|
||||||
|
type={actionButtonType}
|
||||||
|
onActionClick={onModelActionClick}
|
||||||
|
/>
|
||||||
|
<td className="relative whitespace-nowrap px-6 py-4 w-fit text-right text-sm font-medium">
|
||||||
|
<ModelActionMenu onDeleteClick={onDeleteClick} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelRow;
|
||||||
118
web/app/_components/ModelSelector/index.tsx
Normal file
118
web/app/_components/ModelSelector/index.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { Fragment, useEffect } from "react";
|
||||||
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
||||||
|
import { Product } from "@/_models/Product";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { selectedModelAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
|
|
||||||
|
function classNames(...classes: any) {
|
||||||
|
return classes.filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectModels: React.FC = () => {
|
||||||
|
const { downloadedModels } = useGetDownloadedModels();
|
||||||
|
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (downloadedModels && downloadedModels.length > 0) {
|
||||||
|
onModelSelected(downloadedModels[0]);
|
||||||
|
}
|
||||||
|
}, [downloadedModels]);
|
||||||
|
|
||||||
|
const onModelSelected = (model: Product) => {
|
||||||
|
setSelectedModel(model);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!selectedModel) {
|
||||||
|
return <div>You have not downloaded any model!</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Listbox value={selectedModel} onChange={onModelSelected}>
|
||||||
|
{({ open }) => (
|
||||||
|
<div className="w-[461px]">
|
||||||
|
<Listbox.Label className="block text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Select a Model:
|
||||||
|
</Listbox.Label>
|
||||||
|
<div className="relative mt-[19px]">
|
||||||
|
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 sm:text-sm sm:leading-6">
|
||||||
|
<span className="flex items-center">
|
||||||
|
<img
|
||||||
|
src={selectedModel.avatarUrl}
|
||||||
|
alt=""
|
||||||
|
className="h-5 w-5 flex-shrink-0 rounded-full"
|
||||||
|
/>
|
||||||
|
<span className="ml-3 block truncate">
|
||||||
|
{selectedModel.name}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2">
|
||||||
|
<ChevronUpDownIcon
|
||||||
|
className="h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options className="absolute z-10 mt-1 max-h-[188px] w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||||
|
{downloadedModels.map((model) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={model.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
classNames(
|
||||||
|
active ? "bg-indigo-600 text-white" : "text-gray-900",
|
||||||
|
"relative cursor-default select-none py-2 pl-3 pr-9"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={model}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src={model.avatarUrl}
|
||||||
|
alt=""
|
||||||
|
className="h-5 w-5 flex-shrink-0 rounded-full"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
selected ? "font-semibold" : "font-normal",
|
||||||
|
"ml-3 block truncate"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{model.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selected ? (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
active ? "text-white" : "text-indigo-600",
|
||||||
|
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectModels;
|
||||||
46
web/app/_components/ModelStatusComponent/index.tsx
Normal file
46
web/app/_components/ModelStatusComponent/index.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type ModelStatusType = {
|
||||||
|
title: string;
|
||||||
|
textColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ModelStatus {
|
||||||
|
Installed,
|
||||||
|
Active,
|
||||||
|
RunningInNitro,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModelStatusMapper: Record<ModelStatus, ModelStatusType> = {
|
||||||
|
[ModelStatus.Installed]: {
|
||||||
|
title: "Installed",
|
||||||
|
textColor: "text-black",
|
||||||
|
backgroundColor: "bg-gray-100",
|
||||||
|
},
|
||||||
|
[ModelStatus.Active]: {
|
||||||
|
title: "Active",
|
||||||
|
textColor: "text-black",
|
||||||
|
backgroundColor: "bg-green-100",
|
||||||
|
},
|
||||||
|
[ModelStatus.RunningInNitro]: {
|
||||||
|
title: "Running in Nitro",
|
||||||
|
textColor: "text-black",
|
||||||
|
backgroundColor: "bg-green-100",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
status: ModelStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModelStatusComponent: React.FC<Props> = ({ status }) => {
|
||||||
|
const statusType = ModelStatusMapper[status];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`rounded-[10px] py-0.5 px-[10px] w-fit text-xs font-medium ${statusType.backgroundColor}`}
|
||||||
|
>
|
||||||
|
{statusType.title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
34
web/app/_components/ModelTable/index.tsx
Normal file
34
web/app/_components/ModelTable/index.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Product } from "@/_models/Product";
|
||||||
|
import ModelRow from "../ModelRow";
|
||||||
|
import ModelTableHeader from "../ModelTableHeader";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
models: Product[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableHeaders = ["MODEL", "FORMAT", "SIZE", "STATUS", "ACTIONS"];
|
||||||
|
|
||||||
|
const ModelTable: React.FC<Props> = ({ models }) => (
|
||||||
|
<div className="flow-root inline-block border rounded-lg border-gray-200 min-w-full align-middle shadow-lg">
|
||||||
|
<table className="min-w-full">
|
||||||
|
<thead className="bg-gray-50 border-b border-gray-200">
|
||||||
|
<tr className="rounded-t-lg">
|
||||||
|
{tableHeaders.map((item) => (
|
||||||
|
<ModelTableHeader key={item} title={item} />
|
||||||
|
))}
|
||||||
|
<th scope="col" className="relative px-6 py-3 w-fit">
|
||||||
|
<span className="sr-only">Edit</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{models.map((model) => (
|
||||||
|
<ModelRow key={model.id} model={model} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default React.memo(ModelTable);
|
||||||
16
web/app/_components/ModelTableHeader/index.tsx
Normal file
16
web/app/_components/ModelTableHeader/index.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModelTableHeader: React.FC<Props> = ({ title }) => (
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left first:rounded-tl-lg last:rounded-tr-lg text-xs font-medium uppercase tracking-wide text-gray-500"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default React.memo(ModelTableHeader);
|
||||||
25
web/app/_components/ModelVersionItem/index.tsx
Normal file
25
web/app/_components/ModelVersionItem/index.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { toGigabytes } from "@/_utils/converter";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
totalSizeInByte: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModelVersionItem: React.FC<Props> = ({ title, totalSizeInByte }) => (
|
||||||
|
<div className="flex justify-between items-center gap-4 pl-[13px] pt-[13px] pr-[17px] pb-3 border-t border-gray-200 first:border-t-0">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Image src={"/icons/app_icon.svg"} width={14} height={20} alt="" />
|
||||||
|
<span className="font-sm text-gray-900">{title}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="px-[10px] py-0.5 bg-gray-200 text-xs font-medium rounded">
|
||||||
|
{toGigabytes(totalSizeInByte)}
|
||||||
|
</div>
|
||||||
|
<button className="text-indigo-600 text-sm font-medium">Download</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ModelVersionItem;
|
||||||
38
web/app/_components/ModelVersionList/index.tsx
Normal file
38
web/app/_components/ModelVersionList/index.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ModelVersionItem from "../ModelVersionItem";
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
name: "Q4_K_M.gguf",
|
||||||
|
total: 5600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Q4_K_M.gguf",
|
||||||
|
total: 5600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Q4_K_M.gguf",
|
||||||
|
total: 5600,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ModelVersionList: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-5 border-t border-gray-200">
|
||||||
|
<div className="text-sm font-medium text-gray-500">
|
||||||
|
Available Versions
|
||||||
|
</div>
|
||||||
|
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<ModelVersionItem
|
||||||
|
key={index}
|
||||||
|
title={item.name}
|
||||||
|
totalSizeInByte={item.total}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelVersionList;
|
||||||
@ -1,42 +1,22 @@
|
|||||||
import ProgressBar from "../ProgressBar";
|
import ProgressBar from "../ProgressBar";
|
||||||
import SystemItem from "../SystemItem";
|
import SystemItem from "../SystemItem";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import {
|
import { appDownloadProgress } from "@/_helpers/JotaiWrapper";
|
||||||
activeModel,
|
|
||||||
appDownloadProgress,
|
|
||||||
getSystemBarVisibilityAtom,
|
|
||||||
} from "@/_helpers/JotaiWrapper";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager";
|
import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager";
|
||||||
import { SystemMonitoringService } from "../../../shared/coreService";
|
import { SystemMonitoringService } from "../../../shared/coreService";
|
||||||
|
import { getSystemBarVisibilityAtom } from "@/_helpers/atoms/SystemBar.atom";
|
||||||
|
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
|
|
||||||
const MonitorBar: React.FC = () => {
|
const MonitorBar: React.FC = () => {
|
||||||
const show = useAtomValue(getSystemBarVisibilityAtom);
|
const show = useAtomValue(getSystemBarVisibilityAtom);
|
||||||
const progress = useAtomValue(appDownloadProgress);
|
const progress = useAtomValue(appDownloadProgress);
|
||||||
const modelName = useAtomValue(activeModel);
|
const activeModel = useAtomValue(currentProductAtom);
|
||||||
const [ram, setRam] = useState<number>(0);
|
const [ram, setRam] = useState<number>(0);
|
||||||
const [gpu, setGPU] = useState<number>(0);
|
const [gpu, setGPU] = useState<number>(0);
|
||||||
const [cpu, setCPU] = useState<number>(0);
|
const [cpu, setCPU] = useState<number>(0);
|
||||||
const [version, setVersion] = useState<string>("");
|
const [version, setVersion] = useState<string>("");
|
||||||
|
|
||||||
const data = [
|
|
||||||
{
|
|
||||||
name: "CPU",
|
|
||||||
total: 1400,
|
|
||||||
used: 750,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Ram",
|
|
||||||
total: 16000,
|
|
||||||
used: 4500,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "VRAM",
|
|
||||||
total: 1400,
|
|
||||||
used: 1300,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getSystemResources = async () => {
|
const getSystemResources = async () => {
|
||||||
const resourceInfor = await executeSerial(
|
const resourceInfor = await executeSerial(
|
||||||
@ -77,8 +57,8 @@ const MonitorBar: React.FC = () => {
|
|||||||
<SystemItem name="CPU" value={`${cpu}%`} />
|
<SystemItem name="CPU" value={`${cpu}%`} />
|
||||||
<SystemItem name="Mem" value={`${ram}%`} />
|
<SystemItem name="Mem" value={`${ram}%`} />
|
||||||
|
|
||||||
{modelName && modelName.length > 0 && (
|
{activeModel && (
|
||||||
<SystemItem name="Active Models" value={"1"} />
|
<SystemItem name={`Active model: ${activeModel.name}`} value={"1"} />
|
||||||
)}
|
)}
|
||||||
<span className="text-gray-900 text-sm">v{version}</span>
|
<span className="text-gray-900 text-sm">v{version}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
13
web/app/_components/MyModelContainer/index.tsx
Normal file
13
web/app/_components/MyModelContainer/index.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import HeaderTitle from "../HeaderTitle";
|
||||||
|
import DownloadedModelTable from "../DownloadedModelTable";
|
||||||
|
import ActiveModelTable from "../ActiveModelTable";
|
||||||
|
|
||||||
|
const MyModelContainer: React.FC = () => (
|
||||||
|
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px]">
|
||||||
|
<HeaderTitle title="My Models" />
|
||||||
|
<ActiveModelTable />
|
||||||
|
<DownloadedModelTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MyModelContainer;
|
||||||
39
web/app/_components/NewChatButton/index.tsx
Normal file
39
web/app/_components/NewChatButton/index.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import SecondaryButton from "../SecondaryButton";
|
||||||
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
|
import {
|
||||||
|
MainViewState,
|
||||||
|
setMainViewStateAtom,
|
||||||
|
} from "@/_helpers/atoms/MainView.atom";
|
||||||
|
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
|
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||||
|
import useInitModel from "@/_hooks/useInitModel";
|
||||||
|
import { Product } from "@/_models/Product";
|
||||||
|
|
||||||
|
const NewChatButton: React.FC = () => {
|
||||||
|
const activeModel = useAtomValue(currentProductAtom);
|
||||||
|
const setMainView = useSetAtom(setMainViewStateAtom);
|
||||||
|
const { requestCreateConvo } = useCreateConversation();
|
||||||
|
const { initModel } = useInitModel();
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
if (!activeModel) {
|
||||||
|
setMainView(MainViewState.ConversationEmptyModel);
|
||||||
|
} else {
|
||||||
|
createConversationAndInitModel(activeModel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createConversationAndInitModel = async (model: Product) => {
|
||||||
|
await requestCreateConvo(model);
|
||||||
|
await initModel(model);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SecondaryButton title={"New Chat"} onClick={onClick} className="my-5" />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewChatButton;
|
||||||
@ -5,7 +5,7 @@ import {
|
|||||||
plugins,
|
plugins,
|
||||||
extensionPoints,
|
extensionPoints,
|
||||||
activationPoints,
|
activationPoints,
|
||||||
} from "../../../electron/core/plugin-manager/execution/index";
|
} from "@/../../electron/core/plugin-manager/execution/index";
|
||||||
import {
|
import {
|
||||||
ChartPieIcon,
|
ChartPieIcon,
|
||||||
CommandLineIcon,
|
CommandLineIcon,
|
||||||
@ -81,9 +81,7 @@ export const Preferences = () => {
|
|||||||
// Send the filename of the to be installed plugin
|
// Send the filename of the to be installed plugin
|
||||||
// to the main process for installation
|
// to the main process for installation
|
||||||
const installed = await plugins.install([pluginFile]);
|
const installed = await plugins.install([pluginFile]);
|
||||||
if (typeof window !== "undefined") {
|
if (installed) window.electronAPI.relaunch();
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Uninstall a plugin on clicking uninstall
|
// Uninstall a plugin on clicking uninstall
|
||||||
@ -99,6 +97,7 @@ export const Preferences = () => {
|
|||||||
? "Plugin successfully uninstalled"
|
? "Plugin successfully uninstalled"
|
||||||
: "Plugin could not be uninstalled"
|
: "Plugin could not be uninstalled"
|
||||||
);
|
);
|
||||||
|
if (res) window.electronAPI.relaunch();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update all plugins on clicking update plugins
|
// Update all plugins on clicking update plugins
|
||||||
@ -106,6 +105,7 @@ export const Preferences = () => {
|
|||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await window.pluggableElectronIpc.update([plugin], true);
|
await window.pluggableElectronIpc.update([plugin], true);
|
||||||
|
window.electronAPI.relaunch();
|
||||||
}
|
}
|
||||||
// plugins.update(active.map((plg) => plg.name));
|
// plugins.update(active.map((plg) => plg.name));
|
||||||
};
|
};
|
||||||
@ -176,9 +176,7 @@ export const Preferences = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
|
"rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
|
||||||
fileName
|
fileName ? "bg-indigo-600 hover:bg-indigo-500" : "bg-gray-500"
|
||||||
? "bg-indigo-600 hover:bg-indigo-500"
|
|
||||||
: "bg-gray-500"
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Install Plugin
|
Install Plugin
|
||||||
@ -190,7 +188,7 @@ export const Preferences = () => {
|
|||||||
<CommandLineIcon width={30} />
|
<CommandLineIcon width={30} />
|
||||||
Installed Plugins
|
Installed Plugins
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap">
|
<div className="grid grid-cols-2 items-stretch gap-4">
|
||||||
{activePlugins
|
{activePlugins
|
||||||
.filter(
|
.filter(
|
||||||
(e) =>
|
(e) =>
|
||||||
@ -198,41 +196,52 @@ export const Preferences = () => {
|
|||||||
e.name.toLowerCase().includes(search.toLowerCase())
|
e.name.toLowerCase().includes(search.toLowerCase())
|
||||||
)
|
)
|
||||||
.map((e) => (
|
.map((e) => (
|
||||||
<div key={e.name} className="mr-2 my-3 w-[400px]">
|
<div
|
||||||
<a
|
key={e.name}
|
||||||
href="#"
|
data-testid="plugin-item"
|
||||||
className="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"
|
className="flex flex-col h-full p-6 bg-white border border-gray-200 rounded-sm dark:border-gray-300"
|
||||||
>
|
>
|
||||||
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
<div className="flex flex-row space-x-2 items-center">
|
||||||
{e.name}
|
<span className="relative inline-block mt-1">
|
||||||
</h5>
|
<img
|
||||||
<p className="font-normal text-gray-700 dark:text-gray-400">
|
className="h-14 w-14 rounded-md"
|
||||||
Activation: {e.activationPoints}
|
src={e.icon ?? "icons/app_icon.svg"}
|
||||||
</p>
|
alt=""
|
||||||
<p className="font-normal text-gray-700 dark:text-gray-400 h-[24px] truncate w-full">
|
/>
|
||||||
Url: {e.url}
|
</span>
|
||||||
</p>
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-row space-x-5">
|
<p className="text-xl font-bold tracking-tight text-gray-900 dark:text-white capitalize">
|
||||||
<button
|
{e.name.replaceAll("-", " ")}
|
||||||
type="submit"
|
</p>
|
||||||
onClick={() => {
|
<p className="font-normal text-gray-700 dark:text-gray-400">
|
||||||
uninstall(e.name);
|
Version: {e.version}
|
||||||
}}
|
</p>
|
||||||
className="mt-5 rounded-md bg-red-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
|
||||||
>
|
|
||||||
Uninstall
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
onClick={() => {
|
|
||||||
update(e.name);
|
|
||||||
}}
|
|
||||||
className="mt-5 rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
|
<p className="flex-1 mt-2 text-sm font-normal text-gray-500 dark:text-gray-400 w-full">
|
||||||
|
{e.description ?? "Jan's Plugin"}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-row space-x-5">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
uninstall(e.name);
|
||||||
|
}}
|
||||||
|
className="mt-5 rounded-md bg-red-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||||
|
>
|
||||||
|
Uninstall
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
update(e.name);
|
||||||
|
}}
|
||||||
|
className="mt-5 rounded-md bg-blue-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import ChatContainer from "../ChatContainer";
|
import ChatContainer from "../ChatContainer";
|
||||||
import Header from "../Header";
|
|
||||||
import MainChat from "../MainChat";
|
import MainChat from "../MainChat";
|
||||||
import MonitorBar from "../MonitorBar";
|
import MonitorBar from "../MonitorBar";
|
||||||
|
|
||||||
const RightContainer = () => (
|
const RightContainer = () => (
|
||||||
<div className="flex flex-col flex-1 h-screen">
|
<div className="flex flex-col flex-1 h-screen">
|
||||||
<Header />
|
|
||||||
<ChatContainer>
|
<ChatContainer>
|
||||||
<MainChat />
|
<MainChat />
|
||||||
</ChatContainer>
|
</ChatContainer>
|
||||||
|
|||||||
@ -2,11 +2,15 @@ import { searchAtom } from "@/_helpers/JotaiWrapper";
|
|||||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
|
|
||||||
const SearchBar: React.FC = () => {
|
type Props = {
|
||||||
const setText = useSetAtom(searchAtom);
|
placeholder?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SearchBar: React.FC<Props> = ({ placeholder }) => {
|
||||||
|
const setText = useSetAtom(searchAtom);
|
||||||
|
let placeholderText = placeholder ? placeholder : "Search (⌘K)";
|
||||||
return (
|
return (
|
||||||
<div className="relative mx-3 mt-3 flex items-center">
|
<div className="relative mt-3 flex items-center">
|
||||||
<div className="absolute top-0 left-2 h-full flex items-center">
|
<div className="absolute top-0 left-2 h-full flex items-center">
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
width={16}
|
width={16}
|
||||||
@ -19,7 +23,7 @@ const SearchBar: React.FC = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
name="search"
|
name="search"
|
||||||
id="search"
|
id="search"
|
||||||
placeholder="Search (⌘K)"
|
placeholder={placeholderText}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
className="block w-full rounded-md border-0 py-1.5 pl-8 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
className="block w-full rounded-md border-0 py-1.5 pl-8 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -2,14 +2,20 @@ type Props = {
|
|||||||
title: string;
|
title: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SecondaryButton: React.FC<Props> = ({ title, onClick, disabled }) => (
|
const SecondaryButton: React.FC<Props> = ({
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
}) => (
|
||||||
<button
|
<button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
|
className={`rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${className}`}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import {
|
import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
|
||||||
currentConvoStateAtom,
|
import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||||
currentPromptAtom,
|
|
||||||
} from "@/_helpers/JotaiWrapper";
|
|
||||||
import useSendChatMessage from "@/_hooks/useSendChatMessage";
|
import useSendChatMessage from "@/_hooks/useSendChatMessage";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
callback?: () => void;
|
|
||||||
className?: string;
|
|
||||||
icon: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SidebarButton: React.FC<Props> = ({
|
|
||||||
callback,
|
|
||||||
height,
|
|
||||||
icon,
|
|
||||||
className,
|
|
||||||
width,
|
|
||||||
title,
|
|
||||||
}) => (
|
|
||||||
<button onClick={callback} className={className}>
|
|
||||||
<Image src={icon} width={width} height={height} alt="" />
|
|
||||||
<span>{title}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
@ -1,28 +1,56 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { SidebarButton } from "../SidebarButton";
|
|
||||||
import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager";
|
|
||||||
import { DataService } from "../../../shared/coreService";
|
|
||||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||||
|
import PrimaryButton from "../PrimaryButton";
|
||||||
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
|
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
MainViewState,
|
||||||
|
setMainViewStateAtom,
|
||||||
|
} from "@/_helpers/atoms/MainView.atom";
|
||||||
|
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
|
import useInitModel from "@/_hooks/useInitModel";
|
||||||
|
import { Product } from "@/_models/Product";
|
||||||
|
|
||||||
|
enum ActionButton {
|
||||||
|
DownloadModel = "Download a Model",
|
||||||
|
StartChat = "Start a Conversation",
|
||||||
|
}
|
||||||
|
|
||||||
const SidebarEmptyHistory: React.FC = () => {
|
const SidebarEmptyHistory: React.FC = () => {
|
||||||
|
const { downloadedModels } = useGetDownloadedModels();
|
||||||
|
const activeModel = useAtomValue(currentProductAtom);
|
||||||
|
const setMainView = useSetAtom(setMainViewStateAtom);
|
||||||
const { requestCreateConvo } = useCreateConversation();
|
const { requestCreateConvo } = useCreateConversation();
|
||||||
const startChat = async () => {
|
const [action, setAction] = useState(ActionButton.DownloadModel);
|
||||||
// Host
|
|
||||||
if (window && !window.electronAPI) {
|
const { initModel } = useInitModel();
|
||||||
// requestCreateConvo(); // TODO: get model id from somewhere
|
|
||||||
}
|
useEffect(() => {
|
||||||
// Electron
|
if (downloadedModels.length > 0) {
|
||||||
const downloadedModels = await executeSerial(
|
setAction(ActionButton.StartChat);
|
||||||
DataService.GET_FINISHED_DOWNLOAD_MODELS
|
|
||||||
);
|
|
||||||
if (!downloadedModels || downloadedModels?.length === 0) {
|
|
||||||
alert(
|
|
||||||
"Seems like there is no model downloaded yet. Please download a model first."
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
requestCreateConvo(downloadedModels[0]);
|
setAction(ActionButton.DownloadModel);
|
||||||
|
}
|
||||||
|
}, [downloadedModels]);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
if (action === ActionButton.DownloadModel) {
|
||||||
|
setMainView(MainViewState.ExploreModel);
|
||||||
|
} else {
|
||||||
|
if (!activeModel) {
|
||||||
|
setMainView(MainViewState.ConversationEmptyModel);
|
||||||
|
} else {
|
||||||
|
createConversationAndInitModel(activeModel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createConversationAndInitModel = async (model: Product) => {
|
||||||
|
await requestCreateConvo(model);
|
||||||
|
await initModel(model);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center py-10 gap-3">
|
<div className="flex flex-col items-center py-10 gap-3">
|
||||||
<Image
|
<Image
|
||||||
@ -32,22 +60,11 @@ const SidebarEmptyHistory: React.FC = () => {
|
|||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center gap-6">
|
<div className="flex flex-col items-center gap-6">
|
||||||
<div>
|
<div className="text-center text-gray-900 text-sm">No Chat History</div>
|
||||||
<div className="text-center text-gray-900 text-sm">
|
<div className="text-center text-gray-500 text-sm">
|
||||||
No Chat History
|
Get started by creating a new chat.
|
||||||
</div>
|
|
||||||
<div className="text-center text-gray-500 text-sm">
|
|
||||||
Get started by creating a new chat.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<SidebarButton
|
<PrimaryButton title={action} onClick={onClick} />
|
||||||
callback={startChat}
|
|
||||||
className="flex items-center border bg-blue-600 rounded-lg py-[9px] pl-[15px] pr-[17px] gap-2 text-white font-medium text-sm"
|
|
||||||
height={14}
|
|
||||||
icon="icons/Icon_plus.svg"
|
|
||||||
title="New chat"
|
|
||||||
width={14}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,27 +1,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { SidebarButton } from "../SidebarButton";
|
import SecondaryButton from "../SecondaryButton";
|
||||||
|
|
||||||
const SidebarFooter: React.FC = () => (
|
const SidebarFooter: React.FC = () => (
|
||||||
<div className="flex justify-between items-center gap-2">
|
<div className="flex justify-between items-center gap-2">
|
||||||
<SidebarButton
|
<SecondaryButton
|
||||||
className="flex items-center border border-gray-200 rounded-lg p-2 gap-3 flex-1 justify-center text-gray-900 font-medium text-sm"
|
title={"Discord"}
|
||||||
height={24}
|
onClick={() =>
|
||||||
icon="icons/discord.svg"
|
window.electronAPI?.openExternalUrl("https://discord.gg/AsJ8krTT3N")
|
||||||
title="Discord"
|
}
|
||||||
width={24}
|
className="flex-1"
|
||||||
callback={() => {
|
|
||||||
window.electronAPI?.openExternalUrl("https://discord.gg/AsJ8krTT3N");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<SidebarButton
|
<SecondaryButton
|
||||||
className="flex items-center border border-gray-200 rounded-lg p-2 gap-3 flex-1 justify-center text-gray-900 font-medium text-sm"
|
title={"Discord"}
|
||||||
height={24}
|
onClick={() =>
|
||||||
icon="icons/unicorn_twitter.svg"
|
window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai")
|
||||||
title="Twitter"
|
}
|
||||||
width={24}
|
className="flex-1"
|
||||||
callback={() => {
|
|
||||||
window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,69 +1,41 @@
|
|||||||
import {
|
import React from "react";
|
||||||
MainViewState,
|
import SidebarMenuItem from "../SidebarMenuItem";
|
||||||
getMainViewStateAtom,
|
import { MainViewState } from "@/_helpers/atoms/MainView.atom";
|
||||||
setMainViewStateAtom,
|
|
||||||
} from "@/_helpers/JotaiWrapper";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
const SidebarMenu: React.FC = () => {
|
const menu = [
|
||||||
const currentState = useAtomValue(getMainViewStateAtom);
|
{
|
||||||
const setMainViewState = useSetAtom(setMainViewStateAtom);
|
name: "Explore Models",
|
||||||
|
icon: "Search_gray",
|
||||||
|
state: MainViewState.ExploreModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "My Models",
|
||||||
|
icon: "ViewGrid",
|
||||||
|
state: MainViewState.MyModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Settings",
|
||||||
|
icon: "Cog",
|
||||||
|
state: MainViewState.Setting,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const menu = [
|
const SidebarMenu: React.FC = () => (
|
||||||
{
|
<div className="flex flex-col">
|
||||||
name: "Explore Models",
|
<div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3">
|
||||||
icon: "Search_gray",
|
Your Configurations
|
||||||
state: MainViewState.ExploreModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "My Models",
|
|
||||||
icon: "ViewGrid",
|
|
||||||
state: MainViewState.MyModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Settings",
|
|
||||||
icon: "Cog",
|
|
||||||
state: MainViewState.Setting,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const onMenuClick = (state: MainViewState) => {
|
|
||||||
if (state === currentState) return;
|
|
||||||
setMainViewState(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3">
|
|
||||||
Your Configurations
|
|
||||||
</div>
|
|
||||||
<ul role="list" className="-mx-2 mt-2 space-y-1">
|
|
||||||
{menu.map((item) => (
|
|
||||||
<li key={item.name}>
|
|
||||||
<button
|
|
||||||
onClick={() => onMenuClick(item.state)}
|
|
||||||
className={classNames(
|
|
||||||
currentState === item.state
|
|
||||||
? "bg-gray-50 text-indigo-600"
|
|
||||||
: "text-gray-600 hover:text-indigo-600 hover:bg-gray-50",
|
|
||||||
"group flex gap-x-3 rounded-md text-base py-2 px-3 w-full"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={`icons/${item.icon}.svg`}
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<span className="truncate">{item.name}</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<ul role="list" className="-mx-2 mt-2 space-y-1 mb-2">
|
||||||
};
|
{menu.map((item) => (
|
||||||
|
<SidebarMenuItem
|
||||||
|
title={item.name}
|
||||||
|
viewState={item.state}
|
||||||
|
iconName={item.icon}
|
||||||
|
key={item.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default SidebarMenu;
|
export default React.memo(SidebarMenu);
|
||||||
|
|||||||
41
web/app/_components/SidebarMenuItem/index.tsx
Normal file
41
web/app/_components/SidebarMenuItem/index.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
|
import Image from "next/image";
|
||||||
|
import {
|
||||||
|
MainViewState,
|
||||||
|
getMainViewStateAtom,
|
||||||
|
setMainViewStateAtom,
|
||||||
|
} from "@/_helpers/atoms/MainView.atom";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
viewState: MainViewState;
|
||||||
|
iconName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidebarMenuItem: React.FC<Props> = ({ title, viewState, iconName }) => {
|
||||||
|
const currentState = useAtomValue(getMainViewStateAtom);
|
||||||
|
const setMainViewState = useSetAtom(setMainViewStateAtom);
|
||||||
|
|
||||||
|
let className =
|
||||||
|
"text-gray-600 hover:text-indigo-600 hover:bg-gray-50 group flex gap-x-3 rounded-md text-base py-2 px-3 w-full";
|
||||||
|
if (currentState == viewState) {
|
||||||
|
className =
|
||||||
|
"bg-gray-100 text-indigo-600 group flex gap-x-3 rounded-md text-base py-2 px-3 w-full";
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
setMainViewState(viewState);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={title}>
|
||||||
|
<button onClick={onClick} className={className}>
|
||||||
|
<Image src={`icons/${iconName}.svg`} width={24} height={24} alt="" />
|
||||||
|
<span className="truncate">{title}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SidebarMenuItem;
|
||||||
22
web/app/_components/SimpleCheckbox/index.tsx
Normal file
22
web/app/_components/SimpleCheckbox/index.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
type Props = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SimpleCheckbox: React.FC<Props> = ({ name }) => (
|
||||||
|
<div className="relative flex items-center gap-[11px]">
|
||||||
|
<div className="flex h-6 items-center">
|
||||||
|
<input
|
||||||
|
id="offers"
|
||||||
|
aria-describedby="offers-description"
|
||||||
|
name="offers"
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs">
|
||||||
|
<label htmlFor="offers">{name}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SimpleCheckbox;
|
||||||
89
web/app/_components/SimpleTag/index.tsx
Normal file
89
web/app/_components/SimpleTag/index.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export enum TagType {
|
||||||
|
Roleplay = "Roleplay",
|
||||||
|
Llama = "Llama",
|
||||||
|
Story = "Story",
|
||||||
|
Casual = "Casual",
|
||||||
|
Professional = "Professional",
|
||||||
|
CodeLlama = "CodeLlama",
|
||||||
|
Coding = "Coding",
|
||||||
|
|
||||||
|
// Positive
|
||||||
|
Recommended = "Recommended",
|
||||||
|
Compatible = "Compatible",
|
||||||
|
|
||||||
|
// Neutral
|
||||||
|
SlowOnDevice = "This model will be slow on your device",
|
||||||
|
|
||||||
|
// Negative
|
||||||
|
InsufficientRam = "Insufficient RAM",
|
||||||
|
Incompatible = "Incompatible with your device",
|
||||||
|
TooLarge = "This model is too large for your device",
|
||||||
|
|
||||||
|
// Performance
|
||||||
|
Medium = "Medium",
|
||||||
|
BalancedQuality = "Balanced Quality",
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagStyleMapper: Record<TagType, string> = {
|
||||||
|
[TagType.Roleplay]: "bg-red-100 text-red-800",
|
||||||
|
[TagType.Llama]: "bg-green-100 text-green-800",
|
||||||
|
[TagType.Story]: "bg-blue-100 text-blue-800",
|
||||||
|
[TagType.Casual]: "bg-yellow-100 text-yellow-800",
|
||||||
|
[TagType.Professional]: "text-indigo-800 bg-indigo-100",
|
||||||
|
[TagType.CodeLlama]: "bg-pink-100 text-pink-800",
|
||||||
|
[TagType.Coding]: "text-purple-800 bg-purple-100",
|
||||||
|
|
||||||
|
[TagType.Recommended]:
|
||||||
|
"text-green-700 ring-1 ring-inset ring-green-600/20 bg-green-50",
|
||||||
|
[TagType.Compatible]:
|
||||||
|
"bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
|
||||||
|
|
||||||
|
[TagType.SlowOnDevice]:
|
||||||
|
"bg-yellow-50 text-yellow-800 ring-1 ring-inset ring-yellow-600/20",
|
||||||
|
|
||||||
|
[TagType.Incompatible]:
|
||||||
|
"bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
|
||||||
|
[TagType.InsufficientRam]:
|
||||||
|
"bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
|
||||||
|
[TagType.TooLarge]: "bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
|
||||||
|
|
||||||
|
[TagType.Medium]: "bg-yellow-100 text-yellow-800",
|
||||||
|
[TagType.BalancedQuality]: "bg-yellow-100 text-yellow-800",
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
type: TagType;
|
||||||
|
clickable?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SimpleTag: React.FC<Props> = ({
|
||||||
|
onClick,
|
||||||
|
clickable = true,
|
||||||
|
title,
|
||||||
|
type,
|
||||||
|
}) => {
|
||||||
|
if (!clickable) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
|
||||||
|
>
|
||||||
|
{title} x
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(SimpleTag);
|
||||||
@ -4,7 +4,7 @@ import { TextCode } from "../TextCode";
|
|||||||
import { getMessageCode } from "@/_utils/message";
|
import { getMessageCode } from "@/_utils/message";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { currentStreamingMessageAtom } from "@/_helpers/JotaiWrapper";
|
import { currentStreamingMessageAtom } from "@/_helpers/atoms/ChatMessage.atom";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { currentConversationAtom } from "@/_helpers/JotaiWrapper";
|
import { currentConversationAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { SidebarButton } from "../SidebarButton";
|
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { MainViewState, setMainViewStateAtom } from "@/_helpers/JotaiWrapper";
|
import {
|
||||||
|
setMainViewStateAtom,
|
||||||
|
MainViewState,
|
||||||
|
} from "@/_helpers/atoms/MainView.atom";
|
||||||
|
import SecondaryButton from "../SecondaryButton";
|
||||||
|
|
||||||
const Welcome: React.FC = () => {
|
const Welcome: React.FC = () => {
|
||||||
const setMainViewState = useSetAtom(setMainViewStateAtom);
|
const setMainViewState = useSetAtom(setMainViewStateAtom);
|
||||||
@ -15,13 +18,9 @@ const Welcome: React.FC = () => {
|
|||||||
<br />
|
<br />
|
||||||
let’s download your first model
|
let’s download your first model
|
||||||
</span>
|
</span>
|
||||||
<SidebarButton
|
<SecondaryButton
|
||||||
callback={() => setMainViewState(MainViewState.ExploreModel)}
|
title={"Explore models"}
|
||||||
className="flex flex-row-reverse items-center rounded-lg gap-2 px-3 py-2 text-xs font-medium border border-gray-200"
|
onClick={() => setMainViewState(MainViewState.ExploreModel)}
|
||||||
icon={"icons/app_icon.svg"}
|
|
||||||
title="Explore models"
|
|
||||||
height={16}
|
|
||||||
width={16}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtom, useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { ReactNode, useEffect } from "react";
|
import { ReactNode, useEffect } from "react";
|
||||||
import {
|
import { appDownloadProgress } from "./JotaiWrapper";
|
||||||
appDownloadProgress,
|
|
||||||
setDownloadStateAtom,
|
|
||||||
setDownloadStateSuccessAtom,
|
|
||||||
} from "./JotaiWrapper";
|
|
||||||
import { DownloadState } from "@/_models/DownloadState";
|
import { DownloadState } from "@/_models/DownloadState";
|
||||||
import { execute } from "../../../electron/core/plugin-manager/execution/extension-manager";
|
import { execute } from "../../../electron/core/plugin-manager/execution/extension-manager";
|
||||||
import { DataService } from "../../shared/coreService";
|
import { DataService } from "../../shared/coreService";
|
||||||
|
import {
|
||||||
|
setDownloadStateAtom,
|
||||||
|
setDownloadStateSuccessAtom,
|
||||||
|
} from "./atoms/DownloadState.atom";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChatMessage, MessageStatus } from "@/_models/ChatMessage";
|
|
||||||
import { Conversation, ConversationState } from "@/_models/Conversation";
|
|
||||||
import { DownloadState } from "@/_models/DownloadState";
|
|
||||||
import { Product } from "@/_models/Product";
|
|
||||||
import { Provider, atom } from "jotai";
|
import { Provider, atom } from "jotai";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
@ -15,286 +11,11 @@ export default function JotaiWrapper({ children }: Props) {
|
|||||||
return <Provider>{children}</Provider>;
|
return <Provider>{children}</Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeConversationIdAtom = atom<string | undefined>(undefined);
|
|
||||||
export const getActiveConvoIdAtom = atom((get) =>
|
|
||||||
get(activeConversationIdAtom)
|
|
||||||
);
|
|
||||||
export const setActiveConvoIdAtom = atom(
|
|
||||||
null,
|
|
||||||
(_get, set, convoId: string | undefined) => {
|
|
||||||
if (convoId) {
|
|
||||||
console.log(`set active convo id to ${convoId}`);
|
|
||||||
set(setMainViewStateAtom, MainViewState.Conversation);
|
|
||||||
}
|
|
||||||
set(activeConversationIdAtom, convoId);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const currentPromptAtom = atom<string>("");
|
export const currentPromptAtom = atom<string>("");
|
||||||
|
|
||||||
export const showingAdvancedPromptAtom = atom<boolean>(false);
|
|
||||||
export const showingProductDetailAtom = atom<boolean>(false);
|
|
||||||
export const showingMobilePaneAtom = atom<boolean>(false);
|
|
||||||
export const showingTyping = atom<boolean>(false);
|
export const showingTyping = atom<boolean>(false);
|
||||||
|
|
||||||
export const appDownloadProgress = atom<number>(-1);
|
export const appDownloadProgress = atom<number>(-1);
|
||||||
export const activeModel = atom<string | undefined>(undefined);
|
|
||||||
export const searchingModelText = atom<string>("");
|
export const searchingModelText = atom<string>("");
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores all conversations for the current user
|
|
||||||
*/
|
|
||||||
export const userConversationsAtom = atom<Conversation[]>([]);
|
|
||||||
export const currentConversationAtom = atom<Conversation | undefined>((get) =>
|
|
||||||
get(userConversationsAtom).find((c) => c.id === get(activeConversationIdAtom))
|
|
||||||
);
|
|
||||||
export const setConvoUpdatedAtAtom = atom(null, (get, set, convoId: string) => {
|
|
||||||
const convo = get(userConversationsAtom).find((c) => c.id === convoId);
|
|
||||||
if (!convo) return;
|
|
||||||
const newConvo: Conversation = {
|
|
||||||
...convo,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
const newConversations: Conversation[] = get(userConversationsAtom).map((c) =>
|
|
||||||
c.id === convoId ? newConvo : c
|
|
||||||
);
|
|
||||||
|
|
||||||
set(userConversationsAtom, newConversations);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const currentStreamingMessageAtom = atom<ChatMessage | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
export const setConvoLastImageAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, convoId: string, lastImageUrl: string) => {
|
|
||||||
const convo = get(userConversationsAtom).find((c) => c.id === convoId);
|
|
||||||
if (!convo) return;
|
|
||||||
const newConvo: Conversation = { ...convo };
|
|
||||||
const newConversations: Conversation[] = get(userConversationsAtom).map(
|
|
||||||
(c) => (c.id === convoId ? newConvo : c)
|
|
||||||
);
|
|
||||||
|
|
||||||
set(userConversationsAtom, newConversations);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores all conversation states for the current user
|
|
||||||
*/
|
|
||||||
export const conversationStatesAtom = atom<Record<string, ConversationState>>(
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
export const currentConvoStateAtom = atom<ConversationState | undefined>(
|
|
||||||
(get) => {
|
|
||||||
const activeConvoId = get(activeConversationIdAtom);
|
|
||||||
if (!activeConvoId) {
|
|
||||||
console.log("active convo id is undefined");
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return get(conversationStatesAtom)[activeConvoId];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
export const addNewConversationStateAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, conversationId: string, state: ConversationState) => {
|
|
||||||
const currentState = { ...get(conversationStatesAtom) };
|
|
||||||
currentState[conversationId] = state;
|
|
||||||
set(conversationStatesAtom, currentState);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
export const updateConversationWaitingForResponseAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, conversationId: string, waitingForResponse: boolean) => {
|
|
||||||
const currentState = { ...get(conversationStatesAtom) };
|
|
||||||
currentState[conversationId] = {
|
|
||||||
...currentState[conversationId],
|
|
||||||
waitingForResponse,
|
|
||||||
};
|
|
||||||
set(conversationStatesAtom, currentState);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
export const updateConversationHasMoreAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, conversationId: string, hasMore: boolean) => {
|
|
||||||
const currentState = { ...get(conversationStatesAtom) };
|
|
||||||
currentState[conversationId] = { ...currentState[conversationId], hasMore };
|
|
||||||
set(conversationStatesAtom, currentState);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores all chat messages for all conversations
|
|
||||||
*/
|
|
||||||
export const chatMessages = atom<Record<string, ChatMessage[]>>({});
|
|
||||||
export const currentChatMessagesAtom = atom<ChatMessage[]>((get) => {
|
|
||||||
const activeConversationId = get(activeConversationIdAtom);
|
|
||||||
if (!activeConversationId) return [];
|
|
||||||
return get(chatMessages)[activeConversationId] ?? [];
|
|
||||||
});
|
|
||||||
|
|
||||||
export const addOldMessagesAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, newMessages: ChatMessage[]) => {
|
|
||||||
const currentConvoId = get(activeConversationIdAtom);
|
|
||||||
if (!currentConvoId) return;
|
|
||||||
|
|
||||||
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
|
|
||||||
const updatedMessages = [...currentMessages, ...newMessages];
|
|
||||||
|
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
|
||||||
...get(chatMessages),
|
|
||||||
};
|
|
||||||
newData[currentConvoId] = updatedMessages;
|
|
||||||
set(chatMessages, newData);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
export const addNewMessageAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, newMessage: ChatMessage) => {
|
|
||||||
const currentConvoId = get(activeConversationIdAtom);
|
|
||||||
if (!currentConvoId) return;
|
|
||||||
|
|
||||||
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
|
|
||||||
const updatedMessages = [newMessage, ...currentMessages];
|
|
||||||
|
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
|
||||||
...get(chatMessages),
|
|
||||||
};
|
|
||||||
newData[currentConvoId] = updatedMessages;
|
|
||||||
set(chatMessages, newData);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const deleteConversationMessage = atom(null, (get, set, id: string) => {
|
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
|
||||||
...get(chatMessages),
|
|
||||||
};
|
|
||||||
newData[id] = [];
|
|
||||||
set(chatMessages, newData);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateMessageAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, id: string, conversationId: string, text: string) => {
|
|
||||||
const messages = get(chatMessages)[conversationId] ?? [];
|
|
||||||
const message = messages.find((e) => e.id === id);
|
|
||||||
if (message) {
|
|
||||||
message.text = text;
|
|
||||||
const updatedMessages = [...messages];
|
|
||||||
|
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
|
||||||
...get(chatMessages),
|
|
||||||
};
|
|
||||||
newData[conversationId] = updatedMessages;
|
|
||||||
set(chatMessages, newData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
/**
|
|
||||||
* For updating the status of the last AI message that is pending
|
|
||||||
*/
|
|
||||||
export const updateLastMessageAsReadyAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, id, text: string) => {
|
|
||||||
const currentConvoId = get(activeConversationIdAtom);
|
|
||||||
if (!currentConvoId) return;
|
|
||||||
|
|
||||||
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
|
|
||||||
const messageToUpdate = currentMessages.find((e) => e.id === id);
|
|
||||||
|
|
||||||
// if message is not found, do nothing
|
|
||||||
if (!messageToUpdate) return;
|
|
||||||
|
|
||||||
const index = currentMessages.indexOf(messageToUpdate);
|
|
||||||
const updatedMsg: ChatMessage = {
|
|
||||||
...messageToUpdate,
|
|
||||||
status: MessageStatus.Ready,
|
|
||||||
text: text,
|
|
||||||
};
|
|
||||||
|
|
||||||
currentMessages[index] = updatedMsg;
|
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
|
||||||
...get(chatMessages),
|
|
||||||
};
|
|
||||||
newData[currentConvoId] = currentMessages;
|
|
||||||
set(chatMessages, newData);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const currentProductAtom = atom<Product | undefined>(undefined);
|
|
||||||
|
|
||||||
export const searchAtom = atom<string>("");
|
export const searchAtom = atom<string>("");
|
||||||
|
|
||||||
// modal atoms
|
|
||||||
export const showConfirmDeleteConversationModalAtom = atom(false);
|
|
||||||
export const showConfirmSignOutModalAtom = atom(false);
|
|
||||||
export const showConfirmDeleteModalAtom = atom(false);
|
|
||||||
|
|
||||||
export type FileDownloadStates = {
|
|
||||||
[key: string]: DownloadState;
|
|
||||||
};
|
|
||||||
|
|
||||||
// main view state
|
|
||||||
export enum MainViewState {
|
|
||||||
Welcome,
|
|
||||||
ExploreModel,
|
|
||||||
MyModel,
|
|
||||||
ResourceMonitor,
|
|
||||||
Setting,
|
|
||||||
Conversation,
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemBarVisibilityAtom = atom<boolean>(true);
|
|
||||||
export const getSystemBarVisibilityAtom = atom((get) =>
|
|
||||||
get(systemBarVisibilityAtom)
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentMainViewStateAtom = atom<MainViewState>(MainViewState.Welcome);
|
|
||||||
export const getMainViewStateAtom = atom((get) =>
|
|
||||||
get(currentMainViewStateAtom)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const setMainViewStateAtom = atom(
|
|
||||||
null,
|
|
||||||
(_get, set, state: MainViewState) => {
|
|
||||||
if (state !== MainViewState.Conversation) {
|
|
||||||
set(activeConversationIdAtom, undefined);
|
|
||||||
}
|
|
||||||
const showSystemBar = state !== MainViewState.Conversation;
|
|
||||||
set(systemBarVisibilityAtom, showSystemBar);
|
|
||||||
set(currentMainViewStateAtom, state);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// download states
|
|
||||||
export const modelDownloadStateAtom = atom<FileDownloadStates>({});
|
|
||||||
|
|
||||||
export const setDownloadStateAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, state: DownloadState) => {
|
|
||||||
const currentState = { ...get(modelDownloadStateAtom) };
|
|
||||||
console.debug(
|
|
||||||
`current download state for ${state.fileName} is ${JSON.stringify(state)}`
|
|
||||||
);
|
|
||||||
currentState[state.fileName] = state;
|
|
||||||
set(modelDownloadStateAtom, currentState);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const setDownloadStateSuccessAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, fileName: string) => {
|
|
||||||
const currentState = { ...get(modelDownloadStateAtom) };
|
|
||||||
const state = currentState[fileName];
|
|
||||||
if (!state) {
|
|
||||||
console.error(`Cannot find download state for ${fileName}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete currentState[fileName];
|
|
||||||
set(modelDownloadStateAtom, currentState);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|||||||
109
web/app/_helpers/atoms/ChatMessage.atom.ts
Normal file
109
web/app/_helpers/atoms/ChatMessage.atom.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { ChatMessage, MessageStatus } from "@/_models/ChatMessage";
|
||||||
|
import { atom } from "jotai";
|
||||||
|
import { getActiveConvoIdAtom } from "./Conversation.atom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores all chat messages for all conversations
|
||||||
|
*/
|
||||||
|
export const chatMessages = atom<Record<string, ChatMessage[]>>({});
|
||||||
|
|
||||||
|
export const currentChatMessagesAtom = atom<ChatMessage[]>((get) => {
|
||||||
|
const activeConversationId = get(getActiveConvoIdAtom);
|
||||||
|
if (!activeConversationId) return [];
|
||||||
|
return get(chatMessages)[activeConversationId] ?? [];
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addOldMessagesAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, newMessages: ChatMessage[]) => {
|
||||||
|
const currentConvoId = get(getActiveConvoIdAtom);
|
||||||
|
if (!currentConvoId) return;
|
||||||
|
|
||||||
|
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
|
||||||
|
const updatedMessages = [...currentMessages, ...newMessages];
|
||||||
|
|
||||||
|
const newData: Record<string, ChatMessage[]> = {
|
||||||
|
...get(chatMessages),
|
||||||
|
};
|
||||||
|
newData[currentConvoId] = updatedMessages;
|
||||||
|
set(chatMessages, newData);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const addNewMessageAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, newMessage: ChatMessage) => {
|
||||||
|
const currentConvoId = get(getActiveConvoIdAtom);
|
||||||
|
if (!currentConvoId) return;
|
||||||
|
|
||||||
|
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
|
||||||
|
const updatedMessages = [newMessage, ...currentMessages];
|
||||||
|
|
||||||
|
const newData: Record<string, ChatMessage[]> = {
|
||||||
|
...get(chatMessages),
|
||||||
|
};
|
||||||
|
newData[currentConvoId] = updatedMessages;
|
||||||
|
set(chatMessages, newData);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteConversationMessage = atom(null, (get, set, id: string) => {
|
||||||
|
const newData: Record<string, ChatMessage[]> = {
|
||||||
|
...get(chatMessages),
|
||||||
|
};
|
||||||
|
newData[id] = [];
|
||||||
|
set(chatMessages, newData);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateMessageAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, id: string, conversationId: string, text: string) => {
|
||||||
|
const messages = get(chatMessages)[conversationId] ?? [];
|
||||||
|
const message = messages.find((e) => e.id === id);
|
||||||
|
if (message) {
|
||||||
|
message.text = text;
|
||||||
|
const updatedMessages = [...messages];
|
||||||
|
|
||||||
|
const newData: Record<string, ChatMessage[]> = {
|
||||||
|
...get(chatMessages),
|
||||||
|
};
|
||||||
|
newData[conversationId] = updatedMessages;
|
||||||
|
set(chatMessages, newData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For updating the status of the last AI message that is pending
|
||||||
|
*/
|
||||||
|
export const updateLastMessageAsReadyAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, id, text: string) => {
|
||||||
|
const currentConvoId = get(getActiveConvoIdAtom);
|
||||||
|
if (!currentConvoId) return;
|
||||||
|
|
||||||
|
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
|
||||||
|
const messageToUpdate = currentMessages.find((e) => e.id === id);
|
||||||
|
|
||||||
|
// if message is not found, do nothing
|
||||||
|
if (!messageToUpdate) return;
|
||||||
|
|
||||||
|
const index = currentMessages.indexOf(messageToUpdate);
|
||||||
|
const updatedMsg: ChatMessage = {
|
||||||
|
...messageToUpdate,
|
||||||
|
status: MessageStatus.Ready,
|
||||||
|
text: text,
|
||||||
|
};
|
||||||
|
|
||||||
|
currentMessages[index] = updatedMsg;
|
||||||
|
const newData: Record<string, ChatMessage[]> = {
|
||||||
|
...get(chatMessages),
|
||||||
|
};
|
||||||
|
newData[currentConvoId] = currentMessages;
|
||||||
|
set(chatMessages, newData);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const currentStreamingMessageAtom = atom<ChatMessage | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
104
web/app/_helpers/atoms/Conversation.atom.ts
Normal file
104
web/app/_helpers/atoms/Conversation.atom.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import { MainViewState, setMainViewStateAtom } from "./MainView.atom";
|
||||||
|
import { Conversation, ConversationState } from "@/_models/Conversation";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the current active conversation id.
|
||||||
|
*/
|
||||||
|
const activeConversationIdAtom = atom<string | undefined>(undefined);
|
||||||
|
|
||||||
|
export const getActiveConvoIdAtom = atom((get) =>
|
||||||
|
get(activeConversationIdAtom)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setActiveConvoIdAtom = atom(
|
||||||
|
null,
|
||||||
|
(_get, set, convoId: string | undefined) => {
|
||||||
|
if (convoId) {
|
||||||
|
console.debug(`Set active conversation id: ${convoId}`);
|
||||||
|
set(setMainViewStateAtom, MainViewState.Conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(activeConversationIdAtom, convoId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores all conversation states for the current user
|
||||||
|
*/
|
||||||
|
export const conversationStatesAtom = atom<Record<string, ConversationState>>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
export const currentConvoStateAtom = atom<ConversationState | undefined>(
|
||||||
|
(get) => {
|
||||||
|
const activeConvoId = get(activeConversationIdAtom);
|
||||||
|
if (!activeConvoId) {
|
||||||
|
console.log("active convo id is undefined");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return get(conversationStatesAtom)[activeConvoId];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export const addNewConversationStateAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, conversationId: string, state: ConversationState) => {
|
||||||
|
const currentState = { ...get(conversationStatesAtom) };
|
||||||
|
currentState[conversationId] = state;
|
||||||
|
set(conversationStatesAtom, currentState);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export const updateConversationWaitingForResponseAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, conversationId: string, waitingForResponse: boolean) => {
|
||||||
|
const currentState = { ...get(conversationStatesAtom) };
|
||||||
|
currentState[conversationId] = {
|
||||||
|
...currentState[conversationId],
|
||||||
|
waitingForResponse,
|
||||||
|
};
|
||||||
|
set(conversationStatesAtom, currentState);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export const updateConversationHasMoreAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, conversationId: string, hasMore: boolean) => {
|
||||||
|
const currentState = { ...get(conversationStatesAtom) };
|
||||||
|
currentState[conversationId] = { ...currentState[conversationId], hasMore };
|
||||||
|
set(conversationStatesAtom, currentState);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores all conversations for the current user
|
||||||
|
*/
|
||||||
|
export const userConversationsAtom = atom<Conversation[]>([]);
|
||||||
|
export const currentConversationAtom = atom<Conversation | undefined>((get) =>
|
||||||
|
get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom))
|
||||||
|
);
|
||||||
|
export const setConvoUpdatedAtAtom = atom(null, (get, set, convoId: string) => {
|
||||||
|
const convo = get(userConversationsAtom).find((c) => c.id === convoId);
|
||||||
|
if (!convo) return;
|
||||||
|
const newConvo: Conversation = {
|
||||||
|
...convo,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
const newConversations: Conversation[] = get(userConversationsAtom).map((c) =>
|
||||||
|
c.id === convoId ? newConvo : c
|
||||||
|
);
|
||||||
|
|
||||||
|
set(userConversationsAtom, newConversations);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setConvoLastImageAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, convoId: string, lastImageUrl: string) => {
|
||||||
|
const convo = get(userConversationsAtom).find((c) => c.id === convoId);
|
||||||
|
if (!convo) return;
|
||||||
|
const newConvo: Conversation = { ...convo };
|
||||||
|
const newConversations: Conversation[] = get(userConversationsAtom).map(
|
||||||
|
(c) => (c.id === convoId ? newConvo : c)
|
||||||
|
);
|
||||||
|
|
||||||
|
set(userConversationsAtom, newConversations);
|
||||||
|
}
|
||||||
|
);
|
||||||
32
web/app/_helpers/atoms/DownloadState.atom.ts
Normal file
32
web/app/_helpers/atoms/DownloadState.atom.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { DownloadState } from "@/_models/DownloadState";
|
||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
// download states
|
||||||
|
export const modelDownloadStateAtom = atom<Record<string, DownloadState>>({});
|
||||||
|
|
||||||
|
export const setDownloadStateAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, state: DownloadState) => {
|
||||||
|
const currentState = { ...get(modelDownloadStateAtom) };
|
||||||
|
console.debug(
|
||||||
|
`current download state for ${state.fileName} is ${JSON.stringify(state)}`
|
||||||
|
);
|
||||||
|
currentState[state.fileName] = state;
|
||||||
|
set(modelDownloadStateAtom, currentState);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setDownloadStateSuccessAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, fileName: string) => {
|
||||||
|
const currentState = { ...get(modelDownloadStateAtom) };
|
||||||
|
const state = currentState[fileName];
|
||||||
|
if (!state) {
|
||||||
|
console.error(`Cannot find download state for ${fileName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete currentState[fileName];
|
||||||
|
set(modelDownloadStateAtom, currentState);
|
||||||
|
}
|
||||||
|
);
|
||||||
54
web/app/_helpers/atoms/MainView.atom.ts
Normal file
54
web/app/_helpers/atoms/MainView.atom.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import { setActiveConvoIdAtom } from "./Conversation.atom";
|
||||||
|
import { systemBarVisibilityAtom } from "./SystemBar.atom";
|
||||||
|
|
||||||
|
export enum MainViewState {
|
||||||
|
Welcome,
|
||||||
|
ExploreModel,
|
||||||
|
MyModel,
|
||||||
|
ResourceMonitor,
|
||||||
|
Setting,
|
||||||
|
Conversation,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When user wants to create new conversation but haven't selected a model yet.
|
||||||
|
*/
|
||||||
|
ConversationEmptyModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the current main view state. Default is Welcome.
|
||||||
|
*/
|
||||||
|
const currentMainViewStateAtom = atom<MainViewState>(MainViewState.Welcome);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter for current main view state.
|
||||||
|
*/
|
||||||
|
export const getMainViewStateAtom = atom((get) =>
|
||||||
|
get(currentMainViewStateAtom)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setter for current main view state.
|
||||||
|
*/
|
||||||
|
export const setMainViewStateAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, state: MainViewState) => {
|
||||||
|
// return if the state is already set
|
||||||
|
if (get(getMainViewStateAtom) === state) return;
|
||||||
|
|
||||||
|
if (state !== MainViewState.Conversation) {
|
||||||
|
// clear active conversation id if main view state is not Conversation
|
||||||
|
set(setActiveConvoIdAtom, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSystemBar =
|
||||||
|
state !== MainViewState.Conversation &&
|
||||||
|
state !== MainViewState.ConversationEmptyModel;
|
||||||
|
|
||||||
|
// show system bar if state is not Conversation nor ConversationEmptyModel
|
||||||
|
set(systemBarVisibilityAtom, showSystemBar);
|
||||||
|
|
||||||
|
set(currentMainViewStateAtom, state);
|
||||||
|
}
|
||||||
|
);
|
||||||
8
web/app/_helpers/atoms/Modal.atom.ts
Normal file
8
web/app/_helpers/atoms/Modal.atom.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
export const showConfirmDeleteConversationModalAtom = atom(false);
|
||||||
|
export const showConfirmSignOutModalAtom = atom(false);
|
||||||
|
export const showConfirmDeleteModalAtom = atom(false);
|
||||||
|
export const showingAdvancedPromptAtom = atom<boolean>(false);
|
||||||
|
export const showingProductDetailAtom = atom<boolean>(false);
|
||||||
|
export const showingMobilePaneAtom = atom<boolean>(false);
|
||||||
6
web/app/_helpers/atoms/Model.atom.ts
Normal file
6
web/app/_helpers/atoms/Model.atom.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { Product } from "@/_models/Product";
|
||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
export const currentProductAtom = atom<Product | undefined>(undefined);
|
||||||
|
|
||||||
|
export const selectedModelAtom = atom<Product | undefined>(undefined);
|
||||||
7
web/app/_helpers/atoms/SystemBar.atom.ts
Normal file
7
web/app/_helpers/atoms/SystemBar.atom.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
export const systemBarVisibilityAtom = atom<boolean>(true);
|
||||||
|
|
||||||
|
export const getSystemBarVisibilityAtom = atom((get) =>
|
||||||
|
get(systemBarVisibilityAtom)
|
||||||
|
);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user