@@ -60,8 +125,8 @@ export default function AboutPage() {
business runs day-to-day. From there, we design and build
systems that are straightforward to use, dependable over time,
and flexible enough to evolve as your needs change. Our work
- doesn’t end at launch — we stay available to support, refine,
- and improve as your business grows.
+ doesn’t end at launch — we stay available to support,
+ refine, and improve as your business grows.
@@ -109,88 +174,6 @@ export default function AboutPage() {
-
-
-
-
- The Team
-
-
- The People Behind Infinite Robots
-
-
- We’re a small, focused team. We value thoughtful
- engineering, steady collaboration, and systems that feel good to
- use.
-
-
-
-
- Photo
-
- Andrew
-
- Founder / Product & UI/UX
-
-
- Andrew is the founder and product lead, responsible for
- setting the vision and ensuring everything we build is
- thoughtful, polished, and effective. He brings over 10 years
- of full-stack experience across startups and enterprise
- systems. His strength is turning big ideas into software that
- actually delivers.
-
-
-
-
- Photo
-
- Ralph
-
- Distributed Systems & Backend Engineering
-
-
- Ralph is our resident systems wizard — the kind of engineer
- who can summon scalable architecture from thin air and tame
- even the most unruly distributed systems. He’s led teams,
- built platforms, and guided products from vague idea to
- production reality. Around here, we just say: if it’s
- complicated, give it to Ralph.
-
-
-
-
- Photo
-
- Brad
-
- Brand & Marketing Strategy
-
-
- Brad has been making websites since the dawn of time, back
- when you could only Ask Jeeves for help. He has now helped
- over a trillion businesses look and feel as beautiful on the
- outside as they do on the inside with his full stack
- engineering skills to pay the bills.
-
-
-
-
- Photo
-
- Will
-
- All-around Badass and Jack of All Trades
-
-
- Bio coming soon...
-
-
-
-
-
-
-
diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts
new file mode 100644
index 0000000..94d08d4
--- /dev/null
+++ b/app/api/chat/route.ts
@@ -0,0 +1,42 @@
+import {
+ streamText,
+ createUIMessageStreamResponse,
+ convertToModelMessages,
+ createGateway,
+} from "ai";
+import { CHAT_SYSTEM_PROMPT } from "@/lib/prompts/chat-system-prompt";
+
+export const runtime = "edge";
+
+// Use Vercel AI Gateway - model-agnostic!
+// Just specify the model as a string like "anthropic/claude-3.5-haiku"
+// The gateway handles routing to the correct provider
+const gateway = createGateway({
+ apiKey: process.env.AI_GATEWAY_API_KEY,
+});
+
+export async function POST(req: Request) {
+ try {
+ const { messages } = await req.json();
+
+ // Convert UIMessages to ModelMessages for streamText
+ // Previous messages are now included in the messages array (prepended on first message)
+ const modelMessages = convertToModelMessages(messages);
+
+ // Stream AI response (no Discord involvement - that's handled client-side)
+ const result = streamText({
+ model: gateway("anthropic/claude-haiku-4.5"),
+ system: CHAT_SYSTEM_PROMPT,
+ messages: modelMessages,
+ });
+
+ const stream = result.toUIMessageStream();
+
+ return createUIMessageStreamResponse({
+ stream,
+ });
+ } catch (error) {
+ console.error("Chat API error:", error);
+ return new Response("Internal Server Error", { status: 500 });
+ }
+}
diff --git a/app/api/discord/message/route.ts b/app/api/discord/message/route.ts
new file mode 100644
index 0000000..37c78f4
--- /dev/null
+++ b/app/api/discord/message/route.ts
@@ -0,0 +1,134 @@
+export const runtime = "edge";
+
+function getClientIP(req: Request): string | null {
+ // Try various headers that might contain the IP
+ const forwardedFor = req.headers.get("x-forwarded-for");
+ if (forwardedFor) {
+ // x-forwarded-for can contain multiple IPs, take the first one
+ return forwardedFor.split(",")[0].trim();
+ }
+
+ const realIP = req.headers.get("x-real-ip");
+ if (realIP) {
+ return realIP;
+ }
+
+ const cfConnectingIP = req.headers.get("cf-connecting-ip"); // Cloudflare
+ if (cfConnectingIP) {
+ return cfConnectingIP;
+ }
+
+ return null;
+}
+
+// Discord message content limit (2000 characters)
+const DISCORD_MESSAGE_MAX_LENGTH = 2000;
+
+export async function POST(req: Request) {
+ try {
+ const { threadId, content, role } = await req.json();
+
+ if (!threadId || !content || !role) {
+ return new Response(
+ JSON.stringify({ error: "threadId, content, and role are required" }),
+ {
+ status: 400,
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ // Validate content length (Discord limit is 2000 characters)
+ if (
+ typeof content !== "string" ||
+ content.length > DISCORD_MESSAGE_MAX_LENGTH
+ ) {
+ return new Response(
+ JSON.stringify({
+ error: `Content must be a string and less than ${DISCORD_MESSAGE_MAX_LENGTH} characters`,
+ }),
+ {
+ status: 400,
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ // Validate role
+ if (role !== "user" && role !== "assistant") {
+ return new Response(
+ JSON.stringify({ error: "role must be 'user' or 'assistant'" }),
+ {
+ status: 400,
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ const botToken = process.env.DISCORD_BOT_TOKEN;
+
+ if (!botToken) {
+ return new Response(JSON.stringify({ error: "Discord not configured" }), {
+ status: 500,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+
+ // Get client IP address
+ const clientIP = getClientIP(req);
+
+ // Format message for Discord
+ const formattedMessage = formatDiscordMessage({
+ content,
+ role,
+ clientIP,
+ });
+
+ // Post message to thread using Bot API
+ const response = await fetch(
+ `https://discord.com/api/v10/channels/${threadId}/messages`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bot ${botToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ content: formattedMessage,
+ }),
+ },
+ );
+
+ if (!response.ok) {
+ const error = await response.text();
+ console.error("Failed to post to Discord:", error);
+ return new Response(JSON.stringify({ error: "Failed to post message" }), {
+ status: 500,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+
+ return new Response(JSON.stringify({ success: true }), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+ } catch (error) {
+ console.error("Error posting to Discord:", error);
+ return new Response(JSON.stringify({ error: "Internal server error" }), {
+ status: 500,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+}
+
+function formatDiscordMessage(message: {
+ content: string;
+ role: "user" | "assistant";
+ clientIP?: string | null;
+}): string {
+ if (message.role === "user") {
+ const ipLabel = message.clientIP ? ` (${message.clientIP})` : "";
+ return `**User${ipLabel}:**\n${message.content}`;
+ }
+ return `**AI Assistant:**\n${message.content}`;
+}
diff --git a/app/api/discord/thread/route.ts b/app/api/discord/thread/route.ts
new file mode 100644
index 0000000..b155262
--- /dev/null
+++ b/app/api/discord/thread/route.ts
@@ -0,0 +1,83 @@
+/**
+ * API route to create a Discord thread for a chat session
+ * Thread ID is stored client-side in localStorage, so this endpoint just creates new threads
+ */
+
+export const runtime = "edge";
+
+// Discord thread type constants
+const DISCORD_THREAD_TYPE_PUBLIC = 11;
+const DISCORD_AUTO_ARCHIVE_DURATION_24H = 1440; // minutes
+
+export async function POST(req: Request) {
+ try {
+ const { chatId, firstMessage } = await req.json();
+
+ if (!chatId) {
+ return new Response(JSON.stringify({ error: "chatId is required" }), {
+ status: 400,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+
+ const botToken = process.env.DISCORD_BOT_TOKEN;
+ const channelId = process.env.DISCORD_CHANNEL_ID;
+
+ if (!botToken || !channelId) {
+ return new Response(JSON.stringify({ error: "Discord not configured" }), {
+ status: 500,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+
+ // Create new thread
+ const threadName = `Chat ${chatId.slice(0, 8)} - ${new Date().toLocaleDateString()}`;
+ const initialMessage = firstMessage
+ ? `Chat session started. First message: "${firstMessage.substring(0, 200)}${firstMessage.length > 200 ? "..." : ""}"`
+ : "New chat session started";
+
+ const response = await fetch(
+ `https://discord.com/api/v10/channels/${channelId}/threads`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bot ${botToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: threadName,
+ type: DISCORD_THREAD_TYPE_PUBLIC,
+ auto_archive_duration: DISCORD_AUTO_ARCHIVE_DURATION_24H,
+ message: {
+ content: initialMessage,
+ },
+ }),
+ },
+ );
+
+ if (!response.ok) {
+ const error = await response.text();
+ console.error("Failed to create Discord thread:", error);
+ return new Response(
+ JSON.stringify({ error: "Failed to create thread" }),
+ {
+ status: 500,
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ const thread = await response.json();
+
+ return new Response(JSON.stringify({ threadId: thread.id }), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+ } catch (error) {
+ console.error("Error in thread creation:", error);
+ return new Response(JSON.stringify({ error: "Internal server error" }), {
+ status: 500,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+}
diff --git a/app/contact/page.tsx b/app/contact/page.tsx
index d01bf19..cac7f04 100644
--- a/app/contact/page.tsx
+++ b/app/contact/page.tsx
@@ -12,20 +12,9 @@ export default function ContactPage() {
-
-
- Or just send us an email directly at{" "}
-
- hello@infinite-robots.com
-
-
-
We respond to all inquiries within one business day.
diff --git a/app/layout.tsx b/app/layout.tsx
index 5a46e02..9f441ef 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,5 +1,7 @@
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
+import { ChatWidget } from "@/components/chat/ChatWidget";
+import { ChatProvider } from "@/components/chat/ChatContext";
import { ResponsiveHeader } from "@/components/common/ResponsiveHeader";
import { SiteFooter } from "@/components/common/SiteFooter";
import { ThemeProvider } from "@/components/common/ThemeProvider";
@@ -55,13 +57,16 @@ export default function RootLayout({
dangerouslySetInnerHTML={{ __html: themeInitScript }}
/>
-
+
+
+