diff --git a/api/errorHandle.ts b/api/errorHandle.ts index 3adaafd..6fdac06 100644 --- a/api/errorHandle.ts +++ b/api/errorHandle.ts @@ -16,22 +16,52 @@ const HTTP_ERROR_MESSAGES: Record = { 503: "Service temporarily unavailable, please try again later.", 504: "Gateway timeout, please try again later.", }; + /** * 默认错误提示信息 */ -const DEFAULT_ERROR_MESSAGE = - "Please try again if the network is abnormal. If it happens again, please contact us."; +const DEFAULT_ERROR_MESSAGE = "网络异常,请重试。如果问题持续存在,请联系我们。"; /** - * 根据错误码显示对应的提示信息 + * 特殊错误码的处理函数 + */ +const ERROR_HANDLERS: Record void> = { + 401: () => { + // 清除本地存储的 token + localStorage.removeItem('token'); + // 跳转到登录页面 + window.location.href = '/login'; + }, + 403: () => { + // 显示积分不足通知 + import('../utils/notifications').then(({ showInsufficientPointsNotification }) => { + showInsufficientPointsNotification(); + }); + } +}; + +/** + * 根据错误码显示对应的提示信息并执行相应处理 * @param code - HTTP错误码 * @param customMessage - 自定义错误信息(可选) */ export const errorHandle = debounce( (code: number, customMessage?: string): void => { - const errorMessage = + const errorMessage = customMessage || HTTP_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGE; - message.error(errorMessage); + + // 显示错误提示 + message.error({ + content: errorMessage, + duration: 3, + className: 'custom-error-message' + }); + + // 执行特殊错误码的处理函数 + const handler = ERROR_HANDLERS[code]; + if (handler) { + handler(); + } }, 100 ); diff --git a/api/request.ts b/api/request.ts index 4f86d78..73a19bd 100644 --- a/api/request.ts +++ b/api/request.ts @@ -2,6 +2,25 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosR import { message } from "antd"; import { BASE_URL } from './constants' import { errorHandle } from './errorHandle'; + +/** + * 统一的错误处理函数 + * @param error - 错误对象 + * @param defaultMessage - 默认错误信息 + */ +const handleRequestError = (error: any, defaultMessage: string = '请求失败') => { + if (error.response) { + const { status, data } = error.response; + const errorMessage = data?.message || defaultMessage; + errorHandle(status, errorMessage); + } else if (error.request) { + // 请求已发出但没有收到响应 + errorHandle(0, '网络请求失败,请检查网络连接'); + } else { + // 请求配置出错 + errorHandle(0, error.message || defaultMessage); + } +}; // 创建 axios 实例 const request: AxiosInstance = axios.create({ baseURL: BASE_URL, // 设置基础URL @@ -29,23 +48,32 @@ request.interceptors.request.use( // 响应拦截器 request.interceptors.response.use( (response: AxiosResponse) => { - // 直接返回响应数据 - if (response.data?.code !=0) { - // TODO 暂时固定报错信息,后续根据后端返回的错误码进行处理 - errorHandle(0); + // 检查业务状态码 + if (response.data?.code !== 0) { + // 处理业务层面的错误 + const businessCode = response.data?.code; + const errorMessage = response.data?.message; + + // 特殊处理 401 和 403 业务状态码 + if (businessCode === 401) { + errorHandle(401, errorMessage); + return Promise.reject(new Error(errorMessage)); + } + + if (businessCode === 403) { + errorHandle(403, errorMessage); + return Promise.reject(new Error(errorMessage)); + } + + // 其他业务错误 + errorHandle(0, errorMessage); + return Promise.reject(new Error(errorMessage)); } + return response.data; }, (error) => { - if (error.response) { - errorHandle(error.response.status); - } else if (error.request) { - // 请求已发出但没有收到响应 - errorHandle(0); - } else { - // 检修 - console.error(error); - } + handleRequestError(error); return Promise.reject(error); } ); @@ -86,8 +114,23 @@ export async function streamJsonPost( body: JSON.stringify(body), }); + // 处理 HTTP 错误状态 if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + const error = { + response: { + status: response.status, + data: { message: await response.text().then(text => { + try { + const data = JSON.parse(text); + return data.message || `HTTP error! status: ${response.status}`; + } catch { + return `HTTP error! status: ${response.status}`; + } + })} + } + }; + handleRequestError(error); + throw error; } if (!response.body) { @@ -207,7 +250,8 @@ export const stream = async ({ const response = await request(config); onComplete?.(); return response; - } catch (error) { + } catch (error: any) { + handleRequestError(error, '流式请求失败'); onError?.(error); throw error; } @@ -239,8 +283,9 @@ export const downloadStream = async ( window.URL.revokeObjectURL(downloadUrl); return response; - } catch (error) { + } catch (error: any) { console.error('文件下载失败:', error); + handleRequestError(error, '文件下载失败'); throw error; } }; diff --git a/app/globals.css b/app/globals.css index 6916fe7..a37fc43 100644 --- a/app/globals.css +++ b/app/globals.css @@ -239,3 +239,7 @@ body { .animate-fade-in { animation: fade-in 0.2s ease-out forwards; } + +.ant-notification-notice-wrapper { + background: transparent !important; +} \ No newline at end of file diff --git a/components/SmartChatBox/MessageRenderer.tsx b/components/SmartChatBox/MessageRenderer.tsx index f158c7c..510b509 100644 --- a/components/SmartChatBox/MessageRenderer.tsx +++ b/components/SmartChatBox/MessageRenderer.tsx @@ -124,6 +124,8 @@ export function MessageRenderer({ msg }: MessageRendererProps) { ); case "progress": return ; + case "link": + return {b.text}; default: return null; } diff --git a/components/SmartChatBox/api.ts b/components/SmartChatBox/api.ts index 7632e90..e96e942 100644 --- a/components/SmartChatBox/api.ts +++ b/components/SmartChatBox/api.ts @@ -25,7 +25,7 @@ const EMPTY_MESSAGES: RealApiMessage[] = [ role: 'assistant', content: JSON.stringify([{ type: 'text', - content: '🌟欢迎来到 MovieFlow 🎬✨\n快把您的创意告诉我吧~💡\n我是您的专属AI小伙伴🤖,可以帮您:\n🎭 生成专属演员形象\n📽️ 搭建场景 & 分镜\n🎞️ 完成整部视频创作\n\n一起开启奇妙的创作之旅吧!❤️' + content: '🌟Welcome to MovieFlow 🎬✨\nTell me your idea~💡\nI am your AI assistant🤖, I can help you:\n🎭 Generate actor images\n📽️ Generate scene & shot sketches\n🎞️ Complete video creation\n\nLet\'s start our creative journey together!❤️' }]), created_at: new Date().toISOString(), function_name: undefined, @@ -35,6 +35,19 @@ const EMPTY_MESSAGES: RealApiMessage[] = [ } ]; +// 用户积分不足消息 +const NoEnoughCreditsMessageBlocks: MessageBlock[] = [ + { + type: 'text', + text: 'Insufficient credits.' + }, + { + type: 'link', + text: 'Upgrade to continue.', + url: '/pricing' + } +]; + /** * 类型守卫函数 */ @@ -195,7 +208,7 @@ function transformSystemMessage( */ function transformMessage(apiMessage: RealApiMessage): ChatMessage { try { - const { id, role, content, created_at, function_name, custom_data, status, intent_type } = apiMessage; + const { id, role, content, created_at, function_name, custom_data, status, intent_type, error_message } = apiMessage; let message: ChatMessage = { id: id ? id.toString() : Date.now().toString(), role: role, @@ -205,30 +218,36 @@ function transformMessage(apiMessage: RealApiMessage): ChatMessage { status: status || 'success', }; - if (role === 'assistant' || role === 'user') { - try { - const contentObj = JSON.parse(content); - const contentArray = Array.isArray(contentObj) ? contentObj : [contentObj]; - contentArray.forEach((c: ApiMessageContent) => { - if (c.type === "text") { - message.blocks.push({ type: "text", text: c.content }); - } else if (c.type === "image") { - message.blocks.push({ type: "image", url: c.content }); - } else if (c.type === "video") { - message.blocks.push({ type: "video", url: c.content }); - } else if (c.type === "audio") { - message.blocks.push({ type: "audio", url: c.content }); - } - }); - } catch (error) { - // 如果 JSON 解析失败,将整个 content 作为文本内容 + if (error_message && error_message === 'no enough credits') { + message.blocks = NoEnoughCreditsMessageBlocks; + } else { + if (role === 'assistant' || role === 'user') { + try { + const contentObj = JSON.parse(content); + const contentArray = Array.isArray(contentObj) ? contentObj : [contentObj]; + contentArray.forEach((c: ApiMessageContent) => { + if (c.type === "text") { + message.blocks.push({ type: "text", text: c.content }); + } else if (c.type === "image") { + message.blocks.push({ type: "image", url: c.content }); + } else if (c.type === "video") { + message.blocks.push({ type: "video", url: c.content }); + } else if (c.type === "audio") { + message.blocks.push({ type: "audio", url: c.content }); + } else if (c.type === "link") { + message.blocks.push({ type: "link", text: c.content, url: c.url || '' }); + } + }); + } catch (error) { + // 如果 JSON 解析失败,将整个 content 作为文本内容 + message.blocks.push({ type: "text", text: content }); + } + } else if (role === 'system' && function_name && custom_data) { + // 处理系统消息 + message.blocks = transformSystemMessage(function_name, content, custom_data); + } else { message.blocks.push({ type: "text", text: content }); } - } else if (role === 'system' && function_name && custom_data) { - // 处理系统消息 - message.blocks = transformSystemMessage(function_name, content, custom_data); - } else { - message.blocks.push({ type: "text", text: content }); } // 如果没有有效的 blocks,至少添加一个文本块 diff --git a/components/SmartChatBox/types.ts b/components/SmartChatBox/types.ts index 66a2fbb..3587c56 100644 --- a/components/SmartChatBox/types.ts +++ b/components/SmartChatBox/types.ts @@ -7,7 +7,8 @@ export type MessageBlock = | { type: "image"; url: string; alt?: string } | { type: "video"; url: string; poster?: string } | { type: "audio"; url: string } - | { type: "progress"; value: number; total?: number; label?: string }; + | { type: "progress"; value: number; total?: number; label?: string } + | { type: "link"; text: string; url: string }; export interface ChatMessage { id: string; @@ -127,6 +128,7 @@ export interface ShotVideoGeneration { export interface ApiMessageContent { type: ContentType; content: string; + url?: string; } export interface RealApiMessage { @@ -138,4 +140,5 @@ export interface RealApiMessage { custom_data?: ProjectInit | ScriptSummary | CharacterGeneration | SketchGeneration | ShotSketchGeneration | ShotVideoGeneration; status: MessageStatus; intent_type: IntentType; + error_message?: string; } \ No newline at end of file diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index 7391168..cf8082f 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -25,6 +25,7 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT const menuRef = useRef(null); const buttonRef = useRef(null); const currentUser = localStorage.getItem('currentUser'); + const [openModal, setOpenModal] = React.useState(false); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -102,9 +103,9 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT {/* Notifications */} - + */} {/* Theme Toggle */} {/* + + ), + duration: 5, + placement: 'topRight', + style: darkGlassStyle, + className: 'dark-glass-notification', + closeIcon: ( + + ), + }); +}; + +/** + * 全局配置通知样式 + */ +notification.config({ + maxCount: 3, // 最多同时显示3个通知 +});