feat: tauri toolkit

This commit is contained in:
Louis 2025-03-24 18:05:07 +07:00
parent 154e0cba47
commit 27beb46801
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
52 changed files with 6019 additions and 577 deletions

View File

@ -15,7 +15,6 @@ const executeOnMain: (extension: string, method: string, ...args: any[]) => Prom
...args
) => globalThis.core?.api?.invokeExtensionFunc(extension, method, ...args)
/**
* Gets Jan's data folder path.
*
@ -36,8 +35,8 @@ const openFileExplorer: (path: string) => Promise<any> = (path) =>
* @param paths - The paths to join.
* @returns {Promise<string>} A promise that resolves with the joined path.
*/
const joinPath: (paths: string[]) => Promise<string> = (paths) =>
globalThis.core.api?.joinPath(paths)
const joinPath: (args: string[]) => Promise<string> = (args) =>
globalThis.core.api?.joinPath({ args })
/**
* Get dirname of a file path.

View File

@ -1,7 +1,4 @@
import { Model, ModelEvent, SettingComponentProps } from '../types'
import { getJanDataFolderPath, joinPath } from './core'
import { events } from './events'
import { fs } from './fs'
import { Model, SettingComponentProps } from '../types'
import { ModelManager } from './models'
export enum ExtensionTypeEnum {
@ -117,22 +114,13 @@ export abstract class BaseExtension implements ExtensionType {
return
}
const extensionSettingFolderPath = await joinPath([
await getJanDataFolderPath(),
'settings',
this.name,
])
settings.forEach((setting) => {
setting.extensionName = this.name
})
try {
if (!(await fs.existsSync(extensionSettingFolderPath)))
await fs.mkdir(extensionSettingFolderPath)
const settingFilePath = await joinPath([extensionSettingFolderPath, this.settingFileName])
const oldSettings = localStorage.getItem(this.name)
// Persists new settings
if (await fs.existsSync(settingFilePath)) {
const oldSettings = JSON.parse(await fs.readFileSync(settingFilePath, 'utf-8'))
if (oldSettings) {
settings.forEach((setting) => {
// Keep setting value
if (setting.controllerProps && Array.isArray(oldSettings))
@ -141,7 +129,7 @@ export abstract class BaseExtension implements ExtensionType {
)?.controllerProps?.value
})
}
await fs.writeFileSync(settingFilePath, JSON.stringify(settings, null, 2))
localStorage.setItem(this.name, JSON.stringify(settings))
} catch (err) {
console.error(err)
}
@ -180,21 +168,14 @@ export abstract class BaseExtension implements ExtensionType {
async getSettings(): Promise<SettingComponentProps[]> {
if (!this.name) return []
const settingPath = await joinPath([
await getJanDataFolderPath(),
this.settingFolderName,
this.name,
this.settingFileName,
])
try {
if (!(await fs.existsSync(settingPath))) return []
const content = await fs.readFileSync(settingPath, 'utf-8')
const settings: SettingComponentProps[] = JSON.parse(content)
return settings
const settingsString = localStorage.getItem(this.name);
if (!settingsString) return [];
const settings: SettingComponentProps[] = JSON.parse(settingsString);
return settings;
} catch (err) {
console.warn(err)
return []
console.warn(err);
return [];
}
}
@ -220,20 +201,8 @@ export abstract class BaseExtension implements ExtensionType {
if (!updatedSettings.length) updatedSettings = componentProps as SettingComponentProps[]
const settingFolder = await joinPath([
await getJanDataFolderPath(),
this.settingFolderName,
this.name,
])
if (!(await fs.existsSync(settingFolder))) {
await fs.mkdir(settingFolder)
}
const settingPath = await joinPath([settingFolder, this.settingFileName])
await fs.writeFileSync(settingPath, JSON.stringify(updatedSettings, null, 2))
localStorage.setItem(this.name, JSON.stringify(updatedSettings));
updatedSettings.forEach((setting) => {
this.onSettingUpdate<typeof setting.controllerProps.value>(
setting.key,

View File

@ -25,7 +25,7 @@ const readFileSync = (...args: any[]) => globalThis.core.api?.readFileSync(...ar
* @param {string} path
* @returns {boolean} A boolean indicating whether the path is a file.
*/
const existsSync = (...args: any[]) => globalThis.core.api?.existsSync(...args)
const existsSync = (...args: any[]) => globalThis.core.api?.existsSync({ args })
/**
* List the directory files
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.

View File

@ -94,8 +94,6 @@ export default class Extension {
`Package ${this.origin} does not contain a valid manifest: ${error}`
)
}
return true
}
/**

View File

@ -27,7 +27,7 @@ export class Retrieval {
}
private async initialize() {
const apiKey = await window.core?.api.appToken() ?? 'cortex.cpp'
const apiKey = await window.core?.api.appToken()
// declare time-weighted retriever and storage
this.timeWeightedVectorStore = new MemoryVectorStore(
@ -53,7 +53,7 @@ export class Retrieval {
}
public async updateEmbeddingEngine(model: string, engine: string) {
const apiKey = await window.core?.api.appToken() ?? 'cortex.cpp'
const apiKey = await window.core?.api.appToken()
this.embeddingModel = new OpenAIEmbeddings(
{ openAIApiKey: apiKey, model },
// TODO: Raw settings

View File

@ -5,7 +5,6 @@ import {
ThreadMessage,
} from '@janhq/core'
import ky, { KyInstance } from 'ky'
import PQueue from 'p-queue'
type ThreadList = {
data: Thread[]
@ -20,21 +19,22 @@ type MessageList = {
* functionality for managing threads.
*/
export default class CortexConversationalExtension extends ConversationalExtension {
queue = new PQueue({ concurrency: 1 })
api?: KyInstance
/**
* Get the API instance
* @returns
*/
async apiInstance(): Promise<KyInstance> {
if(this.api) return this.api
const apiKey = (await window.core?.api.appToken()) ?? 'cortex.cpp'
if (this.api) return this.api
const apiKey = (await window.core?.api.appToken())
this.api = ky.extend({
prefixUrl: API_URL,
headers: {
Authorization: `Bearer ${apiKey}`,
},
headers: apiKey
? {
Authorization: `Bearer ${apiKey}`,
}
: {},
retry: 10,
})
return this.api
}
@ -42,7 +42,7 @@ export default class CortexConversationalExtension extends ConversationalExtensi
* Called when the extension is loaded.
*/
async onLoad() {
this.queue.add(() => this.healthz())
// this.queue.add(() => this.healthz())
}
/**
@ -54,13 +54,11 @@ export default class CortexConversationalExtension extends ConversationalExtensi
* Returns a Promise that resolves to an array of Conversation objects.
*/
async listThreads(): Promise<Thread[]> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.get('v1/threads?limit=-1')
.json<ThreadList>()
.then((e) => e.data)
)
return this.apiInstance().then((api) =>
api
.get('v1/threads?limit=-1')
.json<ThreadList>()
.then((e) => e.data)
) as Promise<Thread[]>
}
@ -69,10 +67,8 @@ export default class CortexConversationalExtension extends ConversationalExtensi
* @param thread The Thread object to save.
*/
async createThread(thread: Thread): Promise<Thread> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api.post('v1/threads', { json: thread }).json<Thread>()
)
return this.apiInstance().then((api) =>
api.post('v1/threads', { json: thread }).json<Thread>()
) as Promise<Thread>
}
@ -81,12 +77,9 @@ export default class CortexConversationalExtension extends ConversationalExtensi
* @param thread The Thread object to save.
*/
async modifyThread(thread: Thread): Promise<void> {
return this.queue
.add(() =>
this.apiInstance().then((api) =>
api.patch(`v1/threads/${thread.id}`, { json: thread })
)
)
return this.apiInstance()
.then((api) => api.patch(`v1/threads/${thread.id}`, { json: thread }))
.then()
}
@ -95,10 +88,8 @@ export default class CortexConversationalExtension extends ConversationalExtensi
* @param threadId The ID of the thread to delete.
*/
async deleteThread(threadId: string): Promise<void> {
return this.queue
.add(() =>
this.apiInstance().then((api) => api.delete(`v1/threads/${threadId}`))
)
return this.apiInstance()
.then((api) => api.delete(`v1/threads/${threadId}`))
.then()
}
@ -108,14 +99,12 @@ export default class CortexConversationalExtension extends ConversationalExtensi
* @returns A Promise that resolves when the message has been added.
*/
async createMessage(message: ThreadMessage): Promise<ThreadMessage> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.post(`v1/threads/${message.thread_id}/messages`, {
json: message,
})
.json<ThreadMessage>()
)
return this.apiInstance().then((api) =>
api
.post(`v1/threads/${message.thread_id}/messages`, {
json: message,
})
.json<ThreadMessage>()
) as Promise<ThreadMessage>
}
@ -125,14 +114,12 @@ export default class CortexConversationalExtension extends ConversationalExtensi
* @returns
*/
async modifyMessage(message: ThreadMessage): Promise<ThreadMessage> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.patch(`v1/threads/${message.thread_id}/messages/${message.id}`, {
json: message,
})
.json<ThreadMessage>()
)
return this.apiInstance().then((api) =>
api
.patch(`v1/threads/${message.thread_id}/messages/${message.id}`, {
json: message,
})
.json<ThreadMessage>()
) as Promise<ThreadMessage>
}
@ -143,12 +130,8 @@ export default class CortexConversationalExtension extends ConversationalExtensi
* @returns A Promise that resolves when the message has been successfully deleted.
*/
async deleteMessage(threadId: string, messageId: string): Promise<void> {
return this.queue
.add(() =>
this.apiInstance().then((api) =>
api.delete(`v1/threads/${threadId}/messages/${messageId}`)
)
)
return this.apiInstance()
.then((api) => api.delete(`v1/threads/${threadId}/messages/${messageId}`))
.then()
}
@ -158,13 +141,11 @@ export default class CortexConversationalExtension extends ConversationalExtensi
* @returns A Promise that resolves to an array of ThreadMessage objects.
*/
async listMessages(threadId: string): Promise<ThreadMessage[]> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.get(`v1/threads/${threadId}/messages?order=asc&limit=-1`)
.json<MessageList>()
.then((e) => e.data)
)
return this.apiInstance().then((api) =>
api
.get(`v1/threads/${threadId}/messages?order=asc&limit=-1`)
.json<MessageList>()
.then((e) => e.data)
) as Promise<ThreadMessage[]>
}
@ -175,12 +156,8 @@ export default class CortexConversationalExtension extends ConversationalExtensi
* the details of the assistant associated with the specified thread.
*/
async getThreadAssistant(threadId: string): Promise<ThreadAssistantInfo> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.get(`v1/assistants/${threadId}?limit=-1`)
.json<ThreadAssistantInfo>()
)
return this.apiInstance().then((api) =>
api.get(`v1/assistants/${threadId}?limit=-1`).json<ThreadAssistantInfo>()
) as Promise<ThreadAssistantInfo>
}
/**
@ -193,12 +170,10 @@ export default class CortexConversationalExtension extends ConversationalExtensi
threadId: string,
assistant: ThreadAssistantInfo
): Promise<ThreadAssistantInfo> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.post(`v1/assistants/${threadId}`, { json: assistant })
.json<ThreadAssistantInfo>()
)
return this.apiInstance().then((api) =>
api
.post(`v1/assistants/${threadId}`, { json: assistant })
.json<ThreadAssistantInfo>()
) as Promise<ThreadAssistantInfo>
}
@ -212,26 +187,10 @@ export default class CortexConversationalExtension extends ConversationalExtensi
threadId: string,
assistant: ThreadAssistantInfo
): Promise<ThreadAssistantInfo> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.patch(`v1/assistants/${threadId}`, { json: assistant })
.json<ThreadAssistantInfo>()
)
return this.apiInstance().then((api) =>
api
.patch(`v1/assistants/${threadId}`, { json: assistant })
.json<ThreadAssistantInfo>()
) as Promise<ThreadAssistantInfo>
}
/**
* Do health check on cortex.cpp
* @returns
*/
async healthz(): Promise<void> {
return this.apiInstance()
.then((api) =>
api.get('healthz', {
retry: { limit: 20, delay: () => 500, methods: ['get'] },
})
)
.then(() => {})
}
}

