Merge branch 'main' into fix1089

This commit is contained in:
Hieu 2023-12-20 14:42:21 +09:00 committed by GitHub
commit 7a08526784
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 370 additions and 271 deletions

View File

@ -132,14 +132,14 @@ jobs:
continue-on-error: true continue-on-error: true
run: | run: |
ls -al ./electron/dist ls -al ./electron/dist
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.zip" --body "./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.zip" --content-type "application/zip" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.zip" --body "./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.zip" --content-type "application/zip"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.zip" --body "./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.zip" --content-type "application/zip" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.zip" --body "./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.zip" --content-type "application/zip"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/latest-mac.yml" --body "./electron/dist/latest-mac.yml" --content-type "text/yaml" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/latest-mac.yml" --body "./electron/dist/latest-mac.yml" --content-type "text/yaml"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/latest-mac.yml" --body "./electron/dist/latest-mac.yml" --content-type "text/yaml" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/latest-mac.yml" --body "./electron/dist/latest-mac.yml" --content-type "text/yaml"
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
@ -203,15 +203,22 @@ jobs:
jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}]' electron/package.json > /tmp/package.json jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}]' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/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
- name: Install AzureSignTool
run: |
dotnet tool install --global AzureSignTool
- name: Build app - name: Build app
run: | run: |
make build make build
env:
- name: Windows Code Sign with AzureSignTool AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
run: | AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
dotnet tool install --global AzureSignTool AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
cd ./electron/dist AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
azuresigntool.exe sign -kvu "${{ secrets.AZURE_KEY_VAULT_URI }}" -kvi "${{ secrets.AZURE_CLIENT_ID }}" -kvt "${{ secrets.AZURE_TENANT_ID }}" -kvs "${{ secrets.AZURE_CLIENT_SECRET }}" -kvc ${{ secrets.AZURE_CERT_NAME }} -tr http://timestamp.globalsign.com/tsa/r6advanced1 -v "jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe" AZURE_CERT_NAME: ${{ secrets.AZURE_CERT_NAME }}
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
@ -223,10 +230,11 @@ jobs:
shell: bash shell: bash
run: | run: |
ls -al ./electron/dist ls -al ./electron/dist
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe" --body "./electron/dist/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe" --content-type "application/octet-stream" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe" --body "./electron/dist/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe" --body "./electron/dist/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe" --content-type "application/octet-stream" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe.blockmap" --body "./electron/dist/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe.blockmap"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/latest.yml" --body "./electron/dist/latest.yml" --content-type "text/yaml" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe" --body "./electron/dist/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/latest.yml" --body "./electron/dist/latest.yml" --content-type "text/yaml" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/latest.yml" --body "./electron/dist/latest.yml"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/latest.yml" --body "./electron/dist/latest.yml"
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
@ -305,10 +313,10 @@ jobs:
- name: put-object using awscli s3api - name: put-object using awscli s3api
run: | run: |
ls -al ./electron/dist ls -al ./electron/dist
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --body "./electron/dist/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --content-type "application/octet-stream" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --body "./electron/dist/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --content-type "application/octet-stream"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --body "./electron/dist/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --content-type "application/octet-stream" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --body "./electron/dist/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --content-type "application/octet-stream"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/latest-linux.yml" --body "./electron/dist/latest-linux.yml" --content-type "text/yaml" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/latest-linux.yml" --body "./electron/dist/latest-linux.yml" --content-type "text/yaml"
echo "q" | aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/latest-linux.yml" --body "./electron/dist/latest-linux.yml" --content-type "text/yaml" aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/latest-linux.yml" --body "./electron/dist/latest-linux.yml" --content-type "text/yaml"
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}

View File

@ -120,21 +120,26 @@ jobs:
fi fi
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/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
env: env:
VERSION_TAG: ${{ steps.tag.outputs.tag }} VERSION_TAG: ${{ steps.tag.outputs.tag }}
- name: Install AzureSignTool
run: |
dotnet tool install --global AzureSignTool
- name: Build app - name: Build app
run: | run: |
make build make build
env: env:
ANALYTICS_ID: ${{ secrets.JAN_APP_POSTHOG_PROJECT_API_KEY }} ANALYTICS_ID: ${{ secrets.JAN_APP_POSTHOG_PROJECT_API_KEY }}
ANALYTICS_HOST: ${{ secrets.JAN_APP_POSTHOG_URL }} ANALYTICS_HOST: ${{ secrets.JAN_APP_POSTHOG_URL }}
AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
- name: Windows Code Sign with AzureSignTool AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
run: | AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
dotnet tool install --global AzureSignTool AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
cd ./electron/dist AZURE_CERT_NAME: ${{ secrets.AZURE_CERT_NAME }}
azuresigntool.exe sign -kvu "${{ secrets.AZURE_KEY_VAULT_URI }}" -kvi "${{ secrets.AZURE_CLIENT_ID }}" -kvt "${{ secrets.AZURE_TENANT_ID }}" -kvs "${{ secrets.AZURE_CLIENT_SECRET }}" -kvc ${{ secrets.AZURE_CERT_NAME }} -tr http://timestamp.globalsign.com/tsa/r6advanced1 -v "jan-win-x64-${{ needs.create-draft-release.outputs.version }}.exe"
- uses: actions/upload-release-asset@v1.0.1 - uses: actions/upload-release-asset@v1.0.1
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
@ -154,7 +159,7 @@ jobs:
upload_url: ${{ needs.create-draft-release.outputs.upload_url }} upload_url: ${{ needs.create-draft-release.outputs.upload_url }}
asset_path: ./electron/dist/jan-win-x64-${{ needs.create-draft-release.outputs.version }}.exe.blockmap asset_path: ./electron/dist/jan-win-x64-${{ needs.create-draft-release.outputs.version }}.exe.blockmap
asset_name: jan-win-x64-${{ needs.create-draft-release.outputs.version }}.exe.blockmap asset_name: jan-win-x64-${{ needs.create-draft-release.outputs.version }}.exe.blockmap
asset_content_type: text/xml asset_content_type: application/octet-stream
- uses: actions/upload-release-asset@v1.0.1 - uses: actions/upload-release-asset@v1.0.1
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')

