Merge branch 'main' into devhub

This commit is contained in:
0xSage 2023-10-27 16:23:16 +07:00 committed by GitHub
commit f3eb389fb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 690 additions and 203 deletions

View File

@ -88,7 +88,11 @@ jobs:
do
echo $dir
cd $dir
npm install && npm run postinstall && ../../.github/scripts/auto-sign.sh
npm install
if [[ $dir == 'data-plugin' ]]; then
npm run build:deps
fi
npm run postinstall && ../../.github/scripts/auto-sign.sh
if [[ $GITHUB_EVENT_NAME == 'push' && $GITHUB_EVENT_PULL_REQUEST_HEAD_REPO_FULL_NAME != $GITHUB_REPOSITORY ]]; then
npm publish --access public
fi

View File

@ -6,6 +6,7 @@ export enum EventName {
OnNewMessageRequest = "onNewMessageRequest",
OnNewMessageResponse = "onNewMessageResponse",
OnMessageResponseUpdate = "onMessageResponseUpdate",
OnMessageResponseFinished = "OnMessageResponseFinished",
OnDownloadUpdate = "onDownloadUpdate",
OnDownloadSuccess = "onDownloadSuccess",
OnDownloadError = "onDownloadError",

View File

@ -1,6 +1,6 @@
{
"name": "@janhq/core",
"version": "0.1.7",
"version": "0.1.8",
"description": "Plugin core lib",
"keywords": [
"jan",

View File

@ -36,11 +36,6 @@ test.afterAll(async () => {
test("shows my models", async () => {
await page.getByTestId("My Models").first().click();
const header = await page
.getByRole("heading")
.filter({ hasText: "My Models" })
.first()
.isVisible();
expect(header).toBe(false);
await page.getByTestId("testid-mymodels-header").isVisible();
// More test cases here...
});

View File

@ -10,9 +10,10 @@
"noEmitOnError": true,
"baseUrl": ".",
"allowJs": true,
"skipLibCheck": true,
"paths": { "*": ["node_modules/*"] },
"typeRoots": ["node_modules/@types"]
},
"include": ["./**/*.ts"],
"exclude": ["core", "build", "dist", "tests"]
"exclude": ["core", "build", "dist", "tests", "node_modules"]
}

View File

@ -24,7 +24,7 @@
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
"build:electron": "yarn workspace jan build",
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm install && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-management-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-management-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
"build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./plugins/data-plugin && npm install && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-management-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && chmod +x ./.github/scripts/auto-sign.sh && ./.github/scripts/auto-sign.sh && concurrently \"cd ./plugins/data-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-management-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
"build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./plugins/data-plugin && npm install && npm run build:deps && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-management-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && chmod +x ./.github/scripts/auto-sign.sh && ./.github/scripts/auto-sign.sh && concurrently \"cd ./plugins/data-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-management-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
"build": "yarn build:web && yarn build:electron",
"build:darwin": "yarn build:web && yarn workspace jan build:darwin",
"build:win32": "yarn build:web && yarn workspace jan build:win32",

View File

@ -6,3 +6,4 @@
- module.ts: Defines the plugin module which would be executed by the main node process.
- package.json: Defines the plugin metadata.
- tsconfig.json: Defines the typescript configuration.

View File

@ -1,6 +1,6 @@
{
"name": "@janhq/data-plugin",
"version": "1.0.14",
"version": "1.0.15",
"description": "The Data Connector provides easy access to a data API using the PouchDB engine. It offers accessible data management capabilities.",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
"main": "dist/esm/index.js",
@ -12,7 +12,8 @@
],
"scripts": {
"build": "tsc -b ./config/tsconfig.esm.json && tsc -b ./config/tsconfig.cjs.json && webpack --config webpack.config.js",
"postinstall": "electron-rebuild -f -w leveldown@5.6.0 --arch=arm64 -v 26.2.1 && rimraf *.tgz --glob && npm run build",
"build:deps": "electron-rebuild -f -w leveldown@5.6.0 --arch=arm64 -v 26.2.1 && electron-rebuild -f -w leveldown@5.6.0 --arch=x64 -v 26.2.1",
"postinstall": "rimraf *.tgz --glob && npm run build",
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
},
"exports": {
@ -30,10 +31,6 @@
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4"
},
"bundledDependencies": [
"pouchdb-node",
"pouchdb-find"
],
"files": [
"dist/**",
"package.json",

View File

@ -140,6 +140,8 @@ async function handleMessageRequest(data: NewMessageRequest) {
message.message = message.message.trim();
// TODO: Common collections should be able to access via core functions instead of store
await store.updateOne("messages", message._id, message);
events.emit("OnMessageResponseFinished", message);
// events.emit(EventName.OnMessageResponseFinished, message);
},
error: async (err) => {
message.message =

View File

@ -85,6 +85,7 @@ const registerListener = () => {
events.on(EventName.OnNewMessageRequest, handleMessageRequest);
};
// Preference update - reconfigure OpenAI
const onPreferencesUpdate = () => {
setup();
};

View File

@ -1,11 +1,12 @@
{
"name": "@janhq/azure-openai-plugin",
"version": "1.0.6",
"version": "1.0.7",
"description": "Inference plugin for Azure OpenAI",
"icon": "https://static-assets.jan.ai/openai-icon.jpg",
"main": "dist/index.js",
"module": "dist/module.js",
"author": "Jan <service@jan.ai>",
"requiredVersion": "^0.3.1",
"license": "MIT",
"activationPoints": [
"init"

View File

@ -72,3 +72,4 @@ There are a few things to keep in mind when writing your plugin code:
So, what are you waiting for? Go ahead and start customizing your plugin!

View File

@ -1,10 +1,11 @@
{
"name": "retrieval-plugin",
"version": "1.0.2",
"description": "Retrieval plugin for Jan app",
"version": "1.0.3",
"description": "Retrieval plugin for Jan app (experimental)",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
"main": "dist/index.js",
"module": "dist/module.js",
"requiredVersion": "^0.3.1",
"author": "Jan <service@jan.ai>",
"license": "MIT",
"activationPoints": [

View File

@ -1,7 +1,5 @@
import SimpleTag from '../SimpleTag'
import PrimaryButton from '../PrimaryButton'
import { formatDownloadPercentage, toGigabytes } from '@utils/converter'
import SecondaryButton from '../SecondaryButton'
import { useCallback, useEffect, useMemo } from 'react'
import useGetPerformanceTag from '@hooks/useGetPerformanceTag'
import useDownloadModel from '@hooks/useDownloadModel'
@ -58,7 +56,6 @@ const ExploreModelItemHeader: React.FC<Props> = ({
if (isDownloaded) {
downloadButton = (
<Button
size="sm"
themes="accent"
onClick={() => {
setMainViewState(MainViewState.MyModel)
@ -72,17 +69,20 @@ const ExploreModelItemHeader: React.FC<Props> = ({
if (downloadState != null) {
// downloading
downloadButton = (
<SecondaryButton
<Button
disabled
title={`Downloading (${formatDownloadPercentage(
downloadState.percent
)})`}
/>
themes="accent"
onClick={() => {
setMainViewState(MainViewState.MyModel)
}}
>
Downloading {formatDownloadPercentage(downloadState.percent)}
</Button>
)
}
return (
<div className="border-border bg-background/50 flex items-center justify-between rounded-t-md border-b px-4 py-2">
<div className="flex items-center justify-between rounded-t-md border-b border-border bg-background/50 px-4 py-2">
<div className="flex items-center gap-2">
<span>{exploreModel.name}</span>
{performanceTag && (

View File

@ -1,10 +1,8 @@
import React from 'react'
import { useAtomValue, useSetAtom } from 'jotai'
import { ModelManagementService } from '@janhq/core'
import {
getActiveConvoIdAtom,
setActiveConvoIdAtom,
updateConversationWaitingForResponseAtom,
} from '@helpers/atoms/Conversation.atom'
import {
setMainViewStateAtom,
@ -12,11 +10,12 @@ import {
} from '@helpers/atoms/MainView.atom'
import { displayDate } from '@utils/datetime'
import { twMerge } from 'tailwind-merge'
import { executeSerial } from '@services/pluginService'
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
import useStartStopModel from '@hooks/useStartStopModel'
import useGetModelById from '@hooks/useGetModelById'
type Props = {
conversation: Conversation
avatarUrl?: string
name: string
summary?: string
updatedAt?: string
@ -24,24 +23,38 @@ type Props = {
const HistoryItem: React.FC<Props> = ({
conversation,
avatarUrl,
name,
summary,
updatedAt,
}) => {
const setMainViewState = useSetAtom(setMainViewStateAtom)
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
const isSelected = activeConvoId === conversation._id
const activeModel = useAtomValue(activeAssistantModelAtom)
const { startModel } = useStartStopModel()
const { getModelById } = useGetModelById()
const setMainViewState = useSetAtom(setMainViewStateAtom)
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
const onClick = async () => {
const model = await executeSerial(
ModelManagementService.GetModelById,
conversation.modelId
)
if (conversation.modelId == null) {
console.debug('modelId is undefined')
return
}
if (conversation._id) updateConvWaiting(conversation._id, true)
const model = await getModelById(conversation.modelId)
if (model != null) {
if (activeModel == null) {
// if there's no active model, we simply load conversation's model
startModel(model._id)
} else if (activeModel._id !== model._id) {
// display confirmation modal
// TODO: temporarily disabled
// setConfirmationModalProps({
// replacingModel: model,
// })
}
}
if (activeConvoId !== conversation._id) {
setMainViewState(MainViewState.Conversation)

View File

@ -1,5 +1,5 @@
import HistoryItem from '../HistoryItem'
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import ExpandableHeader from '../ExpandableHeader'
import { useAtomValue } from 'jotai'
import { searchAtom } from '@helpers/JotaiWrapper'
@ -33,7 +33,6 @@ const HistoryList: React.FC = () => {
key={convo._id}
conversation={convo}
summary={convo.summary}
avatarUrl={convo.image}
name={convo.name || 'Jan'}
updatedAt={convo.updatedAt ?? ''}
/>

View File

@ -5,91 +5,45 @@ import BasicPromptInput from '../BasicPromptInput'
import BasicPromptAccessories from '../BasicPromptAccessories'
import { useAtomValue, useSetAtom } from 'jotai'
import SecondaryButton from '../SecondaryButton'
import { useEffect, useState } from 'react'
import { PlusIcon } from '@heroicons/react/24/outline'
import useCreateConversation from '@hooks/useCreateConversation'
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
import {
currentConversationAtom,
currentConvoStateAtom,
getActiveConvoIdAtom,
} from '@helpers/atoms/Conversation.atom'
import useGetBots from '@hooks/useGetBots'
import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import useGetInputState from '@hooks/useGetInputState'
import useStartStopModel from '@hooks/useStartStopModel'
import { userConversationsAtom } from '@helpers/atoms/Conversation.atom'
import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom'
const InputToolbar: React.FC = () => {
const activeModel = useAtomValue(activeAssistantModelAtom)
const { requestCreateConvo } = useCreateConversation()
const currentConvoState = useAtomValue(currentConvoStateAtom)
const currentConvo = useAtomValue(currentConversationAtom)
const setActiveBot = useSetAtom(activeBotAtom)
const { getBotById } = useGetBots()
const [inputState, setInputState] = useState<
'available' | 'disabled' | 'loading'
>()
const [error, setError] = useState<string | undefined>()
const { downloadedModels } = useGetDownloadedModels()
useEffect(() => {
const getReplyState = async () => {
setInputState('loading')
if (currentConvo && currentConvo.botId && currentConvo.botId.length > 0) {
// if botId is set, check if bot is available
const bot = await getBotById(currentConvo.botId)
console.debug('Found bot', JSON.stringify(bot, null, 2))
if (bot) {
setActiveBot(bot)
}
setInputState(bot ? 'available' : 'disabled')
setError(
bot
? undefined
: `Bot ${currentConvo.botId} has been deleted by its creator. Your chat history is saved but you won't be able to send new messages.`
)
} else {
const model = downloadedModels.find(
(model) => model._id === activeModel?._id
)
setInputState(model ? 'available' : 'disabled')
setError(
model
? undefined
: `Model ${activeModel?._id} cannot be found. Your chat history is saved but you won't be able to send new messages.`
)
}
}
getReplyState()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentConvo])
const { inputState, currentConvo } = useGetInputState()
const { requestCreateConvo } = useCreateConversation()
const { startModel } = useStartStopModel()
const { loading } = useAtomValue(stateModel)
const conversations = useAtomValue(userConversationsAtom)
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
const setShowModalNoActiveModel = useSetAtom(showingModalNoActiveModel)
const onNewConversationClick = () => {
if (activeModel) {
requestCreateConvo(activeModel)
} else {
setShowModalNoActiveModel(true)
}
}
if (inputState === 'loading') return <div>Loading..</div>
const onStartModelClick = () => {
const modelId = currentConvo?.modelId
if (!modelId) return
startModel(modelId)
}
if (inputState === 'disabled')
if (!activeConvoId) {
return (
<div className="sticky bottom-0 flex items-center justify-center bg-background/90">
<p className="mx-auto my-5 line-clamp-2 text-ellipsis text-center italic text-gray-600">
{error}
</p>
</div>
)
return (
<div className="sticky bottom-0 w-full bg-background/90 px-5 py-0">
{currentConvoState?.error && (
<div className="flex flex-row justify-center">
<span className="mx-5 my-2 text-sm text-red-500">
{currentConvoState?.error?.toString()}
</span>
</div>
)}
<div className="my-3 flex justify-center gap-2">
<SecondaryButton
onClick={onNewConversationClick}
@ -97,15 +51,65 @@ const InputToolbar: React.FC = () => {
icon={<PlusIcon width={16} height={16} />}
/>
</div>
{/* My text input */}
<div className="mb-5 flex items-start space-x-4">
<div className="relative min-w-0 flex-1">
<BasicPromptInput />
<BasicPromptAccessories />
)
}
if (
(activeConvoId && inputState === 'model-mismatch') ||
inputState === 'loading'
) {
// const message = inputState === 'loading' ? 'Loading..' : 'Model mismatch!'
return (
<div className="sticky bottom-0 flex items-center justify-center bg-background/90">
<div className="my-2">
{/* <p className="mx-auto my-5 line-clamp-2 text-ellipsis text-center italic text-gray-600">
{message}
</p> */}
<SecondaryButton
onClick={onStartModelClick}
title={`Start model ${currentConvo?.modelId}`}
/>
</div>
</div>
</div>
)
)
}
if (inputState === 'model-not-found') {
return (
<div className="sticky bottom-0 flex items-center justify-center bg-background/90">
<p className="mx-auto my-5 line-clamp-2 text-ellipsis text-center italic text-gray-600">
Model {currentConvo?.modelId} not found! Please re-download the model
first.
</p>
</div>
)
}
if (conversations.length > 0)
return (
<div className="sticky bottom-0 w-full bg-background/90 px-5 py-0">
{currentConvoState?.error && (
<div className="flex flex-row justify-center">
<span className="mx-5 my-2 text-sm text-red-500">
{currentConvoState?.error?.toString()}
</span>
</div>
)}
<div className="my-3 flex justify-center gap-2">
<SecondaryButton
onClick={onNewConversationClick}
title="New Conversation"
icon={<PlusIcon width={16} height={16} />}
/>
</div>
{/* My text input */}
<div className="mb-5 flex items-start space-x-4">
<div className="relative min-w-0 flex-1">
<BasicPromptInput />
<BasicPromptAccessories />
</div>
</div>
</div>
)
}
export default InputToolbar

View File

@ -34,12 +34,12 @@ const LeftHeaderAction: React.FC = () => {
className="flex-1"
icon={<MagnifyingGlassIcon width={16} height={16} />}
/>
<SecondaryButton
{/* <SecondaryButton
title={'Create bot'}
onClick={onCreateBotClicked}
className="flex-1"
icon={<PlusIcon width={16} height={16} />}
/>
/> */}
</div>
)
}

View File

@ -0,0 +1,73 @@
import React, { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { useAtom, useSetAtom } from 'jotai'
import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom'
import {
MainViewState,
setMainViewStateAtom,
} from '@helpers/atoms/MainView.atom'
const ModalNoActiveModel: React.FC = () => {
const [show, setShow] = useAtom(showingModalNoActiveModel)
const setMainView = useSetAtom(setMainViewStateAtom)
return (
<Transition.Root show={show} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={setShow}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-40 h-full bg-gray-950/90 transition-opacity dark:backdrop-blur-sm" />
</Transition.Child>
<div className="fixed inset-0 z-50 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-border bg-background/90 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<h1 className="font-base mb-4 font-bold">
You dont have any actively running models. Please start a
downloaded model in My Models page to use this feature.
</h1>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
className="inline-flex w-full justify-center rounded-md bg-accent px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-accent/80 sm:ml-3 sm:w-auto"
onClick={() => {
setMainView(MainViewState.MyModel)
setShow(false)
}}
>
Ok
</button>
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
onClick={() => setShow(false)}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default React.memo(ModalNoActiveModel)

View File

@ -10,7 +10,7 @@ const tableHeaders = ['MODEL', 'FORMAT', 'SIZE', 'STATUS', 'ACTIONS']
const ModelTable: React.FC<Props> = ({ models }) => (
<>
<div className="border-border overflow-hidden rounded-lg border align-middle shadow-lg">
<div className="overflow-hidden rounded-lg border border-border align-middle shadow-lg">
<table className="min-w-full">
<thead className="bg-background">
<tr className="rounded-t-lg">

View File

@ -9,6 +9,7 @@ import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import { Button } from '@uikit'
import { MessageCircle } from 'lucide-react'
import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom'
enum ActionButton {
DownloadModel = 'Download a Model',
@ -21,6 +22,7 @@ const SidebarEmptyHistory: React.FC = () => {
const setMainView = useSetAtom(setMainViewStateAtom)
const { requestCreateConvo } = useCreateConversation()
const [action, setAction] = useState(ActionButton.DownloadModel)
const modalNoActiveModel = useSetAtom(showingModalNoActiveModel)
useEffect(() => {
if (downloadedModels.length > 0) {
@ -35,7 +37,7 @@ const SidebarEmptyHistory: React.FC = () => {
setMainView(MainViewState.ExploreModel)
} else {
if (!activeModel) {
setMainView(MainViewState.ConversationEmptyModel)
modalNoActiveModel(true)
} else {
await requestCreateConvo(activeModel)
}
@ -44,10 +46,10 @@ const SidebarEmptyHistory: React.FC = () => {
return (
<div className="flex flex-col items-center gap-3 py-10">
<MessageCircle size={32} />
<div className="flex flex-col items-center gap-y-2">
<MessageCircle size={24} />
<div className="flex flex-col items-center">
<h6 className="text-center text-base">No Chat History</h6>
<p className="mb-6 text-center text-muted-foreground">
<p className="mb-6 mt-1 text-center text-muted-foreground">
Get started by creating a new chat.
</p>
<Button onClick={onClick} themes="accent">

View File

@ -51,7 +51,7 @@ const SimpleTextMessage: React.FC<Props> = ({
return (
<div
className={`border-border/50 flex items-start gap-x-4 gap-y-2 border-b px-4 py-5 last:border-none`}
className={`flex items-start gap-x-4 gap-y-2 border-b border-border/50 px-4 py-5 last:border-none`}
>
<Image
className="rounded-full"
@ -73,7 +73,7 @@ const SimpleTextMessage: React.FC<Props> = ({
<LoadingIndicator />
) : (
<span
className="text-muted-foreground text-xs font-normal leading-loose"
className="message text-xs font-normal leading-loose text-muted-foreground"
dangerouslySetInnerHTML={{ __html: parsedText }}
/>
)}

View File

@ -0,0 +1,129 @@
import React, { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { ExclamationTriangleIcon, XMarkIcon } from '@heroicons/react/24/outline'
import { switchingModelConfirmationModalPropsAtom } from '@helpers/atoms/Modal.atom'
import { useAtom, useAtomValue } from 'jotai'
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
import useStartStopModel from '@hooks/useStartStopModel'
export type SwitchingModelConfirmationModalProps = {
replacingModel: AssistantModel
}
const SwitchingModelConfirmationModal: React.FC = () => {
const [props, setProps] = useAtom(switchingModelConfirmationModalPropsAtom)
const activeModel = useAtomValue(activeAssistantModelAtom)
const { startModel } = useStartStopModel()
const onConfirmSwitchModelClick = () => {
const modelId = props?.replacingModel._id
if (modelId) {
startModel(modelId)
}
setProps(undefined)
}
return (
<Transition.Root show={props != null} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={() => setProps(undefined)}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div className="absolute right-0 top-0 hidden pr-4 pt-4 sm:block">
<button
type="button"
className="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => setProps(undefined)}
>
<span className="sr-only">Close</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationTriangleIcon
className="h-6 w-6 text-red-600"
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title
as="h3"
className="text-base font-semibold leading-6 text-gray-900"
>
Switching model
</Dialog.Title>
<div className="mt-2 flex flex-col">
<p className="text-sm text-gray-500">
Selected conversation is using model{' '}
<span className="font-semibold text-black">
{props?.replacingModel.name}
</span>
, but the active model is using{' '}
<span className="font-semibold text-black">
{activeModel?.name}
</span>
.
</p>
<br />
<p className="text-sm text-gray-500">
Switch to
<span className="font-semibold text-black">
{' '}
{props?.replacingModel.name}?
</span>
</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
onClick={onConfirmSwitchModelClick}
>
Switch
</button>
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
onClick={() => setProps(undefined)}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default SwitchingModelConfirmationModal

View File

@ -19,8 +19,6 @@ const BottomBar = () => {
downloadStates.push(value)
}
console.log(stateModelStartStop)
return (
<div className="fixed bottom-0 left-0 z-20 flex h-8 w-full items-center justify-between border-t border-border bg-background/50 px-4">
<div className="flex gap-x-2">
@ -53,7 +51,7 @@ const BottomBar = () => {
<div className="flex gap-x-2">
<SystemItem name="CPU:" value={`${cpu}%`} />
<SystemItem name="Mem:" value={`${ram}%`} />
<p className="text-xs font-semibold">Jan {appVersion?.version ?? ''}</p>
<p className="text-xs font-semibold">Jan v{appVersion?.version ?? ''}</p>
</div>
</div>
)

View File

@ -69,17 +69,17 @@ const Providers = (props: PropsWithChildren) => {
return (
<JotaiWrapper>
{setupCore && (
<EventListenerWrapper>
<ThemeWrapper>
{activated ? (
<ThemeWrapper>
{activated ? (
<EventListenerWrapper>
<ModalWrapper>{children}</ModalWrapper>
) : (
<div className="bg-background flex h-screen w-screen items-center justify-center">
<CompactLogo width={56} height={56} />
</div>
)}
</ThemeWrapper>
</EventListenerWrapper>
</EventListenerWrapper>
) : (
<div className="flex h-screen w-screen items-center justify-center bg-background">
<CompactLogo width={56} height={56} />
</div>
)}
</ThemeWrapper>
)}
</JotaiWrapper>
)

View File

@ -88,11 +88,11 @@ export const SidebarLeft = () => {
icon: <LayoutGrid size={20} className="flex-shrink-0" />,
state: MainViewState.MyModel,
},
{
name: 'Bot',
icon: <Bot size={20} className="flex-shrink-0" />,
state: MainViewState.CreateBot,
},
// {
// name: 'Bot',
// icon: <Bot size={20} className="flex-shrink-0" />,
// state: MainViewState.CreateBot,
// },
{
name: 'Settings',
icon: <Settings size={20} className="flex-shrink-0" />,

View File

@ -1,16 +1,49 @@
import { addNewMessageAtom, updateMessageAtom } from './atoms/ChatMessage.atom'
import { toChatMessage } from '@models/ChatMessage'
import { events, EventName, NewMessageResponse } from '@janhq/core'
import { events, EventName, NewMessageResponse, DataService } from '@janhq/core'
import { useSetAtom } from 'jotai'
import { ReactNode, useEffect } from 'react'
import useGetBots from '@hooks/useGetBots'
import useGetUserConversations from '@hooks/useGetUserConversations'
import {
updateConversationAtom,
updateConversationWaitingForResponseAtom,
} from './atoms/Conversation.atom'
import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager'
import { debounce } from 'lodash'
let currentConversation: Conversation | undefined = undefined
const debouncedUpdateConversation = debounce(
async (updatedConv: Conversation) => {
await executeSerial(DataService.UpdateConversation, updatedConv)
},
1000
)
export default function EventHandler({ children }: { children: ReactNode }) {
const addNewMessage = useSetAtom(addNewMessageAtom)
const updateMessage = useSetAtom(updateMessageAtom)
const updateConversation = useSetAtom(updateConversationAtom)
const { getBotById } = useGetBots()
const { getConversationById } = useGetUserConversations()
function handleNewMessageResponse(message: NewMessageResponse) {
const newResponse = toChatMessage(message)
addNewMessage(newResponse)
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
async function handleNewMessageResponse(message: NewMessageResponse) {
if (message.conversationId) {
const convo = await getConversationById(message.conversationId)
const botId = convo?.botId
console.debug('botId', botId)
if (botId) {
const bot = await getBotById(botId)
const newResponse = toChatMessage(message, bot)
addNewMessage(newResponse)
} else {
const newResponse = toChatMessage(message)
addNewMessage(newResponse)
}
}
}
async function handleMessageResponseUpdate(
messageResponse: NewMessageResponse
@ -19,18 +52,51 @@ export default function EventHandler({ children }: { children: ReactNode }) {
messageResponse.conversationId &&
messageResponse._id &&
messageResponse.message
)
) {
updateMessage(
messageResponse._id,
messageResponse.conversationId,
messageResponse.message
)
}
if (messageResponse.conversationId) {
if (
!currentConversation ||
currentConversation._id !== messageResponse.conversationId
) {
currentConversation = await getConversationById(
messageResponse.conversationId
)
}
const updatedConv: Conversation = {
...currentConversation,
lastMessage: messageResponse.message,
}
updateConversation(updatedConv)
debouncedUpdateConversation(updatedConv)
}
}
async function handleMessageResponseFinished(
messageResponse: NewMessageResponse
) {
if (!messageResponse.conversationId) return
console.debug('handleMessageResponseFinished', messageResponse)
updateConvWaiting(messageResponse.conversationId, false)
}
useEffect(() => {
if (window.corePlugin.events) {
events.on(EventName.OnNewMessageResponse, handleNewMessageResponse)
events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
events.on(
"OnMessageResponseFinished",
// EventName.OnMessageResponseFinished,
handleMessageResponseFinished
)
}
}, [])
@ -38,7 +104,12 @@ export default function EventHandler({ children }: { children: ReactNode }) {
return () => {
events.off(EventName.OnNewMessageResponse, handleNewMessageResponse)
events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
events.off(
"OnMessageResponseFinished",
// EventName.OnMessageResponseFinished,
handleMessageResponseFinished
)
}
}, [])
return <> {children}</>
return <>{children}</>
}

View File

@ -5,6 +5,8 @@ import ConfirmDeleteConversationModal from '@/_components/ConfirmDeleteConversat
import ConfirmDeleteModelModal from '@/_components/ConfirmDeleteModelModal'
import ConfirmSignOutModal from '@/_components/ConfirmSignOutModal'
import MobileMenuPane from '@/_components/MobileMenuPane'
import SwitchingModelConfirmationModal from '@/_components/SwitchingModelConfirmationModal'
import ModalNoActiveModel from '@/_components/ModalNoActiveModel'
import { ReactNode } from 'react'
type Props = {
@ -18,6 +20,8 @@ export const ModalWrapper: React.FC<Props> = ({ children }) => (
<ConfirmSignOutModal />
<ConfirmDeleteModelModal />
<BotListModal />
<SwitchingModelConfirmationModal />
<ModalNoActiveModel />
{children}
</>
)

View File

@ -1,3 +1,4 @@
import { SwitchingModelConfirmationModalProps } from '@/_components/SwitchingModelConfirmationModal'
import { atom } from 'jotai'
export const showConfirmDeleteConversationModalAtom = atom(false)
@ -7,3 +8,8 @@ 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 switchingModelConfirmationModalPropsAtom = atom<
SwitchingModelConfirmationModalProps | undefined
>(undefined)
export const showingModalNoActiveModel = atom<boolean>(false)

View File

@ -3,11 +3,15 @@ import { executeSerial } from '@services/pluginService'
import { useAtomValue, useSetAtom } from 'jotai'
import { useEffect } from 'react'
import { DataService } from '@janhq/core'
import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom'
import {
getActiveConvoIdAtom,
userConversationsAtom,
} from '@helpers/atoms/Conversation.atom'
import {
getCurrentChatMessagesAtom,
setCurrentChatMessagesAtom,
} from '@helpers/atoms/ChatMessage.atom'
import useGetBots from './useGetBots'
/**
* Custom hooks to get chat messages for current(active) conversation
@ -16,6 +20,8 @@ const useChatMessages = () => {
const setMessages = useSetAtom(setCurrentChatMessagesAtom)
const messages = useAtomValue(getCurrentChatMessagesAtom)
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
const userConversations = useAtomValue(userConversationsAtom)
const { getBotById } = useGetBots()
const getMessages = async (convoId: string) => {
const data: any = await executeSerial(
@ -26,6 +32,12 @@ const useChatMessages = () => {
return []
}
const convo = userConversations.find((c) => c._id === convoId)
if (convo && convo.botId) {
const bot = await getBotById(convo.botId)
return parseMessages(data, bot)
}
return parseMessages(data)
}
@ -47,10 +59,10 @@ const useChatMessages = () => {
return { messages }
}
function parseMessages(messages: RawMessage[]): ChatMessage[] {
function parseMessages(messages: RawMessage[], bot?: Bot): ChatMessage[] {
const newMessages: ChatMessage[] = []
for (const m of messages) {
const chatMessage = toChatMessage(m)
const chatMessage = toChatMessage(m, bot)
newMessages.push(chatMessage)
}
return newMessages

View File

@ -6,19 +6,18 @@ import {
setActiveConvoIdAtom,
addNewConversationStateAtom,
} from '@helpers/atoms/Conversation.atom'
import useGetModelById from './useGetModelById'
const useCreateConversation = () => {
const [userConversations, setUserConversations] = useAtom(
userConversationsAtom
)
const { getModelById } = useGetModelById()
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
const addNewConvoState = useSetAtom(addNewConversationStateAtom)
const createConvoByBot = async (bot: Bot) => {
const model = await executeSerial(
ModelManagementService.GetModelById,
bot.modelId
)
const model = await getModelById(bot.modelId)
if (!model) {
alert(

View File

@ -1,7 +1,11 @@
import { executeSerial } from '@services/pluginService'
import { DataService, ModelManagementService } from '@janhq/core'
import { ModelManagementService } from '@janhq/core'
import { useSetAtom } from 'jotai'
import { setDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
export default function useDownloadModel() {
const setDownloadState = useSetAtom(setDownloadStateAtom)
const assistanModel = (
model: Product,
modelVersion: ModelVersion
@ -37,6 +41,22 @@ export default function useDownloadModel() {
}
const downloadModel = async (model: Product, modelVersion: ModelVersion) => {
// set an initial download state
setDownloadState({
modelId: modelVersion._id,
time: {
elapsed: 0,
remaining: 0,
},
speed: 0,
percent: 0,
size: {
total: 0,
transferred: 0,
},
fileName: modelVersion._id,
})
modelVersion.startDownloadAt = Date.now()
const assistantModel = assistanModel(model, modelVersion)
await executeSerial(ModelManagementService.StoreModel, assistantModel)

View File

@ -0,0 +1,52 @@
import { currentConversationAtom } from '@helpers/atoms/Conversation.atom'
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
import { useAtomValue } from 'jotai'
import { useEffect, useState } from 'react'
import { useGetDownloadedModels } from './useGetDownloadedModels'
export default function useGetInputState() {
const [inputState, setInputState] = useState<InputType>('loading')
const currentConvo = useAtomValue(currentConversationAtom)
const activeModel = useAtomValue(activeAssistantModelAtom)
const { downloadedModels } = useGetDownloadedModels()
const handleInputState = (
convo: Conversation | undefined,
currentModel: AssistantModel | undefined,
models: AssistantModel[]
) => {
if (convo == null) return
if (currentModel == null) {
setInputState('loading')
return
}
// check if convo model id is in downloaded models
const isModelAvailable = downloadedModels.some(
(model) => model._id === convo.modelId
)
if (!isModelAvailable) {
// can't find model in downloaded models
setInputState('model-not-found')
return
}
if (convo.modelId !== currentModel._id) {
// in case convo model and active model is different,
// ask user to init the required model
setInputState('model-mismatch')
return
}
setInputState('available')
}
useEffect(() => {
handleInputState(currentConvo, activeModel, downloadedModels)
}, [currentConvo, activeModel, downloadedModels])
return { inputState, currentConvo }
}
type InputType = 'available' | 'loading' | 'model-mismatch' | 'model-not-found'

View File

@ -0,0 +1,23 @@
import { ModelManagementService } from '@janhq/core'
import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager'
export default function useGetModelById() {
const getModelById = async (
modelId: string
): Promise<AssistantModel | undefined> => {
return queryModelById(modelId)
}
return { getModelById }
}
const queryModelById = async (
modelId: string
): Promise<AssistantModel | undefined> => {
const model = await executeSerial(
ModelManagementService.GetModelById,
modelId
)
return model
}

View File

@ -29,7 +29,14 @@ const useGetUserConversations = () => {
}
}
const getConversationById = async (
id: string
): Promise<Conversation | undefined> => {
return await executeSerial(DataService.GetConversationById, id)
}
return {
getConversationById,
getUserConversations,
}
}

View File

@ -13,13 +13,14 @@ import { addNewMessageAtom } from '@helpers/atoms/ChatMessage.atom'
import {
currentConversationAtom,
updateConversationAtom,
updateConversationWaitingForResponseAtom,
} from '@helpers/atoms/Conversation.atom'
export default function useSendChatMessage() {
const currentConvo = useAtomValue(currentConversationAtom)
const addNewMessage = useSetAtom(addNewMessageAtom)
const updateConversation = useSetAtom(updateConversationAtom)
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
let timeout: any | undefined = undefined
@ -33,18 +34,20 @@ export default function useSendChatMessage() {
if (
!currentConvo?.summary ||
currentConvo.summary === '' ||
currentConvo.summary.startsWith('User request:')
currentConvo.summary.startsWith('Prompt:')
) {
// Request convo summary
setTimeout(async () => {
newMessage.message = 'summary this conversation in 5 words'
newMessage.message =
'summary this conversation in 5 words, the response should just include the summary'
const result = await executeSerial(
InferenceService.InferenceRequest,
newMessage
)
if (
result?.message &&
result.message.split(' ').length <= 7 &&
result.message.split(' ').length <= 10 &&
conv?._id
) {
const updatedConv = {
@ -60,10 +63,15 @@ export default function useSendChatMessage() {
}
const sendChatMessage = async () => {
const convoId = currentConvo?._id
if (!convoId) return
setCurrentPrompt('')
updateConvWaiting(convoId, true)
const prompt = currentPrompt.trim()
const newMessage: RawMessage = {
conversationId: currentConvo?._id,
conversationId: convoId,
message: prompt,
user: 'user',
createdAt: new Date().toISOString(),
@ -77,10 +85,20 @@ export default function useSendChatMessage() {
events.emit(EventName.OnNewMessageRequest, newMessage)
if (!currentConvo?.summary && currentConvo) {
const updatedConv = {
const updatedConv: Conversation = {
...currentConvo,
lastMessage: prompt,
summary: `Prompt: ${prompt}`,
}
updateConversation(updatedConv)
await executeSerial(DataService.UpdateConversation, updatedConv)
} else {
const updatedConv: Conversation = {
...currentConvo,
lastMessage: prompt,
}
updateConversation(updatedConv)
await executeSerial(DataService.UpdateConversation, updatedConv)
}

View File

@ -1,11 +1,12 @@
import { executeSerial } from '@services/pluginService'
import { ModelManagementService, InferenceService } from '@janhq/core'
import { InferenceService } from '@janhq/core'
import { useAtom, useSetAtom } from 'jotai'
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
import { useState } from 'react'
import useGetModelById from './useGetModelById'
export default function useStartStopModel() {
const [activeModel, setActiveModel] = useAtom(activeAssistantModelAtom)
const { getModelById } = useGetModelById()
const setStateModel = useSetAtom(stateModel)
const startModel = async (modelId: string) => {
@ -16,25 +17,27 @@ export default function useStartStopModel() {
setStateModel({ state: 'start', loading: true, model: modelId })
const model = await executeSerial(
ModelManagementService.GetModelById,
modelId
)
const model = await getModelById(modelId)
if (!model) {
alert(`Model ${modelId} not found! Please re-download the model first.`)
setStateModel((prev) => ({ ...prev, loading: false }))
return
}
const currentTime = Date.now()
console.debug('Init model: ', model._id)
const res = await executeSerial(InferenceService.InitModel, model._id)
const res = await initModel(model._id)
if (res?.error) {
const errorMessage = `Failed to init model: ${res.error}`
console.error(errorMessage)
alert(errorMessage)
} else {
console.debug(
`Init model successfully!, take ${Date.now() - currentTime}ms`
`Init model ${modelId} successfully!, take ${
Date.now() - currentTime
}ms`
)
setActiveModel(model)
}
@ -52,3 +55,7 @@ export default function useStartStopModel() {
return { startModel, stopModel }
}
const initModel = async (modelId: string): Promise<any> => {
return executeSerial(InferenceService.InitModel, modelId)
}

View File

@ -41,7 +41,8 @@ export interface RawMessage {
}
export const toChatMessage = (
m: RawMessage | NewMessageResponse
m: RawMessage | NewMessageResponse,
bot?: Bot
): ChatMessage => {
const createdAt = new Date(m.createdAt ?? '').getTime()
const imageUrls: string[] = []
@ -56,18 +57,18 @@ export const toChatMessage = (
const content = m.message ?? ''
let senderName = m.user === 'user' ? 'You' : 'Assistant'
if (senderName === 'Assistant' && bot) {
senderName = bot.name
}
return {
id: (m._id ?? 0).toString(),
conversationId: (m.conversationId ?? 0).toString(),
messageType: messageType,
messageSenderType: messageSenderType,
senderUid: m.user?.toString() || '0',
senderName:
m.user === 'user'
? 'You'
: m.user && m.user !== 'ai' && m.user !== 'assistant'
? m.user
: 'Assistant',
senderName: senderName,
senderAvatarUrl: m.avatar
? m.avatar
: m.user === 'user'

View File

@ -28,10 +28,10 @@
"eslint-config-next": "13.4.10",
"framer-motion": "^10.16.4",
"highlight.js": "^11.9.0",
"react-intersection-observer": "^9.5.2",
"jotai": "^2.4.0",
"jotai-optics": "^0.3.1",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"lucide-react": "^0.288.0",
"marked": "^9.1.2",
"marked-highlight": "^2.0.6",
@ -42,6 +42,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.45.4",
"react-intersection-observer": "^9.5.2",
"sass": "^1.69.4",
"tailwind-merge": "^1.14.0",
"tailwindcss": "3.3.3",
@ -50,6 +51,7 @@
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.4",
"@types/lodash": "^4.14.200",
"@types/node": "20.6.5",
"@types/uuid": "^9.0.6",
"encoding": "^0.1.13",

View File

@ -9,13 +9,13 @@ import LeftHeaderAction from '@/_components/LeftHeaderAction'
const ChatScreen = () => {
return (
<div className="flex h-full">
<div className="border-border flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r ">
<div className="flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r border-border ">
<div className="px-4 py-6 pt-4">
<LeftHeaderAction />
<HistoryList />
</div>
</div>
<div className="bg-background/50 relative flex h-full w-full flex-col">
<div className="relative flex h-full w-full flex-col bg-background/50">
<MainHeader />
<ChatBody />
<InputToolbar />

View File

@ -65,7 +65,7 @@ const MyModelsScreen = () => {
return (
<div className="flex h-full w-full overflow-y-auto">
<div className="w-full p-5">
<h1 className="text-lg font-semibold">My Models</h1>
<h1 data-testid="testid-mymodels-header" className="text-lg font-semibold">My Models</h1>
<p className="mt-2 text-gray-600 dark:text-gray-400">
You have <span>{downloadedModels.length}</span> models downloaded
</p>

View File

@ -11,6 +11,7 @@ import {
} from '@/../../electron/core/plugin-manager/execution/index'
import { executeSerial } from '@services/pluginService'
import { DataService } from '@janhq/core'
import useGetAppVersion from '@hooks/useGetAppVersion'
const PluginCatalog = () => {
// const [search, setSearch] = useState<string>('')
@ -20,23 +21,41 @@ const PluginCatalog = () => {
const [isLoading, setIsLoading] = useState<boolean>(false)
const experimentRef = useRef(null)
const fileInputRef = useRef<HTMLInputElement | null>(null)
const { version } = useGetAppVersion()
/**
* Loads the plugin catalog module from a CDN and sets it as the plugin catalog state.
*/
useEffect(() => {
if (!version) return
// Load plugin manifest from plugin if any
if (extensionPoints.get(DataService.GetPluginManifest)) {
executeSerial(DataService.GetPluginManifest).then((data) => {
setPluginCatalog(data)
setPluginCatalog(
data.filter(
(e: any) =>
!e.requiredVersion ||
e.requiredVersion.replace(/[.^]/g, '') <=
version.replaceAll('.', '')
)
)
})
} else {
// Fallback to app default manifest
import(
/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`
).then((data) => setPluginCatalog(data.default))
).then((data) =>
setPluginCatalog(
data.default.filter(
(e: any) =>
!e.requiredVersion ||
e.requiredVersion.replace(/[.^]/g, '') <=
version.replaceAll('.', '')
)
)
)
}
}, [])
}, [version])
/**
* Fetches the active plugins and their preferences from the `plugins` and `preferences` modules.
@ -141,11 +160,16 @@ const PluginCatalog = () => {
)
.map((item, i) => {
const isActivePlugin = activePlugins.some((x) => x.name === item.name)
const installedPlugin = activePlugins.filter(
(p) => p.name === item.name
)[0]
const updateVersionPlugins = Number(
activePlugins
.filter((p) => p.name === item.name)[0]
?.version.replaceAll('.', '')
installedPlugin?.version.replaceAll('.', '')
)
const hasUpdateVersionPlugins =
item.version.replaceAll('.', '') > updateVersionPlugins
return (
<div
key={i}
@ -163,16 +187,23 @@ const PluginCatalog = () => {
<p className="whitespace-pre-wrap leading-relaxed text-gray-600 dark:text-gray-400">
{item.description}
</p>
{isActivePlugin &&
item.version.replaceAll('.', '') < updateVersionPlugins && (
<Button
size="sm"
themes="outline"
onClick={() => downloadTarball(item.name)}
>
Update
</Button>
)}
{isActivePlugin && (
<p className="whitespace-pre-wrap leading-relaxed text-gray-600 dark:text-gray-400">
Installed{' '}
{hasUpdateVersionPlugins
? `v${installedPlugin.version}`
: 'the latest version'}
</p>
)}
{isActivePlugin && hasUpdateVersionPlugins && (
<Button
size="sm"
themes="outline"
onClick={() => downloadTarball(item.name)}
>
Update
</Button>
)}
</div>
<Switch
checked={isActivePlugin}

View File

@ -126,9 +126,11 @@ const SettingsScreen = () => {
</div>
<div className="mt-5 flex-shrink-0">
<label className="font-bold uppercase text-gray-500">
Core plugins
</label>
{preferencePlugins.length > 0 && (
<label className="font-bold uppercase text-gray-500">
Core plugins
</label>
)}
<div className="mt-1 font-semibold">
{preferencePlugins.map((menu, i) => {
const isActive = activePreferencePlugin === menu

View File

@ -61,6 +61,7 @@
border-radius: 0.4rem;
margin-top: 1rem;
margin-bottom: 1rem;
white-space: pre-wrap;
}
.hljs-emphasis {

View File

@ -7,3 +7,4 @@
@import './global.scss';
@import './code-block.scss';
@import './loader.scss';
@import './message.scss';

7
web/styles/message.scss Normal file
View File

@ -0,0 +1,7 @@
.message {
ul,
ol {
list-style: auto;
padding-left: 24px;
}
}