Merge pull request #319 from janhq/allow-fetch-model-github

feat: allowing user to fetch models from github
This commit is contained in:
0xSage 2023-10-13 10:20:38 +08:00 committed by GitHub
commit e17d0a57fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1164 additions and 1166 deletions

View File

@ -95,6 +95,7 @@ const createConversation = (conversation: any) =>
resolve(undefined);
}
});
const createMessage = (message: any) =>
new Promise((resolve) => {
if (window && window.electronAPI) {

View File

@ -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,50 +91,54 @@ 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;
}
model.type
);
stmt.finalize();
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
);
stmt2.finalize();
});
db.close();
res(undefined);
});
}
@ -119,17 +147,18 @@ function storeModel(model: any) {
*
* @param model Product
*/
function updateFinishedDownloadAt(fileName: string, time: number) {
return new Promise((res) => {
const db = new sqlite3.Database(
path.join(app.getPath("userData"), "jan.db")
function updateFinishedDownloadAt(modelVersionId: string) {
return new Promise((res, rej) => {
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);
rej(err);
} else {
console.log("Updated 1 row");
res("Updated");
@ -145,11 +174,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 +188,93 @@ 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());
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 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 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 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;
} catch (err) {
console.error(err);
return [];
}
}
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 +284,76 @@ function deleteDownloadModel(modelId: string) {
});
}
function getModelById(modelId: string) {
return new Promise((res) => {
const db = new sqlite3.Database(
path.join(app.getPath("userData"), "jan.db")
);
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);
resolve(row);
}
}
);
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<any | undefined> => {
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 +364,10 @@ function getConversations() {
db.close();
});
}
function storeConversation(conversation: any): Promise<number | undefined> {
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 +400,7 @@ function storeConversation(conversation: any): Promise<number | undefined> {
function storeMessage(message: any): Promise<number | undefined> {
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 +430,10 @@ function storeMessage(message: any): Promise<number | undefined> {
db.close();
});
}
function updateMessage(message: any): Promise<number | undefined> {
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 +450,7 @@ function updateMessage(message: any): Promise<number | undefined> {
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 +470,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 +480,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,

View File

@ -1,6 +1,6 @@
{
"name": "data-plugin",
"version": "1.0.0",
"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",

View File

@ -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<void>(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;

View File

@ -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,6 +47,15 @@ const searchModels = async (params) =>
}
});
const getConfiguredModels = () =>
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);
}

View File

@ -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,
};

618
package-lock.json generated
View File

@ -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",

View File

@ -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;

View File

