Merge branch 'main' into docs/install

This commit is contained in:
Hieu 2023-11-20 13:55:10 +09:00 committed by GitHub
commit f2b2247665
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 144 additions and 147 deletions

View File

@ -7,7 +7,6 @@
*/ */
import { import {
ChatCompletionMessage,
ChatCompletionRole, ChatCompletionRole,
EventName, EventName,
MessageRequest, MessageRequest,

View File

@ -109,10 +109,10 @@ async function validateModelStatus(): Promise<InitModelResponse> {
return { error: undefined }; return { error: undefined };
} }
} }
return { error: "Model is not loaded successfully" }; return { error: "Model loading failed" };
}) })
.catch((err) => { .catch((err) => {
return { error: `Model is not loaded successfully. ${err.message}` }; return { error: `Model loading failed. ${err.message}` };
}); });
} }

View 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[]
}

View File

@ -1,8 +1,9 @@
export const parseToModel = (model) => { import { ModelCatalog } from '@janhq/core'
export function parseToModel(schema: ModelSchema): ModelCatalog {
const modelVersions = [] const modelVersions = []
model.versions.forEach((v) => { schema.versions.forEach((v) => {
const version = { const version = {
id: `${model.author}-${v.name}`,
name: v.name, name: v.name,
quantMethod: v.quantMethod, quantMethod: v.quantMethod,
bits: v.bits, bits: v.bits,
@ -10,28 +11,22 @@ export const parseToModel = (model) => {
maxRamRequired: v.maxRamRequired, maxRamRequired: v.maxRamRequired,
usecase: v.usecase, usecase: v.usecase,
downloadLink: v.downloadLink, downloadLink: v.downloadLink,
productId: model.id,
} }
modelVersions.push(version) modelVersions.push(version)
}) })
const product = { const model: ModelCatalog = {
id: model.id, id: schema.id,
name: model.name, name: schema.name,
shortDescription: model.shortDescription, shortDescription: schema.shortDescription,
avatarUrl: model.avatarUrl, avatarUrl: schema.avatarUrl,
author: model.author, author: schema.author,
version: model.version, version: schema.version,
modelUrl: model.modelUrl, modelUrl: schema.modelUrl,
nsfw: model.nsfw, tags: schema.tags,
tags: model.tags, longDescription: schema.longDescription,
greeting: model.defaultGreeting,
type: model.type,
createdAt: model.createdAt,
longDescription: model.longDescription,
status: 'Downloadable',
releaseDate: 0, releaseDate: 0,
availableVersions: modelVersions, availableVersions: modelVersions,
} }
return product return model
} }

View File

@ -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="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 className="relative left-16 flex w-[calc(100%-64px)] items-center justify-between space-x-4 pl-6 pr-2">
<div> <div>
<span className="font-medium">{viewStateName}</span> <span className="font-medium">
{viewStateName.replace(/([A-Z])/g, ' $1').trim()}
</span>
</div> </div>
<CommandSearch /> <CommandSearch />
{/* Command without trigger interface */} {/* Command without trigger interface */}

View File

@ -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)

View File