View File

@ -16,7 +16,6 @@ import {
EngineEvent,
} from '@janhq/core'
import ky, { HTTPError, KyInstance } from 'ky'
import PQueue from 'p-queue'
import { EngineError } from './error'
import { getJanDataFolderPath } from '@janhq/core'
import { engineVariant } from './utils'
@ -29,21 +28,22 @@ interface ModelList {
* functionality for managing engines.
*/
export default class JanEngineManagementExtension extends EngineManagementExtension {
queue = new PQueue({ concurrency: 1 })
api?: KyInstance
/**
* Get the API instance
* @returns
*/
async apiInstance(): Promise<KyInstance> {
if(this.api) return this.api
const apiKey = (await window.core?.api.appToken()) ?? 'cortex.cpp'
if (this.api) return this.api
const apiKey = (await window.core?.api.appToken())
this.api = ky.extend({
prefixUrl: API_URL,
headers: {
Authorization: `Bearer ${apiKey}`,
},
headers: apiKey
? {
Authorization: `Bearer ${apiKey}`,
}
: {},
retry: 10,
})
return this.api
}
@ -52,9 +52,7 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
*/
async onLoad() {
// Symlink Engines Directory
await executeOnMain(NODE, 'symlinkEngines')
// Run Healthcheck
this.queue.add(() => this.healthz())
// await executeOnMain(NODE, 'symlinkEngines')
// Update default local engine
this.updateDefaultEngine()
@ -74,13 +72,11 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
* @returns A Promise that resolves to an object of list engines.
*/
async getEngines(): Promise<Engines> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.get('v1/engines')
.json<Engines>()
.then((e) => e)
)
return this.apiInstance().then((api) =>
api
.get('v1/engines')
.json<Engines>()
.then((e) => e)
) as Promise<Engines>
}
@ -104,13 +100,11 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
* @returns A Promise that resolves to an array of installed engine.
*/
async getInstalledEngines(name: InferenceEngine): Promise<EngineVariant[]> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}`)
.json<EngineVariant[]>()
.then((e) => e)
)
return this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}`)
.json<EngineVariant[]>()
.then((e) => e)
) as Promise<EngineVariant[]>
}
@ -125,15 +119,13 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
version: string,
platform?: string
) {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}/releases/${version}`)
.json<EngineReleased[]>()
.then((e) =>
platform ? e.filter((r) => r.name.includes(platform)) : e
)
)
return this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}/releases/${version}`)
.json<EngineReleased[]>()
.then((e) =>
platform ? e.filter((r) => r.name.includes(platform)) : e
)
) as Promise<EngineReleased[]>
}
@ -143,15 +135,13 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
* @returns A Promise that resolves to an array of latest released engine by version.
*/
async getLatestReleasedEngine(name: InferenceEngine, platform?: string) {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}/releases/latest`)
.json<EngineReleased[]>()
.then((e) =>
platform ? e.filter((r) => r.name.includes(platform)) : e
)
)
return this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}/releases/latest`)
.json<EngineReleased[]>()
.then((e) =>
platform ? e.filter((r) => r.name.includes(platform)) : e
)
) as Promise<EngineReleased[]>
}
@ -160,12 +150,10 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
* @returns A Promise that resolves to intall of engine.
*/
async installEngine(name: string, engineConfig: EngineConfig) {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.post(`v1/engines/${name}/install`, { json: engineConfig })
.then((e) => e)
)
return this.apiInstance().then((api) =>
api
.post(`v1/engines/${name}/install`, { json: engineConfig })
.then((e) => e)
) as Promise<{ messages: string }>
}
@ -195,18 +183,16 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
if (engineConfig.metadata && !engineConfig.metadata?.header_template)
engineConfig.metadata.header_template = DEFAULT_REQUEST_HEADERS_TRANSFORM
return this.queue.add(() =>
this.apiInstance().then((api) =>
api.post('v1/engines', { json: engineConfig }).then((e) => {
if (persistModels && engineConfig.metadata?.get_models_url) {
// Pull /models from remote models endpoint
return this.populateRemoteModels(engineConfig)
.then(() => e)
.catch(() => e)
}
return e
})
)
return this.apiInstance().then((api) =>
api.post('v1/engines', { json: engineConfig }).then((e) => {
if (persistModels && engineConfig.metadata?.get_models_url) {
// Pull /models from remote models endpoint
return this.populateRemoteModels(engineConfig)
.then(() => e)
.catch(() => e)
}
return e
})
) as Promise<{ messages: string }>
}
@ -215,12 +201,10 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
* @returns A Promise that resolves to unintall of engine.
*/
async uninstallEngine(name: InferenceEngine, engineConfig: EngineConfig) {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.delete(`v1/engines/${name}/install`, { json: engineConfig })
.then((e) => e)
)
return this.apiInstance().then((api) =>
api
.delete(`v1/engines/${name}/install`, { json: engineConfig })
.then((e) => e)
) as Promise<{ messages: string }>
}
@ -229,25 +213,22 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
* @param model - Remote model object.
*/
async addRemoteModel(model: Model) {
return this.queue.add(() =>
this.apiInstance()
.then((api) =>
api
.post('v1/models/add', {
json: {
inference_params: {
max_tokens: 4096,
temperature: 0.7,
top_p: 0.95,
stream: true,
frequency_penalty: 0,
presence_penalty: 0,
},
...model,
},
})
.then((e) => e)
)
return this.apiInstance().then((api) =>
api
.post('v1/models/add', {
json: {
inference_params: {
max_tokens: 4096,
temperature: 0.7,
top_p: 0.95,
stream: true,
frequency_penalty: 0,
presence_penalty: 0,
},
...model,
},
})
.then((e) => e)
.then(() => {})
)
}
@ -257,13 +238,11 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
* @returns A Promise that resolves to an object of default engine.
*/
async getDefaultEngineVariant(name: InferenceEngine) {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}/default`)
.json<{ messages: string }>()
.then((e) => e)
)
return this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}/default`)
.json<{ messages: string }>()
.then((e) => e)
) as Promise<DefaultEngineVariant>
}
@ -276,12 +255,10 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
name: InferenceEngine,
engineConfig: EngineConfig
) {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.post(`v1/engines/${name}/default`, { json: engineConfig })
.then((e) => e)
)
return this.apiInstance().then((api) =>
api
.post(`v1/engines/${name}/default`, { json: engineConfig })
.then((e) => e)
) as Promise<{ messages: string }>
}
@ -289,31 +266,13 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
* @returns A Promise that resolves to update engine.
*/
async updateEngine(name: InferenceEngine, engineConfig?: EngineConfig) {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.post(`v1/engines/${name}/update`, { json: engineConfig })
.then((e) => e)
)
return this.apiInstance().then((api) =>
api
.post(`v1/engines/${name}/update`, { json: engineConfig })
.then((e) => e)
) as Promise<{ messages: string }>
}
/**
* Do health check on cortex.cpp
* @returns
*/
async healthz(): Promise<void> {
return this.apiInstance()
.then((api) =>
api.get('healthz', {
retry: { limit: 20, delay: () => 500, methods: ['get'] },
})
)
.then(() => {
this.queue.concurrency = Infinity
})
}
/**
* Update default local engine
* This is to use built-in engine variant in case there is no default engine set
@ -428,8 +387,6 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
*/
migrate = async () => {
// Ensure health check is done
await this.queue.onEmpty()
const version = await this.getSetting<string>('version', '0.0.0')
const engines = await this.getEngines()
if (version < VERSION) {

View File

@ -1,21 +1,15 @@
import { HardwareManagementExtension, HardwareInformation } from '@janhq/core'
import ky, { KyInstance } from 'ky'
import PQueue from 'p-queue'
/**
* JSONHardwareManagementExtension is a HardwareManagementExtension implementation that provides
* functionality for managing engines.
*/
export default class JSONHardwareManagementExtension extends HardwareManagementExtension {
queue = new PQueue({ concurrency: 1 })
/**
* Called when the extension is loaded.
*/
async onLoad() {
// Run Healthcheck
this.queue.add(() => this.healthz())
}
async onLoad() {}
api?: KyInstance
/**
@ -23,13 +17,16 @@ export default class JSONHardwareManagementExtension extends HardwareManagementE
* @returns
*/
async apiInstance(): Promise<KyInstance> {
if(this.api) return this.api
const apiKey = (await window.core?.api.appToken()) ?? 'cortex.cpp'
if (this.api) return this.api
const apiKey = (await window.core?.api.appToken())
this.api = ky.extend({
prefixUrl: API_URL,
headers: {
Authorization: `Bearer ${apiKey}`,
},
headers: apiKey
? {
Authorization: `Bearer ${apiKey}`,
}
: {},
retry: 10,
})
return this.api
}
@ -39,31 +36,15 @@ export default class JSONHardwareManagementExtension extends HardwareManagementE
*/
onUnload() {}
/**
* Do health check on cortex.cpp
* @returns
*/
async healthz(): Promise<void> {
return this.apiInstance().then((api) =>
api
.get('healthz', {
retry: { limit: 20, delay: () => 500, methods: ['get'] },
})
.then(() => {})
)
}
/**
* @returns A Promise that resolves to an object of hardware.
*/
async getHardware(): Promise<HardwareInformation> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.get('v1/hardware')
.json<HardwareInformation>()
.then((e) => e)
)
return this.apiInstance().then((api) =>
api
.get('v1/hardware')
.json<HardwareInformation>()
.then((e) => e)
) as Promise<HardwareInformation>
}
@ -74,10 +55,8 @@ export default class JSONHardwareManagementExtension extends HardwareManagementE
message: string
activated_gpus: number[]
}> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api.post('v1/hardware/activate', { json: data }).then((e) => e)
)
return this.apiInstance().then((api) =>
api.post('v1/hardware/activate', { json: data }).then((e) => e)
) as Promise<{
message: string
activated_gpus: number[]

View File

@ -16,7 +16,6 @@ import {
events,
ModelEvent,
} from '@janhq/core'
import PQueue from 'p-queue'
import ky, { KyInstance } from 'ky'
/**
@ -48,8 +47,6 @@ export enum Settings {
export default class JanInferenceCortexExtension extends LocalOAIEngine {
nodeModule: string = 'node'
queue = new PQueue({ concurrency: 1 })
provider: string = InferenceEngine.cortex
shouldReconnect = true
@ -81,13 +78,16 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
* @returns
*/
async apiInstance(): Promise<KyInstance> {
if(this.api) return this.api
const apiKey = (await window.core?.api.appToken()) ?? 'cortex.cpp'
if (this.api) return this.api
const apiKey = await window.core?.api.appToken()
this.api = ky.extend({
prefixUrl: CORTEX_API_URL,
headers: {
Authorization: `Bearer ${apiKey}`,
},
headers: apiKey
? {
Authorization: `Bearer ${apiKey}`,
}
: {},
retry: 10,
})
return this.api
}
@ -131,8 +131,7 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
// Run the process watchdog
// const systemInfo = await systemInformation()
this.queue.add(() => executeOnMain(NODE, 'run'))
this.queue.add(() => this.healthz())
await executeOnMain(NODE, 'run')
this.subscribeToEvents()
window.addEventListener('beforeunload', () => {
@ -144,7 +143,7 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
console.log('Clean up cortex.cpp services')
this.shouldReconnect = false
this.clean()
await executeOnMain(NODE, 'dispose')
// await executeOnMain(NODE, 'dispose')
super.onUnload()
}
@ -179,35 +178,33 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
this.abortControllers.set(model.id, controller)
return await this.queue.add(() =>
this.apiInstance().then((api) =>
api
.post('v1/models/start', {
json: {
...extractModelLoadParams(model.settings),
model: model.id,
engine:
model.engine === InferenceEngine.nitro // Legacy model cache
? InferenceEngine.cortex_llamacpp
: model.engine,
cont_batching: this.cont_batching,
n_parallel: this.n_parallel,
caching_enabled: this.caching_enabled,
flash_attn: this.flash_attn,
cache_type: this.cache_type,
use_mmap: this.use_mmap,
...(this.cpu_threads ? { cpu_threads: this.cpu_threads } : {}),
},
timeout: false,
signal,
})
.json()
.catch(async (e) => {
throw (await e.response?.json()) ?? e
})
.finally(() => this.abortControllers.delete(model.id))
.then()
)
return await this.apiInstance().then((api) =>
api
.post('v1/models/start', {
json: {
...extractModelLoadParams(model.settings),
model: model.id,
engine:
model.engine === InferenceEngine.nitro // Legacy model cache
? InferenceEngine.cortex_llamacpp
: model.engine,
cont_batching: this.cont_batching,
n_parallel: this.n_parallel,
caching_enabled: this.caching_enabled,
flash_attn: this.flash_attn,
cache_type: this.cache_type,
use_mmap: this.use_mmap,
...(this.cpu_threads ? { cpu_threads: this.cpu_threads } : {}),
},
timeout: false,
signal,
})
.json()
.catch(async (e) => {
throw (await e.response?.json()) ?? e
})
.finally(() => this.abortControllers.delete(model.id))
.then()
)
}
@ -266,76 +263,64 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
* Subscribe to cortex.cpp websocket events
*/
private subscribeToEvents() {
this.queue.add(
() =>
new Promise<void>((resolve) => {
this.socket = new WebSocket(`${CORTEX_SOCKET_URL}/events`)
console.log('Subscribing to events...')
this.socket = new WebSocket(`${CORTEX_SOCKET_URL}/events`)
this.socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data)
this.socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data)
const transferred = data.task.items.reduce(
(acc: number, cur: any) => acc + cur.downloadedBytes,
0
)
const total = data.task.items.reduce(
(acc: number, cur: any) => acc + cur.bytes,
0
)
const percent = total > 0 ? transferred / total : 0
const transferred = data.task.items.reduce(
(acc: number, cur: any) => acc + cur.downloadedBytes,
0
)
const total = data.task.items.reduce(
(acc: number, cur: any) => acc + cur.bytes,
0
)
const percent = total > 0 ? transferred / total : 0
events.emit(
DownloadTypes[data.type as keyof typeof DownloadTypes],
{
modelId: data.task.id,
percent: percent,
size: {
transferred: transferred,
total: total,
},
downloadType: data.task.type,
}
)
events.emit(DownloadTypes[data.type as keyof typeof DownloadTypes], {
modelId: data.task.id,
percent: percent,
size: {
transferred: transferred,
total: total,
},
downloadType: data.task.type,
})
if (data.task.type === 'Engine') {
events.emit(EngineEvent.OnEngineUpdate, {
type: DownloadTypes[data.type as keyof typeof DownloadTypes],
percent: percent,
id: data.task.id,
})
} else {
if (data.type === DownloadTypes.DownloadSuccess) {
// Delay for the state update from cortex.cpp
// Just to be sure
setTimeout(() => {
events.emit(ModelEvent.OnModelsUpdate, {
fetch: true,
})
}, 500)
}
}
})
/**
* This is to handle the server segfault issue
*/
this.socket.onclose = (event) => {
console.log('WebSocket closed:', event)
// Notify app to update model running state
events.emit(ModelEvent.OnModelStopped, {})
// Reconnect to the /events websocket
if (this.shouldReconnect) {
console.log(`Attempting to reconnect...`)
setTimeout(() => this.subscribeToEvents(), 1000)
}
// Queue up health check
this.queue.add(() => this.healthz())
}
resolve()
if (data.task.type === 'Engine') {
events.emit(EngineEvent.OnEngineUpdate, {
type: DownloadTypes[data.type as keyof typeof DownloadTypes],
percent: percent,
id: data.task.id,
})
)
} else {
if (data.type === DownloadTypes.DownloadSuccess) {
// Delay for the state update from cortex.cpp
// Just to be sure
setTimeout(() => {
events.emit(ModelEvent.OnModelsUpdate, {
fetch: true,
})
}, 500)
}
}
})
/**
* This is to handle the server segfault issue
*/
this.socket.onclose = (event) => {
console.log('WebSocket closed:', event)
// Notify app to update model running state
events.emit(ModelEvent.OnModelStopped, {})
// Reconnect to the /events websocket
if (this.shouldReconnect) {
console.log(`Attempting to reconnect...`)
setTimeout(() => this.subscribeToEvents(), 1000)
}
}
}
}

