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:
parent
ccbe18e5b8
commit
01fec49798
@ -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) => {
|
||||
|
||||
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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])
|
||||
}
|
||||
|
||||
21
web/containers/Providers/DataLoader.tsx
Normal file
21
web/containers/Providers/DataLoader.tsx
Normal 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
|
||||
@ -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(() => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 />
|
||||
|
||||
4
web/helpers/atoms/Assistant.atom.ts
Normal file
4
web/helpers/atoms/Assistant.atom.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { Assistant } from '@janhq/core/.'
|
||||
import { atom } from 'jotai'
|
||||
|
||||
export const assistantsAtom = atom<Assistant[]>([])
|
||||
@ -24,3 +24,7 @@ export const removeDownloadingModelAtom = atom(
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const downloadedModelsAtom = atom<Model[]>([])
|
||||
|
||||
export const configuredModelsAtom = atom<Model[]>([])
|
||||
|
||||
@ -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) => {
|
||||
|
||||
28
web/hooks/useAssistants.ts
Normal file
28
web/hooks/useAssistants.ts
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
@ -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 ?? []
|
||||
}
|
||||
@ -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() ?? []
|
||||
@ -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
46
web/hooks/useModels.ts
Normal 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
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
@ -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) ?? []
|
||||
|
||||
@ -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[]> =>
|
||||
|
||||
@ -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)
|
||||
|
||||
65
web/screens/Chat/CleanThreadModal/index.tsx
Normal file
65
web/screens/Chat/CleanThreadModal/index.tsx
Normal 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)
|
||||
68
web/screens/Chat/DeleteThreadModal/index.tsx
Normal file
68
web/screens/Chat/DeleteThreadModal/index.tsx
Normal 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)
|
||||
@ -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(() => {
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user