Merge pull request #5715 from menloresearch/release/v0.6.6
Merge Release/v0.6.6 into release 0.6.5
This commit is contained in:
commit
a2c59e9934
24
.devcontainer/buildAppImage.sh
Normal file
24
.devcontainer/buildAppImage.sh
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
make clean
|
||||||
|
|
||||||
|
# To reproduce https://github.com/menloresearch/jan/pull/5463
|
||||||
|
TAURI_TOOLKIT_PATH="${XDG_CACHE_HOME:-$HOME/.cache}/tauri"
|
||||||
|
mkdir -p "$TAURI_TOOLKIT_PATH"
|
||||||
|
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage -O "$TAURI_TOOLKIT_PATH/linuxdeploy-x86_64.AppImage"
|
||||||
|
chmod +x "$TAURI_TOOLKIT_PATH/linuxdeploy-x86_64.AppImage"
|
||||||
|
|
||||||
|
jq '.bundle.resources = ["resources/pre-install/**/*"] | .bundle.externalBin = ["binaries/cortex-server", "resources/bin/uv"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
|
||||||
|
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
|
||||||
|
|
||||||
|
make build-tauri
|
||||||
|
|
||||||
|
cp ./src-tauri/resources/bin/bun ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/bin/bun
|
||||||
|
mkdir -p ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/engines
|
||||||
|
cp -f ./src-tauri/binaries/deps/*.so* ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/
|
||||||
|
cp -f ./src-tauri/binaries/*.so* ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/
|
||||||
|
cp -rf ./src-tauri/binaries/engines ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/
|
||||||
|
APP_IMAGE=./src-tauri/target/release/bundle/appimage/$(ls ./src-tauri/target/release/bundle/appimage/ | grep AppImage | head -1)
|
||||||
|
echo $APP_IMAGE
|
||||||
|
rm -f $APP_IMAGE
|
||||||
|
/opt/bin/appimagetool ./src-tauri/target/release/bundle/appimage/Jan.AppDir $APP_IMAGE
|
||||||
@ -1,4 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "jan",
|
"name": "Jan",
|
||||||
"image": "node:20"
|
"image": "mcr.microsoft.com/devcontainers/base:jammy",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
"version": "20"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/rust:1": {},
|
||||||
|
"ghcr.io/devcontainers-extra/features/corepack:1": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
"postCreateCommand": "./.devcontainer/postCreateCommand.sh",
|
||||||
|
|
||||||
|
// appimagekit requires fuse to package appimage, to use fuse in the container you need to enable it on the host
|
||||||
|
"runArgs": [
|
||||||
|
"--device", "/dev/fuse",
|
||||||
|
"--cap-add=SYS_ADMIN",
|
||||||
|
"--security-opt", "apparmor:unconfined"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
20
.devcontainer/postCreateCommand.sh
Executable file
20
.devcontainer/postCreateCommand.sh
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# install tauri prerequisites + xdg-utils for xdg-open + libfuse2 for using appimagekit
|
||||||
|
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -yqq libwebkit2gtk-4.1-dev \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
file \
|
||||||
|
libxdo-dev \
|
||||||
|
libssl-dev \
|
||||||
|
libayatana-appindicator3-dev \
|
||||||
|
librsvg2-dev \
|
||||||
|
xdg-utils \
|
||||||
|
libfuse2
|
||||||
|
|
||||||
|
sudo mkdir -p /opt/bin
|
||||||
|
sudo wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /opt/bin/appimagetool
|
||||||
|
sudo chmod +x /opt/bin/appimagetool
|
||||||
24
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: 🐛 Bug Report
|
||||||
|
about: If something isn't working as expected 🤔
|
||||||
|
title: 'bug: '
|
||||||
|
type: Bug
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version:** e.g. 0.5.x-xxx
|
||||||
|
|
||||||
|
## Describe the Bug
|
||||||
|
<!-- A clear & concise description of the bug -->
|
||||||
|
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1.
|
||||||
|
|
||||||
|
## Screenshots / Logs
|
||||||
|
<!-- You can find logs in: Setting -> General -> Data Folder -> App Logs -->
|
||||||
|
|
||||||
|
|
||||||
|
## Operating System
|
||||||
|
- [ ] MacOS
|
||||||
|
- [ ] Windows
|
||||||
|
- [ ] Linux
|
||||||
11
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: 🚀 Feature Request
|
||||||
|
about: Suggest an idea for this project 😻!
|
||||||
|
title: 'idea: '
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
<!-- Describe the problem you're facing -->
|
||||||
|
|
||||||
|
## Feature Idea
|
||||||
|
<!-- Describe what you want instead. Examples are welcome! -->
|
||||||
12
.github/ISSUE_TEMPLATE/3-epic.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/3-epic.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: 🌟 Epic
|
||||||
|
about: Major building block that advances Jan's goals
|
||||||
|
title: 'epic: '
|
||||||
|
type: Epic
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
## Tasklist
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
13
.github/ISSUE_TEMPLATE/4-goal.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/4-goal.md
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: 🎯 Goal
|
||||||
|
about: External communication of Jan's roadmap and objectives
|
||||||
|
title: 'goal: '
|
||||||
|
type: Goal
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
## Tasklist
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
43
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
43
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,43 +0,0 @@
|
|||||||
name: "\U0001F41B Bug Report"
|
|
||||||
description: "If something isn't working as expected \U0001F914"
|
|
||||||
labels: [ "type: bug" ]
|
|
||||||
title: 'bug: [DESCRIPTION]'
|
|
||||||
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: "Jan version"
|
|
||||||
description: "**Tip:** The version is in the app's bottom right corner"
|
|
||||||
placeholder: "e.g. 0.5.x-xxx"
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: "Describe the Bug"
|
|
||||||
description: "A clear & concise description of the bug"
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: "Steps to Reproduce"
|
|
||||||
description: |
|
|
||||||
Please list out steps to reproduce the issue
|
|
||||||
placeholder: |
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '...'
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: "Screenshots / Logs"
|
|
||||||
description: |
|
|
||||||
You can find logs in: ~/jan/logs/app.logs
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: "What is your OS?"
|
|
||||||
options:
|
|
||||||
- label: MacOS
|
|
||||||
- label: Windows
|
|
||||||
- label: Linux
|
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,7 +1,5 @@
|
|||||||
## To encourage contributors to use issue templates, we don't allow blank issues
|
|
||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
|
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: "\1F4AC Jan Discussions"
|
- name: Jan Discussions
|
||||||
url: "https://github.com/orgs/menloresearch/discussions/categories/q-a"
|
url: https://github.com/orgs/menloresearch/discussions/categories/q-a
|
||||||
about: "Get help, discuss features & roadmap, and share your projects"
|
about: Get help, discuss features & roadmap, and share your projects
|
||||||
|
|||||||
20
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
20
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,20 +0,0 @@
|
|||||||
name: "\U0001F680 Feature Request"
|
|
||||||
description: "Suggest an idea for this project \U0001F63B!"
|
|
||||||
title: 'idea: [DESCRIPTION]'
|
|
||||||
labels: 'type: feature request'
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: "Problem Statement"
|
|
||||||
description: "Describe the problem you're facing"
|
|
||||||
placeholder: |
|
|
||||||
I'm always frustrated when ...
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: "Feature Idea"
|
|
||||||
description: "Describe what you want instead. Examples are welcome!"
|
|
||||||
21
.github/ISSUE_TEMPLATE/model_request.yml
vendored
21
.github/ISSUE_TEMPLATE/model_request.yml
vendored
@ -1,21 +0,0 @@
|
|||||||
name: "\U0001F929 Model Request"
|
|
||||||
description: "Request a new model to be compiled"
|
|
||||||
title: 'feat: [DESCRIPTION]'
|
|
||||||
labels: 'type: model request'
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "**Tip:** Download any HuggingFace model in app ([see guides](https://jan.ai/docs/models/manage-models#add-models)). Use this form for unsupported models only."
|
|
||||||
- type: textarea
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: "Model Requests"
|
|
||||||
description: "If applicable, include the source URL, licenses, and any other relevant information"
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: "Which formats?"
|
|
||||||
options:
|
|
||||||
- label: GGUF (llama.cpp)
|
|
||||||
- label: TensorRT (TensorRT-LLM)
|
|
||||||
- label: ONNX (Onnx Runtime)
|
|
||||||
35
.github/ISSUE_TEMPLATE/roadmap.md
vendored
35
.github/ISSUE_TEMPLATE/roadmap.md
vendored
@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
name: Roadmap
|
|
||||||
about: Plan Roadmap items with subtasks
|
|
||||||
title: 'roadmap: '
|
|
||||||
labels: 'type: planning'
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
## Tasklist
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- [ ] link to janhq/jan epics
|
|
||||||
|
|
||||||
**Bugs**
|
|
||||||
- [ ] link to bugs
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- [ ] link to janhq/cortex.cpp epics
|
|
||||||
|
|
||||||
**Bugs**
|
|
||||||
- [ ] link to bug issues
|
|
||||||
|
|
||||||
### Infra
|
|
||||||
- [ ] link to infra issues
|
|
||||||
|
|
||||||
### Administrative / Management
|
|
||||||
- [ ] link to infra issues
|
|
||||||
|
|
||||||
### Marketing
|
|
||||||
|
|
||||||
-------
|
|
||||||
## Resources
|
|
||||||
16
.github/workflows/issues.yaml
vendored
Normal file
16
.github/workflows/issues.yaml
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
name: Adds all issues to project board
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-to-project:
|
||||||
|
name: Add issue to project
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/add-to-project@v1.0.2
|
||||||
|
with:
|
||||||
|
project-url: https://github.com/orgs/${{ vars.ORG_NAME }}/projects/${{ vars.JAN_PROJECT_NUMBER }}
|
||||||
|
github-token: ${{ secrets.AUTO_ADD_TICKET_PAT }}
|
||||||
@ -154,6 +154,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install tauri-driver dependencies
|
||||||
|
run: |
|
||||||
|
cargo install tauri-driver --locked
|
||||||
|
|
||||||
# Clean cache, continue on error
|
# Clean cache, continue on error
|
||||||
- name: 'Cleanup cache'
|
- name: 'Cleanup cache'
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@ -192,11 +196,25 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/cache@v4 # v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
|
||||||
- name: Installing node
|
- name: Installing node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install tauri-driver dependencies
|
||||||
|
run: |
|
||||||
|
cargo install tauri-driver --locked
|
||||||
|
|
||||||
# Clean cache, continue on error
|
# Clean cache, continue on error
|
||||||
- name: 'Cleanup cache'
|
- name: 'Cleanup cache'
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@ -221,6 +239,20 @@ jobs:
|
|||||||
# run: |
|
# run: |
|
||||||
# make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Windows" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
|
# make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Windows" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
|
||||||
|
|
||||||
|
- name: Install Prerequisites
|
||||||
|
shell: 'powershell'
|
||||||
|
# https://github.com/actions/runner-images/issues/9538
|
||||||
|
# https://github.com/microsoft/playwright/pull/30009/files
|
||||||
|
# https://github.com/tauri-apps/wry/issues/1268
|
||||||
|
# Evergreen Bootstrapper
|
||||||
|
# The Bootstrapper is a tiny installer that downloads
|
||||||
|
# the Evergreen Runtime matching device architecture
|
||||||
|
# and installs it locally.
|
||||||
|
# https://developer.microsoft.com/en-us/microsoft-edge/webview2/consumer/?form=MA13LH
|
||||||
|
run: |
|
||||||
|
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
|
||||||
|
Start-Process -FilePath setup.exe -Verb RunAs -Wait
|
||||||
|
|
||||||
- name: Linter and test
|
- name: Linter and test
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
@ -240,6 +272,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install tauri-driver dependencies
|
||||||
|
run: |
|
||||||
|
cargo install tauri-driver --locked
|
||||||
|
|
||||||
# Clean cache, continue on error
|
# Clean cache, continue on error
|
||||||
- name: 'Cleanup cache'
|
- name: 'Cleanup cache'
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@ -272,6 +308,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install Tauri dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y libglib2.0-dev libatk1.0-dev libpango1.0-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev libfuse2 webkit2gtk-driver
|
||||||
|
|
||||||
|
- name: Install tauri-driver dependencies
|
||||||
|
run: |
|
||||||
|
cargo install tauri-driver --locked
|
||||||
|
|
||||||
- name: 'Cleanup cache'
|
- name: 'Cleanup cache'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
@ -368,6 +413,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install Tauri dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y libglib2.0-dev libatk1.0-dev libpango1.0-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev libfuse2 webkit2gtk-driver
|
||||||
|
|
||||||
|
- name: Install tauri-driver dependencies
|
||||||
|
run: |
|
||||||
|
cargo install tauri-driver --locked
|
||||||
|
|
||||||
- name: 'Cleanup cache'
|
- name: 'Cleanup cache'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
1
.github/workflows/publish-npm-core.yml
vendored
1
.github/workflows/publish-npm-core.yml
vendored
@ -6,7 +6,6 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
build-and-publish-plugins:
|
build-and-publish-plugins:
|
||||||
environment: production
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@ -43,7 +43,6 @@ jobs:
|
|||||||
build-linux-x64:
|
build-linux-x64:
|
||||||
if: inputs.public_provider == 'github' || inputs.public_provider == 'none'
|
if: inputs.public_provider == 'github' || inputs.public_provider == 'none'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: production
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
@ -76,7 +75,6 @@ jobs:
|
|||||||
cp electron/icons_dev/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
|
cp electron/icons_dev/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
|
||||||
cp electron/icons_dev/jan-nightly-tray.png electron/icons/icon-tray.png
|
cp electron/icons_dev/jan-nightly-tray.png electron/icons/icon-tray.png
|
||||||
|
|
||||||
|
|
||||||
- name: Installing node
|
- name: Installing node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@ -53,7 +53,6 @@ jobs:
|
|||||||
build-macos:
|
build-macos:
|
||||||
if: inputs.public_provider == 'github' || inputs.public_provider == 'none'
|
if: inputs.public_provider == 'github' || inputs.public_provider == 'none'
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
environment: production
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@ -9,7 +9,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
get-update-version:
|
get-update-version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: production
|
|
||||||
outputs:
|
outputs:
|
||||||
new_version: ${{ steps.version_update.outputs.new_version }}
|
new_version: ${{ steps.version_update.outputs.new_version }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@ -26,7 +26,6 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
noti-discord-and-update-url-readme:
|
noti-discord-and-update-url-readme:
|
||||||
environment: production
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@ -55,7 +55,6 @@ jobs:
|
|||||||
DEB_SIG: ${{ steps.packageinfo.outputs.DEB_SIG }}
|
DEB_SIG: ${{ steps.packageinfo.outputs.DEB_SIG }}
|
||||||
APPIMAGE_SIG: ${{ steps.packageinfo.outputs.APPIMAGE_SIG }}
|
APPIMAGE_SIG: ${{ steps.packageinfo.outputs.APPIMAGE_SIG }}
|
||||||
APPIMAGE_FILE_NAME: ${{ steps.packageinfo.outputs.APPIMAGE_FILE_NAME }}
|
APPIMAGE_FILE_NAME: ${{ steps.packageinfo.outputs.APPIMAGE_FILE_NAME }}
|
||||||
environment: production
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
@ -96,7 +95,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cargo install ctoml
|
cargo install ctoml
|
||||||
|
|
||||||
- name: Install Tauri dependecies
|
- name: Install Tauri dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y libglib2.0-dev libatk1.0-dev libpango1.0-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev libfuse2
|
sudo apt install -y libglib2.0-dev libatk1.0-dev libpango1.0-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev libfuse2
|
||||||
|
|||||||
@ -63,7 +63,6 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
MAC_UNIVERSAL_SIG: ${{ steps.metadata.outputs.MAC_UNIVERSAL_SIG }}
|
MAC_UNIVERSAL_SIG: ${{ steps.metadata.outputs.MAC_UNIVERSAL_SIG }}
|
||||||
TAR_NAME: ${{ steps.metadata.outputs.TAR_NAME }}
|
TAR_NAME: ${{ steps.metadata.outputs.TAR_NAME }}
|
||||||
environment: production
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true,
|
||||||
|
"[rust]": {
|
||||||
|
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
Makefile
11
Makefile
@ -33,23 +33,14 @@ dev: install-and-build
|
|||||||
yarn copy:lib
|
yarn copy:lib
|
||||||
yarn dev
|
yarn dev
|
||||||
|
|
||||||
# Deprecated soon
|
|
||||||
dev-tauri: install-and-build
|
|
||||||
yarn install:cortex
|
|
||||||
yarn download:bin
|
|
||||||
yarn copy:lib
|
|
||||||
yarn dev:tauri
|
|
||||||
|
|
||||||
# Linting
|
# Linting
|
||||||
lint: install-and-build
|
lint: install-and-build
|
||||||
yarn lint
|
yarn lint
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
test: lint
|
test: lint
|
||||||
# yarn build:test
|
|
||||||
# yarn test:coverage
|
|
||||||
# Need e2e setup for tauri backend
|
|
||||||
yarn test
|
yarn test
|
||||||
|
yarn test:e2e
|
||||||
|
|
||||||
# Builds and publishes the app
|
# Builds and publishes the app
|
||||||
build-and-publish: install-and-build
|
build-and-publish: install-and-build
|
||||||
|
|||||||
@ -23,24 +23,24 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@npmcli/arborist": "^7.1.0",
|
"@npmcli/arborist": "^7.1.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
"@types/pacote": "^11.1.7",
|
"@types/pacote": "^11.1.7",
|
||||||
"@types/request": "^2.48.12",
|
"@types/request": "^2.48.12",
|
||||||
"electron": "33.2.1",
|
"electron": "33.2.1",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-plugin-jest": "^27.9.0",
|
"eslint-plugin-jest": "^27.9.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^30.0.3",
|
||||||
"jest-junit": "^16.0.0",
|
"jest-junit": "^16.0.0",
|
||||||
"jest-runner": "^29.7.0",
|
"jest-runner": "^29.7.0",
|
||||||
"pacote": "^21.0.0",
|
"pacote": "^21.0.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-progress": "^3.0.0",
|
"request-progress": "^3.0.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^6.0.1",
|
||||||
"rolldown": "1.0.0-beta.1",
|
"rolldown": "1.0.0-beta.1",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
|||||||
@ -29,7 +29,7 @@ describe('validationRules', () => {
|
|||||||
expect(validationRules.top_k(1)).toBe(true)
|
expect(validationRules.top_k(1)).toBe(true)
|
||||||
expect(validationRules.top_k(0)).toBe(true)
|
expect(validationRules.top_k(0)).toBe(true)
|
||||||
expect(validationRules.top_k(-0.1)).toBe(false)
|
expect(validationRules.top_k(-0.1)).toBe(false)
|
||||||
expect(validationRules.top_k(1.1)).toBe(false)
|
expect(validationRules.top_k(1.1)).toBe(true)
|
||||||
expect(validationRules.top_k('0.5')).toBe(false)
|
expect(validationRules.top_k('0.5')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -68,8 +68,8 @@ describe('validationRules', () => {
|
|||||||
expect(validationRules.frequency_penalty(0.5)).toBe(true)
|
expect(validationRules.frequency_penalty(0.5)).toBe(true)
|
||||||
expect(validationRules.frequency_penalty(1)).toBe(true)
|
expect(validationRules.frequency_penalty(1)).toBe(true)
|
||||||
expect(validationRules.frequency_penalty(0)).toBe(true)
|
expect(validationRules.frequency_penalty(0)).toBe(true)
|
||||||
expect(validationRules.frequency_penalty(-0.1)).toBe(false)
|
expect(validationRules.frequency_penalty(-0.1)).toBe(true)
|
||||||
expect(validationRules.frequency_penalty(1.1)).toBe(false)
|
expect(validationRules.frequency_penalty(1.1)).toBe(true)
|
||||||
expect(validationRules.frequency_penalty('0.5')).toBe(false)
|
expect(validationRules.frequency_penalty('0.5')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -77,8 +77,8 @@ describe('validationRules', () => {
|
|||||||
expect(validationRules.presence_penalty(0.5)).toBe(true)
|
expect(validationRules.presence_penalty(0.5)).toBe(true)
|
||||||
expect(validationRules.presence_penalty(1)).toBe(true)
|
expect(validationRules.presence_penalty(1)).toBe(true)
|
||||||
expect(validationRules.presence_penalty(0)).toBe(true)
|
expect(validationRules.presence_penalty(0)).toBe(true)
|
||||||
expect(validationRules.presence_penalty(-0.1)).toBe(false)
|
expect(validationRules.presence_penalty(-0.1)).toBe(true)
|
||||||
expect(validationRules.presence_penalty(1.1)).toBe(false)
|
expect(validationRules.presence_penalty(1.1)).toBe(true)
|
||||||
expect(validationRules.presence_penalty('0.5')).toBe(false)
|
expect(validationRules.presence_penalty('0.5')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -255,7 +255,7 @@ describe('extractInferenceParams', () => {
|
|||||||
top_p: 0.9,
|
top_p: 0.9,
|
||||||
stream: true,
|
stream: true,
|
||||||
max_tokens: 50.3,
|
max_tokens: 50.3,
|
||||||
invalid_param: 'should_be_ignored'
|
invalid_param: 'should_be_ignored',
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = extractInferenceParams(modelParams as any)
|
const result = extractInferenceParams(modelParams as any)
|
||||||
@ -264,7 +264,7 @@ describe('extractInferenceParams', () => {
|
|||||||
token_limit: 100,
|
token_limit: 100,
|
||||||
top_p: 0.9,
|
top_p: 0.9,
|
||||||
stream: true,
|
stream: true,
|
||||||
max_tokens: 50
|
max_tokens: 50,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -296,7 +296,7 @@ describe('extractModelLoadParams', () => {
|
|||||||
prompt_template: 'template',
|
prompt_template: 'template',
|
||||||
llama_model_path: '/path/to/model',
|
llama_model_path: '/path/to/model',
|
||||||
vision_model: false,
|
vision_model: false,
|
||||||
invalid_param: 'should_be_ignored'
|
invalid_param: 'should_be_ignored',
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = extractModelLoadParams(modelParams as any)
|
const result = extractModelLoadParams(modelParams as any)
|
||||||
@ -308,7 +308,7 @@ describe('extractModelLoadParams', () => {
|
|||||||
cpu_threads: 8,
|
cpu_threads: 8,
|
||||||
prompt_template: 'template',
|
prompt_template: 'template',
|
||||||
llama_model_path: '/path/to/model',
|
llama_model_path: '/path/to/model',
|
||||||
vision_model: false
|
vision_model: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -317,14 +317,14 @@ describe('extractModelLoadParams', () => {
|
|||||||
engine: 'llama',
|
engine: 'llama',
|
||||||
pre_prompt: 'System:',
|
pre_prompt: 'System:',
|
||||||
system_prompt: 'You are helpful',
|
system_prompt: 'You are helpful',
|
||||||
model_path: '/path'
|
model_path: '/path',
|
||||||
}
|
}
|
||||||
const result = extractModelLoadParams(modelParams as any)
|
const result = extractModelLoadParams(modelParams as any)
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
engine: 'llama',
|
engine: 'llama',
|
||||||
pre_prompt: 'System:',
|
pre_prompt: 'System:',
|
||||||
system_prompt: 'You are helpful',
|
system_prompt: 'You are helpful',
|
||||||
model_path: '/path'
|
model_path: '/path',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -8,13 +8,13 @@ import { ModelParams, ModelRuntimeParams, ModelSettingParams } from '../../types
|
|||||||
export const validationRules: { [key: string]: (value: any) => boolean } = {
|
export const validationRules: { [key: string]: (value: any) => boolean } = {
|
||||||
temperature: (value: any) => typeof value === 'number' && value >= 0 && value <= 2,
|
temperature: (value: any) => typeof value === 'number' && value >= 0 && value <= 2,
|
||||||
token_limit: (value: any) => Number.isInteger(value) && value >= 0,
|
token_limit: (value: any) => Number.isInteger(value) && value >= 0,
|
||||||
top_k: (value: any) => typeof value === 'number' && value >= 0 && value <= 1,
|
top_k: (value: any) => typeof value === 'number' && value >= 0,
|
||||||
top_p: (value: any) => typeof value === 'number' && value >= 0 && value <= 1,
|
top_p: (value: any) => typeof value === 'number' && value >= 0 && value <= 1,
|
||||||
stream: (value: any) => typeof value === 'boolean',
|
stream: (value: any) => typeof value === 'boolean',
|
||||||
max_tokens: (value: any) => Number.isInteger(value) && value >= 0,
|
max_tokens: (value: any) => Number.isInteger(value) && value >= 0,
|
||||||
stop: (value: any) => Array.isArray(value) && value.every((v) => typeof v === 'string'),
|
stop: (value: any) => Array.isArray(value) && value.every((v) => typeof v === 'string'),
|
||||||
frequency_penalty: (value: any) => typeof value === 'number' && value >= 0 && value <= 1,
|
frequency_penalty: (value: any) => typeof value === 'number' && value >= -2 && value <= 2,
|
||||||
presence_penalty: (value: any) => typeof value === 'number' && value >= 0 && value <= 1,
|
presence_penalty: (value: any) => typeof value === 'number' && value >= -2 && value <= 2,
|
||||||
repeat_last_n: (value: any) => typeof value === 'number',
|
repeat_last_n: (value: any) => typeof value === 'number',
|
||||||
repeat_penalty: (value: any) => typeof value === 'number',
|
repeat_penalty: (value: any) => typeof value === 'number',
|
||||||
min_p: (value: any) => typeof value === 'number',
|
min_p: (value: any) => typeof value === 'number',
|
||||||
@ -50,6 +50,22 @@ export const normalizeValue = (key: string, value: any) => {
|
|||||||
// Convert to integer
|
// Convert to integer
|
||||||
return Math.floor(Number(value))
|
return Math.floor(Number(value))
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
key === 'temperature' ||
|
||||||
|
key === 'top_k' ||
|
||||||
|
key === 'top_p' ||
|
||||||
|
key === 'min_p' ||
|
||||||
|
key === 'repeat_penalty' ||
|
||||||
|
key === 'frequency_penalty' ||
|
||||||
|
key === 'presence_penalty' ||
|
||||||
|
key === 'repeat_last_n'
|
||||||
|
) {
|
||||||
|
// Convert to float
|
||||||
|
const newValue = parseFloat(value)
|
||||||
|
if (newValue !== null && !isNaN(newValue)) {
|
||||||
|
return newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
"embla-carousel-react": "^8.0.0",
|
"embla-carousel-react": "^8.0.0",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"lucide-react": "^0.372.0",
|
"lucide-react": "^0.522.0",
|
||||||
"next": "^14.1.4",
|
"next": "^14.1.4",
|
||||||
"next-seo": "^6.5.0",
|
"next-seo": "^6.5.0",
|
||||||
"next-sitemap": "^4.2.3",
|
"next-sitemap": "^4.2.3",
|
||||||
|
|||||||
BIN
docs/src/pages/docs/_assets/llama.cpp-01-updated.png
Normal file
BIN
docs/src/pages/docs/_assets/llama.cpp-01-updated.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 512 KiB |
@ -56,36 +56,37 @@ cd ~/.config/Jan/data # Default install
|
|||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
Root directory: `~/jan`
|
Root directory: `~/jan`
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
/assistants
|
/assistants/
|
||||||
/jan
|
/jan/
|
||||||
assistant.json
|
assistant.json
|
||||||
/extensions
|
/engines/
|
||||||
|
/llama.cpp/
|
||||||
|
/extensions/
|
||||||
extensions.json
|
extensions.json
|
||||||
/@janhq
|
/@janhq/
|
||||||
/extension_A
|
/assistant-extension/
|
||||||
package.json
|
/conversational-extension/
|
||||||
/logs
|
/download-extension/
|
||||||
/app.txt
|
/engine-management-extension/
|
||||||
/models
|
/hardware-management-extension/
|
||||||
/model_A
|
/inference-cortex-extension/
|
||||||
model.yaml
|
/model-extension/
|
||||||
|
/files/
|
||||||
|
/logs/
|
||||||
|
app.log
|
||||||
|
/models/
|
||||||
|
/huggingface.co/
|
||||||
|
/Model_Provider_A/
|
||||||
|
/Model_A
|
||||||
|
model_A.gguf
|
||||||
model_A.yaml
|
model_A.yaml
|
||||||
/settings
|
/threads/
|
||||||
settings.json
|
/thread_A/
|
||||||
/@janhq
|
|
||||||
/extension_A_Settings
|
|
||||||
settings.json
|
|
||||||
/themes
|
|
||||||
/dark-dimmed
|
|
||||||
/joi-dark
|
|
||||||
/joi-light
|
|
||||||
/night-blue
|
|
||||||
/threads
|
|
||||||
/jan_thread_A
|
|
||||||
messages.jsonl
|
messages.jsonl
|
||||||
thread.json
|
thread.json
|
||||||
messages.jsonl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### `assistants/`
|
### `assistants/`
|
||||||
@ -93,14 +94,28 @@ Where AI personalities live. The default one (`/assistants/jan/`):
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"avatar": "",
|
"avatar": "👋",
|
||||||
"id": "jan",
|
"id": "jan",
|
||||||
"object": "assistant",
|
"object": "assistant",
|
||||||
"created_at": 1715132389207,
|
"created_at": 1750945742.536,
|
||||||
"name": "Jan",
|
"name": "Jan",
|
||||||
"description": "A default assistant that can use all downloaded models",
|
"description": "Jan is a helpful AI assistant that can use tools and help complete tasks for its users.",
|
||||||
"model": "*",
|
"model": "*",
|
||||||
"instructions": ""
|
"instructions": "You have access to a set of tools to help you answer the user’s question. You can use only one tool per message, and you’ll receive the result of that tool in the user’s next response. To complete a task, use tools step by step—each step should be guided by the outcome of the previous one.\nTool Usage Rules:\n1. Always provide the correct values as arguments when using tools. Do not pass variable names—use actual values instead.\n2. You may perform multiple tool steps to complete a task.\n3. Avoid repeating a tool call with exactly the same parameters to prevent infinite loops.",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"type": "retrieval",
|
||||||
|
"enabled": false,
|
||||||
|
"useTimeWeightedRetriever": false,
|
||||||
|
"settings": {
|
||||||
|
"top_k": 2,
|
||||||
|
"chunk_size": 1024,
|
||||||
|
"chunk_overlap": 64,
|
||||||
|
"retrieval_template": "Use the following pieces of context to answer the question at the end.\n----------------\nCONTEXT: {CONTEXT}\n----------------\nQUESTION: {QUESTION}\n----------------\nHelpful Answer:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"file_ids": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -140,75 +155,47 @@ Debugging headquarters (`/logs/app.txt`):
|
|||||||
The silicon brain collection. Each model has its own `model.json`.
|
The silicon brain collection. Each model has its own `model.json`.
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
Full parameters: [here](/docs/models/model-parameters)
|
Full parameters: [here](/docs/model-parameters)
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
### `settings/`
|
|
||||||
Control panel. Extension settings in `/settings/@janhq/`:
|
|
||||||
|
|
||||||
| Parameter | Description |
|
|
||||||
|----------------|----------------------------------------------------|
|
|
||||||
| key | Setting identifier |
|
|
||||||
| title | Display name |
|
|
||||||
| description | Setting explanation |
|
|
||||||
| controllerType | UI component type |
|
|
||||||
| controllerProps| Component properties |
|
|
||||||
| extensionName | Parent extension link |
|
|
||||||
|
|
||||||
GPU settings (`settings.json`):
|
|
||||||
|
|
||||||
| Parameter | Description |
|
|
||||||
|----------------------|--------------------------------------------|
|
|
||||||
| notify | Notification status |
|
|
||||||
| run_mode | Operating mode |
|
|
||||||
| nvidia_driver.exist | NVIDIA driver presence |
|
|
||||||
| nvidia_driver.version| Driver version |
|
|
||||||
| cuda.exist | CUDA availability |
|
|
||||||
| cuda.version | CUDA version |
|
|
||||||
| gpus[0].id | GPU identifier |
|
|
||||||
| gpus[0].vram | GPU memory (MB) |
|
|
||||||
| gpus[0].name | GPU model |
|
|
||||||
| gpus[0].arch | GPU architecture |
|
|
||||||
| gpu_highest_vram | Most capable GPU |
|
|
||||||
| gpus_in_use | Active GPUs |
|
|
||||||
| is_initial | First run flag |
|
|
||||||
| vulkan | Vulkan support |
|
|
||||||
|
|
||||||
### `themes/`
|
|
||||||
Visual wardrobe. Each theme's `theme.json`:
|
|
||||||
|
|
||||||
| Parameter | Description |
|
|
||||||
|------------------|-------------------------------------------|
|
|
||||||
| id | Theme identifier |
|
|
||||||
| displayName | UI name |
|
|
||||||
| reduceTransparent| Transparency control |
|
|
||||||
| nativeTheme | OS theme sync |
|
|
||||||
| variables | Component settings |
|
|
||||||
|
|
||||||
### `threads/`
|
### `threads/`
|
||||||
Chat archive. Each thread (`/threads/jan_unixstamp/`) contains:
|
Chat archive. Each thread (`/threads/jan_unixstamp/`) contains:
|
||||||
|
|
||||||
- `messages.jsonl`:
|
- `messages.jsonl`:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id":"01J6Y6FH8PFTHQB5PNJTHEN27C",
|
"completed_at": 0,
|
||||||
"thread_id":"jan_1725437954",
|
"content": [
|
||||||
"type":"Thread",
|
|
||||||
"role":"assistant",
|
|
||||||
"content":
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
"type": "text",
|
|
||||||
"text": {
|
"text": {
|
||||||
"value": "Hello! Is there something I can help you with or would you like to chat?",
|
"annotations": [],
|
||||||
"annotations": []
|
"value": "Hello! I can help you with various tasks. I can search for information on the internet, including news, videos, images, shopping, and more. I can also scrape webpages to extract specific information. Let me know what you need!"
|
||||||
}
|
},
|
||||||
|
"type": "text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"created_at": 1751012639307,
|
||||||
|
"id": "01JYR7S0JB5ZBGMJV52KWMW5VW",
|
||||||
|
"metadata": {
|
||||||
|
"assistant": {
|
||||||
|
"avatar": "👋",
|
||||||
|
"id": "jan",
|
||||||
|
"instructions": "You have access to a set of tools to help you answer the user's question. You can use only one tool per message, and you'll receive the result of that tool in the user's next response. To complete a task, use tools step by step—each step should be guided by the outcome of the previous one.\nTool Usage Rules:\n1. Always provide the correct values as arguments when using tools. Do not pass variable names—use actual values instead.\n2. You may perform multiple tool steps to complete a task.\n3. Avoid repeating a tool call with exactly the same parameters to prevent infinite loops.",
|
||||||
|
"name": "Jan",
|
||||||
|
"parameters": ""
|
||||||
|
},
|
||||||
|
"tokenSpeed": {
|
||||||
|
"lastTimestamp": 1751012637097,
|
||||||
|
"message": "01JYR7S0GW5M9PSHMRE7T8VQJM",
|
||||||
|
"tokenCount": 49,
|
||||||
|
"tokenSpeed": 22.653721682847895
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"object": "thread.message",
|
||||||
|
"role": "assistant",
|
||||||
"status": "ready",
|
"status": "ready",
|
||||||
"created": 1725442802966,
|
"thread_id": "8f2c9922-db49-4d1e-8620-279c05baf2d0",
|
||||||
"updated": 1725442802966,
|
"type": "text"
|
||||||
"object": "thread.message"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -216,12 +203,17 @@ Chat archive. Each thread (`/threads/jan_unixstamp/`) contains:
|
|||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|------------|------------------------------------------------|
|
|------------|------------------------------------------------|
|
||||||
|
| assistants | Assistant configuration clone |
|
||||||
|
| created | Creation timestamp |
|
||||||
| id | Thread identifier |
|
| id | Thread identifier |
|
||||||
|
| metadata | Additional thread data |
|
||||||
|
| model | Active model settings |
|
||||||
| object | OpenAI compatibility marker |
|
| object | OpenAI compatibility marker |
|
||||||
| title | Thread name |
|
| title | Thread name |
|
||||||
| assistants | Assistant configuration clone |
|
| updated | Updated timestamp |
|
||||||
| model | Active model settings |
|
|
||||||
| metadata | Additional thread data |
|
|
||||||
|
|
||||||
|
|
||||||
## Delete Jan Data
|
## Delete Jan Data
|
||||||
Uninstall guides: [Mac](/docs/desktop/mac#step-2-clean-up-data-optional),
|
Uninstall guides: [Mac](/docs/desktop/mac#step-2-clean-up-data-optional),
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import { Settings, EllipsisVertical, Plus, FolderOpen, Pencil } from 'lucide-rea
|
|||||||
Jan uses **llama.cpp** for running local AI models. You can find its settings in **Settings** (<Settings width={16} height={16} style={{display:"inline"}}/>) > **Local Engine** > **llama.cpp**:
|
Jan uses **llama.cpp** for running local AI models. You can find its settings in **Settings** (<Settings width={16} height={16} style={{display:"inline"}}/>) > **Local Engine** > **llama.cpp**:
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||

|

|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
These settings are for advanced users, you would want to check these settings when:
|
These settings are for advanced users, you would want to check these settings when:
|
||||||
@ -151,6 +151,7 @@ For detailed hardware compatibility, please visit our guide for [Mac](/docs/desk
|
|||||||
| **Caching** | - Enable to store recent prompts and responses<br></br>- Improves response time for repeated prompts | Enabled |
|
| **Caching** | - Enable to store recent prompts and responses<br></br>- Improves response time for repeated prompts | Enabled |
|
||||||
| **KV Cache Type** | - KV cache implementation type; controls memory usage and precision trade-off<br></br>- Options:<br></br>• f16 (most stable)<br></br>• q8_0 (balanced)<br></br>• q4_0 (lowest memory) | f16 |
|
| **KV Cache Type** | - KV cache implementation type; controls memory usage and precision trade-off<br></br>- Options:<br></br>• f16 (most stable)<br></br>• q8_0 (balanced)<br></br>• q4_0 (lowest memory) | f16 |
|
||||||
| **mmap** | - Enables memory-mapped model loading<br></br>- Reduces memory usage<br></br>- Recommended for large models | Enabled |
|
| **mmap** | - Enables memory-mapped model loading<br></br>- Reduces memory usage<br></br>- Recommended for large models | Enabled |
|
||||||
|
| **Context Shift** | - Automatically shifts the context window when the model is unable to process the entire prompt<br/> - Ensures that the most relevant information is always included <br/> - Recommended for long conversations and multiple tool calls | Disabled |
|
||||||
|
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|||||||
@ -38,8 +38,6 @@ These settings are available in the model settings modal:
|
|||||||
| **Repeat Last N** | Number of tokens to consider for repeat penalty. |
|
| **Repeat Last N** | Number of tokens to consider for repeat penalty. |
|
||||||
| **Repeat Penalty** | Penalize repeating token sequences. |
|
| **Repeat Penalty** | Penalize repeating token sequences. |
|
||||||
| **Presence Penalty**| Penalize alpha presence (encourages new topics). |
|
| **Presence Penalty**| Penalize alpha presence (encourages new topics). |
|
||||||
| **Max Tokens** | Maximum length of the model's response. |
|
|
||||||
| **Stop Sequences** | Tokens or phrases that will end the model's response. |
|
|
||||||
| **Frequency Penalty** | Reduces word repetition. |
|
| **Frequency Penalty** | Reduces word repetition. |
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|||||||
@ -36,11 +36,15 @@ Follow this [guide](https://continue.dev/docs/quickstart) to install the Continu
|
|||||||
|
|
||||||
To set up Continue for use with Jan's Local Server, you must activate the Jan API Server with your chosen model.
|
To set up Continue for use with Jan's Local Server, you must activate the Jan API Server with your chosen model.
|
||||||
|
|
||||||
1. Press the `<>` button. Jan will take you to the **Local API Server** section.
|
1. Press the `⚙️ Settings` button.
|
||||||
|
|
||||||
2. Setup the server, which includes the **IP Port**, **Cross-Origin-Resource-Sharing (CORS)** and **Verbose Server Logs**.
|
2. Locate `Local API Server`.
|
||||||
|
|
||||||
3. Press the **Start Server** button
|
3. Setup the server, which includes the **IP Port**, **Cross-Origin-Resource-Sharing (CORS)** and **Verbose Server Logs**.
|
||||||
|
|
||||||
|
4. Include your user-defined API Key.
|
||||||
|
|
||||||
|
5. Press the **Start Server** button
|
||||||
|
|
||||||
### Step 3: Configure Continue to Use Jan's Local Server
|
### Step 3: Configure Continue to Use Jan's Local Server
|
||||||
|
|
||||||
@ -64,30 +68,35 @@ To set up Continue for use with Jan's Local Server, you must activate the Jan AP
|
|||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
```json title="~/.continue/config.json"
|
```yaml title="~/.continue/config.yaml"
|
||||||
{
|
name: Local Assistant
|
||||||
"models": [
|
version: 1.0.0
|
||||||
{
|
schema: v1
|
||||||
"title": "Jan",
|
models:
|
||||||
"provider": "openai",
|
- name: Jan
|
||||||
"model": "mistral-ins-7b-q4",
|
provider: openai
|
||||||
"apiKey": "EMPTY",
|
model: #MODEL_NAME (e.g. qwen3:0.6b)
|
||||||
"apiBase": "http://localhost:1337/v1"
|
apiKey: #YOUR_USER_DEFINED_API_KEY_HERE (e.g. hello)
|
||||||
}
|
apiBase: http://localhost:1337/v1
|
||||||
]
|
context:
|
||||||
}
|
- provider: code
|
||||||
|
- provider: docs
|
||||||
|
- provider: diff
|
||||||
|
- provider: terminal
|
||||||
|
- provider: problems
|
||||||
|
- provider: folder
|
||||||
|
- provider: codebase
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Ensure the file has the following configurations:
|
2. Ensure the file has the following configurations:
|
||||||
- Ensure `openai` is selected as the `provider`.
|
- Ensure `openai` is selected as the `provider`.
|
||||||
- Match the `model` with the one enabled in the Jan API Server.
|
- Match the `model` with the one enabled in the Jan API Server.
|
||||||
- Set `apiBase` to `http://localhost:1337`.
|
- Set `apiBase` to `http://localhost:1337/v1`.
|
||||||
- Leave the `apiKey` field to `EMPTY`.
|
|
||||||
|
|
||||||
### Step 4: Ensure the Using Model Is Activated in Jan
|
### Step 4: Ensure the Using Model Is Activated in Jan
|
||||||
|
|
||||||
1. Navigate to `Settings` > `My Models`.
|
1. Navigate to `Settings` > `Model Providers`.
|
||||||
2. Click the **three dots (⋮)** button.
|
2. Under Llama.cpp, find the model that you would want to use.
|
||||||
3. Select the **Start Model** button to activate the model.
|
3. Select the **Start Model** button to activate the model.
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|||||||
@ -5503,10 +5503,10 @@ lru-cache@^4.0.1:
|
|||||||
pseudomap "^1.0.2"
|
pseudomap "^1.0.2"
|
||||||
yallist "^2.1.2"
|
yallist "^2.1.2"
|
||||||
|
|
||||||
lucide-react@^0.372.0:
|
lucide-react@^0.522.0:
|
||||||
version "0.372.0"
|
version "0.522.0"
|
||||||
resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.372.0.tgz"
|
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.522.0.tgz#c0951dd32936b6a7bcc474a829a251fede0bdfbd"
|
||||||
integrity sha512-0cKdqmilHXWUwWAWnf6CrrjHD8YaqPMtLrmEHXolZusNTr9epULCsiJwIOHk2q1yFxdEwd96D4zShlAj67UJdA==
|
integrity sha512-jnJbw974yZ7rQHHEFKJOlWAefG3ATSCZHANZxIdx8Rk/16siuwjgA4fBULpXEAWx/RlTs3FzmKW/udWUuO0aRw==
|
||||||
|
|
||||||
lz-string@^1.5.0:
|
lz-string@^1.5.0:
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^6.0.1",
|
||||||
"rolldown": "1.0.0-beta.1",
|
"rolldown": "1.0.0-beta.1",
|
||||||
"run-script-os": "^1.1.6",
|
"run-script-os": "^1.1.6",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^6.0.1",
|
||||||
"rolldown": "1.0.0-beta.1",
|
"rolldown": "1.0.0-beta.1",
|
||||||
"ts-loader": "^9.5.0",
|
"ts-loader": "^9.5.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^6.0.1",
|
||||||
"rolldown": "1.0.0-beta.1",
|
"rolldown": "1.0.0-beta.1",
|
||||||
"run-script-os": "^1.1.6",
|
"run-script-os": "^1.1.6",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
|
|||||||
@ -24,13 +24,7 @@ run = [
|
|||||||
[tasks.install]
|
[tasks.install]
|
||||||
description = "Install dependencies"
|
description = "Install dependencies"
|
||||||
depends = ["config-yarn"]
|
depends = ["config-yarn"]
|
||||||
run = '''
|
run = "yarn install"
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Skip install on Windows per Makefile logic
|
|
||||||
if [[ "$OSTYPE" != "msys" && "$OSTYPE" != "win32" ]]; then
|
|
||||||
yarn install
|
|
||||||
fi
|
|
||||||
'''
|
|
||||||
sources = ['package.json', 'yarn.lock']
|
sources = ['package.json', 'yarn.lock']
|
||||||
outputs = ['node_modules']
|
outputs = ['node_modules']
|
||||||
|
|
||||||
|
|||||||
10
package.json
10
package.json
@ -4,7 +4,8 @@
|
|||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
"core",
|
"core",
|
||||||
"web-app"
|
"web-app",
|
||||||
|
"tests-e2e-js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -13,6 +14,11 @@
|
|||||||
"build": "yarn build:web && yarn build:tauri",
|
"build": "yarn build:web && yarn build:tauri",
|
||||||
"test": "yarn workspace @janhq/web-app test",
|
"test": "yarn workspace @janhq/web-app test",
|
||||||
"test:coverage": "yarn workspace @janhq/web-app test",
|
"test:coverage": "yarn workspace @janhq/web-app test",
|
||||||
|
"test:prepare": "yarn build:icon && yarn copy:lib && yarn copy:assets:tauri && yarn build --no-bundle ",
|
||||||
|
"test:e2e:linux": "yarn test:prepare && xvfb-run yarn workspace tests-e2-js test",
|
||||||
|
"test:e2e:win32": "yarn test:prepare && yarn workspace tests-e2-js test",
|
||||||
|
"test:e2e:darwin": "echo 'E2E tests are not supported on macOS yet due to WebDriver limitations'",
|
||||||
|
"test:e2e": "run-script-os",
|
||||||
"dev:web": "yarn workspace @janhq/web-app dev",
|
"dev:web": "yarn workspace @janhq/web-app dev",
|
||||||
"dev:tauri": "CLEAN=true yarn build:icon && yarn copy:assets:tauri && tauri dev",
|
"dev:tauri": "CLEAN=true yarn build:icon && yarn copy:assets:tauri && tauri dev",
|
||||||
"install:cortex:linux:darwin": "cd src-tauri/binaries && ./download.sh",
|
"install:cortex:linux:darwin": "cd src-tauri/binaries && ./download.sh",
|
||||||
@ -39,7 +45,7 @@
|
|||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"husky": "^9.1.5",
|
"husky": "^9.1.5",
|
||||||
"jest": "^29.7.0",
|
"jest": "^30.0.3",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"run-script-os": "^1.1.6",
|
"run-script-os": "^1.1.6",
|
||||||
|
|||||||
@ -19,8 +19,8 @@ tauri-build = { version = "2.0.2", features = [] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
tauri = { version = "2.4.0", features = [ "protocol-asset", "macos-private-api",
|
tauri = { version = "2.5.0", features = [ "protocol-asset", "macos-private-api",
|
||||||
"test",
|
"test"
|
||||||
] }
|
] }
|
||||||
tauri-plugin-log = "2.0.0-rc"
|
tauri-plugin-log = "2.0.0-rc"
|
||||||
tauri-plugin-shell = "2.2.0"
|
tauri-plugin-shell = "2.2.0"
|
||||||
|
|||||||
@ -277,7 +277,12 @@ pub fn setup_sidecar(app: &App) -> Result<(), String> {
|
|||||||
]);
|
]);
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
let resource_dir = app_handle_for_spawn.path().resource_dir().unwrap();
|
let mut resource_dir = app_handle_for_spawn.path().resource_dir().unwrap();
|
||||||
|
// If debug
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
resource_dir = resource_dir.join("binaries");
|
||||||
|
}
|
||||||
let normalized_path = resource_dir.to_string_lossy().replace(r"\\?\", "");
|
let normalized_path = resource_dir.to_string_lossy().replace(r"\\?\", "");
|
||||||
let normalized_pathbuf = PathBuf::from(normalized_path);
|
let normalized_pathbuf = PathBuf::from(normalized_path);
|
||||||
cmd = cmd.current_dir(normalized_pathbuf);
|
cmd = cmd.current_dir(normalized_pathbuf);
|
||||||
@ -286,12 +291,12 @@ pub fn setup_sidecar(app: &App) -> Result<(), String> {
|
|||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
{
|
||||||
cmd = cmd.env("LD_LIBRARY_PATH", {
|
cmd = cmd.env("LD_LIBRARY_PATH", {
|
||||||
let current_app_data_dir = app_handle_for_spawn
|
let mut resource_dir = app_handle_for_spawn.path().resource_dir().unwrap();
|
||||||
.path()
|
#[cfg(not(debug_assertions))]
|
||||||
.resource_dir()
|
{
|
||||||
.unwrap()
|
resource_dir = resource_dir.join("binaries");
|
||||||
.join("binaries");
|
}
|
||||||
let dest = current_app_data_dir.to_str().unwrap();
|
let dest = resource_dir.to_str().unwrap();
|
||||||
let ld_path_env = std::env::var("LD_LIBRARY_PATH").unwrap_or_default();
|
let ld_path_env = std::env::var("LD_LIBRARY_PATH").unwrap_or_default();
|
||||||
format!("{}{}{}", ld_path_env, ":", dest)
|
format!("{}{}{}", ld_path_env, ":", dest)
|
||||||
});
|
});
|
||||||
|
|||||||
@ -33,7 +33,7 @@ ${StrLoc}
|
|||||||
!define VERSION "jan_version"
|
!define VERSION "jan_version"
|
||||||
!define VERSIONWITHBUILD "jan_build"
|
!define VERSIONWITHBUILD "jan_build"
|
||||||
!define HOMEPAGE ""
|
!define HOMEPAGE ""
|
||||||
!define INSTALLMODE "currentUser"
|
!define INSTALLMODE "both"
|
||||||
!define LICENSE ""
|
!define LICENSE ""
|
||||||
!define INSTALLERICON "D:\a\jan\jan\src-tauri\icons\icon.ico"
|
!define INSTALLERICON "D:\a\jan\jan\src-tauri\icons\icon.ico"
|
||||||
!define SIDEBARIMAGE ""
|
!define SIDEBARIMAGE ""
|
||||||
|
|||||||
1
tests-e2e-js/.gitignore
vendored
Normal file
1
tests-e2e-js/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist
|
||||||
23
tests-e2e-js/package.json
Normal file
23
tests-e2e-js/package.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "tests-e2-js",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "node --test --test-force-exit --loader ts-node/esm ./src/main.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-e2e/selenium": "0.2.2",
|
||||||
|
"log4js": "^6.9.1",
|
||||||
|
"selenium-webdriver": "^4.22.0",
|
||||||
|
"ts-node": "^10.9.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.14.9",
|
||||||
|
"@types/selenium-webdriver": "^4.1.28",
|
||||||
|
"tsimp": "^2.0.11",
|
||||||
|
"typescript": "^5.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
51
tests-e2e-js/src/main.ts
Normal file
51
tests-e2e-js/src/main.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import assert from 'node:assert'
|
||||||
|
import { ChildProcess } from 'node:child_process'
|
||||||
|
import { afterEach, beforeEach, describe, test } from 'node:test'
|
||||||
|
import { By, until, WebDriver } from 'selenium-webdriver'
|
||||||
|
import * as e2e from '@tauri-e2e/selenium'
|
||||||
|
import { default as log4js } from 'log4js'
|
||||||
|
|
||||||
|
let logger = log4js.getLogger()
|
||||||
|
logger.level = 'debug'
|
||||||
|
|
||||||
|
process.env.TAURI_WEBDRIVER_LOGLEVEL = 'debug'
|
||||||
|
process.env.TAURI_WEBDRIVER_BINARY = await e2e.install.PlatformDriver()
|
||||||
|
process.env.TAURI_SELENIUM_BINARY = '../src-tauri/target/release/Jan.exe'
|
||||||
|
process.env.SELENIUM_REMOTE_URL = 'http://127.0.0.1:6655'
|
||||||
|
|
||||||
|
//@ts-ignore fuck you javascript
|
||||||
|
e2e.setLogger(logger)
|
||||||
|
|
||||||
|
describe('Tauri E2E tests', async () => {
|
||||||
|
let driver: WebDriver
|
||||||
|
let webDriver: ChildProcess
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Spawn WebDriver process.
|
||||||
|
webDriver = await e2e.launch.spawnWebDriver()
|
||||||
|
// wait 1 second
|
||||||
|
await new Promise((r) => setTimeout(r, 1000))
|
||||||
|
// Create driver session.
|
||||||
|
driver = new e2e.selenium.Builder().build()
|
||||||
|
// Wait for the body element to be present
|
||||||
|
// await driver.wait(until.elementLocated({ css: 'body' }))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await e2e.selenium.cleanupSession(driver)
|
||||||
|
e2e.launch.killWebDriver(webDriver)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Find hub', async () => {
|
||||||
|
const hub = until.elementLocated(By.css('[data-test-id="menu-common:hub"'))
|
||||||
|
// console.log('GG', hub)
|
||||||
|
// @ts-ignore
|
||||||
|
await driver.wait(hub.fn, 120000)
|
||||||
|
|
||||||
|
const menuElement = await driver.findElement({
|
||||||
|
css: '[data-test-id="menu-common:hub"]',
|
||||||
|
})
|
||||||
|
assert(menuElement !== null, 'Hub menu element should be available')
|
||||||
|
await menuElement.isDisplayed()
|
||||||
|
})
|
||||||
|
})
|
||||||
21
tests-e2e-js/tsconfig.json
Normal file
21
tests-e2e-js/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
}
|
||||||
@ -46,7 +46,7 @@
|
|||||||
"i18next": "^25.0.1",
|
"i18next": "^25.0.1",
|
||||||
"katex": "^0.16.22",
|
"katex": "^0.16.22",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lucide-react": "^0.503.0",
|
"lucide-react": "^0.522.0",
|
||||||
"motion": "^12.10.5",
|
"motion": "^12.10.5",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"posthog-js": "^1.246.0",
|
"posthog-js": "^1.246.0",
|
||||||
@ -56,6 +56,7 @@
|
|||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
"react-joyride": "^2.9.3",
|
"react-joyride": "^2.9.3",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
"react-resizable-panels": "^3.0.3",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"react-syntax-highlighter-virtualized-renderer": "^1.1.0",
|
"react-syntax-highlighter-virtualized-renderer": "^1.1.0",
|
||||||
"react-textarea-autosize": "^8.5.9",
|
"react-textarea-autosize": "^8.5.9",
|
||||||
@ -88,7 +89,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.26.1",
|
"typescript-eslint": "^8.26.1",
|
||||||
"vite": "^6.3.0",
|
"vite": "^6.3.0",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
|
|||||||
54
web-app/src/components/ui/resizable.tsx
Normal file
54
web-app/src/components/ui/resizable.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { GripVerticalIcon } from 'lucide-react'
|
||||||
|
import * as ResizablePrimitive from 'react-resizable-panels'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
function ResizablePanelGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
||||||
|
return (
|
||||||
|
<ResizablePrimitive.PanelGroup
|
||||||
|
data-slot="resizable-panel-group"
|
||||||
|
className={cn(
|
||||||
|
'flex h-full w-full data-[panel-group-direction=vertical]:flex-col',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResizablePanel({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
||||||
|
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResizableHandle({
|
||||||
|
withHandle,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||||
|
withHandle?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ResizablePrimitive.PanelResizeHandle
|
||||||
|
data-slot="resizable-handle"
|
||||||
|
className={cn(
|
||||||
|
'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{withHandle && (
|
||||||
|
<div className="bg-main-view z-10 flex h-4 w-3 items-center justify-center rounded-xs border border-main-view-fg/10 relative left-0.5">
|
||||||
|
<GripVerticalIcon className="size-2.5 text-main-view-fg" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ResizablePrimitive.PanelResizeHandle>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||||
@ -5,6 +5,7 @@ export const route = {
|
|||||||
assistant: '/assistant',
|
assistant: '/assistant',
|
||||||
settings: {
|
settings: {
|
||||||
index: '/settings',
|
index: '/settings',
|
||||||
|
model_providers: '/settings/providers',
|
||||||
providers: '/settings/providers/$providerName',
|
providers: '/settings/providers/$providerName',
|
||||||
general: '/settings/general',
|
general: '/settings/general',
|
||||||
appearance: '/settings/appearance',
|
appearance: '/settings/appearance',
|
||||||
|
|||||||
@ -10,29 +10,35 @@ type CardProps = {
|
|||||||
type CardItemProps = {
|
type CardItemProps = {
|
||||||
title?: string | ReactNode
|
title?: string | ReactNode
|
||||||
description?: string | ReactNode
|
description?: string | ReactNode
|
||||||
|
descriptionOutside?: string | ReactNode
|
||||||
align?: 'start' | 'center' | 'end'
|
align?: 'start' | 'center' | 'end'
|
||||||
actions?: ReactNode
|
actions?: ReactNode
|
||||||
column?: boolean
|
column?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
classNameWrapperAction?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CardItem({
|
export function CardItem({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
descriptionOutside,
|
||||||
className,
|
className,
|
||||||
|
classNameWrapperAction,
|
||||||
align = 'center',
|
align = 'center',
|
||||||
column,
|
column,
|
||||||
actions,
|
actions,
|
||||||
}: CardItemProps) {
|
}: CardItemProps) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex justify-between mt-2 first:mt-0 border-b border-main-view-fg/5 pb-3 last:border-none last:pb-0 gap-8',
|
'flex justify-between mt-2 first:mt-0 border-b border-main-view-fg/5 pb-3 last:border-none last:pb-0 gap-8',
|
||||||
className,
|
descriptionOutside && 'border-0',
|
||||||
align === 'start' && 'items-start',
|
align === 'start' && 'items-start',
|
||||||
align === 'center' && 'items-center',
|
align === 'center' && 'items-center',
|
||||||
align === 'end' && 'items-end',
|
align === 'end' && 'items-end',
|
||||||
column && 'flex-col gap-y-0 items-start'
|
column && 'flex-col gap-y-0 items-start',
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
@ -44,9 +50,23 @@ export function CardItem({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{actions && (
|
{actions && (
|
||||||
<div className={cn('shrink-0', column && 'w-full')}>{actions}</div>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'shrink-0',
|
||||||
|
classNameWrapperAction,
|
||||||
|
column && 'w-full'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{descriptionOutside && (
|
||||||
|
<span className="text-main-view-fg/70 leading-normal">
|
||||||
|
{descriptionOutside}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -365,6 +365,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
|||||||
rows={1}
|
rows={1}
|
||||||
maxRows={10}
|
maxRows={10}
|
||||||
value={prompt}
|
value={prompt}
|
||||||
|
data-test-id={'chat-input'}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setPrompt(e.target.value)
|
setPrompt(e.target.value)
|
||||||
// Count the number of newlines to estimate rows
|
// Count the number of newlines to estimate rows
|
||||||
@ -567,6 +568,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
|||||||
variant={!prompt.trim() ? null : 'default'}
|
variant={!prompt.trim() ? null : 'default'}
|
||||||
size="icon"
|
size="icon"
|
||||||
disabled={!prompt.trim()}
|
disabled={!prompt.trim()}
|
||||||
|
data-test-id="send-message-button"
|
||||||
onClick={() => handleSendMesage(prompt)}
|
onClick={() => handleSendMesage(prompt)}
|
||||||
>
|
>
|
||||||
{streamingContent ? (
|
{streamingContent ? (
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export function ChatWidthSwitcher() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex flex-col sm:flex-row sm:gap-4">
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full overflow-hidden border border-main-view-fg/10 rounded-md my-2 pb-2 cursor-pointer ',
|
'w-full overflow-hidden border border-main-view-fg/10 rounded-md my-2 pb-2 cursor-pointer ',
|
||||||
@ -18,18 +18,22 @@ export function ChatWidthSwitcher() {
|
|||||||
onClick={() => setChatWidth('compact')}
|
onClick={() => setChatWidth('compact')}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
||||||
<span className="font-medium text-xs font-sans">{t('common:compactWidth')}</span>
|
<span className="font-medium text-xs font-sans">
|
||||||
|
{t('common:compactWidth')}
|
||||||
|
</span>
|
||||||
{chatWidth === 'compact' && (
|
{chatWidth === 'compact' && (
|
||||||
<IconCircleCheckFilled className="size-4 text-accent" />
|
<IconCircleCheckFilled className="size-4 text-accent" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-auto p-2">
|
<div className="overflow-auto p-2">
|
||||||
<div className="flex flex-col px-10 gap-2 mt-2">
|
<div className="flex flex-col px-6 gap-2 mt-2">
|
||||||
<Skeleton className="h-2 w-full rounded-full" />
|
<Skeleton className="h-2 w-full rounded-full" />
|
||||||
<Skeleton className="h-2 w-full rounded-full" />
|
<Skeleton className="h-2 w-full rounded-full" />
|
||||||
<Skeleton className="h-2 w-full rounded-full" />
|
<Skeleton className="h-2 w-full rounded-full" />
|
||||||
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-2xl flex items-center">
|
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-sm flex items-center truncate">
|
||||||
<span className="text-main-view-fg/50">{t('common:placeholder.chatInput')}</span>
|
<span className="text-main-view-fg/50 line-clamp-1">
|
||||||
|
{t('common:placeholder.chatInput')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -42,7 +46,9 @@ export function ChatWidthSwitcher() {
|
|||||||
onClick={() => setChatWidth('full')}
|
onClick={() => setChatWidth('full')}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
||||||
<span className="font-medium text-xs font-sans">{t('common:fullWidth')}</span>
|
<span className="font-medium text-xs font-sans">
|
||||||
|
{t('common:fullWidth')}
|
||||||
|
</span>
|
||||||
{chatWidth === 'full' && (
|
{chatWidth === 'full' && (
|
||||||
<IconCircleCheckFilled className="size-4 text-accent" />
|
<IconCircleCheckFilled className="size-4 text-accent" />
|
||||||
)}
|
)}
|
||||||
@ -52,8 +58,10 @@ export function ChatWidthSwitcher() {
|
|||||||
<Skeleton className="h-2 w-full rounded-full" />
|
<Skeleton className="h-2 w-full rounded-full" />
|
||||||
<Skeleton className="h-2 w-full rounded-full" />
|
<Skeleton className="h-2 w-full rounded-full" />
|
||||||
<Skeleton className="h-2 w-full rounded-full" />
|
<Skeleton className="h-2 w-full rounded-full" />
|
||||||
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-2xl flex items-center">
|
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-sm flex items-center">
|
||||||
<span className="text-main-view-fg/50">{t('common:placeholder.chatInput')}</span>
|
<span className="text-main-view-fg/50">
|
||||||
|
{t('common:placeholder.chatInput')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -182,7 +182,9 @@ export function DownloadManagement() {
|
|||||||
getProviders().then(setProviders)
|
getProviders().then(setProviders)
|
||||||
toast.success(t('common:toast.downloadComplete.title'), {
|
toast.success(t('common:toast.downloadComplete.title'), {
|
||||||
id: 'download-complete',
|
id: 'download-complete',
|
||||||
description: t('common:toast.downloadComplete.description', { modelId: state.modelId }),
|
description: t('common:toast.downloadComplete.description', {
|
||||||
|
modelId: state.modelId,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[removeDownload, removeLocalDownloadingModel, setProviders, t]
|
[removeDownload, removeLocalDownloadingModel, setProviders, t]
|
||||||
@ -237,10 +239,14 @@ export function DownloadManagement() {
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
{isLeftPanelOpen ? (
|
{isLeftPanelOpen ? (
|
||||||
<div className="bg-left-panel-fg/10 hover:bg-left-panel-fg/12 p-2 rounded-md my-1 relative border border-left-panel-fg/10 cursor-pointer text-left">
|
<div className="bg-left-panel-fg/10 hover:bg-left-panel-fg/12 p-2 rounded-md my-1 relative border border-left-panel-fg/10 cursor-pointer text-left">
|
||||||
<div className="bg-primary font-bold size-5 rounded-full absolute -top-2 -right-1 flex items-center justify-center text-primary-fg">
|
<div className="text-left-panel-fg/80 font-medium flex gap-2">
|
||||||
|
<span>{t('downloads')}</span>
|
||||||
|
<span>
|
||||||
|
<div className="bg-primary font-bold size-5 rounded-full flex items-center justify-center text-primary-fg">
|
||||||
{downloadCount}
|
{downloadCount}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-left-panel-fg/80 font-medium">{t('downloads')}</p>
|
</span>
|
||||||
|
</div>
|
||||||
<div className="mt-2 flex items-center justify-between space-x-2">
|
<div className="mt-2 flex items-center justify-between space-x-2">
|
||||||
<Progress value={overallProgress * 100} />
|
<Progress value={overallProgress * 100} />
|
||||||
<span className="text-xs font-medium text-left-panel-fg/80 shrink-0">
|
<span className="text-xs font-medium text-left-panel-fg/80 shrink-0">
|
||||||
@ -272,7 +278,9 @@ export function DownloadManagement() {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="p-2 py-1.5 bg-main-view-fg/5 border-b border-main-view-fg/6">
|
<div className="p-2 py-1.5 bg-main-view-fg/5 border-b border-main-view-fg/6">
|
||||||
<p className="text-xs text-main-view-fg/70">{t('downloading')}</p>
|
<p className="text-xs text-main-view-fg/70">
|
||||||
|
{t('downloading')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 max-h-[300px] overflow-y-auto space-y-2">
|
<div className="p-2 max-h-[300px] overflow-y-auto space-y-2">
|
||||||
{appUpdateState.isDownloading && (
|
{appUpdateState.isDownloading && (
|
||||||
@ -309,10 +317,15 @@ export function DownloadManagement() {
|
|||||||
title="Cancel download"
|
title="Cancel download"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
abortDownload(download.name).then(() => {
|
abortDownload(download.name).then(() => {
|
||||||
toast.info(t('common:toast.downloadCancelled.title'), {
|
toast.info(
|
||||||
|
t('common:toast.downloadCancelled.title'),
|
||||||
|
{
|
||||||
id: 'cancel-download',
|
id: 'cancel-download',
|
||||||
description: t('common:toast.downloadCancelled.description'),
|
description: t(
|
||||||
})
|
'common:toast.downloadCancelled.description'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
if (downloadProcesses.length === 0) {
|
if (downloadProcesses.length === 0) {
|
||||||
setIsPopoverOpen(false)
|
setIsPopoverOpen(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,6 +79,9 @@ const LeftPanel = () => {
|
|||||||
const searchContainerRef = useRef<HTMLDivElement>(null)
|
const searchContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const searchContainerMacRef = useRef<HTMLDivElement>(null)
|
const searchContainerMacRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Determine if we're in a resizable context (large screen with panel open)
|
||||||
|
const isResizableContext = !isSmallScreen && open
|
||||||
|
|
||||||
// Use click outside hook for panel with debugging
|
// Use click outside hook for panel with debugging
|
||||||
useClickOutside(
|
useClickOutside(
|
||||||
() => {
|
() => {
|
||||||
@ -189,9 +192,17 @@ const LeftPanel = () => {
|
|||||||
<aside
|
<aside
|
||||||
ref={panelRef}
|
ref={panelRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-48 shrink-0 rounded-lg m-1.5 mr-0 text-left-panel-fg overflow-hidden',
|
'text-left-panel-fg overflow-hidden',
|
||||||
|
// Resizable context: full height and width, no margins
|
||||||
|
isResizableContext && 'h-full w-full',
|
||||||
|
// Small screen context: fixed positioning and styling
|
||||||
isSmallScreen &&
|
isSmallScreen &&
|
||||||
'fixed h-[calc(100%-16px)] bg-main-view z-40 rounded-sm border border-left-panel-fg/10 m-2 px-1',
|
'fixed h-[calc(100%-16px)] bg-main-view z-40 rounded-sm border border-left-panel-fg/10 m-2 px-1 w-48',
|
||||||
|
// Default context: original styling
|
||||||
|
!isResizableContext &&
|
||||||
|
!isSmallScreen &&
|
||||||
|
'w-48 shrink-0 rounded-lg m-1.5 mr-0',
|
||||||
|
// Visibility controls
|
||||||
open
|
open
|
||||||
? 'opacity-100 visibility-visible'
|
? 'opacity-100 visibility-visible'
|
||||||
: 'w-0 absolute -top-100 -left-100 visibility-hidden'
|
: 'w-0 absolute -top-100 -left-100 visibility-hidden'
|
||||||
@ -209,7 +220,12 @@ const LeftPanel = () => {
|
|||||||
{!IS_MACOS && (
|
{!IS_MACOS && (
|
||||||
<div
|
<div
|
||||||
ref={searchContainerRef}
|
ref={searchContainerRef}
|
||||||
className="relative top-1.5 mb-4 mx-1 mt-1 w-[calc(100%-32px)] z-50"
|
className={cn(
|
||||||
|
'relative top-1.5 mb-4 mt-1 z-50',
|
||||||
|
isResizableContext
|
||||||
|
? 'mx-2 w-[calc(100%-48px)]'
|
||||||
|
: 'mx-1 w-[calc(100%-32px)]'
|
||||||
|
)}
|
||||||
data-ignore-outside-clicks
|
data-ignore-outside-clicks
|
||||||
>
|
>
|
||||||
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
||||||
@ -241,7 +257,10 @@ const LeftPanel = () => {
|
|||||||
{IS_MACOS && (
|
{IS_MACOS && (
|
||||||
<div
|
<div
|
||||||
ref={searchContainerMacRef}
|
ref={searchContainerMacRef}
|
||||||
className="relative mb-4 mx-1 mt-1"
|
className={cn(
|
||||||
|
'relative mb-4 mt-1',
|
||||||
|
isResizableContext ? 'mx-2' : 'mx-1'
|
||||||
|
)}
|
||||||
data-ignore-outside-clicks
|
data-ignore-outside-clicks
|
||||||
>
|
>
|
||||||
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
||||||
@ -452,6 +471,7 @@ const LeftPanel = () => {
|
|||||||
key={menu.title}
|
key={menu.title}
|
||||||
to={menu.route}
|
to={menu.route}
|
||||||
onClick={() => isSmallScreen && setLeftPanel(false)}
|
onClick={() => isSmallScreen && setLeftPanel(false)}
|
||||||
|
data-test-id={`menu-${menu.title}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded',
|
'flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded',
|
||||||
isActive
|
isActive
|
||||||
|
|||||||
@ -1,152 +0,0 @@
|
|||||||
import { route } from '@/constants/routes'
|
|
||||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
|
||||||
import { cn, getProviderTitle } from '@/lib/utils'
|
|
||||||
import { useNavigate, useMatches, Link } from '@tanstack/react-router'
|
|
||||||
import { IconArrowLeft, IconCirclePlus } from '@tabler/icons-react'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { useCallback, useState } from 'react'
|
|
||||||
import { openAIProviderSettings } from '@/mock/data'
|
|
||||||
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
|
||||||
import cloneDeep from 'lodash/cloneDeep'
|
|
||||||
import { toast } from 'sonner'
|
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
|
||||||
|
|
||||||
const ProvidersMenu = ({
|
|
||||||
stepSetupRemoteProvider,
|
|
||||||
}: {
|
|
||||||
stepSetupRemoteProvider: boolean
|
|
||||||
}) => {
|
|
||||||
const { providers, addProvider } = useModelProvider()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const matches = useMatches()
|
|
||||||
const [name, setName] = useState('')
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const createProvider = useCallback(() => {
|
|
||||||
if (providers.some((e) => e.provider === name)) {
|
|
||||||
toast.error(t('providerAlreadyExists', { name }))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const newProvider = {
|
|
||||||
provider: name,
|
|
||||||
active: true,
|
|
||||||
models: [],
|
|
||||||
settings: cloneDeep(openAIProviderSettings) as ProviderSetting[],
|
|
||||||
api_key: '',
|
|
||||||
base_url: 'https://api.openai.com/v1',
|
|
||||||
}
|
|
||||||
addProvider(newProvider)
|
|
||||||
setTimeout(() => {
|
|
||||||
navigate({
|
|
||||||
to: route.settings.providers,
|
|
||||||
params: {
|
|
||||||
providerName: name,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, 0)
|
|
||||||
}, [providers, name, addProvider, t, navigate])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-44 py-2 border-r border-main-view-fg/5 pb-10 overflow-y-auto">
|
|
||||||
<Link to={route.settings.general}>
|
|
||||||
<div className="flex items-center gap-0.5 ml-3 mb-4 mt-1">
|
|
||||||
<IconArrowLeft size={16} className="text-main-view-fg/70" />
|
|
||||||
<span className="text-main-view-fg/80">{t('common:back')}</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
<div className="first-step-setup-remote-provider">
|
|
||||||
{providers.map((provider, index) => {
|
|
||||||
const isActive = matches.some(
|
|
||||||
(match) =>
|
|
||||||
match.routeId === '/settings/providers/$providerName' &&
|
|
||||||
'providerName' in match.params &&
|
|
||||||
match.params.providerName === provider.provider
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={index} className="flex flex-col px-2 my-1.5 ">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex px-2 items-center gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5 text-main-view-fg/80',
|
|
||||||
isActive && 'bg-main-view-fg/5',
|
|
||||||
// hidden for llama.cpp provider for setup remote provider
|
|
||||||
provider.provider === 'llama.cpp' &&
|
|
||||||
stepSetupRemoteProvider &&
|
|
||||||
'hidden'
|
|
||||||
)}
|
|
||||||
onClick={() =>
|
|
||||||
navigate({
|
|
||||||
to: route.settings.providers,
|
|
||||||
params: {
|
|
||||||
providerName: provider.provider,
|
|
||||||
},
|
|
||||||
...(stepSetupRemoteProvider
|
|
||||||
? { search: { step: 'setup_remote_provider' } }
|
|
||||||
: {}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ProvidersAvatar provider={provider} />
|
|
||||||
<div className="truncate">
|
|
||||||
<span>{getProviderTitle(provider.provider)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<div className="flex cursor-pointer px-4 my-1.5 items-center gap-1.5 text-main-view-fg/80">
|
|
||||||
<IconCirclePlus size={18} />
|
|
||||||
<span>{t('provider:addProvider')}</span>
|
|
||||||
</div>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t('provider:addOpenAIProvider')}</DialogTitle>
|
|
||||||
<Input
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
className="mt-2"
|
|
||||||
placeholder={t('provider:enterNameForProvider')}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
// Prevent key from being captured by parent components
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DialogFooter className="mt-2 flex items-center">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
className="hover:no-underline"
|
|
||||||
>
|
|
||||||
{t('common:cancel')}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button disabled={!name} onClick={createProvider}>
|
|
||||||
{t('common:create')}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProvidersMenu
|
|
||||||
@ -1,21 +1,57 @@
|
|||||||
import { Link, useMatches } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import { route } from '@/constants/routes'
|
import { route } from '@/constants/routes'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
import { useState, useEffect } from 'react'
|
||||||
|
import {
|
||||||
|
IconChevronDown,
|
||||||
|
IconChevronRight,
|
||||||
|
IconMenu2,
|
||||||
|
IconX,
|
||||||
|
} from '@tabler/icons-react'
|
||||||
|
import { useMatches, useNavigate } from '@tanstack/react-router'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
||||||
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
|
import { getProviderTitle } from '@/lib/utils'
|
||||||
|
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
||||||
|
|
||||||
const SettingsMenu = () => {
|
const SettingsMenu = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { providers } = useModelProvider()
|
const [expandedProviders, setExpandedProviders] = useState(false)
|
||||||
const { experimentalFeatures } = useGeneralSetting()
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||||
const firstItemProvider =
|
|
||||||
providers.length > 0 ? providers[0].provider : 'llama.cpp'
|
|
||||||
const matches = useMatches()
|
const matches = useMatches()
|
||||||
const isActive = matches.some(
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { experimentalFeatures } = useGeneralSetting()
|
||||||
|
const { providers } = useModelProvider()
|
||||||
|
|
||||||
|
// Filter providers that have active API keys (or are llama.cpp which doesn't need one)
|
||||||
|
const activeProviders = providers.filter((provider) => provider.active)
|
||||||
|
|
||||||
|
// Check if current route has a providerName parameter and expand providers submenu
|
||||||
|
useEffect(() => {
|
||||||
|
const hasProviderName = matches.some(
|
||||||
(match) =>
|
(match) =>
|
||||||
match.routeId === '/settings/providers/$providerName' &&
|
match.routeId === '/settings/providers/$providerName' &&
|
||||||
'providerName' in match.params
|
'providerName' in match.params
|
||||||
)
|
)
|
||||||
|
const isProvidersRoute = matches.some(
|
||||||
|
(match) => match.routeId === '/settings/providers/'
|
||||||
|
)
|
||||||
|
if (hasProviderName || isProvidersRoute) {
|
||||||
|
setExpandedProviders(true)
|
||||||
|
}
|
||||||
|
}, [matches])
|
||||||
|
|
||||||
|
// Check if we're in the setup remote provider step
|
||||||
|
const stepSetupRemoteProvider = matches.some(
|
||||||
|
(match) =>
|
||||||
|
match.search &&
|
||||||
|
typeof match.search === 'object' &&
|
||||||
|
'step' in match.search &&
|
||||||
|
match.search.step === 'setup_remote_provider'
|
||||||
|
)
|
||||||
|
|
||||||
const menuSettings = [
|
const menuSettings = [
|
||||||
{
|
{
|
||||||
@ -30,6 +66,11 @@ const SettingsMenu = () => {
|
|||||||
title: 'common:privacy',
|
title: 'common:privacy',
|
||||||
route: route.settings.privacy,
|
route: route.settings.privacy,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'common:modelProviders',
|
||||||
|
route: route.settings.model_providers,
|
||||||
|
hasSubMenu: activeProviders.length > 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'common:keyboardShortcuts',
|
title: 'common:keyboardShortcuts',
|
||||||
route: route.settings.shortcuts,
|
route: route.settings.shortcuts,
|
||||||
@ -61,52 +102,113 @@ const SettingsMenu = () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const toggleProvidersExpansion = () => {
|
||||||
|
setExpandedProviders(!expandedProviders)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
setIsMenuOpen(!isMenuOpen)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-44 shrink-0 px-1.5 pt-3 border-r border-main-view-fg/5">
|
<>
|
||||||
|
<button
|
||||||
|
className="fixed top-4 right-4 sm:hidden size-5 cursor-pointer items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-main-view-fg/10 z-20"
|
||||||
|
onClick={toggleMenu}
|
||||||
|
aria-label="Toggle settings menu"
|
||||||
|
>
|
||||||
|
{isMenuOpen ? (
|
||||||
|
<IconX size={18} className="text-main-view-fg relative z-20" />
|
||||||
|
) : (
|
||||||
|
<IconMenu2 size={18} className="text-main-view-fg relative z-20" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'h-full w-44 shrink-0 px-1.5 pt-3 border-r border-main-view-fg/5 bg-main-view',
|
||||||
|
'sm:flex',
|
||||||
|
isMenuOpen
|
||||||
|
? 'flex fixed sm:hidden top-0 z-10 m-1 h-[calc(100%-8px)] border-r-0 border-l bg-main-view right-0 py-8 rounded-tr-lg rounded-br-lg'
|
||||||
|
: 'hidden'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-1 w-full text-main-view-fg/90 font-medium">
|
<div className="flex flex-col gap-1 w-full text-main-view-fg/90 font-medium">
|
||||||
{menuSettings.map((menu, index) => {
|
{menuSettings.map((menu) => (
|
||||||
// Render the menu item
|
<div key={menu.title}>
|
||||||
const menuItem = (
|
|
||||||
<Link
|
<Link
|
||||||
key={menu.title}
|
|
||||||
to={menu.route}
|
to={menu.route}
|
||||||
className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5"
|
className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5"
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-main-view-fg/80">{t(menu.title)}</span>
|
<span className="text-main-view-fg/80">{t(menu.title)}</span>
|
||||||
</Link>
|
{menu.hasSubMenu && (
|
||||||
)
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
if (index === 2) {
|
e.preventDefault()
|
||||||
return (
|
e.stopPropagation()
|
||||||
<div key={menu.title}>
|
toggleProvidersExpansion()
|
||||||
<span className="mb-1 block">{menuItem}</span>
|
}}
|
||||||
|
className="text-main-view-fg/60 hover:text-main-view-fg/80"
|
||||||
{/* Model Providers Link with default parameter */}
|
|
||||||
{isActive ? (
|
|
||||||
<div className="block px-2 mt-1 gap-1.5 py-1 w-full rounded bg-main-view-fg/5 cursor-pointer">
|
|
||||||
<span>{t('common:modelProviders')}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
key="common.modelProviders"
|
|
||||||
to={route.settings.providers}
|
|
||||||
params={{ providerName: firstItemProvider }}
|
|
||||||
className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded"
|
|
||||||
>
|
>
|
||||||
<span className="text-main-view-fg/80">
|
{expandedProviders ? (
|
||||||
{t('common:modelProviders')}
|
<IconChevronDown size={16} />
|
||||||
</span>
|
) : (
|
||||||
</Link>
|
<IconChevronRight size={16} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
</Link>
|
||||||
}
|
|
||||||
|
|
||||||
// For other menu items, just render them normally
|
{/* Sub-menu for model providers */}
|
||||||
return menuItem
|
{menu.hasSubMenu && expandedProviders && (
|
||||||
|
<div className="ml-2 mt-1 space-y-1 first-step-setup-remote-provider">
|
||||||
|
{activeProviders.map((provider) => {
|
||||||
|
const isActive = matches.some(
|
||||||
|
(match) =>
|
||||||
|
match.routeId === '/settings/providers/$providerName' &&
|
||||||
|
'providerName' in match.params &&
|
||||||
|
match.params.providerName === provider.provider
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={provider.provider}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex px-2 items-center gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5 text-main-view-fg/80',
|
||||||
|
isActive && 'bg-main-view-fg/5',
|
||||||
|
// hidden for llama.cpp provider for setup remote provider
|
||||||
|
provider.provider === 'llama.cpp' &&
|
||||||
|
stepSetupRemoteProvider &&
|
||||||
|
'hidden'
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
navigate({
|
||||||
|
to: route.settings.providers,
|
||||||
|
params: {
|
||||||
|
providerName: provider.provider,
|
||||||
|
},
|
||||||
|
...(stepSetupRemoteProvider
|
||||||
|
? { search: { step: 'setup_remote_provider' } }
|
||||||
|
: {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ProvidersAvatar provider={provider} />
|
||||||
|
<div className="truncate">
|
||||||
|
<span>{getProviderTitle(provider.provider)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -105,8 +105,13 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setOpenDropdown(true)
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'mb-1 rounded hover:bg-left-panel-fg/10 flex items-center justify-between gap-2 px-1.5 group/thread-list transition-all',
|
'mb-1 rounded hover:bg-left-panel-fg/10 flex items-center justify-between gap-2 px-1.5 transition-all',
|
||||||
isDragging ? 'cursor-move' : 'cursor-pointer',
|
isDragging ? 'cursor-move' : 'cursor-pointer',
|
||||||
isActive && 'bg-left-panel-fg/10'
|
isActive && 'bg-left-panel-fg/10'
|
||||||
)}
|
)}
|
||||||
@ -122,7 +127,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<IconDots
|
<IconDots
|
||||||
size={14}
|
size={14}
|
||||||
className="text-left-panel-fg/60 shrink-0 cursor-pointer px-0.5 -mr-1 data-[state=open]:bg-left-panel-fg/10 rounded group-hover/thread-list:data-[state=closed]:size-5 size-5 data-[state=closed]:size-0"
|
className="text-left-panel-fg/60 shrink-0 cursor-pointer px-0.5 -mr-1 data-[state=open]:bg-left-panel-fg/10 rounded size-5"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|||||||
@ -4,14 +4,18 @@ import { localStorageKey } from '@/constants/localStorage'
|
|||||||
|
|
||||||
type LeftPanelStoreState = {
|
type LeftPanelStoreState = {
|
||||||
open: boolean
|
open: boolean
|
||||||
|
size: number
|
||||||
setLeftPanel: (value: boolean) => void
|
setLeftPanel: (value: boolean) => void
|
||||||
|
setLeftPanelSize: (value: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useLeftPanel = create<LeftPanelStoreState>()(
|
export const useLeftPanel = create<LeftPanelStoreState>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
open: true,
|
open: true,
|
||||||
|
size: 20, // Default size of 20%
|
||||||
setLeftPanel: (value) => set({ open: value }),
|
setLeftPanel: (value) => set({ open: value }),
|
||||||
|
setLeftPanelSize: (value) => set({ size: value }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: localStorageKey.LeftPanel,
|
name: localStorageKey.LeftPanel,
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import { Route as SettingsGeneralImport } from './routes/settings/general'
|
|||||||
import { Route as SettingsExtensionsImport } from './routes/settings/extensions'
|
import { Route as SettingsExtensionsImport } from './routes/settings/extensions'
|
||||||
import { Route as SettingsAppearanceImport } from './routes/settings/appearance'
|
import { Route as SettingsAppearanceImport } from './routes/settings/appearance'
|
||||||
import { Route as LocalApiServerLogsImport } from './routes/local-api-server/logs'
|
import { Route as LocalApiServerLogsImport } from './routes/local-api-server/logs'
|
||||||
|
import { Route as SettingsProvidersIndexImport } from './routes/settings/providers/index'
|
||||||
import { Route as SettingsProvidersProviderNameImport } from './routes/settings/providers/$providerName'
|
import { Route as SettingsProvidersProviderNameImport } from './routes/settings/providers/$providerName'
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
@ -127,6 +128,12 @@ const LocalApiServerLogsRoute = LocalApiServerLogsImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const SettingsProvidersIndexRoute = SettingsProvidersIndexImport.update({
|
||||||
|
id: '/settings/providers/',
|
||||||
|
path: '/settings/providers/',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const SettingsProvidersProviderNameRoute =
|
const SettingsProvidersProviderNameRoute =
|
||||||
SettingsProvidersProviderNameImport.update({
|
SettingsProvidersProviderNameImport.update({
|
||||||
id: '/settings/providers/$providerName',
|
id: '/settings/providers/$providerName',
|
||||||
@ -257,6 +264,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof SettingsProvidersProviderNameImport
|
preLoaderRoute: typeof SettingsProvidersProviderNameImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/settings/providers/': {
|
||||||
|
id: '/settings/providers/'
|
||||||
|
path: '/settings/providers'
|
||||||
|
fullPath: '/settings/providers'
|
||||||
|
preLoaderRoute: typeof SettingsProvidersIndexImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,6 +294,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/settings/shortcuts': typeof SettingsShortcutsRoute
|
'/settings/shortcuts': typeof SettingsShortcutsRoute
|
||||||
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
||||||
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
||||||
|
'/settings/providers': typeof SettingsProvidersIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
@ -300,6 +315,7 @@ export interface FileRoutesByTo {
|
|||||||
'/settings/shortcuts': typeof SettingsShortcutsRoute
|
'/settings/shortcuts': typeof SettingsShortcutsRoute
|
||||||
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
||||||
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
||||||
|
'/settings/providers': typeof SettingsProvidersIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
@ -321,6 +337,7 @@ export interface FileRoutesById {
|
|||||||
'/settings/shortcuts': typeof SettingsShortcutsRoute
|
'/settings/shortcuts': typeof SettingsShortcutsRoute
|
||||||
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
||||||
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
||||||
|
'/settings/providers/': typeof SettingsProvidersIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
@ -343,6 +360,7 @@ export interface FileRouteTypes {
|
|||||||
| '/settings/shortcuts'
|
| '/settings/shortcuts'
|
||||||
| '/threads/$threadId'
|
| '/threads/$threadId'
|
||||||
| '/settings/providers/$providerName'
|
| '/settings/providers/$providerName'
|
||||||
|
| '/settings/providers'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
@ -362,6 +380,7 @@ export interface FileRouteTypes {
|
|||||||
| '/settings/shortcuts'
|
| '/settings/shortcuts'
|
||||||
| '/threads/$threadId'
|
| '/threads/$threadId'
|
||||||
| '/settings/providers/$providerName'
|
| '/settings/providers/$providerName'
|
||||||
|
| '/settings/providers'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
@ -381,6 +400,7 @@ export interface FileRouteTypes {
|
|||||||
| '/settings/shortcuts'
|
| '/settings/shortcuts'
|
||||||
| '/threads/$threadId'
|
| '/threads/$threadId'
|
||||||
| '/settings/providers/$providerName'
|
| '/settings/providers/$providerName'
|
||||||
|
| '/settings/providers/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,6 +422,7 @@ export interface RootRouteChildren {
|
|||||||
SettingsShortcutsRoute: typeof SettingsShortcutsRoute
|
SettingsShortcutsRoute: typeof SettingsShortcutsRoute
|
||||||
ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute
|
ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute
|
||||||
SettingsProvidersProviderNameRoute: typeof SettingsProvidersProviderNameRoute
|
SettingsProvidersProviderNameRoute: typeof SettingsProvidersProviderNameRoute
|
||||||
|
SettingsProvidersIndexRoute: typeof SettingsProvidersIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
@ -422,6 +443,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
SettingsShortcutsRoute: SettingsShortcutsRoute,
|
SettingsShortcutsRoute: SettingsShortcutsRoute,
|
||||||
ThreadsThreadIdRoute: ThreadsThreadIdRoute,
|
ThreadsThreadIdRoute: ThreadsThreadIdRoute,
|
||||||
SettingsProvidersProviderNameRoute: SettingsProvidersProviderNameRoute,
|
SettingsProvidersProviderNameRoute: SettingsProvidersProviderNameRoute,
|
||||||
|
SettingsProvidersIndexRoute: SettingsProvidersIndexRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routeTree = rootRoute
|
export const routeTree = rootRoute
|
||||||
@ -450,7 +472,8 @@ export const routeTree = rootRoute
|
|||||||
"/settings/privacy",
|
"/settings/privacy",
|
||||||
"/settings/shortcuts",
|
"/settings/shortcuts",
|
||||||
"/threads/$threadId",
|
"/threads/$threadId",
|
||||||
"/settings/providers/$providerName"
|
"/settings/providers/$providerName",
|
||||||
|
"/settings/providers/"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
@ -503,6 +526,9 @@ export const routeTree = rootRoute
|
|||||||
},
|
},
|
||||||
"/settings/providers/$providerName": {
|
"/settings/providers/$providerName": {
|
||||||
"filePath": "settings/providers/$providerName.tsx"
|
"filePath": "settings/providers/$providerName.tsx"
|
||||||
|
},
|
||||||
|
"/settings/providers/": {
|
||||||
|
"filePath": "settings/providers/index.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,13 @@ import { cn } from '@/lib/utils'
|
|||||||
import ToolApproval from '@/containers/dialogs/ToolApproval'
|
import ToolApproval from '@/containers/dialogs/ToolApproval'
|
||||||
import { TranslationProvider } from '@/i18n/TranslationContext'
|
import { TranslationProvider } from '@/i18n/TranslationContext'
|
||||||
import OutOfContextPromiseModal from '@/containers/dialogs/OutOfContextDialog'
|
import OutOfContextPromiseModal from '@/containers/dialogs/OutOfContextDialog'
|
||||||
|
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||||
|
import {
|
||||||
|
ResizablePanelGroup,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizableHandle,
|
||||||
|
} from '@/components/ui/resizable'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: RootLayout,
|
component: RootLayout,
|
||||||
@ -27,7 +34,33 @@ export const Route = createRootRoute({
|
|||||||
|
|
||||||
const AppLayout = () => {
|
const AppLayout = () => {
|
||||||
const { productAnalyticPrompt } = useAnalytic()
|
const { productAnalyticPrompt } = useAnalytic()
|
||||||
const { open: isLeftPanelOpen } = useLeftPanel()
|
const {
|
||||||
|
open: isLeftPanelOpen,
|
||||||
|
setLeftPanel,
|
||||||
|
size: leftPanelSize,
|
||||||
|
setLeftPanelSize,
|
||||||
|
} = useLeftPanel()
|
||||||
|
const isSmallScreen = useSmallScreen()
|
||||||
|
|
||||||
|
// Minimum width threshold for auto-close (10% of screen width)
|
||||||
|
const MIN_PANEL_WIDTH_THRESHOLD = 14
|
||||||
|
|
||||||
|
// Handle panel size changes
|
||||||
|
const handlePanelLayout = useCallback(
|
||||||
|
(sizes: number[]) => {
|
||||||
|
if (sizes.length > 0) {
|
||||||
|
const newSize = sizes[0]
|
||||||
|
|
||||||
|
// Close panel if resized below minimum threshold
|
||||||
|
if (newSize < MIN_PANEL_WIDTH_THRESHOLD) {
|
||||||
|
setLeftPanel(false)
|
||||||
|
} else {
|
||||||
|
setLeftPanelSize(newSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setLeftPanelSize, setLeftPanel]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@ -37,6 +70,39 @@ const AppLayout = () => {
|
|||||||
{/* Fake absolute panel top to enable window drag */}
|
{/* Fake absolute panel top to enable window drag */}
|
||||||
<div className="absolute w-full h-10 z-10" data-tauri-drag-region />
|
<div className="absolute w-full h-10 z-10" data-tauri-drag-region />
|
||||||
<DialogAppUpdater />
|
<DialogAppUpdater />
|
||||||
|
|
||||||
|
{/* Use ResizablePanelGroup only on larger screens */}
|
||||||
|
{!isSmallScreen && isLeftPanelOpen ? (
|
||||||
|
<ResizablePanelGroup
|
||||||
|
direction="horizontal"
|
||||||
|
className="h-full"
|
||||||
|
onLayout={handlePanelLayout}
|
||||||
|
>
|
||||||
|
{/* Left Panel */}
|
||||||
|
<ResizablePanel
|
||||||
|
defaultSize={leftPanelSize}
|
||||||
|
minSize={MIN_PANEL_WIDTH_THRESHOLD}
|
||||||
|
maxSize={40}
|
||||||
|
collapsible
|
||||||
|
>
|
||||||
|
<div className="h-full p-1">
|
||||||
|
<LeftPanel />
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
|
||||||
|
{/* Resize Handle */}
|
||||||
|
<ResizableHandle withHandle />
|
||||||
|
|
||||||
|
{/* Main Content Panel */}
|
||||||
|
<ResizablePanel defaultSize={100 - leftPanelSize} minSize={60}>
|
||||||
|
<div className="h-full p-1 pl-0">
|
||||||
|
<div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full h-full rounded-lg overflow-hidden">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
) : (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
{/* left content panel - only show if not logs route */}
|
{/* left content panel - only show if not logs route */}
|
||||||
<LeftPanel />
|
<LeftPanel />
|
||||||
@ -53,6 +119,7 @@ const AppLayout = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
{productAnalyticPrompt && <PromptAnalytic />}
|
{productAnalyticPrompt && <PromptAnalytic />}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@ -259,11 +259,16 @@ function Hub() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isDownloaded ? (
|
{isDownloaded ? (
|
||||||
<Button size="sm" onClick={() => handleUseModel(modelId)}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleUseModel(modelId)}
|
||||||
|
data-test-id={`hub-model-${modelId}`}
|
||||||
|
>
|
||||||
{t('hub:use')}
|
{t('hub:use')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
data-test-id={`hub-model-${modelId}`}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
className={cn(isDownloading && 'hidden')}
|
className={cn(isDownloading && 'hidden')}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ function Appareances() {
|
|||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="flex h-full w-full">
|
<div className="flex h-full w-full flex-col sm:flex-row">
|
||||||
<SettingsMenu />
|
<SettingsMenu />
|
||||||
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
||||||
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
|
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
|
||||||
@ -55,26 +55,31 @@ function Appareances() {
|
|||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:appearance.windowBackground')}
|
title={t('settings:appearance.windowBackground')}
|
||||||
description={t('settings:appearance.windowBackgroundDesc')}
|
description={t('settings:appearance.windowBackgroundDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={<ColorPickerAppBgColor />}
|
actions={<ColorPickerAppBgColor />}
|
||||||
/>
|
/>
|
||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:appearance.appMainView')}
|
title={t('settings:appearance.appMainView')}
|
||||||
description={t('settings:appearance.appMainViewDesc')}
|
description={t('settings:appearance.appMainViewDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={<ColorPickerAppMainView />}
|
actions={<ColorPickerAppMainView />}
|
||||||
/>
|
/>
|
||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:appearance.primary')}
|
title={t('settings:appearance.primary')}
|
||||||
description={t('settings:appearance.primaryDesc')}
|
description={t('settings:appearance.primaryDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={<ColorPickerAppPrimaryColor />}
|
actions={<ColorPickerAppPrimaryColor />}
|
||||||
/>
|
/>
|
||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:appearance.accent')}
|
title={t('settings:appearance.accent')}
|
||||||
description={t('settings:appearance.accentDesc')}
|
description={t('settings:appearance.accentDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={<ColorPickerAppAccentColor />}
|
actions={<ColorPickerAppAccentColor />}
|
||||||
/>
|
/>
|
||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:appearance.destructive')}
|
title={t('settings:appearance.destructive')}
|
||||||
description={t('settings:appearance.destructiveDesc')}
|
description={t('settings:appearance.destructiveDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={<ColorPickerAppDestructiveColor />}
|
actions={<ColorPickerAppDestructiveColor />}
|
||||||
/>
|
/>
|
||||||
<CardItem
|
<CardItem
|
||||||
|
|||||||
@ -205,7 +205,7 @@ function General() {
|
|||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="flex h-full w-full">
|
<div className="flex h-full w-full flex-col sm:flex-row">
|
||||||
<SettingsMenu />
|
<SettingsMenu />
|
||||||
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
||||||
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
|
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
|
||||||
@ -222,6 +222,7 @@ function General() {
|
|||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:general.checkForUpdates')}
|
title={t('settings:general.checkForUpdates')}
|
||||||
description={t('settings:general.checkForUpdatesDesc')}
|
description={t('settings:general.checkForUpdatesDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
@ -265,6 +266,7 @@ function General() {
|
|||||||
ns: 'settings',
|
ns: 'settings',
|
||||||
})}
|
})}
|
||||||
align="start"
|
align="start"
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
@ -274,12 +276,14 @@ function General() {
|
|||||||
|
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2 mt-1 ">
|
<div className="flex items-center gap-2 mt-1 ">
|
||||||
|
<div className="">
|
||||||
<span
|
<span
|
||||||
title={janDataFolder}
|
title={janDataFolder}
|
||||||
className="bg-main-view-fg/10 text-xs px-1 py-0.5 rounded-sm text-main-view-fg/80"
|
className="bg-main-view-fg/10 text-xs px-1 py-0.5 rounded-sm text-main-view-fg/80 line-clamp-1 w-fit"
|
||||||
>
|
>
|
||||||
{janDataFolder}
|
{janDataFolder}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
janDataFolder && copyToClipboard(janDataFolder)
|
janDataFolder && copyToClipboard(janDataFolder)
|
||||||
@ -349,6 +353,7 @@ function General() {
|
|||||||
ns: 'settings',
|
ns: 'settings',
|
||||||
})}
|
})}
|
||||||
description={t('settings:dataFolder.appLogsDesc')}
|
description={t('settings:dataFolder.appLogsDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={
|
actions={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -229,9 +229,11 @@ function LocalAPIServer() {
|
|||||||
title={t('settings:localApiServer.apiKey')}
|
title={t('settings:localApiServer.apiKey')}
|
||||||
description={t('settings:localApiServer.apiKeyDesc')}
|
description={t('settings:localApiServer.apiKeyDesc')}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
'flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2',
|
||||||
isServerRunning && 'opacity-50 pointer-events-none',
|
isServerRunning && 'opacity-50 pointer-events-none',
|
||||||
isApiKeyEmpty && showApiKeyError && 'pb-6'
|
isApiKeyEmpty && showApiKeyError && 'pb-6'
|
||||||
)}
|
)}
|
||||||
|
classNameWrapperAction="w-full sm:w-auto"
|
||||||
actions={
|
actions={
|
||||||
<ApiKeyInput
|
<ApiKeyInput
|
||||||
showError={showApiKeyError}
|
showError={showApiKeyError}
|
||||||
@ -243,8 +245,10 @@ function LocalAPIServer() {
|
|||||||
title={t('settings:localApiServer.trustedHosts')}
|
title={t('settings:localApiServer.trustedHosts')}
|
||||||
description={t('settings:localApiServer.trustedHostsDesc')}
|
description={t('settings:localApiServer.trustedHostsDesc')}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
'flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2',
|
||||||
isServerRunning && 'opacity-50 pointer-events-none'
|
isServerRunning && 'opacity-50 pointer-events-none'
|
||||||
)}
|
)}
|
||||||
|
classNameWrapperAction="w-full sm:w-auto"
|
||||||
actions={<TrustedHostsInput />}
|
actions={<TrustedHostsInput />}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -320,7 +320,7 @@ function MCPServers() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
description={
|
descriptionOutside={
|
||||||
<div className="text-sm text-main-view-fg/70">
|
<div className="text-sm text-main-view-fg/70">
|
||||||
<div>
|
<div>
|
||||||
{t('mcp-servers:command')}: {config.command}
|
{t('mcp-servers:command')}: {config.command}
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { Card, CardItem } from '@/containers/Card'
|
import { Card, CardItem } from '@/containers/Card'
|
||||||
import HeaderPage from '@/containers/HeaderPage'
|
import HeaderPage from '@/containers/HeaderPage'
|
||||||
import ProvidersMenu from '@/containers/ProvidersMenu'
|
import SettingsMenu from '@/containers/SettingsMenu'
|
||||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
import { cn, getProviderTitle } from '@/lib/utils'
|
import { cn, getProviderTitle } from '@/lib/utils'
|
||||||
import { Switch } from '@/components/ui/switch'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import {
|
import {
|
||||||
getActiveModels,
|
getActiveModels,
|
||||||
@ -228,23 +227,13 @@ function ProviderDetail() {
|
|||||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="flex h-full w-full">
|
<div className="flex h-full w-full">
|
||||||
<div className="flex">
|
<SettingsMenu />
|
||||||
<ProvidersMenu stepSetupRemoteProvider={isSetup} />
|
|
||||||
</div>
|
|
||||||
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
||||||
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
|
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="font-medium text-base">
|
<h1 className="font-medium text-base">
|
||||||
{getProviderTitle(providerName)}
|
{getProviderTitle(providerName)}
|
||||||
</h1>
|
</h1>
|
||||||
<Switch
|
|
||||||
checked={provider?.active}
|
|
||||||
onCheckedChange={(e) => {
|
|
||||||
if (provider) {
|
|
||||||
updateProvider(providerName, { ...provider, active: e })
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -461,7 +450,12 @@ function ProviderDetail() {
|
|||||||
key={modelIndex}
|
key={modelIndex}
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="font-medium">{model.id}</h1>
|
<h1
|
||||||
|
className="font-medium line-clamp-1"
|
||||||
|
title={model.id}
|
||||||
|
>
|
||||||
|
{model.id}
|
||||||
|
</h1>
|
||||||
<Capabilities capabilities={capabilities} />
|
<Capabilities capabilities={capabilities} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
187
web-app/src/routes/settings/providers/index.tsx
Normal file
187
web-app/src/routes/settings/providers/index.tsx
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { route } from '@/constants/routes'
|
||||||
|
import SettingsMenu from '@/containers/SettingsMenu'
|
||||||
|
import HeaderPage from '@/containers/HeaderPage'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardItem } from '@/containers/Card'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { IconCirclePlus, IconSettings } from '@tabler/icons-react'
|
||||||
|
import { getProviderTitle } from '@/lib/utils'
|
||||||
|
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import { openAIProviderSettings } from '@/mock/data'
|
||||||
|
import cloneDeep from 'lodash/cloneDeep'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const Route = createFileRoute(route.settings.model_providers as any)({
|
||||||
|
component: ModelProviders,
|
||||||
|
})
|
||||||
|
|
||||||
|
function ModelProviders() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { providers, addProvider, updateProvider } = useModelProvider()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
|
||||||
|
const createProvider = useCallback(() => {
|
||||||
|
if (providers.some((e) => e.provider.toLowerCase() === name.toLowerCase())) {
|
||||||
|
toast.error(t('providerAlreadyExists', { name }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const newProvider = {
|
||||||
|
provider: name,
|
||||||
|
active: true,
|
||||||
|
models: [],
|
||||||
|
settings: cloneDeep(openAIProviderSettings) as ProviderSetting[],
|
||||||
|
api_key: '',
|
||||||
|
base_url: 'https://api.openai.com/v1',
|
||||||
|
}
|
||||||
|
addProvider(newProvider)
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate({
|
||||||
|
to: route.settings.providers,
|
||||||
|
params: {
|
||||||
|
providerName: name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, 0)
|
||||||
|
}, [providers, name, addProvider, t, navigate])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<HeaderPage>
|
||||||
|
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||||
|
</HeaderPage>
|
||||||
|
<div className="flex h-full w-full flex-col sm:flex-row">
|
||||||
|
<SettingsMenu />
|
||||||
|
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
||||||
|
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
|
||||||
|
{/* Model Providers */}
|
||||||
|
<Card
|
||||||
|
header={
|
||||||
|
<div className="flex items-center justify-between w-full mb-6">
|
||||||
|
<span className="text-main-view-fg font-medium text-base">
|
||||||
|
{t('common:modelProviders')}
|
||||||
|
</span>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<div className="cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out p-1.5 py-1 gap-1 -mr-2">
|
||||||
|
<IconCirclePlus size={16} />
|
||||||
|
<span>{t('provider:addProvider')}</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t('provider:addOpenAIProvider')}
|
||||||
|
</DialogTitle>
|
||||||
|
<Input
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="mt-2"
|
||||||
|
placeholder={t('provider:enterNameForProvider')}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// Prevent key from being captured by parent components
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DialogFooter className="mt-2 flex items-center">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="hover:no-underline"
|
||||||
|
>
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button disabled={!name} onClick={createProvider}>
|
||||||
|
{t('common:create')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{providers.map((provider, index) => (
|
||||||
|
<CardItem
|
||||||
|
key={index}
|
||||||
|
title={
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<ProvidersAvatar provider={provider} />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium">
|
||||||
|
{getProviderTitle(provider.provider)}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-main-view-fg/70">
|
||||||
|
{provider.models.length} Models
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{provider.active && (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 bg-transparent hover:bg-main-view-fg/10 border-none shadow-none"
|
||||||
|
onClick={() => {
|
||||||
|
navigate({
|
||||||
|
to: route.settings.providers,
|
||||||
|
params: {
|
||||||
|
providerName: provider.provider,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconSettings
|
||||||
|
className="text-main-view-fg/60"
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Switch
|
||||||
|
checked={provider.active}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
updateProvider(provider.provider, {
|
||||||
|
...provider,
|
||||||
|
active: e,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -226,7 +226,7 @@ function ThreadDetail() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
data-test-id={`message-${item.id}`}
|
data-test-id={`message-${item.role}-${item.id}`}
|
||||||
data-message-author-role={item.role}
|
data-message-author-role={item.role}
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
>
|
>
|
||||||
@ -247,7 +247,10 @@ function ThreadDetail() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<StreamingContent threadId={threadId} />
|
<StreamingContent
|
||||||
|
threadId={threadId}
|
||||||
|
data-test-id="thread-content-text"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user