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,
|
||||
...args: any[]
|
||||
) => Promise<any> = (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);
|
||||
window.coreAPI?.invokePluginFunc(plugin, method, ...args)
|
||||
|
||||
/**
|
||||
* 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> = (
|
||||
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);
|
||||
) => window.coreAPI?.downloadFile(url, fileName);
|
||||
|
||||
/**
|
||||
* Aborts the download of a specific file.
|
||||
@ -66,11 +44,18 @@ const appDataPath: () => Promise<any> = () => window.coreAPI?.appDataPath();
|
||||
* Gets the user space path.
|
||||
* @returns {Promise<any>} A Promise that resolves with the user space path.
|
||||
*/
|
||||
const getUserSpace = (): Promise<string> =>
|
||||
window.coreAPI?.getUserSpace() ?? window.electronAPI?.getUserSpace();
|
||||
const getUserSpace = (): Promise<string> => window.coreAPI?.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 = (
|
||||
extensionName: string,
|
||||
@ -79,29 +64,14 @@ export type RegisterExtensionPoint = (
|
||||
priority?: number
|
||||
) => 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
|
||||
*/
|
||||
export {
|
||||
invokePluginFunc,
|
||||
executeOnMain,
|
||||
downloadFile,
|
||||
abortDownload,
|
||||
deleteFile,
|
||||
appDataPath,
|
||||
getUserSpace,
|
||||
openFileExplorer,
|
||||
};
|
||||
|
||||
@ -5,8 +5,7 @@
|
||||
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
||||
*/
|
||||
const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
||||
window.coreAPI?.writeFile(path, data) ??
|
||||
window.electronAPI?.writeFile(path, data);
|
||||
window.coreAPI?.writeFile(path, data);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
@ -22,7 +21,7 @@ const isDirectory = (path: string): Promise<boolean> =>
|
||||
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
||||
*/
|
||||
const readFile: (path: string) => Promise<any> = (path) =>
|
||||
window.coreAPI?.readFile(path) ?? window.electronAPI?.readFile(path);
|
||||
window.coreAPI?.readFile(path);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
@ -38,7 +37,7 @@ const listFiles: (path: string) => Promise<any> = (path) =>
|
||||
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
||||
*/
|
||||
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.
|
||||
@ -46,14 +45,30 @@ const mkdir: (path: string) => Promise<any> = (path) =>
|
||||
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
|
||||
*/
|
||||
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.
|
||||
* @param {string} path - The path of the file to delete.
|
||||
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
||||
*/
|
||||
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 = {
|
||||
isDirectory,
|
||||
@ -63,4 +78,6 @@ export const fs = {
|
||||
mkdir,
|
||||
rmdir,
|
||||
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.
|
||||
* @module
|
||||
*/
|
||||
export {
|
||||
downloadFile,
|
||||
executeOnMain,
|
||||
appDataPath,
|
||||
getUserSpace,
|
||||
abortDownload,
|
||||
} from "./core";
|
||||
export * from "./core";
|
||||
|
||||
/**
|
||||
* Events module exports.
|
||||
* Events events exports.
|
||||
* @module
|
||||
*/
|
||||
export { events } from "./events";
|
||||
export * from "./events";
|
||||
|
||||
/**
|
||||
* Events types exports.
|
||||
* @module
|
||||
*/
|
||||
export * from "./events";
|
||||
|
||||
export * from "./types/index";
|
||||
|
||||
/**
|
||||
* Filesystem module exports.
|
||||
* @module
|
||||
*/
|
||||
export { fs } from "./fs";
|
||||
export * from "./fs";
|
||||
|
||||
/**
|
||||
* Plugin base module export.
|
||||
|
||||
@ -4,6 +4,7 @@ export enum PluginType {
|
||||
Preference = "preference",
|
||||
SystemMonitoring = "systemMonitoring",
|
||||
Model = "model",
|
||||
Assistant = "assistant",
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
/**
|
||||
* Abstract class for conversational plugins.
|
||||
* Abstract class for Thread plugins.
|
||||
* @abstract
|
||||
* @extends JanPlugin
|
||||
*/
|
||||
export abstract class ConversationalPlugin extends JanPlugin {
|
||||
/**
|
||||
* Returns a list of conversations.
|
||||
* Returns a list of thread.
|
||||
* @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
|
||||
* @param {Thread} conversation - The conversation to save.
|
||||
* @returns {Promise<void>} A promise that resolves when the conversation is saved.
|
||||
* @param {Thread} thread - The thread to save.
|
||||
* @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
|
||||
* @param {string} conversationId - The ID of the conversation to delete.
|
||||
* @returns {Promise<void>} A promise that resolves when the conversation is deleted.
|
||||
* @param {string} threadId - The ID of the thread to delete.
|
||||
* @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,7 +14,12 @@ export { InferencePlugin } from "./inference";
|
||||
*/
|
||||
export { MonitoringPlugin } from "./monitoring";
|
||||
|
||||
/**
|
||||
* Assistant plugin for managing assistants.
|
||||
*/
|
||||
export { AssistantPlugin } from "./assistant";
|
||||
|
||||
/**
|
||||
* Model plugin for managing models.
|
||||
*/
|
||||
export { ModelPlugin } from "./model";
|
||||
export { ModelPlugin } from "./model";
|
||||
@ -1,4 +1,4 @@
|
||||
import { MessageRequest, ThreadMessage } from "../index";
|
||||
import { MessageRequest, ModelSettingParams, ThreadMessage } from "../index";
|
||||
import { JanPlugin } from "../plugin";
|
||||
|
||||
/**
|
||||
@ -7,9 +7,9 @@ import { JanPlugin } from "../plugin";
|
||||
export abstract class InferencePlugin extends JanPlugin {
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@ -18,18 +18,17 @@ export abstract class ModelPlugin extends JanPlugin {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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.
|
||||
* @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.
|
||||
*/
|
||||
abstract deleteModel(filePath: string): Promise<void>;
|
||||
abstract deleteModel(modelId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Saves a model.
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
|
||||
/**
|
||||
* The role of the author of this message.
|
||||
* @data_transfer_object
|
||||
*/
|
||||
export enum ChatCompletionRole {
|
||||
System = "system",
|
||||
@ -30,10 +29,20 @@ export type ChatCompletionMessage = {
|
||||
*/
|
||||
export type MessageRequest = {
|
||||
id?: string;
|
||||
|
||||
/** 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?: ChatCompletionMessage[];
|
||||
|
||||
/** Runtime parameters for constructing a chat completion request **/
|
||||
parameters?: ModelRuntimeParam;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -57,17 +66,50 @@ export enum MessageStatus {
|
||||
*/
|
||||
export type ThreadMessage = {
|
||||
/** 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. **/
|
||||
threadId?: string;
|
||||
thread_id: string;
|
||||
/** The role of the author of this message. **/
|
||||
role?: ChatCompletionRole;
|
||||
assistant_id?: string;
|
||||
// TODO: comment
|
||||
role: ChatCompletionRole;
|
||||
/** The content of this message. **/
|
||||
content?: string;
|
||||
content: ThreadContent[];
|
||||
/** The status of this message. **/
|
||||
status: MessageStatus;
|
||||
/** The timestamp indicating when this message was created, represented in ISO 8601 format. **/
|
||||
createdAt?: string;
|
||||
/** The timestamp indicating when this message was created. Represented in Unix time. **/
|
||||
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 {
|
||||
/** Unique identifier for the thread, generated by default using the ULID method. **/
|
||||
id: string;
|
||||
/** The summary of this thread. **/
|
||||
summary?: string;
|
||||
/** The messages of this thread. **/
|
||||
messages: ThreadMessage[];
|
||||
/** Object name **/
|
||||
object: string;
|
||||
/** The title of this thread. **/
|
||||
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. **/
|
||||
createdAt?: string;
|
||||
created: number;
|
||||
/** The timestamp indicating when this thread was updated, represented in ISO 8601 format. **/
|
||||
updatedAt?: string;
|
||||
|
||||
/**
|
||||
* @deprecated This field is deprecated and should not be used.
|
||||
* Read from model file instead.
|
||||
*/
|
||||
modelId?: string;
|
||||
updated: number;
|
||||
/** The additional metadata of this thread. **/
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the information about an assistant in a thread.
|
||||
* @stored
|
||||
*/
|
||||
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.
|
||||
* @stored
|
||||
*/
|
||||
export interface Model {
|
||||
/** Combination of owner and model name.*/
|
||||
id: string;
|
||||
/** The name of the model.*/
|
||||
name: string;
|
||||
/** Quantization method name.*/
|
||||
quantizationName: string;
|
||||
/** The the number of bits represents a number.*/
|
||||
bits: number;
|
||||
/** 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.*/
|
||||
/**
|
||||
* The type of the object.
|
||||
* Default: "model"
|
||||
*/
|
||||
object: string;
|
||||
|
||||
/**
|
||||
* The version of the model.
|
||||
*/
|
||||
version: string;
|
||||
/** The origin url of the model repo.*/
|
||||
modelUrl: string;
|
||||
/** The timestamp indicating when this model was released.*/
|
||||
releaseDate: number;
|
||||
/** The tags attached to the model description */
|
||||
tags: string[];
|
||||
|
||||
/**
|
||||
* The model download source. It can be an external url or a local filepath.
|
||||
*/
|
||||
source_url: 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
|
||||
* @data_transfer_object
|
||||
@ -157,27 +305,37 @@ export interface ModelCatalog {
|
||||
releaseDate: number;
|
||||
/** The tags attached to the model description **/
|
||||
tags: string[];
|
||||
|
||||
/** The available versions of this model to download. */
|
||||
availableVersions: ModelVersion[];
|
||||
availableVersions: Model[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model type which will be present a version of ModelCatalog
|
||||
* @data_transfer_object
|
||||
* Assistant type defines the shape of an assistant object.
|
||||
* @stored
|
||||
*/
|
||||
export type ModelVersion = {
|
||||
/** The name of this model version.*/
|
||||
export type Assistant = {
|
||||
/** 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;
|
||||
/** The quantization method name.*/
|
||||
quantizationName: string;
|
||||
/** The the number of bits represents a number.*/
|
||||
bits: number;
|
||||
/** 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;
|
||||
/** Represents the description of the object. */
|
||||
description: string;
|
||||
/** Represents the model of the object. */
|
||||
model: string;
|
||||
/** Represents the instructions for the object. */
|
||||
instructions: string;
|
||||
/** Represents the tools associated with the object. */
|
||||
tools: any;
|
||||
/** Represents the file identifiers associated with the object. */
|
||||
file_ids: string[];
|
||||
/** Represents the metadata of the object. */
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { app, ipcMain } from 'electron'
|
||||
import * as fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import readline from 'readline'
|
||||
|
||||
/**
|
||||
* Handles file system operations.
|
||||
@ -97,7 +98,7 @@ export function handleFsIPCs() {
|
||||
*/
|
||||
ipcMain.handle('rmdir', async (event, path: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.rmdir(join(userSpacePath, path), { recursive: true }, (err) => {
|
||||
fs.rm(join(userSpacePath, path), { recursive: true }, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
@ -153,4 +154,45 @@ export function handleFsIPCs() {
|
||||
|
||||
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} 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} appendFile - Appends the given data to the file 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} installRemotePlugin - Installs the remote plugin with the given name.
|
||||
@ -58,7 +59,7 @@ import { useFacade } from './core/plugin/facade'
|
||||
|
||||
useFacade()
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
const { contextBridge, ipcRenderer, shell } = require('electron')
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
invokePluginFunc: (plugin: any, method: any, ...args: any[]) =>
|
||||
@ -88,7 +89,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
|
||||
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'),
|
||||
|
||||
@ -99,10 +101,17 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
|
||||
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),
|
||||
|
||||
rmdir: (path: string) => ipcRenderer.invoke('rmdir', path),
|
||||
|
||||
openFileExplorer: (path: string) => shell.openPath(path),
|
||||
|
||||
installRemotePlugin: (pluginName: string) =>
|
||||
ipcRenderer.invoke('installRemotePlugin', pluginName),
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||
"build:electron": "yarn workspace jan build",
|
||||
"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": "yarn build:web && yarn workspace jan build",
|
||||
"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 { 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'
|
||||
|
||||
/**
|
||||
* JSONConversationalPlugin is a ConversationalPlugin implementation that provides
|
||||
* functionality for managing conversations.
|
||||
* functionality for managing threads.
|
||||
*/
|
||||
export default class JSONConversationalPlugin implements ConversationalPlugin {
|
||||
private static readonly _homeDir = 'threads'
|
||||
private static readonly _threadInfoFileName = 'thread.json'
|
||||
private static readonly _threadMessagesFileName = 'messages.jsonl'
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async getConversations(): Promise<Thread[]> {
|
||||
async getThreads(): Promise<Thread[]> {
|
||||
try {
|
||||
const convoIds = await this.getConversationDocs()
|
||||
const threadDirs = await this.getValidThreadDirs()
|
||||
|
||||
const promises = convoIds.map((conversationId) => {
|
||||
return this.readConvo(conversationId)
|
||||
})
|
||||
const promises = threadDirs.map((dirName) => this.readThread(dirName))
|
||||
const promiseResults = await Promise.allSettled(promises)
|
||||
const convos = promiseResults
|
||||
.map((result) => {
|
||||
@ -51,10 +51,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
||||
})
|
||||
.filter((convo) => convo != null)
|
||||
convos.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
(a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime()
|
||||
)
|
||||
console.debug('getConversations: ', JSON.stringify(convos, null, 2))
|
||||
console.debug('getThreads', JSON.stringify(convos, null, 2))
|
||||
return convos
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@ -63,55 +62,145 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a Conversation object to a Markdown file.
|
||||
* @param conversation The Conversation object to save.
|
||||
* Saves a Thread object to a json file.
|
||||
* @param thread The Thread object to save.
|
||||
*/
|
||||
saveConversation(conversation: Thread): Promise<void> {
|
||||
return fs
|
||||
.mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`)
|
||||
.then(() =>
|
||||
fs.writeFile(
|
||||
join(
|
||||
JSONConversationalPlugin._homeDir,
|
||||
conversation.id,
|
||||
`${conversation.id}.json`
|
||||
),
|
||||
JSON.stringify(conversation)
|
||||
)
|
||||
async saveThread(thread: Thread): Promise<void> {
|
||||
try {
|
||||
const threadDirPath = join(JSONConversationalPlugin._homeDir, thread.id)
|
||||
const threadJsonPath = join(
|
||||
threadDirPath,
|
||||
JSONConversationalPlugin._threadInfoFileName
|
||||
)
|
||||
await fs.mkdir(threadDirPath)
|
||||
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
|
||||
Promise.resolve()
|
||||
} catch (err) {
|
||||
Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a conversation with the specified ID.
|
||||
* @param conversationId The ID of the conversation to delete.
|
||||
* Delete a thread with the specified ID.
|
||||
* @param threadId The ID of the thread to delete.
|
||||
*/
|
||||
deleteConversation(conversationId: string): Promise<void> {
|
||||
return fs.rmdir(
|
||||
join(JSONConversationalPlugin._homeDir, `${conversationId}`)
|
||||
)
|
||||
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 conversation from a file.
|
||||
* @param convoId the conversation id we are reading from.
|
||||
* @returns data of the conversation
|
||||
* 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 readConvo(convoId: string): Promise<any> {
|
||||
private async readThread(threadDirName: string): Promise<any> {
|
||||
return fs.readFile(
|
||||
join(JSONConversationalPlugin._homeDir, convoId, `${convoId}.json`)
|
||||
join(
|
||||
JSONConversationalPlugin._homeDir,
|
||||
threadDirName,
|
||||
JSONConversationalPlugin._threadInfoFileName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Returns a Promise that resolves to an array of thread directories.
|
||||
* @private
|
||||
*/
|
||||
private async getConversationDocs(): Promise<string[]> {
|
||||
return fs
|
||||
.listFiles(JSONConversationalPlugin._homeDir)
|
||||
.then((files: string[]) => {
|
||||
return Promise.all(files.filter((file) => file.startsWith('jan-')))
|
||||
private async getValidThreadDirs(): Promise<string[]> {
|
||||
const fileInsideThread: string[] = await fs.listFiles(
|
||||
JSONConversationalPlugin._homeDir
|
||||
)
|
||||
|
||||
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.
|
||||
* @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) => {
|
||||
const requestBody = JSON.stringify({
|
||||
messages: recentMessages,
|
||||
@ -20,7 +23,7 @@ export function requestInference(recentMessages: any[], controller?: AbortContro
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
body: requestBody,
|
||||
signal: controller?.signal
|
||||
signal: controller?.signal,
|
||||
})
|
||||
.then(async (response) => {
|
||||
const stream = response.body;
|
||||
|
||||
@ -8,19 +8,22 @@
|
||||
|
||||
import {
|
||||
ChatCompletionRole,
|
||||
ContentType,
|
||||
EventName,
|
||||
MessageRequest,
|
||||
MessageStatus,
|
||||
ModelSettingParams,
|
||||
PluginType,
|
||||
ThreadContent,
|
||||
ThreadMessage,
|
||||
events,
|
||||
executeOnMain,
|
||||
getUserSpace,
|
||||
} from "@janhq/core";
|
||||
import { InferencePlugin } from "@janhq/core/lib/plugins";
|
||||
import { requestInference } from "./helpers/sse";
|
||||
import { ulid } from "ulid";
|
||||
import { join } from "path";
|
||||
import { getUserSpace } from "@janhq/core";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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.
|
||||
*/
|
||||
async initModel(modelFileName: string): Promise<void> {
|
||||
async initModel(
|
||||
modelId: string,
|
||||
settings?: ModelSettingParams
|
||||
): Promise<void> {
|
||||
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.
|
||||
*/
|
||||
async inferenceRequest(data: MessageRequest): Promise<ThreadMessage> {
|
||||
const timestamp = Date.now();
|
||||
const message: ThreadMessage = {
|
||||
threadId: data.threadId,
|
||||
content: "",
|
||||
createdAt: new Date().toISOString(),
|
||||
thread_id: data.threadId,
|
||||
created: timestamp,
|
||||
updated: timestamp,
|
||||
status: MessageStatus.Ready,
|
||||
id: "",
|
||||
role: ChatCompletionRole.Assistant,
|
||||
object: "thread.message",
|
||||
content: [],
|
||||
};
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
requestInference(data.messages ?? []).subscribe({
|
||||
next: (content) => {
|
||||
message.content = content;
|
||||
},
|
||||
next: (_content) => {},
|
||||
complete: async () => {
|
||||
resolve(message);
|
||||
},
|
||||
@ -121,33 +133,49 @@ export default class JanInferencePlugin implements InferencePlugin {
|
||||
data: MessageRequest,
|
||||
instance: JanInferencePlugin
|
||||
) {
|
||||
const timestamp = Date.now();
|
||||
const message: ThreadMessage = {
|
||||
threadId: data.threadId,
|
||||
content: "",
|
||||
role: ChatCompletionRole.Assistant,
|
||||
createdAt: new Date().toISOString(),
|
||||
id: ulid(),
|
||||
thread_id: data.threadId,
|
||||
assistant_id: data.assistantId,
|
||||
role: ChatCompletionRole.Assistant,
|
||||
content: [],
|
||||
status: MessageStatus.Pending,
|
||||
created: timestamp,
|
||||
updated: timestamp,
|
||||
object: "thread.message",
|
||||
};
|
||||
events.emit(EventName.OnNewMessageResponse, message);
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
|
||||
instance.isCancelled = false;
|
||||
instance.controller = new AbortController();
|
||||
|
||||
requestInference(data.messages, instance.controller).subscribe({
|
||||
next: (content) => {
|
||||
message.content = content;
|
||||
const messageContent: ThreadContent = {
|
||||
type: ContentType.Text,
|
||||
text: {
|
||||
value: content.trim(),
|
||||
annotations: [],
|
||||
},
|
||||
};
|
||||
message.content = [messageContent];
|
||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||
},
|
||||
complete: async () => {
|
||||
message.content = message.content.trim();
|
||||
message.status = MessageStatus.Ready;
|
||||
events.emit(EventName.OnMessageResponseFinished, message);
|
||||
},
|
||||
error: async (err) => {
|
||||
message.content =
|
||||
message.content.trim() +
|
||||
(instance.isCancelled ? "" : "\n" + "Error occurred: " + err.message);
|
||||
const messageContent: ThreadContent = {
|
||||
type: ContentType.Text,
|
||||
text: {
|
||||
value: "Error occurred: " + err.message,
|
||||
annotations: [],
|
||||
},
|
||||
};
|
||||
message.content = [messageContent];
|
||||
message.status = MessageStatus.Ready;
|
||||
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 it be startModel instead?
|
||||
*/
|
||||
function initModel(modelFile: string): Promise<InitModelResponse> {
|
||||
function initModel(wrapper: any): Promise<InitModelResponse> {
|
||||
// 1. Check if the model file exists
|
||||
currentModelFile = modelFile;
|
||||
log.info("Started to load model " + modelFile);
|
||||
currentModelFile = wrapper.modelFullPath;
|
||||
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 (
|
||||
// 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
|
||||
.then(spawnNitroProcess)
|
||||
// 4. Load the model into the Nitro subprocess (HTTP POST request)
|
||||
.then(loadLLMModel)
|
||||
.then(() => loadLLMModel(settings))
|
||||
// 5. Check if the model is loaded successfully
|
||||
.then(validateModelStatus)
|
||||
.catch((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.
|
||||
* @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> {
|
||||
const config = {
|
||||
llama_model_path: currentModelFile,
|
||||
ctx_len: 2048,
|
||||
ngl: 100,
|
||||
cont_batching: false,
|
||||
embedding: false, // Always enable embedding mode on
|
||||
};
|
||||
|
||||
function loadLLMModel(settings): Promise<Response> {
|
||||
// Load model config
|
||||
return fetchRetry(NITRO_HTTP_LOAD_MODEL_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
body: JSON.stringify(settings),
|
||||
retries: 3,
|
||||
retryDelay: 500,
|
||||
}).catch((err) => {
|
||||
@ -151,7 +153,7 @@ function checkAndUnloadNitro() {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
// Fallback to kill the port
|
||||
return killSubprocess();
|
||||
});
|
||||
@ -195,7 +197,7 @@ async function spawnNitroProcess(): Promise<void> {
|
||||
|
||||
// Handle subprocess output
|
||||
subprocess.stdout.on("data", (data) => {
|
||||
console.log(`stdout: ${data}`);
|
||||
console.debug(`stdout: ${data}`);
|
||||
});
|
||||
|
||||
subprocess.stderr.on("data", (data) => {
|
||||
@ -204,7 +206,7 @@ async function spawnNitroProcess(): Promise<void> {
|
||||
});
|
||||
|
||||
subprocess.on("close", (code) => {
|
||||
console.log(`child process exited with code ${code}`);
|
||||
console.debug(`child process exited with code ${code}`);
|
||||
subprocess = null;
|
||||
reject(`Nitro process exited. ${code ?? ""}`);
|
||||
});
|
||||
|
||||
@ -1,32 +1,46 @@
|
||||
import { ModelCatalog } from '@janhq/core'
|
||||
|
||||
export function parseToModel(schema: ModelSchema): ModelCatalog {
|
||||
export const parseToModel = (modelGroup): ModelCatalog => {
|
||||
const modelVersions = []
|
||||
schema.versions.forEach((v) => {
|
||||
const version = {
|
||||
modelGroup.versions.forEach((v) => {
|
||||
const model = {
|
||||
object: 'model',
|
||||
version: modelGroup.version,
|
||||
source_url: v.downloadLink,
|
||||
id: v.name,
|
||||
name: v.name,
|
||||
quantMethod: v.quantMethod,
|
||||
bits: v.bits,
|
||||
size: v.size,
|
||||
maxRamRequired: v.maxRamRequired,
|
||||
usecase: v.usecase,
|
||||
downloadLink: v.downloadLink,
|
||||
owned_by: 'you',
|
||||
created: 0,
|
||||
description: modelGroup.longDescription,
|
||||
state: 'to_download',
|
||||
settings: v.settings,
|
||||
parameters: v.parameters,
|
||||
metadata: {
|
||||
engine: '',
|
||||
quantization: v.quantMethod,
|
||||
size: v.size,
|
||||
binaries: [],
|
||||
maxRamRequired: v.maxRamRequired,
|
||||
author: modelGroup.author,
|
||||
avatarUrl: modelGroup.avatarUrl,
|
||||
},
|
||||
}
|
||||
modelVersions.push(version)
|
||||
modelVersions.push(model)
|
||||
})
|
||||
|
||||
const model: ModelCatalog = {
|
||||
id: schema.id,
|
||||
name: schema.name,
|
||||
shortDescription: schema.shortDescription,
|
||||
avatarUrl: schema.avatarUrl,
|
||||
author: schema.author,
|
||||
version: schema.version,
|
||||
modelUrl: schema.modelUrl,
|
||||
tags: schema.tags,
|
||||
longDescription: schema.longDescription,
|
||||
releaseDate: 0,
|
||||
const modelCatalog: ModelCatalog = {
|
||||
id: modelGroup.id,
|
||||
name: modelGroup.name,
|
||||
avatarUrl: modelGroup.avatarUrl,
|
||||
shortDescription: modelGroup.shortDescription,
|
||||
longDescription: modelGroup.longDescription,
|
||||
author: modelGroup.author,
|
||||
version: modelGroup.version,
|
||||
modelUrl: modelGroup.modelUrl,
|
||||
releaseDate: modelGroup.createdAt,
|
||||
tags: modelGroup.tags,
|
||||
availableVersions: modelVersions,
|
||||
}
|
||||
return model
|
||||
|
||||
return modelCatalog
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ import { join } from 'path'
|
||||
*/
|
||||
export default class JanModelPlugin implements ModelPlugin {
|
||||
private static readonly _homeDir = 'models'
|
||||
private static readonly _modelMetadataFileName = 'model.json'
|
||||
|
||||
/**
|
||||
* Implements type from JanPlugin.
|
||||
* @override
|
||||
@ -42,12 +44,12 @@ export default class JanModelPlugin implements ModelPlugin {
|
||||
*/
|
||||
async downloadModel(model: Model): Promise<void> {
|
||||
// create corresponding directory
|
||||
const directoryPath = join(JanModelPlugin._homeDir, model.name)
|
||||
const directoryPath = join(JanModelPlugin._homeDir, model.id)
|
||||
await fs.mkdir(directoryPath)
|
||||
|
||||
// path to model binary
|
||||
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.
|
||||
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
|
||||
*/
|
||||
async cancelModelDownload(name: string, modelId: string): Promise<void> {
|
||||
return abortDownload(join(JanModelPlugin._homeDir, name, modelId)).then(
|
||||
async cancelModelDownload(modelId: string): Promise<void> {
|
||||
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.
|
||||
* @returns A Promise that resolves when the model is deleted.
|
||||
*/
|
||||
async deleteModel(filePath: string): Promise<void> {
|
||||
async deleteModel(modelId: string): Promise<void> {
|
||||
try {
|
||||
await Promise.allSettled([
|
||||
fs.deleteFile(filePath),
|
||||
fs.deleteFile(`${filePath}.json`),
|
||||
])
|
||||
const dirPath = join(JanModelPlugin._homeDir, modelId)
|
||||
await fs.rmdir(dirPath)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -85,11 +85,14 @@ export default class JanModelPlugin implements ModelPlugin {
|
||||
* @returns A Promise that resolves when the model is saved.
|
||||
*/
|
||||
async saveModel(model: Model): Promise<void> {
|
||||
const directoryPath = join(JanModelPlugin._homeDir, model.name)
|
||||
const jsonFilePath = join(directoryPath, `${model.id}.json`)
|
||||
const jsonFilePath = join(
|
||||
JanModelPlugin._homeDir,
|
||||
model.id,
|
||||
JanModelPlugin._modelMetadataFileName
|
||||
)
|
||||
|
||||
try {
|
||||
await fs.writeFile(jsonFilePath, JSON.stringify(model))
|
||||
await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -111,7 +114,7 @@ export default class JanModelPlugin implements ModelPlugin {
|
||||
}
|
||||
|
||||
const jsonFiles: string[] = (await fs.listFiles(modelDirPath)).filter(
|
||||
(file: string) => file.endsWith('.json')
|
||||
(fileName: string) => fileName === JanModelPlugin._modelMetadataFileName
|
||||
)
|
||||
|
||||
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
|
||||
pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.cancelModelDownload(model.name, item.fileName)
|
||||
?.cancelModelDownload(item.modelId)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
CommandList,
|
||||
} from '@janhq/uikit'
|
||||
|
||||
import { useSetAtom } from 'jotai'
|
||||
import {
|
||||
MessageCircleIcon,
|
||||
SettingsIcon,
|
||||
@ -27,8 +28,12 @@ import { MainViewState } from '@/constants/screens'
|
||||
|
||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||
|
||||
import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
|
||||
|
||||
export default function CommandSearch() {
|
||||
const { setMainViewState } = useMainViewState()
|
||||
const [open, setOpen] = useState(false)
|
||||
const setShowRightSideBar = useSetAtom(showRightSideBarAtom)
|
||||
|
||||
const menus = [
|
||||
{
|
||||
@ -61,8 +66,6 @@ export default function CommandSearch() {
|
||||
},
|
||||
]
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
||||
@ -120,6 +123,13 @@ export default function CommandSearch() {
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { useMemo } from 'react'
|
||||
|
||||
import { PluginType } from '@janhq/core'
|
||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||
import { ModelVersion } from '@janhq/core/lib/types'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
|
||||
import {
|
||||
Modal,
|
||||
@ -25,7 +25,7 @@ import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
import { pluginManager } from '@/plugin'
|
||||
|
||||
type Props = {
|
||||
suitableModel: ModelVersion
|
||||
suitableModel: Model
|
||||
isFromList?: boolean
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ export default function ModalCancelDownload({
|
||||
if (!model) return
|
||||
pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.cancelModelDownload(model.name, downloadState.fileName)
|
||||
?.cancelModelDownload(downloadState.modelId)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@ -16,12 +16,11 @@ import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||
|
||||
import {
|
||||
addNewMessageAtom,
|
||||
chatMessages,
|
||||
updateMessageAtom,
|
||||
} from '@/helpers/atoms/ChatMessage.atom'
|
||||
import {
|
||||
updateConversationWaitingForResponseAtom,
|
||||
userConversationsAtom,
|
||||
threadsAtom,
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
import { pluginManager } from '@/plugin'
|
||||
@ -35,71 +34,60 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
|
||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
||||
const models = useAtomValue(downloadingModelsAtom)
|
||||
const messages = useAtomValue(chatMessages)
|
||||
const conversations = useAtomValue(userConversationsAtom)
|
||||
const messagesRef = useRef(messages)
|
||||
const convoRef = useRef(conversations)
|
||||
const threads = useAtomValue(threadsAtom)
|
||||
const threadsRef = useRef(threads)
|
||||
|
||||
useEffect(() => {
|
||||
messagesRef.current = messages
|
||||
convoRef.current = conversations
|
||||
}, [messages, conversations])
|
||||
threadsRef.current = threads
|
||||
}, [threads])
|
||||
|
||||
async function handleNewMessageResponse(message: ThreadMessage) {
|
||||
if (message.threadId) {
|
||||
const convo = convoRef.current.find((e) => e.id == message.threadId)
|
||||
if (!convo) return
|
||||
addNewMessage(message)
|
||||
}
|
||||
}
|
||||
async function handleMessageResponseUpdate(messageResponse: ThreadMessage) {
|
||||
if (
|
||||
messageResponse.threadId &&
|
||||
messageResponse.id &&
|
||||
messageResponse.content
|
||||
) {
|
||||
updateMessage(
|
||||
messageResponse.id,
|
||||
messageResponse.threadId,
|
||||
messageResponse.content,
|
||||
MessageStatus.Pending
|
||||
)
|
||||
}
|
||||
addNewMessage(message)
|
||||
}
|
||||
|
||||
async function handleMessageResponseFinished(messageResponse: ThreadMessage) {
|
||||
if (!messageResponse.threadId || !convoRef.current) return
|
||||
updateConvWaiting(messageResponse.threadId, false)
|
||||
async function handleMessageResponseUpdate(message: ThreadMessage) {
|
||||
updateMessage(
|
||||
message.id,
|
||||
message.thread_id,
|
||||
message.content,
|
||||
MessageStatus.Pending
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
messageResponse.threadId &&
|
||||
messageResponse.id &&
|
||||
messageResponse.content
|
||||
) {
|
||||
async function handleMessageResponseFinished(message: ThreadMessage) {
|
||||
updateConvWaiting(message.thread_id, false)
|
||||
|
||||
if (message.id && message.content) {
|
||||
updateMessage(
|
||||
messageResponse.id,
|
||||
messageResponse.threadId,
|
||||
messageResponse.content,
|
||||
message.id,
|
||||
message.thread_id,
|
||||
message.content,
|
||||
MessageStatus.Ready
|
||||
)
|
||||
}
|
||||
|
||||
const thread = convoRef.current.find(
|
||||
(e) => e.id == messageResponse.threadId
|
||||
)
|
||||
const thread = threadsRef.current?.find((e) => e.id == message.thread_id)
|
||||
if (thread) {
|
||||
const messageContent = message.content[0]?.text.value ?? ''
|
||||
const metadata = {
|
||||
...thread.metadata,
|
||||
lastMessage: messageContent,
|
||||
}
|
||||
pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveConversation({
|
||||
?.saveThread({
|
||||
...thread,
|
||||
id: thread.id ?? '',
|
||||
messages: messagesRef.current[thread.id] ?? [],
|
||||
metadata,
|
||||
})
|
||||
|
||||
pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.addNewMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownloadUpdate(state: any) {
|
||||
if (!state) return
|
||||
state.fileName = state.fileName.split('/').pop() ?? ''
|
||||
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 {
|
||||
conversationStatesAtom,
|
||||
currentConversationAtom,
|
||||
getActiveConvoIdAtom,
|
||||
getActiveThreadIdAtom,
|
||||
updateThreadStateLastMessageAtom,
|
||||
} from './Conversation.atom'
|
||||
|
||||
/**
|
||||
* Stores all chat messages for all conversations
|
||||
* Stores all chat messages for all threads
|
||||
*/
|
||||
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
|
||||
*/
|
||||
export const getCurrentChatMessagesAtom = atom<ThreadMessage[]>((get) => {
|
||||
const activeConversationId = get(getActiveConvoIdAtom)
|
||||
if (!activeConversationId) return []
|
||||
const messages = get(chatMessages)[activeConversationId]
|
||||
const activeThreadId = get(getActiveThreadIdAtom)
|
||||
if (!activeThreadId) return []
|
||||
const messages = get(chatMessages)[activeThreadId]
|
||||
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(
|
||||
null,
|
||||
(get, set, messages: ThreadMessage[], convoId: string) => {
|
||||
(get, set, threadId: string, messages: ThreadMessage[]) => {
|
||||
const newData: Record<string, ThreadMessage[]> = {
|
||||
...get(chatMessages),
|
||||
}
|
||||
newData[convoId] = messages
|
||||
newData[threadId] = messages
|
||||
set(chatMessages, newData)
|
||||
}
|
||||
)
|
||||
@ -54,7 +43,7 @@ export const setConvoMessagesAtom = atom(
|
||||
export const addOldMessagesAtom = atom(
|
||||
null,
|
||||
(get, set, newMessages: ThreadMessage[]) => {
|
||||
const currentConvoId = get(getActiveConvoIdAtom)
|
||||
const currentConvoId = get(getActiveThreadIdAtom)
|
||||
if (!currentConvoId) return
|
||||
|
||||
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
||||
@ -71,19 +60,19 @@ export const addOldMessagesAtom = atom(
|
||||
export const addNewMessageAtom = atom(
|
||||
null,
|
||||
(get, set, newMessage: ThreadMessage) => {
|
||||
const currentConvoId = get(getActiveConvoIdAtom)
|
||||
if (!currentConvoId) return
|
||||
const threadId = get(getActiveThreadIdAtom)
|
||||
if (!threadId) return
|
||||
|
||||
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
||||
const updatedMessages = [newMessage, ...currentMessages]
|
||||
const currentMessages = get(chatMessages)[threadId] ?? []
|
||||
const updatedMessages = [...currentMessages, newMessage]
|
||||
|
||||
const newData: Record<string, ThreadMessage[]> = {
|
||||
...get(chatMessages),
|
||||
}
|
||||
newData[currentConvoId] = updatedMessages
|
||||
newData[threadId] = updatedMessages
|
||||
set(chatMessages, newData)
|
||||
// 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)
|
||||
})
|
||||
|
||||
export const deleteMessage = atom(null, (get, set, id: string) => {
|
||||
export const deleteMessageAtom = atom(null, (get, set, id: string) => {
|
||||
const newData: Record<string, ThreadMessage[]> = {
|
||||
...get(chatMessages),
|
||||
}
|
||||
const threadId = get(currentConversationAtom)?.id
|
||||
const threadId = get(getActiveThreadIdAtom)
|
||||
if (threadId) {
|
||||
newData[threadId] = newData[threadId].filter((e) => e.id !== id)
|
||||
set(chatMessages, newData)
|
||||
@ -121,7 +110,7 @@ export const updateMessageAtom = atom(
|
||||
set,
|
||||
id: string,
|
||||
conversationId: string,
|
||||
text: string,
|
||||
text: ThreadContent[],
|
||||
status: MessageStatus
|
||||
) => {
|
||||
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 { ThreadState } from '@/types/conversation'
|
||||
|
||||
/**
|
||||
* 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,
|
||||
(_get, set, convoId: string | undefined) => {
|
||||
set(activeConversationIdAtom, convoId)
|
||||
}
|
||||
(_get, set, convoId: string | undefined) => set(activeThreadIdAtom, convoId)
|
||||
)
|
||||
|
||||
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 currentConvoStateAtom = atom<ThreadState | undefined>((get) => {
|
||||
const activeConvoId = get(activeConversationIdAtom)
|
||||
export const threadStatesAtom = atom<Record<string, ThreadState>>({})
|
||||
export const activeThreadStateAtom = atom<ThreadState | undefined>((get) => {
|
||||
const activeConvoId = get(activeThreadIdAtom)
|
||||
if (!activeConvoId) {
|
||||
console.debug('Active convo id is 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(
|
||||
null,
|
||||
(get, set, conversationId: string, waitingForResponse: boolean) => {
|
||||
const currentState = { ...get(conversationStatesAtom) }
|
||||
const currentState = { ...get(threadStatesAtom) }
|
||||
currentState[conversationId] = {
|
||||
...currentState[conversationId],
|
||||
waitingForResponse,
|
||||
error: undefined,
|
||||
}
|
||||
set(conversationStatesAtom, currentState)
|
||||
set(threadStatesAtom, currentState)
|
||||
}
|
||||
)
|
||||
export const updateConversationErrorAtom = atom(
|
||||
null,
|
||||
(get, set, conversationId: string, error?: Error) => {
|
||||
const currentState = { ...get(conversationStatesAtom) }
|
||||
const currentState = { ...get(threadStatesAtom) }
|
||||
currentState[conversationId] = {
|
||||
...currentState[conversationId],
|
||||
error,
|
||||
}
|
||||
set(conversationStatesAtom, currentState)
|
||||
set(threadStatesAtom, currentState)
|
||||
}
|
||||
)
|
||||
export const updateConversationHasMoreAtom = atom(
|
||||
null,
|
||||
(get, set, conversationId: string, hasMore: boolean) => {
|
||||
const currentState = { ...get(conversationStatesAtom) }
|
||||
const currentState = { ...get(threadStatesAtom) }
|
||||
currentState[conversationId] = { ...currentState[conversationId], hasMore }
|
||||
set(conversationStatesAtom, currentState)
|
||||
set(threadStatesAtom, currentState)
|
||||
}
|
||||
)
|
||||
|
||||
export const updateThreadStateLastMessageAtom = atom(
|
||||
null,
|
||||
(get, set, conversationId: string, lastMessage?: string) => {
|
||||
const currentState = { ...get(conversationStatesAtom) }
|
||||
currentState[conversationId] = {
|
||||
...currentState[conversationId],
|
||||
(get, set, threadId: string, lastContent?: ThreadContent[]) => {
|
||||
const currentState = { ...get(threadStatesAtom) }
|
||||
const lastMessage = lastContent?.[0]?.text?.value ?? ''
|
||||
currentState[threadId] = {
|
||||
...currentState[threadId],
|
||||
lastMessage,
|
||||
}
|
||||
set(conversationStatesAtom, currentState)
|
||||
set(threadStatesAtom, currentState)
|
||||
}
|
||||
)
|
||||
|
||||
export const updateConversationAtom = atom(
|
||||
export const updateThreadAtom = atom(
|
||||
null,
|
||||
(get, set, conversation: Thread) => {
|
||||
const id = conversation.id
|
||||
if (!id) return
|
||||
const convo = get(userConversationsAtom).find((c) => c.id === id)
|
||||
if (!convo) return
|
||||
|
||||
const newConversations: Thread[] = get(userConversationsAtom).map((c) =>
|
||||
c.id === id ? conversation : c
|
||||
(get, set, updatedThread: Thread) => {
|
||||
const threads: Thread[] = get(threadsAtom).map((c) =>
|
||||
c.id === updatedThread.id ? updatedThread : c
|
||||
)
|
||||
|
||||
// sort new conversations based on updated at
|
||||
newConversations.sort((a, b) => {
|
||||
const aDate = new Date(a.updatedAt ?? 0)
|
||||
const bDate = new Date(b.updatedAt ?? 0)
|
||||
// sort new threads based on updated at
|
||||
threads.sort((thread1, thread2) => {
|
||||
const aDate = new Date(thread1.updated ?? 0)
|
||||
const bDate = new Date(thread2.updated ?? 0)
|
||||
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 currentConversationAtom = atom<Thread | undefined>((get) =>
|
||||
get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom))
|
||||
export const threadsAtom = atom<Thread[]>([])
|
||||
|
||||
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'
|
||||
|
||||
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 downloadingModelsAtom = atom<Model[]>([])
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { join } from 'path'
|
||||
|
||||
import { PluginType } from '@janhq/core'
|
||||
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 { toaster } from '@/containers/Toast'
|
||||
@ -13,12 +10,12 @@ import { useGetDownloadedModels } from './useGetDownloadedModels'
|
||||
|
||||
import { pluginManager } from '@/plugin'
|
||||
|
||||
const activeAssistantModelAtom = atom<Model | undefined>(undefined)
|
||||
const activeModelAtom = atom<Model | undefined>(undefined)
|
||||
|
||||
const stateModelAtom = atom({ state: 'start', loading: false, model: '' })
|
||||
|
||||
export function useActiveModel() {
|
||||
const [activeModel, setActiveModel] = useAtom(activeAssistantModelAtom)
|
||||
const [activeModel, setActiveModel] = useAtom(activeModelAtom)
|
||||
const [stateModel, setStateModel] = useAtom(stateModelAtom)
|
||||
const { downloadedModels } = useGetDownloadedModels()
|
||||
|
||||
@ -30,6 +27,7 @@ export function useActiveModel() {
|
||||
console.debug(`Model ${modelId} is already initialized. Ignore..`)
|
||||
return
|
||||
}
|
||||
// TODO: incase we have multiple assistants, the configuration will be from assistant
|
||||
|
||||
setActiveModel(undefined)
|
||||
|
||||
@ -52,8 +50,7 @@ export function useActiveModel() {
|
||||
|
||||
const currentTime = Date.now()
|
||||
console.debug('Init model: ', modelId)
|
||||
const path = join('models', model.name, modelId)
|
||||
const res = await initModel(path)
|
||||
const res = await initModel(modelId, model?.settings)
|
||||
if (res && res.error && res.modelFile === stateModel.model) {
|
||||
const errorMessage = `${res.error}`
|
||||
alert(errorMessage)
|
||||
@ -98,8 +95,11 @@ export function useActiveModel() {
|
||||
return { activeModel, startModel, stopModel, stateModel }
|
||||
}
|
||||
|
||||
const initModel = async (modelId: string): Promise<any> => {
|
||||
const initModel = async (
|
||||
modelId: string,
|
||||
settings?: ModelSettingParams
|
||||
): Promise<any> => {
|
||||
return pluginManager
|
||||
.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,71 +16,65 @@ import {
|
||||
getCurrentChatMessagesAtom,
|
||||
} from '@/helpers/atoms/ChatMessage.atom'
|
||||
import {
|
||||
userConversationsAtom,
|
||||
getActiveConvoIdAtom,
|
||||
setActiveConvoIdAtom,
|
||||
threadsAtom,
|
||||
getActiveThreadIdAtom,
|
||||
setActiveThreadIdAtom,
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
|
||||
export default function useDeleteConversation() {
|
||||
export default function useDeleteThread() {
|
||||
const { activeModel } = useActiveModel()
|
||||
const [userConversations, setUserConversations] = useAtom(
|
||||
userConversationsAtom
|
||||
)
|
||||
const [threads, setThreads] = useAtom(threadsAtom)
|
||||
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 cleanMessages = useSetAtom(cleanConversationMessages)
|
||||
const currentMessages = useAtomValue(getCurrentChatMessagesAtom)
|
||||
|
||||
const cleanConvo = async () => {
|
||||
if (activeConvoId) {
|
||||
const currentConversation = userConversations.filter(
|
||||
(c) => c.id === activeConvoId
|
||||
)[0]
|
||||
cleanMessages(activeConvoId)
|
||||
if (currentConversation)
|
||||
const cleanThread = async () => {
|
||||
if (activeThreadId) {
|
||||
const thread = threads.filter((c) => c.id === activeThreadId)[0]
|
||||
cleanMessages(activeThreadId)
|
||||
if (thread)
|
||||
await pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveConversation({
|
||||
...currentConversation,
|
||||
id: activeConvoId,
|
||||
messages: currentMessages.filter(
|
||||
(e) => e.role === ChatCompletionRole.System
|
||||
),
|
||||
})
|
||||
?.writeMessages(
|
||||
activeThreadId,
|
||||
messages.filter((msg) => msg.role === ChatCompletionRole.System)
|
||||
)
|
||||
}
|
||||
}
|
||||
const deleteConvo = async () => {
|
||||
if (activeConvoId) {
|
||||
try {
|
||||
await pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.deleteConversation(activeConvoId)
|
||||
const currentConversations = userConversations.filter(
|
||||
(c) => c.id !== activeConvoId
|
||||
)
|
||||
setUserConversations(currentConversations)
|
||||
deleteMessages(activeConvoId)
|
||||
setCurrentPrompt('')
|
||||
toaster({
|
||||
title: 'Chat successfully deleted.',
|
||||
description: `Chat with ${activeModel?.name} has been successfully deleted.`,
|
||||
})
|
||||
if (currentConversations.length > 0) {
|
||||
setActiveConvoId(currentConversations[0].id)
|
||||
} else {
|
||||
setActiveConvoId(undefined)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
||||
const deleteThread = async () => {
|
||||
if (!activeThreadId) {
|
||||
alert('No active thread')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.deleteThread(activeThreadId)
|
||||
const availableThreads = threads.filter((c) => c.id !== activeThreadId)
|
||||
setThreads(availableThreads)
|
||||
deleteMessages(activeThreadId)
|
||||
setCurrentPrompt('')
|
||||
toaster({
|
||||
title: 'Chat successfully deleted.',
|
||||
description: `Chat with ${activeModel?.name} has been successfully deleted.`,
|
||||
})
|
||||
if (availableThreads.length > 0) {
|
||||
setActiveConvoId(availableThreads[0].id)
|
||||
} else {
|
||||
setActiveConvoId(undefined)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
cleanConvo,
|
||||
deleteConvo,
|
||||
cleanThread,
|
||||
deleteThread,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { join } from 'path'
|
||||
|
||||
import { PluginType } from '@janhq/core'
|
||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
@ -14,8 +12,9 @@ export default function useDeleteModel() {
|
||||
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
||||
|
||||
const deleteModel = async (model: Model) => {
|
||||
const path = join('models', model.name, model.id)
|
||||
await pluginManager.get<ModelPlugin>(PluginType.Model)?.deleteModel(path)
|
||||
await pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.deleteModel(model.id)
|
||||
|
||||
// reload models
|
||||
setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id))
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PluginType } from '@janhq/core'
|
||||
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'
|
||||
|
||||
@ -16,41 +16,10 @@ export default function useDownloadModel() {
|
||||
downloadingModelsAtom
|
||||
)
|
||||
|
||||
const assistanModel = (
|
||||
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
|
||||
) => {
|
||||
const downloadModel = async (model: Model) => {
|
||||
// set an initial download state
|
||||
setDownloadState({
|
||||
modelId: modelVersion.name,
|
||||
modelId: model.id,
|
||||
time: {
|
||||
elapsed: 0,
|
||||
remaining: 0,
|
||||
@ -61,16 +30,11 @@ export default function useDownloadModel() {
|
||||
total: 0,
|
||||
transferred: 0,
|
||||
},
|
||||
fileName: modelVersion.name,
|
||||
fileName: model.id,
|
||||
})
|
||||
|
||||
const assistantModel = assistanModel(model, modelVersion)
|
||||
|
||||
setDownloadingModels([...downloadingModels, assistantModel])
|
||||
|
||||
await pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.downloadModel(assistantModel)
|
||||
setDownloadingModels([...downloadingModels, model])
|
||||
await pluginManager.get<ModelPlugin>(PluginType.Model)?.downloadModel(model)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -10,7 +10,6 @@ const setDownloadStateAtom = atom(null, (get, set, state: DownloadState) => {
|
||||
console.debug(
|
||||
`current download state for ${state.fileName} is ${JSON.stringify(state)}`
|
||||
)
|
||||
state.fileName = state.fileName.replace('models/', '')
|
||||
currentState[state.fileName] = state
|
||||
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'
|
||||
|
||||
const downloadedModelAtom = atom<Model[]>([])
|
||||
import { PluginType } from '@janhq/core'
|
||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
|
||||
import { atom, useAtom } from 'jotai'
|
||||
|
||||
import { pluginManager } from '@/plugin/PluginManager'
|
||||
|
||||
export function useGetDownloadedModels() {
|
||||
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelAtom)
|
||||
const downloadedModelsAtom = atom<Model[]>([])
|
||||
|
||||
async function getDownloadedModels(): Promise<Model[]> {
|
||||
const models = await pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.getDownloadedModels()
|
||||
return models ?? []
|
||||
}
|
||||
export function useGetDownloadedModels() {
|
||||
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
|
||||
|
||||
useEffect(() => {
|
||||
getDownloadedModels().then((downloadedModels) => {
|
||||
@ -26,3 +21,11 @@ export function useGetDownloadedModels() {
|
||||
|
||||
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 { ModelVersion } from '@janhq/core/lib/types'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
|
||||
|
||||
export default function useGetMostSuitableModelVersion() {
|
||||
const [suitableModel, setSuitableModel] = useState<ModelVersion | undefined>()
|
||||
const [suitableModel, setSuitableModel] = useState<Model | undefined>()
|
||||
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%
|
||||
const modelVersion = modelVersions.reduce((prev, current) => {
|
||||
if (current.maxRamRequired > prev.maxRamRequired) {
|
||||
if (current.maxRamRequired < totalRam * 0.8) {
|
||||
if (current.metadata.maxRamRequired > prev.metadata.maxRamRequired) {
|
||||
if (current.metadata.maxRamRequired < totalRam * 0.8) {
|
||||
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'
|
||||
|
||||
@ -9,10 +9,10 @@ import { ModelPerformance, TagType } from '@/constants/tagType'
|
||||
|
||||
export default function useGetPerformanceTag() {
|
||||
async function getPerformanceForModel(
|
||||
modelVersion: ModelVersion,
|
||||
model: Model,
|
||||
totalRam: number
|
||||
): Promise<{ title: string; performanceTag: TagType }> {
|
||||
const requiredRam = modelVersion.maxRamRequired
|
||||
const requiredRam = model.metadata.maxRamRequired
|
||||
const performanceTag = calculateRamPerformance(requiredRam, totalRam)
|
||||
|
||||
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 {
|
||||
ChatCompletionMessage,
|
||||
ChatCompletionRole,
|
||||
ContentType,
|
||||
EventName,
|
||||
MessageRequest,
|
||||
MessageStatus,
|
||||
PluginType,
|
||||
Thread,
|
||||
ThreadMessage,
|
||||
events,
|
||||
} from '@janhq/core'
|
||||
@ -13,8 +15,11 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
import { ulid } from 'ulid'
|
||||
|
||||
import { selectedModelAtom } from '@/containers/DropdownListSidebar'
|
||||
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||
|
||||
import { toaster } from '@/containers/Toast'
|
||||
|
||||
import { useActiveModel } from './useActiveModel'
|
||||
|
||||
import {
|
||||
@ -22,29 +27,30 @@ import {
|
||||
getCurrentChatMessagesAtom,
|
||||
} from '@/helpers/atoms/ChatMessage.atom'
|
||||
import {
|
||||
currentConversationAtom,
|
||||
updateConversationAtom,
|
||||
activeThreadAtom,
|
||||
updateThreadAtom,
|
||||
updateConversationWaitingForResponseAtom,
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
import { pluginManager } from '@/plugin/PluginManager'
|
||||
|
||||
export default function useSendChatMessage() {
|
||||
const currentConvo = useAtomValue(currentConversationAtom)
|
||||
const activeThread = useAtomValue(activeThreadAtom)
|
||||
const addNewMessage = useSetAtom(addNewMessageAtom)
|
||||
const updateConversation = useSetAtom(updateConversationAtom)
|
||||
const updateThread = useSetAtom(updateThreadAtom)
|
||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
||||
|
||||
const currentMessages = useAtomValue(getCurrentChatMessagesAtom)
|
||||
const { activeModel } = useActiveModel()
|
||||
const selectedModel = useAtomValue(selectedModelAtom)
|
||||
const { startModel } = useActiveModel()
|
||||
|
||||
function updateConvSummary(newMessage: MessageRequest) {
|
||||
function updateThreadTitle(newMessage: MessageRequest) {
|
||||
if (
|
||||
currentConvo &&
|
||||
activeThread &&
|
||||
newMessage.messages &&
|
||||
newMessage.messages.length >= 2 &&
|
||||
(!currentConvo.summary ||
|
||||
currentConvo.summary === '' ||
|
||||
currentConvo.summary === activeModel?.name)
|
||||
newMessage.messages.length > 2 &&
|
||||
(activeThread.title === '' || activeThread.title === activeModel?.name)
|
||||
) {
|
||||
const summaryMsg: ChatCompletionMessage = {
|
||||
role: ChatCompletionRole.User,
|
||||
@ -60,70 +66,123 @@ export default function useSendChatMessage() {
|
||||
messages: newMessage.messages?.slice(0, -1).concat([summaryMsg]),
|
||||
})
|
||||
.catch(console.error)
|
||||
const content = result?.content[0]?.text.value.trim()
|
||||
if (
|
||||
currentConvo &&
|
||||
currentConvo.id === newMessage.threadId &&
|
||||
result?.content &&
|
||||
result?.content?.trim().length > 0 &&
|
||||
result.content.split(' ').length <= 20
|
||||
activeThread &&
|
||||
activeThread.id === newMessage.threadId &&
|
||||
content &&
|
||||
content.length > 0 &&
|
||||
content.split(' ').length <= 20
|
||||
) {
|
||||
const updatedConv = {
|
||||
...currentConvo,
|
||||
summary: result.content,
|
||||
const updatedConv: Thread = {
|
||||
...activeThread,
|
||||
title: content,
|
||||
}
|
||||
updateConversation(updatedConv)
|
||||
updateThread(updatedConv)
|
||||
pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveConversation({
|
||||
...updatedConv,
|
||||
messages: currentMessages,
|
||||
})
|
||||
?.saveThread(updatedConv)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const sendChatMessage = async () => {
|
||||
const threadId = currentConvo?.id
|
||||
if (!threadId) {
|
||||
console.error('No conversation id')
|
||||
if (!currentPrompt || currentPrompt.trim().length === 0) {
|
||||
return
|
||||
}
|
||||
if (!activeThread) {
|
||||
console.error('No active thread')
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentPrompt('')
|
||||
updateConvWaiting(threadId, true)
|
||||
if (!activeThread.isFinishInit) {
|
||||
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()
|
||||
setCurrentPrompt('')
|
||||
|
||||
const messages: ChatCompletionMessage[] = currentMessages
|
||||
.map<ChatCompletionMessage>((msg) => ({
|
||||
role: msg.role ?? ChatCompletionRole.User,
|
||||
content: msg.content ?? '',
|
||||
role: msg.role,
|
||||
content: msg.content[0]?.text.value ?? '',
|
||||
}))
|
||||
.reverse()
|
||||
.concat([
|
||||
{
|
||||
role: ChatCompletionRole.User,
|
||||
content: prompt,
|
||||
} as ChatCompletionMessage,
|
||||
])
|
||||
console.debug(`Sending messages: ${JSON.stringify(messages, null, 2)}`)
|
||||
const msgId = ulid()
|
||||
const messageRequest: MessageRequest = {
|
||||
id: ulid(),
|
||||
threadId: threadId,
|
||||
id: msgId,
|
||||
threadId: activeThread.id,
|
||||
messages,
|
||||
parameters: activeThread.assistants[0].model.parameters,
|
||||
}
|
||||
|
||||
const timestamp = Date.now()
|
||||
const threadMessage: ThreadMessage = {
|
||||
id: messageRequest.id,
|
||||
threadId: messageRequest.threadId,
|
||||
content: prompt,
|
||||
id: msgId,
|
||||
thread_id: activeThread.id,
|
||||
role: ChatCompletionRole.User,
|
||||
createdAt: new Date().toISOString(),
|
||||
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)
|
||||
updateConvSummary(messageRequest)
|
||||
}
|
||||
|
||||
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 ChatInstruction from '../ChatInstruction'
|
||||
import ChatItem from '../ChatItem'
|
||||
|
||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||
@ -8,11 +7,10 @@ import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||
const ChatBody: React.FC = () => {
|
||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||
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) => (
|
||||
<ChatItem {...message} key={message.id} />
|
||||
))}
|
||||
{messages.length === 0 && <ChatInstruction />}
|
||||
</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 {
|
||||
ChatCompletionRole,
|
||||
ChatCompletionMessage,
|
||||
@ -9,22 +11,33 @@ import {
|
||||
events,
|
||||
} from '@janhq/core'
|
||||
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 { twMerge } from 'tailwind-merge'
|
||||
|
||||
import { toaster } from '@/containers/Toast'
|
||||
|
||||
import {
|
||||
deleteMessage,
|
||||
deleteMessageAtom,
|
||||
getCurrentChatMessagesAtom,
|
||||
} from '@/helpers/atoms/ChatMessage.atom'
|
||||
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
|
||||
import {
|
||||
activeThreadAtom,
|
||||
threadStatesAtom,
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
import { pluginManager } from '@/plugin'
|
||||
|
||||
const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
||||
const deleteAMessage = useSetAtom(deleteMessage)
|
||||
const thread = useAtomValue(currentConversationAtom)
|
||||
const deleteMessage = useSetAtom(deleteMessageAtom)
|
||||
const thread = useAtomValue(activeThreadAtom)
|
||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||
const threadStateAtom = useMemo(
|
||||
() => atom((get) => get(threadStatesAtom)[thread?.id ?? '']),
|
||||
[thread?.id]
|
||||
)
|
||||
const threadState = useAtomValue(threadStateAtom)
|
||||
|
||||
const stopInference = async () => {
|
||||
await pluginManager
|
||||
.get<InferencePlugin>(PluginType.Inference)
|
||||
@ -33,8 +46,14 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
||||
events.emit(EventName.OnMessageResponseFinished, message)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
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">
|
||||
{message.status === MessageStatus.Pending && (
|
||||
<div
|
||||
@ -45,25 +64,20 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
||||
</div>
|
||||
)}
|
||||
{message.status !== MessageStatus.Pending &&
|
||||
message.id === messages[0]?.id && (
|
||||
message.id === messages[messages.length - 1]?.id && (
|
||||
<div
|
||||
className="cursor-pointer border-r border-border px-2 py-2 hover:bg-background/80"
|
||||
onClick={() => {
|
||||
const messageRequest: MessageRequest = {
|
||||
id: message.id ?? '',
|
||||
messages: messages
|
||||
.slice(1, messages.length)
|
||||
.reverse()
|
||||
.map((e) => {
|
||||
return {
|
||||
content: e.content,
|
||||
role: e.role,
|
||||
} as ChatCompletionMessage
|
||||
}),
|
||||
threadId: message.threadId ?? '',
|
||||
}
|
||||
if (message.role === ChatCompletionRole.Assistant) {
|
||||
deleteAMessage(message.id ?? '')
|
||||
messages: messages.slice(0, -1).map((e) => {
|
||||
const msg: ChatCompletionMessage = {
|
||||
role: e.role,
|
||||
content: e.content[0].text.value,
|
||||
}
|
||||
return msg
|
||||
}),
|
||||
threadId: message.thread_id ?? '',
|
||||
}
|
||||
events.emit(EventName.OnNewMessageRequest, messageRequest)
|
||||
}}
|
||||
@ -74,7 +88,7 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
||||
<div
|
||||
className="cursor-pointer border-r border-border px-2 py-2 hover:bg-background/80"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(message.content ?? '')
|
||||
navigator.clipboard.writeText(message.content[0]?.text?.value ?? '')
|
||||
toaster({
|
||||
title: 'Copied to clipboard',
|
||||
})
|
||||
@ -85,14 +99,14 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
||||
<div
|
||||
className="cursor-pointer px-2 py-2 hover:bg-background/80"
|
||||
onClick={async () => {
|
||||
deleteAMessage(message.id ?? '')
|
||||
deleteMessage(message.id ?? '')
|
||||
if (thread)
|
||||
await pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveConversation({
|
||||
...thread,
|
||||
messages: messages.filter((e) => e.id !== message.id),
|
||||
})
|
||||
?.writeMessages(
|
||||
thread.id,
|
||||
messages.filter((msg) => msg.id !== message.id)
|
||||
)
|
||||
}}
|
||||
>
|
||||
<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 { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||
|
||||
import { displayDate } from '@/utils/datetime'
|
||||
|
||||
import MessageToolbar from '../MessageToolbar'
|
||||
@ -50,7 +48,12 @@ const marked = new Marked(
|
||||
)
|
||||
|
||||
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 isSystem = props.role === ChatCompletionRole.System
|
||||
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
|
||||
if (!lastTimestamp) {
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -89,11 +93,11 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||
>
|
||||
{!isUser && !isSystem && <LogoMark width={20} />}
|
||||
<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
|
||||
className={twMerge(
|
||||
'absolute right-0 cursor-pointer transition-all',
|
||||
messages[0].id === props.id
|
||||
messages[messages.length - 1]?.id === props.id
|
||||
? 'absolute -bottom-10 left-4'
|
||||
: 'hidden group-hover:flex'
|
||||
)}
|
||||
@ -104,7 +108,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||
|
||||
<div className={twMerge('w-full')}>
|
||||
{props.status === MessageStatus.Pending &&
|
||||
(!props.content || props.content === '') ? (
|
||||
(!props.content[0] || props.content[0].text.value === '') ? (
|
||||
<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 { useAtom, useAtomValue } from 'jotai'
|
||||
@ -12,115 +11,71 @@ import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||
|
||||
import ShortCut from '@/containers/Shortcut'
|
||||
|
||||
import { toaster } from '@/containers/Toast'
|
||||
|
||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||
|
||||
import { MainViewState } from '@/constants/screens'
|
||||
|
||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||
|
||||
import { useCreateConversation } from '@/hooks/useCreateConversation'
|
||||
import useDeleteConversation from '@/hooks/useDeleteConversation'
|
||||
import useDeleteThread from '@/hooks/useDeleteConversation'
|
||||
|
||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||
|
||||
import useGetUserConversations from '@/hooks/useGetUserConversations'
|
||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||
|
||||
import useSendChatMessage from '@/hooks/useSendChatMessage'
|
||||
|
||||
import ChatBody from '@/screens/Chat/ChatBody'
|
||||
|
||||
import HistoryList from '@/screens/Chat/HistoryList'
|
||||
import ThreadList from '@/screens/Chat/ThreadList'
|
||||
|
||||
import Sidebar from './Sidebar'
|
||||
|
||||
import {
|
||||
currentConversationAtom,
|
||||
getActiveConvoIdAtom,
|
||||
userConversationsAtom,
|
||||
activeThreadAtom,
|
||||
getActiveThreadIdAtom,
|
||||
threadsAtom,
|
||||
waitingToSendMessage,
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
|
||||
import { currentConvoStateAtom } from '@/helpers/atoms/Conversation.atom'
|
||||
import { activeThreadStateAtom } from '@/helpers/atoms/Conversation.atom'
|
||||
|
||||
const ChatScreen = () => {
|
||||
const currentConvo = useAtomValue(currentConversationAtom)
|
||||
const currentConvo = useAtomValue(activeThreadAtom)
|
||||
const { downloadedModels } = useGetDownloadedModels()
|
||||
const { deleteConvo, cleanConvo } = useDeleteConversation()
|
||||
const { deleteThread, cleanThread } = useDeleteThread()
|
||||
const { activeModel, stateModel } = useActiveModel()
|
||||
const { setMainViewState } = useMainViewState()
|
||||
|
||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
||||
const currentConvoState = useAtomValue(currentConvoStateAtom)
|
||||
const currentConvoState = useAtomValue(activeThreadStateAtom)
|
||||
const { sendChatMessage } = useSendChatMessage()
|
||||
const isWaitingForResponse = currentConvoState?.waitingForResponse ?? false
|
||||
const disabled = currentPrompt.trim().length === 0 || isWaitingForResponse
|
||||
const activeConversationId = useAtomValue(getActiveConvoIdAtom)
|
||||
|
||||
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||
const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
|
||||
const { requestCreateConvo } = useCreateConversation()
|
||||
const { getUserConversations } = useGetUserConversations()
|
||||
const conversations = useAtomValue(userConversationsAtom)
|
||||
const conversations = useAtomValue(threadsAtom)
|
||||
const isEnableChat = (currentConvo && activeModel) || conversations.length > 0
|
||||
|
||||
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 { startModel } = useActiveModel()
|
||||
const modelRef = useRef(activeModel)
|
||||
|
||||
useEffect(() => {
|
||||
modelRef.current = activeModel
|
||||
}, [activeModel])
|
||||
|
||||
useEffect(() => {
|
||||
getUserConversations()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleMessageChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const onPromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCurrentPrompt(e.target.value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsModelAvailable(
|
||||
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) {
|
||||
if (isWaitingToSend && activeThreadId) {
|
||||
setIsWaitingToSend(false)
|
||||
sendChatMessage()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [waitingToSendMessage, activeConversationId])
|
||||
}, [waitingToSendMessage, activeThreadId])
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current !== null) {
|
||||
@ -136,11 +91,11 @@ const ChatScreen = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPrompt])
|
||||
|
||||
const handleKeyDown = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const onKeyDown = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSendMessage()
|
||||
sendChatMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -148,14 +103,14 @@ const ChatScreen = () => {
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<div className="flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r border-border">
|
||||
<HistoryList />
|
||||
<ThreadList />
|
||||
</div>
|
||||
<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">
|
||||
{isEnableChat && currentConvo && (
|
||||
<div className="h-[53px] flex-shrink-0 border-b border-border bg-background p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{currentConvo?.summary ?? ''}</span>
|
||||
<span>{currentConvo.title}</span>
|
||||
<div
|
||||
className={twMerge(
|
||||
'flex items-center space-x-3',
|
||||
@ -167,9 +122,9 @@ const ChatScreen = () => {
|
||||
themes="secondary"
|
||||
className="relative z-10"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onClick={() =>
|
||||
setMainViewState(MainViewState.ExploreModels)
|
||||
}}
|
||||
}
|
||||
>
|
||||
Download Model
|
||||
</Button>
|
||||
@ -177,12 +132,12 @@ const ChatScreen = () => {
|
||||
<Paintbrush
|
||||
size={16}
|
||||
className="cursor-pointer text-muted-foreground"
|
||||
onClick={() => cleanConvo()}
|
||||
onClick={() => cleanThread()}
|
||||
/>
|
||||
<Trash2Icon
|
||||
size={16}
|
||||
className="cursor-pointer text-muted-foreground"
|
||||
onClick={() => deleteConvo()}
|
||||
onClick={() => deleteThread()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -225,25 +180,24 @@ const ChatScreen = () => {
|
||||
<Textarea
|
||||
className="min-h-10 h-10 max-h-16 resize-none pr-20"
|
||||
ref={textareaRef}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
onKeyDown={(e) => onKeyDown(e)}
|
||||
placeholder="Type your message ..."
|
||||
disabled={stateModel.loading || !currentConvo}
|
||||
value={currentPrompt}
|
||||
onChange={(e) => {
|
||||
handleMessageChange(e)
|
||||
}}
|
||||
onChange={(e) => onPromptChange(e)}
|
||||
/>
|
||||
<Button
|
||||
size="lg"
|
||||
disabled={disabled || stateModel.loading || !currentConvo}
|
||||
themes={'primary'}
|
||||
onClick={handleSendMessage}
|
||||
onClick={sendChatMessage}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Sidebar />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -33,8 +33,6 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
||||
return null
|
||||
}
|
||||
|
||||
const { quantizationName, bits, maxRamRequired, usecase } = suitableModel
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -58,19 +56,15 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
||||
<div>
|
||||
<span className="mb-1 font-semibold">Compatibility</span>
|
||||
<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
|
||||
themes="secondary"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@ -81,10 +75,11 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
||||
<span className="font-semibold">Version</span>
|
||||
<div className="mt-2 flex space-x-2">
|
||||
<Badge themes="outline">v{model.version}</Badge>
|
||||
{quantizationName && (
|
||||
<Badge themes="outline">{quantizationName}</Badge>
|
||||
{suitableModel.metadata.quantization && (
|
||||
<Badge themes="outline">
|
||||
{suitableModel.metadata.quantization}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge themes="outline">{`${bits} Bits`}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@ -113,8 +108,7 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
||||
|
||||
{show && (
|
||||
<ModelVersionList
|
||||
model={model}
|
||||
versions={model.availableVersions}
|
||||
models={model.availableVersions}
|
||||
recommendedVersion={suitableModel?.name ?? ''}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
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 { atom, useAtomValue } from 'jotai'
|
||||
@ -23,7 +23,7 @@ import { toGigabytes } from '@/utils/converter'
|
||||
import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
|
||||
|
||||
type Props = {
|
||||
suitableModel: ModelVersion
|
||||
suitableModel: Model
|
||||
exploreModel: ModelCatalog
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
||||
const { setMainViewState } = useMainViewState()
|
||||
|
||||
const calculatePerformance = useCallback(
|
||||
(suitableModel: ModelVersion) => async () => {
|
||||
(suitableModel: Model) => async () => {
|
||||
const { title, performanceTag } = await getPerformanceForModel(
|
||||
suitableModel,
|
||||
totalRam
|
||||
@ -64,9 +64,9 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
||||
}, [suitableModel])
|
||||
|
||||
const onDownloadClick = useCallback(() => {
|
||||
downloadModel(exploreModel, suitableModel)
|
||||
downloadModel(suitableModel)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [exploreModel, suitableModel])
|
||||
}, [suitableModel])
|
||||
|
||||
// TODO: Comparing between Model Id and Version Name?
|
||||
const isDownloaded =
|
||||
@ -74,8 +74,8 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
||||
|
||||
let downloadButton = (
|
||||
<Button onClick={() => onDownloadClick()}>
|
||||
{suitableModel.size
|
||||
? `Download (${toGigabytes(suitableModel.size)})`
|
||||
{suitableModel.metadata.size
|
||||
? `Download (${toGigabytes(suitableModel.metadata.size)})`
|
||||
: 'Download'}
|
||||
</Button>
|
||||
)
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
||||
import { Button, Badge } from '@janhq/uikit'
|
||||
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
import { Badge, Button } from '@janhq/uikit'
|
||||
import { atom, useAtomValue } from 'jotai'
|
||||
|
||||
import ModalCancelDownload from '@/containers/ModalCancelDownload'
|
||||
@ -18,28 +17,29 @@ import { useMainViewState } from '@/hooks/useMainViewState'
|
||||
import { toGigabytes } from '@/utils/converter'
|
||||
|
||||
type Props = {
|
||||
model: ModelCatalog
|
||||
modelVersion: ModelVersion
|
||||
model: Model
|
||||
isRecommended: boolean
|
||||
}
|
||||
|
||||
const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
|
||||
const ModelVersionItem: React.FC<Props> = ({ model }) => {
|
||||
const { downloadModel } = useDownloadModel()
|
||||
const { downloadedModels } = useGetDownloadedModels()
|
||||
const { setMainViewState } = useMainViewState()
|
||||
const isDownloaded =
|
||||
downloadedModels.find((model) => model.id === modelVersion.name) != null
|
||||
downloadedModels.find(
|
||||
(downloadedModel) => downloadedModel.id === model.id
|
||||
) != null
|
||||
|
||||
const { modelDownloadStateAtom, downloadStates } = useDownloadState()
|
||||
|
||||
const downloadAtom = useMemo(
|
||||
() => atom((get) => get(modelDownloadStateAtom)[modelVersion.name ?? '']),
|
||||
[modelVersion.name]
|
||||
() => atom((get) => get(modelDownloadStateAtom)[model.id ?? '']),
|
||||
[model.id]
|
||||
)
|
||||
const downloadState = useAtomValue(downloadAtom)
|
||||
|
||||
const onDownloadClick = () => {
|
||||
downloadModel(model, modelVersion)
|
||||
downloadModel(model)
|
||||
}
|
||||
|
||||
let downloadButton = (
|
||||
@ -63,36 +63,26 @@ const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
|
||||
}
|
||||
|
||||
if (downloadState != null && downloadStates.length > 0) {
|
||||
downloadButton = (
|
||||
<ModalCancelDownload suitableModel={modelVersion} isFromList />
|
||||
)
|
||||
downloadButton = <ModalCancelDownload suitableModel={model} isFromList />
|
||||
}
|
||||
|
||||
const { maxRamRequired, usecase } = modelVersion
|
||||
|
||||
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 gap-2">
|
||||
<span className="line-clamp-1 flex-1" title={modelVersion.name}>
|
||||
{modelVersion.name}
|
||||
<span className="line-clamp-1 flex-1" title={model.name}>
|
||||
{model.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<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
|
||||
themes="secondary"
|
||||
className="line-clamp-1"
|
||||
title={`${toGigabytes(maxRamRequired)} RAM required`}
|
||||
>{`${toGigabytes(maxRamRequired)} RAM required`}</Badge>
|
||||
<Badge themes="secondary">{toGigabytes(modelVersion.size)}</Badge>
|
||||
title={`${toGigabytes(model.metadata.maxRamRequired)} RAM required`}
|
||||
>{`${toGigabytes(
|
||||
model.metadata.maxRamRequired
|
||||
)} RAM required`}</Badge>
|
||||
<Badge themes="secondary">{toGigabytes(model.metadata.size)}</Badge>
|
||||
</div>
|
||||
{downloadButton}
|
||||
</div>
|
||||
|
||||
@ -1,26 +1,23 @@
|
||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
|
||||
import ModelVersionItem from '../ModelVersionItem'
|
||||
|
||||
type Props = {
|
||||
model: ModelCatalog
|
||||
versions: ModelVersion[]
|
||||
models: Model[]
|
||||
recommendedVersion: string
|
||||
}
|
||||
|
||||
export default function ModelVersionList({
|
||||
model,
|
||||
versions,
|
||||
models,
|
||||
recommendedVersion,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="pt-4">
|
||||
{versions.map((item) => (
|
||||
{models.map((model) => (
|
||||
<ModelVersionItem
|
||||
key={item.name}
|
||||
key={model.name}
|
||||
model={model}
|
||||
modelVersion={item}
|
||||
isRecommended={item.name === recommendedVersion}
|
||||
isRecommended={model.name === recommendedVersion}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -64,28 +64,28 @@ const MyModelsScreen = () => {
|
||||
<div className="inline-flex rounded-full border border-border p-1">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage
|
||||
src={model.avatarUrl}
|
||||
alt={model.author}
|
||||
src={model.metadata.avatarUrl}
|
||||
alt={model.metadata.author}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{model.author.charAt(0)}
|
||||
{model.metadata.author.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="mb-1 font-medium capitalize">
|
||||
{model.author}
|
||||
{model.metadata.author}
|
||||
</h2>
|
||||
<p className="line-clamp-1">{model.name}</p>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Badge themes="secondary">v{model.version}</Badge>
|
||||
<Badge themes="outline">GGUF</Badge>
|
||||
<Badge themes="outline">
|
||||
{toGigabytes(model.size)}
|
||||
{toGigabytes(model.metadata.size)}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-2 line-clamp-2 break-all">
|
||||
{model.longDescription}
|
||||
{model.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -102,7 +102,7 @@ const MyModelsScreen = () => {
|
||||
</ModalHeader>
|
||||
<p className="leading-relaxed">
|
||||
Delete model {model.name}, v{model.version},{' '}
|
||||
{toGigabytes(model.size)}.
|
||||
{toGigabytes(model.metadata.size)}.
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<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 = () => {
|
||||
return `jan-${(Date.now() / 1000).toFixed(0)}`
|
||||
export const generateThreadId = (assistantId: string) => {
|
||||
return `${assistantId}_${(Date.now() / 1000).toFixed(0)}`
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { ModelCatalog } from '@janhq/core'
|
||||
import { ModelCatalog, ModelState } from '@janhq/core'
|
||||
|
||||
export const dummyModel: ModelCatalog = {
|
||||
id: 'aladar/TinyLLama-v0-GGUF',
|
||||
@ -14,37 +14,106 @@ export const dummyModel: ModelCatalog = {
|
||||
tags: ['freeform', 'tags'],
|
||||
availableVersions: [
|
||||
{
|
||||
name: 'TinyLLama-v0.Q8_0.gguf',
|
||||
quantizationName: '',
|
||||
bits: 2,
|
||||
size: 5816320,
|
||||
maxRamRequired: 256000000,
|
||||
usecase:
|
||||
'smallest, significant quality loss - not recommended for most purposes',
|
||||
downloadLink:
|
||||
object: 'model',
|
||||
version: '1.0.0',
|
||||
source_url:
|
||||
'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',
|
||||
quantizationName: '',
|
||||
bits: 2,
|
||||
size: 10240000,
|
||||
maxRamRequired: 256000000,
|
||||
usecase:
|
||||
'smallest, significant quality loss - not recommended for most purposes',
|
||||
downloadLink:
|
||||
object: 'model',
|
||||
version: '1.0.0',
|
||||
source_url:
|
||||
'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',
|
||||
quantizationName: '',
|
||||
bits: 2,
|
||||
size: 19660000,
|
||||
maxRamRequired: 256000000,
|
||||
usecase:
|
||||
'smallest, significant quality loss - not recommended for most purposes',
|
||||
downloadLink:
|
||||
object: 'model',
|
||||
version: '1.0.0',
|
||||
source_url:
|
||||
'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