fix: reduce the number of api call (#1896)

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
This commit is contained in:
NamH 2024-02-05 13:13:39 +07:00 committed by GitHub
parent ccbe18e5b8
commit 01fec49798
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 469 additions and 421 deletions

View File

@ -12,6 +12,8 @@ import {
import { JanApiRouteConfiguration } from '../common/configuration'
import { startModel, stopModel } from '../common/startStopModel'
import { ModelSettingParams } from '../../../types'
import { getJanDataFolderPath } from '../../utils'
import { normalizeFilePath } from '../../path'
export const commonRouter = async (app: HttpServer) => {
// Common Routes
@ -52,7 +54,14 @@ export const commonRouter = async (app: HttpServer) => {
// App Routes
app.post(`/app/${AppRoute.joinPath}`, async (request: any, reply: any) => {
const args = JSON.parse(request.body) as any[]
reply.send(JSON.stringify(join(...args[0])))
const paths = args[0].map((arg: string) =>
typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
? join(getJanDataFolderPath(), normalizeFilePath(arg))
: arg
)
reply.send(JSON.stringify(join(...paths)))
})
app.post(`/app/${AppRoute.baseName}`, async (request: any, reply: any) => {

View File

@ -4,55 +4,55 @@ import { DownloadManager } from '../../download'
import { HttpServer } from '../HttpServer'
import { createWriteStream } from 'fs'
import { getJanDataFolderPath } from '../../utils'
import { normalizeFilePath } from "../../path";
import { normalizeFilePath } from '../../path'
export const downloadRouter = async (app: HttpServer) => {
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
const strictSSL = !(req.query.ignoreSSL === "true");
const proxy = req.query.proxy?.startsWith("http") ? req.query.proxy : undefined;
const body = JSON.parse(req.body as any);
const strictSSL = !(req.query.ignoreSSL === 'true')
const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined
const body = JSON.parse(req.body as any)
const normalizedArgs = body.map((arg: any) => {
if (typeof arg === "string") {
return join(getJanDataFolderPath(), normalizeFilePath(arg));
if (typeof arg === 'string' && arg.startsWith('file:')) {
return join(getJanDataFolderPath(), normalizeFilePath(arg))
}
return arg;
});
return arg
})
const localPath = normalizedArgs[1];
const fileName = localPath.split("/").pop() ?? "";
const localPath = normalizedArgs[1]
const fileName = localPath.split('/').pop() ?? ''
const request = require("request");
const progress = require("request-progress");
const request = require('request')
const progress = require('request-progress')
const rq = request({ url: normalizedArgs[0], strictSSL, proxy });
const rq = request({ url: normalizedArgs[0], strictSSL, proxy })
progress(rq, {})
.on("progress", function (state: any) {
console.log("download onProgress", state);
.on('progress', function (state: any) {
console.log('download onProgress', state)
})
.on("error", function (err: Error) {
console.log("download onError", err);
.on('error', function (err: Error) {
console.log('download onError', err)
})
.on("end", function () {
console.log("download onEnd");
.on('end', function () {
console.log('download onEnd')
})
.pipe(createWriteStream(normalizedArgs[1]));
.pipe(createWriteStream(normalizedArgs[1]))
DownloadManager.instance.setRequest(fileName, rq);
});
DownloadManager.instance.setRequest(fileName, rq)
})
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
const body = JSON.parse(req.body as any);
const body = JSON.parse(req.body as any)
const normalizedArgs = body.map((arg: any) => {
if (typeof arg === "string") {
return join(getJanDataFolderPath(), normalizeFilePath(arg));
if (typeof arg === 'string' && arg.startsWith('file:')) {
return join(getJanDataFolderPath(), normalizeFilePath(arg))
}
return arg;
});
return arg
})
const localPath = normalizedArgs[0];
const fileName = localPath.split("/").pop() ?? "";
const rq = DownloadManager.instance.networkRequests[fileName];
DownloadManager.instance.networkRequests[fileName] = undefined;
rq?.abort();
});
};
const localPath = normalizedArgs[0]
const fileName = localPath.split('/').pop() ?? ''
const rq = DownloadManager.instance.networkRequests[fileName]
DownloadManager.instance.networkRequests[fileName] = undefined
rq?.abort()
})
}

View File

@ -12,7 +12,7 @@ import {
* functionality for managing threads.
*/
export default class JSONConversationalExtension extends ConversationalExtension {
private static readonly _homeDir = 'file://threads'
private static readonly _threadFolder = 'file://threads'
private static readonly _threadInfoFileName = 'thread.json'
private static readonly _threadMessagesFileName = 'messages.jsonl'
@ -20,8 +20,8 @@ export default class JSONConversationalExtension extends ConversationalExtension
* Called when the extension is loaded.
*/
async onLoad() {
if (!(await fs.existsSync(JSONConversationalExtension._homeDir)))
await fs.mkdirSync(JSONConversationalExtension._homeDir)
if (!(await fs.existsSync(JSONConversationalExtension._threadFolder)))
await fs.mkdirSync(JSONConversationalExtension._threadFolder)
console.debug('JSONConversationalExtension loaded')
}
@ -68,7 +68,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
async saveThread(thread: Thread): Promise<void> {
try {
const threadDirPath = await joinPath([
JSONConversationalExtension._homeDir,
JSONConversationalExtension._threadFolder,
thread.id,
])
const threadJsonPath = await joinPath([
@ -92,7 +92,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
*/
async deleteThread(threadId: string): Promise<void> {
const path = await joinPath([
JSONConversationalExtension._homeDir,
JSONConversationalExtension._threadFolder,
`${threadId}`,
])
try {
@ -109,7 +109,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
async addNewMessage(message: ThreadMessage): Promise<void> {
try {
const threadDirPath = await joinPath([
JSONConversationalExtension._homeDir,
JSONConversationalExtension._threadFolder,
message.thread_id,
])
const threadMessagePath = await joinPath([
@ -177,7 +177,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
): Promise<void> {
try {
const threadDirPath = await joinPath([
JSONConversationalExtension._homeDir,
JSONConversationalExtension._threadFolder,
threadId,
])
const threadMessagePath = await joinPath([
@ -205,7 +205,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
private async readThread(threadDirName: string): Promise<any> {
return fs.readFileSync(
await joinPath([
JSONConversationalExtension._homeDir,
JSONConversationalExtension._threadFolder,
threadDirName,
JSONConversationalExtension._threadInfoFileName,
]),
@ -219,14 +219,14 @@ export default class JSONConversationalExtension extends ConversationalExtension
*/
private async getValidThreadDirs(): Promise<string[]> {
const fileInsideThread: string[] = await fs.readdirSync(
JSONConversationalExtension._homeDir
JSONConversationalExtension._threadFolder
)
const threadDirs: string[] = []
for (let i = 0; i < fileInsideThread.length; i++) {
if (fileInsideThread[i].includes('.DS_Store')) continue
const path = await joinPath([
JSONConversationalExtension._homeDir,
JSONConversationalExtension._threadFolder,
fileInsideThread[i],
])
@ -246,7 +246,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
async getAllMessages(threadId: string): Promise<ThreadMessage[]> {
try {
const threadDirPath = await joinPath([
JSONConversationalExtension._homeDir,
JSONConversationalExtension._threadFolder,
threadId,
])
@ -263,22 +263,17 @@ export default class JSONConversationalExtension extends ConversationalExtension
JSONConversationalExtension._threadMessagesFileName,
])
const result = await fs
.readFileSync(messageFilePath, 'utf-8')
.then((content) =>
content
.toString()
.split('\n')
.filter((line) => line !== '')
)
let readResult = await fs.readFileSync(messageFilePath, 'utf-8')
if (typeof readResult === 'object') {
readResult = JSON.stringify(readResult)
}
const result = readResult.split('\n').filter((line) => line !== '')
const messages: ThreadMessage[] = []
result.forEach((line: string) => {
try {
messages.push(JSON.parse(line) as ThreadMessage)
} catch (err) {
console.error(err)
}
messages.push(JSON.parse(line))
})
return messages
} catch (err) {

View File

@ -26,11 +26,12 @@ import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useDownloadState } from '@/hooks/useDownloadState'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import useGetSystemResources from '@/hooks/useGetSystemResources'
import { useMainViewState } from '@/hooks/useMainViewState'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const menuLinks = [
{
@ -49,7 +50,8 @@ const BottomBar = () => {
const { activeModel, stateModel } = useActiveModel()
const { ram, cpu } = useGetSystemResources()
const progress = useAtomValue(appDownloadProgress)
const { downloadedModels } = useGetDownloadedModels()
const downloadedModels = useAtomValue(downloadedModelsAtom)
const { setMainViewState } = useMainViewState()
const { downloadStates } = useDownloadState()
const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom)

View File

@ -11,7 +11,7 @@ import {
Badge,
} from '@janhq/uikit'
import { useAtom } from 'jotai'
import { useAtom, useAtomValue } from 'jotai'
import { DatabaseIcon, CpuIcon } from 'lucide-react'
import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener'
@ -19,14 +19,14 @@ import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener'
import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
export default function CommandListDownloadedModel() {
const { setMainViewState } = useMainViewState()
const { downloadedModels } = useGetDownloadedModels()
const downloadedModels = useAtomValue(downloadedModelsAtom)
const { activeModel, startModel, stopModel } = useActiveModel()
const [serverEnabled] = useAtom(serverEnabledAtom)
const [showSelectModelModal, setShowSelectModelModal] = useAtom(

View File

@ -20,7 +20,6 @@ import { MainViewState } from '@/constants/screens'
import { useClickOutside } from '@/hooks/useClickOutside'
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import useGetAssistants, { getAssistants } from '@/hooks/useGetAssistants'
import { useMainViewState } from '@/hooks/useMainViewState'
import { usePath } from '@/hooks/usePath'
@ -29,13 +28,14 @@ import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
import { openFileTitle } from '@/utils/titleUtils'
import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
const TopBar = () => {
const activeThread = useAtomValue(activeThreadAtom)
const { mainViewState } = useMainViewState()
const { requestCreateNewThread } = useCreateNewThread()
const { assistants } = useGetAssistants()
const assistants = useAtomValue(assistantsAtom)
const [showRightSideBar, setShowRightSideBar] = useAtom(showRightSideBarAtom)
const [showLeftSideBar, setShowLeftSideBar] = useAtom(showLeftSideBarAtom)
const showing = useAtomValue(showRightSideBarAtom)
@ -61,12 +61,7 @@ const TopBar = () => {
const onCreateConversationClick = async () => {
if (assistants.length === 0) {
const res = await getAssistants()
if (res.length === 0) {
alert('No assistant available')
return
}
requestCreateNewThread(res[0])
alert('No assistant available')
} else {
requestCreateNewThread(assistants[0])
}

View File

@ -0,0 +1,21 @@
'use client'
import { Fragment, ReactNode } from 'react'
import useAssistants from '@/hooks/useAssistants'
import useModels from '@/hooks/useModels'
import useThreads from '@/hooks/useThreads'
type Props = {
children: ReactNode
}
const DataLoader: React.FC<Props> = ({ children }) => {
useModels()
useThreads()
useAssistants()
return <Fragment>{children}</Fragment>
}
export default DataLoader

View File

@ -18,7 +18,6 @@ import {
loadModelErrorAtom,
stateModelAtom,
} from '@/hooks/useActiveModel'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { queuedMessageAtom } from '@/hooks/useSendChatMessage'
@ -29,6 +28,7 @@ import {
addNewMessageAtom,
updateMessageAtom,
} from '@/helpers/atoms/ChatMessage.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
import {
updateThreadWaitingForResponseAtom,
threadsAtom,
@ -38,7 +38,7 @@ import {
export default function EventHandler({ children }: { children: ReactNode }) {
const addNewMessage = useSetAtom(addNewMessageAtom)
const updateMessage = useSetAtom(updateMessageAtom)
const { downloadedModels } = useGetDownloadedModels()
const downloadedModels = useAtomValue(downloadedModelsAtom)
const setActiveModel = useSetAtom(activeModelAtom)
const setStateModel = useSetAtom(stateModelAtom)
const setQueuedMessage = useSetAtom(queuedMessageAtom)
@ -143,7 +143,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
?.addNewMessage(message)
}
},
[updateMessage, updateThreadWaiting]
[updateMessage, updateThreadWaiting, setIsGeneratingResponse]
)
useEffect(() => {

View File

@ -3,10 +3,9 @@
import { PropsWithChildren, useEffect, useRef } from 'react'
import { baseName } from '@janhq/core'
import { useAtomValue, useSetAtom } from 'jotai'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { useDownloadState } from '@/hooks/useDownloadState'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { modelBinFileName } from '@/utils/model'
@ -14,14 +13,17 @@ import EventHandler from './EventHandler'
import { appDownloadProgress } from './Jotai'
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
import {
downloadedModelsAtom,
downloadingModelsAtom,
} from '@/helpers/atoms/Model.atom'
export default function EventListenerWrapper({ children }: PropsWithChildren) {
const setProgress = useSetAtom(appDownloadProgress)
const models = useAtomValue(downloadingModelsAtom)
const modelsRef = useRef(models)
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
const {
setDownloadState,
setDownloadStateSuccess,

View File

@ -23,6 +23,8 @@ import Umami from '@/utils/umami'
import Loader from '../Loader'
import DataLoader from './DataLoader'
import KeyListener from './KeyListener'
import { extensionManager } from '@/extension'
@ -81,7 +83,9 @@ const Providers = (props: PropsWithChildren) => {
<KeyListener>
<FeatureToggleWrapper>
<EventListenerWrapper>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
<TooltipProvider delayDuration={0}>
<DataLoader>{children}</DataLoader>
</TooltipProvider>
{!isMac && <GPUDriverPrompt />}
</EventListenerWrapper>
<Toaster />

View File

@ -0,0 +1,4 @@
import { Assistant } from '@janhq/core/.'
import { atom } from 'jotai'
export const assistantsAtom = atom<Assistant[]>([])

View File

@ -24,3 +24,7 @@ export const removeDownloadingModelAtom = atom(
)
}
)
export const downloadedModelsAtom = atom<Model[]>([])
export const configuredModelsAtom = atom<Model[]>([])

View File

@ -3,9 +3,9 @@ import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { toaster } from '@/containers/Toast'
import { useGetDownloadedModels } from './useGetDownloadedModels'
import { LAST_USED_MODEL_ID } from './useRecommendedModel'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
export const activeModelAtom = atom<Model | undefined>(undefined)
@ -21,7 +21,7 @@ export function useActiveModel() {
const [activeModel, setActiveModel] = useAtom(activeModelAtom)
const activeThread = useAtomValue(activeThreadAtom)
const [stateModel, setStateModel] = useAtom(stateModelAtom)
const { downloadedModels } = useGetDownloadedModels()
const downloadedModels = useAtomValue(downloadedModelsAtom)
const setLoadModelError = useSetAtom(loadModelErrorAtom)
const startModel = async (modelId: string) => {

View File

@ -0,0 +1,28 @@
import { useEffect } from 'react'
import { Assistant, AssistantExtension, ExtensionTypeEnum } from '@janhq/core'
import { useSetAtom } from 'jotai'
import { extensionManager } from '@/extension'
import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
const useAssistants = () => {
const setAssistants = useSetAtom(assistantsAtom)
useEffect(() => {
const getAssistants = async () => {
const assistants = await getLocalAssistants()
setAssistants(assistants)
}
getAssistants()
}, [setAssistants])
}
const getLocalAssistants = async (): Promise<Assistant[]> =>
extensionManager
.get<AssistantExtension>(ExtensionTypeEnum.Assistant)
?.getAssistants() ?? []
export default useAssistants

View File

@ -1,13 +1,14 @@
import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core'
import { useAtom } from 'jotai'
import { toaster } from '@/containers/Toast'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { extensionManager } from '@/extension/ExtensionManager'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
export default function useDeleteModel() {
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
const deleteModel = async (model: Model) => {
await extensionManager

View File

@ -1,27 +0,0 @@
import { useEffect, useState } from 'react'
import { Assistant, ExtensionTypeEnum, AssistantExtension } from '@janhq/core'
import { extensionManager } from '@/extension/ExtensionManager'
export const getAssistants = async (): Promise<Assistant[]> =>
extensionManager
.get<AssistantExtension>(ExtensionTypeEnum.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 }
}

View File

@ -1,30 +0,0 @@
import { useCallback, useEffect, useState } from 'react'
import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core'
import { extensionManager } from '@/extension/ExtensionManager'
export function useGetConfiguredModels() {
const [loading, setLoading] = useState<boolean>(false)
const [models, setModels] = useState<Model[]>([])
const fetchModels = useCallback(async () => {
setLoading(true)
const models = await getConfiguredModels()
setLoading(false)
setModels(models)
}, [])
useEffect(() => {
fetchModels()
}, [fetchModels])
return { loading, models }
}
const getConfiguredModels = async (): Promise<Model[]> => {
const models = await extensionManager
.get<ModelExtension>(ExtensionTypeEnum.Model)
?.getConfiguredModels()
return models ?? []
}

View File

@ -1,27 +0,0 @@
import { useEffect } from 'react'
import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core'
import { atom, useAtom } from 'jotai'
import { extensionManager } from '@/extension/ExtensionManager'
export const downloadedModelsAtom = atom<Model[]>([])
export function useGetDownloadedModels() {
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
useEffect(() => {
getDownloadedModels().then((downloadedModels) => {
setDownloadedModels(downloadedModels)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return { downloadedModels, setDownloadedModels }
}
export const getDownloadedModels = async (): Promise<Model[]> =>
extensionManager
.get<ModelExtension>(ExtensionTypeEnum.Model)
?.getDownloadedModels() ?? []

View File

@ -58,7 +58,7 @@ export default function useGetSystemResources() {
// There is a possibility that this will be removed and replaced by the process event hook?
const intervalId = setInterval(() => {
getSystemResources()
}, 500)
}, 5000)
// clean up interval
return () => clearInterval(intervalId)

46
web/hooks/useModels.ts Normal file
View File

@ -0,0 +1,46 @@
import { useEffect } from 'react'
import { ExtensionTypeEnum, Model, ModelExtension } from '@janhq/core'
import { useSetAtom } from 'jotai'
import { extensionManager } from '@/extension'
import {
configuredModelsAtom,
downloadedModelsAtom,
} from '@/helpers/atoms/Model.atom'
const useModels = () => {
const setDownloadedModels = useSetAtom(downloadedModelsAtom)
const setConfiguredModels = useSetAtom(configuredModelsAtom)
useEffect(() => {
const getDownloadedModels = async () => {
const models = await getLocalDownloadedModels()
setDownloadedModels(models)
}
getDownloadedModels()
}, [setDownloadedModels])
useEffect(() => {
const getConfiguredModels = async () => {
const models = await getLocalConfiguredModels()
setConfiguredModels(models)
}
getConfiguredModels()
}, [setConfiguredModels])
}
const getLocalConfiguredModels = async (): Promise<Model[]> =>
extensionManager
.get<ModelExtension>(ExtensionTypeEnum.Model)
?.getConfiguredModels() ?? []
const getLocalDownloadedModels = async (): Promise<Model[]> =>
extensionManager
.get<ModelExtension>(ExtensionTypeEnum.Model)
?.getDownloadedModels() ?? []
export default useModels

View File

@ -5,9 +5,9 @@ import { Model, InferenceEngine } from '@janhq/core'
import { atom, useAtomValue } from 'jotai'
import { activeModelAtom } from './useActiveModel'
import { getDownloadedModels } from './useGetDownloadedModels'
import { activeThreadAtom, threadStatesAtom } from '@/helpers/atoms/Thread.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
export const lastUsedModel = atom<Model | undefined>(undefined)
@ -24,19 +24,20 @@ export const LAST_USED_MODEL_ID = 'last-used-model-id'
*/
export default function useRecommendedModel() {
const activeModel = useAtomValue(activeModelAtom)
const [downloadedModels, setDownloadedModels] = useState<Model[]>([])
const [sortedModels, setSortedModels] = useState<Model[]>([])
const [recommendedModel, setRecommendedModel] = useState<Model | undefined>()
const activeThread = useAtomValue(activeThreadAtom)
const downloadedModels = useAtomValue(downloadedModelsAtom)
const getAndSortDownloadedModels = useCallback(async (): Promise<Model[]> => {
const models = (await getDownloadedModels()).sort((a, b) =>
const models = downloadedModels.sort((a, b) =>
a.engine !== InferenceEngine.nitro && b.engine === InferenceEngine.nitro
? 1
: -1
)
setDownloadedModels(models)
setSortedModels(models)
return models
}, [])
}, [downloadedModels])
const getRecommendedModel = useCallback(async (): Promise<
Model | undefined
@ -98,5 +99,5 @@ export default function useRecommendedModel() {
getRecommendedModel()
}, [getRecommendedModel])
return { recommendedModel, downloadedModels }
return { recommendedModel, downloadedModels: sortedModels }
}

View File

@ -1,3 +1,5 @@
import { useCallback } from 'react'
import {
InferenceEvent,
ExtensionTypeEnum,
@ -6,7 +8,7 @@ import {
ConversationalExtension,
} from '@janhq/core'
import { useAtomValue, useSetAtom } from 'jotai'
import { useSetAtom } from 'jotai'
import { loadModelErrorAtom } from './useActiveModel'
@ -14,43 +16,46 @@ import { extensionManager } from '@/extension'
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import {
ModelParams,
getActiveThreadIdAtom,
isGeneratingResponseAtom,
setActiveThreadIdAtom,
setThreadModelParamsAtom,
} from '@/helpers/atoms/Thread.atom'
export default function useSetActiveThread() {
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
const setThreadMessage = useSetAtom(setConvoMessagesAtom)
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
const setLoadModelError = useSetAtom(loadModelErrorAtom)
const setActiveThread = async (thread: Thread) => {
if (activeThreadId === thread.id) {
console.debug('Thread already active')
return
}
const setActiveThread = useCallback(
async (thread: Thread) => {
setIsGeneratingResponse(false)
events.emit(InferenceEvent.OnInferenceStopped, thread.id)
setIsGeneratingResponse(false)
setLoadModelError(undefined)
events.emit(InferenceEvent.OnInferenceStopped, thread.id)
// load the corresponding messages
const messages = await getLocalThreadMessage(thread.id)
setThreadMessage(thread.id, messages)
// load the corresponding messages
const messages = await extensionManager
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
?.getAllMessages(thread.id)
setThreadMessage(thread.id, messages ?? [])
setActiveThreadId(thread.id)
const modelParams: ModelParams = {
...thread.assistants[0]?.model?.parameters,
...thread.assistants[0]?.model?.settings,
}
setThreadModelParams(thread.id, modelParams)
},
[
setActiveThreadId,
setThreadMessage,
setThreadModelParams,
setIsGeneratingResponse,
]
)
setActiveThreadId(thread.id)
const modelParams: ModelParams = {
...thread.assistants[0]?.model?.parameters,
...thread.assistants[0]?.model?.settings,
}
setThreadModelParams(thread.id, modelParams)
}
return { activeThreadId, setActiveThread }
return { setActiveThread }
}
const getLocalThreadMessage = async (threadId: string) =>
extensionManager
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
?.getAllMessages(threadId) ?? []

View File

@ -1,3 +1,5 @@
import { useEffect } from 'react'
import {
ExtensionTypeEnum,
Thread,
@ -5,14 +7,13 @@ import {
ConversationalExtension,
} from '@janhq/core'
import { useAtomValue, useSetAtom } from 'jotai'
import { useSetAtom } from 'jotai'
import useSetActiveThread from './useSetActiveThread'
import { extensionManager } from '@/extension/ExtensionManager'
import {
ModelParams,
activeThreadAtom,
threadModelParamsAtom,
threadStatesAtom,
threadsAtom,
@ -22,11 +23,10 @@ const useThreads = () => {
const setThreadStates = useSetAtom(threadStatesAtom)
const setThreads = useSetAtom(threadsAtom)
const setThreadModelRuntimeParams = useSetAtom(threadModelParamsAtom)
const activeThread = useAtomValue(activeThreadAtom)
const { setActiveThread } = useSetActiveThread()
const getThreads = async () => {
try {
useEffect(() => {
const getThreads = async () => {
const localThreads = await getLocalThreads()
const localThreadStates: Record<string, ThreadState> = {}
const threadModelParams: Record<string, ModelParams> = {}
@ -54,17 +54,19 @@ const useThreads = () => {
setThreadStates(localThreadStates)
setThreads(localThreads)
setThreadModelRuntimeParams(threadModelParams)
if (localThreads.length && !activeThread) {
if (localThreads.length > 0) {
setActiveThread(localThreads[0])
}
} catch (error) {
console.error(error)
}
}
return {
getThreads,
}
getThreads()
}, [
setActiveThread,
setThreadModelRuntimeParams,
setThreadStates,
setThreads,
])
}
const getLocalThreads = async (): Promise<Thread[]> =>

View File

@ -11,7 +11,6 @@ import LogoMark from '@/containers/Brand/Logo/Mark'
import { MainViewState } from '@/constants/screens'
import { loadModelErrorAtom } from '@/hooks/useActiveModel'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState'
@ -20,10 +19,13 @@ import ChatItem from '../ChatItem'
import ErrorMessage from '../ErrorMessage'
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const ChatBody: React.FC = () => {
const messages = useAtomValue(getCurrentChatMessagesAtom)
const { downloadedModels } = useGetDownloadedModels()
const downloadedModels = useAtomValue(downloadedModelsAtom)
const { setMainViewState } = useMainViewState()
if (downloadedModels.length === 0)

View File

@ -0,0 +1,65 @@
import React, { useCallback } from 'react'
import {
Button,
Modal,
ModalClose,
ModalContent,
ModalFooter,
ModalHeader,
ModalPortal,
ModalTitle,
ModalTrigger,
} from '@janhq/uikit'
import { Paintbrush } from 'lucide-react'
import useDeleteThread from '@/hooks/useDeleteThread'
type Props = {
threadId: string
}
const CleanThreadModal: React.FC<Props> = ({ threadId }) => {
const { cleanThread } = useDeleteThread()
const onCleanThreadClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation()
cleanThread(threadId)
},
[cleanThread, threadId]
)
return (
<Modal>
<ModalTrigger asChild onClick={(e) => e.stopPropagation()}>
<div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
<Paintbrush size={16} className="text-muted-foreground" />
<span className="text-bold text-black dark:text-muted-foreground">
Clean thread
</span>
</div>
</ModalTrigger>
<ModalPortal />
<ModalContent>
<ModalHeader>
<ModalTitle>Clean Thread</ModalTitle>
</ModalHeader>
<p>Are you sure you want to clean this thread?</p>
<ModalFooter>
<div className="flex gap-x-2">
<ModalClose asChild onClick={(e) => e.stopPropagation()}>
<Button themes="ghost">No</Button>
</ModalClose>
<ModalClose asChild>
<Button themes="danger" onClick={onCleanThreadClick} autoFocus>
Yes
</Button>
</ModalClose>
</div>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default React.memo(CleanThreadModal)

View File

@ -0,0 +1,68 @@
import React, { useCallback } from 'react'
import {
Modal,
ModalTrigger,
ModalPortal,
ModalContent,
ModalHeader,
ModalTitle,
ModalFooter,
ModalClose,
Button,
} from '@janhq/uikit'
import { Trash2Icon } from 'lucide-react'
import useDeleteThread from '@/hooks/useDeleteThread'
type Props = {
threadId: string
}
const DeleteThreadModal: React.FC<Props> = ({ threadId }) => {
const { deleteThread } = useDeleteThread()
const onDeleteThreadClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation()
deleteThread(threadId)
},
[deleteThread, threadId]
)
return (
<Modal>
<ModalTrigger asChild onClick={(e) => e.stopPropagation()}>
<div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
<Trash2Icon size={16} className="text-red-600 dark:text-red-300" />
<span className="text-bold text-red-600 dark:text-red-300">
Delete thread
</span>
</div>
</ModalTrigger>
<ModalPortal />
<ModalContent>
<ModalHeader>
<ModalTitle>Delete Thread</ModalTitle>
</ModalHeader>
<p>
Are you sure you want to delete this thread? This action cannot be
undone.
</p>
<ModalFooter>
<div className="flex gap-x-2">
<ModalClose asChild onClick={(e) => e.stopPropagation()}>
<Button themes="ghost">No</Button>
</ModalClose>
<ModalClose asChild>
<Button autoFocus themes="danger" onClick={onDeleteThreadClick}>
Yes
</Button>
</ModalClose>
</div>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default React.memo(DeleteThreadModal)

View File

@ -2,15 +2,18 @@ import React, { Fragment, useCallback } from 'react'
import { Button } from '@janhq/uikit'
import { useAtomValue } from 'jotai'
import LogoMark from '@/containers/Brand/Logo/Mark'
import { MainViewState } from '@/constants/screens'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const RequestDownloadModel: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels()
const downloadedModels = useAtomValue(downloadedModelsAtom)
const { setMainViewState } = useMainViewState()
const onClick = useCallback(() => {

View File

@ -1,76 +1,39 @@
import { useEffect, useState } from 'react'
import { useCallback } from 'react'
import {
Modal,
ModalTrigger,
ModalClose,
ModalFooter,
ModalPortal,
ModalContent,
ModalHeader,
ModalTitle,
Button,
} from '@janhq/uikit'
import { Thread } from '@janhq/core/'
import { motion as m } from 'framer-motion'
import { useAtomValue } from 'jotai'
import {
GalleryHorizontalEndIcon,
MoreVerticalIcon,
Trash2Icon,
Paintbrush,
} from 'lucide-react'
import { GalleryHorizontalEndIcon, MoreVerticalIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import useDeleteThread from '@/hooks/useDeleteThread'
import useGetAssistants from '@/hooks/useGetAssistants'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import useSetActiveThread from '@/hooks/useSetActiveThread'
import useThreads from '@/hooks/useThreads'
import { displayDate } from '@/utils/datetime'
import CleanThreadModal from '../CleanThreadModal'
import DeleteThreadModal from '../DeleteThreadModal'
import {
activeThreadAtom,
getActiveThreadIdAtom,
threadStatesAtom,
threadsAtom,
} from '@/helpers/atoms/Thread.atom'
export default function ThreadList() {
const threads = useAtomValue(threadsAtom)
const threadStates = useAtomValue(threadStatesAtom)
const { getThreads } = useThreads()
const { assistants } = useGetAssistants()
const { requestCreateNewThread } = useCreateNewThread()
const activeThread = useAtomValue(activeThreadAtom)
const { deleteThread, cleanThread } = useDeleteThread()
const { downloadedModels } = useGetDownloadedModels()
const [isThreadsReady, setIsThreadsReady] = useState(false)
const threads = useAtomValue(threadsAtom)
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
const { setActiveThread } = useSetActiveThread()
const { activeThreadId, setActiveThread: onThreadClick } =
useSetActiveThread()
useEffect(() => {
getThreads().then(() => setIsThreadsReady(true))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (
isThreadsReady &&
downloadedModels.length !== 0 &&
threads.length === 0 &&
assistants.length !== 0 &&
!activeThread
) {
requestCreateNewThread(assistants[0])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assistants, threads, downloadedModels, activeThread, isThreadsReady])
const onThreadClick = useCallback(
(thread: Thread) => {
setActiveThread(thread)
},
[setActiveThread]
)
return (
<div className="px-3 py-4">
@ -83,133 +46,44 @@ export default function ThreadList() {
<h2 className="font-semibold">No Thread History</h2>
</div>
) : (
threads.map((thread, i) => {
const lastMessage =
threadStates[thread.id]?.lastMessage ?? 'No new message'
return (
<div
key={i}
className={twMerge(
`group/message relative mb-1 flex cursor-pointer flex-col transition-all hover:rounded-lg hover:bg-gray-100 hover:dark:bg-secondary/50`
)}
onClick={() => {
onThreadClick(thread)
}}
>
<div className="relative z-10 p-4 py-4">
<p className="line-clamp-1 text-xs leading-5 text-muted-foreground">
{thread.updated && displayDate(thread.updated)}
</p>
<h2 className="line-clamp-1 font-bold">{thread.title}</h2>
<p className="mt-1 line-clamp-1 text-xs text-gray-700 group-hover/message:max-w-[160px] dark:text-gray-300">
{lastMessage || 'No new message'}
</p>
</div>
<div
className={twMerge(
`group/icon invisible absolute bottom-2 right-2 z-20 rounded-lg p-1 text-muted-foreground hover:bg-gray-200 group-hover/message:visible hover:dark:bg-secondary`
)}
>
<MoreVerticalIcon />
<div className="invisible absolute right-0 z-20 w-40 overflow-hidden rounded-lg border border-border bg-background shadow-lg group-hover/icon:visible">
<Modal>
<ModalTrigger asChild onClick={(e) => e.stopPropagation()}>
<div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
<Paintbrush
size={16}
className="text-muted-foreground"
/>
<span className="text-bold text-black dark:text-muted-foreground">
Clean thread
</span>
</div>
</ModalTrigger>
<ModalPortal />
<ModalContent>
<ModalHeader>
<ModalTitle>Clean Thread</ModalTitle>
</ModalHeader>
<p>Are you sure you want to clean this thread?</p>
<ModalFooter>
<div className="flex gap-x-2">
<ModalClose
asChild
onClick={(e) => e.stopPropagation()}
>
<Button themes="ghost">No</Button>
</ModalClose>
<ModalClose asChild>
<Button
themes="danger"
onClick={(e) => {
e.stopPropagation()
cleanThread(thread.id)
}}
autoFocus
>
Yes
</Button>
</ModalClose>
</div>
</ModalFooter>
</ModalContent>
</Modal>
<Modal>
<ModalTrigger asChild onClick={(e) => e.stopPropagation()}>
<div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
<Trash2Icon
size={16}
className="text-red-600 dark:text-red-300"
/>
<span className="text-bold text-red-600 dark:text-red-300">
Delete thread
</span>
</div>
</ModalTrigger>
<ModalPortal />
<ModalContent>
<ModalHeader>
<ModalTitle>Delete Thread</ModalTitle>
</ModalHeader>
<p>
Are you sure you want to delete this thread? This action
cannot be undone.
</p>
<ModalFooter>
<div className="flex gap-x-2">
<ModalClose
asChild
onClick={(e) => e.stopPropagation()}
>
<Button themes="ghost">No</Button>
</ModalClose>
<ModalClose asChild>
<Button
autoFocus
themes="danger"
onClick={(e) => {
e.stopPropagation()
deleteThread(thread.id)
}}
>
Yes
</Button>
</ModalClose>
</div>
</ModalFooter>
</ModalContent>
</Modal>
</div>
</div>
{activeThreadId === thread.id && (
<m.div
className="absolute inset-0 left-0 h-full w-full rounded-lg bg-gray-100 p-4 dark:bg-secondary/50"
layoutId="active-thread"
/>
)}
threads.map((thread) => (
<div
key={thread.id}
className={twMerge(
`group/message relative mb-1 flex cursor-pointer flex-col transition-all hover:rounded-lg hover:bg-gray-100 hover:dark:bg-secondary/50`
)}
onClick={() => {
onThreadClick(thread)
}}
>
<div className="relative z-10 p-4 py-4">
<p className="line-clamp-1 text-xs leading-5 text-muted-foreground">
{thread.updated && displayDate(thread.updated)}
</p>
<h2 className="line-clamp-1 font-bold">{thread.title}</h2>
<p className="mt-1 line-clamp-1 text-xs text-gray-700 group-hover/message:max-w-[160px] dark:text-gray-300">
{threadStates[thread.id]?.lastMessage ?? 'No new message'}
</p>
</div>
)
})
<div
className={twMerge(
`group/icon invisible absolute bottom-2 right-2 z-20 rounded-lg p-1 text-muted-foreground hover:bg-gray-200 group-hover/message:visible hover:dark:bg-secondary`
)}
>
<MoreVerticalIcon />
<div className="invisible absolute right-0 z-20 w-40 overflow-hidden rounded-lg border border-border bg-background shadow-lg group-hover/icon:visible">
<CleanThreadModal threadId={thread.id} />
<DeleteThreadModal threadId={thread.id} />
</div>
</div>
{activeThreadId === thread.id && (
<m.div
className="absolute inset-0 left-0 h-full w-full rounded-lg bg-gray-100 p-4 dark:bg-secondary/50"
layoutId="active-thread"
/>
)}
</div>
))
)}
</div>
)

View File

@ -27,14 +27,14 @@ import useDownloadModel from '@/hooks/useDownloadModel'
import { useDownloadState } from '@/hooks/useDownloadState'
import { getAssistants } from '@/hooks/useGetAssistants'
import { downloadedModelsAtom } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState'
import { toGibibytes } from '@/utils/converter'
import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
type Props = {
@ -49,7 +49,9 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
const { modelDownloadStateAtom } = useDownloadState()
const { requestCreateNewThread } = useCreateNewThread()
const totalRam = useAtomValue(totalRamAtom)
const serverEnabled = useAtomValue(serverEnabledAtom)
const assistants = useAtomValue(assistantsAtom)
const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
@ -60,7 +62,6 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
const onDownloadClick = useCallback(() => {
downloadModel(model)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [model])
const isDownloaded = downloadedModels.find((md) => md.id === model.id) != null
@ -70,7 +71,6 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
)
const onUseModelClick = useCallback(async () => {
const assistants = await getAssistants()
if (assistants.length === 0) {
alert('No assistant available')
return

View File

@ -10,9 +10,11 @@ import { MainViewState } from '@/constants/screens'
import useDownloadModel from '@/hooks/useDownloadModel'
import { useDownloadState } from '@/hooks/useDownloadState'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
type Props = {
model: Model
isRecommended: boolean
@ -20,7 +22,7 @@ type Props = {
const ModelVersionItem: React.FC<Props> = ({ model }) => {
const { downloadModel } = useDownloadModel()
const { downloadedModels } = useGetDownloadedModels()
const downloadedModels = useAtomValue(downloadedModelsAtom)
const { setMainViewState } = useMainViewState()
const isDownloaded =
downloadedModels.find(

View File

@ -1,4 +1,4 @@
import { useState } from 'react'
import { useCallback, useState } from 'react'
import { openExternalUrl } from '@janhq/core'
import {
@ -12,24 +12,24 @@ import {
SelectItem,
} from '@janhq/uikit'
import { useAtomValue } from 'jotai'
import { SearchIcon } from 'lucide-react'
import Loader from '@/containers/Loader'
import { useGetConfiguredModels } from '@/hooks/useGetConfiguredModels'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import ExploreModelList from './ExploreModelList'
import {
configuredModelsAtom,
downloadedModelsAtom,
} from '@/helpers/atoms/Model.atom'
const ExploreModelsScreen = () => {
const { loading, models } = useGetConfiguredModels()
const configuredModels = useAtomValue(configuredModelsAtom)
const downloadedModels = useAtomValue(downloadedModelsAtom)
const [searchValue, setsearchValue] = useState('')
const { downloadedModels } = useGetDownloadedModels()
const [sortSelected, setSortSelected] = useState('All Models')
const sortMenu = ['All Models', 'Recommended', 'Downloaded']
const filteredModels = models.filter((x) => {
const filteredModels = configuredModels.filter((x) => {
if (sortSelected === 'Downloaded') {
return (
x.name.toLowerCase().includes(searchValue.toLowerCase()) &&
@ -45,11 +45,9 @@ const ExploreModelsScreen = () => {
}
})
const onHowToImportModelClick = () => {
const onHowToImportModelClick = useCallback(() => {
openExternalUrl('https://jan.ai/guides/using-models/import-manually/')
}
if (loading) return <Loader description="loading ..." />
}, [])
return (
<div

View File

@ -2,16 +2,17 @@ import { useState } from 'react'
import { Input } from '@janhq/uikit'
import { useAtomValue } from 'jotai'
import { SearchIcon } from 'lucide-react'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import RowModel from './Row'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
export default function Models() {
const { downloadedModels } = useGetDownloadedModels()
const downloadedModels = useAtomValue(downloadedModelsAtom)
const [searchValue, setsearchValue] = useState('')
const filteredDownloadedModels = downloadedModels.filter((x) => {