From a66ab4212485fd67c04b5851669f3e863ccbf298 Mon Sep 17 00:00:00 2001 From: Prashant Varma Date: Wed, 18 Jun 2025 14:27:54 +0530 Subject: [PATCH 1/3] fix: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06697cb..2a4b090 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 𝙏3𝙘𝒉𝙖𝒕 -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2F100xEnginners%2Ft3dotgg&env=DATABASE_URL,BETTER_AUTH_SECRET,BETTER_AUTH_URL,BETTER_AUTH_TRUSTED_ORIGINS,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GOOGLE_REDIRECT_URI,GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET&envDescription=For%20more%20info%20on%20setting%20up%20your%20API%20keys%2C%20checkout%20the%20Readme%20below&envLink=https%3A%2F%2Fgithub.com%2F100xEnginners%2Ft3dotgg%2Fblob%2Fmain%2FREADME.md&project-name=zero-email&repository-name=zero-email&redirect-url=zero.email&demo-title=Zero&demo-description=An%20open%20source%20email%20app&demo-url=zero.email) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2F100xEnginners%2Ft3dotgg&env=DATABASE_URL,BETTER_AUTH_SECRET,BETTER_AUTH_URL,BETTER_AUTH_TRUSTED_ORIGINS,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GOOGLE_REDIRECT_URI,GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET&envDescription=For%20more%20info%20on%20setting%20up%20your%20API%20keys%2C%20checkout%20the%20Readme%20below&envLink=https%3A%2F%2Fgithub.com%2F100xEnginners%2Ft3dotgg%2Fblob%2Fmain%2FREADME.md&project-name=t3chat&repository-name=t3chat&redirect-url=t3chat&demo-title=T3chat&demo-description=An%20open%20source%20t3chat%20clone&demo-url=t3chat.xyz) An Open-Source Version of t3.chat. From 1f03c4deb6ae77501559d2994a0d4057b3b1eb92 Mon Sep 17 00:00:00 2001 From: Prashant Varma Date: Wed, 18 Jun 2025 17:58:07 +0530 Subject: [PATCH 2/3] fix: siteconfig --- src/app/layout.tsx | 11 +++-------- src/config/site.ts | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 src/config/site.ts 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/config/site.ts b/src/config/site.ts new file mode 100644 index 0000000..7d17427 --- /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 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 From a81a7bff0fc3dae942d41026fe59b314cef39f28 Mon Sep 17 00:00:00 2001 From: Prashant Varma Date: Thu, 19 Jun 2025 00:09:24 +0530 Subject: [PATCH 3/3] feat: share chat last moment lfg --- .github/workflows/deploy.yml | 30 +- src/app/(app)/chat/share/[chatId]/page.tsx | 13 + src/app/_components/SharedChat.tsx | 327 +++++++++++++++++++++ src/app/_components/get-started.tsx | 5 +- src/components/ui/pricing-button.tsx | 7 +- src/components/ui/pricing-card.tsx | 6 +- src/components/ui/ui-structure.tsx | 31 ++ src/config/site.ts | 2 +- src/server/api/routers/chat.ts | 50 +++- 9 files changed, 448 insertions(+), 23 deletions(-) create mode 100644 src/app/(app)/chat/share/[chatId]/page.tsx create mode 100644 src/app/_components/SharedChat.tsx diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d72e63e..12261b2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,6 @@ name: Deploy to Production on: push: branches: [ master ] - workflow_dispatch: jobs: deploy: @@ -12,25 +11,24 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: 20 - - name: Install dependencies run: bun install --frozen-lockfile - name: Build the application run: bun run build - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Docker login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: 20 - \ No newline at end of file + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: ./docker/Dockerfile.frontend + build-args: + - DATABASE_URL=${{ secrets.DATABASE_URL }} + push: true + tags: t3-chat:${{ github.sha }} diff --git a/src/app/(app)/chat/share/[chatId]/page.tsx b/src/app/(app)/chat/share/[chatId]/page.tsx new file mode 100644 index 0000000..3609e22 --- /dev/null +++ b/src/app/(app)/chat/share/[chatId]/page.tsx @@ -0,0 +1,13 @@ +"use client"; +import SharedChat from "@/app/_components/SharedChat"; +import { useParams } from "next/navigation"; + +export default function SingleChatPage() { + const { chatId } = useParams(); + console.log(chatId) + return ( + <> + + + ); +} diff --git a/src/app/_components/SharedChat.tsx b/src/app/_components/SharedChat.tsx new file mode 100644 index 0000000..7feb790 --- /dev/null +++ b/src/app/_components/SharedChat.tsx @@ -0,0 +1,327 @@ +"use client"; +import React, { useState, useRef, useEffect } from "react"; +import { + CopyIcon, + ThumbsDownIcon, + ThumbsUpIcon, + SpeakerHighIcon, + SpeakerXIcon, + CheckIcon, + CheckCircleIcon, +} from "@phosphor-icons/react"; +import ReactMarkdown from "react-markdown"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import remarkGfm from "remark-gfm"; +import { Geist_Mono } from "next/font/google"; +import { cn } from "@/lib/utils"; +import { useSpeechSynthesis } from "react-speech-kit"; +import { useTheme } from "next-themes"; +import { Loader2Icon, WrapText } from "lucide-react"; +import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; +import { api } from "@/trpc/react"; + +const geistMono = Geist_Mono({ + subsets: ["latin"], + variable: "--font-mono", + preload: true, + display: "swap", +}); + +interface ChatMessage { + id: string; + role: "user" | "assistant"; + content: string; +} + + +const SharedChat = ({ chatId: initialChatId }: { chatId: string }) => { + const [messages, setMessages] = useState([]); + const [copied, setCopied] = useState(false); + const messagesEndRef = useRef(null); + const { resolvedTheme } = useTheme(); + const [chatId, setChatId] = useState(initialChatId); + + const {data: chatMessages} = api.chat.getChatById.useQuery({ + chatId: chatId, + }); + + useEffect(() => { + setChatId(initialChatId); + }, [initialChatId]); + + useEffect(() => { + if (chatMessages) { + setMessages(chatMessages.messages.map((message) => ({ + id: message.id, + role: message.role === "USER" ? "user" : "assistant", + content: message.content, + }))); + } + }, [chatMessages]); + + + + const { + speak, + cancel, + speaking, + supported: ttsSupported, + voices, + } = useSpeechSynthesis(); + const [selectedVoice, setSelectedVoice] = + useState(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/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 index 7d17427..2409bc2 100644 --- a/src/config/site.ts +++ b/src/config/site.ts @@ -1,6 +1,6 @@ import type { Metadata } from 'next'; -const TITLE = 'T3chat - An Open-source, user-friendly fast AI response app'; +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.'; 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({