diff --git a/app/src/assets/main.less b/app/src/assets/main.less index 331776d..6f05de9 100644 --- a/app/src/assets/main.less +++ b/app/src/assets/main.less @@ -117,3 +117,37 @@ strong { .cent { font-weight: normal !important; } + +.error-boundary { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: max-content; + min-height: calc(100vh - 56px); + overflow: hidden; + background: hsl(var(--background-container)); + padding: 2.5rem 5rem; + + @media (max-width: 720px) { + & { + padding: 2.5rem 1rem; + } + } + + .error-provider { + text-align: center; + margin: 0.5rem auto; + + p { + margin: 0.35rem; + } + } + + .error-tips { + text-align: center; + padding: 1rem 2rem; + color: hsl(var(--text-secondary)); + } +} diff --git a/app/src/components/ErrorBoundary.tsx b/app/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..6a37b64 --- /dev/null +++ b/app/src/components/ErrorBoundary.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { AlertCircle, Download } from "lucide-react"; +import { withTranslation, WithTranslation } from "react-i18next"; +import { version } from "@/conf.ts"; +import { getMemoryPerformance } from "@/utils/app.ts"; +import { Button } from "@/components/ui/button.tsx"; +import { saveAsFile } from "@/utils/dom.ts"; + +type ErrorBoundaryProps = { children: React.ReactNode } & WithTranslation; + +class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + { errorCaught: Error | null } +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { errorCaught: null }; + } + + static getDerivedStateFromError(error: Error) { + return { errorCaught: error }; + } + + render() { + const { t } = this.props; + const ua = navigator.userAgent || "unknown"; + const memory = getMemoryPerformance(); + const time = new Date().toLocaleString(); + const stamp = new Date().getTime(); + const path = window.location.pathname; + + const message = `Raised-Path: ${path}\nApp-Version: ${version}\nMemory-Usage: ${ + !isNaN(memory) ? memory.toFixed(2) + " MB" : "unknown" + }\nLocale-Time: ${time}\nError-Message: ${ + this.state.errorCaught?.message || "unknown" + }\nUser-Agent: ${ua}\nStack-Trace: ${ + this.state.errorCaught?.stack || "unknown" + }`; + + return this.state.errorCaught ? ( +
+ +

{t("fatal")}

+
+

Raised-Path: {path}

+

App-Version: {version}

+

+ Memory-Usage:{" "} + {!isNaN(memory) ? memory.toFixed(2) + " MB" : "unknown"} +

+

Locale-Time: {time}

+

Error-Message: {this.state.errorCaught.message}

+

User-Agent: {ua}

+
+
+ +
+
+

{t("fatal-tips")}

+
+
+ ) : ( + this.props.children + ); + } +} + +export default withTranslation()(ErrorBoundary); diff --git a/app/src/components/Message.tsx b/app/src/components/Message.tsx index 6abdb3c..af1a32c 100644 --- a/app/src/components/Message.tsx +++ b/app/src/components/Message.tsx @@ -1,6 +1,7 @@ import { Message } from "@/api/types.ts"; import Markdown from "@/components/Markdown.tsx"; import { + CircleSlash, Cloud, CloudFog, Copy, @@ -77,6 +78,8 @@ function MessageContent({ message, end, onEvent }: MessageProps) {
{message.content.length ? ( + ) : message.end === true ? ( + ) : ( )} diff --git a/app/src/i18n.ts b/app/src/i18n.ts index b0b0282..057622e 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -34,6 +34,10 @@ const resources = { unknown: "未知", "scroll-down": "滚至最新", broadcast: "公告", + fatal: "应用崩溃", + "download-fatal-log": "下载错误日志", + "fatal-tips": + "请您先检查您的网络,浏览器兼容性,尝试清除浏览器缓存并刷新页面。如果问题仍然存在,请将日志提供给开发者以便我们排查问题。", tag: { free: "免费", official: "官方", @@ -370,6 +374,10 @@ const resources = { unknown: "Unknown", "scroll-down": "Scroll to latest", broadcast: "Broadcast", + fatal: "App crashed", + "download-fatal-log": "Download error log", + "fatal-tips": + "Please try to check your network, browser compatibility, try to clear the browser cache and refresh the page. If the problem still exists, please provide the log to the developer so that we can troubleshoot the problem.", tag: { free: "Free", official: "Official", @@ -722,6 +730,10 @@ const resources = { unknown: "Неизвестный", "scroll-down": "Прокрутите вниз", broadcast: "Объявление", + fatal: "Приложение вылетело", + "download-fatal-log": "Скачать журнал ошибок", + "fatal-tips": + "Пожалуйста, попробуйте проверить свою сеть, совместимость браузера, попробуйте очистить кэш браузера и обновить страницу. Если проблема все еще существует, пожалуйста, предоставьте журнал разработчику, чтобы мы могли устранить проблему.", tag: { free: "Бесплатно", official: "Официальный", diff --git a/app/src/routes/Home.tsx b/app/src/routes/Home.tsx index 5ec878c..7053ecf 100644 --- a/app/src/routes/Home.tsx +++ b/app/src/routes/Home.tsx @@ -5,15 +5,18 @@ import SideBar from "@/components/home/SideBar.tsx"; import { useSelector } from "react-redux"; import { selectMarket } from "@/store/chat.ts"; import ModelMarket from "@/components/home/ModelMarket.tsx"; +import ErrorBoundary from "@/components/ErrorBoundary.tsx"; function Home() { const market = useSelector(selectMarket); return ( -
- - {market ? : } -
+ +
+ + {market ? : } +
+
); }