View File

@ -70,7 +70,7 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
<tr style="text-align: center"> <tr style="text-align: center">
<td style="text-align:center"><b>Experimental (Nighlty Build)</b></td> <td style="text-align:center"><b>Experimental (Nighlty Build)</b></td>
<td style="text-align:center" colspan="4"> <td style="text-align:center" colspan="4">
<a href='https://github.com/janhq/jan/actions/runs/7253202390'> <a href='https://github.com/janhq/jan/actions/runs/7266993261'>
<b>Github action artifactory</b> <b>Github action artifactory</b>
</a> </a>
</td> </td>

View File

@ -45,7 +45,10 @@
"icon": "icons/" "icon": "icons/"
}, },
"win": { "win": {
"icon": "icons/icon.png" "icon": "icons/icon.png",
"target": [
"nsis"
]
}, },
"artifactName": "jan-${os}-${arch}-${version}.${ext}" "artifactName": "jan-${os}-${arch}-${version}.${ext}"
}, },
@ -73,7 +76,7 @@
"@types/request": "^2.48.12", "@types/request": "^2.48.12",
"@uiball/loaders": "^1.3.0", "@uiball/loaders": "^1.3.0",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"electron-updater": "^6.1.4", "electron-updater": "^6.1.7",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"pacote": "^17.0.4", "pacote": "^17.0.4",
"request": "^2.88.2", "request": "^2.88.2",
@ -88,7 +91,7 @@
"@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3", "@typescript-eslint/parser": "^6.7.3",
"electron": "28.0.0", "electron": "28.0.0",
"electron-builder": "^24.6.4", "electron-builder": "^24.9.1",
"electron-playwright-helpers": "^1.6.0", "electron-playwright-helpers": "^1.6.0",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"run-script-os": "^1.1.6" "run-script-os": "^1.1.6"

44
electron/sign.js Normal file
View File

@ -0,0 +1,44 @@
const { exec } = require('child_process');
function sign({ path, name, certUrl, clientId, tenantId, clientSecret, certName, timestampServer, version }) {
return new Promise((resolve, reject) => {
const command = `azuresigntool.exe sign -kvu "${certUrl}" -kvi "${clientId}" -kvt "${tenantId}" -kvs "${clientSecret}" -kvc "${certName}" -tr "${timestampServer}" -v "${path}"`;
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`Error: ${error}`);
return reject(error);
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
resolve();
});
});
}
exports.default = async function(options) {
const certUrl = process.env.AZURE_KEY_VAULT_URI;
const clientId = process.env.AZURE_CLIENT_ID;
const tenantId = process.env.AZURE_TENANT_ID;
const clientSecret = process.env.AZURE_CLIENT_SECRET;
const certName = process.env.AZURE_CERT_NAME;
const timestampServer = 'http://timestamp.globalsign.com/tsa/r6advanced1';
await sign({
path: options.path,
name: "jan-win-x64",
certUrl,
clientId,
tenantId,
clientSecret,
certName,
timestampServer,
version: options.version
});
};

View File

