(null);
+
+
+
+ useEffect(() => {
+ if (ttsSupported && voices.length > 0) {
+ const defaultVoice = voices.find((v) => v.default) || voices[0];
+ setSelectedVoice(defaultVoice!);
+ }
+ }, [voices, ttsSupported]);
+
+
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ };
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messages]);
+
+
+
+
+ const handleCopy = async (content: string) => {
+ try {
+ await navigator.clipboard.writeText(content);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000); // Reset after 2s
+ } catch (err) {
+ console.error("Failed to copy: ", err);
+ }
+ };
+
+
+
+ return (
+
+
+
+
+ {messages.length === 0 ? (
+
+
+
+ ) : (
+
+
+ {messages.map((message) => (
+
+
+
+ {children}
+
+ ) : (
+
+
+
{match ? match[1] : "text"}
+
+
+
+
+
+
+ {codeContent}
+
+
+ );
+ },
+ strong: (props) => (
+
+ {props.children}
+
+ ),
+ a: (props) => (
+
+ {props.children}
+
+ ),
+ h1: (props) => (
+
+ {props.children}
+
+ ),
+ h2: (props) => (
+
+ {props.children}
+
+ ),
+ h3: (props) => (
+
+ {props.children}
+
+ ),
+ }}
+ >
+ {message.content}
+
+
+
+ {message.role === "assistant" && (
+
+
+
+
+
+
+ )}
+ {message.role === "user" && (
+
+ )}
+
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default SharedChat;
diff --git a/src/app/_components/get-started.tsx b/src/app/_components/get-started.tsx
index 590db97..f37d567 100644
--- a/src/app/_components/get-started.tsx
+++ b/src/app/_components/get-started.tsx
@@ -1,4 +1,5 @@
import { Button } from "@/components/ui/button";
+import Link from "next/link";
export const GetStarted = () => {
return (
@@ -25,7 +26,9 @@ export const GetStarted = () => {
We promise , we dont spam with useless mails
-
+
>
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index ef6ecdb..a46e6cc 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,18 +1,13 @@
import "@/styles/globals.css";
-
-import { type Metadata } from "next";
+import type { Metadata } from "next";
import { Geist, Inter, Playfair_Display, Roboto } from "next/font/google";
import { TRPCReactProvider } from "@/trpc/react";
import localFont from "next/font/local";
import { FontProvider } from "@/contexts/font-context";
import { BlurProvider } from "@/contexts/blur-context";
import { Toaster } from "sonner";
-
-export const metadata: Metadata = {
- title: "t3.chat",
- description: "t3.chat",
- icons: [{ rel: "icon", url: "/favicon.ico" }],
-};
+import { siteConfig } from "@/config/site";
+export const metadata: Metadata = siteConfig;
const proxima = localFont({
src: "../app/proxima_vara.woff2",
diff --git a/src/components/ui/pricing-button.tsx b/src/components/ui/pricing-button.tsx
index bc2bdf3..6f4c2d9 100644
--- a/src/components/ui/pricing-button.tsx
+++ b/src/components/ui/pricing-button.tsx
@@ -3,6 +3,7 @@ import axios from "axios";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
+import { toast } from "sonner";
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
@@ -28,16 +29,20 @@ export default function PricingButton({
amount,
});
+ if(response.status === 429){
+ toast.error("Too Many Request on checkout page, please try again after sometime!")
+ }
const session = response.data;
if (!session?.id) throw new Error("No session ID received");
const result = await stripe.redirectToCheckout({ sessionId: session.id });
+
if (result.error) {
throw new Error(result.error.message);
}
+
} catch (error) {
console.error("Checkout error:", error);
- alert("Something went wrong. Please try again.");
} finally {
setIsLoading(false);
}
diff --git a/src/components/ui/pricing-card.tsx b/src/components/ui/pricing-card.tsx
index 1db8336..097cf22 100644
--- a/src/components/ui/pricing-card.tsx
+++ b/src/components/ui/pricing-card.tsx
@@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { CheckIcon, ArrowRightIcon } from "@radix-ui/react-icons";
import PricingButton from "./pricing-button";
-import { useRouter } from "next/navigation";
+import { redirect, useRouter } from "next/navigation";
export interface PricingFeature {
name: string;
@@ -130,9 +130,9 @@ export function PricingCards({
)}
onClick={() => {
if (tier.name === "Free") {
- router.push("/auth");
+ redirect("https://x.com/10Xpraash");
} else if (tier.name === "Enterprise") {
- router.push("/feedback");
+ redirect("https://x.com/10Xpraash");
}
}}
>
diff --git a/src/components/ui/ui-structure.tsx b/src/components/ui/ui-structure.tsx
index d2a70a4..d2c2615 100644
--- a/src/components/ui/ui-structure.tsx
+++ b/src/components/ui/ui-structure.tsx
@@ -22,6 +22,7 @@ import {
BookmarkIcon,
DotsThreeVertical,
MagnifyingGlassIcon,
+ ShareFatIcon,
TrashIcon,
} from "@phosphor-icons/react";
import { Separator } from "./separator";
@@ -37,6 +38,7 @@ import { useRouter } from "next/navigation";
import { T3Chat } from "../svgs/t3chat";
import type { User } from "@prisma/client";
import { useSession } from "next-auth/react";
+import { Share, ShareIcon } from "lucide-react";
const giest = Geist({
display: "swap",
@@ -193,6 +195,20 @@ export function UIStructure() {
className="hover:text-foreground size-4"
/>
+ {
+ e.preventDefault();
+ const shareLink = process.env.NEXT_PUBLIC_APP_URL + `/chat/share/${chat.id}`
+ navigator.clipboard.writeText(shareLink)
+ toast.success("Share link copied to clipboard")
+ }}
+ >
+
+
{
@@ -263,6 +279,21 @@ export function UIStructure() {
/>
+ {
+ e.preventDefault();
+ const shareLink = process.env.NEXT_PUBLIC_APP_URL + `/chat/share/${chat.id}`
+ navigator.clipboard.writeText(shareLink)
+ toast.success("Share link copied to clipboard")
+ }}
+ >
+
+
+
handleDeleteChat(chat.id)}
diff --git a/src/config/site.ts b/src/config/site.ts
new file mode 100644
index 0000000..2409bc2
--- /dev/null
+++ b/src/config/site.ts
@@ -0,0 +1,39 @@
+import type { Metadata } from 'next';
+
+const TITLE = 'T3chat - An Open-source, user-friendly fast AI response chat app';
+const DESCRIPTION =
+ 'T3chat is a platform that allows you to chat with AI, support different LLM, respond very fast, user friendly, have customization, cheap.';
+
+const BASE_URL = process.env.NEXT_PUBLIC_APP_URL;
+
+export const siteConfig: Metadata = {
+ title: TITLE,
+ description: DESCRIPTION,
+ icons: {
+ icon: '/favicon.ico',
+ },
+ applicationName: 'T3chat',
+ creator: 'praash',
+
+ category: 'AI',
+ alternates: {
+ canonical: BASE_URL,
+ },
+ keywords: [
+ 'T3chat',
+ 'AI',
+ 'LLM',
+ 'Fast',
+ 'User friendly',
+ 'Customization',
+ 'Cheap',
+ 'web3',
+ 'blockchain',
+ 'open-source',
+ 'self-hosted',
+ 'self-hosting',
+ 'self-host',
+ 'self-hosting',
+ ],
+ metadataBase: new URL(BASE_URL!),
+};
\ No newline at end of file
diff --git a/src/server/api/routers/chat.ts b/src/server/api/routers/chat.ts
index 889287e..c68a083 100644
--- a/src/server/api/routers/chat.ts
+++ b/src/server/api/routers/chat.ts
@@ -1,4 +1,4 @@
-import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
+import { createTRPCRouter, protectedProcedure, publicProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { z } from "zod";
@@ -122,6 +122,54 @@ export const chatRouter = createTRPCRouter({
};
}
}),
+ getChatById: publicProcedure
+ .input(z.object({
+ chatId: z.string(),
+ }))
+ .query(async ({ input }) => {
+
+ try {
+ // Verify the chat belongs to the user
+ const chat = await db.chat.findFirst({
+ where: {
+ id: input.chatId,
+ },
+ include: {
+ messages: {
+ orderBy: {
+ createdAt: "asc",
+ },
+ },
+ },
+ });
+
+ if (!chat) {
+ return {
+ message: "Chat not found or access denied",
+ success: false,
+ messages: [],
+ };
+ }
+
+ return {
+ message: "Messages retrieved successfully",
+ success: true,
+ messages: chat.messages.map((message) => ({
+ id: message.id,
+ content: message.content,
+ role: message.role,
+ createdAt: message.createdAt,
+ })),
+ };
+ } catch (error) {
+ console.error("Error getting messages:", error);
+ return {
+ message: "Something went wrong. Please try again.",
+ success: false,
+ messages: [],
+ };
+ }
+ }),
saveMessage: protectedProcedure
.input(z.object({