diff --git a/app/api/n8n/chat/route.ts b/app/api/n8n/chat/route.ts index aefe543..abc1d36 100644 --- a/app/api/n8n/chat/route.ts +++ b/app/api/n8n/chat/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { decodeHtmlEntitiesServer } from "@/lib/html-decode"; export async function POST(request: NextRequest) { let userMessage = ""; @@ -36,29 +37,43 @@ export async function POST(request: NextRequest) { }); } - console.log(`Sending to n8n: ${n8nWebhookUrl}/webhook/chat`); + const webhookUrl = `${n8nWebhookUrl}/webhook/chat`; + console.log(`Sending to n8n: ${webhookUrl}`); - const response = await fetch(`${n8nWebhookUrl}/webhook/chat`, { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(process.env.N8N_API_KEY && { - Authorization: `Bearer ${process.env.N8N_API_KEY}`, + // Add timeout to prevent hanging requests + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout + + try { + const response = await fetch(webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(process.env.N8N_SECRET_TOKEN && { + Authorization: `Bearer ${process.env.N8N_SECRET_TOKEN}`, + }), + ...(process.env.N8N_API_KEY && { + "X-API-Key": process.env.N8N_API_KEY, + }), + }, + body: JSON.stringify({ + message: userMessage, + history: history, }), - }, - body: JSON.stringify({ - message: userMessage, - history: history, - }), - }); - if (!response.ok) { - console.error(`n8n webhook failed with status: ${response.status}`); - throw new Error(`n8n webhook failed: ${response.status}`); - } + signal: controller.signal, + }); - const data = await response.json(); + clearTimeout(timeoutId); - console.log("n8n response data:", data); + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + console.error(`n8n webhook failed with status: ${response.status}`, errorText); + throw new Error(`n8n webhook failed: ${response.status} - ${errorText}`); + } + + const data = await response.json(); + + console.log("n8n response data:", data); const reply = data.reply || @@ -77,17 +92,41 @@ export async function POST(request: NextRequest) { if (data && typeof data === "object" && Object.keys(data).length > 0) { // It returned something, but we don't know what field to use. // Check for common n8n structure - if (data.output) return NextResponse.json({ reply: data.output }); - if (data.data) return NextResponse.json({ reply: data.data }); + if (data.output) { + const decoded = decodeHtmlEntitiesServer(String(data.output)); + return NextResponse.json({ reply: decoded }); + } + if (data.data) { + const decoded = decodeHtmlEntitiesServer(String(data.data)); + return NextResponse.json({ reply: decoded }); + } } throw new Error("Invalid response format from n8n"); } - return NextResponse.json({ - reply: reply, - }); - } catch (error) { + // Decode HTML entities in the reply + const decodedReply = decodeHtmlEntitiesServer(String(reply)); + + return NextResponse.json({ + reply: decodedReply, + }); + } catch (fetchError: unknown) { + clearTimeout(timeoutId); + + if (fetchError instanceof Error && fetchError.name === 'AbortError') { + console.error("n8n webhook request timed out"); + } else { + console.error("n8n webhook fetch error:", fetchError); + } + throw fetchError; + } + } catch (error: unknown) { console.error("Chat API error:", error); + console.error("Error details:", { + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + n8nUrl: process.env.N8N_WEBHOOK_URL ? 'configured' : 'missing', + }); // Fallback to mock responses // Now using the variable captured at the start diff --git a/app/api/n8n/status/route.ts b/app/api/n8n/status/route.ts index 713ff38..b8a6c91 100644 --- a/app/api/n8n/status/route.ts +++ b/app/api/n8n/status/route.ts @@ -20,6 +20,7 @@ export async function GET(request: NextRequest) { const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL; if (!n8nWebhookUrl) { + console.warn("N8N_WEBHOOK_URL not configured for status endpoint"); // Return fallback if n8n is not configured return NextResponse.json({ status: { text: "offline", color: "gray" }, @@ -31,42 +32,70 @@ export async function GET(request: NextRequest) { // Rufe den n8n Webhook auf // Add timestamp to query to bypass Cloudflare cache - const res = await fetch( - `${n8nWebhookUrl}/webhook/denshooter-71242/status?t=${Date.now()}`, - { + const statusUrl = `${n8nWebhookUrl}/webhook/denshooter-71242/status?t=${Date.now()}`; + console.log(`Fetching status from: ${statusUrl}`); + + // Add timeout to prevent hanging requests + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout + + try { + const res = await fetch(statusUrl, { method: "GET", headers: { "Content-Type": "application/json", + ...(process.env.N8N_SECRET_TOKEN && { + Authorization: `Bearer ${process.env.N8N_SECRET_TOKEN}`, + }), }, next: { revalidate: 30 }, - }, - ); + signal: controller.signal, + }); - if (!res.ok) { - throw new Error(`n8n error: ${res.status}`); + clearTimeout(timeoutId); + + if (!res.ok) { + const errorText = await res.text().catch(() => 'Unknown error'); + console.error(`n8n status webhook failed: ${res.status}`, errorText); + throw new Error(`n8n error: ${res.status} - ${errorText}`); + } + + const data = await res.json(); + + // n8n gibt oft ein Array zurück: [{...}]. Wir wollen nur das Objekt. + const statusData = Array.isArray(data) ? data[0] : data; + + // Safety check: if statusData is still undefined/null (e.g. empty array), use fallback + if (!statusData) { + throw new Error("Empty data received from n8n"); + } + + // Ensure coding object has proper structure + if (statusData.coding && typeof statusData.coding === "object") { + // Already properly formatted from n8n + } else if (statusData.coding === null || statusData.coding === undefined) { + // No coding data - keep as null + statusData.coding = null; + } + + return NextResponse.json(statusData); + } catch (fetchError: unknown) { + clearTimeout(timeoutId); + + if (fetchError instanceof Error && fetchError.name === 'AbortError') { + console.error("n8n status webhook request timed out"); + } else { + console.error("n8n status webhook fetch error:", fetchError); + } + throw fetchError; } - - const data = await res.json(); - - // n8n gibt oft ein Array zurück: [{...}]. Wir wollen nur das Objekt. - const statusData = Array.isArray(data) ? data[0] : data; - - // Safety check: if statusData is still undefined/null (e.g. empty array), use fallback - if (!statusData) { - throw new Error("Empty data received from n8n"); - } - - // Ensure coding object has proper structure - if (statusData.coding && typeof statusData.coding === "object") { - // Already properly formatted from n8n - } else if (statusData.coding === null || statusData.coding === undefined) { - // No coding data - keep as null - statusData.coding = null; - } - - return NextResponse.json(statusData); - } catch (error) { + } catch (error: unknown) { console.error("Error fetching n8n status:", error); + console.error("Error details:", { + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + n8nUrl: process.env.N8N_WEBHOOK_URL ? 'configured' : 'missing', + }); // Leeres Fallback-Objekt, damit die Seite nicht abstürzt return NextResponse.json({ status: { text: "offline", color: "gray" }, diff --git a/app/components/ChatWidget.tsx b/app/components/ChatWidget.tsx index 2446660..e18efe0 100644 --- a/app/components/ChatWidget.tsx +++ b/app/components/ChatWidget.tsx @@ -125,9 +125,19 @@ export default function ChatWidget() { const data = await response.json(); + // Decode HTML entities in the reply + let replyText = data.reply || "Sorry, I couldn't process that. Please try again."; + + // Decode HTML entities client-side as well (double safety) + if (typeof window !== 'undefined') { + const textarea = document.createElement('textarea'); + textarea.innerHTML = replyText; + replyText = textarea.value; + } + const botMessage: Message = { id: (Date.now() + 1).toString(), - text: data.reply || "Sorry, I couldn't process that. Please try again.", + text: replyText, sender: "bot", timestamp: new Date(), }; diff --git a/lib/html-decode.ts b/lib/html-decode.ts new file mode 100644 index 0000000..13b2911 --- /dev/null +++ b/lib/html-decode.ts @@ -0,0 +1,41 @@ +/** + * Decode HTML entities in strings + * Converts ' " & < > etc. to their actual characters + */ +export function decodeHtmlEntities(text: string): string { + if (!text || typeof text !== 'string') { + return text; + } + + // Create a temporary element to decode HTML entities + const textarea = document.createElement('textarea'); + textarea.innerHTML = text; + return textarea.value; +} + +/** + * Server-side HTML entity decoding (for Node.js/Next.js API routes) + */ +export function decodeHtmlEntitiesServer(text: string): string { + if (!text || typeof text !== 'string') { + return text; + } + + // Map of common HTML entities + const entityMap: Record = { + ''': "'", + '"': '"', + '&': '&', + '<': '<', + '>': '>', + ''': "'", + ''': "'", + '/': '/', + '`': '`', + '=': '=', + }; + + return text.replace(/&[#\w]+;/g, (entity) => { + return entityMap[entity] || entity; + }); +}