Merge branch 'main' into docs/install
This commit is contained in:
commit
f2b2247665
@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
ChatCompletionMessage,
|
||||
ChatCompletionRole,
|
||||
EventName,
|
||||
MessageRequest,
|
||||
|
||||
@ -109,10 +109,10 @@ async function validateModelStatus(): Promise<InitModelResponse> {
|
||||
return { error: undefined };
|
||||
}
|
||||
}
|
||||
return { error: "Model is not loaded successfully" };
|
||||
return { error: "Model loading failed" };
|
||||
})
|
||||
.catch((err) => {
|
||||
return { error: `Model is not loaded successfully. ${err.message}` };
|
||||
return { error: `Model loading failed. ${err.message}` };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
21
plugins/model-plugin/src/@types/schema.ts
Normal file
21
plugins/model-plugin/src/@types/schema.ts
Normal file
@ -0,0 +1,21 @@
|
||||
interface Version {
|
||||
name: string
|
||||
quantMethod: string
|
||||
bits: number
|
||||
size: number
|
||||
maxRamRequired: number
|
||||
usecase: string
|
||||
downloadLink: string
|
||||
}
|
||||
interface ModelSchema {
|
||||
id: string
|
||||
name: string
|
||||
shortDescription: string
|
||||
avatarUrl: string
|
||||
longDescription: string
|
||||
author: string
|
||||
version: string
|
||||
modelUrl: string
|
||||
tags: string[]
|
||||
versions: Version[]
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
export const parseToModel = (model) => {
|
||||
import { ModelCatalog } from '@janhq/core'
|
||||
|
||||
export function parseToModel(schema: ModelSchema): ModelCatalog {
|
||||
const modelVersions = []
|
||||
model.versions.forEach((v) => {
|
||||
schema.versions.forEach((v) => {
|
||||
const version = {
|
||||
id: `${model.author}-${v.name}`,
|
||||
name: v.name,
|
||||
quantMethod: v.quantMethod,
|
||||
bits: v.bits,
|
||||
@ -10,28 +11,22 @@ export const parseToModel = (model) => {
|
||||
maxRamRequired: v.maxRamRequired,
|
||||
usecase: v.usecase,
|
||||
downloadLink: v.downloadLink,
|
||||
productId: model.id,
|
||||
}
|
||||
modelVersions.push(version)
|
||||
})
|
||||
|
||||
const product = {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
shortDescription: model.shortDescription,
|
||||
avatarUrl: model.avatarUrl,
|
||||
author: model.author,
|
||||
version: model.version,
|
||||
modelUrl: model.modelUrl,
|
||||
nsfw: model.nsfw,
|
||||
tags: model.tags,
|
||||
greeting: model.defaultGreeting,
|
||||
type: model.type,
|
||||
createdAt: model.createdAt,
|
||||
longDescription: model.longDescription,
|
||||
status: 'Downloadable',
|
||||
const model: ModelCatalog = {
|
||||
id: schema.id,
|
||||
name: schema.name,
|
||||
shortDescription: schema.shortDescription,
|
||||
avatarUrl: schema.avatarUrl,
|
||||
author: schema.author,
|
||||
version: schema.version,
|
||||
modelUrl: schema.modelUrl,
|
||||
tags: schema.tags,
|
||||
longDescription: schema.longDescription,
|
||||
releaseDate: 0,
|
||||
availableVersions: modelVersions,
|
||||
}
|
||||
return product
|
||||
return model
|
||||
}
|
||||
|
||||
@ -10,7 +10,9 @@ const TopBar = () => {
|
||||
<div className="fixed left-0 top-0 z-50 flex h-12 w-full border-b border-border bg-background/50">
|
||||
<div className="relative left-16 flex w-[calc(100%-64px)] items-center justify-between space-x-4 pl-6 pr-2">
|
||||
<div>
|
||||
<span className="font-medium">{viewStateName}</span>
|
||||
<span className="font-medium">
|
||||
{viewStateName.replace(/([A-Z])/g, ' $1').trim()}
|
||||
</span>
|
||||
</div>
|
||||
<CommandSearch />
|
||||
{/* Command without trigger interface */}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import { atom } from 'jotai'
|
||||
|
||||
export const showConfirmDeleteConversationModalAtom = atom(false)
|
||||
export const showConfirmSignOutModalAtom = atom(false)
|
||||
export const showConfirmDeleteModalAtom = atom(false)
|
||||
export const showingAdvancedPromptAtom = atom<boolean>(false)
|
||||
export const showingProductDetailAtom = atom<boolean>(false)
|
||||
export const showingMobilePaneAtom = atom<boolean>(false)
|
||||
export const showingBotListModalAtom = atom<boolean>(false)
|
||||
export const showingCancelDownloadModalAtom = atom<boolean>(false)
|
||||
|
||||
export const showingModalNoActiveModel = atom<boolean>(false)
|
||||
@ -33,7 +33,10 @@ export function useActiveModel() {
|
||||
const model = downloadedModels.find((e) => e.id === modelId)
|
||||
|
||||
if (!model) {
|
||||
alert(`Model ${modelId} not found! Please re-download the model first.`)
|
||||
toaster({
|
||||
title: `Model ${modelId} not found!`,
|
||||
description: `Please download the model first.`,
|
||||
})
|
||||
setStateModel(() => ({
|
||||
state: 'start',
|
||||
loading: false,
|
||||
|
||||
@ -20,11 +20,10 @@ export const useCreateConversation = () => {
|
||||
const addNewConvoState = useSetAtom(addNewConversationStateAtom)
|
||||
|
||||
const requestCreateConvo = async (model: Model) => {
|
||||
const summary = model.name
|
||||
const mappedConvo: Thread = {
|
||||
id: generateConversationId(),
|
||||
modelId: model.id,
|
||||
summary,
|
||||
summary: model.name,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
messages: [],
|
||||
@ -35,7 +34,7 @@ export const useCreateConversation = () => {
|
||||
waitingForResponse: false,
|
||||
})
|
||||
|
||||
pluginManager
|
||||
await pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveConversation(mappedConvo)
|
||||
setUserConversations([mappedConvo, ...userConversations])
|
||||
|
||||
@ -16,10 +16,6 @@ import {
|
||||
getActiveConvoIdAtom,
|
||||
setActiveConvoIdAtom,
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
import {
|
||||
showingProductDetailAtom,
|
||||
showingAdvancedPromptAtom,
|
||||
} from '@/helpers/atoms/Modal.atom'
|
||||
|
||||
export default function useDeleteConversation() {
|
||||
const { activeModel } = useActiveModel()
|
||||
@ -27,8 +23,6 @@ export default function useDeleteConversation() {
|
||||
userConversationsAtom
|
||||
)
|
||||
const setCurrentPrompt = useSetAtom(currentPromptAtom)
|
||||
const setShowingProductDetail = useSetAtom(showingProductDetailAtom)
|
||||
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom)
|
||||
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
||||
|
||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
||||
@ -45,18 +39,16 @@ export default function useDeleteConversation() {
|
||||
)
|
||||
setUserConversations(currentConversations)
|
||||
deleteMessages(activeConvoId)
|
||||
setCurrentPrompt('')
|
||||
toaster({
|
||||
title: 'Succes delete a chat',
|
||||
description: `Delete chat with ${activeModel?.name} has been completed`,
|
||||
title: 'Chat successfully deleted.',
|
||||
description: `Chat with ${activeModel?.name} has been successfully deleted.`,
|
||||
})
|
||||
if (currentConversations.length > 0) {
|
||||
setActiveConvoId(currentConversations[0].id)
|
||||
} else {
|
||||
setActiveConvoId(undefined)
|
||||
}
|
||||
setCurrentPrompt('')
|
||||
setShowingProductDetail(false)
|
||||
setShowingAdvancedPrompt(false)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
@ -20,8 +20,8 @@ export default function useDeleteModel() {
|
||||
// reload models
|
||||
setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id))
|
||||
toaster({
|
||||
title: 'Delete a Model',
|
||||
description: `Model ${model.id} has been deleted.`,
|
||||
title: 'Model Deletion Successful',
|
||||
description: `The model ${model.id} has been successfully deleted.`,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -20,14 +20,13 @@ export function useGetConfiguredModels() {
|
||||
const [models, setModels] = useState<ModelCatalog[]>([])
|
||||
|
||||
async function getConfiguredModels(): Promise<ModelCatalog[]> {
|
||||
return (
|
||||
((await pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.getConfiguredModels()) as ModelCatalog[]) ?? []
|
||||
)
|
||||
const models = await pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.getConfiguredModels()
|
||||
return models ?? []
|
||||
}
|
||||
|
||||
const fetchModels = async () => {
|
||||
async function fetchModels() {
|
||||
setLoading(true)
|
||||
let models = await getConfiguredModels()
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
@ -37,10 +36,8 @@ export function useGetConfiguredModels() {
|
||||
setModels(models)
|
||||
}
|
||||
|
||||
// TODO allow user for filter
|
||||
useEffect(() => {
|
||||
fetchModels()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return { loading, models }
|
||||
|
||||
@ -12,11 +12,10 @@ export function useGetDownloadedModels() {
|
||||
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelAtom)
|
||||
|
||||
async function getDownloadedModels(): Promise<Model[]> {
|
||||
const models =
|
||||
((await pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.getDownloadedModels()) as Model[]) ?? []
|
||||
return models
|
||||
const models = await pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.getDownloadedModels()
|
||||
return models ?? []
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -10,15 +10,15 @@ import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
|
||||
|
||||
export default function useGetInputState() {
|
||||
const [inputState, setInputState] = useState<InputType>('loading')
|
||||
const currentConvo = useAtomValue(currentConversationAtom)
|
||||
const currentThread = useAtomValue(currentConversationAtom)
|
||||
const { activeModel } = useActiveModel()
|
||||
const { downloadedModels } = useGetDownloadedModels()
|
||||
|
||||
const handleInputState = (
|
||||
convo: Thread | undefined,
|
||||
thread: Thread | undefined,
|
||||
currentModel: Model | undefined
|
||||
) => {
|
||||
if (convo == null) return
|
||||
if (thread == null) return
|
||||
if (currentModel == null) {
|
||||
setInputState('loading')
|
||||
return
|
||||
@ -26,7 +26,7 @@ export default function useGetInputState() {
|
||||
|
||||
// check if convo model id is in downloaded models
|
||||
const isModelAvailable = downloadedModels.some(
|
||||
(model) => model.id === convo.modelId
|
||||
(model) => model.id === thread.modelId
|
||||
)
|
||||
|
||||
if (!isModelAvailable) {
|
||||
@ -35,7 +35,7 @@ export default function useGetInputState() {
|
||||
return
|
||||
}
|
||||
|
||||
if (convo.modelId !== currentModel.id) {
|
||||
if (thread.modelId !== currentModel.id) {
|
||||
// in case convo model and active model is different,
|
||||
// ask user to init the required model
|
||||
setInputState('model-mismatch')
|
||||
@ -46,11 +46,11 @@ export default function useGetInputState() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleInputState(currentConvo, activeModel)
|
||||
handleInputState(currentThread, activeModel)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return { inputState, currentConvo }
|
||||
return { inputState, currentThread }
|
||||
}
|
||||
|
||||
type InputType = 'available' | 'loading' | 'model-mismatch' | 'model-not-found'
|
||||
|
||||
@ -34,14 +34,15 @@ export default function useGetSystemResources() {
|
||||
useEffect(() => {
|
||||
getSystemResources()
|
||||
|
||||
// Fetch interval - every 3s
|
||||
// Fetch interval - every 5s
|
||||
// TODO: Will we really need this?
|
||||
// There is a possibility that this will be removed and replaced by the process event hook?
|
||||
const intervalId = setInterval(() => {
|
||||
getSystemResources()
|
||||
}, 5000)
|
||||
|
||||
// clean up
|
||||
// clean up interval
|
||||
return () => clearInterval(intervalId)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return {
|
||||
|
||||
@ -16,6 +16,8 @@ import { ulid } from 'ulid'
|
||||
|
||||
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||
|
||||
import { useActiveModel } from './useActiveModel'
|
||||
|
||||
import {
|
||||
addNewMessageAtom,
|
||||
getCurrentChatMessagesAtom,
|
||||
@ -34,54 +36,51 @@ export default function useSendChatMessage() {
|
||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
||||
const currentMessages = useAtomValue(getCurrentChatMessagesAtom)
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
const { activeModel } = useActiveModel()
|
||||
|
||||
function updateConvSummary(newMessage: MessageRequest) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
const conv = currentConvo
|
||||
if (
|
||||
!currentConvo?.summary ||
|
||||
if (
|
||||
currentConvo &&
|
||||
newMessage.messages &&
|
||||
newMessage.messages.length > 2 &&
|
||||
(!currentConvo.summary ||
|
||||
currentConvo.summary === '' ||
|
||||
currentConvo.summary.startsWith('Prompt:')
|
||||
) {
|
||||
const summaryMsg: ChatCompletionMessage = {
|
||||
role: ChatCompletionRole.User,
|
||||
content:
|
||||
'summary this conversation in 5 words, the response should just include the summary',
|
||||
}
|
||||
// Request convo summary
|
||||
setTimeout(async () => {
|
||||
const result = await pluginManager
|
||||
.get<InferencePlugin>(PluginType.Inference)
|
||||
?.inferenceRequest({
|
||||
...newMessage,
|
||||
messages: newMessage.messages?.concat([summaryMsg]),
|
||||
})
|
||||
|
||||
if (
|
||||
result?.message &&
|
||||
result.message.split(' ').length <= 10 &&
|
||||
conv?.id
|
||||
) {
|
||||
const updatedConv = {
|
||||
...conv,
|
||||
summary: result.message,
|
||||
}
|
||||
updateConversation(updatedConv)
|
||||
pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveConversation({
|
||||
...updatedConv,
|
||||
messages: currentMessages,
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
currentConvo.summary === activeModel?.name)
|
||||
) {
|
||||
const summaryMsg: ChatCompletionMessage = {
|
||||
role: ChatCompletionRole.User,
|
||||
content:
|
||||
'summary this conversation in a few words, the response should just include the summary',
|
||||
}
|
||||
}, 100)
|
||||
// Request convo summary
|
||||
setTimeout(async () => {
|
||||
const result = await pluginManager
|
||||
.get<InferencePlugin>(PluginType.Inference)
|
||||
?.inferenceRequest({
|
||||
...newMessage,
|
||||
messages: newMessage.messages?.slice(0, -1).concat([summaryMsg]),
|
||||
})
|
||||
if (
|
||||
currentConvo &&
|
||||
currentConvo.id === newMessage.threadId &&
|
||||
result?.message &&
|
||||
result?.message?.trim().length > 0 &&
|
||||
result.message.split(' ').length <= 10
|
||||
) {
|
||||
const updatedConv = {
|
||||
...currentConvo,
|
||||
summary: result.message,
|
||||
}
|
||||
updateConversation(updatedConv)
|
||||
pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveConversation({
|
||||
...updatedConv,
|
||||
messages: currentMessages,
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const sendChatMessage = async () => {
|
||||
@ -123,21 +122,7 @@ export default function useSendChatMessage() {
|
||||
}
|
||||
addNewMessage(threadMessage)
|
||||
|
||||
// delay randomly from 50 - 100ms
|
||||
// to prevent duplicate message id
|
||||
const delay = Math.floor(Math.random() * 50) + 50
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
|
||||
events.emit(EventName.OnNewMessageRequest, messageRequest)
|
||||
if (!currentConvo?.summary && currentConvo) {
|
||||
const updatedConv: Thread = {
|
||||
...currentConvo,
|
||||
summary: `Prompt: ${prompt}`,
|
||||
}
|
||||
|
||||
updateConversation(updatedConv)
|
||||
}
|
||||
|
||||
updateConvSummary(messageRequest)
|
||||
}
|
||||
|
||||
|
||||
@ -58,10 +58,18 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
||||
<div>
|
||||
<span className="mb-1 font-semibold">Compatibility</span>
|
||||
<div className="mt-1 flex gap-2">
|
||||
<Badge themes="secondary" className="line-clamp-1 max-w-[400px]">
|
||||
<Badge
|
||||
themes="secondary"
|
||||
className="line-clamp-1 max-w-[400px] lg:line-clamp-none lg:max-w-none"
|
||||
title={usecase}
|
||||
>
|
||||
{usecase}
|
||||
</Badge>
|
||||
<Badge themes="secondary" className="line-clamp-1">
|
||||
<Badge
|
||||
themes="secondary"
|
||||
className="line-clamp-1 lg:line-clamp-none"
|
||||
title={`${toGigabytes(maxRamRequired)} RAM required`}
|
||||
>
|
||||
{toGigabytes(maxRamRequired)} RAM required
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
||||
import { Button } from '@janhq/uikit'
|
||||
import { Badge } from '@janhq/uikit'
|
||||
import { Button, Badge } from '@janhq/uikit'
|
||||
|
||||
import { atom, useAtomValue } from 'jotai'
|
||||
|
||||
import ModalCancelDownload from '@/containers/ModalCancelDownload'
|
||||
@ -73,16 +73,25 @@ const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 border-t border-border pb-3 pl-3 pr-4 pt-3 first:border-t-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="mb-4 line-clamp-1 flex-1">{modelVersion.name}</span>
|
||||
<span className="line-clamp-1 flex-1" title={modelVersion.name}>
|
||||
{modelVersion.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Badge themes="secondary" className="line-clamp-1 max-w-[240px]">
|
||||
<Badge
|
||||
themes="secondary"
|
||||
className="line-clamp-1 max-w-[240px] lg:line-clamp-none lg:max-w-none"
|
||||
title={usecase}
|
||||
>
|
||||
{usecase}
|
||||
</Badge>
|
||||
<Badge themes="secondary" className="line-clamp-1 ">{`${toGigabytes(
|
||||
maxRamRequired
|
||||
)} RAM required`}</Badge>
|
||||
|
||||
<Badge
|
||||
themes="secondary"
|
||||
className="line-clamp-1"
|
||||
title={`${toGigabytes(maxRamRequired)} RAM required`}
|
||||
>{`${toGigabytes(maxRamRequired)} RAM required`}</Badge>
|
||||
<Badge themes="secondary">{toGigabytes(modelVersion.size)}</Badge>
|
||||
</div>
|
||||
{downloadButton}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { toast } from 'react-toastify'
|
||||
const API_BASE_PATH: string = '/api/v1'
|
||||
@ -48,7 +47,7 @@ export async function fetchApi(
|
||||
method: pluginFunc,
|
||||
args: args,
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': '' },
|
||||
headers: { contentType: 'application/json', Authorization: '' },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
export class EventEmitter {
|
||||
private handlers: Map<string, Function[]>
|
||||
@ -28,6 +27,7 @@ export class EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public emit(eventName: string, args: any): void {
|
||||
if (!this.handlers.has(eventName)) {
|
||||
return
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { ModelCatalog, ModelVersion } from '@janhq/core'
|
||||
import { ModelCatalog } from '@janhq/core'
|
||||
|
||||
export const dummyModel: ModelCatalog = {
|
||||
id: 'aladar/TinyLLama-v0-GGUF',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user