From d982dce090231f012aa2e4c4e16c5e85b0b3c08b Mon Sep 17 00:00:00 2001 From: James Date: Tue, 10 Oct 2023 07:18:40 -0700 Subject: [PATCH 1/5] feat: allowing user to fetch models from github Signed-off-by: James --- electron/core/plugins/data-plugin/index.ts | 1 + electron/core/plugins/data-plugin/module.ts | 391 +++++++---- .../core/plugins/inference-plugin/module.ts | 8 +- .../plugins/model-management-plugin/index.js | 10 + .../plugins/model-management-plugin/module.js | 225 ++++--- package-lock.json | 618 +++++------------- .../_components/ActiveModelTable/index.tsx | 6 +- .../_components/AvailableModelCard/index.tsx | 28 +- .../_components/ConversationalCard/index.tsx | 12 +- .../_components/ConversationalList/index.tsx | 10 +- .../_components/DownloadedModelCard/index.tsx | 18 +- .../_components/ExploreModelItem/index.tsx | 2 +- .../ExploreModelItemHeader/index.tsx | 2 +- .../_components/ExploreModelList/index.tsx | 44 +- web/app/_components/InputToolbar/index.tsx | 9 +- web/app/_components/ModelRow/index.tsx | 31 +- web/app/_components/ModelSelector/index.tsx | 4 +- web/app/_components/ModelTable/index.tsx | 4 +- .../_components/ModelVersionItem/index.tsx | 13 +- .../_components/ModelVersionList/index.tsx | 5 +- web/app/_components/MonitorBar/index.tsx | 4 +- web/app/_components/NewChatButton/index.tsx | 8 +- .../_components/SidebarEmptyHistory/index.tsx | 8 +- .../_helpers/atoms/DownloadedModel.atom.ts | 7 +- web/app/_helpers/atoms/Model.atom.ts | 8 +- web/app/_hooks/useCreateConversation.ts | 4 +- web/app/_hooks/useDeleteModel.ts | 6 +- web/app/_hooks/useDownloadModel.ts | 32 +- web/app/_hooks/useGetAvailableModels.ts | 54 -- web/app/_hooks/useGetConfiguredModels.ts | 20 + web/app/_hooks/useGetDownloadedModels.ts | 75 +-- web/app/_hooks/useInitModel.ts | 11 +- web/app/_hooks/useStartStopModel.ts | 4 +- web/app/_models/AssistantModel.ts | 65 ++ web/app/_models/ModelVersion.ts | 23 + web/app/_models/Product.ts | 35 +- web/app/_services/pluginService.ts | 4 +- web/shared/coreService.ts | 3 +- 38 files changed, 798 insertions(+), 1014 deletions(-) delete mode 100644 web/app/_hooks/useGetAvailableModels.ts create mode 100644 web/app/_hooks/useGetConfiguredModels.ts create mode 100644 web/app/_models/AssistantModel.ts create mode 100644 web/app/_models/ModelVersion.ts diff --git a/electron/core/plugins/data-plugin/index.ts b/electron/core/plugins/data-plugin/index.ts index 3609c629b..cec71c820 100644 --- a/electron/core/plugins/data-plugin/index.ts +++ b/electron/core/plugins/data-plugin/index.ts @@ -95,6 +95,7 @@ const createConversation = (conversation: any) => resolve(undefined); } }); + const createMessage = (message: any) => new Promise((resolve) => { if (window && window.electronAPI) { diff --git a/electron/core/plugins/data-plugin/module.ts b/electron/core/plugins/data-plugin/module.ts index 3e01fe502..a9bc20040 100644 --- a/electron/core/plugins/data-plugin/module.ts +++ b/electron/core/plugins/data-plugin/module.ts @@ -5,54 +5,78 @@ const { app } = require("electron"); const MODEL_TABLE_CREATION = ` CREATE TABLE IF NOT EXISTS models ( id TEXT PRIMARY KEY, - slug TEXT NOT NULL, name TEXT NOT NULL, - description TEXT NOT NULL, + short_description TEXT NOT NULL, avatar_url TEXT, long_description TEXT NOT NULL, - technical_description TEXT NOT NULL, author TEXT NOT NULL, version TEXT NOT NULL, model_url TEXT NOT NULL, nsfw INTEGER NOT NULL, - greeting TEXT NOT NULL, + tags TEXT NOT NULL, + default_greeting TEXT NOT NULL, type TEXT NOT NULL, - file_name TEXT NOT NULL, - download_url TEXT NOT NULL, - start_download_at INTEGER DEFAULT -1, - finish_download_at INTEGER DEFAULT -1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP );`; +const MODEL_VERSION_TABLE_CREATION = ` +CREATE TABLE IF NOT EXISTS model_versions ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + quant_method TEXT NOT NULL, + bits INTEGER NOT NULL, + size INTEGER NOT NULL, + max_ram_required INTEGER NOT NULL, + usecase TEXT NOT NULL, + download_link TEXT NOT NULL, + model_id TEXT NOT NULL, + start_download_at INTEGER DEFAULT -1, + finish_download_at INTEGER DEFAULT -1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +);`; + const MODEL_TABLE_INSERTION = ` -INSERT INTO models ( +INSERT OR IGNORE INTO models ( id, - slug, name, - description, + short_description, avatar_url, long_description, - technical_description, author, version, model_url, nsfw, - greeting, - type, - file_name, - download_url, + tags, + default_greeting, + type +) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`; + +const MODEL_VERSION_TABLE_INSERTION = ` +INSERT INTO model_versions ( + id, + name, + quant_method, + bits, + size, + max_ram_required, + usecase, + download_link, + model_id, start_download_at -) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`; +) VALUES (?,?,?,?,?,?,?,?,?,?)`; + +const getDbPath = () => { + return path.join(app.getPath("userData"), "jan.db"); +}; function init() { - const db = new sqlite3.Database(path.join(app.getPath("userData"), "jan.db")); - console.log( - `Database located at ${path.join(app.getPath("userData"), "jan.db")}` - ); + const db = new sqlite3.Database(getDbPath()); + console.debug(`Database located at ${getDbPath()}`); db.serialize(() => { db.run(MODEL_TABLE_CREATION); + db.run(MODEL_VERSION_TABLE_CREATION); db.run( "CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY, name TEXT, model_id TEXT, image TEXT, message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP);" ); @@ -67,33 +91,59 @@ function init() { /** * Store a model in the database when user start downloading it * - * @param model Product + * @param params: { model, modelVersion } */ -function storeModel(model: any) { +function storeModel(params: any) { return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") - ); - console.debug("Inserting", JSON.stringify(model)); + const db = new sqlite3.Database(getDbPath()); + console.debug("Inserting", JSON.stringify(params)); + + const model = params.model; + const modelTags = model.tags.join(","); + const modelVersion = params.modelVersion; + db.serialize(() => { const stmt = db.prepare(MODEL_TABLE_INSERTION); stmt.run( model.id, - model.slug, model.name, - model.description, + model.shortDescription, model.avatarUrl, model.longDescription, - model.technicalDescription, model.author, model.version, model.modelUrl, model.nsfw, + modelTags, model.greeting, model.type, - model.fileName, - model.downloadUrl, - Date.now(), + function (err: any) { + if (err) { + // Handle the insertion error here + console.error(err.message); + res(undefined); + return; + } + // @ts-ignoreF + const id = this.lastID; + res(id); + return; + } + ); + + // insert modelVersion to MODEL_VERSION_TABLE_INSERTION + const stmt2 = db.prepare(MODEL_VERSION_TABLE_INSERTION); + stmt2.run( + modelVersion.id, + modelVersion.name, + modelVersion.quantMethod, + modelVersion.bits, + modelVersion.size, + modelVersion.maxRamRequired, + modelVersion.usecase, + modelVersion.downloadLink, + model.id, + modelVersion.startDownloadAt, function (err: any) { if (err) { // Handle the insertion error here @@ -108,6 +158,7 @@ function storeModel(model: any) { } ); stmt.finalize(); + stmt2.finalize(); }); db.close(); @@ -119,14 +170,15 @@ function storeModel(model: any) { * * @param model Product */ -function updateFinishedDownloadAt(fileName: string, time: number) { +function updateFinishedDownloadAt(modelVersionId: string) { return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") + const db = new sqlite3.Database(getDbPath()); + const time = Date.now(); + console.debug( + `Updating finished downloaded model version ${modelVersionId}` ); - console.debug(`Updating fileName ${fileName} to ${time}`); - const stmt = `UPDATE models SET finish_download_at = ? WHERE file_name = ?`; - db.run(stmt, [time, fileName], (err: any) => { + const stmt = `UPDATE model_versions SET finish_download_at = ? WHERE id = ?`; + db.run(stmt, [time, modelVersionId], (err: any) => { if (err) { console.log(err); res(undefined); @@ -145,11 +197,9 @@ function updateFinishedDownloadAt(fileName: string, time: number) { */ function getUnfinishedDownloadModels() { return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") - ); + const db = new sqlite3.Database(getDbPath()); - const query = `SELECT * FROM models WHERE finish_download_at = -1 ORDER BY start_download_at DESC`; + const query = `SELECT * FROM model_versions WHERE finish_download_at = -1 ORDER BY start_download_at DESC`; db.all(query, (err: Error, row: any) => { if (row) { res(row); @@ -161,28 +211,85 @@ function getUnfinishedDownloadModels() { }); } -function getFinishedDownloadModels() { - return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") - ); +async function getFinishedDownloadModels() { + const db = new sqlite3.Database(getDbPath()); - const query = `SELECT * FROM models WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`; - db.all(query, (err: Error, row: any) => { - res(row?.map((item: any) => parseToProduct(item)) ?? []); + const query = `SELECT * FROM model_versions WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`; + const modelVersions: any = await new Promise((resolve, reject) => { + db.all(query, (err: Error, rows: any[]) => { + if (err) { + reject(err); + } else { + resolve(rows); + } }); - db.close(); }); + + const models = await Promise.all( + modelVersions.map(async (modelVersion) => { + const modelQuery = `SELECT * FROM models WHERE id = ?`; + return new Promise((resolve, reject) => { + db.get(modelQuery, [modelVersion.model_id], (err: Error, row: any) => { + if (err) { + reject(err); + } else { + resolve(row); + } + }); + }); + }) + ); + + const downloadedModels = []; + modelVersions.forEach((modelVersion: any) => { + const model = models.find((m: any) => m.id === modelVersion.model_id); + + if (!model) { + return; + } + + const assistantModel = { + id: modelVersion.id, + name: modelVersion.name, + quantMethod: modelVersion.quant_method, + bits: modelVersion.bits, + size: modelVersion.size, + maxRamRequired: modelVersion.max_ram_required, + usecase: modelVersion.usecase, + downloadLink: modelVersion.download_link, + startDownloadAt: modelVersion.start_download_at, + finishDownloadAt: modelVersion.finish_download_at, + productId: model.id, + productName: model.name, + shortDescription: model.short_description, + longDescription: model.long_description, + avatarUrl: model.avatar_url, + author: model.author, + version: model.version, + modelUrl: model.model_url, + nsfw: model.nsfw === 0 ? false : true, + greeting: model.default_greeting, + type: model.type, + createdAt: new Date(model.created_at).getTime(), + updatedAt: new Date(model.updated_at ?? "").getTime(), + status: "", + releaseDate: -1, + tags: model.tags.split(","), + }; + downloadedModels.push(assistantModel); + }); + + db.close(); + + return downloadedModels; } function deleteDownloadModel(modelId: string) { return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") - ); - console.log(`Deleting ${modelId}`); + const db = new sqlite3.Database(getDbPath()); + console.debug(`Deleting ${modelId}`); db.serialize(() => { - const stmt = db.prepare("DELETE FROM models WHERE id = ?"); + const stmt = db.prepare("DELETE FROM model_versions WHERE id = ?"); stmt.run(modelId); stmt.finalize(); res(modelId); @@ -192,54 +299,101 @@ function deleteDownloadModel(modelId: string) { }); } -function getModelById(modelId: string) { - return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") - ); - +async function fetchModelVersion(db: any, versionId: string) { + return new Promise((resolve, reject) => { db.get( - `SELECT * FROM models WHERE id = ?`, - [modelId], - (err: any, row: any) => { - if (row) { - const product = { - id: row.id, - slug: row.slug, - name: row.name, - description: row.description, - avatarUrl: row.avatar_url, - longDescription: row.long_description, - technicalDescription: row.technical_description, - author: row.author, - version: row.version, - modelUrl: row.model_url, - nsfw: row.nsfw, - greeting: row.greeting, - type: row.type, - inputs: row.inputs, - outputs: row.outputs, - createdAt: new Date(row.created_at), - updatedAt: new Date(row.updated_at), - fileName: row.file_name, - downloadUrl: row.download_url, - }; - res(product); + "SELECT * FROM model_versions WHERE id = ?", + [versionId], + (err, row) => { + if (err) { + reject(err); } else { - res(undefined); + if (row) { + const product = { + id: row.id, + slug: row.slug, + name: row.name, + description: row.description, + avatarUrl: row.avatar_url, + longDescription: row.long_description, + technicalDescription: row.technical_description, + author: row.author, + version: row.version, + modelUrl: row.model_url, + nsfw: row.nsfw, + greeting: row.greeting, + type: row.type, + inputs: row.inputs, + outputs: row.outputs, + createdAt: new Date(row.created_at), + updatedAt: new Date(row.updated_at), + fileName: row.file_name, + downloadUrl: row.download_url, + }; + resolve(product); + } else { + resolve(undefined); + } } } ); - - db.close(); }); } +async function fetchModel(db: any, modelId: string) { + return new Promise((resolve, reject) => { + db.get("SELECT * FROM models WHERE id = ?", [modelId], (err, row) => { + if (err) { + reject(err); + } else { + resolve(row); + } + }); + }); +} + +const getModelById = async (versionId: string): Promise => { + const db = new sqlite3.Database(getDbPath()); + const modelVersion: any | undefined = await fetchModelVersion(db, versionId); + if (!modelVersion) return undefined; + const model: any | undefined = await fetchModel(db, modelVersion.model_id); + if (!model) return undefined; + + const assistantModel = { + id: modelVersion.id, + name: modelVersion.name, + quantMethod: modelVersion.quant_method, + bits: modelVersion.bits, + size: modelVersion.size, + maxRamRequired: modelVersion.max_ram_required, + usecase: modelVersion.usecase, + downloadLink: modelVersion.download_link, + startDownloadAt: modelVersion.start_download_at, + finishDownloadAt: modelVersion.finish_download_at, + productId: model.id, + productName: model.name, + shortDescription: model.short_description, + longDescription: model.long_description, + avatarUrl: model.avatar_url, + author: model.author, + version: model.version, + modelUrl: model.model_url, + nsfw: model.nsfw === 0 ? false : true, + greeting: model.default_greeting, + type: model.type, + createdAt: new Date(model.created_at).getTime(), + updatedAt: new Date(model.updated_at ?? "").getTime(), + status: "", + releaseDate: -1, + tags: model.tags.split(","), + }; + + return assistantModel; +}; + function getConversations() { return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") - ); + const db = new sqlite3.Database(getDbPath()); db.all( "SELECT * FROM conversations ORDER BY updated_at DESC", @@ -250,11 +404,10 @@ function getConversations() { db.close(); }); } + function storeConversation(conversation: any): Promise { return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") - ); + const db = new sqlite3.Database(getDbPath()); db.serialize(() => { const stmt = db.prepare( @@ -287,9 +440,7 @@ function storeConversation(conversation: any): Promise { function storeMessage(message: any): Promise { return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") - ); + const db = new sqlite3.Database(getDbPath()); db.serialize(() => { const stmt = db.prepare( @@ -319,11 +470,10 @@ function storeMessage(message: any): Promise { db.close(); }); } + function updateMessage(message: any): Promise { return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") - ); + const db = new sqlite3.Database(getDbPath()); db.serialize(() => { const stmt = db.prepare( @@ -340,9 +490,7 @@ function updateMessage(message: any): Promise { function deleteConversation(id: any) { return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") - ); + const db = new sqlite3.Database(getDbPath()); db.serialize(() => { const deleteConv = db.prepare("DELETE FROM conversations WHERE id = ?"); @@ -362,9 +510,7 @@ function deleteConversation(id: any) { function getConversationMessages(conversation_id: any) { return new Promise((res) => { - const db = new sqlite3.Database( - path.join(app.getPath("userData"), "jan.db") - ); + const db = new sqlite3.Database(getDbPath()); const query = `SELECT * FROM messages WHERE conversation_id = ${conversation_id} ORDER BY id DESC`; db.all(query, (err: Error, row: any) => { @@ -374,31 +520,6 @@ function getConversationMessages(conversation_id: any) { }); } -function parseToProduct(row: any) { - const product = { - id: row.id, - slug: row.slug, - name: row.name, - description: row.description, - avatarUrl: row.avatar_url, - longDescription: row.long_description, - technicalDescription: row.technical_description, - author: row.author, - version: row.version, - modelUrl: row.model_url, - nsfw: row.nsfw, - greeting: row.greeting, - type: row.type, - inputs: row.inputs, - outputs: row.outputs, - createdAt: new Date(row.created_at), - updatedAt: new Date(row.updated_at), - fileName: row.file_name, - downloadUrl: row.download_url, - }; - return product; -} - module.exports = { init, getConversations, diff --git a/electron/core/plugins/inference-plugin/module.ts b/electron/core/plugins/inference-plugin/module.ts index ac70b8c68..e80bc804c 100644 --- a/electron/core/plugins/inference-plugin/module.ts +++ b/electron/core/plugins/inference-plugin/module.ts @@ -8,10 +8,10 @@ const { killPortProcess } = require("kill-port-process"); let subprocess = null; const PORT = 3928; -const initModel = (product) => { +const initModel = (fileName) => { return ( new Promise(async (resolve, reject) => { - if (!product?.fileName) { + if (!fileName) { reject("Model not found, please download again."); } if (subprocess) { @@ -20,7 +20,7 @@ const initModel = (product) => { ); killSubprocess(); } - resolve(product?.fileName); + resolve(fileName); }) // Kill port process if it is already in use .then((fileName) => @@ -46,7 +46,7 @@ const initModel = (product) => { config.custom_config = {}; } - const modelPath = path.join(app.getPath("userData"), product.fileName); + const modelPath = path.join(app.getPath("userData"), fileName); config.custom_config.llama_model_path = modelPath; diff --git a/electron/core/plugins/model-management-plugin/index.js b/electron/core/plugins/model-management-plugin/index.js index 702a04374..f19382d66 100644 --- a/electron/core/plugins/model-management-plugin/index.js +++ b/electron/core/plugins/model-management-plugin/index.js @@ -47,6 +47,15 @@ const searchModels = async (params) => } }); +const getConfiguredModels = async () => + new Promise(async (resolve) => { + if (window.electronAPI) { + window.electronAPI + .invokePluginFunc(MODULE_PATH, "getConfiguredModels") + .then((res) => resolve(res)); + } + }); + // Register all the above functions and objects with the relevant extension points export function init({ register }) { register("getDownloadedModels", "getDownloadedModels", getDownloadedModels); @@ -54,4 +63,5 @@ export function init({ register }) { register("downloadModel", "downloadModel", downloadModel); register("deleteModel", "deleteModel", deleteModel); register("searchModels", "searchModels", searchModels); + register("getConfiguredModels", "getConfiguredModels", getConfiguredModels); } diff --git a/electron/core/plugins/model-management-plugin/module.js b/electron/core/plugins/model-management-plugin/module.js index ced07973e..a5a4ff02c 100644 --- a/electron/core/plugins/model-management-plugin/module.js +++ b/electron/core/plugins/model-management-plugin/module.js @@ -1,95 +1,16 @@ -const path = require("path"); -const { readdirSync, lstatSync } = require("fs"); -const { app } = require("electron"); const { listModels, listFiles, fileDownloadInfo } = require("@huggingface/hub"); +const https = require("https"); let modelsIterator = undefined; let currentSearchOwner = undefined; -const ALL_MODELS = [ - { - id: "llama-2-7b-chat.Q4_K_M.gguf.bin", - slug: "llama-2-7b-chat.Q4_K_M.gguf.bin", - name: "Llama 2 7B Chat - GGUF", - description: "medium, balanced quality - recommended", - avatarUrl: - "https://aeiljuispo.cloudimg.io/v7/https://cdn-uploads.huggingface.co/production/uploads/6426d3f3a7723d62b53c259b/tvPikpAzKTKGN5wrpadOJ.jpeg?w=200&h=200&f=face", - longDescription: - "GGUF is a new format introduced by the llama.cpp team on August 21st 2023. It is a replacement for GGML, which is no longer supported by llama.cpp. GGUF offers numerous advantages over GGML, such as better tokenisation, and support for special tokens. It is also supports metadata, and is designed to be extensible.", - technicalDescription: - 'GGML_TYPE_Q4_K - "type-1" 4-bit quantization in super-blocks containing 8 blocks, each block having 32 weights. Scales and mins are quantized with 6 bits. This ends up using 4.5 bpw.', - author: "The Bloke", - version: "1.0.0", - modelUrl: "https://google.com", - nsfw: false, - greeting: "Hello there", - type: "LLM", - inputs: undefined, - outputs: undefined, - createdAt: 0, - updatedAt: undefined, - fileName: "llama-2-7b-chat.Q4_K_M.gguf.bin", - downloadUrl: - "https://huggingface.co/TheBloke/Llama-2-7b-Chat-GGUF/resolve/main/llama-2-7b-chat.Q4_K_M.gguf", - }, - { - id: "llama-2-13b-chat.Q4_K_M.gguf", - slug: "llama-2-13b-chat.Q4_K_M.gguf", - name: "Llama 2 13B Chat - GGUF", - description: - "medium, balanced quality - not recommended for RAM 16GB and below", - avatarUrl: - "https://aeiljuispo.cloudimg.io/v7/https://cdn-uploads.huggingface.co/production/uploads/6426d3f3a7723d62b53c259b/tvPikpAzKTKGN5wrpadOJ.jpeg?w=200&h=200&f=face", - longDescription: - "GGUF is a new format introduced by the llama.cpp team on August 21st 2023. It is a replacement for GGML, which is no longer supported by llama.cpp. GGUF offers numerous advantages over GGML, such as better tokenisation, and support for special tokens. It is also supports metadata, and is designed to be extensible.", - technicalDescription: - 'GGML_TYPE_Q4_K - "type-1" 4-bit quantization in super-blocks containing 8 blocks, each block having 32 weights. Scales and mins are quantized with 6 bits. This ends up using 4.5 bpw.', - author: "The Bloke", - version: "1.0.0", - modelUrl: "https://google.com", - nsfw: false, - greeting: "Hello there", - type: "LLM", - inputs: undefined, - outputs: undefined, - createdAt: 0, - updatedAt: undefined, - fileName: "llama-2-13b-chat.Q4_K_M.gguf.bin", - downloadUrl: - "https://huggingface.co/TheBloke/Llama-2-13B-chat-GGUF/resolve/main/llama-2-13b-chat.Q4_K_M.gguf", - }, -]; - -function getDownloadedModels() { - const userDataPath = app.getPath("userData"); - - const allBinariesName = []; - var files = readdirSync(userDataPath); - for (var i = 0; i < files.length; i++) { - var filename = path.join(userDataPath, files[i]); - var stat = lstatSync(filename); - if (stat.isDirectory()) { - // ignore - } else if (filename.endsWith(".bin")) { - var binaryName = path.basename(filename); - allBinariesName.push(binaryName); - } - } - - const downloadedModels = ALL_MODELS.map((model) => { - if ( - model.fileName && - allBinariesName - .map((t) => t.toLowerCase()) - .includes(model.fileName.toLowerCase()) - ) { - return model; - } - return undefined; - }).filter((m) => m !== undefined); - - return downloadedModels; -} +// Github API +const githubHostName = "api.github.com"; +const githubHeaders = { + "User-Agent": "node.js", + Accept: "application/vnd.github.v3+json", +}; +const githubPath = "/repos/janhq/models/contents"; const getNextModels = async (count) => { const models = []; @@ -161,17 +82,131 @@ const listFilesByName = async (modelName) => { return fileDownloadInfoMap; }; -function getAvailableModels() { - const downloadedModelIds = getDownloadedModels().map((model) => model.id); - return ALL_MODELS.filter((model) => { - if (!downloadedModelIds.includes(model.id)) { - return model; - } +async function getConfiguredModels() { + const files = await getModelFiles(); + + const promises = files.map((file) => getContent(file)); + const response = await Promise.all(promises); + + const models = []; + response.forEach((model) => { + models.push(parseToModel(model)); }); + + return models; +} + +const parseToModel = (model) => { + const modelVersions = []; + model.versions.forEach((v) => { + const version = { + id: `${model.author}-${v.name}`, + name: v.name, + quantMethod: v.quantMethod, + bits: v.bits, + size: v.size, + maxRamRequired: v.maxRamRequired, + usecase: v.usecase, + downloadLink: v.downloadLink, + productId: model.id, + }; + modelVersions.push(version); + }); + + const product = { + id: model.id, + name: model.name, + shortDescription: model.shortDescription, + avatarUrl: model.avatarUrl, + author: model.author, + version: model.version, + modelUrl: model.modelUrl, + nsfw: model.nsfw, + tags: model.tags, + greeting: model.defaultGreeting, + type: model.type, + createdAt: model.createdAt, + longDescription: model.longDescription, + status: "Downloadable", + releaseDate: 0, + availableVersions: modelVersions, + }; + return product; +}; + +async function getModelFiles() { + const options = { + hostname: githubHostName, + path: githubPath, + headers: githubHeaders, + }; + + const data = await new Promise((resolve, reject) => { + const req = https.request(options, (res) => { + let data = ""; + + res.on("data", (chunk) => { + data += chunk; + }); + + res.on("end", () => { + const files = JSON.parse(data); + + if (files.filter == null) { + console.error(files.message); + reject(files.message ?? "No files found"); + } + if (!files || files.length === 0) { + resolve([]); + } + const jsonFiles = files.filter((file) => file.name.endsWith(".json")); + resolve(jsonFiles); + }); + }); + + req.on("error", (error) => { + console.error(error); + }); + + req.end(); + }); + + return data; +} + +async function getContent(file) { + const options = { + hostname: githubHostName, + path: `${githubPath}/${file.path}`, + headers: githubHeaders, + }; + + const data = await new Promise((resolve) => { + const req = https.request(options, (res) => { + let data = ""; + + res.on("data", (chunk) => { + data += chunk; + }); + + res.on("end", () => { + const fileData = JSON.parse(data); + const fileContent = Buffer.from(fileData.content, "base64").toString(); + resolve(JSON.parse(fileContent)); + }); + }); + + req.on("error", (error) => { + console.error(error); + }); + + req.end(); + }); + + return data; } module.exports = { - getDownloadedModels, - getAvailableModels, searchModels, + getConfiguredModels, }; diff --git a/package-lock.json b/package-lock.json index ccedb15ba..5143bc970 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,22 +32,24 @@ "license": "MIT", "dependencies": { "@npmcli/arborist": "^7.1.0", - "electron-mocha": "^12.1.0", + "@uiball/loaders": "^1.3.0", "electron-store": "^8.1.0", "electron-updater": "^6.1.4", "pacote": "^17.0.4", + "react-intersection-observer": "^9.5.2", "request": "^2.88.2", - "request-progress": "^3.0.0" + "request-progress": "^3.0.0", + "use-debounce": "^9.0.4" }, "devDependencies": { + "@electron/notarize": "^2.1.0", "@playwright/test": "^1.38.1", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", "electron": "26.2.1", "electron-builder": "^24.6.4", "electron-playwright-helpers": "^1.6.0", - "eslint-plugin-react": "^7.33.2", - "xvfb-maybe": "^0.2.1" + "eslint-plugin-react": "^7.33.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -711,6 +713,126 @@ "node": ">= 10" } }, + "node_modules/@next/swc-darwin-x64": { + "version": "13.4.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.10.tgz", + "integrity": "sha512-ngXhUBbcZIWZWqNbQSNxQrB9T1V+wgfCzAor2olYuo/YpaL6mUYNUEgeBMhr8qwV0ARSgKaOp35lRvB7EmCRBg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.4.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.10.tgz", + "integrity": "sha512-SjCZZCOmHD4uyM75MVArSAmF5Y+IJSGroPRj2v9/jnBT36SYFTORN8Ag/lhw81W9EeexKY/CUg2e9mdebZOwsg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "13.4.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.10.tgz", + "integrity": "sha512-F+VlcWijX5qteoYIOxNiBbNE8ruaWuRlcYyIRK10CugqI/BIeCDzEDyrHIHY8AWwbkTwe6GRHabMdE688Rqq4Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "13.4.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.10.tgz", + "integrity": "sha512-WDv1YtAV07nhfy3i1visr5p/tjiH6CeXp4wX78lzP1jI07t4PnHHG1WEDFOduXh3WT4hG6yN82EQBQHDi7hBrQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "13.4.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.10.tgz", + "integrity": "sha512-zFkzqc737xr6qoBgDa3AwC7jPQzGLjDlkNmt/ljvQJ/Veri5ECdHjZCUuiTUfVjshNIIpki6FuP0RaQYK9iCRg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.4.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.10.tgz", + "integrity": "sha512-IboRS8IWz5mWfnjAdCekkl8s0B7ijpWeDwK2O8CdgZkoCDY0ZQHBSGiJ2KViAG6+BJVfLvcP+a2fh6cdyBr9QQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.4.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.10.tgz", + "integrity": "sha512-bSA+4j8jY4EEiwD/M2bol4uVEu1lBlgsGdvM+mmBm/BbqofNBfaZ2qwSbwE2OwbAmzNdVJRFRXQZ0dkjopTRaQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "13.4.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.10.tgz", + "integrity": "sha512-g2+tU63yTWmcVQKDGY0MV1PjjqgZtwM4rB1oVVi/v0brdZAcrcTV+04agKzWtvWroyFz6IqtT0MoZJA7PNyLVw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1666,6 +1788,15 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiball/loaders": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@uiball/loaders/-/loaders-1.3.0.tgz", + "integrity": "sha512-w372e7PMt/s6LZ321HoghgDDU8fomamAzJfrVAdBUhsWERJEpxJMqG37NFztUq/T4J7nzzjkvZI4UX7Z2F/O6A==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -1788,14 +1919,6 @@ "ajv": "^6.9.1" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2610,11 +2733,6 @@ "node": ">=8" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" - }, "node_modules/browserslist": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", @@ -2856,17 +2974,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -3104,6 +3211,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -3607,17 +3715,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "license": "MIT" }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -4070,38 +4167,6 @@ "node": ">=14.0.0" } }, - "node_modules/electron-mocha": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/electron-mocha/-/electron-mocha-12.1.0.tgz", - "integrity": "sha512-9ZIvyHGbet4ZtvF2NYYjGm7/yPljnTxbHo8psVX/HQAFPp9vZE0mCNWlzwE42keq/42gBW6W40MtKmgn1v42hQ==", - "dependencies": { - "ansi-colors": "^4.1.1", - "electron-window": "^0.8.0", - "mocha": "^10.2.0", - "which": "^3.0.0", - "yargs": "^17.7.2" - }, - "bin": { - "electron-mocha": "bin/electron-mocha" - }, - "engines": { - "node": ">= 16.0.0" - } - }, - "node_modules/electron-mocha/node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/electron-playwright-helpers": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/electron-playwright-helpers/-/electron-playwright-helpers-1.6.0.tgz", @@ -4163,14 +4228,6 @@ "tiny-typed-emitter": "^2.1.0" } }, - "node_modules/electron-window": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/electron-window/-/electron-window-0.8.1.tgz", - "integrity": "sha512-W1i9LfnZJozk3MXE8VgsL2E5wOUHSgyCvcg1H2vQQjj+gqhO9lVudgY3z3SF7LJAmi+0vy3CJkbMqsynWB49EA==", - "dependencies": { - "is-electron-renderer": "^2.0.0" - } - }, "node_modules/electron/node_modules/@types/node": { "version": "18.18.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.3.tgz", @@ -5250,14 +5307,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "bin": { - "flat": "cli.js" - } - }, "node_modules/flat-cache": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", @@ -5512,6 +5561,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6175,14 +6225,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": { - "he": "bin/he" - } - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -6710,11 +6752,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-electron-renderer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz", - "integrity": "sha512-pRlQnpaCFhDVPtkXkP+g9Ybv/CjbiQDjnKFQTEjpBfDKeV6dRDBczuFRDpM6DVfk2EjpMS8t5kwE5jPnqYl3zA==" - }, "node_modules/is-equal-shallow": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", @@ -7021,17 +7058,6 @@ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "license": "MIT" }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", @@ -7540,21 +7566,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -8552,249 +8563,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/mocha/node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/mocha/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/mocha/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/mocha/node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mocha/node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/mocha/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "engines": { - "node": ">=10" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -10637,14 +10405,6 @@ "node": ">=0.10.0" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -10686,6 +10446,14 @@ "react": "^16.8.0 || ^17 || ^18" } }, + "node_modules/react-intersection-observer": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.5.2.tgz", + "integrity": "sha512-EmoV66/yvksJcGa1rdW0nDNc4I1RifDWkT50gXSFnPLYQ4xUptuDD4V7k+Rj1OgVAlww628KLGcxPXFlOkkU/Q==", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -11476,6 +11244,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11845,14 +11614,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -12606,6 +12367,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -13569,6 +13331,17 @@ "node": ">=0.10.0" } }, + "node_modules/use-debounce": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz", + "integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==", + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -13876,15 +13649,11 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==" - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -13954,54 +13723,11 @@ "node": ">=0.4" } }, - "node_modules/xvfb-maybe": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/xvfb-maybe/-/xvfb-maybe-0.2.1.tgz", - "integrity": "sha512-9IyRz3l6Qyhl6LvnGRF5jMPB4oBEepQnuzvVAFTynP6ACLLSevqigICJ9d/+ofl29m2daeaVBChnPYUnaeJ7yA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^2.2.0", - "which": "^1.2.4" - }, - "bin": { - "xvfb-maybe": "src/xvfb-maybe.js" - } - }, - "node_modules/xvfb-maybe/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/xvfb-maybe/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/xvfb-maybe/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -14026,6 +13752,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -14044,33 +13771,12 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser/node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "engines": { - "node": ">=8" - } - }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/web/app/_components/ActiveModelTable/index.tsx b/web/app/_components/ActiveModelTable/index.tsx index 800740422..72951e33d 100644 --- a/web/app/_components/ActiveModelTable/index.tsx +++ b/web/app/_components/ActiveModelTable/index.tsx @@ -1,10 +1,10 @@ import { useAtomValue } from "jotai"; -import React, { Fragment } from "react"; +import React from "react"; import ModelTable from "../ModelTable"; -import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; +import { activeAssistantModelAtom } from "@/_helpers/atoms/Model.atom"; const ActiveModelTable: React.FC = () => { - const activeModel = useAtomValue(currentProductAtom); + const activeModel = useAtomValue(activeAssistantModelAtom); if (!activeModel) return null; diff --git a/web/app/_components/AvailableModelCard/index.tsx b/web/app/_components/AvailableModelCard/index.tsx index 31e0ee2d4..b493ccac8 100644 --- a/web/app/_components/AvailableModelCard/index.tsx +++ b/web/app/_components/AvailableModelCard/index.tsx @@ -1,19 +1,19 @@ -import { Product } from "@/_models/Product"; import DownloadModelContent from "../DownloadModelContent"; import ModelDownloadButton from "../ModelDownloadButton"; import ModelDownloadingButton from "../ModelDownloadingButton"; import { useAtomValue } from "jotai"; import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom"; +import { AssistantModel } from "@/_models/AssistantModel"; type Props = { - product: Product; + model: AssistantModel; isRecommend: boolean; required?: string; - onDownloadClick?: (product: Product) => void; + onDownloadClick?: (model: AssistantModel) => void; }; const AvailableModelCard: React.FC = ({ - product, + model, isRecommend, required, onDownloadClick, @@ -24,14 +24,14 @@ const AvailableModelCard: React.FC = ({ let total = 0; let transferred = 0; - if (product.fileName && downloadState[product.fileName]) { + if (model.id && downloadState[model.id]) { isDownloading = - downloadState[product.fileName].error == null && - downloadState[product.fileName].percent < 1; + downloadState[model.id].error == null && + downloadState[model.id].percent < 1; if (isDownloading) { - total = downloadState[product.fileName].size.total; - transferred = downloadState[product.fileName].size.transferred; + total = downloadState[model.id].size.total; + transferred = downloadState[model.id].size.transferred; } } @@ -41,7 +41,7 @@ const AvailableModelCard: React.FC = ({ ) : (
- onDownloadClick?.(product)} /> + onDownloadClick?.(model)} />
); @@ -50,11 +50,11 @@ const AvailableModelCard: React.FC = ({
{downloadButton}
diff --git a/web/app/_components/ConversationalCard/index.tsx b/web/app/_components/ConversationalCard/index.tsx index 9878c29b1..8f09d1caf 100644 --- a/web/app/_components/ConversationalCard/index.tsx +++ b/web/app/_components/ConversationalCard/index.tsx @@ -1,20 +1,20 @@ import React from "react"; import Image from "next/image"; import useCreateConversation from "@/_hooks/useCreateConversation"; -import { Product } from "@/_models/Product"; +import { AssistantModel } from "@/_models/AssistantModel"; type Props = { - product: Product; + model: AssistantModel; }; -const ConversationalCard: React.FC = ({ product }) => { +const ConversationalCard: React.FC = ({ model }) => { const { requestCreateConvo } = useCreateConversation(); - const { name, avatarUrl, description } = product; + const { name, avatarUrl, shortDescription } = model; return ( + diff --git a/web/app/_components/ExploreModelItem/index.tsx b/web/app/_components/ExploreModelItem/index.tsx index 218166a8c..a06a99a1f 100644 --- a/web/app/_components/ExploreModelItem/index.tsx +++ b/web/app/_components/ExploreModelItem/index.tsx @@ -81,7 +81,7 @@ const ExploreModelItem = forwardRef(({ model }, ref) => { Tags - {model.availableVersions.length > 0 && ( + {model.availableVersions?.length > 0 && ( {show && ( { - const [loadMoreInProgress, setLoadMoreInProress] = useAtom(modelLoadMoreAtom); - const modelSearch = useAtomValue(modelSearchAtom); - const { modelList, getHuggingFaceModel } = useGetHuggingFaceModel(); - const { ref, inView } = useInView({ - threshold: 0, - triggerOnce: true, - }); + const { models } = useGetConfiguredModels(); useEffect(() => { - if (modelList.length === 0 && modelSearch.length > 0) { - setLoadMoreInProress(true); - } - getHuggingFaceModel(modelSearch); - }, [modelSearch]); - - useEffect(() => { - if (inView) { - console.debug("Load more models.."); - setLoadMoreInProress(true); - getHuggingFaceModel(modelSearch); - } - }, [inView]); + getConfiguredModels(); + }, []); return (
- {modelList.map((item, index) => ( - + {models.map((item) => ( + ))} - {loadMoreInProgress && ( -
- -
- )}
); }; diff --git a/web/app/_components/InputToolbar/index.tsx b/web/app/_components/InputToolbar/index.tsx index 9dbd02f92..6cea8f35b 100644 --- a/web/app/_components/InputToolbar/index.tsx +++ b/web/app/_components/InputToolbar/index.tsx @@ -8,13 +8,13 @@ import SecondaryButton from "../SecondaryButton"; import { Fragment } from "react"; import { PlusIcon } from "@heroicons/react/24/outline"; import useCreateConversation from "@/_hooks/useCreateConversation"; -import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; +import { activeAssistantModelAtom } from "@/_helpers/atoms/Model.atom"; import LoadingIndicator from "../LoadingIndicator"; import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom"; const InputToolbar: React.FC = () => { const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom); - const currentProduct = useAtomValue(currentProductAtom); + const activeModel = useAtomValue(activeAssistantModelAtom); const { requestCreateConvo } = useCreateConversation(); const currentConvoState = useAtomValue(currentConvoStateAtom); @@ -26,8 +26,8 @@ const InputToolbar: React.FC = () => { // const onRegenerateClick = () => {}; const onNewConversationClick = () => { - if (currentProduct) { - requestCreateConvo(currentProduct); + if (activeModel) { + requestCreateConvo(activeModel); } }; @@ -49,7 +49,6 @@ const InputToolbar: React.FC = () => { )} - {/* */} = ({ model }) => { const { startModel, stopModel } = useStartStopModel(); - const activeModel = useAtomValue(currentProductAtom); + const activeModel = useAtomValue(activeAssistantModelAtom); const { deleteModel } = useDeleteModel(); let status = ModelStatus.Installed; @@ -36,32 +36,23 @@ const ModelRow: React.FC = ({ model }) => { } }; - const onDeleteClick = () => { + const onDeleteClick = useCallback(() => { deleteModel(model); - }; + }, [model]); return ( - + {model.name} {model.version}
- {model.format} - {model.accelerated && ( - - - GPU Accelerated - - )} + GGUF
- {model.totalSize} + {toGigabytes(model.size)} diff --git a/web/app/_components/ModelSelector/index.tsx b/web/app/_components/ModelSelector/index.tsx index b32cd7c4e..66461e6c6 100644 --- a/web/app/_components/ModelSelector/index.tsx +++ b/web/app/_components/ModelSelector/index.tsx @@ -1,10 +1,10 @@ import { Fragment, useEffect } from "react"; import { Listbox, Transition } from "@headlessui/react"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid"; -import { Product } from "@/_models/Product"; import { useAtom, useAtomValue } from "jotai"; import { selectedModelAtom } from "@/_helpers/atoms/Model.atom"; import { downloadedModelAtom } from "@/_helpers/atoms/DownloadedModel.atom"; +import { AssistantModel } from "@/_models/AssistantModel"; function classNames(...classes: any) { return classes.filter(Boolean).join(" "); @@ -20,7 +20,7 @@ const SelectModels: React.FC = () => { } }, [downloadedModels]); - const onModelSelected = (model: Product) => { + const onModelSelected = (model: AssistantModel) => { setSelectedModel(model); }; diff --git a/web/app/_components/ModelTable/index.tsx b/web/app/_components/ModelTable/index.tsx index 99d15cacb..e632e12ab 100644 --- a/web/app/_components/ModelTable/index.tsx +++ b/web/app/_components/ModelTable/index.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { Product } from "@/_models/Product"; import ModelRow from "../ModelRow"; import ModelTableHeader from "../ModelTableHeader"; +import { AssistantModel } from "@/_models/AssistantModel"; type Props = { - models: Product[]; + models: AssistantModel[]; }; const tableHeaders = ["MODEL", "FORMAT", "SIZE", "STATUS", "ACTIONS"]; diff --git a/web/app/_components/ModelVersionItem/index.tsx b/web/app/_components/ModelVersionItem/index.tsx index ab530a87a..4d9d40a53 100644 --- a/web/app/_components/ModelVersionItem/index.tsx +++ b/web/app/_components/ModelVersionItem/index.tsx @@ -1,10 +1,11 @@ import React, { useMemo } from "react"; import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter"; import Image from "next/image"; -import { ModelVersion, Product } from "@/_models/Product"; +import { Product } from "@/_models/Product"; import useDownloadModel from "@/_hooks/useDownloadModel"; import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom"; import { atom, useAtomValue } from "jotai"; +import { ModelVersion } from "@/_models/ModelVersion"; type Props = { model: Product; @@ -12,15 +13,15 @@ type Props = { }; const ModelVersionItem: React.FC = ({ model, modelVersion }) => { - const { downloadHfModel } = useDownloadModel(); + const { downloadModel } = useDownloadModel(); const downloadAtom = useMemo( - () => atom((get) => get(modelDownloadStateAtom)[modelVersion.path ?? ""]), - [modelVersion.path ?? ""] + () => atom((get) => get(modelDownloadStateAtom)[modelVersion.id ?? ""]), + [modelVersion.id ?? ""] ); const downloadState = useAtomValue(downloadAtom); const onDownloadClick = () => { - downloadHfModel(model, modelVersion); + downloadModel(model, modelVersion); }; let downloadButton = ( @@ -42,7 +43,7 @@ const ModelVersionItem: React.FC = ({ model, modelVersion }) => {
- {modelVersion.path} + {modelVersion.name}
diff --git a/web/app/_components/ModelVersionList/index.tsx b/web/app/_components/ModelVersionList/index.tsx index 09af4a623..95c03039a 100644 --- a/web/app/_components/ModelVersionList/index.tsx +++ b/web/app/_components/ModelVersionList/index.tsx @@ -1,6 +1,7 @@ import React from "react"; import ModelVersionItem from "../ModelVersionItem"; -import { ModelVersion, Product } from "@/_models/Product"; +import { Product } from "@/_models/Product"; +import { ModelVersion } from "@/_models/ModelVersion"; type Props = { model: Product; @@ -12,7 +13,7 @@ const ModelVersionList: React.FC = ({ model, versions }) => (
Available Versions
{versions.map((item) => ( - + ))}
diff --git a/web/app/_components/MonitorBar/index.tsx b/web/app/_components/MonitorBar/index.tsx index 7c6b3b559..3ded033f6 100644 --- a/web/app/_components/MonitorBar/index.tsx +++ b/web/app/_components/MonitorBar/index.tsx @@ -2,16 +2,16 @@ import ProgressBar from "../ProgressBar"; import SystemItem from "../SystemItem"; import { useAtomValue } from "jotai"; import { appDownloadProgress } from "@/_helpers/JotaiWrapper"; -import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; import useGetAppVersion from "@/_hooks/useGetAppVersion"; import useGetSystemResources from "@/_hooks/useGetSystemResources"; import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom"; import { DownloadState } from "@/_models/DownloadState"; import { formatDownloadPercentage } from "@/_utils/converter"; +import { activeAssistantModelAtom } from "@/_helpers/atoms/Model.atom"; const MonitorBar: React.FC = () => { const progress = useAtomValue(appDownloadProgress); - const activeModel = useAtomValue(currentProductAtom); + const activeModel = useAtomValue(activeAssistantModelAtom); const { version } = useGetAppVersion(); const { ram, cpu } = useGetSystemResources(); const modelDownloadStates = useAtomValue(modelDownloadStateAtom); diff --git a/web/app/_components/NewChatButton/index.tsx b/web/app/_components/NewChatButton/index.tsx index 509da36ba..2ac6ad184 100644 --- a/web/app/_components/NewChatButton/index.tsx +++ b/web/app/_components/NewChatButton/index.tsx @@ -7,14 +7,14 @@ import { MainViewState, setMainViewStateAtom, } from "@/_helpers/atoms/MainView.atom"; -import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; import useCreateConversation from "@/_hooks/useCreateConversation"; import useInitModel from "@/_hooks/useInitModel"; -import { Product } from "@/_models/Product"; import { PlusIcon } from "@heroicons/react/24/outline"; +import { activeAssistantModelAtom } from "@/_helpers/atoms/Model.atom"; +import { AssistantModel } from "@/_models/AssistantModel"; const NewChatButton: React.FC = () => { - const activeModel = useAtomValue(currentProductAtom); + const activeModel = useAtomValue(activeAssistantModelAtom); const setMainView = useSetAtom(setMainViewStateAtom); const { requestCreateConvo } = useCreateConversation(); const { initModel } = useInitModel(); @@ -27,7 +27,7 @@ const NewChatButton: React.FC = () => { } }; - const createConversationAndInitModel = async (model: Product) => { + const createConversationAndInitModel = async (model: AssistantModel) => { await requestCreateConvo(model); await initModel(model); }; diff --git a/web/app/_components/SidebarEmptyHistory/index.tsx b/web/app/_components/SidebarEmptyHistory/index.tsx index 5749da64e..0ab03f4ca 100644 --- a/web/app/_components/SidebarEmptyHistory/index.tsx +++ b/web/app/_components/SidebarEmptyHistory/index.tsx @@ -7,10 +7,10 @@ import { MainViewState, setMainViewStateAtom, } from "@/_helpers/atoms/MainView.atom"; -import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; +import { activeAssistantModelAtom } from "@/_helpers/atoms/Model.atom"; import useInitModel from "@/_hooks/useInitModel"; -import { Product } from "@/_models/Product"; import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels"; +import { AssistantModel } from "@/_models/AssistantModel"; enum ActionButton { DownloadModel = "Download a Model", @@ -19,7 +19,7 @@ enum ActionButton { const SidebarEmptyHistory: React.FC = () => { const { downloadedModels } = useGetDownloadedModels(); - const activeModel = useAtomValue(currentProductAtom); + const activeModel = useAtomValue(activeAssistantModelAtom); const setMainView = useSetAtom(setMainViewStateAtom); const { requestCreateConvo } = useCreateConversation(); const [action, setAction] = useState(ActionButton.DownloadModel); @@ -46,7 +46,7 @@ const SidebarEmptyHistory: React.FC = () => { } }; - const createConversationAndInitModel = async (model: Product) => { + const createConversationAndInitModel = async (model: AssistantModel) => { await requestCreateConvo(model); await initModel(model); }; diff --git a/web/app/_helpers/atoms/DownloadedModel.atom.ts b/web/app/_helpers/atoms/DownloadedModel.atom.ts index 9e924ea74..28763aa0b 100644 --- a/web/app/_helpers/atoms/DownloadedModel.atom.ts +++ b/web/app/_helpers/atoms/DownloadedModel.atom.ts @@ -1,4 +1,7 @@ -import { Product } from "@/_models/Product"; +import { AssistantModel } from "@/_models/AssistantModel"; import { atom } from "jotai"; -export const downloadedModelAtom = atom([]); +/** + * @description: This atom is used to store the downloaded models + */ +export const downloadedModelAtom = atom([]); diff --git a/web/app/_helpers/atoms/Model.atom.ts b/web/app/_helpers/atoms/Model.atom.ts index 053f03ac6..807179964 100644 --- a/web/app/_helpers/atoms/Model.atom.ts +++ b/web/app/_helpers/atoms/Model.atom.ts @@ -1,6 +1,8 @@ -import { Product } from "@/_models/Product"; +import { AssistantModel } from "@/_models/AssistantModel"; import { atom } from "jotai"; -export const currentProductAtom = atom(undefined); +export const selectedModelAtom = atom(undefined); -export const selectedModelAtom = atom(undefined); +export const activeAssistantModelAtom = atom( + undefined +); diff --git a/web/app/_hooks/useCreateConversation.ts b/web/app/_hooks/useCreateConversation.ts index 100ffa945..3aa970420 100644 --- a/web/app/_hooks/useCreateConversation.ts +++ b/web/app/_hooks/useCreateConversation.ts @@ -2,7 +2,6 @@ import { useAtom, useSetAtom } from "jotai"; import { Conversation } from "@/_models/Conversation"; import { executeSerial } from "@/_services/pluginService"; import { DataService } from "../../shared/coreService"; -import { Product } from "@/_models/Product"; import { userConversationsAtom, setActiveConvoIdAtom, @@ -11,6 +10,7 @@ import { updateConversationErrorAtom, } from "@/_helpers/atoms/Conversation.atom"; import useInitModel from "./useInitModel"; +import { AssistantModel } from "@/_models/AssistantModel"; const useCreateConversation = () => { const { initModel } = useInitModel(); @@ -24,7 +24,7 @@ const useCreateConversation = () => { ); const updateConvError = useSetAtom(updateConversationErrorAtom); - const requestCreateConvo = async (model: Product) => { + const requestCreateConvo = async (model: AssistantModel) => { const conversationName = model.name; const conv: Conversation = { model_id: model.id, diff --git a/web/app/_hooks/useDeleteModel.ts b/web/app/_hooks/useDeleteModel.ts index 77646b8e9..3857ae31c 100644 --- a/web/app/_hooks/useDeleteModel.ts +++ b/web/app/_hooks/useDeleteModel.ts @@ -1,16 +1,16 @@ import { execute, executeSerial } from "@/_services/pluginService"; import { DataService, ModelManagementService } from "../../shared/coreService"; -import { Product } from "@/_models/Product"; import { useSetAtom } from "jotai"; import { downloadedModelAtom } from "@/_helpers/atoms/DownloadedModel.atom"; import { getDownloadedModels } from "./useGetDownloadedModels"; +import { AssistantModel } from "@/_models/AssistantModel"; export default function useDeleteModel() { const setDownloadedModels = useSetAtom(downloadedModelAtom); - const deleteModel = async (model: Product) => { + const deleteModel = async (model: AssistantModel) => { execute(DataService.DELETE_DOWNLOAD_MODEL, model.id); - await executeSerial(ModelManagementService.DELETE_MODEL, model.fileName); + await executeSerial(ModelManagementService.DELETE_MODEL, model.id); // reload models const downloadedModels = await getDownloadedModels(); diff --git a/web/app/_hooks/useDownloadModel.ts b/web/app/_hooks/useDownloadModel.ts index de2ce898b..5f2ff26e4 100644 --- a/web/app/_hooks/useDownloadModel.ts +++ b/web/app/_hooks/useDownloadModel.ts @@ -1,38 +1,20 @@ import { executeSerial } from "@/_services/pluginService"; import { DataService, ModelManagementService } from "../../shared/coreService"; -import { ModelVersion, Product } from "@/_models/Product"; +import { Product } from "@/_models/Product"; +import { ModelVersion } from "@/_models/ModelVersion"; export default function useDownloadModel() { - const downloadModel = async (model: Product) => { - await executeSerial(DataService.STORE_MODEL, model); - await executeSerial(ModelManagementService.DOWNLOAD_MODEL, { - downloadUrl: model.downloadUrl, - fileName: model.fileName, - }); - }; + const downloadModel = async (model: Product, modelVersion: ModelVersion) => { + modelVersion.startDownloadAt = Date.now(); - const downloadHfModel = async ( - model: Product, - modelVersion: ModelVersion - ) => { - const hfModel: Product = { - ...model, - id: `${model.author}.${modelVersion.path}`, - slug: `${model.author}.${modelVersion.path}`, - name: `${model.name} - ${modelVersion.path}`, - fileName: modelVersion.path, - totalSize: modelVersion.size, - downloadUrl: modelVersion.downloadUrl, - }; - await executeSerial(DataService.STORE_MODEL, hfModel); + await executeSerial(DataService.STORE_MODEL, { model, modelVersion }); await executeSerial(ModelManagementService.DOWNLOAD_MODEL, { - downloadUrl: hfModel.downloadUrl, - fileName: hfModel.fileName, + downloadUrl: modelVersion.downloadLink, + fileName: modelVersion.id, }); }; return { downloadModel, - downloadHfModel, }; } diff --git a/web/app/_hooks/useGetAvailableModels.ts b/web/app/_hooks/useGetAvailableModels.ts deleted file mode 100644 index b10cc617b..000000000 --- a/web/app/_hooks/useGetAvailableModels.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Product } from "@/_models/Product"; -import { executeSerial } from "@/_services/pluginService"; -import { ModelManagementService } from "../../shared/coreService"; -import { useEffect, useState } from "react"; -import { getModelFiles } from "./useGetDownloadedModels"; -import { useAtomValue } from "jotai"; -import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom"; - -export default function useGetAvailableModels() { - const downloadState = useAtomValue(modelDownloadStateAtom); - const [allAvailableModels, setAllAvailableModels] = useState([]); - const [availableModels, setAvailableModels] = useState([]); - const [downloadedModels, setDownloadedModels] = useState([]); - - const getAvailableModelExceptDownloaded = async () => { - const avails = await getAvailableModels(); - const downloaded = await getModelFiles(); - - setAllAvailableModels(avails); - const availableOrDownloadingModels: Product[] = avails; - const successfullDownloadModels: Product[] = []; - - downloaded.forEach((item) => { - if (item.fileName && downloadState[item.fileName] == null) { - // if not downloading, consider as downloaded - successfullDownloadModels.push(item); - } else { - availableOrDownloadingModels.push(item); - } - }); - - setAvailableModels(availableOrDownloadingModels); - setDownloadedModels(successfullDownloadModels); - }; - - useEffect(() => { - getAvailableModelExceptDownloaded(); - }, []); - - return { - allAvailableModels, - availableModels, - downloadedModels, - getAvailableModelExceptDownloaded, - }; -} - -export async function getAvailableModels(): Promise { - const avails: Product[] = await executeSerial( - ModelManagementService.GET_AVAILABLE_MODELS - ); - - return avails ?? []; -} diff --git a/web/app/_hooks/useGetConfiguredModels.ts b/web/app/_hooks/useGetConfiguredModels.ts new file mode 100644 index 000000000..c5dd5d46a --- /dev/null +++ b/web/app/_hooks/useGetConfiguredModels.ts @@ -0,0 +1,20 @@ +import { Product } from "@/_models/Product"; +import { useEffect, useState } from "react"; +import { getConfiguredModels } from "./useGetDownloadedModels"; + +export default function useGetConfiguredModels() { + const [models, setModels] = useState([]); + + const fetchModels = async () => { + const models = await getConfiguredModels(); + + setModels(models); + }; + + // TODO allow user for filter + useEffect(() => { + fetchModels(); + }, []); + + return { models }; +} diff --git a/web/app/_hooks/useGetDownloadedModels.ts b/web/app/_hooks/useGetDownloadedModels.ts index 3784d36d7..da7526717 100644 --- a/web/app/_hooks/useGetDownloadedModels.ts +++ b/web/app/_hooks/useGetDownloadedModels.ts @@ -1,10 +1,10 @@ -import { ModelVersion, Product, ProductType } from "@/_models/Product"; +import { Product } from "@/_models/Product"; import { useEffect } from "react"; import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager"; import { DataService, ModelManagementService } from "../../shared/coreService"; -import { SearchModelParamHf } from "@/_models/hf/SearchModelParam.hf"; import { useAtom } from "jotai"; import { downloadedModelAtom } from "@/_helpers/atoms/DownloadedModel.atom"; +import { AssistantModel } from "@/_models/AssistantModel"; export function useGetDownloadedModels() { const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelAtom); @@ -18,76 +18,13 @@ export function useGetDownloadedModels() { return { downloadedModels }; } -export async function getDownloadedModels(): Promise { - const downloadedModels: Product[] = await executeSerial( +export async function getDownloadedModels(): Promise { + const downloadedModels: AssistantModel[] = await executeSerial( DataService.GET_FINISHED_DOWNLOAD_MODELS ); return downloadedModels ?? []; } -export async function getModelFiles(): Promise { - const downloadedModels: Product[] = await executeSerial( - ModelManagementService.GET_DOWNLOADED_MODELS - ); - return downloadedModels ?? []; +export async function getConfiguredModels(): Promise { + return executeSerial(ModelManagementService.GET_CONFIGURED_MODELS); } - -export async function searchModels( - params: SearchModelParamHf -): Promise { - const result = await executeSerial( - ModelManagementService.SEARCH_MODELS, - params - ); - - const products: Product[] = result.data.map((model: any) => { - const modelVersions: ModelVersion[] = []; - - for (const [, file] of Object.entries(model.files)) { - const fileData: any = file as any; - const modelVersion: ModelVersion = { - path: fileData.path, - type: fileData.type, - downloadUrl: fileData.downloadLink, - size: fileData.size, - }; - modelVersions.push(modelVersion); - } - - const p = { - id: model.id, - slug: model.name, - name: model.name, - description: model.name, - avatarUrl: "", - longDescription: model.name, - technicalDescription: model.name, - author: model.name.split("/")[0], - version: "1.0.0", - modelUrl: "https://google.com", - nsfw: false, - greeting: "Hello there", - type: ProductType.LLM, - createdAt: -1, - accelerated: true, - totalSize: -1, - format: "", - status: "Not downloaded", - releaseDate: -1, - availableVersions: modelVersions, - }; - - return p; - }); - - return { - data: products, - hasMore: result.hasMore, - }; -} - -// TODO define somewhere else -export type QueryProductResult = { - data: Product[]; - hasMore: boolean; -}; diff --git a/web/app/_hooks/useInitModel.ts b/web/app/_hooks/useInitModel.ts index 15ed78929..1795c2ddd 100644 --- a/web/app/_hooks/useInitModel.ts +++ b/web/app/_hooks/useInitModel.ts @@ -1,18 +1,19 @@ -import { Product } from "@/_models/Product"; import { executeSerial } from "@/_services/pluginService"; import { InferenceService } from "../../shared/coreService"; import { useAtom } from "jotai"; -import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; +import { activeAssistantModelAtom } from "@/_helpers/atoms/Model.atom"; +import { AssistantModel } from "@/_models/AssistantModel"; export default function useInitModel() { - const [activeModel, setActiveModel] = useAtom(currentProductAtom); + const [activeModel, setActiveModel] = useAtom(activeAssistantModelAtom); - const initModel = async (model: Product) => { + const initModel = async (model: AssistantModel) => { if (activeModel && activeModel.id === model.id) { console.debug(`Model ${model.id} is already init. Ignore..`); return; } - const res = await executeSerial(InferenceService.INIT_MODEL, model); + + const res = await executeSerial(InferenceService.INIT_MODEL, model.id); if (res?.error) { console.log("error occured: ", res); return res; diff --git a/web/app/_hooks/useStartStopModel.ts b/web/app/_hooks/useStartStopModel.ts index 8d9e0c7bb..5c5db9647 100644 --- a/web/app/_hooks/useStartStopModel.ts +++ b/web/app/_hooks/useStartStopModel.ts @@ -2,11 +2,11 @@ import { executeSerial } from "@/_services/pluginService"; import { DataService, InferenceService } from "../../shared/coreService"; import useInitModel from "./useInitModel"; import { useSetAtom } from "jotai"; -import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; +import { activeAssistantModelAtom } from "@/_helpers/atoms/Model.atom"; export default function useStartStopModel() { const { initModel } = useInitModel(); - const setActiveModel = useSetAtom(currentProductAtom); + const setActiveModel = useSetAtom(activeAssistantModelAtom); const startModel = async (modelId: string) => { const model = await executeSerial(DataService.GET_MODEL_BY_ID, modelId); diff --git a/web/app/_models/AssistantModel.ts b/web/app/_models/AssistantModel.ts new file mode 100644 index 000000000..57caf64a7 --- /dev/null +++ b/web/app/_models/AssistantModel.ts @@ -0,0 +1,65 @@ +import { ProductType } from "./Product"; + +/** + * Represent a model + */ +export type AssistantModel = { + /** + * Combination of owner and model name. + * Being used as file name. MUST be unique. + */ + id: string; + + name: string; + + quantMethod: string; + + bits: number; + + size: number; + + maxRamRequired: number; + + usecase: string; + + downloadLink: string; + + /** + * For tracking download info + */ + startDownloadAt?: number; + + finishDownloadAt?: number; + + productId: string; + + productName: string; + + shortDescription: string; + + longDescription: string; + + avatarUrl: string; + + author: string; + + version: string; + + modelUrl: string; + + nsfw: boolean; + + greeting: string; + + type: ProductType; + + createdAt: number; + + updatedAt?: number; + + status: string; // TODO: add this in the database // Downloaded, Active + + releaseDate: number; // TODO: add this in the database + + tags: string[]; +}; diff --git a/web/app/_models/ModelVersion.ts b/web/app/_models/ModelVersion.ts new file mode 100644 index 000000000..d9d89d92d --- /dev/null +++ b/web/app/_models/ModelVersion.ts @@ -0,0 +1,23 @@ +/** + * Model type which will be stored in the database + */ +export type ModelVersion = { + /** + * Combination of owner and model name. + * Being used as file name. Should be unique. + */ + id: string; + name: string; + quantMethod: string; + bits: number; + size: number; + maxRamRequired: number; + usecase: string; + downloadLink: string; + productId: string; + /** + * For tracking download state + */ + startDownloadAt?: number; + finishDownloadAt?: number; +}; diff --git a/web/app/_models/Product.ts b/web/app/_models/Product.ts index d16d5a9b0..545a9bb82 100644 --- a/web/app/_models/Product.ts +++ b/web/app/_models/Product.ts @@ -1,3 +1,4 @@ +import { ModelVersion } from "./ModelVersion"; import { ProductInput } from "./ProductInput"; import { ProductOutput } from "./ProductOutput"; @@ -9,12 +10,10 @@ export enum ProductType { export interface Product { id: string; - slug: string; name: string; - description: string; + shortDescription: string; avatarUrl: string; longDescription: string; - technicalDescription: string; author: string; version: string; modelUrl: string; @@ -25,36 +24,8 @@ export interface Product { outputs?: ProductOutput; createdAt: number; updatedAt?: number; - fileName?: string; - downloadUrl?: string; - - accelerated: boolean; // TODO: add this in the database - totalSize: number; // TODO: add this in the database - format: string; // TODO: add this in the database // GGUF or something else status: string; // TODO: add this in the database // Downloaded, Active releaseDate: number; // TODO: add this in the database - + tags: string[]; availableVersions: ModelVersion[]; } - -export interface ModelVersion { - /** - * Act as the id of the model version - */ - path: string; - - /** - * currently, we only have `file` type - */ - type: string; - - /** - * The download url for the model version - */ - downloadUrl: string; - - /** - * File size in bytes - */ - size: number; -} diff --git a/web/app/_services/pluginService.ts b/web/app/_services/pluginService.ts index deb215938..04c4052fd 100644 --- a/web/app/_services/pluginService.ts +++ b/web/app/_services/pluginService.ts @@ -17,7 +17,7 @@ export const isCorePluginInstalled = () => { if (!extensionPoints.get(InferenceService.INIT_MODEL)) { return false; } - if (!extensionPoints.get(ModelManagementService.GET_DOWNLOADED_MODELS)) { + if (!extensionPoints.get(ModelManagementService.DOWNLOAD_MODEL)) { return false; } return true; @@ -34,7 +34,7 @@ export const setupBasePlugins = async () => { if ( !extensionPoints.get(DataService.GET_CONVERSATIONS) || !extensionPoints.get(InferenceService.INIT_MODEL) || - !extensionPoints.get(ModelManagementService.GET_DOWNLOADED_MODELS) + !extensionPoints.get(ModelManagementService.DOWNLOAD_MODEL) ) { const installed = await plugins.install(basePlugins); if (installed) { diff --git a/web/shared/coreService.ts b/web/shared/coreService.ts index 8a2c5372f..cebbe39a9 100644 --- a/web/shared/coreService.ts +++ b/web/shared/coreService.ts @@ -34,11 +34,10 @@ export enum InferenceService { } export enum ModelManagementService { - GET_DOWNLOADED_MODELS = "getDownloadedModels", - GET_AVAILABLE_MODELS = "getAvailableModels", DELETE_MODEL = "deleteModel", DOWNLOAD_MODEL = "downloadModel", SEARCH_MODELS = "searchModels", + GET_CONFIGURED_MODELS = "getConfiguredModels", } export enum PreferenceService { From 773bbaf4ccc9ab0cc702ed62c99514af5ea3dd45 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 10 Oct 2023 18:53:56 -0700 Subject: [PATCH 2/5] update Signed-off-by: James --- electron/core/plugins/data-plugin/module.ts | 66 +++------------ .../core/plugins/data-plugin/package.json | 2 +- .../plugins/model-management-plugin/index.js | 12 +-- .../ExploreModelContainer/index.tsx | 4 +- .../_components/ExploreModelFilter/index.tsx | 3 +- .../_components/ExploreModelItem/index.tsx | 39 ++++++--- .../ExploreModelItemHeader/index.tsx | 80 ++++++++++++++----- .../_components/ModelVersionItem/index.tsx | 23 +++++- .../_components/ModelVersionList/index.tsx | 32 +++++--- .../_components/SimpleTag/TagStyleMapper.ts | 16 ++++ web/app/_components/SimpleTag/TagType.ts | 32 ++++++++ web/app/_components/SimpleTag/index.tsx | 55 +------------ web/app/_hooks/useGetHuggingFaceModel.ts | 17 ++-- .../_hooks/useGetMostSuitableModelVersion.ts | 29 +++++++ web/app/_hooks/useGetPerformanceTag.ts | 53 ++++++++++++ web/app/_models/AssistantModel.ts | 4 +- web/app/_models/Product.ts | 4 +- 17 files changed, 298 insertions(+), 173 deletions(-) create mode 100644 web/app/_components/SimpleTag/TagStyleMapper.ts create mode 100644 web/app/_components/SimpleTag/TagType.ts create mode 100644 web/app/_hooks/useGetMostSuitableModelVersion.ts create mode 100644 web/app/_hooks/useGetPerformanceTag.ts diff --git a/electron/core/plugins/data-plugin/module.ts b/electron/core/plugins/data-plugin/module.ts index a9bc20040..8ee49798a 100644 --- a/electron/core/plugins/data-plugin/module.ts +++ b/electron/core/plugins/data-plugin/module.ts @@ -116,22 +116,10 @@ function storeModel(params: any) { model.nsfw, modelTags, model.greeting, - model.type, - function (err: any) { - if (err) { - // Handle the insertion error here - console.error(err.message); - res(undefined); - return; - } - // @ts-ignoreF - const id = this.lastID; - res(id); - return; - } + model.type ); + stmt.finalize(); - // insert modelVersion to MODEL_VERSION_TABLE_INSERTION const stmt2 = db.prepare(MODEL_VERSION_TABLE_INSERTION); stmt2.run( modelVersion.id, @@ -143,25 +131,14 @@ function storeModel(params: any) { modelVersion.usecase, modelVersion.downloadLink, model.id, - modelVersion.startDownloadAt, - function (err: any) { - if (err) { - // Handle the insertion error here - console.error(err.message); - res(undefined); - return; - } - // @ts-ignoreF - const id = this.lastID; - res(id); - return; - } + modelVersion.startDownloadAt ); - stmt.finalize(); + stmt2.finalize(); }); db.close(); + res(undefined); }); } @@ -171,7 +148,7 @@ function storeModel(params: any) { * @param model Product */ function updateFinishedDownloadAt(modelVersionId: string) { - return new Promise((res) => { + return new Promise((res, rej) => { const db = new sqlite3.Database(getDbPath()); const time = Date.now(); console.debug( @@ -181,7 +158,7 @@ function updateFinishedDownloadAt(modelVersionId: string) { db.run(stmt, [time, modelVersionId], (err: any) => { if (err) { console.log(err); - res(undefined); + rej(err); } else { console.log("Updated 1 row"); res("Updated"); @@ -299,7 +276,7 @@ function deleteDownloadModel(modelId: string) { }); } -async function fetchModelVersion(db: any, versionId: string) { +function fetchModelVersion(db: any, versionId: string) { return new Promise((resolve, reject) => { db.get( "SELECT * FROM model_versions WHERE id = ?", @@ -308,32 +285,7 @@ async function fetchModelVersion(db: any, versionId: string) { if (err) { reject(err); } else { - if (row) { - const product = { - id: row.id, - slug: row.slug, - name: row.name, - description: row.description, - avatarUrl: row.avatar_url, - longDescription: row.long_description, - technicalDescription: row.technical_description, - author: row.author, - version: row.version, - modelUrl: row.model_url, - nsfw: row.nsfw, - greeting: row.greeting, - type: row.type, - inputs: row.inputs, - outputs: row.outputs, - createdAt: new Date(row.created_at), - updatedAt: new Date(row.updated_at), - fileName: row.file_name, - downloadUrl: row.download_url, - }; - resolve(product); - } else { - resolve(undefined); - } + resolve(row); } } ); diff --git a/electron/core/plugins/data-plugin/package.json b/electron/core/plugins/data-plugin/package.json index 2bdbf5ad4..062c53c46 100644 --- a/electron/core/plugins/data-plugin/package.json +++ b/electron/core/plugins/data-plugin/package.json @@ -1,6 +1,6 @@ { "name": "data-plugin", - "version": "1.0.0", + "version": "1.0.1", "description": "Jan Database Plugin efficiently stores conversation and model data using SQLite, providing accessible data management", "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg", "main": "dist/index.js", diff --git a/electron/core/plugins/model-management-plugin/index.js b/electron/core/plugins/model-management-plugin/index.js index f19382d66..699770a93 100644 --- a/electron/core/plugins/model-management-plugin/index.js +++ b/electron/core/plugins/model-management-plugin/index.js @@ -1,6 +1,6 @@ const MODULE_PATH = "model-management-plugin/dist/module.js"; -const getDownloadedModels = async () => +const getDownloadedModels = () => new Promise(async (resolve) => { if (window.electronAPI) { window.electronAPI @@ -9,7 +9,7 @@ const getDownloadedModels = async () => } }); -const getAvailableModels = async () => +const getAvailableModels = () => new Promise(async (resolve) => { if (window.electronAPI) { window.electronAPI @@ -18,7 +18,7 @@ const getAvailableModels = async () => } }); -const downloadModel = async (product) => +const downloadModel = (product) => new Promise(async (resolve) => { if (window && window.electronAPI) { window.electronAPI @@ -29,7 +29,7 @@ const downloadModel = async (product) => } }); -const deleteModel = async (path) => +const deleteModel = (path) => new Promise(async (resolve) => { if (window.electronAPI) { console.debug(`Delete model model management plugin: ${path}`); @@ -38,7 +38,7 @@ const deleteModel = async (path) => } }); -const searchModels = async (params) => +const searchModels = (params) => new Promise(async (resolve) => { if (window.electronAPI) { window.electronAPI @@ -47,7 +47,7 @@ const searchModels = async (params) => } }); -const getConfiguredModels = async () => +const getConfiguredModels = () => new Promise(async (resolve) => { if (window.electronAPI) { window.electronAPI diff --git a/web/app/_components/ExploreModelContainer/index.tsx b/web/app/_components/ExploreModelContainer/index.tsx index 09743d142..3102b2b49 100644 --- a/web/app/_components/ExploreModelContainer/index.tsx +++ b/web/app/_components/ExploreModelContainer/index.tsx @@ -6,10 +6,10 @@ import ExploreModelFilter from "../ExploreModelFilter"; const ExploreModelContainer: React.FC = () => (
- + /> */}
diff --git a/web/app/_components/ExploreModelFilter/index.tsx b/web/app/_components/ExploreModelFilter/index.tsx index c04353e72..a3c173130 100644 --- a/web/app/_components/ExploreModelFilter/index.tsx +++ b/web/app/_components/ExploreModelFilter/index.tsx @@ -1,7 +1,8 @@ import React from "react"; import SearchBar from "../SearchBar"; import SimpleCheckbox from "../SimpleCheckbox"; -import SimpleTag, { TagType } from "../SimpleTag"; +import SimpleTag from "../SimpleTag"; +import { TagType } from "../SimpleTag/TagType"; const tags = [ "Roleplay", diff --git a/web/app/_components/ExploreModelItem/index.tsx b/web/app/_components/ExploreModelItem/index.tsx index a06a99a1f..ff16ce924 100644 --- a/web/app/_components/ExploreModelItem/index.tsx +++ b/web/app/_components/ExploreModelItem/index.tsx @@ -4,10 +4,11 @@ import ExploreModelItemHeader from "../ExploreModelItemHeader"; import ModelVersionList from "../ModelVersionList"; -import { Fragment, forwardRef, useState } from "react"; -import SimpleTag, { TagType } from "../SimpleTag"; +import { Fragment, forwardRef, useEffect, useState } from "react"; +import SimpleTag from "../SimpleTag"; import { displayDate } from "@/_utils/datetime"; import { Product } from "@/_models/Product"; +import useGetMostSuitableModelVersion from "@/_hooks/useGetMostSuitableModelVersion"; type Props = { model: Product; @@ -16,15 +17,26 @@ type Props = { const ExploreModelItem = forwardRef(({ model }, ref) => { const [show, setShow] = useState(false); + const { availableVersions } = model; + const { suitableModel, getMostSuitableModelVersion } = + useGetMostSuitableModelVersion(); + + useEffect(() => { + getMostSuitableModelVersion(availableVersions); + }, [availableVersions]); + + if (!suitableModel) { + return null; + } + return (
@@ -42,11 +54,11 @@ const ExploreModelItem = forwardRef(({ model }, ref) => { Hardware Compatibility
- + /> */}
@@ -63,11 +75,11 @@ const ExploreModelItem = forwardRef(({ model }, ref) => {
Expected Performance
- + /> */}
@@ -77,8 +89,14 @@ const ExploreModelItem = forwardRef(({ model }, ref) => { {model.longDescription}
-
+
Tags +
+ {model.tags.map((tag) => ( + // @ts-ignore + + ))} +
{model.availableVersions?.length > 0 && ( @@ -87,6 +105,7 @@ const ExploreModelItem = forwardRef(({ model }, ref) => { )} From a289e6e2767406147a2a94a871086a5c89c0952a Mon Sep 17 00:00:00 2001 From: James Date: Thu, 12 Oct 2023 00:31:08 -0700 Subject: [PATCH 5/5] fix the exception caused by race condition Signed-off-by: James --- electron/core/plugins/data-plugin/module.ts | 134 ++++++++++-------- .../core/plugins/data-plugin/package.json | 2 +- 2 files changed, 72 insertions(+), 64 deletions(-) diff --git a/electron/core/plugins/data-plugin/module.ts b/electron/core/plugins/data-plugin/module.ts index 8ee49798a..da4994f2a 100644 --- a/electron/core/plugins/data-plugin/module.ts +++ b/electron/core/plugins/data-plugin/module.ts @@ -190,75 +190,83 @@ function getUnfinishedDownloadModels() { async function getFinishedDownloadModels() { const db = new sqlite3.Database(getDbPath()); - - const query = `SELECT * FROM model_versions WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`; - const modelVersions: any = await new Promise((resolve, reject) => { - db.all(query, (err: Error, rows: any[]) => { - if (err) { - reject(err); - } else { - resolve(rows); - } - }); - }); - - const models = await Promise.all( - modelVersions.map(async (modelVersion) => { - const modelQuery = `SELECT * FROM models WHERE id = ?`; - return new Promise((resolve, reject) => { - db.get(modelQuery, [modelVersion.model_id], (err: Error, row: any) => { - if (err) { - reject(err); - } else { - resolve(row); - } - }); + try { + const query = `SELECT * FROM model_versions WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`; + const modelVersions: any = await new Promise((resolve, reject) => { + db.all(query, (err: Error, rows: any[]) => { + if (err) { + reject(err); + } else { + resolve(rows); + } }); - }) - ); + }); - const downloadedModels = []; - modelVersions.forEach((modelVersion: any) => { - const model = models.find((m: any) => m.id === modelVersion.model_id); + const models = await Promise.all( + modelVersions.map(async (modelVersion) => { + const modelQuery = `SELECT * FROM models WHERE id = ?`; + return new Promise((resolve, reject) => { + db.get( + modelQuery, + [modelVersion.model_id], + (err: Error, row: any) => { + if (err) { + reject(err); + } else { + resolve(row); + } + } + ); + }); + }) + ); - if (!model) { - return; - } + const downloadedModels = []; + modelVersions.forEach((modelVersion: any) => { + const model = models.find((m: any) => m.id === modelVersion.model_id); - const assistantModel = { - id: modelVersion.id, - name: modelVersion.name, - quantMethod: modelVersion.quant_method, - bits: modelVersion.bits, - size: modelVersion.size, - maxRamRequired: modelVersion.max_ram_required, - usecase: modelVersion.usecase, - downloadLink: modelVersion.download_link, - startDownloadAt: modelVersion.start_download_at, - finishDownloadAt: modelVersion.finish_download_at, - productId: model.id, - productName: model.name, - shortDescription: model.short_description, - longDescription: model.long_description, - avatarUrl: model.avatar_url, - author: model.author, - version: model.version, - modelUrl: model.model_url, - nsfw: model.nsfw === 0 ? false : true, - greeting: model.default_greeting, - type: model.type, - createdAt: new Date(model.created_at).getTime(), - updatedAt: new Date(model.updated_at ?? "").getTime(), - status: "", - releaseDate: -1, - tags: model.tags.split(","), - }; - downloadedModels.push(assistantModel); - }); + if (!model) { + return; + } - db.close(); + const assistantModel = { + id: modelVersion.id, + name: modelVersion.name, + quantMethod: modelVersion.quant_method, + bits: modelVersion.bits, + size: modelVersion.size, + maxRamRequired: modelVersion.max_ram_required, + usecase: modelVersion.usecase, + downloadLink: modelVersion.download_link, + startDownloadAt: modelVersion.start_download_at, + finishDownloadAt: modelVersion.finish_download_at, + productId: model.id, + productName: model.name, + shortDescription: model.short_description, + longDescription: model.long_description, + avatarUrl: model.avatar_url, + author: model.author, + version: model.version, + modelUrl: model.model_url, + nsfw: model.nsfw === 0 ? false : true, + greeting: model.default_greeting, + type: model.type, + createdAt: new Date(model.created_at).getTime(), + updatedAt: new Date(model.updated_at ?? "").getTime(), + status: "", + releaseDate: -1, + tags: model.tags.split(","), + }; + downloadedModels.push(assistantModel); + }); - return downloadedModels; + db.close(); + + return downloadedModels; + } catch (err) { + console.error(err); + return []; + } } function deleteDownloadModel(modelId: string) { diff --git a/electron/core/plugins/data-plugin/package.json b/electron/core/plugins/data-plugin/package.json index 062c53c46..9eda7c99d 100644 --- a/electron/core/plugins/data-plugin/package.json +++ b/electron/core/plugins/data-plugin/package.json @@ -1,6 +1,6 @@ { "name": "data-plugin", - "version": "1.0.1", + "version": "1.0.2", "description": "Jan Database Plugin efficiently stores conversation and model data using SQLite, providing accessible data management", "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg", "main": "dist/index.js",