View File

@ -12,7 +12,6 @@ import {
} from '@janhq/core'
import { scanModelsFolder } from './legacy/model-json'
import { deleteModelFiles } from './legacy/delete'
import PQueue from 'p-queue'
import ky, { KyInstance } from 'ky'
/**
@ -31,21 +30,22 @@ type Data<T> = {
* A extension for models
*/
export default class JanModelExtension extends ModelExtension {
queue = new PQueue({ concurrency: 1 })
api?: KyInstance
/**
* Get the API instance
* @returns
*/
async apiInstance(): Promise<KyInstance> {
if(this.api) return this.api
const apiKey = (await window.core?.api.appToken()) ?? 'cortex.cpp'
if (this.api) return this.api
const apiKey = (await window.core?.api.appToken())
this.api = ky.extend({
prefixUrl: CORTEX_API_URL,
headers: {
Authorization: `Bearer ${apiKey}`,
},
headers: apiKey
? {
Authorization: `Bearer ${apiKey}`,
}
: {},
retry: 10
})
return this.api
}
@ -53,8 +53,6 @@ export default class JanModelExtension extends ModelExtension {
* Called when the extension is loaded.
*/
async onLoad() {
this.queue.add(() => this.healthz())
this.registerSettings(SETTINGS)
// Configure huggingface token if available
@ -97,16 +95,14 @@ export default class JanModelExtension extends ModelExtension {
/**
* Sending POST to /models/pull/{id} endpoint to pull the model
*/
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.post('v1/models/pull', { json: { model, id, name }, timeout: false })
.json()
.catch(async (e) => {
throw (await e.response?.json()) ?? e
})
.then()
)
return this.apiInstance().then((api) =>
api
.post('v1/models/pull', { json: { model, id, name }, timeout: false })
.json()
.catch(async (e) => {
throw (await e.response?.json()) ?? e
})
.then()
)
}
@ -120,13 +116,11 @@ export default class JanModelExtension extends ModelExtension {
/**
* Sending DELETE to /models/pull/{id} endpoint to cancel a model pull
*/
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.delete('v1/models/pull', { json: { taskId: model } })
.json()
.then()
)
return this.apiInstance().then((api) =>
api
.delete('v1/models/pull', { json: { taskId: model } })
.json()
.then()
)
}
@ -136,12 +130,8 @@ export default class JanModelExtension extends ModelExtension {
* @returns A Promise that resolves when the model is deleted.
*/
async deleteModel(model: string): Promise<void> {
return this.queue
.add(() =>
this.apiInstance().then((api) =>
api.delete(`v1/models/${model}`).json().then()
)
)
return this.apiInstance()
.then((api) => api.delete(`v1/models/${model}`).json().then())
.catch((e) => console.debug(e))
.finally(async () => {
// Delete legacy model files
@ -241,17 +231,15 @@ export default class JanModelExtension extends ModelExtension {
* @param model - The metadata of the model
*/
async updateModel(model: Partial<Model>): Promise<Model> {
return this.queue
.add(() =>
this.apiInstance().then((api) =>
api
.patch(`v1/models/${model.id}`, {
json: { ...model },
timeout: false,
})
.json()
.then()
)
return this.apiInstance()
.then((api) =>
api
.patch(`v1/models/${model.id}`, {
json: { ...model },
timeout: false,
})
.json()
.then()
)
.then(() => this.getModel(model.id))
}
@ -261,13 +249,11 @@ export default class JanModelExtension extends ModelExtension {
* @param model - The ID of the model
*/
async getModel(model: string): Promise<Model> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.get(`v1/models/${model}`)
.json()
.then((e) => this.transformModel(e))
)
return this.apiInstance().then((api) =>
api
.get(`v1/models/${model}`)
.json()
.then((e) => this.transformModel(e))
) as Promise<Model>
}
@ -282,17 +268,15 @@ export default class JanModelExtension extends ModelExtension {
name?: string,
option?: OptionType
): Promise<void> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api
.post('v1/models/import', {
json: { model, modelPath, name, option },
timeout: false,
})
.json()
.catch((e) => console.debug(e)) // Ignore error
.then()
)
return this.apiInstance().then((api) =>
api
.post('v1/models/import', {
json: { model, modelPath, name, option },
timeout: false,
})
.json()
.catch((e) => console.debug(e)) // Ignore error
.then()
)
}
@ -302,12 +286,8 @@ export default class JanModelExtension extends ModelExtension {
* @param model
*/
async getSources(): Promise<ModelSource[]> {
const sources = await this.queue
.add(() =>
this.apiInstance().then((api) =>
api.get('v1/models/sources').json<Data<ModelSource>>()
)
)
const sources = await this.apiInstance()
.then((api) => api.get('v1/models/sources').json<Data<ModelSource>>())
.then((e) => (typeof e === 'object' ? (e.data as ModelSource[]) : []))
.catch(() => [])
return sources.concat(
@ -320,14 +300,12 @@ export default class JanModelExtension extends ModelExtension {
* @param model
*/
async addSource(source: string): Promise<any> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api.post('v1/models/sources', {
json: {
source,
},
})
)
return this.apiInstance().then((api) =>
api.post('v1/models/sources', {
json: {
source,
},
})
)
}
@ -336,15 +314,13 @@ export default class JanModelExtension extends ModelExtension {
* @param model
*/
async deleteSource(source: string): Promise<any> {
return this.queue.add(() =>
this.apiInstance().then((api) =>
api.delete('v1/models/sources', {
json: {
source,
},
timeout: false,
})
)
return this.apiInstance().then((api) =>
api.delete('v1/models/sources', {
json: {
source,
},
timeout: false,
})
)
}
// END - Model Sources
@ -354,10 +330,8 @@ export default class JanModelExtension extends ModelExtension {
* @param model
*/
async isModelLoaded(model: string): Promise<boolean> {
return this.queue
.add(() =>
this.apiInstance().then((api) => api.get(`v1/models/status/${model}`))
)
return this.apiInstance()
.then((api) => api.get(`v1/models/status/${model}`))
.then((e) => true)
.catch(() => false)
}
@ -375,12 +349,8 @@ export default class JanModelExtension extends ModelExtension {
* @returns
*/
async fetchModels(): Promise<Model[]> {
return this.queue
.add(() =>
this.apiInstance().then((api) =>
api.get('v1/models?limit=-1').json<Data<Model>>()
)
)
return this.apiInstance()
.then((api) => api.get('v1/models?limit=-1').json<Data<Model>>())
.then((e) =>
typeof e === 'object' ? e.data.map((e) => this.transformModel(e)) : []
)
@ -418,33 +388,9 @@ export default class JanModelExtension extends ModelExtension {
private async updateCortexConfig(body: {
[key: string]: any
}): Promise<void> {
return this.queue
.add(() =>
this.apiInstance().then((api) =>
api.patch('v1/configs', { json: body }).then(() => {})
)
)
.catch((e) => console.debug(e))
}
/**
* Do health check on cortex.cpp
* @returns
*/
private healthz(): Promise<void> {
return this.apiInstance()
.then((api) =>
api.get('healthz', {
retry: {
limit: 20,
delay: () => 500,
methods: ['get'],
},
})
)
.then(() => {
this.queue.concurrency = Infinity
})
.then((api) => api.patch('v1/configs', { json: body }).then(() => {}))
.catch((e) => console.debug(e))
}
/**
@ -453,25 +399,23 @@ export default class JanModelExtension extends ModelExtension {
fetchModelsHub = async () => {
const models = await this.fetchModels()
return this.queue.add(() =>
this.apiInstance()
.then((api) =>
api
.get('v1/models/hub?author=cortexso&tag=cortex.cpp')
.json<Data<string>>()
.then((e) => {
e.data?.forEach((model) => {
if (
!models.some(
(e) => 'modelSource' in e && e.modelSource === model
)
return this.apiInstance()
.then((api) =>
api
.get('v1/models/hub?author=cortexso&tag=cortex.cpp')
.json<Data<string>>()
.then((e) => {
e.data?.forEach((model) => {
if (
!models.some(
(e) => 'modelSource' in e && e.modelSource === model
)
this.addSource(model).catch((e) => console.debug(e))
})
)
this.addSource(model).catch((e) => console.debug(e))
})
)
.catch((e) => console.debug(e))
)
})
)
.catch((e) => console.debug(e))
}
// END: - Private API
}

View File

@ -18,9 +18,11 @@
"test-local": "yarn lint && yarn build:test && yarn test",
"copy:assets": "cpx \"pre-install/*.tgz\" \"electron/pre-install/\" && cpx \"themes/**\" \"electron/themes\"",
"dev:electron": "yarn copy:assets && yarn workspace jan dev",
"dev:web:standalone": "concurrently \"yarn workspace @janhq/web dev\" \"wait-on http://localhost:3000 && rsync -av --prune-empty-dirs --include '*/' --include 'dist/***' --include 'package.json' --include 'tsconfig.json' --exclude '*' ./extensions/ web/.next/static/extensions/\"",
"dev:web": "yarn workspace @janhq/web dev",
"dev:server": "yarn workspace @janhq/server dev",
"dev": "concurrently -n \"NEXT,ELECTRON\" -c \"yellow,blue\" --kill-others \"yarn dev:web\" \"yarn dev:electron\"",
"dev:tauri": "tauri dev",
"build:server": "cd server && yarn build",
"build:core": "cd core && yarn build && yarn pack",
"build:web": "yarn workspace @janhq/web build && cpx \"web/out/**\" \"electron/renderer/\"",
@ -35,12 +37,14 @@
"prepare": "husky"
},
"devDependencies": {
"@tauri-apps/cli": "^2.2.5",
"concurrently": "^9.1.0",
"cpx": "^1.5.0",
"husky": "^9.1.5",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"rimraf": "^3.0.2",
"run-script-os": "^1.1.6",
"wait-on": "^7.0.1"
},
"version": "0.0.0",

4
src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

4908
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

30
src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,30 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0.2", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.1.0", features = [ "protocol-asset",'macos-private-api'] }
tauri-plugin-log = "2.0.0-rc"
tauri-plugin-shell = "2.2.0"
flate2 = "1.0"
tar = "0.4"
rand = "0.8"
tauri-plugin-http = { version = "2", features = ["unsafe-headers"] }

64
src-tauri/binaries/download.sh Executable file
View File

@ -0,0 +1,64 @@
#!/bin/bash
download() {
URL="$1"
EXTRA_ARGS="${@:2}"
OUTPUT_DIR="${EXTRA_ARGS[${#EXTRA_ARGS[@]} -1]}"
mkdir -p "$OUTPUT_DIR"
echo "Downloading $URL to $OUTPUT_DIR using curl..."
curl -L "$URL" -o "$OUTPUT_DIR/$(basename "$URL")"
tar -xzf "$OUTPUT_DIR/$(basename "$URL")" -C "$OUTPUT_DIR" --strip-components 1
rm "$OUTPUT_DIR/$(basename "$URL")"
}
# Read CORTEX_VERSION
CORTEX_VERSION=1.0.12
ENGINE_VERSION=0.1.55
CORTEX_RELEASE_URL="https://github.com/menloresearch/cortex.cpp/releases/download"
ENGINE_DOWNLOAD_URL="https://github.com/menloresearch/cortex.llamacpp/releases/download/v${ENGINE_VERSION}/cortex.llamacpp-${ENGINE_VERSION}"
CUDA_DOWNLOAD_URL="https://github.com/menloresearch/cortex.llamacpp/releases/download/v${ENGINE_VERSION}"
BIN_PATH=./
SHARED_PATH="."
# Detect platform
OS_TYPE=$(uname)
if [ "$OS_TYPE" == "Linux" ]; then
# Linux downloads
download "${CORTEX_RELEASE_URL}/v${CORTEX_VERSION}/cortex-${CORTEX_VERSION}-linux-amd64.tar.gz" "${BIN_PATH}"
mv .cortex-server-beta ./cortex-server
rm -rf ./cortex
rm -rf ./cortex-beta
chmod +x "./cortex-server"
# Download engines for Linux
download "${ENGINE_DOWNLOAD_URL}-linux-amd64-noavx.tar.gz" "${SHARED_PATH}/engines/cortex.llamacpp/linux-amd64-noavx/v${ENGINE_VERSION}"
download "${ENGINE_DOWNLOAD_URL}-linux-amd64-avx.tar.gz" "${SHARED_PATH}/engines/cortex.llamacpp/linux-amd64-avx/v${ENGINE_VERSION}"
download "${ENGINE_DOWNLOAD_URL}-linux-amd64-avx2.tar.gz" "${SHARED_PATH}/engines/cortex.llamacpp/linux-amd64-avx2/v${ENGINE_VERSION}"
download "${ENGINE_DOWNLOAD_URL}-linux-amd64-avx512.tar.gz" "${SHARED_PATH}/engines/cortex.llamacpp/linux-amd64-avx512/v${ENGINE_VERSION}"
download "${ENGINE_DOWNLOAD_URL}-linux-amd64-avx2-cuda-12-0.tar.gz" "${SHARED_PATH}/engines/cortex.llamacpp/linux-amd64-avx2-cuda-12-0/v${ENGINE_VERSION}"
download "${ENGINE_DOWNLOAD_URL}-linux-amd64-avx2-cuda-11-7.tar.gz" "${SHARED_PATH}/engines/cortex.llamacpp/linux-amd64-avx2-cuda-11-7/v${ENGINE_VERSION}"
download "${ENGINE_DOWNLOAD_URL}-linux-amd64-noavx-cuda-12-0.tar.gz" "${SHARED_PATH}/engines/cortex.llamacpp/linux-amd64-noavx-cuda-12-0/v${ENGINE_VERSION}"
download "${ENGINE_DOWNLOAD_URL}-linux-amd64-noavx-cuda-11-7.tar.gz" "${SHARED_PATH}/engines/cortex.llamacpp/linux-amd64-noavx-cuda-11-7/v${ENGINE_VERSION}"
download "${ENGINE_DOWNLOAD_URL}-linux-amd64-vulkan.tar.gz" "${SHARED_PATH}/engines/cortex.llamacpp/linux-amd64-vulkan/v${ENGINE_VERSION}"
download "${CUDA_DOWNLOAD_URL}/cuda-12-0-linux-amd64.tar.gz" "${BIN_PATH}"
download "${CUDA_DOWNLOAD_URL}/cuda-11-7-linux-amd64.tar.gz" "${BIN_PATH}"
elif [ "$OS_TYPE" == "Darwin" ]; then
# macOS downloads
download "${CORTEX_RELEASE_URL}/v${CORTEX_VERSION}/cortex-${CORTEX_VERSION}-mac-universal.tar.gz" "${BIN_PATH}"
mv ./cortex-server-beta ./cortex-server
rm -rf ./cortex
rm -rf ./cortex-beta
chmod +x "./cortex-server"
mv ./cortex-server ./cortex-server-universal-apple-darwin
# Download engines for macOS
download "${ENGINE_DOWNLOAD_URL}-mac-arm64.tar.gz" "${SHARED_PATH}/engines/cortex.llamacpp/mac-arm64/v${ENGINE_VERSION}"
download "${ENGINE_DOWNLOAD_URL}-mac-amd64.tar.gz" "${SHARED_PATH}/engines/cortex.llamacpp/mac-amd64/v${ENGINE_VERSION}"
else
echo "Unsupported operating system: $OS_TYPE"
exit 1
fi

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,45 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": ["main"],
"remote": {
"urls": ["http://*"]
},
"permissions": [
"core:default",
"shell:allow-spawn",
{
"identifier": "http:default",
"allow": [{ "url": "https://*:*" }, { "url": "http://*:*" }],
"deny": []
},
{
"identifier": "shell:allow-execute",
"allow": [
{
"args": [
"--start-server",
{
"validator": "\\S+"
},
"--port",
{
"validator": "\\S+"
},
"--config_file_path",
{
"validator": "\\S+"
},
"--data_folder_path",
{
"validator": "\\S+"
}
],
"name": "binaries/cortex-server",
"sidecar": true
}
]
}
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -0,0 +1,291 @@
use flate2::read::GzDecoder;
use serde_json::Value;
use std::fs;
use std::fs::File;
use std::io::Read;
use tar::Archive;
use tauri::AppHandle;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tauri::Manager;
const CONFIGURATION_FILE_NAME: &str = "settings.json";
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AppConfiguration {
pub data_folder: String,
// Add other fields as needed
}
impl AppConfiguration {
pub fn default() -> Self {
Self {
data_folder: String::from("./data"), // Set a default value for the data_folder
// Add other fields with default values as needed
}
}
}
#[tauri::command]
pub fn get_app_configurations(app_handle: tauri::AppHandle) -> AppConfiguration {
let mut app_default_configuration = AppConfiguration::default();
if std::env::var("CI").unwrap_or_default() == "e2e" {
return app_default_configuration;
}
let configuration_file = get_configuration_file_path(app_handle.clone());
let default_data_folder = default_data_folder_path(app_handle.clone());
if !configuration_file.exists() {
println!(
"App config not found, creating default config at {:?}",
configuration_file
);
app_default_configuration.data_folder = default_data_folder;
if let Err(err) = fs::write(
&configuration_file,
serde_json::to_string(&app_default_configuration).unwrap(),
) {
eprintln!("Failed to create default config: {}", err);
}
return app_default_configuration;
}
match fs::read_to_string(&configuration_file) {
Ok(content) => match serde_json::from_str::<AppConfiguration>(&content) {
Ok(app_configurations) => app_configurations,
Err(err) => {
eprintln!(
"Failed to parse app config, returning default config instead. Error: {}",
err
);
app_default_configuration
}
},
Err(err) => {
eprintln!(
"Failed to read app config, returning default config instead. Error: {}",
err
);
app_default_configuration
}
}
}
#[tauri::command]
pub fn update_app_configuration(
app_handle: tauri::AppHandle,
configuration: AppConfiguration,
) -> Result<(), String> {
let configuration_file = get_configuration_file_path(app_handle);
println!(
"update_app_configuration, configuration_file: {:?}",
configuration_file
);
fs::write(
configuration_file,
serde_json::to_string(&configuration).map_err(|e| e.to_string())?,
)
.map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_jan_data_folder_path(app_handle: tauri::AppHandle) -> PathBuf {
let app_configurations = get_app_configurations(app_handle);
PathBuf::from(app_configurations.data_folder)
}
#[tauri::command]
pub fn get_jan_extensions_path(app_handle: tauri::AppHandle) -> PathBuf {
get_jan_data_folder_path(app_handle).join("extensions")
}
#[tauri::command]
pub fn get_configuration_file_path(app_handle: tauri::AppHandle) -> PathBuf {
let app_path = app_handle.path().app_data_dir().unwrap_or_else(|err| {
let home_dir = std::env::var(if cfg!(target_os = "windows") {
"USERPROFILE"
} else {
"HOME"
})
.expect("Failed to determine the home directory");
PathBuf::from(home_dir)
});
app_path.join(CONFIGURATION_FILE_NAME)
}
#[tauri::command]
pub fn default_data_folder_path(app_handle: tauri::AppHandle) -> String {
return app_handle
.path()
.app_data_dir()
.unwrap()
.to_str()
.unwrap()
.to_string();
}
#[tauri::command]
pub fn get_active_extensions(app: AppHandle) -> Vec<serde_json::Value> {
let mut path = get_jan_extensions_path(app);
path.push("extensions.json");
println!("get jan extensions, path: {:?}", path);
let contents = fs::read_to_string(path);
let contents: Vec<serde_json::Value> = match contents {
Ok(data) => match serde_json::from_str::<Vec<serde_json::Value>>(&data) {
Ok(exts) => exts
.into_iter()
.map(|ext| {
serde_json::json!({
"url": ext["url"],
"name": ext["name"],
"productName": ext["productName"],
"active": ext["_active"],
"description": ext["description"],
"version": ext["version"]
})
})
.collect(),
Err(_) => vec![],
},
Err(_) => vec![],
};
return contents;
}
#[tauri::command]
pub fn get_user_home_path(app: AppHandle) -> String {
return get_app_configurations(app.clone()).data_folder;
}
fn extract_extension_manifest<R: Read>(archive: &mut Archive<R>) -> Result<Option<Value>, String> {
let entry = archive
.entries()
.map_err(|e| e.to_string())?
.filter_map(|e| e.ok()) // Ignore errors in individual entries
.find(|entry| {
if let Ok(file_path) = entry.path() {
let path_str = file_path.to_string_lossy();
path_str == "package/package.json" || path_str == "package.json"
} else {
false
}
});
if let Some(mut entry) = entry {
let mut content = String::new();
entry
.read_to_string(&mut content)
.map_err(|e| e.to_string())?;
let package_json: Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
return Ok(Some(package_json));
}
Ok(None)
}
pub fn install_extensions(app: tauri::AppHandle) -> Result<(), String> {
let extensions_path = get_jan_extensions_path(app.clone());
let pre_install_path = PathBuf::from("./../pre-install");
if !extensions_path.exists() {
fs::create_dir_all(&extensions_path).map_err(|e| e.to_string())?;
}
let extensions_json_path = extensions_path.join("extensions.json");
let mut extensions_list = if extensions_json_path.exists() {
let existing_data =
fs::read_to_string(&extensions_json_path).unwrap_or_else(|_| "[]".to_string());
serde_json::from_str::<Vec<Value>>(&existing_data).unwrap_or_else(|_| vec![])
} else {
vec![]
};
for entry in fs::read_dir(&pre_install_path).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "tgz") {
println!("Installing extension from {:?}", path);
let tar_gz = File::open(&path).map_err(|e| e.to_string())?;
let gz_decoder = GzDecoder::new(tar_gz);
let mut archive = Archive::new(gz_decoder);
let mut extension_name = None;
let mut extension_manifest = None;
extract_extension_manifest(&mut archive)
.map_err(|e| e.to_string())
.and_then(|manifest| match manifest {
Some(manifest) => {
extension_name = manifest["name"].as_str().map(|s| s.to_string());
extension_manifest = Some(manifest);
Ok(())
}
None => Err("Manifest is None".to_string()),
})?;
let extension_name = extension_name.ok_or("package.json not found in archive")?;
let extension_dir = extensions_path.join(extension_name.clone());
fs::create_dir_all(&extension_dir).map_err(|e| e.to_string())?;
let tar_gz = File::open(&path).map_err(|e| e.to_string())?;
let gz_decoder = GzDecoder::new(tar_gz);
let mut archive = Archive::new(gz_decoder);
for entry in archive.entries().map_err(|e| e.to_string())? {
let mut entry = entry.map_err(|e| e.to_string())?;
let file_path = entry.path().map_err(|e| e.to_string())?;
let components: Vec<_> = file_path.components().collect();
if components.len() > 1 {
let relative_path: PathBuf = components[1..].iter().collect();
let target_path = extension_dir.join(relative_path);
if let Some(parent) = target_path.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let _result = entry.unpack(&target_path).map_err(|e| e.to_string())?;
}
}
let main_entry = extension_manifest
.as_ref()
.and_then(|manifest| manifest["main"].as_str())
.unwrap_or("index.js");
let url = extension_dir.join(main_entry).to_string_lossy().to_string();
let new_extension = serde_json::json!({
"url": url,
"name": extension_name.clone(),
"origin": extension_dir.to_string_lossy(),
"active": true,
"description": extension_manifest
.as_ref()
.and_then(|manifest| manifest["description"].as_str())
.unwrap_or(""),
"version": extension_manifest
.as_ref()
.and_then(|manifest| manifest["version"].as_str())
.unwrap_or(""),
});
extensions_list.push(new_extension);
println!("Installed extension to {:?}", extension_dir);
}
}
fs::write(
&extensions_json_path,
serde_json::to_string_pretty(&extensions_list).map_err(|e| e.to_string())?,
)
.map_err(|e| e.to_string())?;
Ok(())
}

View File

@ -0,0 +1,61 @@
use crate::handlers::cmd::get_jan_data_folder_path;
use std::fs;
use std::path::PathBuf;
#[tauri::command]
pub fn rm(app_handle: tauri::AppHandle, args: Vec<String>) -> Result<(), String> {
if args.is_empty() || args[0].is_empty() {
return Err("rm error: Invalid argument".to_string());
}
let path = resolve_path(app_handle, &args[0]);
fs::remove_dir_all(&path).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn mkdir(app_handle: tauri::AppHandle, args: Vec<String>) -> Result<(), String> {
if args.is_empty() || args[0].is_empty() {
return Err("mkdir error: Invalid argument".to_string());
}
let path = resolve_path(app_handle, &args[0]);
fs::create_dir_all(&path).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn join_path(app_handle: tauri::AppHandle, args: Vec<String>) -> Result<String, String> {
if args.is_empty() {
return Err("join_path error: Invalid argument".to_string());
}
let path = resolve_path(app_handle, &args[0]);
let joined_path = path.join(args[1..].join("/"));
Ok(joined_path.to_string_lossy().to_string())
}
#[tauri::command]
pub fn exists_sync(app_handle: tauri::AppHandle, args: Vec<String>) -> Result<bool, String> {
if args.is_empty() || args[0].is_empty() {
return Err("exist_sync error: Invalid argument".to_string());
}
let path = resolve_path(app_handle, &args[0]);
Ok(path.exists())
}
fn normalize_file_path(path: &str) -> String {
path.replace("file:/", "").replace("file:\\", "")
}
fn resolve_path(app_handle: tauri::AppHandle, path: &str) -> PathBuf {
let path = if path.starts_with("file:/") || path.starts_with("file:\\") {
let normalized = normalize_file_path(path);
get_jan_data_folder_path(app_handle).join(normalized)
} else {
PathBuf::from(path)
};
if path.starts_with("http://") || path.starts_with("https://") {
path
} else {
path.canonicalize().unwrap_or(path)
}
}

View File

@ -0,0 +1,2 @@
pub mod cmd;
pub mod fs;

126
src-tauri/src/lib.rs Normal file
View File

@ -0,0 +1,126 @@
use std::fs;
use std::path::PathBuf;
mod handlers;
use crate::handlers::cmd;
use rand::{distributions::Alphanumeric, Rng};
use tauri::{command, Manager, State};
use tauri_plugin_shell::{process::CommandEvent, ShellExt};
struct AppState {
app_token: Option<String>,
}
#[command]
fn app_token(state: State<'_, AppState>) -> Option<String> {
// state.app_token.clone()
None
}
fn generate_app_token() -> String {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect()
}
fn copy_dir_all(src: PathBuf, dst: PathBuf) -> Result<(), String> {
fs::create_dir_all(&dst).map_err(|e| e.to_string())?;
println!("Copying from {:?} to {:?}", src, dst);
for entry in fs::read_dir(src).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let ty = entry.file_type().map_err(|e| e.to_string())?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.join(entry.file_name())).map_err(|e| e.to_string())?;
} else {
fs::copy(entry.path(), dst.join(entry.file_name())).map_err(|e| e.to_string())?;
}
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![
// handlers::fs::join_path,
// handlers::fs::mkdir,
// handlers::fs::exists_sync,
// handlers::fs::rm,
handlers::cmd::get_app_configurations,
handlers::cmd::get_active_extensions,
handlers::cmd::get_user_home_path,
handlers::cmd::update_app_configuration,
handlers::cmd::get_jan_data_folder_path,
handlers::cmd::get_jan_extensions_path,
app_token,
])
.manage(AppState {
app_token: Some(generate_app_token()),
})
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
// Setup sidecar
let sidecar_command = app.shell().sidecar("cortex-server").unwrap().args([
"--start-server",
"--port",
"39291",
"--config_file_path",
app.app_handle()
.path()
.app_data_dir()
.unwrap()
.join(".janrc")
.to_str()
.unwrap(),
"--data_folder_path",
app.app_handle()
.path()
.app_data_dir()
.unwrap()
.to_str()
.unwrap(),
// "config",
// "--api_keys",
]);
let (mut rx, mut _child) = sidecar_command.spawn().expect("Failed to spawn sidecar");
tauri::async_runtime::spawn(async move {
// read events such as stdout
while let Some(event) = rx.recv().await {
if let CommandEvent::Stdout(line_bytes) = event {
let line = String::from_utf8_lossy(&line_bytes);
println!("Outputs: {:?}", line)
}
}
});
// Install extensions
if let Err(e) = cmd::install_extensions(app.handle().clone()) {
eprintln!("Failed to install extensions: {}", e);
}
// Copy binaries to app_data
let app_data_dir = app.app_handle().path().app_data_dir().unwrap();
let binaries_dir = app.app_handle().path().resource_dir().unwrap().join("binaries");
if let Err(e) = copy_dir_all(binaries_dir, app_data_dir) {
eprintln!("Failed to copy binaries: {}", e);
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}

58
src-tauri/tauri.conf.json Normal file
View File

@ -0,0 +1,58 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "jan-app",
"version": "0.1.0",
"identifier": "jan.ai",
"build": {
"frontendDist": "../web/out",
"devUrl": "http://localhost:3000",
"beforeDevCommand": "yarn dev:web",
"beforeBuildCommand": "yarn build:web"
},
"app": {
"macOSPrivateApi": true,
"windows": [
{
"title": "jan-app",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false,
"hiddenTitle": true,
"transparent": true,
"titleBarStyle": "Overlay"
}
],
"security": {
"csp": {
"default-src": "'self' customprotocol: asset: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*",
"connect-src": "ipc: http://ipc.localhost",
"font-src": ["https://fonts.gstatic.com"],
"img-src": "'self' asset: http://asset.localhost blob: data:",
"style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com"
},
"assetProtocol": {
"enable": true,
"scope": {
"requireLiteralLeadingDot": false,
"allow": ["**/*"]
}
}
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [
"binaries/engines/**/*"
],
"externalBin": ["binaries/cortex-server"]
}
}

View File

@ -2,8 +2,9 @@
import { AIEngine, BaseExtension, ExtensionTypeEnum } from '@janhq/core'
import Extension from './Extension'
import { convertFileSrc } from '@tauri-apps/api/core'
import Extension from './Extension'
/**
* Manages the registration and retrieval of extensions.
*/
@ -123,13 +124,8 @@ export class ExtensionManager {
*/
async activateExtension(extension: Extension) {
// Import class
const extensionUrl = window.electronAPI
? extension.url
: extension.url.replace(
'extension://',
`${window.core?.api?.baseApiUrl ?? ''}/extensions/`
)
await import(/* webpackIgnore: true */ extensionUrl).then(
const extensionUrl = extension.url
await import(/* webpackIgnore: true */ convertFileSrc(extensionUrl)).then(
(extensionClass) => {
// Register class if it has a default export
if (
@ -158,7 +154,7 @@ export class ExtensionManager {
*/
async registerActive() {
// Get active extensions
const activeExtensions = await this.getActive()
const activeExtensions = (await this.getActive()) ?? []
// Activate all
await Promise.all(
activeExtensions.map((ext: Extension) => this.activateExtension(ext))

View File

@ -60,7 +60,7 @@ export default function useFactoryReset() {
quick_ask: appConfiguration?.quick_ask ?? false,
distinct_id: appConfiguration?.distinct_id,
}
await window.core?.api?.updateAppConfiguration(configuration)
await window.core?.api?.updateAppConfiguration({ configuration })
}
// Perform factory reset

View File

@ -8,6 +8,8 @@ import { useAtom, useAtomValue } from 'jotai'
import cssVars from '@/utils/jsonToCssVariables'
import themeData from '@/../../public/theme.json' with { type: 'json' }
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
import {
selectedThemeIdAtom,
@ -26,12 +28,13 @@ export const useLoadTheme = () => {
const setNativeTheme = useCallback(
(nativeTheme: NativeThemeProps) => {
if (!('setNativeThemeDark' in window.core.api)) return
if (nativeTheme === 'dark') {
window?.electronAPI?.setNativeThemeDark()
window?.core?.api?.setNativeThemeDark()
setTheme('dark')
localStorage.setItem('nativeTheme', 'dark')
} else {
window?.electronAPI?.setNativeThemeLight()
window?.core?.api?.setNativeThemeLight()
setTheme('light')
localStorage.setItem('nativeTheme', 'light')
}
@ -74,6 +77,13 @@ export const useLoadTheme = () => {
setThemeData(theme)
setNativeTheme(theme.nativeTheme)
applyTheme(theme)
} else {
// Apply default bundled theme
const theme: Theme | undefined = themeData
if (theme) {
setThemeData(theme)
applyTheme(theme)
}
}
}, [
janDataFolderPath,

View File

@ -42,10 +42,9 @@ const useModels = () => {
}))
.filter((e) => !('status' in e) || e.status !== 'downloadable')
const remoteModels = ModelManager.instance()
.models.values()
.toArray()
.filter((e) => e.engine !== InferenceEngine.cortex_llamacpp)
const remoteModels = Array.from(
ModelManager.instance().models.values()
).filter((e) => e.engine !== InferenceEngine.cortex_llamacpp)
const toUpdate = [
...localModels,
...remoteModels.filter(
@ -70,7 +69,7 @@ const useModels = () => {
}
const getExtensionModels = () => {
const models = ModelManager.instance().models.values().toArray()
const models = Array.from(ModelManager.instance().models.values())
setExtensionModels(models)
}
// Fetch all data
@ -81,7 +80,7 @@ const useModels = () => {
const reloadData = useDebouncedCallback(() => getData(), 300)
const updateStates = useCallback(() => {
const cachedModels = ModelManager.instance().models.values().toArray()
const cachedModels = Array.from(ModelManager.instance().models.values())
setDownloadedModels((downloadedModels) => [
...downloadedModels,
...cachedModels.filter(

View File

@ -229,14 +229,10 @@ export default function useSendChatMessage() {
}
setIsGeneratingResponse(true)
// Process message request with Assistants tools
const request = await ToolManager.instance().process(
requestBuilder.build(),
activeAssistantRef?.current.tools ?? []
)
// Request for inference
EngineManager.instance().get(InferenceEngine.cortex)?.inference(request)
EngineManager.instance()
.get(InferenceEngine.cortex)
?.inference(requestBuilder.build())
// Reset states
setReloadModel(false)

View File

@ -42,6 +42,7 @@ const nextConfig = {
isWindows: process.platform === 'win32',
isLinux: process.platform === 'linux',
PLATFORM: JSON.stringify(process.platform),
IS_TAURI: true,
}),
]
return config

View File

@ -20,6 +20,8 @@
"@janhq/joi": "link:../joi",
"@radix-ui/react-icons": "^1.3.2",
"@tanstack/react-virtual": "^3.10.9",
"@tauri-apps/api": "^2.4.0",
"@tauri-apps/plugin-http": "^2.4.2",
"@uppy/core": "^4.3.0",
"@uppy/react": "^4.0.4",
"@uppy/xhr-upload": "^4.2.3",
@ -81,6 +83,7 @@
"@types/uuid": "^9.0.6",
"@typescript-eslint/eslint-plugin": "^6.8.0",
"@typescript-eslint/parser": "^6.8.0",
"babel-loader": "^10.0.0",
"encoding": "^0.1.13",
"eslint": "8.52.0",
"eslint-config-next": "14.0.1",

View File

@ -73,7 +73,9 @@ const Advanced = ({ setSubdir }: { setSubdir: (subdir: string) => void }) => {
const appConfiguration: AppConfiguration =
await window.core?.api?.getAppConfigurations()
appConfiguration.quick_ask = e
await window.core?.api?.updateAppConfiguration(appConfiguration)
await window.core?.api?.updateAppConfiguration({
configuration: appConfiguration,
})
if (relaunchApp) relaunch()
}

View File

@ -3,6 +3,7 @@ import { EngineManager, ToolManager } from '@janhq/core'
import { appService } from './appService'
import { EventEmitter } from './eventsService'
import { restAPI } from './restService'
import { tauriAPI } from './tauriService'
export const setupCoreServices = () => {
if (typeof window === 'undefined') {
@ -17,7 +18,11 @@ export const setupCoreServices = () => {
engineManager: new EngineManager(),
toolManager: new ToolManager(),
api: {
...(window.electronAPI ? window.electronAPI : restAPI),
...(window.electronAPI
? window.electronAPI
: IS_TAURI
? tauriAPI
: restAPI),
...appService,
},
}

View File

@ -17,6 +17,7 @@ export const restAPI = {
return {
...acc,
[proxy.route]: (...args: any) => {
return Promise.resolve(undefined)
// For each route, define a function that sends a request to the API
return fetch(
`${window.core?.api.baseApiUrl}/v1/${proxy.path}/${proxy.route}`,
@ -41,6 +42,6 @@ export const restAPI = {
}, {}),
openExternalUrl,
// Jan Server URL
baseApiUrl: process.env.API_BASE_URL ?? API_BASE_URL,
baseApiUrl: undefined, //process.env.API_BASE_URL ?? API_BASE_URL,
pollingInterval: 5000,
}

View File

@ -0,0 +1,34 @@
import { CoreRoutes, APIRoutes } from '@janhq/core'
import { invoke } from '@tauri-apps/api/core'
// Define API routes based on different route types
export const Routes = [...CoreRoutes, ...APIRoutes].map((r) => ({
path: `app`,
route: r,
}))
// Function to open an external URL in a new browser window
export function openExternalUrl(url: string) {
window?.open(url, '_blank')
}
// Define the restAPI object with methods for each API route
export const tauriAPI = {
...Object.values(Routes).reduce((acc, proxy) => {
return {
...acc,
/* eslint-disable @typescript-eslint/no-explicit-any */
[proxy.route]: (...args: any) => {
// For each route, define a function that sends a request to the API
return invoke(
proxy.route.replace(/([A-Z])/g, '_$1').toLowerCase(),
...args
)
},
}
}, {}),
openExternalUrl,
// Jan Server URL
baseApiUrl: undefined, //process.env.API_BASE_URL ?? API_BASE_URL,
pollingInterval: 5000,
}

View File

@ -14,12 +14,13 @@ declare global {
declare const isWindows: boolean
declare const isLinux: boolean
declare const PLATFORM: string
declare const IS_TAURI: boolean
interface Core {
api: APIFunctions
events: EventEmitter
}
interface Window {
core?: Core | undefined
electronAPI?: any | undefined
core?: Core
electronAPI?: any
}
}

View File

@ -8,7 +8,9 @@ export const updateDistinctId = async (id: string) => {
const appConfiguration: AppConfiguration =
await window.core?.api?.getAppConfigurations()
appConfiguration.distinct_id = id
await window.core?.api?.updateAppConfiguration(appConfiguration)
await window.core?.api?.updateAppConfiguration({
configuration: appConfiguration,
})
}
/**