refactor: model plugin to follow new specs (#682)
* refactor: model plugin to follow new specs Signed-off-by: James <james@jan.ai> * chore: rebase main chore: rebase main --------- Signed-off-by: James <james@jan.ai> Co-authored-by: James <james@jan.ai> Co-authored-by: Louis <louis@jan.ai>
This commit is contained in:
parent
a990fa6c07
commit
86e693b250
@ -12,20 +12,7 @@ const executeOnMain: (
|
|||||||
method: string,
|
method: string,
|
||||||
...args: any[]
|
...args: any[]
|
||||||
) => Promise<any> = (plugin, method, ...args) =>
|
) => Promise<any> = (plugin, method, ...args) =>
|
||||||
window.coreAPI?.invokePluginFunc(plugin, method, ...args) ??
|
window.coreAPI?.invokePluginFunc(plugin, method, ...args)
|
||||||
window.electronAPI?.invokePluginFunc(plugin, method, ...args);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This object is deprecated and should not be used.
|
|
||||||
* Use individual functions instead.
|
|
||||||
*/
|
|
||||||
const invokePluginFunc: (
|
|
||||||
plugin: string,
|
|
||||||
method: string,
|
|
||||||
...args: any[]
|
|
||||||
) => Promise<any> = (plugin, method, ...args) =>
|
|
||||||
window.coreAPI?.invokePluginFunc(plugin, method, ...args) ??
|
|
||||||
window.electronAPI?.invokePluginFunc(plugin, method, ...args);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a file from a URL and saves it to the local file system.
|
* Downloads a file from a URL and saves it to the local file system.
|
||||||
@ -36,16 +23,7 @@ const invokePluginFunc: (
|
|||||||
const downloadFile: (url: string, fileName: string) => Promise<any> = (
|
const downloadFile: (url: string, fileName: string) => Promise<any> = (
|
||||||
url,
|
url,
|
||||||
fileName
|
fileName
|
||||||
) =>
|
) => window.coreAPI?.downloadFile(url, fileName);
|
||||||
window.coreAPI?.downloadFile(url, fileName) ??
|
|
||||||
window.electronAPI?.downloadFile(url, fileName);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This object is deprecated and should not be used.
|
|
||||||
* Use fs module instead.
|
|
||||||
*/
|
|
||||||
const deleteFile: (path: string) => Promise<any> = (path) =>
|
|
||||||
window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aborts the download of a specific file.
|
* Aborts the download of a specific file.
|
||||||
@ -66,11 +44,18 @@ const appDataPath: () => Promise<any> = () => window.coreAPI?.appDataPath();
|
|||||||
* Gets the user space path.
|
* Gets the user space path.
|
||||||
* @returns {Promise<any>} A Promise that resolves with the user space path.
|
* @returns {Promise<any>} A Promise that resolves with the user space path.
|
||||||
*/
|
*/
|
||||||
const getUserSpace = (): Promise<string> =>
|
const getUserSpace = (): Promise<string> => window.coreAPI?.getUserSpace();
|
||||||
window.coreAPI?.getUserSpace() ?? window.electronAPI?.getUserSpace();
|
|
||||||
|
|
||||||
/** Register extension point function type definition
|
/**
|
||||||
*
|
* Opens the file explorer at a specific path.
|
||||||
|
* @param {string} path - The path to open in the file explorer.
|
||||||
|
* @returns {Promise<any>} A promise that resolves when the file explorer is opened.
|
||||||
|
*/
|
||||||
|
const openFileExplorer: (path: string) => Promise<any> = (path) =>
|
||||||
|
window.coreAPI?.openFileExplorer(path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register extension point function type definition
|
||||||
*/
|
*/
|
||||||
export type RegisterExtensionPoint = (
|
export type RegisterExtensionPoint = (
|
||||||
extensionName: string,
|
extensionName: string,
|
||||||
@ -79,29 +64,14 @@ export type RegisterExtensionPoint = (
|
|||||||
priority?: number
|
priority?: number
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This object is deprecated and should not be used.
|
|
||||||
* Use individual functions instead.
|
|
||||||
*/
|
|
||||||
export const core = {
|
|
||||||
invokePluginFunc,
|
|
||||||
executeOnMain,
|
|
||||||
downloadFile,
|
|
||||||
abortDownload,
|
|
||||||
deleteFile,
|
|
||||||
appDataPath,
|
|
||||||
getUserSpace,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functions exports
|
* Functions exports
|
||||||
*/
|
*/
|
||||||
export {
|
export {
|
||||||
invokePluginFunc,
|
|
||||||
executeOnMain,
|
executeOnMain,
|
||||||
downloadFile,
|
downloadFile,
|
||||||
abortDownload,
|
abortDownload,
|
||||||
deleteFile,
|
|
||||||
appDataPath,
|
appDataPath,
|
||||||
getUserSpace,
|
getUserSpace,
|
||||||
|
openFileExplorer,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,8 +5,7 @@
|
|||||||
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
||||||
*/
|
*/
|
||||||
const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
||||||
window.coreAPI?.writeFile(path, data) ??
|
window.coreAPI?.writeFile(path, data);
|
||||||
window.electronAPI?.writeFile(path, data);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the path is a directory.
|
* Checks whether the path is a directory.
|
||||||
@ -14,7 +13,7 @@ const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
|||||||
* @returns {boolean} A boolean indicating whether the path is a directory.
|
* @returns {boolean} A boolean indicating whether the path is a directory.
|
||||||
*/
|
*/
|
||||||
const isDirectory = (path: string): Promise<boolean> =>
|
const isDirectory = (path: string): Promise<boolean> =>
|
||||||
window.coreAPI?.isDirectory(path) ?? window.electronAPI?.isDirectory(path);
|
window.coreAPI?.isDirectory(path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the contents of a file at the specified path.
|
* Reads the contents of a file at the specified path.
|
||||||
@ -22,7 +21,7 @@ const isDirectory = (path: string): Promise<boolean> =>
|
|||||||
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
||||||
*/
|
*/
|
||||||
const readFile: (path: string) => Promise<any> = (path) =>
|
const readFile: (path: string) => Promise<any> = (path) =>
|
||||||
window.coreAPI?.readFile(path) ?? window.electronAPI?.readFile(path);
|
window.coreAPI?.readFile(path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List the directory files
|
* List the directory files
|
||||||
@ -30,7 +29,7 @@ const readFile: (path: string) => Promise<any> = (path) =>
|
|||||||
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.
|
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.
|
||||||
*/
|
*/
|
||||||
const listFiles: (path: string) => Promise<any> = (path) =>
|
const listFiles: (path: string) => Promise<any> = (path) =>
|
||||||
window.coreAPI?.listFiles(path) ?? window.electronAPI?.listFiles(path);
|
window.coreAPI?.listFiles(path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a directory at the specified path.
|
* Creates a directory at the specified path.
|
||||||
@ -38,7 +37,7 @@ const listFiles: (path: string) => Promise<any> = (path) =>
|
|||||||
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
||||||
*/
|
*/
|
||||||
const mkdir: (path: string) => Promise<any> = (path) =>
|
const mkdir: (path: string) => Promise<any> = (path) =>
|
||||||
window.coreAPI?.mkdir(path) ?? window.electronAPI?.mkdir(path);
|
window.coreAPI?.mkdir(path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a directory at the specified path.
|
* Removes a directory at the specified path.
|
||||||
@ -46,14 +45,30 @@ const mkdir: (path: string) => Promise<any> = (path) =>
|
|||||||
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
|
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
|
||||||
*/
|
*/
|
||||||
const rmdir: (path: string) => Promise<any> = (path) =>
|
const rmdir: (path: string) => Promise<any> = (path) =>
|
||||||
window.coreAPI?.rmdir(path) ?? window.electronAPI?.rmdir(path);
|
window.coreAPI?.rmdir(path);
|
||||||
/**
|
/**
|
||||||
* Deletes a file from the local file system.
|
* Deletes a file from the local file system.
|
||||||
* @param {string} path - The path of the file to delete.
|
* @param {string} path - The path of the file to delete.
|
||||||
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
||||||
*/
|
*/
|
||||||
const deleteFile: (path: string) => Promise<any> = (path) =>
|
const deleteFile: (path: string) => Promise<any> = (path) =>
|
||||||
window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path);
|
window.coreAPI?.deleteFile(path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends data to a file at the specified path.
|
||||||
|
* @param path path to the file
|
||||||
|
* @param data data to append
|
||||||
|
*/
|
||||||
|
const appendFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
||||||
|
window.coreAPI?.appendFile(path, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a file line by line.
|
||||||
|
* @param {string} path - The path of the file to read.
|
||||||
|
* @returns {Promise<any>} A promise that resolves to the lines of the file.
|
||||||
|
*/
|
||||||
|
const readLineByLine: (path: string) => Promise<any> = (path) =>
|
||||||
|
window.coreAPI?.readLineByLine(path);
|
||||||
|
|
||||||
export const fs = {
|
export const fs = {
|
||||||
isDirectory,
|
isDirectory,
|
||||||
@ -63,4 +78,6 @@ export const fs = {
|
|||||||
mkdir,
|
mkdir,
|
||||||
rmdir,
|
rmdir,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
|
appendFile,
|
||||||
|
readLineByLine,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,40 +1,26 @@
|
|||||||
/**
|
|
||||||
* @deprecated This object is deprecated and should not be used.
|
|
||||||
* Use individual functions instead.
|
|
||||||
*/
|
|
||||||
export { core, deleteFile, invokePluginFunc } from "./core";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core module exports.
|
* Core module exports.
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
export {
|
export * from "./core";
|
||||||
downloadFile,
|
|
||||||
executeOnMain,
|
|
||||||
appDataPath,
|
|
||||||
getUserSpace,
|
|
||||||
abortDownload,
|
|
||||||
} from "./core";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events module exports.
|
* Events events exports.
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
export { events } from "./events";
|
export * from "./events";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events types exports.
|
* Events types exports.
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
export * from "./events";
|
|
||||||
|
|
||||||
export * from "./types/index";
|
export * from "./types/index";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filesystem module exports.
|
* Filesystem module exports.
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
export { fs } from "./fs";
|
export * from "./fs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin base module export.
|
* Plugin base module export.
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export enum PluginType {
|
|||||||
Preference = "preference",
|
Preference = "preference",
|
||||||
SystemMonitoring = "systemMonitoring",
|
SystemMonitoring = "systemMonitoring",
|
||||||
Model = "model",
|
Model = "model",
|
||||||
|
Assistant = "assistant",
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class JanPlugin {
|
export abstract class JanPlugin {
|
||||||
|
|||||||
28
core/src/plugins/assistant.ts
Normal file
28
core/src/plugins/assistant.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Assistant } from "../index";
|
||||||
|
import { JanPlugin } from "../plugin";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for assistant plugins.
|
||||||
|
* @extends JanPlugin
|
||||||
|
*/
|
||||||
|
export abstract class AssistantPlugin extends JanPlugin {
|
||||||
|
/**
|
||||||
|
* Creates a new assistant.
|
||||||
|
* @param {Assistant} assistant - The assistant object to be created.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the assistant has been created.
|
||||||
|
*/
|
||||||
|
abstract createAssistant(assistant: Assistant): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an existing assistant.
|
||||||
|
* @param {Assistant} assistant - The assistant object to be deleted.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the assistant has been deleted.
|
||||||
|
*/
|
||||||
|
abstract deleteAssistant(assistant: Assistant): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all existing assistants.
|
||||||
|
* @returns {Promise<Assistant[]>} A promise that resolves to an array of all assistants.
|
||||||
|
*/
|
||||||
|
abstract getAssistants(): Promise<Assistant[]>;
|
||||||
|
}
|
||||||
@ -1,32 +1,57 @@
|
|||||||
import { Thread } from "../index";
|
import { Thread, ThreadMessage } from "../index";
|
||||||
import { JanPlugin } from "../plugin";
|
import { JanPlugin } from "../plugin";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class for conversational plugins.
|
* Abstract class for Thread plugins.
|
||||||
* @abstract
|
* @abstract
|
||||||
* @extends JanPlugin
|
* @extends JanPlugin
|
||||||
*/
|
*/
|
||||||
export abstract class ConversationalPlugin extends JanPlugin {
|
export abstract class ConversationalPlugin extends JanPlugin {
|
||||||
/**
|
/**
|
||||||
* Returns a list of conversations.
|
* Returns a list of thread.
|
||||||
* @abstract
|
* @abstract
|
||||||
* @returns {Promise<any[]>} A promise that resolves to an array of conversations.
|
* @returns {Promise<Thread[]>} A promise that resolves to an array of threads.
|
||||||
*/
|
*/
|
||||||
abstract getConversations(): Promise<any[]>;
|
abstract getThreads(): Promise<Thread[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves a conversation.
|
* Saves a thread.
|
||||||
* @abstract
|
* @abstract
|
||||||
* @param {Thread} conversation - The conversation to save.
|
* @param {Thread} thread - The thread to save.
|
||||||
* @returns {Promise<void>} A promise that resolves when the conversation is saved.
|
* @returns {Promise<void>} A promise that resolves when the thread is saved.
|
||||||
*/
|
*/
|
||||||
abstract saveConversation(conversation: Thread): Promise<void>;
|
abstract saveThread(thread: Thread): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a conversation.
|
* Deletes a thread.
|
||||||
* @abstract
|
* @abstract
|
||||||
* @param {string} conversationId - The ID of the conversation to delete.
|
* @param {string} threadId - The ID of the thread to delete.
|
||||||
* @returns {Promise<void>} A promise that resolves when the conversation is deleted.
|
* @returns {Promise<void>} A promise that resolves when the thread is deleted.
|
||||||
*/
|
*/
|
||||||
abstract deleteConversation(conversationId: string): Promise<void>;
|
abstract deleteThread(threadId: string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new message to the thread.
|
||||||
|
* @param {ThreadMessage} message - The message to be added.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the message has been added.
|
||||||
|
*/
|
||||||
|
abstract addNewMessage(message: ThreadMessage): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes an array of messages to a specific thread.
|
||||||
|
* @param {string} threadId - The ID of the thread to write the messages to.
|
||||||
|
* @param {ThreadMessage[]} messages - The array of messages to be written.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the messages have been written.
|
||||||
|
*/
|
||||||
|
abstract writeMessages(
|
||||||
|
threadId: string,
|
||||||
|
messages: ThreadMessage[]
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all messages from a specific thread.
|
||||||
|
* @param {string} threadId - The ID of the thread to retrieve the messages from.
|
||||||
|
* @returns {Promise<ThreadMessage[]>} A promise that resolves to an array of messages from the thread.
|
||||||
|
*/
|
||||||
|
abstract getAllMessages(threadId: string): Promise<ThreadMessage[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,11 @@ export { InferencePlugin } from "./inference";
|
|||||||
*/
|
*/
|
||||||
export { MonitoringPlugin } from "./monitoring";
|
export { MonitoringPlugin } from "./monitoring";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assistant plugin for managing assistants.
|
||||||
|
*/
|
||||||
|
export { AssistantPlugin } from "./assistant";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model plugin for managing models.
|
* Model plugin for managing models.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { MessageRequest, ThreadMessage } from "../index";
|
import { MessageRequest, ModelSettingParams, ThreadMessage } from "../index";
|
||||||
import { JanPlugin } from "../plugin";
|
import { JanPlugin } from "../plugin";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -7,9 +7,9 @@ import { JanPlugin } from "../plugin";
|
|||||||
export abstract class InferencePlugin extends JanPlugin {
|
export abstract class InferencePlugin extends JanPlugin {
|
||||||
/**
|
/**
|
||||||
* Initializes the model for the plugin.
|
* Initializes the model for the plugin.
|
||||||
* @param modelFileName - The name of the file containing the model.
|
* @param modelId - The ID of the model to initialize.
|
||||||
*/
|
*/
|
||||||
abstract initModel(modelFileName: string): Promise<void>;
|
abstract initModel(modelId: string, settings?: ModelSettingParams): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the model for the plugin.
|
* Stops the model for the plugin.
|
||||||
|
|||||||
@ -18,18 +18,17 @@ export abstract class ModelPlugin extends JanPlugin {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the download of a specific model.
|
* Cancels the download of a specific model.
|
||||||
* @param {string} name - The name of the model to cancel the download for.
|
|
||||||
* @param {string} modelId - The ID of the model to cancel the download for.
|
* @param {string} modelId - The ID of the model to cancel the download for.
|
||||||
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
|
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
|
||||||
*/
|
*/
|
||||||
abstract cancelModelDownload(name: string, modelId: string): Promise<void>;
|
abstract cancelModelDownload(modelId: string): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a model.
|
* Deletes a model.
|
||||||
* @param filePath - The file path of the model to delete.
|
* @param modelId - The ID of the model to delete.
|
||||||
* @returns A Promise that resolves when the model has been deleted.
|
* @returns A Promise that resolves when the model has been deleted.
|
||||||
*/
|
*/
|
||||||
abstract deleteModel(filePath: string): Promise<void>;
|
abstract deleteModel(modelId: string): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves a model.
|
* Saves a model.
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The role of the author of this message.
|
* The role of the author of this message.
|
||||||
* @data_transfer_object
|
|
||||||
*/
|
*/
|
||||||
export enum ChatCompletionRole {
|
export enum ChatCompletionRole {
|
||||||
System = "system",
|
System = "system",
|
||||||
@ -30,10 +29,20 @@ export type ChatCompletionMessage = {
|
|||||||
*/
|
*/
|
||||||
export type MessageRequest = {
|
export type MessageRequest = {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
||||||
/** The thread id of the message request. **/
|
/** The thread id of the message request. **/
|
||||||
threadId?: string;
|
threadId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The assistant id of the message request.
|
||||||
|
*/
|
||||||
|
assistantId?: string;
|
||||||
|
|
||||||
/** Messages for constructing a chat completion request **/
|
/** Messages for constructing a chat completion request **/
|
||||||
messages?: ChatCompletionMessage[];
|
messages?: ChatCompletionMessage[];
|
||||||
|
|
||||||
|
/** Runtime parameters for constructing a chat completion request **/
|
||||||
|
parameters?: ModelRuntimeParam;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,17 +66,50 @@ export enum MessageStatus {
|
|||||||
*/
|
*/
|
||||||
export type ThreadMessage = {
|
export type ThreadMessage = {
|
||||||
/** Unique identifier for the message, generated by default using the ULID method. **/
|
/** Unique identifier for the message, generated by default using the ULID method. **/
|
||||||
id?: string;
|
id: string;
|
||||||
|
/** Object name **/
|
||||||
|
object: string;
|
||||||
/** Thread id, default is a ulid. **/
|
/** Thread id, default is a ulid. **/
|
||||||
threadId?: string;
|
thread_id: string;
|
||||||
/** The role of the author of this message. **/
|
/** The role of the author of this message. **/
|
||||||
role?: ChatCompletionRole;
|
assistant_id?: string;
|
||||||
|
// TODO: comment
|
||||||
|
role: ChatCompletionRole;
|
||||||
/** The content of this message. **/
|
/** The content of this message. **/
|
||||||
content?: string;
|
content: ThreadContent[];
|
||||||
/** The status of this message. **/
|
/** The status of this message. **/
|
||||||
status: MessageStatus;
|
status: MessageStatus;
|
||||||
/** The timestamp indicating when this message was created, represented in ISO 8601 format. **/
|
/** The timestamp indicating when this message was created. Represented in Unix time. **/
|
||||||
createdAt?: string;
|
created: number;
|
||||||
|
/** The timestamp indicating when this message was updated. Represented in Unix time. **/
|
||||||
|
updated: number;
|
||||||
|
/** The additional metadata of this message. **/
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content type of the message.
|
||||||
|
*/
|
||||||
|
export enum ContentType {
|
||||||
|
Text = "text",
|
||||||
|
Image = "image",
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The `ThreadContent` type defines the shape of a message's content object
|
||||||
|
* @data_transfer_object
|
||||||
|
*/
|
||||||
|
export type ThreadContent = {
|
||||||
|
type: ContentType;
|
||||||
|
text: ContentValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `ContentValue` type defines the shape of a content value object
|
||||||
|
* @data_transfer_object
|
||||||
|
*/
|
||||||
|
export type ContentValue = {
|
||||||
|
value: string;
|
||||||
|
annotations: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,61 +119,167 @@ export type ThreadMessage = {
|
|||||||
export interface Thread {
|
export interface Thread {
|
||||||
/** Unique identifier for the thread, generated by default using the ULID method. **/
|
/** Unique identifier for the thread, generated by default using the ULID method. **/
|
||||||
id: string;
|
id: string;
|
||||||
/** The summary of this thread. **/
|
/** Object name **/
|
||||||
summary?: string;
|
object: string;
|
||||||
/** The messages of this thread. **/
|
/** The title of this thread. **/
|
||||||
messages: ThreadMessage[];
|
title: string;
|
||||||
|
/** Assistants in this thread. **/
|
||||||
|
assistants: ThreadAssistantInfo[];
|
||||||
|
// if the thread has been init will full assistant info
|
||||||
|
isFinishInit: boolean;
|
||||||
/** The timestamp indicating when this thread was created, represented in ISO 8601 format. **/
|
/** The timestamp indicating when this thread was created, represented in ISO 8601 format. **/
|
||||||
createdAt?: string;
|
created: number;
|
||||||
/** The timestamp indicating when this thread was updated, represented in ISO 8601 format. **/
|
/** The timestamp indicating when this thread was updated, represented in ISO 8601 format. **/
|
||||||
updatedAt?: string;
|
updated: number;
|
||||||
|
/** The additional metadata of this thread. **/
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated This field is deprecated and should not be used.
|
* Represents the information about an assistant in a thread.
|
||||||
* Read from model file instead.
|
* @stored
|
||||||
*/
|
*/
|
||||||
modelId?: string;
|
export type ThreadAssistantInfo = {
|
||||||
}
|
assistant_id: string;
|
||||||
|
assistant_name: string;
|
||||||
|
model: ModelInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the information about a model.
|
||||||
|
* @stored
|
||||||
|
*/
|
||||||
|
export type ModelInfo = {
|
||||||
|
id: string;
|
||||||
|
settings: ModelSettingParams;
|
||||||
|
parameters: ModelRuntimeParam;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state of a thread.
|
||||||
|
* @stored
|
||||||
|
*/
|
||||||
|
export type ThreadState = {
|
||||||
|
hasMore: boolean;
|
||||||
|
waitingForResponse: boolean;
|
||||||
|
error?: Error;
|
||||||
|
lastMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model type defines the shape of a model object.
|
* Model type defines the shape of a model object.
|
||||||
* @stored
|
* @stored
|
||||||
*/
|
*/
|
||||||
export interface Model {
|
export interface Model {
|
||||||
/** Combination of owner and model name.*/
|
/**
|
||||||
id: string;
|
* The type of the object.
|
||||||
/** The name of the model.*/
|
* Default: "model"
|
||||||
name: string;
|
*/
|
||||||
/** Quantization method name.*/
|
object: string;
|
||||||
quantizationName: string;
|
|
||||||
/** The the number of bits represents a number.*/
|
/**
|
||||||
bits: number;
|
* The version of the model.
|
||||||
/** The size of the model file in bytes.*/
|
*/
|
||||||
size: number;
|
|
||||||
/** The maximum RAM required to run the model in bytes.*/
|
|
||||||
maxRamRequired: number;
|
|
||||||
/** The use case of the model.*/
|
|
||||||
usecase: string;
|
|
||||||
/** The download link of the model.*/
|
|
||||||
downloadLink: string;
|
|
||||||
/** The short description of the model.*/
|
|
||||||
shortDescription: string;
|
|
||||||
/** The long description of the model.*/
|
|
||||||
longDescription: string;
|
|
||||||
/** The avatar url of the model.*/
|
|
||||||
avatarUrl: string;
|
|
||||||
/** The author name of the model.*/
|
|
||||||
author: string;
|
|
||||||
/** The version of the model.*/
|
|
||||||
version: string;
|
version: string;
|
||||||
/** The origin url of the model repo.*/
|
|
||||||
modelUrl: string;
|
/**
|
||||||
/** The timestamp indicating when this model was released.*/
|
* The model download source. It can be an external url or a local filepath.
|
||||||
releaseDate: number;
|
*/
|
||||||
/** The tags attached to the model description */
|
source_url: string;
|
||||||
tags: string[];
|
|
||||||
|
/**
|
||||||
|
* The model identifier, which can be referenced in the API endpoints.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human-readable name that is used for UI.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The organization that owns the model (you!)
|
||||||
|
* Default: "you"
|
||||||
|
*/
|
||||||
|
owned_by: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Unix timestamp (in seconds) for when the model was created
|
||||||
|
*/
|
||||||
|
created: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default: "A cool model from Huggingface"
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model state.
|
||||||
|
* Default: "to_download"
|
||||||
|
* Enum: "to_download" "downloading" "ready" "running"
|
||||||
|
*/
|
||||||
|
state: ModelState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model settings.
|
||||||
|
*/
|
||||||
|
settings: ModelSettingParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model runtime parameters.
|
||||||
|
*/
|
||||||
|
parameters: ModelRuntimeParam;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata of the model.
|
||||||
|
*/
|
||||||
|
metadata: ModelMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Model transition states.
|
||||||
|
*/
|
||||||
|
export enum ModelState {
|
||||||
|
ToDownload = "to_download",
|
||||||
|
Downloading = "downloading",
|
||||||
|
Ready = "ready",
|
||||||
|
Running = "running",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available model settings.
|
||||||
|
*/
|
||||||
|
export type ModelSettingParams = {
|
||||||
|
ctx_len: number;
|
||||||
|
ngl: number;
|
||||||
|
embedding: boolean;
|
||||||
|
n_parallel: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available model runtime parameters.
|
||||||
|
*/
|
||||||
|
export type ModelRuntimeParam = {
|
||||||
|
temperature: number;
|
||||||
|
token_limit: number;
|
||||||
|
top_k: number;
|
||||||
|
top_p: number;
|
||||||
|
stream: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata of the model.
|
||||||
|
*/
|
||||||
|
export type ModelMetadata = {
|
||||||
|
engine: string;
|
||||||
|
quantization: string;
|
||||||
|
size: number;
|
||||||
|
binaries: string[];
|
||||||
|
maxRamRequired: number;
|
||||||
|
author: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model type of the presentation object which will be presented to the user
|
* Model type of the presentation object which will be presented to the user
|
||||||
* @data_transfer_object
|
* @data_transfer_object
|
||||||
@ -157,27 +305,37 @@ export interface ModelCatalog {
|
|||||||
releaseDate: number;
|
releaseDate: number;
|
||||||
/** The tags attached to the model description **/
|
/** The tags attached to the model description **/
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
|
||||||
/** The available versions of this model to download. */
|
/** The available versions of this model to download. */
|
||||||
availableVersions: ModelVersion[];
|
availableVersions: Model[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model type which will be present a version of ModelCatalog
|
* Assistant type defines the shape of an assistant object.
|
||||||
* @data_transfer_object
|
* @stored
|
||||||
*/
|
*/
|
||||||
export type ModelVersion = {
|
export type Assistant = {
|
||||||
/** The name of this model version.*/
|
/** Represents the avatar of the user. */
|
||||||
|
avatar: string;
|
||||||
|
/** Represents the location of the thread. */
|
||||||
|
thread_location: string | undefined;
|
||||||
|
/** Represents the unique identifier of the object. */
|
||||||
|
id: string;
|
||||||
|
/** Represents the object. */
|
||||||
|
object: string;
|
||||||
|
/** Represents the creation timestamp of the object. */
|
||||||
|
created_at: number;
|
||||||
|
/** Represents the name of the object. */
|
||||||
name: string;
|
name: string;
|
||||||
/** The quantization method name.*/
|
/** Represents the description of the object. */
|
||||||
quantizationName: string;
|
description: string;
|
||||||
/** The the number of bits represents a number.*/
|
/** Represents the model of the object. */
|
||||||
bits: number;
|
model: string;
|
||||||
/** The size of the model file in bytes.*/
|
/** Represents the instructions for the object. */
|
||||||
size: number;
|
instructions: string;
|
||||||
/** The maximum RAM required to run the model in bytes.*/
|
/** Represents the tools associated with the object. */
|
||||||
maxRamRequired: number;
|
tools: any;
|
||||||
/** The use case of the model.*/
|
/** Represents the file identifiers associated with the object. */
|
||||||
usecase: string;
|
file_ids: string[];
|
||||||
/** The download link of the model.*/
|
/** Represents the metadata of the object. */
|
||||||
downloadLink: string;
|
metadata?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { app, ipcMain } from 'electron'
|
import { app, ipcMain } from 'electron'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import readline from 'readline'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles file system operations.
|
* Handles file system operations.
|
||||||
@ -97,7 +98,7 @@ export function handleFsIPCs() {
|
|||||||
*/
|
*/
|
||||||
ipcMain.handle('rmdir', async (event, path: string): Promise<void> => {
|
ipcMain.handle('rmdir', async (event, path: string): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.rmdir(join(userSpacePath, path), { recursive: true }, (err) => {
|
fs.rm(join(userSpacePath, path), { recursive: true }, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
@ -153,4 +154,45 @@ export function handleFsIPCs() {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends data to a file in the user data directory.
|
||||||
|
* @param event - The event object.
|
||||||
|
* @param path - The path of the file to append to.
|
||||||
|
* @param data - The data to append to the file.
|
||||||
|
* @returns A promise that resolves when the file has been written.
|
||||||
|
*/
|
||||||
|
ipcMain.handle('appendFile', async (_event, path: string, data: string) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.appendFile(join(userSpacePath, path), data, 'utf8', (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('readLineByLine', async (_event, path: string) => {
|
||||||
|
const fullPath = join(userSpacePath, path)
|
||||||
|
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
try {
|
||||||
|
const readInterface = readline.createInterface({
|
||||||
|
input: fs.createReadStream(fullPath),
|
||||||
|
})
|
||||||
|
const lines: any = []
|
||||||
|
readInterface
|
||||||
|
.on('line', function (line) {
|
||||||
|
lines.push(line)
|
||||||
|
})
|
||||||
|
.on('close', function () {
|
||||||
|
res(lines)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
rej(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@
|
|||||||
* @property {Function} readFile - Reads the file at the given path.
|
* @property {Function} readFile - Reads the file at the given path.
|
||||||
* @property {Function} writeFile - Writes the given data to the file at the given path.
|
* @property {Function} writeFile - Writes the given data to the file at the given path.
|
||||||
* @property {Function} listFiles - Lists the files in the directory at the given path.
|
* @property {Function} listFiles - Lists the files in the directory at the given path.
|
||||||
|
* @property {Function} appendFile - Appends the given data to the file at the given path.
|
||||||
* @property {Function} mkdir - Creates a directory at the given path.
|
* @property {Function} mkdir - Creates a directory at the given path.
|
||||||
* @property {Function} rmdir - Removes a directory at the given path recursively.
|
* @property {Function} rmdir - Removes a directory at the given path recursively.
|
||||||
* @property {Function} installRemotePlugin - Installs the remote plugin with the given name.
|
* @property {Function} installRemotePlugin - Installs the remote plugin with the given name.
|
||||||
@ -58,7 +59,7 @@ import { useFacade } from './core/plugin/facade'
|
|||||||
|
|
||||||
useFacade()
|
useFacade()
|
||||||
|
|
||||||
const { contextBridge, ipcRenderer } = require('electron')
|
const { contextBridge, ipcRenderer, shell } = require('electron')
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
invokePluginFunc: (plugin: any, method: any, ...args: any[]) =>
|
invokePluginFunc: (plugin: any, method: any, ...args: any[]) =>
|
||||||
@ -88,7 +89,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
|
|
||||||
deleteFile: (filePath: string) => ipcRenderer.invoke('deleteFile', filePath),
|
deleteFile: (filePath: string) => ipcRenderer.invoke('deleteFile', filePath),
|
||||||
|
|
||||||
isDirectory: (filePath: string) => ipcRenderer.invoke('isDirectory', filePath),
|
isDirectory: (filePath: string) =>
|
||||||
|
ipcRenderer.invoke('isDirectory', filePath),
|
||||||
|
|
||||||
getUserSpace: () => ipcRenderer.invoke('getUserSpace'),
|
getUserSpace: () => ipcRenderer.invoke('getUserSpace'),
|
||||||
|
|
||||||
@ -99,10 +101,17 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
|
|
||||||
listFiles: (path: string) => ipcRenderer.invoke('listFiles', path),
|
listFiles: (path: string) => ipcRenderer.invoke('listFiles', path),
|
||||||
|
|
||||||
|
appendFile: (path: string, data: string) =>
|
||||||
|
ipcRenderer.invoke('appendFile', path, data),
|
||||||
|
|
||||||
|
readLineByLine: (path: string) => ipcRenderer.invoke('readLineByLine', path),
|
||||||
|
|
||||||
mkdir: (path: string) => ipcRenderer.invoke('mkdir', path),
|
mkdir: (path: string) => ipcRenderer.invoke('mkdir', path),
|
||||||
|
|
||||||
rmdir: (path: string) => ipcRenderer.invoke('rmdir', path),
|
rmdir: (path: string) => ipcRenderer.invoke('rmdir', path),
|
||||||
|
|
||||||
|
openFileExplorer: (path: string) => shell.openPath(path),
|
||||||
|
|
||||||
installRemotePlugin: (pluginName: string) =>
|
installRemotePlugin: (pluginName: string) =>
|
||||||
ipcRenderer.invoke('installRemotePlugin', pluginName),
|
ipcRenderer.invoke('installRemotePlugin', pluginName),
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||||
"build:electron": "yarn workspace jan build",
|
"build:electron": "yarn workspace jan build",
|
||||||
"build:electron:test": "yarn workspace jan build:test",
|
"build:electron:test": "yarn workspace jan build:test",
|
||||||
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\" \"cd ./plugins/assistant-plugin && npm install && npm run build:publish\"",
|
||||||
"build:test": "yarn build:web && yarn workspace jan build:test",
|
"build:test": "yarn build:web && yarn workspace jan build:test",
|
||||||
"build": "yarn build:web && yarn workspace jan build",
|
"build": "yarn build:web && yarn workspace jan build",
|
||||||
"build:publish": "yarn build:web && yarn workspace jan build:publish"
|
"build:publish": "yarn build:web && yarn workspace jan build:publish"
|
||||||
|
|||||||
77
plugins/assistant-plugin/README.md
Normal file
77
plugins/assistant-plugin/README.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# Jan Assistant plugin
|
||||||
|
|
||||||
|
Created using Jan app example
|
||||||
|
|
||||||
|
# Create a Jan Plugin using Typescript
|
||||||
|
|
||||||
|
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
|
||||||
|
|
||||||
|
## Create Your Own Plugin
|
||||||
|
|
||||||
|
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
|
||||||
|
|
||||||
|
1. Click the Use this template button at the top of the repository
|
||||||
|
2. Select Create a new repository
|
||||||
|
3. Select an owner and name for your new repository
|
||||||
|
4. Click Create repository
|
||||||
|
5. Clone your new repository
|
||||||
|
|
||||||
|
## Initial Setup
|
||||||
|
|
||||||
|
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
>
|
||||||
|
> You'll need to have a reasonably modern version of
|
||||||
|
> [Node.js](https://nodejs.org) handy. If you are using a version manager like
|
||||||
|
> [`nodenv`](https://github.com/nodenv/nodenv) or
|
||||||
|
> [`nvm`](https://github.com/nvm-sh/nvm), you can run `nodenv install` in the
|
||||||
|
> root of your repository to install the version specified in
|
||||||
|
> [`package.json`](./package.json). Otherwise, 20.x or later should work!
|
||||||
|
|
||||||
|
1. :hammer_and_wrench: Install the dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
1. :building_construction: Package the TypeScript for distribution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
1. :white_check_mark: Check your artifact
|
||||||
|
|
||||||
|
There will be a tgz file in your plugin directory now
|
||||||
|
|
||||||
|
## Update the Plugin Metadata
|
||||||
|
|
||||||
|
The [`package.json`](package.json) file defines metadata about your plugin, such as
|
||||||
|
plugin name, main entry, description and version.
|
||||||
|
|
||||||
|
When you copy this repository, update `package.json` with the name, description for your plugin.
|
||||||
|
|
||||||
|
## Update the Plugin Code
|
||||||
|
|
||||||
|
The [`src/`](./src/) directory is the heart of your plugin! This contains the
|
||||||
|
source code that will be run when your plugin extension functions are invoked. You can replace the
|
||||||
|
contents of this directory with your own code.
|
||||||
|
|
||||||
|
There are a few things to keep in mind when writing your plugin code:
|
||||||
|
|
||||||
|
- Most Jan Plugin Extension functions are processed asynchronously.
|
||||||
|
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { core } from "@janhq/core";
|
||||||
|
|
||||||
|
function onStart(): Promise<any> {
|
||||||
|
return core.invokePluginFunc(MODULE_PATH, "run", 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information about the Jan Plugin Core module, see the
|
||||||
|
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
|
||||||
|
|
||||||
|
So, what are you waiting for? Go ahead and start customizing your plugin!
|
||||||
33
plugins/assistant-plugin/package.json
Normal file
33
plugins/assistant-plugin/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@janhq/assistant-plugin",
|
||||||
|
"version": "1.0.9",
|
||||||
|
"description": "Assistant",
|
||||||
|
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/cpu-chip.svg",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/module.js",
|
||||||
|
"author": "Jan <service@jan.ai>",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"url": "/plugins/assistant-plugin/index.js",
|
||||||
|
"activationPoints": [
|
||||||
|
"init"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||||
|
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"webpack": "^5.88.2",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@janhq/core": "file:../../core",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"ts-loader": "^9.5.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*",
|
||||||
|
"package.json",
|
||||||
|
"README.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
plugins/assistant-plugin/src/@types/global.d.ts
vendored
Normal file
1
plugins/assistant-plugin/src/@types/global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare const MODULE: string;
|
||||||
107
plugins/assistant-plugin/src/index.ts
Normal file
107
plugins/assistant-plugin/src/index.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { PluginType, fs, Assistant } from "@janhq/core";
|
||||||
|
import { AssistantPlugin } from "@janhq/core/lib/plugins";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
export default class JanAssistantPlugin implements AssistantPlugin {
|
||||||
|
private static readonly _homeDir = "assistants";
|
||||||
|
|
||||||
|
type(): PluginType {
|
||||||
|
return PluginType.Assistant;
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(): void {
|
||||||
|
// making the assistant directory
|
||||||
|
fs.mkdir(JanAssistantPlugin._homeDir).then(() => {
|
||||||
|
this.createJanAssistant();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the plugin is unloaded.
|
||||||
|
*/
|
||||||
|
onUnload(): void {}
|
||||||
|
|
||||||
|
async createAssistant(assistant: Assistant): Promise<void> {
|
||||||
|
// assuming that assistants/ directory is already created in the onLoad above
|
||||||
|
|
||||||
|
// TODO: check if the directory already exists, then ignore creation for now
|
||||||
|
|
||||||
|
const assistantDir = join(JanAssistantPlugin._homeDir, assistant.id);
|
||||||
|
await fs.mkdir(assistantDir);
|
||||||
|
|
||||||
|
// store the assistant metadata json
|
||||||
|
const assistantMetadataPath = join(assistantDir, "assistant.json");
|
||||||
|
try {
|
||||||
|
await fs.writeFile(
|
||||||
|
assistantMetadataPath,
|
||||||
|
JSON.stringify(assistant, null, 2)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAssistants(): Promise<Assistant[]> {
|
||||||
|
// get all the assistant directories
|
||||||
|
// get all the assistant metadata json
|
||||||
|
const results: Assistant[] = [];
|
||||||
|
const allFileName: string[] = await fs.listFiles(
|
||||||
|
JanAssistantPlugin._homeDir
|
||||||
|
);
|
||||||
|
for (const fileName of allFileName) {
|
||||||
|
const filePath = join(JanAssistantPlugin._homeDir, fileName);
|
||||||
|
const isDirectory = await fs.isDirectory(filePath);
|
||||||
|
if (!isDirectory) {
|
||||||
|
// if not a directory, ignore
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonFiles: string[] = (await fs.listFiles(filePath)).filter(
|
||||||
|
(file: string) => file === "assistant.json"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (jsonFiles.length !== 1) {
|
||||||
|
// has more than one assistant file -> ignore
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assistant: Assistant = JSON.parse(
|
||||||
|
await fs.readFile(join(filePath, jsonFiles[0]))
|
||||||
|
);
|
||||||
|
|
||||||
|
results.push(assistant);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAssistant(assistant: Assistant): Promise<void> {
|
||||||
|
if (assistant.id === "jan") {
|
||||||
|
return Promise.reject("Cannot delete Jan Assistant");
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the directory
|
||||||
|
const assistantDir = join(JanAssistantPlugin._homeDir, assistant.id);
|
||||||
|
await fs.rmdir(assistantDir);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createJanAssistant(): Promise<void> {
|
||||||
|
const janAssistant: Assistant = {
|
||||||
|
avatar: "",
|
||||||
|
thread_location: undefined, // TODO: make this property ?
|
||||||
|
id: "jan",
|
||||||
|
object: "assistant", // TODO: maybe we can set default value for this?
|
||||||
|
created_at: Date.now(),
|
||||||
|
name: "Jan Assistant",
|
||||||
|
description: "Just Jan Assistant",
|
||||||
|
model: "*",
|
||||||
|
instructions: "Your name is Jan.",
|
||||||
|
tools: undefined,
|
||||||
|
file_ids: [],
|
||||||
|
metadata: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.createAssistant(janAssistant);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
plugins/assistant-plugin/tsconfig.json
Normal file
14
plugins/assistant-plugin/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2016",
|
||||||
|
"module": "ES6",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["./src"]
|
||||||
|
}
|
||||||
38
plugins/assistant-plugin/webpack.config.js
Normal file
38
plugins/assistant-plugin/webpack.config.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const packageJson = require("./package.json");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
experiments: { outputModule: true },
|
||||||
|
entry: "./src/index.ts", // Adjust the entry point to match your project's main file
|
||||||
|
mode: "production",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: "ts-loader",
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: "index.js", // Adjust the output file name as needed
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
library: { type: "module" }, // Specify ESM output format
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".js"],
|
||||||
|
fallback: {
|
||||||
|
path: require.resolve("path-browserify"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: false,
|
||||||
|
},
|
||||||
|
// Add loaders and other configuration as needed for your project
|
||||||
|
};
|
||||||
@ -1,14 +1,16 @@
|
|||||||
import { PluginType, fs } from '@janhq/core'
|
import { PluginType, fs } from '@janhq/core'
|
||||||
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { Thread } from '@janhq/core/lib/types'
|
import { Thread, ThreadMessage } from '@janhq/core/lib/types'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSONConversationalPlugin is a ConversationalPlugin implementation that provides
|
* JSONConversationalPlugin is a ConversationalPlugin implementation that provides
|
||||||
* functionality for managing conversations.
|
* functionality for managing threads.
|
||||||
*/
|
*/
|
||||||
export default class JSONConversationalPlugin implements ConversationalPlugin {
|
export default class JSONConversationalPlugin implements ConversationalPlugin {
|
||||||
private static readonly _homeDir = 'threads'
|
private static readonly _homeDir = 'threads'
|
||||||
|
private static readonly _threadInfoFileName = 'thread.json'
|
||||||
|
private static readonly _threadMessagesFileName = 'messages.jsonl'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the type of the plugin.
|
* Returns the type of the plugin.
|
||||||
@ -35,13 +37,11 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
/**
|
/**
|
||||||
* Returns a Promise that resolves to an array of Conversation objects.
|
* Returns a Promise that resolves to an array of Conversation objects.
|
||||||
*/
|
*/
|
||||||
async getConversations(): Promise<Thread[]> {
|
async getThreads(): Promise<Thread[]> {
|
||||||
try {
|
try {
|
||||||
const convoIds = await this.getConversationDocs()
|
const threadDirs = await this.getValidThreadDirs()
|
||||||
|
|
||||||
const promises = convoIds.map((conversationId) => {
|
const promises = threadDirs.map((dirName) => this.readThread(dirName))
|
||||||
return this.readConvo(conversationId)
|
|
||||||
})
|
|
||||||
const promiseResults = await Promise.allSettled(promises)
|
const promiseResults = await Promise.allSettled(promises)
|
||||||
const convos = promiseResults
|
const convos = promiseResults
|
||||||
.map((result) => {
|
.map((result) => {
|
||||||
@ -51,10 +51,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
})
|
})
|
||||||
.filter((convo) => convo != null)
|
.filter((convo) => convo != null)
|
||||||
convos.sort(
|
convos.sort(
|
||||||
(a, b) =>
|
(a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime()
|
||||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
||||||
)
|
)
|
||||||
console.debug('getConversations: ', JSON.stringify(convos, null, 2))
|
console.debug('getThreads', JSON.stringify(convos, null, 2))
|
||||||
return convos
|
return convos
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@ -63,55 +62,145 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves a Conversation object to a Markdown file.
|
* Saves a Thread object to a json file.
|
||||||
* @param conversation The Conversation object to save.
|
* @param thread The Thread object to save.
|
||||||
*/
|
*/
|
||||||
saveConversation(conversation: Thread): Promise<void> {
|
async saveThread(thread: Thread): Promise<void> {
|
||||||
return fs
|
try {
|
||||||
.mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`)
|
const threadDirPath = join(JSONConversationalPlugin._homeDir, thread.id)
|
||||||
.then(() =>
|
const threadJsonPath = join(
|
||||||
fs.writeFile(
|
threadDirPath,
|
||||||
|
JSONConversationalPlugin._threadInfoFileName
|
||||||
|
)
|
||||||
|
await fs.mkdir(threadDirPath)
|
||||||
|
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
|
||||||
|
Promise.resolve()
|
||||||
|
} catch (err) {
|
||||||
|
Promise.reject(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a thread with the specified ID.
|
||||||
|
* @param threadId The ID of the thread to delete.
|
||||||
|
*/
|
||||||
|
deleteThread(threadId: string): Promise<void> {
|
||||||
|
return fs.rmdir(join(JSONConversationalPlugin._homeDir, `${threadId}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
async addNewMessage(message: ThreadMessage): Promise<void> {
|
||||||
|
try {
|
||||||
|
const threadDirPath = join(
|
||||||
|
JSONConversationalPlugin._homeDir,
|
||||||
|
message.thread_id
|
||||||
|
)
|
||||||
|
const threadMessagePath = join(
|
||||||
|
threadDirPath,
|
||||||
|
JSONConversationalPlugin._threadMessagesFileName
|
||||||
|
)
|
||||||
|
await fs.mkdir(threadDirPath)
|
||||||
|
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
|
||||||
|
Promise.resolve()
|
||||||
|
} catch (err) {
|
||||||
|
Promise.reject(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeMessages(
|
||||||
|
threadId: string,
|
||||||
|
messages: ThreadMessage[]
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const threadDirPath = join(JSONConversationalPlugin._homeDir, threadId)
|
||||||
|
const threadMessagePath = join(
|
||||||
|
threadDirPath,
|
||||||
|
JSONConversationalPlugin._threadMessagesFileName
|
||||||
|
)
|
||||||
|
await fs.mkdir(threadDirPath)
|
||||||
|
await fs.writeFile(
|
||||||
|
threadMessagePath,
|
||||||
|
messages.map((msg) => JSON.stringify(msg)).join('\n')
|
||||||
|
)
|
||||||
|
Promise.resolve()
|
||||||
|
} catch (err) {
|
||||||
|
Promise.reject(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A promise builder for reading a thread from a file.
|
||||||
|
* @param threadDirName the thread dir we are reading from.
|
||||||
|
* @returns data of the thread
|
||||||
|
*/
|
||||||
|
private async readThread(threadDirName: string): Promise<any> {
|
||||||
|
return fs.readFile(
|
||||||
join(
|
join(
|
||||||
JSONConversationalPlugin._homeDir,
|
JSONConversationalPlugin._homeDir,
|
||||||
conversation.id,
|
threadDirName,
|
||||||
`${conversation.id}.json`
|
JSONConversationalPlugin._threadInfoFileName
|
||||||
),
|
|
||||||
JSON.stringify(conversation)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a conversation with the specified ID.
|
* Returns a Promise that resolves to an array of thread directories.
|
||||||
* @param conversationId The ID of the conversation to delete.
|
|
||||||
*/
|
|
||||||
deleteConversation(conversationId: string): Promise<void> {
|
|
||||||
return fs.rmdir(
|
|
||||||
join(JSONConversationalPlugin._homeDir, `${conversationId}`)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A promise builder for reading a conversation from a file.
|
|
||||||
* @param convoId the conversation id we are reading from.
|
|
||||||
* @returns data of the conversation
|
|
||||||
*/
|
|
||||||
private async readConvo(convoId: string): Promise<any> {
|
|
||||||
return fs.readFile(
|
|
||||||
join(JSONConversationalPlugin._homeDir, convoId, `${convoId}.json`)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a Promise that resolves to an array of conversation IDs.
|
|
||||||
* The conversation IDs are the names of the Markdown files in the "conversations" directory.
|
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async getConversationDocs(): Promise<string[]> {
|
private async getValidThreadDirs(): Promise<string[]> {
|
||||||
return fs
|
const fileInsideThread: string[] = await fs.listFiles(
|
||||||
.listFiles(JSONConversationalPlugin._homeDir)
|
JSONConversationalPlugin._homeDir
|
||||||
.then((files: string[]) => {
|
)
|
||||||
return Promise.all(files.filter((file) => file.startsWith('jan-')))
|
|
||||||
|
const threadDirs: string[] = []
|
||||||
|
for (let i = 0; i < fileInsideThread.length; i++) {
|
||||||
|
const path = join(JSONConversationalPlugin._homeDir, fileInsideThread[i])
|
||||||
|
const isDirectory = await fs.isDirectory(path)
|
||||||
|
if (!isDirectory) {
|
||||||
|
console.debug(`Ignore ${path} because it is not a directory`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHavingThreadInfo = (await fs.listFiles(path)).includes(
|
||||||
|
JSONConversationalPlugin._threadInfoFileName
|
||||||
|
)
|
||||||
|
if (!isHavingThreadInfo) {
|
||||||
|
console.debug(`Ignore ${path} because it does not have thread info`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
threadDirs.push(fileInsideThread[i])
|
||||||
|
}
|
||||||
|
return threadDirs
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllMessages(threadId: string): Promise<ThreadMessage[]> {
|
||||||
|
try {
|
||||||
|
const threadDirPath = join(JSONConversationalPlugin._homeDir, threadId)
|
||||||
|
const isDir = await fs.isDirectory(threadDirPath)
|
||||||
|
if (!isDir) {
|
||||||
|
throw Error(`${threadDirPath} is not directory`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: string[] = await fs.listFiles(threadDirPath)
|
||||||
|
if (!files.includes(JSONConversationalPlugin._threadMessagesFileName)) {
|
||||||
|
throw Error(`${threadDirPath} not contains message file`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageFilePath = join(
|
||||||
|
threadDirPath,
|
||||||
|
JSONConversationalPlugin._threadMessagesFileName
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await fs.readLineByLine(messageFilePath)
|
||||||
|
|
||||||
|
const messages: ThreadMessage[] = []
|
||||||
|
result.forEach((line: string) => {
|
||||||
|
messages.push(JSON.parse(line) as ThreadMessage)
|
||||||
})
|
})
|
||||||
|
return messages
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,10 @@ import { Observable } from "rxjs";
|
|||||||
* @param recentMessages - An array of recent messages to use as context for the inference.
|
* @param recentMessages - An array of recent messages to use as context for the inference.
|
||||||
* @returns An Observable that emits the generated response as a string.
|
* @returns An Observable that emits the generated response as a string.
|
||||||
*/
|
*/
|
||||||
export function requestInference(recentMessages: any[], controller?: AbortController): Observable<string> {
|
export function requestInference(
|
||||||
|
recentMessages: any[],
|
||||||
|
controller?: AbortController
|
||||||
|
): Observable<string> {
|
||||||
return new Observable((subscriber) => {
|
return new Observable((subscriber) => {
|
||||||
const requestBody = JSON.stringify({
|
const requestBody = JSON.stringify({
|
||||||
messages: recentMessages,
|
messages: recentMessages,
|
||||||
@ -20,7 +23,7 @@ export function requestInference(recentMessages: any[], controller?: AbortContro
|
|||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
},
|
},
|
||||||
body: requestBody,
|
body: requestBody,
|
||||||
signal: controller?.signal
|
signal: controller?.signal,
|
||||||
})
|
})
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const stream = response.body;
|
const stream = response.body;
|
||||||
|
|||||||
@ -8,19 +8,22 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ChatCompletionRole,
|
ChatCompletionRole,
|
||||||
|
ContentType,
|
||||||
EventName,
|
EventName,
|
||||||
MessageRequest,
|
MessageRequest,
|
||||||
MessageStatus,
|
MessageStatus,
|
||||||
|
ModelSettingParams,
|
||||||
PluginType,
|
PluginType,
|
||||||
|
ThreadContent,
|
||||||
ThreadMessage,
|
ThreadMessage,
|
||||||
events,
|
events,
|
||||||
executeOnMain,
|
executeOnMain,
|
||||||
|
getUserSpace,
|
||||||
} from "@janhq/core";
|
} from "@janhq/core";
|
||||||
import { InferencePlugin } from "@janhq/core/lib/plugins";
|
import { InferencePlugin } from "@janhq/core/lib/plugins";
|
||||||
import { requestInference } from "./helpers/sse";
|
import { requestInference } from "./helpers/sse";
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { getUserSpace } from "@janhq/core";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class that implements the InferencePlugin interface from the @janhq/core package.
|
* A class that implements the InferencePlugin interface from the @janhq/core package.
|
||||||
@ -56,14 +59,20 @@ export default class JanInferencePlugin implements InferencePlugin {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the model with the specified file name.
|
* Initializes the model with the specified file name.
|
||||||
* @param {string} modelFileName - The file name of the model file.
|
* @param {string} modelId - The ID of the model to initialize.
|
||||||
* @returns {Promise<void>} A promise that resolves when the model is initialized.
|
* @returns {Promise<void>} A promise that resolves when the model is initialized.
|
||||||
*/
|
*/
|
||||||
async initModel(modelFileName: string): Promise<void> {
|
async initModel(
|
||||||
|
modelId: string,
|
||||||
|
settings?: ModelSettingParams
|
||||||
|
): Promise<void> {
|
||||||
const userSpacePath = await getUserSpace();
|
const userSpacePath = await getUserSpace();
|
||||||
const modelFullPath = join(userSpacePath, modelFileName);
|
const modelFullPath = join(userSpacePath, "models", modelId, modelId);
|
||||||
|
|
||||||
return executeOnMain(MODULE, "initModel", modelFullPath);
|
return executeOnMain(MODULE, "initModel", {
|
||||||
|
modelFullPath,
|
||||||
|
settings,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,18 +98,21 @@ export default class JanInferencePlugin implements InferencePlugin {
|
|||||||
* @returns {Promise<any>} A promise that resolves with the inference response.
|
* @returns {Promise<any>} A promise that resolves with the inference response.
|
||||||
*/
|
*/
|
||||||
async inferenceRequest(data: MessageRequest): Promise<ThreadMessage> {
|
async inferenceRequest(data: MessageRequest): Promise<ThreadMessage> {
|
||||||
|
const timestamp = Date.now();
|
||||||
const message: ThreadMessage = {
|
const message: ThreadMessage = {
|
||||||
threadId: data.threadId,
|
thread_id: data.threadId,
|
||||||
content: "",
|
created: timestamp,
|
||||||
createdAt: new Date().toISOString(),
|
updated: timestamp,
|
||||||
status: MessageStatus.Ready,
|
status: MessageStatus.Ready,
|
||||||
|
id: "",
|
||||||
|
role: ChatCompletionRole.Assistant,
|
||||||
|
object: "thread.message",
|
||||||
|
content: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
requestInference(data.messages ?? []).subscribe({
|
requestInference(data.messages ?? []).subscribe({
|
||||||
next: (content) => {
|
next: (_content) => {},
|
||||||
message.content = content;
|
|
||||||
},
|
|
||||||
complete: async () => {
|
complete: async () => {
|
||||||
resolve(message);
|
resolve(message);
|
||||||
},
|
},
|
||||||
@ -121,33 +133,49 @@ export default class JanInferencePlugin implements InferencePlugin {
|
|||||||
data: MessageRequest,
|
data: MessageRequest,
|
||||||
instance: JanInferencePlugin
|
instance: JanInferencePlugin
|
||||||
) {
|
) {
|
||||||
|
const timestamp = Date.now();
|
||||||
const message: ThreadMessage = {
|
const message: ThreadMessage = {
|
||||||
threadId: data.threadId,
|
|
||||||
content: "",
|
|
||||||
role: ChatCompletionRole.Assistant,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
id: ulid(),
|
id: ulid(),
|
||||||
|
thread_id: data.threadId,
|
||||||
|
assistant_id: data.assistantId,
|
||||||
|
role: ChatCompletionRole.Assistant,
|
||||||
|
content: [],
|
||||||
status: MessageStatus.Pending,
|
status: MessageStatus.Pending,
|
||||||
|
created: timestamp,
|
||||||
|
updated: timestamp,
|
||||||
|
object: "thread.message",
|
||||||
};
|
};
|
||||||
events.emit(EventName.OnNewMessageResponse, message);
|
events.emit(EventName.OnNewMessageResponse, message);
|
||||||
|
console.log(JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
instance.isCancelled = false;
|
instance.isCancelled = false;
|
||||||
instance.controller = new AbortController();
|
instance.controller = new AbortController();
|
||||||
|
|
||||||
requestInference(data.messages, instance.controller).subscribe({
|
requestInference(data.messages, instance.controller).subscribe({
|
||||||
next: (content) => {
|
next: (content) => {
|
||||||
message.content = content;
|
const messageContent: ThreadContent = {
|
||||||
|
type: ContentType.Text,
|
||||||
|
text: {
|
||||||
|
value: content.trim(),
|
||||||
|
annotations: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
message.content = [messageContent];
|
||||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||||
},
|
},
|
||||||
complete: async () => {
|
complete: async () => {
|
||||||
message.content = message.content.trim();
|
|
||||||
message.status = MessageStatus.Ready;
|
message.status = MessageStatus.Ready;
|
||||||
events.emit(EventName.OnMessageResponseFinished, message);
|
events.emit(EventName.OnMessageResponseFinished, message);
|
||||||
},
|
},
|
||||||
error: async (err) => {
|
error: async (err) => {
|
||||||
message.content =
|
const messageContent: ThreadContent = {
|
||||||
message.content.trim() +
|
type: ContentType.Text,
|
||||||
(instance.isCancelled ? "" : "\n" + "Error occurred: " + err.message);
|
text: {
|
||||||
|
value: "Error occurred: " + err.message,
|
||||||
|
annotations: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
message.content = [messageContent];
|
||||||
message.status = MessageStatus.Ready;
|
message.status = MessageStatus.Ready;
|
||||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -35,10 +35,20 @@ interface InitModelResponse {
|
|||||||
* TODO: Should pass absolute of the model file instead of just the name - So we can modurize the module.ts to npm package
|
* TODO: Should pass absolute of the model file instead of just the name - So we can modurize the module.ts to npm package
|
||||||
* TODO: Should it be startModel instead?
|
* TODO: Should it be startModel instead?
|
||||||
*/
|
*/
|
||||||
function initModel(modelFile: string): Promise<InitModelResponse> {
|
function initModel(wrapper: any): Promise<InitModelResponse> {
|
||||||
// 1. Check if the model file exists
|
// 1. Check if the model file exists
|
||||||
currentModelFile = modelFile;
|
currentModelFile = wrapper.modelFullPath;
|
||||||
log.info("Started to load model " + modelFile);
|
log.info("Started to load model " + wrapper.modelFullPath);
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
llama_model_path: currentModelFile,
|
||||||
|
ctx_len: 2048,
|
||||||
|
ngl: 100,
|
||||||
|
cont_batching: false,
|
||||||
|
embedding: false, // Always enable embedding mode on
|
||||||
|
...wrapper.settings,
|
||||||
|
};
|
||||||
|
log.info(`Load model settings: ${JSON.stringify(settings, null, 2)}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// 1. Check if the port is used, if used, attempt to unload model / kill nitro process
|
// 1. Check if the port is used, if used, attempt to unload model / kill nitro process
|
||||||
@ -47,12 +57,12 @@ function initModel(modelFile: string): Promise<InitModelResponse> {
|
|||||||
// 2. Spawn the Nitro subprocess
|
// 2. Spawn the Nitro subprocess
|
||||||
.then(spawnNitroProcess)
|
.then(spawnNitroProcess)
|
||||||
// 4. Load the model into the Nitro subprocess (HTTP POST request)
|
// 4. Load the model into the Nitro subprocess (HTTP POST request)
|
||||||
.then(loadLLMModel)
|
.then(() => loadLLMModel(settings))
|
||||||
// 5. Check if the model is loaded successfully
|
// 5. Check if the model is loaded successfully
|
||||||
.then(validateModelStatus)
|
.then(validateModelStatus)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
log.error("error: " + JSON.stringify(err));
|
log.error("error: " + JSON.stringify(err));
|
||||||
return { error: err, modelFile };
|
return { error: err, currentModelFile };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -61,22 +71,14 @@ function initModel(modelFile: string): Promise<InitModelResponse> {
|
|||||||
* Loads a LLM model into the Nitro subprocess by sending a HTTP POST request.
|
* Loads a LLM model into the Nitro subprocess by sending a HTTP POST request.
|
||||||
* @returns A Promise that resolves when the model is loaded successfully, or rejects with an error message if the model is not found or fails to load.
|
* @returns A Promise that resolves when the model is loaded successfully, or rejects with an error message if the model is not found or fails to load.
|
||||||
*/
|
*/
|
||||||
function loadLLMModel(): Promise<Response> {
|
function loadLLMModel(settings): Promise<Response> {
|
||||||
const config = {
|
|
||||||
llama_model_path: currentModelFile,
|
|
||||||
ctx_len: 2048,
|
|
||||||
ngl: 100,
|
|
||||||
cont_batching: false,
|
|
||||||
embedding: false, // Always enable embedding mode on
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load model config
|
// Load model config
|
||||||
return fetchRetry(NITRO_HTTP_LOAD_MODEL_URL, {
|
return fetchRetry(NITRO_HTTP_LOAD_MODEL_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(config),
|
body: JSON.stringify(settings),
|
||||||
retries: 3,
|
retries: 3,
|
||||||
retryDelay: 500,
|
retryDelay: 500,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
@ -151,7 +153,7 @@ function checkAndUnloadNitro() {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.log(err);
|
console.error(err);
|
||||||
// Fallback to kill the port
|
// Fallback to kill the port
|
||||||
return killSubprocess();
|
return killSubprocess();
|
||||||
});
|
});
|
||||||
@ -195,7 +197,7 @@ async function spawnNitroProcess(): Promise<void> {
|
|||||||
|
|
||||||
// Handle subprocess output
|
// Handle subprocess output
|
||||||
subprocess.stdout.on("data", (data) => {
|
subprocess.stdout.on("data", (data) => {
|
||||||
console.log(`stdout: ${data}`);
|
console.debug(`stdout: ${data}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
subprocess.stderr.on("data", (data) => {
|
subprocess.stderr.on("data", (data) => {
|
||||||
@ -204,7 +206,7 @@ async function spawnNitroProcess(): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
subprocess.on("close", (code) => {
|
subprocess.on("close", (code) => {
|
||||||
console.log(`child process exited with code ${code}`);
|
console.debug(`child process exited with code ${code}`);
|
||||||
subprocess = null;
|
subprocess = null;
|
||||||
reject(`Nitro process exited. ${code ?? ""}`);
|
reject(`Nitro process exited. ${code ?? ""}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,32 +1,46 @@
|
|||||||
import { ModelCatalog } from '@janhq/core'
|
import { ModelCatalog } from '@janhq/core'
|
||||||
|
|
||||||
export function parseToModel(schema: ModelSchema): ModelCatalog {
|
export const parseToModel = (modelGroup): ModelCatalog => {
|
||||||
const modelVersions = []
|
const modelVersions = []
|
||||||
schema.versions.forEach((v) => {
|
modelGroup.versions.forEach((v) => {
|
||||||
const version = {
|
const model = {
|
||||||
|
object: 'model',
|
||||||
|
version: modelGroup.version,
|
||||||
|
source_url: v.downloadLink,
|
||||||
|
id: v.name,
|
||||||
name: v.name,
|
name: v.name,
|
||||||
quantMethod: v.quantMethod,
|
owned_by: 'you',
|
||||||
bits: v.bits,
|
created: 0,
|
||||||
|
description: modelGroup.longDescription,
|
||||||
|
state: 'to_download',
|
||||||
|
settings: v.settings,
|
||||||
|
parameters: v.parameters,
|
||||||
|
metadata: {
|
||||||
|
engine: '',
|
||||||
|
quantization: v.quantMethod,
|
||||||
size: v.size,
|
size: v.size,
|
||||||
|
binaries: [],
|
||||||
maxRamRequired: v.maxRamRequired,
|
maxRamRequired: v.maxRamRequired,
|
||||||
usecase: v.usecase,
|
author: modelGroup.author,
|
||||||
downloadLink: v.downloadLink,
|
avatarUrl: modelGroup.avatarUrl,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
modelVersions.push(version)
|
modelVersions.push(model)
|
||||||
})
|
})
|
||||||
|
|
||||||
const model: ModelCatalog = {
|
const modelCatalog: ModelCatalog = {
|
||||||
id: schema.id,
|
id: modelGroup.id,
|
||||||
name: schema.name,
|
name: modelGroup.name,
|
||||||
shortDescription: schema.shortDescription,
|
avatarUrl: modelGroup.avatarUrl,
|
||||||
avatarUrl: schema.avatarUrl,
|
shortDescription: modelGroup.shortDescription,
|
||||||
author: schema.author,
|
longDescription: modelGroup.longDescription,
|
||||||
version: schema.version,
|
author: modelGroup.author,
|
||||||
modelUrl: schema.modelUrl,
|
version: modelGroup.version,
|
||||||
tags: schema.tags,
|
modelUrl: modelGroup.modelUrl,
|
||||||
longDescription: schema.longDescription,
|
releaseDate: modelGroup.createdAt,
|
||||||
releaseDate: 0,
|
tags: modelGroup.tags,
|
||||||
availableVersions: modelVersions,
|
availableVersions: modelVersions,
|
||||||
}
|
}
|
||||||
return model
|
|
||||||
|
return modelCatalog
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import { join } from 'path'
|
|||||||
*/
|
*/
|
||||||
export default class JanModelPlugin implements ModelPlugin {
|
export default class JanModelPlugin implements ModelPlugin {
|
||||||
private static readonly _homeDir = 'models'
|
private static readonly _homeDir = 'models'
|
||||||
|
private static readonly _modelMetadataFileName = 'model.json'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements type from JanPlugin.
|
* Implements type from JanPlugin.
|
||||||
* @override
|
* @override
|
||||||
@ -42,12 +44,12 @@ export default class JanModelPlugin implements ModelPlugin {
|
|||||||
*/
|
*/
|
||||||
async downloadModel(model: Model): Promise<void> {
|
async downloadModel(model: Model): Promise<void> {
|
||||||
// create corresponding directory
|
// create corresponding directory
|
||||||
const directoryPath = join(JanModelPlugin._homeDir, model.name)
|
const directoryPath = join(JanModelPlugin._homeDir, model.id)
|
||||||
await fs.mkdir(directoryPath)
|
await fs.mkdir(directoryPath)
|
||||||
|
|
||||||
// path to model binary
|
// path to model binary
|
||||||
const path = join(directoryPath, model.id)
|
const path = join(directoryPath, model.id)
|
||||||
downloadFile(model.downloadLink, path)
|
downloadFile(model.source_url, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,10 +57,10 @@ export default class JanModelPlugin implements ModelPlugin {
|
|||||||
* @param {string} modelId - The ID of the model whose download is to be cancelled.
|
* @param {string} modelId - The ID of the model whose download is to be cancelled.
|
||||||
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
|
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
|
||||||
*/
|
*/
|
||||||
async cancelModelDownload(name: string, modelId: string): Promise<void> {
|
async cancelModelDownload(modelId: string): Promise<void> {
|
||||||
return abortDownload(join(JanModelPlugin._homeDir, name, modelId)).then(
|
return abortDownload(join(JanModelPlugin._homeDir, modelId, modelId)).then(
|
||||||
() => {
|
() => {
|
||||||
fs.deleteFile(join(JanModelPlugin._homeDir, name, modelId))
|
fs.rmdir(join(JanModelPlugin._homeDir, modelId))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -68,12 +70,10 @@ export default class JanModelPlugin implements ModelPlugin {
|
|||||||
* @param filePath - The path to the model file to delete.
|
* @param filePath - The path to the model file to delete.
|
||||||
* @returns A Promise that resolves when the model is deleted.
|
* @returns A Promise that resolves when the model is deleted.
|
||||||
*/
|
*/
|
||||||
async deleteModel(filePath: string): Promise<void> {
|
async deleteModel(modelId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await Promise.allSettled([
|
const dirPath = join(JanModelPlugin._homeDir, modelId)
|
||||||
fs.deleteFile(filePath),
|
await fs.rmdir(dirPath)
|
||||||
fs.deleteFile(`${filePath}.json`),
|
|
||||||
])
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -85,11 +85,14 @@ export default class JanModelPlugin implements ModelPlugin {
|
|||||||
* @returns A Promise that resolves when the model is saved.
|
* @returns A Promise that resolves when the model is saved.
|
||||||
*/
|
*/
|
||||||
async saveModel(model: Model): Promise<void> {
|
async saveModel(model: Model): Promise<void> {
|
||||||
const directoryPath = join(JanModelPlugin._homeDir, model.name)
|
const jsonFilePath = join(
|
||||||
const jsonFilePath = join(directoryPath, `${model.id}.json`)
|
JanModelPlugin._homeDir,
|
||||||
|
model.id,
|
||||||
|
JanModelPlugin._modelMetadataFileName
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.writeFile(jsonFilePath, JSON.stringify(model))
|
await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -111,7 +114,7 @@ export default class JanModelPlugin implements ModelPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const jsonFiles: string[] = (await fs.listFiles(modelDirPath)).filter(
|
const jsonFiles: string[] = (await fs.listFiles(modelDirPath)).filter(
|
||||||
(file: string) => file.endsWith('.json')
|
(fileName: string) => fileName === JanModelPlugin._modelMetadataFileName
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const json of jsonFiles) {
|
for (const json of jsonFiles) {
|
||||||
|
|||||||
87
web/containers/CardSidebar/index.tsx
Normal file
87
web/containers/CardSidebar/index.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { ReactNode, useState } from 'react'
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
|
||||||
|
import { Menu, Transition } from '@headlessui/react'
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
EllipsisVerticalIcon,
|
||||||
|
} from '@heroicons/react/20/solid'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
title: string
|
||||||
|
onRevealInFinderClick: (type: string) => void
|
||||||
|
onViewJsonClick: (type: string) => void
|
||||||
|
}
|
||||||
|
export default function CardSidebar({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
onRevealInFinderClick,
|
||||||
|
onViewJsonClick,
|
||||||
|
}: Props) {
|
||||||
|
const [show, setShow] = useState(true)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col">
|
||||||
|
<div className="flex items-center rounded-lg border border-border">
|
||||||
|
<button
|
||||||
|
onClick={() => setShow(!show)}
|
||||||
|
className="flex w-full flex-1 items-center py-2"
|
||||||
|
>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={`h-5 w-5 flex-none text-gray-400 ${
|
||||||
|
show && 'rotate-180'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="text-xs uppercase">{title}</span>
|
||||||
|
</button>
|
||||||
|
<Menu as="div" className="relative flex-none">
|
||||||
|
<Menu.Button className="-m-2.5 block p-2.5 text-gray-500 hover:text-gray-900">
|
||||||
|
<span className="sr-only">Open options</span>
|
||||||
|
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</Menu.Button>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="absolute right-0 z-10 mt-2 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
onClick={() => onRevealInFinderClick(title)}
|
||||||
|
className={twMerge(
|
||||||
|
active ? 'bg-gray-50' : '',
|
||||||
|
'block cursor-pointer px-3 py-1 text-xs leading-6 text-gray-900'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Reveal in finder
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
onClick={() => onViewJsonClick(title)}
|
||||||
|
className={twMerge(
|
||||||
|
active ? 'bg-gray-50' : '',
|
||||||
|
'block cursor-pointer px-3 py-1 text-xs leading-6 text-gray-900'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
View a JSON
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
{show && <div className="flex flex-col gap-2 p-2">{children}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
104
web/containers/DropdownListSidebar/index.tsx
Normal file
104
web/containers/DropdownListSidebar/index.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { Fragment, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { Listbox, Transition } from '@headlessui/react'
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'
|
||||||
|
|
||||||
|
import { Model } from '@janhq/core/lib/types'
|
||||||
|
import { atom, useSetAtom } from 'jotai'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
import { getDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
|
|
||||||
|
export const selectedModelAtom = atom<Model | undefined>(undefined)
|
||||||
|
|
||||||
|
export default function DropdownListSidebar() {
|
||||||
|
const [downloadedModels, setDownloadedModels] = useState<Model[]>([])
|
||||||
|
const [selected, setSelected] = useState<Model | undefined>()
|
||||||
|
const setSelectedModel = useSetAtom(selectedModelAtom)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getDownloadedModels().then((downloadedModels) => {
|
||||||
|
setDownloadedModels(downloadedModels)
|
||||||
|
|
||||||
|
if (downloadedModels.length > 0) {
|
||||||
|
setSelected(downloadedModels[0])
|
||||||
|
setSelectedModel(downloadedModels[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!selected) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Listbox
|
||||||
|
value={selected}
|
||||||
|
onChange={(model) => {
|
||||||
|
setSelected(model)
|
||||||
|
setSelectedModel(model)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div className="relative mt-2">
|
||||||
|
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||||
|
<span className="block truncate">{selected.name}</span>
|
||||||
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<ChevronUpDownIcon
|
||||||
|
className="h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||||
|
{downloadedModels.map((model) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={model.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
twMerge(
|
||||||
|
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
|
||||||
|
'relative cursor-default select-none py-2 pl-3 pr-9'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={model}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={twMerge(
|
||||||
|
selected ? 'font-semibold' : 'font-normal',
|
||||||
|
'block truncate'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{model.name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{selected ? (
|
||||||
|
<span
|
||||||
|
className={twMerge(
|
||||||
|
active ? 'text-white' : 'text-indigo-600',
|
||||||
|
'absolute inset-y-0 right-0 flex items-center pr-4'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
web/containers/ItemCardSidebar/index.tsx
Normal file
20
web/containers/ItemCardSidebar/index.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ItemCardSidebar({ description, title }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
value={description}
|
||||||
|
type="text"
|
||||||
|
className="block w-full rounded-md border-0 px-1 py-1.5 text-white shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
imageUrl: string
|
|
||||||
className?: string
|
|
||||||
alt?: string
|
|
||||||
width?: number
|
|
||||||
height?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const JanImage: React.FC<Props> = ({
|
|
||||||
imageUrl,
|
|
||||||
className = '',
|
|
||||||
alt = '',
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
}) => {
|
|
||||||
const [attempt, setAttempt] = React.useState(0)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
src={imageUrl}
|
|
||||||
alt={alt}
|
|
||||||
className={className}
|
|
||||||
key={attempt}
|
|
||||||
onError={() => setAttempt(attempt + 1)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default JanImage
|
|
||||||
@ -83,7 +83,7 @@ export default function DownloadingState() {
|
|||||||
if (!model) return
|
if (!model) return
|
||||||
pluginManager
|
pluginManager
|
||||||
.get<ModelPlugin>(PluginType.Model)
|
.get<ModelPlugin>(PluginType.Model)
|
||||||
?.cancelModelDownload(model.name, item.fileName)
|
?.cancelModelDownload(item.modelId)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
import {
|
import {
|
||||||
MessageCircleIcon,
|
MessageCircleIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
@ -27,8 +28,12 @@ import { MainViewState } from '@/constants/screens'
|
|||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||||
|
|
||||||
|
import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
|
||||||
|
|
||||||
export default function CommandSearch() {
|
export default function CommandSearch() {
|
||||||
const { setMainViewState } = useMainViewState()
|
const { setMainViewState } = useMainViewState()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const setShowRightSideBar = useSetAtom(showRightSideBarAtom)
|
||||||
|
|
||||||
const menus = [
|
const menus = [
|
||||||
{
|
{
|
||||||
@ -61,8 +66,6 @@ export default function CommandSearch() {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
||||||
@ -120,6 +123,13 @@ export default function CommandSearch() {
|
|||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandModal>
|
</CommandModal>
|
||||||
|
<Button
|
||||||
|
themes="outline"
|
||||||
|
className="unset-drag justify-start text-left text-xs font-normal text-muted-foreground focus:ring-0"
|
||||||
|
onClick={() => setShowRightSideBar((show) => !show)}
|
||||||
|
>
|
||||||
|
Toggle right
|
||||||
|
</Button>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useMemo } from 'react'
|
|||||||
|
|
||||||
import { PluginType } from '@janhq/core'
|
import { PluginType } from '@janhq/core'
|
||||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { ModelVersion } from '@janhq/core/lib/types'
|
import { Model } from '@janhq/core/lib/types'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
@ -25,7 +25,7 @@ import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
|||||||
import { pluginManager } from '@/plugin'
|
import { pluginManager } from '@/plugin'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
suitableModel: ModelVersion
|
suitableModel: Model
|
||||||
isFromList?: boolean
|
isFromList?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ export default function ModalCancelDownload({
|
|||||||
if (!model) return
|
if (!model) return
|
||||||
pluginManager
|
pluginManager
|
||||||
.get<ModelPlugin>(PluginType.Model)
|
.get<ModelPlugin>(PluginType.Model)
|
||||||
?.cancelModelDownload(model.name, downloadState.fileName)
|
?.cancelModelDownload(downloadState.modelId)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -16,12 +16,11 @@ import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
addNewMessageAtom,
|
addNewMessageAtom,
|
||||||
chatMessages,
|
|
||||||
updateMessageAtom,
|
updateMessageAtom,
|
||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import {
|
import {
|
||||||
updateConversationWaitingForResponseAtom,
|
updateConversationWaitingForResponseAtom,
|
||||||
userConversationsAtom,
|
threadsAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
import { pluginManager } from '@/plugin'
|
import { pluginManager } from '@/plugin'
|
||||||
@ -35,71 +34,60 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
||||||
const models = useAtomValue(downloadingModelsAtom)
|
const models = useAtomValue(downloadingModelsAtom)
|
||||||
const messages = useAtomValue(chatMessages)
|
const threads = useAtomValue(threadsAtom)
|
||||||
const conversations = useAtomValue(userConversationsAtom)
|
const threadsRef = useRef(threads)
|
||||||
const messagesRef = useRef(messages)
|
|
||||||
const convoRef = useRef(conversations)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesRef.current = messages
|
threadsRef.current = threads
|
||||||
convoRef.current = conversations
|
}, [threads])
|
||||||
}, [messages, conversations])
|
|
||||||
|
|
||||||
async function handleNewMessageResponse(message: ThreadMessage) {
|
async function handleNewMessageResponse(message: ThreadMessage) {
|
||||||
if (message.threadId) {
|
|
||||||
const convo = convoRef.current.find((e) => e.id == message.threadId)
|
|
||||||
if (!convo) return
|
|
||||||
addNewMessage(message)
|
addNewMessage(message)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
async function handleMessageResponseUpdate(messageResponse: ThreadMessage) {
|
async function handleMessageResponseUpdate(message: ThreadMessage) {
|
||||||
if (
|
|
||||||
messageResponse.threadId &&
|
|
||||||
messageResponse.id &&
|
|
||||||
messageResponse.content
|
|
||||||
) {
|
|
||||||
updateMessage(
|
updateMessage(
|
||||||
messageResponse.id,
|
message.id,
|
||||||
messageResponse.threadId,
|
message.thread_id,
|
||||||
messageResponse.content,
|
message.content,
|
||||||
MessageStatus.Pending
|
MessageStatus.Pending
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function handleMessageResponseFinished(messageResponse: ThreadMessage) {
|
async function handleMessageResponseFinished(message: ThreadMessage) {
|
||||||
if (!messageResponse.threadId || !convoRef.current) return
|
updateConvWaiting(message.thread_id, false)
|
||||||
updateConvWaiting(messageResponse.threadId, false)
|
|
||||||
|
|
||||||
if (
|
if (message.id && message.content) {
|
||||||
messageResponse.threadId &&
|
|
||||||
messageResponse.id &&
|
|
||||||
messageResponse.content
|
|
||||||
) {
|
|
||||||
updateMessage(
|
updateMessage(
|
||||||
messageResponse.id,
|
message.id,
|
||||||
messageResponse.threadId,
|
message.thread_id,
|
||||||
messageResponse.content,
|
message.content,
|
||||||
MessageStatus.Ready
|
MessageStatus.Ready
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const thread = threadsRef.current?.find((e) => e.id == message.thread_id)
|
||||||
const thread = convoRef.current.find(
|
|
||||||
(e) => e.id == messageResponse.threadId
|
|
||||||
)
|
|
||||||
if (thread) {
|
if (thread) {
|
||||||
|
const messageContent = message.content[0]?.text.value ?? ''
|
||||||
|
const metadata = {
|
||||||
|
...thread.metadata,
|
||||||
|
lastMessage: messageContent,
|
||||||
|
}
|
||||||
pluginManager
|
pluginManager
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
?.saveConversation({
|
?.saveThread({
|
||||||
...thread,
|
...thread,
|
||||||
id: thread.id ?? '',
|
metadata,
|
||||||
messages: messagesRef.current[thread.id] ?? [],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
pluginManager
|
||||||
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
|
?.addNewMessage(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDownloadUpdate(state: any) {
|
function handleDownloadUpdate(state: any) {
|
||||||
if (!state) return
|
if (!state) return
|
||||||
|
state.fileName = state.fileName.split('/').pop() ?? ''
|
||||||
setDownloadState(state)
|
setDownloadState(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
import { ChatCompletionRole, MessageStatus, ThreadMessage } from '@janhq/core'
|
import {
|
||||||
|
ChatCompletionRole,
|
||||||
|
MessageStatus,
|
||||||
|
ThreadContent,
|
||||||
|
ThreadMessage,
|
||||||
|
} from '@janhq/core'
|
||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
conversationStatesAtom,
|
getActiveThreadIdAtom,
|
||||||
currentConversationAtom,
|
|
||||||
getActiveConvoIdAtom,
|
|
||||||
updateThreadStateLastMessageAtom,
|
updateThreadStateLastMessageAtom,
|
||||||
} from './Conversation.atom'
|
} from './Conversation.atom'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all chat messages for all conversations
|
* Stores all chat messages for all threads
|
||||||
*/
|
*/
|
||||||
export const chatMessages = atom<Record<string, ThreadMessage[]>>({})
|
export const chatMessages = atom<Record<string, ThreadMessage[]>>({})
|
||||||
|
|
||||||
@ -17,33 +20,19 @@ export const chatMessages = atom<Record<string, ThreadMessage[]>>({})
|
|||||||
* Return the chat messages for the current active conversation
|
* Return the chat messages for the current active conversation
|
||||||
*/
|
*/
|
||||||
export const getCurrentChatMessagesAtom = atom<ThreadMessage[]>((get) => {
|
export const getCurrentChatMessagesAtom = atom<ThreadMessage[]>((get) => {
|
||||||
const activeConversationId = get(getActiveConvoIdAtom)
|
const activeThreadId = get(getActiveThreadIdAtom)
|
||||||
if (!activeConversationId) return []
|
if (!activeThreadId) return []
|
||||||
const messages = get(chatMessages)[activeConversationId]
|
const messages = get(chatMessages)[activeThreadId]
|
||||||
return messages ?? []
|
return messages ?? []
|
||||||
})
|
})
|
||||||
|
|
||||||
export const setCurrentChatMessagesAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, messages: ThreadMessage[]) => {
|
|
||||||
const currentConvoId = get(getActiveConvoIdAtom)
|
|
||||||
if (!currentConvoId) return
|
|
||||||
|
|
||||||
const newData: Record<string, ThreadMessage[]> = {
|
|
||||||
...get(chatMessages),
|
|
||||||
}
|
|
||||||
newData[currentConvoId] = messages
|
|
||||||
set(chatMessages, newData)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const setConvoMessagesAtom = atom(
|
export const setConvoMessagesAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, messages: ThreadMessage[], convoId: string) => {
|
(get, set, threadId: string, messages: ThreadMessage[]) => {
|
||||||
const newData: Record<string, ThreadMessage[]> = {
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
...get(chatMessages),
|
...get(chatMessages),
|
||||||
}
|
}
|
||||||
newData[convoId] = messages
|
newData[threadId] = messages
|
||||||
set(chatMessages, newData)
|
set(chatMessages, newData)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -54,7 +43,7 @@ export const setConvoMessagesAtom = atom(
|
|||||||
export const addOldMessagesAtom = atom(
|
export const addOldMessagesAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, newMessages: ThreadMessage[]) => {
|
(get, set, newMessages: ThreadMessage[]) => {
|
||||||
const currentConvoId = get(getActiveConvoIdAtom)
|
const currentConvoId = get(getActiveThreadIdAtom)
|
||||||
if (!currentConvoId) return
|
if (!currentConvoId) return
|
||||||
|
|
||||||
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
||||||
@ -71,19 +60,19 @@ export const addOldMessagesAtom = atom(
|
|||||||
export const addNewMessageAtom = atom(
|
export const addNewMessageAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, newMessage: ThreadMessage) => {
|
(get, set, newMessage: ThreadMessage) => {
|
||||||
const currentConvoId = get(getActiveConvoIdAtom)
|
const threadId = get(getActiveThreadIdAtom)
|
||||||
if (!currentConvoId) return
|
if (!threadId) return
|
||||||
|
|
||||||
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
const currentMessages = get(chatMessages)[threadId] ?? []
|
||||||
const updatedMessages = [newMessage, ...currentMessages]
|
const updatedMessages = [...currentMessages, newMessage]
|
||||||
|
|
||||||
const newData: Record<string, ThreadMessage[]> = {
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
...get(chatMessages),
|
...get(chatMessages),
|
||||||
}
|
}
|
||||||
newData[currentConvoId] = updatedMessages
|
newData[threadId] = updatedMessages
|
||||||
set(chatMessages, newData)
|
set(chatMessages, newData)
|
||||||
// Update thread last message
|
// Update thread last message
|
||||||
set(updateThreadStateLastMessageAtom, currentConvoId, newMessage.content)
|
set(updateThreadStateLastMessageAtom, threadId, newMessage.content)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -103,11 +92,11 @@ export const cleanConversationMessages = atom(null, (get, set, id: string) => {
|
|||||||
set(chatMessages, newData)
|
set(chatMessages, newData)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deleteMessage = atom(null, (get, set, id: string) => {
|
export const deleteMessageAtom = atom(null, (get, set, id: string) => {
|
||||||
const newData: Record<string, ThreadMessage[]> = {
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
...get(chatMessages),
|
...get(chatMessages),
|
||||||
}
|
}
|
||||||
const threadId = get(currentConversationAtom)?.id
|
const threadId = get(getActiveThreadIdAtom)
|
||||||
if (threadId) {
|
if (threadId) {
|
||||||
newData[threadId] = newData[threadId].filter((e) => e.id !== id)
|
newData[threadId] = newData[threadId].filter((e) => e.id !== id)
|
||||||
set(chatMessages, newData)
|
set(chatMessages, newData)
|
||||||
@ -121,7 +110,7 @@ export const updateMessageAtom = atom(
|
|||||||
set,
|
set,
|
||||||
id: string,
|
id: string,
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
text: string,
|
text: ThreadContent[],
|
||||||
status: MessageStatus
|
status: MessageStatus
|
||||||
) => {
|
) => {
|
||||||
const messages = get(chatMessages)[conversationId] ?? []
|
const messages = get(chatMessages)[conversationId] ?? []
|
||||||
@ -141,34 +130,3 @@ export const updateMessageAtom = atom(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* For updating the status of the last AI message that is pending
|
|
||||||
*/
|
|
||||||
export const updateLastMessageAsReadyAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, id, text: string) => {
|
|
||||||
const currentConvoId = get(getActiveConvoIdAtom)
|
|
||||||
if (!currentConvoId) return
|
|
||||||
|
|
||||||
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
|
||||||
const messageToUpdate = currentMessages.find((e) => e.id === id)
|
|
||||||
|
|
||||||
// if message is not found, do nothing
|
|
||||||
if (!messageToUpdate) return
|
|
||||||
|
|
||||||
const index = currentMessages.indexOf(messageToUpdate)
|
|
||||||
const updatedMsg: ThreadMessage = {
|
|
||||||
...messageToUpdate,
|
|
||||||
status: MessageStatus.Ready,
|
|
||||||
content: text,
|
|
||||||
}
|
|
||||||
|
|
||||||
currentMessages[index] = updatedMsg
|
|
||||||
const newData: Record<string, ThreadMessage[]> = {
|
|
||||||
...get(chatMessages),
|
|
||||||
}
|
|
||||||
newData[currentConvoId] = currentMessages
|
|
||||||
set(chatMessages, newData)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,115 +1,102 @@
|
|||||||
import { Thread } from '@janhq/core'
|
import { Thread, ThreadContent, ThreadState } from '@janhq/core'
|
||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
import { ThreadState } from '@/types/conversation'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the current active conversation id.
|
* Stores the current active conversation id.
|
||||||
*/
|
*/
|
||||||
const activeConversationIdAtom = atom<string | undefined>(undefined)
|
const activeThreadIdAtom = atom<string | undefined>(undefined)
|
||||||
|
|
||||||
export const getActiveConvoIdAtom = atom((get) => get(activeConversationIdAtom))
|
export const getActiveThreadIdAtom = atom((get) => get(activeThreadIdAtom))
|
||||||
|
|
||||||
export const setActiveConvoIdAtom = atom(
|
export const setActiveThreadIdAtom = atom(
|
||||||
null,
|
null,
|
||||||
(_get, set, convoId: string | undefined) => {
|
(_get, set, convoId: string | undefined) => set(activeThreadIdAtom, convoId)
|
||||||
set(activeConversationIdAtom, convoId)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export const waitingToSendMessage = atom<boolean | undefined>(undefined)
|
export const waitingToSendMessage = atom<boolean | undefined>(undefined)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all conversation states for the current user
|
* Stores all thread states for the current user
|
||||||
*/
|
*/
|
||||||
export const conversationStatesAtom = atom<Record<string, ThreadState>>({})
|
export const threadStatesAtom = atom<Record<string, ThreadState>>({})
|
||||||
export const currentConvoStateAtom = atom<ThreadState | undefined>((get) => {
|
export const activeThreadStateAtom = atom<ThreadState | undefined>((get) => {
|
||||||
const activeConvoId = get(activeConversationIdAtom)
|
const activeConvoId = get(activeThreadIdAtom)
|
||||||
if (!activeConvoId) {
|
if (!activeConvoId) {
|
||||||
console.debug('Active convo id is undefined')
|
console.debug('Active convo id is undefined')
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return get(conversationStatesAtom)[activeConvoId]
|
return get(threadStatesAtom)[activeConvoId]
|
||||||
})
|
})
|
||||||
export const addNewConversationStateAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, conversationId: string, state: ThreadState) => {
|
|
||||||
const currentState = { ...get(conversationStatesAtom) }
|
|
||||||
currentState[conversationId] = state
|
|
||||||
set(conversationStatesAtom, currentState)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
export const updateConversationWaitingForResponseAtom = atom(
|
export const updateConversationWaitingForResponseAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, conversationId: string, waitingForResponse: boolean) => {
|
(get, set, conversationId: string, waitingForResponse: boolean) => {
|
||||||
const currentState = { ...get(conversationStatesAtom) }
|
const currentState = { ...get(threadStatesAtom) }
|
||||||
currentState[conversationId] = {
|
currentState[conversationId] = {
|
||||||
...currentState[conversationId],
|
...currentState[conversationId],
|
||||||
waitingForResponse,
|
waitingForResponse,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
}
|
}
|
||||||
set(conversationStatesAtom, currentState)
|
set(threadStatesAtom, currentState)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
export const updateConversationErrorAtom = atom(
|
export const updateConversationErrorAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, conversationId: string, error?: Error) => {
|
(get, set, conversationId: string, error?: Error) => {
|
||||||
const currentState = { ...get(conversationStatesAtom) }
|
const currentState = { ...get(threadStatesAtom) }
|
||||||
currentState[conversationId] = {
|
currentState[conversationId] = {
|
||||||
...currentState[conversationId],
|
...currentState[conversationId],
|
||||||
error,
|
error,
|
||||||
}
|
}
|
||||||
set(conversationStatesAtom, currentState)
|
set(threadStatesAtom, currentState)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
export const updateConversationHasMoreAtom = atom(
|
export const updateConversationHasMoreAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, conversationId: string, hasMore: boolean) => {
|
(get, set, conversationId: string, hasMore: boolean) => {
|
||||||
const currentState = { ...get(conversationStatesAtom) }
|
const currentState = { ...get(threadStatesAtom) }
|
||||||
currentState[conversationId] = { ...currentState[conversationId], hasMore }
|
currentState[conversationId] = { ...currentState[conversationId], hasMore }
|
||||||
set(conversationStatesAtom, currentState)
|
set(threadStatesAtom, currentState)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const updateThreadStateLastMessageAtom = atom(
|
export const updateThreadStateLastMessageAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, conversationId: string, lastMessage?: string) => {
|
(get, set, threadId: string, lastContent?: ThreadContent[]) => {
|
||||||
const currentState = { ...get(conversationStatesAtom) }
|
const currentState = { ...get(threadStatesAtom) }
|
||||||
currentState[conversationId] = {
|
const lastMessage = lastContent?.[0]?.text?.value ?? ''
|
||||||
...currentState[conversationId],
|
currentState[threadId] = {
|
||||||
|
...currentState[threadId],
|
||||||
lastMessage,
|
lastMessage,
|
||||||
}
|
}
|
||||||
set(conversationStatesAtom, currentState)
|
set(threadStatesAtom, currentState)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const updateConversationAtom = atom(
|
export const updateThreadAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, conversation: Thread) => {
|
(get, set, updatedThread: Thread) => {
|
||||||
const id = conversation.id
|
const threads: Thread[] = get(threadsAtom).map((c) =>
|
||||||
if (!id) return
|
c.id === updatedThread.id ? updatedThread : c
|
||||||
const convo = get(userConversationsAtom).find((c) => c.id === id)
|
|
||||||
if (!convo) return
|
|
||||||
|
|
||||||
const newConversations: Thread[] = get(userConversationsAtom).map((c) =>
|
|
||||||
c.id === id ? conversation : c
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// sort new conversations based on updated at
|
// sort new threads based on updated at
|
||||||
newConversations.sort((a, b) => {
|
threads.sort((thread1, thread2) => {
|
||||||
const aDate = new Date(a.updatedAt ?? 0)
|
const aDate = new Date(thread1.updated ?? 0)
|
||||||
const bDate = new Date(b.updatedAt ?? 0)
|
const bDate = new Date(thread2.updated ?? 0)
|
||||||
return bDate.getTime() - aDate.getTime()
|
return bDate.getTime() - aDate.getTime()
|
||||||
})
|
})
|
||||||
|
|
||||||
set(userConversationsAtom, newConversations)
|
set(threadsAtom, threads)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all conversations for the current user
|
* Stores all threads for the current user
|
||||||
*/
|
*/
|
||||||
export const userConversationsAtom = atom<Thread[]>([])
|
export const threadsAtom = atom<Thread[]>([])
|
||||||
export const currentConversationAtom = atom<Thread | undefined>((get) =>
|
|
||||||
get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom))
|
export const activeThreadAtom = atom<Thread | undefined>((get) =>
|
||||||
|
get(threadsAtom).find((c) => c.id === get(getActiveThreadIdAtom))
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,6 +2,5 @@ import { Model } from '@janhq/core/lib/types'
|
|||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
export const stateModel = atom({ state: 'start', loading: false, model: '' })
|
export const stateModel = atom({ state: 'start', loading: false, model: '' })
|
||||||
export const selectedModelAtom = atom<Model | undefined>(undefined)
|
|
||||||
export const activeAssistantModelAtom = atom<Model | undefined>(undefined)
|
export const activeAssistantModelAtom = atom<Model | undefined>(undefined)
|
||||||
export const downloadingModelsAtom = atom<Model[]>([])
|
export const downloadingModelsAtom = atom<Model[]>([])
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
import { PluginType } from '@janhq/core'
|
import { PluginType } from '@janhq/core'
|
||||||
import { InferencePlugin } from '@janhq/core/lib/plugins'
|
import { InferencePlugin } from '@janhq/core/lib/plugins'
|
||||||
import { Model } from '@janhq/core/lib/types'
|
import { Model, ModelSettingParams } from '@janhq/core/lib/types'
|
||||||
|
|
||||||
import { atom, useAtom } from 'jotai'
|
import { atom, useAtom } from 'jotai'
|
||||||
|
|
||||||
import { toaster } from '@/containers/Toast'
|
import { toaster } from '@/containers/Toast'
|
||||||
@ -13,12 +10,12 @@ import { useGetDownloadedModels } from './useGetDownloadedModels'
|
|||||||
|
|
||||||
import { pluginManager } from '@/plugin'
|
import { pluginManager } from '@/plugin'
|
||||||
|
|
||||||
const activeAssistantModelAtom = atom<Model | undefined>(undefined)
|
const activeModelAtom = atom<Model | undefined>(undefined)
|
||||||
|
|
||||||
const stateModelAtom = atom({ state: 'start', loading: false, model: '' })
|
const stateModelAtom = atom({ state: 'start', loading: false, model: '' })
|
||||||
|
|
||||||
export function useActiveModel() {
|
export function useActiveModel() {
|
||||||
const [activeModel, setActiveModel] = useAtom(activeAssistantModelAtom)
|
const [activeModel, setActiveModel] = useAtom(activeModelAtom)
|
||||||
const [stateModel, setStateModel] = useAtom(stateModelAtom)
|
const [stateModel, setStateModel] = useAtom(stateModelAtom)
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
const { downloadedModels } = useGetDownloadedModels()
|
||||||
|
|
||||||
@ -30,6 +27,7 @@ export function useActiveModel() {
|
|||||||
console.debug(`Model ${modelId} is already initialized. Ignore..`)
|
console.debug(`Model ${modelId} is already initialized. Ignore..`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// TODO: incase we have multiple assistants, the configuration will be from assistant
|
||||||
|
|
||||||
setActiveModel(undefined)
|
setActiveModel(undefined)
|
||||||
|
|
||||||
@ -52,8 +50,7 @@ export function useActiveModel() {
|
|||||||
|
|
||||||
const currentTime = Date.now()
|
const currentTime = Date.now()
|
||||||
console.debug('Init model: ', modelId)
|
console.debug('Init model: ', modelId)
|
||||||
const path = join('models', model.name, modelId)
|
const res = await initModel(modelId, model?.settings)
|
||||||
const res = await initModel(path)
|
|
||||||
if (res && res.error && res.modelFile === stateModel.model) {
|
if (res && res.error && res.modelFile === stateModel.model) {
|
||||||
const errorMessage = `${res.error}`
|
const errorMessage = `${res.error}`
|
||||||
alert(errorMessage)
|
alert(errorMessage)
|
||||||
@ -98,8 +95,11 @@ export function useActiveModel() {
|
|||||||
return { activeModel, startModel, stopModel, stateModel }
|
return { activeModel, startModel, stopModel, stateModel }
|
||||||
}
|
}
|
||||||
|
|
||||||
const initModel = async (modelId: string): Promise<any> => {
|
const initModel = async (
|
||||||
|
modelId: string,
|
||||||
|
settings?: ModelSettingParams
|
||||||
|
): Promise<any> => {
|
||||||
return pluginManager
|
return pluginManager
|
||||||
.get<InferencePlugin>(PluginType.Inference)
|
.get<InferencePlugin>(PluginType.Inference)
|
||||||
?.initModel(modelId)
|
?.initModel(modelId, settings)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
import { PluginType } from '@janhq/core'
|
|
||||||
import { Thread, Model } from '@janhq/core'
|
|
||||||
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
|
||||||
import { useAtom, useSetAtom } from 'jotai'
|
|
||||||
|
|
||||||
import { generateConversationId } from '@/utils/conversation'
|
|
||||||
|
|
||||||
import {
|
|
||||||
userConversationsAtom,
|
|
||||||
setActiveConvoIdAtom,
|
|
||||||
addNewConversationStateAtom,
|
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
|
||||||
import { pluginManager } from '@/plugin'
|
|
||||||
|
|
||||||
export const useCreateConversation = () => {
|
|
||||||
const [userConversations, setUserConversations] = useAtom(
|
|
||||||
userConversationsAtom
|
|
||||||
)
|
|
||||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
|
||||||
const addNewConvoState = useSetAtom(addNewConversationStateAtom)
|
|
||||||
|
|
||||||
const requestCreateConvo = async (model: Model) => {
|
|
||||||
const mappedConvo: Thread = {
|
|
||||||
id: generateConversationId(),
|
|
||||||
modelId: model.id,
|
|
||||||
summary: model.name,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
messages: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
addNewConvoState(mappedConvo.id, {
|
|
||||||
hasMore: true,
|
|
||||||
waitingForResponse: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
await pluginManager
|
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
|
||||||
?.saveConversation(mappedConvo)
|
|
||||||
setUserConversations([mappedConvo, ...userConversations])
|
|
||||||
setActiveConvoId(mappedConvo.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
requestCreateConvo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
92
web/hooks/useCreateNewThread.ts
Normal file
92
web/hooks/useCreateNewThread.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
Assistant,
|
||||||
|
Thread,
|
||||||
|
ThreadAssistantInfo,
|
||||||
|
ThreadState,
|
||||||
|
} from '@janhq/core/lib/types'
|
||||||
|
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { generateThreadId } from '@/utils/conversation'
|
||||||
|
|
||||||
|
import {
|
||||||
|
threadsAtom,
|
||||||
|
setActiveThreadIdAtom,
|
||||||
|
threadStatesAtom,
|
||||||
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
|
const createNewThreadAtom = atom(null, (get, set, newThread: Thread) => {
|
||||||
|
// create thread state for this new thread
|
||||||
|
const currentState = { ...get(threadStatesAtom) }
|
||||||
|
|
||||||
|
const threadState: ThreadState = {
|
||||||
|
hasMore: false,
|
||||||
|
waitingForResponse: false,
|
||||||
|
}
|
||||||
|
currentState[newThread.id] = threadState
|
||||||
|
set(threadStatesAtom, currentState)
|
||||||
|
|
||||||
|
// add the new thread on top of the thread list to the state
|
||||||
|
const threads = get(threadsAtom)
|
||||||
|
set(threadsAtom, [newThread, ...threads])
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useCreateNewThread = () => {
|
||||||
|
const createNewThread = useSetAtom(createNewThreadAtom)
|
||||||
|
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
||||||
|
const [threadStates, setThreadStates] = useAtom(threadStatesAtom)
|
||||||
|
const threads = useAtomValue(threadsAtom)
|
||||||
|
|
||||||
|
const requestCreateNewThread = async (assistant: Assistant) => {
|
||||||
|
const unfinishedThreads = threads.filter((t) => t.isFinishInit === false)
|
||||||
|
if (unfinishedThreads.length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdAt = Date.now()
|
||||||
|
const assistantInfo: ThreadAssistantInfo = {
|
||||||
|
assistant_id: assistant.id,
|
||||||
|
assistant_name: assistant.name,
|
||||||
|
model: {
|
||||||
|
id: '*',
|
||||||
|
settings: {
|
||||||
|
ctx_len: 0,
|
||||||
|
ngl: 0,
|
||||||
|
embedding: false,
|
||||||
|
n_parallel: 0,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
temperature: 0,
|
||||||
|
token_limit: 0,
|
||||||
|
top_k: 0,
|
||||||
|
top_p: 0,
|
||||||
|
stream: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const threadId = generateThreadId(assistant.id)
|
||||||
|
const thread: Thread = {
|
||||||
|
id: threadId,
|
||||||
|
object: 'thread',
|
||||||
|
title: 'New Thread',
|
||||||
|
assistants: [assistantInfo],
|
||||||
|
created: createdAt,
|
||||||
|
updated: createdAt,
|
||||||
|
isFinishInit: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move isFinishInit here
|
||||||
|
const threadState: ThreadState = {
|
||||||
|
hasMore: false,
|
||||||
|
waitingForResponse: false,
|
||||||
|
lastMessage: undefined,
|
||||||
|
}
|
||||||
|
setThreadStates({ ...threadStates, [threadId]: threadState })
|
||||||
|
// add the new thread on top of the thread list to the state
|
||||||
|
createNewThread(thread)
|
||||||
|
setActiveThreadId(thread.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestCreateNewThread,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,60 +16,55 @@ import {
|
|||||||
getCurrentChatMessagesAtom,
|
getCurrentChatMessagesAtom,
|
||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import {
|
import {
|
||||||
userConversationsAtom,
|
threadsAtom,
|
||||||
getActiveConvoIdAtom,
|
getActiveThreadIdAtom,
|
||||||
setActiveConvoIdAtom,
|
setActiveThreadIdAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
export default function useDeleteConversation() {
|
export default function useDeleteThread() {
|
||||||
const { activeModel } = useActiveModel()
|
const { activeModel } = useActiveModel()
|
||||||
const [userConversations, setUserConversations] = useAtom(
|
const [threads, setThreads] = useAtom(threadsAtom)
|
||||||
userConversationsAtom
|
|
||||||
)
|
|
||||||
const setCurrentPrompt = useSetAtom(currentPromptAtom)
|
const setCurrentPrompt = useSetAtom(currentPromptAtom)
|
||||||
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
|
|
||||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
const setActiveConvoId = useSetAtom(setActiveThreadIdAtom)
|
||||||
const deleteMessages = useSetAtom(deleteConversationMessage)
|
const deleteMessages = useSetAtom(deleteConversationMessage)
|
||||||
const cleanMessages = useSetAtom(cleanConversationMessages)
|
const cleanMessages = useSetAtom(cleanConversationMessages)
|
||||||
const currentMessages = useAtomValue(getCurrentChatMessagesAtom)
|
|
||||||
|
|
||||||
const cleanConvo = async () => {
|
const cleanThread = async () => {
|
||||||
if (activeConvoId) {
|
if (activeThreadId) {
|
||||||
const currentConversation = userConversations.filter(
|
const thread = threads.filter((c) => c.id === activeThreadId)[0]
|
||||||
(c) => c.id === activeConvoId
|
cleanMessages(activeThreadId)
|
||||||
)[0]
|
if (thread)
|
||||||
cleanMessages(activeConvoId)
|
|
||||||
if (currentConversation)
|
|
||||||
await pluginManager
|
await pluginManager
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
?.saveConversation({
|
?.writeMessages(
|
||||||
...currentConversation,
|
activeThreadId,
|
||||||
id: activeConvoId,
|
messages.filter((msg) => msg.role === ChatCompletionRole.System)
|
||||||
messages: currentMessages.filter(
|
)
|
||||||
(e) => e.role === ChatCompletionRole.System
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const deleteConvo = async () => {
|
|
||||||
if (activeConvoId) {
|
const deleteThread = async () => {
|
||||||
|
if (!activeThreadId) {
|
||||||
|
alert('No active thread')
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await pluginManager
|
await pluginManager
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
?.deleteConversation(activeConvoId)
|
?.deleteThread(activeThreadId)
|
||||||
const currentConversations = userConversations.filter(
|
const availableThreads = threads.filter((c) => c.id !== activeThreadId)
|
||||||
(c) => c.id !== activeConvoId
|
setThreads(availableThreads)
|
||||||
)
|
deleteMessages(activeThreadId)
|
||||||
setUserConversations(currentConversations)
|
|
||||||
deleteMessages(activeConvoId)
|
|
||||||
setCurrentPrompt('')
|
setCurrentPrompt('')
|
||||||
toaster({
|
toaster({
|
||||||
title: 'Chat successfully deleted.',
|
title: 'Chat successfully deleted.',
|
||||||
description: `Chat with ${activeModel?.name} has been successfully deleted.`,
|
description: `Chat with ${activeModel?.name} has been successfully deleted.`,
|
||||||
})
|
})
|
||||||
if (currentConversations.length > 0) {
|
if (availableThreads.length > 0) {
|
||||||
setActiveConvoId(currentConversations[0].id)
|
setActiveConvoId(availableThreads[0].id)
|
||||||
} else {
|
} else {
|
||||||
setActiveConvoId(undefined)
|
setActiveConvoId(undefined)
|
||||||
}
|
}
|
||||||
@ -77,10 +72,9 @@ export default function useDeleteConversation() {
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cleanConvo,
|
cleanThread,
|
||||||
deleteConvo,
|
deleteThread,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
import { PluginType } from '@janhq/core'
|
import { PluginType } from '@janhq/core'
|
||||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { Model } from '@janhq/core/lib/types'
|
import { Model } from '@janhq/core/lib/types'
|
||||||
@ -14,8 +12,9 @@ export default function useDeleteModel() {
|
|||||||
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
||||||
|
|
||||||
const deleteModel = async (model: Model) => {
|
const deleteModel = async (model: Model) => {
|
||||||
const path = join('models', model.name, model.id)
|
await pluginManager
|
||||||
await pluginManager.get<ModelPlugin>(PluginType.Model)?.deleteModel(path)
|
.get<ModelPlugin>(PluginType.Model)
|
||||||
|
?.deleteModel(model.id)
|
||||||
|
|
||||||
// reload models
|
// reload models
|
||||||
setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id))
|
setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id))
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { PluginType } from '@janhq/core'
|
import { PluginType } from '@janhq/core'
|
||||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { Model, ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
import { Model } from '@janhq/core/lib/types'
|
||||||
|
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
|
|
||||||
@ -16,41 +16,10 @@ export default function useDownloadModel() {
|
|||||||
downloadingModelsAtom
|
downloadingModelsAtom
|
||||||
)
|
)
|
||||||
|
|
||||||
const assistanModel = (
|
const downloadModel = async (model: Model) => {
|
||||||
model: ModelCatalog,
|
|
||||||
modelVersion: ModelVersion
|
|
||||||
): Model => {
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* Id will be used for the model file name
|
|
||||||
* Should be the version name
|
|
||||||
*/
|
|
||||||
id: modelVersion.name,
|
|
||||||
name: model.name,
|
|
||||||
quantizationName: modelVersion.quantizationName,
|
|
||||||
bits: modelVersion.bits,
|
|
||||||
size: modelVersion.size,
|
|
||||||
maxRamRequired: modelVersion.maxRamRequired,
|
|
||||||
usecase: modelVersion.usecase,
|
|
||||||
downloadLink: modelVersion.downloadLink,
|
|
||||||
shortDescription: model.shortDescription,
|
|
||||||
longDescription: model.longDescription,
|
|
||||||
avatarUrl: model.avatarUrl,
|
|
||||||
author: model.author,
|
|
||||||
version: model.version,
|
|
||||||
modelUrl: model.modelUrl,
|
|
||||||
releaseDate: -1,
|
|
||||||
tags: model.tags,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadModel = async (
|
|
||||||
model: ModelCatalog,
|
|
||||||
modelVersion: ModelVersion
|
|
||||||
) => {
|
|
||||||
// set an initial download state
|
// set an initial download state
|
||||||
setDownloadState({
|
setDownloadState({
|
||||||
modelId: modelVersion.name,
|
modelId: model.id,
|
||||||
time: {
|
time: {
|
||||||
elapsed: 0,
|
elapsed: 0,
|
||||||
remaining: 0,
|
remaining: 0,
|
||||||
@ -61,16 +30,11 @@ export default function useDownloadModel() {
|
|||||||
total: 0,
|
total: 0,
|
||||||
transferred: 0,
|
transferred: 0,
|
||||||
},
|
},
|
||||||
fileName: modelVersion.name,
|
fileName: model.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
const assistantModel = assistanModel(model, modelVersion)
|
setDownloadingModels([...downloadingModels, model])
|
||||||
|
await pluginManager.get<ModelPlugin>(PluginType.Model)?.downloadModel(model)
|
||||||
setDownloadingModels([...downloadingModels, assistantModel])
|
|
||||||
|
|
||||||
await pluginManager
|
|
||||||
.get<ModelPlugin>(PluginType.Model)
|
|
||||||
?.downloadModel(assistantModel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -10,7 +10,6 @@ const setDownloadStateAtom = atom(null, (get, set, state: DownloadState) => {
|
|||||||
console.debug(
|
console.debug(
|
||||||
`current download state for ${state.fileName} is ${JSON.stringify(state)}`
|
`current download state for ${state.fileName} is ${JSON.stringify(state)}`
|
||||||
)
|
)
|
||||||
state.fileName = state.fileName.replace('models/', '')
|
|
||||||
currentState[state.fileName] = state
|
currentState[state.fileName] = state
|
||||||
set(modelDownloadStateAtom, currentState)
|
set(modelDownloadStateAtom, currentState)
|
||||||
})
|
})
|
||||||
|
|||||||
43
web/hooks/useGetAllThreads.ts
Normal file
43
web/hooks/useGetAllThreads.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { PluginType, ThreadState } from '@janhq/core'
|
||||||
|
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import {
|
||||||
|
threadStatesAtom,
|
||||||
|
threadsAtom,
|
||||||
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
|
import { pluginManager } from '@/plugin/PluginManager'
|
||||||
|
|
||||||
|
const useGetAllThreads = () => {
|
||||||
|
const setConversationStates = useSetAtom(threadStatesAtom)
|
||||||
|
const setConversations = useSetAtom(threadsAtom)
|
||||||
|
|
||||||
|
const getAllThreads = async () => {
|
||||||
|
try {
|
||||||
|
const threads = await pluginManager
|
||||||
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
|
?.getThreads()
|
||||||
|
const threadStates: Record<string, ThreadState> = {}
|
||||||
|
threads?.forEach((thread) => {
|
||||||
|
if (thread.id != null) {
|
||||||
|
const lastMessage = (thread.metadata?.lastMessage as string) ?? ''
|
||||||
|
threadStates[thread.id] = {
|
||||||
|
hasMore: true,
|
||||||
|
waitingForResponse: false,
|
||||||
|
lastMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setConversationStates(threadStates)
|
||||||
|
setConversations(threads ?? [])
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getAllThreads,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useGetAllThreads
|
||||||
35
web/hooks/useGetAssistants.ts
Normal file
35
web/hooks/useGetAssistants.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { Assistant, PluginType } from '@janhq/core'
|
||||||
|
|
||||||
|
import { AssistantPlugin } from '@janhq/core/lib/plugins'
|
||||||
|
|
||||||
|
import { pluginManager } from '@/plugin/PluginManager'
|
||||||
|
|
||||||
|
const getAssistants = async (): Promise<Assistant[]> => {
|
||||||
|
return (
|
||||||
|
pluginManager.get<AssistantPlugin>(PluginType.Assistant)?.getAssistants() ??
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks for get assistants
|
||||||
|
*
|
||||||
|
* @returns assistants
|
||||||
|
*/
|
||||||
|
export default function useGetAssistants() {
|
||||||
|
const [assistants, setAssistants] = useState<Assistant[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAssistants()
|
||||||
|
.then((data) => {
|
||||||
|
setAssistants(data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { assistants }
|
||||||
|
}
|
||||||
@ -1,22 +1,17 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
const downloadedModelAtom = atom<Model[]>([])
|
|
||||||
import { PluginType } from '@janhq/core'
|
import { PluginType } from '@janhq/core'
|
||||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { Model } from '@janhq/core/lib/types'
|
import { Model } from '@janhq/core/lib/types'
|
||||||
|
|
||||||
import { atom, useAtom } from 'jotai'
|
import { atom, useAtom } from 'jotai'
|
||||||
|
|
||||||
import { pluginManager } from '@/plugin/PluginManager'
|
import { pluginManager } from '@/plugin/PluginManager'
|
||||||
|
|
||||||
export function useGetDownloadedModels() {
|
const downloadedModelsAtom = atom<Model[]>([])
|
||||||
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelAtom)
|
|
||||||
|
|
||||||
async function getDownloadedModels(): Promise<Model[]> {
|
export function useGetDownloadedModels() {
|
||||||
const models = await pluginManager
|
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
|
||||||
.get<ModelPlugin>(PluginType.Model)
|
|
||||||
?.getDownloadedModels()
|
|
||||||
return models ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getDownloadedModels().then((downloadedModels) => {
|
getDownloadedModels().then((downloadedModels) => {
|
||||||
@ -26,3 +21,11 @@ export function useGetDownloadedModels() {
|
|||||||
|
|
||||||
return { downloadedModels, setDownloadedModels }
|
return { downloadedModels, setDownloadedModels }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDownloadedModels(): Promise<Model[]> {
|
||||||
|
const models = await pluginManager
|
||||||
|
.get<ModelPlugin>(PluginType.Model)
|
||||||
|
?.getDownloadedModels()
|
||||||
|
|
||||||
|
return models ?? []
|
||||||
|
}
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import { Model, Thread } from '@janhq/core'
|
|
||||||
import { useAtomValue } from 'jotai'
|
|
||||||
|
|
||||||
import { useActiveModel } from './useActiveModel'
|
|
||||||
import { useGetDownloadedModels } from './useGetDownloadedModels'
|
|
||||||
|
|
||||||
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
|
|
||||||
|
|
||||||
export default function useGetInputState() {
|
|
||||||
const [inputState, setInputState] = useState<InputType>('loading')
|
|
||||||
const currentThread = useAtomValue(currentConversationAtom)
|
|
||||||
const { activeModel } = useActiveModel()
|
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
|
||||||
|
|
||||||
const handleInputState = (
|
|
||||||
thread: Thread | undefined,
|
|
||||||
currentModel: Model | undefined
|
|
||||||
) => {
|
|
||||||
if (thread == null) return
|
|
||||||
if (currentModel == null) {
|
|
||||||
setInputState('loading')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if convo model id is in downloaded models
|
|
||||||
const isModelAvailable = downloadedModels.some(
|
|
||||||
(model) => model.id === thread.modelId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isModelAvailable) {
|
|
||||||
// can't find model in downloaded models
|
|
||||||
setInputState('model-not-found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thread.modelId !== currentModel.id) {
|
|
||||||
// in case convo model and active model is different,
|
|
||||||
// ask user to init the required model
|
|
||||||
setInputState('model-mismatch')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setInputState('available')
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleInputState(currentThread, activeModel)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return { inputState, currentThread }
|
|
||||||
}
|
|
||||||
|
|
||||||
type InputType = 'available' | 'loading' | 'model-mismatch' | 'model-not-found'
|
|
||||||
@ -1,19 +1,19 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { ModelVersion } from '@janhq/core/lib/types'
|
import { Model } from '@janhq/core/lib/types'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
|
import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
|
||||||
|
|
||||||
export default function useGetMostSuitableModelVersion() {
|
export default function useGetMostSuitableModelVersion() {
|
||||||
const [suitableModel, setSuitableModel] = useState<ModelVersion | undefined>()
|
const [suitableModel, setSuitableModel] = useState<Model | undefined>()
|
||||||
const totalRam = useAtomValue(totalRamAtom)
|
const totalRam = useAtomValue(totalRamAtom)
|
||||||
|
|
||||||
const getMostSuitableModelVersion = async (modelVersions: ModelVersion[]) => {
|
const getMostSuitableModelVersion = async (modelVersions: Model[]) => {
|
||||||
// find the model version with the highest required RAM that is still below the user's RAM by 80%
|
// 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) => {
|
const modelVersion = modelVersions.reduce((prev, current) => {
|
||||||
if (current.maxRamRequired > prev.maxRamRequired) {
|
if (current.metadata.maxRamRequired > prev.metadata.maxRamRequired) {
|
||||||
if (current.maxRamRequired < totalRam * 0.8) {
|
if (current.metadata.maxRamRequired < totalRam * 0.8) {
|
||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ModelVersion } from '@janhq/core/lib/types'
|
import { Model } from '@janhq/core/lib/types'
|
||||||
|
|
||||||
import { ModelPerformance, TagType } from '@/constants/tagType'
|
import { ModelPerformance, TagType } from '@/constants/tagType'
|
||||||
|
|
||||||
@ -9,10 +9,10 @@ import { ModelPerformance, TagType } from '@/constants/tagType'
|
|||||||
|
|
||||||
export default function useGetPerformanceTag() {
|
export default function useGetPerformanceTag() {
|
||||||
async function getPerformanceForModel(
|
async function getPerformanceForModel(
|
||||||
modelVersion: ModelVersion,
|
model: Model,
|
||||||
totalRam: number
|
totalRam: number
|
||||||
): Promise<{ title: string; performanceTag: TagType }> {
|
): Promise<{ title: string; performanceTag: TagType }> {
|
||||||
const requiredRam = modelVersion.maxRamRequired
|
const requiredRam = model.metadata.maxRamRequired
|
||||||
const performanceTag = calculateRamPerformance(requiredRam, totalRam)
|
const performanceTag = calculateRamPerformance(requiredRam, totalRam)
|
||||||
|
|
||||||
let title = ''
|
let title = ''
|
||||||
|
|||||||
@ -1,44 +0,0 @@
|
|||||||
import { PluginType, Thread } from '@janhq/core'
|
|
||||||
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
|
|
||||||
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
|
||||||
import {
|
|
||||||
conversationStatesAtom,
|
|
||||||
userConversationsAtom,
|
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
|
||||||
import { pluginManager } from '@/plugin/PluginManager'
|
|
||||||
import { ThreadState } from '@/types/conversation'
|
|
||||||
|
|
||||||
const useGetUserConversations = () => {
|
|
||||||
const setConversationStates = useSetAtom(conversationStatesAtom)
|
|
||||||
const setConversations = useSetAtom(userConversationsAtom)
|
|
||||||
const setConvoMessages = useSetAtom(setConvoMessagesAtom)
|
|
||||||
|
|
||||||
const getUserConversations = async () => {
|
|
||||||
try {
|
|
||||||
const convos: Thread[] | undefined = await pluginManager
|
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
|
||||||
?.getConversations()
|
|
||||||
const convoStates: Record<string, ThreadState> = {}
|
|
||||||
convos?.forEach((convo) => {
|
|
||||||
convoStates[convo.id ?? ''] = {
|
|
||||||
hasMore: true,
|
|
||||||
waitingForResponse: false,
|
|
||||||
lastMessage: convo.messages[0]?.content ?? '',
|
|
||||||
}
|
|
||||||
setConvoMessages(convo.messages, convo.id ?? '')
|
|
||||||
})
|
|
||||||
setConversationStates(convoStates)
|
|
||||||
setConversations(convos ?? [])
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getUserConversations,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useGetUserConversations
|
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
ChatCompletionMessage,
|
ChatCompletionMessage,
|
||||||
ChatCompletionRole,
|
ChatCompletionRole,
|
||||||
|
ContentType,
|
||||||
EventName,
|
EventName,
|
||||||
MessageRequest,
|
MessageRequest,
|
||||||
MessageStatus,
|
MessageStatus,
|
||||||
PluginType,
|
PluginType,
|
||||||
|
Thread,
|
||||||
ThreadMessage,
|
ThreadMessage,
|
||||||
events,
|
events,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
@ -13,8 +15,11 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
|||||||
|
|
||||||
import { ulid } from 'ulid'
|
import { ulid } from 'ulid'
|
||||||
|
|
||||||
|
import { selectedModelAtom } from '@/containers/DropdownListSidebar'
|
||||||
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||||
|
|
||||||
|
import { toaster } from '@/containers/Toast'
|
||||||
|
|
||||||
import { useActiveModel } from './useActiveModel'
|
import { useActiveModel } from './useActiveModel'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -22,29 +27,30 @@ import {
|
|||||||
getCurrentChatMessagesAtom,
|
getCurrentChatMessagesAtom,
|
||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import {
|
import {
|
||||||
currentConversationAtom,
|
activeThreadAtom,
|
||||||
updateConversationAtom,
|
updateThreadAtom,
|
||||||
updateConversationWaitingForResponseAtom,
|
updateConversationWaitingForResponseAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
import { pluginManager } from '@/plugin/PluginManager'
|
import { pluginManager } from '@/plugin/PluginManager'
|
||||||
|
|
||||||
export default function useSendChatMessage() {
|
export default function useSendChatMessage() {
|
||||||
const currentConvo = useAtomValue(currentConversationAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const addNewMessage = useSetAtom(addNewMessageAtom)
|
const addNewMessage = useSetAtom(addNewMessageAtom)
|
||||||
const updateConversation = useSetAtom(updateConversationAtom)
|
const updateThread = useSetAtom(updateThreadAtom)
|
||||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
||||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
||||||
|
|
||||||
const currentMessages = useAtomValue(getCurrentChatMessagesAtom)
|
const currentMessages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
const { activeModel } = useActiveModel()
|
const { activeModel } = useActiveModel()
|
||||||
|
const selectedModel = useAtomValue(selectedModelAtom)
|
||||||
|
const { startModel } = useActiveModel()
|
||||||
|
|
||||||
function updateConvSummary(newMessage: MessageRequest) {
|
function updateThreadTitle(newMessage: MessageRequest) {
|
||||||
if (
|
if (
|
||||||
currentConvo &&
|
activeThread &&
|
||||||
newMessage.messages &&
|
newMessage.messages &&
|
||||||
newMessage.messages.length >= 2 &&
|
newMessage.messages.length > 2 &&
|
||||||
(!currentConvo.summary ||
|
(activeThread.title === '' || activeThread.title === activeModel?.name)
|
||||||
currentConvo.summary === '' ||
|
|
||||||
currentConvo.summary === activeModel?.name)
|
|
||||||
) {
|
) {
|
||||||
const summaryMsg: ChatCompletionMessage = {
|
const summaryMsg: ChatCompletionMessage = {
|
||||||
role: ChatCompletionRole.User,
|
role: ChatCompletionRole.User,
|
||||||
@ -60,70 +66,123 @@ export default function useSendChatMessage() {
|
|||||||
messages: newMessage.messages?.slice(0, -1).concat([summaryMsg]),
|
messages: newMessage.messages?.slice(0, -1).concat([summaryMsg]),
|
||||||
})
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
|
const content = result?.content[0]?.text.value.trim()
|
||||||
if (
|
if (
|
||||||
currentConvo &&
|
activeThread &&
|
||||||
currentConvo.id === newMessage.threadId &&
|
activeThread.id === newMessage.threadId &&
|
||||||
result?.content &&
|
content &&
|
||||||
result?.content?.trim().length > 0 &&
|
content.length > 0 &&
|
||||||
result.content.split(' ').length <= 20
|
content.split(' ').length <= 20
|
||||||
) {
|
) {
|
||||||
const updatedConv = {
|
const updatedConv: Thread = {
|
||||||
...currentConvo,
|
...activeThread,
|
||||||
summary: result.content,
|
title: content,
|
||||||
}
|
}
|
||||||
updateConversation(updatedConv)
|
updateThread(updatedConv)
|
||||||
pluginManager
|
pluginManager
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
?.saveConversation({
|
?.saveThread(updatedConv)
|
||||||
...updatedConv,
|
|
||||||
messages: currentMessages,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendChatMessage = async () => {
|
const sendChatMessage = async () => {
|
||||||
const threadId = currentConvo?.id
|
if (!currentPrompt || currentPrompt.trim().length === 0) {
|
||||||
if (!threadId) {
|
return
|
||||||
console.error('No conversation id')
|
}
|
||||||
|
if (!activeThread) {
|
||||||
|
console.error('No active thread')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentPrompt('')
|
if (!activeThread.isFinishInit) {
|
||||||
updateConvWaiting(threadId, true)
|
if (!selectedModel) {
|
||||||
|
toaster({ title: 'Please select a model' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const assistantId = activeThread.assistants[0].assistant_id ?? ''
|
||||||
|
const assistantName = activeThread.assistants[0].assistant_name ?? ''
|
||||||
|
const updatedThread: Thread = {
|
||||||
|
...activeThread,
|
||||||
|
isFinishInit: true,
|
||||||
|
title: `${activeThread.assistants[0].assistant_name} with ${selectedModel.name}`,
|
||||||
|
assistants: [
|
||||||
|
{
|
||||||
|
assistant_id: assistantId,
|
||||||
|
assistant_name: assistantName,
|
||||||
|
model: {
|
||||||
|
id: selectedModel.id,
|
||||||
|
settings: selectedModel.settings,
|
||||||
|
parameters: selectedModel.parameters,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
updateThread(updatedThread)
|
||||||
|
|
||||||
|
pluginManager
|
||||||
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
|
?.saveThread(updatedThread)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConvWaiting(activeThread.id, true)
|
||||||
|
|
||||||
const prompt = currentPrompt.trim()
|
const prompt = currentPrompt.trim()
|
||||||
|
setCurrentPrompt('')
|
||||||
|
|
||||||
const messages: ChatCompletionMessage[] = currentMessages
|
const messages: ChatCompletionMessage[] = currentMessages
|
||||||
.map<ChatCompletionMessage>((msg) => ({
|
.map<ChatCompletionMessage>((msg) => ({
|
||||||
role: msg.role ?? ChatCompletionRole.User,
|
role: msg.role,
|
||||||
content: msg.content ?? '',
|
content: msg.content[0]?.text.value ?? '',
|
||||||
}))
|
}))
|
||||||
.reverse()
|
|
||||||
.concat([
|
.concat([
|
||||||
{
|
{
|
||||||
role: ChatCompletionRole.User,
|
role: ChatCompletionRole.User,
|
||||||
content: prompt,
|
content: prompt,
|
||||||
} as ChatCompletionMessage,
|
} as ChatCompletionMessage,
|
||||||
])
|
])
|
||||||
|
console.debug(`Sending messages: ${JSON.stringify(messages, null, 2)}`)
|
||||||
|
const msgId = ulid()
|
||||||
const messageRequest: MessageRequest = {
|
const messageRequest: MessageRequest = {
|
||||||
id: ulid(),
|
id: msgId,
|
||||||
threadId: threadId,
|
threadId: activeThread.id,
|
||||||
messages,
|
messages,
|
||||||
|
parameters: activeThread.assistants[0].model.parameters,
|
||||||
}
|
}
|
||||||
|
const timestamp = Date.now()
|
||||||
const threadMessage: ThreadMessage = {
|
const threadMessage: ThreadMessage = {
|
||||||
id: messageRequest.id,
|
id: msgId,
|
||||||
threadId: messageRequest.threadId,
|
thread_id: activeThread.id,
|
||||||
content: prompt,
|
|
||||||
role: ChatCompletionRole.User,
|
role: ChatCompletionRole.User,
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
status: MessageStatus.Ready,
|
status: MessageStatus.Ready,
|
||||||
|
created: timestamp,
|
||||||
|
updated: timestamp,
|
||||||
|
object: 'thread.message',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: ContentType.Text,
|
||||||
|
text: {
|
||||||
|
value: prompt,
|
||||||
|
annotations: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
addNewMessage(threadMessage)
|
|
||||||
|
|
||||||
|
addNewMessage(threadMessage)
|
||||||
|
updateThreadTitle(messageRequest)
|
||||||
|
|
||||||
|
await pluginManager
|
||||||
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
|
?.addNewMessage(threadMessage)
|
||||||
|
|
||||||
|
const modelId = selectedModel?.id ?? activeThread.assistants[0].model.id
|
||||||
|
if (activeModel?.id !== modelId) {
|
||||||
|
await startModel(modelId)
|
||||||
|
}
|
||||||
events.emit(EventName.OnNewMessageRequest, messageRequest)
|
events.emit(EventName.OnNewMessageRequest, messageRequest)
|
||||||
updateConvSummary(messageRequest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
40
web/hooks/useSetActiveThread.ts
Normal file
40
web/hooks/useSetActiveThread.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { PluginType, Thread } from '@janhq/core'
|
||||||
|
|
||||||
|
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
||||||
|
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
|
import {
|
||||||
|
getActiveThreadIdAtom,
|
||||||
|
setActiveThreadIdAtom,
|
||||||
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
|
import { pluginManager } from '@/plugin'
|
||||||
|
|
||||||
|
export default function useSetActiveThread() {
|
||||||
|
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
|
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
||||||
|
const setThreadMessage = useSetAtom(setConvoMessagesAtom)
|
||||||
|
|
||||||
|
const setActiveThread = async (thread: Thread) => {
|
||||||
|
if (activeThreadId === thread.id) {
|
||||||
|
console.debug('Thread already active')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!thread.isFinishInit) {
|
||||||
|
console.debug('Thread not finish init')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the corresponding messages
|
||||||
|
const messages = await pluginManager
|
||||||
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
|
?.getAllMessages(thread.id)
|
||||||
|
setThreadMessage(thread.id, messages ?? [])
|
||||||
|
|
||||||
|
setActiveThreadId(thread.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { activeThreadId, setActiveThread }
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import ChatInstruction from '../ChatInstruction'
|
|
||||||
import ChatItem from '../ChatItem'
|
import ChatItem from '../ChatItem'
|
||||||
|
|
||||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
@ -8,11 +7,10 @@ import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
|||||||
const ChatBody: React.FC = () => {
|
const ChatBody: React.FC = () => {
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col-reverse overflow-y-auto">
|
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<ChatItem {...message} key={message.id} />
|
<ChatItem {...message} key={message.id} />
|
||||||
))}
|
))}
|
||||||
{messages.length === 0 && <ChatInstruction />}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
ChatCompletionRole,
|
|
||||||
EventName,
|
|
||||||
MessageStatus,
|
|
||||||
ThreadMessage,
|
|
||||||
events,
|
|
||||||
} from '@janhq/core'
|
|
||||||
|
|
||||||
import { Button, Textarea } from '@janhq/uikit'
|
|
||||||
import { useAtomValue } from 'jotai'
|
|
||||||
|
|
||||||
import { getActiveConvoIdAtom } from '@/helpers/atoms/Conversation.atom'
|
|
||||||
|
|
||||||
const ChatInstruction = () => {
|
|
||||||
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
|
||||||
const [isSettingInstruction, setIsSettingInstruction] = useState(false)
|
|
||||||
const [instruction, setInstruction] = useState('')
|
|
||||||
const setSystemPrompt = (instruction: string) => {
|
|
||||||
const message: ThreadMessage = {
|
|
||||||
id: 'system-prompt',
|
|
||||||
content: instruction,
|
|
||||||
role: ChatCompletionRole.System,
|
|
||||||
status: MessageStatus.Ready,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
threadId: activeConvoId,
|
|
||||||
}
|
|
||||||
events.emit(EventName.OnNewMessageResponse, message)
|
|
||||||
events.emit(EventName.OnMessageResponseFinished, message)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="mx-auto mb-20 flex flex-col space-y-2">
|
|
||||||
<p>(Optional) Give your assistant an initial prompt.</p>
|
|
||||||
{!isSettingInstruction && activeConvoId && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
themes={'outline'}
|
|
||||||
className="w-32"
|
|
||||||
onClick={() => setIsSettingInstruction(true)}
|
|
||||||
>
|
|
||||||
Set a Prompt
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isSettingInstruction && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Textarea
|
|
||||||
placeholder={`Enter your instructions`}
|
|
||||||
onChange={(e) => {
|
|
||||||
setInstruction(e.target.value)
|
|
||||||
}}
|
|
||||||
className="h-24"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
themes={'outline'}
|
|
||||||
className="w-32"
|
|
||||||
onClick={() => setSystemPrompt(instruction)}
|
|
||||||
>
|
|
||||||
Set a Prompt
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default ChatInstruction
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
import { Thread, Model } from '@janhq/core'
|
|
||||||
import { Button } from '@janhq/uikit'
|
|
||||||
import { motion as m } from 'framer-motion'
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
|
||||||
|
|
||||||
import { GalleryHorizontalEndIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
|
||||||
|
|
||||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
|
||||||
import { useCreateConversation } from '@/hooks/useCreateConversation'
|
|
||||||
import useGetUserConversations from '@/hooks/useGetUserConversations'
|
|
||||||
|
|
||||||
import { displayDate } from '@/utils/datetime'
|
|
||||||
|
|
||||||
import {
|
|
||||||
conversationStatesAtom,
|
|
||||||
getActiveConvoIdAtom,
|
|
||||||
setActiveConvoIdAtom,
|
|
||||||
userConversationsAtom,
|
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
|
||||||
|
|
||||||
export default function HistoryList() {
|
|
||||||
const conversations = useAtomValue(userConversationsAtom)
|
|
||||||
const threadStates = useAtomValue(conversationStatesAtom)
|
|
||||||
const { getUserConversations } = useGetUserConversations()
|
|
||||||
const { activeModel } = useActiveModel()
|
|
||||||
const { requestCreateConvo } = useCreateConversation()
|
|
||||||
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
|
||||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getUserConversations()
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleClickConversation = () => {
|
|
||||||
if (activeModel) requestCreateConvo(activeModel as Model)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleActiveModel = async (convo: Thread) => {
|
|
||||||
if (convo.modelId == null) {
|
|
||||||
console.debug('modelId is undefined')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (activeConvoId !== convo.id) {
|
|
||||||
setActiveConvoId(convo.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="sticky top-0 z-20 flex flex-col border-b border-border bg-background px-4 py-3">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
themes="secondary"
|
|
||||||
onClick={handleClickConversation}
|
|
||||||
disabled={!activeModel}
|
|
||||||
>
|
|
||||||
Create New Chat
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{conversations.length === 0 ? (
|
|
||||||
<div className="px-4 py-8 text-center">
|
|
||||||
<GalleryHorizontalEndIcon
|
|
||||||
size={26}
|
|
||||||
className="mx-auto mb-3 text-muted-foreground"
|
|
||||||
/>
|
|
||||||
<h2 className="font-semibold">No Chat History</h2>
|
|
||||||
<p className="mt-1 text-xs">Get started by creating a new chat</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
conversations.map((convo, i) => {
|
|
||||||
const lastMessage = threadStates[convo.id]?.lastMessage
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={twMerge(
|
|
||||||
'relative flex cursor-pointer flex-col border-b border-border px-4 py-2 hover:bg-secondary/20',
|
|
||||||
activeConvoId === convo.id && 'bg-secondary-10'
|
|
||||||
)}
|
|
||||||
onClick={() => handleActiveModel(convo as Thread)}
|
|
||||||
>
|
|
||||||
<p className="mb-1 line-clamp-1 text-xs leading-5">
|
|
||||||
{convo.updatedAt &&
|
|
||||||
displayDate(new Date(convo.updatedAt).getTime())}
|
|
||||||
</p>
|
|
||||||
<h2 className="line-clamp-1">{convo.summary}</h2>
|
|
||||||
<p className="mt-1 line-clamp-2 text-xs">
|
|
||||||
{/* TODO: Check latest message update */}
|
|
||||||
{lastMessage && lastMessage.length > 0
|
|
||||||
? lastMessage
|
|
||||||
: 'No new message'}
|
|
||||||
</p>
|
|
||||||
{activeModel && activeConvoId === convo.id && (
|
|
||||||
<m.div
|
|
||||||
className="absolute right-0 top-0 h-full w-1 bg-primary/50"
|
|
||||||
layoutId="active-convo"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChatCompletionRole,
|
ChatCompletionRole,
|
||||||
ChatCompletionMessage,
|
ChatCompletionMessage,
|
||||||
@ -9,22 +11,33 @@ import {
|
|||||||
events,
|
events,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins'
|
import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { atom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { RefreshCcw, ClipboardCopy, Trash2Icon, StopCircle } from 'lucide-react'
|
import { RefreshCcw, ClipboardCopy, Trash2Icon, StopCircle } from 'lucide-react'
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import { toaster } from '@/containers/Toast'
|
import { toaster } from '@/containers/Toast'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deleteMessage,
|
deleteMessageAtom,
|
||||||
getCurrentChatMessagesAtom,
|
getCurrentChatMessagesAtom,
|
||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
|
import {
|
||||||
|
activeThreadAtom,
|
||||||
|
threadStatesAtom,
|
||||||
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
import { pluginManager } from '@/plugin'
|
import { pluginManager } from '@/plugin'
|
||||||
|
|
||||||
const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
||||||
const deleteAMessage = useSetAtom(deleteMessage)
|
const deleteMessage = useSetAtom(deleteMessageAtom)
|
||||||
const thread = useAtomValue(currentConversationAtom)
|
const thread = useAtomValue(activeThreadAtom)
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
|
const threadStateAtom = useMemo(
|
||||||
|
() => atom((get) => get(threadStatesAtom)[thread?.id ?? '']),
|
||||||
|
[thread?.id]
|
||||||
|
)
|
||||||
|
const threadState = useAtomValue(threadStateAtom)
|
||||||
|
|
||||||
const stopInference = async () => {
|
const stopInference = async () => {
|
||||||
await pluginManager
|
await pluginManager
|
||||||
.get<InferencePlugin>(PluginType.Inference)
|
.get<InferencePlugin>(PluginType.Inference)
|
||||||
@ -33,8 +46,14 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
|||||||
events.emit(EventName.OnMessageResponseFinished, message)
|
events.emit(EventName.OnMessageResponseFinished, message)
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center">
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'flex-row items-center',
|
||||||
|
threadState.waitingForResponse ? 'hidden' : 'flex'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex overflow-hidden rounded-md border border-border bg-background/20">
|
<div className="flex overflow-hidden rounded-md border border-border bg-background/20">
|
||||||
{message.status === MessageStatus.Pending && (
|
{message.status === MessageStatus.Pending && (
|
||||||
<div
|
<div
|
||||||
@ -45,25 +64,20 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{message.status !== MessageStatus.Pending &&
|
{message.status !== MessageStatus.Pending &&
|
||||||
message.id === messages[0]?.id && (
|
message.id === messages[messages.length - 1]?.id && (
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer border-r border-border px-2 py-2 hover:bg-background/80"
|
className="cursor-pointer border-r border-border px-2 py-2 hover:bg-background/80"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const messageRequest: MessageRequest = {
|
const messageRequest: MessageRequest = {
|
||||||
id: message.id ?? '',
|
id: message.id ?? '',
|
||||||
messages: messages
|
messages: messages.slice(0, -1).map((e) => {
|
||||||
.slice(1, messages.length)
|
const msg: ChatCompletionMessage = {
|
||||||
.reverse()
|
|
||||||
.map((e) => {
|
|
||||||
return {
|
|
||||||
content: e.content,
|
|
||||||
role: e.role,
|
role: e.role,
|
||||||
} as ChatCompletionMessage
|
content: e.content[0].text.value,
|
||||||
}),
|
|
||||||
threadId: message.threadId ?? '',
|
|
||||||
}
|
}
|
||||||
if (message.role === ChatCompletionRole.Assistant) {
|
return msg
|
||||||
deleteAMessage(message.id ?? '')
|
}),
|
||||||
|
threadId: message.thread_id ?? '',
|
||||||
}
|
}
|
||||||
events.emit(EventName.OnNewMessageRequest, messageRequest)
|
events.emit(EventName.OnNewMessageRequest, messageRequest)
|
||||||
}}
|
}}
|
||||||
@ -74,7 +88,7 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
|||||||
<div
|
<div
|
||||||
className="cursor-pointer border-r border-border px-2 py-2 hover:bg-background/80"
|
className="cursor-pointer border-r border-border px-2 py-2 hover:bg-background/80"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(message.content ?? '')
|
navigator.clipboard.writeText(message.content[0]?.text?.value ?? '')
|
||||||
toaster({
|
toaster({
|
||||||
title: 'Copied to clipboard',
|
title: 'Copied to clipboard',
|
||||||
})
|
})
|
||||||
@ -85,14 +99,14 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
|||||||
<div
|
<div
|
||||||
className="cursor-pointer px-2 py-2 hover:bg-background/80"
|
className="cursor-pointer px-2 py-2 hover:bg-background/80"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
deleteAMessage(message.id ?? '')
|
deleteMessage(message.id ?? '')
|
||||||
if (thread)
|
if (thread)
|
||||||
await pluginManager
|
await pluginManager
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
?.saveConversation({
|
?.writeMessages(
|
||||||
...thread,
|
thread.id,
|
||||||
messages: messages.filter((e) => e.id !== message.id),
|
messages.filter((msg) => msg.id !== message.id)
|
||||||
})
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2Icon size={14} />
|
<Trash2Icon size={14} />
|
||||||
|
|||||||
122
web/screens/Chat/Sidebar/index.tsx
Normal file
122
web/screens/Chat/Sidebar/index.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
import { getUserSpace, openFileExplorer } from '@janhq/core'
|
||||||
|
import { atom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import CardSidebar from '@/containers/CardSidebar'
|
||||||
|
import DropdownListSidebar, {
|
||||||
|
selectedModelAtom,
|
||||||
|
} from '@/containers/DropdownListSidebar'
|
||||||
|
import ItemCardSidebar from '@/containers/ItemCardSidebar'
|
||||||
|
|
||||||
|
import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
|
export const showRightSideBarAtom = atom<boolean>(false)
|
||||||
|
|
||||||
|
export default function Sidebar() {
|
||||||
|
const showing = useAtomValue(showRightSideBarAtom)
|
||||||
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
|
const selectedModel = useAtomValue(selectedModelAtom)
|
||||||
|
|
||||||
|
const onReviewInFinderClick = async (type: string) => {
|
||||||
|
if (!activeThread) return
|
||||||
|
if (!activeThread.isFinishInit) {
|
||||||
|
alert('Thread is not ready')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userSpace = await getUserSpace()
|
||||||
|
let filePath = undefined
|
||||||
|
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||||
|
switch (type) {
|
||||||
|
case 'Thread':
|
||||||
|
filePath = join('threads', activeThread.id)
|
||||||
|
break
|
||||||
|
case 'Model':
|
||||||
|
if (!selectedModel) return
|
||||||
|
filePath = join('models', selectedModel.id)
|
||||||
|
break
|
||||||
|
case 'Assistant':
|
||||||
|
if (!assistantId) return
|
||||||
|
filePath = join('assistants', assistantId)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filePath) return
|
||||||
|
|
||||||
|
const fullPath = join(userSpace, filePath)
|
||||||
|
console.log(fullPath)
|
||||||
|
openFileExplorer(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onViewJsonClick = async (type: string) => {
|
||||||
|
if (!activeThread) return
|
||||||
|
if (!activeThread.isFinishInit) {
|
||||||
|
alert('Thread is not ready')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userSpace = await getUserSpace()
|
||||||
|
let filePath = undefined
|
||||||
|
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||||
|
switch (type) {
|
||||||
|
case 'Thread':
|
||||||
|
filePath = join('threads', activeThread.id, 'thread.json')
|
||||||
|
break
|
||||||
|
case 'Model':
|
||||||
|
if (!selectedModel) return
|
||||||
|
filePath = join('models', selectedModel.id, 'model.json')
|
||||||
|
break
|
||||||
|
case 'Assistant':
|
||||||
|
if (!assistantId) return
|
||||||
|
filePath = join('assistants', assistantId, 'assistant.json')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filePath) return
|
||||||
|
|
||||||
|
const fullPath = join(userSpace, filePath)
|
||||||
|
console.log(fullPath)
|
||||||
|
openFileExplorer(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`h-full overflow-x-hidden border-l border-border duration-300 ease-linear ${
|
||||||
|
showing ? 'w-80' : 'w-0'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1 p-2">
|
||||||
|
<CardSidebar
|
||||||
|
title="Thread"
|
||||||
|
onRevealInFinderClick={onReviewInFinderClick}
|
||||||
|
onViewJsonClick={onViewJsonClick}
|
||||||
|
>
|
||||||
|
<ItemCardSidebar description={activeThread?.id} title="Thread ID" />
|
||||||
|
<ItemCardSidebar title="Thread title" />
|
||||||
|
</CardSidebar>
|
||||||
|
<CardSidebar
|
||||||
|
title="Assistant"
|
||||||
|
onRevealInFinderClick={onReviewInFinderClick}
|
||||||
|
onViewJsonClick={onViewJsonClick}
|
||||||
|
>
|
||||||
|
<ItemCardSidebar
|
||||||
|
description={activeThread?.assistants[0].assistant_name ?? ''}
|
||||||
|
title="Assistant"
|
||||||
|
/>
|
||||||
|
</CardSidebar>
|
||||||
|
<CardSidebar
|
||||||
|
title="Model"
|
||||||
|
onRevealInFinderClick={onReviewInFinderClick}
|
||||||
|
onViewJsonClick={onViewJsonClick}
|
||||||
|
>
|
||||||
|
<DropdownListSidebar />
|
||||||
|
</CardSidebar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -16,8 +16,6 @@ import LogoMark from '@/containers/Brand/Logo/Mark'
|
|||||||
|
|
||||||
import BubbleLoader from '@/containers/Loader/Bubble'
|
import BubbleLoader from '@/containers/Loader/Bubble'
|
||||||
|
|
||||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
|
||||||
|
|
||||||
import { displayDate } from '@/utils/datetime'
|
import { displayDate } from '@/utils/datetime'
|
||||||
|
|
||||||
import MessageToolbar from '../MessageToolbar'
|
import MessageToolbar from '../MessageToolbar'
|
||||||
@ -50,7 +48,12 @@ const marked = new Marked(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||||
const parsedText = marked.parse(props.content ?? '')
|
let text = ''
|
||||||
|
if (props.content && props.content.length > 0) {
|
||||||
|
text = props.content[0]?.text?.value ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedText = marked.parse(text)
|
||||||
const isUser = props.role === ChatCompletionRole.User
|
const isUser = props.role === ChatCompletionRole.User
|
||||||
const isSystem = props.role === ChatCompletionRole.System
|
const isSystem = props.role === ChatCompletionRole.System
|
||||||
const [tokenCount, setTokenCount] = useState(0)
|
const [tokenCount, setTokenCount] = useState(0)
|
||||||
@ -66,7 +69,8 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
const currentTimestamp = new Date().getTime() // Get current time in milliseconds
|
const currentTimestamp = new Date().getTime() // Get current time in milliseconds
|
||||||
if (!lastTimestamp) {
|
if (!lastTimestamp) {
|
||||||
// If this is the first update, just set the lastTimestamp and return
|
// If this is the first update, just set the lastTimestamp and return
|
||||||
if (props.content !== '') setLastTimestamp(currentTimestamp)
|
if (props.content[0]?.text?.value !== '')
|
||||||
|
setLastTimestamp(currentTimestamp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,11 +93,11 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
>
|
>
|
||||||
{!isUser && !isSystem && <LogoMark width={20} />}
|
{!isUser && !isSystem && <LogoMark width={20} />}
|
||||||
<div className="text-sm font-extrabold capitalize">{props.role}</div>
|
<div className="text-sm font-extrabold capitalize">{props.role}</div>
|
||||||
<p className="text-xs font-medium">{displayDate(props.createdAt)}</p>
|
<p className="text-xs font-medium">{displayDate(props.created)}</p>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'absolute right-0 cursor-pointer transition-all',
|
'absolute right-0 cursor-pointer transition-all',
|
||||||
messages[0].id === props.id
|
messages[messages.length - 1]?.id === props.id
|
||||||
? 'absolute -bottom-10 left-4'
|
? 'absolute -bottom-10 left-4'
|
||||||
: 'hidden group-hover:flex'
|
: 'hidden group-hover:flex'
|
||||||
)}
|
)}
|
||||||
@ -104,7 +108,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
|
|
||||||
<div className={twMerge('w-full')}>
|
<div className={twMerge('w-full')}>
|
||||||
{props.status === MessageStatus.Pending &&
|
{props.status === MessageStatus.Pending &&
|
||||||
(!props.content || props.content === '') ? (
|
(!props.content[0] || props.content[0].text.value === '') ? (
|
||||||
<BubbleLoader />
|
<BubbleLoader />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
97
web/screens/Chat/ThreadList/index.tsx
Normal file
97
web/screens/Chat/ThreadList/index.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@janhq/uikit'
|
||||||
|
import { motion as m } from 'framer-motion'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
import { GalleryHorizontalEndIcon } from 'lucide-react'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||||
|
|
||||||
|
import useGetAllThreads from '@/hooks/useGetAllThreads'
|
||||||
|
import useGetAssistants from '@/hooks/useGetAssistants'
|
||||||
|
|
||||||
|
import useSetActiveThread from '@/hooks/useSetActiveThread'
|
||||||
|
|
||||||
|
import { displayDate } from '@/utils/datetime'
|
||||||
|
|
||||||
|
import {
|
||||||
|
threadStatesAtom,
|
||||||
|
threadsAtom,
|
||||||
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
|
export default function ThreadList() {
|
||||||
|
const threads = useAtomValue(threadsAtom)
|
||||||
|
const threadStates = useAtomValue(threadStatesAtom)
|
||||||
|
const { requestCreateNewThread } = useCreateNewThread()
|
||||||
|
const { assistants } = useGetAssistants()
|
||||||
|
const { getAllThreads } = useGetAllThreads()
|
||||||
|
|
||||||
|
const { activeThreadId, setActiveThread: onThreadClick } =
|
||||||
|
useSetActiveThread()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAllThreads()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onCreateConversationClick = async () => {
|
||||||
|
if (assistants.length === 0) {
|
||||||
|
alert('No assistant available')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestCreateNewThread(assistants[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="sticky top-0 z-20 flex flex-col border-b border-border bg-background px-4 py-3">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
themes="secondary"
|
||||||
|
onClick={onCreateConversationClick}
|
||||||
|
>
|
||||||
|
Create New Chat
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{threads.length === 0 ? (
|
||||||
|
<div className="px-4 py-8 text-center">
|
||||||
|
<GalleryHorizontalEndIcon
|
||||||
|
size={26}
|
||||||
|
className="mx-auto mb-3 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<h2 className="font-semibold">No Chat History</h2>
|
||||||
|
<p className="mt-1 text-xs">Get started by creating a new chat</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
threads.map((thread, i) => {
|
||||||
|
const lastMessage = threadStates[thread.id]?.lastMessage ?? ''
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={twMerge(
|
||||||
|
'relative flex cursor-pointer flex-col border-b border-border px-4 py-2 hover:bg-secondary/20',
|
||||||
|
activeThreadId === thread.id && 'bg-secondary-10'
|
||||||
|
)}
|
||||||
|
onClick={() => onThreadClick(thread)}
|
||||||
|
>
|
||||||
|
<p className="mb-1 line-clamp-1 text-xs leading-5">
|
||||||
|
{thread.updated &&
|
||||||
|
displayDate(new Date(thread.updated).getTime())}
|
||||||
|
</p>
|
||||||
|
<h2 className="line-clamp-1">{thread.title}</h2>
|
||||||
|
<p className="mt-1 line-clamp-2 text-xs">{lastMessage}</p>
|
||||||
|
{activeThreadId === thread.id && (
|
||||||
|
<m.div
|
||||||
|
className="absolute right-0 top-0 h-full w-1 bg-primary/50"
|
||||||
|
layoutId="active-convo"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { Fragment, useContext, useEffect, useRef, useState } from 'react'
|
import { Fragment, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { Model } from '@janhq/core/lib/types'
|
|
||||||
import { Button, Badge, Textarea } from '@janhq/uikit'
|
import { Button, Badge, Textarea } from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
@ -12,115 +11,71 @@ import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
|||||||
|
|
||||||
import ShortCut from '@/containers/Shortcut'
|
import ShortCut from '@/containers/Shortcut'
|
||||||
|
|
||||||
import { toaster } from '@/containers/Toast'
|
|
||||||
|
|
||||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
|
||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||||
|
import useDeleteThread from '@/hooks/useDeleteConversation'
|
||||||
import { useCreateConversation } from '@/hooks/useCreateConversation'
|
|
||||||
import useDeleteConversation from '@/hooks/useDeleteConversation'
|
|
||||||
|
|
||||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
|
|
||||||
import useGetUserConversations from '@/hooks/useGetUserConversations'
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||||
|
|
||||||
import useSendChatMessage from '@/hooks/useSendChatMessage'
|
import useSendChatMessage from '@/hooks/useSendChatMessage'
|
||||||
|
|
||||||
import ChatBody from '@/screens/Chat/ChatBody'
|
import ChatBody from '@/screens/Chat/ChatBody'
|
||||||
|
|
||||||
import HistoryList from '@/screens/Chat/HistoryList'
|
import ThreadList from '@/screens/Chat/ThreadList'
|
||||||
|
|
||||||
|
import Sidebar from './Sidebar'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
currentConversationAtom,
|
activeThreadAtom,
|
||||||
getActiveConvoIdAtom,
|
getActiveThreadIdAtom,
|
||||||
userConversationsAtom,
|
threadsAtom,
|
||||||
waitingToSendMessage,
|
waitingToSendMessage,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
import { currentConvoStateAtom } from '@/helpers/atoms/Conversation.atom'
|
import { activeThreadStateAtom } from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
const ChatScreen = () => {
|
const ChatScreen = () => {
|
||||||
const currentConvo = useAtomValue(currentConversationAtom)
|
const currentConvo = useAtomValue(activeThreadAtom)
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
const { downloadedModels } = useGetDownloadedModels()
|
||||||
const { deleteConvo, cleanConvo } = useDeleteConversation()
|
const { deleteThread, cleanThread } = useDeleteThread()
|
||||||
const { activeModel, stateModel } = useActiveModel()
|
const { activeModel, stateModel } = useActiveModel()
|
||||||
const { setMainViewState } = useMainViewState()
|
const { setMainViewState } = useMainViewState()
|
||||||
|
|
||||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
||||||
const currentConvoState = useAtomValue(currentConvoStateAtom)
|
const currentConvoState = useAtomValue(activeThreadStateAtom)
|
||||||
const { sendChatMessage } = useSendChatMessage()
|
const { sendChatMessage } = useSendChatMessage()
|
||||||
const isWaitingForResponse = currentConvoState?.waitingForResponse ?? false
|
const isWaitingForResponse = currentConvoState?.waitingForResponse ?? false
|
||||||
const disabled = currentPrompt.trim().length === 0 || isWaitingForResponse
|
const disabled = currentPrompt.trim().length === 0 || isWaitingForResponse
|
||||||
const activeConversationId = useAtomValue(getActiveConvoIdAtom)
|
|
||||||
|
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
|
const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
|
||||||
const { requestCreateConvo } = useCreateConversation()
|
const conversations = useAtomValue(threadsAtom)
|
||||||
const { getUserConversations } = useGetUserConversations()
|
|
||||||
const conversations = useAtomValue(userConversationsAtom)
|
|
||||||
const isEnableChat = (currentConvo && activeModel) || conversations.length > 0
|
const isEnableChat = (currentConvo && activeModel) || conversations.length > 0
|
||||||
|
|
||||||
const [isModelAvailable, setIsModelAvailable] = useState(
|
const [isModelAvailable, setIsModelAvailable] = useState(
|
||||||
downloadedModels.some((x) => x.id === currentConvo?.modelId)
|
true
|
||||||
|
// downloadedModels.some((x) => x.id === currentConvo?.modelId)
|
||||||
)
|
)
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const { startModel } = useActiveModel()
|
|
||||||
const modelRef = useRef(activeModel)
|
const modelRef = useRef(activeModel)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
modelRef.current = activeModel
|
modelRef.current = activeModel
|
||||||
}, [activeModel])
|
}, [activeModel])
|
||||||
|
|
||||||
useEffect(() => {
|
const onPromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
getUserConversations()
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleMessageChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
setCurrentPrompt(e.target.value)
|
setCurrentPrompt(e.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsModelAvailable(
|
if (isWaitingToSend && activeThreadId) {
|
||||||
downloadedModels.some((x) => x.id === currentConvo?.modelId)
|
|
||||||
)
|
|
||||||
}, [currentConvo, downloadedModels])
|
|
||||||
|
|
||||||
const handleSendMessage = async () => {
|
|
||||||
if (!activeModel || activeModel.id !== currentConvo?.modelId) {
|
|
||||||
const model = downloadedModels.find((e) => e.id === currentConvo?.modelId)
|
|
||||||
|
|
||||||
// Model is available to start
|
|
||||||
if (model != null) {
|
|
||||||
toaster({
|
|
||||||
title: 'Message queued.',
|
|
||||||
description: 'It will be sent once the model is done loading.',
|
|
||||||
})
|
|
||||||
startModel(model.id).then(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (modelRef?.current?.id === currentConvo?.modelId)
|
|
||||||
sendChatMessage()
|
|
||||||
}, 300)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (activeConversationId) {
|
|
||||||
sendChatMessage()
|
|
||||||
} else {
|
|
||||||
setIsWaitingToSend(true)
|
|
||||||
await requestCreateConvo(activeModel as Model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isWaitingToSend && activeConversationId) {
|
|
||||||
setIsWaitingToSend(false)
|
setIsWaitingToSend(false)
|
||||||
sendChatMessage()
|
sendChatMessage()
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [waitingToSendMessage, activeConversationId])
|
}, [waitingToSendMessage, activeThreadId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textareaRef.current !== null) {
|
if (textareaRef.current !== null) {
|
||||||
@ -136,11 +91,11 @@ const ChatScreen = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentPrompt])
|
}, [currentPrompt])
|
||||||
|
|
||||||
const handleKeyDown = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const onKeyDown = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
if (!e.shiftKey) {
|
if (!e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleSendMessage()
|
sendChatMessage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,14 +103,14 @@ const ChatScreen = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
<div className="flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r border-border">
|
<div className="flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r border-border">
|
||||||
<HistoryList />
|
<ThreadList />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex h-full w-[calc(100%-256px)] flex-col bg-muted/10">
|
<div className="relative flex h-full w-[calc(100%-256px)] flex-col bg-muted/10">
|
||||||
<div className="flex h-full w-full flex-col justify-between">
|
<div className="flex h-full w-full flex-col justify-between">
|
||||||
{isEnableChat && currentConvo && (
|
{isEnableChat && currentConvo && (
|
||||||
<div className="h-[53px] flex-shrink-0 border-b border-border bg-background p-4">
|
<div className="h-[53px] flex-shrink-0 border-b border-border bg-background p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>{currentConvo?.summary ?? ''}</span>
|
<span>{currentConvo.title}</span>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'flex items-center space-x-3',
|
'flex items-center space-x-3',
|
||||||
@ -167,9 +122,9 @@ const ChatScreen = () => {
|
|||||||
themes="secondary"
|
themes="secondary"
|
||||||
className="relative z-10"
|
className="relative z-10"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
setMainViewState(MainViewState.ExploreModels)
|
setMainViewState(MainViewState.ExploreModels)
|
||||||
}}
|
}
|
||||||
>
|
>
|
||||||
Download Model
|
Download Model
|
||||||
</Button>
|
</Button>
|
||||||
@ -177,12 +132,12 @@ const ChatScreen = () => {
|
|||||||
<Paintbrush
|
<Paintbrush
|
||||||
size={16}
|
size={16}
|
||||||
className="cursor-pointer text-muted-foreground"
|
className="cursor-pointer text-muted-foreground"
|
||||||
onClick={() => cleanConvo()}
|
onClick={() => cleanThread()}
|
||||||
/>
|
/>
|
||||||
<Trash2Icon
|
<Trash2Icon
|
||||||
size={16}
|
size={16}
|
||||||
className="cursor-pointer text-muted-foreground"
|
className="cursor-pointer text-muted-foreground"
|
||||||
onClick={() => deleteConvo()}
|
onClick={() => deleteThread()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -225,25 +180,24 @@ const ChatScreen = () => {
|
|||||||
<Textarea
|
<Textarea
|
||||||
className="min-h-10 h-10 max-h-16 resize-none pr-20"
|
className="min-h-10 h-10 max-h-16 resize-none pr-20"
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
onKeyDown={(e) => handleKeyDown(e)}
|
onKeyDown={(e) => onKeyDown(e)}
|
||||||
placeholder="Type your message ..."
|
placeholder="Type your message ..."
|
||||||
disabled={stateModel.loading || !currentConvo}
|
disabled={stateModel.loading || !currentConvo}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => {
|
onChange={(e) => onPromptChange(e)}
|
||||||
handleMessageChange(e)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={disabled || stateModel.loading || !currentConvo}
|
disabled={disabled || stateModel.loading || !currentConvo}
|
||||||
themes={'primary'}
|
themes={'primary'}
|
||||||
onClick={handleSendMessage}
|
onClick={sendChatMessage}
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,8 +33,6 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { quantizationName, bits, maxRamRequired, usecase } = suitableModel
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -58,19 +56,15 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
|||||||
<div>
|
<div>
|
||||||
<span className="mb-1 font-semibold">Compatibility</span>
|
<span className="mb-1 font-semibold">Compatibility</span>
|
||||||
<div className="mt-1 flex gap-2">
|
<div className="mt-1 flex gap-2">
|
||||||
<Badge
|
|
||||||
themes="secondary"
|
|
||||||
className="line-clamp-1 max-w-[400px] lg:line-clamp-none lg:max-w-none"
|
|
||||||
title={usecase}
|
|
||||||
>
|
|
||||||
{usecase}
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
<Badge
|
||||||
themes="secondary"
|
themes="secondary"
|
||||||
className="line-clamp-1 lg:line-clamp-none"
|
className="line-clamp-1 lg:line-clamp-none"
|
||||||
title={`${toGigabytes(maxRamRequired)} RAM required`}
|
title={`${toGigabytes(
|
||||||
|
suitableModel.metadata.maxRamRequired
|
||||||
|
)} RAM required`}
|
||||||
>
|
>
|
||||||
{toGigabytes(maxRamRequired)} RAM required
|
{toGigabytes(suitableModel.metadata.maxRamRequired)} RAM
|
||||||
|
required
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -81,10 +75,11 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
|||||||
<span className="font-semibold">Version</span>
|
<span className="font-semibold">Version</span>
|
||||||
<div className="mt-2 flex space-x-2">
|
<div className="mt-2 flex space-x-2">
|
||||||
<Badge themes="outline">v{model.version}</Badge>
|
<Badge themes="outline">v{model.version}</Badge>
|
||||||
{quantizationName && (
|
{suitableModel.metadata.quantization && (
|
||||||
<Badge themes="outline">{quantizationName}</Badge>
|
<Badge themes="outline">
|
||||||
|
{suitableModel.metadata.quantization}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<Badge themes="outline">{`${bits} Bits`}</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -113,8 +108,7 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
|||||||
|
|
||||||
{show && (
|
{show && (
|
||||||
<ModelVersionList
|
<ModelVersionList
|
||||||
model={model}
|
models={model.availableVersions}
|
||||||
versions={model.availableVersions}
|
|
||||||
recommendedVersion={suitableModel?.name ?? ''}
|
recommendedVersion={suitableModel?.name ?? ''}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
import { Model, ModelCatalog } from '@janhq/core/lib/types'
|
||||||
import { Badge, Button } from '@janhq/uikit'
|
import { Badge, Button } from '@janhq/uikit'
|
||||||
|
|
||||||
import { atom, useAtomValue } from 'jotai'
|
import { atom, useAtomValue } from 'jotai'
|
||||||
@ -23,7 +23,7 @@ import { toGigabytes } from '@/utils/converter'
|
|||||||
import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
|
import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
suitableModel: ModelVersion
|
suitableModel: Model
|
||||||
exploreModel: ModelCatalog
|
exploreModel: ModelCatalog
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
|||||||
const { setMainViewState } = useMainViewState()
|
const { setMainViewState } = useMainViewState()
|
||||||
|
|
||||||
const calculatePerformance = useCallback(
|
const calculatePerformance = useCallback(
|
||||||
(suitableModel: ModelVersion) => async () => {
|
(suitableModel: Model) => async () => {
|
||||||
const { title, performanceTag } = await getPerformanceForModel(
|
const { title, performanceTag } = await getPerformanceForModel(
|
||||||
suitableModel,
|
suitableModel,
|
||||||
totalRam
|
totalRam
|
||||||
@ -64,9 +64,9 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
|||||||
}, [suitableModel])
|
}, [suitableModel])
|
||||||
|
|
||||||
const onDownloadClick = useCallback(() => {
|
const onDownloadClick = useCallback(() => {
|
||||||
downloadModel(exploreModel, suitableModel)
|
downloadModel(suitableModel)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [exploreModel, suitableModel])
|
}, [suitableModel])
|
||||||
|
|
||||||
// TODO: Comparing between Model Id and Version Name?
|
// TODO: Comparing between Model Id and Version Name?
|
||||||
const isDownloaded =
|
const isDownloaded =
|
||||||
@ -74,8 +74,8 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
|||||||
|
|
||||||
let downloadButton = (
|
let downloadButton = (
|
||||||
<Button onClick={() => onDownloadClick()}>
|
<Button onClick={() => onDownloadClick()}>
|
||||||
{suitableModel.size
|
{suitableModel.metadata.size
|
||||||
? `Download (${toGigabytes(suitableModel.size)})`
|
? `Download (${toGigabytes(suitableModel.metadata.size)})`
|
||||||
: 'Download'}
|
: 'Download'}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
|
|
||||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
import { Model } from '@janhq/core/lib/types'
|
||||||
import { Button, Badge } from '@janhq/uikit'
|
import { Badge, Button } from '@janhq/uikit'
|
||||||
|
|
||||||
import { atom, useAtomValue } from 'jotai'
|
import { atom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import ModalCancelDownload from '@/containers/ModalCancelDownload'
|
import ModalCancelDownload from '@/containers/ModalCancelDownload'
|
||||||
@ -18,28 +17,29 @@ import { useMainViewState } from '@/hooks/useMainViewState'
|
|||||||
import { toGigabytes } from '@/utils/converter'
|
import { toGigabytes } from '@/utils/converter'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
model: ModelCatalog
|
model: Model
|
||||||
modelVersion: ModelVersion
|
|
||||||
isRecommended: boolean
|
isRecommended: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
|
const ModelVersionItem: React.FC<Props> = ({ model }) => {
|
||||||
const { downloadModel } = useDownloadModel()
|
const { downloadModel } = useDownloadModel()
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
const { downloadedModels } = useGetDownloadedModels()
|
||||||
const { setMainViewState } = useMainViewState()
|
const { setMainViewState } = useMainViewState()
|
||||||
const isDownloaded =
|
const isDownloaded =
|
||||||
downloadedModels.find((model) => model.id === modelVersion.name) != null
|
downloadedModels.find(
|
||||||
|
(downloadedModel) => downloadedModel.id === model.id
|
||||||
|
) != null
|
||||||
|
|
||||||
const { modelDownloadStateAtom, downloadStates } = useDownloadState()
|
const { modelDownloadStateAtom, downloadStates } = useDownloadState()
|
||||||
|
|
||||||
const downloadAtom = useMemo(
|
const downloadAtom = useMemo(
|
||||||
() => atom((get) => get(modelDownloadStateAtom)[modelVersion.name ?? '']),
|
() => atom((get) => get(modelDownloadStateAtom)[model.id ?? '']),
|
||||||
[modelVersion.name]
|
[model.id]
|
||||||
)
|
)
|
||||||
const downloadState = useAtomValue(downloadAtom)
|
const downloadState = useAtomValue(downloadAtom)
|
||||||
|
|
||||||
const onDownloadClick = () => {
|
const onDownloadClick = () => {
|
||||||
downloadModel(model, modelVersion)
|
downloadModel(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
let downloadButton = (
|
let downloadButton = (
|
||||||
@ -63,36 +63,26 @@ const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (downloadState != null && downloadStates.length > 0) {
|
if (downloadState != null && downloadStates.length > 0) {
|
||||||
downloadButton = (
|
downloadButton = <ModalCancelDownload suitableModel={model} isFromList />
|
||||||
<ModalCancelDownload suitableModel={modelVersion} isFromList />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { maxRamRequired, usecase } = modelVersion
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-4 border-t border-border pb-3 pl-3 pr-4 pt-3 first:border-t-0">
|
<div className="flex items-center justify-between gap-4 border-t border-border pb-3 pl-3 pr-4 pt-3 first:border-t-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="line-clamp-1 flex-1" title={modelVersion.name}>
|
<span className="line-clamp-1 flex-1" title={model.name}>
|
||||||
{modelVersion.name}
|
{model.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Badge
|
|
||||||
themes="secondary"
|
|
||||||
className="line-clamp-1 max-w-[240px] lg:line-clamp-none lg:max-w-none"
|
|
||||||
title={usecase}
|
|
||||||
>
|
|
||||||
{usecase}
|
|
||||||
</Badge>
|
|
||||||
|
|
||||||
<Badge
|
<Badge
|
||||||
themes="secondary"
|
themes="secondary"
|
||||||
className="line-clamp-1"
|
className="line-clamp-1"
|
||||||
title={`${toGigabytes(maxRamRequired)} RAM required`}
|
title={`${toGigabytes(model.metadata.maxRamRequired)} RAM required`}
|
||||||
>{`${toGigabytes(maxRamRequired)} RAM required`}</Badge>
|
>{`${toGigabytes(
|
||||||
<Badge themes="secondary">{toGigabytes(modelVersion.size)}</Badge>
|
model.metadata.maxRamRequired
|
||||||
|
)} RAM required`}</Badge>
|
||||||
|
<Badge themes="secondary">{toGigabytes(model.metadata.size)}</Badge>
|
||||||
</div>
|
</div>
|
||||||
{downloadButton}
|
{downloadButton}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,26 +1,23 @@
|
|||||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
import { Model } from '@janhq/core/lib/types'
|
||||||
|
|
||||||
import ModelVersionItem from '../ModelVersionItem'
|
import ModelVersionItem from '../ModelVersionItem'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
model: ModelCatalog
|
models: Model[]
|
||||||
versions: ModelVersion[]
|
|
||||||
recommendedVersion: string
|
recommendedVersion: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ModelVersionList({
|
export default function ModelVersionList({
|
||||||
model,
|
models,
|
||||||
versions,
|
|
||||||
recommendedVersion,
|
recommendedVersion,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
{versions.map((item) => (
|
{models.map((model) => (
|
||||||
<ModelVersionItem
|
<ModelVersionItem
|
||||||
key={item.name}
|
key={model.name}
|
||||||
model={model}
|
model={model}
|
||||||
modelVersion={item}
|
isRecommended={model.name === recommendedVersion}
|
||||||
isRecommended={item.name === recommendedVersion}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -64,28 +64,28 @@ const MyModelsScreen = () => {
|
|||||||
<div className="inline-flex rounded-full border border-border p-1">
|
<div className="inline-flex rounded-full border border-border p-1">
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
src={model.avatarUrl}
|
src={model.metadata.avatarUrl}
|
||||||
alt={model.author}
|
alt={model.metadata.author}
|
||||||
/>
|
/>
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{model.author.charAt(0)}
|
{model.metadata.author.charAt(0)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="mb-1 font-medium capitalize">
|
<h2 className="mb-1 font-medium capitalize">
|
||||||
{model.author}
|
{model.metadata.author}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="line-clamp-1">{model.name}</p>
|
<p className="line-clamp-1">{model.name}</p>
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<Badge themes="secondary">v{model.version}</Badge>
|
<Badge themes="secondary">v{model.version}</Badge>
|
||||||
<Badge themes="outline">GGUF</Badge>
|
<Badge themes="outline">GGUF</Badge>
|
||||||
<Badge themes="outline">
|
<Badge themes="outline">
|
||||||
{toGigabytes(model.size)}
|
{toGigabytes(model.metadata.size)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 line-clamp-2 break-all">
|
<p className="mt-2 line-clamp-2 break-all">
|
||||||
{model.longDescription}
|
{model.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -102,7 +102,7 @@ const MyModelsScreen = () => {
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p className="leading-relaxed">
|
<p className="leading-relaxed">
|
||||||
Delete model {model.name}, v{model.version},{' '}
|
Delete model {model.name}, v{model.version},{' '}
|
||||||
{toGigabytes(model.size)}.
|
{toGigabytes(model.metadata.size)}.
|
||||||
</p>
|
</p>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
|
|||||||
6
web/types/conversation.d.ts
vendored
6
web/types/conversation.d.ts
vendored
@ -1,6 +0,0 @@
|
|||||||
export type ThreadState = {
|
|
||||||
hasMore: boolean
|
|
||||||
waitingForResponse: boolean
|
|
||||||
error?: Error
|
|
||||||
lastMessage?: string
|
|
||||||
}
|
|
||||||
@ -1,3 +1,3 @@
|
|||||||
export const generateConversationId = () => {
|
export const generateThreadId = (assistantId: string) => {
|
||||||
return `jan-${(Date.now() / 1000).toFixed(0)}`
|
return `${assistantId}_${(Date.now() / 1000).toFixed(0)}`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { ModelCatalog } from '@janhq/core'
|
import { ModelCatalog, ModelState } from '@janhq/core'
|
||||||
|
|
||||||
export const dummyModel: ModelCatalog = {
|
export const dummyModel: ModelCatalog = {
|
||||||
id: 'aladar/TinyLLama-v0-GGUF',
|
id: 'aladar/TinyLLama-v0-GGUF',
|
||||||
@ -14,37 +14,106 @@ export const dummyModel: ModelCatalog = {
|
|||||||
tags: ['freeform', 'tags'],
|
tags: ['freeform', 'tags'],
|
||||||
availableVersions: [
|
availableVersions: [
|
||||||
{
|
{
|
||||||
name: 'TinyLLama-v0.Q8_0.gguf',
|
object: 'model',
|
||||||
quantizationName: '',
|
version: '1.0.0',
|
||||||
bits: 2,
|
source_url:
|
||||||
size: 5816320,
|
|
||||||
maxRamRequired: 256000000,
|
|
||||||
usecase:
|
|
||||||
'smallest, significant quality loss - not recommended for most purposes',
|
|
||||||
downloadLink:
|
|
||||||
'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.Q8_0.gguf',
|
'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.Q8_0.gguf',
|
||||||
|
id: 'TinyLLama-v0.Q8_0.gguf',
|
||||||
|
name: 'TinyLLama-v0.Q8_0.gguf',
|
||||||
|
owned_by: 'you',
|
||||||
|
created: 0,
|
||||||
|
description: '',
|
||||||
|
state: ModelState.ToDownload,
|
||||||
|
settings: {
|
||||||
|
ctx_len: 2048,
|
||||||
|
ngl: 100,
|
||||||
|
embedding: true,
|
||||||
|
n_parallel: 4,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
temperature: 0.7,
|
||||||
|
token_limit: 2048,
|
||||||
|
top_k: 0,
|
||||||
|
top_p: 1,
|
||||||
|
stream: true,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
engine: '',
|
||||||
|
quantization: '',
|
||||||
|
size: 5816320,
|
||||||
|
binaries: [],
|
||||||
|
maxRamRequired: 256000000,
|
||||||
|
author: 'aladar',
|
||||||
|
avatarUrl: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'TinyLLama-v0.f16.gguf',
|
object: 'model',
|
||||||
quantizationName: '',
|
version: '1.0.0',
|
||||||
bits: 2,
|
source_url:
|
||||||
size: 10240000,
|
|
||||||
maxRamRequired: 256000000,
|
|
||||||
usecase:
|
|
||||||
'smallest, significant quality loss - not recommended for most purposes',
|
|
||||||
downloadLink:
|
|
||||||
'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.f16.gguf',
|
'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.f16.gguf',
|
||||||
|
id: 'TinyLLama-v0.f16.gguf',
|
||||||
|
name: 'TinyLLama-v0.f16.gguf',
|
||||||
|
owned_by: 'you',
|
||||||
|
created: 0,
|
||||||
|
description: '',
|
||||||
|
state: ModelState.ToDownload,
|
||||||
|
settings: {
|
||||||
|
ctx_len: 2048,
|
||||||
|
ngl: 100,
|
||||||
|
embedding: true,
|
||||||
|
n_parallel: 4,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
temperature: 0.7,
|
||||||
|
token_limit: 2048,
|
||||||
|
top_k: 0,
|
||||||
|
top_p: 1,
|
||||||
|
stream: true,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
engine: '',
|
||||||
|
quantization: '',
|
||||||
|
size: 5816320,
|
||||||
|
binaries: [],
|
||||||
|
maxRamRequired: 256000000,
|
||||||
|
author: 'aladar',
|
||||||
|
avatarUrl: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'TinyLLama-v0.f32.gguf',
|
object: 'model',
|
||||||
quantizationName: '',
|
version: '1.0.0',
|
||||||
bits: 2,
|
source_url:
|
||||||
size: 19660000,
|
|
||||||
maxRamRequired: 256000000,
|
|
||||||
usecase:
|
|
||||||
'smallest, significant quality loss - not recommended for most purposes',
|
|
||||||
downloadLink:
|
|
||||||
'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.f32.gguf',
|
'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.f32.gguf',
|
||||||
|
id: 'TinyLLama-v0.f32.gguf',
|
||||||
|
name: 'TinyLLama-v0.f32.gguf',
|
||||||
|
owned_by: 'you',
|
||||||
|
created: 0,
|
||||||
|
description: '',
|
||||||
|
state: ModelState.ToDownload,
|
||||||
|
settings: {
|
||||||
|
ctx_len: 2048,
|
||||||
|
ngl: 100,
|
||||||
|
embedding: true,
|
||||||
|
n_parallel: 4,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
temperature: 0.7,
|
||||||
|
token_limit: 2048,
|
||||||
|
top_k: 0,
|
||||||
|
top_p: 1,
|
||||||
|
stream: true,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
engine: '',
|
||||||
|
quantization: '',
|
||||||
|
size: 5816320,
|
||||||
|
binaries: [],
|
||||||
|
maxRamRequired: 256000000,
|
||||||
|
author: 'aladar',
|
||||||
|
avatarUrl: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user