diff --git a/core/src/types/index.ts b/core/src/types/index.ts index bbd1e98de..7580c2432 100644 --- a/core/src/types/index.ts +++ b/core/src/types/index.ts @@ -143,6 +143,7 @@ export type ThreadAssistantInfo = { assistant_id: string; assistant_name: string; model: ModelInfo; + instructions?: string; }; /** @@ -288,13 +289,13 @@ export type Assistant = { /** Represents the name of the object. */ name: string; /** Represents the description of the object. */ - description: string; + description?: string; /** Represents the model of the object. */ model: string; /** Represents the instructions for the object. */ - instructions: string; + instructions?: string; /** Represents the tools associated with the object. */ - tools: any; + tools?: any; /** Represents the file identifiers associated with the object. */ file_ids: string[]; /** Represents the metadata of the object. */ diff --git a/electron/managers/window.ts b/electron/managers/window.ts index c930dd5ec..0d5a0eaf4 100644 --- a/electron/managers/window.ts +++ b/electron/managers/window.ts @@ -1,15 +1,15 @@ -import { BrowserWindow } from "electron"; +import { BrowserWindow } from 'electron' /** * Manages the current window instance. */ export class WindowManager { - public static instance: WindowManager = new WindowManager(); - public currentWindow?: BrowserWindow; + public static instance: WindowManager = new WindowManager() + public currentWindow?: BrowserWindow constructor() { if (WindowManager.instance) { - return WindowManager.instance; + return WindowManager.instance } } @@ -21,17 +21,17 @@ export class WindowManager { createWindow(options?: Electron.BrowserWindowConstructorOptions | undefined) { this.currentWindow = new BrowserWindow({ width: 1200, - minWidth: 800, + minWidth: 1200, height: 800, show: false, trafficLightPosition: { x: 10, y: 15, }, - titleBarStyle: "hidden", - vibrancy: "sidebar", + titleBarStyle: 'hidden', + vibrancy: 'sidebar', ...options, - }); - return this.currentWindow; + }) + return this.currentWindow } } diff --git a/electron/tests/explore.e2e.spec.ts b/electron/tests/explore.e2e.spec.ts index 5a4412cb3..77eb3dbda 100644 --- a/electron/tests/explore.e2e.spec.ts +++ b/electron/tests/explore.e2e.spec.ts @@ -1,41 +1,41 @@ -import { _electron as electron } from "playwright"; -import { ElectronApplication, Page, expect, test } from "@playwright/test"; +import { _electron as electron } from 'playwright' +import { ElectronApplication, Page, expect, test } from '@playwright/test' import { findLatestBuild, parseElectronApp, stubDialog, -} from "electron-playwright-helpers"; +} from 'electron-playwright-helpers' -let electronApp: ElectronApplication; -let page: Page; +let electronApp: ElectronApplication +let page: Page test.beforeAll(async () => { - process.env.CI = "e2e"; + process.env.CI = 'e2e' - const latestBuild = findLatestBuild("dist"); - expect(latestBuild).toBeTruthy(); + const latestBuild = findLatestBuild('dist') + expect(latestBuild).toBeTruthy() // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild); - expect(appInfo).toBeTruthy(); + const appInfo = parseElectronApp(latestBuild) + expect(appInfo).toBeTruthy() electronApp = await electron.launch({ args: [appInfo.main], // main file from package.json executablePath: appInfo.executable, // path to the Electron executable - }); - await stubDialog(electronApp, "showMessageBox", { response: 1 }); + }) + await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - page = await electronApp.firstWindow(); -}); + page = await electronApp.firstWindow() +}) test.afterAll(async () => { - await electronApp.close(); - await page.close(); -}); + await electronApp.close() + await page.close() +}) -test("explores models", async () => { - await page.getByTestId("Explore Models").first().click(); - await page.getByTestId("testid-explore-models").isVisible(); +test('explores models', async () => { + await page.getByTestId('Hub').first().click() + await page.getByTestId('testid-explore-models').isVisible() // More test cases here... -}); +}) diff --git a/electron/tests/main.e2e.spec.ts b/electron/tests/main.e2e.spec.ts index d6df31ca4..1a5bfe696 100644 --- a/electron/tests/main.e2e.spec.ts +++ b/electron/tests/main.e2e.spec.ts @@ -1,55 +1,55 @@ -import { _electron as electron } from "playwright"; -import { ElectronApplication, Page, expect, test } from "@playwright/test"; +import { _electron as electron } from 'playwright' +import { ElectronApplication, Page, expect, test } from '@playwright/test' import { findLatestBuild, parseElectronApp, stubDialog, -} from "electron-playwright-helpers"; +} from 'electron-playwright-helpers' -let electronApp: ElectronApplication; -let page: Page; +let electronApp: ElectronApplication +let page: Page test.beforeAll(async () => { - process.env.CI = "e2e"; + process.env.CI = 'e2e' - const latestBuild = findLatestBuild("dist"); - expect(latestBuild).toBeTruthy(); + const latestBuild = findLatestBuild('dist') + expect(latestBuild).toBeTruthy() // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild); - expect(appInfo).toBeTruthy(); - expect(appInfo.asar).toBe(true); - expect(appInfo.executable).toBeTruthy(); - expect(appInfo.main).toBeTruthy(); - expect(appInfo.name).toBe("jan"); - expect(appInfo.packageJson).toBeTruthy(); - expect(appInfo.packageJson.name).toBe("jan"); - expect(appInfo.platform).toBeTruthy(); - expect(appInfo.platform).toBe(process.platform); - expect(appInfo.resourcesDir).toBeTruthy(); + const appInfo = parseElectronApp(latestBuild) + expect(appInfo).toBeTruthy() + expect(appInfo.asar).toBe(true) + expect(appInfo.executable).toBeTruthy() + expect(appInfo.main).toBeTruthy() + expect(appInfo.name).toBe('jan') + expect(appInfo.packageJson).toBeTruthy() + expect(appInfo.packageJson.name).toBe('jan') + expect(appInfo.platform).toBeTruthy() + expect(appInfo.platform).toBe(process.platform) + expect(appInfo.resourcesDir).toBeTruthy() electronApp = await electron.launch({ args: [appInfo.main], // main file from package.json executablePath: appInfo.executable, // path to the Electron executable - }); - await stubDialog(electronApp, "showMessageBox", { response: 1 }); + }) + await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - page = await electronApp.firstWindow(); -}); + page = await electronApp.firstWindow() +}) test.afterAll(async () => { - await electronApp.close(); - await page.close(); -}); + await electronApp.close() + await page.close() +}) -test("renders the home page", async () => { - expect(page).toBeDefined(); +test('renders the home page', async () => { + expect(page).toBeDefined() // Welcome text is available const welcomeText = await page - .getByTestId("testid-welcome-title") + .getByTestId('testid-welcome-title') .first() - .isVisible(); - expect(welcomeText).toBe(false); -}); + .isVisible() + expect(welcomeText).toBe(false) +}) diff --git a/electron/tests/my-models.e2e.spec.ts b/electron/tests/my-models.e2e.spec.ts deleted file mode 100644 index a3355fb33..000000000 --- a/electron/tests/my-models.e2e.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { _electron as electron } from "playwright"; -import { ElectronApplication, Page, expect, test } from "@playwright/test"; - -import { - findLatestBuild, - parseElectronApp, - stubDialog, -} from "electron-playwright-helpers"; - -let electronApp: ElectronApplication; -let page: Page; - -test.beforeAll(async () => { - process.env.CI = "e2e"; - - const latestBuild = findLatestBuild("dist"); - expect(latestBuild).toBeTruthy(); - - // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild); - expect(appInfo).toBeTruthy(); - - electronApp = await electron.launch({ - args: [appInfo.main], // main file from package.json - executablePath: appInfo.executable, // path to the Electron executable - }); - await stubDialog(electronApp, "showMessageBox", { response: 1 }); - - page = await electronApp.firstWindow(); -}); - -test.afterAll(async () => { - await electronApp.close(); - await page.close(); -}); - -test("shows my models", async () => { - await page.getByTestId("My Models").first().click(); - await page.getByTestId("testid-my-models").isVisible(); - // More test cases here... -}); diff --git a/electron/tests/navigation.e2e.spec.ts b/electron/tests/navigation.e2e.spec.ts index 104333650..2f4f7b767 100644 --- a/electron/tests/navigation.e2e.spec.ts +++ b/electron/tests/navigation.e2e.spec.ts @@ -1,43 +1,43 @@ -import { _electron as electron } from "playwright"; -import { ElectronApplication, Page, expect, test } from "@playwright/test"; +import { _electron as electron } from 'playwright' +import { ElectronApplication, Page, expect, test } from '@playwright/test' import { findLatestBuild, parseElectronApp, stubDialog, -} from "electron-playwright-helpers"; +} from 'electron-playwright-helpers' -let electronApp: ElectronApplication; -let page: Page; +let electronApp: ElectronApplication +let page: Page test.beforeAll(async () => { - process.env.CI = "e2e"; + process.env.CI = 'e2e' - const latestBuild = findLatestBuild("dist"); - expect(latestBuild).toBeTruthy(); + const latestBuild = findLatestBuild('dist') + expect(latestBuild).toBeTruthy() // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild); - expect(appInfo).toBeTruthy(); + const appInfo = parseElectronApp(latestBuild) + expect(appInfo).toBeTruthy() electronApp = await electron.launch({ args: [appInfo.main], // main file from package.json executablePath: appInfo.executable, // path to the Electron executable - }); - await stubDialog(electronApp, "showMessageBox", { response: 1 }); + }) + await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - page = await electronApp.firstWindow(); -}); + page = await electronApp.firstWindow() +}) test.afterAll(async () => { - await electronApp.close(); - await page.close(); -}); + await electronApp.close() + await page.close() +}) -test("renders left navigation panel", async () => { +test('renders left navigation panel', async () => { // Chat section should be there - const chatSection = await page.getByTestId("Chat").first().isVisible(); - expect(chatSection).toBe(false); + const chatSection = await page.getByTestId('Chat').first().isVisible() + expect(chatSection).toBe(false) // Home actions /* Disable unstable feature tests @@ -45,7 +45,10 @@ test("renders left navigation panel", async () => { ** Enable back when it is whitelisted */ - const myModelsBtn = await page.getByTestId("My Models").first().isEnabled(); - const settingsBtn = await page.getByTestId("Settings").first().isEnabled(); - expect([myModelsBtn, settingsBtn].filter((e) => !e).length).toBe(0); -}); + const systemMonitorBtn = await page + .getByTestId('System Monitor') + .first() + .isEnabled() + const settingsBtn = await page.getByTestId('Settings').first().isEnabled() + expect([systemMonitorBtn, settingsBtn].filter((e) => !e).length).toBe(0) +}) diff --git a/electron/tests/settings.e2e.spec.ts b/electron/tests/settings.e2e.spec.ts index 2f8d6465b..798504c70 100644 --- a/electron/tests/settings.e2e.spec.ts +++ b/electron/tests/settings.e2e.spec.ts @@ -1,40 +1,40 @@ -import { _electron as electron } from "playwright"; -import { ElectronApplication, Page, expect, test } from "@playwright/test"; +import { _electron as electron } from 'playwright' +import { ElectronApplication, Page, expect, test } from '@playwright/test' import { findLatestBuild, parseElectronApp, stubDialog, -} from "electron-playwright-helpers"; +} from 'electron-playwright-helpers' -let electronApp: ElectronApplication; -let page: Page; +let electronApp: ElectronApplication +let page: Page test.beforeAll(async () => { - process.env.CI = "e2e"; + process.env.CI = 'e2e' - const latestBuild = findLatestBuild("dist"); - expect(latestBuild).toBeTruthy(); + const latestBuild = findLatestBuild('dist') + expect(latestBuild).toBeTruthy() // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild); - expect(appInfo).toBeTruthy(); + const appInfo = parseElectronApp(latestBuild) + expect(appInfo).toBeTruthy() electronApp = await electron.launch({ args: [appInfo.main], // main file from package.json executablePath: appInfo.executable, // path to the Electron executable - }); - await stubDialog(electronApp, "showMessageBox", { response: 1 }); + }) + await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - page = await electronApp.firstWindow(); -}); + page = await electronApp.firstWindow() +}) test.afterAll(async () => { - await electronApp.close(); - await page.close(); -}); + await electronApp.close() + await page.close() +}) -test("shows settings", async () => { - await page.getByTestId("Settings").first().click(); - await page.getByTestId("testid-setting-description").isVisible(); -}); +test('shows settings', async () => { + await page.getByTestId('Settings').first().click() + await page.getByTestId('testid-setting-description').isVisible() +}) diff --git a/electron/tests/system-monitor.e2e.spec.ts b/electron/tests/system-monitor.e2e.spec.ts new file mode 100644 index 000000000..747a8ae18 --- /dev/null +++ b/electron/tests/system-monitor.e2e.spec.ts @@ -0,0 +1,41 @@ +import { _electron as electron } from 'playwright' +import { ElectronApplication, Page, expect, test } from '@playwright/test' + +import { + findLatestBuild, + parseElectronApp, + stubDialog, +} from 'electron-playwright-helpers' + +let electronApp: ElectronApplication +let page: Page + +test.beforeAll(async () => { + process.env.CI = 'e2e' + + const latestBuild = findLatestBuild('dist') + expect(latestBuild).toBeTruthy() + + // parse the packaged Electron app and find paths and other info + const appInfo = parseElectronApp(latestBuild) + expect(appInfo).toBeTruthy() + + electronApp = await electron.launch({ + args: [appInfo.main], // main file from package.json + executablePath: appInfo.executable, // path to the Electron executable + }) + await stubDialog(electronApp, 'showMessageBox', { response: 1 }) + + page = await electronApp.firstWindow() +}) + +test.afterAll(async () => { + await electronApp.close() + await page.close() +}) + +test('shows system monitor', async () => { + await page.getByTestId('System Monitor').first().click() + await page.getByTestId('testid-system-monitor').isVisible() + // More test cases here... +}) diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index 7321a0660..8d01021b7 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -89,12 +89,12 @@ export default class JanAssistantExtension implements AssistantExtension { private async createJanAssistant(): Promise { const janAssistant: Assistant = { avatar: "", - thread_location: undefined, // TODO: make this property ? + thread_location: undefined, id: "jan", object: "assistant", // TODO: maybe we can set default value for this? created_at: Date.now(), - name: "Jan Assistant", - description: "Just Jan Assistant", + name: "Jan", + description: "A default assistant that can use all downloaded models", model: "*", instructions: "Your name is Jan.", tools: undefined, diff --git a/uikit/package.json b/uikit/package.json index dd67be599..a96b5d37e 100644 --- a/uikit/package.json +++ b/uikit/package.json @@ -20,9 +20,11 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-context": "^1.0.1", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-toast": "^1.1.5", diff --git a/uikit/src/badge/styles.scss b/uikit/src/badge/styles.scss index e5a783d88..cf8e52c8b 100644 --- a/uikit/src/badge/styles.scss +++ b/uikit/src/badge/styles.scss @@ -6,7 +6,7 @@ } &-success { - @apply border-transparent bg-green-500 text-green-900 hover:bg-green-500/80; + @apply border-transparent bg-green-100 text-green-600; } &-secondary { diff --git a/uikit/src/command/styles.scss b/uikit/src/command/styles.scss index 80171ef50..a832792d6 100644 --- a/uikit/src/command/styles.scss +++ b/uikit/src/command/styles.scss @@ -25,7 +25,7 @@ } &-list-item { - @apply text-foreground aria-selected:bg-primary relative flex cursor-pointer select-none items-center rounded-md px-2 py-2 text-sm outline-none; + @apply text-foreground aria-selected:bg-secondary relative flex cursor-pointer select-none items-center rounded-md px-2 py-2 text-sm outline-none; } &-empty { diff --git a/uikit/src/index.ts b/uikit/src/index.ts index 67c3af93f..067752de0 100644 --- a/uikit/src/index.ts +++ b/uikit/src/index.ts @@ -10,3 +10,4 @@ export * from './tooltip' export * from './modal' export * from './command' export * from './textarea' +export * from './select' diff --git a/uikit/src/input/index.tsx b/uikit/src/input/index.tsx index 8d90ab232..9b7808055 100644 --- a/uikit/src/input/index.tsx +++ b/uikit/src/input/index.tsx @@ -9,7 +9,7 @@ const Input = forwardRef( return ( diff --git a/uikit/src/main.scss b/uikit/src/main.scss index 562e09532..1eca363b4 100644 --- a/uikit/src/main.scss +++ b/uikit/src/main.scss @@ -14,6 +14,7 @@ @import './modal/styles.scss'; @import './command/styles.scss'; @import './textarea/styles.scss'; +@import './select/styles.scss'; .animate-spin { animation: spin 1s linear infinite; @@ -104,7 +105,3 @@ --secondary-foreground: 210 20% 98%; } } - -:is(p) { - @apply text-muted-foreground; -} diff --git a/uikit/src/select/index.tsx b/uikit/src/select/index.tsx new file mode 100644 index 000000000..9bee7a153 --- /dev/null +++ b/uikit/src/select/index.tsx @@ -0,0 +1,139 @@ +'use client' + +import * as React from 'react' +import { + CaretSortIcon, + // CheckIcon, + ChevronDownIcon, + ChevronUpIcon, +} from '@radix-ui/react-icons' + +import * as SelectPrimitive from '@radix-ui/react-select' + +import { twMerge } from 'tailwind-merge' + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {/* + + + + */} + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/uikit/src/select/styles.scss b/uikit/src/select/styles.scss new file mode 100644 index 000000000..a0bf625f0 --- /dev/null +++ b/uikit/src/select/styles.scss @@ -0,0 +1,31 @@ +.select { + @apply ring-offset-background placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1; + + &-caret { + @apply h-4 w-4 opacity-50; + } + + &-scroll-up-button { + @apply flex cursor-default items-center justify-center py-1; + } + + &-scroll-down-button { + @apply flex cursor-default items-center justify-center py-1; + } + + &-label { + @apply px-2 py-1.5 text-sm font-semibold; + } + + &-item { + @apply hover:bg-secondary relative my-1 block w-full cursor-pointer select-none items-center rounded-sm px-4 py-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50; + } + + &-trigger-viewport { + @apply w-full py-1; + } + + &-content { + @apply bg-background border-border relative z-50 mt-1 block max-h-96 w-full min-w-[8rem] overflow-hidden rounded-md border shadow-md; + } +} diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 38dee2056..c62390ba5 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -15,7 +15,7 @@ export const metadata: Metadata = { export default function RootLayout({ children }: PropsWithChildren) { return ( - +
{children} diff --git a/web/app/page.tsx b/web/app/page.tsx index 20abda6f9..cae3262a7 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -8,29 +8,25 @@ import { useMainViewState } from '@/hooks/useMainViewState' import ChatScreen from '@/screens/Chat' import ExploreModelsScreen from '@/screens/ExploreModels' -import MyModelsScreen from '@/screens/MyModels' + import SettingsScreen from '@/screens/Settings' -import WelcomeScreen from '@/screens/Welcome' +import SystemMonitorScreen from '@/screens/SystemMonitor' export default function Page() { const { mainViewState } = useMainViewState() let children = null switch (mainViewState) { - case MainViewState.Welcome: - children = - break - - case MainViewState.ExploreModels: + case MainViewState.Hub: children = break - case MainViewState.MyModels: - children = + case MainViewState.Settings: + children = break - case MainViewState.Setting: - children = + case MainViewState.SystemMonitor: + children = break default: diff --git a/web/constants/screens.ts b/web/constants/screens.ts index 76ad6fab5..19f82aaac 100644 --- a/web/constants/screens.ts +++ b/web/constants/screens.ts @@ -1,7 +1,7 @@ export enum MainViewState { - Welcome, - ExploreModels, + Hub, MyModels, - Setting, - Chat, + Settings, + Thread, + SystemMonitor, } diff --git a/web/containers/CardSidebar/index.tsx b/web/containers/CardSidebar/index.tsx index 42f975aaf..38264e457 100644 --- a/web/containers/CardSidebar/index.tsx +++ b/web/containers/CardSidebar/index.tsx @@ -1,13 +1,15 @@ -import { ReactNode, useState } from 'react' -import { Fragment } from 'react' +import { ReactNode, useState, useRef } from 'react' -import { Menu, Transition } from '@headlessui/react' import { ChevronDownIcon, - EllipsisVerticalIcon, -} from '@heroicons/react/20/solid' + MoreVerticalIcon, + FolderOpenIcon, + Code2Icon, +} from 'lucide-react' import { twMerge } from 'tailwind-merge' +import { useClickOutside } from '@/hooks/useClickOutside' + interface Props { children: ReactNode title: string @@ -21,65 +23,75 @@ export default function CardSidebar({ onViewJsonClick, }: Props) { const [show, setShow] = useState(true) + const [more, setMore] = useState(false) + const [menu, setMenu] = useState(null) + const [toggle, setToggle] = useState(null) + + useClickOutside(() => setMore(false), null, [menu, toggle]) return ( -
-
+
+
- - - Open options - - setMore(!more)} + > + +
+ {more && ( +
- - - {({ active }) => ( - onRevealInFinderClick(title)} - className={twMerge( - active ? 'bg-gray-50' : '', - 'block cursor-pointer px-3 py-1 text-xs leading-6 text-gray-900' - )} - > - Reveal in finder - - )} - - - {({ active }) => ( - onViewJsonClick(title)} - className={twMerge( - active ? 'bg-gray-50' : '', - 'block cursor-pointer px-3 py-1 text-xs leading-6 text-gray-900' - )} - > - View a JSON - - )} - - - - +
{ + onRevealInFinderClick(title) + setMore(false) + }} + > + + + Reveal in Finder + +
+
{ + onViewJsonClick(title) + setMore(false) + }} + > + + + View as JSON + +
+
+ )}
{show &&
{children}
}
diff --git a/web/containers/DropdownListSidebar/index.tsx b/web/containers/DropdownListSidebar/index.tsx index b159a131e..589847fdf 100644 --- a/web/containers/DropdownListSidebar/index.tsx +++ b/web/containers/DropdownListSidebar/index.tsx @@ -1,104 +1,114 @@ -import { Fragment, useEffect, useState } from 'react' - -import { Listbox, Transition } from '@headlessui/react' -import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid' +import { useEffect, useState } from 'react' import { Model } from '@janhq/core' -import { atom, useSetAtom } from 'jotai' +import { + Button, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@janhq/uikit' + +import { atom, useAtomValue, useSetAtom } from 'jotai' + +import { MonitorIcon } from 'lucide-react' + import { twMerge } from 'tailwind-merge' +import { MainViewState } from '@/constants/screens' + import { getDownloadedModels } from '@/hooks/useGetDownloadedModels' +import { useMainViewState } from '@/hooks/useMainViewState' + +import { toGigabytes } from '@/utils/converter' + +import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom' + export const selectedModelAtom = atom(undefined) export default function DropdownListSidebar() { const [downloadedModels, setDownloadedModels] = useState([]) - const [selected, setSelected] = useState() const setSelectedModel = useSetAtom(selectedModelAtom) + const activeThread = useAtomValue(activeThreadAtom) + const [selected, setSelected] = useState() + const { setMainViewState } = useMainViewState() useEffect(() => { getDownloadedModels().then((downloadedModels) => { setDownloadedModels(downloadedModels) - if (downloadedModels.length > 0) { - setSelected(downloadedModels[0]) - setSelectedModel(downloadedModels[0]) + setSelected( + downloadedModels.filter( + (x) => x.id === activeThread?.assistants[0].model.id + )[0] || downloadedModels[0] + ) + setSelectedModel( + downloadedModels.filter( + (x) => x.id === activeThread?.assistants[0].model.id + )[0] || downloadedModels[0] + ) } }) - }, []) - - if (!selected) return null + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeThread]) return ( - { - setSelected(model) - setSelectedModel(model) + ) } diff --git a/web/containers/Layout/BottomBar/DownloadingState/index.tsx b/web/containers/Layout/BottomBar/DownloadingState/index.tsx index 1aad0fb1c..0648508d0 100644 --- a/web/containers/Layout/BottomBar/DownloadingState/index.tsx +++ b/web/containers/Layout/BottomBar/DownloadingState/index.tsx @@ -12,18 +12,14 @@ import { ModalTrigger, } from '@janhq/uikit' -import { useAtomValue } from 'jotai' - import { useDownloadState } from '@/hooks/useDownloadState' import { formatDownloadPercentage } from '@/utils/converter' import { extensionManager } from '@/extension' -import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' export default function DownloadingState() { const { downloadStates } = useDownloadState() - const models = useAtomValue(downloadingModelsAtom) const totalCurrentProgress = downloadStates .map((a) => a.size.transferred + a.size.transferred) diff --git a/web/containers/Layout/BottomBar/index.tsx b/web/containers/Layout/BottomBar/index.tsx index 1a264da02..fb0ef5ed6 100644 --- a/web/containers/Layout/BottomBar/index.tsx +++ b/web/containers/Layout/BottomBar/index.tsx @@ -30,7 +30,7 @@ const BottomBar = () => { const { downloadStates } = useDownloadState() return ( -
+
{progress && progress > 0 ? ( @@ -49,7 +49,7 @@ const BottomBar = () => { name="Active model:" value={ activeModel?.id || ( - +   to show your model @@ -63,7 +63,7 @@ const BottomBar = () => { diff --git a/web/containers/Layout/Ribbon/index.tsx b/web/containers/Layout/Ribbon/index.tsx index 6babadb9d..fa6d53193 100644 --- a/web/containers/Layout/Ribbon/index.tsx +++ b/web/containers/Layout/Ribbon/index.tsx @@ -1,5 +1,3 @@ -import { useContext } from 'react' - import { Tooltip, TooltipContent, @@ -11,9 +9,8 @@ import { motion as m } from 'framer-motion' import { MessageCircleIcon, SettingsIcon, - DatabaseIcon, - CpuIcon, - BookOpenIcon, + MonitorIcon, + LayoutGridIcon, } from 'lucide-react' import { twMerge } from 'tailwind-merge' @@ -34,36 +31,51 @@ export default function RibbonNav() { const primaryMenus = [ { - name: 'Getting Started', - icon: , - state: MainViewState.Welcome, + name: 'Thread', + icon: ( + + ), + state: MainViewState.Thread, }, { - name: 'Chat', - icon: , - state: MainViewState.Chat, + name: 'Hub', + icon: ( + + ), + state: MainViewState.Hub, }, ] const secondaryMenus = [ { - name: 'Explore Models', - icon: , - state: MainViewState.ExploreModels, - }, - { - name: 'My Models', - icon: , - state: MainViewState.MyModels, + name: 'System Monitor', + icon: ( + + ), + state: MainViewState.SystemMonitor, }, { name: 'Settings', - icon: , - state: MainViewState.Setting, + icon: ( + + ), + state: MainViewState.Settings, }, ] return ( -
+
@@ -90,7 +102,7 @@ export default function RibbonNav() {
{isActive && ( )} @@ -126,7 +138,7 @@ export default function RibbonNav() {
{isActive && ( )} diff --git a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx index 0fb278080..d0ea6b26b 100644 --- a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx +++ b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx @@ -85,12 +85,12 @@ export default function CommandListDownloadedModel() { { - setMainViewState(MainViewState.ExploreModels) + setMainViewState(MainViewState.Hub) setOpen(false) }} > - Explore Models + Explore The Hub diff --git a/web/containers/Layout/TopBar/CommandSearch/index.tsx b/web/containers/Layout/TopBar/CommandSearch/index.tsx index 2e20ff583..d83feb22e 100644 --- a/web/containers/Layout/TopBar/CommandSearch/index.tsx +++ b/web/containers/Layout/TopBar/CommandSearch/index.tsx @@ -1,7 +1,6 @@ import { Fragment, useState, useEffect } from 'react' import { - Button, CommandModal, CommandEmpty, CommandGroup, @@ -11,14 +10,7 @@ import { CommandList, } from '@janhq/uikit' -import { useAtomValue, useSetAtom } from 'jotai' -import { - MessageCircleIcon, - SettingsIcon, - DatabaseIcon, - CpuIcon, - BookOpenIcon, -} from 'lucide-react' +import { MessageCircleIcon, SettingsIcon, LayoutGridIcon } from 'lucide-react' import ShortCut from '@/containers/Shortcut' @@ -26,43 +18,27 @@ import { MainViewState } from '@/constants/screens' import { useMainViewState } from '@/hooks/useMainViewState' -import { showRightSideBarAtom } from '@/screens/Chat/Sidebar' - -import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom' - export default function CommandSearch() { const { setMainViewState } = useMainViewState() const [open, setOpen] = useState(false) - const setShowRightSideBar = useSetAtom(showRightSideBarAtom) - const activeThread = useAtomValue(activeThreadAtom) const menus = [ - { - name: 'Getting Started', - icon: , - state: MainViewState.Welcome, - }, { name: 'Chat', icon: ( ), - state: MainViewState.Chat, + state: MainViewState.Thread, }, { - name: 'Explore Models', - icon: , - state: MainViewState.ExploreModels, - }, - { - name: 'My Models', - icon: , - state: MainViewState.MyModels, + name: 'Hub', + icon: , + state: MainViewState.Hub, }, { name: 'Settings', icon: , - state: MainViewState.Setting, + state: MainViewState.Settings, shortcut: , }, ] @@ -75,7 +51,7 @@ export default function CommandSearch() { } if (e.key === ',' && (e.metaKey || e.ctrlKey)) { e.preventDefault() - setMainViewState(MainViewState.Setting) + setMainViewState(MainViewState.Settings) } } document.addEventListener('keydown', down) @@ -85,7 +61,8 @@ export default function CommandSearch() { return ( -
+ {/* Temporary disable view search input until we have proper UI placement, but we keep function cmd + K for showing list page */} + {/*
-
- +
*/} @@ -124,15 +100,6 @@ export default function CommandSearch() { - {activeThread && ( - - )} ) } diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx index 5ab4ebc84..aa7912bd3 100644 --- a/web/containers/Layout/TopBar/index.tsx +++ b/web/containers/Layout/TopBar/index.tsx @@ -1,21 +1,86 @@ +import { useAtomValue, useSetAtom } from 'jotai' +import { PanelLeftIcon, PenSquareIcon, PanelRightIcon } from 'lucide-react' + import CommandListDownloadedModel from '@/containers/Layout/TopBar/CommandListDownloadedModel' import CommandSearch from '@/containers/Layout/TopBar/CommandSearch' +import { MainViewState } from '@/constants/screens' + +import { useCreateNewThread } from '@/hooks/useCreateNewThread' +import useGetAssistants from '@/hooks/useGetAssistants' import { useMainViewState } from '@/hooks/useMainViewState' +import { showRightSideBarAtom } from '@/screens/Chat/Sidebar' + +import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom' + const TopBar = () => { - const { viewStateName } = useMainViewState() + const activeThread = useAtomValue(activeThreadAtom) + const { mainViewState } = useMainViewState() + const { requestCreateNewThread } = useCreateNewThread() + const { assistants } = useGetAssistants() + const setShowRightSideBar = useSetAtom(showRightSideBarAtom) + + const titleScreen = (viewStateName: MainViewState) => { + switch (viewStateName) { + case MainViewState.Thread: + return activeThread ? activeThread?.title : 'New Thread' + + default: + return MainViewState[viewStateName]?.replace(/([A-Z])/g, ' $1').trim() + } + } + + const onCreateConversationClick = async () => { + if (assistants.length === 0) { + alert('No assistant available') + return + } + requestCreateNewThread(assistants[0]) + } return ( -
+
+ {mainViewState === MainViewState.Thread && ( +
+ )}
-
- - {viewStateName.replace(/([A-Z])/g, ' $1').trim()} - -
+ {mainViewState === MainViewState.Thread ? ( +
+
+
+ +
+
+ +
+
+ + {titleScreen(mainViewState)} + + {activeThread && ( +
setShowRightSideBar((show) => !show)} + > + +
+ )} +
+ ) : ( +
+ + {titleScreen(mainViewState)} + +
+ )} - {/* Command without trigger interface */}
diff --git a/web/containers/ModalCancelDownload/index.tsx b/web/containers/ModalCancelDownload/index.tsx index 8619c543c..4153b89ee 100644 --- a/web/containers/ModalCancelDownload/index.tsx +++ b/web/containers/ModalCancelDownload/index.tsx @@ -35,7 +35,6 @@ export default function ModalCancelDownload({ model, isFromList }: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps [model.id] ) - const models = useAtomValue(downloadingModelsAtom) const downloadState = useAtomValue(downloadAtom) const cancelText = `Cancel ${formatDownloadPercentage(downloadState.percent)}` diff --git a/web/containers/Shortcut/index.tsx b/web/containers/Shortcut/index.tsx index 67a5f8d0c..ae93a827e 100644 --- a/web/containers/Shortcut/index.tsx +++ b/web/containers/Shortcut/index.tsx @@ -14,7 +14,7 @@ export default function ShortCut(props: { menu: string }) { } return ( -
+

{getSymbol(os) + ' + ' + menu}

) diff --git a/web/containers/Toast/index.tsx b/web/containers/Toast/index.tsx index 50f1f0f29..c5e5f03da 100644 --- a/web/containers/Toast/index.tsx +++ b/web/containers/Toast/index.tsx @@ -16,7 +16,7 @@ export function toaster(props: Props) { return (
{ const newData: Record = { ...get(chatMessages), } - newData[id] = newData[id].filter((e) => e.role === ChatCompletionRole.System) + newData[id] = newData[id]?.filter((e) => e.role === ChatCompletionRole.System) set(chatMessages, newData) }) diff --git a/web/helpers/atoms/SystemBar.atom.ts b/web/helpers/atoms/SystemBar.atom.ts index 9b44c2e92..aa5e77d58 100644 --- a/web/helpers/atoms/SystemBar.atom.ts +++ b/web/helpers/atoms/SystemBar.atom.ts @@ -1,3 +1,6 @@ import { atom } from 'jotai' export const totalRamAtom = atom(0) +export const usedRamAtom = atom(0) + +export const cpuUsageAtom = atom(0) diff --git a/web/hooks/useClickOutside.ts b/web/hooks/useClickOutside.ts new file mode 100644 index 000000000..4e8e5d2c3 --- /dev/null +++ b/web/hooks/useClickOutside.ts @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useRef } from 'react' + +const DEFAULT_EVENTS = ['mousedown', 'touchstart'] + +export function useClickOutside( + handler: () => void, + events?: string[] | null, + nodes?: (HTMLElement | null)[] +) { + const ref = useRef() + + useEffect(() => { + const listener = (event: any) => { + const { target } = event ?? {} + if (Array.isArray(nodes)) { + const shouldIgnore = + target?.hasAttribute('data-ignore-outside-clicks') || + (!document.body.contains(target) && target.tagName !== 'HTML') + const shouldTrigger = nodes.every( + (node) => !!node && !event.composedPath().includes(node) + ) + shouldTrigger && !shouldIgnore && handler() + } else if (ref.current && !ref.current.contains(target)) { + handler() + } + } + + ;(events || DEFAULT_EVENTS).forEach((fn) => + document.addEventListener(fn, listener) + ) + + return () => { + ;(events || DEFAULT_EVENTS).forEach((fn) => + document.removeEventListener(fn, listener) + ) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ref, handler, nodes]) + + return ref +} diff --git a/web/hooks/useCreateNewThread.ts b/web/hooks/useCreateNewThread.ts index 9ccecee7a..7526feb49 100644 --- a/web/hooks/useCreateNewThread.ts +++ b/web/hooks/useCreateNewThread.ts @@ -40,7 +40,6 @@ export const useCreateNewThread = () => { const setActiveThreadId = useSetAtom(setActiveThreadIdAtom) const [threadStates, setThreadStates] = useAtom(threadStatesAtom) const threads = useAtomValue(threadsAtom) - const activeThread = useAtomValue(activeThreadAtom) const updateThread = useSetAtom(updateThreadAtom) const requestCreateNewThread = async (assistant: Assistant) => { @@ -69,6 +68,7 @@ export const useCreateNewThread = () => { stream: false, }, }, + instructions: assistant.instructions, } const threadId = generateThreadId(assistant.id) const thread: Thread = { @@ -93,20 +93,18 @@ export const useCreateNewThread = () => { setActiveThreadId(thread.id) } - function updateThreadTitle(title: string) { - if (!activeThread) return - const updatedConv: Thread = { - ...activeThread, - title, + function updateThreadMetadata(thread: Thread) { + const updatedThread: Thread = { + ...thread, } - updateThread(updatedConv) + updateThread(updatedThread) extensionManager .get(ExtensionType.Conversational) - ?.saveThread(updatedConv) + ?.saveThread(updatedThread) } return { requestCreateNewThread, - updateThreadTitle, + updateThreadMetadata, } } diff --git a/web/hooks/useDeleteConversation.ts b/web/hooks/useDeleteConversation.ts index 1cfceebcf..b02796b10 100644 --- a/web/hooks/useDeleteConversation.ts +++ b/web/hooks/useDeleteConversation.ts @@ -17,7 +17,6 @@ import { } from '@/helpers/atoms/ChatMessage.atom' import { threadsAtom, - getActiveThreadIdAtom, setActiveThreadIdAtom, } from '@/helpers/atoms/Conversation.atom' @@ -25,14 +24,13 @@ export default function useDeleteThread() { const { activeModel } = useActiveModel() const [threads, setThreads] = useAtom(threadsAtom) const setCurrentPrompt = useSetAtom(currentPromptAtom) - const activeThreadId = useAtomValue(getActiveThreadIdAtom) const messages = useAtomValue(getCurrentChatMessagesAtom) const setActiveConvoId = useSetAtom(setActiveThreadIdAtom) const deleteMessages = useSetAtom(deleteConversationMessage) const cleanMessages = useSetAtom(cleanConversationMessages) - const cleanThread = async () => { + const cleanThread = async (activeThreadId: string) => { if (activeThreadId) { const thread = threads.filter((c) => c.id === activeThreadId)[0] cleanMessages(activeThreadId) @@ -46,7 +44,7 @@ export default function useDeleteThread() { } } - const deleteThread = async () => { + const deleteThread = async (activeThreadId: string) => { if (!activeThreadId) { alert('No active thread') return @@ -60,8 +58,8 @@ export default function useDeleteThread() { deleteMessages(activeThreadId) setCurrentPrompt('') toaster({ - title: 'Chat successfully deleted.', - description: `Chat with ${activeModel?.name} has been successfully deleted.`, + title: 'Thread successfully deleted.', + description: `Thread with ${activeModel?.name} has been successfully deleted.`, }) if (availableThreads.length > 0) { setActiveConvoId(availableThreads[0].id) diff --git a/web/hooks/useDownloadModel.ts b/web/hooks/useDownloadModel.ts index 6bcffdaed..b91ac2a57 100644 --- a/web/hooks/useDownloadModel.ts +++ b/web/hooks/useDownloadModel.ts @@ -1,6 +1,6 @@ import { Model, ExtensionType, ModelExtension } from '@janhq/core' -import { useAtom, useAtomValue } from 'jotai' +import { useAtom } from 'jotai' import { useDownloadState } from './useDownloadState' diff --git a/web/hooks/useGetSystemResources.ts b/web/hooks/useGetSystemResources.ts index ef4b2ef08..e2de61519 100644 --- a/web/hooks/useGetSystemResources.ts +++ b/web/hooks/useGetSystemResources.ts @@ -6,12 +6,18 @@ import { MonitoringExtension } from '@janhq/core' import { useSetAtom } from 'jotai' import { extensionManager } from '@/extension/ExtensionManager' -import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom' +import { + cpuUsageAtom, + totalRamAtom, + usedRamAtom, +} from '@/helpers/atoms/SystemBar.atom' export default function useGetSystemResources() { const [ram, setRam] = useState(0) const [cpu, setCPU] = useState(0) const setTotalRam = useSetAtom(totalRamAtom) + const setUsedRam = useSetAtom(usedRamAtom) + const setCpuUsage = useSetAtom(cpuUsageAtom) const getSystemResources = async () => { if ( @@ -27,10 +33,12 @@ export default function useGetSystemResources() { const ram = (resourceInfor?.mem?.active ?? 0) / (resourceInfor?.mem?.total ?? 1) + if (resourceInfor?.mem?.active) setUsedRam(resourceInfor.mem.active) if (resourceInfor?.mem?.total) setTotalRam(resourceInfor.mem.total) setRam(Math.round(ram * 100)) setCPU(Math.round(currentLoadInfor?.currentLoad ?? 0)) + setCpuUsage(Math.round(currentLoadInfor?.currentLoad ?? 0)) } useEffect(() => { @@ -45,6 +53,7 @@ export default function useGetSystemResources() { // clean up interval return () => clearInterval(intervalId) + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return { diff --git a/web/hooks/useMainViewState.ts b/web/hooks/useMainViewState.ts index 3dccbb704..91c1a1c4d 100644 --- a/web/hooks/useMainViewState.ts +++ b/web/hooks/useMainViewState.ts @@ -2,7 +2,7 @@ import { atom, useAtom } from 'jotai' import { MainViewState } from '@/constants/screens' -const currentMainViewState = atom(MainViewState.Welcome) +const currentMainViewState = atom(MainViewState.Thread) export function useMainViewState() { const [mainViewState, setMainViewState] = useAtom(currentMainViewState) diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 6b60a0e04..9cf61969d 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -1,3 +1,5 @@ +import { useState } from 'react' + import { ChatCompletionMessage, ChatCompletionRole, @@ -10,7 +12,7 @@ import { ThreadMessage, events, } from '@janhq/core' -import { ConversationalExtension, InferenceExtension } from '@janhq/core' +import { ConversationalExtension } from '@janhq/core' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { ulid } from 'ulid' @@ -44,6 +46,7 @@ export default function useSendChatMessage() { const { activeModel } = useActiveModel() const selectedModel = useAtomValue(selectedModelAtom) const { startModel } = useActiveModel() + const [queuedMessage, setQueuedMessage] = useState(false) const sendChatMessage = async () => { if (!currentPrompt || currentPrompt.trim().length === 0) { @@ -61,14 +64,15 @@ export default function useSendChatMessage() { } const assistantId = activeThread.assistants[0].assistant_id ?? '' const assistantName = activeThread.assistants[0].assistant_name ?? '' + const instructions = activeThread.assistants[0].instructions ?? '' const updatedThread: Thread = { ...activeThread, isFinishInit: true, - title: `${activeThread.assistants[0].assistant_name} with ${selectedModel.name}`, assistants: [ { assistant_id: assistantId, assistant_name: assistantName, + instructions: instructions, model: { id: selectedModel.id, settings: selectedModel.settings, @@ -90,18 +94,29 @@ export default function useSendChatMessage() { const prompt = currentPrompt.trim() setCurrentPrompt('') - const messages: ChatCompletionMessage[] = currentMessages - .map((msg) => ({ - role: msg.role, - content: msg.content[0]?.text.value ?? '', - })) - .concat([ - { - role: ChatCompletionRole.User, - content: prompt, - } as ChatCompletionMessage, - ]) - console.debug(`Sending messages: ${JSON.stringify(messages, null, 2)}`) + const messages: ChatCompletionMessage[] = [ + activeThread.assistants[0]?.instructions, + ] + .map((instructions) => { + const systemMessage: ChatCompletionMessage = { + role: ChatCompletionRole.System, + content: instructions, + } + return systemMessage + }) + .concat( + currentMessages + .map((msg) => ({ + role: msg.role, + content: msg.content[0]?.text.value ?? '', + })) + .concat([ + { + role: ChatCompletionRole.User, + content: prompt, + } as ChatCompletionMessage, + ]) + ) const msgId = ulid() const messageRequest: MessageRequest = { id: msgId, @@ -136,17 +151,17 @@ export default function useSendChatMessage() { ?.addNewMessage(threadMessage) const modelId = selectedModel?.id ?? activeThread.assistants[0].model.id + if (activeModel?.id !== modelId) { - toaster({ - title: 'Message queued.', - description: 'It will be sent once the model is done loading', - }) + setQueuedMessage(true) await startModel(modelId) + setQueuedMessage(false) } events.emit(EventName.OnMessageSent, messageRequest) } return { sendChatMessage, + queuedMessage, } } diff --git a/web/package.json b/web/package.json index 16522cace..922bc556a 100644 --- a/web/package.json +++ b/web/package.json @@ -33,6 +33,7 @@ "react-dom": "18.2.0", "react-hook-form": "^7.47.0", "react-hot-toast": "^2.4.1", + "react-scroll-to-bottom": "^4.2.0", "react-toastify": "^9.1.3", "sass": "^1.69.4", "tailwind-merge": "^2.0.0", @@ -48,6 +49,7 @@ "@types/node": "20.8.10", "@types/react": "18.2.34", "@types/react-dom": "18.2.14", + "@types/react-scroll-to-bottom": "^4.2.4", "@types/uuid": "^9.0.6", "@typescript-eslint/eslint-plugin": "^6.8.0", "@typescript-eslint/parser": "^6.8.0", diff --git a/web/screens/Chat/ChatBody/index.tsx b/web/screens/Chat/ChatBody/index.tsx index 10d008661..0a92b7a6c 100644 --- a/web/screens/Chat/ChatBody/index.tsx +++ b/web/screens/Chat/ChatBody/index.tsx @@ -1,17 +1,65 @@ +import { Fragment } from 'react' + +import ScrollToBottom from 'react-scroll-to-bottom' + +import { Button } from '@janhq/uikit' import { useAtomValue } from 'jotai' +import LogoMark from '@/containers/Brand/Logo/Mark' + +import { MainViewState } from '@/constants/screens' + +import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' + +import { useMainViewState } from '@/hooks/useMainViewState' + import ChatItem from '../ChatItem' import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' const ChatBody: React.FC = () => { const messages = useAtomValue(getCurrentChatMessagesAtom) + const { downloadedModels } = useGetDownloadedModels() + const { setMainViewState } = useMainViewState() + + if (downloadedModels.length === 0) + return ( +
+ +

Welcome!

+

You need to download your first model

+ +
+ ) + return ( -
- {messages.map((message) => ( - - ))} -
+ + {messages.length === 0 ? ( +
+ +

How can I help you?

+
+ ) : ( + + {messages.map((message) => ( + + ))} + + )} +
) } diff --git a/web/screens/Chat/ChatItem/index.tsx b/web/screens/Chat/ChatItem/index.tsx index 5f192d436..fcc6cbab5 100644 --- a/web/screens/Chat/ChatItem/index.tsx +++ b/web/screens/Chat/ChatItem/index.tsx @@ -7,10 +7,7 @@ import SimpleTextMessage from '../SimpleTextMessage' type Ref = HTMLDivElement const ChatItem = forwardRef((message, ref) => ( -
+
)) diff --git a/web/screens/Chat/MessageToolbar/index.tsx b/web/screens/Chat/MessageToolbar/index.tsx index 5fe432e62..5380c7e29 100644 --- a/web/screens/Chat/MessageToolbar/index.tsx +++ b/web/screens/Chat/MessageToolbar/index.tsx @@ -1,7 +1,4 @@ -import { useMemo } from 'react' - import { - ChatCompletionRole, ChatCompletionMessage, EventName, MessageRequest, @@ -11,8 +8,8 @@ import { events, } from '@janhq/core' import { ConversationalExtension, InferenceExtension } from '@janhq/core' -import { atom, useAtomValue, useSetAtom } from 'jotai' -import { RefreshCcw, ClipboardCopy, Trash2Icon, StopCircle } from 'lucide-react' +import { useAtomValue, useSetAtom } from 'jotai' +import { RefreshCcw, Copy, Trash2Icon, StopCircle } from 'lucide-react' import { twMerge } from 'tailwind-merge' @@ -23,21 +20,17 @@ import { deleteMessageAtom, getCurrentChatMessagesAtom, } from '@/helpers/atoms/ChatMessage.atom' -import { - activeThreadAtom, - threadStatesAtom, -} from '@/helpers/atoms/Conversation.atom' +import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom' const MessageToolbar = ({ message }: { message: ThreadMessage }) => { const deleteMessage = useSetAtom(deleteMessageAtom) const thread = useAtomValue(activeThreadAtom) const messages = useAtomValue(getCurrentChatMessagesAtom) - const threadStateAtom = useMemo( - () => atom((get) => get(threadStatesAtom)[thread?.id ?? '']), - [thread?.id] - ) - const threadState = useAtomValue(threadStateAtom) - + // const threadStateAtom = useMemo( + // () => atom((get) => get(threadStatesAtom)[thread?.id ?? '']), + // [thread?.id] + // ) + // const threadState = useAtomValue(threadStateAtom) const stopInference = async () => { await extensionManager .get(ExtensionType.Inference) @@ -51,12 +44,7 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => { } return ( -
+
{message.status === MessageStatus.Pending && (
{ }) }} > - +
(false) +export const showRightSideBarAtom = atom(true) export default function Sidebar() { const showing = useAtomValue(showRightSideBarAtom) const activeThread = useAtomValue(activeThreadAtom) const selectedModel = useAtomValue(selectedModelAtom) - const { updateThreadTitle } = useCreateNewThread() + const { updateThreadMetadata } = useCreateNewThread() const onReviewInFinderClick = async (type: string) => { if (!activeThread) return if (!activeThread.isFinishInit) { - alert('Thread is not ready') + alert('Thread is not started yet') return } @@ -56,7 +61,7 @@ export default function Sidebar() { const onViewJsonClick = async (type: string) => { if (!activeThread) return if (!activeThread.isFinishInit) { - alert('Thread is not ready') + alert('Thread is not started yet') return } @@ -87,44 +92,104 @@ export default function Sidebar() { return (
-
+
- - updateThreadTitle(title ?? '')} - /> +
+
+ + { + if (activeThread) + updateThreadMetadata({ + ...activeThread, + title: e.target.value || '', + }) + }} + /> +
+
+ + + {activeThread?.id || '-'} + +
+
- +
+
+ + + {activeThread?.assistants[0].assistant_name ?? '-'} + +
+
+ +