@ -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<Props> = ({
product,
model,
isRecommend,
required,
onDownloadClick,
@ -24,14 +24,14 @@ const AvailableModelCard: React.FC<Props> = ({
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<Props> = ({
</div>
) : (
<div className="w-1/5 flex items-center justify-end">
<ModelDownloadButton callback={() => onDownloadClick?.(product)} />
<ModelDownloadButton callback={() => onDownloadClick?.(model)} />
</div>
);
@ -50,11 +50,11 @@ const AvailableModelCard: React.FC<Props> = ({
<div className="flex justify-between py-4 px-3 gap-2.5">
<DownloadModelContent
required={required}
author={product.author}
description={product.description}
author={model.author}
description={model.shortDescription}
isRecommend={isRecommend}
name={product.name}
type={product.type}
name={model.name}
type={model.type}
/>
{downloadButton}
</div>

View File

@ -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<Props> = ({ product }) => {
const ConversationalCard: React.FC<Props> = ({ model }) => {
const { requestCreateConvo } = useCreateConversation();
const { name, avatarUrl, description } = product;
const { name, avatarUrl, shortDescription } = model;
return (
<button
onClick={() => requestCreateConvo(product)}
onClick={() => requestCreateConvo(model)}
className="flex flex-col justify-between flex-shrink-0 gap-3 bg-white p-4 w-52 rounded-lg text-left dark:bg-gray-700 hover:opacity-20"
>
<div className="flex flex-col gap-2 box-border">
@ -29,7 +29,7 @@ const ConversationalCard: React.FC<Props> = ({ product }) => {
{name}
</h2>
<span className="text-gray-600 mt-1 font-normal line-clamp-2">
{description}
{shortDescription}
</span>
</div>
<span className="flex text-xs leading-5 text-gray-500 items-center gap-0.5">

View File

@ -1,12 +1,12 @@
import { Product } from "@/_models/Product";
import { AssistantModel } from "@/_models/AssistantModel";
import ConversationalCard from "../ConversationalCard";
import { ChatBubbleBottomCenterTextIcon } from "@heroicons/react/24/outline";
type Props = {
products: Product[];
models: AssistantModel[];
};
const ConversationalList: React.FC<Props> = ({ products }) => (
const ConversationalList: React.FC<Props> = ({ models }) => (
<>
<div className="flex items-center gap-3 mt-8 mb-2">
<ChatBubbleBottomCenterTextIcon width={24} height={24} className="ml-6" />
@ -15,8 +15,8 @@ const ConversationalList: React.FC<Props> = ({ products }) => (
</span>
</div>
<div className="mt-2 pl-6 flex w-full gap-2 overflow-x-scroll scroll overflow-hidden">
{products.map((item) => (
<ConversationalCard key={item.slug} product={item} />
{models.map((item) => (
<ConversationalCard key={item.id} model={item} />
))}
</div>
</>

View File

@ -1,16 +1,16 @@
import { Product } from "@/_models/Product";
import { AssistantModel } from "@/_models/AssistantModel";
import DownloadModelContent from "../DownloadModelContent";
type Props = {
product: Product;
model: AssistantModel;
isRecommend: boolean;
required?: string;
transferred?: number;
onDeleteClick?: (product: Product) => void;
onDeleteClick?: (model: AssistantModel) => void;
};
const DownloadedModelCard: React.FC<Props> = ({
product,
model,
isRecommend,
required,
onDeleteClick,
@ -19,14 +19,14 @@ const DownloadedModelCard: React.FC<Props> = ({
<div className="flex justify-between py-4 px-3 gap-2.5">
<DownloadModelContent
required={required}
author={product.author}
description={product.description}
author={model.author}
description={model.shortDescription}
isRecommend={isRecommend}
name={product.name}
type={product.type}
name={model.name}
type={model.type}
/>
<div className="flex flex-col justify-center">
<button onClick={() => onDeleteClick?.(product)}>Delete</button>
<button onClick={() => onDeleteClick?.(model)}>Delete</button>
</div>
</div>
</div>

View File

@ -6,10 +6,10 @@ import ExploreModelFilter from "../ExploreModelFilter";
const ExploreModelContainer: React.FC = () => (
<div className="flex flex-col flex-1 px-16 pt-14 overflow-hidden">
<HeaderTitle title="Explore Models" />
<SearchBar
{/* <SearchBar
type={SearchType.Model}
placeholder="Owner name like TheBloke, bhlim etc.."
/>
/> */}
<div className="flex flex-1 gap-x-10 mt-9 overflow-hidden">
<ExploreModelFilter />
<ExploreModelList />

View File

@ -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",

View File

@ -4,10 +4,20 @@
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 {
MiscellanousTag,
NumOfBit,
QuantMethodTag,
RamRequired,
UsecaseTag,
VersionTag,
} from "@/_components/SimpleTag/TagType";
import { displayDate } from "@/_utils/datetime";
import { Product } from "@/_models/Product";
import useGetMostSuitableModelVersion from "@/_hooks/useGetMostSuitableModelVersion";
import { toGigabytes } from "@/_utils/converter";
type Props = {
model: Product;
@ -16,42 +26,33 @@ type Props = {
const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
const [show, setShow] = useState(false);
const { availableVersions } = model;
const { suitableModel, getMostSuitableModelVersion } =
useGetMostSuitableModelVersion();
useEffect(() => {
getMostSuitableModelVersion(availableVersions);
}, [availableVersions]);
if (!suitableModel) {
return null;
}
const { quantMethod, bits, maxRamRequired, usecase } = suitableModel;
return (
<div
ref={ref}
className="flex flex-col border border-gray-200 rounded-md mb-4"
>
<ExploreModelItemHeader
name={model.name}
status={TagType.Recommended}
versions={model.availableVersions}
suitableModel={suitableModel}
exploreModel={model}
/>
<div className="flex flex-col px-[26px] py-[22px]">
<div className="flex justify-between">
<div className="flex-1 flex flex-col gap-8">
<div className="flex flex-col gap-1">
<div className="text-sm font-medium text-gray-500">
Model Format
</div>
<div className="px-2.5 py-0.5 bg-gray-100 text-xs text-gray-800 w-fit">
GGUF
</div>
</div>
<div className="flex flex-col">
<div className="text-sm font-medium text-gray-500">
Hardware Compatibility
</div>
<div className="flex gap-2">
<SimpleTag
clickable={false}
title={TagType.Compatible}
type={TagType.Compatible}
/>
</div>
</div>
</div>
<div className="flex-1 flex flex-col gap-8">
<div>
<div className="text-sm font-medium text-gray-500">
Release Date
</div>
@ -60,14 +61,49 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
</div>
</div>
<div className="flex flex-col gap-2">
<div className="text-sm font-medium text-gray-500">
Expected Performance
<div className="text-sm font-medium text-gray-500">Version</div>
<div className="flex gap-2">
<SimpleTag
title={model.version}
type={VersionTag.Version}
clickable={false}
/>
<SimpleTag
title={quantMethod}
type={QuantMethodTag.Default}
clickable={false}
/>
<SimpleTag
title={`${bits} Bits`}
type={NumOfBit.Default}
clickable={false}
/>
</div>
</div>
</div>
<div className="flex-1 flex flex-col gap-8">
<div>
<div className="text-sm font-medium text-gray-500">Author</div>
<div className="text-sm font-normal text-gray-900">
{model.author}
</div>
</div>
<div className="flex flex-col gap-2">
<div className="text-sm font-medium text-gray-500">
Compatibility
</div>
<div className="flex gap-2">
<SimpleTag
title={usecase}
type={UsecaseTag.UsecaseDefault}
clickable={false}
/>
<SimpleTag
title={`${toGigabytes(maxRamRequired)} RAM required`}
type={RamRequired.RamDefault}
clickable={false}
/>
</div>
<SimpleTag
title={TagType.Medium}
type={TagType.Medium}
clickable={false}
/>
</div>
</div>
</div>
@ -77,16 +113,27 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
{model.longDescription}
</span>
</div>
<div className="flex flex-col">
<div className="flex flex-col mt-5 gap-2">
<span className="text-sm font-medium text-gray-500">Tags</span>
<div className="flex flex-wrap gap-2">
{model.tags.map((tag) => (
<SimpleTag
key={tag}
title={tag}
type={MiscellanousTag.MiscellanousDefault}
clickable={false}
/>
))}
</div>
</div>
</div>
{model.availableVersions.length > 0 && (
{model.availableVersions?.length > 0 && (
<Fragment>
{show && (
<ModelVersionList
model={model}
versions={model.availableVersions}
recommendedVersion={suitableModel?.id ?? ""}
/>
)}
<button

View File

@ -1,34 +1,74 @@
import SimpleTag, { TagType } from "../SimpleTag";
import SimpleTag from "../SimpleTag";
import PrimaryButton from "../PrimaryButton";
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
import { DownloadState } from "@/_models/DownloadState";
import SecondaryButton from "../SecondaryButton";
import { ModelVersion } from "@/_models/Product";
import { Product } from "@/_models/Product";
import { useCallback, useEffect, useMemo } from "react";
import { ModelVersion } from "@/_models/ModelVersion";
import useGetPerformanceTag from "@/_hooks/useGetPerformanceTag";
import useDownloadModel from "@/_hooks/useDownloadModel";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
import { atom, useAtomValue, useSetAtom } from "jotai";
import {
MainViewState,
setMainViewStateAtom,
} from "@/_helpers/atoms/MainView.atom";
type Props = {
name: string;
status: TagType;
versions: ModelVersion[];
size?: number;
downloadState?: DownloadState;
onDownloadClick?: () => void;
suitableModel: ModelVersion;
exploreModel: Product;
};
const ExploreModelItemHeader: React.FC<Props> = ({
name,
status,
size,
versions,
downloadState,
onDownloadClick,
suitableModel,
exploreModel,
}) => {
const { downloadModel } = useDownloadModel();
const { downloadedModels } = useGetDownloadedModels();
const { performanceTag, title, getPerformanceForModel } =
useGetPerformanceTag();
const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[suitableModel.id]),
[suitableModel.id]
);
const downloadState = useAtomValue(downloadAtom);
const setMainViewState = useSetAtom(setMainViewStateAtom);
useEffect(() => {
getPerformanceForModel(suitableModel);
}, [suitableModel]);
const onDownloadClick = useCallback(() => {
downloadModel(exploreModel, suitableModel);
}, [exploreModel, suitableModel]);
const isDownloaded =
downloadedModels.find((model) => model.id === suitableModel.id) != null;
let downloadButton = (
<PrimaryButton
title={size ? `Download (${toGigabytes(size)})` : "Download"}
onClick={() => onDownloadClick?.()}
title={
suitableModel.size
? `Download (${toGigabytes(suitableModel.size)})`
: "Download"
}
onClick={() => onDownloadClick()}
/>
);
if (isDownloaded) {
downloadButton = (
<PrimaryButton
title="View Downloaded Model"
onClick={() => {
setMainViewState(MainViewState.MyModel);
}}
className="bg-green-500 hover:bg-green-400"
/>
);
}
if (downloadState != null) {
// downloading
downloadButton = (
@ -39,15 +79,15 @@ const ExploreModelItemHeader: React.FC<Props> = ({
)})`}
/>
);
} else if (versions.length === 0) {
downloadButton = <SecondaryButton disabled title="No files available" />;
}
return (
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<div className="flex items-center gap-2">
<span>{name}</span>
<SimpleTag title={status} type={status} clickable={false} />
<span>{exploreModel.name}</span>
{performanceTag && (
<SimpleTag title={title} type={performanceTag} clickable={false} />
)}
</div>
{downloadButton}
</div>

View File

@ -1,50 +1,26 @@
import React, { useEffect } from "react";
import ExploreModelItem from "../ExploreModelItem";
import { modelSearchAtom } from "@/_helpers/JotaiWrapper";
import useGetHuggingFaceModel from "@/_hooks/useGetHuggingFaceModel";
import { useAtom, useAtomValue } from "jotai";
import { useInView } from "react-intersection-observer";
import { modelLoadMoreAtom } from "@/_helpers/atoms/ExploreModelLoading.atom";
import { getConfiguredModels } from "@/_hooks/useGetDownloadedModels";
import useGetConfiguredModels from "@/_hooks/useGetConfiguredModels";
import { Waveform } from "@uiball/loaders";
const ExploreModelList: React.FC = () => {
const [loadMoreInProgress, setLoadMoreInProress] = useAtom(modelLoadMoreAtom);
const modelSearch = useAtomValue(modelSearchAtom);
const { modelList, getHuggingFaceModel } = useGetHuggingFaceModel();
const { ref, inView } = useInView({
threshold: 0,
triggerOnce: true,
});
const { loading, 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 (
<div className="flex flex-col flex-1 overflow-y-auto scroll">
{modelList.map((item, index) => (
<ExploreModelItem
ref={index === modelList.length - 1 ? ref : null}
key={item.id}
model={item}
/>
))}
{loadMoreInProgress && (
<div className="mx-auto mt-2 mb-4">
<Waveform size={24} color="#9CA3AF" />
{loading && (
<div className="mx-auto">
<Waveform size={24} color="#CBD5E0" />
</div>
)}
{models.map((item) => (
<ExploreModelItem key={item.id} model={item} />
))}
</div>
);
};

View File

@ -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 = () => {
</div>
)}
</div>
{/* <SecondaryButton title="Regenerate" onClick={onRegenerateClick} /> */}
<SecondaryButton
onClick={onNewConversationClick}

View File

@ -1,21 +1,21 @@
import React from "react";
import { Product } from "@/_models/Product";
import Image from "next/image";
import React, { useCallback } from "react";
import { ModelStatus, ModelStatusComponent } from "../ModelStatusComponent";
import ModelActionMenu from "../ModelActionMenu";
import { useAtomValue } from "jotai";
import ModelActionButton, { ModelActionType } from "../ModelActionButton";
import useStartStopModel from "@/_hooks/useStartStopModel";
import useDeleteModel from "@/_hooks/useDeleteModel";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import { AssistantModel } from "@/_models/AssistantModel";
import { activeAssistantModelAtom } from "@/_helpers/atoms/Model.atom";
import { toGigabytes } from "@/_utils/converter";
type Props = {
model: Product;
model: AssistantModel;
};
const ModelRow: React.FC<Props> = ({ 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<Props> = ({ model }) => {
}
};
const onDeleteClick = () => {
const onDeleteClick = useCallback(() => {
deleteModel(model);
};
}, [model]);
return (
<tr
className="border-b border-gray-200 last:border-b-0 last:rounded-lg"
key={model.id}
>
<tr className="border-b border-gray-200 last:border-b-0 last:rounded-lg">
<td className="flex flex-col whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900">
{model.name}
<span className="text-gray-500 font-normal">{model.version}</span>
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
<div className="flex flex-col justify-start">
<span>{model.format}</span>
{model.accelerated && (
<span className="flex items-center text-gray-500 text-sm font-normal gap-0.5">
<Image src={"/icons/flash.svg"} width={20} height={20} alt="" />
GPU Accelerated
</span>
)}
<span>GGUF</span>
</div>
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{model.totalSize}
{toGigabytes(model.size)}
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
<ModelStatusComponent status={status} />

View File

@ -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);
};

View File

@ -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"];

View File

@ -1,26 +1,39 @@
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";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
import SimpleTag from "../SimpleTag";
import { RamRequired, UsecaseTag } from "../SimpleTag/TagType";
type Props = {
model: Product;
modelVersion: ModelVersion;
isRecommended: boolean;
};
const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
const { downloadHfModel } = useDownloadModel();
const ModelVersionItem: React.FC<Props> = ({
model,
modelVersion,
isRecommended,
}) => {
const { downloadModel } = useDownloadModel();
const { downloadedModels } = useGetDownloadedModels();
const isDownloaded =
downloadedModels.find((model) => model.id === modelVersion.id) != null;
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 = (
@ -36,15 +49,33 @@ const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
downloadButton = (
<div>{formatDownloadPercentage(downloadState.percent)}</div>
);
} else if (isDownloaded) {
downloadButton = <div>Downloaded</div>;
}
const { maxRamRequired, usecase } = modelVersion;
return (
<div className="flex justify-between items-center gap-4 pl-3 pt-3 pr-4 pb-3 border-t border-gray-200 first:border-t-0">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Image src={"/icons/app_icon.svg"} width={14} height={20} alt="" />
<span className="font-sm text-gray-900">{modelVersion.path}</span>
<span className="font-sm text-gray-900 flex-1">
{modelVersion.name}
</span>
</div>
<div className="flex items-center gap-4">
<div className="flex gap-2 justify-end">
<SimpleTag
title={usecase}
type={UsecaseTag.UsecaseDefault}
clickable={false}
/>
<SimpleTag
title={`${toGigabytes(maxRamRequired)} RAM required`}
type={RamRequired.RamDefault}
clickable={false}
/>
</div>
<div className="px-2.5 py-0.5 bg-gray-200 text-xs font-medium rounded">
{toGigabytes(modelVersion.size)}
</div>

View File

@ -1,21 +1,36 @@
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;
versions: ModelVersion[];
recommendedVersion: string;
};
const ModelVersionList: React.FC<Props> = ({ model, versions }) => (
<div className="px-4 py-5 border-t border-gray-200">
<div className="text-sm font-medium text-gray-500">Available Versions</div>
<div className="border border-gray-200 rounded-lg overflow-hidden">
{versions.map((item) => (
<ModelVersionItem key={item.path} model={model} modelVersion={item} />
))}
const ModelVersionList: React.FC<Props> = ({
model,
versions,
recommendedVersion,
}) => {
return (
<div className="px-4 py-5 border-t border-gray-200">
<div className="text-sm font-medium text-gray-500">
Available Versions
</div>
<div className="border border-gray-200 rounded-lg overflow-hidden">
{versions.map((item) => (
<ModelVersionItem
key={item.id}
model={model}
modelVersion={item}
isRecommended={item.id === recommendedVersion}
/>
))}
</div>
</div>
</div>
);
);
};
export default ModelVersionList;

View File

@ -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);

View File

@ -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);
};

View File

@ -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);
};

View File

@ -0,0 +1,21 @@
import { TagType } from "./TagType";
export const tagStyleMapper: Record<TagType, string> = {
GGUF: "bg-yellow-100 text-yellow-800",
PerformancePositive:
"text-green-700 ring-1 ring-inset ring-green-600/20 bg-green-50",
PerformanceNeutral:
"bg-yellow-50 text-yellow-800 ring-1 ring-inset ring-yellow-600/20",
PerformanceNegative:
"bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
HardwareCompatible: "bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
HardwareIncompatible:
"bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
FreeStyle: "bg-gray-100 text-gray-800",
ExpectPerformanceMedium: "bg-yellow-100 text-yellow-800",
Version: "bg-red-100 text-yellow-800",
Default: "bg-blue-100 text-blue-800",
RamDefault: "bg-green-50 text-green-700",
UsecaseDefault: "bg-orange-100 text-yellow-800",
MiscellanousDefault: "bg-blue-100 text-blue-800",
};

View File

@ -0,0 +1,62 @@
export enum ModelPerformance {
PerformancePositive = "PerformancePositive",
PerformanceNeutral = "PerformanceNeutral",
PerformanceNegative = "PerformanceNegative",
}
export enum HardwareCompatibility {
HardwareCompatible = "HardwareCompatible",
HardwareIncompatible = "HardwareIncompatible",
}
export enum ExpectedPerformance {
ExpectPerformanceMedium = "ExpectPerformanceMedium",
}
export enum ModelFormat {
GGUF = "GGUF",
}
export enum FreestyleTag {
FreeStyle = "FreeStyle",
}
export enum VersionTag {
Version = "Version",
}
export enum QuantMethodTag {
Default = "Default",
}
export enum NumOfBit {
Default = "Default",
}
export enum RamRequired {
RamDefault = "RamDefault",
}
export enum UsecaseTag {
UsecaseDefault = "UsecaseDefault",
}
export enum MiscellanousTag {
MiscellanousDefault = "MiscellanousDefault",
}
export type TagType =
| ModelPerformance
| HardwareCompatibility
| ExpectedPerformance
| ModelFormat
| FreestyleTag
| VersionTag
| QuantMethodTag
| NumOfBit
| RamRequired
| UsecaseTag
| MiscellanousTag;

View File

@ -1,57 +1,6 @@
import React from "react";
export enum TagType {
Roleplay = "Roleplay",
Llama = "Llama",
Story = "Story",
Casual = "Casual",
Professional = "Professional",
CodeLlama = "CodeLlama",
Coding = "Coding",
// Positive
Recommended = "Recommended",
Compatible = "Compatible",
// Neutral
SlowOnDevice = "This model will be slow on your device",
// Negative
InsufficientRam = "Insufficient RAM",
Incompatible = "Incompatible with your device",
TooLarge = "This model is too large for your device",
// Performance
Medium = "Medium",
BalancedQuality = "Balanced Quality",
}
const tagStyleMapper: Record<TagType, string> = {
[TagType.Roleplay]: "bg-red-100 text-red-800",
[TagType.Llama]: "bg-green-100 text-green-800",
[TagType.Story]: "bg-blue-100 text-blue-800",
[TagType.Casual]: "bg-yellow-100 text-yellow-800",
[TagType.Professional]: "text-indigo-800 bg-indigo-100",
[TagType.CodeLlama]: "bg-pink-100 text-pink-800",
[TagType.Coding]: "text-purple-800 bg-purple-100",
[TagType.Recommended]:
"text-green-700 ring-1 ring-inset ring-green-600/20 bg-green-50",
[TagType.Compatible]:
"bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
[TagType.SlowOnDevice]:
"bg-yellow-50 text-yellow-800 ring-1 ring-inset ring-yellow-600/20",
[TagType.Incompatible]:
"bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
[TagType.InsufficientRam]:
"bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
[TagType.TooLarge]: "bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
[TagType.Medium]: "bg-yellow-100 text-yellow-800",
[TagType.BalancedQuality]: "bg-yellow-100 text-yellow-800",
};
import { TagType } from "./TagType";
import { tagStyleMapper } from "./TagStyleMapper";
type Props = {
title: string;
@ -66,10 +15,11 @@ const SimpleTag: React.FC<Props> = ({
title,
type,
}) => {
if (!title || title.length === 0) return null;
if (!clickable) {
return (
<div
className={`px-2.5 py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
className={`px-2.5 py-0.5 rounded text-xs font-medium items-center line-clamp-1 max-w-[40%] ${tagStyleMapper[type]}`}
>
{title}
</div>
@ -79,7 +29,7 @@ const SimpleTag: React.FC<Props> = ({
return (
<button
onClick={onClick}
className={`px-2.5 py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
className={`px-2.5 py-0.5 rounded text-xs font-medium items-center line-clamp-1 max-w-[40%] ${tagStyleMapper[type]}`}
>
{title} x
</button>

View File

@ -1,4 +1,7 @@
import { Product } from "@/_models/Product";
import { AssistantModel } from "@/_models/AssistantModel";
import { atom } from "jotai";
export const downloadedModelAtom = atom<Product[]>([]);
/**
* @description: This atom is used to store the downloaded models
*/
export const downloadedModelAtom = atom<AssistantModel[]>([]);

View File

@ -1,6 +1,8 @@
import { Product } from "@/_models/Product";
import { AssistantModel } from "@/_models/AssistantModel";
import { atom } from "jotai";
export const currentProductAtom = atom<Product | undefined>(undefined);
export const selectedModelAtom = atom<AssistantModel | undefined>(undefined);
export const selectedModelAtom = atom<Product | undefined>(undefined);
export const activeAssistantModelAtom = atom<AssistantModel | undefined>(
undefined
);

View File

@ -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,

View File

@ -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();

View File

@ -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,
};
}

View File

@ -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<Product[]>([]);
const [availableModels, setAvailableModels] = useState<Product[]>([]);
const [downloadedModels, setDownloadedModels] = useState<Product[]>([]);
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<Product[]> {
const avails: Product[] = await executeSerial(
ModelManagementService.GET_AVAILABLE_MODELS
);
return avails ?? [];
}

View File

@ -0,0 +1,22 @@
import { Product } from "@/_models/Product";
import { useEffect, useState } from "react";
import { getConfiguredModels } from "./useGetDownloadedModels";
export default function useGetConfiguredModels() {
const [loading, setLoading] = useState<boolean>(false);
const [models, setModels] = useState<Product[]>([]);
const fetchModels = async () => {
setLoading(true);
const models = await getConfiguredModels();
setLoading(false);
setModels(models);
};
// TODO allow user for filter
useEffect(() => {
fetchModels();
}, []);
return { loading, models };
}

View File

@ -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<Product[]> {
const downloadedModels: Product[] = await executeSerial(
export async function getDownloadedModels(): Promise<AssistantModel[]> {
const downloadedModels: AssistantModel[] = await executeSerial(
DataService.GET_FINISHED_DOWNLOAD_MODELS
);
return downloadedModels ?? [];
}
export async function getModelFiles(): Promise<Product[]> {
const downloadedModels: Product[] = await executeSerial(
ModelManagementService.GET_DOWNLOADED_MODELS
);
return downloadedModels ?? [];
export async function getConfiguredModels(): Promise<Product[]> {
return executeSerial(ModelManagementService.GET_CONFIGURED_MODELS);
}
export async function searchModels(
params: SearchModelParamHf
): Promise<QueryProductResult> {
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;
};

View File

@ -1,5 +1,4 @@
import { useState } from "react";
import { searchModels } from "./useGetDownloadedModels";
import { SearchModelParamHf } from "@/_models/hf/SearchModelParam.hf";
import { Product } from "@/_models/Product";
import { useSetAtom } from "jotai";
@ -22,14 +21,14 @@ export default function useGetHuggingFaceModel() {
search: { owner },
limit: 5,
};
const result = await searchModels(searchParams);
console.debug("result", JSON.stringify(result));
if (owner !== currentOwner) {
setModelList(result.data);
setCurrentOwner(owner);
} else {
setModelList([...modelList, ...result.data]);
}
// const result = await searchModels(searchParams);
// console.debug("result", JSON.stringify(result));
// if (owner !== currentOwner) {
// setModelList(result.data);
// setCurrentOwner(owner);
// } else {
// setModelList([...modelList, ...result.data]);
// }
setLoadMoreInProgress(false);
};

View File

@ -0,0 +1,29 @@
import { executeSerial } from "@/_services/pluginService";
import { SystemMonitoringService } from "../../shared/coreService";
import { ModelVersion } from "@/_models/ModelVersion";
import { useState } from "react";
export default function useGetMostSuitableModelVersion() {
const [suitableModel, setSuitableModel] = useState<ModelVersion | undefined>();
const getMostSuitableModelVersion = async (modelVersions: ModelVersion[]) => {
const resourceInfo = await executeSerial(
SystemMonitoringService.GET_RESOURCES_INFORMATION
);
const totalRam = resourceInfo.mem.total;
// find the model version with the highest required RAM that is still below the user's RAM by 80%
const modelVersion = modelVersions.reduce((prev, current) => {
if (current.maxRamRequired > prev.maxRamRequired) {
if (current.maxRamRequired < totalRam * 0.8) {
return current;
}
}
return prev;
});
setSuitableModel(modelVersion);
};
return { suitableModel, getMostSuitableModelVersion };
}

View File

@ -0,0 +1,53 @@
import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager";
import { SystemMonitoringService } from "../../shared/coreService";
import { useState } from "react";
import { ModelVersion } from "@/_models/ModelVersion";
import { ModelPerformance, TagType } from "@/_components/SimpleTag/TagType";
// Recommendation:
// `Recommended (green)`: "Max RAM required" is 80% of users max RAM.
// `Slow on your device (yellow)`: Max RAM required is 80-100% of users max RAM
// `Not enough RAM (red)`: User RAM is below "Max RAM required"
export default function useGetPerformanceTag() {
const [performanceTag, setPerformanceTag] = useState<TagType | undefined>();
const getPerformanceForModel = async (modelVersion: ModelVersion) => {
const resourceInfo = await executeSerial(
SystemMonitoringService.GET_RESOURCES_INFORMATION
);
const totalRam = resourceInfo.mem.total;
const requiredRam = modelVersion.maxRamRequired;
setPerformanceTag(calculateRamPerformance(requiredRam, totalRam));
};
let title = "";
switch (performanceTag) {
case ModelPerformance.PerformancePositive:
title = "Recommended";
break;
case ModelPerformance.PerformanceNeutral:
title = "Slow on your device";
break;
case ModelPerformance.PerformanceNegative:
title = "Not enough RAM";
break;
}
return { performanceTag, title, getPerformanceForModel };
}
const calculateRamPerformance = (
requiredRamAmt: number,
totalRamAmt: number
) => {
const percentage = requiredRamAmt / totalRamAmt;
if (percentage < 0.8) {
return ModelPerformance.PerformancePositive;
} else if (percentage >= 0.8 && percentage < 1) {
return ModelPerformance.PerformanceNeutral;
} else {
return ModelPerformance.PerformanceNegative;
}
};

View File

@ -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;

View File

@ -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);

View File

@ -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;
releaseDate: number;
tags: string[];
};

View File

@ -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;
};

View File

@ -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
status: string;
releaseDate: number;
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;
}

View File

@ -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) {

View File

@ -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 {