diff --git a/core/src/plugins/conversational.ts b/core/src/plugins/conversational.ts index a76c41a51..ebeb77333 100644 --- a/core/src/plugins/conversational.ts +++ b/core/src/plugins/conversational.ts @@ -1,5 +1,5 @@ +import { Thread } from "../index"; import { JanPlugin } from "../plugin"; -import { Conversation } from "../types/index"; /** * Abstract class for conversational plugins. @@ -17,10 +17,10 @@ export abstract class ConversationalPlugin extends JanPlugin { /** * Saves a conversation. * @abstract - * @param {Conversation} conversation - The conversation to save. + * @param {Thread} conversation - The conversation to save. * @returns {Promise} A promise that resolves when the conversation is saved. */ - abstract saveConversation(conversation: Conversation): Promise; + abstract saveConversation(conversation: Thread): Promise; /** * Deletes a conversation. diff --git a/core/src/plugins/inference.ts b/core/src/plugins/inference.ts index 8da7f5059..663d0b258 100644 --- a/core/src/plugins/inference.ts +++ b/core/src/plugins/inference.ts @@ -1,4 +1,4 @@ -import { NewMessageRequest } from "../events"; +import { MessageRequest } from "../index"; import { JanPlugin } from "../plugin"; /** @@ -21,5 +21,5 @@ export abstract class InferencePlugin extends JanPlugin { * @param data - The data for the inference request. * @returns The result of the inference request. */ - abstract inferenceRequest(data: NewMessageRequest): Promise; + abstract inferenceRequest(data: MessageRequest): Promise; } diff --git a/core/src/types/index.ts b/core/src/types/index.ts index 948bbd1c6..dd227081a 100644 --- a/core/src/types/index.ts +++ b/core/src/types/index.ts @@ -43,7 +43,7 @@ export type MessageRequest = { /** * The status of the message. - * @presentation_object + * @data_transfer_object */ export enum MessageStatus { /** Message is fully loaded. **/ @@ -53,7 +53,7 @@ export enum MessageStatus { } /** * The `ThreadMessage` type defines the shape of a thread's message object. - * @stored_in_workspace + * @stored */ export type ThreadMessage = { /** Unique identifier for the message, generated by default using the ULID method. **/ @@ -72,7 +72,7 @@ export type ThreadMessage = { /** * The `Thread` type defines the shape of a thread object. - * @stored_in_workspace + * @stored */ export interface Thread { /** Unique identifier for the thread, generated by default using the ULID method. **/ @@ -95,7 +95,7 @@ export interface Thread { /** * Model type defines the shape of a model object. - * @stored_in_workspace + * @stored */ export interface Model { /** Combination of owner and model name.*/ diff --git a/plugins/conversational-json/src/index.ts b/plugins/conversational-json/src/index.ts index 0e8465fd5..94082bb45 100644 --- a/plugins/conversational-json/src/index.ts +++ b/plugins/conversational-json/src/index.ts @@ -1,6 +1,6 @@ import { PluginType, fs } from '@janhq/core' import { ConversationalPlugin } from '@janhq/core/lib/plugins' -import { Conversation } from '@janhq/core/lib/types' +import { Thread } from '@janhq/core/lib/types' import { join } from 'path' /** @@ -35,7 +35,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { /** * Returns a Promise that resolves to an array of Conversation objects. */ - async getConversations(): Promise { + async getConversations(): Promise { try { const convoIds = await this.getConversationDocs() @@ -46,7 +46,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { const convos = promiseResults .map((result) => { if (result.status === 'fulfilled') { - return JSON.parse(result.value) as Conversation + return JSON.parse(result.value) as Thread } }) .filter((convo) => convo != null) @@ -66,7 +66,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { * Saves a Conversation object to a Markdown file. * @param conversation The Conversation object to save. */ - saveConversation(conversation: Conversation): Promise { + saveConversation(conversation: Thread): Promise { return fs .mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`) .then(() => diff --git a/plugins/conversational-plugin/package.json b/plugins/conversational-plugin/package.json deleted file mode 100644 index e7a29f9e7..000000000 --- a/plugins/conversational-plugin/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@janhq/conversational-plugin", - "version": "1.0.7", - "description": "Conversational Plugin - Stores jan app conversations", - "main": "dist/index.js", - "author": "Jan ", - "requiredVersion": "^0.3.1", - "license": "MIT", - "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" - }, - "exports": { - ".": "./dist/index.js", - "./main": "./dist/module.js" - }, - "devDependencies": { - "cpx": "^1.5.0", - "rimraf": "^3.0.2", - "webpack": "^5.88.2", - "webpack-cli": "^5.1.4" - }, - "dependencies": { - "@janhq/core": "file:../../core", - "ts-loader": "^9.5.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "files": [ - "dist/*", - "package.json", - "README.md" - ], - "bundleDependencies": [] -} diff --git a/plugins/conversational-plugin/src/index.ts b/plugins/conversational-plugin/src/index.ts deleted file mode 100644 index 01045b6c8..000000000 --- a/plugins/conversational-plugin/src/index.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { PluginType, fs } from "@janhq/core"; -import { ConversationalPlugin } from "@janhq/core/lib/plugins"; -import { Message, Conversation } from "@janhq/core/lib/types"; - -/** - * JanConversationalPlugin is a ConversationalPlugin implementation that provides - * functionality for managing conversations. - */ -export default class JanConversationalPlugin implements ConversationalPlugin { - /** - * Returns the type of the plugin. - */ - type(): PluginType { - return PluginType.Conversational; - } - - /** - * Called when the plugin is loaded. - */ - onLoad() { - console.debug("JanConversationalPlugin loaded"); - fs.mkdir("conversations"); - } - - /** - * Called when the plugin is unloaded. - */ - onUnload() { - console.debug("JanConversationalPlugin unloaded"); - } - - /** - * Returns a Promise that resolves to an array of Conversation objects. - */ - getConversations(): Promise { - return this.getConversationDocs().then((conversationIds) => - Promise.all( - conversationIds.map((conversationId) => - this.loadConversationFromMarkdownFile( - `conversations/${conversationId}/${conversationId}.md` - ) - ) - ).then((conversations) => - conversations.sort( - (a, b) => - new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() - ) - ) - ); - } - - /** - * Saves a Conversation object to a Markdown file. - * @param conversation The Conversation object to save. - */ - saveConversation(conversation: Conversation): Promise { - return this.writeMarkdownToFile(conversation); - } - - /** - * Deletes a conversation with the specified ID. - * @param conversationId The ID of the conversation to delete. - */ - deleteConversation(conversationId: string): Promise { - return fs.rmdir(`conversations/${conversationId}`); - } - - /** - * Returns a Promise that resolves to an array of conversation IDs. - * The conversation IDs are the names of the Markdown files in the "conversations" directory. - * @private - */ - private async getConversationDocs(): Promise { - return fs.listFiles("conversations").then((files: string[]) => { - return Promise.all(files.filter((file) => file.startsWith("jan-"))); - }); - } - - /** - * Parses a Markdown string and returns a Conversation object. - * @param markdown The Markdown string to parse. - * @private - */ - private parseConversationMarkdown(markdown: string): Conversation { - const conversation: Conversation = { - id: "", - name: "", - messages: [], - }; - var currentMessage: Message | undefined = undefined; - for (const line of markdown.split("\n")) { - const trimmedLine = line.trim(); - if (trimmedLine.startsWith("- id:")) { - conversation.id = trimmedLine.replace("- id:", "").trim(); - } else if (trimmedLine.startsWith("- modelId:")) { - conversation.modelId = trimmedLine.replace("- modelId:", "").trim(); - } else if (trimmedLine.startsWith("- name:")) { - conversation.name = trimmedLine.replace("- name:", "").trim(); - } else if (trimmedLine.startsWith("- lastMessage:")) { - conversation.message = trimmedLine.replace("- lastMessage:", "").trim(); - } else if (trimmedLine.startsWith("- summary:")) { - conversation.summary = trimmedLine.replace("- summary:", "").trim(); - } else if ( - trimmedLine.startsWith("- createdAt:") && - currentMessage === undefined - ) { - conversation.createdAt = trimmedLine.replace("- createdAt:", "").trim(); - } else if (trimmedLine.startsWith("- updatedAt:")) { - conversation.updatedAt = trimmedLine.replace("- updatedAt:", "").trim(); - } else if (trimmedLine.startsWith("- botId:")) { - conversation.botId = trimmedLine.replace("- botId:", "").trim(); - } else if (trimmedLine.startsWith("- user:")) { - if (currentMessage) - currentMessage.user = trimmedLine.replace("- user:", "").trim(); - } else if (trimmedLine.startsWith("- createdAt:")) { - if (currentMessage) - currentMessage.createdAt = trimmedLine - .replace("- createdAt:", "") - .trim(); - - currentMessage.updatedAt = currentMessage.createdAt; - } else if (trimmedLine.startsWith("- message:")) { - if (currentMessage) - currentMessage.message = trimmedLine.replace("- message:", "").trim(); - } else if (trimmedLine.startsWith("- Message ")) { - const messageMatch = trimmedLine.match(/- Message (m-\d+):/); - if (messageMatch) { - if (currentMessage) { - conversation.messages.push(currentMessage); - } - currentMessage = { id: messageMatch[1] }; - } - } else if ( - currentMessage?.message && - !trimmedLine.startsWith("## Messages") - ) { - currentMessage.message = currentMessage.message + "\n" + line.trim(); - } else if (trimmedLine.startsWith("## Messages")) { - currentMessage = undefined; - } - } - - if (currentMessage) { - conversation.messages.push(currentMessage); - } - return conversation; - } - - /** - * Loads a Conversation object from a Markdown file. - * @param filePath The path to the Markdown file. - * @private - */ - private async loadConversationFromMarkdownFile( - filePath: string - ): Promise { - try { - const markdown: string = await fs.readFile(filePath); - return this.parseConversationMarkdown(markdown); - } catch (err) { - return undefined; - } - } - - /** - * Generates a Markdown string from a Conversation object. - * @param conversation The Conversation object to generate Markdown from. - * @private - */ - private generateMarkdown(conversation: Conversation): string { - // Generate the Markdown content based on the Conversation object - const conversationMetadata = ` - - id: ${conversation.id} - - modelId: ${conversation.modelId} - - name: ${conversation.name} - - lastMessage: ${conversation.message} - - summary: ${conversation.summary} - - createdAt: ${conversation.createdAt} - - updatedAt: ${conversation.updatedAt} - - botId: ${conversation.botId} - `; - - const messages = conversation.messages.map( - (message) => ` - - Message ${message.id}: - - createdAt: ${message.createdAt} - - user: ${message.user} - - message: ${message.message?.trim()} - ` - ); - - return `## Conversation Metadata - ${conversationMetadata} -## Messages - ${messages.map((msg) => msg.trim()).join("\n")} - `; - } - - /** - * Writes a Conversation object to a Markdown file. - * @param conversation The Conversation object to write to a Markdown file. - * @private - */ - private async writeMarkdownToFile(conversation: Conversation) { - // Generate the Markdown content - const markdownContent = this.generateMarkdown(conversation); - await fs.mkdir(`conversations/${conversation.id}`); - // Write the content to a Markdown file - await fs.writeFile( - `conversations/${conversation.id}/${conversation.id}.md`, - markdownContent - ); - } -} diff --git a/plugins/conversational-plugin/tsconfig.json b/plugins/conversational-plugin/tsconfig.json deleted file mode 100644 index 2477d58ce..000000000 --- a/plugins/conversational-plugin/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "es2016", - "module": "ES6", - "moduleResolution": "node", - "outDir": "./dist", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": false, - "skipLibCheck": true, - "rootDir": "./src" - }, - "include": ["./src"] -} diff --git a/plugins/conversational-plugin/webpack.config.js b/plugins/conversational-plugin/webpack.config.js deleted file mode 100644 index d4b0db2bd..000000000 --- a/plugins/conversational-plugin/webpack.config.js +++ /dev/null @@ -1,31 +0,0 @@ -const path = require("path"); -const webpack = require("webpack"); - -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({})], - resolve: { - extensions: [".ts", ".js"], - }, - // Do not minify the output, otherwise it breaks the class registration - optimization: { - minimize: false, - }, - // Add loaders and other configuration as needed for your project -}; diff --git a/plugins/inference-plugin/src/index.ts b/plugins/inference-plugin/src/index.ts index b02c0f628..3ce1e3b4c 100644 --- a/plugins/inference-plugin/src/index.ts +++ b/plugins/inference-plugin/src/index.ts @@ -7,10 +7,13 @@ */ import { + ChatCompletionMessage, + ChatCompletionRole, EventName, - MessageHistory, - NewMessageRequest, + MessageRequest, + MessageStatus, PluginType, + ThreadMessage, events, executeOnMain, } from "@janhq/core"; @@ -70,29 +73,19 @@ export default class JanInferencePlugin implements InferencePlugin { /** * Makes a single response inference request. - * @param {NewMessageRequest} data - The data for the inference request. + * @param {MessageRequest} data - The data for the inference request. * @returns {Promise} A promise that resolves with the inference response. */ - async inferenceRequest(data: NewMessageRequest): Promise { + async inferenceRequest(data: MessageRequest): Promise { const message = { ...data, message: "", user: "assistant", createdAt: new Date().toISOString(), }; - const prompts: [MessageHistory] = [ - { - role: "user", - content: data.message, - }, - ]; - const recentMessages = data.history ?? prompts; return new Promise(async (resolve, reject) => { - requestInference([ - ...recentMessages, - { role: "user", content: data.message }, - ]).subscribe({ + requestInference(data.messages ?? []).subscribe({ next: (content) => { message.message = content; }, @@ -108,37 +101,33 @@ export default class JanInferencePlugin implements InferencePlugin { /** * Handles a new message request by making an inference request and emitting events. - * @param {NewMessageRequest} data - The data for the new message request. + * @param {MessageRequest} data - The data for the new message request. */ - private async handleMessageRequest(data: NewMessageRequest) { - const prompts: [MessageHistory] = [ - { - role: "user", - content: data.message, - }, - ]; - const recentMessages = data.history ?? prompts; - const message = { + private async handleMessageRequest(data: MessageRequest) { + const message: ThreadMessage = { ...data, - message: "", - user: "assistant", + content: "", + role: ChatCompletionRole.Assistant, createdAt: new Date().toISOString(), id: ulid(), + status: MessageStatus.Pending, }; events.emit(EventName.OnNewMessageResponse, message); - requestInference(recentMessages).subscribe({ + requestInference(data.messages).subscribe({ next: (content) => { - message.message = content; + message.content = content; events.emit(EventName.OnMessageResponseUpdate, message); }, complete: async () => { - message.message = message.message.trim(); + message.content = message.content.trim(); + message.status = MessageStatus.Ready; events.emit(EventName.OnMessageResponseFinished, message); }, error: async (err) => { - message.message = - message.message.trim() + "\n" + "Error occurred: " + err.message; + message.content = + message.content.trim() + "\n" + "Error occurred: " + err.message; + message.status = MessageStatus.Ready; events.emit(EventName.OnMessageResponseUpdate, message); }, }); diff --git a/plugins/model-plugin/src/index.ts b/plugins/model-plugin/src/index.ts index 2e599c2d4..5fb487017 100644 --- a/plugins/model-plugin/src/index.ts +++ b/plugins/model-plugin/src/index.ts @@ -42,7 +42,7 @@ export default class JanModelPlugin implements ModelPlugin { */ async downloadModel(model: Model): Promise { // create corresponding directory - const directoryPath = join(JanModelPlugin._homeDir, model.productName) + const directoryPath = join(JanModelPlugin._homeDir, model.name) await fs.mkdir(directoryPath) // path to model binary @@ -72,7 +72,7 @@ export default class JanModelPlugin implements ModelPlugin { * @returns A Promise that resolves when the model is saved. */ async saveModel(model: Model): Promise { - const directoryPath = join(JanModelPlugin._homeDir, model.productName) + const directoryPath = join(JanModelPlugin._homeDir, model.name) const jsonFilePath = join(directoryPath, `${model.id}.json`) try { diff --git a/web/containers/ModalCancelDownload/index.tsx b/web/containers/ModalCancelDownload/index.tsx index e62dda0ca..3413d02c4 100644 --- a/web/containers/ModalCancelDownload/index.tsx +++ b/web/containers/ModalCancelDownload/index.tsx @@ -32,9 +32,9 @@ export default function ModalCancelDownload({ const { modelDownloadStateAtom } = useDownloadState() useGetPerformanceTag() const downloadAtom = useMemo( - () => atom((get) => get(modelDownloadStateAtom)[suitableModel.id]), + () => atom((get) => get(modelDownloadStateAtom)[suitableModel.name]), // eslint-disable-next-line react-hooks/exhaustive-deps - [suitableModel.id] + [suitableModel.name] ) const downloadState = useAtomValue(downloadAtom) diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx index ff2934282..575b0f97f 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -4,38 +4,31 @@ import { ReactNode, useEffect, useRef } from 'react' import { events, EventName, - NewMessageResponse, + ThreadMessage, PluginType, - ChatMessage, + MessageStatus, } from '@janhq/core' -import { Conversation, Message, MessageStatus } from '@janhq/core' import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins' import { useAtomValue, useSetAtom } from 'jotai' import { useDownloadState } from '@/hooks/useDownloadState' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' -import { toChatMessage } from '@/utils/message' - import { addNewMessageAtom, chatMessages, updateMessageAtom, } from '@/helpers/atoms/ChatMessage.atom' import { - updateConversationAtom, updateConversationWaitingForResponseAtom, userConversationsAtom, } from '@/helpers/atoms/Conversation.atom' import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' import { pluginManager } from '@/plugin' -let currentConversation: Conversation | undefined = undefined - export default function EventHandler({ children }: { children: ReactNode }) { const addNewMessage = useSetAtom(addNewMessageAtom) const updateMessage = useSetAtom(updateMessageAtom) - const updateConversation = useSetAtom(updateConversationAtom) const { setDownloadState, setDownloadStateSuccess } = useDownloadState() const { downloadedModels, setDownloadedModels } = useGetDownloadedModels() @@ -52,92 +45,55 @@ export default function EventHandler({ children }: { children: ReactNode }) { convoRef.current = conversations }, [messages, conversations]) - async function handleNewMessageResponse(message: NewMessageResponse) { - if (message.conversationId) { - const convo = convoRef.current.find((e) => e.id == message.conversationId) + async function handleNewMessageResponse(message: ThreadMessage) { + if (message.threadId) { + const convo = convoRef.current.find((e) => e.id == message.threadId) if (!convo) return - const newResponse = toChatMessage(message) - addNewMessage(newResponse) + addNewMessage(message) } } - async function handleMessageResponseUpdate( - messageResponse: NewMessageResponse - ) { + async function handleMessageResponseUpdate(messageResponse: ThreadMessage) { if ( - messageResponse.conversationId && + messageResponse.threadId && messageResponse.id && - messageResponse.message + messageResponse.content ) { updateMessage( messageResponse.id, - messageResponse.conversationId, - messageResponse.message, + messageResponse.threadId, + messageResponse.content, MessageStatus.Pending ) } - - if (messageResponse.conversationId) { - if ( - !currentConversation || - currentConversation.id !== messageResponse.conversationId - ) { - if (convoRef.current && messageResponse.conversationId) - currentConversation = convoRef.current.find( - (e) => e.id == messageResponse.conversationId - ) - } - - if (currentConversation) { - const updatedConv: Conversation = { - ...currentConversation, - lastMessage: messageResponse.message, - } - - updateConversation(updatedConv) - } - } } - async function handleMessageResponseFinished( - messageResponse: NewMessageResponse - ) { - if (!messageResponse.conversationId || !convoRef.current) return - updateConvWaiting(messageResponse.conversationId, false) + async function handleMessageResponseFinished(messageResponse: ThreadMessage) { + if (!messageResponse.threadId || !convoRef.current) return + updateConvWaiting(messageResponse.threadId, false) if ( - messageResponse.conversationId && + messageResponse.threadId && messageResponse.id && - messageResponse.message + messageResponse.content ) { updateMessage( messageResponse.id, - messageResponse.conversationId, - messageResponse.message, + messageResponse.threadId, + messageResponse.content, MessageStatus.Ready ) } - const convo = convoRef.current.find( - (e) => e.id == messageResponse.conversationId + const thread = convoRef.current.find( + (e) => e.id == messageResponse.threadId ) - if (convo) { - const messagesData = (messagesRef.current ?? [])[convo.id].map( - (e: ChatMessage) => ({ - id: e.id, - message: e.text, - user: e.senderUid, - updatedAt: new Date(e.createdAt).toISOString(), - createdAt: new Date(e.createdAt).toISOString(), - }) - ) + if (thread) { pluginManager .get(PluginType.Conversational) ?.saveConversation({ - ...convo, - id: convo.id ?? '', - name: convo.name ?? '', - message: convo.lastMessage ?? '', - messages: messagesData, + ...thread, + id: thread.id ?? '', + messages: messagesRef.current[thread.id] ?? [], }) } } diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts index d343d0f7c..5b0a84bbe 100644 --- a/web/helpers/atoms/ChatMessage.atom.ts +++ b/web/helpers/atoms/ChatMessage.atom.ts @@ -1,4 +1,4 @@ -import { ChatMessage, MessageStatus } from '@janhq/core' +import { MessageStatus, ThreadMessage } from '@janhq/core' import { atom } from 'jotai' import { getActiveConvoIdAtom } from './Conversation.atom' @@ -6,12 +6,12 @@ import { getActiveConvoIdAtom } from './Conversation.atom' /** * Stores all chat messages for all conversations */ -export const chatMessages = atom>({}) +export const chatMessages = atom>({}) /** * Return the chat messages for the current active conversation */ -export const getCurrentChatMessagesAtom = atom((get) => { +export const getCurrentChatMessagesAtom = atom((get) => { const activeConversationId = get(getActiveConvoIdAtom) if (!activeConversationId) return [] const messages = get(chatMessages)[activeConversationId] @@ -20,11 +20,11 @@ export const getCurrentChatMessagesAtom = atom((get) => { export const setCurrentChatMessagesAtom = atom( null, - (get, set, messages: ChatMessage[]) => { + (get, set, messages: ThreadMessage[]) => { const currentConvoId = get(getActiveConvoIdAtom) if (!currentConvoId) return - const newData: Record = { + const newData: Record = { ...get(chatMessages), } newData[currentConvoId] = messages @@ -34,8 +34,8 @@ export const setCurrentChatMessagesAtom = atom( export const setConvoMessagesAtom = atom( null, - (get, set, messages: ChatMessage[], convoId: string) => { - const newData: Record = { + (get, set, messages: ThreadMessage[], convoId: string) => { + const newData: Record = { ...get(chatMessages), } newData[convoId] = messages @@ -48,14 +48,14 @@ export const setConvoMessagesAtom = atom( */ export const addOldMessagesAtom = atom( null, - (get, set, newMessages: ChatMessage[]) => { + (get, set, newMessages: ThreadMessage[]) => { const currentConvoId = get(getActiveConvoIdAtom) if (!currentConvoId) return const currentMessages = get(chatMessages)[currentConvoId] ?? [] const updatedMessages = [...currentMessages, ...newMessages] - const newData: Record = { + const newData: Record = { ...get(chatMessages), } newData[currentConvoId] = updatedMessages @@ -65,14 +65,14 @@ export const addOldMessagesAtom = atom( export const addNewMessageAtom = atom( null, - (get, set, newMessage: ChatMessage) => { + (get, set, newMessage: ThreadMessage) => { const currentConvoId = get(getActiveConvoIdAtom) if (!currentConvoId) return const currentMessages = get(chatMessages)[currentConvoId] ?? [] const updatedMessages = [newMessage, ...currentMessages] - const newData: Record = { + const newData: Record = { ...get(chatMessages), } newData[currentConvoId] = updatedMessages @@ -81,7 +81,7 @@ export const addNewMessageAtom = atom( ) export const deleteConversationMessage = atom(null, (get, set, id: string) => { - const newData: Record = { + const newData: Record = { ...get(chatMessages), } newData[id] = [] @@ -101,11 +101,11 @@ export const updateMessageAtom = atom( const messages = get(chatMessages)[conversationId] ?? [] const message = messages.find((e) => e.id === id) if (message) { - message.text = text + message.content = text message.status = status const updatedMessages = [...messages] - const newData: Record = { + const newData: Record = { ...get(chatMessages), } newData[conversationId] = updatedMessages @@ -130,14 +130,14 @@ export const updateLastMessageAsReadyAtom = atom( if (!messageToUpdate) return const index = currentMessages.indexOf(messageToUpdate) - const updatedMsg: ChatMessage = { + const updatedMsg: ThreadMessage = { ...messageToUpdate, status: MessageStatus.Ready, - text: text, + content: text, } currentMessages[index] = updatedMsg - const newData: Record = { + const newData: Record = { ...get(chatMessages), } newData[currentConvoId] = currentMessages diff --git a/web/helpers/atoms/Conversation.atom.ts b/web/helpers/atoms/Conversation.atom.ts index 2265c5c2e..d6ca4046a 100644 --- a/web/helpers/atoms/Conversation.atom.ts +++ b/web/helpers/atoms/Conversation.atom.ts @@ -1,6 +1,8 @@ -import { Conversation, ConversationState } from '@janhq/core' +import { Thread } from '@janhq/core' import { atom } from 'jotai' +import { ThreadState } from '@/types/conversation' + /** * Stores the current active conversation id. */ @@ -19,23 +21,19 @@ export const waitingToSendMessage = atom(undefined) /** * Stores all conversation states for the current user */ -export const conversationStatesAtom = atom>( - {} -) -export const currentConvoStateAtom = atom( - (get) => { - const activeConvoId = get(activeConversationIdAtom) - if (!activeConvoId) { - console.debug('Active convo id is undefined') - return undefined - } - - return get(conversationStatesAtom)[activeConvoId] +export const conversationStatesAtom = atom>({}) +export const currentConvoStateAtom = atom((get) => { + const activeConvoId = get(activeConversationIdAtom) + if (!activeConvoId) { + console.debug('Active convo id is undefined') + return undefined } -) + + return get(conversationStatesAtom)[activeConvoId] +}) export const addNewConversationStateAtom = atom( null, - (get, set, conversationId: string, state: ConversationState) => { + (get, set, conversationId: string, state: ThreadState) => { const currentState = { ...get(conversationStatesAtom) } currentState[conversationId] = state set(conversationStatesAtom, currentState) @@ -75,14 +73,14 @@ export const updateConversationHasMoreAtom = atom( export const updateConversationAtom = atom( null, - (get, set, conversation: Conversation) => { + (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: Conversation[] = get(userConversationsAtom).map( - (c) => (c.id === id ? conversation : c) + const newConversations: Thread[] = get(userConversationsAtom).map((c) => + c.id === id ? conversation : c ) // sort new conversations based on updated at @@ -99,7 +97,7 @@ export const updateConversationAtom = atom( /** * Stores all conversations for the current user */ -export const userConversationsAtom = atom([]) -export const currentConversationAtom = atom((get) => +export const userConversationsAtom = atom([]) +export const currentConversationAtom = atom((get) => get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom)) ) diff --git a/web/hooks/useActiveModel.ts b/web/hooks/useActiveModel.ts index efe05672e..3496f8396 100644 --- a/web/hooks/useActiveModel.ts +++ b/web/hooks/useActiveModel.ts @@ -1,4 +1,6 @@ /* 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' @@ -10,7 +12,6 @@ import { toaster } from '@/containers/Toast' import { useGetDownloadedModels } from './useGetDownloadedModels' import { pluginManager } from '@/plugin' -import { join } from 'path' const activeAssistantModelAtom = atom(undefined) @@ -43,7 +44,7 @@ export function useActiveModel() { const currentTime = Date.now() console.debug('Init model: ', modelId) - const path = join('models', model.productName, modelId) + const path = join('models', model.name, modelId) const res = await initModel(path) if (res?.error) { const errorMessage = `${res.error}` diff --git a/web/hooks/useCreateConversation.ts b/web/hooks/useCreateConversation.ts index e5f2a669f..8984c0bfc 100644 --- a/web/hooks/useCreateConversation.ts +++ b/web/hooks/useCreateConversation.ts @@ -1,5 +1,5 @@ import { PluginType } from '@janhq/core' -import { Conversation, Model } from '@janhq/core' +import { Thread, Model } from '@janhq/core' import { ConversationalPlugin } from '@janhq/core/lib/plugins' import { useAtom, useSetAtom } from 'jotai' @@ -20,11 +20,11 @@ export const useCreateConversation = () => { const addNewConvoState = useSetAtom(addNewConversationStateAtom) const requestCreateConvo = async (model: Model) => { - const conversationName = model.name - const mappedConvo: Conversation = { + const summary = model.name + const mappedConvo: Thread = { id: generateConversationId(), modelId: model.id, - name: conversationName, + summary, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), messages: [], @@ -37,11 +37,7 @@ export const useCreateConversation = () => { pluginManager .get(PluginType.Conversational) - ?.saveConversation({ - ...mappedConvo, - name: mappedConvo.name ?? '', - messages: [], - }) + ?.saveConversation(mappedConvo) setUserConversations([mappedConvo, ...userConversations]) setActiveConvoId(mappedConvo.id) } diff --git a/web/hooks/useDeleteModel.ts b/web/hooks/useDeleteModel.ts index 4d527e8cb..f59860719 100644 --- a/web/hooks/useDeleteModel.ts +++ b/web/hooks/useDeleteModel.ts @@ -1,3 +1,5 @@ +import { join } from 'path' + import { PluginType } from '@janhq/core' import { ModelPlugin } from '@janhq/core/lib/plugins' import { Model } from '@janhq/core/lib/types' @@ -7,13 +9,12 @@ import { toaster } from '@/containers/Toast' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { pluginManager } from '@/plugin/PluginManager' -import { join } from 'path' export default function useDeleteModel() { const { setDownloadedModels, downloadedModels } = useGetDownloadedModels() const deleteModel = async (model: Model) => { - const path = join('models', model.productName, model.id) + const path = join('models', model.name, model.id) await pluginManager.get(PluginType.Model)?.deleteModel(path) // reload models diff --git a/web/hooks/useDownloadModel.ts b/web/hooks/useDownloadModel.ts index 3ec1bf330..bbe48f397 100644 --- a/web/hooks/useDownloadModel.ts +++ b/web/hooks/useDownloadModel.ts @@ -7,6 +7,7 @@ import { useAtom } from 'jotai' import { useDownloadState } from './useDownloadState' import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' + import { pluginManager } from '@/plugin/PluginManager' export default function useDownloadModel() { @@ -20,28 +21,24 @@ export default function useDownloadModel() { modelVersion: ModelVersion ): Model => { return { - // eslint-disable-next-line @typescript-eslint/naming-convention - id: modelVersion.id, - name: modelVersion.name, - quantMethod: modelVersion.quantMethod, + /** + * 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, - startDownloadAt: modelVersion.startDownloadAt, - finishDownloadAt: modelVersion.finishDownloadAt, - productId: model.id, - productName: model.name, shortDescription: model.shortDescription, longDescription: model.longDescription, avatarUrl: model.avatarUrl, author: model.author, version: model.version, modelUrl: model.modelUrl, - createdAt: new Date(model.createdAt).getTime(), - updatedAt: new Date(model.updatedAt ?? '').getTime(), - status: '', releaseDate: -1, tags: model.tags, } @@ -53,7 +50,7 @@ export default function useDownloadModel() { ) => { // set an initial download state setDownloadState({ - modelId: modelVersion.id, + modelId: modelVersion.name, time: { elapsed: 0, remaining: 0, @@ -64,10 +61,9 @@ export default function useDownloadModel() { total: 0, transferred: 0, }, - fileName: modelVersion.id, + fileName: modelVersion.name, }) - modelVersion.startDownloadAt = Date.now() const assistantModel = assistanModel(model, modelVersion) setDownloadingModels([...downloadingModels, assistantModel]) diff --git a/web/hooks/useGetInputState.ts b/web/hooks/useGetInputState.ts index d1b52f080..f7934ae2c 100644 --- a/web/hooks/useGetInputState.ts +++ b/web/hooks/useGetInputState.ts @@ -1,8 +1,11 @@ import { useEffect, useState } from 'react' -import { Model, Conversation } from '@janhq/core' + +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() { @@ -12,7 +15,7 @@ export default function useGetInputState() { const { downloadedModels } = useGetDownloadedModels() const handleInputState = ( - convo: Conversation | undefined, + convo: Thread | undefined, currentModel: Model | undefined ) => { if (convo == null) return diff --git a/web/hooks/useGetUserConversations.ts b/web/hooks/useGetUserConversations.ts index 5f0ad7435..be210944f 100644 --- a/web/hooks/useGetUserConversations.ts +++ b/web/hooks/useGetUserConversations.ts @@ -1,16 +1,14 @@ -import { PluginType, ChatMessage, ConversationState } from '@janhq/core' +import { PluginType, Thread } from '@janhq/core' import { ConversationalPlugin } from '@janhq/core/lib/plugins' -import { Conversation } from '@janhq/core/lib/types' import { useSetAtom } from 'jotai' -import { toChatMessage } from '@/utils/message' - 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) @@ -19,19 +17,16 @@ const useGetUserConversations = () => { const getUserConversations = async () => { try { - const convos: Conversation[] | undefined = await pluginManager + const convos: Thread[] | undefined = await pluginManager .get(PluginType.Conversational) ?.getConversations() - const convoStates: Record = {} + const convoStates: Record = {} convos?.forEach((convo) => { convoStates[convo.id ?? ''] = { hasMore: true, waitingForResponse: false, } - setConvoMessages( - convo.messages.map((msg) => toChatMessage(msg)), - convo.id ?? '' - ) + setConvoMessages(convo.messages, convo.id ?? '') }) setConversationStates(convoStates) setConversations(convos ?? []) diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 5d5e1598c..8ff334b27 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -1,18 +1,20 @@ import { + ChatCompletionMessage, + ChatCompletionRole, EventName, - MessageHistory, - NewMessageRequest, + MessageRequest, + MessageStatus, PluginType, + Thread, events, - ChatMessage, - Message, - Conversation, - MessageSenderType, } from '@janhq/core' import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins' import { useAtom, useAtomValue, useSetAtom } from 'jotai' -import { currentPromptAtom } from '@/containers/Providers/Jotai' + import { ulid } from 'ulid' + +import { currentPromptAtom } from '@/containers/Providers/Jotai' + import { addNewMessageAtom, getCurrentChatMessagesAtom, @@ -23,7 +25,6 @@ import { updateConversationWaitingForResponseAtom, } from '@/helpers/atoms/Conversation.atom' import { pluginManager } from '@/plugin/PluginManager' -import { toChatMessage } from '@/utils/message' export default function useSendChatMessage() { const currentConvo = useAtomValue(currentConversationAtom) @@ -35,7 +36,7 @@ export default function useSendChatMessage() { let timeout: NodeJS.Timeout | undefined = undefined - function updateConvSummary(newMessage: NewMessageRequest) { + function updateConvSummary(newMessage: MessageRequest) { if (timeout) { clearTimeout(timeout) } @@ -46,13 +47,19 @@ export default function useSendChatMessage() { currentConvo.summary === '' || currentConvo.summary.startsWith('Prompt:') ) { + const summaryMsg: ChatCompletionMessage = { + role: ChatCompletionRole.User, + content: + 'summary this conversation in 5 words, the response should just include the summary', + } // Request convo summary setTimeout(async () => { - newMessage.message = - 'summary this conversation in 5 words, the response should just include the summary' const result = await pluginManager .get(PluginType.Inference) - ?.inferenceRequest(newMessage) + ?.inferenceRequest({ + ...newMessage, + messages: newMessage.messages?.concat([summaryMsg]), + }) if ( result?.message && @@ -68,15 +75,7 @@ export default function useSendChatMessage() { .get(PluginType.Conversational) ?.saveConversation({ ...updatedConv, - name: updatedConv.name ?? '', - message: updatedConv.lastMessage ?? '', - messages: currentMessages.map((e: ChatMessage) => ({ - id: e.id, - message: e.text, - user: e.senderUid, - updatedAt: new Date(e.createdAt).toISOString(), - createdAt: new Date(e.createdAt).toISOString(), - })), + messages: currentMessages, }) } }, 1000) @@ -95,29 +94,32 @@ export default function useSendChatMessage() { updateConvWaiting(convoId, true) const prompt = currentPrompt.trim() - const messageHistory: MessageHistory[] = currentMessages - .map((msg) => ({ - role: msg.senderUid, - content: msg.text ?? '', + const messages: ChatCompletionMessage[] = currentMessages + .map((msg) => ({ + role: msg.role ?? ChatCompletionRole.User, + content: msg.content ?? '', })) .reverse() .concat([ { - role: MessageSenderType.User, + role: ChatCompletionRole.User, content: prompt, - } as MessageHistory, + } as ChatCompletionMessage, ]) - const newMessage: NewMessageRequest = { + const newMessage: MessageRequest = { id: ulid(), - conversationId: convoId, - message: prompt, - user: MessageSenderType.User, - createdAt: new Date().toISOString(), - history: messageHistory, + threadId: convoId, + messages, } - const newChatMessage = toChatMessage(newMessage) - addNewMessage(newChatMessage) + addNewMessage({ + id: newMessage.id, + threadId: newMessage.threadId, + content: prompt, + role: ChatCompletionRole.User, + createdAt: new Date().toISOString(), + status: MessageStatus.Ready, + }) // delay randomly from 50 - 100ms // to prevent duplicate message id @@ -126,19 +128,11 @@ export default function useSendChatMessage() { events.emit(EventName.OnNewMessageRequest, newMessage) if (!currentConvo?.summary && currentConvo) { - const updatedConv: Conversation = { + const updatedConv: Thread = { ...currentConvo, - lastMessage: prompt, summary: `Prompt: ${prompt}`, } - updateConversation(updatedConv) - } else if (currentConvo) { - const updatedConv: Conversation = { - ...currentConvo, - lastMessage: prompt, - } - updateConversation(updatedConv) } diff --git a/web/screens/Chat/ChatBody/index.tsx b/web/screens/Chat/ChatBody/index.tsx index 755e3d6b7..b5c635262 100644 --- a/web/screens/Chat/ChatBody/index.tsx +++ b/web/screens/Chat/ChatBody/index.tsx @@ -10,7 +10,7 @@ const ChatBody: React.FC = () => { return (
{messages.map((message) => ( - + ))}
) diff --git a/web/screens/Chat/ChatItem/index.tsx b/web/screens/Chat/ChatItem/index.tsx index 86163bbbe..a085c3dc8 100644 --- a/web/screens/Chat/ChatItem/index.tsx +++ b/web/screens/Chat/ChatItem/index.tsx @@ -1,24 +1,14 @@ import React, { forwardRef } from 'react' -import { ChatMessage } from '@janhq/core' -import SimpleTextMessage from '../SimpleTextMessage' -type Props = { - message: ChatMessage -} +import { ThreadMessage } from '@janhq/core' + +import SimpleTextMessage from '../SimpleTextMessage' type Ref = HTMLDivElement -const ChatItem = forwardRef(({ message }, ref) => ( +const ChatItem = forwardRef((message, ref) => (
- +
)) diff --git a/web/screens/Chat/HistoryList/index.tsx b/web/screens/Chat/HistoryList/index.tsx index 34587e2ae..6b5e2390c 100644 --- a/web/screens/Chat/HistoryList/index.tsx +++ b/web/screens/Chat/HistoryList/index.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { Conversation, Model } from '@janhq/core/lib/types' +import { Thread, Model } from '@janhq/core' import { Button } from '@janhq/uikit' import { motion as m } from 'framer-motion' import { useAtomValue, useSetAtom } from 'jotai' @@ -41,7 +41,7 @@ export default function HistoryList() { return } - const handleActiveModel = async (convo: Conversation) => { + const handleActiveModel = async (convo: Thread) => { if (convo.modelId == null) { console.debug('modelId is undefined') return @@ -90,15 +90,16 @@ export default function HistoryList() { '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 Conversation)} + onClick={() => handleActiveModel(convo as Thread)} >

{convo.updatedAt && displayDate(new Date(convo.updatedAt).getTime())}

-

{convo.summary ?? convo.name}

+

{convo.summary}

- {convo?.lastMessage ?? 'No new message'} + {/* TODO: Check latest message update */} + {convo?.messages[0]?.content ?? 'No new message'}

{activeModel && activeConvoId === convo.id && ( = ({ - senderName, - senderType, - createdAt, - // will use status as streaming text - // status, - text = '', -}) => { - const parsedText = marked.parse(text) - const isUser = senderType === 'user' +const SimpleTextMessage: React.FC = (props) => { + const parsedText = marked.parse(props.content ?? '') + const isUser = props.role === ChatCompletionRole.User return (
@@ -70,12 +54,12 @@ const SimpleTextMessage: React.FC = ({ )} > {!isUser && } -
{senderName}
-

{displayDate(createdAt)}

+
{props.role}
+

{displayDate(props.createdAt)}

- {text === '' ? ( + {!props.content || props.content === '' ? ( ) : ( <> diff --git a/web/screens/Chat/index.tsx b/web/screens/Chat/index.tsx index 35936575d..5d7d22f37 100644 --- a/web/screens/Chat/index.tsx +++ b/web/screens/Chat/index.tsx @@ -126,7 +126,7 @@ const ChatScreen = () => { {isEnableChat && currentConvo && (
- {currentConvo?.name ?? ''} + {currentConvo?.summary ?? ''}
(({ model }, ref) => { return null } - const { quantMethod, bits, maxRamRequired, usecase } = suitableModel + const { quantizationName, bits, maxRamRequired, usecase } = suitableModel return (
(({ model }, ref) => { Version
v{model.version} - {quantMethod && {quantMethod}} + {quantizationName && ( + {quantizationName} + )} {`${bits} Bits`}
@@ -105,7 +107,7 @@ const ExploreModelItem = forwardRef(({ model }, ref) => { )}
diff --git a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx index bf55aec61..fe3c9c3e9 100644 --- a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx +++ b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx @@ -35,8 +35,8 @@ const ExploreModelItemHeader: React.FC = ({ const { performanceTag, title, getPerformanceForModel } = useGetPerformanceTag() const downloadAtom = useMemo( - () => atom((get) => get(modelDownloadStateAtom)[suitableModel.id]), - [suitableModel.id] + () => atom((get) => get(modelDownloadStateAtom)[suitableModel.name]), + [suitableModel.name] ) const downloadState = useAtomValue(downloadAtom) const { setMainViewState } = useMainViewState() @@ -51,8 +51,9 @@ const ExploreModelItemHeader: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [exploreModel, suitableModel]) + // TODO: Comparing between Model Id and Version Name? const isDownloaded = - downloadedModels.find((model) => model.id === suitableModel.id) != null + downloadedModels.find((model) => model.id === suitableModel.name) != null let downloadButton = (