Merge branch 'dev' into allow-assistant-message-edits
This commit is contained in:
commit
a1ff097336
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
|
||||||
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
|
||||||
12
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: 🚀 Feature Request
|
||||||
|
about: Suggest an idea for this project 😻!
|
||||||
|
title: 'idea: '
|
||||||
|
type: 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
|
||||||
|
|
||||||
42
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
42
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,42 +0,0 @@
|
|||||||
name: "\U0001F41B Bug Report"
|
|
||||||
description: "If something isn't working as expected \U0001F914"
|
|
||||||
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: '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!"
|
|
||||||
16
.github/ISSUE_TEMPLATE/goal.md
vendored
16
.github/ISSUE_TEMPLATE/goal.md
vendored
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
name: Goal
|
|
||||||
about: Team-wide Quarterly Goals for Jan
|
|
||||||
title: 'goal: '
|
|
||||||
labels: ''
|
|
||||||
assignees: freelerobot
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
## Motivation
|
|
||||||
|
|
||||||
## Key Outcomes
|
|
||||||
|
|
||||||
## Related Epics
|
|
||||||
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)
|
|
||||||
215
.github/workflows/jan-electron-build-nightly.yml
vendored
215
.github/workflows/jan-electron-build-nightly.yml
vendored
@ -1,215 +0,0 @@
|
|||||||
name: Electron Builder - Nightly / Manual
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 20 * * 1,2,3' # At 8 PM UTC on Monday, Tuesday, and Wednesday which is 3 AM UTC+7 Tuesday, Wednesday, and Thursday
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
public_provider:
|
|
||||||
type: choice
|
|
||||||
description: 'Public Provider'
|
|
||||||
options:
|
|
||||||
- none
|
|
||||||
- aws-s3
|
|
||||||
default: none
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- release/**
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
set-public-provider:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
public_provider: ${{ steps.set-public-provider.outputs.public_provider }}
|
|
||||||
ref: ${{ steps.set-public-provider.outputs.ref }}
|
|
||||||
steps:
|
|
||||||
- name: Set public provider
|
|
||||||
id: set-public-provider
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
|
||||||
echo "::set-output name=public_provider::${{ github.event.inputs.public_provider }}"
|
|
||||||
echo "::set-output name=ref::${{ github.ref }}"
|
|
||||||
else
|
|
||||||
if [ "${{ github.event_name }}" == "schedule" ]; then
|
|
||||||
echo "::set-output name=public_provider::aws-s3"
|
|
||||||
echo "::set-output name=ref::refs/heads/dev"
|
|
||||||
elif [ "${{ github.event_name }}" == "push" ]; then
|
|
||||||
echo "::set-output name=public_provider::aws-s3"
|
|
||||||
echo "::set-output name=ref::${{ github.ref }}"
|
|
||||||
elif [ "${{ github.event_name }}" == "pull_request_review" ]; then
|
|
||||||
echo "::set-output name=public_provider::none"
|
|
||||||
echo "::set-output name=ref::${{ github.ref }}"
|
|
||||||
else
|
|
||||||
echo "::set-output name=public_provider::none"
|
|
||||||
echo "::set-output name=ref::${{ github.ref }}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# Job create Update app version based on latest release tag with build number and save to output
|
|
||||||
get-update-version:
|
|
||||||
uses: ./.github/workflows/template-get-update-version.yml
|
|
||||||
|
|
||||||
build-tauri-macos:
|
|
||||||
uses: ./.github/workflows/template-tauri-build-macos.yml
|
|
||||||
secrets: inherit
|
|
||||||
needs: [get-update-version, set-public-provider]
|
|
||||||
with:
|
|
||||||
ref: ${{ needs.set-public-provider.outputs.ref }}
|
|
||||||
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
|
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
channel: nightly
|
|
||||||
cortex_api_port: "39261"
|
|
||||||
|
|
||||||
build-tauri-windows-x64:
|
|
||||||
uses: ./.github/workflows/template-tauri-build-windows-x64.yml
|
|
||||||
secrets: inherit
|
|
||||||
needs: [get-update-version, set-public-provider]
|
|
||||||
with:
|
|
||||||
ref: ${{ needs.set-public-provider.outputs.ref }}
|
|
||||||
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
|
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
channel: nightly
|
|
||||||
cortex_api_port: "39261"
|
|
||||||
|
|
||||||
build-tauri-linux-x64:
|
|
||||||
uses: ./.github/workflows/template-tauri-build-linux-x64.yml
|
|
||||||
secrets: inherit
|
|
||||||
needs: [get-update-version, set-public-provider]
|
|
||||||
with:
|
|
||||||
ref: ${{ needs.set-public-provider.outputs.ref }}
|
|
||||||
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
|
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
channel: nightly
|
|
||||||
cortex_api_port: "39261"
|
|
||||||
|
|
||||||
sync-temp-to-latest:
|
|
||||||
needs: [get-update-version, set-public-provider, build-tauri-windows-x64, build-tauri-linux-x64, build-tauri-macos]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Install jq
|
|
||||||
uses: dcarbone/install-jq-action@v2.0.1
|
|
||||||
- name: create latest.json file
|
|
||||||
run: |
|
|
||||||
VERSION=${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
||||||
LINUX_SIGNATURE="${{ needs.build-tauri-linux-x64.outputs.APPIMAGE_SIG }}"
|
|
||||||
LINUX_URL="https://delta.jan.ai/nightly/${{ needs.build-tauri-linux-x64.outputs.APPIMAGE_FILE_NAME }}"
|
|
||||||
WINDOWS_SIGNATURE="${{ needs.build-tauri-windows-x64.outputs.WIN_SIG }}"
|
|
||||||
WINDOWS_URL="https://delta.jan.ai/nightly/${{ needs.build-tauri-windows-x64.outputs.FILE_NAME }}"
|
|
||||||
DARWIN_SIGNATURE="${{ needs.build-tauri-macos.outputs.MAC_UNIVERSAL_SIG }}"
|
|
||||||
DARWIN_URL="https://delta.jan.ai/nightly/Jan-nightly_${{ needs.get-update-version.outputs.new_version }}.app.tar.gz"
|
|
||||||
|
|
||||||
jq --arg version "$VERSION" \
|
|
||||||
--arg pub_date "$PUB_DATE" \
|
|
||||||
--arg linux_signature "$LINUX_SIGNATURE" \
|
|
||||||
--arg linux_url "$LINUX_URL" \
|
|
||||||
--arg windows_signature "$WINDOWS_SIGNATURE" \
|
|
||||||
--arg windows_url "$WINDOWS_URL" \
|
|
||||||
--arg darwin_arm_signature "$DARWIN_SIGNATURE" \
|
|
||||||
--arg darwin_arm_url "$DARWIN_URL" \
|
|
||||||
--arg darwin_amd_signature "$DARWIN_SIGNATURE" \
|
|
||||||
--arg darwin_amd_url "$DARWIN_URL" \
|
|
||||||
'.version = $version
|
|
||||||
| .pub_date = $pub_date
|
|
||||||
| .platforms["linux-x86_64"].signature = $linux_signature
|
|
||||||
| .platforms["linux-x86_64"].url = $linux_url
|
|
||||||
| .platforms["windows-x86_64"].signature = $windows_signature
|
|
||||||
| .platforms["windows-x86_64"].url = $windows_url
|
|
||||||
| .platforms["darwin-aarch64"].signature = $darwin_arm_signature
|
|
||||||
| .platforms["darwin-aarch64"].url = $darwin_arm_url
|
|
||||||
| .platforms["darwin-x86_64"].signature = $darwin_amd_signature
|
|
||||||
| .platforms["darwin-x86_64"].url = $darwin_amd_url' \
|
|
||||||
src-tauri/latest.json.template > latest.json
|
|
||||||
cat latest.json
|
|
||||||
- name: Sync temp to latest
|
|
||||||
if: ${{ needs.set-public-provider.outputs.public_provider == 'aws-s3' }}
|
|
||||||
run: |
|
|
||||||
aws s3 cp ./latest.json s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-nightly/latest.json
|
|
||||||
aws s3 sync s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-nightly/ s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/nightly/
|
|
||||||
env:
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }}
|
|
||||||
AWS_EC2_METADATA_DISABLED: "true"
|
|
||||||
|
|
||||||
noti-discord-nightly-and-update-url-readme:
|
|
||||||
needs: [
|
|
||||||
build-tauri-macos,
|
|
||||||
build-tauri-windows-x64,
|
|
||||||
build-tauri-linux-x64,
|
|
||||||
get-update-version,
|
|
||||||
set-public-provider,
|
|
||||||
sync-temp-to-latest
|
|
||||||
]
|
|
||||||
secrets: inherit
|
|
||||||
if: github.event_name == 'schedule'
|
|
||||||
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
|
|
||||||
with:
|
|
||||||
ref: refs/heads/dev
|
|
||||||
build_reason: Nightly
|
|
||||||
push_to_branch: dev
|
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
|
|
||||||
noti-discord-pre-release-and-update-url-readme:
|
|
||||||
needs: [
|
|
||||||
build-tauri-macos,
|
|
||||||
build-tauri-windows-x64,
|
|
||||||
build-tauri-linux-x64,
|
|
||||||
get-update-version,
|
|
||||||
set-public-provider,
|
|
||||||
sync-temp-to-latest
|
|
||||||
]
|
|
||||||
secrets: inherit
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
|
|
||||||
with:
|
|
||||||
ref: refs/heads/dev
|
|
||||||
build_reason: Pre-release
|
|
||||||
push_to_branch: dev
|
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
|
|
||||||
noti-discord-manual-and-update-url-readme:
|
|
||||||
needs: [
|
|
||||||
build-tauri-macos,
|
|
||||||
build-tauri-windows-x64,
|
|
||||||
build-tauri-linux-x64,
|
|
||||||
get-update-version,
|
|
||||||
set-public-provider,
|
|
||||||
sync-temp-to-latest
|
|
||||||
]
|
|
||||||
secrets: inherit
|
|
||||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.public_provider == 'aws-s3'
|
|
||||||
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
|
|
||||||
with:
|
|
||||||
ref: refs/heads/dev
|
|
||||||
build_reason: Manual
|
|
||||||
push_to_branch: dev
|
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
|
|
||||||
|
|
||||||
# comment-pr-build-url:
|
|
||||||
# needs: [
|
|
||||||
# build-tauri-macos,
|
|
||||||
# build-tauri-windows-x64,
|
|
||||||
# build-tauri-linux-x64,
|
|
||||||
# get-update-version,
|
|
||||||
# set-public-provider,
|
|
||||||
# sync-temp-to-latest
|
|
||||||
# ]
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# if: github.event_name == 'pull_request_review'
|
|
||||||
# steps:
|
|
||||||
# - name: Set up GitHub CLI
|
|
||||||
# run: |
|
|
||||||
# curl -sSL https://github.com/cli/cli/releases/download/v2.33.0/gh_2.33.0_linux_amd64.tar.gz | tar xz
|
|
||||||
# sudo cp gh_2.33.0_linux_amd64/bin/gh /usr/local/bin/
|
|
||||||
|
|
||||||
# - name: Comment build URL on PR
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# run: |
|
|
||||||
# PR_URL=${{ github.event.pull_request.html_url }}
|
|
||||||
# RUN_ID=${{ github.run_id }}
|
|
||||||
# COMMENT="This is the build for this pull request. You can download it from the Artifacts section here: [Build URL](https://github.com/${{ github.repository }}/actions/runs/${RUN_ID})."
|
|
||||||
# gh pr comment $PR_URL --body "$COMMENT"
|
|
||||||
131
.github/workflows/jan-electron-build.yml
vendored
131
.github/workflows/jan-electron-build.yml
vendored
@ -1,131 +0,0 @@
|
|||||||
name: Electron Builder - Tag
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: ["v[0-9]+.[0-9]+.[0-9]+"]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Job create Update app version based on latest release tag with build number and save to output
|
|
||||||
get-update-version:
|
|
||||||
uses: ./.github/workflows/template-get-update-version.yml
|
|
||||||
|
|
||||||
create-draft-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
|
||||||
outputs:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
version: ${{ steps.get_version.outputs.version }}
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Extract tag name without v prefix
|
|
||||||
id: get_version
|
|
||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV && echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
|
|
||||||
env:
|
|
||||||
GITHUB_REF: ${{ github.ref }}
|
|
||||||
- name: Create Draft Release
|
|
||||||
id: create_release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ github.ref_name }}
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
name: "${{ env.VERSION }}"
|
|
||||||
draft: true
|
|
||||||
prerelease: false
|
|
||||||
|
|
||||||
build-electron-macos:
|
|
||||||
uses: ./.github/workflows/template-electron-build-macos.yml
|
|
||||||
secrets: inherit
|
|
||||||
needs: [get-update-version]
|
|
||||||
with:
|
|
||||||
ref: ${{ github.ref }}
|
|
||||||
public_provider: github
|
|
||||||
beta: false
|
|
||||||
nightly: false
|
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
|
|
||||||
build-electron-windows-x64:
|
|
||||||
uses: ./.github/workflows/template-electron-build-windows-x64.yml
|
|
||||||
secrets: inherit
|
|
||||||
needs: [get-update-version]
|
|
||||||
with:
|
|
||||||
ref: ${{ github.ref }}
|
|
||||||
public_provider: github
|
|
||||||
beta: false
|
|
||||||
nightly: false
|
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
|
|
||||||
build-electron-linux-x64:
|
|
||||||
uses: ./.github/workflows/template-electron-build-linux-x64.yml
|
|
||||||
secrets: inherit
|
|
||||||
needs: [get-update-version]
|
|
||||||
with:
|
|
||||||
ref: ${{ github.ref }}
|
|
||||||
public_provider: github
|
|
||||||
beta: false
|
|
||||||
nightly: false
|
|
||||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
|
|
||||||
# build-tauri-macos:
|
|
||||||
# uses: ./.github/workflows/template-tauri-build-macos.yml
|
|
||||||
# secrets: inherit
|
|
||||||
# needs: [get-update-version, create-draft-release]
|
|
||||||
# with:
|
|
||||||
# ref: ${{ github.ref }}
|
|
||||||
# public_provider: github
|
|
||||||
# channel: stable
|
|
||||||
# new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
# upload_url: ${{ needs.create-draft-release.outputs.upload_url }}
|
|
||||||
|
|
||||||
# build-tauri-windows-x64:
|
|
||||||
# uses: ./.github/workflows/template-tauri-build-windows-x64.yml
|
|
||||||
# secrets: inherit
|
|
||||||
# needs: [get-update-version, create-draft-release]
|
|
||||||
# with:
|
|
||||||
# ref: ${{ github.ref }}
|
|
||||||
# public_provider: github
|
|
||||||
# channel: stable
|
|
||||||
# new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
# upload_url: ${{ needs.create-draft-release.outputs.upload_url }}
|
|
||||||
|
|
||||||
# build-tauri-linux-x64:
|
|
||||||
# uses: ./.github/workflows/template-tauri-build-linux-x64.yml
|
|
||||||
# secrets: inherit
|
|
||||||
# needs: [get-update-version, create-draft-release]
|
|
||||||
# with:
|
|
||||||
# ref: ${{ github.ref }}
|
|
||||||
# public_provider: github
|
|
||||||
# channel: stable
|
|
||||||
# new_version: ${{ needs.get-update-version.outputs.new_version }}
|
|
||||||
# upload_url: ${{ needs.create-draft-release.outputs.upload_url }}
|
|
||||||
|
|
||||||
update_release_draft:
|
|
||||||
needs: [
|
|
||||||
build-electron-windows-x64,
|
|
||||||
build-electron-linux-x64,
|
|
||||||
build-electron-macos,
|
|
||||||
build-tauri-windows-x64,
|
|
||||||
build-tauri-linux-x64,
|
|
||||||
build-tauri-macos
|
|
||||||
]
|
|
||||||
permissions:
|
|
||||||
# write permission is required to create a github release
|
|
||||||
contents: write
|
|
||||||
# write permission is required for autolabeler
|
|
||||||
# otherwise, read permission is required at least
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# (Optional) GitHub Enterprise requires GHE_HOST variable set
|
|
||||||
#- name: Set GHE_HOST
|
|
||||||
# run: |
|
|
||||||
# echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# Drafts your next Release notes as Pull Requests are merged into "master"
|
|
||||||
- uses: release-drafter/release-drafter@v5
|
|
||||||
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
|
||||||
# with:
|
|
||||||
# config-name: my-config.yml
|
|
||||||
# disable-autolabeler: true
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
435
.github/workflows/jan-electron-linter-and-test.yml
vendored
435
.github/workflows/jan-electron-linter-and-test.yml
vendored
@ -1,435 +0,0 @@
|
|||||||
name: Test - Linter & Playwright
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
paths:
|
|
||||||
- 'electron/**'
|
|
||||||
- .github/workflows/jan-electron-linter-and-test.yml
|
|
||||||
- 'web/**'
|
|
||||||
- 'joi/**'
|
|
||||||
- 'package.json'
|
|
||||||
- 'node_modules/**'
|
|
||||||
- 'yarn.lock'
|
|
||||||
- 'core/**'
|
|
||||||
- 'extensions/**'
|
|
||||||
- '!README.md'
|
|
||||||
- 'Makefile'
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
- release/**
|
|
||||||
paths:
|
|
||||||
- 'electron/**'
|
|
||||||
- .github/workflows/jan-electron-linter-and-test.yml
|
|
||||||
- 'web/**'
|
|
||||||
- 'joi/**'
|
|
||||||
- 'package.json'
|
|
||||||
- 'node_modules/**'
|
|
||||||
- 'yarn.lock'
|
|
||||||
- 'Makefile'
|
|
||||||
- 'extensions/**'
|
|
||||||
- 'core/**'
|
|
||||||
- 'src-tauri/**'
|
|
||||||
- 'web-app/**'
|
|
||||||
- '!README.md'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
base_branch_cov:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
continue-on-error: true
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
ref: ${{ github.base_ref }}
|
|
||||||
- name: Use Node.js 20.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
make config-yarn
|
|
||||||
yarn
|
|
||||||
yarn build:core
|
|
||||||
|
|
||||||
- name: Run test coverage
|
|
||||||
run: yarn test:coverage
|
|
||||||
|
|
||||||
- name: Upload code coverage for ref branch
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ref-lcov.info
|
|
||||||
path: ./coverage/lcov.info
|
|
||||||
|
|
||||||
test-on-macos:
|
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Installing node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Set IS_TEST environment variable
|
|
||||||
run: |
|
|
||||||
echo "IS_TEST=true" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: 'Cleanup cache'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
rm -rf ~/jan
|
|
||||||
make clean
|
|
||||||
|
|
||||||
- name: Get Commit Message for PR
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
run: |
|
|
||||||
echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}})" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Get Commit Message for push event
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
run: |
|
|
||||||
echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}})" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# - name: 'Config report portal'
|
|
||||||
# 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 macos" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
|
|
||||||
|
|
||||||
- name: Linter and test
|
|
||||||
run: |
|
|
||||||
make test
|
|
||||||
env:
|
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
|
|
||||||
|
|
||||||
test-on-macos-pr-target:
|
|
||||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Installing node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: 'Cleanup cache'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
rm -rf ~/jan
|
|
||||||
make clean
|
|
||||||
|
|
||||||
- name: Linter and test
|
|
||||||
run: |
|
|
||||||
make test
|
|
||||||
env:
|
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
|
|
||||||
|
|
||||||
test-on-windows:
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
antivirus-tools: ['mcafee', 'default-windows-security', 'bit-defender']
|
|
||||||
runs-on: windows-desktop-${{ matrix.antivirus-tools }}
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Installing node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Install tauri-driver dependencies
|
|
||||||
run: |
|
|
||||||
cargo install tauri-driver --locked
|
|
||||||
|
|
||||||
# Clean cache, continue on error
|
|
||||||
- name: 'Cleanup cache'
|
|
||||||
shell: powershell
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
$path = "$Env:APPDATA\jan"
|
|
||||||
if (Test-Path $path) {
|
|
||||||
Remove-Item "\\?\$path" -Recurse -Force
|
|
||||||
} else {
|
|
||||||
Write-Output "Folder does not exist."
|
|
||||||
}
|
|
||||||
make clean
|
|
||||||
|
|
||||||
- name: Get Commit Message for push event
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# - name: 'Config report portal'
|
|
||||||
# shell: bash
|
|
||||||
# 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 ${{ matrix.antivirus-tools }}" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
|
|
||||||
|
|
||||||
- name: Linter and test
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
make test
|
|
||||||
|
|
||||||
test-on-windows-pr:
|
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'workflow_dispatch'
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
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
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Install tauri-driver dependencies
|
|
||||||
run: |
|
|
||||||
cargo install tauri-driver --locked
|
|
||||||
|
|
||||||
# Clean cache, continue on error
|
|
||||||
- name: 'Cleanup cache'
|
|
||||||
shell: powershell
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
$path = "$Env:APPDATA\jan"
|
|
||||||
if (Test-Path $path) {
|
|
||||||
Remove-Item "\\?\$path" -Recurse -Force
|
|
||||||
} else {
|
|
||||||
Write-Output "Folder does not exist."
|
|
||||||
}
|
|
||||||
make clean
|
|
||||||
|
|
||||||
- name: Get Commit Message for PR
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# - name: 'Config report portal'
|
|
||||||
# shell: bash
|
|
||||||
# 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}}"
|
|
||||||
|
|
||||||
- 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
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
make test
|
|
||||||
|
|
||||||
test-on-windows-pr-target:
|
|
||||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Installing node
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Install tauri-driver dependencies
|
|
||||||
run: |
|
|
||||||
cargo install tauri-driver --locked
|
|
||||||
|
|
||||||
# Clean cache, continue on error
|
|
||||||
- name: 'Cleanup cache'
|
|
||||||
shell: powershell
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
$path = "$Env:APPDATA\jan"
|
|
||||||
if (Test-Path $path) {
|
|
||||||
Remove-Item "\\?\$path" -Recurse -Force
|
|
||||||
} else {
|
|
||||||
Write-Output "Folder does not exist."
|
|
||||||
}
|
|
||||||
make clean
|
|
||||||
|
|
||||||
- name: Linter and test
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
make test
|
|
||||||
|
|
||||||
test-on-ubuntu:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Installing node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
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'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
rm -rf ~/jan
|
|
||||||
make clean
|
|
||||||
|
|
||||||
- name: Get Commit Message for PR
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
run: |
|
|
||||||
echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Get Commit Message for push event
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
run: |
|
|
||||||
echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# - name: 'Config report portal'
|
|
||||||
# shell: bash
|
|
||||||
# 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 Linux" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
|
|
||||||
|
|
||||||
- name: Linter and test
|
|
||||||
run: |
|
|
||||||
export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
|
|
||||||
echo -e "Display ID: $DISPLAY"
|
|
||||||
make test
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: playwright-report
|
|
||||||
path: electron/playwright-report/
|
|
||||||
retention-days: 2
|
|
||||||
|
|
||||||
# coverage-check:
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# needs: base_branch_cov
|
|
||||||
# continue-on-error: true
|
|
||||||
# if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
||||||
# steps:
|
|
||||||
# - name: Getting the repo
|
|
||||||
# uses: actions/checkout@v3
|
|
||||||
# with:
|
|
||||||
# fetch-depth: 0
|
|
||||||
|
|
||||||
# - name: Installing node
|
|
||||||
# uses: actions/setup-node@v3
|
|
||||||
# with:
|
|
||||||
# node-version: 20
|
|
||||||
|
|
||||||
# - name: Install yarn
|
|
||||||
# run: npm install -g yarn
|
|
||||||
|
|
||||||
# - name: 'Cleanup cache'
|
|
||||||
# continue-on-error: true
|
|
||||||
# run: |
|
|
||||||
# rm -rf ~/jan
|
|
||||||
# make clean
|
|
||||||
|
|
||||||
# - name: Download code coverage report from base branch
|
|
||||||
# uses: actions/download-artifact@v4
|
|
||||||
# with:
|
|
||||||
# name: ref-lcov.info
|
|
||||||
|
|
||||||
# - name: Linter and test coverage
|
|
||||||
# run: |
|
|
||||||
# export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
|
|
||||||
# echo -e "Display ID: $DISPLAY"
|
|
||||||
# make lint
|
|
||||||
# yarn build:test
|
|
||||||
# yarn test:coverage
|
|
||||||
|
|
||||||
# - name: Generate Code Coverage report
|
|
||||||
# id: code-coverage
|
|
||||||
# uses: barecheck/code-coverage-action@v1
|
|
||||||
# with:
|
|
||||||
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# lcov-file: './coverage/lcov.info'
|
|
||||||
# base-lcov-file: './lcov.info'
|
|
||||||
# send-summary-comment: true
|
|
||||||
# show-annotations: 'warning'
|
|
||||||
|
|
||||||
test-on-ubuntu-pr-target:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Installing node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
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'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
rm -rf ~/jan
|
|
||||||
make clean
|
|
||||||
|
|
||||||
- name: Linter and test
|
|
||||||
run: |
|
|
||||||
export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
|
|
||||||
echo -e "Display ID: $DISPLAY"
|
|
||||||
make test
|
|
||||||
272
.github/workflows/jan-linter-and-test.yml
vendored
Normal file
272
.github/workflows/jan-linter-and-test.yml
vendored
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
name: Test - Linter & Playwright
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- .github/workflows/jan-linter-and-test.yml
|
||||||
|
- 'web/**'
|
||||||
|
- 'joi/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'node_modules/**'
|
||||||
|
- 'yarn.lock'
|
||||||
|
- 'core/**'
|
||||||
|
- 'extensions/**'
|
||||||
|
- '!README.md'
|
||||||
|
- 'Makefile'
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- release/**
|
||||||
|
paths:
|
||||||
|
- .github/workflows/jan-linter-and-test.yml
|
||||||
|
- 'web/**'
|
||||||
|
- 'joi/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'node_modules/**'
|
||||||
|
- 'yarn.lock'
|
||||||
|
- 'Makefile'
|
||||||
|
- 'extensions/**'
|
||||||
|
- 'core/**'
|
||||||
|
- 'src-tauri/**'
|
||||||
|
- 'web-app/**'
|
||||||
|
- '!README.md'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
base_branch_cov:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
- name: Use Node.js 20.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
make config-yarn
|
||||||
|
yarn
|
||||||
|
yarn build:core
|
||||||
|
|
||||||
|
- name: Run test coverage
|
||||||
|
run: yarn test:coverage
|
||||||
|
|
||||||
|
- name: Upload code coverage for ref branch
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ref-lcov.info
|
||||||
|
path: ./coverage/lcov.info
|
||||||
|
|
||||||
|
test-on-macos:
|
||||||
|
runs-on: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'macos-latest' || 'macos-selfhosted-12-arm64' }}
|
||||||
|
if: github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
|
steps:
|
||||||
|
- name: Getting the repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Installing node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Set IS_TEST environment variable
|
||||||
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
run: echo "IS_TEST=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: 'Cleanup cache'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
rm -rf ~/jan
|
||||||
|
make clean
|
||||||
|
|
||||||
|
- name: Linter and test
|
||||||
|
run: |
|
||||||
|
make test
|
||||||
|
env:
|
||||||
|
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
|
||||||
|
|
||||||
|
test-on-windows:
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
antivirus-tools: ['mcafee', 'default-windows-security', 'bit-defender']
|
||||||
|
runs-on: windows-desktop-${{ matrix.antivirus-tools }}
|
||||||
|
steps:
|
||||||
|
- name: Getting the repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Installing node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install tauri-driver dependencies
|
||||||
|
run: |
|
||||||
|
cargo install tauri-driver --locked
|
||||||
|
|
||||||
|
# Clean cache, continue on error
|
||||||
|
- name: 'Cleanup cache'
|
||||||
|
shell: powershell
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
$path = "$Env:APPDATA\jan"
|
||||||
|
if (Test-Path $path) {
|
||||||
|
Remove-Item "\\?\$path" -Recurse -Force
|
||||||
|
} else {
|
||||||
|
Write-Output "Folder does not exist."
|
||||||
|
}
|
||||||
|
make clean
|
||||||
|
|
||||||
|
- name: Linter and test
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
make test
|
||||||
|
|
||||||
|
test-on-windows-pr:
|
||||||
|
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'windows-latest' || 'WINDOWS-11' }}
|
||||||
|
steps:
|
||||||
|
- name: Getting the repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: install dependencies
|
||||||
|
run: |
|
||||||
|
choco install --yes --no-progress make
|
||||||
|
|
||||||
|
- name: Installing node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install tauri-driver dependencies
|
||||||
|
run: |
|
||||||
|
cargo install tauri-driver --locked
|
||||||
|
|
||||||
|
- name: 'Cleanup cache'
|
||||||
|
shell: powershell
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
$path = "$Env:APPDATA\jan"
|
||||||
|
if (Test-Path $path) {
|
||||||
|
Remove-Item "\\?\$path" -Recurse -Force
|
||||||
|
} else {
|
||||||
|
Write-Output "Folder does not exist."
|
||||||
|
}
|
||||||
|
make clean
|
||||||
|
|
||||||
|
- name: Install WebView2 Runtime (Bootstrapper)
|
||||||
|
shell: powershell
|
||||||
|
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
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
make test
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max-old-space-size=2048'
|
||||||
|
|
||||||
|
test-on-ubuntu:
|
||||||
|
runs-on: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'ubuntu-latest' || 'ubuntu-latest' }}
|
||||||
|
if: github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
|
steps:
|
||||||
|
- name: Getting the repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Installing node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
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'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
rm -rf ~/jan
|
||||||
|
make clean
|
||||||
|
|
||||||
|
- name: Linter and test
|
||||||
|
run: |
|
||||||
|
export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
|
||||||
|
echo -e "Display ID: $DISPLAY"
|
||||||
|
make test
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: electron/playwright-report/
|
||||||
|
retention-days: 2
|
||||||
|
|
||||||
|
# coverage-check:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# needs: base_branch_cov
|
||||||
|
# continue-on-error: true
|
||||||
|
# if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
|
# steps:
|
||||||
|
# - name: Getting the repo
|
||||||
|
# uses: actions/checkout@v3
|
||||||
|
# with:
|
||||||
|
# fetch-depth: 0
|
||||||
|
|
||||||
|
# - name: Installing node
|
||||||
|
# uses: actions/setup-node@v3
|
||||||
|
# with:
|
||||||
|
# node-version: 20
|
||||||
|
|
||||||
|
# - name: Install yarn
|
||||||
|
# run: npm install -g yarn
|
||||||
|
|
||||||
|
# - name: 'Cleanup cache'
|
||||||
|
# continue-on-error: true
|
||||||
|
# run: |
|
||||||
|
# rm -rf ~/jan
|
||||||
|
# make clean
|
||||||
|
|
||||||
|
# - name: Download code coverage report from base branch
|
||||||
|
# uses: actions/download-artifact@v4
|
||||||
|
# with:
|
||||||
|
# name: ref-lcov.info
|
||||||
|
|
||||||
|
# - name: Linter and test coverage
|
||||||
|
# run: |
|
||||||
|
# export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
|
||||||
|
# echo -e "Display ID: $DISPLAY"
|
||||||
|
# make lint
|
||||||
|
# yarn build:test
|
||||||
|
# yarn test:coverage
|
||||||
|
|
||||||
|
# - name: Generate Code Coverage report
|
||||||
|
# id: code-coverage
|
||||||
|
# uses: barecheck/code-coverage-action@v1
|
||||||
|
# with:
|
||||||
|
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# lcov-file: './coverage/lcov.info'
|
||||||
|
# base-lcov-file: './lcov.info'
|
||||||
|
# send-summary-comment: true
|
||||||
|
# show-annotations: 'warning'
|
||||||
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
|
||||||
|
|||||||
@ -1,188 +0,0 @@
|
|||||||
name: build-linux-x64
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
ref:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: 'refs/heads/main'
|
|
||||||
public_provider:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: none
|
|
||||||
description: 'none: build only, github: build and publish to github, aws s3: build and publish to aws s3'
|
|
||||||
new_version:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
aws_s3_prefix:
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: '/latest/'
|
|
||||||
beta:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
nightly:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
cortex_api_port:
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: null
|
|
||||||
secrets:
|
|
||||||
DELTA_AWS_S3_BUCKET_NAME:
|
|
||||||
required: false
|
|
||||||
DELTA_AWS_ACCESS_KEY_ID:
|
|
||||||
required: false
|
|
||||||
DELTA_AWS_SECRET_ACCESS_KEY:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-linux-x64:
|
|
||||||
if: inputs.public_provider == 'github' || inputs.public_provider == 'none'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: production
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.ref }}
|
|
||||||
|
|
||||||
- name: Replace Icons for Beta Build
|
|
||||||
if: inputs.beta == true && inputs.nightly != true
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
rm -rf electron/icons/*
|
|
||||||
|
|
||||||
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
|
|
||||||
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
|
|
||||||
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
|
|
||||||
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
|
|
||||||
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
|
|
||||||
|
|
||||||
- name: Replace Icons for Nightly Build
|
|
||||||
if: inputs.nightly == true && inputs.beta != true
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
rm -rf electron/icons/*
|
|
||||||
|
|
||||||
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
|
|
||||||
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
|
|
||||||
cp electron/icons_dev/jan-nightly.png electron/icons/icon.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
|
|
||||||
|
|
||||||
|
|
||||||
- name: Installing node
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Install jq
|
|
||||||
uses: dcarbone/install-jq-action@v2.0.1
|
|
||||||
|
|
||||||
- name: Update app version base public_provider
|
|
||||||
if: inputs.public_provider != 'github'
|
|
||||||
run: |
|
|
||||||
echo "Version: ${{ inputs.new_version }}"
|
|
||||||
# Update the version in electron/package.json
|
|
||||||
jq --arg version "${{ inputs.new_version }}" '.version = $version' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json web/package.json
|
|
||||||
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/nightly", "channel": "latest"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-nightly", "channel": "latest"}]' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
cat electron/package.json
|
|
||||||
chmod +x .github/scripts/rename-app.sh
|
|
||||||
.github/scripts/rename-app.sh ./electron/package.json nightly
|
|
||||||
chmod +x .github/scripts/rename-workspace.sh
|
|
||||||
.github/scripts/rename-workspace.sh ./package.json nightly
|
|
||||||
echo "------------------------"
|
|
||||||
cat ./electron/package.json
|
|
||||||
echo "------------------------"
|
|
||||||
|
|
||||||
- name: Change App Name for beta version
|
|
||||||
if: inputs.beta == true
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
chmod +x .github/scripts/rename-app.sh
|
|
||||||
.github/scripts/rename-app.sh ./electron/package.json beta
|
|
||||||
chmod +x .github/scripts/rename-workspace.sh
|
|
||||||
.github/scripts/rename-workspace.sh ./package.json beta
|
|
||||||
echo "------------------------"
|
|
||||||
cat ./electron/package.json
|
|
||||||
echo "------------------------"
|
|
||||||
cat ./package.json
|
|
||||||
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
cat electron/package.json
|
|
||||||
|
|
||||||
- name: Update app version base on tag
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github'
|
|
||||||
run: |
|
|
||||||
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
jq --arg version "${VERSION_TAG#v}" '.version = $version' web/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json web/package.json
|
|
||||||
env:
|
|
||||||
VERSION_TAG: ${{ inputs.new_version }}
|
|
||||||
|
|
||||||
- name: Build and publish app to aws s3 r2 or github artifactory
|
|
||||||
if: inputs.public_provider != 'github'
|
|
||||||
run: |
|
|
||||||
# check public_provider is true or not
|
|
||||||
echo "public_provider is ${{ inputs.public_provider }}"
|
|
||||||
if [ "${{ inputs.public_provider }}" == "none" ]; then
|
|
||||||
make build
|
|
||||||
else
|
|
||||||
make build-and-publish
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_EC2_METADATA_DISABLED: 'true'
|
|
||||||
AWS_MAX_ATTEMPTS: '5'
|
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
|
||||||
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
|
|
||||||
|
|
||||||
- name: Build and publish app to github
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
|
|
||||||
run: |
|
|
||||||
make build-and-publish
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
|
||||||
|
|
||||||
- name: Build and publish app to github
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true
|
|
||||||
run: |
|
|
||||||
make build-and-publish
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_EC2_METADATA_DISABLED: 'true'
|
|
||||||
AWS_MAX_ATTEMPTS: '5'
|
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
|
||||||
|
|
||||||
- name: Upload Artifact .deb file
|
|
||||||
if: inputs.public_provider != 'github'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: jan-electron-linux-amd64-${{ inputs.new_version }}-deb
|
|
||||||
path: ./electron/dist/*.deb
|
|
||||||
|
|
||||||
- name: Upload Artifact .AppImage file
|
|
||||||
if: inputs.public_provider != 'github'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: jan-electron-linux-amd64-${{ inputs.new_version }}-AppImage
|
|
||||||
path: ./electron/dist/*.AppImage
|
|
||||||
234
.github/workflows/template-electron-build-macos.yml
vendored
234
.github/workflows/template-electron-build-macos.yml
vendored
@ -1,234 +0,0 @@
|
|||||||
name: build-macos
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
ref:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: 'refs/heads/main'
|
|
||||||
public_provider:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: none
|
|
||||||
description: 'none: build only, github: build and publish to github, aws s3: build and publish to aws s3'
|
|
||||||
new_version:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
aws_s3_prefix:
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: '/latest/'
|
|
||||||
beta:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
nightly:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
cortex_api_port:
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: null
|
|
||||||
secrets:
|
|
||||||
DELTA_AWS_S3_BUCKET_NAME:
|
|
||||||
required: false
|
|
||||||
DELTA_AWS_ACCESS_KEY_ID:
|
|
||||||
required: false
|
|
||||||
DELTA_AWS_SECRET_ACCESS_KEY:
|
|
||||||
required: false
|
|
||||||
CODE_SIGN_P12_BASE64:
|
|
||||||
required: false
|
|
||||||
CODE_SIGN_P12_PASSWORD:
|
|
||||||
required: false
|
|
||||||
APPLE_ID:
|
|
||||||
required: false
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD:
|
|
||||||
required: false
|
|
||||||
DEVELOPER_ID:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-macos:
|
|
||||||
if: inputs.public_provider == 'github' || inputs.public_provider == 'none'
|
|
||||||
runs-on: macos-latest
|
|
||||||
environment: production
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.ref }}
|
|
||||||
|
|
||||||
- name: Replace Icons for Beta Build
|
|
||||||
if: inputs.beta == true && inputs.nightly != true
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
rm -rf electron/icons/*
|
|
||||||
|
|
||||||
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
|
|
||||||
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
|
|
||||||
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
|
|
||||||
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
|
|
||||||
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
|
|
||||||
|
|
||||||
- name: Replace Icons for Nightly Build
|
|
||||||
if: inputs.nightly == true && inputs.beta != true
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
rm -rf electron/icons/*
|
|
||||||
|
|
||||||
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
|
|
||||||
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
|
|
||||||
cp electron/icons_dev/jan-nightly.png electron/icons/icon.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
|
|
||||||
|
|
||||||
- name: Installing node
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Install jq
|
|
||||||
uses: dcarbone/install-jq-action@v2.0.1
|
|
||||||
|
|
||||||
- name: Update app version based on latest release tag with build number
|
|
||||||
if: inputs.public_provider != 'github'
|
|
||||||
run: |
|
|
||||||
echo "Version: ${{ inputs.new_version }}"
|
|
||||||
# Update the version in electron/package.json
|
|
||||||
jq --arg version "${{ inputs.new_version }}" '.version = $version' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
|
|
||||||
jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json web/package.json
|
|
||||||
|
|
||||||
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/nightly", "channel": "latest"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-nightly", "channel": "latest"}]' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
|
|
||||||
jq --arg teamid "${{ secrets.APPLE_TEAM_ID }}" '.build.mac.notarize.teamId = $teamid' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
|
|
||||||
# cat electron/package.json
|
|
||||||
chmod +x .github/scripts/rename-app.sh
|
|
||||||
.github/scripts/rename-app.sh ./electron/package.json nightly
|
|
||||||
chmod +x .github/scripts/rename-workspace.sh
|
|
||||||
.github/scripts/rename-workspace.sh ./package.json nightly
|
|
||||||
echo "------------------------"
|
|
||||||
cat ./electron/package.json
|
|
||||||
echo "------------------------"
|
|
||||||
|
|
||||||
- name: Change App Name for beta version
|
|
||||||
if: inputs.beta == true
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
chmod +x .github/scripts/rename-app.sh
|
|
||||||
.github/scripts/rename-app.sh ./electron/package.json beta
|
|
||||||
chmod +x .github/scripts/rename-workspace.sh
|
|
||||||
.github/scripts/rename-workspace.sh ./package.json beta
|
|
||||||
echo "------------------------"
|
|
||||||
cat ./electron/package.json
|
|
||||||
echo "------------------------"
|
|
||||||
cat ./package.json
|
|
||||||
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
cat electron/package.json
|
|
||||||
|
|
||||||
- name: Update app version base on tag
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github'
|
|
||||||
run: |
|
|
||||||
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
jq --arg version "${VERSION_TAG#v}" '.version = $version' web/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json web/package.json
|
|
||||||
jq --arg teamid "${{ secrets.APPLE_TEAM_ID }}" '.build.mac.notarize.teamId = $teamid' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
cat electron/package.json
|
|
||||||
env:
|
|
||||||
VERSION_TAG: ${{ inputs.new_version }}
|
|
||||||
|
|
||||||
- name: Get Cer for code signing
|
|
||||||
run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
|
||||||
|
|
||||||
- uses: apple-actions/import-codesign-certs@v2
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
p12-file-base64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
|
||||||
p12-password: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Build and publish app to aws s3 r2 or github artifactory
|
|
||||||
if: inputs.public_provider != 'github'
|
|
||||||
run: |
|
|
||||||
# check public_provider is true or not
|
|
||||||
echo "public_provider is ${{ inputs.public_provider }}"
|
|
||||||
if [ "${{ inputs.public_provider }}" == "none" ]; then
|
|
||||||
make build
|
|
||||||
else
|
|
||||||
make build-and-publish
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CSC_LINK: '/tmp/codesign.p12'
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
|
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: 'true'
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APP_PATH: '.'
|
|
||||||
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_DEFAULT_REGION: auto
|
|
||||||
AWS_EC2_METADATA_DISABLED: 'true'
|
|
||||||
AWS_MAX_ATTEMPTS: '5'
|
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
|
||||||
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
|
|
||||||
|
|
||||||
- name: Build and publish app to github
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
|
|
||||||
run: |
|
|
||||||
make build-and-publish
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CSC_LINK: '/tmp/codesign.p12'
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
|
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: 'true'
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APP_PATH: '.'
|
|
||||||
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
|
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
|
||||||
|
|
||||||
- name: Build and publish app to github
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true
|
|
||||||
run: |
|
|
||||||
make build-and-publish
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CSC_LINK: '/tmp/codesign.p12'
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
|
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: 'true'
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APP_PATH: '.'
|
|
||||||
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_DEFAULT_REGION: auto
|
|
||||||
AWS_EC2_METADATA_DISABLED: 'true'
|
|
||||||
AWS_MAX_ATTEMPTS: '5'
|
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
if: inputs.public_provider != 'github'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: jan-electron-mac-universal-${{ inputs.new_version }}
|
|
||||||
path: ./electron/dist/*.dmg
|
|
||||||
@ -1,230 +0,0 @@
|
|||||||
name: build-windows-x64
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
ref:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: 'refs/heads/main'
|
|
||||||
public_provider:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: none
|
|
||||||
description: 'none: build only, github: build and publish to github, aws s3: build and publish to aws s3'
|
|
||||||
new_version:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
aws_s3_prefix:
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: '/latest/'
|
|
||||||
beta:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
nightly:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
cortex_api_port:
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: null
|
|
||||||
secrets:
|
|
||||||
DELTA_AWS_S3_BUCKET_NAME:
|
|
||||||
required: false
|
|
||||||
DELTA_AWS_ACCESS_KEY_ID:
|
|
||||||
required: false
|
|
||||||
DELTA_AWS_SECRET_ACCESS_KEY:
|
|
||||||
required: false
|
|
||||||
AZURE_KEY_VAULT_URI:
|
|
||||||
required: false
|
|
||||||
AZURE_CLIENT_ID:
|
|
||||||
required: false
|
|
||||||
AZURE_TENANT_ID:
|
|
||||||
required: false
|
|
||||||
AZURE_CLIENT_SECRET:
|
|
||||||
required: false
|
|
||||||
AZURE_CERT_NAME:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-windows-x64:
|
|
||||||
if: inputs.public_provider == 'github' || inputs.public_provider == 'none'
|
|
||||||
runs-on: windows-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Getting the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.ref }}
|
|
||||||
|
|
||||||
- name: Replace Icons for Beta Build
|
|
||||||
if: inputs.beta == true && inputs.nightly != true
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
rm -rf electron/icons/*
|
|
||||||
|
|
||||||
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
|
|
||||||
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
|
|
||||||
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
|
|
||||||
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
|
|
||||||
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
|
|
||||||
|
|
||||||
- name: Replace Icons for Nightly Build
|
|
||||||
if: inputs.nightly == true && inputs.beta != true
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
rm -rf electron/icons/*
|
|
||||||
|
|
||||||
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
|
|
||||||
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
|
|
||||||
cp electron/icons_dev/jan-nightly.png electron/icons/icon.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
|
|
||||||
|
|
||||||
- name: Installing node
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Install jq
|
|
||||||
uses: dcarbone/install-jq-action@v2.0.1
|
|
||||||
|
|
||||||
- name: Update app version base on tag
|
|
||||||
if: inputs.public_provider != 'github'
|
|
||||||
id: version_update
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "Version: ${{ inputs.new_version }}"
|
|
||||||
# Update the version in electron/package.json
|
|
||||||
jq --arg version "${{ inputs.new_version }}" '.version = $version' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
|
|
||||||
jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json web/package.json
|
|
||||||
|
|
||||||
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/nightly", "channel": "latest"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-nightly", "channel": "latest"}]' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
|
|
||||||
jq '.build.win.sign = "./sign.js"' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
cat electron/package.json
|
|
||||||
|
|
||||||
chmod +x .github/scripts/rename-app.sh
|
|
||||||
.github/scripts/rename-app.sh ./electron/package.json nightly
|
|
||||||
chmod +x .github/scripts/rename-workspace.sh
|
|
||||||
.github/scripts/rename-workspace.sh ./package.json nightly
|
|
||||||
chmod +x .github/scripts/rename-uninstaller.sh
|
|
||||||
.github/scripts/rename-uninstaller.sh nightly
|
|
||||||
echo "------------------------"
|
|
||||||
cat ./electron/package.json
|
|
||||||
echo "------------------------"
|
|
||||||
cat ./package.json
|
|
||||||
echo "------------------------"
|
|
||||||
|
|
||||||
- name: Change App Name for beta version
|
|
||||||
if: inputs.beta == true
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
chmod +x .github/scripts/rename-app.sh
|
|
||||||
.github/scripts/rename-app.sh ./electron/package.json beta
|
|
||||||
chmod +x .github/scripts/rename-workspace.sh
|
|
||||||
.github/scripts/rename-workspace.sh ./package.json beta
|
|
||||||
chmod +x .github/scripts/rename-uninstaller.sh
|
|
||||||
.github/scripts/rename-uninstaller.sh beta
|
|
||||||
echo "------------------------"
|
|
||||||
cat ./electron/package.json
|
|
||||||
echo "------------------------"
|
|
||||||
cat ./package.json
|
|
||||||
echo "------------------------"
|
|
||||||
cat ./electron/scripts/uninstaller.nsh
|
|
||||||
jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
cat electron/package.json
|
|
||||||
|
|
||||||
- name: Update app version base on tag
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
jq --arg version "${VERSION_TAG#v}" '.version = $version' web/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json web/package.json
|
|
||||||
jq '.build.win.sign = "./sign.js"' electron/package.json > /tmp/package.json
|
|
||||||
mv /tmp/package.json electron/package.json
|
|
||||||
env:
|
|
||||||
VERSION_TAG: ${{ inputs.new_version }}
|
|
||||||
|
|
||||||
- name: Install AzureSignTool
|
|
||||||
run: |
|
|
||||||
dotnet tool install --global AzureSignTool
|
|
||||||
|
|
||||||
- name: Build and publish app to aws s3 r2 or github artifactory
|
|
||||||
shell: bash
|
|
||||||
if: inputs.public_provider != 'github'
|
|
||||||
run: |
|
|
||||||
# check public_provider is true or not
|
|
||||||
echo "public_provider is ${{ inputs.public_provider }}"
|
|
||||||
if [ "${{ inputs.public_provider }}" == "none" ]; then
|
|
||||||
make build
|
|
||||||
else
|
|
||||||
make build-and-publish
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
|
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
|
||||||
AZURE_CERT_NAME: homebrewltd
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_DEFAULT_REGION: auto
|
|
||||||
AWS_EC2_METADATA_DISABLED: 'true'
|
|
||||||
AWS_MAX_ATTEMPTS: '5'
|
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
|
||||||
CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
|
|
||||||
|
|
||||||
- name: Build app and publish app to github
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
|
|
||||||
run: |
|
|
||||||
make build-and-publish
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
|
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
|
||||||
AZURE_CERT_NAME: homebrewltd
|
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
|
||||||
|
|
||||||
- name: Build app and publish app to github
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true
|
|
||||||
run: |
|
|
||||||
make build-and-publish
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_DEFAULT_REGION: auto
|
|
||||||
AWS_EC2_METADATA_DISABLED: 'true'
|
|
||||||
AWS_MAX_ATTEMPTS: '5'
|
|
||||||
AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
|
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
|
||||||
# AZURE_CERT_NAME: ${{ secrets.AZURE_CERT_NAME }}
|
|
||||||
AZURE_CERT_NAME: homebrewltd
|
|
||||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
|
||||||
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
if: inputs.public_provider != 'github'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: jan-electron-win-x64-${{ inputs.new_version }}
|
|
||||||
path: ./electron/dist/*.exe
|
|
||||||
@ -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:
|
||||||
@ -151,6 +150,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Build app
|
- name: Build app
|
||||||
run: |
|
run: |
|
||||||
|
# Pin linuxdeploy version to prevent @tauri-apps/cli-linux-x64-gnu from pulling in an outdated version
|
||||||
|
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"
|
||||||
|
|
||||||
make build-tauri
|
make build-tauri
|
||||||
# Copy engines and bun to appimage
|
# Copy engines and bun to appimage
|
||||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O ./appimagetool
|
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O ./appimagetool
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -23,20 +23,20 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
|
|||||||
133
core/src/browser/models/manager.test.ts
Normal file
133
core/src/browser/models/manager.test.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { ModelManager } from './manager'
|
||||||
|
import { Model, ModelEvent } from '../../types'
|
||||||
|
import { events } from '../events'
|
||||||
|
|
||||||
|
jest.mock('../events', () => ({
|
||||||
|
events: {
|
||||||
|
emit: jest.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
Object.defineProperty(global, 'window', {
|
||||||
|
value: {
|
||||||
|
core: {},
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ModelManager', () => {
|
||||||
|
let modelManager: ModelManager
|
||||||
|
let mockModel: Model
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
;(global.window as any).core = {}
|
||||||
|
modelManager = new ModelManager()
|
||||||
|
mockModel = {
|
||||||
|
id: 'test-model-1',
|
||||||
|
name: 'Test Model',
|
||||||
|
version: '1.0.0',
|
||||||
|
} as Model
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should set itself on window.core.modelManager when window exists', () => {
|
||||||
|
expect((global.window as any).core.modelManager).toBe(modelManager)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('register', () => {
|
||||||
|
it('should register a new model', () => {
|
||||||
|
modelManager.register(mockModel)
|
||||||
|
|
||||||
|
expect(modelManager.models.has('test-model-1')).toBe(true)
|
||||||
|
expect(modelManager.models.get('test-model-1')).toEqual(mockModel)
|
||||||
|
expect(events.emit).toHaveBeenCalledWith(ModelEvent.OnModelsUpdate, {})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should merge existing model with new model data', () => {
|
||||||
|
const existingModel: Model = {
|
||||||
|
id: 'test-model-1',
|
||||||
|
name: 'Existing Model',
|
||||||
|
description: 'Existing description',
|
||||||
|
} as Model
|
||||||
|
|
||||||
|
const updatedModel: Model = {
|
||||||
|
id: 'test-model-1',
|
||||||
|
name: 'Updated Model',
|
||||||
|
version: '2.0.0',
|
||||||
|
} as Model
|
||||||
|
|
||||||
|
modelManager.register(existingModel)
|
||||||
|
modelManager.register(updatedModel)
|
||||||
|
|
||||||
|
const registeredModel = modelManager.models.get('test-model-1')
|
||||||
|
expect(registeredModel).toEqual({
|
||||||
|
id: 'test-model-1',
|
||||||
|
name: 'Existing Model',
|
||||||
|
description: 'Existing description',
|
||||||
|
version: '2.0.0',
|
||||||
|
})
|
||||||
|
expect(events.emit).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('get', () => {
|
||||||
|
it('should retrieve a registered model by id', () => {
|
||||||
|
modelManager.register(mockModel)
|
||||||
|
|
||||||
|
const retrievedModel = modelManager.get('test-model-1')
|
||||||
|
expect(retrievedModel).toEqual(mockModel)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined for non-existent model', () => {
|
||||||
|
const retrievedModel = modelManager.get('non-existent-model')
|
||||||
|
expect(retrievedModel).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return correctly typed model', () => {
|
||||||
|
modelManager.register(mockModel)
|
||||||
|
|
||||||
|
const retrievedModel = modelManager.get<Model>('test-model-1')
|
||||||
|
expect(retrievedModel?.id).toBe('test-model-1')
|
||||||
|
expect(retrievedModel?.name).toBe('Test Model')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('instance', () => {
|
||||||
|
it('should create a new instance when none exists on window.core', () => {
|
||||||
|
;(global.window as any).core = {}
|
||||||
|
|
||||||
|
const instance = ModelManager.instance()
|
||||||
|
expect(instance).toBeInstanceOf(ModelManager)
|
||||||
|
expect((global.window as any).core.modelManager).toBe(instance)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return existing instance when it exists on window.core', () => {
|
||||||
|
const existingManager = new ModelManager()
|
||||||
|
;(global.window as any).core.modelManager = existingManager
|
||||||
|
|
||||||
|
const instance = ModelManager.instance()
|
||||||
|
expect(instance).toBe(existingManager)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('models property', () => {
|
||||||
|
it('should initialize with empty Map', () => {
|
||||||
|
expect(modelManager.models).toBeInstanceOf(Map)
|
||||||
|
expect(modelManager.models.size).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should maintain multiple models', () => {
|
||||||
|
const model1: Model = { id: 'model-1', name: 'Model 1' } as Model
|
||||||
|
const model2: Model = { id: 'model-2', name: 'Model 2' } as Model
|
||||||
|
|
||||||
|
modelManager.register(model1)
|
||||||
|
modelManager.register(model2)
|
||||||
|
|
||||||
|
expect(modelManager.models.size).toBe(2)
|
||||||
|
expect(modelManager.models.get('model-1')).toEqual(model1)
|
||||||
|
expect(modelManager.models.get('model-2')).toEqual(model2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -152,6 +152,33 @@ describe('validationRules', () => {
|
|||||||
expect(validationRules.text_model('true')).toBe(false)
|
expect(validationRules.text_model('true')).toBe(false)
|
||||||
expect(validationRules.text_model(1)).toBe(false)
|
expect(validationRules.text_model(1)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should validate repeat_last_n correctly', () => {
|
||||||
|
expect(validationRules.repeat_last_n(5)).toBe(true)
|
||||||
|
expect(validationRules.repeat_last_n(-5)).toBe(true)
|
||||||
|
expect(validationRules.repeat_last_n(0)).toBe(true)
|
||||||
|
expect(validationRules.repeat_last_n(1.5)).toBe(true)
|
||||||
|
expect(validationRules.repeat_last_n('5')).toBe(false)
|
||||||
|
expect(validationRules.repeat_last_n(null)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should validate repeat_penalty correctly', () => {
|
||||||
|
expect(validationRules.repeat_penalty(1.1)).toBe(true)
|
||||||
|
expect(validationRules.repeat_penalty(0.9)).toBe(true)
|
||||||
|
expect(validationRules.repeat_penalty(0)).toBe(true)
|
||||||
|
expect(validationRules.repeat_penalty(-1)).toBe(true)
|
||||||
|
expect(validationRules.repeat_penalty('1.1')).toBe(false)
|
||||||
|
expect(validationRules.repeat_penalty(null)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should validate min_p correctly', () => {
|
||||||
|
expect(validationRules.min_p(0.1)).toBe(true)
|
||||||
|
expect(validationRules.min_p(0)).toBe(true)
|
||||||
|
expect(validationRules.min_p(-0.1)).toBe(true)
|
||||||
|
expect(validationRules.min_p(1.5)).toBe(true)
|
||||||
|
expect(validationRules.min_p('0.1')).toBe(false)
|
||||||
|
expect(validationRules.min_p(null)).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should normalize invalid values for keys not listed in validationRules', () => {
|
it('should normalize invalid values for keys not listed in validationRules', () => {
|
||||||
@ -192,18 +219,125 @@ describe('normalizeValue', () => {
|
|||||||
expect(normalizeValue('cpu_threads', '4')).toBe(4)
|
expect(normalizeValue('cpu_threads', '4')).toBe(4)
|
||||||
expect(normalizeValue('cpu_threads', 0)).toBe(0)
|
expect(normalizeValue('cpu_threads', 0)).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should handle edge cases for normalization', () => {
|
||||||
|
expect(normalizeValue('ctx_len', -5.7)).toBe(-6)
|
||||||
|
expect(normalizeValue('token_limit', 'abc')).toBeNaN()
|
||||||
|
expect(normalizeValue('max_tokens', null)).toBe(0)
|
||||||
|
expect(normalizeValue('ngl', undefined)).toBeNaN()
|
||||||
|
expect(normalizeValue('n_parallel', Infinity)).toBe(Infinity)
|
||||||
|
expect(normalizeValue('cpu_threads', -Infinity)).toBe(-Infinity)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not normalize non-integer parameters', () => {
|
||||||
|
expect(normalizeValue('temperature', 1.5)).toBe(1.5)
|
||||||
|
expect(normalizeValue('top_p', 0.9)).toBe(0.9)
|
||||||
|
expect(normalizeValue('stream', true)).toBe(true)
|
||||||
|
expect(normalizeValue('prompt_template', 'template')).toBe('template')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle invalid values correctly by falling back to originParams', () => {
|
describe('extractInferenceParams', () => {
|
||||||
const modelParams = { temperature: 'invalid', token_limit: -1 }
|
it('should handle invalid values correctly by falling back to originParams', () => {
|
||||||
const originParams = { temperature: 0.5, token_limit: 100 }
|
const modelParams = { temperature: 'invalid', token_limit: -1 }
|
||||||
expect(extractInferenceParams(modelParams as any, originParams)).toEqual(originParams)
|
const originParams = { temperature: 0.5, token_limit: 100 }
|
||||||
|
expect(extractInferenceParams(modelParams as any, originParams)).toEqual(originParams)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an empty object when no modelParams are provided', () => {
|
||||||
|
expect(extractInferenceParams()).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should extract and normalize valid inference parameters', () => {
|
||||||
|
const modelParams = {
|
||||||
|
temperature: 1.5,
|
||||||
|
token_limit: 100.7,
|
||||||
|
top_p: 0.9,
|
||||||
|
stream: true,
|
||||||
|
max_tokens: 50.3,
|
||||||
|
invalid_param: 'should_be_ignored',
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = extractInferenceParams(modelParams as any)
|
||||||
|
expect(result).toEqual({
|
||||||
|
temperature: 1.5,
|
||||||
|
token_limit: 100,
|
||||||
|
top_p: 0.9,
|
||||||
|
stream: true,
|
||||||
|
max_tokens: 50,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle parameters without validation rules', () => {
|
||||||
|
const modelParams = { engine: 'llama' }
|
||||||
|
const result = extractInferenceParams(modelParams as any)
|
||||||
|
expect(result).toEqual({ engine: 'llama' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should skip invalid values when no origin params provided', () => {
|
||||||
|
const modelParams = { temperature: 'invalid', top_p: 0.8 }
|
||||||
|
const result = extractInferenceParams(modelParams as any)
|
||||||
|
expect(result).toEqual({ top_p: 0.8 })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an empty object when no modelParams are provided', () => {
|
describe('extractModelLoadParams', () => {
|
||||||
expect(extractModelLoadParams()).toEqual({})
|
it('should return an empty object when no modelParams are provided', () => {
|
||||||
})
|
expect(extractModelLoadParams()).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
it('should return an empty object when no modelParams are provided', () => {
|
it('should extract and normalize valid model load parameters', () => {
|
||||||
expect(extractInferenceParams()).toEqual({})
|
const modelParams = {
|
||||||
|
ctx_len: 2048.5,
|
||||||
|
ngl: 12.7,
|
||||||
|
embedding: true,
|
||||||
|
n_parallel: 4.2,
|
||||||
|
cpu_threads: 8.9,
|
||||||
|
prompt_template: 'template',
|
||||||
|
llama_model_path: '/path/to/model',
|
||||||
|
vision_model: false,
|
||||||
|
invalid_param: 'should_be_ignored',
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = extractModelLoadParams(modelParams as any)
|
||||||
|
expect(result).toEqual({
|
||||||
|
ctx_len: 2048,
|
||||||
|
ngl: 12,
|
||||||
|
embedding: true,
|
||||||
|
n_parallel: 4,
|
||||||
|
cpu_threads: 8,
|
||||||
|
prompt_template: 'template',
|
||||||
|
llama_model_path: '/path/to/model',
|
||||||
|
vision_model: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle parameters without validation rules', () => {
|
||||||
|
const modelParams = {
|
||||||
|
engine: 'llama',
|
||||||
|
pre_prompt: 'System:',
|
||||||
|
system_prompt: 'You are helpful',
|
||||||
|
model_path: '/path',
|
||||||
|
}
|
||||||
|
const result = extractModelLoadParams(modelParams as any)
|
||||||
|
expect(result).toEqual({
|
||||||
|
engine: 'llama',
|
||||||
|
pre_prompt: 'System:',
|
||||||
|
system_prompt: 'You are helpful',
|
||||||
|
model_path: '/path',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fall back to origin params for invalid values', () => {
|
||||||
|
const modelParams = { ctx_len: -1, ngl: 'invalid' }
|
||||||
|
const originParams = { ctx_len: 2048, ngl: 12 }
|
||||||
|
const result = extractModelLoadParams(modelParams as any, originParams)
|
||||||
|
expect(result).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should skip invalid values when no origin params provided', () => {
|
||||||
|
const modelParams = { ctx_len: -1, embedding: true }
|
||||||
|
const result = extractModelLoadParams(modelParams as any)
|
||||||
|
expect(result).toEqual({ embedding: true })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -8,18 +8,19 @@ 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',
|
||||||
|
|
||||||
ctx_len: (value: any) => Number.isInteger(value) && value >= 0,
|
ctx_len: (value: any) => Number.isInteger(value) && value >= 0,
|
||||||
ngl: (value: any) => Number.isInteger(value),
|
ngl: (value: any) => Number.isInteger(value) && value >= 0,
|
||||||
embedding: (value: any) => typeof value === 'boolean',
|
embedding: (value: any) => typeof value === 'boolean',
|
||||||
n_parallel: (value: any) => Number.isInteger(value) && value >= 0,
|
n_parallel: (value: any) => Number.isInteger(value) && value >= 0,
|
||||||
cpu_threads: (value: any) => Number.isInteger(value) && value >= 0,
|
cpu_threads: (value: any) => Number.isInteger(value) && value >= 0,
|
||||||
@ -49,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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/
|
||||||
model_A.yaml
|
/files/
|
||||||
/settings
|
/logs/
|
||||||
settings.json
|
app.log
|
||||||
/@janhq
|
/models/
|
||||||
/extension_A_Settings
|
/huggingface.co/
|
||||||
settings.json
|
/Model_Provider_A/
|
||||||
/themes
|
/Model_A
|
||||||
/dark-dimmed
|
model_A.gguf
|
||||||
/joi-dark
|
model_A.yaml
|
||||||
/joi-light
|
/threads/
|
||||||
/night-blue
|
/thread_A/
|
||||||
/threads
|
messages.jsonl
|
||||||
/jan_thread_A
|
thread.json
|
||||||
messages.jsonl
|
|
||||||
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,88 +155,65 @@ 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",
|
"text": {
|
||||||
"content":
|
"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",
|
"type": "text"
|
||||||
"text": {
|
}
|
||||||
"value": "Hello! Is there something I can help you with or would you like to chat?",
|
],
|
||||||
"annotations": []
|
"created_at": 1751012639307,
|
||||||
}
|
"id": "01JYR7S0JB5ZBGMJV52KWMW5VW",
|
||||||
}
|
"metadata": {
|
||||||
],
|
"assistant": {
|
||||||
"status": "ready",
|
"avatar": "👋",
|
||||||
"created": 1725442802966,
|
"id": "jan",
|
||||||
"updated": 1725442802966,
|
"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.",
|
||||||
"object": "thread.message"
|
"name": "Jan",
|
||||||
}
|
"parameters": ""
|
||||||
|
},
|
||||||
|
"tokenSpeed": {
|
||||||
|
"lastTimestamp": 1751012637097,
|
||||||
|
"message": "01JYR7S0GW5M9PSHMRE7T8VQJM",
|
||||||
|
"tokenCount": 49,
|
||||||
|
"tokenSpeed": 22.653721682847895
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"object": "thread.message",
|
||||||
|
"role": "assistant",
|
||||||
|
"status": "ready",
|
||||||
|
"thread_id": "8f2c9922-db49-4d1e-8620-279c05baf2d0",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `thread.json`:
|
- `thread.json`:
|
||||||
|
|
||||||
| 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>
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -45,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",
|
||||||
|
|||||||
@ -17,6 +17,8 @@
|
|||||||
"label": "main",
|
"label": "main",
|
||||||
"title": "Jan",
|
"title": "Jan",
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
|
"minWidth": 375,
|
||||||
|
"minHeight": 667,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
|
|||||||
@ -67,7 +67,7 @@
|
|||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"token.js": "npm:token.js-fork@0.7.9",
|
"token.js": "npm:token.js-fork@0.7.12",
|
||||||
"tw-animate-css": "^1.2.7",
|
"tw-animate-css": "^1.2.7",
|
||||||
"ulidx": "^2.4.1",
|
"ulidx": "^2.4.1",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
|
|||||||
@ -67,7 +67,7 @@ function DialogContent({
|
|||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
aria-describedby={ariaDescribedBy}
|
aria-describedby={ariaDescribedBy}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-main-view max-h-[calc(100%-48px)] overflow-auto border-main-view-fg/10 text-main-view-fg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
'bg-main-view max-h-[calc(100%-80px)] overflow-auto border-main-view-fg/10 text-main-view-fg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -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,43 +10,63 @@ 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
|
<>
|
||||||
className={cn(
|
<div
|
||||||
'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={cn(
|
||||||
className,
|
'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',
|
||||||
align === 'start' && 'items-start',
|
descriptionOutside && 'border-0',
|
||||||
align === 'center' && 'items-center',
|
align === 'start' && 'items-start',
|
||||||
align === 'end' && 'items-end',
|
align === 'center' && 'items-center',
|
||||||
column && 'flex-col gap-y-0 items-start'
|
align === 'end' && 'items-end',
|
||||||
)}
|
column && 'flex-col gap-y-0 items-start',
|
||||||
>
|
className
|
||||||
<div className="space-y-1.5">
|
)}
|
||||||
<h1 className="font-medium">{title}</h1>
|
>
|
||||||
{description && (
|
<div className="space-y-1.5">
|
||||||
<span className="text-main-view-fg/70 leading-normal">
|
<h1 className="font-medium">{title}</h1>
|
||||||
{description}
|
{description && (
|
||||||
</span>
|
<span className="text-main-view-fg/70 leading-normal">
|
||||||
|
{description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{actions && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'shrink-0',
|
||||||
|
classNameWrapperAction,
|
||||||
|
column && 'w-full'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{actions && (
|
{descriptionOutside && (
|
||||||
<div className={cn('shrink-0', column && 'w-full')}>{actions}</div>
|
<span className="text-main-view-fg/70 leading-normal">
|
||||||
|
{descriptionOutside}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,27 +9,31 @@ 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 ',
|
||||||
chatWidth === 'compact' && 'border-accent'
|
chatWidth === 'compact' && 'border-accent'
|
||||||
)}
|
)}
|
||||||
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>
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import {
|
|||||||
import { useThreads } from '@/hooks/useThreads'
|
import { useThreads } from '@/hooks/useThreads'
|
||||||
|
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState, useEffect, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
@ -40,6 +40,8 @@ import {
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { DownloadManagement } from '@/containers/DownloadManegement'
|
import { DownloadManagement } from '@/containers/DownloadManegement'
|
||||||
|
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||||
|
import { useClickOutside } from '@/hooks/useClickOutside'
|
||||||
|
|
||||||
const mainMenus = [
|
const mainMenus = [
|
||||||
{
|
{
|
||||||
@ -70,6 +72,68 @@ const LeftPanel = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
|
||||||
|
const isSmallScreen = useSmallScreen()
|
||||||
|
const prevScreenSizeRef = useRef<boolean | null>(null)
|
||||||
|
const isInitialMountRef = useRef(true)
|
||||||
|
const panelRef = useRef<HTMLElement>(null)
|
||||||
|
const searchContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const searchContainerMacRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Use click outside hook for panel with debugging
|
||||||
|
useClickOutside(
|
||||||
|
() => {
|
||||||
|
if (isSmallScreen && open) {
|
||||||
|
setLeftPanel(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
panelRef.current,
|
||||||
|
searchContainerRef.current,
|
||||||
|
searchContainerMacRef.current,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-collapse panel only when window is resized
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
const currentIsSmallScreen = window.innerWidth <= 768
|
||||||
|
|
||||||
|
// Skip on initial mount
|
||||||
|
if (isInitialMountRef.current) {
|
||||||
|
isInitialMountRef.current = false
|
||||||
|
prevScreenSizeRef.current = currentIsSmallScreen
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only trigger if the screen size actually changed
|
||||||
|
if (
|
||||||
|
prevScreenSizeRef.current !== null &&
|
||||||
|
prevScreenSizeRef.current !== currentIsSmallScreen
|
||||||
|
) {
|
||||||
|
if (currentIsSmallScreen) {
|
||||||
|
setLeftPanel(false)
|
||||||
|
} else {
|
||||||
|
setLeftPanel(true)
|
||||||
|
}
|
||||||
|
prevScreenSizeRef.current = currentIsSmallScreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add resize listener
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
// Initialize the previous screen size on mount
|
||||||
|
if (isInitialMountRef.current) {
|
||||||
|
prevScreenSizeRef.current = window.innerWidth <= 768
|
||||||
|
isInitialMountRef.current = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
}, [setLeftPanel])
|
||||||
|
|
||||||
const currentPath = useRouterState({
|
const currentPath = useRouterState({
|
||||||
select: (state) => state.location.pathname,
|
select: (state) => state.location.pathname,
|
||||||
})
|
})
|
||||||
@ -91,50 +155,63 @@ const LeftPanel = () => {
|
|||||||
return filteredThreads.filter((t) => !t.isFavorite)
|
return filteredThreads.filter((t) => !t.isFavorite)
|
||||||
}, [filteredThreads])
|
}, [filteredThreads])
|
||||||
|
|
||||||
return (
|
// Disable body scroll when panel is open on small screens
|
||||||
<aside
|
useEffect(() => {
|
||||||
className={cn(
|
if (isSmallScreen && open) {
|
||||||
'w-48 shrink-0 rounded-lg m-1.5 mr-0 text-left-panel-fg',
|
document.body.style.overflow = 'hidden'
|
||||||
open
|
} else {
|
||||||
? 'opacity-100 visibility-visible'
|
document.body.style.overflow = ''
|
||||||
: 'w-0 absolute -top-100 -left-100 visibility-hidden'
|
}
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="relative h-10">
|
|
||||||
<button
|
|
||||||
className="absolute top-1/2 right-0 -translate-y-1/2 z-20"
|
|
||||||
onClick={() => setLeftPanel(!open)}
|
|
||||||
>
|
|
||||||
<div className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out">
|
|
||||||
<IconLayoutSidebar size={18} className="text-left-panel-fg" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{!IS_MACOS && (
|
|
||||||
<div className="relative top-1.5 mb-4 mx-1 mt-1 w-[calc(100%-32px)] z-50">
|
|
||||||
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={t('common:search')}
|
|
||||||
className="w-full pl-7 pr-8 py-1 bg-left-panel-fg/10 rounded-sm text-left-panel-fg focus:outline-none focus:ring-1 focus:ring-left-panel-fg/10"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
{searchTerm && (
|
|
||||||
<button
|
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-left-panel-fg/70 hover:text-left-panel-fg"
|
|
||||||
onClick={() => setSearchTerm('')}
|
|
||||||
>
|
|
||||||
<IconX size={14} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col justify-between h-[calc(100%-42px)] mt-0">
|
return () => {
|
||||||
<div className="flex flex-col justify-between h-full">
|
document.body.style.overflow = ''
|
||||||
{IS_MACOS && (
|
}
|
||||||
<div className="relative mb-4 mx-1 mt-1">
|
}, [isSmallScreen, open])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Backdrop overlay for small screens */}
|
||||||
|
{isSmallScreen && open && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur z-30"
|
||||||
|
onClick={(e) => {
|
||||||
|
// Don't close if clicking on search container or if currently searching
|
||||||
|
if (
|
||||||
|
searchContainerRef.current?.contains(e.target as Node) ||
|
||||||
|
searchContainerMacRef.current?.contains(e.target as Node)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLeftPanel(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<aside
|
||||||
|
ref={panelRef}
|
||||||
|
className={cn(
|
||||||
|
'w-48 shrink-0 rounded-lg m-1.5 mr-0 text-left-panel-fg overflow-hidden',
|
||||||
|
isSmallScreen &&
|
||||||
|
'fixed h-[calc(100%-16px)] bg-main-view z-40 rounded-sm border border-left-panel-fg/10 m-2 px-1',
|
||||||
|
open
|
||||||
|
? 'opacity-100 visibility-visible'
|
||||||
|
: 'w-0 absolute -top-100 -left-100 visibility-hidden'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="relative h-10">
|
||||||
|
<button
|
||||||
|
className="absolute top-1/2 right-0 -translate-y-1/2 z-20"
|
||||||
|
onClick={() => setLeftPanel(!open)}
|
||||||
|
>
|
||||||
|
<div className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10">
|
||||||
|
<IconLayoutSidebar size={18} className="text-left-panel-fg" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{!IS_MACOS && (
|
||||||
|
<div
|
||||||
|
ref={searchContainerRef}
|
||||||
|
className="relative top-1.5 mb-4 mx-1 mt-1 w-[calc(100%-32px)] z-50"
|
||||||
|
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" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -146,176 +223,226 @@ const LeftPanel = () => {
|
|||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<button
|
<button
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-left-panel-fg/70 hover:text-left-panel-fg"
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-left-panel-fg/70 hover:text-left-panel-fg"
|
||||||
onClick={() => setSearchTerm('')}
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation() // prevent bubbling
|
||||||
|
setSearchTerm('')
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<IconX size={14} />
|
<IconX size={14} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col w-full h-full overflow-hidden">
|
</div>
|
||||||
<div className="h-full overflow-y-auto overflow-x-hidden">
|
|
||||||
{favoritedThreads.length > 0 && (
|
<div className="flex flex-col justify-between overflow-hidden mt-0 !h-[calc(100%-42px)]">
|
||||||
<>
|
<div className="flex flex-col !h-[calc(100%-140px)]">
|
||||||
|
{IS_MACOS && (
|
||||||
|
<div
|
||||||
|
ref={searchContainerMacRef}
|
||||||
|
className="relative mb-4 mx-1 mt-1"
|
||||||
|
data-ignore-outside-clicks
|
||||||
|
>
|
||||||
|
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('common:search')}
|
||||||
|
className="w-full pl-7 pr-8 py-1 bg-left-panel-fg/10 rounded-sm text-left-panel-fg focus:outline-none focus:ring-1 focus:ring-left-panel-fg/10"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
{searchTerm && (
|
||||||
|
<button
|
||||||
|
data-ignore-outside-clicks
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-left-panel-fg/70 hover:text-left-panel-fg"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation() // prevent bubbling
|
||||||
|
setSearchTerm('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col w-full overflow-y-auto overflow-x-hidden">
|
||||||
|
<div className="h-full w-full overflow-y-auto">
|
||||||
|
{favoritedThreads.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold sticky top-0">
|
||||||
|
{t('common:favorites')}
|
||||||
|
</span>
|
||||||
|
<div className="relative">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="size-6 flex cursor-pointer items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10">
|
||||||
|
<IconDots
|
||||||
|
size={18}
|
||||||
|
className="text-left-panel-fg/60"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="bottom" align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
unstarAllThreads()
|
||||||
|
toast.success(
|
||||||
|
t('common:toast.allThreadsUnfavorited.title'),
|
||||||
|
{
|
||||||
|
id: 'unfav-all-threads',
|
||||||
|
description: t(
|
||||||
|
'common:toast.allThreadsUnfavorited.description'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconStar size={16} />
|
||||||
|
<span>{t('common:unstarAll')}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col mb-4">
|
||||||
|
<ThreadList
|
||||||
|
threads={favoritedThreads}
|
||||||
|
isFavoriteSection={true}
|
||||||
|
/>
|
||||||
|
{favoritedThreads.length === 0 && (
|
||||||
|
<p className="text-xs text-left-panel-fg/50 px-1 font-semibold">
|
||||||
|
{t('chat.status.empty', { ns: 'chat' })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{unFavoritedThreads.length > 0 && (
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold sticky top-0">
|
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold">
|
||||||
{t('common:favorites')}
|
{t('common:recents')}
|
||||||
</span>
|
</span>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<DropdownMenu>
|
<Dialog>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<button className="size-6 flex cursor-pointer items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10">
|
<DropdownMenuTrigger asChild>
|
||||||
<IconDots
|
<button
|
||||||
size={18}
|
className="size-6 flex cursor-pointer items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10"
|
||||||
className="text-left-panel-fg/60"
|
onClick={(e) => {
|
||||||
/>
|
e.preventDefault()
|
||||||
</button>
|
e.stopPropagation()
|
||||||
</DropdownMenuTrigger>
|
}}
|
||||||
<DropdownMenuContent side="bottom" align="end">
|
>
|
||||||
<DropdownMenuItem
|
<IconDots
|
||||||
onClick={() => {
|
size={18}
|
||||||
unstarAllThreads()
|
className="text-left-panel-fg/60"
|
||||||
toast.success(t('common:toast.allThreadsUnfavorited.title'), {
|
/>
|
||||||
id: 'unfav-all-threads',
|
</button>
|
||||||
description: t('common:toast.allThreadsUnfavorited.description'),
|
</DropdownMenuTrigger>
|
||||||
})
|
<DropdownMenuContent side="bottom" align="end">
|
||||||
}}
|
<DialogTrigger asChild>
|
||||||
>
|
<DropdownMenuItem
|
||||||
<IconStar size={16} />
|
onSelect={(e) => e.preventDefault()}
|
||||||
<span>{t('common:unstarAll')}</span>
|
>
|
||||||
</DropdownMenuItem>
|
<IconTrash size={16} />
|
||||||
</DropdownMenuContent>
|
<span>{t('common:deleteAll')}</span>
|
||||||
</DropdownMenu>
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t('common:dialogs.deleteAllThreads.title')}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t(
|
||||||
|
'common:dialogs.deleteAllThreads.description'
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogFooter className="mt-2">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="hover:no-underline"
|
||||||
|
>
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
deleteAllThreads()
|
||||||
|
toast.success(
|
||||||
|
t(
|
||||||
|
'common:toast.deleteAllThreads.title'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: 'delete-all-thread',
|
||||||
|
description: t(
|
||||||
|
'common:toast.deleteAllThreads.description'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate({ to: route.home })
|
||||||
|
}, 0)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('common:deleteAll')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col mb-4">
|
)}
|
||||||
<ThreadList
|
|
||||||
threads={favoritedThreads}
|
|
||||||
isFavoriteSection={true}
|
|
||||||
/>
|
|
||||||
{favoritedThreads.length === 0 && (
|
|
||||||
<p className="text-xs text-left-panel-fg/50 px-1 font-semibold">
|
|
||||||
{t('chat.status.empty', { ns: 'chat' })}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{unFavoritedThreads.length > 0 && (
|
{filteredThreads.length === 0 && searchTerm.length > 0 && (
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold">
|
|
||||||
{t('common:recents')}
|
|
||||||
</span>
|
|
||||||
<div className="relative">
|
|
||||||
<Dialog>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
className="size-6 flex cursor-pointer items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconDots
|
|
||||||
size={18}
|
|
||||||
className="text-left-panel-fg/60"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent side="bottom" align="end">
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<IconTrash size={16} />
|
|
||||||
<span>{t('common:deleteAll')}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
{t('common:dialogs.deleteAllThreads.title')}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t(
|
|
||||||
'common:dialogs.deleteAllThreads.description'
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
|
||||||
<DialogFooter className="mt-2">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
className="hover:no-underline"
|
|
||||||
>
|
|
||||||
{t('common:cancel')}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
deleteAllThreads()
|
|
||||||
toast.success(t('common:toast.deleteAllThreads.title'), {
|
|
||||||
id: 'delete-all-thread',
|
|
||||||
description: t('common:toast.deleteAllThreads.description'),
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
navigate({ to: route.home })
|
|
||||||
}, 0)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('common:deleteAll')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{filteredThreads.length === 0 && searchTerm.length > 0 && (
|
|
||||||
<div className="px-1 mt-2">
|
|
||||||
<div className="flex items-center gap-1 text-left-panel-fg/80">
|
|
||||||
<IconSearch size={18} />
|
|
||||||
<h6 className="font-medium text-base">
|
|
||||||
{t('common:noResultsFound')}
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
|
|
||||||
{t('common:noResultsFoundDesc')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{Object.keys(threads).length === 0 && !searchTerm && (
|
|
||||||
<>
|
|
||||||
<div className="px-1 mt-2">
|
<div className="px-1 mt-2">
|
||||||
<div className="flex items-center gap-1 text-left-panel-fg/80">
|
<div className="flex items-center gap-1 text-left-panel-fg/80">
|
||||||
<IconMessageFilled size={18} />
|
<IconSearch size={18} />
|
||||||
<h6 className="font-medium text-base">
|
<h6 className="font-medium text-base">
|
||||||
{t('common:noThreadsYet')}
|
{t('common:noResultsFound')}
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
|
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
|
||||||
{t('common:noThreadsYetDesc')}
|
{t('common:noResultsFoundDesc')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col">
|
{Object.keys(threads).length === 0 && !searchTerm && (
|
||||||
<ThreadList threads={unFavoritedThreads} />
|
<>
|
||||||
|
<div className="px-1 mt-2">
|
||||||
|
<div className="flex items-center gap-1 text-left-panel-fg/80">
|
||||||
|
<IconMessageFilled size={18} />
|
||||||
|
<h6 className="font-medium text-base">
|
||||||
|
{t('common:noThreadsYet')}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
|
||||||
|
{t('common:noThreadsYetDesc')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<ThreadList threads={unFavoritedThreads} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1 py-1 mt-2">
|
<div className="space-y-1 shrink-0 py-1 mt-2">
|
||||||
{mainMenus.map((menu) => {
|
{mainMenus.map((menu) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
currentPath.includes(route.settings.index) &&
|
currentPath.includes(route.settings.index) &&
|
||||||
@ -324,6 +451,7 @@ const LeftPanel = () => {
|
|||||||
<Link
|
<Link
|
||||||
key={menu.title}
|
key={menu.title}
|
||||||
to={menu.route}
|
to={menu.route}
|
||||||
|
onClick={() => isSmallScreen && setLeftPanel(false)}
|
||||||
data-test-id={`menu-${menu.title}`}
|
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',
|
||||||
@ -342,8 +470,8 @@ const LeftPanel = () => {
|
|||||||
</div>
|
</div>
|
||||||
<DownloadManagement />
|
<DownloadManagement />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
</aside>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,20 +1,56 @@
|
|||||||
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.routeId === '/settings/providers/$providerName' &&
|
||||||
|
'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) =>
|
||||||
match.routeId === '/settings/providers/$providerName' &&
|
match.search &&
|
||||||
'providerName' in match.params
|
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">
|
<>
|
||||||
<div className="flex flex-col gap-1 w-full text-main-view-fg/90 font-medium">
|
<button
|
||||||
{menuSettings.map((menu, index) => {
|
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"
|
||||||
// Render the menu item
|
onClick={toggleMenu}
|
||||||
const menuItem = (
|
aria-label="Toggle settings menu"
|
||||||
<Link
|
>
|
||||||
key={menu.title}
|
{isMenuOpen ? (
|
||||||
to={menu.route}
|
<IconX size={18} className="text-main-view-fg relative z-20" />
|
||||||
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"
|
) : (
|
||||||
>
|
<IconMenu2 size={18} className="text-main-view-fg relative z-20" />
|
||||||
<span className="text-main-view-fg/80">{t(menu.title)}</span>
|
)}
|
||||||
</Link>
|
</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">
|
||||||
|
{menuSettings.map((menu) => (
|
||||||
|
<div key={menu.title}>
|
||||||
|
<Link
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-main-view-fg/80">{t(menu.title)}</span>
|
||||||
|
{menu.hasSubMenu && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
toggleProvidersExpansion()
|
||||||
|
}}
|
||||||
|
className="text-main-view-fg/60 hover:text-main-view-fg/80"
|
||||||
|
>
|
||||||
|
{expandedProviders ? (
|
||||||
|
<IconChevronDown size={16} />
|
||||||
|
) : (
|
||||||
|
<IconChevronRight size={16} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
if (index === 2) {
|
{/* Sub-menu for model providers */}
|
||||||
return (
|
{menu.hasSubMenu && expandedProviders && (
|
||||||
<div key={menu.title}>
|
<div className="ml-2 mt-1 space-y-1 first-step-setup-remote-provider">
|
||||||
<span className="mb-1 block">{menuItem}</span>
|
{activeProviders.map((provider) => {
|
||||||
|
const isActive = matches.some(
|
||||||
|
(match) =>
|
||||||
|
match.routeId === '/settings/providers/$providerName' &&
|
||||||
|
'providerName' in match.params &&
|
||||||
|
match.params.providerName === provider.provider
|
||||||
|
)
|
||||||
|
|
||||||
{/* Model Providers Link with default parameter */}
|
return (
|
||||||
{isActive ? (
|
<div key={provider.provider}>
|
||||||
<div className="block px-2 mt-1 gap-1.5 py-1 w-full rounded bg-main-view-fg/5 cursor-pointer">
|
<div
|
||||||
<span>{t('common:modelProviders')}</span>
|
className={cn(
|
||||||
</div>
|
'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',
|
||||||
<Link
|
// hidden for llama.cpp provider for setup remote provider
|
||||||
key="common.modelProviders"
|
provider.provider === 'llama.cpp' &&
|
||||||
to={route.settings.providers}
|
stepSetupRemoteProvider &&
|
||||||
params={{ providerName: firstItemProvider }}
|
'hidden'
|
||||||
className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded"
|
)}
|
||||||
>
|
onClick={() =>
|
||||||
<span className="text-main-view-fg/80">
|
navigate({
|
||||||
{t('common:modelProviders')}
|
to: route.settings.providers,
|
||||||
</span>
|
params: {
|
||||||
</Link>
|
providerName: provider.provider,
|
||||||
)}
|
},
|
||||||
</div>
|
...(stepSetupRemoteProvider
|
||||||
)
|
? { search: { step: 'setup_remote_provider' } }
|
||||||
}
|
: {}),
|
||||||
|
})
|
||||||
// For other menu items, just render them normally
|
}
|
||||||
return menuItem
|
>
|
||||||
})}
|
<ProvidersAvatar provider={provider} />
|
||||||
|
<div className="truncate">
|
||||||
|
<span>{getProviderTitle(provider.provider)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,8 +20,10 @@ import {
|
|||||||
IconStar,
|
IconStar,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { useThreads } from '@/hooks/useThreads'
|
import { useThreads } from '@/hooks/useThreads'
|
||||||
|
import { useLeftPanel } from '@/hooks/useLeftPanel'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { route } from '@/constants/routes'
|
import { route } from '@/constants/routes'
|
||||||
|
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -55,6 +57,9 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
isDragging,
|
isDragging,
|
||||||
} = useSortable({ id: thread.id, disabled: true })
|
} = useSortable({ id: thread.id, disabled: true })
|
||||||
|
|
||||||
|
const isSmallScreen = useSmallScreen()
|
||||||
|
const { setLeftPanel } = useLeftPanel()
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
@ -75,7 +80,11 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!isDragging) {
|
if (!isDragging) {
|
||||||
navigate({ to: route.threadsDetail, params: { threadId: thread.id } })
|
// Only close panel and navigate if the thread is not already active
|
||||||
|
if (!isActive) {
|
||||||
|
if (isSmallScreen) setLeftPanel(false)
|
||||||
|
navigate({ to: route.threadsDetail, params: { threadId: thread.id } })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +94,9 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
return (thread.title || '').replace(/<span[^>]*>|<\/span>/g, '')
|
return (thread.title || '').replace(/<span[^>]*>|<\/span>/g, '')
|
||||||
}, [thread.title])
|
}, [thread.title])
|
||||||
|
|
||||||
const [title, setTitle] = useState(plainTitleForRename || t('common:newThread'))
|
const [title, setTitle] = useState(
|
||||||
|
plainTitleForRename || t('common:newThread')
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -185,7 +196,10 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
setOpenDropdown(false)
|
setOpenDropdown(false)
|
||||||
toast.success(t('common:toast.renameThread.title'), {
|
toast.success(t('common:toast.renameThread.title'), {
|
||||||
id: 'rename-thread',
|
id: 'rename-thread',
|
||||||
description: t('common:toast.renameThread.description', { title }),
|
description: t(
|
||||||
|
'common:toast.renameThread.description',
|
||||||
|
{ title }
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -231,7 +245,9 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
setOpenDropdown(false)
|
setOpenDropdown(false)
|
||||||
toast.success(t('common:toast.deleteThread.title'), {
|
toast.success(t('common:toast.deleteThread.title'), {
|
||||||
id: 'delete-thread',
|
id: 'delete-thread',
|
||||||
description: t('common:toast.deleteThread.description'),
|
description: t(
|
||||||
|
'common:toast.deleteThread.description'
|
||||||
|
),
|
||||||
})
|
})
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate({ to: route.home })
|
navigate({ to: route.home })
|
||||||
|
|||||||
@ -378,73 +378,27 @@ export default function AddEditAssistant({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{paramsKeys.map((key, index) => (
|
{paramsKeys.map((key, index) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<div key={index} className="flex items-center gap-4">
|
||||||
<Input
|
<div
|
||||||
value={key}
|
key={index}
|
||||||
onChange={(e) =>
|
className="flex items-center flex-col sm:flex-row w-full gap-2"
|
||||||
handleParameterChange(index, e.target.value, 'key')
|
>
|
||||||
}
|
<Input
|
||||||
placeholder={t('assistants:key')}
|
value={key}
|
||||||
className="w-24"
|
onChange={(e) =>
|
||||||
/>
|
handleParameterChange(index, e.target.value, 'key')
|
||||||
|
}
|
||||||
|
placeholder={t('assistants:key')}
|
||||||
|
className="w-full sm:w-24"
|
||||||
|
/>
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<div className="relative w-30">
|
|
||||||
<Input
|
|
||||||
value={
|
|
||||||
paramsTypes[index].charAt(0).toUpperCase() +
|
|
||||||
paramsTypes[index].slice(1)
|
|
||||||
}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<IconChevronDown
|
|
||||||
size={14}
|
|
||||||
className="text-main-view-fg/50 absolute right-2 top-1/2 -translate-y-1/2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="w-32" align="start">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
handleParameterChange(index, 'string', 'type')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('assistants:stringValue')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
handleParameterChange(index, 'number', 'type')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('assistants:numberValue')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
handleParameterChange(index, 'boolean', 'type')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('assistants:booleanValue')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
handleParameterChange(index, 'json', 'type')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('assistants:jsonValue')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
{paramsTypes[index] === 'boolean' ? (
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="relative flex-1">
|
<div className="relative w-full sm:w-30">
|
||||||
<Input
|
<Input
|
||||||
value={
|
value={
|
||||||
paramsValues[index]
|
paramsTypes[index].charAt(0).toUpperCase() +
|
||||||
? t('assistants:trueValue')
|
paramsTypes[index].slice(1)
|
||||||
: t('assistants:falseValue')
|
|
||||||
}
|
}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
@ -454,48 +408,98 @@ export default function AddEditAssistant({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-24" align="start">
|
<DropdownMenuContent className="w-32" align="start">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleParameterChange(index, true, 'value')
|
handleParameterChange(index, 'string', 'type')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('assistants:trueValue')}
|
{t('assistants:stringValue')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleParameterChange(index, false, 'value')
|
handleParameterChange(index, 'number', 'type')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('assistants:falseValue')}
|
{t('assistants:numberValue')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
handleParameterChange(index, 'boolean', 'type')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('assistants:booleanValue')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
handleParameterChange(index, 'json', 'type')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('assistants:jsonValue')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : paramsTypes[index] === 'json' ? (
|
|
||||||
<Input
|
|
||||||
value={
|
|
||||||
typeof paramsValues[index] === 'object'
|
|
||||||
? JSON.stringify(paramsValues[index], null, 2)
|
|
||||||
: paramsValues[index]?.toString() || ''
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleParameterChange(index, e.target.value, 'value')
|
|
||||||
}
|
|
||||||
placeholder={t('assistants:jsonValuePlaceholder')}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
value={paramsValues[index]?.toString() || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleParameterChange(index, e.target.value, 'value')
|
|
||||||
}
|
|
||||||
type={paramsTypes[index] === 'number' ? 'number' : 'text'}
|
|
||||||
placeholder={t('assistants:value')}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
{paramsTypes[index] === 'boolean' ? (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<div className="relative sm:flex-1 w-full">
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
paramsValues[index]
|
||||||
|
? t('assistants:trueValue')
|
||||||
|
: t('assistants:falseValue')
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<IconChevronDown
|
||||||
|
size={14}
|
||||||
|
className="text-main-view-fg/50 absolute right-2 top-1/2 -translate-y-1/2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-24" align="start">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
handleParameterChange(index, true, 'value')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('assistants:trueValue')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
handleParameterChange(index, false, 'value')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('assistants:falseValue')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
) : paramsTypes[index] === 'json' ? (
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
typeof paramsValues[index] === 'object'
|
||||||
|
? JSON.stringify(paramsValues[index], null, 2)
|
||||||
|
: paramsValues[index]?.toString() || ''
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleParameterChange(index, e.target.value, 'value')
|
||||||
|
}
|
||||||
|
placeholder={t('assistants:jsonValuePlaceholder')}
|
||||||
|
className="sm:flex-1 h-[36px] w-full"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={paramsValues[index]?.toString() || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleParameterChange(index, e.target.value, 'value')
|
||||||
|
}
|
||||||
|
type={paramsTypes[index] === 'number' ? 'number' : 'text'}
|
||||||
|
placeholder={t('assistants:value')}
|
||||||
|
className="sm:flex-1 h-[36px] w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||||
onClick={() => handleRemoveParameter(index)}
|
onClick={() => handleRemoveParameter(index)}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { listen } from '@tauri-apps/api/event'
|
import { listen } from '@tauri-apps/api/event'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { t } from 'i18next'
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -11,8 +11,10 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { useTranslation } from '@/i18n'
|
||||||
|
|
||||||
export function CortexFailureDialog() {
|
export function CortexFailureDialog() {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [showDialog, setShowDialog] = useState(false)
|
const [showDialog, setShowDialog] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -52,15 +54,10 @@ export function CortexFailureDialog() {
|
|||||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>{t('cortexFailureDialog.title')}</DialogTitle>
|
||||||
{t('cortexFailureDialog.title', 'Local AI Engine Issue')}
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t(
|
{t('cortexFailureDialog.description')}
|
||||||
'cortexFailureDialog.description',
|
|
||||||
'The local AI engine (Cortex) failed to start after multiple attempts. This might prevent some features from working correctly.'
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
<DialogFooter className="flex gap-2">
|
<DialogFooter className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
@ -77,12 +74,12 @@ export function CortexFailureDialog() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<span className="text-main-view-fg/70">
|
<span className="text-main-view-fg/70">
|
||||||
{t('cortexFailureDialog.contactSupport', 'Contact Support')}
|
{t('cortexFailureDialog.contactSupport')}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleRestartJan}>
|
<Button onClick={handleRestartJan}>
|
||||||
{t('cortexFailureDialog.restartJan', 'Restart Jan')}
|
{t('cortexFailureDialog.restartJan')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
42
web-app/src/hooks/useClickOutside.ts
Normal file
42
web-app/src/hooks/useClickOutside.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
const DEFAULT_EVENTS = ['mousedown', 'touchstart']
|
||||||
|
|
||||||
|
export function useClickOutside<T extends HTMLElement = any>(
|
||||||
|
handler: () => void,
|
||||||
|
events?: string[] | null,
|
||||||
|
nodes?: (HTMLElement | null)[]
|
||||||
|
) {
|
||||||
|
const ref = useRef<T>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (event: any) => {
|
||||||
|
const { target } = event ?? {}
|
||||||
|
if (Array.isArray(nodes)) {
|
||||||
|
const shouldIgnore =
|
||||||
|
target?.hasAttribute('data-ignore-outside-clicks') ||
|
||||||
|
(!document.body.contains(target) && target.tagName !== 'HTML')
|
||||||
|
const shouldTrigger = nodes.every(
|
||||||
|
(node) => !!node && !event.composedPath().includes(node)
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
|
shouldTrigger && !shouldIgnore && handler()
|
||||||
|
} else if (ref.current && !ref.current.contains(target)) {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
;(events || DEFAULT_EVENTS).forEach((fn) =>
|
||||||
|
document.addEventListener(fn, listener)
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
;(events || DEFAULT_EVENTS).forEach((fn) =>
|
||||||
|
document.removeEventListener(fn, listener)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [ref, handler, nodes, events])
|
||||||
|
|
||||||
|
return ref
|
||||||
|
}
|
||||||
90
web-app/src/hooks/useMediaQuery.ts
Normal file
90
web-app/src/hooks/useMediaQuery.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
export interface UseMediaQueryOptions {
|
||||||
|
getInitialValueInEffect: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaQueryCallback = (event: { matches: boolean; media: string }) => void
|
||||||
|
|
||||||
|
// Zustand store for small screen state
|
||||||
|
type SmallScreenState = {
|
||||||
|
isSmallScreen: boolean
|
||||||
|
setIsSmallScreen: (isSmall: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSmallScreenStore = create<SmallScreenState>((set) => ({
|
||||||
|
isSmallScreen: false,
|
||||||
|
setIsSmallScreen: (isSmall) => set({ isSmallScreen: isSmall }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Older versions of Safari (shipped withCatalina and before) do not support addEventListener on matchMedia
|
||||||
|
* https://stackoverflow.com/questions/56466261/matchmedia-addlistener-marked-as-deprecated-addeventlistener-equivalent
|
||||||
|
* */
|
||||||
|
function attachMediaListener(
|
||||||
|
query: MediaQueryList,
|
||||||
|
callback: MediaQueryCallback
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
query.addEventListener('change', callback)
|
||||||
|
return () => query.removeEventListener('change', callback)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
// eslint-disable @typescript-eslint/no-deprecated
|
||||||
|
query.addListener(callback)
|
||||||
|
return () => query.removeListener(callback)
|
||||||
|
// eslint-enable @typescript-eslint/no-deprecated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialValue(query: string, initialValue?: boolean) {
|
||||||
|
if (typeof initialValue === 'boolean') {
|
||||||
|
return initialValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && 'matchMedia' in window) {
|
||||||
|
return window.matchMedia(query).matches
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMediaQuery(
|
||||||
|
query: string,
|
||||||
|
initialValue?: boolean,
|
||||||
|
{ getInitialValueInEffect }: UseMediaQueryOptions = {
|
||||||
|
getInitialValueInEffect: true,
|
||||||
|
}
|
||||||
|
): boolean {
|
||||||
|
const [matches, setMatches] = useState(
|
||||||
|
getInitialValueInEffect ? initialValue : getInitialValue(query)
|
||||||
|
)
|
||||||
|
const queryRef = useRef<MediaQueryList>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ('matchMedia' in window) {
|
||||||
|
queryRef.current = window.matchMedia(query)
|
||||||
|
setMatches(queryRef.current.matches)
|
||||||
|
return attachMediaListener(queryRef.current, (event) =>
|
||||||
|
setMatches(event.matches)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
return matches || false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific hook for small screen detection with state management
|
||||||
|
export const useSmallScreen = (): boolean => {
|
||||||
|
const { isSmallScreen, setIsSmallScreen } = useSmallScreenStore()
|
||||||
|
const mediaQuery = useMediaQuery('(max-width: 768px)')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsSmallScreen(mediaQuery)
|
||||||
|
}, [mediaQuery, setIsSmallScreen])
|
||||||
|
|
||||||
|
return isSmallScreen
|
||||||
|
}
|
||||||
@ -13,7 +13,6 @@
|
|||||||
"useModel": "Use this model",
|
"useModel": "Use this model",
|
||||||
"downloadModel": "Download model",
|
"downloadModel": "Download model",
|
||||||
"searchPlaceholder": "Search for models on Hugging Face...",
|
"searchPlaceholder": "Search for models on Hugging Face...",
|
||||||
"editTheme": "Edit Theme",
|
|
||||||
"joyride": {
|
"joyride": {
|
||||||
"recommendedModelTitle": "Recommended Model",
|
"recommendedModelTitle": "Recommended Model",
|
||||||
"recommendedModelContent": "Browse and download powerful AI models from various providers, all in one place. We suggest starting with Jan-Nano - a model optimized for function calling, tool integration, and research capabilities. It's ideal for building interactive AI agents.",
|
"recommendedModelContent": "Browse and download powerful AI models from various providers, all in one place. We suggest starting with Jan-Nano - a model optimized for function calling, tool integration, and research capabilities. It's ideal for building interactive AI agents.",
|
||||||
|
|||||||
@ -256,12 +256,5 @@
|
|||||||
"description": "Cortex gagal dimulai. Silakan periksa log untuk detail lebih lanjut.",
|
"description": "Cortex gagal dimulai. Silakan periksa log untuk detail lebih lanjut.",
|
||||||
"contactSupport": "Hubungi Dukungan",
|
"contactSupport": "Hubungi Dukungan",
|
||||||
"restartJan": "Restart Jan"
|
"restartJan": "Restart Jan"
|
||||||
},
|
|
||||||
"outOfContextError": {
|
|
||||||
"title": "Kesalahan di luar konteks",
|
|
||||||
"description": "Obrolan ini mencapai batas memori AI, seperti papan tulis yang penuh. Kami dapat memperluas jendela memori (disebut ukuran konteks) sehingga mengingat lebih banyak, tetapi mungkin menggunakan lebih banyak memori komputer Anda. Kami juga dapat memotong input, yang berarti akan melupakan sebagian riwayat obrolan untuk memberi ruang bagi pesan baru.",
|
|
||||||
"increaseContextSizeDescription": "Apakah Anda ingin meningkatkan ukuran konteks?",
|
|
||||||
"truncateInput": "Potong Input",
|
|
||||||
"increaseContextSize": "Tingkatkan Ukuran Konteks"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
"useModel": "Gunakan model ini",
|
"useModel": "Gunakan model ini",
|
||||||
"downloadModel": "Unduh model",
|
"downloadModel": "Unduh model",
|
||||||
"searchPlaceholder": "Cari model di Hugging Face...",
|
"searchPlaceholder": "Cari model di Hugging Face...",
|
||||||
"editTheme": "Edit Tema",
|
|
||||||
"joyride": {
|
"joyride": {
|
||||||
"recommendedModelTitle": "Model yang Direkomendasikan",
|
"recommendedModelTitle": "Model yang Direkomendasikan",
|
||||||
"recommendedModelContent": "Jelajahi dan unduh model AI yang kuat dari berbagai penyedia, semuanya di satu tempat. Kami sarankan memulai dengan Jan-Nano - model yang dioptimalkan untuk pemanggilan fungsi, integrasi alat, dan kemampuan penelitian. Ini ideal untuk membangun agen AI interaktif.",
|
"recommendedModelContent": "Jelajahi dan unduh model AI yang kuat dari berbagai penyedia, semuanya di satu tempat. Kami sarankan memulai dengan Jan-Nano - model yang dioptimalkan untuk pemanggilan fungsi, integrasi alat, dan kemampuan penelitian. Ini ideal untuk membangun agen AI interaktif.",
|
||||||
|
|||||||
@ -256,12 +256,5 @@
|
|||||||
"description": "Cortex không khởi động được. Vui lòng kiểm tra log để biết thêm chi tiết.",
|
"description": "Cortex không khởi động được. Vui lòng kiểm tra log để biết thêm chi tiết.",
|
||||||
"contactSupport": "Liên hệ Hỗ trợ",
|
"contactSupport": "Liên hệ Hỗ trợ",
|
||||||
"restartJan": "Khởi động lại Jan"
|
"restartJan": "Khởi động lại Jan"
|
||||||
},
|
|
||||||
"outOfContextError": {
|
|
||||||
"title": "Lỗi ngoài ngữ cảnh",
|
|
||||||
"description": "Cuộc trò chuyện này đang đạt đến giới hạn bộ nhớ của AI, giống như một bảng trắng đang đầy. Chúng ta có thể mở rộng cửa sổ bộ nhớ (gọi là kích thước ngữ cảnh) để nó nhớ nhiều hơn, nhưng có thể sử dụng nhiều bộ nhớ máy tính của bạn hơn. Chúng ta cũng có thể cắt bớt đầu vào, có nghĩa là nó sẽ quên một phần lịch sử trò chuyện để nhường chỗ cho tin nhắn mới.",
|
|
||||||
"increaseContextSizeDescription": "Bạn có muốn tăng kích thước ngữ cảnh không?",
|
|
||||||
"truncateInput": "Cắt bớt Đầu vào",
|
|
||||||
"increaseContextSize": "Tăng Kích thước Ngữ cảnh"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
"useModel": "Sử dụng mô hình này",
|
"useModel": "Sử dụng mô hình này",
|
||||||
"downloadModel": "Tải xuống mô hình",
|
"downloadModel": "Tải xuống mô hình",
|
||||||
"searchPlaceholder": "Tìm kiếm các mô hình trên Hugging Face...",
|
"searchPlaceholder": "Tìm kiếm các mô hình trên Hugging Face...",
|
||||||
"editTheme": "Chỉnh sửa chủ đề",
|
|
||||||
"joyride": {
|
"joyride": {
|
||||||
"recommendedModelTitle": "Mô hình được đề xuất",
|
"recommendedModelTitle": "Mô hình được đề xuất",
|
||||||
"recommendedModelContent": "Duyệt và tải xuống các mô hình AI mạnh mẽ từ nhiều nhà cung cấp khác nhau, tất cả ở cùng một nơi. Chúng tôi khuyên bạn nên bắt đầu với Jan-Nano - một mô hình được tối ưu hóa cho các khả năng gọi hàm, tích hợp công cụ và nghiên cứu. Nó lý tưởng để xây dựng các tác nhân AI tương tác.",
|
"recommendedModelContent": "Duyệt và tải xuống các mô hình AI mạnh mẽ từ nhiều nhà cung cấp khác nhau, tất cả ở cùng một nơi. Chúng tôi khuyên bạn nên bắt đầu với Jan-Nano - một mô hình được tối ưu hóa cho các khả năng gọi hàm, tích hợp công cụ và nghiên cứu. Nó lý tưởng để xây dựng các tác nhân AI tương tác.",
|
||||||
|
|||||||
@ -256,12 +256,5 @@
|
|||||||
"description": "Cortex 启动失败。请检查日志以获取更多详细信息。",
|
"description": "Cortex 启动失败。请检查日志以获取更多详细信息。",
|
||||||
"contactSupport": "联系支持",
|
"contactSupport": "联系支持",
|
||||||
"restartJan": "重启 Jan"
|
"restartJan": "重启 Jan"
|
||||||
},
|
|
||||||
"outOfContextError": {
|
|
||||||
"title": "超出上下文错误",
|
|
||||||
"description": "此聊天正在达到AI的内存限制,就像白板填满了一样。我们可以扩展内存窗口(称为上下文大小),使其记住更多内容,但可能会使用更多计算机内存。我们也可以截断输入,这意味着它会忘记一些聊天历史记录,为新消息腾出空间。",
|
|
||||||
"increaseContextSizeDescription": "您想要增加上下文大小吗?",
|
|
||||||
"truncateInput": "截断输入",
|
|
||||||
"increaseContextSize": "增加上下文大小"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
"useModel": "使用此模型",
|
"useModel": "使用此模型",
|
||||||
"downloadModel": "下载模型",
|
"downloadModel": "下载模型",
|
||||||
"searchPlaceholder": "在 Hugging Face 上搜索模型...",
|
"searchPlaceholder": "在 Hugging Face 上搜索模型...",
|
||||||
"editTheme": "编辑主题",
|
|
||||||
"joyride": {
|
"joyride": {
|
||||||
"recommendedModelTitle": "推荐模型",
|
"recommendedModelTitle": "推荐模型",
|
||||||
"recommendedModelContent": "在一个地方浏览和下载来自不同提供商的强大 AI 模型。我们建议从 Jan-Nano 开始 - 这是一个针对函数调用、工具集成和研究功能进行优化的模型。它非常适合构建交互式 AI 代理。",
|
"recommendedModelContent": "在一个地方浏览和下载来自不同提供商的强大 AI 模型。我们建议从 Jan-Nano 开始 - 这是一个针对函数调用、工具集成和研究功能进行优化的模型。它非常适合构建交互式 AI 代理。",
|
||||||
|
|||||||
@ -256,12 +256,5 @@
|
|||||||
"description": "Cortex 啟動失敗。請檢查日誌以獲取更多詳細信息。",
|
"description": "Cortex 啟動失敗。請檢查日誌以獲取更多詳細信息。",
|
||||||
"contactSupport": "聯繫支援",
|
"contactSupport": "聯繫支援",
|
||||||
"restartJan": "重啟 Jan"
|
"restartJan": "重啟 Jan"
|
||||||
},
|
|
||||||
"outOfContextError": {
|
|
||||||
"title": "超出上下文錯誤",
|
|
||||||
"description": "此聊天正在達到AI的記憶體限制,就像白板填滿了一樣。我們可以擴展記憶體視窗(稱為上下文大小),使其記住更多內容,但可能會使用更多電腦記憶體。我們也可以截斷輸入,這意味著它會忘記一些聊天歷史記錄,為新訊息騰出空間。",
|
|
||||||
"increaseContextSizeDescription": "您想要增加上下文大小嗎?",
|
|
||||||
"truncateInput": "截斷輸入",
|
|
||||||
"increaseContextSize": "增加上下文大小"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
"useModel": "使用此模型",
|
"useModel": "使用此模型",
|
||||||
"downloadModel": "下載模型",
|
"downloadModel": "下載模型",
|
||||||
"searchPlaceholder": "在 Hugging Face 上搜尋模型...",
|
"searchPlaceholder": "在 Hugging Face 上搜尋模型...",
|
||||||
"editTheme": "編輯主題",
|
|
||||||
"joyride": {
|
"joyride": {
|
||||||
"recommendedModelTitle": "推薦模型",
|
"recommendedModelTitle": "推薦模型",
|
||||||
"recommendedModelContent": "在一個地方瀏覽和下載來自不同提供商的強大 AI 模型。我們建議從 Jan-Nano 開始 - 這是一個針對函數調用、工具整合和研究功能進行優化的模型。它非常適合構建互動式 AI 代理。",
|
"recommendedModelContent": "在一個地方瀏覽和下載來自不同提供商的強大 AI 模型。我們建議從 Jan-Nano 開始 - 這是一個針對函數調用、工具整合和研究功能進行優化的模型。它非常適合構建互動式 AI 代理。",
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,8 +44,8 @@ const AppLayout = () => {
|
|||||||
{/* Main content panel */}
|
{/* Main content panel */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-full flex w-full p-1',
|
'h-full flex w-full p-1 ',
|
||||||
isLeftPanelOpen && 'w-[calc(100%-198px)]'
|
isLeftPanelOpen && 'w-full md:w-[calc(100%-198px)]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full rounded-lg overflow-hidden">
|
<div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full rounded-lg overflow-hidden">
|
||||||
|
|||||||
@ -62,57 +62,60 @@ function Assistant() {
|
|||||||
<span>{t('assistants:title')}</span>
|
<span>{t('assistants:title')}</span>
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="h-full p-4 overflow-y-auto">
|
<div className="h-full p-4 overflow-y-auto">
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
{assistants.map((assistant) => (
|
{assistants
|
||||||
<div
|
.slice().sort((a, b) => a.created_at - b.created_at)
|
||||||
className="bg-main-view-fg/3 p-3 rounded-md"
|
.map((assistant) => (
|
||||||
key={assistant.id}
|
<div
|
||||||
>
|
className="bg-main-view-fg/3 p-3 rounded-md"
|
||||||
<div className="flex items-center justify-between gap-2">
|
key={assistant.id}
|
||||||
<h3 className="text-base font-medium text-main-view-fg/80">
|
>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center justify-between gap-2">
|
||||||
{assistant?.avatar && (
|
<h3 className="text-base font-medium text-main-view-fg/80">
|
||||||
<span className="shrink-0 w-4 h-4 relative flex items-center justify-center">
|
<div className="flex items-center gap-1">
|
||||||
<AvatarEmoji
|
{assistant?.avatar && (
|
||||||
avatar={assistant?.avatar}
|
<span className="shrink-0 w-4 h-4 relative flex items-center justify-center">
|
||||||
imageClassName="object-cover"
|
<AvatarEmoji
|
||||||
textClassName="text-sm"
|
avatar={assistant?.avatar}
|
||||||
/>
|
imageClassName="object-cover"
|
||||||
</span>
|
textClassName="text-sm"
|
||||||
)}
|
/>
|
||||||
<span className="line-clamp-1">{assistant.name}</span>
|
</span>
|
||||||
</div>
|
)}
|
||||||
</h3>
|
<span className="line-clamp-1">{assistant.name}</span>
|
||||||
<div className="flex items-center gap-0.5">
|
</div>
|
||||||
<div
|
</h3>
|
||||||
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
<div className="flex items-center gap-0.5">
|
||||||
title={t('assistants:editAssistant')}
|
<div
|
||||||
onClick={() => {
|
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||||
setEditingKey(assistant.id)
|
title={t('assistants:editAssistant')}
|
||||||
setOpen(true)
|
onClick={() => {
|
||||||
}}
|
setEditingKey(assistant.id)
|
||||||
>
|
setOpen(true)
|
||||||
<IconPencil size={18} className="text-main-view-fg/50" />
|
}}
|
||||||
</div>
|
>
|
||||||
<div
|
<IconPencil size={18} className="text-main-view-fg/50" />
|
||||||
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
</div>
|
||||||
title={t('assistants:deleteAssistant')}
|
<div
|
||||||
onClick={() => handleDelete(assistant.id)}
|
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||||
>
|
title={t('assistants:deleteAssistant')}
|
||||||
<IconTrash size={18} className="text-main-view-fg/50" />
|
onClick={() => handleDelete(assistant.id)}
|
||||||
|
>
|
||||||
|
<IconTrash size={18} className="text-main-view-fg/50" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
className="text-main-view-fg/50 mt-1 line-clamp-2"
|
||||||
|
title={assistant.description}
|
||||||
|
>
|
||||||
|
{assistant.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p
|
))}
|
||||||
className="text-main-view-fg/50 mt-1 line-clamp-2"
|
|
||||||
title={assistant.description}
|
|
||||||
>
|
|
||||||
{assistant.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div
|
<div
|
||||||
className="bg-main-view p-3 rounded-md border border-dashed border-main-view-fg/10 flex items-center justify-center cursor-pointer hover:bg-main-view-fg/1 transition-all duration-200 ease-in-out"
|
className="bg-main-view p-3 min-h-[88px] rounded-md border border-dashed border-main-view-fg/10 flex items-center justify-center cursor-pointer hover:bg-main-view-fg/1 transition-all duration-200 ease-in-out"
|
||||||
key="new-assistant"
|
key="new-assistant"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingKey(null)
|
setEditingKey(null)
|
||||||
|
|||||||
@ -363,6 +363,46 @@ function Hub() {
|
|||||||
// Check if we're on the last step
|
// Check if we're on the last step
|
||||||
const isLastStep = currentStepIndex === steps.length - 1
|
const isLastStep = currentStepIndex === steps.length - 1
|
||||||
|
|
||||||
|
const renderFilter = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<span className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium">
|
||||||
|
{
|
||||||
|
sortOptions.find((option) => option.value === sortSelected)
|
||||||
|
?.name
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="bottom" align="end">
|
||||||
|
{sortOptions.map((option) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer my-0.5',
|
||||||
|
sortSelected === option.value && 'bg-main-view-fg/5'
|
||||||
|
)}
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => setSortSelected(option.value)}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={showOnlyDownloaded}
|
||||||
|
onCheckedChange={setShowOnlyDownloaded}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-main-view-fg/70 font-medium whitespace-nowrap">
|
||||||
|
{t('hub:downloaded')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Joyride
|
<Joyride
|
||||||
@ -377,6 +417,7 @@ function Hub() {
|
|||||||
showSkipButton={!isLastStep}
|
showSkipButton={!isLastStep}
|
||||||
hideCloseButton={true}
|
hideCloseButton={true}
|
||||||
spotlightClicks={true}
|
spotlightClicks={true}
|
||||||
|
disableOverlay={IS_LINUX}
|
||||||
disableOverlayClose={true}
|
disableOverlayClose={true}
|
||||||
callback={handleJoyrideCallback}
|
callback={handleJoyrideCallback}
|
||||||
locale={{
|
locale={{
|
||||||
@ -395,9 +436,12 @@ function Hub() {
|
|||||||
<div className="pr-4 py-3 h-10 w-full flex items-center justify-between relative z-20">
|
<div className="pr-4 py-3 h-10 w-full flex items-center justify-between relative z-20">
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<Loader className="size-4 animate-spin text-main-view-fg/60" />
|
<Loader className="shrink-0 size-4 animate-spin text-main-view-fg/60" />
|
||||||
) : (
|
) : (
|
||||||
<IconSearch className="text-main-view-fg/60" size={14} />
|
<IconSearch
|
||||||
|
className="shrink-0 text-main-view-fg/60"
|
||||||
|
size={14}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
placeholder={t('hub:searchPlaceholder')}
|
placeholder={t('hub:searchPlaceholder')}
|
||||||
@ -406,49 +450,13 @@ function Hub() {
|
|||||||
className="w-full focus:outline-none"
|
className="w-full focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="sm:flex items-center gap-2 shrink-0 hidden">
|
||||||
<DropdownMenu>
|
{renderFilter()}
|
||||||
<DropdownMenuTrigger>
|
|
||||||
<span
|
|
||||||
title={t('hub:editTheme')}
|
|
||||||
className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium"
|
|
||||||
>
|
|
||||||
{
|
|
||||||
sortOptions.find(
|
|
||||||
(option) => option.value === sortSelected
|
|
||||||
)?.name
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent side="bottom" align="end">
|
|
||||||
{sortOptions.map((option) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className={cn(
|
|
||||||
'cursor-pointer my-0.5',
|
|
||||||
sortSelected === option.value && 'bg-main-view-fg/5'
|
|
||||||
)}
|
|
||||||
key={option.value}
|
|
||||||
onClick={() => setSortSelected(option.value)}
|
|
||||||
>
|
|
||||||
{option.name}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
checked={showOnlyDownloaded}
|
|
||||||
onCheckedChange={setShowOnlyDownloaded}
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-main-view-fg/70 font-medium whitespace-nowrap">
|
|
||||||
{t('hub:downloaded')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="p-4 w-full h-[calc(100%-32px)] !overflow-y-auto first-step-setup-local-provider">
|
<div className="p-4 w-full h-[calc(100%-32px)] !overflow-y-auto first-step-setup-local-provider">
|
||||||
<div className="flex flex-col h-full justify-between gap-4 gap-y-3 w-4/5 mx-auto">
|
<div className="flex flex-col h-full justify-between gap-4 gap-y-3 w-full md:w-4/5 mx-auto">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<div className="text-center text-muted-foreground">
|
<div className="text-center text-muted-foreground">
|
||||||
@ -463,6 +471,9 @@ function Hub() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pb-2 mb-2 gap-2 ">
|
<div className="flex flex-col pb-2 mb-2 gap-2 ">
|
||||||
|
<div className="flex items-center gap-2 justify-end sm:hidden">
|
||||||
|
{renderFilter()}
|
||||||
|
</div>
|
||||||
{filteredModels.map((model) => (
|
{filteredModels.map((model) => (
|
||||||
<div key={model.id}>
|
<div key={model.id}>
|
||||||
<Card
|
<Card
|
||||||
@ -476,11 +487,14 @@ function Hub() {
|
|||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-main-view-fg font-medium text-base capitalize truncate',
|
'text-main-view-fg font-medium text-base capitalize truncate max-w-38 sm:max-w-none',
|
||||||
isRecommendedModel(model.metadata?.id)
|
isRecommendedModel(model.metadata?.id)
|
||||||
? 'hub-model-card-step'
|
? 'hub-model-card-step'
|
||||||
: ''
|
: ''
|
||||||
)}
|
)}
|
||||||
|
title={
|
||||||
|
extractModelName(model.metadata?.id) || ''
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{extractModelName(model.metadata?.id) || ''}
|
{extractModelName(model.metadata?.id) || ''}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@ -53,8 +53,8 @@ function Index() {
|
|||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
<DropdownAssistant />
|
<DropdownAssistant />
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="h-full px-8 overflow-y-auto flex flex-col gap-2 justify-center">
|
<div className="h-full px-4 md:px-8 overflow-y-auto flex flex-col gap-2 justify-center">
|
||||||
<div className="w-4/6 mx-auto">
|
<div className="w-full md:w-4/6 mx-auto">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<h1 className="font-editorialnew text-main-view-fg text-4xl">
|
<h1 className="font-editorialnew text-main-view-fg text-4xl">
|
||||||
{t('chat:welcome')}
|
{t('chat:welcome')}
|
||||||
|
|||||||
@ -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>
|
||||||
@ -273,13 +275,15 @@ function General() {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1 ">
|
||||||
<span
|
<div className="">
|
||||||
title={janDataFolder}
|
<span
|
||||||
className="bg-main-view-fg/10 text-xs px-1 py-0.5 rounded-sm text-main-view-fg/80"
|
title={janDataFolder}
|
||||||
>
|
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}
|
>
|
||||||
</span>
|
{janDataFolder}
|
||||||
|
</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,
|
||||||
@ -212,6 +211,7 @@ function ProviderDetail() {
|
|||||||
showSkipButton={true}
|
showSkipButton={true}
|
||||||
hideCloseButton={true}
|
hideCloseButton={true}
|
||||||
spotlightClicks={true}
|
spotlightClicks={true}
|
||||||
|
disableOverlay={IS_LINUX}
|
||||||
disableOverlayClose={true}
|
disableOverlayClose={true}
|
||||||
callback={handleJoyrideCallback}
|
callback={handleJoyrideCallback}
|
||||||
locale={{
|
locale={{
|
||||||
@ -227,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
|
||||||
@ -460,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 === 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="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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ import { useAppearance } from '@/hooks/useAppearance'
|
|||||||
import { ContentType, ThreadMessage } from '@janhq/core'
|
import { ContentType, ThreadMessage } from '@janhq/core'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { useChat } from '@/hooks/useChat'
|
import { useChat } from '@/hooks/useChat'
|
||||||
|
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||||
|
|
||||||
// as route.threadsDetail
|
// as route.threadsDetail
|
||||||
export const Route = createFileRoute('/threads/$threadId')({
|
export const Route = createFileRoute('/threads/$threadId')({
|
||||||
@ -42,6 +43,7 @@ function ThreadDetail() {
|
|||||||
const { streamingContent } = useAppState()
|
const { streamingContent } = useAppState()
|
||||||
const { appMainViewBgColor, chatWidth } = useAppearance()
|
const { appMainViewBgColor, chatWidth } = useAppearance()
|
||||||
const { sendMessage } = useChat()
|
const { sendMessage } = useChat()
|
||||||
|
const isSmallScreen = useSmallScreen()
|
||||||
|
|
||||||
const { messages } = useMessages(
|
const { messages } = useMessages(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
@ -250,7 +252,8 @@ function ThreadDetail() {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-4/6 mx-auto flex max-w-full flex-col grow',
|
'w-4/6 mx-auto flex max-w-full flex-col grow',
|
||||||
chatWidth === 'compact' ? 'w-4/6' : 'w-full'
|
chatWidth === 'compact' ? 'w-full md:w-4/6' : 'w-full',
|
||||||
|
isSmallScreen && 'w-full'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{messages &&
|
{messages &&
|
||||||
@ -290,8 +293,9 @@ function ThreadDetail() {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
' mx-auto pt-2 pb-3 shrink-0 relative',
|
'mx-auto pt-2 pb-3 shrink-0 relative px-2',
|
||||||
chatWidth === 'compact' ? 'w-4/6' : 'w-full px-3'
|
chatWidth === 'compact' ? 'w-full md:w-4/6' : 'w-full',
|
||||||
|
isSmallScreen && 'w-full'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user