Merge branch 'main' into docs/label-api-reference
This commit is contained in:
commit
a38901b2c8
18
.github/workflows/jan-electron-build.yml
vendored
18
.github/workflows/jan-electron-build.yml
vendored
@ -121,22 +121,10 @@ jobs:
|
||||
env:
|
||||
VERSION_TAG: ${{ steps.tag.outputs.tag }}
|
||||
|
||||
- name: Build uikit
|
||||
- name: Build app
|
||||
shell: cmd
|
||||
run: |
|
||||
cd uikit
|
||||
yarn config set network-timeout 300000
|
||||
yarn install
|
||||
yarn build
|
||||
|
||||
- name: Install yarn dependencies
|
||||
shell: powershell
|
||||
run: |
|
||||
yarn config set network-timeout 300000
|
||||
yarn build:core
|
||||
yarn install
|
||||
$env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION
|
||||
yarn build:plugins
|
||||
yarn build
|
||||
make build
|
||||
|
||||
- name: Windows Code Sign with AzureSignTool
|
||||
run: |
|
||||
|
||||
20
Makefile
20
Makefile
@ -15,14 +15,11 @@ endif
|
||||
# Installs yarn dependencies and builds core and plugins
|
||||
install-and-build: build-uikit
|
||||
ifeq ($(OS),Windows_NT)
|
||||
powershell -Command "yarn config set network-timeout 300000; \
|
||||
$$env:NITRO_VERSION = Get-Content .\\plugins\\inference-plugin\\nitro\\version.txt; \
|
||||
Write-Output \"Nitro version: $$env:NITRO_VERSION\"; yarn build:core; yarn install; yarn build:plugins"
|
||||
else
|
||||
yarn config set network-timeout 300000
|
||||
endif
|
||||
yarn build:core
|
||||
yarn install
|
||||
yarn build:plugins
|
||||
endif
|
||||
|
||||
dev: install-and-build
|
||||
yarn dev
|
||||
@ -47,8 +44,21 @@ build: install-and-build
|
||||
clean:
|
||||
ifeq ($(OS),Windows_NT)
|
||||
powershell -Command "Get-ChildItem -Path . -Include node_modules, .next, dist -Recurse -Directory | Remove-Item -Recurse -Force"
|
||||
rmdir /s /q "%USERPROFILE%\AppData\Roaming\jan"
|
||||
rmdir /s /q "%USERPROFILE%\AppData\Roaming\jan-electron"
|
||||
rmdir /s /q "%USERPROFILE%\AppData\Local\jan*"
|
||||
else ifeq ($(shell uname -s),Linux)
|
||||
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name ".next" -type d -exec rm -rf '{}' +
|
||||
find . -name "dist" -type d -exec rm -rf '{}' +
|
||||
rm -rf "~/.config/jan"
|
||||
rm -rf "~/.config/jan-electron"
|
||||
rm -rf "~/.cache/jan*"
|
||||
else
|
||||
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name ".next" -type d -exec rm -rf '{}' +
|
||||
find . -name "dist" -type d -exec rm -rf '{}' +
|
||||
rm -rf ~/Library/Application\ Support/jan
|
||||
rm -rf ~/Library/Application\ Support/jan-electron
|
||||
rm -rf ~/Library/Caches/jan*
|
||||
endif
|
||||
|
||||
@ -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,15 @@ const invokePluginFunc: (
|
||||
const downloadFile: (url: string, fileName: string) => Promise<any> = (
|
||||
url,
|
||||
fileName
|
||||
) =>
|
||||
window.coreAPI?.downloadFile(url, fileName) ??
|
||||
window.electronAPI?.downloadFile(url, fileName);
|
||||
) => window.coreAPI?.downloadFile(url, fileName);
|
||||
|
||||
/**
|
||||
* @deprecated This object is deprecated and should not be used.
|
||||
* Use fs module instead.
|
||||
* Aborts the download of a specific file.
|
||||
* @param {string} fileName - The name of the file whose download is to be aborted.
|
||||
* @returns {Promise<any>} A promise that resolves when the download has been aborted.
|
||||
*/
|
||||
const deleteFile: (path: string) => Promise<any> = (path) =>
|
||||
window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path);
|
||||
const abortDownload: (fileName: string) => Promise<any> = (fileName) =>
|
||||
window.coreAPI?.abortDownload(fileName);
|
||||
|
||||
/**
|
||||
* Retrieves the path to the app data directory using the `coreAPI` object.
|
||||
@ -58,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,
|
||||
@ -71,27 +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,
|
||||
deleteFile,
|
||||
appDataPath,
|
||||
getUserSpace,
|
||||
};
|
||||
|
||||
/**
|
||||
* Functions exports
|
||||
*/
|
||||
export {
|
||||
invokePluginFunc,
|
||||
executeOnMain,
|
||||
downloadFile,
|
||||
deleteFile,
|
||||
abortDownload,
|
||||
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,34 +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 } 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.
|
||||
|
||||
@ -16,12 +16,19 @@ export abstract class ModelPlugin extends JanPlugin {
|
||||
*/
|
||||
abstract downloadModel(model: Model): Promise<void>;
|
||||
|
||||
/**
|
||||
* Cancels the download of a specific model.
|
||||
* @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(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,5 +1,6 @@
|
||||
---
|
||||
title: Architecture
|
||||
slug: /specs
|
||||
---
|
||||
|
||||
:::warning
|
||||
@ -10,35 +11,28 @@ This page is still under construction, and should be read as a scratchpad
|
||||
|
||||
## Overview
|
||||
|
||||
- Jan built a modular infrastructure on top of Electron, in order to support extensions and AI functionality.
|
||||
- Jan is largely built on top of its own modules.
|
||||
- Jan has a modular architecture and is largely built on top of its own modules.
|
||||
- Jan uses a local [file-based approach](/specs/file-based) for data persistence.
|
||||
|
||||
## Modules
|
||||
|
||||
Modules are low level, system services. It is similar to OS kernel modules, in that `modules` provide abstractions to device level, basic functionality like the filesystem, device system, databases, AI inference engines, etc.
|
||||
|
||||
## Pluggable Modules
|
||||
|
||||
Jan exports modules that mirror OpenAI’s, exposing similar APIs and objects:
|
||||
|
||||
- Modules are modular, atomic implementations of a single OpenAI-compatible endpoint
|
||||
- Modules can be swapped out for alternate implementations
|
||||
- The default `messages` module persists messages in thread-specific `.json`
|
||||
- `messages-postgresql` uses Postgres for production-grade cloud-native environments
|
||||
|
||||
| Jan Module | Description | API Docs |
|
||||
| ---------- | ------------- | ---------------------------- |
|
||||
| Chat | Inference | [/chat](/api/chat) |
|
||||
| Models | Models | [/model](/api/model) |
|
||||
| Assistants | Apps | [/assistant](/api/assistant) |
|
||||
| Threads | Conversations | [/thread](/api/thread) |
|
||||
| Messages | Messages | [/message](/api/message) |
|
||||
|
||||
<!-- TODO: link npm modules -->
|
||||
- Jan currently supports an Electron-based [Desktop UI](https://github.com/janhq/jan) and a C++ inference engine called [Nitro](https://nitro.jan.ai/docs/).
|
||||
|
||||
## Extensions
|
||||
|
||||
Extensions are feature level services that include both UI and logic implementation.
|
||||
Jan has an Extensions API inspired by VSCode. In fact, most of Jan's core services are built as extensions.
|
||||
|
||||
<!-- TODO[@linh]: add all of @linh's specs here -->
|
||||
Jan supports the following OpenAI compatible extensions:
|
||||
|
||||
| Jan Module | Description | API Docs |
|
||||
| ---------- | ------------- | --------------------------------------------- |
|
||||
| Chat | Inference | [/chats](/api-reference/#tag/Chat-Completion) |
|
||||
| Models | Models | [/models](/api-reference/#tag/Models) |
|
||||
| Assistants | Apps | [/assistants](/api-reference/#tag/Assistants) |
|
||||
| Threads | Conversations | [/threads](/api-reference/#tag/Threads) |
|
||||
| Messages | Messages | [/messages](/api-reference/#tag/Messages) |
|
||||
|
||||
<!-- TODO: link npm modules -->
|
||||
|
||||
## Modules
|
||||
|
||||
Modules are low level, system services. It is similar to OS kernel modules. Modules provide abstractions to basic, device level functionality like working with the filesystem, device system, databases, AI inference engines, etc.
|
||||
|
||||
Jan follows the [dependency inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) such that `modules` expose the interfaces that `extensions` can then implement.
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
---
|
||||
title: "Assistants"
|
||||
slug: /specs/assistants
|
||||
---
|
||||
|
||||
:::caution
|
||||
@ -14,7 +15,7 @@ In Jan, assistants are `primary` entities with the following capabilities:
|
||||
|
||||
- Assistants can use `models`, `tools`, handle and emit `events`, and invoke `custom code`.
|
||||
- Users can create custom assistants with saved `model` settings and parameters.
|
||||
- An [OpenAI Assistants API](https://platform.openai.com/docs/api-reference/assistants) compatible endpoint at `localhost:3000/v1/assistants`.
|
||||
- An [OpenAI Assistants API](https://platform.openai.com/docs/api-reference/assistants) compatible endpoint at `localhost:1337/v1/assistants`.
|
||||
- Jan ships with a default assistant called "Jan" that lets you use all models.
|
||||
|
||||
## Folder Structure
|
||||
@ -49,6 +50,7 @@ In Jan, assistants are `primary` entities with the following capabilities:
|
||||
"models": [ // Defaults to "*" all models
|
||||
{ ...model_0 }
|
||||
],
|
||||
"instructions": "Be concise", // A system prompt for the assistant
|
||||
"events": [], // Defaults to "*"
|
||||
"metadata": {}, // Defaults to {}
|
||||
// "tools": [], // Coming soon
|
||||
@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Chats
|
||||
slug: /specs/chats
|
||||
---
|
||||
|
||||
:::caution
|
||||
@ -13,7 +14,7 @@ This is currently under development.
|
||||
In Jan, `chats` are LLM responses in the form of OpenAI compatible `chat completion objects`.
|
||||
|
||||
- Models take a list of messages and return a model-generated response as output.
|
||||
- An [OpenAI Chat API](https://platform.openai.com/docs/api-reference/chat) compatible endpoint at `localhost:3000/v1/chats`.
|
||||
- An [OpenAI Chat API](https://platform.openai.com/docs/api-reference/chat) compatible endpoint at `localhost:1337/v1/chats`.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
---
|
||||
title: "Files"
|
||||
slug: /specs/files
|
||||
---
|
||||
|
||||
:::warning
|
||||
6
docs/docs/specs/engineering/fine-tuning.md
Normal file
6
docs/docs/specs/engineering/fine-tuning.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: "Fine-tuning"
|
||||
slug: /specs/finetuning
|
||||
---
|
||||
|
||||
Todo: @hiro
|
||||
@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Messages
|
||||
slug: /specs/messages
|
||||
---
|
||||
|
||||
:::caution
|
||||
@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Models
|
||||
slug: /specs/models
|
||||
---
|
||||
|
||||
:::caution
|
||||
@ -13,7 +14,7 @@ This is currently under development.
|
||||
In Jan, models are primary entities with the following capabilities:
|
||||
|
||||
- Users can import, configure, and run models locally.
|
||||
- An [OpenAI Model API](https://platform.openai.com/docs/api-reference/models) compatible endpoint at `localhost:3000/v1/models`.
|
||||
- An [OpenAI Model API](https://platform.openai.com/docs/api-reference/models) compatible endpoint at `localhost:1337/v1/models`.
|
||||
- Supported model formats: `ggufv3`, and more.
|
||||
|
||||
## Folder Structure
|
||||
@ -64,17 +65,19 @@ Here's a standard example `model.json` for a GGUF model.
|
||||
"state": enum[null, "downloading", "ready", "starting", "stopping", ...]
|
||||
"format": "ggufv3", // Defaults to "ggufv3"
|
||||
"settings": { // Models are initialized with settings
|
||||
"ctx_len": "2048",
|
||||
"ngl": "100",
|
||||
"embedding": "true",
|
||||
"n_parallel": "4",
|
||||
"ctx_len": 2048,
|
||||
"ngl": 100,
|
||||
"embedding": true,
|
||||
"n_parallel": 4,
|
||||
},
|
||||
"parameters": { // Models are called parameters
|
||||
"temperature": "0.7",
|
||||
"token_limit": "2048",
|
||||
"top_k": "0",
|
||||
"top_p": "1",
|
||||
"stream": "true"
|
||||
"stream": true,
|
||||
"max_tokens": 2048,
|
||||
"stop": ["<endofstring>"], // This usually can be left blank, only used with specific need from model author
|
||||
"frequency_penalty": 0,
|
||||
"presence_penalty": 0,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95
|
||||
},
|
||||
"metadata": {}, // Defaults to {}
|
||||
"assets": [ // Defaults to current dir
|
||||
@ -82,6 +85,10 @@ Here's a standard example `model.json` for a GGUF model.
|
||||
]
|
||||
```
|
||||
|
||||
The model settings in the example can be found at: [Nitro's model settings](https://nitro.jan.ai/features/load-unload#table-of-parameters)
|
||||
|
||||
The model parameters in the example can be found at: [Nitro's model parameters](https://nitro.jan.ai/api-reference#tag/Chat-Completion)
|
||||
|
||||
## API Reference
|
||||
|
||||
Jan's Model API is compatible with [OpenAI's Models API](https://platform.openai.com/docs/api-reference/models), with additional methods for managing and running models locally.
|
||||
@ -1,7 +1,8 @@
|
||||
---
|
||||
title: Prompts
|
||||
slug: /specs/prompts
|
||||
---
|
||||
|
||||
- [ ] /prompts folder
|
||||
- [ ] How to add to prompts
|
||||
- [ ] Assistants can have suggested Prompts
|
||||
- [ ] Assistants can have suggested Prompts
|
||||
@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Threads
|
||||
slug: /specs/threads
|
||||
---
|
||||
|
||||
:::caution
|
||||
@ -1,4 +0,0 @@
|
||||
---
|
||||
title: "Fine-tuning"
|
||||
---
|
||||
Todo: @hiro
|
||||
@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Home
|
||||
---
|
||||
@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Hub
|
||||
---
|
||||
BIN
docs/docs/specs/img/chat-screen.png
Normal file
BIN
docs/docs/specs/img/chat-screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
BIN
docs/docs/specs/img/hub-screen.png
Normal file
BIN
docs/docs/specs/img/hub-screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 629 KiB |
BIN
docs/docs/specs/img/settings-screen.png
Normal file
BIN
docs/docs/specs/img/settings-screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
BIN
docs/docs/specs/img/system-screen.png
Normal file
BIN
docs/docs/specs/img/system-screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
17
docs/docs/specs/product/chat.md
Normal file
17
docs/docs/specs/product/chat.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Chat
|
||||
slug: /specs/chat
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
A home screen for users to chat with [assistants](/specs/assistants) via conversation [threads](/specs/threads).
|
||||
|
||||

|
||||
|
||||
## User Stories
|
||||
|
||||
<!-- Can also be used as a QA Checklist -->
|
||||
|
||||
- Users can chat with `Jan` the default assistant
|
||||
- Users can customize chat settings like model parameters via both the GUI & `thread.json`
|
||||
18
docs/docs/specs/product/hub.md
Normal file
18
docs/docs/specs/product/hub.md
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Hub
|
||||
slug: /specs/hub
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Hub is like a store for everything, where users can discover and download models, assistants, and more.
|
||||
|
||||

|
||||
|
||||
## User Stories
|
||||
|
||||
<!-- Can also be used as a QA Checklist -->
|
||||
|
||||
- Users can discover recommended models (Jan ships with a few preconfigured `model.json` files)
|
||||
- Users can download models suitable for their devices, e.g. compatible with their RAM
|
||||
- Users can download models via a HuggingFace URL (coming soon)
|
||||
32
docs/docs/specs/product/settings.md
Normal file
32
docs/docs/specs/product/settings.md
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Settings
|
||||
slug: /specs/settings
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
A settings page for users to add extensions, configure model settings, change app appearance, add keyboard shortcuts, and a plethora of other personalizations.
|
||||
|
||||

|
||||
|
||||
## User Stories
|
||||
|
||||
<!-- Can also be used as a QA Checklist -->
|
||||
|
||||
### General Settings
|
||||
|
||||
- Users can customize `port` number
|
||||
- Users can customize `janroot` folder location
|
||||
|
||||
### Extensions Settings
|
||||
|
||||
- Users can add, delete, and configure extensions
|
||||
|
||||
### Model Settings
|
||||
|
||||
- Users can configure default model parameters and settings
|
||||
- Users can delete models
|
||||
|
||||
### Appearance
|
||||
|
||||
- Users can set color themes and dark/light modes
|
||||
17
docs/docs/specs/product/system-monitor.md
Normal file
17
docs/docs/specs/product/system-monitor.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
title: System Monitor
|
||||
slug: /specs/system-monitor
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
An activity screen to monitor system health and running models.
|
||||
|
||||

|
||||
|
||||
## User Stories
|
||||
|
||||
<!-- Can also be used as a QA Checklist -->
|
||||
|
||||
- Users can see disk and ram utilization
|
||||
- Users can start and stop models based on system health
|
||||
@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Settings
|
||||
---
|
||||
|
||||
- [ ] .jan folder in jan root
|
||||
@ -1,3 +0,0 @@
|
||||
---
|
||||
title: System Monitor
|
||||
---
|
||||
@ -66,10 +66,10 @@ const sidebars = {
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
items: [
|
||||
"specs/home",
|
||||
"specs/hub",
|
||||
"specs/system-monitor",
|
||||
"specs/settings",
|
||||
"specs/product/chat",
|
||||
"specs/product/hub",
|
||||
"specs/product/system-monitor",
|
||||
"specs/product/settings",
|
||||
],
|
||||
},
|
||||
|
||||
@ -79,16 +79,16 @@ const sidebars = {
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
items: [
|
||||
"specs/chats",
|
||||
"specs/models",
|
||||
"specs/threads",
|
||||
"specs/messages",
|
||||
"specs/assistants",
|
||||
// "specs/files",
|
||||
// "specs/jan",
|
||||
// "specs/fine-tuning",
|
||||
// "specs/settings",
|
||||
// "specs/prompts",
|
||||
"specs/engineering/chats",
|
||||
"specs/engineering/models",
|
||||
"specs/engineering/threads",
|
||||
"specs/engineering/messages",
|
||||
"specs/engineering/assistants",
|
||||
// "specs/engineering/files",
|
||||
// "specs/engineering/jan",
|
||||
// "specs/engineering/fine-tuning",
|
||||
// "specs/engineering/settings",
|
||||
// "specs/engineering/prompts",
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@ -29,7 +29,8 @@ export function handleAppIPCs() {
|
||||
* @param _event - The IPC event object.
|
||||
*/
|
||||
ipcMain.handle("openAppDirectory", async (_event) => {
|
||||
shell.openPath(app.getPath("userData"));
|
||||
const userSpacePath = join(app.getPath('home'), 'jan')
|
||||
shell.openPath(userSpacePath);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -34,22 +34,7 @@ export function handleDownloaderIPCs() {
|
||||
ipcMain.handle('abortDownload', async (_event, fileName) => {
|
||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||
const userDataPath = app.getPath('userData')
|
||||
const fullPath = join(userDataPath, fileName)
|
||||
rq?.abort()
|
||||
let result = 'NULL'
|
||||
unlink(fullPath, function (err) {
|
||||
if (err && err.code == 'ENOENT') {
|
||||
result = `File not exist: ${err}`
|
||||
} else if (err) {
|
||||
result = `File delete error: ${err}`
|
||||
} else {
|
||||
result = 'File deleted successfully'
|
||||
}
|
||||
console.debug(
|
||||
`Delete file ${fileName} from ${fullPath} result: ${result}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@ -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
plugins/inference-plugin/download.bat
Normal file
4
plugins/inference-plugin/download.bat
Normal file
@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set /p NITRO_VERSION=<./nitro/version.txt
|
||||
.\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.zip -e --strip 1 -o ./nitro/win-cuda
|
||||
.\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.zip -e --strip 1 -o ./nitro/win-cpu
|
||||
@ -1 +1 @@
|
||||
0.1.11
|
||||
0.1.17
|
||||
@ -13,9 +13,9 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"downloadnitro:linux": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.zip -e --strip 1 -o ./nitro/linux-cpu && chmod +x ./nitro/linux-cpu/nitro && chmod +x ./nitro/linux-start.sh && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.zip -e --strip 1 -o ./nitro/linux-cuda && chmod +x ./nitro/linux-cuda/nitro && chmod +x ./nitro/linux-start.sh",
|
||||
"downloadnitro:darwin": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.zip -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.zip -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro",
|
||||
"downloadnitro:win32": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.zip -e --strip 1 -o ./nitro/win-cpu && download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.zip -e --strip 1 -o ./nitro/win-cuda",
|
||||
"downloadnitro:linux": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.tar.gz -e --strip 1 -o ./nitro/linux-cpu && chmod +x ./nitro/linux-cpu/nitro && chmod +x ./nitro/linux-start.sh && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.tar.gz -e --strip 1 -o ./nitro/linux-cuda && chmod +x ./nitro/linux-cuda/nitro && chmod +x ./nitro/linux-start.sh",
|
||||
"downloadnitro:darwin": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.tar.gz -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.tar.gz -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro",
|
||||
"downloadnitro:win32": "download.bat",
|
||||
"downloadnitro": "run-script-os",
|
||||
"build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PluginType, fs, downloadFile } from '@janhq/core'
|
||||
import { PluginType, fs, downloadFile, abortDownload } from '@janhq/core'
|
||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||
import { Model, ModelCatalog } from '@janhq/core/lib/types'
|
||||
import { parseToModel } from './helpers/modelParser'
|
||||
@ -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,25 @@ 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the download of a specific machine learning model.
|
||||
* @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(modelId: string): Promise<void> {
|
||||
return abortDownload(join(JanModelPlugin._homeDir, modelId, modelId)).then(
|
||||
() => {
|
||||
fs.rmdir(join(JanModelPlugin._homeDir, modelId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,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)
|
||||
}
|
||||
@ -72,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)
|
||||
}
|
||||
@ -98,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
|
||||
@ -1,5 +1,7 @@
|
||||
import { Fragment } from 'react'
|
||||
|
||||
import { PluginType } from '@janhq/core'
|
||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||
import {
|
||||
Progress,
|
||||
Modal,
|
||||
@ -10,12 +12,18 @@ import {
|
||||
ModalTrigger,
|
||||
} from '@janhq/uikit'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
|
||||
import { formatDownloadPercentage } from '@/utils/converter'
|
||||
|
||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
import { pluginManager } from '@/plugin'
|
||||
|
||||
export default function DownloadingState() {
|
||||
const { downloadStates } = useDownloadState()
|
||||
const models = useAtomValue(downloadingModelsAtom)
|
||||
|
||||
const totalCurrentProgress = downloadStates
|
||||
.map((a) => a.size.transferred + a.size.transferred)
|
||||
@ -67,11 +75,17 @@ export default function DownloadingState() {
|
||||
<Button
|
||||
themes="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
window.coreAPI?.abortDownload(
|
||||
`models/${item?.fileName}`
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
if (item?.fileName) {
|
||||
const model = models.find(
|
||||
(e) => e.id === item?.fileName
|
||||
)
|
||||
if (!model) return
|
||||
pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.cancelModelDownload(item.modelId)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@ -20,15 +20,12 @@ import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import LogoMark from '@/containers/Brand/Logo/Mark'
|
||||
|
||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||
|
||||
import { MainViewState } from '@/constants/screens'
|
||||
|
||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||
|
||||
export default function RibbonNav() {
|
||||
const { mainViewState, setMainViewState } = useMainViewState()
|
||||
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||
|
||||
const onMenuClick = (state: MainViewState) => {
|
||||
if (mainViewState === state) return
|
||||
@ -49,8 +46,6 @@ export default function RibbonNav() {
|
||||
]
|
||||
|
||||
const secondaryMenus = [
|
||||
// Add menu if experimental feature
|
||||
...(experimentalFeatureEnabed ? [] : []),
|
||||
{
|
||||
name: 'Explore Models',
|
||||
icon: <CpuIcon size={20} className="flex-shrink-0" />,
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
CommandList,
|
||||
} from '@janhq/uikit'
|
||||
|
||||
import { useSetAtom } from 'jotai'
|
||||
import {
|
||||
MessageCircleIcon,
|
||||
SettingsIcon,
|
||||
@ -27,9 +28,12 @@ import { MainViewState } from '@/constants/screens'
|
||||
|
||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||
|
||||
import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
|
||||
|
||||
export default function CommandSearch() {
|
||||
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||
const { setMainViewState } = useMainViewState()
|
||||
const [open, setOpen] = useState(false)
|
||||
const setShowRightSideBar = useSetAtom(showRightSideBarAtom)
|
||||
|
||||
const menus = [
|
||||
{
|
||||
@ -44,8 +48,6 @@ export default function CommandSearch() {
|
||||
),
|
||||
state: MainViewState.Chat,
|
||||
},
|
||||
// Added experimental feature here
|
||||
...(experimentalFeatureEnabed ? [] : []),
|
||||
{
|
||||
name: 'Explore Models',
|
||||
icon: <CpuIcon size={16} className="mr-3 text-muted-foreground" />,
|
||||
@ -64,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)) {
|
||||
@ -123,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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { ModelVersion } from '@janhq/core/lib/types'
|
||||
import { PluginType } from '@janhq/core'
|
||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
|
||||
import {
|
||||
Modal,
|
||||
@ -19,8 +21,11 @@ import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
|
||||
import { formatDownloadPercentage } from '@/utils/converter'
|
||||
|
||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
import { pluginManager } from '@/plugin'
|
||||
|
||||
type Props = {
|
||||
suitableModel: ModelVersion
|
||||
suitableModel: Model
|
||||
isFromList?: boolean
|
||||
}
|
||||
|
||||
@ -34,6 +39,7 @@ export default function ModalCancelDownload({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[suitableModel.name]
|
||||
)
|
||||
const models = useAtomValue(downloadingModelsAtom)
|
||||
const downloadState = useAtomValue(downloadAtom)
|
||||
|
||||
return (
|
||||
@ -66,10 +72,15 @@ export default function ModalCancelDownload({
|
||||
<Button
|
||||
themes="danger"
|
||||
onClick={() => {
|
||||
if (downloadState?.fileName)
|
||||
window.coreAPI?.abortDownload(
|
||||
`models/${downloadState?.fileName}`
|
||||
if (downloadState?.fileName) {
|
||||
const model = models.find(
|
||||
(e) => e.id === downloadState?.fileName
|
||||
)
|
||||
if (!model) return
|
||||
pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.cancelModelDownload(downloadState.modelId)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -27,9 +24,10 @@ export function useActiveModel() {
|
||||
(activeModel && activeModel.id === modelId) ||
|
||||
(stateModel.model === modelId && stateModel.loading)
|
||||
) {
|
||||
console.debug(`Model ${modelId} is already init. Ignore..`)
|
||||
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)
|
||||
@ -64,13 +61,13 @@ export function useActiveModel() {
|
||||
}))
|
||||
} else {
|
||||
console.debug(
|
||||
`Init model ${modelId} successfully!, take ${
|
||||
`Model ${modelId} successfully initialized! Took ${
|
||||
Date.now() - currentTime
|
||||
}ms`
|
||||
)
|
||||
setActiveModel(model)
|
||||
toaster({
|
||||
title: 'Success start a Model',
|
||||
title: 'Success!',
|
||||
description: `Model ${modelId} has been started.`,
|
||||
})
|
||||
setStateModel(() => ({
|
||||
@ -89,7 +86,7 @@ export function useActiveModel() {
|
||||
setActiveModel(undefined)
|
||||
setStateModel({ state: 'start', loading: false, model: '' })
|
||||
toaster({
|
||||
title: 'Success stop a Model',
|
||||
title: 'Success!',
|
||||
description: `Model ${modelId} has been stopped.`,
|
||||
})
|
||||
}, 500)
|
||||
@ -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,34 +27,35 @@ 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,
|
||||
content:
|
||||
'summary this conversation in less than 5 words, the response should just include the summary',
|
||||
'Summarize this conversation in less than 5 words, the response should just include the summary',
|
||||
}
|
||||
// Request convo summary
|
||||
setTimeout(async () => {
|
||||
@ -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,25 +1,16 @@
|
||||
import { useContext } from 'react'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||
|
||||
import ChatInstruction from '../ChatInstruction'
|
||||
import ChatItem from '../ChatItem'
|
||||
|
||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||
|
||||
const ChatBody: React.FC = () => {
|
||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||
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} />
|
||||
))}
|
||||
{experimentalFeatureEnabed && messages.length === 0 && (
|
||||
<ChatInstruction />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,70 +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">
|
||||
{!isSettingInstruction && activeConvoId && (
|
||||
<>
|
||||
<p>
|
||||
What does this Assistant do? How does it behave? What should it
|
||||
avoid doing?
|
||||
</p>
|
||||
<Button
|
||||
themes={'outline'}
|
||||
className="w-32"
|
||||
onClick={() => setIsSettingInstruction(true)}
|
||||
>
|
||||
Give Instruction
|
||||
</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 Instruction
|
||||
</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,8 +48,12 @@ const marked = new Marked(
|
||||
)
|
||||
|
||||
const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||
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)
|
||||
@ -61,13 +63,14 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.status === MessageStatus.Ready || !experimentalFeatureEnabed) {
|
||||
if (props.status === MessageStatus.Ready) {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -90,24 +93,22 @@ 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>
|
||||
{experimentalFeatureEnabed && (
|
||||
<div
|
||||
className={twMerge(
|
||||
'absolute right-0 cursor-pointer transition-all',
|
||||
messages[0].id === props.id
|
||||
? 'absolute -bottom-10 left-4'
|
||||
: 'hidden group-hover:flex'
|
||||
)}
|
||||
>
|
||||
<MessageToolbar message={props} />
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs font-medium">{displayDate(props.created)}</p>
|
||||
<div
|
||||
className={twMerge(
|
||||
'absolute right-0 cursor-pointer transition-all',
|
||||
messages[messages.length - 1]?.id === props.id
|
||||
? 'absolute -bottom-10 left-4'
|
||||
: 'hidden group-hover:flex'
|
||||
)}
|
||||
>
|
||||
<MessageToolbar message={props} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={twMerge('w-full')}>
|
||||
{props.status === MessageStatus.Pending &&
|
||||
(!props.content || props.content === '') ? (
|
||||
(!props.content[0] || props.content[0].text.value === '') ? (
|
||||
<BubbleLoader />
|
||||
) : (
|
||||
<>
|
||||
@ -122,12 +123,11 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{experimentalFeatureEnabed &&
|
||||
(props.status === MessageStatus.Pending || tokenSpeed > 0) && (
|
||||
<p className="mt-2 text-xs font-medium text-foreground">
|
||||
Token Speed: {Number(tokenSpeed).toFixed(2)}/s
|
||||
</p>
|
||||
)}
|
||||
{(props.status === MessageStatus.Pending || tokenSpeed > 0) && (
|
||||
<p className="mt-2 text-xs font-medium text-foreground">
|
||||
Token Speed: {Number(tokenSpeed).toFixed(2)}/s
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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,117 +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 [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
|
||||
const { requestCreateConvo } = useCreateConversation()
|
||||
const { getUserConversations } = useGetUserConversations()
|
||||
const conversations = useAtomValue(userConversationsAtom)
|
||||
const isEnableChat = (currentConvo && activeModel) || conversations.length > 0
|
||||
const [isModelAvailable, setIsModelAvailable] = useState(
|
||||
downloadedModels.some((x) => x.id === currentConvo?.modelId)
|
||||
)
|
||||
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||
|
||||
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||
const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
|
||||
const conversations = useAtomValue(threadsAtom)
|
||||
const isEnableChat = (currentConvo && activeModel) || conversations.length > 0
|
||||
|
||||
const [isModelAvailable, setIsModelAvailable] = useState(
|
||||
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) {
|
||||
@ -138,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -150,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',
|
||||
@ -169,27 +122,23 @@ const ChatScreen = () => {
|
||||
themes="secondary"
|
||||
className="relative z-10"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onClick={() =>
|
||||
setMainViewState(MainViewState.ExploreModels)
|
||||
}}
|
||||
}
|
||||
>
|
||||
Download Model
|
||||
</Button>
|
||||
)}
|
||||
{experimentalFeatureEnabed && (
|
||||
<Paintbrush
|
||||
size={16}
|
||||
className="cursor-pointer text-muted-foreground"
|
||||
onClick={() => cleanConvo()}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
<Trash2Icon
|
||||
size={16}
|
||||
className="cursor-pointer text-muted-foreground"
|
||||
onClick={() => deleteConvo()}
|
||||
/>
|
||||
}
|
||||
<Paintbrush
|
||||
size={16}
|
||||
className="cursor-pointer text-muted-foreground"
|
||||
onClick={() => cleanThread()}
|
||||
/>
|
||||
<Trash2Icon
|
||||
size={16}
|
||||
className="cursor-pointer text-muted-foreground"
|
||||
onClick={() => deleteThread()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -203,8 +152,8 @@ const ChatScreen = () => {
|
||||
<div className="mx-auto mt-8 flex h-full w-3/4 flex-col items-center justify-center text-center">
|
||||
{downloadedModels.length === 0 && (
|
||||
<Fragment>
|
||||
<h1 className="text-lg font-medium">{`Ups, you don't have a Model`}</h1>
|
||||
<p className="mt-1">{`let’s download your first model.`}</p>
|
||||
<h1 className="text-lg font-medium">{`Oops, you don't have a Model`}</h1>
|
||||
<p className="mt-1">{`Let’s download your first model.`}</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() =>
|
||||
@ -218,7 +167,7 @@ const ChatScreen = () => {
|
||||
{!activeModel && downloadedModels.length > 0 && (
|
||||
<Fragment>
|
||||
<h1 className="text-lg font-medium">{`You don’t have any actively running models`}</h1>
|
||||
<p className="mt-1">{`Please start a downloaded model in My Models page to use this feature.`}</p>
|
||||
<p className="mt-1">{`Please start a downloaded model to use this feature.`}</p>
|
||||
<Badge className="mt-4" themes="outline">
|
||||
<ShortCut menu="E" />
|
||||
to show your model
|
||||
@ -231,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>
|
||||
|
||||
@ -25,17 +25,17 @@ export default function BlankStateMyModel() {
|
||||
<div className="text-center">
|
||||
<DatabaseIcon size={32} className="mx-auto text-muted-foreground" />
|
||||
<div className="mt-4">
|
||||
<h1 className="text-xl font-bold leading-snug">{`Ups, You don't have a model.`}</h1>
|
||||
<h1 className="text-xl font-bold leading-snug">{`Oops, you don't have a model yet.`}</h1>
|
||||
<p className="mt-1 text-base">
|
||||
{downloadStates.length > 0
|
||||
? `Downloading model ... `
|
||||
: `let’s download your first model`}
|
||||
: `Let’s download your first model`}
|
||||
</p>
|
||||
{downloadStates?.length > 0 && (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<Button themes="outline" className="mr-2 mt-6">
|
||||
<span>{downloadStates.length} Downloading model</span>
|
||||
<span>Downloading {downloadStates.length} model(s)</span>
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent>
|
||||
|
||||
@ -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">
|
||||
@ -149,7 +149,7 @@ const MyModelsScreen = () => {
|
||||
<div className="rounded-lg border border-border bg-background p-4 hover:border-primary/60">
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">Download more model?</h2>
|
||||
<h2 className="text-lg font-medium">Download more models?</h2>
|
||||
<p className="mt-2 leading-relaxed">
|
||||
You have <span>{downloadedModels.length}</span> model(s)
|
||||
downloaded.
|
||||
|
||||
@ -52,7 +52,7 @@ const PreferencePlugins = (props: Props) => {
|
||||
}
|
||||
toaster({
|
||||
title: formatPluginsName(pluginName),
|
||||
description: 'Success update preferences',
|
||||
description: 'Successfully updated preferences',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ const WelcomeScreen = () => {
|
||||
>
|
||||
Welcome to Jan
|
||||
</h1>
|
||||
<p className="mt-1">{`let’s download your first model`}</p>
|
||||
<p className="mt-1">{`Let’s download your first model`}</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => setMainViewState(MainViewState.ExploreModels)}
|
||||
@ -47,7 +47,7 @@ const WelcomeScreen = () => {
|
||||
{downloadedModels.length >= 1 && !activeModel && (
|
||||
<Fragment>
|
||||
<h1 className="mt-2 text-lg font-medium">{`You don’t have any actively running models`}</h1>
|
||||
<p className="mt-1">{`Please start a downloaded model in My Models page to use this feature.`}</p>
|
||||
<p className="mt-1">{`Please start a downloaded model to use this feature.`}</p>
|
||||
<Badge className="mt-4" themes="outline">
|
||||
<ShortCut menu="E" />
|
||||
to show your model
|
||||
@ -57,7 +57,7 @@ const WelcomeScreen = () => {
|
||||
{downloadedModels.length >= 1 && activeModel && (
|
||||
<Fragment>
|
||||
<h1 className="mt-2 text-lg font-medium">{`Your Model is Active`}</h1>
|
||||
<p className="mt-1">{`You are ready to start conversations.`}</p>
|
||||
<p className="mt-1">{`You are ready to converse.`}</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => setMainViewState(MainViewState.Chat)}
|
||||
|
||||
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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user