Unified Error Handling System
Implement a centralized, type-safe error handling system for Next.js applications with structured logging and internationalization support
使用
参考 ai elements 官方文档 的实践。也可以结合它的基础 ai-sdk 的的 sdk 。
"use client";// docs see: https://ai-sdk.dev/elements/examples/chatbotimport { Action, Actions } from "@/components/ai-elements/actions";import { Conversation, ConversationContent, ConversationScrollButton,} from "@/components/ai-elements/conversation";import { Loader } from "@/components/ai-elements/loader";import { Message, MessageContent } from "@/components/ai-elements/message";import { PromptInput, PromptInputActionAddAttachments, PromptInputActionMenu, PromptInputActionMenuContent, PromptInputActionMenuTrigger, PromptInputAttachment, PromptInputAttachments, PromptInputBody, PromptInputButton, type PromptInputMessage, PromptInputModelSelect, PromptInputModelSelectContent, PromptInputModelSelectItem, PromptInputModelSelectTrigger, PromptInputModelSelectValue, PromptInputSubmit, PromptInputTextarea, PromptInputToolbar, PromptInputTools,} from "@/components/ai-elements/prompt-input";import { Reasoning, ReasoningContent, ReasoningTrigger,} from "@/components/ai-elements/reasoning";import { Response } from "@/components/ai-elements/response";import { Source, Sources, SourcesContent, SourcesTrigger,} from "@/components/ai-elements/sources";import { useChat } from "@ai-sdk/react";import { CopyIcon, GlobeIcon, RefreshCcwIcon } from "lucide-react";import { Fragment, useState } from "react";const models = [ { name: "GPT 4o", value: "openai/gpt-4o", }, { name: "Deepseek R1", value: "deepseek/deepseek-r1", },];const ChatBotDemo = () => { const [input, setInput] = useState(""); const [model, setModel] = useState<string>(models[0].value); const [webSearch, setWebSearch] = useState(false); const { messages, sendMessage, status, regenerate } = useChat(); const handleSubmit = (message: PromptInputMessage) => { const hasText = Boolean(message.text); const hasAttachments = Boolean(message.files?.length); if (!(hasText || hasAttachments)) { return; } sendMessage( { text: message.text || "Sent with attachments", files: message.files, }, { body: { model: model, webSearch: webSearch, }, }, ); setInput(""); }; return ( <div className="relative mx-auto size-full h-screen max-w-4xl p-6"> <div className="flex h-full flex-col"> <Conversation className="h-full"> <ConversationContent> {messages.map((message) => ( <div key={message.id}> {message.role === "assistant" && message.parts.filter((part) => part.type === "source-url") .length > 0 && ( <Sources> <SourcesTrigger count={ message.parts.filter( (part) => part.type === "source-url", ).length } /> {message.parts .filter((part) => part.type === "source-url") .map((part, i) => ( <SourcesContent key={`${message.id}-${i}`}> <Source key={`${message.id}-${i}`} href={part.url} title={part.url} /> </SourcesContent> ))} </Sources> )} {message.parts.map((part, i) => { switch (part.type) { case "text": return ( <Fragment key={`${message.id}-${i}`}> <Message from={message.role}> <MessageContent> <Response>{part.text}</Response> </MessageContent> </Message> {message.role === "assistant" && i === messages.length - 1 && ( <Actions className="mt-2"> <Action onClick={() => regenerate()} label="Retry" > <RefreshCcwIcon className="size-3" /> </Action> <Action onClick={() => navigator.clipboard.writeText(part.text) } label="Copy" > <CopyIcon className="size-3" /> </Action> </Actions> )} </Fragment> ); case "reasoning": return ( <Reasoning key={`${message.id}-${i}`} className="w-full" isStreaming={ status === "streaming" && i === message.parts.length - 1 && message.id === messages.at(-1)?.id } > <ReasoningTrigger /> <ReasoningContent>{part.text}</ReasoningContent> </Reasoning> ); default: return null; } })} </div> ))} {status === "submitted" && <Loader />} </ConversationContent> <ConversationScrollButton /> </Conversation> <PromptInput onSubmit={handleSubmit} className="mt-4" globalDrop multiple > <PromptInputBody> <PromptInputAttachments> {(attachment) => <PromptInputAttachment data={attachment} />} </PromptInputAttachments> <PromptInputTextarea onChange={(e) => setInput(e.target.value)} value={input} /> </PromptInputBody> <PromptInputToolbar> <PromptInputTools> <PromptInputActionMenu> <PromptInputActionMenuTrigger /> <PromptInputActionMenuContent> <PromptInputActionAddAttachments /> </PromptInputActionMenuContent> </PromptInputActionMenu> <PromptInputButton variant={webSearch ? "default" : "ghost"} onClick={() => setWebSearch(!webSearch)} > <GlobeIcon size={16} /> <span>Search</span> </PromptInputButton> <PromptInputModelSelect onValueChange={(value) => { setModel(value); }} value={model} > <PromptInputModelSelectTrigger> <PromptInputModelSelectValue /> </PromptInputModelSelectTrigger> <PromptInputModelSelectContent> {models.map((model) => ( <PromptInputModelSelectItem key={model.value} value={model.value} > {model.name} </PromptInputModelSelectItem> ))} </PromptInputModelSelectContent> </PromptInputModelSelect> </PromptInputTools> <PromptInputSubmit disabled={!input && !status} status={status} /> </PromptInputToolbar> </PromptInput> </div> </div> );};export default ChatBotDemo;