diff --git a/apps/portal/src/components/AI/api.ts b/apps/portal/src/components/AI/api.ts index 0dc58b6d015..619d123cbd6 100644 --- a/apps/portal/src/components/AI/api.ts +++ b/apps/portal/src/components/AI/api.ts @@ -1,23 +1,60 @@ "use server"; -const serviceKey = process.env.SIWA_SERVICE_KEY as string; -const apiUrl = process.env.SIWA_URL; +import { headers } from "next/headers"; + +const secretKey = process.env.THIRDWEB_AI_SECRET_KEY as string; +const apiUrl = process.env.THIRDWEB_AI_URL || "https://api.thirdweb.com/ai"; +const clientId = process.env.THIRDWEB_AI_CLIENT_ID as string; + +const WINDOW_MS = 60_000; +const MAX_PER_WINDOW = 10; +const hits = new Map(); + +async function withinRateLimit() { + const key = (await headers()).get("x-forwarded-for")?.split(",")[0]?.trim(); + if (!key) { + return true; + } + + const now = Date.now(); + const recent = (hits.get(key) ?? []).filter((t) => now - t < WINDOW_MS); + recent.push(now); + hits.set(key, recent); + + if (hits.size > 10_000) { + for (const [k, v] of hits) { + if (v.every((t) => now - t >= WINDOW_MS)) { + hits.delete(k); + } + } + } + + return recent.length <= MAX_PER_WINDOW; +} export const getChatResponse = async ( userMessage: string, sessionId: string | undefined, ) => { try { - const payload = { - conversationId: sessionId, - message: userMessage, - source: "portal", - }; - const response = await fetch(`${apiUrl}/v1/chat`, { - body: JSON.stringify(payload), + if (!(await withinRateLimit())) { + return { + conversationId: sessionId, + data: "You're sending messages too quickly. Please wait a moment and try again.", + requestId: undefined, + }; + } + + const response = await fetch(`${apiUrl}/chat`, { + body: JSON.stringify({ + context: { session_id: sessionId }, + messages: [{ content: userMessage, role: "user" }], + stream: false, + }), headers: { "Content-Type": "application/json", - "x-service-api-key": serviceKey, + "x-client-id": clientId, + "x-secret-key": secretKey, }, method: "POST", }); @@ -30,10 +67,16 @@ export const getChatResponse = async ( } const data = (await response.json()) as { - data: string; - conversationId: string; + message: string; + session_id: string; + request_id: string; + }; + + return { + conversationId: data.session_id, + data: data.message, + requestId: data.request_id, }; - return data; } catch (error) { console.error( "Chat API error:", @@ -45,14 +88,20 @@ export const getChatResponse = async ( export const sendFeedback = async ( conversationId: string, + requestId: string, feedbackRating: 1 | -1, ) => { try { - const response = await fetch(`${apiUrl}/v1/chat/feedback`, { - body: JSON.stringify({ conversationId, feedbackRating }), + const response = await fetch(`${apiUrl}/feedback`, { + body: JSON.stringify({ + feedback_rating: feedbackRating, + request_id: requestId, + session_id: conversationId, + }), headers: { "Content-Type": "application/json", - "x-service-api-key": serviceKey, + "x-client-id": clientId, + "x-secret-key": secretKey, }, method: "POST", }); diff --git a/apps/portal/src/components/AI/chat.tsx b/apps/portal/src/components/AI/chat.tsx index 04a203abebb..2b7347993f6 100644 --- a/apps/portal/src/components/AI/chat.tsx +++ b/apps/portal/src/components/AI/chat.tsx @@ -37,6 +37,7 @@ interface Message { content: string; isLoading?: boolean; feedback?: 1 | -1; + requestId?: string; } const predefinedPrompts = [ @@ -140,7 +141,12 @@ export function Chat() { setMessages((prevMessages) => prevMessages.map((msg) => msg.id === loadingMessageId - ? { ...msg, content: response?.data ?? "", isLoading: false } + ? { + ...msg, + content: response?.data ?? "", + isLoading: false, + requestId: response?.requestId, + } : msg, ), ); @@ -262,21 +268,23 @@ function RenderAIResponse(props: { conversationId: string | undefined; message: Message; }) { + const requestId = props.message.requestId; + const thumbsUpFeedbackMutation = useMutation({ mutationFn: () => { - if (!props.conversationId) { + if (!props.conversationId || !requestId) { throw new Error("No conversation ID"); } - return sendFeedback(props.conversationId, 1); + return sendFeedback(props.conversationId, requestId, 1); }, }); const thumbsDownFeedbackMutation = useMutation({ mutationFn: () => { - if (!props.conversationId) { + if (!props.conversationId || !requestId) { throw new Error("No conversation ID"); } - return sendFeedback(props.conversationId, -1); + return sendFeedback(props.conversationId, requestId, -1); }, }); @@ -290,7 +298,7 @@ function RenderAIResponse(props: { isMessagePending={false} /> - {props.conversationId && ( + {props.conversationId && requestId && (