diff --git a/web/containers/ModalTroubleShoot/AppLogs.tsx b/web/containers/ModalTroubleShoot/AppLogs.tsx new file mode 100644 index 000000000..d4f6bddb8 --- /dev/null +++ b/web/containers/ModalTroubleShoot/AppLogs.tsx @@ -0,0 +1,203 @@ +import React, { useEffect, useState } from 'react' + +import { Button } from '@janhq/uikit' + +import { CopyIcon, CheckIcon } from 'lucide-react' + +import { useClipboard } from '@/hooks/useClipboard' +import { useLogs } from '@/hooks/useLogs' + +const AppLogs = () => { + const { getLogs } = useLogs() + const [logs, setLogs] = useState([]) + + useEffect(() => { + getLogs('app').then((log) => { + if (typeof log?.split === 'function') { + setLogs(log.split(/\r?\n|\r|\n/g)) + } + }) + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const clipboard = useClipboard({ timeout: 1000 }) + + return ( + <> +
+ +
+
+ {logs.length > 1 ? ( +
+ + {logs.slice(-100).map((log, i) => { + return ( +

+ {log} +

+ ) + })} +
+
+ ) : ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Empty logs

+
+ )} +
+ + ) +} + +export default AppLogs diff --git a/web/containers/ModalTroubleShoot/DeviceSpecs.tsx b/web/containers/ModalTroubleShoot/DeviceSpecs.tsx new file mode 100644 index 000000000..5ebb610d1 --- /dev/null +++ b/web/containers/ModalTroubleShoot/DeviceSpecs.tsx @@ -0,0 +1,46 @@ +import React from 'react' + +import { Button } from '@janhq/uikit' + +import { CopyIcon, CheckIcon } from 'lucide-react' + +import { useClipboard } from '@/hooks/useClipboard' + +// TODO @Louis help add missing information device specs +const DeviceSpecs = () => { + const userAgent = window.navigator.userAgent + const clipboard = useClipboard({ timeout: 1000 }) + + return ( + <> +
+ +
+
+

{userAgent}

+
+ + ) +} + +export default DeviceSpecs diff --git a/web/containers/ModalTroubleShoot/index.tsx b/web/containers/ModalTroubleShoot/index.tsx new file mode 100644 index 000000000..547398c4f --- /dev/null +++ b/web/containers/ModalTroubleShoot/index.tsx @@ -0,0 +1,121 @@ +import { useState } from 'react' +import ScrollToBottom from 'react-scroll-to-bottom' + +import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit' +import { motion as m } from 'framer-motion' +import { atom, useAtom } from 'jotai' +import { twMerge } from 'tailwind-merge' + +import ServerLogs from '../ServerLogs' + +import AppLogs from './AppLogs' +import DeviceSpecs from './DeviceSpecs' + +export const modalTroubleShootingAtom = atom(false) +const logOption = ['App Logs', 'Server Logs', 'Device Specs'] + +const ModalTroubleShooting: React.FC = () => { + const [modalTroubleShooting, setModalTroubleShooting] = useAtom( + modalTroubleShootingAtom + ) + const [isTabActive, setIsTabActivbe] = useState(0) + + return ( + + + + Troubleshooting Assistance + +

+ {`We're here to help! Your report is crucial for debugging and shaping + the next version. Here’s how you can report & get further support:`} +

+ +
+

Step 1

+

+ Follow our  + + troubleshooting guide + +  for step-by-step solutions. +

+
+ +
+
+

Step 2

+

+ {`If you can't find what you need in our troubleshooting guide, feel + free reach out to us for extra help:`} +

+
    +
  • +

    + Copy your 2-hour logs & device specifications provided below.{' '} +

    +
  • +
  • +

    + Go to our  + + Discord + +   & send it to #🆘|get-help channel for further support. +

    +
  • +
+
+ +
+ {/* TODO @faisal replace this once we have better tabs component UI */} +
+
    + {logOption.map((name, i) => { + return ( +
  • setIsTabActivbe(i)} + > + + {name} + + {isTabActive === i && ( + + )} +
  • + ) + })} +
