Commit 99f78a49 authored by AI-甘富林's avatar AI-甘富林

前端代码修复

parent 77a77a8f
This diff is collapsed.
import { useState, useEffect, useCallback, useRef } from "react";
import { flushSync } from "react-dom";
import { supabase } from "@/integrations/supabase/client";
import { ProductCard } from "@/components/chat/ProductCard";
import { Button } from "@/components/ui/button";
......@@ -305,16 +306,32 @@ export default function ShopPage() {
// AI response handler
const simulateAIResponse = useCallback(async (userMessage: string) => {
setIsAiTyping(true);
const streamMsgId = `ai_${Date.now()}`;
// 先插入一个占位消息,后续流式更新
const placeholderMessage: ChatMessageType = {
id: streamMsgId,
content: '',
sender: "ai",
timestamp: new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
}),
type: "text",
agent: currentAgent,
};
setMessages(prev => [...prev, placeholderMessage]);
try {
let currentProducts = products;
if (currentProducts.length === 0) {
currentProducts = await fetchProducts();
}
if (currentProducts.length === 0) {
throw new Error('产品数据加载失败');
}
const catalog = currentProducts.map(p => ({
id: p.id,
name: p.name,
......@@ -323,7 +340,9 @@ export default function ShopPage() {
category: p.category,
description: p.description
}));
// ─── 流式接收 AI 回复文字 ───
let fullContent = '';
const { data: intentCard } = await callCozeEdge({
message: userMessage,
userId: user?.id || 'anonymous',
......@@ -333,21 +352,38 @@ export default function ShopPage() {
description: currentProducts[0].description || '',
price: Number(currentProducts[0].price)
},
catalog
catalog,
onText: (text: string) => {
fullContent += text;
flushSync(() => {
setMessages(prev => prev.map(msg =>
msg.id === streamMsgId
? { ...msg, content: fullContent }
: msg
));
});
}
});
if (!intentCard) throw new Error('系统繁忙,请稍后再试');
let aiContent = intentCard.title || '收到您的消息';
if (intentCard.highlights && intentCard.highlights.length > 0) {
aiContent += '\n\n' + intentCard.highlights.map(h => `• ${h}`).join('\n');
// 清洗 JSON 代码块,避免裸露协议数据出现在正文
let aiContent = fullContent
.replace(/```(?:json)?\s*\{[\s\S]*?"(?:picks|intent)"[\s\S]*?\}\s*```/gi, '')
.replace(/\{\s*"(?:picks|intent)"\s*:[\s\S]*?\}\s*$/g, '')
.trim();
if (!aiContent) {
aiContent = intentCard.title || '收到您的消息';
if (intentCard.highlights && intentCard.highlights.length > 0) {
aiContent += '\n\n' + intentCard.highlights.map(h => `• ${h}`).join('\n');
}
}
if (intentCard.picks && intentCard.picks.length > 0) {
aiContent += '\n\n为您推荐了 ' + intentCard.picks.length + ' 个商品';
}
let messageType: ChatMessageType['type'] = "text";
let responseMetadata: ChatMessageType['metadata'] = {};
......@@ -370,50 +406,39 @@ export default function ShopPage() {
}))
};
}
const aiMessage: ChatMessageType = {
id: `ai_${Date.now()}`,
content: aiContent,
sender: "ai",
timestamp: new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
}),
type: messageType,
agent: currentAgent,
metadata: responseMetadata
};
setMessages(prev => [...prev, aiMessage]);
// 更新占位消息为最终内容 + 类型 + 元数据
setMessages(prev => prev.map(msg =>
msg.id === streamMsgId
? { ...msg, content: aiContent, type: messageType, metadata: responseMetadata }
: msg
));
if (activeConversationId) {
await saveMessage({
content: aiMessage.content,
sender_type: aiMessage.sender,
message_type: aiMessage.type as DBMessage['message_type'],
metadata: aiMessage.metadata
content: aiContent,
sender_type: "ai",
message_type: messageType as DBMessage['message_type'],
metadata: responseMetadata
}, activeConversationId);
}
} catch (error) {
console.error('AI回复失败:', error);
const errorMessage: ChatMessageType = {
id: `ai_error_${Date.now()}`,
content: `抱歉,遇到了技术问题,请稍后再试。`,
sender: "ai",
timestamp: new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
}),
type: "text",
agent: currentAgent
};
setMessages(prev => [...prev, errorMessage]);
// 把占位消息替换为错误消息
setMessages(prev => prev.map(msg =>
msg.id === streamMsgId
? { ...msg, content: '抱歉,遇到了技术问题,请稍后再试。' }
: msg
));
} finally {
setIsAiTyping(false);
}
}, [currentAgent, products, activeConversationId, saveMessage, user]);
const handleSendMessage = useCallback(async (content: string) => {
// 防止并发流式调用(如快速连点建议按钮)
if (isAiTyping) return;
const userMessage: ChatMessageType = {
id: `user_${Date.now()}`,
content,
......@@ -437,7 +462,7 @@ export default function ShopPage() {
}
await simulateAIResponse(content);
}, [simulateAIResponse, activeConversationId, saveMessage]);
}, [simulateAIResponse, activeConversationId, saveMessage, isAiTyping]);
return (
<div className="min-h-screen bg-background">
......
This diff is collapsed.
......@@ -51,6 +51,9 @@ export async function streamDifyChat(
try {
console.log('🚀 [DIFY STREAM] 开始流式调用');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 65000);
const response = await fetch(`${SUPABASE_URL}/functions/v1/dify-chat-stream`, {
method: 'POST',
headers: {
......@@ -60,8 +63,11 @@ export async function streamDifyChat(
'X-User-Id': options.userId || 'guest',
},
body: JSON.stringify(requestBody),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Dify 请求失败 (${response.status}): ${errorText}`);
......@@ -79,7 +85,7 @@ export async function streamDifyChat(
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
processSSELine(buffer, callbacks);
await processSSELine(buffer, callbacks);
}
break;
}
......@@ -89,18 +95,22 @@ export async function streamDifyChat(
buffer = lines.pop() || '';
for (const line of lines) {
processSSELine(line, callbacks);
await processSSELine(line, callbacks);
}
}
console.log('✅ [DIFY STREAM] 流式调用完成');
} catch (error) {
console.error('❌ [DIFY STREAM] 错误:', error);
callbacks.onError(error instanceof Error ? error.message : '流式请求失败');
if (error instanceof DOMException && error.name === 'AbortError') {
callbacks.onError('AI响应超时(65秒),请重新发送消息');
} else {
callbacks.onError(error instanceof Error ? error.message : '流式请求失败');
}
}
}
function processSSELine(line: string, callbacks: CozeStreamCallbacks) {
async function processSSELine(line: string, callbacks: CozeStreamCallbacks) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith(':')) return;
if (trimmedLine === 'data: [DONE]') return;
......@@ -114,15 +124,20 @@ function processSSELine(line: string, callbacks: CozeStreamCallbacks) {
callbacks.onText(event.content);
break;
case 'done':
callbacks.onDone(event);
await callbacks.onDone(event);
break;
case 'error':
callbacks.onError(event.message);
break;
}
} catch (e) {
// JSON.parse SyntaxError or callback rejection — safe fallback
if (jsonStr && !jsonStr.startsWith('{')) {
callbacks.onText(jsonStr);
try {
callbacks.onText(jsonStr);
} catch {
// onText itself failed; nothing more we can do
}
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment