Sawana Huang Avatar

Sawana Huang

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;

On this page