+
+ + {isTabActive === 0 && } + {isTabActive === 1 && } + {isTabActive === 2 && } + +
+
+
+
+ ) +} + +export default ModalTroubleShooting diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx index cfd2c5629..170ec5e64 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -95,12 +95,12 @@ export default function EventHandler({ children }: { children: ReactNode }) { (res: any) => { const errorMessage = `${res.error}` console.error('Failed to load model: ' + errorMessage) - setLoadModelError(errorMessage) setStateModel(() => ({ state: 'start', loading: false, model: res.modelId, })) + setLoadModelError(errorMessage) setQueuedMessage(false) }, [setStateModel, setQueuedMessage, setLoadModelError] diff --git a/web/containers/ServerLogs/index.tsx b/web/containers/ServerLogs/index.tsx new file mode 100644 index 000000000..c97643769 --- /dev/null +++ b/web/containers/ServerLogs/index.tsx @@ -0,0 +1,213 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { useEffect, useState } from 'react' + +import React from 'react' + +import { Button } from '@janhq/uikit' +import { useAtomValue } from 'jotai' + +import { CopyIcon, CheckIcon } from 'lucide-react' + +import { useClipboard } from '@/hooks/useClipboard' +import { useLogs } from '@/hooks/useLogs' + +import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' + +type ServerLogsProps = { limit?: number; withCopy?: boolean } + +const ServerLogs = (props: ServerLogsProps) => { + const { limit = 0 } = props + const { getLogs } = useLogs() + const serverEnabled = useAtomValue(serverEnabledAtom) + const [logs, setLogs] = useState([]) + + const clipboard = useClipboard({ timeout: 1000 }) + + useEffect(() => { + getLogs('server').then((log) => { + if (typeof log?.split === 'function') { + setLogs(log.split(/\r?\n|\r|\n/g)) + } + }) + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [logs, serverEnabled]) + + return ( + <> +
+ +
+
+ {logs.length > 1 ? ( +
+ + {logs.slice(-limit).map((log, i) => { + return ( +

+ {log} +

+ ) + })} +
+
+ ) : ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Empty logs

+
+ )} +
+ + ) +} + +export default ServerLogs diff --git a/web/hooks/useServerLog.ts b/web/hooks/useLogs.tsx similarity index 67% rename from web/hooks/useServerLog.ts rename to web/hooks/useLogs.tsx index b263534b6..7c504428f 100644 --- a/web/hooks/useServerLog.ts +++ b/web/hooks/useLogs.tsx @@ -5,12 +5,12 @@ import { getJanDataFolderPath, } from '@janhq/core' -export const useServerLog = () => { - const getServerLog = async () => { - if (!(await fs.existsSync(await joinPath(['file://logs', 'server.log'])))) +export const useLogs = () => { + const getLogs = async (file: string) => { + if (!(await fs.existsSync(await joinPath(['file://logs', `${file}.log`])))) return {} const logs = await fs.readFileSync( - await joinPath(['file://logs', 'server.log']), + await joinPath(['file://logs', `${file}.log`]), 'utf-8' ) @@ -25,5 +25,5 @@ export const useServerLog = () => { const clearServerLog = async () => { await fs.writeFileSync(await joinPath(['file://logs', 'server.log']), '') } - return { getServerLog, openServerLog, clearServerLog } + return { getLogs, openServerLog, clearServerLog } } diff --git a/web/screens/Chat/ChatBody/index.tsx b/web/screens/Chat/ChatBody/index.tsx index 0e8d55c0b..ee0b4592d 100644 --- a/web/screens/Chat/ChatBody/index.tsx +++ b/web/screens/Chat/ChatBody/index.tsx @@ -81,7 +81,8 @@ const ChatBody: React.FC = () => { {messages.map((message, index) => (
- {(message.status !== MessageStatus.Pending || + {((message.status !== MessageStatus.Error && + message.status !== MessageStatus.Pending) || message.content.length > 0) && ( )} diff --git a/web/screens/Chat/ErrorMessage/index.tsx b/web/screens/Chat/ErrorMessage/index.tsx index b73884659..ea9906335 100644 --- a/web/screens/Chat/ErrorMessage/index.tsx +++ b/web/screens/Chat/ErrorMessage/index.tsx @@ -9,6 +9,10 @@ import { Button } from '@janhq/uikit' import { useAtomValue, useSetAtom } from 'jotai' import { RefreshCcw } from 'lucide-react' +import ModalTroubleShooting, { + modalTroubleShootingAtom, +} from '@/containers/ModalTroubleShoot' + import useSendChatMessage from '@/hooks/useSendChatMessage' import { extensionManager } from '@/extension' @@ -23,6 +27,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => { const thread = useAtomValue(activeThreadAtom) const deleteMessage = useSetAtom(deleteMessageAtom) const { resendChatMessage } = useSendChatMessage() + const setModalTroubleShooting = useSetAtom(modalTroubleShootingAtom) const regenerateMessage = async () => { const lastMessageIndex = messages.length - 1 @@ -64,29 +69,22 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
)} {message.status === MessageStatus.Error && ( -
- - <> -

Apologies, something's amiss!

- Jan's in beta. Find troubleshooting guides{' '} - - here - {' '} - or reach out to us on{' '} - - Discord - {' '} - for assistance. - -
+
+

{`Apologies, something’s amiss!`}

+

+ Jan’s in beta. Access  + setModalTroubleShooting(true)} + > + troubleshooting assistance + +  now. +

+
)} diff --git a/web/screens/LocalServer/Logs.tsx b/web/screens/LocalServer/Logs.tsx deleted file mode 100644 index 125bd93ef..000000000 --- a/web/screens/LocalServer/Logs.tsx +++ /dev/null @@ -1,179 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { useEffect, useState } from 'react' - -import React from 'react' - -import { useAtomValue } from 'jotai' - -import { useServerLog } from '@/hooks/useServerLog' - -import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' - -const Logs = () => { - const { getServerLog } = useServerLog() - const serverEnabled = useAtomValue(serverEnabledAtom) - const [logs, setLogs] = useState([]) - - useEffect(() => { - getServerLog().then((log) => { - if (typeof log?.split === 'function') { - setLogs(log.split(/\r?\n|\r|\n/g)) - } - }) - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [logs, serverEnabled]) - - return ( -
- {logs.length > 1 ? ( -
- - {logs.map((log, i) => { - return ( -

- {log} -

- ) - })} -
-
- ) : ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Empty logs

-
- )} -
- ) -} - -export default Logs diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx index b96f4c228..65b6c8563 100644 --- a/web/screens/LocalServer/index.tsx +++ b/web/screens/LocalServer/index.tsx @@ -20,11 +20,12 @@ import { SelectValue, } from '@janhq/uikit' -import { atom, useAtom, useAtomValue } from 'jotai' +import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' import { Paintbrush, CodeIcon } from 'lucide-react' import { ExternalLinkIcon, InfoIcon } from 'lucide-react' +import { AlertTriangleIcon } from 'lucide-react' import { twMerge } from 'tailwind-merge' import CardSidebar from '@/containers/CardSidebar' @@ -33,8 +34,13 @@ import DropdownListSidebar, { selectedModelAtom, } from '@/containers/DropdownListSidebar' -import { useActiveModel } from '@/hooks/useActiveModel' -import { useServerLog } from '@/hooks/useServerLog' +import ModalTroubleShooting, { + modalTroubleShootingAtom, +} from '@/containers/ModalTroubleShoot' +import ServerLogs from '@/containers/ServerLogs' + +import { loadModelErrorAtom, useActiveModel } from '@/hooks/useActiveModel' +import { useLogs } from '@/hooks/useLogs' import { getConfigurationsData } from '@/utils/componentSettings' import { toSettingParams } from '@/utils/modelParam' @@ -45,8 +51,6 @@ import SettingComponentBuilder from '../Chat/ModelSetting/SettingComponent' import { showRightSideBarAtom } from '../Chat/Sidebar' -import Logs from './Logs' - import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' import { getActiveThreadModelParamsAtom } from '@/helpers/atoms/Thread.atom' @@ -60,11 +64,12 @@ const LocalServerScreen = () => { const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom) const showRightSideBar = useAtomValue(showRightSideBarAtom) const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) + const setModalTroubleShooting = useSetAtom(modalTroubleShootingAtom) const modelEngineParams = toSettingParams(activeModelParams) const componentDataEngineSetting = getConfigurationsData(modelEngineParams) - const { openServerLog, clearServerLog } = useServerLog() + const { openServerLog, clearServerLog } = useLogs() const { startModel, stateModel } = useActiveModel() const selectedModel = useAtomValue(selectedModelAtom) @@ -72,6 +77,7 @@ const LocalServerScreen = () => { const [isVerboseEnabled, setIsVerboseEnabled] = useAtom(verboseEnabledAtom) const [host, setHost] = useAtom(hostAtom) const [port, setPort] = useAtom(portAtom) + const [loadModelError, setLoadModelError] = useAtom(loadModelErrorAtom) const hostOptions = ['127.0.0.1', '0.0.0.0'] @@ -122,6 +128,7 @@ const LocalServerScreen = () => { if (serverEnabled) { window.core?.api?.stopServer() setServerEnabled(false) + setLoadModelError(undefined) } else { startModel(String(selectedModel?.id)) window.core?.api?.startServer({ @@ -350,7 +357,9 @@ const LocalServerScreen = () => {
) : ( - +
+ +
)}
@@ -365,6 +374,20 @@ const LocalServerScreen = () => { >
+ {loadModelError && ( +
+ + + Model failed to start. Access{' '} + setModalTroubleShooting(true)} + > + troubleshooting assistance + + +
+ )} {componentDataEngineSetting.filter( (x) => x.name === 'prompt_template' @@ -393,6 +416,7 @@ const LocalServerScreen = () => { )}
+ ) } diff --git a/web/tsconfig.json b/web/tsconfig.json index 26f0e8ef3..1729c971f 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -17,13 +17,13 @@ "incremental": true, "plugins": [ { - "name": "next", - }, + "name": "next" + } ], "paths": { - "@/*": ["./*"], - }, + "@/*": ["./*"] + } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"], + "exclude": ["node_modules"] }