Skip to content

Commit 99dd8ed

Browse files
committed
feat: initial working implementation
1 parent 5d307ed commit 99dd8ed

File tree

15 files changed

+325
-257
lines changed

15 files changed

+325
-257
lines changed

src/app/api/chat/route.js

Lines changed: 76 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,56 @@
1-
// import { env } from "node:process";
1+
import { env } from "node:process";
22
import { createVertex } from "@ai-sdk/google-vertex";
3-
import { streamText } from "ai";
3+
import { streamText, convertToCoreMessages } from "ai";
44
import { StatusCodes, ReasonPhrases } from "http-status-codes";
5-
import { getContext } from "@/lib/smart-search.mjs";
5+
import { smartSearchTool } from "@/lib/rag.mjs";
66

7-
const vertex = createVertex();
7+
console.log("Initializing Google Vertex AI...");
8+
// Ensure all required environment variables are set
9+
if (!env.GOOGLE_VERTEX_PROJECT) {
10+
throw new Error("GOOGLE_VERTEX_PROJECT is not set");
11+
}
12+
13+
if (!env.GOOGLE_VERTEX_LOCATION) {
14+
throw new Error("GOOGLE_VERTEX_LOCATION is not set");
15+
}
16+
17+
if (!env.GOOGLE_VERTEX_CLIENT_EMAIL) {
18+
throw new Error("GOOGLE_VERTEX_CLIENT_EMAIL is not set");
19+
}
20+
21+
if (!env.GOOGLE_VERTEX_PRIVATE_KEY) {
22+
throw new Error("GOOGLE_VERTEX_PRIVATE_KEY is not set");
23+
}
24+
25+
if (!env.GOOGLE_VERTEX_PRIVATE_KEY.includes("-----BEGIN PRIVATE KEY-----")) {
26+
throw new Error("GOOGLE_VERTEX_PRIVATE_KEY is not formatted correctly");
27+
}
28+
29+
const vertex = createVertex({
30+
project: env.GOOGLE_VERTEX_PROJECT,
31+
location: env.GOOGLE_VERTEX_LOCATION,
32+
googleAuthOptions: {
33+
credentials: {
34+
client_email: env.GOOGLE_VERTEX_CLIENT_EMAIL,
35+
private_key: env.GOOGLE_VERTEX_PRIVATE_KEY.replaceAll(
36+
String.raw`\n`,
37+
"\n",
38+
), // Ensure newlines are correctly formatted
39+
},
40+
},
41+
});
42+
43+
const smartSearchPrompt = `
44+
- You can use the 'smartSearchTool' to find information relating to Faust.
45+
- WP Engine Smart Search is a powerful tool for finding information about Faust.
46+
- After the 'smartSearchTool' provides results (even if it's an error or no information found)
47+
- You MUST then formulate a conversational response to the user based on those results but also use the tool if the users query is deemed plausible.
48+
- If search results are found, summarize them for the user.
49+
- If no information is found or an error occurs, inform the user clearly.`;
50+
51+
const systemPromptContent = `
52+
- You are a friendly and helpful AI assistant
53+
- Do not invent information. Stick to the data provided by the tool.`;
854

955
export async function POST(req) {
1056
try {
@@ -16,74 +62,36 @@ export async function POST(req) {
1662
});
1763
}
1864

19-
const [latestMessage] = messages.slice(-1);
20-
const previousMessages = messages.slice(-11, -1);
21-
22-
const promptContext = await getContext(latestMessage.content);
23-
24-
if (
25-
!promptContext ||
26-
!promptContext.data ||
27-
!promptContext.data.similarity
28-
) {
29-
console.error(
30-
"No context found for the latest message:",
31-
latestMessage.content,
32-
"Context response:",
33-
promptContext,
34-
);
35-
return new Response(ReasonPhrases.INTERNAL_SERVER_ERROR, {
36-
status: StatusCodes.INTERNAL_SERVER_ERROR,
37-
});
38-
}
39-
40-
const messageContext = promptContext.data.similarity.docs.map((doc) => {
41-
return `
42-
ID: ${doc.id}
43-
Title: ${doc.data.post_title}
44-
Content: ${doc.data.post_content}
45-
SearchScore: ${doc.score}
46-
`;
47-
});
48-
49-
const prompt = {
50-
role: "assistant",
51-
content: `You are a AI assistant that provides information about Faust.js and headless WordPress.
52-
AI assistant is a brand new, powerful, human-like artificial intelligence.
53-
The traits of AI include expert knowledge, helpfulness, cleverness, and articulateness.
54-
AI is a well-behaved and well-mannered individual.
55-
AI is always friendly, kind, and inspiring, and he is eager to provide vivid and thoughtful responses to the user.
56-
AI has the sum of all knowledge in their brain, and is able to accurately answer nearly any question about any topic in conversation.
57-
AI assistant is a big fan of WP Engine Smart Search.
58-
AI assistant uses WP Engine Smart Search to provide the most accurate and relevant information to the user.
59-
AI assistant data from WP Engine Smart Search is based on TV Shows.
60-
START CONTEXT BLOCK
61-
${messageContext.join("----------------\n\n")}
62-
END OF CONTEXT BLOCK
63-
64-
START OF HISTORY BLOCK
65-
${JSON.stringify(previousMessages)}
66-
END OF HISTORY BLOCK
67-
AI assistant will take into account any CONTEXT BLOCK that is provided in a conversation.
68-
AI assistant will take into account any HISTORY BLOCK that is provided in a conversation.
69-
If the context does not provide the answer to question, the AI assistant will say, "I'm sorry, but I don't know the answer to that question".
70-
AI assistant will not apologize for previous responses, but instead will indicated new information was gained.
71-
AI assistant will not invent anything that is not drawn directly from the context.
72-
AI assistant will answer coding questions.
73-
`,
74-
};
75-
76-
console.log("Prompt context:", prompt);
65+
const coreMessages = convertToCoreMessages(messages);
7766

