├── public
├── g22.png
├── g23.png
├── p8.png
└── spark.png
├── src
├── app
│ ├── icon.png
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ ├── route.ts
│ │ │ │ └── options.ts
│ │ ├── corePrompt
│ │ │ └── save
│ │ │ │ └── route.ts
│ │ ├── interaction
│ │ │ ├── delete
│ │ │ │ └── route.ts
│ │ │ ├── get
│ │ │ │ └── route.ts
│ │ │ └── save
│ │ │ │ └── route.ts
│ │ ├── improve
│ │ │ └── route.ts
│ │ └── generate
│ │ │ └── route.ts
│ ├── font.ts
│ ├── page.tsx
│ ├── Providers.tsx
│ ├── layout.tsx
│ ├── history
│ │ ├── components
│ │ │ └── Utility.tsx
│ │ └── page.tsx
│ ├── i
│ │ └── [interactionId]
│ │ │ └── page.tsx
│ └── globals.css
├── types
│ ├── UtilityProps.ts
│ ├── ApiResponse.ts
│ ├── LoginModelProps.ts
│ ├── InteractionPageProps.ts
│ ├── HistoryType.ts
│ └── ResultProps.ts
├── lib
│ ├── genAI.ts
│ ├── utils.ts
│ └── prisma.ts
├── constants
│ ├── corePromptInputPlaceholder.ts
│ └── smaples.ts
├── components
│ ├── Hero.tsx
│ ├── ui
│ │ ├── skeleton.tsx
│ │ ├── textarea.tsx
│ │ ├── input.tsx
│ │ ├── separator.tsx
│ │ ├── tooltip.tsx
│ │ ├── popover.tsx
│ │ ├── avatar.tsx
│ │ ├── border-beam.tsx
│ │ ├── button.tsx
│ │ ├── shiny-button.tsx
│ │ ├── shine-border.tsx
│ │ ├── dialog.tsx
│ │ ├── sheet.tsx
│ │ ├── select.tsx
│ │ ├── dropdown-menu.tsx
│ │ └── sidebar.tsx
│ ├── theme-provider.tsx
│ ├── UsageCount.tsx
│ ├── BackgroundImage.tsx
│ ├── Appbar.tsx
│ ├── LoginModel.tsx
│ ├── Samples.tsx
│ ├── ThemeToggleButton.tsx
│ ├── Footer.tsx
│ ├── TypeWriter.tsx
│ ├── CorePromptForm.tsx
│ ├── Profile.tsx
│ ├── Result.tsx
│ ├── app-sidebar.tsx
│ └── Main.tsx
├── hooks
│ ├── useResult.ts
│ ├── useTweet.ts
│ ├── use-mobile.tsx
│ └── useUsageTracker.ts
└── context
│ ├── ResultContext.tsx
│ └── TweetContext.tsx
├── .vscode
└── settings.json
├── prisma
├── migrations
│ ├── 20250128080146_added_coreprompt
│ │ └── migration.sql
│ ├── migration_lock.toml
│ ├── 20250519143011_added_ip_model
│ │ └── migration.sql
│ ├── 20250123082253_init
│ │ └── migration.sql
│ ├── 20250125060127_changed_interaction_id_type
│ │ └── migration.sql
│ └── 20250123115123_added_interaction_model
│ │ └── migration.sql
└── schema.prisma
├── next.config.ts
├── postcss.config.mjs
├── eslint.config.mjs
├── components.json
├── .gitignore
├── tsconfig.json
├── README.md
├── package.json
└── tailwind.config.ts
/public/g22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fardeen26/flick-ai/HEAD/public/g22.png
--------------------------------------------------------------------------------
/public/g23.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fardeen26/flick-ai/HEAD/public/g23.png
--------------------------------------------------------------------------------
/public/p8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fardeen26/flick-ai/HEAD/public/p8.png
--------------------------------------------------------------------------------
/public/spark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fardeen26/flick-ai/HEAD/public/spark.png
--------------------------------------------------------------------------------
/src/app/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fardeen26/flick-ai/HEAD/src/app/icon.png
--------------------------------------------------------------------------------
/src/types/UtilityProps.ts:
--------------------------------------------------------------------------------
1 | export interface UtilityProps {
2 | aiResponse: string,
3 | id: string
4 | }
--------------------------------------------------------------------------------
/src/types/ApiResponse.ts:
--------------------------------------------------------------------------------
1 | export interface ApiResponse {
2 | success: boolean;
3 | message: string;
4 | };
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "crackboard.sessionKey": "31ab2416049d4fb530c581415028d6c438706bdb65d39c16aeca972c20abbf96"
3 | }
--------------------------------------------------------------------------------
/src/types/LoginModelProps.ts:
--------------------------------------------------------------------------------
1 | export interface LoginModelProps {
2 | onClose: () => void,
3 | showLoginModal: boolean
4 | }
--------------------------------------------------------------------------------
/prisma/migrations/20250128080146_added_coreprompt/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ADD COLUMN "corePrompt" TEXT;
3 |
--------------------------------------------------------------------------------
/src/types/InteractionPageProps.ts:
--------------------------------------------------------------------------------
1 | export interface InteractionPageProps {
2 | params: Promise<{
3 | interactionId: string
4 | }>
5 | }
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (e.g., Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/src/lib/genAI.ts:
--------------------------------------------------------------------------------
1 | import { GoogleGenerativeAI } from '@google/generative-ai';
2 |
3 | export const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY as string);
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | };
6 |
7 | export default nextConfig;
8 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/constants/corePromptInputPlaceholder.ts:
--------------------------------------------------------------------------------
1 | export const corePromptPlaceholder = `e.g. Make tweets engaging, concise, and goal-aligned. Use multi-lines, relevant emojis, and a [tone, e.g., professional, casual].`;
--------------------------------------------------------------------------------
/src/constants/smaples.ts:
--------------------------------------------------------------------------------
1 | export const samples = [
2 | "Coding all night again. Need coffee.",
3 | "I think feb I gotta take some health time off",
4 | "I love reading about human psychology"
5 | ]
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth/next';
2 | import { authOptions } from './options';
3 |
4 | const handler = NextAuth(authOptions);
5 |
6 | export { handler as GET, handler as POST };
--------------------------------------------------------------------------------
/src/components/Hero.tsx:
--------------------------------------------------------------------------------
1 | export default function Hero() {
2 | return (
3 |
4 |
What can I help you refine?
5 |
6 | )
7 | }
--------------------------------------------------------------------------------
/prisma/migrations/20250519143011_added_ip_model/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "UserIp" (
3 | "id" TEXT NOT NULL,
4 | "ipAddress" TEXT NOT NULL,
5 |
6 | CONSTRAINT "UserIp_pkey" PRIMARY KEY ("id")
7 | );
8 |
--------------------------------------------------------------------------------
/src/app/font.ts:
--------------------------------------------------------------------------------
1 | import { Inter } from "next/font/google";
2 |
3 | const inter_init = Inter({
4 | subsets: ["latin"],
5 | display: "swap",
6 | })
7 |
8 | const fontInter = inter_init.className;
9 | export default fontInter;
--------------------------------------------------------------------------------
/src/types/HistoryType.ts:
--------------------------------------------------------------------------------
1 | export interface HistoryType {
2 | id: string,
3 | userPrompt: string,
4 | aiResponse: string,
5 | mood: string | null,
6 | action: string | null,
7 | createdAt: Date,
8 | userId: number
9 | }
--------------------------------------------------------------------------------
/src/types/ResultProps.ts:
--------------------------------------------------------------------------------
1 | export interface ResultProps {
2 | improvePrompt: string;
3 | isImprovingField: boolean;
4 | setImprovePrompt: (improvePrompt: string) => void;
5 | handleRegenerate: () => void;
6 | copyToClipboard: () => void;
7 | saveInteraction: () => void
8 | }
--------------------------------------------------------------------------------
/src/lib/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client'
2 |
3 | const globalForPrisma = global as unknown as { prisma: PrismaClient }
4 |
5 | export const prisma = globalForPrisma.prisma || new PrismaClient()
6 |
7 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
--------------------------------------------------------------------------------
/prisma/migrations/20250123082253_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" SERIAL NOT NULL,
4 | "email" TEXT NOT NULL,
5 | "name" TEXT,
6 | "profileImage" TEXT,
7 |
8 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
9 | );
10 |
11 | -- CreateIndex
12 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
13 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/src/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 |
6 | export function ThemeProvider({
7 | children,
8 | ...props
9 | }: React.ComponentProps) {
10 | return {children}
11 | }
12 |
--------------------------------------------------------------------------------
/src/hooks/useResult.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useContext } from "react";
4 | import { ResultContext } from "@/context/ResultContext";
5 |
6 | export default function useResult() {
7 | const context = useContext(ResultContext);
8 | if (!context) {
9 | throw new Error('useResult must be used within a ResultProvider');
10 | }
11 | return context;
12 | }
13 |
--------------------------------------------------------------------------------
/src/hooks/useTweet.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useContext } from 'react';
4 | import { TweetContext } from "@/context/TweetContext";
5 |
6 | export default function useTweet() {
7 | const context = useContext(TweetContext);
8 |
9 | if (!context) {
10 | throw new Error('useTweet must be used within a TweetProvider');
11 | }
12 |
13 | return context;
14 | }
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/prisma/migrations/20250125060127_changed_interaction_id_type/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - The primary key for the `Interaction` table will be changed. If it partially fails, the table could be left without primary key constraint.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "Interaction" DROP CONSTRAINT "Interaction_pkey",
9 | ALTER COLUMN "id" DROP DEFAULT,
10 | ALTER COLUMN "id" SET DATA TYPE TEXT,
11 | ADD CONSTRAINT "Interaction_pkey" PRIMARY KEY ("id");
12 | DROP SEQUENCE "Interaction_id_seq";
13 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/src/context/ResultContext.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { createContext, useState } from "react";
4 |
5 | type ResultContextType = {
6 | result: string;
7 | setResult: (result: string) => void;
8 | };
9 |
10 | export const ResultContext = createContext(undefined);
11 |
12 | export default function ResultProvider({ children }: { children: React.ReactNode }) {
13 | const [result, setResult] = useState('');
14 | return {children};
15 | }
--------------------------------------------------------------------------------
/src/components/UsageCount.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { MAX_FREE_USES, useUsageTracker } from "@/hooks/useUsageTracker";
4 | import { useSession } from "next-auth/react";
5 | import { CgInfinity } from "react-icons/cg";
6 |
7 | export default function UsageCount() {
8 | const { usageCount } = useUsageTracker();
9 | const { data: session } = useSession()
10 | return (
11 | Credit Left: {session?.user ? : MAX_FREE_USES - usageCount}
12 | )
13 | }
--------------------------------------------------------------------------------
/src/context/TweetContext.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { createContext, useState } from "react";
4 |
5 | type TweetContextType = {
6 | tweet: string;
7 | setTweet: (tweet: string) => void;
8 | };
9 |
10 | export const TweetContext = createContext(undefined);
11 |
12 | export default function TweetProvider({ children }:
13 | { children: React.ReactNode }
14 | ) {
15 | const [tweet, setTweet] = useState('');
16 | return
17 | {children}
18 | ;
19 | };
--------------------------------------------------------------------------------
/prisma/migrations/20250123115123_added_interaction_model/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Interaction" (
3 | "id" SERIAL NOT NULL,
4 | "userPrompt" TEXT NOT NULL,
5 | "aiResponse" TEXT NOT NULL,
6 | "mood" TEXT,
7 | "action" TEXT,
8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
9 | "userId" INTEGER NOT NULL,
10 |
11 | CONSTRAINT "Interaction_pkey" PRIMARY KEY ("id")
12 | );
13 |
14 | -- AddForeignKey
15 | ALTER TABLE "Interaction" ADD CONSTRAINT "Interaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
16 |
--------------------------------------------------------------------------------
/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Appbar from "@/components/Appbar";
2 | import Footer from "@/components/Footer";
3 | import Hero from "@/components/Hero";
4 | import Main from "@/components/Main";
5 | import Samples from "@/components/Samples";
6 |
7 | export default function Home() {
8 | return (
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/src/components/BackgroundImage.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes";
4 | import Image from "next/image";
5 | import { useEffect, useState } from "react";
6 |
7 | export default function BackgroundImage() {
8 | const { theme } = useTheme()
9 | const [background, setBackground] = useState('/g23.png');
10 |
11 | useEffect(() => {
12 | setBackground(theme == 'dark' ? '/g23.png' : '/g22.png')
13 | }, [theme])
14 |
15 | return (
16 |
24 | )
25 | }
--------------------------------------------------------------------------------
/src/app/Providers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import ResultProvider from "@/context/ResultContext";
4 | import TweetProvider from "@/context/TweetContext";
5 | import { ThemeProvider } from "@/components/theme-provider";
6 | import { SessionProvider } from "next-auth/react";
7 |
8 | export default function Providers({ children }: { children: React.ReactNode }) {
9 | return
10 |
16 |
17 |
18 | {children}
19 |
20 |
21 |
22 | ;
23 | }
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id Int @id @default(autoincrement())
12 | email String @unique
13 | name String?
14 | profileImage String?
15 | corePrompt String?
16 | interactions Interaction[]
17 | }
18 |
19 | model Interaction {
20 | id String @id @default(uuid())
21 | userPrompt String
22 | aiResponse String
23 | mood String?
24 | action String?
25 | createdAt DateTime @default(now())
26 | userId Int
27 | user User @relation(fields: [userId], references: [id])
28 | }
29 |
30 | model UserIp {
31 | id String @id @default(uuid())
32 | ipAddress String
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Appbar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import ThemeToggleButton from "./ThemeToggleButton";
3 | import Profile from "./Profile";
4 |
5 | export default function Appbar() {
6 | return (
7 |
8 |
20 |
21 | )
22 | }
--------------------------------------------------------------------------------
/src/hooks/useUsageTracker.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export const MAX_FREE_USES = 1;
4 |
5 | export const useUsageTracker = () => {
6 | const [usageCount, setUsageCount] = useState(0);
7 |
8 | useEffect(() => {
9 | const count = localStorage.getItem("usageCount");
10 | setUsageCount(count ? parseInt(count, 10) : 0);
11 | }, []);
12 |
13 | const incrementUsage = () => {
14 | const newCount = usageCount + 1;
15 | localStorage.setItem("usageCount", newCount.toString());
16 | setUsageCount(newCount);
17 | };
18 |
19 | const resetUsage = () => {
20 | localStorage.removeItem("usageCount");
21 | setUsageCount(0);
22 | };
23 |
24 | return { usageCount, incrementUsage, resetUsage, isLimitReached: usageCount >= MAX_FREE_USES };
25 | };
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/src/components/LoginModel.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { signIn } from "next-auth/react";
3 | import {
4 | Dialog,
5 | DialogContent,
6 | DialogDescription,
7 | DialogHeader,
8 | DialogTitle,
9 | } from "@/components/ui/dialog"
10 | import { LoginModelProps } from "@/types/LoginModelProps";
11 |
12 | export default function LoginModal({ onClose, showLoginModal }: LoginModelProps) {
13 | return (
14 |
29 | );
30 | };
--------------------------------------------------------------------------------
/src/components/Samples.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import useTweet from "@/hooks/useTweet";
4 | import { samples } from "../constants/smaples";
5 | import { FiArrowUpRight } from "react-icons/fi";
6 | import useResult from "@/hooks/useResult";
7 |
8 | export default function Samples() {
9 | const { setTweet } = useTweet();
10 | const { result } = useResult();
11 | return (
12 |
13 |
14 | {samples.map((sample, index) => (
15 |
23 | ))}
24 |
25 |
26 | )
27 | }
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 10, ...props }, ref) => (
18 |
19 |
28 |
29 | ))
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
31 |
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
33 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Bricolage_Grotesque } from "next/font/google";
3 | import BackgroundImage from "@/components/BackgroundImage";
4 | import { Toaster } from 'sonner'
5 | import Providers from "./Providers";
6 | import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
7 | import { AppSidebar } from "@/components/app-sidebar"
8 | import { Analytics } from "@vercel/analytics/react"
9 | import "./globals.css";
10 |
11 | const bricolage_grotesque_init = Bricolage_Grotesque({
12 | subsets: ["latin"],
13 | display: "swap",
14 | });
15 |
16 | export const metadata: Metadata = {
17 | title: "flick.ai",
18 | description: "Refine your tweet with AI",
19 | };
20 |
21 | export default function RootLayout({
22 | children,
23 | }: Readonly<{
24 | children: React.ReactNode;
25 | }>) {
26 | return (
27 |
28 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {children}
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/ThemeToggleButton.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { Moon, Sun } from "lucide-react"
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuTrigger,
10 | } from "@/components/ui/dropdown-menu"
11 | import { useTheme } from "next-themes";
12 |
13 | export default function ThemeToggleButton() {
14 | const { setTheme } = useTheme()
15 | return
16 |
17 |
22 |
23 |
24 | setTheme("light")}>
25 | Light
26 |
27 | setTheme("dark")}>
28 | Dark
29 |
30 |
31 |
32 | }
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link";
4 | import fontInter from "@/app/font";
5 | import useResult from "@/hooks/useResult";
6 |
7 | export default function Footer() {
8 | const { result } = useResult()
9 | return (
10 |
33 | )
34 | }
--------------------------------------------------------------------------------
/src/components/TypeWriter.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import ShineBorder from './ui/shine-border';
3 |
4 | interface TypeWriterProps {
5 | text: string;
6 | speed?: number;
7 | }
8 |
9 | export default function TypeWriter({ text, speed = 50 }: TypeWriterProps) {
10 | const [displayedText, setDisplayedText] = useState('');
11 | const [currentIndex, setCurrentIndex] = useState(0);
12 |
13 | useEffect(() => {
14 | if (currentIndex < text.length) {
15 | const timer = setTimeout(() => {
16 | setDisplayedText(prev => prev + text[currentIndex]);
17 | setCurrentIndex(prev => prev + 1);
18 | }, speed);
19 |
20 | return () => clearTimeout(timer);
21 | }
22 | }, [currentIndex, text, speed]);
23 |
24 | useEffect(() => {
25 | setDisplayedText('');
26 | setCurrentIndex(0);
27 | }, [text]);
28 |
29 | return (
30 |
31 |
35 | {displayedText}
36 |
37 |
38 | );
39 | }
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverAnchor = PopoverPrimitive.Anchor
13 |
14 | const PopoverContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18 |
19 |
29 |
30 | ))
31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
32 |
33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
34 |
--------------------------------------------------------------------------------
/src/app/api/corePrompt/save/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/prisma";
2 | import { getServerSession } from "next-auth";
3 | import { NextResponse } from "next/server";
4 | import { authOptions } from "../../auth/[...nextauth]/options";
5 |
6 | export async function POST(req: Request) {
7 | const session = await getServerSession(authOptions);
8 | const userMail = session?.user?.email;
9 |
10 | if (!userMail) {
11 | return NextResponse.json(
12 | { success: false, message: "Unauthorized" },
13 | { status: 401 }
14 | );
15 | }
16 |
17 | const { corePrompt } = await req.json();
18 |
19 | if (!corePrompt) {
20 | return NextResponse.json(
21 | { success: false, message: "Core prompt is required" },
22 | { status: 400 }
23 | );
24 | }
25 |
26 | try {
27 | await prisma.user.update({
28 | where: { email: userMail },
29 | data: {
30 | corePrompt
31 | }
32 | });
33 |
34 | return NextResponse.json(
35 | { success: true, message: "Core prompt updated successfully" },
36 | { status: 200 }
37 | );
38 | } catch (error) {
39 | return NextResponse.json(
40 | { success: false, message: `Failed to update core prompt: ${error instanceof Error ? error.message : 'Unknown error'}` },
41 | { status: 500 }
42 | );
43 | }
44 | }
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/options.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/prisma";
2 | import { NextAuthOptions } from "next-auth"
3 | import GoogleProvider from "next-auth/providers/google";
4 |
5 | export const authOptions: NextAuthOptions = {
6 | providers: [
7 | GoogleProvider({
8 | clientId: process.env.GOOGLE_CLIENT_ID ?? "",
9 | clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? ""
10 | })
11 | ],
12 | secret: process.env.NEXTAUTH_SECRET,
13 | callbacks: {
14 | async signIn({ user }) {
15 | try {
16 | const existingUser = await prisma.user.findUnique({
17 | where: { email: user.email! },
18 | });
19 |
20 | if (!existingUser) {
21 | await prisma.user.create({
22 | data: {
23 | email: user.email!,
24 | name: user.name,
25 | profileImage: user.image,
26 | corePrompt: process.env.SYSTEM_PROMPT
27 | },
28 | });
29 | }
30 | return true;
31 | } catch (error) {
32 | console.error("Error during sign-in:", error);
33 | return false;
34 | } finally {
35 | await prisma.$disconnect();
36 | }
37 | },
38 | },
39 | session: {
40 | strategy: "jwt",
41 | },
42 | }
--------------------------------------------------------------------------------
/src/app/api/interaction/delete/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/prisma";
2 | import { Prisma } from "@prisma/client";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function DELETE(req: Request) {
6 | try {
7 | const { id } = await req.json();
8 |
9 | if (!id) {
10 | return NextResponse.json(
11 | { success: false, message: "Interaction ID is required" },
12 | { status: 400 }
13 | );
14 | }
15 |
16 | await prisma.interaction.delete({
17 | where: { id },
18 | include: { user: true }
19 | });
20 |
21 | return NextResponse.json(
22 | { success: true, message: "Interaction deleted successfully" },
23 | { status: 200 }
24 | );
25 |
26 | } catch (error) {
27 | if (error instanceof Prisma.PrismaClientKnownRequestError) {
28 | if (error.code === 'P2025') {
29 | return NextResponse.json(
30 | { success: false, message: "Interaction not found" },
31 | { status: 404 }
32 | );
33 | }
34 | return NextResponse.json(
35 | { success: false, message: "Database error", error: error.meta },
36 | { status: 500 }
37 | );
38 | }
39 |
40 | return NextResponse.json(
41 | { success: false, message: "Internal server error" },
42 | { status: 500 }
43 | );
44 | }
45 | }
--------------------------------------------------------------------------------
/src/app/history/components/Utility.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ApiResponse } from "@/types/ApiResponse";
4 | import { UtilityProps } from "@/types/UtilityProps";
5 | import axios, { AxiosError } from "axios";
6 | import { useRouter } from "next/navigation";
7 | import { IoMdCopy } from "react-icons/io";
8 | import { RiDeleteBin3Line } from "react-icons/ri";
9 | import { toast } from "sonner";
10 |
11 | export default function Utility({ aiResponse, id }: UtilityProps) {
12 | const router = useRouter();
13 | const copyToClipboard = () => {
14 | if (!aiResponse) return;
15 | navigator.clipboard.writeText(aiResponse);
16 | toast.success('Text copied to clipboard')
17 | };
18 |
19 | const handleDelete = async () => {
20 | try {
21 | const response = await axios.delete('/api/interaction/delete', { data: { id } });
22 | toast.success(response.data.message ?? 'Interaction deleted successfully')
23 | router.refresh()
24 | } catch (error) {
25 | const axiosError = error as AxiosError;
26 | toast.error(axiosError.response?.data?.message ?? 'Failed to delete interaction')
27 | }
28 | }
29 |
30 | return (
31 | <>
32 |
33 |
34 | >
35 | )
36 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flick.AI
2 |
3 | ## 📖 Introduction
4 |
5 | Meet Flick.AI: Your ultimate AI-powered tweet-refining assistant. Whether you're aiming for casual charm, sharp wit, or serious impact, Flick.AI transforms your thoughts into scroll-stopping tweets. Correct, format, and perfect your tone effortlessly—your voice, amplified.
6 |
7 | ## 🛠️ Technologies Used
8 |
9 | - Next.js
10 | - TypeScript
11 | - Prisma
12 | - PostgreSQL
13 | - Google Gemini
14 | - NextAuth
15 | - Tailwind CSS
16 |
17 | ## 🚀 Getting Started
18 |
19 | ### Cloning the Repository
20 |
21 | To clone the repository locally, use the following commands:
22 |
23 | ```bash
24 | git clone https://github.com/your-username/flick-ai.git
25 | cd ghostgram
26 | ```
27 |
28 | ### Installation
29 | ```bash
30 | npm install
31 | ```
32 | ### Configuration
33 | Create a `.env` file in the root folder of your project.
34 | Here's an example:
35 | ```
36 | DATABASE_URL=
37 | GEMINI_API_KEY=
38 | SYSTEM_PROMPT=
39 | AI_MODEL=
40 | GOOGLE_CLIENT_ID=
41 | GOOGLE_CLIENT_SECRET=
42 | NEXTAUTH_SECRET=
43 | ```
44 |
45 | ### Running the Project
46 | ```bash
47 | npm run dev
48 | ```
49 |
50 | ## 🤝 Contribution Guide
51 | I hearty welcome contributions! Please follow these steps:
52 | - Fork the repository.
53 | - Create a new branch `(git checkout -b feature-branch-name)`.
54 | - Make your changes and commit them `(git commit -m "Add feature description")`.
55 | - Push your changes `(git push origin feature-branch-name)`.
56 | Create a Pull Request.
57 |
58 | ***
59 | Thank you for checking out this project! Feel free to open an issue if you have any questions or suggestions.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tweet-refiner",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "prisma generate && next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@ai-sdk/google": "^1.0.15",
13 | "@google/generative-ai": "^0.21.0",
14 | "@prisma/client": "^6.2.1",
15 | "@radix-ui/react-avatar": "^1.1.2",
16 | "@radix-ui/react-dialog": "^1.1.5",
17 | "@radix-ui/react-dropdown-menu": "^2.1.5",
18 | "@radix-ui/react-popover": "^1.1.5",
19 | "@radix-ui/react-select": "^2.1.4",
20 | "@radix-ui/react-separator": "^1.1.1",
21 | "@radix-ui/react-slot": "^1.1.1",
22 | "@radix-ui/react-tooltip": "^1.1.7",
23 | "@vercel/analytics": "^1.4.1",
24 | "ai": "^4.0.33",
25 | "axios": "^1.7.9",
26 | "class-variance-authority": "^0.7.1",
27 | "clsx": "^2.1.1",
28 | "lucide-react": "^0.471.2",
29 | "motion": "^11.18.1",
30 | "next": "15.1.4",
31 | "next-auth": "^4.24.11",
32 | "next-themes": "^0.4.4",
33 | "react": "^19.0.0",
34 | "react-dom": "^19.0.0",
35 | "react-icons": "^5.4.0",
36 | "sonner": "^1.7.2",
37 | "tailwind-merge": "^2.6.0",
38 | "tailwindcss-animate": "^1.0.7"
39 | },
40 | "devDependencies": {
41 | "@eslint/eslintrc": "^3",
42 | "@types/node": "^20",
43 | "@types/react": "^19",
44 | "@types/react-dom": "^19",
45 | "eslint": "^9",
46 | "eslint-config-next": "15.1.4",
47 | "postcss": "^8",
48 | "prisma": "^6.2.1",
49 | "tailwindcss": "^3.4.1",
50 | "typescript": "^5"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/src/components/ui/border-beam.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | interface BorderBeamProps {
4 | className?: string;
5 | size?: number;
6 | duration?: number;
7 | borderWidth?: number;
8 | anchor?: number;
9 | colorFrom?: string;
10 | colorTo?: string;
11 | delay?: number;
12 | }
13 |
14 | export const BorderBeam = ({
15 | className,
16 | size = 200,
17 | duration = 15,
18 | anchor = 90,
19 | borderWidth = 1.5,
20 | colorFrom = "#ffaa40",
21 | colorTo = "#9c40ff",
22 | delay = 0,
23 | }: BorderBeamProps) => {
24 | return (
25 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/components/CorePromptForm.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React, { useRef } from 'react';
4 | import { Button } from './ui/button';
5 | import { toast } from 'sonner';
6 | import { corePromptPlaceholder } from '@/constants/corePromptInputPlaceholder';
7 | import axios, { AxiosError } from 'axios';
8 | import { ApiResponse } from '@/types/ApiResponse';
9 |
10 | export default function CorePromptForm() {
11 | const promptRef = useRef(null);
12 |
13 | const saveCorePrompt = async () => {
14 | if (!promptRef.current?.value) {
15 | toast.info('Provide the core prompt');
16 | return;
17 | }
18 | try {
19 | const response = await axios.post('/api/corePrompt/save', { corePrompt: promptRef.current?.value })
20 | toast.success(response.data.message);
21 | promptRef.current.value = '';
22 | } catch (error) {
23 | const axiosError = error as AxiosError;
24 | toast.error(axiosError.response?.data.message ?? 'Failed to refine the tweet');
25 | }
26 | }
27 |
28 | return (
29 | <>
30 |
31 |
Add Core Prompt
32 |
This will determine how your tweet will be refined.
33 |
34 |
35 |
36 | >
37 | )
38 | }
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-transparent border-none outline-none text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border-none outline-none bg-transparent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/app/api/interaction/get/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from '@/lib/prisma';
2 | import { getServerSession } from 'next-auth/next'
3 | import { authOptions } from "../../auth/[...nextauth]/options";
4 | import { NextResponse } from 'next/server';
5 |
6 | export async function GET() {
7 | const session = await getServerSession(authOptions)
8 |
9 | if (!session?.user) {
10 | return NextResponse.json(
11 | { success: false, message: 'Authentication required. Please sign in to access this resource.' },
12 | { status: 401 }
13 | );
14 | }
15 |
16 | try {
17 | const user = await prisma.user.findFirst({
18 | where: {
19 | email: session.user.email ?? ""
20 | }
21 | })
22 |
23 | if (!user) {
24 | return NextResponse.json(
25 | { success: false, message: 'User account not found. Please verify your account status.' },
26 | { status: 404 }
27 | );
28 | }
29 |
30 | const interactions = await prisma.interaction.findMany({
31 | where: {
32 | userId: user.id
33 | },
34 | include: {
35 | user: false,
36 | },
37 | orderBy: {
38 | createdAt: 'desc'
39 | }
40 | });
41 |
42 | if (!interactions) {
43 | return NextResponse.json(
44 | { success: true, message: 'No interactions found.' },
45 | { status: 200 }
46 | );
47 | }
48 |
49 | return NextResponse.json(
50 | { success: true, message: interactions },
51 | { status: 200 }
52 | );
53 | } catch (error) {
54 | return NextResponse.json(
55 | {
56 | success: false,
57 | message: error instanceof Error ? `Error fetching interactions ${error.message}` : 'An unexpected error occurred while fetching your interactions.'
58 | },
59 | { status: 500 }
60 | );
61 | }
62 | }
--------------------------------------------------------------------------------
/src/components/Profile.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuItem,
7 | DropdownMenuLabel,
8 | DropdownMenuSeparator,
9 | DropdownMenuTrigger,
10 | } from "@/components/ui/dropdown-menu"
11 | import {
12 | Avatar,
13 | AvatarFallback,
14 | AvatarImage,
15 | } from "@/components/ui/avatar"
16 | import { signIn, signOut, useSession } from "next-auth/react"
17 | import { CgMenuGridR } from "react-icons/cg";
18 | import UsageCount from "./UsageCount"
19 | import Link from "next/link";
20 |
21 |
22 | export default function Profile() {
23 | const { data: session } = useSession()
24 | return (
25 |
26 |
27 | {
28 | session?.user ? (
29 |
30 |
31 | AI
32 |
33 | ) :
34 | }
35 |
36 |
37 |
38 |
39 | {
40 | session?.user ? (
41 | <>
42 |
43 |
44 | History
45 |
46 |
47 | signOut()}>Logout
48 | >
49 | ) : signIn('google')}>SignIn
50 | }
51 |
52 |
53 | )
54 | }
--------------------------------------------------------------------------------
/src/app/api/interaction/save/route.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from 'next-auth/next'
2 | import { authOptions } from "../../auth/[...nextauth]/options";
3 | import { prisma } from '@/lib/prisma';
4 | import { NextResponse } from 'next/server';
5 |
6 | export async function POST(req: Request) {
7 | const { tweet, mood, action, result } = await req.json();
8 | const session = await getServerSession(authOptions)
9 |
10 | if (!session) {
11 | return NextResponse.json(
12 | { success: false, message: 'Please sign in to save your interaction' },
13 | { status: 401 }
14 | );
15 | }
16 |
17 | try {
18 | const user = await prisma.user.findFirst({
19 | where: {
20 | email: session.user?.email ?? ""
21 | }
22 | })
23 |
24 | if (!user) {
25 | return NextResponse.json(
26 | { success: false, message: 'Your account could not be found. Please try signing in again' },
27 | { status: 404 }
28 | );
29 | }
30 |
31 | const interaction = await prisma.interaction.findFirst({
32 | where: {
33 | userPrompt: tweet,
34 | mood,
35 | action
36 | }
37 | })
38 |
39 | if (interaction) {
40 | return NextResponse.json(
41 | { success: false, message: 'This interaction has already been saved' },
42 | { status: 409 }
43 | );
44 | }
45 |
46 | await prisma.interaction.create({
47 | data: {
48 | userPrompt: tweet,
49 | aiResponse: result,
50 | mood: mood,
51 | action: action,
52 | userId: user?.id
53 | }
54 | });
55 |
56 | return NextResponse.json(
57 | { success: true, message: "Your interaction has been saved" },
58 | { status: 200 }
59 | )
60 | } catch (error) {
61 | return NextResponse.json(
62 | { success: false, message: error instanceof Error ? `We encountered an issue while saving: ${error.message}` : 'An unexpected error occurred while saving your interaction. Please try again later.' },
63 | { status: 500 }
64 | )
65 | }
66 | }
--------------------------------------------------------------------------------
/src/components/ui/shiny-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import {
5 | motion,
6 | type AnimationProps,
7 | type HTMLMotionProps,
8 | } from "motion/react";
9 | import { cn } from "@/lib/utils";
10 |
11 | const animationProps = {
12 | initial: { "--x": "100%", scale: 0.8 },
13 | animate: { "--x": "-100%", scale: 1 },
14 | whileTap: { scale: 0.95 },
15 | transition: {
16 | repeat: Infinity,
17 | repeatType: "loop",
18 | repeatDelay: 1,
19 | type: "spring",
20 | stiffness: 20,
21 | damping: 15,
22 | mass: 2,
23 | scale: {
24 | type: "spring",
25 | stiffness: 200,
26 | damping: 5,
27 | mass: 0.5,
28 | },
29 | },
30 | } as AnimationProps;
31 |
32 | interface ShinyButtonProps extends Omit, "ref"> {
33 | children: React.ReactNode;
34 | className?: string;
35 | }
36 |
37 | export const ShinyButton = React.forwardRef<
38 | HTMLButtonElement,
39 | ShinyButtonProps
40 | >(({ children, className, ...props }, ref) => {
41 | return (
42 |
51 |
58 | {children}
59 |
60 |
67 |
68 | );
69 | });
70 |
71 | ShinyButton.displayName = "ShinyButton";
72 |
--------------------------------------------------------------------------------
/src/components/ui/shine-border.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | type TColorProp = string | string[];
6 |
7 | interface ShineBorderProps {
8 | borderRadius?: number;
9 | borderWidth?: number;
10 | duration?: number;
11 | color?: TColorProp;
12 | className?: string;
13 | children: React.ReactNode;
14 | }
15 |
16 | /**
17 | * @name Shine Border
18 | * @description It is an animated background border effect component with easy to use and configurable props.
19 | * @param borderRadius defines the radius of the border.
20 | * @param borderWidth defines the width of the border.
21 | * @param duration defines the animation duration to be applied on the shining border
22 | * @param color a string or string array to define border color.
23 | * @param className defines the class name to be applied to the component
24 | * @param children contains react node elements.
25 | */
26 | export default function ShineBorder({
27 | borderRadius = 8,
28 | borderWidth = 1,
29 | duration = 14,
30 | color = "#000000",
31 | className,
32 | children,
33 | }: ShineBorderProps) {
34 | return (
35 |
46 |
58 | {children}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/i/[interactionId]/page.tsx:
--------------------------------------------------------------------------------
1 | import Utility from "@/app/history/components/Utility";
2 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
3 | import { authOptions } from "@/app/api/auth/[...nextauth]/options";
4 | import { prisma } from "@/lib/prisma";
5 | import { HistoryType } from "@/types/HistoryType";
6 | import { InteractionPageProps } from "@/types/InteractionPageProps";
7 | import { getServerSession } from "next-auth";
8 | import { redirect } from 'next/navigation'
9 |
10 | export default async function InteractionPage({ params }: InteractionPageProps) {
11 | const { interactionId } = await params;
12 | const session = await getServerSession(authOptions)
13 |
14 | if (!session?.user) return redirect('/')
15 |
16 | const interaction: HistoryType | null = await prisma.interaction.findUnique({
17 | where: {
18 | id: interactionId
19 | }
20 | })
21 |
22 | if (!interaction) return redirect('/')
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 | {interaction.mood}
31 | {interaction.action}
32 |
33 |
{interaction.userPrompt}
34 |
35 |
36 |
37 |
38 |
39 |
40 | AI
41 |
42 |
43 |
44 |
"{interaction.aiResponse}"
45 |
46 | {interaction.createdAt.toDateString()}
47 |
48 |
49 |
50 |
51 |
52 |
53 | )
54 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | darkMode: 'class',
10 | theme: {
11 | extend: {
12 | colors: {
13 | background: 'hsl(var(--background))',
14 | foreground: 'hsl(var(--foreground))',
15 | card: {
16 | DEFAULT: 'hsl(var(--card))',
17 | foreground: 'hsl(var(--card-foreground))'
18 | },
19 | popover: {
20 | DEFAULT: 'hsl(var(--popover))',
21 | foreground: 'hsl(var(--popover-foreground))'
22 | },
23 | primary: {
24 | DEFAULT: 'hsl(var(--primary))',
25 | foreground: 'hsl(var(--primary-foreground))'
26 | },
27 | secondary: {
28 | DEFAULT: 'hsl(var(--secondary))',
29 | foreground: 'hsl(var(--secondary-foreground))'
30 | },
31 | muted: {
32 | DEFAULT: 'hsl(var(--muted))',
33 | foreground: 'hsl(var(--muted-foreground))'
34 | },
35 | accent: {
36 | DEFAULT: 'hsl(var(--accent))',
37 | foreground: 'hsl(var(--accent-foreground))'
38 | },
39 | destructive: {
40 | DEFAULT: 'hsl(var(--destructive))',
41 | foreground: 'hsl(var(--destructive-foreground))'
42 | },
43 | border: 'hsl(var(--border))',
44 | input: 'hsl(var(--input))',
45 | ring: 'hsl(var(--ring))',
46 | chart: {
47 | '1': 'hsl(var(--chart-1))',
48 | '2': 'hsl(var(--chart-2))',
49 | '3': 'hsl(var(--chart-3))',
50 | '4': 'hsl(var(--chart-4))',
51 | '5': 'hsl(var(--chart-5))'
52 | },
53 | sidebar: {
54 | DEFAULT: 'hsl(var(--sidebar-background))',
55 | foreground: 'hsl(var(--sidebar-foreground))',
56 | primary: 'hsl(var(--sidebar-primary))',
57 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
58 | accent: 'hsl(var(--sidebar-accent))',
59 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
60 | border: 'hsl(var(--sidebar-border))',
61 | ring: 'hsl(var(--sidebar-ring))'
62 | }
63 | },
64 | borderRadius: {
65 | lg: 'var(--radius)',
66 | md: 'calc(var(--radius) - 2px)',
67 | sm: 'calc(var(--radius) - 4px)'
68 | },
69 | animation: {
70 | grid: 'grid 15s linear infinite',
71 | 'border-rotate': 'border-rotate 3s linear infinite',
72 | shine: 'shine var(--duration) infinite linear',
73 | 'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear'
74 | },
75 | keyframes: {
76 | grid: {
77 | '0%': {
78 | transform: 'translateY(-50%)'
79 | },
80 | '100%': {
81 | transform: 'translateY(0)'
82 | }
83 | },
84 | 'border-rotate': {
85 | '0%, 100%': {
86 | backgroundPosition: '0% 50%'
87 | },
88 | '50%': {
89 | backgroundPosition: '100% 50%'
90 | }
91 | },
92 | shine: {
93 | '0%': {
94 | 'background-position': '0% 0%'
95 | },
96 | '50%': {
97 | 'background-position': '100% 100%'
98 | },
99 | to: {
100 | 'background-position': '0% 0%'
101 | }
102 | },
103 | 'border-beam': {
104 | '100%': {
105 | 'offset-distance': '100%'
106 | }
107 | }
108 | }
109 | }
110 | },
111 | plugins: [require("tailwindcss-animate")],
112 | } satisfies Config;
113 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: Arial, Helvetica, sans-serif;
7 | }
8 |
9 | @layer base {
10 | :root {
11 | --background: 0 0% 100%;
12 | --foreground: 240 10% 3.9%;
13 | --card: 0 0% 100%;
14 | --card-foreground: 240 10% 3.9%;
15 | --popover: 0 0% 100%;
16 | --popover-foreground: 240 10% 3.9%;
17 | --primary: 240 5.9% 10%;
18 | --primary-foreground: 0 0% 98%;
19 | --secondary: 240 4.8% 95.9%;
20 | --secondary-foreground: 240 5.9% 10%;
21 | --muted: 240 4.8% 95.9%;
22 | --muted-foreground: 240 3.8% 46.1%;
23 | --accent: 240 4.8% 95.9%;
24 | --accent-foreground: 240 5.9% 10%;
25 | --destructive: 0 84.2% 60.2%;
26 | --destructive-foreground: 0 0% 98%;
27 | --border: 240 5.9% 90%;
28 | --input: 240 5.9% 90%;
29 | --ring: 240 10% 3.9%;
30 | --chart-1: 12 76% 61%;
31 | --chart-2: 173 58% 39%;
32 | --chart-3: 197 37% 24%;
33 | --chart-4: 43 74% 66%;
34 | --chart-5: 27 87% 67%;
35 | --radius: 0.5rem;
36 | --sidebar-background: 0 0% 98%;
37 | --sidebar-foreground: 240 5.3% 26.1%;
38 | --sidebar-primary: 240 5.9% 10%;
39 | --sidebar-primary-foreground: 0 0% 98%;
40 | --sidebar-accent: 240 4.8% 95.9%;
41 | --sidebar-accent-foreground: 240 5.9% 10%;
42 | --sidebar-border: 220 13% 91%;
43 | --sidebar-ring: 217.2 91.2% 59.8%;
44 | }
45 |
46 | .dark {
47 | --background: 240 10% 0%;
48 | --foreground: 0 0% 98%;
49 | --card: 240 10% 3.9%;
50 | --card-foreground: 0 0% 98%;
51 | --popover: 240 10% 3.9%;
52 | --popover-foreground: 0 0% 98%;
53 | --primary: 0 0% 98%;
54 | --primary-foreground: 240 5.9% 10%;
55 | --secondary: 240 3.7% 15.9%;
56 | --secondary-foreground: 0 0% 98%;
57 | --muted: 240 3.7% 15.9%;
58 | --muted-foreground: 240 5% 64.9%;
59 | --accent: 240 3.7% 15.9%;
60 | --accent-foreground: 0 0% 98%;
61 | --destructive: 0 62.8% 30.6%;
62 | --destructive-foreground: 0 0% 98%;
63 | --border: 240 3.7% 15.9%;
64 | --input: 240 3.7% 15.9%;
65 | --ring: 240 4.9% 83.9%;
66 | --chart-1: 220 70% 50%;
67 | --chart-2: 160 60% 45%;
68 | --chart-3: 30 80% 55%;
69 | --chart-4: 280 65% 60%;
70 | --chart-5: 340 75% 55%;
71 | --sidebar-background: 240 5.9% 10%;
72 | --sidebar-foreground: 240 4.8% 95.9%;
73 | --sidebar-primary: 224.3 76.3% 48%;
74 | --sidebar-primary-foreground: 0 0% 100%;
75 | --sidebar-accent: 240 3.7% 15.9%;
76 | --sidebar-accent-foreground: 240 4.8% 95.9%;
77 | --sidebar-border: 240 3.7% 15.9%;
78 | --sidebar-ring: 217.2 91.2% 59.8%;
79 | }
80 | }
81 |
82 | @supports (-webkit-backdrop-filter: none) or (backdrop-filter: none) {
83 | .backdrop-blur-lg {
84 | -webkit-backdrop-filter: blur(16px);
85 | backdrop-filter: blur(16px);
86 | }
87 | }
88 |
89 | @layer base {
90 | * {
91 | @apply border-border;
92 | }
93 |
94 | body {
95 | @apply bg-background text-foreground;
96 | }
97 | }
98 |
99 |
100 |
101 |
102 | .btn {
103 | appearance: none;
104 | -webkit-appearance: none; /* Safari-specific reset */
105 | -moz-appearance: none;
106 | /* background-color: #222; intended background */
107 | /* color: white; */
108 | }
109 | .btn {
110 | -webkit-backdrop-filter: none; /* Disable background filter for Safari */
111 | background-clip: padding-box; /* Ensure background fills button */
112 | }
--------------------------------------------------------------------------------
/src/app/history/page.tsx:
--------------------------------------------------------------------------------
1 | import { getServerSession } from 'next-auth/next'
2 | import { authOptions } from '../api/auth/[...nextauth]/options'
3 | import { prisma } from '@/lib/prisma'
4 | import { HistoryType } from '@/types/HistoryType'
5 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
6 | import Utility from './components/Utility'
7 | import Link from 'next/link'
8 | import { redirect } from 'next/navigation'
9 |
10 |
11 | export default async function History() {
12 | const session = await getServerSession(authOptions)
13 |
14 | if (!session?.user) return redirect('/')
15 |
16 | const user = await prisma.user.findFirst({
17 | where: {
18 | email: session.user?.email ?? ""
19 | }
20 | })
21 |
22 | if (!user) return User not found
23 |
24 | const interactions: HistoryType[] = await prisma.interaction.findMany({
25 | where: {
26 | userId: user.id
27 | },
28 | include: {
29 | user: false,
30 | },
31 | orderBy: {
32 | createdAt: 'desc'
33 | }
34 | });
35 |
36 | return (
37 |
38 | {
39 | interactions.length < 1 &&
40 | No Interactions found!
41 | Go Back
42 |
43 | }
44 | {
45 | interactions.length > 0 && interactions.map((item, idx) => (
46 |
47 |
48 |
49 |
50 | {item.mood}
51 | {item.action}
52 |
53 |
{item.userPrompt}
54 |
55 |
56 |
57 |
58 |
59 |
60 | AI
61 |
62 |
63 |
64 |
"{item.aiResponse}"
65 |
66 | {item.createdAt.toDateString()}
67 |
68 |
69 |
70 |
71 |
72 | ))
73 | }
74 |
75 | )
76 | }
--------------------------------------------------------------------------------
/src/app/api/improve/route.ts:
--------------------------------------------------------------------------------
1 | import { genAI } from '@/lib/genAI';
2 | import { getServerSession } from 'next-auth';
3 | import { NextResponse } from 'next/server';
4 | import { authOptions } from '../auth/[...nextauth]/options';
5 | import { prisma } from '@/lib/prisma';
6 |
7 | export async function POST(req: Request) {
8 | const { tweet, result, improvePrompt } = await req.json();
9 | const session = await getServerSession(authOptions);
10 | const forwarded = req.headers.get('x-forwarded-for');
11 | const userIp: string = forwarded?.split(',')[0]?.trim() || 'Unknown IP';
12 |
13 | const ipResult = await prisma.userIp.findFirst({
14 | where: {
15 | ipAddress: userIp ?? ''
16 | }
17 | })
18 |
19 | if (!session || !session.user && ipResult) {
20 | return Response.json(
21 | { success: false, message: 'Credit limit reached, signup to get more credits!' },
22 | { status: 401 }
23 | );
24 | }
25 |
26 | const prompt = `You are a tweet EDITOR executing specific user-requested changes. Follow these rules:
27 |
28 | [CRITICAL RULES]
29 | 1. MAKE ONLY REQUESTED CHANGES: Never modify unmentioned aspects
30 | 2. PRESERVE EXISTING STRUCTURE: Keep intact what user hasn't specified to change
31 | 3. STRICT INSTRUCTION ADHERENCE: Implement ${improvePrompt} exactly
32 | 4. NO NEW CONTENT: Never add emojis, hashtags, or unsolicited ideas
33 | 5. LENGTH CAP: Absolute maximum 270 characters
34 | 6. If the user provides you with a tweet, your task is to refine it, not comment on it or make it longer than the original tweet.
35 |
36 | [CONTEXT]
37 | Original: "${tweet}"
38 | Previous Version: "${result}"
39 | User's Exact Request: "${improvePrompt}"
40 |
41 | [REQUIRED PROCESS]
42 | 1. Compare previous version and user request
43 | 2. Identify SPECIFIC elements to change/keep
44 | 3. Apply ONLY requested modifications
45 | 4. Preserve unrelated aspects from previous version
46 | 5. Validate against all rules before output
47 |
48 | [BAD EXAMPLE]
49 | User Request: "Make it shorter"
50 | Bad Change: Added more words "Leverage blockchain AI synergies" (new concept)
51 | Good Change: Make it shorter and if possible try to match the length with the original tweet
52 |
53 | [OUTPUT REQUIREMENTS]
54 | - Maintain previous version's line breaks/formatting
55 | - Keep unchanged portions verbatim where possible
56 | - Make minimal alterations to fulfill request
57 | - Use only vocabulary from existing versions unless instructed
58 |
59 | [VALIDATION CHECKLIST]
60 | Before responding, verify:
61 | ☑ Changes match EXACTLY what user requested if short then ensure it has lesser words then previous response
62 | ☑ Unrelated content remains identical
63 | ☑ No new concepts/terms added
64 | ☑ Length under 270 chars
65 | ☑ No emojis/hashtags
66 |
67 | Refined version (ONLY OUTPUT THIS):`
68 |
69 | try {
70 | const model = genAI.getGenerativeModel({
71 | model: process.env.AI_MODEL ?? ""
72 | });
73 | const res = await model.generateContent(prompt);
74 | const text = res.response.text();
75 |
76 | return NextResponse.json(
77 | { success: true, message: text },
78 | {
79 | status: 200,
80 | }
81 | );
82 | } catch (error) {
83 | return NextResponse.json(
84 | {
85 | success: false,
86 | message: error instanceof Error ?
87 | `Tweet improvement failed: ${error.message}` :
88 | 'Our tweet improvement service is currently unavailable. Please try again later.'
89 | },
90 | {
91 | status: 500,
92 | }
93 | );
94 | }
95 |
96 | }
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/Result.tsx:
--------------------------------------------------------------------------------
1 | import { IoMdCopy } from "react-icons/io";
2 | import TypeWriter from "./TypeWriter";
3 | import useResult from "@/hooks/useResult";
4 | import { FaWandMagicSparkles } from "react-icons/fa6";
5 | import { ShinyButton } from '@/components/ui/shiny-button'
6 | import { FaHeart } from "react-icons/fa";
7 | import { ResultProps } from "@/types/ResultProps";
8 | import { RiTwitterXLine } from "react-icons/ri";
9 | import {
10 | Tooltip,
11 | TooltipContent,
12 | TooltipProvider,
13 | TooltipTrigger,
14 | } from "@/components/ui/tooltip"
15 | import Link from "next/link";
16 |
17 |
18 | export default function Result({ improvePrompt, isImprovingField, setImprovePrompt, handleRegenerate, copyToClipboard, saveInteraction }: ResultProps) {
19 | const { result } = useResult();
20 | return (
21 |
22 |
23 |
setImprovePrompt(e.target.value)}
26 | onKeyDown={(e) => {
27 | if (e.key === 'Enter') {
28 | handleRegenerate();
29 | }
30 | }}
31 | value={improvePrompt}
32 | placeholder="Follow Up"
33 | className={`dark:text-white bg-transparent text-xs w-0 py-0 transition-all duration-300 ${isImprovingField ? 'w-[35vw] max-sm:w-full px-2 border border-gray-400/50 dark:border-white/20' : 'w-0'} rounded-lg bg-opacity-10 backdrop-blur-lg dark:focus:outline-none dark:focus:border-white/20`}
34 | />
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Follow up
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
55 |
56 |
57 | Save
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
69 |
70 |
71 |
72 | Share on X
73 |
74 |
75 |
76 |
77 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | )
87 | }
--------------------------------------------------------------------------------
/src/app/api/generate/route.ts:
--------------------------------------------------------------------------------
1 | import { genAI } from '@/lib/genAI';
2 | import { getServerSession } from 'next-auth';
3 | import { NextResponse } from 'next/server';
4 | import { authOptions } from '../auth/[...nextauth]/options';
5 | import { prisma } from '@/lib/prisma';
6 |
7 | export async function POST(req: Request) {
8 | const { tweet, mood, action } = await req.json();
9 | const session = await getServerSession(authOptions);
10 | const forwarded = req.headers.get('x-forwarded-for');
11 | const userIp: string = forwarded?.split(',')[0]?.trim() || 'Unknown IP';
12 |
13 | const ipResult = await prisma.userIp.findFirst({
14 | where: {
15 | ipAddress: userIp ?? ''
16 | }
17 | })
18 |
19 | if (!session && ipResult) {
20 | return Response.json(
21 | { success: false, message: 'Credit limit reached, signup to get more credits!' },
22 | { status: 401 }
23 | );
24 | }
25 |
26 | let corePrompt;
27 |
28 | try {
29 | if (!session?.user || !ipResult) {
30 | corePrompt = process.env.SYSTEM_PROMPT;
31 | await prisma.userIp.create({
32 | data: {
33 | ipAddress: userIp
34 | }
35 | })
36 | } else {
37 | if (!session?.user) return;
38 |
39 | const user = await prisma.user.findFirst({
40 | where: {
41 | email: session.user.email ?? ""
42 | }
43 | })
44 | corePrompt = user?.corePrompt;
45 | }
46 |
47 | const prompt = `You are an expert tweet refinement engine. Strictly follow these rules:
48 |
49 | [CRITICAL RULES]
50 | 1. NEVER use emojis, hashtags, or markdown - strictly prohibited
51 | 2. NO NEW CONTENT: Never add motivational phrases, opinions, advise or commentary. It's strict rule
52 | 3. NEVER add new content - only refine what's provided
53 | 4. ALWAYS maintain original intent while enhancing clarity
54 | 5. STRICT length limit: Max 280 characters (hard stop)
55 | 6. NEVER mention your actions or process - output only the refined tweet no other bullshit
56 | 7. If the user provides you with a tweet, your task is to refine it, not comment on it or make it longer than the original tweet.
57 |
58 | [PROCESS]
59 | 1. PRIMARY FOCUS: ${corePrompt} - make this drive all changes
60 | 2. TONE: Convert to ${mood} tone while preserving message
61 | 3. ACTION: Execute "${action}" with:
62 | - Formatting: Logical line breaks, remove fluff
63 | - Improving: Boost impact using mindset, tighten phrasing no commentary and opinions
64 | - Correcting: Fix errors, improve readability
65 |
66 | [OUTPUT REQUIREMENTS]
67 | - Multi-line format unless user specifies single-line
68 | - Preserve original formatting style when possible
69 | - Remove redundant phrases while keeping core message
70 | - Use active voice and concise language
71 |
72 | [BAD EXAMPLE TO AVOID]
73 | Input: "I'm a software engineer looking for job"
74 | BAD Output: "You are software engineer seeking job"
75 | GOOD Output: "Experienced SWE passionate about [specific tech] seeking roles in [domain]"
76 |
77 | [INPUT TO REFINE]
78 | "${tweet}"
79 |
80 | [FINAL INSTRUCTIONS]
81 | 1. Analyze input against core prompt (${corePrompt})
82 | 2. Apply ${mood} tone and ${action} action
83 | 3. Generate ONLY the refined tweet meeting all rules
84 | 4. Validate against all constraints before outputting`
85 |
86 | const model = genAI.getGenerativeModel({
87 | model: process.env.AI_MODEL ?? ""
88 | });
89 |
90 | const result = await model.generateContent(prompt);
91 | const text = result.response.text();
92 |
93 | return NextResponse.json(
94 | { success: true, message: text },
95 | {
96 | status: 200,
97 | }
98 | );
99 | } catch (error) {
100 | return NextResponse.json(
101 | {
102 | success: false,
103 | message: error instanceof Error ?
104 | `Tweet refinement failed: ${error.message}` :
105 | 'Our tweet refinement service is currently unavailable. Please try again later.'
106 | },
107 | {
108 | status: 500,
109 | }
110 | );
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 |
68 |
69 | Close
70 |
71 | {children}
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 | {children}
132 |
133 | ))
134 | SelectItem.displayName = SelectPrimitive.Item.displayName
135 |
136 | const SelectSeparator = React.forwardRef<
137 | React.ElementRef,
138 | React.ComponentPropsWithoutRef
139 | >(({ className, ...props }, ref) => (
140 |
145 | ))
146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
147 |
148 | export {
149 | Select,
150 | SelectGroup,
151 | SelectValue,
152 | SelectTrigger,
153 | SelectContent,
154 | SelectLabel,
155 | SelectItem,
156 | SelectSeparator,
157 | SelectScrollUpButton,
158 | SelectScrollDownButton,
159 | }
160 |
--------------------------------------------------------------------------------
/src/components/app-sidebar.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Sidebar,
3 | SidebarContent,
4 | SidebarFooter,
5 | SidebarGroup,
6 | SidebarGroupContent,
7 | SidebarGroupLabel,
8 | SidebarMenu,
9 | SidebarMenuButton,
10 | SidebarMenuItem,
11 | } from "@/components/ui/sidebar"
12 | import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"
13 | import { prisma } from "@/lib/prisma"
14 | import { HistoryType } from "@/types/HistoryType"
15 | import { authOptions } from "@/app/api/auth/[...nextauth]/options"
16 | import { getServerSession } from "next-auth"
17 | import {
18 | Tooltip,
19 | TooltipContent,
20 | TooltipProvider,
21 | TooltipTrigger,
22 | } from "@/components/ui/tooltip"
23 | import {
24 | Popover,
25 | PopoverContent,
26 | PopoverTrigger,
27 | } from "@/components/ui/popover"
28 | import fontInter from "@/app/font"
29 | import Link from "next/link"
30 | import UsageCount from "./UsageCount"
31 | import { Settings } from "lucide-react"
32 | import CorePromptForm from "./CorePromptForm"
33 |
34 | export async function AppSidebar() {
35 | const session = await getServerSession(authOptions)
36 |
37 | if (!session?.user) return null;
38 |
39 | const user = await prisma.user.findFirst({
40 | where: {
41 | email: session.user?.email ?? ""
42 | }
43 | })
44 |
45 | if (!user) return User not found
46 |
47 | const interactions: HistoryType[] = await prisma.interaction.findMany({
48 | where: {
49 | userId: user.id
50 | },
51 | include: {
52 | user: false,
53 | },
54 | orderBy: {
55 | createdAt: 'desc'
56 | }
57 | });
58 |
59 | const now = new Date();
60 | const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
61 | const yesterdayStart = new Date(todayStart);
62 | yesterdayStart.setDate(yesterdayStart.getDate() - 1);
63 | const sevenDaysAgo = new Date(todayStart);
64 | sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
65 | const thirtyDaysAgo = new Date(todayStart);
66 | thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
67 |
68 | const groupedInteractions = interactions.reduce((acc, interaction) => {
69 | const interactionDate = new Date(interaction.createdAt);
70 |
71 | if (interactionDate >= todayStart) {
72 | acc['Today'] = acc['Today'] || [];
73 | acc['Today'].push(interaction);
74 | } else if (interactionDate >= yesterdayStart) {
75 | acc['Yesterday'] = acc['Yesterday'] || [];
76 | acc['Yesterday'].push(interaction);
77 | } else if (interactionDate >= sevenDaysAgo) {
78 | acc['Previous 7 Days'] = acc['Previous 7 Days'] || [];
79 | acc['Previous 7 Days'].push(interaction);
80 | } else if (interactionDate >= thirtyDaysAgo) {
81 | acc['Previous 30 Days'] = acc['Previous 30 Days'] || [];
82 | acc['Previous 30 Days'].push(interaction);
83 | } else {
84 | acc['2024'] = acc['2024'] || [];
85 | acc['2024'].push(interaction);
86 | }
87 |
88 | return acc;
89 | }, {} as Record);
90 |
91 | return (
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | AI
101 |
102 |
103 |
Flick.AI
104 |
105 |
106 |
107 |
108 | {Object.entries(groupedInteractions).map(([period, items]) => (
109 |
110 |
{period}
111 | {items.map((item) => (
112 |
113 |
114 |
115 |
116 |
117 |
118 | {item.userPrompt}
119 |
120 |
121 | {item.userPrompt}
122 |
123 |
124 |
125 |
126 |
127 |
128 | ))}
129 |
130 | ))}
131 | {interactions.length < 1 && No interactions yet!}
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | Core Prompt
141 |
142 | New
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | )
157 | }
158 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
74 |
75 | ))
76 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
77 |
78 | const DropdownMenuItem = React.forwardRef<
79 | React.ElementRef,
80 | React.ComponentPropsWithoutRef & {
81 | inset?: boolean
82 | }
83 | >(({ className, inset, ...props }, ref) => (
84 | svg]:size-4 [&>svg]:shrink-0",
88 | inset && "pl-8",
89 | className
90 | )}
91 | {...props}
92 | />
93 | ))
94 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
95 |
96 | const DropdownMenuCheckboxItem = React.forwardRef<
97 | React.ElementRef,
98 | React.ComponentPropsWithoutRef
99 | >(({ className, children, checked, ...props }, ref) => (
100 |
109 |
110 |
111 |
112 |
113 |
114 | {children}
115 |
116 | ))
117 | DropdownMenuCheckboxItem.displayName =
118 | DropdownMenuPrimitive.CheckboxItem.displayName
119 |
120 | const DropdownMenuRadioItem = React.forwardRef<
121 | React.ElementRef,
122 | React.ComponentPropsWithoutRef
123 | >(({ className, children, ...props }, ref) => (
124 |
132 |
133 |
134 |
135 |
136 |
137 | {children}
138 |
139 | ))
140 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
141 |
142 | const DropdownMenuLabel = React.forwardRef<
143 | React.ElementRef,
144 | React.ComponentPropsWithoutRef & {
145 | inset?: boolean
146 | }
147 | >(({ className, inset, ...props }, ref) => (
148 |
157 | ))
158 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
159 |
160 | const DropdownMenuSeparator = React.forwardRef<
161 | React.ElementRef,
162 | React.ComponentPropsWithoutRef
163 | >(({ className, ...props }, ref) => (
164 |
169 | ))
170 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
171 |
172 | const DropdownMenuShortcut = ({
173 | className,
174 | ...props
175 | }: React.HTMLAttributes) => {
176 | return (
177 |
181 | )
182 | }
183 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
184 |
185 | export {
186 | DropdownMenu,
187 | DropdownMenuTrigger,
188 | DropdownMenuContent,
189 | DropdownMenuItem,
190 | DropdownMenuCheckboxItem,
191 | DropdownMenuRadioItem,
192 | DropdownMenuLabel,
193 | DropdownMenuSeparator,
194 | DropdownMenuShortcut,
195 | DropdownMenuGroup,
196 | DropdownMenuPortal,
197 | DropdownMenuSub,
198 | DropdownMenuSubContent,
199 | DropdownMenuSubTrigger,
200 | DropdownMenuRadioGroup,
201 | }
202 |
--------------------------------------------------------------------------------
/src/components/Main.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState, useRef, useEffect } from "react";
4 | import axios, { AxiosError } from "axios";
5 | import { FaTurnUp } from "react-icons/fa6";
6 | import {
7 | Select,
8 | SelectContent,
9 | SelectItem,
10 | SelectTrigger,
11 | SelectValue,
12 | } from "@/components/ui/select"
13 | import { Textarea } from "./ui/textarea";
14 | import useTweet from "@/hooks/useTweet";
15 | import Result from "./Result";
16 | import useResult from "@/hooks/useResult";
17 | import { HiStop } from "react-icons/hi2";
18 | import { toast } from "sonner";
19 | import { useSession } from "next-auth/react";
20 | import { useUsageTracker } from "@/hooks/useUsageTracker";
21 | import { ApiResponse } from "@/types/ApiResponse";
22 | import LoginModal from "./LoginModel";
23 | import { useRouter } from "next/navigation";
24 |
25 | export default function Main() {
26 | const [improvePrompt, setImprovePrompt] = useState('');
27 | const [isImprovingField, setIsImprovingField] = useState(false);
28 | const [isGenerating, setIsGenerating] = useState(false);
29 | const [showLoginModal, setShowLoginModal] = useState(false);
30 | const moodRef = useRef('Casual');
31 | const actionRef = useRef('Formatting');
32 | const textareaRef = useRef(null);
33 | const { tweet, setTweet } = useTweet();
34 | const { result, setResult } = useResult();
35 | const { incrementUsage, isLimitReached, resetUsage } = useUsageTracker();
36 | const { data: session } = useSession();
37 | const router = useRouter()
38 |
39 | const adjustTextareaHeight = () => {
40 | const textarea = textareaRef.current;
41 | if (textarea) {
42 | textarea.style.height = 'auto';
43 | textarea.style.height = `${textarea.scrollHeight}px`;
44 | }
45 | };
46 |
47 | const handleGenerate = async () => {
48 | if (!session && isLimitReached) {
49 | setShowLoginModal(true);
50 | return;
51 | }
52 |
53 | setIsGenerating(true);
54 | try {
55 | const response = await axios.post('/api/generate', { tweet, mood: moodRef.current, action: actionRef.current });
56 | incrementUsage();
57 | setResult(response.data.message);
58 | } catch (error) {
59 | const axiosError = error as AxiosError;
60 | toast.error(axiosError.response?.data.message ?? 'Failed to refine the tweet')
61 | } finally {
62 | setIsGenerating(false);
63 | }
64 | }
65 |
66 | const handleRegenerate = async () => {
67 | if (!isImprovingField && !improvePrompt) {
68 | setIsImprovingField(true);
69 | return;
70 | };
71 | if (isImprovingField && !improvePrompt) {
72 | setIsImprovingField(false);
73 | return;
74 | }
75 | setIsGenerating(true);
76 | try {
77 | const response = await axios.post('/api/improve', { tweet, result, improvePrompt });
78 | setResult(response.data.message);
79 | setImprovePrompt('');
80 | setIsImprovingField(false);
81 | } catch (error) {
82 | const axiosError = error as AxiosError;
83 | toast.error(axiosError.response?.data.message ?? 'Failed to refine the tweet')
84 | } finally {
85 | setIsGenerating(false);
86 | }
87 | }
88 |
89 | const saveInteraction = async () => {
90 | try {
91 | const response = await axios.post('/api/interaction/save', { tweet, mood: moodRef.current, action: actionRef.current, result })
92 | toast.success(response.data.message)
93 | router.refresh()
94 | } catch (error) {
95 | const axiosError = error as AxiosError;
96 | toast.error(axiosError.response?.data.message ?? 'Failed to save interaction')
97 | }
98 | }
99 |
100 | const copyToClipboard = () => {
101 | if (!result) return;
102 | navigator.clipboard.writeText(result);
103 | toast.success('Text copied to clipboard')
104 | };
105 |
106 | useEffect(() => {
107 | if (session) {
108 | resetUsage();
109 | }
110 | }, [session, resetUsage]);
111 |
112 | return (
113 |
114 | {showLoginModal && setShowLoginModal(false)} showLoginModal={showLoginModal} />}
115 |
171 |
172 |
173 | )
174 | }
--------------------------------------------------------------------------------
/src/components/ui/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Slot } from "@radix-ui/react-slot"
5 | import { VariantProps, cva } from "class-variance-authority"
6 | import { PanelLeft } from "lucide-react"
7 |
8 | import { useIsMobile } from "@/hooks/use-mobile"
9 | import { cn } from "@/lib/utils"
10 | import { Button } from "@/components/ui/button"
11 | import { Input } from "@/components/ui/input"
12 | import { Separator } from "@/components/ui/separator"
13 | import { Sheet, SheetContent } from "@/components/ui/sheet"
14 | import { Skeleton } from "@/components/ui/skeleton"
15 | import {
16 | Tooltip,
17 | TooltipContent,
18 | TooltipProvider,
19 | TooltipTrigger,
20 | } from "@/components/ui/tooltip"
21 | import { useSession } from "next-auth/react"
22 |
23 | const SIDEBAR_COOKIE_NAME = "sidebar:state"
24 | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
25 | const SIDEBAR_WIDTH = "16rem"
26 | const SIDEBAR_WIDTH_MOBILE = "18rem"
27 | const SIDEBAR_WIDTH_ICON = "3rem"
28 | const SIDEBAR_KEYBOARD_SHORTCUT = "b"
29 |
30 | type SidebarContext = {
31 | state: "expanded" | "collapsed"
32 | open: boolean
33 | setOpen: (open: boolean) => void
34 | openMobile: boolean
35 | setOpenMobile: (open: boolean) => void
36 | isMobile: boolean
37 | toggleSidebar: () => void
38 | }
39 |
40 | const SidebarContext = React.createContext(null)
41 |
42 | function useSidebar() {
43 | const context = React.useContext(SidebarContext)
44 | if (!context) {
45 | throw new Error("useSidebar must be used within a SidebarProvider.")
46 | }
47 |
48 | return context
49 | }
50 |
51 | const SidebarProvider = React.forwardRef<
52 | HTMLDivElement,
53 | React.ComponentProps<"div"> & {
54 | defaultOpen?: boolean
55 | open?: boolean
56 | onOpenChange?: (open: boolean) => void
57 | }
58 | >(
59 | (
60 | {
61 | defaultOpen = true,
62 | open: openProp,
63 | onOpenChange: setOpenProp,
64 | className,
65 | style,
66 | children,
67 | ...props
68 | },
69 | ref
70 | ) => {
71 | const isMobile = useIsMobile()
72 | const [openMobile, setOpenMobile] = React.useState(false)
73 |
74 | // This is the internal state of the sidebar.
75 | // We use openProp and setOpenProp for control from outside the component.
76 | const [_open, _setOpen] = React.useState(defaultOpen)
77 | const open = openProp ?? _open
78 | const setOpen = React.useCallback(
79 | (value: boolean | ((value: boolean) => boolean)) => {
80 | const openState = typeof value === "function" ? value(open) : value
81 | if (setOpenProp) {
82 | setOpenProp(openState)
83 | } else {
84 | _setOpen(openState)
85 | }
86 |
87 | // This sets the cookie to keep the sidebar state.
88 | document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
89 | },
90 | [setOpenProp, open]
91 | )
92 |
93 | // Helper to toggle the sidebar.
94 | const toggleSidebar = React.useCallback(() => {
95 | return isMobile
96 | ? setOpenMobile((open) => !open)
97 | : setOpen((open) => !open)
98 | }, [isMobile, setOpen, setOpenMobile])
99 |
100 | // Adds a keyboard shortcut to toggle the sidebar.
101 | React.useEffect(() => {
102 | const handleKeyDown = (event: KeyboardEvent) => {
103 | if (
104 | event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
105 | (event.metaKey || event.ctrlKey)
106 | ) {
107 | event.preventDefault()
108 | toggleSidebar()
109 | }
110 | }
111 |
112 | window.addEventListener("keydown", handleKeyDown)
113 | return () => window.removeEventListener("keydown", handleKeyDown)
114 | }, [toggleSidebar])
115 |
116 | // We add a state so that we can do data-state="expanded" or "collapsed".
117 | // This makes it easier to style the sidebar with Tailwind classes.
118 | const state = open ? "expanded" : "collapsed"
119 |
120 | const contextValue = React.useMemo(
121 | () => ({
122 | state,
123 | open,
124 | setOpen,
125 | isMobile,
126 | openMobile,
127 | setOpenMobile,
128 | toggleSidebar,
129 | }),
130 | [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
131 | )
132 |
133 | return (
134 |
135 |
136 |
151 | {children}
152 |
153 |
154 |
155 | )
156 | }
157 | )
158 | SidebarProvider.displayName = "SidebarProvider"
159 |
160 | const Sidebar = React.forwardRef<
161 | HTMLDivElement,
162 | React.ComponentProps<"div"> & {
163 | side?: "left" | "right"
164 | variant?: "sidebar" | "floating" | "inset"
165 | collapsible?: "offcanvas" | "icon" | "none"
166 | }
167 | >(
168 | (
169 | {
170 | side = "left",
171 | variant = "sidebar",
172 | collapsible = "offcanvas",
173 | className,
174 | children,
175 | ...props
176 | },
177 | ref
178 | ) => {
179 | const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
180 |
181 | if (collapsible === "none") {
182 | return (
183 |
191 | {children}
192 |
193 | )
194 | }
195 |
196 | if (isMobile) {
197 | return (
198 |
199 |
210 | {children}
211 |
212 |
213 | )
214 | }
215 |
216 | return (
217 |
225 | {/* This is what handles the sidebar gap on desktop */}
226 |
236 |
250 |
254 | {children}
255 |
256 |
257 |
258 | )
259 | }
260 | )
261 | Sidebar.displayName = "Sidebar"
262 |
263 | const SidebarTrigger = React.forwardRef<
264 | React.ElementRef,
265 | React.ComponentProps
266 | >(({ className, onClick, ...props }, ref) => {
267 | const { toggleSidebar } = useSidebar()
268 | const { status } = useSession()
269 | return (
270 |
285 | )
286 | })
287 | SidebarTrigger.displayName = "SidebarTrigger"
288 |
289 | const SidebarRail = React.forwardRef<
290 | HTMLButtonElement,
291 | React.ComponentProps<"button">
292 | >(({ className, ...props }, ref) => {
293 | const { toggleSidebar } = useSidebar()
294 |
295 | return (
296 |
314 | )
315 | })
316 | SidebarRail.displayName = "SidebarRail"
317 |
318 | const SidebarInset = React.forwardRef<
319 | HTMLDivElement,
320 | React.ComponentProps<"main">
321 | >(({ className, ...props }, ref) => {
322 | return (
323 |
332 | )
333 | })
334 | SidebarInset.displayName = "SidebarInset"
335 |
336 | const SidebarInput = React.forwardRef<
337 | React.ElementRef,
338 | React.ComponentProps
339 | >(({ className, ...props }, ref) => {
340 | return (
341 |
350 | )
351 | })
352 | SidebarInput.displayName = "SidebarInput"
353 |
354 | const SidebarHeader = React.forwardRef<
355 | HTMLDivElement,
356 | React.ComponentProps<"div">
357 | >(({ className, ...props }, ref) => {
358 | return (
359 |
365 | )
366 | })
367 | SidebarHeader.displayName = "SidebarHeader"
368 |
369 | const SidebarFooter = React.forwardRef<
370 | HTMLDivElement,
371 | React.ComponentProps<"div">
372 | >(({ className, ...props }, ref) => {
373 | return (
374 |
380 | )
381 | })
382 | SidebarFooter.displayName = "SidebarFooter"
383 |
384 | const SidebarSeparator = React.forwardRef<
385 | React.ElementRef,
386 | React.ComponentProps
387 | >(({ className, ...props }, ref) => {
388 | return (
389 |
395 | )
396 | })
397 | SidebarSeparator.displayName = "SidebarSeparator"
398 |
399 | const SidebarContent = React.forwardRef<
400 | HTMLDivElement,
401 | React.ComponentProps<"div">
402 | >(({ className, ...props }, ref) => {
403 | return (
404 |
413 | )
414 | })
415 | SidebarContent.displayName = "SidebarContent"
416 |
417 | const SidebarGroup = React.forwardRef<
418 | HTMLDivElement,
419 | React.ComponentProps<"div">
420 | >(({ className, ...props }, ref) => {
421 | return (
422 |
428 | )
429 | })
430 | SidebarGroup.displayName = "SidebarGroup"
431 |
432 | const SidebarGroupLabel = React.forwardRef<
433 | HTMLDivElement,
434 | React.ComponentProps<"div"> & { asChild?: boolean }
435 | >(({ className, asChild = false, ...props }, ref) => {
436 | const Comp = asChild ? Slot : "div"
437 |
438 | return (
439 | svg]:size-4 [&>svg]:shrink-0",
444 | "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
445 | className
446 | )}
447 | {...props}
448 | />
449 | )
450 | })
451 | SidebarGroupLabel.displayName = "SidebarGroupLabel"
452 |
453 | const SidebarGroupAction = React.forwardRef<
454 | HTMLButtonElement,
455 | React.ComponentProps<"button"> & { asChild?: boolean }
456 | >(({ className, asChild = false, ...props }, ref) => {
457 | const Comp = asChild ? Slot : "button"
458 |
459 | return (
460 | svg]:size-4 [&>svg]:shrink-0",
465 | // Increases the hit area of the button on mobile.
466 | "after:absolute after:-inset-2 after:md:hidden",
467 | "group-data-[collapsible=icon]:hidden",
468 | className
469 | )}
470 | {...props}
471 | />
472 | )
473 | })
474 | SidebarGroupAction.displayName = "SidebarGroupAction"
475 |
476 | const SidebarGroupContent = React.forwardRef<
477 | HTMLDivElement,
478 | React.ComponentProps<"div">
479 | >(({ className, ...props }, ref) => (
480 |
486 | ))
487 | SidebarGroupContent.displayName = "SidebarGroupContent"
488 |
489 | const SidebarMenu = React.forwardRef<
490 | HTMLUListElement,
491 | React.ComponentProps<"ul">
492 | >(({ className, ...props }, ref) => (
493 |
499 | ))
500 | SidebarMenu.displayName = "SidebarMenu"
501 |
502 | const SidebarMenuItem = React.forwardRef<
503 | HTMLLIElement,
504 | React.ComponentProps<"li">
505 | >(({ className, ...props }, ref) => (
506 |
512 | ))
513 | SidebarMenuItem.displayName = "SidebarMenuItem"
514 |
515 | const sidebarMenuButtonVariants = cva(
516 | "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
517 | {
518 | variants: {
519 | variant: {
520 | default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
521 | outline:
522 | "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
523 | },
524 | size: {
525 | default: "h-8 text-sm",
526 | sm: "h-7 text-xs",
527 | lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
528 | },
529 | },
530 | defaultVariants: {
531 | variant: "default",
532 | size: "default",
533 | },
534 | }
535 | )
536 |
537 | const SidebarMenuButton = React.forwardRef<
538 | HTMLButtonElement,
539 | React.ComponentProps<"button"> & {
540 | asChild?: boolean
541 | isActive?: boolean
542 | tooltip?: string | React.ComponentProps
543 | } & VariantProps
544 | >(
545 | (
546 | {
547 | asChild = false,
548 | isActive = false,
549 | variant = "default",
550 | size = "default",
551 | tooltip,
552 | className,
553 | ...props
554 | },
555 | ref
556 | ) => {
557 | const Comp = asChild ? Slot : "button"
558 | const { isMobile, state } = useSidebar()
559 |
560 | const button = (
561 |
569 | )
570 |
571 | if (!tooltip) {
572 | return button
573 | }
574 |
575 | if (typeof tooltip === "string") {
576 | tooltip = {
577 | children: tooltip,
578 | }
579 | }
580 |
581 | return (
582 |
583 | {button}
584 |
590 |
591 | )
592 | }
593 | )
594 | SidebarMenuButton.displayName = "SidebarMenuButton"
595 |
596 | const SidebarMenuAction = React.forwardRef<
597 | HTMLButtonElement,
598 | React.ComponentProps<"button"> & {
599 | asChild?: boolean
600 | showOnHover?: boolean
601 | }
602 | >(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
603 | const Comp = asChild ? Slot : "button"
604 |
605 | return (
606 | svg]:size-4 [&>svg]:shrink-0",
611 | // Increases the hit area of the button on mobile.
612 | "after:absolute after:-inset-2 after:md:hidden",
613 | "peer-data-[size=sm]/menu-button:top-1",
614 | "peer-data-[size=default]/menu-button:top-1.5",
615 | "peer-data-[size=lg]/menu-button:top-2.5",
616 | "group-data-[collapsible=icon]:hidden",
617 | showOnHover &&
618 | "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
619 | className
620 | )}
621 | {...props}
622 | />
623 | )
624 | })
625 | SidebarMenuAction.displayName = "SidebarMenuAction"
626 |
627 | const SidebarMenuBadge = React.forwardRef<
628 | HTMLDivElement,
629 | React.ComponentProps<"div">
630 | >(({ className, ...props }, ref) => (
631 |
645 | ))
646 | SidebarMenuBadge.displayName = "SidebarMenuBadge"
647 |
648 | const SidebarMenuSkeleton = React.forwardRef<
649 | HTMLDivElement,
650 | React.ComponentProps<"div"> & {
651 | showIcon?: boolean
652 | }
653 | >(({ className, showIcon = false, ...props }, ref) => {
654 | // Random width between 50 to 90%.
655 | const width = React.useMemo(() => {
656 | return `${Math.floor(Math.random() * 40) + 50}%`
657 | }, [])
658 |
659 | return (
660 |
666 | {showIcon && (
667 |
671 | )}
672 |
681 |
682 | )
683 | })
684 | SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
685 |
686 | const SidebarMenuSub = React.forwardRef<
687 | HTMLUListElement,
688 | React.ComponentProps<"ul">
689 | >(({ className, ...props }, ref) => (
690 |
700 | ))
701 | SidebarMenuSub.displayName = "SidebarMenuSub"
702 |
703 | const SidebarMenuSubItem = React.forwardRef<
704 | HTMLLIElement,
705 | React.ComponentProps<"li">
706 | >(({ ...props }, ref) => )
707 | SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
708 |
709 | const SidebarMenuSubButton = React.forwardRef<
710 | HTMLAnchorElement,
711 | React.ComponentProps<"a"> & {
712 | asChild?: boolean
713 | size?: "sm" | "md"
714 | isActive?: boolean
715 | }
716 | >(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
717 | const Comp = asChild ? Slot : "a"
718 |
719 | return (
720 | span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
727 | "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
728 | size === "sm" && "text-xs",
729 | size === "md" && "text-sm",
730 | "group-data-[collapsible=icon]:hidden",
731 | className
732 | )}
733 | {...props}
734 | />
735 | )
736 | })
737 | SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
738 |
739 | export {
740 | Sidebar,
741 | SidebarContent,
742 | SidebarFooter,
743 | SidebarGroup,
744 | SidebarGroupAction,
745 | SidebarGroupContent,
746 | SidebarGroupLabel,
747 | SidebarHeader,
748 | SidebarInput,
749 | SidebarInset,
750 | SidebarMenu,
751 | SidebarMenuAction,
752 | SidebarMenuBadge,
753 | SidebarMenuButton,
754 | SidebarMenuItem,
755 | SidebarMenuSkeleton,
756 | SidebarMenuSub,
757 | SidebarMenuSubButton,
758 | SidebarMenuSubItem,
759 | SidebarProvider,
760 | SidebarRail,
761 | SidebarSeparator,
762 | SidebarTrigger,
763 | useSidebar,
764 | }
765 |
--------------------------------------------------------------------------------