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

前端代码修复

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