Merge branch 'main' into devhub
This commit is contained in:
commit
f3eb389fb3
6
.github/workflows/jan-plugins.yml
vendored
6
.github/workflows/jan-plugins.yml
vendored
@ -88,7 +88,11 @@ jobs:
|
|||||||
do
|
do
|
||||||
echo $dir
|
echo $dir
|
||||||
cd $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
|
if [[ $GITHUB_EVENT_NAME == 'push' && $GITHUB_EVENT_PULL_REQUEST_HEAD_REPO_FULL_NAME != $GITHUB_REPOSITORY ]]; then
|
||||||
npm publish --access public
|
npm publish --access public
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export enum EventName {
|
|||||||
OnNewMessageRequest = "onNewMessageRequest",
|
OnNewMessageRequest = "onNewMessageRequest",
|
||||||
OnNewMessageResponse = "onNewMessageResponse",
|
OnNewMessageResponse = "onNewMessageResponse",
|
||||||
OnMessageResponseUpdate = "onMessageResponseUpdate",
|
OnMessageResponseUpdate = "onMessageResponseUpdate",
|
||||||
|
OnMessageResponseFinished = "OnMessageResponseFinished",
|
||||||
OnDownloadUpdate = "onDownloadUpdate",
|
OnDownloadUpdate = "onDownloadUpdate",
|
||||||
OnDownloadSuccess = "onDownloadSuccess",
|
OnDownloadSuccess = "onDownloadSuccess",
|
||||||
OnDownloadError = "onDownloadError",
|
OnDownloadError = "onDownloadError",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/core",
|
"name": "@janhq/core",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"description": "Plugin core lib",
|
"description": "Plugin core lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"jan",
|
"jan",
|
||||||
|
|||||||
@ -36,11 +36,6 @@ test.afterAll(async () => {
|
|||||||
|
|
||||||
test("shows my models", async () => {
|
test("shows my models", async () => {
|
||||||
await page.getByTestId("My Models").first().click();
|
await page.getByTestId("My Models").first().click();
|
||||||
const header = await page
|
await page.getByTestId("testid-mymodels-header").isVisible();
|
||||||
.getByRole("heading")
|
|
||||||
.filter({ hasText: "My Models" })
|
|
||||||
.first()
|
|
||||||
.isVisible();
|
|
||||||
expect(header).toBe(false);
|
|
||||||
// More test cases here...
|
// More test cases here...
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,9 +10,10 @@
|
|||||||
"noEmitOnError": true,
|
"noEmitOnError": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
"paths": { "*": ["node_modules/*"] },
|
"paths": { "*": ["node_modules/*"] },
|
||||||
"typeRoots": ["node_modules/@types"]
|
"typeRoots": ["node_modules/@types"]
|
||||||
},
|
},
|
||||||
"include": ["./**/*.ts"],
|
"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:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||||
"build:electron": "yarn workspace jan build",
|
"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": "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": "yarn build:web && yarn build:electron",
|
||||||
"build:darwin": "yarn build:web && yarn workspace jan build:darwin",
|
"build:darwin": "yarn build:web && yarn workspace jan build:darwin",
|
||||||
"build:win32": "yarn build:web && yarn workspace jan build:win32",
|
"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.
|
- module.ts: Defines the plugin module which would be executed by the main node process.
|
||||||
- package.json: Defines the plugin metadata.
|
- package.json: Defines the plugin metadata.
|
||||||
- tsconfig.json: Defines the typescript configuration.
|
- tsconfig.json: Defines the typescript configuration.
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/data-plugin",
|
"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.",
|
"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",
|
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
|
||||||
"main": "dist/esm/index.js",
|
"main": "dist/esm/index.js",
|
||||||
@ -12,7 +12,8 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b ./config/tsconfig.esm.json && tsc -b ./config/tsconfig.cjs.json && webpack --config webpack.config.js",
|
"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"
|
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
@ -30,10 +31,6 @@
|
|||||||
"webpack": "^5.88.2",
|
"webpack": "^5.88.2",
|
||||||
"webpack-cli": "^5.1.4"
|
"webpack-cli": "^5.1.4"
|
||||||
},
|
},
|
||||||
"bundledDependencies": [
|
|
||||||
"pouchdb-node",
|
|
||||||
"pouchdb-find"
|
|
||||||
],
|
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**",
|
"dist/**",
|
||||||
"package.json",
|
"package.json",
|
||||||
|
|||||||
@ -140,6 +140,8 @@ async function handleMessageRequest(data: NewMessageRequest) {
|
|||||||
message.message = message.message.trim();
|
message.message = message.message.trim();
|
||||||
// TODO: Common collections should be able to access via core functions instead of store
|
// TODO: Common collections should be able to access via core functions instead of store
|
||||||
await store.updateOne("messages", message._id, message);
|
await store.updateOne("messages", message._id, message);
|
||||||
|
events.emit("OnMessageResponseFinished", message);
|
||||||
|
// events.emit(EventName.OnMessageResponseFinished, message);
|
||||||
},
|
},
|
||||||
error: async (err) => {
|
error: async (err) => {
|
||||||
message.message =
|
message.message =
|
||||||
|
|||||||
@ -85,6 +85,7 @@ const registerListener = () => {
|
|||||||
events.on(EventName.OnNewMessageRequest, handleMessageRequest);
|
events.on(EventName.OnNewMessageRequest, handleMessageRequest);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Preference update - reconfigure OpenAI
|
||||||
const onPreferencesUpdate = () => {
|
const onPreferencesUpdate = () => {
|
||||||
setup();
|
setup();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/azure-openai-plugin",
|
"name": "@janhq/azure-openai-plugin",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"description": "Inference plugin for Azure OpenAI",
|
"description": "Inference plugin for Azure OpenAI",
|
||||||
"icon": "https://static-assets.jan.ai/openai-icon.jpg",
|
"icon": "https://static-assets.jan.ai/openai-icon.jpg",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/module.js",
|
"module": "dist/module.js",
|
||||||
"author": "Jan <service@jan.ai>",
|
"author": "Jan <service@jan.ai>",
|
||||||
|
"requiredVersion": "^0.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"activationPoints": [
|
"activationPoints": [
|
||||||
"init"
|
"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!
|
So, what are you waiting for? Go ahead and start customizing your plugin!
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "retrieval-plugin",
|
"name": "retrieval-plugin",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"description": "Retrieval plugin for Jan app",
|
"description": "Retrieval plugin for Jan app (experimental)",
|
||||||
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
|
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/module.js",
|
"module": "dist/module.js",
|
||||||
|
"requiredVersion": "^0.3.1",
|
||||||
"author": "Jan <service@jan.ai>",
|
"author": "Jan <service@jan.ai>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"activationPoints": [
|
"activationPoints": [
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import SimpleTag from '../SimpleTag'
|
import SimpleTag from '../SimpleTag'
|
||||||
import PrimaryButton from '../PrimaryButton'
|
|
||||||
import { formatDownloadPercentage, toGigabytes } from '@utils/converter'
|
import { formatDownloadPercentage, toGigabytes } from '@utils/converter'
|
||||||
import SecondaryButton from '../SecondaryButton'
|
|
||||||
import { useCallback, useEffect, useMemo } from 'react'
|
import { useCallback, useEffect, useMemo } from 'react'
|
||||||
import useGetPerformanceTag from '@hooks/useGetPerformanceTag'
|
import useGetPerformanceTag from '@hooks/useGetPerformanceTag'
|
||||||
import useDownloadModel from '@hooks/useDownloadModel'
|
import useDownloadModel from '@hooks/useDownloadModel'
|
||||||
@ -58,7 +56,6 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
|||||||
if (isDownloaded) {
|
if (isDownloaded) {
|
||||||
downloadButton = (
|
downloadButton = (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
|
||||||
themes="accent"
|
themes="accent"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMainViewState(MainViewState.MyModel)
|
setMainViewState(MainViewState.MyModel)
|
||||||
@ -72,17 +69,20 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
|||||||
if (downloadState != null) {
|
if (downloadState != null) {
|
||||||
// downloading
|
// downloading
|
||||||
downloadButton = (
|
downloadButton = (
|
||||||
<SecondaryButton
|
<Button
|
||||||
disabled
|
disabled
|
||||||
title={`Downloading (${formatDownloadPercentage(
|
themes="accent"
|
||||||
downloadState.percent
|
onClick={() => {
|
||||||
)})`}
|
setMainViewState(MainViewState.MyModel)
|
||||||
/>
|
}}
|
||||||
|
>
|
||||||
|
Downloading {formatDownloadPercentage(downloadState.percent)}
|
||||||
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<span>{exploreModel.name}</span>
|
<span>{exploreModel.name}</span>
|
||||||
{performanceTag && (
|
{performanceTag && (
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { ModelManagementService } from '@janhq/core'
|
|
||||||
import {
|
import {
|
||||||
getActiveConvoIdAtom,
|
getActiveConvoIdAtom,
|
||||||
setActiveConvoIdAtom,
|
setActiveConvoIdAtom,
|
||||||
updateConversationWaitingForResponseAtom,
|
|
||||||
} from '@helpers/atoms/Conversation.atom'
|
} from '@helpers/atoms/Conversation.atom'
|
||||||
import {
|
import {
|
||||||
setMainViewStateAtom,
|
setMainViewStateAtom,
|
||||||
@ -12,11 +10,12 @@ import {
|
|||||||
} from '@helpers/atoms/MainView.atom'
|
} from '@helpers/atoms/MainView.atom'
|
||||||
import { displayDate } from '@utils/datetime'
|
import { displayDate } from '@utils/datetime'
|
||||||
import { twMerge } from 'tailwind-merge'
|
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 = {
|
type Props = {
|
||||||
conversation: Conversation
|
conversation: Conversation
|
||||||
avatarUrl?: string
|
|
||||||
name: string
|
name: string
|
||||||
summary?: string
|
summary?: string
|
||||||
updatedAt?: string
|
updatedAt?: string
|
||||||
@ -24,24 +23,38 @@ type Props = {
|
|||||||
|
|
||||||
const HistoryItem: React.FC<Props> = ({
|
const HistoryItem: React.FC<Props> = ({
|
||||||
conversation,
|
conversation,
|
||||||
avatarUrl,
|
|
||||||
name,
|
name,
|
||||||
summary,
|
summary,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
}) => {
|
}) => {
|
||||||
const setMainViewState = useSetAtom(setMainViewStateAtom)
|
|
||||||
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
||||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
|
||||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
|
||||||
const isSelected = activeConvoId === conversation._id
|
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 onClick = async () => {
|
||||||
const model = await executeSerial(
|
if (conversation.modelId == null) {
|
||||||
ModelManagementService.GetModelById,
|
console.debug('modelId is undefined')
|
||||||
conversation.modelId
|
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) {
|
if (activeConvoId !== conversation._id) {
|
||||||
setMainViewState(MainViewState.Conversation)
|
setMainViewState(MainViewState.Conversation)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import HistoryItem from '../HistoryItem'
|
import HistoryItem from '../HistoryItem'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
import ExpandableHeader from '../ExpandableHeader'
|
import ExpandableHeader from '../ExpandableHeader'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { searchAtom } from '@helpers/JotaiWrapper'
|
import { searchAtom } from '@helpers/JotaiWrapper'
|
||||||
@ -33,7 +33,6 @@ const HistoryList: React.FC = () => {
|
|||||||
key={convo._id}
|
key={convo._id}
|
||||||
conversation={convo}
|
conversation={convo}
|
||||||
summary={convo.summary}
|
summary={convo.summary}
|
||||||
avatarUrl={convo.image}
|
|
||||||
name={convo.name || 'Jan'}
|
name={convo.name || 'Jan'}
|
||||||
updatedAt={convo.updatedAt ?? ''}
|
updatedAt={convo.updatedAt ?? ''}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -5,82 +5,86 @@ import BasicPromptInput from '../BasicPromptInput'
|
|||||||
import BasicPromptAccessories from '../BasicPromptAccessories'
|
import BasicPromptAccessories from '../BasicPromptAccessories'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import SecondaryButton from '../SecondaryButton'
|
import SecondaryButton from '../SecondaryButton'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { PlusIcon } from '@heroicons/react/24/outline'
|
import { PlusIcon } from '@heroicons/react/24/outline'
|
||||||
import useCreateConversation from '@hooks/useCreateConversation'
|
import useCreateConversation from '@hooks/useCreateConversation'
|
||||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
||||||
import {
|
import {
|
||||||
currentConversationAtom,
|
|
||||||
currentConvoStateAtom,
|
currentConvoStateAtom,
|
||||||
|
getActiveConvoIdAtom,
|
||||||
} from '@helpers/atoms/Conversation.atom'
|
} from '@helpers/atoms/Conversation.atom'
|
||||||
import useGetBots from '@hooks/useGetBots'
|
import useGetInputState from '@hooks/useGetInputState'
|
||||||
import { activeBotAtom } from '@helpers/atoms/Bot.atom'
|
import useStartStopModel from '@hooks/useStartStopModel'
|
||||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
import { userConversationsAtom } from '@helpers/atoms/Conversation.atom'
|
||||||
|
import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom'
|
||||||
|
|
||||||
const InputToolbar: React.FC = () => {
|
const InputToolbar: React.FC = () => {
|
||||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||||
const { requestCreateConvo } = useCreateConversation()
|
|
||||||
const currentConvoState = useAtomValue(currentConvoStateAtom)
|
const currentConvoState = useAtomValue(currentConvoStateAtom)
|
||||||
const currentConvo = useAtomValue(currentConversationAtom)
|
const { inputState, currentConvo } = useGetInputState()
|
||||||
|
const { requestCreateConvo } = useCreateConversation()
|
||||||
const setActiveBot = useSetAtom(activeBotAtom)
|
const { startModel } = useStartStopModel()
|
||||||
const { getBotById } = useGetBots()
|
const { loading } = useAtomValue(stateModel)
|
||||||
const [inputState, setInputState] = useState<
|
const conversations = useAtomValue(userConversationsAtom)
|
||||||
'available' | 'disabled' | 'loading'
|
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
||||||
>()
|
const setShowModalNoActiveModel = useSetAtom(showingModalNoActiveModel)
|
||||||
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 onNewConversationClick = () => {
|
const onNewConversationClick = () => {
|
||||||
if (activeModel) {
|
if (activeModel) {
|
||||||
requestCreateConvo(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="my-3 flex justify-center gap-2">
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={onNewConversationClick}
|
||||||
|
title="New Conversation"
|
||||||
|
icon={<PlusIcon width={16} height={16} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputState === 'model-not-found') {
|
||||||
return (
|
return (
|
||||||
<div className="sticky bottom-0 flex items-center justify-center bg-background/90">
|
<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">
|
<p className="mx-auto my-5 line-clamp-2 text-ellipsis text-center italic text-gray-600">
|
||||||
{error}
|
Model {currentConvo?.modelId} not found! Please re-download the model
|
||||||
|
first.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversations.length > 0)
|
||||||
return (
|
return (
|
||||||
<div className="sticky bottom-0 w-full bg-background/90 px-5 py-0">
|
<div className="sticky bottom-0 w-full bg-background/90 px-5 py-0">
|
||||||
{currentConvoState?.error && (
|
{currentConvoState?.error && (
|
||||||
|
|||||||
@ -34,12 +34,12 @@ const LeftHeaderAction: React.FC = () => {
|
|||||||
className="flex-1"
|
className="flex-1"
|
||||||
icon={<MagnifyingGlassIcon width={16} height={16} />}
|
icon={<MagnifyingGlassIcon width={16} height={16} />}
|
||||||
/>
|
/>
|
||||||
<SecondaryButton
|
{/* <SecondaryButton
|
||||||
title={'Create bot'}
|
title={'Create bot'}
|
||||||
onClick={onCreateBotClicked}
|
onClick={onCreateBotClicked}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
icon={<PlusIcon width={16} height={16} />}
|
icon={<PlusIcon width={16} height={16} />}
|
||||||
/>
|
/> */}
|
||||||
</div>
|
</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 }) => (
|
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">
|
<table className="min-w-full">
|
||||||
<thead className="bg-background">
|
<thead className="bg-background">
|
||||||
<tr className="rounded-t-lg">
|
<tr className="rounded-t-lg">
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
|||||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
||||||
import { Button } from '@uikit'
|
import { Button } from '@uikit'
|
||||||
import { MessageCircle } from 'lucide-react'
|
import { MessageCircle } from 'lucide-react'
|
||||||
|
import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom'
|
||||||
|
|
||||||
enum ActionButton {
|
enum ActionButton {
|
||||||
DownloadModel = 'Download a Model',
|
DownloadModel = 'Download a Model',
|
||||||
@ -21,6 +22,7 @@ const SidebarEmptyHistory: React.FC = () => {
|
|||||||
const setMainView = useSetAtom(setMainViewStateAtom)
|
const setMainView = useSetAtom(setMainViewStateAtom)
|
||||||
const { requestCreateConvo } = useCreateConversation()
|
const { requestCreateConvo } = useCreateConversation()
|
||||||
const [action, setAction] = useState(ActionButton.DownloadModel)
|
const [action, setAction] = useState(ActionButton.DownloadModel)
|
||||||
|
const modalNoActiveModel = useSetAtom(showingModalNoActiveModel)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (downloadedModels.length > 0) {
|
if (downloadedModels.length > 0) {
|
||||||
@ -35,7 +37,7 @@ const SidebarEmptyHistory: React.FC = () => {
|
|||||||
setMainView(MainViewState.ExploreModel)
|
setMainView(MainViewState.ExploreModel)
|
||||||
} else {
|
} else {
|
||||||
if (!activeModel) {
|
if (!activeModel) {
|
||||||
setMainView(MainViewState.ConversationEmptyModel)
|
modalNoActiveModel(true)
|
||||||
} else {
|
} else {
|
||||||
await requestCreateConvo(activeModel)
|
await requestCreateConvo(activeModel)
|
||||||
}
|
}
|
||||||
@ -44,10 +46,10 @@ const SidebarEmptyHistory: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-3 py-10">
|
<div className="flex flex-col items-center gap-3 py-10">
|
||||||
<MessageCircle size={32} />
|
<MessageCircle size={24} />
|
||||||
<div className="flex flex-col items-center gap-y-2">
|
<div className="flex flex-col items-center">
|
||||||
<h6 className="text-center text-base">No Chat History</h6>
|
<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.
|
Get started by creating a new chat.
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={onClick} themes="accent">
|
<Button onClick={onClick} themes="accent">
|
||||||
|
|||||||
@ -51,7 +51,7 @@ const SimpleTextMessage: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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
|
<Image
|
||||||
className="rounded-full"
|
className="rounded-full"
|
||||||
@ -73,7 +73,7 @@ const SimpleTextMessage: React.FC<Props> = ({
|
|||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
) : (
|
) : (
|
||||||
<span
|
<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 }}
|
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)
|
downloadStates.push(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(stateModelStartStop)
|
|
||||||
|
|
||||||
return (
|
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="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">
|
<div className="flex gap-x-2">
|
||||||
@ -53,7 +51,7 @@ const BottomBar = () => {
|
|||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
<SystemItem name="CPU:" value={`${cpu}%`} />
|
<SystemItem name="CPU:" value={`${cpu}%`} />
|
||||||
<SystemItem name="Mem:" value={`${ram}%`} />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -69,17 +69,17 @@ const Providers = (props: PropsWithChildren) => {
|
|||||||
return (
|
return (
|
||||||
<JotaiWrapper>
|
<JotaiWrapper>
|
||||||
{setupCore && (
|
{setupCore && (
|
||||||
<EventListenerWrapper>
|
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
{activated ? (
|
{activated ? (
|
||||||
|
<EventListenerWrapper>
|
||||||
<ModalWrapper>{children}</ModalWrapper>
|
<ModalWrapper>{children}</ModalWrapper>
|
||||||
|
</EventListenerWrapper>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-background flex h-screen w-screen items-center justify-center">
|
<div className="flex h-screen w-screen items-center justify-center bg-background">
|
||||||
<CompactLogo width={56} height={56} />
|
<CompactLogo width={56} height={56} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ThemeWrapper>
|
</ThemeWrapper>
|
||||||
</EventListenerWrapper>
|
|
||||||
)}
|
)}
|
||||||
</JotaiWrapper>
|
</JotaiWrapper>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -88,11 +88,11 @@ export const SidebarLeft = () => {
|
|||||||
icon: <LayoutGrid size={20} className="flex-shrink-0" />,
|
icon: <LayoutGrid size={20} className="flex-shrink-0" />,
|
||||||
state: MainViewState.MyModel,
|
state: MainViewState.MyModel,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: 'Bot',
|
// name: 'Bot',
|
||||||
icon: <Bot size={20} className="flex-shrink-0" />,
|
// icon: <Bot size={20} className="flex-shrink-0" />,
|
||||||
state: MainViewState.CreateBot,
|
// state: MainViewState.CreateBot,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
icon: <Settings size={20} className="flex-shrink-0" />,
|
icon: <Settings size={20} className="flex-shrink-0" />,
|
||||||
|
|||||||
@ -1,17 +1,50 @@
|
|||||||
import { addNewMessageAtom, updateMessageAtom } from './atoms/ChatMessage.atom'
|
import { addNewMessageAtom, updateMessageAtom } from './atoms/ChatMessage.atom'
|
||||||
import { toChatMessage } from '@models/ChatMessage'
|
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 { useSetAtom } from 'jotai'
|
||||||
import { ReactNode, useEffect } from 'react'
|
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 }) {
|
export default function EventHandler({ children }: { children: ReactNode }) {
|
||||||
const addNewMessage = useSetAtom(addNewMessageAtom)
|
const addNewMessage = useSetAtom(addNewMessageAtom)
|
||||||
const updateMessage = useSetAtom(updateMessageAtom)
|
const updateMessage = useSetAtom(updateMessageAtom)
|
||||||
|
const updateConversation = useSetAtom(updateConversationAtom)
|
||||||
|
const { getBotById } = useGetBots()
|
||||||
|
const { getConversationById } = useGetUserConversations()
|
||||||
|
|
||||||
function handleNewMessageResponse(message: NewMessageResponse) {
|
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)
|
const newResponse = toChatMessage(message)
|
||||||
addNewMessage(newResponse)
|
addNewMessage(newResponse)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
async function handleMessageResponseUpdate(
|
async function handleMessageResponseUpdate(
|
||||||
messageResponse: NewMessageResponse
|
messageResponse: NewMessageResponse
|
||||||
) {
|
) {
|
||||||
@ -19,7 +52,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
|||||||
messageResponse.conversationId &&
|
messageResponse.conversationId &&
|
||||||
messageResponse._id &&
|
messageResponse._id &&
|
||||||
messageResponse.message
|
messageResponse.message
|
||||||
)
|
) {
|
||||||
updateMessage(
|
updateMessage(
|
||||||
messageResponse._id,
|
messageResponse._id,
|
||||||
messageResponse.conversationId,
|
messageResponse.conversationId,
|
||||||
@ -27,10 +60,43 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (window.corePlugin.events) {
|
if (window.corePlugin.events) {
|
||||||
events.on(EventName.OnNewMessageResponse, handleNewMessageResponse)
|
events.on(EventName.OnNewMessageResponse, handleNewMessageResponse)
|
||||||
events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
|
events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
|
||||||
|
events.on(
|
||||||
|
"OnMessageResponseFinished",
|
||||||
|
// EventName.OnMessageResponseFinished,
|
||||||
|
handleMessageResponseFinished
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -38,7 +104,12 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
|||||||
return () => {
|
return () => {
|
||||||
events.off(EventName.OnNewMessageResponse, handleNewMessageResponse)
|
events.off(EventName.OnNewMessageResponse, handleNewMessageResponse)
|
||||||
events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
|
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 ConfirmDeleteModelModal from '@/_components/ConfirmDeleteModelModal'
|
||||||
import ConfirmSignOutModal from '@/_components/ConfirmSignOutModal'
|
import ConfirmSignOutModal from '@/_components/ConfirmSignOutModal'
|
||||||
import MobileMenuPane from '@/_components/MobileMenuPane'
|
import MobileMenuPane from '@/_components/MobileMenuPane'
|
||||||
|
import SwitchingModelConfirmationModal from '@/_components/SwitchingModelConfirmationModal'
|
||||||
|
import ModalNoActiveModel from '@/_components/ModalNoActiveModel'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -18,6 +20,8 @@ export const ModalWrapper: React.FC<Props> = ({ children }) => (
|
|||||||
<ConfirmSignOutModal />
|
<ConfirmSignOutModal />
|
||||||
<ConfirmDeleteModelModal />
|
<ConfirmDeleteModelModal />
|
||||||
<BotListModal />
|
<BotListModal />
|
||||||
|
<SwitchingModelConfirmationModal />
|
||||||
|
<ModalNoActiveModel />
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { SwitchingModelConfirmationModalProps } from '@/_components/SwitchingModelConfirmationModal'
|
||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
export const showConfirmDeleteConversationModalAtom = atom(false)
|
export const showConfirmDeleteConversationModalAtom = atom(false)
|
||||||
@ -7,3 +8,8 @@ export const showingAdvancedPromptAtom = atom<boolean>(false)
|
|||||||
export const showingProductDetailAtom = atom<boolean>(false)
|
export const showingProductDetailAtom = atom<boolean>(false)
|
||||||
export const showingMobilePaneAtom = atom<boolean>(false)
|
export const showingMobilePaneAtom = atom<boolean>(false)
|
||||||
export const showingBotListModalAtom = 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 { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { DataService } from '@janhq/core'
|
import { DataService } from '@janhq/core'
|
||||||
import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom'
|
import {
|
||||||
|
getActiveConvoIdAtom,
|
||||||
|
userConversationsAtom,
|
||||||
|
} from '@helpers/atoms/Conversation.atom'
|
||||||
import {
|
import {
|
||||||
getCurrentChatMessagesAtom,
|
getCurrentChatMessagesAtom,
|
||||||
setCurrentChatMessagesAtom,
|
setCurrentChatMessagesAtom,
|
||||||
} from '@helpers/atoms/ChatMessage.atom'
|
} from '@helpers/atoms/ChatMessage.atom'
|
||||||
|
import useGetBots from './useGetBots'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hooks to get chat messages for current(active) conversation
|
* Custom hooks to get chat messages for current(active) conversation
|
||||||
@ -16,6 +20,8 @@ const useChatMessages = () => {
|
|||||||
const setMessages = useSetAtom(setCurrentChatMessagesAtom)
|
const setMessages = useSetAtom(setCurrentChatMessagesAtom)
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
||||||
|
const userConversations = useAtomValue(userConversationsAtom)
|
||||||
|
const { getBotById } = useGetBots()
|
||||||
|
|
||||||
const getMessages = async (convoId: string) => {
|
const getMessages = async (convoId: string) => {
|
||||||
const data: any = await executeSerial(
|
const data: any = await executeSerial(
|
||||||
@ -26,6 +32,12 @@ const useChatMessages = () => {
|
|||||||
return []
|
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)
|
return parseMessages(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,10 +59,10 @@ const useChatMessages = () => {
|
|||||||
return { messages }
|
return { messages }
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMessages(messages: RawMessage[]): ChatMessage[] {
|
function parseMessages(messages: RawMessage[], bot?: Bot): ChatMessage[] {
|
||||||
const newMessages: ChatMessage[] = []
|
const newMessages: ChatMessage[] = []
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const chatMessage = toChatMessage(m)
|
const chatMessage = toChatMessage(m, bot)
|
||||||
newMessages.push(chatMessage)
|
newMessages.push(chatMessage)
|
||||||
}
|
}
|
||||||
return newMessages
|
return newMessages
|
||||||
|
|||||||
@ -6,19 +6,18 @@ import {
|
|||||||
setActiveConvoIdAtom,
|
setActiveConvoIdAtom,
|
||||||
addNewConversationStateAtom,
|
addNewConversationStateAtom,
|
||||||
} from '@helpers/atoms/Conversation.atom'
|
} from '@helpers/atoms/Conversation.atom'
|
||||||
|
import useGetModelById from './useGetModelById'
|
||||||
|
|
||||||
const useCreateConversation = () => {
|
const useCreateConversation = () => {
|
||||||
const [userConversations, setUserConversations] = useAtom(
|
const [userConversations, setUserConversations] = useAtom(
|
||||||
userConversationsAtom
|
userConversationsAtom
|
||||||
)
|
)
|
||||||
|
const { getModelById } = useGetModelById()
|
||||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
||||||
const addNewConvoState = useSetAtom(addNewConversationStateAtom)
|
const addNewConvoState = useSetAtom(addNewConversationStateAtom)
|
||||||
|
|
||||||
const createConvoByBot = async (bot: Bot) => {
|
const createConvoByBot = async (bot: Bot) => {
|
||||||
const model = await executeSerial(
|
const model = await getModelById(bot.modelId)
|
||||||
ModelManagementService.GetModelById,
|
|
||||||
bot.modelId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
alert(
|
alert(
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { executeSerial } from '@services/pluginService'
|
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() {
|
export default function useDownloadModel() {
|
||||||
|
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
||||||
|
|
||||||
const assistanModel = (
|
const assistanModel = (
|
||||||
model: Product,
|
model: Product,
|
||||||
modelVersion: ModelVersion
|
modelVersion: ModelVersion
|
||||||
@ -37,6 +41,22 @@ export default function useDownloadModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const downloadModel = async (model: Product, modelVersion: ModelVersion) => {
|
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()
|
modelVersion.startDownloadAt = Date.now()
|
||||||
const assistantModel = assistanModel(model, modelVersion)
|
const assistantModel = assistanModel(model, modelVersion)
|
||||||
await executeSerial(ModelManagementService.StoreModel, assistantModel)
|
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 {
|
return {
|
||||||
|
getConversationById,
|
||||||
getUserConversations,
|
getUserConversations,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,13 +13,14 @@ import { addNewMessageAtom } from '@helpers/atoms/ChatMessage.atom'
|
|||||||
import {
|
import {
|
||||||
currentConversationAtom,
|
currentConversationAtom,
|
||||||
updateConversationAtom,
|
updateConversationAtom,
|
||||||
|
updateConversationWaitingForResponseAtom,
|
||||||
} from '@helpers/atoms/Conversation.atom'
|
} from '@helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
export default function useSendChatMessage() {
|
export default function useSendChatMessage() {
|
||||||
const currentConvo = useAtomValue(currentConversationAtom)
|
const currentConvo = useAtomValue(currentConversationAtom)
|
||||||
const addNewMessage = useSetAtom(addNewMessageAtom)
|
const addNewMessage = useSetAtom(addNewMessageAtom)
|
||||||
const updateConversation = useSetAtom(updateConversationAtom)
|
const updateConversation = useSetAtom(updateConversationAtom)
|
||||||
|
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
||||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
||||||
|
|
||||||
let timeout: any | undefined = undefined
|
let timeout: any | undefined = undefined
|
||||||
@ -33,18 +34,20 @@ export default function useSendChatMessage() {
|
|||||||
if (
|
if (
|
||||||
!currentConvo?.summary ||
|
!currentConvo?.summary ||
|
||||||
currentConvo.summary === '' ||
|
currentConvo.summary === '' ||
|
||||||
currentConvo.summary.startsWith('User request:')
|
currentConvo.summary.startsWith('Prompt:')
|
||||||
) {
|
) {
|
||||||
// Request convo summary
|
// Request convo summary
|
||||||
setTimeout(async () => {
|
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(
|
const result = await executeSerial(
|
||||||
InferenceService.InferenceRequest,
|
InferenceService.InferenceRequest,
|
||||||
newMessage
|
newMessage
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
result?.message &&
|
result?.message &&
|
||||||
result.message.split(' ').length <= 7 &&
|
result.message.split(' ').length <= 10 &&
|
||||||
conv?._id
|
conv?._id
|
||||||
) {
|
) {
|
||||||
const updatedConv = {
|
const updatedConv = {
|
||||||
@ -60,10 +63,15 @@ export default function useSendChatMessage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendChatMessage = async () => {
|
const sendChatMessage = async () => {
|
||||||
|
const convoId = currentConvo?._id
|
||||||
|
|
||||||
|
if (!convoId) return
|
||||||
setCurrentPrompt('')
|
setCurrentPrompt('')
|
||||||
|
updateConvWaiting(convoId, true)
|
||||||
|
|
||||||
const prompt = currentPrompt.trim()
|
const prompt = currentPrompt.trim()
|
||||||
const newMessage: RawMessage = {
|
const newMessage: RawMessage = {
|
||||||
conversationId: currentConvo?._id,
|
conversationId: convoId,
|
||||||
message: prompt,
|
message: prompt,
|
||||||
user: 'user',
|
user: 'user',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@ -77,10 +85,20 @@ export default function useSendChatMessage() {
|
|||||||
events.emit(EventName.OnNewMessageRequest, newMessage)
|
events.emit(EventName.OnNewMessageRequest, newMessage)
|
||||||
|
|
||||||
if (!currentConvo?.summary && currentConvo) {
|
if (!currentConvo?.summary && currentConvo) {
|
||||||
const updatedConv = {
|
const updatedConv: Conversation = {
|
||||||
...currentConvo,
|
...currentConvo,
|
||||||
|
lastMessage: prompt,
|
||||||
summary: `Prompt: ${prompt}`,
|
summary: `Prompt: ${prompt}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateConversation(updatedConv)
|
||||||
|
await executeSerial(DataService.UpdateConversation, updatedConv)
|
||||||
|
} else {
|
||||||
|
const updatedConv: Conversation = {
|
||||||
|
...currentConvo,
|
||||||
|
lastMessage: prompt,
|
||||||
|
}
|
||||||
|
|
||||||
updateConversation(updatedConv)
|
updateConversation(updatedConv)
|
||||||
await executeSerial(DataService.UpdateConversation, updatedConv)
|
await executeSerial(DataService.UpdateConversation, updatedConv)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { executeSerial } from '@services/pluginService'
|
import { executeSerial } from '@services/pluginService'
|
||||||
import { ModelManagementService, InferenceService } from '@janhq/core'
|
import { InferenceService } from '@janhq/core'
|
||||||
import { useAtom, useSetAtom } from 'jotai'
|
import { useAtom, useSetAtom } from 'jotai'
|
||||||
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
||||||
import { useState } from 'react'
|
import useGetModelById from './useGetModelById'
|
||||||
|
|
||||||
export default function useStartStopModel() {
|
export default function useStartStopModel() {
|
||||||
const [activeModel, setActiveModel] = useAtom(activeAssistantModelAtom)
|
const [activeModel, setActiveModel] = useAtom(activeAssistantModelAtom)
|
||||||
|
const { getModelById } = useGetModelById()
|
||||||
const setStateModel = useSetAtom(stateModel)
|
const setStateModel = useSetAtom(stateModel)
|
||||||
|
|
||||||
const startModel = async (modelId: string) => {
|
const startModel = async (modelId: string) => {
|
||||||
@ -16,25 +17,27 @@ export default function useStartStopModel() {
|
|||||||
|
|
||||||
setStateModel({ state: 'start', loading: true, model: modelId })
|
setStateModel({ state: 'start', loading: true, model: modelId })
|
||||||
|
|
||||||
const model = await executeSerial(
|
const model = await getModelById(modelId)
|
||||||
ModelManagementService.GetModelById,
|
|
||||||
modelId
|
|
||||||
)
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
alert(`Model ${modelId} not found! Please re-download the model first.`)
|
alert(`Model ${modelId} not found! Please re-download the model first.`)
|
||||||
setStateModel((prev) => ({ ...prev, loading: false }))
|
setStateModel((prev) => ({ ...prev, loading: false }))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTime = Date.now()
|
const currentTime = Date.now()
|
||||||
console.debug('Init model: ', model._id)
|
console.debug('Init model: ', model._id)
|
||||||
|
|
||||||
const res = await executeSerial(InferenceService.InitModel, model._id)
|
const res = await initModel(model._id)
|
||||||
if (res?.error) {
|
if (res?.error) {
|
||||||
const errorMessage = `Failed to init model: ${res.error}`
|
const errorMessage = `Failed to init model: ${res.error}`
|
||||||
console.error(errorMessage)
|
console.error(errorMessage)
|
||||||
alert(errorMessage)
|
alert(errorMessage)
|
||||||
} else {
|
} else {
|
||||||
console.debug(
|
console.debug(
|
||||||
`Init model successfully!, take ${Date.now() - currentTime}ms`
|
`Init model ${modelId} successfully!, take ${
|
||||||
|
Date.now() - currentTime
|
||||||
|
}ms`
|
||||||
)
|
)
|
||||||
setActiveModel(model)
|
setActiveModel(model)
|
||||||
}
|
}
|
||||||
@ -52,3 +55,7 @@ export default function useStartStopModel() {
|
|||||||
|
|
||||||
return { startModel, stopModel }
|
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 = (
|
export const toChatMessage = (
|
||||||
m: RawMessage | NewMessageResponse
|
m: RawMessage | NewMessageResponse,
|
||||||
|
bot?: Bot
|
||||||
): ChatMessage => {
|
): ChatMessage => {
|
||||||
const createdAt = new Date(m.createdAt ?? '').getTime()
|
const createdAt = new Date(m.createdAt ?? '').getTime()
|
||||||
const imageUrls: string[] = []
|
const imageUrls: string[] = []
|
||||||
@ -56,18 +57,18 @@ export const toChatMessage = (
|
|||||||
|
|
||||||
const content = m.message ?? ''
|
const content = m.message ?? ''
|
||||||
|
|
||||||
|
let senderName = m.user === 'user' ? 'You' : 'Assistant'
|
||||||
|
if (senderName === 'Assistant' && bot) {
|
||||||
|
senderName = bot.name
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: (m._id ?? 0).toString(),
|
id: (m._id ?? 0).toString(),
|
||||||
conversationId: (m.conversationId ?? 0).toString(),
|
conversationId: (m.conversationId ?? 0).toString(),
|
||||||
messageType: messageType,
|
messageType: messageType,
|
||||||
messageSenderType: messageSenderType,
|
messageSenderType: messageSenderType,
|
||||||
senderUid: m.user?.toString() || '0',
|
senderUid: m.user?.toString() || '0',
|
||||||
senderName:
|
senderName: senderName,
|
||||||
m.user === 'user'
|
|
||||||
? 'You'
|
|
||||||
: m.user && m.user !== 'ai' && m.user !== 'assistant'
|
|
||||||
? m.user
|
|
||||||
: 'Assistant',
|
|
||||||
senderAvatarUrl: m.avatar
|
senderAvatarUrl: m.avatar
|
||||||
? m.avatar
|
? m.avatar
|
||||||
: m.user === 'user'
|
: m.user === 'user'
|
||||||
|
|||||||
@ -28,10 +28,10 @@
|
|||||||
"eslint-config-next": "13.4.10",
|
"eslint-config-next": "13.4.10",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"react-intersection-observer": "^9.5.2",
|
|
||||||
"jotai": "^2.4.0",
|
"jotai": "^2.4.0",
|
||||||
"jotai-optics": "^0.3.1",
|
"jotai-optics": "^0.3.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.288.0",
|
"lucide-react": "^0.288.0",
|
||||||
"marked": "^9.1.2",
|
"marked": "^9.1.2",
|
||||||
"marked-highlight": "^2.0.6",
|
"marked-highlight": "^2.0.6",
|
||||||
@ -42,6 +42,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.45.4",
|
"react-hook-form": "^7.45.4",
|
||||||
|
"react-intersection-observer": "^9.5.2",
|
||||||
"sass": "^1.69.4",
|
"sass": "^1.69.4",
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
"tailwindcss": "3.3.3",
|
"tailwindcss": "3.3.3",
|
||||||
@ -50,6 +51,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.4",
|
"@tailwindcss/forms": "^0.5.4",
|
||||||
|
"@types/lodash": "^4.14.200",
|
||||||
"@types/node": "20.6.5",
|
"@types/node": "20.6.5",
|
||||||
"@types/uuid": "^9.0.6",
|
"@types/uuid": "^9.0.6",
|
||||||
"encoding": "^0.1.13",
|
"encoding": "^0.1.13",
|
||||||
|
|||||||
@ -9,13 +9,13 @@ import LeftHeaderAction from '@/_components/LeftHeaderAction'
|
|||||||
const ChatScreen = () => {
|
const ChatScreen = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<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">
|
<div className="px-4 py-6 pt-4">
|
||||||
<LeftHeaderAction />
|
<LeftHeaderAction />
|
||||||
<HistoryList />
|
<HistoryList />
|
||||||
</div>
|
</div>
|
||||||
</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 />
|
<MainHeader />
|
||||||
<ChatBody />
|
<ChatBody />
|
||||||
<InputToolbar />
|
<InputToolbar />
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const MyModelsScreen = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full overflow-y-auto">
|
<div className="flex h-full w-full overflow-y-auto">
|
||||||
<div className="w-full p-5">
|
<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">
|
<p className="mt-2 text-gray-600 dark:text-gray-400">
|
||||||
You have <span>{downloadedModels.length}</span> models downloaded
|
You have <span>{downloadedModels.length}</span> models downloaded
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
} from '@/../../electron/core/plugin-manager/execution/index'
|
} from '@/../../electron/core/plugin-manager/execution/index'
|
||||||
import { executeSerial } from '@services/pluginService'
|
import { executeSerial } from '@services/pluginService'
|
||||||
import { DataService } from '@janhq/core'
|
import { DataService } from '@janhq/core'
|
||||||
|
import useGetAppVersion from '@hooks/useGetAppVersion'
|
||||||
|
|
||||||
const PluginCatalog = () => {
|
const PluginCatalog = () => {
|
||||||
// const [search, setSearch] = useState<string>('')
|
// const [search, setSearch] = useState<string>('')
|
||||||
@ -20,23 +21,41 @@ const PluginCatalog = () => {
|
|||||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||||
const experimentRef = useRef(null)
|
const experimentRef = useRef(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(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.
|
* Loads the plugin catalog module from a CDN and sets it as the plugin catalog state.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!version) return
|
||||||
|
|
||||||
// Load plugin manifest from plugin if any
|
// Load plugin manifest from plugin if any
|
||||||
if (extensionPoints.get(DataService.GetPluginManifest)) {
|
if (extensionPoints.get(DataService.GetPluginManifest)) {
|
||||||
executeSerial(DataService.GetPluginManifest).then((data) => {
|
executeSerial(DataService.GetPluginManifest).then((data) => {
|
||||||
setPluginCatalog(data)
|
setPluginCatalog(
|
||||||
|
data.filter(
|
||||||
|
(e: any) =>
|
||||||
|
!e.requiredVersion ||
|
||||||
|
e.requiredVersion.replace(/[.^]/g, '') <=
|
||||||
|
version.replaceAll('.', '')
|
||||||
|
)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Fallback to app default manifest
|
// Fallback to app default manifest
|
||||||
import(
|
import(
|
||||||
/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`
|
/* 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.
|
* Fetches the active plugins and their preferences from the `plugins` and `preferences` modules.
|
||||||
@ -141,11 +160,16 @@ const PluginCatalog = () => {
|
|||||||
)
|
)
|
||||||
.map((item, i) => {
|
.map((item, i) => {
|
||||||
const isActivePlugin = activePlugins.some((x) => x.name === item.name)
|
const isActivePlugin = activePlugins.some((x) => x.name === item.name)
|
||||||
|
const installedPlugin = activePlugins.filter(
|
||||||
|
(p) => p.name === item.name
|
||||||
|
)[0]
|
||||||
const updateVersionPlugins = Number(
|
const updateVersionPlugins = Number(
|
||||||
activePlugins
|
installedPlugin?.version.replaceAll('.', '')
|
||||||
.filter((p) => p.name === item.name)[0]
|
|
||||||
?.version.replaceAll('.', '')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const hasUpdateVersionPlugins =
|
||||||
|
item.version.replaceAll('.', '') > updateVersionPlugins
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
@ -163,8 +187,15 @@ const PluginCatalog = () => {
|
|||||||
<p className="whitespace-pre-wrap leading-relaxed text-gray-600 dark:text-gray-400">
|
<p className="whitespace-pre-wrap leading-relaxed text-gray-600 dark:text-gray-400">
|
||||||
{item.description}
|
{item.description}
|
||||||
</p>
|
</p>
|
||||||
{isActivePlugin &&
|
{isActivePlugin && (
|
||||||
item.version.replaceAll('.', '') < updateVersionPlugins && (
|
<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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
themes="outline"
|
themes="outline"
|
||||||
|
|||||||
@ -126,9 +126,11 @@ const SettingsScreen = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex-shrink-0">
|
<div className="mt-5 flex-shrink-0">
|
||||||
|
{preferencePlugins.length > 0 && (
|
||||||
<label className="font-bold uppercase text-gray-500">
|
<label className="font-bold uppercase text-gray-500">
|
||||||
Core plugins
|
Core plugins
|
||||||
</label>
|
</label>
|
||||||
|
)}
|
||||||
<div className="mt-1 font-semibold">
|
<div className="mt-1 font-semibold">
|
||||||
{preferencePlugins.map((menu, i) => {
|
{preferencePlugins.map((menu, i) => {
|
||||||
const isActive = activePreferencePlugin === menu
|
const isActive = activePreferencePlugin === menu
|
||||||
|
|||||||
@ -61,6 +61,7 @@
|
|||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-emphasis {
|
.hljs-emphasis {
|
||||||
|
|||||||
@ -7,3 +7,4 @@
|
|||||||
@import './global.scss';
|
@import './global.scss';
|
||||||
@import './code-block.scss';
|
@import './code-block.scss';
|
||||||
@import './loader.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