diff --git a/electron/core/plugins/data-plugin/module.ts b/electron/core/plugins/data-plugin/module.ts index 7d50a87081..0724da075f 100644 --- a/electron/core/plugins/data-plugin/module.ts +++ b/electron/core/plugins/data-plugin/module.ts @@ -193,13 +193,10 @@ function getModelById(modelId: string) { path.join(app.getPath("userData"), "jan.db") ); - console.debug("Get model by id", modelId); db.get( `SELECT * FROM models WHERE id = ?`, [modelId], (err: any, row: any) => { - console.debug("Get model by id result", row); - if (row) { const product = { id: row.id, diff --git a/electron/core/plugins/inference-plugin/module.ts b/electron/core/plugins/inference-plugin/module.ts index 0452fe1172..288e0c6f76 100644 --- a/electron/core/plugins/inference-plugin/module.ts +++ b/electron/core/plugins/inference-plugin/module.ts @@ -3,8 +3,10 @@ const { app, dialog } = require("electron"); const { spawn } = require("child_process"); const fs = require("fs"); const tcpPortUsed = require("tcp-port-used"); +const { killPortProcess } = require("kill-port-process"); let subprocess = null; +const PORT = 3928; const initModel = (product) => new Promise(async (res) => { @@ -29,8 +31,12 @@ const initModel = (product) => } res(); }) - .then(tcpPortUsed.waitUntilFree(3928, 200, 3000)) - .then((res) => { + .then(() => tcpPortUsed.waitUntilFree(PORT, 200, 3000)) + .catch(async () => { + await killPortProcess(PORT); + return; + }) + .then(() => { let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default // Read the existing config @@ -86,7 +92,10 @@ const initModel = (product) => subprocess = null; }); }) - .then(() => tcpPortUsed.waitUntilUsed(3928, 300, 4000)) + .then(() => tcpPortUsed.waitUntilUsed(PORT, 300, 30000)) + .then(() => { + return {}; + }) .catch((err) => { return { error: err }; }); @@ -102,6 +111,7 @@ function killSubprocess() { subprocess = null; console.log("Subprocess terminated."); } else { + killPortProcess(PORT); console.error("No subprocess is currently running."); } } diff --git a/electron/core/plugins/inference-plugin/package.json b/electron/core/plugins/inference-plugin/package.json index 65287a8a6d..3983432f64 100644 --- a/electron/core/plugins/inference-plugin/package.json +++ b/electron/core/plugins/inference-plugin/package.json @@ -25,11 +25,13 @@ "webpack-cli": "^5.1.4" }, "dependencies": { + "kill-port-process": "^3.2.0", "tcp-port-used": "^1.0.2", "ts-loader": "^9.5.0" }, "bundledDependencies": [ - "tcp-port-used" + "tcp-port-used", + "kill-port-process" ], "engines": { "node": ">=18.0.0" diff --git a/web/app/_components/HistoryItem/index.tsx b/web/app/_components/HistoryItem/index.tsx index 6aaadaac27..f8f2e2ea16 100644 --- a/web/app/_components/HistoryItem/index.tsx +++ b/web/app/_components/HistoryItem/index.tsx @@ -9,6 +9,8 @@ import { conversationStatesAtom, getActiveConvoIdAtom, setActiveConvoIdAtom, + updateConversationErrorAtom, + updateConversationWaitingForResponseAtom, } from "@/_helpers/atoms/Conversation.atom"; import { setMainViewStateAtom, @@ -33,6 +35,10 @@ const HistoryItem: React.FC = ({ const conversationStates = useAtomValue(conversationStatesAtom); const activeConvoId = useAtomValue(getActiveConvoIdAtom); const setActiveConvoId = useSetAtom(setActiveConvoIdAtom); + const updateConvWaiting = useSetAtom( + updateConversationWaitingForResponseAtom + ); + const updateConvError = useSetAtom(updateConversationErrorAtom); const isSelected = activeConvoId === conversation.id; const { initModel } = useInitModel(); @@ -42,13 +48,15 @@ const HistoryItem: React.FC = ({ DataService.GET_MODEL_BY_ID, conversation.model_id ); - if (!model) { - alert( - `Model ${conversation.model_id} not found! Please re-download the model first.` - ); - } else { - initModel(model); - } + + if (conversation.id) updateConvWaiting(conversation.id, true); + initModel(model).then((res: any) => { + if (res?.error && conversation.id) { + updateConvError(conversation.id, res.error); + } + if (conversation.id) updateConvWaiting(conversation.id, false); + }); + if (activeConvoId !== conversation.id) { setMainViewState(MainViewState.Conversation); setActiveConvoId(conversation.id); diff --git a/web/app/_components/InputToolbar/index.tsx b/web/app/_components/InputToolbar/index.tsx index 99d362b19f..9dbd02f926 100644 --- a/web/app/_components/InputToolbar/index.tsx +++ b/web/app/_components/InputToolbar/index.tsx @@ -9,14 +9,14 @@ import { Fragment } from "react"; import { PlusIcon } from "@heroicons/react/24/outline"; import useCreateConversation from "@/_hooks/useCreateConversation"; import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; -import { showingTyping } from "@/_helpers/JotaiWrapper"; import LoadingIndicator from "../LoadingIndicator"; +import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom"; const InputToolbar: React.FC = () => { const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom); const currentProduct = useAtomValue(currentProductAtom); const { requestCreateConvo } = useCreateConversation(); - const isTyping = useAtomValue(showingTyping); + const currentConvoState = useAtomValue(currentConvoStateAtom); if (showingAdvancedPrompt) { return
; @@ -34,12 +34,20 @@ const InputToolbar: React.FC = () => { return (
-
- {isTyping && ( -
+
+ {currentConvoState?.waitingForResponse === true && ( +
- )}{" "} + )} + {!currentConvoState?.waitingForResponse && + currentConvoState?.error && ( +
+ + {currentConvoState?.error?.toString()} + +
+ )}
{/* */} diff --git a/web/app/_helpers/JotaiWrapper.tsx b/web/app/_helpers/JotaiWrapper.tsx index 0c0f97fd58..9862f75956 100644 --- a/web/app/_helpers/JotaiWrapper.tsx +++ b/web/app/_helpers/JotaiWrapper.tsx @@ -13,8 +13,6 @@ export default function JotaiWrapper({ children }: Props) { export const currentPromptAtom = atom(""); -export const showingTyping = atom(false); - export const appDownloadProgress = atom(-1); export const searchingModelText = atom(""); diff --git a/web/app/_helpers/atoms/Conversation.atom.ts b/web/app/_helpers/atoms/Conversation.atom.ts index 7f1b312c9e..535fa145c9 100644 --- a/web/app/_helpers/atoms/Conversation.atom.ts +++ b/web/app/_helpers/atoms/Conversation.atom.ts @@ -59,6 +59,17 @@ export const updateConversationWaitingForResponseAtom = atom( set(conversationStatesAtom, currentState); } ); +export const updateConversationErrorAtom = atom( + null, + (get, set, conversationId: string, error?: Error) => { + const currentState = { ...get(conversationStatesAtom) }; + currentState[conversationId] = { + ...currentState[conversationId], + error, + }; + set(conversationStatesAtom, currentState); + } +); export const updateConversationHasMoreAtom = atom( null, (get, set, conversationId: string, hasMore: boolean) => { diff --git a/web/app/_hooks/useCreateConversation.ts b/web/app/_hooks/useCreateConversation.ts index 89c80144a5..53ff82c933 100644 --- a/web/app/_hooks/useCreateConversation.ts +++ b/web/app/_hooks/useCreateConversation.ts @@ -7,6 +7,8 @@ import { userConversationsAtom, setActiveConvoIdAtom, addNewConversationStateAtom, + updateConversationWaitingForResponseAtom, + updateConversationErrorAtom, } from "@/_helpers/atoms/Conversation.atom"; import useInitModel from "./useInitModel"; @@ -17,6 +19,10 @@ const useCreateConversation = () => { ); const setActiveConvoId = useSetAtom(setActiveConvoIdAtom); const addNewConvoState = useSetAtom(addNewConversationStateAtom); + const updateConvWaiting = useSetAtom( + updateConversationWaitingForResponseAtom + ); + const updateConvError = useSetAtom(updateConversationErrorAtom); const requestCreateConvo = async (model: Product) => { const conversationName = model.name; @@ -27,7 +33,14 @@ const useCreateConversation = () => { name: conversationName, }; const id = await executeSerial(DataService.CREATE_CONVERSATION, conv); - await initModel(model); + + if (id) updateConvWaiting(id, true); + initModel(model).then((res: any) => { + if (res?.error) { + updateConvError(id, res.error); + } + if (id) updateConvWaiting(id, false); + }); const mappedConvo: Conversation = { id, diff --git a/web/app/_hooks/useInitModel.ts b/web/app/_hooks/useInitModel.ts index 93cc8d968d..9a9ad3214e 100644 --- a/web/app/_hooks/useInitModel.ts +++ b/web/app/_hooks/useInitModel.ts @@ -1,27 +1,29 @@ import { Product } from "@/_models/Product"; import { executeSerial } from "@/_services/pluginService"; import { InfereceService } from "../../shared/coreService"; -import { useAtom } from "jotai"; +import { useAtom, useSetAtom } from "jotai"; import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; +import { updateConversationWaitingForResponseAtom } from "@/_helpers/atoms/Conversation.atom"; export default function useInitModel() { const [activeModel, setActiveModel] = useAtom(currentProductAtom); + const updateConvWaiting = useSetAtom( + updateConversationWaitingForResponseAtom + ); const initModel = async (model: Product) => { if (activeModel && activeModel.id === model.id) { console.debug(`Model ${model.id} is already init. Ignore..`); return; } - try { - const res = await executeSerial(InfereceService.INIT_MODEL, model); - if (res?.error) { - console.log("error occured: ", res); - } else { - console.debug(`Init model ${model.name} successfully!`); - } + const res = await executeSerial(InfereceService.INIT_MODEL, model); + if (res?.error) { + console.log("error occured: ", res); + return res; + } else { + console.log(`Init model successfully!`); setActiveModel(model); - } catch (err) { - console.error(`Init model ${model.name} failed: ${err}`); + return {}; } }; diff --git a/web/app/_hooks/useSendChatMessage.ts b/web/app/_hooks/useSendChatMessage.ts index ccc037d2f6..865c1b396a 100644 --- a/web/app/_hooks/useSendChatMessage.ts +++ b/web/app/_hooks/useSendChatMessage.ts @@ -1,4 +1,4 @@ -import { currentPromptAtom, showingTyping } from "@/_helpers/JotaiWrapper"; +import { currentPromptAtom } from "@/_helpers/JotaiWrapper"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { selectAtom } from "jotai/utils"; import { DataService, InfereceService } from "../../shared/coreService"; @@ -18,6 +18,7 @@ import { import { currentConversationAtom, getActiveConvoIdAtom, + updateConversationWaitingForResponseAtom, } from "@/_helpers/atoms/Conversation.atom"; export default function useSendChatMessage() { @@ -26,6 +27,9 @@ export default function useSendChatMessage() { const addNewMessage = useSetAtom(addNewMessageAtom); const updateMessage = useSetAtom(updateMessageAtom); const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? ""; + const updateConvWaiting = useSetAtom( + updateConversationWaitingForResponseAtom + ); const chatMessagesHistory = useAtomValue( selectAtom( @@ -34,10 +38,11 @@ export default function useSendChatMessage() { ) ); const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom); - const [, setIsTyping] = useAtom(showingTyping); + const sendChatMessage = async () => { - setIsTyping(true); setCurrentPrompt(""); + const conversationId = activeConversationId; + updateConvWaiting(conversationId, true); const prompt = currentPrompt.trim(); const newMessage: RawMessage = { conversation_id: parseInt(currentConvo?.id ?? "0") ?? 0, @@ -108,7 +113,7 @@ export default function useSendChatMessage() { const lines = text.trim().split("\n"); for (const line of lines) { if (line.startsWith("data: ") && !line.includes("data: [DONE]")) { - setIsTyping(false); + updateConvWaiting(conversationId, false); const data = JSON.parse(line.replace("data: ", "")); answer += data.choices[0]?.delta?.content ?? ""; if (answer.startsWith("assistant: ")) { @@ -139,7 +144,7 @@ export default function useSendChatMessage() { .replace("T", " ") .replace(/\.\d+Z$/, ""), }); - setIsTyping(false); + updateConvWaiting(conversationId, false); }; return { sendChatMessage, diff --git a/web/app/_models/Conversation.ts b/web/app/_models/Conversation.ts index 1d435fd891..3010bba0d0 100644 --- a/web/app/_models/Conversation.ts +++ b/web/app/_models/Conversation.ts @@ -14,4 +14,5 @@ export interface Conversation { export type ConversationState = { hasMore: boolean; waitingForResponse: boolean; + error?: Error; };