"use client"; import { useState } from "react"; import { ImagePlus, LoaderCircle, Send, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { httpRequest } from "@/lib/request"; import { pretty, type ChatCompletionResponse, type ChatContentPart, type ChatMessage } from "./types"; type SelectedImage = { id: string; name: string; size: number; url: string; }; const MAX_IMAGE_BYTES = 10 * 1024 * 1024; function readImage(file: File): Promise { return new Promise((resolve, reject) => { if (!file.type.startsWith("image/")) { reject(new Error(`${file.name} 不是图片文件`)); return; } if (file.size > MAX_IMAGE_BYTES) { reject(new Error(`${file.name} 超过 10MB`)); return; } const reader = new FileReader(); reader.onload = () => { const url = String(reader.result || ""); if (!url.startsWith("data:image/")) { reject(new Error(`${file.name} 读取失败`)); return; } resolve({ id: `${file.name}-${file.size}-${file.lastModified}-${Math.random().toString(16).slice(2)}`, name: file.name, size: file.size, url, }); }; reader.onerror = () => reject(reader.error || new Error(`${file.name} 读取失败`)); reader.readAsDataURL(file); }); } function messageText(message: ChatMessage): string { if (typeof message.content === "string") { return message.content; } return message.content .filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text) .join(""); } function messageImages(message: ChatMessage): string[] { if (!Array.isArray(message.content)) { return []; } return message.content .filter((part): part is { type: "image_url"; image_url: { url: string } } => part.type === "image_url") .map((part) => part.image_url.url); } export function ChatPanel() { const [model, setModel] = useState("auto"); const [reasoningEffort, setReasoningEffort] = useState(""); const [input, setInput] = useState("你好,先记住我的项目叫 chatgpt2api。"); const [selectedImages, setSelectedImages] = useState([]); const [messages, setMessages] = useState([]); const [raw, setRaw] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const handleImagesChange = async (files: FileList | null) => { if (!files?.length) return; setError(""); try { const images = await Promise.all(Array.from(files).map(readImage)); setSelectedImages((current) => [...current, ...images].slice(0, 4)); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }; const sendChat = async () => { const text = input.trim(); if (!text && !selectedImages.length) return; const content: string | ChatContentPart[] = selectedImages.length ? [ ...(text ? [{ type: "text" as const, text }] : []), ...selectedImages.map((image) => ({ type: "image_url" as const, image_url: { url: image.url } })), ] : text; const nextMessages: ChatMessage[] = [...messages, { role: "user", content }]; setMessages(nextMessages); setInput(""); setSelectedImages([]); setLoading(true); setError(""); try { const body = { model: model.trim() || "auto", messages: nextMessages, ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}), }; const result = await httpRequest("/v1/chat/completions", { method: "POST", body }); setRaw(result); setMessages([...nextMessages, { role: "assistant", content: String(result.choices?.[0]?.message?.content || "") }]); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }; const clearChat = () => { setMessages([]); setSelectedImages([]); setRaw(null); setError(""); }; return (

请求

setModel(event.target.value)} className="rounded-md border-stone-200/70 bg-transparent shadow-none dark:border-white/10" />