kit初版,模型引入,agent优化

This commit is contained in:
2026-04-13 00:41:23 +08:00
parent 9b053e302b
commit c435ab15cf
14097 changed files with 5032 additions and 2676248 deletions

View File

@@ -1,25 +1,52 @@
"use client";
import { useRef, useState, type KeyboardEvent } from "react";
import { useRef, useState, useCallback, type KeyboardEvent, type DragEvent } from "react";
import { ModelSelector } from "./model-selector";
import { uploadRefImage, type UploadProgress } from "@/lib/api";
type UploadStatus = "idle" | "uploading" | "done" | "error";
interface UploadState {
status: UploadStatus;
/** 本地预览 URL */
previewUrl: string | null;
/** 上传成功后的服务端路径 */
serverUrl: string | null;
/** 上传进度 0-100 */
progress: number;
/** 错误信息 */
errorMsg: string | null;
}
const INITIAL_UPLOAD: UploadState = {
status: "idle",
previewUrl: null,
serverUrl: null,
progress: 0,
errorMsg: null,
};
interface ChatInputProps {
onSend: (text: string, refImage: File | null) => void;
onSend: (text: string, refImageServerUrl: string | null, imageModel: string | null) => void;
disabled: boolean;
}
export function ChatInput({ onSend, disabled }: ChatInputProps) {
const [text, setText] = useState("");
const [refImage, setRefImage] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [upload, setUpload] = useState<UploadState>(INITIAL_UPLOAD);
const [isDragging, setIsDragging] = useState(false);
const [selectedModel, setSelectedModel] = useState<string>("");
const fileInputRef = useRef<HTMLInputElement>(null);
const abortRef = useRef<AbortController | null>(null);
const handleSubmit = () => {
const trimmed = text.trim();
if (!trimmed || disabled) return;
onSend(trimmed, refImage);
if (upload.status === "uploading") return;
onSend(trimmed, upload.serverUrl, selectedModel || null);
setText("");
setRefImage(null);
setPreviewUrl(null);
clearUpload();
};
const handleKeyDown = (e: KeyboardEvent) => {
@@ -29,54 +56,235 @@ export function ChatInput({ onSend, disabled }: ChatInputProps) {
}
};
const clearUpload = useCallback(() => {
abortRef.current?.abort();
abortRef.current = null;
setUpload((prev) => {
if (prev.previewUrl) URL.revokeObjectURL(prev.previewUrl);
return INITIAL_UPLOAD;
});
if (fileInputRef.current) fileInputRef.current.value = "";
}, []);
const startUpload = useCallback(async (file: File) => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
const previewUrl = URL.createObjectURL(file);
setUpload({
status: "uploading",
previewUrl,
serverUrl: null,
progress: 0,
errorMsg: null,
});
try {
const result = await uploadRefImage(
file,
(p: UploadProgress) => {
setUpload((prev) => ({ ...prev, progress: p.percent }));
},
controller.signal
);
setUpload((prev) => ({
...prev,
status: "done",
serverUrl: result.url,
progress: 100,
}));
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return;
const msg = err instanceof Error ? err.message : "上传失败";
setUpload((prev) => ({
...prev,
status: "error",
progress: 0,
errorMsg: msg,
}));
}
}, []);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setRefImage(file);
setPreviewUrl(URL.createObjectURL(file));
};
const removeRefImage = () => {
setRefImage(null);
if (previewUrl) URL.revokeObjectURL(previewUrl);
setPreviewUrl(null);
if (file) {
startUpload(file);
}
// 重置 input value确保选同一文件也能触发
if (fileInputRef.current) fileInputRef.current.value = "";
};
const retryUpload = useCallback(() => {
if (fileInputRef.current) fileInputRef.current.click();
}, []);
// 拖拽
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = () => setIsDragging(false);
const handleDrop = (e: DragEvent) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith("image/")) {
startUpload(file);
}
};
const statusLabel = (() => {
switch (upload.status) {
case "uploading":
return `上传中 ${upload.progress}%`;
case "done":
return "参考图已就绪";
case "error":
return upload.errorMsg || "上传失败";
default:
return null;
}
})();
const statusColor = (() => {
switch (upload.status) {
case "uploading":
return "text-[var(--accent)]";
case "done":
return "text-green-400";
case "error":
return "text-red-400";
default:
return "";
}
})();
return (
<div className="border-t border-[var(--border)] bg-[var(--bg-secondary)] px-4 py-3">
{/* 参考图预览 */}
{previewUrl && (
<div
className={`border-t border-[var(--border)] bg-[var(--bg-secondary)] px-3 md:px-4 py-2 md:py-3 transition-colors ${
isDragging ? "bg-[var(--accent)]/5 border-[var(--accent)]" : ""
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* 拖拽提示 */}
{isDragging && (
<div className="mb-2 text-center text-xs text-[var(--accent)]">
</div>
)}
{/* 参考图上传状态区 */}
{upload.status !== "idle" && (
<div className="mb-3 flex items-start gap-2">
<div className="relative">
<img
src={previewUrl}
alt="参考图"
className="w-16 h-16 rounded-lg object-cover border border-[var(--border)]"
/>
<button
onClick={removeRefImage}
className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full
bg-red-500 text-white text-xs flex items-center justify-center
hover:bg-red-600 cursor-pointer"
>
×
</button>
<div className="relative flex-shrink-0">
{upload.previewUrl && (
<img
src={upload.previewUrl}
alt="参考图"
className={`w-16 h-16 rounded-lg object-cover border border-[var(--border)] transition-opacity ${
upload.status === "uploading" ? "opacity-60" : ""
}`}
/>
)}
{/* 上传中的遮罩进度环 */}
{upload.status === "uploading" && (
<div className="absolute inset-0 flex items-center justify-center">
<svg className="w-8 h-8 -rotate-90" viewBox="0 0 36 36">
<circle
cx="18" cy="18" r="14"
fill="none"
stroke="rgba(255,255,255,0.2)"
strokeWidth="3"
/>
<circle
cx="18" cy="18" r="14"
fill="none"
stroke="var(--accent)"
strokeWidth="3"
strokeLinecap="round"
strokeDasharray={`${upload.progress * 0.88} 88`}
className="transition-all duration-200"
/>
</svg>
</div>
)}
{/* 成功 ✓ 角标 */}
{upload.status === "done" && (
<div className="absolute -top-1 -right-1 w-5 h-5 rounded-full bg-green-500
text-white text-[10px] flex items-center justify-center font-bold">
</div>
)}
{/* 失败 ! 角标 */}
{upload.status === "error" && (
<div className="absolute -top-1 -right-1 w-5 h-5 rounded-full bg-red-500
text-white text-[10px] flex items-center justify-center font-bold">
!
</div>
)}
{/* 删除按钮(非上传中时显示) */}
{upload.status !== "uploading" && (
<button
onClick={clearUpload}
className="absolute -top-1.5 -left-1.5 w-5 h-5 rounded-full
bg-[var(--bg-secondary)] border border-[var(--border)]
text-[var(--text-secondary)] text-xs flex items-center justify-center
hover:bg-red-500 hover:text-white hover:border-red-500
cursor-pointer transition-colors"
>
×
</button>
)}
</div>
<div className="flex flex-col gap-1 mt-0.5 min-w-0">
<span className={`text-xs ${statusColor} flex items-center gap-1.5`}>
{upload.status === "uploading" && (
<span className="inline-block w-2 h-2 rounded-full bg-[var(--accent)] animate-pulse" />
)}
{statusLabel}
</span>
{/* 进度条 */}
{upload.status === "uploading" && (
<div className="w-32 h-1 rounded-full bg-[var(--bg-tertiary)] overflow-hidden">
<div
className="h-full bg-[var(--accent)] rounded-full transition-all duration-200"
style={{ width: `${upload.progress}%` }}
/>
</div>
)}
{/* 失败时显示重试 */}
{upload.status === "error" && (
<button
onClick={retryUpload}
className="text-xs text-[var(--accent)] hover:underline cursor-pointer w-fit"
>
</button>
)}
</div>
<span className="text-xs text-[var(--text-secondary)] mt-1"></span>
</div>
)}
<div className="flex items-end gap-2">
{/* 模型选择器 */}
<ModelSelector value={selectedModel} onChange={setSelectedModel} />
{/* 上传参考图按钮 */}
<button
onClick={() => fileInputRef.current?.click()}
disabled={disabled}
className="flex-shrink-0 w-10 h-10 rounded-lg border border-[var(--border)]
disabled={disabled || upload.status === "uploading"}
className={`flex-shrink-0 w-10 h-10 rounded-lg border
bg-[var(--bg-tertiary)] text-[var(--text-secondary)]
hover:text-[var(--text-primary)] hover:border-[var(--accent)]
flex items-center justify-center transition-colors
disabled:opacity-50 cursor-pointer"
disabled:opacity-50 cursor-pointer ${
upload.status === "done"
? "border-green-500/50 text-green-400"
: "border-[var(--border)]"
}`}
title="上传参考图"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -114,7 +322,7 @@ export function ChatInput({ onSend, disabled }: ChatInputProps) {
{/* 发送按钮 */}
<button
onClick={handleSubmit}
disabled={disabled || !text.trim()}
disabled={disabled || !text.trim() || upload.status === "uploading"}
className="flex-shrink-0 w-10 h-10 rounded-lg
bg-[var(--accent)] text-white
hover:bg-[var(--accent-hover)]