feat: tauri toolkit
@ -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.
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -94,8 +94,6 @@ export default class Extension {
|
||||
`Package ${this.origin} does not contain a valid manifest: ${error}`
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
4908
src-tauri/Cargo.lock
generated
Normal file
30
src-tauri/Cargo.toml
Normal 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
@ -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
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
45
src-tauri/capabilities/default.json
Normal 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
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
291
src-tauri/src/handlers/cmd.rs
Normal 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(())
|
||||
}
|
||||
61
src-tauri/src/handlers/fs.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
2
src-tauri/src/handlers/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod cmd;
|
||||
pub mod fs;
|
||||
126
src-tauri/src/lib.rs
Normal 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
@ -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
@ -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"]
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -42,6 +42,7 @@ const nextConfig = {
|
||||
isWindows: process.platform === 'win32',
|
||||
isLinux: process.platform === 'linux',
|
||||
PLATFORM: JSON.stringify(process.platform),
|
||||
IS_TAURI: true,
|
||||
}),
|
||||
]
|
||||
return config
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
34
web/services/tauriService.ts
Normal 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,
|
||||
}
|
||||
5
web/types/index.d.ts
vendored
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||