7867
const response = await streamText({
79-
model: vertex.languageModel("gemini-2.0-flash-exp"),
80-
prompt,
81-
messages: messages.filter((message) => message.role === "user"),
68+
model: vertex("gemini-2.5-pro"),
69+
system: [systemPromptContent, smartSearchPrompt].join("\n"),
70+
messages: coreMessages,
71+
tools: {
72+
smartSearchTool,
73+
},
74+
onError: (error) => {
75+
console.error("Error during streaming:", error);
76+
return new Response(ReasonPhrases.INTERNAL_SERVER_ERROR, {
77+
status: StatusCodes.INTERNAL_SERVER_ERROR,
78+
});
79+
},
80+
onToolCall: async (toolCall) => {
81+
console.log("Tool call initiated:", toolCall);
82+
},
83+
onStepFinish: async (result) => {
84+
console.log("Step finished:", result);
85+
if (result.usage) {
86+
console.log(
87+
`[Token Usage] Prompt tokens: ${result.usage.promptTokens}, Completion tokens: ${result.usage.completionTokens}, Total tokens: ${result.usage.totalTokens}`,
88+
);
89+
}
90+
},
91+
maxSteps: 5,
8292
});
8393

84-
console.log("Response from AI:", response);
85-
86-
return response.toTextStreamResponse();
94+
return response.toDataStreamResponse();
8795
} catch (error) {
8896
console.error("Error in chat API:", error);
8997
return new Response(ReasonPhrases.INTERNAL_SERVER_ERROR, {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useEffect, useState } from "react";
2+
import {
3+
HiOutlineChatBubbleLeftRight,
4+
HiOutlineXCircle,
5+
} from "react-icons/hi2";
6+
import { useChatDialog } from "./state";
7+
8+
export default function ChatButton() {
9+
const { dialog } = useChatDialog();
10+
const [isOpen, setIsOpen] = useState(false);
11+
12+
useEffect(() => {
13+
const dialogElement = dialog.current;
14+
15+
const handleDialogToggle = () => {
16+
setIsOpen(!isOpen);
17+
};
18+
19+
dialogElement?.addEventListener("toggle", handleDialogToggle);
20+
21+
return () => {
22+
dialogElement?.removeEventListener("toggle", handleDialogToggle);
23+
};
24+
}, [dialog, isOpen, setIsOpen]);
25+
26+
return (
27+
<button
28+
id="chat-button"
29+
type="button"
30+
className="fixed right-4 bottom-4 z-50 flex h-12 w-12 animate-pulse cursor-pointer items-center justify-center rounded-full bg-blue-600 text-white shadow-lg transition-transform hover:scale-105 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 md:right-8 md:bottom-12"
31+
aria-label={isOpen ? "Open chat" : "Close chat"}
32+
onClick={() => {
33+
return isOpen ? dialog.current?.close() : dialog.current?.show();
34+
}}
35+
>
36+
<span className="sr-only">{isOpen ? "Open chat" : "Close chat"}</span>
37+
{isOpen ? (
38+
<HiOutlineXCircle className="h-6 w-6" />
39+
) : (
40+
<HiOutlineChatBubbleLeftRight className="h-6 w-6" />
41+
)}
42+
</button>
43+
);
44+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useChat } from "ai/react";
2+
import { useEffect } from "react";
3+
import { HiXCircle } from "react-icons/hi2";
4+
import { useChatDialog } from "./state";
5+
import Chat from "@/components/chat/chat";
6+
import "./chat.css";
7+
8+
export default function ChatDialog() {
9+
const { dialog } = useChatDialog();
10+
const {
11+
messages,
12+
input,
13+
handleInputChange,
14+
handleSubmit,
15+
setMessages,
16+
status,
17+
} = useChat();
18+
19+
useEffect(() => {
20+
if (messages.length === 0) {
21+
setMessages([
22+
{
23+
role: "assistant",
24+
content: "Welcome to the Smart Search chatbot!",
25+
id: "welcome",
26+
},
27+
]);
28+
}
29+
}, [messages, setMessages]);
30+
31+
return (
32+
<dialog
33+
ref={dialog}
34+
id="chat-dialog"
35+
role="application"
36+
className="fixed right-4 bottom-8 left-auto max-w-sm overflow-visible rounded-lg bg-gray-800 p-4 md:right-8 md:bottom-32 md:p-6"
37+
// eslint-disable-next-line react/no-unknown-property
38+
closedby="any"
39+
>
40+
<button
41+
// eslint-disable-next-line react/no-unknown-property
42+
formmethod="dialog"
43+
type="button"
44+
form="chat-form"
45+
aria-label="Close chat"
46+
className="absolute -top-2 -right-2 text-gray-500 hover:text-gray-300"
47+
onClick={() => {
48+
dialog.current?.close();
49+
}}
50+
>
51+
<span className="sr-only">Close chat</span>
52+
<HiXCircle className="h-6 w-6" />
53+
</button>
54+
<section className="bg-slate-950">
55+
<Chat
56+
input={input}
57+
handleInputChange={handleInputChange}
58+
handleMessageSubmit={handleSubmit}
59+
messages={messages}
60+
status={status}
61+
/>
62+
</section>
63+
</dialog>
64+
);
65+
}

src/components/chat/chat-input.jsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { HiOutlinePaperAirplane } from "react-icons/hi2";
2+
3+
export default function Input({ input, handleInputChange }) {
4+
return (
5+
<div className="mx-auto w-full max-w-2xl rounded-xl bg-gray-800 p-4 shadow-lg">
6+
<input
7+
type="text"
8+
value={input}
9+
onChange={handleInputChange}
10+
autoFocus
11+
placeholder="Ask Smart Search about Faust..."
12+
className="text-md mb-3 w-full bg-transparent text-gray-200 placeholder-gray-500 focus:outline-none"
13+
/>
14+
15+
<div className="flex">
16+
<button
17+
type="submit"
18+
className="ml-auto rounded-md p-1 transition-colors hover:bg-gray-700"
19+
aria-label="Send message"
20+
disabled={!input.trim()}
21+
>
22+
<HiOutlinePaperAirplane className="h-5 w-5" />
23+
</button>
24+
</div>
25+
</div>
26+
);
27+
}

src/components/chat/chat.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.gemini-text {
2+
font-family:
3+
Google Sans,
4+
Helvetica Neue,
5+
sans-serif;
6+
/* Choose a font that matches, Arial is a common sans-serif */
7+
/* font-size: 100px; */
8+
/* Adjust size as needed */
9+
/* Gradient: Adjust colors and angles as needed */
10+
background: linear-gradient(to right, #6b50d2, #c25cb6, #ea668a);
11+
/*
12+
Color Hex codes approximated from the image:
13+
Left (Blue-Purple): #6B50D2
14+
Middle (Pink-Purple): #C25CB6
15+
Right (Reddish-Pink): #EA668A
16+
*/
17+
18+
/* Crucial properties for text gradient */
19+
-webkit-background-clip: text;
20+
/* For Safari/Chrome */
21+
background-clip: text;
22+
color: transparent;
23+
/* Makes the original text color transparent */
24+
25+
/* Optional: A slight text shadow can make it pop on some backgrounds */
26+
/* text-shadow: 2px 2px 4px rgba(0,0,0,0.1); */
27+
}

src/components/chat/chat.jsx

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,26 @@
1-
// import { Message } from "ai/react";
2-
import Messages from "./Messages";
3-
4-
// interface Chat {
5-
// input: string;
6-
// handleInputChange: (e: ChangeEvent<HTMLInputElement>) => void;
7-
// handleMessageSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
8-
// messages: Message[];
9-
// }
1+
import { HiOutlineArrowPath } from "react-icons/hi2";
2+
import ChatInput from "./chat-input";
3+
import Messages from "./messages";
104

115
export default function Chat({
126
input,
137
handleInputChange,
148
handleMessageSubmit,
9+
status,
1510
messages,
1611
}) {
1712
return (
18-
<div id="chat" className="mx-2 flex w-full flex-col">
13+
<div id="chat" className="flex h-full w-full flex-col">
1914
<Messages messages={messages} />
15+
{status === "submitted" && (
16+
<HiOutlineArrowPath className="mx-auto h-5 w-5 animate-spin" />
17+
)}
2018
<form
19+
id="chat-form"
2120
onSubmit={handleMessageSubmit}
22-
className="relative mt-5 mb-5 ml-1 rounded-lg bg-gray-500"
21+
className="relative mt-5 mb-5 ml-1 rounded-lg"
2322
>
24-
<input
25-
type="text"
26-
className="input-glow focus:shadow-outline w-full appearance-none rounded border border-gray-100 bg-gray-100 px-3 py-2 pr-10 pl-3 leading-tight text-gray-700 transition-shadow duration-200 focus:outline-none"
27-
value={input}
28-
onChange={handleInputChange}
29-
/>
30-
31-
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400">
32-
Press ⮐ to send
33-
</span>
23+
<ChatInput input={input} handleInputChange={handleInputChange} />
3424
</form>
3525
</div>
3626
);

0 commit comments

Comments
 (0)