@ -33,7 +33,10 @@ export function useActiveModel() {
const model = downloadedModels.find((e) => e.id === modelId) const model = downloadedModels.find((e) => e.id === modelId)
if (!model) { 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(() => ({ setStateModel(() => ({
state: 'start', state: 'start',
loading: false, loading: false,

View File

@ -20,11 +20,10 @@ export const useCreateConversation = () => {
const addNewConvoState = useSetAtom(addNewConversationStateAtom) const addNewConvoState = useSetAtom(addNewConversationStateAtom)
const requestCreateConvo = async (model: Model) => { const requestCreateConvo = async (model: Model) => {
const summary = model.name
const mappedConvo: Thread = { const mappedConvo: Thread = {
id: generateConversationId(), id: generateConversationId(),
modelId: model.id, modelId: model.id,
summary, summary: model.name,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
messages: [], messages: [],
@ -35,7 +34,7 @@ export const useCreateConversation = () => {
waitingForResponse: false, waitingForResponse: false,
}) })
pluginManager await pluginManager
.get<ConversationalPlugin>(PluginType.Conversational) .get<ConversationalPlugin>(PluginType.Conversational)
?.saveConversation(mappedConvo) ?.saveConversation(mappedConvo)
setUserConversations([mappedConvo, ...userConversations]) setUserConversations([mappedConvo, ...userConversations])

View File

@ -16,10 +16,6 @@ import {
getActiveConvoIdAtom, getActiveConvoIdAtom,
setActiveConvoIdAtom, setActiveConvoIdAtom,
} from '@/helpers/atoms/Conversation.atom' } from '@/helpers/atoms/Conversation.atom'
import {
showingProductDetailAtom,
showingAdvancedPromptAtom,
} from '@/helpers/atoms/Modal.atom'
export default function useDeleteConversation() { export default function useDeleteConversation() {
const { activeModel } = useActiveModel() const { activeModel } = useActiveModel()
@ -27,8 +23,6 @@ export default function useDeleteConversation() {
userConversationsAtom userConversationsAtom
) )
const setCurrentPrompt = useSetAtom(currentPromptAtom) const setCurrentPrompt = useSetAtom(currentPromptAtom)
const setShowingProductDetail = useSetAtom(showingProductDetailAtom)
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom)
const activeConvoId = useAtomValue(getActiveConvoIdAtom) const activeConvoId = useAtomValue(getActiveConvoIdAtom)
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom) const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
@ -45,18 +39,16 @@ export default function useDeleteConversation() {
) )
setUserConversations(currentConversations) setUserConversations(currentConversations)
deleteMessages(activeConvoId) deleteMessages(activeConvoId)
setCurrentPrompt('')
toaster({ toaster({
title: 'Succes delete a chat', title: 'Chat successfully deleted.',
description: `Delete chat with ${activeModel?.name} has been completed`, description: `Chat with ${activeModel?.name} has been successfully deleted.`,
}) })
if (currentConversations.length > 0) { if (currentConversations.length > 0) {
setActiveConvoId(currentConversations[0].id) setActiveConvoId(currentConversations[0].id)
} else { } else {
setActiveConvoId(undefined) setActiveConvoId(undefined)
} }
setCurrentPrompt('')
setShowingProductDetail(false)
setShowingAdvancedPrompt(false)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }

View File

@ -20,8 +20,8 @@ export default function useDeleteModel() {
// reload models // reload models
setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id)) setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id))
toaster({ toaster({
title: 'Delete a Model', title: 'Model Deletion Successful',
description: `Model ${model.id} has been deleted.`, description: `The model ${model.id} has been successfully deleted.`,
}) })
} }

View File

@ -20,14 +20,13 @@ export function useGetConfiguredModels() {
const [models, setModels] = useState<ModelCatalog[]>([]) const [models, setModels] = useState<ModelCatalog[]>([])
async function getConfiguredModels(): Promise<ModelCatalog[]> { async function getConfiguredModels(): Promise<ModelCatalog[]> {
return ( const models = await pluginManager
((await pluginManager .get<ModelPlugin>(PluginType.Model)
.get<ModelPlugin>(PluginType.Model) ?.getConfiguredModels()
?.getConfiguredModels()) as ModelCatalog[]) ?? [] return models ?? []
)
} }
const fetchModels = async () => { async function fetchModels() {
setLoading(true) setLoading(true)
let models = await getConfiguredModels() let models = await getConfiguredModels()
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
@ -37,10 +36,8 @@ export function useGetConfiguredModels() {
setModels(models) setModels(models)
} }
// TODO allow user for filter
useEffect(() => { useEffect(() => {
fetchModels() fetchModels()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
return { loading, models } return { loading, models }

View File

@ -12,11 +12,10 @@ export function useGetDownloadedModels() {
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelAtom) const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelAtom)
async function getDownloadedModels(): Promise<Model[]> { async function getDownloadedModels(): Promise<Model[]> {
const models = const models = await pluginManager
((await pluginManager .get<ModelPlugin>(PluginType.Model)
.get<ModelPlugin>(PluginType.Model) ?.getDownloadedModels()
?.getDownloadedModels()) as Model[]) ?? [] return models ?? []
return models
} }
useEffect(() => { useEffect(() => {

View File

@ -10,15 +10,15 @@ import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
export default function useGetInputState() { export default function useGetInputState() {
const [inputState, setInputState] = useState<InputType>('loading') const [inputState, setInputState] = useState<InputType>('loading')
const currentConvo = useAtomValue(currentConversationAtom) const currentThread = useAtomValue(currentConversationAtom)
const { activeModel } = useActiveModel() const { activeModel } = useActiveModel()
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
const handleInputState = ( const handleInputState = (
convo: Thread | undefined, thread: Thread | undefined,
currentModel: Model | undefined currentModel: Model | undefined
) => { ) => {
if (convo == null) return if (thread == null) return
if (currentModel == null) { if (currentModel == null) {
setInputState('loading') setInputState('loading')
return return
@ -26,7 +26,7 @@ export default function useGetInputState() {
// check if convo model id is in downloaded models // check if convo model id is in downloaded models
const isModelAvailable = downloadedModels.some( const isModelAvailable = downloadedModels.some(
(model) => model.id === convo.modelId (model) => model.id === thread.modelId
) )
if (!isModelAvailable) { if (!isModelAvailable) {
@ -35,7 +35,7 @@ export default function useGetInputState() {
return return
} }
if (convo.modelId !== currentModel.id) { if (thread.modelId !== currentModel.id) {
// in case convo model and active model is different, // in case convo model and active model is different,
// ask user to init the required model // ask user to init the required model
setInputState('model-mismatch') setInputState('model-mismatch')
@ -46,11 +46,11 @@ export default function useGetInputState() {
} }
useEffect(() => { useEffect(() => {
handleInputState(currentConvo, activeModel) handleInputState(currentThread, activeModel)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
return { inputState, currentConvo } return { inputState, currentThread }
} }
type InputType = 'available' | 'loading' | 'model-mismatch' | 'model-not-found' type InputType = 'available' | 'loading' | 'model-mismatch' | 'model-not-found'

View File

@ -34,14 +34,15 @@ export default function useGetSystemResources() {
useEffect(() => { useEffect(() => {
getSystemResources() 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(() => { const intervalId = setInterval(() => {
getSystemResources() getSystemResources()
}, 5000) }, 5000)
// clean up // clean up interval
return () => clearInterval(intervalId) return () => clearInterval(intervalId)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
return { return {

View File

@ -16,6 +16,8 @@ import { ulid } from 'ulid'
import { currentPromptAtom } from '@/containers/Providers/Jotai' import { currentPromptAtom } from '@/containers/Providers/Jotai'
import { useActiveModel } from './useActiveModel'
import { import {
addNewMessageAtom, addNewMessageAtom,
getCurrentChatMessagesAtom, getCurrentChatMessagesAtom,
@ -34,54 +36,51 @@ export default function useSendChatMessage() {
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom) const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom) const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
const currentMessages = useAtomValue(getCurrentChatMessagesAtom) const currentMessages = useAtomValue(getCurrentChatMessagesAtom)
const { activeModel } = useActiveModel()
let timeout: NodeJS.Timeout | undefined = undefined
function updateConvSummary(newMessage: MessageRequest) { function updateConvSummary(newMessage: MessageRequest) {
if (timeout) { if (
clearTimeout(timeout) currentConvo &&
} newMessage.messages &&
timeout = setTimeout(() => { newMessage.messages.length > 2 &&
const conv = currentConvo (!currentConvo.summary ||
if (
!currentConvo?.summary ||
currentConvo.summary === '' || currentConvo.summary === '' ||
currentConvo.summary.startsWith('Prompt:') currentConvo.summary === activeModel?.name)
) { ) {
const summaryMsg: ChatCompletionMessage = { const summaryMsg: ChatCompletionMessage = {
role: ChatCompletionRole.User, role: ChatCompletionRole.User,
content: content:
'summary this conversation in 5 words, the response should just include the summary', 'summary this conversation in a few 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)
} }
}, 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 () => { const sendChatMessage = async () => {
@ -123,21 +122,7 @@ export default function useSendChatMessage() {
} }
addNewMessage(threadMessage) 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) events.emit(EventName.OnNewMessageRequest, messageRequest)
if (!currentConvo?.summary && currentConvo) {
const updatedConv: Thread = {
...currentConvo,
summary: `Prompt: ${prompt}`,
}
updateConversation(updatedConv)
}
updateConvSummary(messageRequest) updateConvSummary(messageRequest)
} }

View File

@ -58,10 +58,18 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
<div> <div>
<span className="mb-1 font-semibold">Compatibility</span> <span className="mb-1 font-semibold">Compatibility</span>
<div className="mt-1 flex gap-2"> <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} {usecase}
</Badge> </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 {toGigabytes(maxRamRequired)} RAM required
</Badge> </Badge>
</div> </div>

View File

@ -2,8 +2,8 @@
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types' import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
import { Button } from '@janhq/uikit' import { Button, Badge } from '@janhq/uikit'
import { Badge } from '@janhq/uikit'
import { atom, useAtomValue } from 'jotai' import { atom, useAtomValue } from 'jotai'
import ModalCancelDownload from '@/containers/ModalCancelDownload' import ModalCancelDownload from '@/containers/ModalCancelDownload'
@ -73,16 +73,25 @@ const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
return ( 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 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"> <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>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex justify-end gap-2"> <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} {usecase}
</Badge> </Badge>
<Badge themes="secondary" className="line-clamp-1 ">{`${toGigabytes(
maxRamRequired <Badge
)} RAM required`}</Badge> themes="secondary"
className="line-clamp-1"
title={`${toGigabytes(maxRamRequired)} RAM required`}
>{`${toGigabytes(maxRamRequired)} RAM required`}</Badge>
<Badge themes="secondary">{toGigabytes(modelVersion.size)}</Badge> <Badge themes="secondary">{toGigabytes(modelVersion.size)}</Badge>
</div> </div>
{downloadButton} {downloadButton}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
const API_BASE_PATH: string = '/api/v1' const API_BASE_PATH: string = '/api/v1'
@ -48,7 +47,7 @@ export async function fetchApi(
method: pluginFunc, method: pluginFunc,
args: args, args: args,
}), }),
headers: { 'Content-Type': 'application/json', 'Authorization': '' }, headers: { contentType: 'application/json', Authorization: '' },
}) })
if (!response.ok) { if (!response.ok) {

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/ban-types */
export class EventEmitter { export class EventEmitter {
private handlers: Map<string, Function[]> 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 { public emit(eventName: string, args: any): void {
if (!this.handlers.has(eventName)) { if (!this.handlers.has(eventName)) {
return return

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { ModelCatalog, ModelVersion } from '@janhq/core' import { ModelCatalog } from '@janhq/core'
export const dummyModel: ModelCatalog = { export const dummyModel: ModelCatalog = {
id: 'aladar/TinyLLama-v0-GGUF', id: 'aladar/TinyLLama-v0-GGUF',