Merge branch 'devhub' of https://github.com/janhq/jan into devhub
This commit is contained in:
commit
a619c115ac
6
.github/workflows/jan-plugins.yml
vendored
6
.github/workflows/jan-plugins.yml
vendored
@ -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
|
||||
|
||||
@ -6,6 +6,7 @@ export enum EventName {
|
||||
OnNewMessageRequest = "onNewMessageRequest",
|
||||
OnNewMessageResponse = "onNewMessageResponse",
|
||||
OnMessageResponseUpdate = "onMessageResponseUpdate",
|
||||
OnMessageResponseFinished = "OnMessageResponseFinished",
|
||||
OnDownloadUpdate = "onDownloadUpdate",
|
||||
OnDownloadSuccess = "onDownloadSuccess",
|
||||
OnDownloadError = "onDownloadError",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@janhq/core",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"description": "Plugin core lib",
|
||||
"keywords": [
|
||||
"jan",
|
||||
|
||||
@ -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...
|
||||
});
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -85,6 +85,7 @@ const registerListener = () => {
|
||||
events.on(EventName.OnNewMessageRequest, handleMessageRequest);
|
||||
};
|
||||
|
||||
// Preference update - reconfigure OpenAI
|
||||
const onPreferencesUpdate = () => {
|
||||
setup();
|
||||
};
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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!
|
||||
|
||||
|
||||
|
||||
@ -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": [
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 ?? ''}
|
||||
/>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
73
web/app/_components/ModalNoActiveModel/index.tsx
Normal file
73
web/app/_components/ModalNoActiveModel/index.tsx
Normal 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 don’t 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)
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
129
web/app/_components/SwitchingModelConfirmationModal/index.tsx
Normal file
129
web/app/_components/SwitchingModelConfirmationModal/index.tsx
Normal 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
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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" />,
|
||||
|
||||
@ -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}</>
|
||||
}
|
||||
|
||||
@ -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}
|
||||
</>
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
52
web/hooks/useGetInputState.ts
Normal file
52
web/hooks/useGetInputState.ts
Normal 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'
|
||||
23
web/hooks/useGetModelById.ts
Normal file
23
web/hooks/useGetModelById.ts
Normal 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
|
||||
}
|
||||
@ -29,7 +29,14 @@ const useGetUserConversations = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getConversationById = async (
|
||||
id: string
|
||||
): Promise<Conversation | undefined> => {
|
||||
return await executeSerial(DataService.GetConversationById, id)
|
||||
}
|
||||
|
||||
return {
|
||||
getConversationById,
|
||||
getUserConversations,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -61,6 +61,7 @@
|
||||
border-radius: 0.4rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
|
||||
@ -7,3 +7,4 @@
|
||||
@import './global.scss';
|
||||
@import './code-block.scss';
|
||||
@import './loader.scss';
|
||||
@import './message.scss';
|
||||
|
||||
7
web/styles/message.scss
Normal file
7
web/styles/message.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.message {
|
||||
ul,
|
||||
ol {
|
||||
list-style: auto;
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user