@ -55,7 +55,7 @@ export default class JSONConversationalExtension
convos.sort( convos.sort(
(a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime() (a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime()
) )
console.debug('getThreads', JSON.stringify(convos, null, 2))
return convos return convos
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View File

@ -1 +1 @@
0.1.30 0.1.32

View File

@ -227,7 +227,7 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
events.emit(EventName.OnMessageUpdate, message); events.emit(EventName.OnMessageUpdate, message);
}, },
error: async (err) => { error: async (err) => {
if (instance.isCancelled) { if (instance.isCancelled || message.content.length > 0) {
message.status = MessageStatus.Ready; message.status = MessageStatus.Ready;
events.emit(EventName.OnMessageUpdate, message); events.emit(EventName.OnMessageUpdate, message);
return; return;

View File

@ -71,7 +71,7 @@ async function loadModel(nitroResourceProbe: any | undefined) {
.then(() => loadLLMModel(currentSettings)) .then(() => loadLLMModel(currentSettings))
.then(validateModelStatus) .then(validateModelStatus)
.catch((err) => { .catch((err) => {
console.log("error: ", err); console.error("error: ", err);
// TODO: Broadcast error so app could display proper error message // TODO: Broadcast error so app could display proper error message
return { error: err, currentModelFile }; return { error: err, currentModelFile };
}); });
@ -172,7 +172,7 @@ async function validateModelStatus(): Promise<ModelOperationResponse> {
async function killSubprocess(): Promise<void> { async function killSubprocess(): Promise<void> {
const controller = new AbortController(); const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); setTimeout(() => controller.abort(), 5000);
console.log("Start requesting to kill Nitro..."); console.debug("Start requesting to kill Nitro...");
return fetch(NITRO_HTTP_KILL_URL, { return fetch(NITRO_HTTP_KILL_URL, {
method: "DELETE", method: "DELETE",
signal: controller.signal, signal: controller.signal,
@ -183,7 +183,7 @@ async function killSubprocess(): Promise<void> {
}) })
.catch(() => {}) .catch(() => {})
.then(() => tcpPortUsed.waitUntilFree(PORT, 300, 5000)) .then(() => tcpPortUsed.waitUntilFree(PORT, 300, 5000))
.then(() => console.log("Nitro is killed")); .then(() => console.debug("Nitro is killed"));
} }
/** /**
* Look for the Nitro binary and execute it * Look for the Nitro binary and execute it
@ -191,7 +191,7 @@ async function killSubprocess(): Promise<void> {
* Should run exactly platform specified Nitro binary version * Should run exactly platform specified Nitro binary version
*/ */
function spawnNitroProcess(nitroResourceProbe: any): Promise<any> { function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
console.log("Starting Nitro subprocess..."); console.debug("Starting Nitro subprocess...");
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let binaryFolder = path.join(__dirname, "bin"); // Current directory by default let binaryFolder = path.join(__dirname, "bin"); // Current directory by default
let binaryName; let binaryName;
@ -221,7 +221,7 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
}); });
subprocess.stderr.on("data", (data) => { subprocess.stderr.on("data", (data) => {
console.log("subprocess error:" + data.toString()); console.error("subprocess error:" + data.toString());
console.error(`stderr: ${data}`); console.error(`stderr: ${data}`);
}); });

View File

@ -217,7 +217,7 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension {
events.emit(EventName.OnMessageUpdate, message); events.emit(EventName.OnMessageUpdate, message);
}, },
error: async (err) => { error: async (err) => {
if (instance.isCancelled) { if (instance.isCancelled || message.content.length > 0) {
message.status = MessageStatus.Ready; message.status = MessageStatus.Ready;
events.emit(EventName.OnMessageUpdate, message); events.emit(EventName.OnMessageUpdate, message);
return; return;

View File

@ -1,6 +1,6 @@
.btn { .btn {
@apply inline-flex items-center justify-center whitespace-nowrap rounded-lg font-semibold transition-colors; @apply inline-flex items-center justify-center whitespace-nowrap rounded-lg font-semibold transition-colors;
@apply focus-visible:ring-ring cursor-pointer focus-visible:outline-none focus-visible:ring-1; @apply cursor-pointer focus:outline-none focus-visible:outline-none focus-visible:ring-0;
@apply disabled:pointer-events-none disabled:opacity-50; @apply disabled:pointer-events-none disabled:opacity-50;
&-primary { &-primary {
@ -28,7 +28,7 @@
} }
&-ghost { &-ghost {
@apply hover:bg-primary hover:text-primary-foreground; @apply hover:bg-secondary hover:text-secondary-foreground;
} }
&-sm { &-sm {

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import { FieldValues, UseFormRegister } from 'react-hook-form'
import { useEffect, useState } from 'react'
import { ModelRuntimeParams } from '@janhq/core'
import { Switch } from '@janhq/uikit' import { Switch } from '@janhq/uikit'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
@ -16,44 +16,32 @@ type Props = {
name: string name: string
title: string title: string
checked: boolean checked: boolean
register: any register: UseFormRegister<FieldValues>
} }
const Checkbox: React.FC<Props> = ({ name, title, checked, register }) => { const Checkbox: React.FC<Props> = ({ name, title, checked, register }) => {
const [currentChecked, setCurrentChecked] = useState<boolean>(checked)
const { updateModelParameter } = useUpdateModelParameters() const { updateModelParameter } = useUpdateModelParameters()
const threadId = useAtomValue(getActiveThreadIdAtom) const threadId = useAtomValue(getActiveThreadIdAtom)
const activeModelParams = useAtomValue(getActiveThreadModelRuntimeParamsAtom) const activeModelParams = useAtomValue(getActiveThreadModelRuntimeParamsAtom)
useEffect(() => { const onCheckedChange = (checked: boolean) => {
setCurrentChecked(checked) if (!threadId || !activeModelParams) return
}, [checked])
useEffect(() => { const updatedModelParams: ModelRuntimeParams = {
updateSetting()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentChecked])
const updateValue = [name].reduce((accumulator, value) => {
return { ...accumulator, [value]: currentChecked }
}, {})
const updateSetting = () => {
return updateModelParameter(String(threadId), {
...activeModelParams, ...activeModelParams,
...updateValue, [name]: checked,
}) }
updateModelParameter(threadId, updatedModelParams)
} }
return ( return (
<div className="flex justify-between"> <div className="flex justify-between">
<label>{title}</label> <label>{title}</label>
<Switch <Switch
checked={currentChecked} checked={checked}
{...register(name)} {...register(name)}
onCheckedChange={(e) => { onCheckedChange={onCheckedChange}
setCurrentChecked(e)
}}
/> />
</div> </div>
) )

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import { FieldValues, UseFormRegister } from 'react-hook-form'
import { useEffect, useState } from 'react'
import { ModelRuntimeParams } from '@janhq/core'
import { Slider, Input } from '@janhq/uikit' import { Slider, Input } from '@janhq/uikit'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
@ -18,7 +18,7 @@ type Props = {
max: number max: number
step: number step: number
value: number value: number
register: any register: UseFormRegister<FieldValues>
} }
const SliderRightPanel: React.FC<Props> = ({ const SliderRightPanel: React.FC<Props> = ({
@ -30,30 +30,19 @@ const SliderRightPanel: React.FC<Props> = ({
value, value,
register, register,
}) => { }) => {
const [currentValue, setCurrentValue] = useState<number>(value)
const { updateModelParameter } = useUpdateModelParameters() const { updateModelParameter } = useUpdateModelParameters()
const threadId = useAtomValue(getActiveThreadIdAtom) const threadId = useAtomValue(getActiveThreadIdAtom)
const activeModelParams = useAtomValue(getActiveThreadModelRuntimeParamsAtom) const activeModelParams = useAtomValue(getActiveThreadModelRuntimeParamsAtom)
useEffect(() => { const onValueChanged = (e: number[]) => {
setCurrentValue(value) if (!threadId || !activeModelParams) return
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value])
useEffect(() => { const updatedModelParams: ModelRuntimeParams = {
updateSetting()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentValue])
const updateValue = [name].reduce((accumulator, value) => {
return { ...accumulator, [value]: currentValue }
}, {})
const updateSetting = () => {
return updateModelParameter(String(threadId), {
...activeModelParams, ...activeModelParams,
...updateValue, [name]: Number(e[0]),
}) }
updateModelParameter(threadId, updatedModelParams)
} }
return ( return (
@ -63,14 +52,10 @@ const SliderRightPanel: React.FC<Props> = ({
<div className="relative w-full"> <div className="relative w-full">
<Slider <Slider
{...register(name, { {...register(name, {
setValueAs: (v: any) => parseInt(v), setValueAs: (v: string) => parseInt(v),
})} })}
value={[currentValue]} value={[value]}
onValueChange={async (e) => { onValueChange={onValueChanged}
setCurrentValue(Number(e[0]))
await updateSetting()
}}
type="range"
min={min} min={min}
max={max} max={max}
step={step} step={step}
@ -87,11 +72,8 @@ const SliderRightPanel: React.FC<Props> = ({
className="-mt-4 h-8 w-16" className="-mt-4 h-8 w-16"
min={min} min={min}
max={max} max={max}
value={String(currentValue)} value={String(value)}
onChange={async (e) => { onChange={(e) => onValueChanged([Number(e.target.value)])}
setCurrentValue(Number(e.target.value))
await updateSetting()
}}
/> />
</div> </div>
</div> </div>

View File

@ -60,19 +60,21 @@ export const addOldMessagesAtom = atom(
export const addNewMessageAtom = atom( export const addNewMessageAtom = atom(
null, null,
(get, set, newMessage: ThreadMessage) => { (get, set, newMessage: ThreadMessage) => {
const threadId = get(getActiveThreadIdAtom) const currentMessages = get(chatMessages)[newMessage.thread_id] ?? []
if (!threadId) return
const currentMessages = get(chatMessages)[threadId] ?? []
const updatedMessages = [...currentMessages, newMessage] const updatedMessages = [...currentMessages, newMessage]
const newData: Record<string, ThreadMessage[]> = { const newData: Record<string, ThreadMessage[]> = {
...get(chatMessages), ...get(chatMessages),
} }
newData[threadId] = updatedMessages newData[newMessage.thread_id] = updatedMessages
set(chatMessages, newData) set(chatMessages, newData)
// Update thread last message // Update thread last message
set(updateThreadStateLastMessageAtom, threadId, newMessage.content) set(
updateThreadStateLastMessageAtom,
newMessage.thread_id,
newMessage.content
)
} }
) )

View File

@ -5,11 +5,14 @@ import {
Thread, Thread,
ThreadAssistantInfo, ThreadAssistantInfo,
ThreadState, ThreadState,
Model,
} from '@janhq/core' } from '@janhq/core'
import { atom, useAtomValue, useSetAtom } from 'jotai' import { atom, useAtomValue, useSetAtom } from 'jotai'
import { generateThreadId } from '@/utils/thread' import { generateThreadId } from '@/utils/thread'
import useDeleteThread from './useDeleteThread'
import { extensionManager } from '@/extension' import { extensionManager } from '@/extension'
import { import {
threadsAtom, threadsAtom,
@ -46,27 +49,33 @@ export const useCreateNewThread = () => {
setThreadModelRuntimeParamsAtom setThreadModelRuntimeParamsAtom
) )
const requestCreateNewThread = async (assistant: Assistant) => { const { deleteThread } = useDeleteThread()
const requestCreateNewThread = async (
assistant: Assistant,
model?: Model | undefined
) => {
// loop through threads state and filter if there's any thread that is not finish init // loop through threads state and filter if there's any thread that is not finish init
let hasUnfinishedInitThread = false let unfinishedInitThreadId: string | undefined = undefined
for (const key in threadStates) { for (const key in threadStates) {
const isFinishInit = threadStates[key].isFinishInit ?? true const isFinishInit = threadStates[key].isFinishInit ?? true
if (!isFinishInit) { if (!isFinishInit) {
hasUnfinishedInitThread = true unfinishedInitThreadId = key
break break
} }
} }
if (hasUnfinishedInitThread) { if (unfinishedInitThreadId) {
return await deleteThread(unfinishedInitThreadId)
} }
const modelId = model ? model.id : '*'
const createdAt = Date.now() const createdAt = Date.now()
const assistantInfo: ThreadAssistantInfo = { const assistantInfo: ThreadAssistantInfo = {
assistant_id: assistant.id, assistant_id: assistant.id,
assistant_name: assistant.name, assistant_name: assistant.name,
model: { model: {
id: '*', id: modelId,
settings: {}, settings: {},
parameters: { parameters: {
stream: true, stream: true,

View File

@ -1,5 +1,9 @@
import { ChatCompletionRole, ExtensionType } from '@janhq/core' import {
import { ConversationalExtension } from '@janhq/core' ChatCompletionRole,
ExtensionType,
ConversationalExtension,
} from '@janhq/core'
import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { currentPromptAtom } from '@/containers/Providers/Jotai' import { currentPromptAtom } from '@/containers/Providers/Jotai'
@ -19,6 +23,7 @@ import {
threadsAtom, threadsAtom,
setActiveThreadIdAtom, setActiveThreadIdAtom,
deleteThreadStateAtom, deleteThreadStateAtom,
threadStatesAtom,
} from '@/helpers/atoms/Thread.atom' } from '@/helpers/atoms/Thread.atom'
export default function useDeleteThread() { export default function useDeleteThread() {
@ -32,6 +37,8 @@ export default function useDeleteThread() {
const cleanMessages = useSetAtom(cleanChatMessagesAtom) const cleanMessages = useSetAtom(cleanChatMessagesAtom)
const deleteThreadState = useSetAtom(deleteThreadStateAtom) const deleteThreadState = useSetAtom(deleteThreadStateAtom)
const threadStates = useAtomValue(threadStatesAtom)
const cleanThread = async (threadId: string) => { const cleanThread = async (threadId: string) => {
if (threadId) { if (threadId) {
const thread = threads.filter((c) => c.id === threadId)[0] const thread = threads.filter((c) => c.id === threadId)[0]
@ -59,15 +66,21 @@ export default function useDeleteThread() {
const availableThreads = threads.filter((c) => c.id !== threadId) const availableThreads = threads.filter((c) => c.id !== threadId)
setThreads(availableThreads) setThreads(availableThreads)
const deletingThreadState = threadStates[threadId]
const isFinishInit = deletingThreadState?.isFinishInit ?? true
// delete the thread state // delete the thread state
deleteThreadState(threadId) deleteThreadState(threadId)
deleteMessages(threadId) if (isFinishInit) {
setCurrentPrompt('') deleteMessages(threadId)
toaster({ setCurrentPrompt('')
title: 'Thread successfully deleted.', toaster({
description: `Thread with ${activeModel?.name} has been successfully deleted.`, title: 'Thread successfully deleted.',
}) description: `Thread with ${activeModel?.name} has been successfully deleted.`,
})
}
if (availableThreads.length > 0) { if (availableThreads.length > 0) {
setActiveThreadId(availableThreads[0].id) setActiveThreadId(availableThreads[0].id)
} else { } else {

View File

@ -1,58 +0,0 @@
import { ExtensionType, ModelRuntimeParams, ThreadState } from '@janhq/core'
import { ConversationalExtension } from '@janhq/core'
import { useSetAtom } from 'jotai'
import { extensionManager } from '@/extension/ExtensionManager'
import {
threadModelRuntimeParamsAtom,
threadStatesAtom,
threadsAtom,
} from '@/helpers/atoms/Thread.atom'
const useGetAllThreads = () => {
const setThreadStates = useSetAtom(threadStatesAtom)
const setThreads = useSetAtom(threadsAtom)
const setThreadModelRuntimeParams = useSetAtom(threadModelRuntimeParamsAtom)
const getAllThreads = async () => {
try {
const threads =
(await extensionManager
.get<ConversationalExtension>(ExtensionType.Conversational)
?.getThreads()) ?? []
const threadStates: Record<string, ThreadState> = {}
const threadModelParams: Record<string, ModelRuntimeParams> = {}
threads.forEach((thread) => {
if (thread.id != null) {
const lastMessage = (thread.metadata?.lastMessage as string) ?? ''
threadStates[thread.id] = {
hasMore: true,
waitingForResponse: false,
lastMessage,
isFinishInit: true,
}
// model params
const modelParams = thread.assistants?.[0]?.model?.parameters
threadModelParams[thread.id] = modelParams
}
})
// updating app states
setThreadStates(threadStates)
setThreads(threads)
setThreadModelRuntimeParams(threadModelParams)
} catch (error) {
console.error(error)
}
}
return {
getAllThreads,
}
}
export default useGetAllThreads

View File

@ -4,13 +4,10 @@ import { Assistant, ExtensionType, AssistantExtension } from '@janhq/core'
import { extensionManager } from '@/extension/ExtensionManager' import { extensionManager } from '@/extension/ExtensionManager'
export const getAssistants = async (): Promise<Assistant[]> => { export const getAssistants = async (): Promise<Assistant[]> =>
return ( extensionManager
extensionManager .get<AssistantExtension>(ExtensionType.Assistant)
.get<AssistantExtension>(ExtensionType.Assistant) ?.getAssistants() ?? []
?.getAssistants() ?? []
)
}
/** /**
* Hooks for get assistants * Hooks for get assistants

View File

@ -57,6 +57,17 @@ export default function useRecommendedModel() {
} }
return return
} else {
const modelId = activeThread.assistants[0]?.model.id
if (modelId !== '*') {
const models = await getAndSortDownloadedModels()
const model = models.find((model) => model.id === modelId)
if (model) {
setRecommendedModel(model)
}
return
}
} }
if (activeModel) { if (activeModel) {

95
web/hooks/useThreads.ts Normal file
View File

@ -0,0 +1,95 @@
import {
ExtensionType,
ModelRuntimeParams,
Thread,
ThreadState,
} from '@janhq/core'
import { ConversationalExtension } from '@janhq/core'
import { useAtom } from 'jotai'
import { extensionManager } from '@/extension/ExtensionManager'
import {
threadModelRuntimeParamsAtom,
threadStatesAtom,
threadsAtom,
} from '@/helpers/atoms/Thread.atom'
const useThreads = () => {
const [threadStates, setThreadStates] = useAtom(threadStatesAtom)
const [threads, setThreads] = useAtom(threadsAtom)
const [threadModelRuntimeParams, setThreadModelRuntimeParams] = useAtom(
threadModelRuntimeParamsAtom
)
const getThreads = async () => {
try {
const localThreads = await getLocalThreads()
const localThreadStates: Record<string, ThreadState> = {}
const threadModelParams: Record<string, ModelRuntimeParams> = {}
localThreads.forEach((thread) => {
if (thread.id != null) {
const lastMessage = (thread.metadata?.lastMessage as string) ?? ''
localThreadStates[thread.id] = {
hasMore: false,
waitingForResponse: false,
lastMessage,
isFinishInit: true,
}
// model params
const modelParams = thread.assistants?.[0]?.model?.parameters
threadModelParams[thread.id] = modelParams
}
})
// allow at max 1 unfinished init thread and it should be at the top of the list
let unfinishedThreadId: string | undefined = undefined
const unfinishedThreadState: Record<string, ThreadState> = {}
for (const key of Object.keys(threadStates)) {
const threadState = threadStates[key]
if (threadState.isFinishInit === false) {
unfinishedThreadState[key] = threadState
unfinishedThreadId = key
break
}
}
const unfinishedThread: Thread | undefined = threads.find(
(thread) => thread.id === unfinishedThreadId
)
let allThreads: Thread[] = [...localThreads]
if (unfinishedThread) {
allThreads = [unfinishedThread, ...localThreads]
}
if (unfinishedThreadId) {
localThreadStates[unfinishedThreadId] =
unfinishedThreadState[unfinishedThreadId]
threadModelParams[unfinishedThreadId] =
threadModelRuntimeParams[unfinishedThreadId]
}
// updating app states
setThreadStates(localThreadStates)
setThreads(allThreads)
setThreadModelRuntimeParams(threadModelParams)
} catch (error) {
console.error(error)
}
}
return {
getAllThreads: getThreads,
}
}
const getLocalThreads = async (): Promise<Thread[]> =>
(await extensionManager
.get<ConversationalExtension>(ExtensionType.Conversational)
?.getThreads()) ?? []
export default useThreads

View File

@ -1,5 +1,3 @@
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { ModelRuntimeParams } from '@janhq/core' import { ModelRuntimeParams } from '@janhq/core'
@ -11,42 +9,31 @@ import settingComponentBuilder, {
SettingComponentData, SettingComponentData,
} from './settingComponentBuilder' } from './settingComponentBuilder'
import { import { getActiveThreadModelRuntimeParamsAtom } from '@/helpers/atoms/Thread.atom'
getActiveThreadIdAtom,
getActiveThreadModelRuntimeParamsAtom,
} from '@/helpers/atoms/Thread.atom'
export default function ModelSetting() { export default function ModelSetting() {
const threadId = useAtomValue(getActiveThreadIdAtom)
const activeModelParams = useAtomValue(getActiveThreadModelRuntimeParamsAtom)
const [modelParams, setModelParams] = useState<
ModelRuntimeParams | undefined
>(activeModelParams)
const { register } = useForm() const { register } = useForm()
const activeModelParams = useAtomValue(getActiveThreadModelRuntimeParamsAtom)
useEffect(() => { if (!activeModelParams) {
setModelParams(activeModelParams) return null
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [threadId])
if (!modelParams) {
return <div>This thread has no model parameters</div>
} }
const componentData: SettingComponentData[] = [] const componentData: SettingComponentData[] = []
Object.keys(modelParams).forEach((key) => { Object.keys(activeModelParams).forEach((key) => {
const componentSetting = presetConfiguration[key] const componentSetting = presetConfiguration[key]
if (componentSetting) { if (componentSetting) {
if ('value' in componentSetting.controllerData) { if ('value' in componentSetting.controllerData) {
componentSetting.controllerData.value = Number( componentSetting.controllerData.value = Number(
modelParams[key as keyof ModelRuntimeParams] activeModelParams[key as keyof ModelRuntimeParams]
) )
} else if ('checked' in componentSetting.controllerData) { } else if ('checked' in componentSetting.controllerData) {
componentSetting.controllerData.checked = modelParams[ const checked = activeModelParams[
key as keyof ModelRuntimeParams key as keyof ModelRuntimeParams
] as boolean ] as boolean
componentSetting.controllerData.checked = checked
} }
componentData.push(componentSetting) componentData.push(componentSetting)
} }

View File

@ -1,5 +1,5 @@
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
/* eslint-disable @typescript-eslint/no-explicit-any */ import { FieldValues, UseFormRegister } from 'react-hook-form'
import Checkbox from '@/containers/Checkbox' import Checkbox from '@/containers/Checkbox'
import Slider from '@/containers/Slider' import Slider from '@/containers/Slider'
@ -27,7 +27,7 @@ type CheckboxData = {
const settingComponentBuilder = ( const settingComponentBuilder = (
componentData: SettingComponentData[], componentData: SettingComponentData[],
register: any register: UseFormRegister<FieldValues>
) => { ) => {
const components = componentData.map((data) => { const components = componentData.map((data) => {
switch (data.controllerType) { switch (data.controllerType) {

View File

@ -1,5 +1,16 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import {
Modal,
ModalTrigger,
ModalClose,
ModalFooter,
ModalContent,
ModalHeader,
ModalTitle,
Button,
} from '@janhq/uikit'
import { motion as m } from 'framer-motion' import { motion as m } from 'framer-motion'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { import {
@ -13,12 +24,13 @@ import { twMerge } from 'tailwind-merge'
import { useCreateNewThread } from '@/hooks/useCreateNewThread' import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import useDeleteThread from '@/hooks/useDeleteThread' import useDeleteThread from '@/hooks/useDeleteThread'
import useGetAllThreads from '@/hooks/useGetAllThreads'
import useGetAssistants from '@/hooks/useGetAssistants' import useGetAssistants from '@/hooks/useGetAssistants'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import useSetActiveThread from '@/hooks/useSetActiveThread' import useSetActiveThread from '@/hooks/useSetActiveThread'
import useThreads from '@/hooks/useThreads'
import { displayDate } from '@/utils/datetime' import { displayDate } from '@/utils/datetime'
import { import {
@ -30,7 +42,7 @@ import {
export default function ThreadList() { export default function ThreadList() {
const threads = useAtomValue(threadsAtom) const threads = useAtomValue(threadsAtom)
const threadStates = useAtomValue(threadStatesAtom) const threadStates = useAtomValue(threadStatesAtom)
const { getAllThreads } = useGetAllThreads() const { getAllThreads } = useThreads()
const { assistants } = useGetAssistants() const { assistants } = useGetAssistants()
const { requestCreateNewThread } = useCreateNewThread() const { requestCreateNewThread } = useCreateNewThread()
const activeThread = useAtomValue(activeThreadAtom) const activeThread = useAtomValue(activeThreadAtom)
@ -107,15 +119,40 @@ export default function ThreadList() {
Clean thread Clean thread
</span> </span>
</div> </div>
<div <Modal>
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary" <ModalTrigger asChild>
onClick={() => deleteThread(thread.id)} <div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
> <Trash2Icon
<Trash2Icon size={16} className="text-muted-foreground" /> size={16}
<span className="text-bold text-black dark:text-muted-foreground"> className="text-muted-foreground"
Delete thread />
</span> <span className="text-bold text-black dark:text-muted-foreground">
</div> Delete thread
</span>
</div>
</ModalTrigger>
<ModalContent>
<ModalHeader>
<ModalTitle>Delete Thread</ModalTitle>
</ModalHeader>
<p>Are you sure you want to delete this thread?</p>
<ModalFooter>
<div className="flex gap-x-2">
<ModalClose asChild>
<Button themes="ghost">No</Button>
</ModalClose>
<ModalClose asChild>
<Button
themes="danger"
onClick={() => deleteThread(thread.id)}
>
Yes
</Button>
</ModalClose>
</div>
</ModalFooter>
</ModalContent>
</Modal>
</div> </div>
</div> </div>
{/* {messages.length > 0 && ( {/* {messages.length > 0 && (

View File

@ -75,14 +75,12 @@ const ChatScreen = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [waitingToSendMessage, activeThreadId]) }, [waitingToSendMessage, activeThreadId])
const resizeTextArea = () => { useEffect(() => {
if (textareaRef.current) { if (textareaRef.current) {
textareaRef.current.style.height = '40px' textareaRef.current.style.height = '40px'
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px' textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'
} }
} }, [currentPrompt])
useEffect(resizeTextArea, [currentPrompt])
const onKeyDown = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => { const onKeyDown = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {

View File

@ -14,11 +14,10 @@ import ModalCancelDownload from '@/containers/ModalCancelDownload'
import { MainViewState } from '@/constants/screens' import { MainViewState } from '@/constants/screens'
// import { ModelPerformance, TagType } from '@/constants/tagType' import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import { useActiveModel } from '@/hooks/useActiveModel'
import useDownloadModel from '@/hooks/useDownloadModel' import useDownloadModel from '@/hooks/useDownloadModel'
import { useDownloadState } from '@/hooks/useDownloadState' import { useDownloadState } from '@/hooks/useDownloadState'
import { getAssistants } from '@/hooks/useGetAssistants'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState' import { useMainViewState } from '@/hooks/useMainViewState'
@ -34,12 +33,7 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
const { downloadModel } = useDownloadModel() const { downloadModel } = useDownloadModel()
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
const { modelDownloadStateAtom, downloadStates } = useDownloadState() const { modelDownloadStateAtom, downloadStates } = useDownloadState()
const { startModel } = useActiveModel() const { requestCreateNewThread } = useCreateNewThread()
// const [title, setTitle] = useState<string>('Recommended')
// const [performanceTag, setPerformanceTag] = useState<TagType>(
// ModelPerformance.PerformancePositive
// )
const downloadAtom = useMemo( const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[model.id]), () => atom((get) => get(modelDownloadStateAtom)[model.id]),
@ -59,10 +53,15 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
<Button onClick={() => onDownloadClick()}>Download</Button> <Button onClick={() => onDownloadClick()}>Download</Button>
) )
const onUseModelClick = () => { const onUseModelClick = useCallback(async () => {
startModel(model.id) const assistants = await getAssistants()
if (assistants.length === 0) {
alert('No assistant available')
return
}
await requestCreateNewThread(assistants[0], model)
setMainViewState(MainViewState.Thread) setMainViewState(MainViewState.Thread)
} }, [])
if (isDownloaded) { if (isDownloaded) {
downloadButton = ( downloadButton = (
@ -80,22 +79,6 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
downloadButton = <ModalCancelDownload model={model} /> downloadButton = <ModalCancelDownload model={model} />
} }
// const renderBadge = (performance: TagType) => {
// switch (performance) {
// case ModelPerformance.PerformancePositive:
// return <Badge themes="success">{title}</Badge>
// case ModelPerformance.PerformanceNeutral:
// return <Badge themes="secondary">{title}</Badge>
// case ModelPerformance.PerformanceNegative:
// return <Badge themes="danger">{title}</Badge>
// default:
// break
// }
// }
return ( return (
<div <div
className="cursor-pointer rounded-t-md bg-background/50" className="cursor-pointer rounded-t-md bg-background/50"

View File

@ -3,10 +3,6 @@ import { useState } from 'react'
import { import {
Input, Input,
ScrollArea, ScrollArea,
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipArrow,
Select, Select,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
@ -17,10 +13,6 @@ import {
import { SearchIcon } from 'lucide-react' import { SearchIcon } from 'lucide-react'
import { Code2Icon, UserIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import Loader from '@/containers/Loader' import Loader from '@/containers/Loader'
import { useGetConfiguredModels } from '@/hooks/useGetConfiguredModels' import { useGetConfiguredModels } from '@/hooks/useGetConfiguredModels'
@ -32,7 +24,6 @@ import ExploreModelList from './ExploreModelList'
const ExploreModelsScreen = () => { const ExploreModelsScreen = () => {
const { loading, models } = useGetConfiguredModels() const { loading, models } = useGetConfiguredModels()
const [searchValue, setsearchValue] = useState('') const [searchValue, setsearchValue] = useState('')
const [tabActive, setTabActive] = useState('Model')
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
const [sortSelected, setSortSelected] = useState('All Models') const [sortSelected, setSortSelected] = useState('All Models')
const sortMenu = ['All Models', 'Recommended', 'Downloaded'] const sortMenu = ['All Models', 'Recommended', 'Downloaded']
@ -45,8 +36,8 @@ const ExploreModelsScreen = () => {
) )
} else if (sortSelected === 'Recommended') { } else if (sortSelected === 'Recommended') {
return ( return (
x.metadata.tags.includes('Featured') || x.name.toLowerCase().includes(searchValue.toLowerCase()) &&
x.metadata.tags.includes('Recommended') x.metadata.tags.includes('Featured')
) )
} else { } else {
return x.name.toLowerCase().includes(searchValue.toLowerCase()) return x.name.toLowerCase().includes(searchValue.toLowerCase())

View File

@ -6,15 +6,19 @@ export default function AppearanceOptions() {
<div className="block w-full"> <div className="block w-full">
<div className="flex w-full items-center justify-between border-b border-border py-3 first:pt-0 last:border-none"> <div className="flex w-full items-center justify-between border-b border-border py-3 first:pt-0 last:border-none">
<div className="flex-shrink-0 space-y-1"> <div className="flex-shrink-0 space-y-1">
<h6 className="text-sm font-semibold capitalize">Themes</h6> <h6 className="text-sm font-semibold capitalize">
<p className="leading-relaxed ">Choose your default theme.</p> Base color scheme
</h6>
<p className="leading-relaxed ">Choose Jan default color scheme.</p>
</div> </div>
<ToggleTheme /> <ToggleTheme />
</div> </div>
<div className="flex w-full items-center justify-between border-b border-border py-3 first:pt-0 last:border-none"> <div className="flex w-full items-center justify-between border-b border-border py-3 first:pt-0 last:border-none">
<div className="flex-shrink-0 space-y-1"> <div className="flex-shrink-0 space-y-1">
<h6 className="text-sm font-semibold capitalize">Primary color</h6> <h6 className="text-sm font-semibold capitalize">Accent Color</h6>
<p className="leading-relaxed ">Choose your primary color.</p> <p className="leading-relaxed ">
Choose the accent color used throughout the app.
</p>
</div> </div>
<ToggleAccent /> <ToggleAccent />
</div> </div>

View File

@ -25,13 +25,13 @@ export default function SystemMonitorScreen() {
<ScrollArea className="h-full w-full"> <ScrollArea className="h-full w-full">
<div className="h-full p-8" data-test-id="testid-system-monitor"> <div className="h-full p-8" data-test-id="testid-system-monitor">
<div className="grid grid-cols-2 gap-8 lg:grid-cols-3"> <div className="grid grid-cols-2 gap-8 lg:grid-cols-3">
<div className="rounded-xl border border-border px-8 py-6"> <div className="rounded-xl border border-border p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-base font-bold uppercase"> <h4 className="text-base font-bold uppercase">
ram ({Math.round((usedRam / totalRam) * 100)}%) ram ({Math.round((usedRam / totalRam) * 100)}%)
</h4> </h4>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{toGigabytes(usedRam)} GB of {toGigabytes(totalRam)} GB used {toGigabytes(usedRam)} of {toGigabytes(totalRam)} used
</span> </span>
</div> </div>
<div className="mt-2"> <div className="mt-2">
@ -41,7 +41,7 @@ export default function SystemMonitorScreen() {
/> />
</div> </div>
</div> </div>
<div className="rounded-xl border border-border px-8 py-6"> <div className="rounded-xl border border-border p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-base font-bold uppercase"> <h4 className="text-base font-bold uppercase">
cpu ({cpuUsage}%) cpu ({cpuUsage}%)