feat: 图像拓展功能优化和防重复点击
- 图像拓展API改为使用URL方式,避免Base64长度限制(61440字节) - 先将图片上传到S3获取公网URL,再调用Dashscope API - 添加防重复点击功能,处理中时禁用所有图片处理按钮 - 添加自动滚动功能,点击处理按钮后自动滚动到最新消息 - 处理完成后也会自动滚动到底部显示结果
This commit is contained in:
parent
7526a0159c
commit
1f213bb92d
@ -85,26 +85,61 @@ async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/jp
|
|||||||
export const maxDuration = 180;
|
export const maxDuration = 180;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图像拓展核心实现
|
* 将图片上传到S3并返回公网URL(用于API调用)
|
||||||
* @param imageBase64 - Base64编码的图片数据
|
*/
|
||||||
|
async function uploadImageToS3ForAPI(imageBase64: string, mimeType: string = "image/jpeg"): Promise<string> {
|
||||||
|
try {
|
||||||
|
// 解析 base64 数据
|
||||||
|
const buffer = Buffer.from(imageBase64, "base64");
|
||||||
|
|
||||||
|
// 生成文件名
|
||||||
|
const ext = mimeType.split("/")[1] || "jpg";
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const randomStr = Math.random().toString(36).slice(2, 8);
|
||||||
|
const filename = `extend-input-${timestamp}-${randomStr}.${ext}`;
|
||||||
|
|
||||||
|
console.log(`[Extend API] 上传原图到S3,大小: ${(buffer.length / 1024).toFixed(2)}KB`);
|
||||||
|
|
||||||
|
// 转换为 ArrayBuffer
|
||||||
|
const arrayBuffer = buffer.buffer.slice(
|
||||||
|
buffer.byteOffset,
|
||||||
|
buffer.byteOffset + buffer.byteLength
|
||||||
|
) as ArrayBuffer;
|
||||||
|
|
||||||
|
// 上传到 S3
|
||||||
|
const result = await uploadFile(filename, arrayBuffer);
|
||||||
|
|
||||||
|
console.log(`[Extend API] ✅ 原图已上传到S3: ${result.url}`);
|
||||||
|
|
||||||
|
return result.url;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Extend API] 上传到S3失败:", error);
|
||||||
|
throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图像拓展核心实现(使用URL方式,避免Base64长度限制)
|
||||||
|
* @param imageUrl - 图片的公网URL
|
||||||
* @param apiKey - Dashscope API Key
|
* @param apiKey - Dashscope API Key
|
||||||
* @param xScale - X轴放大倍数(默认1.5)
|
* @param xScale - X轴放大倍数(默认1.5)
|
||||||
* @param yScale - Y轴放大倍数(默认1.5)
|
* @param yScale - Y轴放大倍数(默认1.5)
|
||||||
* @returns 拓展后的图片URL
|
* @returns 拓展后的图片URL
|
||||||
*/
|
*/
|
||||||
async function extendImage(
|
async function extendImage(
|
||||||
imageBase64: string,
|
imageUrl: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
xScale: number = 1.5,
|
xScale: number = 1.5,
|
||||||
yScale: number = 1.5
|
yScale: number = 1.5
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
console.log("[Extend API] 开始图像拓展...");
|
console.log("[Extend API] 开始图像拓展...");
|
||||||
|
console.log("[Extend API] 使用图片URL:", imageUrl);
|
||||||
|
|
||||||
// 1. 提交拓展任务
|
// 1. 提交拓展任务(使用URL方式,避免Base64长度限制)
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
model: "image-out-painting",
|
model: "image-out-painting",
|
||||||
input: {
|
input: {
|
||||||
image_base64: imageBase64,
|
image_url: imageUrl,
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
x_scale: xScale,
|
x_scale: xScale,
|
||||||
@ -187,6 +222,7 @@ async function extendImage(
|
|||||||
throw new Error(`图像拓展任务超时(等待${maxAttempts * 2 + 3}秒)`);
|
throw new Error(`图像拓展任务超时(等待${maxAttempts * 2 + 3}秒)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图像拓展API路由
|
* 图像拓展API路由
|
||||||
* 从数据库读取图像拓展API配置,调用Dashscope API进行图像拓展处理
|
* 从数据库读取图像拓展API配置,调用Dashscope API进行图像拓展处理
|
||||||
@ -286,14 +322,19 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
console.log(`[Extend API] 图片Base64长度: ${imageBase64.length} 字符`);
|
console.log(`[Extend API] 图片Base64长度: ${imageBase64.length} 字符`);
|
||||||
|
|
||||||
// 3. 调用图像拓展API
|
// 3. 先将图片上传到S3获取公网URL(使用URL方式避免Base64长度限制)
|
||||||
|
console.log("[Extend API] 上传原图到S3获取公网URL...");
|
||||||
|
const inputImageUrl = await uploadImageToS3ForAPI(imageBase64, "image/jpeg");
|
||||||
|
console.log("[Extend API] 原图URL:", inputImageUrl);
|
||||||
|
|
||||||
|
// 4. 调用图像拓展API(使用URL方式)
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const xScale = config.xScale || 1.5;
|
const xScale = config.xScale || 1.5;
|
||||||
const yScale = config.yScale || 1.5;
|
const yScale = config.yScale || 1.5;
|
||||||
|
|
||||||
console.log(`[Extend API] 拓展参数: X=${xScale}, Y=${yScale}`);
|
console.log(`[Extend API] 拓展参数: X=${xScale}, Y=${yScale}`);
|
||||||
|
|
||||||
const extendedImageUrl = await extendImage(imageBase64, config.apiKey, xScale, yScale);
|
const extendedImageUrl = await extendImage(inputImageUrl, config.apiKey, xScale, yScale);
|
||||||
console.log("[Extend API] 拓展后图片URL:", extendedImageUrl);
|
console.log("[Extend API] 拓展后图片URL:", extendedImageUrl);
|
||||||
|
|
||||||
// 4. 下载拓展后的图片
|
// 4. 下载拓展后的图片
|
||||||
@ -350,3 +391,4 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -111,6 +111,10 @@ export function Chat({
|
|||||||
const [usage, setUsage] = useState<AppUsage | undefined>(initialLastContext);
|
const [usage, setUsage] = useState<AppUsage | undefined>(initialLastContext);
|
||||||
const [showCreditCardAlert, setShowCreditCardAlert] = useState(false);
|
const [showCreditCardAlert, setShowCreditCardAlert] = useState(false);
|
||||||
const [currentModelId, setCurrentModelId] = useState(initialChatModel);
|
const [currentModelId, setCurrentModelId] = useState(initialChatModel);
|
||||||
|
// 图片处理状态,防止重复点击
|
||||||
|
const [isProcessingEnhance, setIsProcessingEnhance] = useState(false);
|
||||||
|
const [isProcessingRmbg, setIsProcessingRmbg] = useState(false);
|
||||||
|
const [isProcessingExtend, setIsProcessingExtend] = useState(false);
|
||||||
const currentModelIdRef = useRef(currentModelId);
|
const currentModelIdRef = useRef(currentModelId);
|
||||||
|
|
||||||
// 🆕 视频参数状态
|
// 🆕 视频参数状态
|
||||||
@ -502,6 +506,13 @@ export function Chat({
|
|||||||
|
|
||||||
// 处理图片变清晰
|
// 处理图片变清晰
|
||||||
const handleImageEnhance = async (imageUrl: string) => {
|
const handleImageEnhance = async (imageUrl: string) => {
|
||||||
|
// 防止重复点击
|
||||||
|
if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) {
|
||||||
|
toast.info("正在处理中,请稍候...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessingEnhance(true);
|
||||||
const messageId = generateUUID();
|
const messageId = generateUUID();
|
||||||
|
|
||||||
// 创建临时加载消息
|
// 创建临时加载消息
|
||||||
@ -514,6 +525,17 @@ export function Chat({
|
|||||||
|
|
||||||
setMessages((prev) => [...prev, loadingMessage]);
|
setMessages((prev) => [...prev, loadingMessage]);
|
||||||
|
|
||||||
|
// 延迟滚动到底部,确保DOM已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
const messagesContainer = document.querySelector('[role="log"]');
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.scrollTo({
|
||||||
|
top: messagesContainer.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取图片并转换为Base64
|
// 获取图片并转换为Base64
|
||||||
const response = await fetch(imageUrl);
|
const response = await fetch(imageUrl);
|
||||||
@ -562,6 +584,17 @@ export function Chat({
|
|||||||
prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
|
prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 滚动到底部,显示处理结果
|
||||||
|
setTimeout(() => {
|
||||||
|
const messagesContainer = document.querySelector('[role="log"]');
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.scrollTo({
|
||||||
|
top: messagesContainer.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
// 保存消息到数据库
|
// 保存消息到数据库
|
||||||
try {
|
try {
|
||||||
await fetch("/api/messages", {
|
await fetch("/api/messages", {
|
||||||
@ -593,12 +626,15 @@ export function Chat({
|
|||||||
toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试");
|
toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试");
|
||||||
// 移除加载消息
|
// 移除加载消息
|
||||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
|
} finally {
|
||||||
|
setIsProcessingEnhance(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = () => {
|
reader.onerror = () => {
|
||||||
toast.error("图片读取失败");
|
toast.error("图片读取失败");
|
||||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
|
setIsProcessingEnhance(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
@ -606,11 +642,19 @@ export function Chat({
|
|||||||
console.error("[变清晰] 错误:", error);
|
console.error("[变清晰] 错误:", error);
|
||||||
toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试");
|
toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试");
|
||||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
|
setIsProcessingEnhance(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理图片抠图
|
// 处理图片抠图
|
||||||
const handleImageRmbg = async (imageUrl: string) => {
|
const handleImageRmbg = async (imageUrl: string) => {
|
||||||
|
// 防止重复点击
|
||||||
|
if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) {
|
||||||
|
toast.info("正在处理中,请稍候...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessingRmbg(true);
|
||||||
const messageId = generateUUID();
|
const messageId = generateUUID();
|
||||||
|
|
||||||
// 创建临时加载消息
|
// 创建临时加载消息
|
||||||
@ -623,6 +667,17 @@ export function Chat({
|
|||||||
|
|
||||||
setMessages((prev) => [...prev, loadingMessage]);
|
setMessages((prev) => [...prev, loadingMessage]);
|
||||||
|
|
||||||
|
// 延迟滚动到底部,确保DOM已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
const messagesContainer = document.querySelector('[role="log"]');
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.scrollTo({
|
||||||
|
top: messagesContainer.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取图片并转换为Base64
|
// 获取图片并转换为Base64
|
||||||
const response = await fetch(imageUrl);
|
const response = await fetch(imageUrl);
|
||||||
@ -671,6 +726,17 @@ export function Chat({
|
|||||||
prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
|
prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 滚动到底部,显示处理结果
|
||||||
|
setTimeout(() => {
|
||||||
|
const messagesContainer = document.querySelector('[role="log"]');
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.scrollTo({
|
||||||
|
top: messagesContainer.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
// 保存消息到数据库
|
// 保存消息到数据库
|
||||||
try {
|
try {
|
||||||
await fetch("/api/messages", {
|
await fetch("/api/messages", {
|
||||||
@ -702,12 +768,15 @@ export function Chat({
|
|||||||
toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试");
|
toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试");
|
||||||
// 移除加载消息
|
// 移除加载消息
|
||||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
|
} finally {
|
||||||
|
setIsProcessingRmbg(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = () => {
|
reader.onerror = () => {
|
||||||
toast.error("图片读取失败");
|
toast.error("图片读取失败");
|
||||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
|
setIsProcessingRmbg(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
@ -715,11 +784,19 @@ export function Chat({
|
|||||||
console.error("[抠图] 错误:", error);
|
console.error("[抠图] 错误:", error);
|
||||||
toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试");
|
toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试");
|
||||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
|
setIsProcessingRmbg(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理图像拓展
|
// 处理图像拓展
|
||||||
const handleImageExtend = async (imageUrl: string) => {
|
const handleImageExtend = async (imageUrl: string) => {
|
||||||
|
// 防止重复点击
|
||||||
|
if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) {
|
||||||
|
toast.info("正在处理中,请稍候...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessingExtend(true);
|
||||||
const messageId = generateUUID();
|
const messageId = generateUUID();
|
||||||
|
|
||||||
// 创建临时加载消息
|
// 创建临时加载消息
|
||||||
@ -734,6 +811,17 @@ export function Chat({
|
|||||||
|
|
||||||
setMessages((prev) => [...prev, loadingMessage]);
|
setMessages((prev) => [...prev, loadingMessage]);
|
||||||
|
|
||||||
|
// 延迟滚动到底部,确保DOM已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
const messagesContainer = document.querySelector('[role="log"]');
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.scrollTo({
|
||||||
|
top: messagesContainer.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取图片并转换为Base64
|
// 获取图片并转换为Base64
|
||||||
const response = await fetch(imageUrl);
|
const response = await fetch(imageUrl);
|
||||||
@ -782,6 +870,17 @@ export function Chat({
|
|||||||
prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
|
prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 滚动到底部,显示处理结果
|
||||||
|
setTimeout(() => {
|
||||||
|
const messagesContainer = document.querySelector('[role="log"]');
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.scrollTo({
|
||||||
|
top: messagesContainer.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
// 保存消息到数据库
|
// 保存消息到数据库
|
||||||
try {
|
try {
|
||||||
await fetch("/api/messages", {
|
await fetch("/api/messages", {
|
||||||
@ -813,12 +912,15 @@ export function Chat({
|
|||||||
toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试");
|
toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试");
|
||||||
// 移除加载消息
|
// 移除加载消息
|
||||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
|
} finally {
|
||||||
|
setIsProcessingExtend(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = () => {
|
reader.onerror = () => {
|
||||||
toast.error("图片读取失败");
|
toast.error("图片读取失败");
|
||||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
|
setIsProcessingExtend(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
@ -826,6 +928,7 @@ export function Chat({
|
|||||||
console.error("[图像拓展] 错误:", error);
|
console.error("[图像拓展] 错误:", error);
|
||||||
toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试");
|
toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试");
|
||||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
|
setIsProcessingExtend(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -861,6 +964,9 @@ export function Chat({
|
|||||||
onImageEnhance={handleImageEnhance}
|
onImageEnhance={handleImageEnhance}
|
||||||
onImageRmbg={handleImageRmbg}
|
onImageRmbg={handleImageRmbg}
|
||||||
onImageExtend={handleImageExtend}
|
onImageExtend={handleImageExtend}
|
||||||
|
isProcessingEnhance={isProcessingEnhance}
|
||||||
|
isProcessingRmbg={isProcessingRmbg}
|
||||||
|
isProcessingExtend={isProcessingExtend}
|
||||||
pendingGeneration={pendingGeneration}
|
pendingGeneration={pendingGeneration}
|
||||||
imageCount={imageParams?.imageCount || 1}
|
imageCount={imageParams?.imageCount || 1}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -12,10 +12,13 @@ type ResponseProps = ComponentProps<typeof Streamdown> & {
|
|||||||
onImageEnhance?: (imageUrl: string) => void;
|
onImageEnhance?: (imageUrl: string) => void;
|
||||||
onImageRmbg?: (imageUrl: string) => void;
|
onImageRmbg?: (imageUrl: string) => void;
|
||||||
onImageExtend?: (imageUrl: string) => void;
|
onImageExtend?: (imageUrl: string) => void;
|
||||||
|
isProcessingEnhance?: boolean;
|
||||||
|
isProcessingRmbg?: boolean;
|
||||||
|
isProcessingExtend?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Response = memo(
|
export const Response = memo(
|
||||||
({ className, onImageEdit, onDrawingComplete, onImageEnhance, onImageRmbg, onImageExtend, ...props }: ResponseProps) => (
|
({ className, onImageEdit, onDrawingComplete, onImageEnhance, onImageRmbg, onImageExtend, isProcessingEnhance, isProcessingRmbg, isProcessingExtend, ...props }: ResponseProps) => (
|
||||||
<Streamdown
|
<Streamdown
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto",
|
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto",
|
||||||
@ -34,6 +37,9 @@ export const Response = memo(
|
|||||||
onImageEnhance={onImageEnhance}
|
onImageEnhance={onImageEnhance}
|
||||||
onImageRmbg={onImageRmbg}
|
onImageRmbg={onImageRmbg}
|
||||||
onImageExtend={onImageExtend}
|
onImageExtend={onImageExtend}
|
||||||
|
isProcessingEnhance={isProcessingEnhance}
|
||||||
|
isProcessingRmbg={isProcessingRmbg}
|
||||||
|
isProcessingExtend={isProcessingExtend}
|
||||||
/>;
|
/>;
|
||||||
},
|
},
|
||||||
video: ({ src, alt }: { src?: string; alt?: string }) => {
|
video: ({ src, alt }: { src?: string; alt?: string }) => {
|
||||||
@ -52,7 +58,10 @@ export const Response = memo(
|
|||||||
prevProps.onDrawingComplete === nextProps.onDrawingComplete &&
|
prevProps.onDrawingComplete === nextProps.onDrawingComplete &&
|
||||||
prevProps.onImageEnhance === nextProps.onImageEnhance &&
|
prevProps.onImageEnhance === nextProps.onImageEnhance &&
|
||||||
prevProps.onImageRmbg === nextProps.onImageRmbg &&
|
prevProps.onImageRmbg === nextProps.onImageRmbg &&
|
||||||
prevProps.onImageExtend === nextProps.onImageExtend
|
prevProps.onImageExtend === nextProps.onImageExtend &&
|
||||||
|
prevProps.isProcessingEnhance === nextProps.isProcessingEnhance &&
|
||||||
|
prevProps.isProcessingRmbg === nextProps.isProcessingRmbg &&
|
||||||
|
prevProps.isProcessingExtend === nextProps.isProcessingExtend
|
||||||
);
|
);
|
||||||
|
|
||||||
Response.displayName = "Response";
|
Response.displayName = "Response";
|
||||||
|
|||||||
@ -14,6 +14,9 @@ export function ImagePreview({
|
|||||||
onImageEnhance,
|
onImageEnhance,
|
||||||
onImageRmbg,
|
onImageRmbg,
|
||||||
onImageExtend,
|
onImageExtend,
|
||||||
|
isProcessingEnhance,
|
||||||
|
isProcessingRmbg,
|
||||||
|
isProcessingExtend,
|
||||||
imageIndex
|
imageIndex
|
||||||
}: {
|
}: {
|
||||||
src: string;
|
src: string;
|
||||||
@ -28,6 +31,9 @@ export function ImagePreview({
|
|||||||
onImageEnhance?: (imageUrl: string) => void;
|
onImageEnhance?: (imageUrl: string) => void;
|
||||||
onImageRmbg?: (imageUrl: string) => void;
|
onImageRmbg?: (imageUrl: string) => void;
|
||||||
onImageExtend?: (imageUrl: string) => void;
|
onImageExtend?: (imageUrl: string) => void;
|
||||||
|
isProcessingEnhance?: boolean;
|
||||||
|
isProcessingRmbg?: boolean;
|
||||||
|
isProcessingExtend?: boolean;
|
||||||
imageIndex?: number;
|
imageIndex?: number;
|
||||||
}) {
|
}) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@ -226,11 +232,14 @@ export function ImagePreview({
|
|||||||
<div className="absolute top-2 left-2 flex gap-1.5 z-10">
|
<div className="absolute top-2 left-2 flex gap-1.5 z-10">
|
||||||
{onImageEnhance && (
|
{onImageEnhance && (
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1.5 rounded-full bg-blue-600/90 hover:bg-blue-700 px-2.5 py-1.5 text-white transition-colors shadow-lg"
|
className="flex items-center gap-1.5 rounded-full bg-blue-600/90 hover:bg-blue-700 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onImageEnhance(original);
|
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend) {
|
||||||
|
onImageEnhance(original);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="变清晰"
|
aria-label="变清晰"
|
||||||
title="变清晰"
|
title="变清晰"
|
||||||
@ -241,11 +250,14 @@ export function ImagePreview({
|
|||||||
)}
|
)}
|
||||||
{onImageRmbg && (
|
{onImageRmbg && (
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1.5 rounded-full bg-purple-600/90 hover:bg-purple-700 px-2.5 py-1.5 text-white transition-colors shadow-lg"
|
className="flex items-center gap-1.5 rounded-full bg-purple-600/90 hover:bg-purple-700 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onImageRmbg(original);
|
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend) {
|
||||||
|
onImageRmbg(original);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="抠图"
|
aria-label="抠图"
|
||||||
title="抠图"
|
title="抠图"
|
||||||
@ -256,11 +268,14 @@ export function ImagePreview({
|
|||||||
)}
|
)}
|
||||||
{onImageExtend && (
|
{onImageExtend && (
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1.5 rounded-full bg-orange-600/90 hover:bg-orange-700 px-2.5 py-1.5 text-white transition-colors shadow-lg"
|
className="flex items-center gap-1.5 rounded-full bg-orange-600/90 hover:bg-orange-700 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onImageExtend(original);
|
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend) {
|
||||||
|
onImageExtend(original);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="图像拓展"
|
aria-label="图像拓展"
|
||||||
title="图像拓展"
|
title="图像拓展"
|
||||||
|
|||||||
@ -151,6 +151,9 @@ const PurePreviewMessage = ({
|
|||||||
onImageEnhance,
|
onImageEnhance,
|
||||||
onImageRmbg,
|
onImageRmbg,
|
||||||
onImageExtend,
|
onImageExtend,
|
||||||
|
isProcessingEnhance,
|
||||||
|
isProcessingRmbg,
|
||||||
|
isProcessingExtend,
|
||||||
}: {
|
}: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
message: ChatMessage;
|
message: ChatMessage;
|
||||||
@ -166,6 +169,9 @@ const PurePreviewMessage = ({
|
|||||||
onImageEnhance?: (imageUrl: string) => void;
|
onImageEnhance?: (imageUrl: string) => void;
|
||||||
onImageRmbg?: (imageUrl: string) => void;
|
onImageRmbg?: (imageUrl: string) => void;
|
||||||
onImageExtend?: (imageUrl: string) => void;
|
onImageExtend?: (imageUrl: string) => void;
|
||||||
|
isProcessingEnhance?: boolean;
|
||||||
|
isProcessingRmbg?: boolean;
|
||||||
|
isProcessingExtend?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [mode, setMode] = useState<"view" | "edit">("view");
|
const [mode, setMode] = useState<"view" | "edit">("view");
|
||||||
|
|
||||||
@ -299,6 +305,9 @@ const PurePreviewMessage = ({
|
|||||||
onImageEnhance={onImageEnhance}
|
onImageEnhance={onImageEnhance}
|
||||||
onImageRmbg={onImageRmbg}
|
onImageRmbg={onImageRmbg}
|
||||||
onImageExtend={onImageExtend}
|
onImageExtend={onImageExtend}
|
||||||
|
isProcessingEnhance={isProcessingEnhance}
|
||||||
|
isProcessingRmbg={isProcessingRmbg}
|
||||||
|
isProcessingExtend={isProcessingExtend}
|
||||||
>
|
>
|
||||||
{sanitizeText(part.content)}
|
{sanitizeText(part.content)}
|
||||||
</Response>
|
</Response>
|
||||||
|
|||||||
@ -40,6 +40,9 @@ type MessagesProps = {
|
|||||||
onImageEnhance?: (imageUrl: string) => void;
|
onImageEnhance?: (imageUrl: string) => void;
|
||||||
onImageRmbg?: (imageUrl: string) => void;
|
onImageRmbg?: (imageUrl: string) => void;
|
||||||
onImageExtend?: (imageUrl: string) => void;
|
onImageExtend?: (imageUrl: string) => void;
|
||||||
|
isProcessingEnhance?: boolean;
|
||||||
|
isProcessingRmbg?: boolean;
|
||||||
|
isProcessingExtend?: boolean;
|
||||||
pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态
|
pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态
|
||||||
imageCount?: number; // 🆕 用户选择的图片数量
|
imageCount?: number; // 🆕 用户选择的图片数量
|
||||||
};
|
};
|
||||||
@ -59,6 +62,9 @@ function PureMessages({
|
|||||||
onImageEnhance,
|
onImageEnhance,
|
||||||
onImageRmbg,
|
onImageRmbg,
|
||||||
onImageExtend,
|
onImageExtend,
|
||||||
|
isProcessingEnhance = false,
|
||||||
|
isProcessingRmbg = false,
|
||||||
|
isProcessingExtend = false,
|
||||||
pendingGeneration,
|
pendingGeneration,
|
||||||
imageCount = 1,
|
imageCount = 1,
|
||||||
}: MessagesProps) {
|
}: MessagesProps) {
|
||||||
@ -176,7 +182,7 @@ function PureMessages({
|
|||||||
}
|
}
|
||||||
}, [status, messages, isGeminiModel, isQianwenVideoModel, isVolcanoVisionModel, isVideoModel]);
|
}, [status, messages, isGeminiModel, isQianwenVideoModel, isVolcanoVisionModel, isVideoModel]);
|
||||||
|
|
||||||
// 🆕 监听新消息添加,当用户发送带图片的消息时,强制滚动到底部
|
// 🆕 监听新消息添加,当用户发送带图片的消息或添加加载消息时,强制滚动到底部
|
||||||
const prevMessagesLengthRef = useRef(messages.length);
|
const prevMessagesLengthRef = useRef(messages.length);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messages.length > prevMessagesLengthRef.current) {
|
if (messages.length > prevMessagesLengthRef.current) {
|
||||||
@ -191,6 +197,17 @@ function PureMessages({
|
|||||||
setTimeout(() => scrollToBottom("smooth"), 100);
|
setTimeout(() => scrollToBottom("smooth"), 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 如果是新添加的助手消息且是加载消息(正在变清晰/抠图/图像拓展),强制滚动到底部
|
||||||
|
if (lastMessage?.role === "assistant") {
|
||||||
|
const firstPart = lastMessage.parts?.[0];
|
||||||
|
if (firstPart && "text" in firstPart) {
|
||||||
|
const loadingText = firstPart.text;
|
||||||
|
if (loadingText === "正在变清晰..." || loadingText === "正在抠图..." || loadingText === "正在图像拓展...") {
|
||||||
|
// 延迟一小段时间确保DOM已更新
|
||||||
|
setTimeout(() => scrollToBottom("smooth"), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
prevMessagesLengthRef.current = messages.length;
|
prevMessagesLengthRef.current = messages.length;
|
||||||
}, [messages, scrollToBottom]);
|
}, [messages, scrollToBottom]);
|
||||||
@ -229,6 +246,9 @@ function PureMessages({
|
|||||||
onImageEnhance={onImageEnhance}
|
onImageEnhance={onImageEnhance}
|
||||||
onImageRmbg={onImageRmbg}
|
onImageRmbg={onImageRmbg}
|
||||||
onImageExtend={onImageExtend}
|
onImageExtend={onImageExtend}
|
||||||
|
isProcessingEnhance={isProcessingEnhance}
|
||||||
|
isProcessingRmbg={isProcessingRmbg}
|
||||||
|
isProcessingExtend={isProcessingExtend}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user